fix(core): strip stale response item IDs before serialization

This commit is contained in:
Aiden Cline 2026-07-22 04:27:56 +00:00
commit 73ee68b6a2
5 changed files with 207 additions and 23 deletions

View file

@ -270,6 +270,140 @@ it.effect("projects replay metadata onto AI SDK prompt parts", () =>
}),
)
it.effect("strips stale Azure item IDs before stateless AI SDK serialization", () =>
Effect.gen(function* () {
const aisdk = yield* AISDK.Service
yield* aisdk.hook.sdk((event) => {
event.sdk = { languageModel: () => ({ provider: event.model.providerID }) }
})
const messages = [
Message.assistant([
{
type: "reasoning" as const,
text: "Think",
providerMetadata: {
azure: { itemId: "rs_123", reasoningEncryptedContent: "encrypted" },
},
},
{
type: "tool-call" as const,
id: "call_1",
name: "lookup",
input: { query: "Effect" },
providerMetadata: { azure: { itemId: "fc_123", otherOption: "value" } },
},
]),
]
const resolved = yield* aisdk.model(model("@ai-sdk/azure", { store: false }))
const prepared = yield* LLMClient.prepare<LanguageModelV3CallOptions>(
LLM.request({
model: resolved,
messages,
}),
)
expect(prepared.body.prompt).toEqual([
{
role: "assistant",
content: [
{
type: "reasoning",
text: "Think",
providerOptions: { azure: { reasoningEncryptedContent: "encrypted" } },
},
{
type: "tool-call",
toolCallId: "call_1",
toolName: "lookup",
input: { query: "Effect" },
providerExecuted: undefined,
providerOptions: { azure: { otherOption: "value" } },
},
],
},
])
const stateful = yield* aisdk.model(model("@ai-sdk/azure", { store: true }))
const statefulPrepared = yield* LLMClient.prepare<LanguageModelV3CallOptions>(
LLM.request({ model: stateful, messages }),
)
expect(statefulPrepared.body.prompt[0]).toMatchObject({
role: "assistant",
content: [
{ providerOptions: { azure: { itemId: "rs_123" } } },
{ providerOptions: { azure: { itemId: "fc_123" } } },
],
})
const copilot = yield* aisdk.model(model("@ai-sdk/github-copilot", { store: false }))
const copilotPrepared = yield* LLMClient.prepare<LanguageModelV3CallOptions>(
LLM.request({
model: copilot,
messages: [
Message.assistant([
{
type: "reasoning",
text: "Think",
providerMetadata: {
copilot: { itemId: "rs_123", reasoningEncryptedContent: "encrypted" },
},
},
]),
],
}),
)
expect(copilotPrepared.body.prompt[0]).toMatchObject({
role: "assistant",
content: [
{
providerOptions: { copilot: { reasoningEncryptedContent: "encrypted" } },
},
],
})
}),
)
it.effect("does not corrupt AI SDK item reference payloads", () =>
Effect.gen(function* () {
const aisdk = yield* AISDK.Service
let sent: unknown
const server = yield* Effect.acquireRelease(
Effect.sync(() =>
Bun.serve({
port: 0,
async fetch(request) {
sent = await request.json()
return new Response(null, { status: 200 })
},
}),
),
(server) => Effect.sync(() => server.stop(true)),
)
let wrapped: typeof fetch | undefined
yield* aisdk.hook.sdk((event) => {
wrapped = event.options.fetch
event.sdk = { languageModel: () => ({ provider: event.model.providerID }) }
})
yield* aisdk.language(model("@ai-sdk/azure"))
yield* Effect.promise(() =>
wrapped!(new URL("responses", server.url), {
method: "POST",
body: JSON.stringify({
store: false,
input: [{ type: "item_reference", id: "fc_123" }],
}),
}),
)
expect(sent).toEqual({
store: false,
input: [{ type: "item_reference", id: "fc_123" }],
})
}),
)
it.effect("emits malformed AI SDK tool input without executing it", () =>
Effect.gen(function* () {
const aisdk = yield* AISDK.Service

View file

@ -163,7 +163,35 @@ describe("convertToOpenAIResponsesInput", () => {
])
})
test("drops reasoning items with no copilot itemId and warns, as before", async () => {
test("preserves encrypted reasoning with no copilot itemId", async () => {
const { input, warnings } = await convertToOpenAIResponsesInput({
prompt: [
{
role: "assistant",
content: [
{
type: "reasoning",
text: "thinking...",
providerOptions: { copilot: { reasoningEncryptedContent: "enc_1" } },
},
],
},
],
systemMessageMode: "system",
store: false,
})
expect(warnings).toEqual([])
expect(input).toEqual([
{
type: "reasoning",
encrypted_content: "enc_1",
summary: [{ type: "summary_text", text: "thinking..." }],
},
])
})
test("drops reasoning with neither a copilot itemId nor encrypted content", async () => {
const { input, warnings } = await convertToOpenAIResponsesInput({
prompt: [
{