Skip to content

Commit 7c3eeeb

Browse files
authored
fix: gpt id stuff fr fr this time :/ (#9006)
1 parent e8357a8 commit 7c3eeeb

File tree

3 files changed

+87
-79
lines changed

3 files changed

+87
-79
lines changed

packages/opencode/src/provider/provider.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -999,6 +999,24 @@ export namespace Provider {
999999
opts.signal = combined
10001000
}
10011001

1002+
// Strip openai itemId metadata following what codex does
1003+
// Codex uses #[serde(skip_serializing)] on id fields for all item types:
1004+
// Message, Reasoning, FunctionCall, LocalShellCall, CustomToolCall, WebSearchCall
1005+
// IDs are only re-attached for Azure with store=true
1006+
if (model.api.npm === "@ai-sdk/openai" && opts.body && opts.method === "POST") {
1007+
const body = JSON.parse(opts.body as string)
1008+
const isAzure = model.providerID.includes("azure")
1009+
const keepIds = isAzure && body.store === true
1010+
if (!keepIds && Array.isArray(body.input)) {
1011+
for (const item of body.input) {
1012+
if ("id" in item) {
1013+
delete item.id
1014+
}
1015+
}
1016+
opts.body = JSON.stringify(body)
1017+
}
1018+
}
1019+
10021020
return fetchFn(input, {
10031021
...opts,
10041022
// @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682

packages/opencode/src/provider/transform.ts

Lines changed: 46 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -16,34 +16,33 @@ function mimeToModality(mime: string): Modality | undefined {
1616
}
1717

1818
export namespace ProviderTransform {
19+
// Maps npm package to the key the AI SDK expects for providerOptions
20+
function sdkKey(npm: string): string | undefined {
21+
switch (npm) {
22+
case "@ai-sdk/github-copilot":
23+
case "@ai-sdk/openai":
24+
case "@ai-sdk/azure":
25+
return "openai"
26+
case "@ai-sdk/amazon-bedrock":
27+
return "bedrock"
28+
case "@ai-sdk/anthropic":
29+
return "anthropic"
30+
case "@ai-sdk/google-vertex":
31+
case "@ai-sdk/google":
32+
return "google"
33+
case "@ai-sdk/gateway":
34+
return "gateway"
35+
case "@openrouter/ai-sdk-provider":
36+
return "openrouter"
37+
}
38+
return undefined
39+
}
40+
1941
function normalizeMessages(
2042
msgs: ModelMessage[],
2143
model: Provider.Model,
2244
options: Record<string, unknown>,
2345
): ModelMessage[] {
24-
// Strip openai itemId metadata following what codex does
25-
if (model.api.npm === "@ai-sdk/openai" || options.store === false) {
26-
msgs = msgs.map((msg) => {
27-
if (msg.providerOptions) {
28-
for (const options of Object.values(msg.providerOptions)) {
29-
delete options["itemId"]
30-
}
31-
}
32-
if (!Array.isArray(msg.content)) {
33-
return msg
34-
}
35-
const content = msg.content.map((part) => {
36-
if (part.providerOptions) {
37-
for (const options of Object.values(part.providerOptions)) {
38-
delete options["itemId"]
39-
}
40-
}
41-
return part
42-
})
43-
return { ...msg, content } as typeof msg
44-
})
45-
}
46-
4746
// Anthropic rejects messages with empty content - filter out empty string messages
4847
// and remove empty text/reasoning parts from array content
4948
if (model.api.npm === "@ai-sdk/anthropic") {
@@ -257,6 +256,28 @@ export namespace ProviderTransform {
257256
msgs = applyCaching(msgs, model.providerID)
258257
}
259258

259+
// Remap providerOptions keys from stored providerID to expected SDK key
260+
const key = sdkKey(model.api.npm)
261+
if (key && key !== model.providerID) {
262+
const remap = (opts: Record<string, any> | undefined) => {
263+
if (!opts) return opts
264+
if (!(model.providerID in opts)) return opts
265+
const result = { ...opts }
266+
result[key] = result[model.providerID]
267+
delete result[model.providerID]
268+
return result
269+
}
270+
271+
msgs = msgs.map((msg) => {
272+
if (!Array.isArray(msg.content)) return { ...msg, providerOptions: remap(msg.providerOptions) }
273+
return {
274+
...msg,
275+
providerOptions: remap(msg.providerOptions),
276+
content: msg.content.map((part) => ({ ...part, providerOptions: remap(part.providerOptions) })),
277+
} as typeof msg
278+
})
279+
}
280+
260281
return msgs
261282
}
262283

@@ -574,39 +595,8 @@ export namespace ProviderTransform {
574595
}
575596

576597
export function providerOptions(model: Provider.Model, options: { [x: string]: any }) {
577-
switch (model.api.npm) {
578-
case "@ai-sdk/github-copilot":
579-
case "@ai-sdk/openai":
580-
case "@ai-sdk/azure":
581-
return {
582-
["openai" as string]: options,
583-
}
584-
case "@ai-sdk/amazon-bedrock":
585-
return {
586-
["bedrock" as string]: options,
587-
}
588-
case "@ai-sdk/anthropic":
589-
return {
590-
["anthropic" as string]: options,
591-
}
592-
case "@ai-sdk/google-vertex":
593-
case "@ai-sdk/google":
594-
return {
595-
["google" as string]: options,
596-
}
597-
case "@ai-sdk/gateway":
598-
return {
599-
["gateway" as string]: options,
600-
}
601-
case "@openrouter/ai-sdk-provider":
602-
return {
603-
["openrouter" as string]: options,
604-
}
605-
default:
606-
return {
607-
[model.providerID]: options,
608-
}
609-
}
598+
const key = sdkKey(model.api.npm) ?? model.providerID
599+
return { [key]: options }
610600
}
611601

612602
export function maxOutputTokens(

packages/opencode/test/provider/transform.test.ts

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -649,7 +649,7 @@ describe("ProviderTransform.message - strip openai metadata when store=false", (
649649
headers: {},
650650
} as any
651651

652-
test("strips itemId and reasoningEncryptedContent when store=false", () => {
652+
test("preserves itemId and reasoningEncryptedContent when store=false", () => {
653653
const msgs = [
654654
{
655655
role: "assistant",
@@ -680,11 +680,11 @@ describe("ProviderTransform.message - strip openai metadata when store=false", (
680680
const result = ProviderTransform.message(msgs, openaiModel, { store: false }) as any[]
681681

682682
expect(result).toHaveLength(1)
683-
expect(result[0].content[0].providerOptions?.openai?.itemId).toBeUndefined()
684-
expect(result[0].content[1].providerOptions?.openai?.itemId).toBeUndefined()
683+
expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("rs_123")
684+
expect(result[0].content[1].providerOptions?.openai?.itemId).toBe("msg_456")
685685
})
686686

687-
test("strips itemId and reasoningEncryptedContent when store=false even when not openai", () => {
687+
test("preserves itemId and reasoningEncryptedContent when store=false even when not openai", () => {
688688
const zenModel = {
689689
...openaiModel,
690690
providerID: "zen",
@@ -719,11 +719,11 @@ describe("ProviderTransform.message - strip openai metadata when store=false", (
719719
const result = ProviderTransform.message(msgs, zenModel, { store: false }) as any[]
720720

721721
expect(result).toHaveLength(1)
722-
expect(result[0].content[0].providerOptions?.openai?.itemId).toBeUndefined()
723-
expect(result[0].content[1].providerOptions?.openai?.itemId).toBeUndefined()
722+
expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("rs_123")
723+
expect(result[0].content[1].providerOptions?.openai?.itemId).toBe("msg_456")
724724
})
725725

726-
test("preserves other openai options when stripping itemId", () => {
726+
test("preserves other openai options including itemId", () => {
727727
const msgs = [
728728
{
729729
role: "assistant",
@@ -744,11 +744,11 @@ describe("ProviderTransform.message - strip openai metadata when store=false", (
744744

745745
const result = ProviderTransform.message(msgs, openaiModel, { store: false }) as any[]
746746

747-
expect(result[0].content[0].providerOptions?.openai?.itemId).toBeUndefined()
747+
expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_123")
748748
expect(result[0].content[0].providerOptions?.openai?.otherOption).toBe("value")
749749
})
750750

751-
test("strips metadata for openai package even when store is true", () => {
751+
test("preserves metadata for openai package when store is true", () => {
752752
const msgs = [
753753
{
754754
role: "assistant",
@@ -766,13 +766,13 @@ describe("ProviderTransform.message - strip openai metadata when store=false", (
766766
},
767767
] as any[]
768768

769-
// openai package always strips itemId regardless of store value
769+
// openai package preserves itemId regardless of store value
770770
const result = ProviderTransform.message(msgs, openaiModel, { store: true }) as any[]
771771

772-
expect(result[0].content[0].providerOptions?.openai?.itemId).toBeUndefined()
772+
expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_123")
773773
})
774774

775-
test("strips metadata for non-openai packages when store is false", () => {
775+
test("preserves metadata for non-openai packages when store is false", () => {
776776
const anthropicModel = {
777777
...openaiModel,
778778
providerID: "anthropic",
@@ -799,13 +799,13 @@ describe("ProviderTransform.message - strip openai metadata when store=false", (
799799
},
800800
] as any[]
801801

802-
// store=false triggers stripping even for non-openai packages
802+
// store=false preserves metadata for non-openai packages
803803
const result = ProviderTransform.message(msgs, anthropicModel, { store: false }) as any[]
804804

805-
expect(result[0].content[0].providerOptions?.openai?.itemId).toBeUndefined()
805+
expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_123")
806806
})
807807

808-
test("strips metadata using providerID key when store is false", () => {
808+
test("preserves metadata using providerID key when store is false", () => {
809809
const opencodeModel = {
810810
...openaiModel,
811811
providerID: "opencode",
@@ -835,11 +835,11 @@ describe("ProviderTransform.message - strip openai metadata when store=false", (
835835

836836
const result = ProviderTransform.message(msgs, opencodeModel, { store: false }) as any[]
837837

838-
expect(result[0].content[0].providerOptions?.opencode?.itemId).toBeUndefined()
838+
expect(result[0].content[0].providerOptions?.opencode?.itemId).toBe("msg_123")
839839
expect(result[0].content[0].providerOptions?.opencode?.otherOption).toBe("value")
840840
})
841841

842-
test("strips itemId across all providerOptions keys", () => {
842+
test("preserves itemId across all providerOptions keys", () => {
843843
const opencodeModel = {
844844
...openaiModel,
845845
providerID: "opencode",
@@ -873,12 +873,12 @@ describe("ProviderTransform.message - strip openai metadata when store=false", (
873873

874874
const result = ProviderTransform.message(msgs, opencodeModel, { store: false }) as any[]
875875

876-
expect(result[0].providerOptions?.openai?.itemId).toBeUndefined()
877-
expect(result[0].providerOptions?.opencode?.itemId).toBeUndefined()
878-
expect(result[0].providerOptions?.extra?.itemId).toBeUndefined()
879-
expect(result[0].content[0].providerOptions?.openai?.itemId).toBeUndefined()
880-
expect(result[0].content[0].providerOptions?.opencode?.itemId).toBeUndefined()
881-
expect(result[0].content[0].providerOptions?.extra?.itemId).toBeUndefined()
876+
expect(result[0].providerOptions?.openai?.itemId).toBe("msg_root")
877+
expect(result[0].providerOptions?.opencode?.itemId).toBe("msg_opencode")
878+
expect(result[0].providerOptions?.extra?.itemId).toBe("msg_extra")
879+
expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_openai_part")
880+
expect(result[0].content[0].providerOptions?.opencode?.itemId).toBe("msg_opencode_part")
881+
expect(result[0].content[0].providerOptions?.extra?.itemId).toBe("msg_extra_part")
882882
})
883883

884884
test("does not strip metadata for non-openai packages when store is not false", () => {

0 commit comments

Comments
 (0)