summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAiden Cline <[email protected]>2026-01-14 23:31:50 -0800
committerGitHub <[email protected]>2026-01-15 01:31:50 -0600
commit92931437c4ce48d2c4fdcad14067bff9a6f5d3ef (patch)
tree8f39ea211783ab8df38fe783c9dbec364284194f
parent08ca1237cc25634e2af97a62d036a48a80dc8d6e (diff)
downloadopencode-92931437c4ce48d2c4fdcad14067bff9a6f5d3ef.tar.gz
opencode-92931437c4ce48d2c4fdcad14067bff9a6f5d3ef.zip
fix: codex id issue (#8605)
-rw-r--r--packages/opencode/src/provider/transform.ts75
-rw-r--r--packages/opencode/src/session/llm.ts9
-rw-r--r--packages/opencode/test/provider/transform.test.ts411
3 files changed, 391 insertions, 104 deletions
diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts
index 4acc28fbf..1dbc24db5 100644
--- a/packages/opencode/src/provider/transform.ts
+++ b/packages/opencode/src/provider/transform.ts
@@ -16,7 +16,31 @@ function mimeToModality(mime: string): Modality | undefined {
}
export namespace ProviderTransform {
- function normalizeMessages(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] {
+ 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 (!Array.isArray(msg.content)) return msg
+ const content = msg.content.map((part) => {
+ if (!part.providerOptions?.openai) return part
+ const { itemId, reasoningEncryptedContent, ...rest } = part.providerOptions.openai as Record<string, unknown>
+ const openai = Object.keys(rest).length > 0 ? rest : undefined
+ return {
+ ...part,
+ providerOptions: {
+ ...part.providerOptions,
+ openai,
+ },
+ }
+ })
+ 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") {
@@ -218,9 +242,9 @@ export namespace ProviderTransform {
})
}
- export function message(msgs: ModelMessage[], model: Provider.Model) {
+ export function message(msgs: ModelMessage[], model: Provider.Model, options: Record<string, unknown>) {
msgs = unsupportedParts(msgs, model)
- msgs = normalizeMessages(msgs, model)
+ msgs = normalizeMessages(msgs, model, options)
if (
model.providerID === "anthropic" ||
model.api.id.includes("anthropic") ||
@@ -453,64 +477,69 @@ export namespace ProviderTransform {
return {}
}
- export function options(
- model: Provider.Model,
- sessionID: string,
- providerOptions?: Record<string, any>,
- ): Record<string, any> {
+ export function options(input: {
+ model: Provider.Model
+ sessionID: string
+ providerOptions?: Record<string, any>
+ }): Record<string, any> {
const result: Record<string, any> = {}
- if (model.api.npm === "@openrouter/ai-sdk-provider") {
+ // openai and providers using openai package should set store to false by default.
+ if (input.model.providerID === "openai" || input.model.api.npm === "@ai-sdk/openai") {
+ result["store"] = false
+ }
+
+ if (input.model.api.npm === "@openrouter/ai-sdk-provider") {
result["usage"] = {
include: true,
}
- if (model.api.id.includes("gemini-3")) {
+ if (input.model.api.id.includes("gemini-3")) {
result["reasoning"] = { effort: "high" }
}
}
if (
- model.providerID === "baseten" ||
- (model.providerID === "opencode" && ["kimi-k2-thinking", "glm-4.6"].includes(model.api.id))
+ input.model.providerID === "baseten" ||
+ (input.model.providerID === "opencode" && ["kimi-k2-thinking", "glm-4.6"].includes(input.model.api.id))
) {
result["chat_template_args"] = { enable_thinking: true }
}
- if (["zai", "zhipuai"].includes(model.providerID) && model.api.npm === "@ai-sdk/openai-compatible") {
+ if (["zai", "zhipuai"].includes(input.model.providerID) && input.model.api.npm === "@ai-sdk/openai-compatible") {
result["thinking"] = {
type: "enabled",
clear_thinking: false,
}
}
- if (model.providerID === "openai" || providerOptions?.setCacheKey) {
- result["promptCacheKey"] = sessionID
+ if (input.model.providerID === "openai" || input.providerOptions?.setCacheKey) {
+ result["promptCacheKey"] = input.sessionID
}
- if (model.api.npm === "@ai-sdk/google" || model.api.npm === "@ai-sdk/google-vertex") {
+ if (input.model.api.npm === "@ai-sdk/google" || input.model.api.npm === "@ai-sdk/google-vertex") {
result["thinkingConfig"] = {
includeThoughts: true,
}
- if (model.api.id.includes("gemini-3")) {
+ if (input.model.api.id.includes("gemini-3")) {
result["thinkingConfig"]["thinkingLevel"] = "high"
}
}
- if (model.api.id.includes("gpt-5") && !model.api.id.includes("gpt-5-chat")) {
- if (model.providerID.includes("codex")) {
+ if (input.model.api.id.includes("gpt-5") && !input.model.api.id.includes("gpt-5-chat")) {
+ if (input.model.providerID.includes("codex")) {
result["store"] = false
}
- if (!model.api.id.includes("codex") && !model.api.id.includes("gpt-5-pro")) {
+ if (!input.model.api.id.includes("codex") && !input.model.api.id.includes("gpt-5-pro")) {
result["reasoningEffort"] = "medium"
}
- if (model.api.id.endsWith("gpt-5.") && model.providerID !== "azure") {
+ if (input.model.api.id.endsWith("gpt-5.") && input.model.providerID !== "azure") {
result["textVerbosity"] = "low"
}
- if (model.providerID.startsWith("opencode")) {
- result["promptCacheKey"] = sessionID
+ if (input.model.providerID.startsWith("opencode")) {
+ result["promptCacheKey"] = input.sessionID
result["include"] = ["reasoning.encrypted_content"]
result["reasoningSummary"] = "auto"
}
diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts
index ebc22637e..5b6178bc0 100644
--- a/packages/opencode/src/session/llm.ts
+++ b/packages/opencode/src/session/llm.ts
@@ -95,7 +95,11 @@ export namespace LLM {
!input.small && input.model.variants && input.user.variant ? input.model.variants[input.user.variant] : {}
const base = input.small
? ProviderTransform.smallOptions(input.model)
- : ProviderTransform.options(input.model, input.sessionID, provider.options)
+ : ProviderTransform.options({
+ model: input.model,
+ sessionID: input.sessionID,
+ providerOptions: provider.options,
+ })
const options: Record<string, any> = pipe(
base,
mergeDeep(input.model.options),
@@ -104,7 +108,6 @@ export namespace LLM {
)
if (isCodex) {
options.instructions = SystemPrompt.instructions()
- options.store = false
}
const params = await Plugin.trigger(
@@ -214,7 +217,7 @@ export namespace LLM {
async transformParams(args) {
if (args.type === "stream") {
// @ts-expect-error
- args.params.prompt = ProviderTransform.message(args.params.prompt, input.model)
+ args.params.prompt = ProviderTransform.message(args.params.prompt, input.model, options)
}
return args.params
},
diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts
index 32b1ecb24..3814e9d99 100644
--- a/packages/opencode/test/provider/transform.test.ts
+++ b/packages/opencode/test/provider/transform.test.ts
@@ -39,22 +39,34 @@ describe("ProviderTransform.options - setCacheKey", () => {
} as any
test("should set promptCacheKey when providerOptions.setCacheKey is true", () => {
- const result = ProviderTransform.options(mockModel, sessionID, { setCacheKey: true })
+ const result = ProviderTransform.options({
+ model: mockModel,
+ sessionID,
+ providerOptions: { setCacheKey: true },
+ })
expect(result.promptCacheKey).toBe(sessionID)
})
test("should not set promptCacheKey when providerOptions.setCacheKey is false", () => {
- const result = ProviderTransform.options(mockModel, sessionID, { setCacheKey: false })
+ const result = ProviderTransform.options({
+ model: mockModel,
+ sessionID,
+ providerOptions: { setCacheKey: false },
+ })
expect(result.promptCacheKey).toBeUndefined()
})
test("should not set promptCacheKey when providerOptions is undefined", () => {
- const result = ProviderTransform.options(mockModel, sessionID, undefined)
+ const result = ProviderTransform.options({
+ model: mockModel,
+ sessionID,
+ providerOptions: undefined,
+ })
expect(result.promptCacheKey).toBeUndefined()
})
test("should not set promptCacheKey when providerOptions does not have setCacheKey", () => {
- const result = ProviderTransform.options(mockModel, sessionID, {})
+ const result = ProviderTransform.options({ model: mockModel, sessionID, providerOptions: {} })
expect(result.promptCacheKey).toBeUndefined()
})
@@ -68,9 +80,27 @@ describe("ProviderTransform.options - setCacheKey", () => {
npm: "@ai-sdk/openai",
},
}
- const result = ProviderTransform.options(openaiModel, sessionID, {})
+ const result = ProviderTransform.options({ model: openaiModel, sessionID, providerOptions: {} })
expect(result.promptCacheKey).toBe(sessionID)
})
+
+ test("should set store=false for openai provider", () => {
+ const openaiModel = {
+ ...mockModel,
+ providerID: "openai",
+ api: {
+ id: "gpt-4",
+ url: "https://api.openai.com",
+ npm: "@ai-sdk/openai",
+ },
+ }
+ const result = ProviderTransform.options({
+ model: openaiModel,
+ sessionID,
+ providerOptions: {},
+ })
+ expect(result.store).toBe(false)
+ })
})
describe("ProviderTransform.maxOutputTokens", () => {
@@ -208,40 +238,44 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => {
},
] as any[]
- const result = ProviderTransform.message(msgs, {
- id: "deepseek/deepseek-chat",
- providerID: "deepseek",
- api: {
- id: "deepseek-chat",
- url: "https://api.deepseek.com",
- npm: "@ai-sdk/openai-compatible",
- },
- name: "DeepSeek Chat",
- capabilities: {
- temperature: true,
- reasoning: true,
- attachment: false,
- toolcall: true,
- input: { text: true, audio: false, image: false, video: false, pdf: false },
- output: { text: true, audio: false, image: false, video: false, pdf: false },
- interleaved: {
- field: "reasoning_content",
+ const result = ProviderTransform.message(
+ msgs,
+ {
+ id: "deepseek/deepseek-chat",
+ providerID: "deepseek",
+ api: {
+ id: "deepseek-chat",
+ url: "https://api.deepseek.com",
+ npm: "@ai-sdk/openai-compatible",
},
+ name: "DeepSeek Chat",
+ capabilities: {
+ temperature: true,
+ reasoning: true,
+ attachment: false,
+ toolcall: true,
+ input: { text: true, audio: false, image: false, video: false, pdf: false },
+ output: { text: true, audio: false, image: false, video: false, pdf: false },
+ interleaved: {
+ field: "reasoning_content",
+ },
+ },
+ cost: {
+ input: 0.001,
+ output: 0.002,
+ cache: { read: 0.0001, write: 0.0002 },
+ },
+ limit: {
+ context: 128000,
+ output: 8192,
+ },
+ status: "active",
+ options: {},
+ headers: {},
+ release_date: "2023-04-01",
},
- cost: {
- input: 0.001,
- output: 0.002,
- cache: { read: 0.0001, write: 0.0002 },
- },
- limit: {
- context: 128000,
- output: 8192,
- },
- status: "active",
- options: {},
- headers: {},
- release_date: "2023-04-01",
- })
+ {},
+ )
expect(result).toHaveLength(1)
expect(result[0].content).toEqual([
@@ -266,38 +300,42 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => {
},
] as any[]
- const result = ProviderTransform.message(msgs, {
- id: "openai/gpt-4",
- providerID: "openai",
- api: {
- id: "gpt-4",
- url: "https://api.openai.com",
- npm: "@ai-sdk/openai",
- },
- name: "GPT-4",
- capabilities: {
- temperature: true,
- reasoning: false,
- attachment: true,
- toolcall: true,
- input: { text: true, audio: false, image: true, video: false, pdf: false },
- output: { text: true, audio: false, image: false, video: false, pdf: false },
- interleaved: false,
- },
- cost: {
- input: 0.03,
- output: 0.06,
- cache: { read: 0.001, write: 0.002 },
- },
- limit: {
- context: 128000,
- output: 4096,
+ const result = ProviderTransform.message(
+ msgs,
+ {
+ id: "openai/gpt-4",
+ providerID: "openai",
+ api: {
+ id: "gpt-4",
+ url: "https://api.openai.com",
+ npm: "@ai-sdk/openai",
+ },
+ name: "GPT-4",
+ capabilities: {
+ temperature: true,
+ reasoning: false,
+ attachment: true,
+ toolcall: true,
+ input: { text: true, audio: false, image: true, video: false, pdf: false },
+ output: { text: true, audio: false, image: false, video: false, pdf: false },
+ interleaved: false,
+ },
+ cost: {
+ input: 0.03,
+ output: 0.06,
+ cache: { read: 0.001, write: 0.002 },
+ },
+ limit: {
+ context: 128000,
+ output: 4096,
+ },
+ status: "active",
+ options: {},
+ headers: {},
+ release_date: "2023-04-01",
},
- status: "active",
- options: {},
- headers: {},
- release_date: "2023-04-01",
- })
+ {},
+ )
expect(result[0].content).toEqual([
{ type: "reasoning", text: "Should not be processed" },
@@ -351,7 +389,7 @@ describe("ProviderTransform.message - empty image handling", () => {
},
] as any[]
- const result = ProviderTransform.message(msgs, mockModel)
+ const result = ProviderTransform.message(msgs, mockModel, {})
expect(result).toHaveLength(1)
expect(result[0].content).toHaveLength(2)
@@ -375,7 +413,7 @@ describe("ProviderTransform.message - empty image handling", () => {
},
] as any[]
- const result = ProviderTransform.message(msgs, mockModel)
+ const result = ProviderTransform.message(msgs, mockModel, {})
expect(result).toHaveLength(1)
expect(result[0].content).toHaveLength(2)
@@ -397,7 +435,7 @@ describe("ProviderTransform.message - empty image handling", () => {
},
] as any[]
- const result = ProviderTransform.message(msgs, mockModel)
+ const result = ProviderTransform.message(msgs, mockModel, {})
expect(result).toHaveLength(1)
expect(result[0].content).toHaveLength(3)
@@ -450,7 +488,7 @@ describe("ProviderTransform.message - anthropic empty content filtering", () =>
{ role: "user", content: "World" },
] as any[]
- const result = ProviderTransform.message(msgs, anthropicModel)
+ const result = ProviderTransform.message(msgs, anthropicModel, {})
expect(result).toHaveLength(2)
expect(result[0].content).toBe("Hello")
@@ -469,7 +507,7 @@ describe("ProviderTransform.message - anthropic empty content filtering", () =>
},
] as any[]
- const result = ProviderTransform.message(msgs, anthropicModel)
+ const result = ProviderTransform.message(msgs, anthropicModel, {})
expect(result).toHaveLength(1)
expect(result[0].content).toHaveLength(1)
@@ -488,7 +526,7 @@ describe("ProviderTransform.message - anthropic empty content filtering", () =>
},
] as any[]
- const result = ProviderTransform.message(msgs, anthropicModel)
+ const result = ProviderTransform.message(msgs, anthropicModel, {})
expect(result).toHaveLength(1)
expect(result[0].content).toHaveLength(1)
@@ -508,7 +546,7 @@ describe("ProviderTransform.message - anthropic empty content filtering", () =>
{ role: "user", content: "World" },
] as any[]
- const result = ProviderTransform.message(msgs, anthropicModel)
+ const result = ProviderTransform.message(msgs, anthropicModel, {})
expect(result).toHaveLength(2)
expect(result[0].content).toBe("Hello")
@@ -526,7 +564,7 @@ describe("ProviderTransform.message - anthropic empty content filtering", () =>
},
] as any[]
- const result = ProviderTransform.message(msgs, anthropicModel)
+ const result = ProviderTransform.message(msgs, anthropicModel, {})
expect(result).toHaveLength(1)
expect(result[0].content).toHaveLength(1)
@@ -550,7 +588,7 @@ describe("ProviderTransform.message - anthropic empty content filtering", () =>
},
] as any[]
- const result = ProviderTransform.message(msgs, anthropicModel)
+ const result = ProviderTransform.message(msgs, anthropicModel, {})
expect(result).toHaveLength(1)
expect(result[0].content).toHaveLength(2)
@@ -577,7 +615,7 @@ describe("ProviderTransform.message - anthropic empty content filtering", () =>
},
] as any[]
- const result = ProviderTransform.message(msgs, openaiModel)
+ const result = ProviderTransform.message(msgs, openaiModel, {})
expect(result).toHaveLength(2)
expect(result[0].content).toBe("")
@@ -585,6 +623,223 @@ describe("ProviderTransform.message - anthropic empty content filtering", () =>
})
})
+describe("ProviderTransform.message - strip openai metadata when store=false", () => {
+ const openaiModel = {
+ id: "openai/gpt-5",
+ providerID: "openai",
+ api: {
+ id: "gpt-5",
+ url: "https://api.openai.com",
+ npm: "@ai-sdk/openai",
+ },
+ name: "GPT-5",
+ capabilities: {
+ temperature: true,
+ reasoning: true,
+ attachment: true,
+ toolcall: true,
+ input: { text: true, audio: false, image: true, video: false, pdf: false },
+ output: { text: true, audio: false, image: false, video: false, pdf: false },
+ interleaved: false,
+ },
+ cost: { input: 0.03, output: 0.06, cache: { read: 0.001, write: 0.002 } },
+ limit: { context: 128000, output: 4096 },
+ status: "active",
+ options: {},
+ headers: {},
+ } as any
+
+ test("strips itemId and reasoningEncryptedContent when store=false", () => {
+ const msgs = [
+ {
+ role: "assistant",
+ content: [
+ {
+ type: "reasoning",
+ text: "thinking...",
+ providerOptions: {
+ openai: {
+ itemId: "rs_123",
+ reasoningEncryptedContent: "encrypted",
+ },
+ },
+ },
+ {
+ type: "text",
+ text: "Hello",
+ providerOptions: {
+ openai: {
+ itemId: "msg_456",
+ },
+ },
+ },
+ ],
+ },
+ ] as any[]
+
+ 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[0].providerOptions?.openai?.reasoningEncryptedContent).toBeUndefined()
+ expect(result[0].content[1].providerOptions?.openai?.itemId).toBeUndefined()
+ })
+
+ test("strips itemId and reasoningEncryptedContent when store=false even when not openai", () => {
+ const zenModel = {
+ ...openaiModel,
+ providerID: "zen",
+ }
+ const msgs = [
+ {
+ role: "assistant",
+ content: [
+ {
+ type: "reasoning",
+ text: "thinking...",
+ providerOptions: {
+ openai: {
+ itemId: "rs_123",
+ reasoningEncryptedContent: "encrypted",
+ },
+ },
+ },
+ {
+ type: "text",
+ text: "Hello",
+ providerOptions: {
+ openai: {
+ itemId: "msg_456",
+ },
+ },
+ },
+ ],
+ },
+ ] as any[]
+
+ 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[0].providerOptions?.openai?.reasoningEncryptedContent).toBeUndefined()
+ expect(result[0].content[1].providerOptions?.openai?.itemId).toBeUndefined()
+ })
+
+ test("preserves other openai options when stripping itemId", () => {
+ const msgs = [
+ {
+ role: "assistant",
+ content: [
+ {
+ type: "text",
+ text: "Hello",
+ providerOptions: {
+ openai: {
+ itemId: "msg_123",
+ otherOption: "value",
+ },
+ },
+ },
+ ],
+ },
+ ] as any[]
+
+ 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?.otherOption).toBe("value")
+ })
+
+ test("strips metadata for openai package even when store is true", () => {
+ const msgs = [
+ {
+ role: "assistant",
+ content: [
+ {
+ type: "text",
+ text: "Hello",
+ providerOptions: {
+ openai: {
+ itemId: "msg_123",
+ },
+ },
+ },
+ ],
+ },
+ ] as any[]
+
+ // openai package always strips itemId regardless of store value
+ const result = ProviderTransform.message(msgs, openaiModel, { store: true }) as any[]
+
+ expect(result[0].content[0].providerOptions?.openai?.itemId).toBeUndefined()
+ })
+
+ test("strips metadata for non-openai packages when store is false", () => {
+ const anthropicModel = {
+ ...openaiModel,
+ providerID: "anthropic",
+ api: {
+ id: "claude-3",
+ url: "https://api.anthropic.com",
+ npm: "@ai-sdk/anthropic",
+ },
+ }
+ const msgs = [
+ {
+ role: "assistant",
+ content: [
+ {
+ type: "text",
+ text: "Hello",
+ providerOptions: {
+ openai: {
+ itemId: "msg_123",
+ },
+ },
+ },
+ ],
+ },
+ ] as any[]
+
+ // store=false triggers stripping even for non-openai packages
+ const result = ProviderTransform.message(msgs, anthropicModel, { store: false }) as any[]
+
+ expect(result[0].content[0].providerOptions?.openai?.itemId).toBeUndefined()
+ })
+
+ test("does not strip metadata for non-openai packages when store is not false", () => {
+ const anthropicModel = {
+ ...openaiModel,
+ providerID: "anthropic",
+ api: {
+ id: "claude-3",
+ url: "https://api.anthropic.com",
+ npm: "@ai-sdk/anthropic",
+ },
+ }
+ const msgs = [
+ {
+ role: "assistant",
+ content: [
+ {
+ type: "text",
+ text: "Hello",
+ providerOptions: {
+ openai: {
+ itemId: "msg_123",
+ },
+ },
+ },
+ ],
+ },
+ ] as any[]
+
+ const result = ProviderTransform.message(msgs, anthropicModel, {}) as any[]
+
+ expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_123")
+ })
+})
+
describe("ProviderTransform.variants", () => {
const createMockModel = (overrides: Partial<any> = {}): any => ({
id: "test/test-model",