diff --git a/packages/ai/src/protocols/gemini.ts b/packages/ai/src/protocols/gemini.ts index 1252885335f..eb0536e9c4f 100644 --- a/packages/ai/src/protocols/gemini.ts +++ b/packages/ai/src/protocols/gemini.ts @@ -41,6 +41,13 @@ const requiresThoughtSignatureFallback = (modelID: string) => { // so their tool-result attachments lower as a separate user turn instead. const routesLegacyToolMedia = (modelID: string) => /gemini-2[.-]5(?:[.-]|$)/i.test(modelID) +// Blacklist: Gemini 1.x/2.x ignore or reject explicit function call ids. +// Every other model id (Gemini 3+, gemma, anything unrecognized) gets them. +const omitsFunctionCallIds = (modelID: string) => { + const match = /^gemini(?:-live)?-(\d+)/i.exec(modelID) + return match !== null && Number(match[1]) < 3 +} + export interface OptionsInput { readonly [key: string]: unknown readonly cachedContent?: string @@ -217,6 +224,7 @@ interface ParserState { readonly lifecycle: Lifecycle.State readonly reasoningSignature?: string readonly textSignature?: string + readonly seenCallIds?: ReadonlySet } // ============================================================================= @@ -274,20 +282,14 @@ const thoughtSignature = (providerMetadata: ProviderMetadata | undefined) => { : undefined } -const functionCallId = (providerMetadata: ProviderMetadata | undefined) => { - const google = providerMetadata?.google - return ProviderShared.isRecord(google) && typeof google.functionCallId === "string" - ? google.functionCallId - : undefined -} - -const lowerToolCall = (part: ToolCallPart) => ({ - functionCall: { id: functionCallId(part.providerMetadata), name: part.name, args: part.input }, +const lowerToolCall = (part: ToolCallPart, omitIds: boolean) => ({ + functionCall: { ...(omitIds ? {} : { id: part.id }), name: part.name, args: part.input }, thoughtSignature: thoughtSignature(part.providerMetadata), }) const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMRequest) { const contents: GeminiContent[] = [] + const omitCallIds = omitsFunctionCallIds(request.model.id) const legacyToolMedia = routesLegacyToolMedia(request.model.id) let pendingMedia: GeminiInlineDataPart[] | undefined const flushMedia = () => { @@ -336,7 +338,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR continue } if (part.type === "tool-call") { - const lowered = lowerToolCall(part) + const lowered = lowerToolCall(part, omitCallIds) const signature = lowered.thoughtSignature parts.push({ ...lowered, @@ -361,7 +363,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR if (part.result.type !== "content") { parts.push({ functionResponse: { - id: functionCallId(part.providerMetadata), + ...(omitCallIds ? {} : { id: part.id }), name: part.name, response: { name: part.name, @@ -382,7 +384,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR if (legacyToolMedia && media.length > 0) (pendingMedia ??= []).push(...media) parts.push({ functionResponse: { - id: functionCallId(part.providerMetadata), + ...(omitCallIds ? {} : { id: part.id }), name: part.name, response: { name: part.name, @@ -581,6 +583,8 @@ const step = (state: ParserState, event: GeminiEvent) => { let lifecycle = nextState.lifecycle let reasoningSignature = nextState.reasoningSignature let textSignature = nextState.textSignature + // Supplier ids must be tracked across chunks of the same response, not just within one event's parts. + const seenCallIds = new Set(nextState.seenCallIds) for (const part of candidate.content.parts) { const signature = "thoughtSignature" in part && part.thoughtSignature ? part.thoughtSignature : undefined @@ -618,13 +622,13 @@ const step = (state: ParserState, event: GeminiEvent) => { if ("functionCall" in part) { const input = part.functionCall.args === undefined ? {} : part.functionCall.args - // Gemini 2.0+ and Vertex supply a unique function call ID on the part; when omitted (e.g. Gemini 1.5), + // Gemini 2.0+ supplies a unique function call ID on the part; when omitted (e.g. Gemini 1.5), // generate a globally unique ID rather than a per-request counter to prevent cross-request collisions in downstream registries. - const id = part.functionCall.id ?? `tool_${crypto.randomUUID().replaceAll("-", "")}` - const metadata = { - ...(part.functionCall.id === undefined ? {} : { functionCallId: part.functionCall.id }), - ...(part.thoughtSignature === undefined ? {} : { thoughtSignature: part.thoughtSignature }), - } + // A repeated supplier id would replay as two identical calls, so only the first occurrence keeps it. + const supplied = part.functionCall.id + const duplicate = supplied !== undefined && seenCallIds.has(supplied) + if (supplied !== undefined) seenCallIds.add(supplied) + const id = supplied !== undefined && !duplicate ? supplied : `tool_${crypto.randomUUID().replaceAll("-", "")}` lifecycle = Lifecycle.reasoningEnd( lifecycle, events, @@ -637,7 +641,8 @@ const step = (state: ParserState, event: GeminiEvent) => { id, name: part.functionCall.name, input, - providerMetadata: Object.keys(metadata).length > 0 ? googleMetadata(metadata) : undefined, + providerMetadata: + part.thoughtSignature === undefined ? undefined : googleMetadata({ thoughtSignature: part.thoughtSignature }), }), ) hasToolCalls = true @@ -651,6 +656,7 @@ const step = (state: ParserState, event: GeminiEvent) => { lifecycle, reasoningSignature, textSignature, + seenCallIds, finishReason: candidate.finishReason ?? nextState.finishReason, }, events, diff --git a/packages/ai/src/providers/google-vertex.ts b/packages/ai/src/providers/google-vertex.ts index e0bf9357303..20f816997ee 100644 --- a/packages/ai/src/providers/google-vertex.ts +++ b/packages/ai/src/providers/google-vertex.ts @@ -38,13 +38,23 @@ export type Settings = ProviderPackage.Settings & const fromRequest = Effect.fn("GoogleVertex.fromRequest")(function* (request: LLMRequest) { const body = yield* Gemini.protocol.body.from(request) + // Vertex's native REST schema rejects `id` on FunctionCall/FunctionResponse parts with HTTP 400, + // unlike AI Studio, so history minted there cannot be lowered verbatim. + const contents = body.contents.map((content) => ({ + ...content, + parts: content.parts.map((part) => { + if ("functionCall" in part) return { ...part, functionCall: { ...part.functionCall, id: undefined } } + if ("functionResponse" in part) return { ...part, functionResponse: { ...part.functionResponse, id: undefined } } + return part + }), + })) const value = request.providerOptions?.labels const labels = ProviderShared.isRecord(value) ? Object.fromEntries( Object.entries(value).filter((entry): entry is [string, string] => typeof entry[1] === "string"), ) : undefined - return { ...body, labels } + return { ...body, contents, labels } }) const protocol = { diff --git a/packages/ai/test/fixtures/recordings/azure/chat-streams-text.json b/packages/ai/test/fixtures/recordings/azure/chat-streams-text.json new file mode 100644 index 00000000000..0dc0816b326 --- /dev/null +++ b/packages/ai/test/fixtures/recordings/azure/chat-streams-text.json @@ -0,0 +1,31 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:azure", + "provider:azure" + ], + "name": "azure/chat-streams-text", + "recordedAt": "2026-08-23T17:21:53.198Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://aiden-azury-group.openai.azure.com/openai/v1/chat/completions?api-version=v1", + "headers": { + "content-type": "application/json" + }, + "body": "{\"model\":\"gpt-5.6-luna\",\"messages\":[{\"role\":\"user\",\"content\":\"Reply with exactly one word: hello\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"store\":false,\"reasoning_effort\":\"medium\"}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream; charset=utf-8" + }, + "body": "data: {\"choices\":[],\"created\":0,\"id\":\"\",\"model\":\"\",\"object\":\"\",\"prompt_filter_results\":[{\"prompt_index\":0,\"content_filter_results\":{}}]}\n\ndata: {\"choices\":[{\"content_filter_results\":{},\"delta\":{\"content\":\"\",\"refusal\":null,\"role\":\"assistant\"},\"finish_reason\":null,\"index\":0,\"logprobs\":null}],\"created\":1787505712,\"id\":\"chatcmpl-EG6BEiYSfrcTSI2WX8PqNzERZDcPc\",\"model\":\"gpt-5.6-luna-2026-07-09\",\"obfuscation\":\"Mxr\",\"object\":\"chat.completion.chunk\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"usage\":null}\n\ndata: {\"choices\":[{\"content_filter_results\":{},\"delta\":{\"content\":\"hello\"},\"finish_reason\":null,\"index\":0,\"logprobs\":null}],\"created\":1787505712,\"id\":\"chatcmpl-EG6BEiYSfrcTSI2WX8PqNzERZDcPc\",\"model\":\"gpt-5.6-luna-2026-07-09\",\"obfuscation\":\"\",\"object\":\"chat.completion.chunk\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"usage\":null}\n\ndata: {\"choices\":[{\"content_filter_results\":{},\"delta\":{},\"finish_reason\":\"stop\",\"index\":0,\"logprobs\":null}],\"created\":1787505712,\"id\":\"chatcmpl-EG6BEiYSfrcTSI2WX8PqNzERZDcPc\",\"model\":\"gpt-5.6-luna-2026-07-09\",\"obfuscation\":\"WyZa5AY1CaCeFdS\",\"object\":\"chat.completion.chunk\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"usage\":null}\n\ndata: {\"choices\":[],\"created\":1787505712,\"id\":\"chatcmpl-EG6BEiYSfrcTSI2WX8PqNzERZDcPc\",\"latency_checkpoint\":{\"engine_tbt_ms\":20,\"engine_ttft_ms\":106,\"engine_ttlt_ms\":206,\"pre_inference_ms\":89,\"service_tbt_ms\":20,\"service_ttft_ms\":480,\"service_ttlt_ms\":576,\"total_duration_ms\":491,\"user_visible_ttft_ms\":391},\"model\":\"gpt-5.6-luna-2026-07-09\",\"obfuscation\":\"6\",\"object\":\"chat.completion.chunk\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"usage\":{\"completion_tokens\":5,\"completion_tokens_details\":{\"accepted_prediction_tokens\":0,\"audio_tokens\":0,\"reasoning_tokens\":0,\"rejected_prediction_tokens\":0},\"prompt_tokens\":13,\"prompt_tokens_details\":{\"audio_tokens\":0,\"cache_write_tokens\":0,\"cached_tokens\":0},\"total_tokens\":18}}\n\ndata: [DONE]\n\n" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/azure/responses-calls-a-tool.json b/packages/ai/test/fixtures/recordings/azure/responses-calls-a-tool.json new file mode 100644 index 00000000000..2c349382505 --- /dev/null +++ b/packages/ai/test/fixtures/recordings/azure/responses-calls-a-tool.json @@ -0,0 +1,31 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:azure", + "provider:azure" + ], + "name": "azure/responses-calls-a-tool", + "recordedAt": "2026-08-23T17:21:55.170Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://aiden-azury-group.openai.azure.com/openai/v1/responses?api-version=v1", + "headers": { + "content-type": "application/json" + }, + "body": "{\"model\":\"gpt-5.6-luna\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What is the weather in Paris? Use the lookup_weather tool.\"}]}],\"tools\":[{\"type\":\"function\",\"name\":\"lookup_weather\",\"description\":\"Look up the current weather for a city\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]},\"strict\":false}],\"store\":false,\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"stream\":true}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream; charset=utf-8" + }, + "body": "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_038668c35ae811d3016a8b2c3273888196a6e34efef3bd3193\",\"object\":\"response\",\"created_at\":1787505714,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"content_filters\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.6-luna\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"all_turns\",\"effort\":\"medium\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Look up the current weather for a city\",\"name\":\"lookup_weather\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}\n\nevent: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_038668c35ae811d3016a8b2c3273888196a6e34efef3bd3193\",\"object\":\"response\",\"created_at\":1787505714,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"content_filters\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.6-luna\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"all_turns\",\"effort\":\"medium\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Look up the current weather for a city\",\"name\":\"lookup_weather\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"fc_038668c35ae811d3016a8b2c332090819687d2f367bda0452b\",\"type\":\"function_call\",\"status\":\"in_progress\",\"arguments\":\"\",\"call_id\":\"call_urtlVDzFCdgMMgPqZHCTGmEL\",\"name\":\"lookup_weather\"},\"output_index\":0,\"sequence_number\":2}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"{\\\"\",\"item_id\":\"fc_038668c35ae811d3016a8b2c332090819687d2f367bda0452b\",\"obfuscation\":\"APjMzP58C5JCVD\",\"output_index\":0,\"sequence_number\":3}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"city\",\"item_id\":\"fc_038668c35ae811d3016a8b2c332090819687d2f367bda0452b\",\"obfuscation\":\"zoCPWwIaPzzU\",\"output_index\":0,\"sequence_number\":4}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\\\"\",\"item_id\":\"fc_038668c35ae811d3016a8b2c332090819687d2f367bda0452b\",\"obfuscation\":\"iTTH49MwEj9fT\",\"output_index\":0,\"sequence_number\":5}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"Paris\",\"item_id\":\"fc_038668c35ae811d3016a8b2c332090819687d2f367bda0452b\",\"obfuscation\":\"a4qZjBh3OWk\",\"output_index\":0,\"sequence_number\":6}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\"}\",\"item_id\":\"fc_038668c35ae811d3016a8b2c332090819687d2f367bda0452b\",\"obfuscation\":\"6Gj7WmQUxBa5qn\",\"output_index\":0,\"sequence_number\":7}\n\nevent: response.function_call_arguments.done\ndata: {\"type\":\"response.function_call_arguments.done\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\",\"item_id\":\"fc_038668c35ae811d3016a8b2c332090819687d2f367bda0452b\",\"output_index\":0,\"sequence_number\":8}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"fc_038668c35ae811d3016a8b2c332090819687d2f367bda0452b\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\",\"call_id\":\"call_urtlVDzFCdgMMgPqZHCTGmEL\",\"name\":\"lookup_weather\"},\"output_index\":0,\"sequence_number\":9}\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_038668c35ae811d3016a8b2c3273888196a6e34efef3bd3193\",\"object\":\"response\",\"created_at\":1787505714,\"status\":\"completed\",\"background\":false,\"completed_at\":1787505715,\"content_filters\":[{\"blocked\":false,\"source_type\":\"prompt\",\"content_filter_raw\":[],\"content_filter_results\":{},\"content_filter_offsets\":{\"start_offset\":0,\"end_offset\":91,\"check_offset\":0}}],\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.6-luna\",\"moderation\":null,\"output\":[{\"id\":\"fc_038668c35ae811d3016a8b2c332090819687d2f367bda0452b\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\",\"call_id\":\"call_urtlVDzFCdgMMgPqZHCTGmEL\",\"name\":\"lookup_weather\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"all_turns\",\"effort\":\"medium\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Look up the current weather for a city\",\"name\":\"lookup_weather\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":58,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":18,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":76},\"user\":null,\"metadata\":{}},\"sequence_number\":10}\n\n" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/azure/responses-continues-after-a-tool-result.json b/packages/ai/test/fixtures/recordings/azure/responses-continues-after-a-tool-result.json new file mode 100644 index 00000000000..b6c05e154c2 --- /dev/null +++ b/packages/ai/test/fixtures/recordings/azure/responses-continues-after-a-tool-result.json @@ -0,0 +1,31 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:azure", + "provider:azure" + ], + "name": "azure/responses-continues-after-a-tool-result", + "recordedAt": "2026-08-23T17:21:56.397Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://aiden-azury-group.openai.azure.com/openai/v1/responses?api-version=v1", + "headers": { + "content-type": "application/json" + }, + "body": "{\"model\":\"gpt-5.6-luna\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What is the weather in Paris?\"}]},{\"type\":\"function_call\",\"call_id\":\"call_paris_1\",\"name\":\"lookup_weather\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"},{\"type\":\"function_call_output\",\"call_id\":\"call_paris_1\",\"output\":\"18C, light rain\"}],\"tools\":[{\"type\":\"function\",\"name\":\"lookup_weather\",\"description\":\"Look up the current weather for a city\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]},\"strict\":false}],\"store\":false,\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"stream\":true}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream; charset=utf-8" + }, + "body": "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_01b60cb95ad658c9016a8b2c33725c819799bf17e133419dd9\",\"object\":\"response\",\"created_at\":1787505715,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"content_filters\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.6-luna\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"all_turns\",\"effort\":\"medium\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Look up the current weather for a city\",\"name\":\"lookup_weather\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}\n\nevent: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_01b60cb95ad658c9016a8b2c33725c819799bf17e133419dd9\",\"object\":\"response\",\"created_at\":1787505715,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"content_filters\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.6-luna\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"all_turns\",\"effort\":\"medium\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Look up the current weather for a city\",\"name\":\"lookup_weather\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_01b60cb95ad658c9016a8b2c3400d881979cbec18db62017b9\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":2}\n\nevent: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_01b60cb95ad658c9016a8b2c3400d881979cbec18db62017b9\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":3}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"Paris\",\"item_id\":\"msg_01b60cb95ad658c9016a8b2c3400d881979cbec18db62017b9\",\"logprobs\":[],\"obfuscation\":\"kVtk9YHGUCO\",\"output_index\":0,\"sequence_number\":4}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\":\",\"item_id\":\"msg_01b60cb95ad658c9016a8b2c3400d881979cbec18db62017b9\",\"logprobs\":[],\"obfuscation\":\"SvvJ5nQlhPVWRNp\",\"output_index\":0,\"sequence_number\":5}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" **\",\"item_id\":\"msg_01b60cb95ad658c9016a8b2c3400d881979cbec18db62017b9\",\"logprobs\":[],\"obfuscation\":\"CeAHvAozI8NCP\",\"output_index\":0,\"sequence_number\":6}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"18\",\"item_id\":\"msg_01b60cb95ad658c9016a8b2c3400d881979cbec18db62017b9\",\"logprobs\":[],\"obfuscation\":\"Jr3HTqogJ6b7s3\",\"output_index\":0,\"sequence_number\":7}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"°C\",\"item_id\":\"msg_01b60cb95ad658c9016a8b2c3400d881979cbec18db62017b9\",\"logprobs\":[],\"obfuscation\":\"MLPlrlsNmC9TDe\",\"output_index\":0,\"sequence_number\":8}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" with\",\"item_id\":\"msg_01b60cb95ad658c9016a8b2c3400d881979cbec18db62017b9\",\"logprobs\":[],\"obfuscation\":\"nDwBt5Ks23U\",\"output_index\":0,\"sequence_number\":9}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" light\",\"item_id\":\"msg_01b60cb95ad658c9016a8b2c3400d881979cbec18db62017b9\",\"logprobs\":[],\"obfuscation\":\"WU6DZAKbhG\",\"output_index\":0,\"sequence_number\":10}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" rain\",\"item_id\":\"msg_01b60cb95ad658c9016a8b2c3400d881979cbec18db62017b9\",\"logprobs\":[],\"obfuscation\":\"4WoPotQCvLO\",\"output_index\":0,\"sequence_number\":11}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\".\",\"item_id\":\"msg_01b60cb95ad658c9016a8b2c3400d881979cbec18db62017b9\",\"logprobs\":[],\"obfuscation\":\"WTemzAbrHlE0uRw\",\"output_index\":0,\"sequence_number\":12}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"**\",\"item_id\":\"msg_01b60cb95ad658c9016a8b2c3400d881979cbec18db62017b9\",\"logprobs\":[],\"obfuscation\":\"YL8WVDyEI9cvFL\",\"output_index\":0,\"sequence_number\":13}\n\nevent: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_01b60cb95ad658c9016a8b2c3400d881979cbec18db62017b9\",\"logprobs\":[],\"output_index\":0,\"sequence_number\":14,\"text\":\"Paris: **18°C with light rain.**\"}\n\nevent: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_01b60cb95ad658c9016a8b2c3400d881979cbec18db62017b9\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Paris: **18°C with light rain.**\"},\"sequence_number\":15}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_01b60cb95ad658c9016a8b2c3400d881979cbec18db62017b9\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Paris: **18°C with light rain.**\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":16}\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_01b60cb95ad658c9016a8b2c33725c819799bf17e133419dd9\",\"object\":\"response\",\"created_at\":1787505715,\"status\":\"completed\",\"background\":false,\"completed_at\":1787505716,\"content_filters\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.6-luna\",\"moderation\":null,\"output\":[{\"id\":\"msg_01b60cb95ad658c9016a8b2c3400d881979cbec18db62017b9\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Paris: **18°C with light rain.**\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"all_turns\",\"effort\":\"medium\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Look up the current weather for a city\",\"name\":\"lookup_weather\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":86,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":14,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":100},\"user\":null,\"metadata\":{}},\"sequence_number\":17}\n\n" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/azure/responses-streams-text.json b/packages/ai/test/fixtures/recordings/azure/responses-streams-text.json new file mode 100644 index 00000000000..937227ee4f7 --- /dev/null +++ b/packages/ai/test/fixtures/recordings/azure/responses-streams-text.json @@ -0,0 +1,31 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:azure", + "provider:azure" + ], + "name": "azure/responses-streams-text", + "recordedAt": "2026-08-23T17:21:54.158Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://aiden-azury-group.openai.azure.com/openai/v1/responses?api-version=v1", + "headers": { + "content-type": "application/json" + }, + "body": "{\"model\":\"gpt-5.6-luna\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Reply with exactly one word: bonjour\"}]}],\"store\":false,\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"stream\":true}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream; charset=utf-8" + }, + "body": "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_027c7373d7896543016a8b2c3166e08195b8ef40ca578fd90a\",\"object\":\"response\",\"created_at\":1787505713,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"content_filters\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.6-luna\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"all_turns\",\"effort\":\"medium\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}\n\nevent: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_027c7373d7896543016a8b2c3166e08195b8ef40ca578fd90a\",\"object\":\"response\",\"created_at\":1787505713,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"content_filters\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.6-luna\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"all_turns\",\"effort\":\"medium\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_027c7373d7896543016a8b2c3216e08195aa1e666726147dd0\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":2}\n\nevent: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_027c7373d7896543016a8b2c3216e08195aa1e666726147dd0\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":3}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"bonjour\",\"item_id\":\"msg_027c7373d7896543016a8b2c3216e08195aa1e666726147dd0\",\"logprobs\":[],\"obfuscation\":\"V8jlePTOW\",\"output_index\":0,\"sequence_number\":4}\n\nevent: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_027c7373d7896543016a8b2c3216e08195aa1e666726147dd0\",\"logprobs\":[],\"output_index\":0,\"sequence_number\":5,\"text\":\"bonjour\"}\n\nevent: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_027c7373d7896543016a8b2c3216e08195aa1e666726147dd0\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"bonjour\"},\"sequence_number\":6}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_027c7373d7896543016a8b2c3216e08195aa1e666726147dd0\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"bonjour\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":7}\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_027c7373d7896543016a8b2c3166e08195b8ef40ca578fd90a\",\"object\":\"response\",\"created_at\":1787505713,\"status\":\"completed\",\"background\":false,\"completed_at\":1787505714,\"content_filters\":[{\"blocked\":false,\"source_type\":\"prompt\",\"content_filter_raw\":[],\"content_filter_results\":{},\"content_filter_offsets\":{\"start_offset\":0,\"end_offset\":814,\"check_offset\":0}}],\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.6-luna\",\"moderation\":null,\"output\":[{\"id\":\"msg_027c7373d7896543016a8b2c3216e08195aa1e666726147dd0\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"bonjour\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"all_turns\",\"effort\":\"medium\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":14,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":5,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":19},\"user\":null,\"metadata\":{}},\"sequence_number\":8}\n\n" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/google-vertex/calls-a-tool.json b/packages/ai/test/fixtures/recordings/google-vertex/calls-a-tool.json new file mode 100644 index 00000000000..cc91c018bd0 --- /dev/null +++ b/packages/ai/test/fixtures/recordings/google-vertex/calls-a-tool.json @@ -0,0 +1,32 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:google-vertex", + "provider:google-vertex", + "protocol:gemini" + ], + "name": "google-vertex/calls-a-tool", + "recordedAt": "2026-08-23T17:21:51.036Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://aiplatform.googleapis.com/v1/publishers/google/models/gemini-3.5-flash:streamGenerateContent?alt=sse", + "headers": { + "content-type": "application/json" + }, + "body": "{\"contents\":[{\"role\":\"user\",\"parts\":[{\"text\":\"What is the weather in Paris? Use the lookup_weather tool.\"}]}],\"tools\":[{\"functionDeclarations\":[{\"name\":\"lookup_weather\",\"description\":\"Look up the current weather for a city\",\"parameters\":{\"required\":[\"city\"],\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}}}}]}]}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream" + }, + "body": "data: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"functionCall\": {\"name\": \"lookup_weather\",\"args\": {\"city\": \"Paris\"},\"id\": \"call_425130\"},\"thoughtSignature\": \"AY89a1+1fXnLgYhHMuN3Ak6LBhT6PcrYOW7iPav4LfsacvG/Z6l1yJ+AsU7vWhFj/JyPIbsJJQ+GjohM9sCIZ6nqUOIg3reo/7osmrCvFrVHedTHQcwiPzoz2Kp3gb+uWjFAXxk1EX4IRAKcu0ox1W/Z9PpuZvHkTerGO2a82e02N6MAF1YhhtbXFvSdqLRih2Os68rdOk5/Bcld7ol8qUgeyIZ3CtI3OJ5jwRcD8LjvK33A7ZFzH5Bxp/peUmXvqnu5iNhnGBxZaJy/vupCtxRZxjaS+ojG0/UhyrnRiKIpbzQ0FBkxePPn8GCX/LOe2y3GUc98co8lN8OOuCd9ZmEdx5AjHmQkPO9fAV9SxG6Bda6SDWVL8o/Uz3WSQYoUEfAdoajEWIBvcisoeCJjb7zgmRRZ9VQSPl3RXj5LFRvX8jn0YKV1CahYbc24jA==\"}]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-08-23T17:21:50.308576Z\",\"responseId\": \"LiyLauDqErCErb8Pj8aWkAs\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"\"}]},\"finishReason\": \"STOP\"}],\"usageMetadata\": {\"promptTokenCount\": 39,\"candidatesTokenCount\": 16,\"totalTokenCount\": 102,\"trafficType\": \"ON_DEMAND\",\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 39}],\"candidatesTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 16}],\"thoughtsTokenCount\": 47},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-08-23T17:21:50.308576Z\",\"responseId\": \"LiyLauDqErCErb8Pj8aWkAs\"}\r\n\r\n" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/google-vertex/continues-after-a-tool-result.json b/packages/ai/test/fixtures/recordings/google-vertex/continues-after-a-tool-result.json new file mode 100644 index 00000000000..f419d6e5c9f --- /dev/null +++ b/packages/ai/test/fixtures/recordings/google-vertex/continues-after-a-tool-result.json @@ -0,0 +1,32 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:google-vertex", + "provider:google-vertex", + "protocol:gemini" + ], + "name": "google-vertex/continues-after-a-tool-result", + "recordedAt": "2026-08-23T17:21:51.853Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://aiplatform.googleapis.com/v1/publishers/google/models/gemini-3.5-flash:streamGenerateContent?alt=sse", + "headers": { + "content-type": "application/json" + }, + "body": "{\"contents\":[{\"role\":\"user\",\"parts\":[{\"text\":\"What is the weather in Paris?\"}]},{\"role\":\"model\",\"parts\":[{\"functionCall\":{\"name\":\"lookup_weather\",\"args\":{\"city\":\"Paris\"}},\"thoughtSignature\":\"skip_thought_signature_validator\"}]},{\"role\":\"user\",\"parts\":[{\"functionResponse\":{\"name\":\"lookup_weather\",\"response\":{\"name\":\"lookup_weather\",\"content\":\"18C, light rain\"}}}]}],\"tools\":[{\"functionDeclarations\":[{\"name\":\"lookup_weather\",\"description\":\"Look up the current weather for a city\",\"parameters\":{\"required\":[\"city\"],\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}}}}]}]}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream" + }, + "body": "data: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"The weather in Paris is currently 18°C with light rain.\"}]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-08-23T17:21:51.220919Z\",\"responseId\": \"LyyLave9DbWnrb8P1IjLmQQ\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"\",\"thoughtSignature\": \"AY89a197c+fpHJftPtcufnqMAyoRQKVEQK+KeG+RVHVx2wKil3L4jP4YWvfVbcuOFr2jio4Kre/hCrDANAoMFSvaZrdaPeo1b5bXQSmJKMH03yM5M6q6ME6JiBvXym143U4exIde4UbOh2tMeyXMvB3aWxcavIHd78g5G5QPLreo6A3LO5871cYYVeRwteY+/zbEdqfaAq1hlk6WYpWkNljYpjMyKwr15YC8rFLh3HYayS9tTN++GGrk/reZn6C3OEPlzPou/pXRATzcEAGVl/TW\"}]},\"finishReason\": \"STOP\"}],\"usageMetadata\": {\"promptTokenCount\": 59,\"candidatesTokenCount\": 15,\"totalTokenCount\": 98,\"trafficType\": \"ON_DEMAND\",\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 59}],\"candidatesTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 15}],\"thoughtsTokenCount\": 24},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-08-23T17:21:51.220919Z\",\"responseId\": \"LyyLave9DbWnrb8P1IjLmQQ\"}\r\n\r\n" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/google-vertex/streams-text.json b/packages/ai/test/fixtures/recordings/google-vertex/streams-text.json new file mode 100644 index 00000000000..cc95b74191b --- /dev/null +++ b/packages/ai/test/fixtures/recordings/google-vertex/streams-text.json @@ -0,0 +1,32 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:google-vertex", + "provider:google-vertex", + "protocol:gemini" + ], + "name": "google-vertex/streams-text", + "recordedAt": "2026-08-23T17:21:50.112Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://aiplatform.googleapis.com/v1/publishers/google/models/gemini-3.5-flash:streamGenerateContent?alt=sse", + "headers": { + "content-type": "application/json" + }, + "body": "{\"contents\":[{\"role\":\"user\",\"parts\":[{\"text\":\"Reply with exactly one word: hello\"}]}]}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream" + }, + "body": "data: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"Hello\"}]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-08-23T17:21:48.528714Z\",\"responseId\": \"LCyLasqiIO6crb8P1sDboQc\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"\",\"thoughtSignature\": \"AY89a1+BGsRqlGpfT0psLB4jeTkT5rDV2HFOlrRuF7aVxDOjqNVUku6t4azeSnxpd+msHWuwXj4RS+7gmVlzVs+JNi8uj+iZWTBCi71vSh9kdK9ed/sHv9J7uL9ZWSOcgbhX/hxdXaUp5yVbQzHFXPjR9A/IkEkHV8VKarDZVFE1T1uASia74lkmyBeZZz+DQmRsLwbUHzFUKlF3qnk/SliLo21ZgASd7itlALQ0PBLJZwgeI3g7tDscDSE18hnB11Fky8q7MLd3HY16zbDvHBEMb18pmmPelPI01KdrCIwMSou/01/u5jiSUCc3pFksZawUj3tAHocHSC3ZKAQQQuUXGe5tm61C2E40/NANBeePc1S4HYE6Yo/vtX6tE02LDky5IQWX09H6+DZ7fpopP5nCUfcKPHa3hVjYquWYYMtZgXO4ZpxfVd3lt1VUDuJNN3BMMCZapjBoJZFPXPJ5t/yg9Rnd791+msGH77b4wztz1vtsPrT9oV9g6SDo9ZUH6BaOcbK7fw8FaXcGw+55malEwQy6zpRLGecooBu70p6RwhaAUyKIMX49y+F2hkNxQxDeBUNckJnu6n4w+KLyjP+bR0gqPJbGjVfteHm+QujqjJdBBT/m1u9kPo1nIbzdEs/PIADBdbuV7TkD/HoRFKpLnNmM2no8ioTtFEjKBDz4ippGi15r8pGgA6wIb/1HAvOGh+PVERdGcbelVTgfONwBqjQ7B1wmEizCfyYuMIskfwjxDGayfKlpDxrnNeogtEct9u5/DjEKlURlg9MtmW1B9P8BXYJ+7SCiRJWwW6bzB+5C+MLCnETl/mljDizoJMHK8DKIhI4oxBsrWXEuoHFwEwGIeOZq0BofH2Jz/l6+KIboV/zd581Kk0zPg/rlI6acfjUEtXtbF+t0+jzoJN7006x4i2tqXeJZ+4e5yisSArEsfJ0YzNWoJtBHG9V9/euDcEP3+jsr98efaQaQbLMPvT/Hb7CYQ7ChhGfcGxQ=\"}]},\"finishReason\": \"STOP\"}],\"usageMetadata\": {\"promptTokenCount\": 7,\"candidatesTokenCount\": 1,\"totalTokenCount\": 150,\"trafficType\": \"ON_DEMAND\",\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 7}],\"candidatesTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 1}],\"thoughtsTokenCount\": 142},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-08-23T17:21:48.528714Z\",\"responseId\": \"LCyLasqiIO6crb8P1sDboQc\"}\r\n\r\n" + } + } + ] +} diff --git a/packages/ai/test/provider/azure.recorded.test.ts b/packages/ai/test/provider/azure.recorded.test.ts new file mode 100644 index 00000000000..51e79f5a5b2 --- /dev/null +++ b/packages/ai/test/provider/azure.recorded.test.ts @@ -0,0 +1,93 @@ +import { describe, expect } from "bun:test" +import { Effect } from "effect" +import { LLM, Message, ToolDefinition, ToolCallPart } from "../../src/index.js" +import { Azure } from "../../src/providers.js" +import { LLMClient } from "../../src/route.js" +import { recordedTests } from "../recorded-test.js" + +const resourceName = process.env.AZURE_OPENAI_RESOURCE_NAME ?? "aiden-azury-group" + +const chatModel = Azure.configure({ + resourceName, + apiKey: process.env.AZURE_OPENAI_API_KEY ?? "fixture", +}).chat("gpt-5.6-luna") + +const responsesModel = Azure.configure({ + resourceName, + apiKey: process.env.AZURE_OPENAI_API_KEY ?? "fixture", +}).responses("gpt-5.6-luna") + +const lookupWeather = ToolDefinition.make({ + name: "lookup_weather", + description: "Look up the current weather for a city", + inputSchema: { type: "object", properties: { city: { type: "string" } }, required: ["city"] }, +}) + +const recorded = recordedTests({ + prefix: "azure", + provider: "azure", + requires: ["AZURE_OPENAI_API_KEY"], +}) + +describe("Azure OpenAI recorded", () => { + recorded.effect("chat streams text", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate( + LLM.request({ model: chatModel, prompt: "Reply with exactly one word: hello" }), + ) + + expect(response.text.toLowerCase()).toContain("hello") + }), + ) + + recorded.effect("responses streams text", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate( + LLM.request({ model: responsesModel, prompt: "Reply with exactly one word: bonjour" }), + ) + + expect(response.text.toLowerCase()).toContain("bonjour") + }), + ) + + recorded.effect("responses calls a tool", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate( + LLM.request({ + model: responsesModel, + prompt: "What is the weather in Paris? Use the lookup_weather tool.", + tools: [lookupWeather], + }), + ) + + const call = response.toolCalls.find((part) => part.name === "lookup_weather") + expect(call).toBeDefined() + expect(call?.input).toMatchObject({ city: "Paris" }) + }), + ) + + recorded.effect("responses continues after a tool result", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate( + LLM.request({ + model: responsesModel, + messages: [ + Message.user("What is the weather in Paris?"), + Message.assistant([ + ToolCallPart.make({ id: "call_paris_1", name: "lookup_weather", input: { city: "Paris" } }), + ]), + Message.tool({ + id: "call_paris_1", + name: "lookup_weather", + result: "18C, light rain", + resultType: "text", + }), + ], + tools: [lookupWeather], + }), + ) + + expect(response.text.length).toBeGreaterThan(0) + }), + ) +}) diff --git a/packages/ai/test/provider/gemini.test.ts b/packages/ai/test/provider/gemini.test.ts index 0c6766822df..dcbc8a79b40 100644 --- a/packages/ai/test/provider/gemini.test.ts +++ b/packages/ai/test/provider/gemini.test.ts @@ -156,14 +156,13 @@ describe("Gemini route", () => { expect(prepared.body.contents).toEqual([ { role: "model", - parts: [{ functionCall: { id: undefined, name: "lookup", args: { query: "weather" } } }], + parts: [{ functionCall: { name: "lookup", args: { query: "weather" } } }], }, { role: "user", parts: [ { functionResponse: { - id: undefined, name: "lookup", response: { name: "lookup", content: "done" }, }, @@ -201,8 +200,8 @@ describe("Gemini route", () => { { role: "model", parts: [ - { functionCall: { id: undefined, name: "lookup", args: { query: "weather" } } }, - { functionCall: { id: undefined, name: "lookup", args: { query: "time" } } }, + { functionCall: { name: "lookup", args: { query: "weather" } } }, + { functionCall: { name: "lookup", args: { query: "time" } } }, ], }, { @@ -210,14 +209,12 @@ describe("Gemini route", () => { parts: [ { functionResponse: { - id: undefined, name: "lookup", response: { name: "lookup", content: "sunny" }, }, }, { functionResponse: { - id: undefined, name: "lookup", response: { name: "lookup", content: "noon" }, }, @@ -228,6 +225,104 @@ describe("Gemini route", () => { }), ) + it.effect("lowers function call ids for gemini 3 models", () => + Effect.gen(function* () { + const prepared = yield* compileRequest( + LLM.request({ + model: gemini3, + messages: [ + Message.assistant([ToolCallPart.make({ id: "call_1", name: "lookup", input: { query: "weather" } })]), + Message.tool({ id: "call_1", name: "lookup", result: "done", resultType: "text" }), + ], + }), + ) + + expect(prepared.body.contents).toEqual([ + { + role: "model", + parts: [ + { + functionCall: { id: "call_1", name: "lookup", args: { query: "weather" } }, + thoughtSignature: "skip_thought_signature_validator", + }, + ], + }, + { + role: "user", + parts: [ + { + functionResponse: { + id: "call_1", + name: "lookup", + response: { name: "lookup", content: "done" }, + }, + }, + ], + }, + ]) + }), + ) + + it.effect("omits function call ids entirely for pre-gemini-3 models", () => + Effect.gen(function* () { + const messages = [ + Message.assistant([ToolCallPart.make({ id: "call_1", name: "lookup", input: { query: "weather" } })]), + Message.tool({ id: "call_1", name: "lookup", result: "done", resultType: "text" }), + ] + const legacy = yield* compileRequest(LLM.request({ model, messages })) + const older = yield* compileRequest( + LLM.request({ + model: Gemini.route + .with({ + endpoint: { baseURL: "https://generativelanguage.test/v1beta/" }, + auth: Auth.header("x-goog-api-key", "test"), + }) + .model({ id: "gemini-1.5-flash" }), + messages, + }), + ) + + expect(legacy.body.contents).toEqual([ + { role: "model", parts: [{ functionCall: { name: "lookup", args: { query: "weather" } } }] }, + { + role: "user", + parts: [{ functionResponse: { name: "lookup", response: { name: "lookup", content: "done" } } }], + }, + ]) + expect(JSON.stringify(legacy.body.contents)).not.toContain('"id"') + expect(JSON.stringify(older.body.contents)).not.toContain('"id"') + }), + ) + + it.effect("includes function call ids for non-gemini model ids", () => + Effect.gen(function* () { + const prepared = yield* compileRequest( + LLM.request({ + model: Gemini.route + .with({ + endpoint: { baseURL: "https://generativelanguage.test/v1beta/" }, + auth: Auth.header("x-goog-api-key", "test"), + }) + .model({ id: "gemma-3-27b-it" }), + messages: [ + Message.assistant([ToolCallPart.make({ id: "call_1", name: "lookup", input: { query: "weather" } })]), + Message.tool({ id: "call_1", name: "lookup", result: "done", resultType: "text" }), + ], + }), + ) + + expect(prepared.body.contents).toEqual([ + { role: "model", parts: [{ functionCall: { id: "call_1", name: "lookup", args: { query: "weather" } } }] }, + { + role: "user", + parts: [ + { functionResponse: { id: "call_1", name: "lookup", response: { name: "lookup", content: "done" } } }, + ], + }, + ]) + }), + ) + it.effect("prepares multimodal user input and tool history", () => Effect.gen(function* () { const prepared = yield* compileRequest( @@ -419,13 +514,16 @@ describe("Gemini route", () => { expect(prepared.body.contents).toEqual([ { role: "model", - parts: [{ functionCall: { name: "read", args: { path: "pixel.png" } }, thoughtSignature: "sig_1" }], + parts: [ + { functionCall: { id: "call_image", name: "read", args: { path: "pixel.png" } }, thoughtSignature: "sig_1" }, + ], }, { role: "user", parts: [ { functionResponse: { + id: "call_image", name: "read", response: { name: "read", content: "Image read successfully" }, parts: [{ inlineData: { mimeType: "image/png", data: "AAECAw==" } }], @@ -849,7 +947,7 @@ describe("Gemini route", () => { }) expect(toolCall).toMatchObject({ id: "provider_call", - providerMetadata: { google: { functionCallId: "provider_call", thoughtSignature: "tool_sig" } }, + providerMetadata: { google: { thoughtSignature: "tool_sig" } }, }) expect(response.events.findIndex((event) => event.type === "reasoning-end")).toBeLessThan( response.events.findIndex((event) => event.type === "tool-call"), @@ -857,7 +955,7 @@ describe("Gemini route", () => { const prepared = yield* compileRequest( LLM.request({ - model, + model: gemini3, messages: [ Message.assistant([ { type: "reasoning", text: "thinking", providerMetadata: reasoningEnd?.providerMetadata }, @@ -873,7 +971,6 @@ describe("Gemini route", () => { name: "lookup", result: "done", resultType: "text", - providerMetadata: toolCall?.providerMetadata, }), ], }), @@ -977,7 +1074,7 @@ describe("Gemini route", () => { role: "model", parts: [ { - functionCall: { id: undefined, name: "lookup", args: { query: "weather" } }, + functionCall: { id: "tool_0", name: "lookup", args: { query: "weather" } }, thoughtSignature: "skip_thought_signature_validator", }, ], @@ -987,7 +1084,7 @@ describe("Gemini route", () => { parts: [ { functionResponse: { - id: undefined, + id: "tool_0", name: "lookup", response: { name: "lookup", content: "done" }, }, @@ -1023,15 +1120,15 @@ describe("Gemini route", () => { role: "model", parts: [ { - functionCall: { id: undefined, name: "lookup", args: { query: "weather" } }, + functionCall: { id: "tool_0", name: "lookup", args: { query: "weather" } }, thoughtSignature: "parallel_signature", }, { - functionCall: { id: undefined, name: "lookup", args: { query: "news" } }, + functionCall: { id: "tool_1", name: "lookup", args: { query: "news" } }, thoughtSignature: undefined, }, { - functionCall: { id: undefined, name: "lookup", args: { query: "sports" } }, + functionCall: { id: "tool_2", name: "lookup", args: { query: "sports" } }, thoughtSignature: undefined, }, ], @@ -1059,11 +1156,11 @@ describe("Gemini route", () => { role: "model", parts: [ { - functionCall: { id: undefined, name: "lookup", args: { query: "weather" } }, + functionCall: { id: "tool_0", name: "lookup", args: { query: "weather" } }, thoughtSignature: "skip_thought_signature_validator", }, { - functionCall: { id: undefined, name: "lookup", args: { query: "news" } }, + functionCall: { id: "tool_1", name: "lookup", args: { query: "news" } }, thoughtSignature: "skip_thought_signature_validator", }, ], @@ -1214,7 +1311,6 @@ describe("Gemini route", () => { id: "call_0", name: "lookup", input: { query: "weather" }, - providerMetadata: { google: { functionCallId: "call_0" } }, }) expect(response.toolCalls[1]).toMatchObject({ type: "tool-call", @@ -1230,6 +1326,37 @@ describe("Gemini route", () => { }), ) + it.effect("replaces repeated supplier ids with fresh fallback ids", () => + Effect.gen(function* () { + const body = sseEvents({ + candidates: [ + { + content: { + role: "model", + parts: [ + { functionCall: { id: "dup_call", name: "lookup", args: { query: "weather" } } }, + { functionCall: { id: "dup_call", name: "lookup", args: { query: "news" } } }, + ], + }, + finishReason: "STOP", + }, + ], + }) + const response = yield* LLMClient.generate( + LLMRequest.update(request, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], + }), + ).pipe(Effect.provide(fixedResponse(body))) + + expect(response.toolCalls[0]).toMatchObject({ + id: "dup_call", + providerMetadata: undefined, + }) + expect(response.toolCalls[1].id).toMatch(/^tool_[0-9a-zA-Z]+$/) + expect(response.toolCalls[1].id).not.toBe(response.toolCalls[0].id) + }), + ) + it.effect("assigns distinct unique fallback ids across separate requests", () => Effect.gen(function* () { const body = sseEvents({ diff --git a/packages/ai/test/provider/google-vertex.recorded.test.ts b/packages/ai/test/provider/google-vertex.recorded.test.ts new file mode 100644 index 00000000000..3fab9bf12b2 --- /dev/null +++ b/packages/ai/test/provider/google-vertex.recorded.test.ts @@ -0,0 +1,77 @@ +import { describe, expect } from "bun:test" +import { Effect } from "effect" + +import { LLM, Message, ToolDefinition, ToolCallPart } from "../../src/index.js" +import { GoogleVertex } from "../../src/providers.js" +import { LLMClient } from "../../src/route.js" +import { recordedTests } from "../recorded-test.js" + +const model = GoogleVertex.configure({ + apiKey: process.env.GOOGLE_VERTEX_API_KEY ?? "fixture", +}).model("gemini-3.5-flash") + +const lookupWeather = ToolDefinition.make({ + name: "lookup_weather", + description: "Look up the current weather for a city", + inputSchema: { type: "object", properties: { city: { type: "string" } }, required: ["city"] }, +}) + +const recorded = recordedTests({ + prefix: "google-vertex", + provider: "google-vertex", + protocol: "gemini", + requires: ["GOOGLE_VERTEX_API_KEY"], +}) + +describe("Google Vertex Gemini recorded", () => { + recorded.effect("streams text", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate( + LLM.request({ model, prompt: "Reply with exactly one word: hello" }), + ) + + expect(response.text.toLowerCase()).toContain("hello") + }), + ) + + recorded.effect("calls a tool", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate( + LLM.request({ + model, + prompt: "What is the weather in Paris? Use the lookup_weather tool.", + tools: [lookupWeather], + }), + ) + + const call = response.toolCalls.find((part) => part.name === "lookup_weather") + expect(call).toBeDefined() + expect(call?.input).toMatchObject({ city: "Paris" }) + }), + ) + + recorded.effect("continues after a tool result", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate( + LLM.request({ + model, + messages: [ + Message.user("What is the weather in Paris?"), + Message.assistant([ + ToolCallPart.make({ id: "call_paris_1", name: "lookup_weather", input: { city: "Paris" } }), + ]), + Message.tool({ + id: "call_paris_1", + name: "lookup_weather", + result: "18C, light rain", + resultType: "text", + }), + ], + tools: [lookupWeather], + }), + ) + + expect(response.text.length).toBeGreaterThan(0) + }), + ) +}) diff --git a/packages/ai/test/provider/google-vertex.test.ts b/packages/ai/test/provider/google-vertex.test.ts index ac9f61cdc99..e7ed8a0b09b 100644 --- a/packages/ai/test/provider/google-vertex.test.ts +++ b/packages/ai/test/provider/google-vertex.test.ts @@ -1,7 +1,7 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" import { HttpClientRequest } from "effect/unstable/http" -import { LLM } from "../../src/index.js" +import { LLM, Message, ToolCallPart } from "../../src/index.js" import { GoogleVertex, GoogleVertexChat, GoogleVertexMessages, GoogleVertexResponses } from "../../src/providers.js" import { LLMClient } from "../../src/route.js" import { compileRequest } from "../../src/route/client.js" @@ -75,6 +75,53 @@ describe("Google Vertex providers", () => { }), ) + it.effect("strips function call ids Vertex does not accept from lowered bodies", () => + Effect.gen(function* () { + const prepared = yield* compileRequest( + LLM.request({ + model: GoogleVertex.configure({ + accessToken: "vertex-token", + project: "vertex-project", + }).model("gemini-3.5-flash"), + messages: [ + Message.assistant([ + ToolCallPart.make({ + id: "call_1", + name: "lookup", + input: { query: "weather" }, + providerMetadata: { google: { functionCallId: "provider_call_1" } }, + }), + ]), + Message.tool({ + id: "call_1", + name: "lookup", + result: "sunny", + resultType: "text", + providerMetadata: { google: { functionCallId: "provider_call_1" } }, + }), + ], + }), + ) + + expect(JSON.stringify(prepared.body.contents)).not.toContain('"id"') + expect(prepared.body.contents).toMatchObject([ + { role: "model", parts: [{ functionCall: { id: undefined, name: "lookup", args: { query: "weather" } } }] }, + { + role: "user", + parts: [ + { + functionResponse: { + id: undefined, + name: "lookup", + response: { name: "lookup", content: "sunny" }, + }, + }, + ], + }, + ]) + }), + ) + it.effect("projects Anthropic Messages onto the Vertex raw-predict API", () => Effect.gen(function* () { const model = GoogleVertexMessages.configure({ diff --git a/packages/ai/test/tool-runtime.test.ts b/packages/ai/test/tool-runtime.test.ts index 2000c1f99ed..e1af1fc55df 100644 --- a/packages/ai/test/tool-runtime.test.ts +++ b/packages/ai/test/tool-runtime.test.ts @@ -191,7 +191,7 @@ describe("LLMClient tools", () => { success: Schema.String, execute: () => Effect.succeed("hello"), }) - const providerMetadata = { google: { functionCallId: "provider_call" } } + const providerMetadata = { google: { thoughtSignature: "provider_sig" } } const dispatched = yield* ToolRuntime.dispatch( { tool }, LLMEvent.toolCall({ id: "call_1", name: "tool", input: {}, providerMetadata }),