Add opencode provider.ts patch for missing tool_call ids

This commit is contained in:
Zac Gaetano 2026-05-26 00:27:18 -04:00
parent ec4587345a
commit a1c90c92d7

86
patch_provider.py Normal file
View file

@ -0,0 +1,86 @@
path = '/mnt/NVME/Docker/opencode-build/opencode/packages/opencode/src/provider/provider.ts'
with open(path, 'r') as f:
src = f.read()
MARKER_1 = 'statusText: res.statusText,\n })\n}\n\nfunction googleVertexAnthropicBaseURL'
PATCH_FN = r"""statusText: res.statusText,
})
}
// Fix missing/null tool_call ids in SSE streams from OpenAI-compatible providers.
// Some proxies (e.g. 9Router/LiteLLM forwarding Anthropic) emit tool-call chunks
// without an `id` field, causing @ai-sdk/openai-compatible to throw
// "Expected 'id' to be a string." Inject a stable synthetic id per tool-call index.
function patchToolCallIds(res: Response, npm: string): Response {
if (npm !== "@ai-sdk/openai-compatible" && npm !== "@ai-sdk/openai") return res
if (!res.body) return res
if (!res.headers.get("content-type")?.includes("text/event-stream")) return res
const reader = res.body.getReader()
const encoder = new TextEncoder()
const decoder = new TextDecoder()
const idByIndex = new Map<number, string>()
let buf = ""
const body = new ReadableStream<Uint8Array>({
async pull(ctrl) {
const { done, value } = await reader.read()
if (done) { if (buf) ctrl.enqueue(encoder.encode(buf)); ctrl.close(); return }
buf += decoder.decode(value, { stream: true })
const nl = buf.lastIndexOf("\n")
if (nl === -1) return
const complete = buf.slice(0, nl + 1)
buf = buf.slice(nl + 1)
const lines = complete.split("\n")
const out: string[] = []
for (const line of lines) {
if (!line.startsWith("data: ")) { out.push(line); continue }
const payload = line.slice(6)
if (payload === "[DONE]") { out.push(line); continue }
try {
const parsed = JSON.parse(payload)
let dirty = false
for (const choice of parsed?.choices ?? []) {
for (const tc of choice?.delta?.tool_calls ?? []) {
if (typeof tc.id !== "string" || tc.id === "") {
const idx: number = tc.index ?? 0
if (!idByIndex.has(idx))
idByIndex.set(idx, `call_${crypto.randomUUID().replace(/-/g,"").slice(0,24)}`)
tc.id = idByIndex.get(idx); dirty = true
}
}
}
out.push(dirty ? `data: ${JSON.stringify(parsed)}` : line)
} catch { out.push(line) }
}
ctrl.enqueue(encoder.encode(out.join("\n")))
},
async cancel(reason) { await reader.cancel(reason) },
})
return new Response(body, {
headers: new Headers(res.headers),
status: res.status,
statusText: res.statusText,
})
}
function googleVertexAnthropicBaseURL"""
if MARKER_1 in src:
src = src.replace(MARKER_1, PATCH_FN, 1)
print('OK: inserted patchToolCallIds()')
else:
print('ERROR: marker 1 not found')
raise SystemExit(1)
MARKER_2 = ' if (!chunkAbortCtl) return res\n return wrapSSE(res, chunkTimeout, chunkAbortCtl)'
PATCH_RETURN = ' const patched = patchToolCallIds(res, model.api.npm)\n if (!chunkAbortCtl) return patched\n return wrapSSE(patched, chunkTimeout, chunkAbortCtl)'
if MARKER_2 in src:
src = src.replace(MARKER_2, PATCH_RETURN, 1)
print('OK: patched fetch return')
else:
print('ERROR: marker 2 not found')
raise SystemExit(1)
with open(path, 'w') as f:
f.write(src)
print('DONE')