Skip to content

Commit b47c680

Browse files
fix(ai): accept null Responses function call namespace (#55631)
Co-authored-by: kksingh000 <[email protected]>
1 parent ae8d59f commit b47c680

2 files changed

Lines changed: 25 additions & 2 deletions

File tree

‎packages/ai/src/protocols/open-responses.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -334,14 +334,14 @@ export const StreamItem = Schema.StructWithRest(
334334
id: Schema.optional(Schema.String),
335335
call_id: Schema.optional(Schema.String),
336336
name: Schema.optional(Schema.String),
337-
namespace: Schema.optional(Schema.String),
337+
namespace: optionalNull(Schema.String),
338338
arguments: Schema.optional(Schema.String),
339339
encrypted_content: optionalNull(Schema.String),
340340
}),
341341
[Schema.Record(Schema.String, Schema.Unknown)],
342342
)
343343
export type StreamItem = Schema.Schema.Type<typeof StreamItem>
344-
export type OutputItem = StreamItem & { readonly id: string }
344+
export type OutputItem = StreamItem & { readonly id: string; readonly namespace?: string }
345345

346346
// Responses-compatible providers put error details at the top level, under `error`, or under
347347
// `response.error`, and gateways reshape them freely: strings, numeric codes, extra fields. Those
@@ -946,6 +946,8 @@ const resolveItem = (
946946
index: number | undefined,
947947
): OutputItem => ({
948948
...item,
949+
// Some gateways serialize an absent namespace as null.
950+
namespace: item.namespace ?? undefined,
949951
id:
950952
item.id ??
951953
(index === undefined ? undefined : state.outputItems[index]) ??

‎packages/ai/test/provider/openai-compatible-responses.test.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,27 @@ describe("Open Responses-compatible route", () => {
644644
}),
645645
)
646646

647+
it.effect("treats a null function call namespace as absent", () =>
648+
Effect.gen(function* () {
649+
const item = { type: "function_call", id: "fc_1", call_id: "call_1", name: "lookup", namespace: null }
650+
const response = yield* LLMClient.generate(request).pipe(
651+
Effect.provide(
652+
fixedResponse(
653+
sseEvents(
654+
{ type: "response.output_item.added", output_index: 0, item: { ...item, arguments: "" } },
655+
{ type: "response.function_call_arguments.done", item_id: "fc_1", output_index: 0, arguments: "{}" },
656+
{ type: "response.output_item.done", output_index: 0, item: { ...item, arguments: "{}" } },
657+
{ type: "response.completed", response: { id: "resp_1", output: [{ ...item, arguments: "{}" }] } },
658+
),
659+
),
660+
),
661+
)
662+
663+
expect(response.toolCalls).toEqual([expect.objectContaining({ id: "call_1", name: "lookup", input: {} })])
664+
expect(response.toolCalls[0]?.namespace).toBeUndefined()
665+
}),
666+
)
667+
647668
it.effect("rejects missing, null, and non-string event IDs even with a registered output index", () =>
648669
Effect.gen(function* () {
649670
yield* Effect.forEach(

0 commit comments

Comments
 (0)