fix: gpt id stuff fr fr this time :/ (#9006)
This commit is contained in:
@@ -999,6 +999,24 @@ export namespace Provider {
|
|||||||
opts.signal = combined
|
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, {
|
return fetchFn(input, {
|
||||||
...opts,
|
...opts,
|
||||||
// @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682
|
// @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682
|
||||||
|
|||||||
@@ -16,34 +16,33 @@ function mimeToModality(mime: string): Modality | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export namespace ProviderTransform {
|
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(
|
function normalizeMessages(
|
||||||
msgs: ModelMessage[],
|
msgs: ModelMessage[],
|
||||||
model: Provider.Model,
|
model: Provider.Model,
|
||||||
options: Record<string, unknown>,
|
options: Record<string, unknown>,
|
||||||
): ModelMessage[] {
|
): 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
|
// Anthropic rejects messages with empty content - filter out empty string messages
|
||||||
// and remove empty text/reasoning parts from array content
|
// and remove empty text/reasoning parts from array content
|
||||||
if (model.api.npm === "@ai-sdk/anthropic") {
|
if (model.api.npm === "@ai-sdk/anthropic") {
|
||||||
@@ -257,6 +256,28 @@ export namespace ProviderTransform {
|
|||||||
msgs = applyCaching(msgs, model.providerID)
|
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
|
return msgs
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -574,39 +595,8 @@ export namespace ProviderTransform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function providerOptions(model: Provider.Model, options: { [x: string]: any }) {
|
export function providerOptions(model: Provider.Model, options: { [x: string]: any }) {
|
||||||
switch (model.api.npm) {
|
const key = sdkKey(model.api.npm) ?? model.providerID
|
||||||
case "@ai-sdk/github-copilot":
|
return { [key]: options }
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function maxOutputTokens(
|
export function maxOutputTokens(
|
||||||
|
|||||||
@@ -649,7 +649,7 @@ describe("ProviderTransform.message - strip openai metadata when store=false", (
|
|||||||
headers: {},
|
headers: {},
|
||||||
} as any
|
} as any
|
||||||
|
|
||||||
test("strips itemId and reasoningEncryptedContent when store=false", () => {
|
test("preserves itemId and reasoningEncryptedContent when store=false", () => {
|
||||||
const msgs = [
|
const msgs = [
|
||||||
{
|
{
|
||||||
role: "assistant",
|
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[]
|
const result = ProviderTransform.message(msgs, openaiModel, { store: false }) as any[]
|
||||||
|
|
||||||
expect(result).toHaveLength(1)
|
expect(result).toHaveLength(1)
|
||||||
expect(result[0].content[0].providerOptions?.openai?.itemId).toBeUndefined()
|
expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("rs_123")
|
||||||
expect(result[0].content[1].providerOptions?.openai?.itemId).toBeUndefined()
|
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 = {
|
const zenModel = {
|
||||||
...openaiModel,
|
...openaiModel,
|
||||||
providerID: "zen",
|
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[]
|
const result = ProviderTransform.message(msgs, zenModel, { store: false }) as any[]
|
||||||
|
|
||||||
expect(result).toHaveLength(1)
|
expect(result).toHaveLength(1)
|
||||||
expect(result[0].content[0].providerOptions?.openai?.itemId).toBeUndefined()
|
expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("rs_123")
|
||||||
expect(result[0].content[1].providerOptions?.openai?.itemId).toBeUndefined()
|
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 = [
|
const msgs = [
|
||||||
{
|
{
|
||||||
role: "assistant",
|
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[]
|
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")
|
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 = [
|
const msgs = [
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
@@ -766,13 +766,13 @@ describe("ProviderTransform.message - strip openai metadata when store=false", (
|
|||||||
},
|
},
|
||||||
] as any[]
|
] 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[]
|
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 = {
|
const anthropicModel = {
|
||||||
...openaiModel,
|
...openaiModel,
|
||||||
providerID: "anthropic",
|
providerID: "anthropic",
|
||||||
@@ -799,13 +799,13 @@ describe("ProviderTransform.message - strip openai metadata when store=false", (
|
|||||||
},
|
},
|
||||||
] as any[]
|
] 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[]
|
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 = {
|
const opencodeModel = {
|
||||||
...openaiModel,
|
...openaiModel,
|
||||||
providerID: "opencode",
|
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[]
|
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")
|
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 = {
|
const opencodeModel = {
|
||||||
...openaiModel,
|
...openaiModel,
|
||||||
providerID: "opencode",
|
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[]
|
const result = ProviderTransform.message(msgs, opencodeModel, { store: false }) as any[]
|
||||||
|
|
||||||
expect(result[0].providerOptions?.openai?.itemId).toBeUndefined()
|
expect(result[0].providerOptions?.openai?.itemId).toBe("msg_root")
|
||||||
expect(result[0].providerOptions?.opencode?.itemId).toBeUndefined()
|
expect(result[0].providerOptions?.opencode?.itemId).toBe("msg_opencode")
|
||||||
expect(result[0].providerOptions?.extra?.itemId).toBeUndefined()
|
expect(result[0].providerOptions?.extra?.itemId).toBe("msg_extra")
|
||||||
expect(result[0].content[0].providerOptions?.openai?.itemId).toBeUndefined()
|
expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_openai_part")
|
||||||
expect(result[0].content[0].providerOptions?.opencode?.itemId).toBeUndefined()
|
expect(result[0].content[0].providerOptions?.opencode?.itemId).toBe("msg_opencode_part")
|
||||||
expect(result[0].content[0].providerOptions?.extra?.itemId).toBeUndefined()
|
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", () => {
|
test("does not strip metadata for non-openai packages when store is not false", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user