diff --git a/lib/i18n/batch.test.ts b/lib/i18n/batch.test.ts index fa7be492e..23c61ed0f 100644 --- a/lib/i18n/batch.test.ts +++ b/lib/i18n/batch.test.ts @@ -296,6 +296,62 @@ describe('prefetchTranslations', () => { expect([...pending]).toEqual([requestKey(r)]) }) + it('keeps polling a just-submitted batch that is not readable yet', async () => { + const a = req('One') + let polls = 0 + let clock = 0 + const fakeFetch = (async (_url: string, init?: RequestInit) => { + if (init?.method === 'POST') return json({ id: 'b1', status: 'validating' }) + polls++ + clock += 60_000 + if (polls <= 2) return json({ error: 'not found' }, 404) + return json({ + id: 'b1', + status: 'completed', + results: [ + { + custom_id: requestKey(a), + response: { + status_code: 200, + body: { choices: [{ message: { content: 'Eins' }, finish_reason: 'stop' }] }, + }, + error: null, + }, + ], + }) + }) as typeof fetch + const { answers } = await prefetchTranslations(byKey(a), { + ...baseOpts, + waitMs: 3600_000, + ledgerPath, + fetch: fakeFetch, + now: () => clock, + }) + expect(polls).toBe(3) + expect(answers.get(requestKey(a))).toEqual({ text: 'Eins', finishReason: 'stop' }) + }) + + it('gives up on a new batch that stays unreadable past the grace period', async () => { + const a = req('One') + let clock = 0 + const fakeFetch = (async (_url: string, init?: RequestInit) => { + if (init?.method === 'POST') return json({ id: 'b1', status: 'validating' }) + clock += 4 * 60_000 + return json({ error: 'not found' }, 404) + }) as typeof fetch + const { answers, pending } = await prefetchTranslations(byKey(a), { + ...baseOpts, + waitMs: 3600_000, + ledgerPath, + fetch: fakeFetch, + now: () => clock, + }) + expect(answers.size).toBe(0) + expect(pending.size).toBe(0) + expect(clock).toBeLessThan(3600_000) + expect(JSON.parse(await readFile(ledgerPath, 'utf8'))).toEqual({}) + }) + it('drops a carried batch that this run no longer needs or that is gone', async () => { const r = req('One') await writeFile( diff --git a/lib/i18n/batch.ts b/lib/i18n/batch.ts index 0ad88adaf..3fc30323e 100644 --- a/lib/i18n/batch.ts +++ b/lib/i18n/batch.ts @@ -116,6 +116,12 @@ type Ledger = Record */ const UNNEEDED_RETENTION_MS = 48 * 3600_000 +/** + * A batch OpenRouter has just accepted can answer 404 for a while before it becomes + * readable, so a 404 this soon after submit is polled again rather than taken as gone. + */ +const NEW_BATCH_GRACE_MS = 10 * 60_000 + async function readLedger(path: string): Promise { try { const parsed = JSON.parse(await readFile(path, 'utf8')) @@ -244,6 +250,9 @@ export async function prefetchTranslations( let batch: BatchObject try { const res = await doFetch(`${BATCHES_URL}/${id}`, { headers, signal: timeout() }) + if (res.status === 404 && now() - (known.get(id)?.at ?? 0) < NEW_BATCH_GRACE_MS) { + throw new Error('HTTP 404, not readable yet') + } // Any 4xx but a timeout or rate limit will not change by asking again, so stop // waiting and let the requests run live. Only a 404 means the batch is gone; // an auth failure keeps it in the ledger for a run with a working key.