Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions lib/i18n/batch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
9 changes: 9 additions & 0 deletions lib/i18n/batch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ type Ledger = Record<string, { model: string; keys: string[]; at: number }>
*/
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<Ledger> {
try {
const parsed = JSON.parse(await readFile(path, 'utf8'))
Expand Down Expand Up @@ -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.
Expand Down
Loading