@tanstack/openai-base 0.9.14 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/esm/adapters/chat-completions-text.js +2 -1
  2. package/dist/esm/adapters/chat-completions-text.js.map +1 -1
  3. package/dist/esm/adapters/responses-text.d.ts +1 -1
  4. package/dist/esm/adapters/responses-text.js +26 -3
  5. package/dist/esm/adapters/responses-text.js.map +1 -1
  6. package/dist/esm/index.js +1 -0
  7. package/dist/esm/tools/apply-patch-tool.js +3 -2
  8. package/dist/esm/tools/apply-patch-tool.js.map +1 -1
  9. package/dist/esm/tools/code-interpreter-tool.js +4 -3
  10. package/dist/esm/tools/code-interpreter-tool.js.map +1 -1
  11. package/dist/esm/tools/computer-use-tool.js +4 -3
  12. package/dist/esm/tools/computer-use-tool.js.map +1 -1
  13. package/dist/esm/tools/custom-tool.js +4 -3
  14. package/dist/esm/tools/custom-tool.js.map +1 -1
  15. package/dist/esm/tools/file-search-tool.js +4 -3
  16. package/dist/esm/tools/file-search-tool.js.map +1 -1
  17. package/dist/esm/tools/image-generation-tool.d.ts +2 -4
  18. package/dist/esm/tools/image-generation-tool.js +6 -7
  19. package/dist/esm/tools/image-generation-tool.js.map +1 -1
  20. package/dist/esm/tools/index.js +14 -0
  21. package/dist/esm/tools/local-shell-tool.js +3 -2
  22. package/dist/esm/tools/local-shell-tool.js.map +1 -1
  23. package/dist/esm/tools/mcp-tool.js +7 -6
  24. package/dist/esm/tools/mcp-tool.js.map +1 -1
  25. package/dist/esm/tools/openai-provider-tool.d.ts +20 -0
  26. package/dist/esm/tools/openai-provider-tool.js +49 -0
  27. package/dist/esm/tools/openai-provider-tool.js.map +1 -0
  28. package/dist/esm/tools/shell-tool.js +4 -3
  29. package/dist/esm/tools/shell-tool.js.map +1 -1
  30. package/dist/esm/tools/tool-converter.js +7 -18
  31. package/dist/esm/tools/tool-converter.js.map +1 -1
  32. package/dist/esm/tools/web-search-preview-tool.d.ts +1 -2
  33. package/dist/esm/tools/web-search-preview-tool.js +5 -5
  34. package/dist/esm/tools/web-search-preview-tool.js.map +1 -1
  35. package/dist/esm/tools/web-search-tool.d.ts +2 -4
  36. package/dist/esm/tools/web-search-tool.js +6 -7
  37. package/dist/esm/tools/web-search-tool.js.map +1 -1
  38. package/package.json +3 -3
  39. package/src/adapters/chat-completions-text.ts +11 -0
  40. package/src/adapters/responses-text.ts +86 -7
  41. package/src/tools/apply-patch-tool.ts +9 -5
  42. package/src/tools/code-interpreter-tool.ts +17 -8
  43. package/src/tools/computer-use-tool.ts +14 -7
  44. package/src/tools/custom-tool.ts +14 -7
  45. package/src/tools/file-search-tool.ts +14 -7
  46. package/src/tools/image-generation-tool.ts +19 -11
  47. package/src/tools/local-shell-tool.ts +9 -5
  48. package/src/tools/mcp-tool.ts +19 -9
  49. package/src/tools/openai-provider-tool.ts +72 -0
  50. package/src/tools/shell-tool.ts +17 -9
  51. package/src/tools/tool-converter.ts +28 -43
  52. package/src/tools/web-search-preview-tool.ts +17 -8
  53. package/src/tools/web-search-tool.ts +18 -10
@@ -3,8 +3,8 @@ import { buildChatCompletionsUsage } from "../usage.js";
3
3
  import { extractRequestOptions } from "../utils/request-options.js";
4
4
  import { convertToolsToChatCompletionsFormat } from "./chat-completions-tool-converter.js";
5
5
  import { EventType, normalizeSystemPrompts } from "@tanstack/ai";
6
- import { BaseTextAdapter } from "@tanstack/ai/adapters";
7
6
  import { toRunErrorPayload, toRunErrorRawEvent } from "@tanstack/ai/adapter-internals";
7
+ import { BaseTextAdapter } from "@tanstack/ai/adapters";
8
8
  import { generateId } from "@tanstack/ai-utils";
9
9
  //#region src/adapters/chat-completions-text.ts
10
10
  /**
@@ -868,6 +868,7 @@ var OpenAIBaseChatCompletionsTextAdapter = class extends BaseTextAdapter {
868
868
  }
869
869
  };
870
870
  }
871
+ if (part.type === "document") throw new Error(`${this.name} does not support document parts on the Chat Completions API; use the Responses adapter, which sends them as input_file.`);
871
872
  return null;
872
873
  }
873
874
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"chat-completions-text.js","names":[],"sources":["../../../src/adapters/chat-completions-text.ts"],"sourcesContent":["import { EventType, normalizeSystemPrompts } from '@tanstack/ai'\nimport { BaseTextAdapter } from '@tanstack/ai/adapters'\nimport {\n toRunErrorPayload,\n toRunErrorRawEvent,\n} from '@tanstack/ai/adapter-internals'\nimport { generateId } from '@tanstack/ai-utils'\nimport { extractRequestOptions } from '../utils/request-options'\nimport { makeStructuredOutputCompatible } from '../utils/schema-converter'\nimport { buildChatCompletionsUsage } from '../usage'\nimport { convertToolsToChatCompletionsFormat } from './chat-completions-tool-converter'\nimport type OpenAI from 'openai'\nimport type {\n StructuredOutputOptions,\n StructuredOutputResult,\n} from '@tanstack/ai/adapters'\nimport type {\n ChatCompletionChunk,\n ChatCompletionContentPart,\n ChatCompletionCreateParamsStreaming,\n ChatCompletionMessageParam,\n} from 'openai/resources/chat/completions/completions'\nimport type {\n ContentPart,\n DefaultMessageMetadataByModality,\n Modality,\n ModelMessage,\n RunFinishedEvent,\n StreamChunk,\n TextOptions,\n} from '@tanstack/ai'\n\n/**\n * Shared implementation of the OpenAI Chat Completions API. Holds the\n * stream-accumulator + AG-UI lifecycle logic and calls the OpenAI SDK\n * directly. Subclasses (ai-openai, ai-grok, ai-groq) construct an OpenAI\n * client with their provider-specific `baseURL` / headers and pass it in.\n */\nexport abstract class OpenAIBaseChatCompletionsTextAdapter<\n TModel extends string,\n TProviderOptions extends Record<string, unknown> = Record<string, unknown>,\n TInputModalities extends ReadonlyArray<Modality> = ReadonlyArray<Modality>,\n TMessageMetadata extends DefaultMessageMetadataByModality =\n DefaultMessageMetadataByModality,\n TToolCapabilities extends ReadonlyArray<string> = ReadonlyArray<string>,\n> extends BaseTextAdapter<\n TModel,\n TProviderOptions,\n TInputModalities,\n TMessageMetadata,\n TToolCapabilities\n> {\n override readonly kind = 'text' as const\n readonly name: string\n protected client: OpenAI\n\n constructor(model: TModel, name: string, client: OpenAI) {\n super({}, model)\n this.name = name\n this.client = client\n }\n\n async *chatStream(\n options: TextOptions<TProviderOptions>,\n ): AsyncIterable<StreamChunk> {\n // AG-UI lifecycle tracking (mutable state object for ESLint compatibility)\n const aguiState = {\n runId: generateId(this.name),\n threadId: options.threadId ?? generateId(this.name),\n messageId: generateId(this.name),\n hasEmittedRunStarted: false,\n }\n\n try {\n // mapOptionsToRequest can throw (e.g. fail-loud guards in convertMessage\n // for empty content or unsupported parts). Keep it inside the try so\n // those failures surface as a single RUN_ERROR event, matching every\n // other failure mode here — callers iterating chatStream then only need\n // one error-handling path instead of both a try/catch around iteration\n // and a RUN_ERROR handler.\n const requestParams = this.mapOptionsToRequest(options)\n options.logger.request(\n `activity=chat provider=${this.name} model=${this.model} messages=${options.messages.length} tools=${options.tools?.length ?? 0} stream=true`,\n { provider: this.name, model: this.model },\n )\n const stream = await this.client.chat.completions.create(\n {\n ...requestParams,\n stream: true,\n stream_options: { include_usage: true },\n },\n extractRequestOptions(options.request),\n )\n\n yield* this.processStreamChunks(stream, options, aguiState)\n } catch (error: unknown) {\n // Narrow before logging: raw SDK errors can carry request metadata\n // (including auth headers) which we must never surface to user loggers.\n const errorPayload = toRunErrorPayload(\n error,\n `${this.name}.chatStream failed`,\n )\n const rawEvent = toRunErrorRawEvent(error)\n\n // Emit RUN_STARTED if not yet emitted\n if (!aguiState.hasEmittedRunStarted) {\n aguiState.hasEmittedRunStarted = true\n yield {\n type: EventType.RUN_STARTED,\n runId: aguiState.runId,\n threadId: aguiState.threadId,\n model: options.model,\n timestamp: Date.now(),\n parentRunId: options.parentRunId,\n }\n }\n\n const rejectedToolCall = this.extractRejectedToolCall(\n rawEvent,\n errorPayload.message,\n )\n if (rejectedToolCall) {\n const toolCallId = generateId(this.name)\n yield {\n type: EventType.TOOL_CALL_START,\n toolCallId,\n toolCallName: rejectedToolCall.toolName,\n toolName: rejectedToolCall.toolName,\n parentMessageId: aguiState.messageId,\n model: options.model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.TOOL_CALL_ARGS,\n toolCallId,\n delta: rejectedToolCall.arguments,\n args: rejectedToolCall.arguments,\n model: options.model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.TOOL_CALL_END,\n toolCallId,\n toolCallName: rejectedToolCall.toolName,\n toolName: rejectedToolCall.toolName,\n ...(rejectedToolCall.input !== undefined && {\n input: rejectedToolCall.input,\n }),\n result: JSON.stringify({ error: rejectedToolCall.error }),\n state: 'output-error',\n model: options.model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.RUN_FINISHED,\n runId: aguiState.runId,\n threadId: aguiState.threadId,\n model: options.model,\n timestamp: Date.now(),\n finishReason: 'tool_calls',\n }\n return\n }\n\n // Emit AG-UI RUN_ERROR. Conditional `code` spread keeps the wire\n // shape spec-compliant under `exactOptionalPropertyTypes`: AG-UI's\n // `RunErrorEvent.code` is `string?` (absent vs explicit `undefined`\n // matter), so we omit the key when there's no code.\n yield {\n type: EventType.RUN_ERROR,\n model: options.model,\n timestamp: Date.now(),\n message: errorPayload.message,\n code: errorPayload.code,\n // Forward the provider's structured error body so consumers can recover\n // the upstream detail the `{ message, code }` payload drops. Omitted\n // when the error carried no provider body (see toRunErrorRawEvent).\n ...(rawEvent !== undefined && { rawEvent }),\n error: {\n message: errorPayload.message,\n code: errorPayload.code,\n },\n }\n\n options.logger.errors(`${this.name}.chatStream fatal`, {\n error: errorPayload,\n source: `${this.name}.chatStream`,\n })\n }\n }\n\n /**\n * Extracts a rejected tool call from a provider error. Returned calls are\n * emitted as non-executable `output-error` results so the model can repair them.\n */\n protected extractRejectedToolCall(\n _rawEvent: unknown,\n _fallbackMessage: string,\n ):\n | {\n toolName: string\n arguments: string\n input?: unknown\n error: string\n }\n | undefined {\n return undefined\n }\n\n /**\n * Generate structured output using the provider's JSON Schema response format.\n * Uses stream: false to get the complete response in one call.\n *\n * OpenAI-compatible APIs have strict requirements for structured output:\n * - All properties must be in the `required` array\n * - Optional fields should have null added to their type union\n * - additionalProperties must be false for all objects\n *\n * The outputSchema is already JSON Schema (converted in the ai layer).\n * We apply provider-specific transformations for structured output compatibility.\n */\n async structuredOutput(\n options: StructuredOutputOptions<TProviderOptions>,\n ): Promise<StructuredOutputResult<unknown>> {\n const { chatOptions, outputSchema } = options\n const requestParams = this.mapOptionsToRequest(chatOptions)\n\n const jsonSchema = this.makeStructuredOutputCompatible(\n outputSchema,\n outputSchema.required,\n )\n\n try {\n // Strip stream_options which is only valid for streaming calls\n const {\n stream_options: _,\n stream: __,\n ...cleanParams\n } = requestParams as any\n chatOptions.logger.request(\n `activity=structuredOutput provider=${this.name} model=${this.model} messages=${chatOptions.messages.length}`,\n { provider: this.name, model: this.model },\n )\n const response = await this.client.chat.completions.create(\n {\n ...cleanParams,\n stream: false,\n response_format: {\n type: 'json_schema',\n json_schema: {\n name: 'structured_output',\n schema: jsonSchema,\n strict: true,\n },\n },\n },\n extractRequestOptions(chatOptions.request),\n )\n\n // Extract text content from the response. Fail loud on empty content\n // rather than letting it cascade into a JSON-parse error on '' — the\n // root cause (the model returned no content for the structured request)\n // is then visible in logs.\n const rawText = response.choices[0]?.message.content\n if (typeof rawText !== 'string' || rawText.length === 0) {\n throw new Error(\n `${this.name}.structuredOutput: response contained no content`,\n )\n }\n\n // Parse the JSON response\n let parsed: unknown\n try {\n parsed = JSON.parse(rawText)\n } catch {\n throw new Error(\n `Failed to parse structured output as JSON. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? '...' : ''}`,\n )\n }\n\n // Final provider-specific shaping pass (default passthrough). Null-widening\n // from strict mode is undone by the engine, not here.\n const transformed = this.transformStructuredOutput(parsed)\n\n // Surface usage so non-stream structured paths (and\n // fallbackStructuredOutputStream) can forward tokens to middleware.\n const usage = buildChatCompletionsUsage(response.usage)\n return {\n data: transformed,\n rawText,\n ...(usage && { usage }),\n }\n } catch (error: unknown) {\n // Narrow before logging: raw SDK errors can carry request metadata\n // (including auth headers) which we must never surface to user loggers.\n chatOptions.logger.errors(`${this.name}.structuredOutput fatal`, {\n error: toRunErrorPayload(error, `${this.name}.structuredOutput failed`),\n source: `${this.name}.structuredOutput`,\n })\n throw error\n }\n }\n\n /**\n * Stream structured output. Single Chat Completions request with\n * `response_format: json_schema` + `stream: true`. Emits the standard\n * AG-UI lifecycle (`RUN_STARTED` → `REASONING_*?` → `TEXT_MESSAGE_*`\n * carrying raw JSON deltas → terminal `CUSTOM 'structured-output.complete'`\n * → `RUN_FINISHED`). Subclasses use the same SDK-call / reasoning /\n * structured-output-transform hooks as `chatStream` / `structuredOutput` —\n * no per-subclass override should be needed.\n */\n async *structuredOutputStream(\n options: StructuredOutputOptions<TProviderOptions>,\n ): AsyncIterable<StreamChunk> {\n const { chatOptions, outputSchema } = options\n const requestParams = this.mapOptionsToRequest(chatOptions)\n\n const jsonSchema = this.makeStructuredOutputCompatible(\n outputSchema,\n outputSchema.required,\n )\n\n const aguiState = {\n runId: generateId(this.name),\n threadId: chatOptions.threadId ?? generateId(this.name),\n messageId: generateId(this.name),\n hasEmittedRunStarted: false,\n }\n\n let accumulatedContent = ''\n let accumulatedReasoning = ''\n let hasEmittedTextMessageStart = false\n let reasoningMessageId: string | undefined\n let hasClosedReasoning = false\n let stepId: string | undefined\n let lastModel: string | undefined\n let lastUsage:\n | OpenAI.Chat.Completions.ChatCompletionChunk['usage']\n | undefined\n\n const closeReasoningLifecycle = function* (this: {\n name: string\n }): Generator<StreamChunk> {\n if (reasoningMessageId && !hasClosedReasoning) {\n hasClosedReasoning = true\n yield {\n type: EventType.REASONING_MESSAGE_END,\n messageId: reasoningMessageId,\n model: lastModel || chatOptions.model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.REASONING_END,\n messageId: reasoningMessageId,\n model: lastModel || chatOptions.model,\n timestamp: Date.now(),\n }\n if (stepId) {\n yield {\n type: EventType.STEP_FINISHED,\n stepName: stepId,\n stepId,\n model: lastModel || chatOptions.model,\n timestamp: Date.now(),\n content: accumulatedReasoning,\n }\n }\n }\n }.bind(this)\n\n try {\n // Strip stream_options + tools from the base request. Structured output\n // sends `response_format: json_schema` and doesn't carry tools — keeping\n // them in the request can confuse strict-mode validation upstream.\n const {\n stream_options: _so,\n stream: _s,\n tools: _t,\n ...cleanParams\n } = requestParams\n\n chatOptions.logger.request(\n `activity=structuredOutputStream provider=${this.name} model=${this.model} messages=${chatOptions.messages.length}`,\n { provider: this.name, model: this.model },\n )\n\n const stream = await this.client.chat.completions.create(\n {\n ...cleanParams,\n stream: true,\n stream_options: { include_usage: true },\n response_format: {\n type: 'json_schema',\n json_schema: {\n name: 'structured_output',\n schema: jsonSchema,\n strict: true,\n },\n },\n },\n extractRequestOptions(chatOptions.request),\n )\n\n for await (const chunk of stream) {\n const choiceForLog = chunk.choices[0]\n chatOptions.logger.provider(\n `provider=${this.name} finish_reason=${choiceForLog?.finish_reason ?? 'none'} hasContent=${!!choiceForLog?.delta.content} hasUsage=${!!chunk.usage}`,\n { provider: this.name, model: chunk.model },\n )\n\n if (chunk.model) lastModel = chunk.model\n\n // Usage may arrive on a chunk with empty `choices` (OpenAI's\n // include_usage terminal chunk) or piggybacked on a finish chunk\n // (`x_groq.usage` on Groq). Capture from either independent of\n // choices[0].\n const usage =\n chunk.usage ??\n (chunk as { x_groq?: { usage?: typeof chunk.usage } }).x_groq?.usage\n if (usage) lastUsage = usage\n\n if (!aguiState.hasEmittedRunStarted) {\n aguiState.hasEmittedRunStarted = true\n yield {\n type: EventType.RUN_STARTED,\n runId: aguiState.runId,\n threadId: aguiState.threadId,\n model: chunk.model || chatOptions.model,\n timestamp: Date.now(),\n parentRunId: chatOptions.parentRunId,\n }\n }\n\n // Reasoning (via the extractReasoning hook — same hook as chatStream).\n const reasoning = this.extractReasoning(chunk)\n if (reasoning && reasoning.text) {\n if (!reasoningMessageId) {\n reasoningMessageId = generateId(this.name)\n stepId = generateId(this.name)\n yield {\n type: EventType.REASONING_START,\n messageId: reasoningMessageId,\n model: chunk.model || chatOptions.model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.REASONING_MESSAGE_START,\n messageId: reasoningMessageId,\n role: 'reasoning' as const,\n model: chunk.model || chatOptions.model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.STEP_STARTED,\n stepName: stepId,\n stepId,\n model: chunk.model || chatOptions.model,\n timestamp: Date.now(),\n stepType: 'thinking',\n }\n }\n accumulatedReasoning += reasoning.text\n yield {\n type: EventType.REASONING_MESSAGE_CONTENT,\n messageId: reasoningMessageId,\n delta: reasoning.text,\n model: chunk.model || chatOptions.model,\n timestamp: Date.now(),\n }\n }\n\n const choice = chunk.choices[0]\n if (!choice) continue\n\n const deltaContent = choice.delta.content\n if (deltaContent) {\n yield* closeReasoningLifecycle()\n\n if (!hasEmittedTextMessageStart) {\n hasEmittedTextMessageStart = true\n yield {\n type: EventType.TEXT_MESSAGE_START,\n messageId: aguiState.messageId,\n model: chunk.model || chatOptions.model,\n timestamp: Date.now(),\n role: 'assistant',\n }\n }\n\n accumulatedContent += deltaContent\n\n yield {\n type: EventType.TEXT_MESSAGE_CONTENT,\n messageId: aguiState.messageId,\n model: chunk.model || chatOptions.model,\n timestamp: Date.now(),\n delta: deltaContent,\n content: accumulatedContent,\n }\n }\n }\n\n // Finalisation: close any open lifecycle, parse + validate, emit\n // terminal events. This block always runs unless the loop threw — abort\n // and SDK errors land in the catch block below.\n yield* closeReasoningLifecycle()\n\n if (hasEmittedTextMessageStart) {\n yield {\n type: EventType.TEXT_MESSAGE_END,\n messageId: aguiState.messageId,\n model: lastModel || chatOptions.model,\n timestamp: Date.now(),\n }\n }\n\n if (accumulatedContent.length === 0) {\n yield {\n type: EventType.RUN_ERROR,\n runId: aguiState.runId,\n model: lastModel || chatOptions.model,\n timestamp: Date.now(),\n message: `${this.name}.structuredOutputStream: response contained no content`,\n code: 'empty-response',\n error: {\n message: `${this.name}.structuredOutputStream: response contained no content`,\n code: 'empty-response',\n },\n }\n return\n }\n\n let parsed: unknown\n try {\n parsed = JSON.parse(accumulatedContent)\n } catch {\n yield {\n type: EventType.RUN_ERROR,\n runId: aguiState.runId,\n model: lastModel || chatOptions.model,\n timestamp: Date.now(),\n message: `Failed to parse structured output as JSON. Content: ${accumulatedContent.slice(0, 200)}${accumulatedContent.length > 200 ? '...' : ''}`,\n code: 'parse-error',\n error: {\n message: 'Failed to parse structured output as JSON',\n code: 'parse-error',\n },\n }\n return\n }\n\n const transformed = this.transformStructuredOutput(parsed)\n\n yield {\n type: EventType.CUSTOM,\n name: 'structured-output.complete',\n value: {\n object: transformed,\n raw: accumulatedContent,\n ...(accumulatedReasoning ? { reasoning: accumulatedReasoning } : {}),\n },\n model: lastModel || chatOptions.model,\n timestamp: Date.now(),\n }\n\n yield {\n type: EventType.RUN_FINISHED,\n runId: aguiState.runId,\n threadId: aguiState.threadId,\n model: lastModel || chatOptions.model,\n timestamp: Date.now(),\n finishReason: 'stop',\n ...(lastUsage && {\n usage: buildChatCompletionsUsage(lastUsage),\n }),\n }\n } catch (error: unknown) {\n if (!aguiState.hasEmittedRunStarted) {\n aguiState.hasEmittedRunStarted = true\n yield {\n type: EventType.RUN_STARTED,\n runId: aguiState.runId,\n threadId: aguiState.threadId,\n model: chatOptions.model,\n timestamp: Date.now(),\n parentRunId: chatOptions.parentRunId,\n }\n }\n\n const isAbort = this.isAbortError(error)\n const errorPayload = toRunErrorPayload(\n error,\n `${this.name}.structuredOutputStream failed`,\n )\n\n // Conditional `code` spread keeps the wire shape spec-compliant under\n // `exactOptionalPropertyTypes`: AG-UI's `RunErrorEvent.code` is `string?`\n // (absent vs explicit `undefined` matter).\n const resolvedCode = isAbort ? 'aborted' : errorPayload.code\n const rawEvent = isAbort ? undefined : toRunErrorRawEvent(error)\n yield {\n type: EventType.RUN_ERROR,\n runId: aguiState.runId,\n model: lastModel || chatOptions.model,\n timestamp: Date.now(),\n message: errorPayload.message,\n ...(resolvedCode !== undefined && { code: resolvedCode }),\n ...(rawEvent !== undefined && { rawEvent }),\n error: {\n message: errorPayload.message,\n ...(resolvedCode !== undefined && { code: resolvedCode }),\n },\n }\n\n chatOptions.logger.errors(`${this.name}.structuredOutputStream fatal`, {\n error: errorPayload,\n source: `${this.name}.structuredOutputStream`,\n })\n }\n }\n\n /**\n * Cross-SDK abort detection for `structuredOutputStream`. Default duck-types\n * on `name === 'APIUserAbortError'` (OpenAI SDK), `code === 'ERR_CANCELED'`,\n * and standard `AbortError`s. Subclasses with proprietary error types (e.g.\n * `@openrouter/sdk`'s `RequestAbortedError`) override to extend the check.\n */\n protected isAbortError(error: unknown): boolean {\n if (!error || typeof error !== 'object') return false\n const e = error as { name?: unknown; code?: unknown }\n return (\n e.name === 'APIUserAbortError' ||\n e.name === 'AbortError' ||\n e.code === 'ERR_CANCELED'\n )\n }\n\n /**\n * Applies provider-specific transformations for structured output compatibility.\n * Override this in subclasses to handle provider-specific quirks.\n */\n protected makeStructuredOutputCompatible(\n schema: Record<string, any>,\n originalRequired?: Array<string>,\n ): Record<string, any> {\n return makeStructuredOutputCompatible(schema, originalRequired)\n }\n\n /**\n * Extract reasoning content from a stream chunk. Default returns\n * `undefined` because the OpenAI Chat Completions chunk shape doesn't\n * carry reasoning. The chunk param is typed `unknown` so an override can\n * narrow to its own SDK chunk type without an `as` dance — the base only\n * passes through `processStreamChunks`'s structurally-iterated chunk.\n */\n protected extractReasoning(_chunk: unknown): { text: string } | undefined {\n return undefined\n }\n\n /**\n * Final shaping pass applied to parsed structured-output JSON before it is\n * returned to the caller. Default is a passthrough.\n *\n * Provider `null`s are no longer stripped here: strict-mode null-widening is\n * now undone precisely by the engine (`undoNullWidening`, driven by the\n * schema's null-widening map) the moment the result is captured, so a blind\n * `transformNullsToUndefined` at the adapter would only destroy genuine\n * `.nullable()` nulls. Subclasses may still override to remap or reshape the\n * provider's structured output.\n */\n protected transformStructuredOutput(parsed: unknown): unknown {\n return parsed\n }\n\n /**\n * Processes streamed chunks from the Chat Completions API and yields AG-UI events.\n * Override this in subclasses to handle provider-specific stream behavior.\n */\n protected async *processStreamChunks(\n stream: AsyncIterable<ChatCompletionChunk>,\n options: TextOptions,\n aguiState: {\n runId: string\n threadId: string\n messageId: string\n hasEmittedRunStarted: boolean\n },\n ): AsyncIterable<StreamChunk> {\n let accumulatedContent = ''\n let hasEmittedTextMessageStart = false\n let lastModel: string | undefined\n // Track usage from any chunk that carries it. With\n // `stream_options: { include_usage: true }` OpenAI emits a terminal chunk\n // whose `choices` is `[]` and only the `usage` field is populated; the\n // earlier `finish_reason` chunk does NOT include token counts. We must\n // therefore defer RUN_FINISHED until the iterator is exhausted so we can\n // pick up usage from the trailing chunk regardless of arrival order.\n let lastUsage: ChatCompletionChunk['usage'] | undefined\n let pendingFinishReason:\n | ChatCompletionChunk['choices'][number]['finish_reason']\n | undefined\n\n // Track tool calls being streamed (arguments come in chunks)\n const toolCallsInProgress = new Map<\n number,\n {\n id: string\n name: string\n arguments: string\n started: boolean // Track if TOOL_CALL_START has been emitted\n }\n >()\n\n // Reasoning lifecycle (driven by extractReasoning() hook — see method\n // docs). The base wire format (OpenAI Chat Completions) has no reasoning,\n // so these stay unused for openai/grok/groq. OpenRouter etc. opt in.\n let reasoningMessageId: string | undefined\n let hasClosedReasoning = false\n // Legacy STEP_STARTED/STEP_FINISHED pair emitted alongside REASONING_*\n // for back-compat with consumers (UI, devtools) that haven't migrated\n // to the spec REASONING_* events yet.\n let stepId: string | undefined\n let accumulatedReasoning = ''\n // Track whether ANY tool call lifecycle was actually completed across the\n // entire stream. Lets us downgrade a `tool_calls` finish_reason to `stop`\n // when the upstream signalled tool calls but never produced a complete\n // start/end pair — emitting RUN_FINISHED { finishReason: 'tool_calls' }\n // with no matching TOOL_CALL_END would leave consumers waiting for tool\n // results that never arrive.\n let emittedAnyToolCallEnd = false\n\n try {\n for await (const chunk of stream) {\n const choiceForLog = chunk.choices[0]\n options.logger.provider(\n `provider=${this.name} finish_reason=${choiceForLog?.finish_reason ?? 'none'} hasContent=${!!choiceForLog?.delta.content} hasToolCalls=${!!choiceForLog?.delta.tool_calls} hasUsage=${!!chunk.usage}`,\n { provider: this.name, model: chunk.model },\n )\n\n // Capture usage from any chunk (including the terminal usage-only\n // chunk emitted when `stream_options.include_usage` is on).\n if (chunk.usage) {\n lastUsage = chunk.usage\n }\n if (chunk.model) {\n lastModel = chunk.model\n }\n\n // Emit RUN_STARTED on the first chunk of any kind so callers see a\n // run lifecycle even on streams that arrive entirely as usage-only\n // (no choices). Without this, a usage-first stream would skip\n // RUN_STARTED via `if (!choice) continue` below and the post-loop\n // synthetic block would also skip RUN_FINISHED (it gates on\n // `hasEmittedRunStarted`).\n if (!aguiState.hasEmittedRunStarted) {\n aguiState.hasEmittedRunStarted = true\n yield {\n type: EventType.RUN_STARTED,\n runId: aguiState.runId,\n threadId: aguiState.threadId,\n model: chunk.model || options.model,\n timestamp: Date.now(),\n parentRunId: options.parentRunId,\n }\n }\n\n // Reasoning content (extractReasoning() hook). Run before reading\n // choice/delta so reasoning-only chunks (no `choices`) still drive\n // the REASONING_* lifecycle on providers that send reasoning out of\n // band. The base default returns undefined.\n const reasoning = this.extractReasoning(chunk)\n if (reasoning && reasoning.text) {\n if (!reasoningMessageId) {\n reasoningMessageId = generateId(this.name)\n stepId = generateId(this.name)\n yield {\n type: EventType.REASONING_START,\n messageId: reasoningMessageId,\n model: chunk.model || options.model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.REASONING_MESSAGE_START,\n messageId: reasoningMessageId,\n role: 'reasoning' as const,\n model: chunk.model || options.model,\n timestamp: Date.now(),\n }\n // Legacy STEP_STARTED (single emission, paired with the\n // STEP_FINISHED below when reasoning closes).\n yield {\n type: EventType.STEP_STARTED,\n stepName: stepId,\n stepId,\n model: chunk.model || options.model,\n timestamp: Date.now(),\n stepType: 'thinking',\n }\n }\n accumulatedReasoning += reasoning.text\n yield {\n type: EventType.REASONING_MESSAGE_CONTENT,\n messageId: reasoningMessageId,\n delta: reasoning.text,\n model: chunk.model || options.model,\n timestamp: Date.now(),\n }\n }\n\n const choice = chunk.choices[0]\n\n if (!choice) continue\n\n const delta = choice.delta\n const deltaContent = delta.content\n const deltaToolCalls = delta.tool_calls\n\n // Handle content delta\n if (deltaContent) {\n // Close reasoning before text starts so consumers see a clean\n // REASONING_END before any TEXT_MESSAGE_START.\n if (reasoningMessageId && !hasClosedReasoning) {\n hasClosedReasoning = true\n yield {\n type: EventType.REASONING_MESSAGE_END,\n messageId: reasoningMessageId,\n model: chunk.model || options.model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.REASONING_END,\n messageId: reasoningMessageId,\n model: chunk.model || options.model,\n timestamp: Date.now(),\n }\n if (stepId) {\n yield {\n type: EventType.STEP_FINISHED,\n stepName: stepId,\n stepId,\n model: chunk.model || options.model,\n timestamp: Date.now(),\n content: accumulatedReasoning,\n }\n }\n }\n\n // Emit TEXT_MESSAGE_START on first text content\n if (!hasEmittedTextMessageStart) {\n hasEmittedTextMessageStart = true\n yield {\n type: EventType.TEXT_MESSAGE_START,\n messageId: aguiState.messageId,\n model: chunk.model || options.model,\n timestamp: Date.now(),\n role: 'assistant',\n }\n }\n\n accumulatedContent += deltaContent\n\n // Emit AG-UI TEXT_MESSAGE_CONTENT\n yield {\n type: EventType.TEXT_MESSAGE_CONTENT,\n messageId: aguiState.messageId,\n model: chunk.model || options.model,\n timestamp: Date.now(),\n delta: deltaContent,\n content: accumulatedContent,\n }\n }\n\n // Handle tool calls - they come in as deltas\n if (deltaToolCalls) {\n for (const toolCallDelta of deltaToolCalls) {\n const index = toolCallDelta.index\n\n // Initialize or update the tool call in progress\n let toolCall = toolCallsInProgress.get(index)\n if (!toolCall) {\n toolCall = {\n id: toolCallDelta.id || '',\n name: toolCallDelta.function?.name || '',\n arguments: '',\n started: false,\n }\n toolCallsInProgress.set(index, toolCall)\n }\n\n // Update with any new data from the delta\n if (toolCallDelta.id) {\n toolCall.id = toolCallDelta.id\n }\n if (toolCallDelta.function?.name) {\n toolCall.name = toolCallDelta.function.name\n }\n if (toolCallDelta.function?.arguments) {\n toolCall.arguments += toolCallDelta.function.arguments\n }\n\n // Emit TOOL_CALL_START when we have id and name\n if (toolCall.id && toolCall.name && !toolCall.started) {\n toolCall.started = true\n yield {\n type: EventType.TOOL_CALL_START,\n toolCallId: toolCall.id,\n toolCallName: toolCall.name,\n toolName: toolCall.name,\n parentMessageId: aguiState.messageId,\n model: chunk.model || options.model,\n timestamp: Date.now(),\n index,\n }\n }\n\n // Emit TOOL_CALL_ARGS for argument deltas\n if (toolCallDelta.function?.arguments && toolCall.started) {\n yield {\n type: EventType.TOOL_CALL_ARGS,\n toolCallId: toolCall.id,\n model: chunk.model || options.model,\n timestamp: Date.now(),\n delta: toolCallDelta.function.arguments,\n }\n }\n }\n }\n\n // Handle finish reason. We DO emit TOOL_CALL_END and TEXT_MESSAGE_END\n // here because the corresponding _START events have already fired,\n // and tool execution downstream wants to begin as soon as possible.\n // RUN_FINISHED is deferred until the iterator is fully exhausted so\n // we can capture the trailing usage chunk that arrives AFTER this\n // chunk when stream_options.include_usage is on.\n if (choice.finish_reason) {\n if (\n choice.finish_reason === 'tool_calls' ||\n toolCallsInProgress.size > 0\n ) {\n for (const [, toolCall] of toolCallsInProgress) {\n // Skip tool calls that never emitted TOOL_CALL_START — emitting\n // a stray TOOL_CALL_END here would violate AG-UI lifecycle\n // (END without matching START) for partial deltas where the\n // upstream never sent both id and name.\n if (!toolCall.started) continue\n\n // Parse arguments for TOOL_CALL_END. Surface parse failures via\n // the logger so a model emitting malformed JSON for tool args\n // is debuggable instead of silently invoking the tool with {}.\n // Non-object JSON (e.g. a bare string or number) is also coerced\n // to {} so downstream tool execution doesn't receive a primitive\n // input, mirroring the Responses adapter's guard.\n let parsedInput: unknown = {}\n if (toolCall.arguments) {\n try {\n const parsed: unknown = JSON.parse(toolCall.arguments)\n parsedInput =\n parsed && typeof parsed === 'object' ? parsed : {}\n } catch (parseError) {\n options.logger.errors(\n `${this.name}.processStreamChunks tool-args JSON parse failed`,\n {\n error: toRunErrorPayload(\n parseError,\n `tool ${toolCall.name} (${toolCall.id}) returned malformed JSON arguments`,\n ),\n source: `${this.name}.processStreamChunks`,\n toolCallId: toolCall.id,\n toolName: toolCall.name,\n rawArguments: toolCall.arguments,\n },\n )\n parsedInput = {}\n }\n }\n\n // Emit AG-UI TOOL_CALL_END\n yield {\n type: EventType.TOOL_CALL_END,\n toolCallId: toolCall.id,\n toolCallName: toolCall.name,\n toolName: toolCall.name,\n model: chunk.model || options.model,\n timestamp: Date.now(),\n input: parsedInput,\n }\n emittedAnyToolCallEnd = true\n }\n // Clear tool-call state after emission so a subsequent\n // `finish_reason: 'stop'` chunk (or the post-loop synthetic\n // block) doesn't see lingering entries and misreport the finish.\n toolCallsInProgress.clear()\n }\n\n // Emit TEXT_MESSAGE_END if we had text content\n if (hasEmittedTextMessageStart) {\n yield {\n type: EventType.TEXT_MESSAGE_END,\n messageId: aguiState.messageId,\n model: chunk.model || options.model,\n timestamp: Date.now(),\n }\n hasEmittedTextMessageStart = false\n }\n\n // Remember the upstream finish_reason; RUN_FINISHED is emitted at\n // end-of-stream so we pick up the trailing usage-only chunk too.\n pendingFinishReason = choice.finish_reason\n }\n }\n\n // Emit a single terminal RUN_FINISHED after the iterator is exhausted.\n // This both delivers accurate token counts (the trailing usage chunk\n // may arrive AFTER the finish_reason chunk) and gives consumers a\n // guaranteed terminal event even when the upstream cuts off mid-stream\n // (no finish_reason chunk ever arrives).\n if (aguiState.hasEmittedRunStarted) {\n // Close any started tool calls that never got finish_reason. A\n // truncated stream that emitted TOOL_CALL_START but never reached\n // finish_reason would otherwise leave consumers with an unbalanced\n // start. Skip non-started entries (no matching START to close).\n let pendingToolCount = 0\n for (const [, toolCall] of toolCallsInProgress) {\n if (!toolCall.started) continue\n let parsedInput: unknown = {}\n if (toolCall.arguments) {\n try {\n const parsed: unknown = JSON.parse(toolCall.arguments)\n parsedInput = parsed && typeof parsed === 'object' ? parsed : {}\n } catch (parseError) {\n // Mirror the finish_reason path's logger call — a truncated\n // stream emitting malformed tool-call JSON would otherwise\n // silently invoke the tool with `{}`, the exact failure the\n // finish_reason logger was added to prevent.\n options.logger.errors(\n `${this.name}.processStreamChunks tool-args JSON parse failed (drain)`,\n {\n error: toRunErrorPayload(\n parseError,\n `tool ${toolCall.name} (${toolCall.id}) returned malformed JSON arguments`,\n ),\n source: `${this.name}.processStreamChunks`,\n toolCallId: toolCall.id,\n toolName: toolCall.name,\n rawArguments: toolCall.arguments,\n },\n )\n parsedInput = {}\n }\n }\n yield {\n type: EventType.TOOL_CALL_END,\n toolCallId: toolCall.id,\n toolCallName: toolCall.name,\n toolName: toolCall.name,\n model: lastModel || options.model,\n timestamp: Date.now(),\n input: parsedInput,\n }\n pendingToolCount += 1\n emittedAnyToolCallEnd = true\n }\n toolCallsInProgress.clear()\n\n // Make sure the text message lifecycle is closed even on early\n // termination paths where finish_reason never arrives.\n if (hasEmittedTextMessageStart) {\n yield {\n type: EventType.TEXT_MESSAGE_END,\n messageId: aguiState.messageId,\n model: lastModel || options.model,\n timestamp: Date.now(),\n }\n }\n\n // Close any reasoning lifecycle that text never closed (no text\n // content arrived, or the stream cut off before text started).\n if (reasoningMessageId && !hasClosedReasoning) {\n hasClosedReasoning = true\n yield {\n type: EventType.REASONING_MESSAGE_END,\n messageId: reasoningMessageId,\n model: lastModel || options.model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.REASONING_END,\n messageId: reasoningMessageId,\n model: lastModel || options.model,\n timestamp: Date.now(),\n }\n if (stepId) {\n yield {\n type: EventType.STEP_FINISHED,\n stepName: stepId,\n stepId,\n model: lastModel || options.model,\n timestamp: Date.now(),\n content: accumulatedReasoning,\n }\n }\n }\n\n // Map upstream finish_reason to AG-UI's narrower vocabulary.\n // Collapsing length / content_filter to 'stop' would hide why the\n // run terminated — surface it instead. Use `tool_calls` only when\n // a TOOL_CALL_END was actually emitted: an upstream that signalled\n // `tool_calls` but never produced a started/ended pair must NOT\n // surface `tool_calls` here, since downstream consumers wait for\n // tool results that would never arrive. OpenAI's legacy\n // `function_call` value (from the v1 function-calling API) is\n // normalized to `tool_calls` — semantically the same termination.\n const finishReason: NonNullable<RunFinishedEvent['finishReason']> =\n emittedAnyToolCallEnd\n ? 'tool_calls'\n : pendingFinishReason === 'tool_calls'\n ? 'stop'\n : pendingFinishReason === 'function_call'\n ? 'tool_calls'\n : (pendingFinishReason ?? 'stop')\n\n // Conditional `usage` spread: AG-UI's `RunFinishedEvent.usage` is\n // optional with no `| undefined`; omit the key entirely when no usage\n // arrived rather than emitting `usage: undefined`.\n yield {\n type: EventType.RUN_FINISHED,\n runId: aguiState.runId,\n threadId: aguiState.threadId,\n model: lastModel || options.model,\n timestamp: Date.now(),\n ...(lastUsage && {\n usage: buildChatCompletionsUsage(lastUsage),\n }),\n finishReason,\n }\n }\n } catch (error: unknown) {\n // Narrow before logging: raw SDK errors can carry request metadata\n // (including auth headers) which we must never surface to user loggers.\n const errorPayload = toRunErrorPayload(\n error,\n `${this.name}.processStreamChunks failed`,\n )\n const rawEvent = toRunErrorRawEvent(error)\n options.logger.errors(`${this.name}.processStreamChunks fatal`, {\n error: errorPayload,\n source: `${this.name}.processStreamChunks`,\n })\n\n // Emit AG-UI RUN_ERROR with conditional `code` spread (see chatStream's\n // catch block for the rationale). `rawEvent` carries the provider's\n // structured error body when present.\n yield {\n type: EventType.RUN_ERROR,\n model: options.model,\n timestamp: Date.now(),\n message: errorPayload.message,\n ...(errorPayload.code !== undefined && { code: errorPayload.code }),\n ...(rawEvent !== undefined && { rawEvent }),\n error: {\n message: errorPayload.message,\n ...(errorPayload.code !== undefined && { code: errorPayload.code }),\n },\n }\n }\n }\n\n /**\n * Maps common TextOptions to Chat Completions API request format.\n * Override this in subclasses to add provider-specific options.\n */\n protected mapOptionsToRequest(\n options: TextOptions,\n ): ChatCompletionCreateParamsStreaming {\n const tools = options.tools\n ? convertToolsToChatCompletionsFormat(\n options.tools,\n this.makeStructuredOutputCompatible.bind(this),\n )\n : undefined\n\n // Build messages array with system prompts\n const messages: Array<ChatCompletionMessageParam> = []\n\n // Add system prompts first\n const systemPrompts = normalizeSystemPrompts(options.systemPrompts)\n if (systemPrompts.length > 0) {\n messages.push({\n role: 'system',\n content: systemPrompts.map((p) => p.content).join('\\n'),\n })\n }\n\n // Convert messages\n for (const message of options.messages) {\n messages.push(this.convertMessage(message))\n }\n\n const modelOptions = options.modelOptions\n\n // Native combined mode (issue #605): when the engine threads\n // `outputSchema` through TextOptions, the adapter declared\n // `supportsCombinedToolsAndSchema` and the schema is already JSON Schema\n // (pre-converted at the activity boundary). Wire it into\n // `response_format` alongside any `tools`. Modern OpenAI-compatible\n // Chat Completions accepts both together and emits the schema-\n // constrained text on the natural final turn.\n const combinedSchema = options.outputSchema as\n | Record<string, unknown>\n | undefined\n const responseFormat = combinedSchema\n ? {\n response_format: {\n type: 'json_schema' as const,\n json_schema: {\n name: 'structured_output',\n schema: this.makeStructuredOutputCompatible(\n combinedSchema,\n Array.isArray(combinedSchema.required)\n ? (combinedSchema.required as Array<string>)\n : undefined,\n ),\n strict: true,\n },\n },\n }\n : undefined\n\n // `modelOptions` is the sole sampling surface: callers set provider-native\n // wire names (`temperature`, `top_p`, `max_tokens`/`max_completion_tokens`)\n // there and they flow through the spread below. The root\n // `temperature`/`topP`/`maxTokens` fields are intentionally NOT read here.\n return {\n ...modelOptions,\n model: options.model,\n messages,\n // Conditional spread: `tools: undefined` would clobber any\n // modelOptions.tools the caller set above.\n ...(tools &&\n tools.length > 0 && {\n tools,\n }),\n ...(responseFormat ?? {}),\n stream: true,\n }\n }\n\n /**\n * Modern OpenAI-compatible Chat Completions APIs support `tools` and\n * `response_format: json_schema` together in a single streaming request\n * (per issue #605). Subclasses can override — Groq, for instance, must\n * return `false` because its API rejects schema + tools + stream with a\n * 400.\n */\n supportsCombinedToolsAndSchema(): boolean {\n return true\n }\n\n /**\n * Converts a single ModelMessage to the Chat Completions API message format.\n * Override this in subclasses to handle provider-specific message formats.\n */\n protected convertMessage(message: ModelMessage): ChatCompletionMessageParam {\n // Handle tool messages\n if (message.role === 'tool') {\n // The Chat Completions API has no multimodal `tool` message support\n // (unlike the Responses API's `function_call_output`). A tool that\n // returns an `Array<ContentPart>` is therefore stringified here — the\n // documented fallback for providers on the chat-completions path\n // (Groq, Ollama, Grok, OpenRouter chat). Multimodal tool results are\n // only delivered structurally via the Responses adapter.\n return {\n role: 'tool',\n tool_call_id: message.toolCallId || '',\n content:\n typeof message.content === 'string'\n ? message.content\n : JSON.stringify(message.content),\n }\n }\n\n // Handle assistant messages\n if (message.role === 'assistant') {\n const toolCalls = message.toolCalls?.map((tc) => ({\n id: tc.id,\n type: 'function' as const,\n function: {\n name: tc.function.name,\n arguments:\n typeof tc.function.arguments === 'string'\n ? tc.function.arguments\n : JSON.stringify(tc.function.arguments),\n },\n }))\n const hasToolCalls = !!toolCalls && toolCalls.length > 0\n const textContent = this.extractTextContent(message.content)\n\n // Per the OpenAI Chat Completions contract, an assistant message that\n // only carries tool_calls should have `content: null` (or omit content)\n // rather than `content: ''`. Empty-string content interacts oddly with\n // tokenization on some backends; null is the documented shape.\n return {\n role: 'assistant',\n content: hasToolCalls && !textContent ? null : textContent,\n ...(hasToolCalls ? { tool_calls: toolCalls } : {}),\n }\n }\n\n // Handle user messages - support multimodal content\n const contentParts = this.normalizeContent(message.content)\n\n // If only text, use simple string format\n if (contentParts.length === 1 && contentParts[0]?.type === 'text') {\n const text = contentParts[0].content\n if (text.length === 0) {\n // Single empty text part is the same fail-loud condition as below —\n // an empty paid request mask a real intent (caller passed `null`/'',\n // or an upstream step normalised everything to an empty string).\n throw new Error(\n `User message for ${this.name} has empty text content. ` +\n `Empty user messages would produce a paid request with no input; ` +\n `provide non-empty content or omit the message.`,\n )\n }\n return {\n role: 'user',\n content: text,\n }\n }\n\n // Otherwise, use array format for multimodal. Fail fast on unsupported\n // content parts rather than silently dropping them — a message of all\n // unsupported parts would otherwise turn into an empty user prompt and\n // mask a real capability mismatch.\n const parts: Array<ChatCompletionContentPart> = []\n for (const part of contentParts) {\n const converted = this.convertContentPart(part)\n if (!converted) {\n throw new Error(\n `Unsupported content part type for ${this.name}: ${part.type}. ` +\n `Override convertContentPart() in a subclass to handle this type, ` +\n `or remove it from the message.`,\n )\n }\n parts.push(converted)\n }\n\n if (parts.length === 0) {\n // The original message had no content parts at all (e.g. content was\n // explicitly null or []). Sending an empty user message to OpenAI\n // produces a paid request with no signal — fail loud instead.\n throw new Error(\n `User message for ${this.name} has no content parts. ` +\n `Empty user messages would produce a paid request with no input; ` +\n `provide at least one text/image/audio part or omit the message.`,\n )\n }\n\n return {\n role: 'user',\n content: parts,\n }\n }\n\n /**\n * Converts a single ContentPart to the Chat Completions API content part format.\n * Override this in subclasses to handle additional content types or provider-specific metadata.\n */\n protected convertContentPart(\n part: ContentPart,\n ): ChatCompletionContentPart | null {\n if (part.type === 'text') {\n return { type: 'text', text: part.content }\n }\n\n if (part.type === 'image') {\n const imageMetadata = part.metadata as\n | { detail?: 'auto' | 'low' | 'high' }\n | undefined\n\n // For base64 data, construct a data URI using the mimeType from source.\n // Default to a generic octet-stream MIME if the source didn't provide\n // one — interpolating `undefined` into the URI (\"data:undefined;base64,\n // ...\") would produce an invalid URI the API rejects.\n const imageValue = part.source.value\n const imageMime = part.source.mimeType || 'application/octet-stream'\n const imageUrl =\n part.source.type === 'data' && !imageValue.startsWith('data:')\n ? `data:${imageMime};base64,${imageValue}`\n : imageValue\n\n return {\n type: 'image_url',\n image_url: {\n url: imageUrl,\n detail: imageMetadata?.detail || 'auto',\n },\n }\n }\n\n // Unsupported content type — subclasses can override to handle more types\n return null\n }\n\n /**\n * Normalizes message content to an array of ContentPart.\n * Handles backward compatibility with string content.\n */\n protected normalizeContent(\n content: string | null | Array<ContentPart>,\n ): Array<ContentPart> {\n if (content === null) {\n return []\n }\n if (typeof content === 'string') {\n return [{ type: 'text', content: content }]\n }\n return content\n }\n\n /**\n * Extracts text content from a content value that may be string, null, or ContentPart array.\n */\n protected extractTextContent(\n content: string | null | Array<ContentPart>,\n ): string {\n if (content === null) {\n return ''\n }\n if (typeof content === 'string') {\n return content\n }\n // It's an array of ContentPart\n return content\n .filter((p) => p.type === 'text')\n .map((p) => p.content)\n .join('')\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"chat-completions-text.js","names":[],"sources":["../../../src/adapters/chat-completions-text.ts"],"sourcesContent":["import { EventType, normalizeSystemPrompts } from '@tanstack/ai'\nimport { BaseTextAdapter } from '@tanstack/ai/adapters'\nimport {\n toRunErrorPayload,\n toRunErrorRawEvent,\n} from '@tanstack/ai/adapter-internals'\nimport { generateId } from '@tanstack/ai-utils'\nimport { extractRequestOptions } from '../utils/request-options'\nimport { makeStructuredOutputCompatible } from '../utils/schema-converter'\nimport { buildChatCompletionsUsage } from '../usage'\nimport { convertToolsToChatCompletionsFormat } from './chat-completions-tool-converter'\nimport type OpenAI from 'openai'\nimport type {\n StructuredOutputOptions,\n StructuredOutputResult,\n} from '@tanstack/ai/adapters'\nimport type {\n ChatCompletionChunk,\n ChatCompletionContentPart,\n ChatCompletionCreateParamsStreaming,\n ChatCompletionMessageParam,\n} from 'openai/resources/chat/completions/completions'\nimport type {\n ContentPart,\n DefaultMessageMetadataByModality,\n Modality,\n ModelMessage,\n RunFinishedEvent,\n StreamChunk,\n TextOptions,\n} from '@tanstack/ai'\n\n/**\n * Shared implementation of the OpenAI Chat Completions API. Holds the\n * stream-accumulator + AG-UI lifecycle logic and calls the OpenAI SDK\n * directly. Subclasses (ai-openai, ai-grok, ai-groq) construct an OpenAI\n * client with their provider-specific `baseURL` / headers and pass it in.\n */\nexport abstract class OpenAIBaseChatCompletionsTextAdapter<\n TModel extends string,\n TProviderOptions extends Record<string, unknown> = Record<string, unknown>,\n TInputModalities extends ReadonlyArray<Modality> = ReadonlyArray<Modality>,\n TMessageMetadata extends DefaultMessageMetadataByModality =\n DefaultMessageMetadataByModality,\n TToolCapabilities extends ReadonlyArray<string> = ReadonlyArray<string>,\n> extends BaseTextAdapter<\n TModel,\n TProviderOptions,\n TInputModalities,\n TMessageMetadata,\n TToolCapabilities\n> {\n override readonly kind = 'text' as const\n readonly name: string\n protected client: OpenAI\n\n constructor(model: TModel, name: string, client: OpenAI) {\n super({}, model)\n this.name = name\n this.client = client\n }\n\n async *chatStream(\n options: TextOptions<TProviderOptions>,\n ): AsyncIterable<StreamChunk> {\n // AG-UI lifecycle tracking (mutable state object for ESLint compatibility)\n const aguiState = {\n runId: generateId(this.name),\n threadId: options.threadId ?? generateId(this.name),\n messageId: generateId(this.name),\n hasEmittedRunStarted: false,\n }\n\n try {\n // mapOptionsToRequest can throw (e.g. fail-loud guards in convertMessage\n // for empty content or unsupported parts). Keep it inside the try so\n // those failures surface as a single RUN_ERROR event, matching every\n // other failure mode here — callers iterating chatStream then only need\n // one error-handling path instead of both a try/catch around iteration\n // and a RUN_ERROR handler.\n const requestParams = this.mapOptionsToRequest(options)\n options.logger.request(\n `activity=chat provider=${this.name} model=${this.model} messages=${options.messages.length} tools=${options.tools?.length ?? 0} stream=true`,\n { provider: this.name, model: this.model },\n )\n const stream = await this.client.chat.completions.create(\n {\n ...requestParams,\n stream: true,\n stream_options: { include_usage: true },\n },\n extractRequestOptions(options.request),\n )\n\n yield* this.processStreamChunks(stream, options, aguiState)\n } catch (error: unknown) {\n // Narrow before logging: raw SDK errors can carry request metadata\n // (including auth headers) which we must never surface to user loggers.\n const errorPayload = toRunErrorPayload(\n error,\n `${this.name}.chatStream failed`,\n )\n const rawEvent = toRunErrorRawEvent(error)\n\n // Emit RUN_STARTED if not yet emitted\n if (!aguiState.hasEmittedRunStarted) {\n aguiState.hasEmittedRunStarted = true\n yield {\n type: EventType.RUN_STARTED,\n runId: aguiState.runId,\n threadId: aguiState.threadId,\n model: options.model,\n timestamp: Date.now(),\n parentRunId: options.parentRunId,\n }\n }\n\n const rejectedToolCall = this.extractRejectedToolCall(\n rawEvent,\n errorPayload.message,\n )\n if (rejectedToolCall) {\n const toolCallId = generateId(this.name)\n yield {\n type: EventType.TOOL_CALL_START,\n toolCallId,\n toolCallName: rejectedToolCall.toolName,\n toolName: rejectedToolCall.toolName,\n parentMessageId: aguiState.messageId,\n model: options.model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.TOOL_CALL_ARGS,\n toolCallId,\n delta: rejectedToolCall.arguments,\n args: rejectedToolCall.arguments,\n model: options.model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.TOOL_CALL_END,\n toolCallId,\n toolCallName: rejectedToolCall.toolName,\n toolName: rejectedToolCall.toolName,\n ...(rejectedToolCall.input !== undefined && {\n input: rejectedToolCall.input,\n }),\n result: JSON.stringify({ error: rejectedToolCall.error }),\n state: 'output-error',\n model: options.model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.RUN_FINISHED,\n runId: aguiState.runId,\n threadId: aguiState.threadId,\n model: options.model,\n timestamp: Date.now(),\n finishReason: 'tool_calls',\n }\n return\n }\n\n // Emit AG-UI RUN_ERROR. Conditional `code` spread keeps the wire\n // shape spec-compliant under `exactOptionalPropertyTypes`: AG-UI's\n // `RunErrorEvent.code` is `string?` (absent vs explicit `undefined`\n // matter), so we omit the key when there's no code.\n yield {\n type: EventType.RUN_ERROR,\n model: options.model,\n timestamp: Date.now(),\n message: errorPayload.message,\n code: errorPayload.code,\n // Forward the provider's structured error body so consumers can recover\n // the upstream detail the `{ message, code }` payload drops. Omitted\n // when the error carried no provider body (see toRunErrorRawEvent).\n ...(rawEvent !== undefined && { rawEvent }),\n error: {\n message: errorPayload.message,\n code: errorPayload.code,\n },\n }\n\n options.logger.errors(`${this.name}.chatStream fatal`, {\n error: errorPayload,\n source: `${this.name}.chatStream`,\n })\n }\n }\n\n /**\n * Extracts a rejected tool call from a provider error. Returned calls are\n * emitted as non-executable `output-error` results so the model can repair them.\n */\n protected extractRejectedToolCall(\n _rawEvent: unknown,\n _fallbackMessage: string,\n ):\n | {\n toolName: string\n arguments: string\n input?: unknown\n error: string\n }\n | undefined {\n return undefined\n }\n\n /**\n * Generate structured output using the provider's JSON Schema response format.\n * Uses stream: false to get the complete response in one call.\n *\n * OpenAI-compatible APIs have strict requirements for structured output:\n * - All properties must be in the `required` array\n * - Optional fields should have null added to their type union\n * - additionalProperties must be false for all objects\n *\n * The outputSchema is already JSON Schema (converted in the ai layer).\n * We apply provider-specific transformations for structured output compatibility.\n */\n async structuredOutput(\n options: StructuredOutputOptions<TProviderOptions>,\n ): Promise<StructuredOutputResult<unknown>> {\n const { chatOptions, outputSchema } = options\n const requestParams = this.mapOptionsToRequest(chatOptions)\n\n const jsonSchema = this.makeStructuredOutputCompatible(\n outputSchema,\n outputSchema.required,\n )\n\n try {\n // Strip stream_options which is only valid for streaming calls\n const {\n stream_options: _,\n stream: __,\n ...cleanParams\n } = requestParams as any\n chatOptions.logger.request(\n `activity=structuredOutput provider=${this.name} model=${this.model} messages=${chatOptions.messages.length}`,\n { provider: this.name, model: this.model },\n )\n const response = await this.client.chat.completions.create(\n {\n ...cleanParams,\n stream: false,\n response_format: {\n type: 'json_schema',\n json_schema: {\n name: 'structured_output',\n schema: jsonSchema,\n strict: true,\n },\n },\n },\n extractRequestOptions(chatOptions.request),\n )\n\n // Extract text content from the response. Fail loud on empty content\n // rather than letting it cascade into a JSON-parse error on '' — the\n // root cause (the model returned no content for the structured request)\n // is then visible in logs.\n const rawText = response.choices[0]?.message.content\n if (typeof rawText !== 'string' || rawText.length === 0) {\n throw new Error(\n `${this.name}.structuredOutput: response contained no content`,\n )\n }\n\n // Parse the JSON response\n let parsed: unknown\n try {\n parsed = JSON.parse(rawText)\n } catch {\n throw new Error(\n `Failed to parse structured output as JSON. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? '...' : ''}`,\n )\n }\n\n // Final provider-specific shaping pass (default passthrough). Null-widening\n // from strict mode is undone by the engine, not here.\n const transformed = this.transformStructuredOutput(parsed)\n\n // Surface usage so non-stream structured paths (and\n // fallbackStructuredOutputStream) can forward tokens to middleware.\n const usage = buildChatCompletionsUsage(response.usage)\n return {\n data: transformed,\n rawText,\n ...(usage && { usage }),\n }\n } catch (error: unknown) {\n // Narrow before logging: raw SDK errors can carry request metadata\n // (including auth headers) which we must never surface to user loggers.\n chatOptions.logger.errors(`${this.name}.structuredOutput fatal`, {\n error: toRunErrorPayload(error, `${this.name}.structuredOutput failed`),\n source: `${this.name}.structuredOutput`,\n })\n throw error\n }\n }\n\n /**\n * Stream structured output. Single Chat Completions request with\n * `response_format: json_schema` + `stream: true`. Emits the standard\n * AG-UI lifecycle (`RUN_STARTED` → `REASONING_*?` → `TEXT_MESSAGE_*`\n * carrying raw JSON deltas → terminal `CUSTOM 'structured-output.complete'`\n * → `RUN_FINISHED`). Subclasses use the same SDK-call / reasoning /\n * structured-output-transform hooks as `chatStream` / `structuredOutput` —\n * no per-subclass override should be needed.\n */\n async *structuredOutputStream(\n options: StructuredOutputOptions<TProviderOptions>,\n ): AsyncIterable<StreamChunk> {\n const { chatOptions, outputSchema } = options\n const requestParams = this.mapOptionsToRequest(chatOptions)\n\n const jsonSchema = this.makeStructuredOutputCompatible(\n outputSchema,\n outputSchema.required,\n )\n\n const aguiState = {\n runId: generateId(this.name),\n threadId: chatOptions.threadId ?? generateId(this.name),\n messageId: generateId(this.name),\n hasEmittedRunStarted: false,\n }\n\n let accumulatedContent = ''\n let accumulatedReasoning = ''\n let hasEmittedTextMessageStart = false\n let reasoningMessageId: string | undefined\n let hasClosedReasoning = false\n let stepId: string | undefined\n let lastModel: string | undefined\n let lastUsage:\n | OpenAI.Chat.Completions.ChatCompletionChunk['usage']\n | undefined\n\n const closeReasoningLifecycle = function* (this: {\n name: string\n }): Generator<StreamChunk> {\n if (reasoningMessageId && !hasClosedReasoning) {\n hasClosedReasoning = true\n yield {\n type: EventType.REASONING_MESSAGE_END,\n messageId: reasoningMessageId,\n model: lastModel || chatOptions.model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.REASONING_END,\n messageId: reasoningMessageId,\n model: lastModel || chatOptions.model,\n timestamp: Date.now(),\n }\n if (stepId) {\n yield {\n type: EventType.STEP_FINISHED,\n stepName: stepId,\n stepId,\n model: lastModel || chatOptions.model,\n timestamp: Date.now(),\n content: accumulatedReasoning,\n }\n }\n }\n }.bind(this)\n\n try {\n // Strip stream_options + tools from the base request. Structured output\n // sends `response_format: json_schema` and doesn't carry tools — keeping\n // them in the request can confuse strict-mode validation upstream.\n const {\n stream_options: _so,\n stream: _s,\n tools: _t,\n ...cleanParams\n } = requestParams\n\n chatOptions.logger.request(\n `activity=structuredOutputStream provider=${this.name} model=${this.model} messages=${chatOptions.messages.length}`,\n { provider: this.name, model: this.model },\n )\n\n const stream = await this.client.chat.completions.create(\n {\n ...cleanParams,\n stream: true,\n stream_options: { include_usage: true },\n response_format: {\n type: 'json_schema',\n json_schema: {\n name: 'structured_output',\n schema: jsonSchema,\n strict: true,\n },\n },\n },\n extractRequestOptions(chatOptions.request),\n )\n\n for await (const chunk of stream) {\n const choiceForLog = chunk.choices[0]\n chatOptions.logger.provider(\n `provider=${this.name} finish_reason=${choiceForLog?.finish_reason ?? 'none'} hasContent=${!!choiceForLog?.delta.content} hasUsage=${!!chunk.usage}`,\n { provider: this.name, model: chunk.model },\n )\n\n if (chunk.model) lastModel = chunk.model\n\n // Usage may arrive on a chunk with empty `choices` (OpenAI's\n // include_usage terminal chunk) or piggybacked on a finish chunk\n // (`x_groq.usage` on Groq). Capture from either independent of\n // choices[0].\n const usage =\n chunk.usage ??\n (chunk as { x_groq?: { usage?: typeof chunk.usage } }).x_groq?.usage\n if (usage) lastUsage = usage\n\n if (!aguiState.hasEmittedRunStarted) {\n aguiState.hasEmittedRunStarted = true\n yield {\n type: EventType.RUN_STARTED,\n runId: aguiState.runId,\n threadId: aguiState.threadId,\n model: chunk.model || chatOptions.model,\n timestamp: Date.now(),\n parentRunId: chatOptions.parentRunId,\n }\n }\n\n // Reasoning (via the extractReasoning hook — same hook as chatStream).\n const reasoning = this.extractReasoning(chunk)\n if (reasoning && reasoning.text) {\n if (!reasoningMessageId) {\n reasoningMessageId = generateId(this.name)\n stepId = generateId(this.name)\n yield {\n type: EventType.REASONING_START,\n messageId: reasoningMessageId,\n model: chunk.model || chatOptions.model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.REASONING_MESSAGE_START,\n messageId: reasoningMessageId,\n role: 'reasoning' as const,\n model: chunk.model || chatOptions.model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.STEP_STARTED,\n stepName: stepId,\n stepId,\n model: chunk.model || chatOptions.model,\n timestamp: Date.now(),\n stepType: 'thinking',\n }\n }\n accumulatedReasoning += reasoning.text\n yield {\n type: EventType.REASONING_MESSAGE_CONTENT,\n messageId: reasoningMessageId,\n delta: reasoning.text,\n model: chunk.model || chatOptions.model,\n timestamp: Date.now(),\n }\n }\n\n const choice = chunk.choices[0]\n if (!choice) continue\n\n const deltaContent = choice.delta.content\n if (deltaContent) {\n yield* closeReasoningLifecycle()\n\n if (!hasEmittedTextMessageStart) {\n hasEmittedTextMessageStart = true\n yield {\n type: EventType.TEXT_MESSAGE_START,\n messageId: aguiState.messageId,\n model: chunk.model || chatOptions.model,\n timestamp: Date.now(),\n role: 'assistant',\n }\n }\n\n accumulatedContent += deltaContent\n\n yield {\n type: EventType.TEXT_MESSAGE_CONTENT,\n messageId: aguiState.messageId,\n model: chunk.model || chatOptions.model,\n timestamp: Date.now(),\n delta: deltaContent,\n content: accumulatedContent,\n }\n }\n }\n\n // Finalisation: close any open lifecycle, parse + validate, emit\n // terminal events. This block always runs unless the loop threw — abort\n // and SDK errors land in the catch block below.\n yield* closeReasoningLifecycle()\n\n if (hasEmittedTextMessageStart) {\n yield {\n type: EventType.TEXT_MESSAGE_END,\n messageId: aguiState.messageId,\n model: lastModel || chatOptions.model,\n timestamp: Date.now(),\n }\n }\n\n if (accumulatedContent.length === 0) {\n yield {\n type: EventType.RUN_ERROR,\n runId: aguiState.runId,\n model: lastModel || chatOptions.model,\n timestamp: Date.now(),\n message: `${this.name}.structuredOutputStream: response contained no content`,\n code: 'empty-response',\n error: {\n message: `${this.name}.structuredOutputStream: response contained no content`,\n code: 'empty-response',\n },\n }\n return\n }\n\n let parsed: unknown\n try {\n parsed = JSON.parse(accumulatedContent)\n } catch {\n yield {\n type: EventType.RUN_ERROR,\n runId: aguiState.runId,\n model: lastModel || chatOptions.model,\n timestamp: Date.now(),\n message: `Failed to parse structured output as JSON. Content: ${accumulatedContent.slice(0, 200)}${accumulatedContent.length > 200 ? '...' : ''}`,\n code: 'parse-error',\n error: {\n message: 'Failed to parse structured output as JSON',\n code: 'parse-error',\n },\n }\n return\n }\n\n const transformed = this.transformStructuredOutput(parsed)\n\n yield {\n type: EventType.CUSTOM,\n name: 'structured-output.complete',\n value: {\n object: transformed,\n raw: accumulatedContent,\n ...(accumulatedReasoning ? { reasoning: accumulatedReasoning } : {}),\n },\n model: lastModel || chatOptions.model,\n timestamp: Date.now(),\n }\n\n yield {\n type: EventType.RUN_FINISHED,\n runId: aguiState.runId,\n threadId: aguiState.threadId,\n model: lastModel || chatOptions.model,\n timestamp: Date.now(),\n finishReason: 'stop',\n ...(lastUsage && {\n usage: buildChatCompletionsUsage(lastUsage),\n }),\n }\n } catch (error: unknown) {\n if (!aguiState.hasEmittedRunStarted) {\n aguiState.hasEmittedRunStarted = true\n yield {\n type: EventType.RUN_STARTED,\n runId: aguiState.runId,\n threadId: aguiState.threadId,\n model: chatOptions.model,\n timestamp: Date.now(),\n parentRunId: chatOptions.parentRunId,\n }\n }\n\n const isAbort = this.isAbortError(error)\n const errorPayload = toRunErrorPayload(\n error,\n `${this.name}.structuredOutputStream failed`,\n )\n\n // Conditional `code` spread keeps the wire shape spec-compliant under\n // `exactOptionalPropertyTypes`: AG-UI's `RunErrorEvent.code` is `string?`\n // (absent vs explicit `undefined` matter).\n const resolvedCode = isAbort ? 'aborted' : errorPayload.code\n const rawEvent = isAbort ? undefined : toRunErrorRawEvent(error)\n yield {\n type: EventType.RUN_ERROR,\n runId: aguiState.runId,\n model: lastModel || chatOptions.model,\n timestamp: Date.now(),\n message: errorPayload.message,\n ...(resolvedCode !== undefined && { code: resolvedCode }),\n ...(rawEvent !== undefined && { rawEvent }),\n error: {\n message: errorPayload.message,\n ...(resolvedCode !== undefined && { code: resolvedCode }),\n },\n }\n\n chatOptions.logger.errors(`${this.name}.structuredOutputStream fatal`, {\n error: errorPayload,\n source: `${this.name}.structuredOutputStream`,\n })\n }\n }\n\n /**\n * Cross-SDK abort detection for `structuredOutputStream`. Default duck-types\n * on `name === 'APIUserAbortError'` (OpenAI SDK), `code === 'ERR_CANCELED'`,\n * and standard `AbortError`s. Subclasses with proprietary error types (e.g.\n * `@openrouter/sdk`'s `RequestAbortedError`) override to extend the check.\n */\n protected isAbortError(error: unknown): boolean {\n if (!error || typeof error !== 'object') return false\n const e = error as { name?: unknown; code?: unknown }\n return (\n e.name === 'APIUserAbortError' ||\n e.name === 'AbortError' ||\n e.code === 'ERR_CANCELED'\n )\n }\n\n /**\n * Applies provider-specific transformations for structured output compatibility.\n * Override this in subclasses to handle provider-specific quirks.\n */\n protected makeStructuredOutputCompatible(\n schema: Record<string, any>,\n originalRequired?: Array<string>,\n ): Record<string, any> {\n return makeStructuredOutputCompatible(schema, originalRequired)\n }\n\n /**\n * Extract reasoning content from a stream chunk. Default returns\n * `undefined` because the OpenAI Chat Completions chunk shape doesn't\n * carry reasoning. The chunk param is typed `unknown` so an override can\n * narrow to its own SDK chunk type without an `as` dance — the base only\n * passes through `processStreamChunks`'s structurally-iterated chunk.\n */\n protected extractReasoning(_chunk: unknown): { text: string } | undefined {\n return undefined\n }\n\n /**\n * Final shaping pass applied to parsed structured-output JSON before it is\n * returned to the caller. Default is a passthrough.\n *\n * Provider `null`s are no longer stripped here: strict-mode null-widening is\n * now undone precisely by the engine (`undoNullWidening`, driven by the\n * schema's null-widening map) the moment the result is captured, so a blind\n * `transformNullsToUndefined` at the adapter would only destroy genuine\n * `.nullable()` nulls. Subclasses may still override to remap or reshape the\n * provider's structured output.\n */\n protected transformStructuredOutput(parsed: unknown): unknown {\n return parsed\n }\n\n /**\n * Processes streamed chunks from the Chat Completions API and yields AG-UI events.\n * Override this in subclasses to handle provider-specific stream behavior.\n */\n protected async *processStreamChunks(\n stream: AsyncIterable<ChatCompletionChunk>,\n options: TextOptions,\n aguiState: {\n runId: string\n threadId: string\n messageId: string\n hasEmittedRunStarted: boolean\n },\n ): AsyncIterable<StreamChunk> {\n let accumulatedContent = ''\n let hasEmittedTextMessageStart = false\n let lastModel: string | undefined\n // Track usage from any chunk that carries it. With\n // `stream_options: { include_usage: true }` OpenAI emits a terminal chunk\n // whose `choices` is `[]` and only the `usage` field is populated; the\n // earlier `finish_reason` chunk does NOT include token counts. We must\n // therefore defer RUN_FINISHED until the iterator is exhausted so we can\n // pick up usage from the trailing chunk regardless of arrival order.\n let lastUsage: ChatCompletionChunk['usage'] | undefined\n let pendingFinishReason:\n | ChatCompletionChunk['choices'][number]['finish_reason']\n | undefined\n\n // Track tool calls being streamed (arguments come in chunks)\n const toolCallsInProgress = new Map<\n number,\n {\n id: string\n name: string\n arguments: string\n started: boolean // Track if TOOL_CALL_START has been emitted\n }\n >()\n\n // Reasoning lifecycle (driven by extractReasoning() hook — see method\n // docs). The base wire format (OpenAI Chat Completions) has no reasoning,\n // so these stay unused for openai/grok/groq. OpenRouter etc. opt in.\n let reasoningMessageId: string | undefined\n let hasClosedReasoning = false\n // Legacy STEP_STARTED/STEP_FINISHED pair emitted alongside REASONING_*\n // for back-compat with consumers (UI, devtools) that haven't migrated\n // to the spec REASONING_* events yet.\n let stepId: string | undefined\n let accumulatedReasoning = ''\n // Track whether ANY tool call lifecycle was actually completed across the\n // entire stream. Lets us downgrade a `tool_calls` finish_reason to `stop`\n // when the upstream signalled tool calls but never produced a complete\n // start/end pair — emitting RUN_FINISHED { finishReason: 'tool_calls' }\n // with no matching TOOL_CALL_END would leave consumers waiting for tool\n // results that never arrive.\n let emittedAnyToolCallEnd = false\n\n try {\n for await (const chunk of stream) {\n const choiceForLog = chunk.choices[0]\n options.logger.provider(\n `provider=${this.name} finish_reason=${choiceForLog?.finish_reason ?? 'none'} hasContent=${!!choiceForLog?.delta.content} hasToolCalls=${!!choiceForLog?.delta.tool_calls} hasUsage=${!!chunk.usage}`,\n { provider: this.name, model: chunk.model },\n )\n\n // Capture usage from any chunk (including the terminal usage-only\n // chunk emitted when `stream_options.include_usage` is on).\n if (chunk.usage) {\n lastUsage = chunk.usage\n }\n if (chunk.model) {\n lastModel = chunk.model\n }\n\n // Emit RUN_STARTED on the first chunk of any kind so callers see a\n // run lifecycle even on streams that arrive entirely as usage-only\n // (no choices). Without this, a usage-first stream would skip\n // RUN_STARTED via `if (!choice) continue` below and the post-loop\n // synthetic block would also skip RUN_FINISHED (it gates on\n // `hasEmittedRunStarted`).\n if (!aguiState.hasEmittedRunStarted) {\n aguiState.hasEmittedRunStarted = true\n yield {\n type: EventType.RUN_STARTED,\n runId: aguiState.runId,\n threadId: aguiState.threadId,\n model: chunk.model || options.model,\n timestamp: Date.now(),\n parentRunId: options.parentRunId,\n }\n }\n\n // Reasoning content (extractReasoning() hook). Run before reading\n // choice/delta so reasoning-only chunks (no `choices`) still drive\n // the REASONING_* lifecycle on providers that send reasoning out of\n // band. The base default returns undefined.\n const reasoning = this.extractReasoning(chunk)\n if (reasoning && reasoning.text) {\n if (!reasoningMessageId) {\n reasoningMessageId = generateId(this.name)\n stepId = generateId(this.name)\n yield {\n type: EventType.REASONING_START,\n messageId: reasoningMessageId,\n model: chunk.model || options.model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.REASONING_MESSAGE_START,\n messageId: reasoningMessageId,\n role: 'reasoning' as const,\n model: chunk.model || options.model,\n timestamp: Date.now(),\n }\n // Legacy STEP_STARTED (single emission, paired with the\n // STEP_FINISHED below when reasoning closes).\n yield {\n type: EventType.STEP_STARTED,\n stepName: stepId,\n stepId,\n model: chunk.model || options.model,\n timestamp: Date.now(),\n stepType: 'thinking',\n }\n }\n accumulatedReasoning += reasoning.text\n yield {\n type: EventType.REASONING_MESSAGE_CONTENT,\n messageId: reasoningMessageId,\n delta: reasoning.text,\n model: chunk.model || options.model,\n timestamp: Date.now(),\n }\n }\n\n const choice = chunk.choices[0]\n\n if (!choice) continue\n\n const delta = choice.delta\n const deltaContent = delta.content\n const deltaToolCalls = delta.tool_calls\n\n // Handle content delta\n if (deltaContent) {\n // Close reasoning before text starts so consumers see a clean\n // REASONING_END before any TEXT_MESSAGE_START.\n if (reasoningMessageId && !hasClosedReasoning) {\n hasClosedReasoning = true\n yield {\n type: EventType.REASONING_MESSAGE_END,\n messageId: reasoningMessageId,\n model: chunk.model || options.model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.REASONING_END,\n messageId: reasoningMessageId,\n model: chunk.model || options.model,\n timestamp: Date.now(),\n }\n if (stepId) {\n yield {\n type: EventType.STEP_FINISHED,\n stepName: stepId,\n stepId,\n model: chunk.model || options.model,\n timestamp: Date.now(),\n content: accumulatedReasoning,\n }\n }\n }\n\n // Emit TEXT_MESSAGE_START on first text content\n if (!hasEmittedTextMessageStart) {\n hasEmittedTextMessageStart = true\n yield {\n type: EventType.TEXT_MESSAGE_START,\n messageId: aguiState.messageId,\n model: chunk.model || options.model,\n timestamp: Date.now(),\n role: 'assistant',\n }\n }\n\n accumulatedContent += deltaContent\n\n // Emit AG-UI TEXT_MESSAGE_CONTENT\n yield {\n type: EventType.TEXT_MESSAGE_CONTENT,\n messageId: aguiState.messageId,\n model: chunk.model || options.model,\n timestamp: Date.now(),\n delta: deltaContent,\n content: accumulatedContent,\n }\n }\n\n // Handle tool calls - they come in as deltas\n if (deltaToolCalls) {\n for (const toolCallDelta of deltaToolCalls) {\n const index = toolCallDelta.index\n\n // Initialize or update the tool call in progress\n let toolCall = toolCallsInProgress.get(index)\n if (!toolCall) {\n toolCall = {\n id: toolCallDelta.id || '',\n name: toolCallDelta.function?.name || '',\n arguments: '',\n started: false,\n }\n toolCallsInProgress.set(index, toolCall)\n }\n\n // Update with any new data from the delta\n if (toolCallDelta.id) {\n toolCall.id = toolCallDelta.id\n }\n if (toolCallDelta.function?.name) {\n toolCall.name = toolCallDelta.function.name\n }\n if (toolCallDelta.function?.arguments) {\n toolCall.arguments += toolCallDelta.function.arguments\n }\n\n // Emit TOOL_CALL_START when we have id and name\n if (toolCall.id && toolCall.name && !toolCall.started) {\n toolCall.started = true\n yield {\n type: EventType.TOOL_CALL_START,\n toolCallId: toolCall.id,\n toolCallName: toolCall.name,\n toolName: toolCall.name,\n parentMessageId: aguiState.messageId,\n model: chunk.model || options.model,\n timestamp: Date.now(),\n index,\n }\n }\n\n // Emit TOOL_CALL_ARGS for argument deltas\n if (toolCallDelta.function?.arguments && toolCall.started) {\n yield {\n type: EventType.TOOL_CALL_ARGS,\n toolCallId: toolCall.id,\n model: chunk.model || options.model,\n timestamp: Date.now(),\n delta: toolCallDelta.function.arguments,\n }\n }\n }\n }\n\n // Handle finish reason. We DO emit TOOL_CALL_END and TEXT_MESSAGE_END\n // here because the corresponding _START events have already fired,\n // and tool execution downstream wants to begin as soon as possible.\n // RUN_FINISHED is deferred until the iterator is fully exhausted so\n // we can capture the trailing usage chunk that arrives AFTER this\n // chunk when stream_options.include_usage is on.\n if (choice.finish_reason) {\n if (\n choice.finish_reason === 'tool_calls' ||\n toolCallsInProgress.size > 0\n ) {\n for (const [, toolCall] of toolCallsInProgress) {\n // Skip tool calls that never emitted TOOL_CALL_START — emitting\n // a stray TOOL_CALL_END here would violate AG-UI lifecycle\n // (END without matching START) for partial deltas where the\n // upstream never sent both id and name.\n if (!toolCall.started) continue\n\n // Parse arguments for TOOL_CALL_END. Surface parse failures via\n // the logger so a model emitting malformed JSON for tool args\n // is debuggable instead of silently invoking the tool with {}.\n // Non-object JSON (e.g. a bare string or number) is also coerced\n // to {} so downstream tool execution doesn't receive a primitive\n // input, mirroring the Responses adapter's guard.\n let parsedInput: unknown = {}\n if (toolCall.arguments) {\n try {\n const parsed: unknown = JSON.parse(toolCall.arguments)\n parsedInput =\n parsed && typeof parsed === 'object' ? parsed : {}\n } catch (parseError) {\n options.logger.errors(\n `${this.name}.processStreamChunks tool-args JSON parse failed`,\n {\n error: toRunErrorPayload(\n parseError,\n `tool ${toolCall.name} (${toolCall.id}) returned malformed JSON arguments`,\n ),\n source: `${this.name}.processStreamChunks`,\n toolCallId: toolCall.id,\n toolName: toolCall.name,\n rawArguments: toolCall.arguments,\n },\n )\n parsedInput = {}\n }\n }\n\n // Emit AG-UI TOOL_CALL_END\n yield {\n type: EventType.TOOL_CALL_END,\n toolCallId: toolCall.id,\n toolCallName: toolCall.name,\n toolName: toolCall.name,\n model: chunk.model || options.model,\n timestamp: Date.now(),\n input: parsedInput,\n }\n emittedAnyToolCallEnd = true\n }\n // Clear tool-call state after emission so a subsequent\n // `finish_reason: 'stop'` chunk (or the post-loop synthetic\n // block) doesn't see lingering entries and misreport the finish.\n toolCallsInProgress.clear()\n }\n\n // Emit TEXT_MESSAGE_END if we had text content\n if (hasEmittedTextMessageStart) {\n yield {\n type: EventType.TEXT_MESSAGE_END,\n messageId: aguiState.messageId,\n model: chunk.model || options.model,\n timestamp: Date.now(),\n }\n hasEmittedTextMessageStart = false\n }\n\n // Remember the upstream finish_reason; RUN_FINISHED is emitted at\n // end-of-stream so we pick up the trailing usage-only chunk too.\n pendingFinishReason = choice.finish_reason\n }\n }\n\n // Emit a single terminal RUN_FINISHED after the iterator is exhausted.\n // This both delivers accurate token counts (the trailing usage chunk\n // may arrive AFTER the finish_reason chunk) and gives consumers a\n // guaranteed terminal event even when the upstream cuts off mid-stream\n // (no finish_reason chunk ever arrives).\n if (aguiState.hasEmittedRunStarted) {\n // Close any started tool calls that never got finish_reason. A\n // truncated stream that emitted TOOL_CALL_START but never reached\n // finish_reason would otherwise leave consumers with an unbalanced\n // start. Skip non-started entries (no matching START to close).\n let pendingToolCount = 0\n for (const [, toolCall] of toolCallsInProgress) {\n if (!toolCall.started) continue\n let parsedInput: unknown = {}\n if (toolCall.arguments) {\n try {\n const parsed: unknown = JSON.parse(toolCall.arguments)\n parsedInput = parsed && typeof parsed === 'object' ? parsed : {}\n } catch (parseError) {\n // Mirror the finish_reason path's logger call — a truncated\n // stream emitting malformed tool-call JSON would otherwise\n // silently invoke the tool with `{}`, the exact failure the\n // finish_reason logger was added to prevent.\n options.logger.errors(\n `${this.name}.processStreamChunks tool-args JSON parse failed (drain)`,\n {\n error: toRunErrorPayload(\n parseError,\n `tool ${toolCall.name} (${toolCall.id}) returned malformed JSON arguments`,\n ),\n source: `${this.name}.processStreamChunks`,\n toolCallId: toolCall.id,\n toolName: toolCall.name,\n rawArguments: toolCall.arguments,\n },\n )\n parsedInput = {}\n }\n }\n yield {\n type: EventType.TOOL_CALL_END,\n toolCallId: toolCall.id,\n toolCallName: toolCall.name,\n toolName: toolCall.name,\n model: lastModel || options.model,\n timestamp: Date.now(),\n input: parsedInput,\n }\n pendingToolCount += 1\n emittedAnyToolCallEnd = true\n }\n toolCallsInProgress.clear()\n\n // Make sure the text message lifecycle is closed even on early\n // termination paths where finish_reason never arrives.\n if (hasEmittedTextMessageStart) {\n yield {\n type: EventType.TEXT_MESSAGE_END,\n messageId: aguiState.messageId,\n model: lastModel || options.model,\n timestamp: Date.now(),\n }\n }\n\n // Close any reasoning lifecycle that text never closed (no text\n // content arrived, or the stream cut off before text started).\n if (reasoningMessageId && !hasClosedReasoning) {\n hasClosedReasoning = true\n yield {\n type: EventType.REASONING_MESSAGE_END,\n messageId: reasoningMessageId,\n model: lastModel || options.model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.REASONING_END,\n messageId: reasoningMessageId,\n model: lastModel || options.model,\n timestamp: Date.now(),\n }\n if (stepId) {\n yield {\n type: EventType.STEP_FINISHED,\n stepName: stepId,\n stepId,\n model: lastModel || options.model,\n timestamp: Date.now(),\n content: accumulatedReasoning,\n }\n }\n }\n\n // Map upstream finish_reason to AG-UI's narrower vocabulary.\n // Collapsing length / content_filter to 'stop' would hide why the\n // run terminated — surface it instead. Use `tool_calls` only when\n // a TOOL_CALL_END was actually emitted: an upstream that signalled\n // `tool_calls` but never produced a started/ended pair must NOT\n // surface `tool_calls` here, since downstream consumers wait for\n // tool results that would never arrive. OpenAI's legacy\n // `function_call` value (from the v1 function-calling API) is\n // normalized to `tool_calls` — semantically the same termination.\n const finishReason: NonNullable<RunFinishedEvent['finishReason']> =\n emittedAnyToolCallEnd\n ? 'tool_calls'\n : pendingFinishReason === 'tool_calls'\n ? 'stop'\n : pendingFinishReason === 'function_call'\n ? 'tool_calls'\n : (pendingFinishReason ?? 'stop')\n\n // Conditional `usage` spread: AG-UI's `RunFinishedEvent.usage` is\n // optional with no `| undefined`; omit the key entirely when no usage\n // arrived rather than emitting `usage: undefined`.\n yield {\n type: EventType.RUN_FINISHED,\n runId: aguiState.runId,\n threadId: aguiState.threadId,\n model: lastModel || options.model,\n timestamp: Date.now(),\n ...(lastUsage && {\n usage: buildChatCompletionsUsage(lastUsage),\n }),\n finishReason,\n }\n }\n } catch (error: unknown) {\n // Narrow before logging: raw SDK errors can carry request metadata\n // (including auth headers) which we must never surface to user loggers.\n const errorPayload = toRunErrorPayload(\n error,\n `${this.name}.processStreamChunks failed`,\n )\n const rawEvent = toRunErrorRawEvent(error)\n options.logger.errors(`${this.name}.processStreamChunks fatal`, {\n error: errorPayload,\n source: `${this.name}.processStreamChunks`,\n })\n\n // Emit AG-UI RUN_ERROR with conditional `code` spread (see chatStream's\n // catch block for the rationale). `rawEvent` carries the provider's\n // structured error body when present.\n yield {\n type: EventType.RUN_ERROR,\n model: options.model,\n timestamp: Date.now(),\n message: errorPayload.message,\n ...(errorPayload.code !== undefined && { code: errorPayload.code }),\n ...(rawEvent !== undefined && { rawEvent }),\n error: {\n message: errorPayload.message,\n ...(errorPayload.code !== undefined && { code: errorPayload.code }),\n },\n }\n }\n }\n\n /**\n * Maps common TextOptions to Chat Completions API request format.\n * Override this in subclasses to add provider-specific options.\n */\n protected mapOptionsToRequest(\n options: TextOptions,\n ): ChatCompletionCreateParamsStreaming {\n const tools = options.tools\n ? convertToolsToChatCompletionsFormat(\n options.tools,\n this.makeStructuredOutputCompatible.bind(this),\n )\n : undefined\n\n // Build messages array with system prompts\n const messages: Array<ChatCompletionMessageParam> = []\n\n // Add system prompts first\n const systemPrompts = normalizeSystemPrompts(options.systemPrompts)\n if (systemPrompts.length > 0) {\n messages.push({\n role: 'system',\n content: systemPrompts.map((p) => p.content).join('\\n'),\n })\n }\n\n // Convert messages\n for (const message of options.messages) {\n messages.push(this.convertMessage(message))\n }\n\n const modelOptions = options.modelOptions\n\n // Native combined mode (issue #605): when the engine threads\n // `outputSchema` through TextOptions, the adapter declared\n // `supportsCombinedToolsAndSchema` and the schema is already JSON Schema\n // (pre-converted at the activity boundary). Wire it into\n // `response_format` alongside any `tools`. Modern OpenAI-compatible\n // Chat Completions accepts both together and emits the schema-\n // constrained text on the natural final turn.\n const combinedSchema = options.outputSchema as\n | Record<string, unknown>\n | undefined\n const responseFormat = combinedSchema\n ? {\n response_format: {\n type: 'json_schema' as const,\n json_schema: {\n name: 'structured_output',\n schema: this.makeStructuredOutputCompatible(\n combinedSchema,\n Array.isArray(combinedSchema.required)\n ? (combinedSchema.required as Array<string>)\n : undefined,\n ),\n strict: true,\n },\n },\n }\n : undefined\n\n // `modelOptions` is the sole sampling surface: callers set provider-native\n // wire names (`temperature`, `top_p`, `max_tokens`/`max_completion_tokens`)\n // there and they flow through the spread below. The root\n // `temperature`/`topP`/`maxTokens` fields are intentionally NOT read here.\n return {\n ...modelOptions,\n model: options.model,\n messages,\n // Conditional spread: `tools: undefined` would clobber any\n // modelOptions.tools the caller set above.\n ...(tools &&\n tools.length > 0 && {\n tools,\n }),\n ...(responseFormat ?? {}),\n stream: true,\n }\n }\n\n /**\n * Modern OpenAI-compatible Chat Completions APIs support `tools` and\n * `response_format: json_schema` together in a single streaming request\n * (per issue #605). Subclasses can override — Groq, for instance, must\n * return `false` because its API rejects schema + tools + stream with a\n * 400.\n */\n supportsCombinedToolsAndSchema(): boolean {\n return true\n }\n\n /**\n * Converts a single ModelMessage to the Chat Completions API message format.\n * Override this in subclasses to handle provider-specific message formats.\n */\n protected convertMessage(message: ModelMessage): ChatCompletionMessageParam {\n // Handle tool messages\n if (message.role === 'tool') {\n // The Chat Completions API has no multimodal `tool` message support\n // (unlike the Responses API's `function_call_output`). A tool that\n // returns an `Array<ContentPart>` is therefore stringified here — the\n // documented fallback for providers on the chat-completions path\n // (Groq, Ollama, Grok, OpenRouter chat). Multimodal tool results are\n // only delivered structurally via the Responses adapter.\n return {\n role: 'tool',\n tool_call_id: message.toolCallId || '',\n content:\n typeof message.content === 'string'\n ? message.content\n : JSON.stringify(message.content),\n }\n }\n\n // Handle assistant messages\n if (message.role === 'assistant') {\n const toolCalls = message.toolCalls?.map((tc) => ({\n id: tc.id,\n type: 'function' as const,\n function: {\n name: tc.function.name,\n arguments:\n typeof tc.function.arguments === 'string'\n ? tc.function.arguments\n : JSON.stringify(tc.function.arguments),\n },\n }))\n const hasToolCalls = !!toolCalls && toolCalls.length > 0\n const textContent = this.extractTextContent(message.content)\n\n // Per the OpenAI Chat Completions contract, an assistant message that\n // only carries tool_calls should have `content: null` (or omit content)\n // rather than `content: ''`. Empty-string content interacts oddly with\n // tokenization on some backends; null is the documented shape.\n return {\n role: 'assistant',\n content: hasToolCalls && !textContent ? null : textContent,\n ...(hasToolCalls ? { tool_calls: toolCalls } : {}),\n }\n }\n\n // Handle user messages - support multimodal content\n const contentParts = this.normalizeContent(message.content)\n\n // If only text, use simple string format\n if (contentParts.length === 1 && contentParts[0]?.type === 'text') {\n const text = contentParts[0].content\n if (text.length === 0) {\n // Single empty text part is the same fail-loud condition as below —\n // an empty paid request mask a real intent (caller passed `null`/'',\n // or an upstream step normalised everything to an empty string).\n throw new Error(\n `User message for ${this.name} has empty text content. ` +\n `Empty user messages would produce a paid request with no input; ` +\n `provide non-empty content or omit the message.`,\n )\n }\n return {\n role: 'user',\n content: text,\n }\n }\n\n // Otherwise, use array format for multimodal. Fail fast on unsupported\n // content parts rather than silently dropping them — a message of all\n // unsupported parts would otherwise turn into an empty user prompt and\n // mask a real capability mismatch.\n const parts: Array<ChatCompletionContentPart> = []\n for (const part of contentParts) {\n const converted = this.convertContentPart(part)\n if (!converted) {\n throw new Error(\n `Unsupported content part type for ${this.name}: ${part.type}. ` +\n `Override convertContentPart() in a subclass to handle this type, ` +\n `or remove it from the message.`,\n )\n }\n parts.push(converted)\n }\n\n if (parts.length === 0) {\n // The original message had no content parts at all (e.g. content was\n // explicitly null or []). Sending an empty user message to OpenAI\n // produces a paid request with no signal — fail loud instead.\n throw new Error(\n `User message for ${this.name} has no content parts. ` +\n `Empty user messages would produce a paid request with no input; ` +\n `provide at least one text/image/audio part or omit the message.`,\n )\n }\n\n return {\n role: 'user',\n content: parts,\n }\n }\n\n /**\n * Converts a single ContentPart to the Chat Completions API content part format.\n * Override this in subclasses to handle additional content types or provider-specific metadata.\n */\n protected convertContentPart(\n part: ContentPart,\n ): ChatCompletionContentPart | null {\n if (part.type === 'text') {\n return { type: 'text', text: part.content }\n }\n\n if (part.type === 'image') {\n const imageMetadata = part.metadata as\n | { detail?: 'auto' | 'low' | 'high' }\n | undefined\n\n // For base64 data, construct a data URI using the mimeType from source.\n // Default to a generic octet-stream MIME if the source didn't provide\n // one — interpolating `undefined` into the URI (\"data:undefined;base64,\n // ...\") would produce an invalid URI the API rejects.\n const imageValue = part.source.value\n const imageMime = part.source.mimeType || 'application/octet-stream'\n const imageUrl =\n part.source.type === 'data' && !imageValue.startsWith('data:')\n ? `data:${imageMime};base64,${imageValue}`\n : imageValue\n\n return {\n type: 'image_url',\n image_url: {\n url: imageUrl,\n detail: imageMetadata?.detail || 'auto',\n },\n }\n }\n\n if (part.type === 'document') {\n // Documents (PDF) are implemented on the Responses adapter, which maps\n // them to `input_file`. Model modality arrays are not endpoint-scoped,\n // so a document part can type-check for a model this adapter serves —\n // point callers at the supported path instead of a generic error.\n throw new Error(\n `${this.name} does not support document parts on the Chat Completions ` +\n `API; use the Responses adapter, which sends them as input_file.`,\n )\n }\n\n // Unsupported content type — subclasses can override to handle more types\n return null\n }\n\n /**\n * Normalizes message content to an array of ContentPart.\n * Handles backward compatibility with string content.\n */\n protected normalizeContent(\n content: string | null | Array<ContentPart>,\n ): Array<ContentPart> {\n if (content === null) {\n return []\n }\n if (typeof content === 'string') {\n return [{ type: 'text', content: content }]\n }\n return content\n }\n\n /**\n * Extracts text content from a content value that may be string, null, or ContentPart array.\n */\n protected extractTextContent(\n content: string | null | Array<ContentPart>,\n ): string {\n if (content === null) {\n return ''\n }\n if (typeof content === 'string') {\n return content\n }\n // It's an array of ContentPart\n return content\n .filter((p) => p.type === 'text')\n .map((p) => p.content)\n .join('')\n }\n}\n"],"mappings":"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"}
@@ -132,7 +132,7 @@ export declare abstract class OpenAIBaseResponsesTextAdapter<TModel extends stri
132
132
  protected convertMessagesToInput(messages: Array<ModelMessage>): ResponseInput;
133
133
  /**
134
134
  * Converts a ContentPart to Responses API input content item.
135
- * Handles text, image, and audio content parts.
135
+ * Handles text, image, audio, and document (PDF) content parts.
136
136
  * Override this in subclasses for additional content types or provider-specific metadata.
137
137
  */
138
138
  protected convertContentPartToInput(part: ContentPart): ResponseInputContent;
@@ -3,10 +3,11 @@ import { buildResponsesUsage } from "../usage.js";
3
3
  import { extractRequestOptions } from "../utils/request-options.js";
4
4
  import { convertToolsToResponsesFormat } from "./responses-tool-converter.js";
5
5
  import { EventType, normalizeSystemPrompts } from "@tanstack/ai";
6
- import { BaseTextAdapter } from "@tanstack/ai/adapters";
7
6
  import { toRunErrorPayload, toRunErrorRawEvent } from "@tanstack/ai/adapter-internals";
7
+ import { BaseTextAdapter } from "@tanstack/ai/adapters";
8
8
  import { generateId } from "@tanstack/ai-utils";
9
9
  //#region src/adapters/responses-text.ts
10
+ var PDF_BASE64_MAGIC = "JVBERi";
10
11
  /**
11
12
  * Shared implementation of the OpenAI Responses API. Holds the stream-event
12
13
  * accumulator + AG-UI lifecycle and calls the OpenAI SDK directly. Subclasses
@@ -1122,7 +1123,7 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
1122
1123
  const contentParts = this.normalizeContent(message.content);
1123
1124
  const inputContent = [];
1124
1125
  for (const part of contentParts) inputContent.push(this.convertContentPartToInput(part));
1125
- if (inputContent.length === 0) throw new Error(`User message for ${this.name} has no content parts. Empty user messages would produce a paid request with no input; provide at least one text/image/audio part or omit the message.`);
1126
+ if (inputContent.length === 0) throw new Error(`User message for ${this.name} has no content parts. Empty user messages would produce a paid request with no input; provide at least one text/image/audio/document part or omit the message.`);
1126
1127
  result.push({
1127
1128
  type: "message",
1128
1129
  role: "user",
@@ -1133,7 +1134,7 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
1133
1134
  }
1134
1135
  /**
1135
1136
  * Converts a ContentPart to Responses API input content item.
1136
- * Handles text, image, and audio content parts.
1137
+ * Handles text, image, audio, and document (PDF) content parts.
1137
1138
  * Override this in subclasses for additional content types or provider-specific metadata.
1138
1139
  */
1139
1140
  convertContentPartToInput(part) {
@@ -1169,6 +1170,28 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
1169
1170
  file_data: audioValue.startsWith("data:") ? audioValue : `data:${audioMime};base64,${audioValue}`
1170
1171
  };
1171
1172
  }
1173
+ case "document": {
1174
+ const documentMetadata = part.metadata;
1175
+ const documentDetail = documentMetadata?.detail !== void 0 ? { detail: documentMetadata.detail } : {};
1176
+ if (part.source.type === "url") return {
1177
+ type: "input_file",
1178
+ file_url: part.source.value,
1179
+ ...documentDetail
1180
+ };
1181
+ const documentValue = part.source.value;
1182
+ const documentMime = ((part.source.mimeType || "application/pdf").split(";")[0] ?? "").trim().toLowerCase();
1183
+ if (documentMime !== "application/pdf") throw new Error(`${this.name} document parts only support application/pdf (received ${documentMime})`);
1184
+ if (documentValue.startsWith("data:") && !/^data:application\/pdf[;,]/i.test(documentValue)) throw new Error(`${this.name} document parts only support application/pdf (received data URL with non-PDF media type)`);
1185
+ const documentBase64 = documentValue.startsWith("data:") ? /;base64,/i.test(documentValue) ? documentValue.slice(documentValue.indexOf(",") + 1) : "" : documentValue;
1186
+ if (documentBase64 && !documentBase64.startsWith(PDF_BASE64_MAGIC)) throw new Error(`${this.name} document parts only support application/pdf (inline data does not start with the %PDF header)`);
1187
+ const documentFileData = documentValue.startsWith("data:") ? documentValue : `data:${documentMime};base64,${documentValue}`;
1188
+ return {
1189
+ type: "input_file",
1190
+ filename: documentMetadata?.filename || "document.pdf",
1191
+ file_data: documentFileData,
1192
+ ...documentDetail
1193
+ };
1194
+ }
1172
1195
  default: throw new Error(`Unsupported content part type: ${part.type}`);
1173
1196
  }
1174
1197
  }