diff --git a/packages/ai/src/protocols/open-responses.ts b/packages/ai/src/protocols/open-responses.ts index 1cc4c331ea2..423ba38de6f 100644 --- a/packages/ai/src/protocols/open-responses.ts +++ b/packages/ai/src/protocols/open-responses.ts @@ -334,14 +334,14 @@ export const StreamItem = Schema.StructWithRest( id: Schema.optional(Schema.String), call_id: Schema.optional(Schema.String), name: Schema.optional(Schema.String), - namespace: Schema.optional(Schema.String), + namespace: optionalNull(Schema.String), arguments: Schema.optional(Schema.String), encrypted_content: optionalNull(Schema.String), }), [Schema.Record(Schema.String, Schema.Unknown)], ) export type StreamItem = Schema.Schema.Type -export type OutputItem = StreamItem & { readonly id: string } +export type OutputItem = StreamItem & { readonly id: string; readonly namespace?: string } // Responses-compatible providers put error details at the top level, under `error`, or under // `response.error`, and gateways reshape them freely: strings, numeric codes, extra fields. Those @@ -946,6 +946,8 @@ const resolveItem = ( index: number | undefined, ): OutputItem => ({ ...item, + // Some gateways serialize an absent namespace as null. + namespace: item.namespace ?? undefined, id: item.id ?? (index === undefined ? undefined : state.outputItems[index]) ?? diff --git a/packages/ai/test/provider/openai-compatible-responses.test.ts b/packages/ai/test/provider/openai-compatible-responses.test.ts index 81860398d6d..2b1427aabc8 100644 --- a/packages/ai/test/provider/openai-compatible-responses.test.ts +++ b/packages/ai/test/provider/openai-compatible-responses.test.ts @@ -644,6 +644,27 @@ describe("Open Responses-compatible route", () => { }), ) + it.effect("treats a null function call namespace as absent", () => + Effect.gen(function* () { + const item = { type: "function_call", id: "fc_1", call_id: "call_1", name: "lookup", namespace: null } + const response = yield* LLMClient.generate(request).pipe( + Effect.provide( + fixedResponse( + sseEvents( + { type: "response.output_item.added", output_index: 0, item: { ...item, arguments: "" } }, + { type: "response.function_call_arguments.done", item_id: "fc_1", output_index: 0, arguments: "{}" }, + { type: "response.output_item.done", output_index: 0, item: { ...item, arguments: "{}" } }, + { type: "response.completed", response: { id: "resp_1", output: [{ ...item, arguments: "{}" }] } }, + ), + ), + ), + ) + + expect(response.toolCalls).toEqual([expect.objectContaining({ id: "call_1", name: "lookup", input: {} })]) + expect(response.toolCalls[0]?.namespace).toBeUndefined() + }), + ) + it.effect("rejects missing, null, and non-string event IDs even with a registered output index", () => Effect.gen(function* () { yield* Effect.forEach(