summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/opencode/src/provider/provider.ts18
-rw-r--r--packages/opencode/src/provider/transform.ts102
-rw-r--r--packages/opencode/test/provider/transform.test.ts46
3 files changed, 87 insertions, 79 deletions
diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts
index 9e2dd0ba0..bcb115edf 100644
--- a/packages/opencode/src/provider/provider.ts
+++ b/packages/opencode/src/provider/provider.ts
@@ -999,6 +999,24 @@ export namespace Provider {
opts.signal = combined
}
+ // Strip openai itemId metadata following what codex does
+ // Codex uses #[serde(skip_serializing)] on id fields for all item types:
+ // Message, Reasoning, FunctionCall, LocalShellCall, CustomToolCall, WebSearchCall
+ // IDs are only re-attached for Azure with store=true
+ if (model.api.npm === "@ai-sdk/openai" && opts.body && opts.method === "POST") {
+ const body = JSON.parse(opts.body as string)
+ const isAzure = model.providerID.includes("azure")
+ const keepIds = isAzure && body.store === true
+ if (!keepIds && Array.isArray(body.input)) {
+ for (const item of body.input) {
+ if ("id" in item) {
+ delete item.id
+ }
+ }
+ opts.body = JSON.stringify(body)
+ }
+ }
+
return fetchFn(input, {
...opts,
// @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682
diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts
index 023b00dd3..71295ba2b 100644
--- a/packages/opencode/src/provider/transform.ts
+++ b/packages/opencode/src/provider/transform.ts
@@ -16,34 +16,33 @@ function mimeToModality(mime: string): Modality | undefined {
}
export namespace ProviderTransform {
+ // Maps npm package to the key the AI SDK expects for providerOptions
+ function sdkKey(npm: string): string | undefined {
+ switch (npm) {
+ case "@ai-sdk/github-copilot":
+ case "@ai-sdk/openai":
+ case "@ai-sdk/azure":
+ return "openai"
+ case "@ai-sdk/amazon-bedrock":
+ return "bedrock"
+ case "@ai-sdk/anthropic":
+ return "anthropic"
+ case "@ai-sdk/google-vertex":
+ case "@ai-sdk/google":
+ return "google"
+ case "@ai-sdk/gateway":
+ return "gateway"
+ case "@openrouter/ai-sdk-provider":
+ return "openrouter"
+ }
+ return undefined
+ }
+
function normalizeMessages(
msgs: ModelMessage[],
model: Provider.Model,
options: Record<string, unknown>,
): ModelMessage[] {
- // Strip openai itemId metadata following what codex does
- if (model.api.npm === "@ai-sdk/openai" || options.store === false) {
- msgs = msgs.map((msg) => {
- if (msg.providerOptions) {
- for (const options of Object.values(msg.providerOptions)) {
- delete options["itemId"]
- }
- }
- if (!Array.isArray(msg.content)) {
- return msg
- }
- const content = msg.content.map((part) => {
- if (part.providerOptions) {
- for (const options of Object.values(part.providerOptions)) {
- delete options["itemId"]
- }
- }
- return part
- })
- return { ...msg, content } as typeof msg
- })
- }
-
// Anthropic rejects messages with empty content - filter out empty string messages
// and remove empty text/reasoning parts from array content
if (model.api.npm === "@ai-sdk/anthropic") {
@@ -257,6 +256,28 @@ export namespace ProviderTransform {
msgs = applyCaching(msgs, model.providerID)
}
+ // Remap providerOptions keys from stored providerID to expected SDK key
+ const key = sdkKey(model.api.npm)
+ if (key && key !== model.providerID) {
+ const remap = (opts: Record<string, any> | undefined) => {
+ if (!opts) return opts
+ if (!(model.providerID in opts)) return opts
+ const result = { ...opts }
+ result[key] = result[model.providerID]
+ delete result[model.providerID]
+ return result
+ }
+
+ msgs = msgs.map((msg) => {
+ if (!Array.isArray(msg.content)) return { ...msg, providerOptions: remap(msg.providerOptions) }
+ return {
+ ...msg,
+ providerOptions: remap(msg.providerOptions),
+ content: msg.content.map((part) => ({ ...part, providerOptions: remap(part.providerOptions) })),
+ } as typeof msg
+ })
+ }
+
return msgs
}
@@ -574,39 +595,8 @@ export namespace ProviderTransform {
}
export function providerOptions(model: Provider.Model, options: { [x: string]: any }) {
- switch (model.api.npm) {
- case "@ai-sdk/github-copilot":
- case "@ai-sdk/openai":
- case "@ai-sdk/azure":
- return {
- ["openai" as string]: options,
- }
- case "@ai-sdk/amazon-bedrock":
- return {
- ["bedrock" as string]: options,
- }
- case "@ai-sdk/anthropic":
- return {
- ["anthropic" as string]: options,
- }
- case "@ai-sdk/google-vertex":
- case "@ai-sdk/google":
- return {
- ["google" as string]: options,
- }
- case "@ai-sdk/gateway":
- return {
- ["gateway" as string]: options,
- }
- case "@openrouter/ai-sdk-provider":
- return {
- ["openrouter" as string]: options,
- }
- default:
- return {
- [model.providerID]: options,
- }
- }
+ const key = sdkKey(model.api.npm) ?? model.providerID
+ return { [key]: options }
}
export function maxOutputTokens(
diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts
index 33047b5bc..0f203b930 100644
--- a/packages/opencode/test/provider/transform.test.ts
+++ b/packages/opencode/test/provider/transform.test.ts
@@ -649,7 +649,7 @@ describe("ProviderTransform.message - strip openai metadata when store=false", (
headers: {},
} as any
- test("strips itemId and reasoningEncryptedContent when store=false", () => {
+ test("preserves itemId and reasoningEncryptedContent when store=false", () => {
const msgs = [
{
role: "assistant",
@@ -680,11 +680,11 @@ describe("ProviderTransform.message - strip openai metadata when store=false", (
const result = ProviderTransform.message(msgs, openaiModel, { store: false }) as any[]
expect(result).toHaveLength(1)
- expect(result[0].content[0].providerOptions?.openai?.itemId).toBeUndefined()
- expect(result[0].content[1].providerOptions?.openai?.itemId).toBeUndefined()
+ expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("rs_123")
+ expect(result[0].content[1].providerOptions?.openai?.itemId).toBe("msg_456")
})
- test("strips itemId and reasoningEncryptedContent when store=false even when not openai", () => {
+ test("preserves itemId and reasoningEncryptedContent when store=false even when not openai", () => {
const zenModel = {
...openaiModel,
providerID: "zen",
@@ -719,11 +719,11 @@ describe("ProviderTransform.message - strip openai metadata when store=false", (
const result = ProviderTransform.message(msgs, zenModel, { store: false }) as any[]
expect(result).toHaveLength(1)
- expect(result[0].content[0].providerOptions?.openai?.itemId).toBeUndefined()
- expect(result[0].content[1].providerOptions?.openai?.itemId).toBeUndefined()
+ expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("rs_123")
+ expect(result[0].content[1].providerOptions?.openai?.itemId).toBe("msg_456")
})
- test("preserves other openai options when stripping itemId", () => {
+ test("preserves other openai options including itemId", () => {
const msgs = [
{
role: "assistant",
@@ -744,11 +744,11 @@ describe("ProviderTransform.message - strip openai metadata when store=false", (
const result = ProviderTransform.message(msgs, openaiModel, { store: false }) as any[]
- expect(result[0].content[0].providerOptions?.openai?.itemId).toBeUndefined()
+ expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_123")
expect(result[0].content[0].providerOptions?.openai?.otherOption).toBe("value")
})
- test("strips metadata for openai package even when store is true", () => {
+ test("preserves metadata for openai package when store is true", () => {
const msgs = [
{
role: "assistant",
@@ -766,13 +766,13 @@ describe("ProviderTransform.message - strip openai metadata when store=false", (
},
] as any[]
- // openai package always strips itemId regardless of store value
+ // openai package preserves itemId regardless of store value
const result = ProviderTransform.message(msgs, openaiModel, { store: true }) as any[]
- expect(result[0].content[0].providerOptions?.openai?.itemId).toBeUndefined()
+ expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_123")
})
- test("strips metadata for non-openai packages when store is false", () => {
+ test("preserves metadata for non-openai packages when store is false", () => {
const anthropicModel = {
...openaiModel,
providerID: "anthropic",
@@ -799,13 +799,13 @@ describe("ProviderTransform.message - strip openai metadata when store=false", (
},
] as any[]
- // store=false triggers stripping even for non-openai packages
+ // store=false preserves metadata for non-openai packages
const result = ProviderTransform.message(msgs, anthropicModel, { store: false }) as any[]
- expect(result[0].content[0].providerOptions?.openai?.itemId).toBeUndefined()
+ expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_123")
})
- test("strips metadata using providerID key when store is false", () => {
+ test("preserves metadata using providerID key when store is false", () => {
const opencodeModel = {
...openaiModel,
providerID: "opencode",
@@ -835,11 +835,11 @@ describe("ProviderTransform.message - strip openai metadata when store=false", (
const result = ProviderTransform.message(msgs, opencodeModel, { store: false }) as any[]
- expect(result[0].content[0].providerOptions?.opencode?.itemId).toBeUndefined()
+ expect(result[0].content[0].providerOptions?.opencode?.itemId).toBe("msg_123")
expect(result[0].content[0].providerOptions?.opencode?.otherOption).toBe("value")
})
- test("strips itemId across all providerOptions keys", () => {
+ test("preserves itemId across all providerOptions keys", () => {
const opencodeModel = {
...openaiModel,
providerID: "opencode",
@@ -873,12 +873,12 @@ describe("ProviderTransform.message - strip openai metadata when store=false", (
const result = ProviderTransform.message(msgs, opencodeModel, { store: false }) as any[]
- expect(result[0].providerOptions?.openai?.itemId).toBeUndefined()
- expect(result[0].providerOptions?.opencode?.itemId).toBeUndefined()
- expect(result[0].providerOptions?.extra?.itemId).toBeUndefined()
- expect(result[0].content[0].providerOptions?.openai?.itemId).toBeUndefined()
- expect(result[0].content[0].providerOptions?.opencode?.itemId).toBeUndefined()
- expect(result[0].content[0].providerOptions?.extra?.itemId).toBeUndefined()
+ expect(result[0].providerOptions?.openai?.itemId).toBe("msg_root")
+ expect(result[0].providerOptions?.opencode?.itemId).toBe("msg_opencode")
+ expect(result[0].providerOptions?.extra?.itemId).toBe("msg_extra")
+ expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_openai_part")
+ expect(result[0].content[0].providerOptions?.opencode?.itemId).toBe("msg_opencode_part")
+ expect(result[0].content[0].providerOptions?.extra?.itemId).toBe("msg_extra_part")
})
test("does not strip metadata for non-openai packages when store is not false", () => {