Add opencode provider.ts patch for missing tool_call ids
This commit is contained in:
parent
ec4587345a
commit
a1c90c92d7
1 changed files with 86 additions and 0 deletions
86
patch_provider.py
Normal file
86
patch_provider.py
Normal 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')
|
||||||
Loading…
Reference in a new issue