@fgv/ts-extras 5.1.0-56 → 5.1.0-57
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.
- package/CAPABILITIES.md +3 -2
- package/dist/packlets/ai-assist/chatRequestBuilders.js +2 -2
- package/dist/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
- package/dist/packlets/ai-assist/completionClient.js +74 -14
- package/dist/packlets/ai-assist/completionClient.js.map +1 -1
- package/dist/packlets/ai-assist/converters.js +54 -0
- package/dist/packlets/ai-assist/converters.js.map +1 -1
- package/dist/packlets/ai-assist/embeddingClient.js +16 -1
- package/dist/packlets/ai-assist/embeddingClient.js.map +1 -1
- package/dist/packlets/ai-assist/imageGenerationClient.js +16 -1
- package/dist/packlets/ai-assist/imageGenerationClient.js.map +1 -1
- package/dist/packlets/ai-assist/listModelsClient.js +19 -1
- package/dist/packlets/ai-assist/listModelsClient.js.map +1 -1
- package/dist/packlets/ai-assist/model.js.map +1 -1
- package/dist/packlets/ai-assist/streamUsageCapability.js +16 -6
- package/dist/packlets/ai-assist/streamUsageCapability.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/anthropic.js +8 -4
- package/dist/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +22 -7
- package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js +17 -9
- package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js +12 -9
- package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/proxy.js +62 -4
- package/dist/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -1
- package/dist/packlets/ai-assist/streamingClient.js +20 -6
- package/dist/packlets/ai-assist/streamingClient.js.map +1 -1
- package/dist/packlets/ai-assist/thinkingOptionsResolver.js +5 -2
- package/dist/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -1
- package/dist/packlets/ai-assist/toolTypes.js.map +1 -1
- package/dist/ts-extras.d.ts +223 -24
- package/lib/packlets/ai-assist/chatRequestBuilders.d.ts +2 -2
- package/lib/packlets/ai-assist/chatRequestBuilders.js +2 -2
- package/lib/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
- package/lib/packlets/ai-assist/completionClient.d.ts +13 -0
- package/lib/packlets/ai-assist/completionClient.d.ts.map +1 -1
- package/lib/packlets/ai-assist/completionClient.js +74 -14
- package/lib/packlets/ai-assist/completionClient.js.map +1 -1
- package/lib/packlets/ai-assist/converters.d.ts +46 -0
- package/lib/packlets/ai-assist/converters.d.ts.map +1 -1
- package/lib/packlets/ai-assist/converters.js +55 -1
- package/lib/packlets/ai-assist/converters.js.map +1 -1
- package/lib/packlets/ai-assist/embeddingClient.d.ts +9 -0
- package/lib/packlets/ai-assist/embeddingClient.d.ts.map +1 -1
- package/lib/packlets/ai-assist/embeddingClient.js +16 -1
- package/lib/packlets/ai-assist/embeddingClient.js.map +1 -1
- package/lib/packlets/ai-assist/imageGenerationClient.d.ts +9 -0
- package/lib/packlets/ai-assist/imageGenerationClient.d.ts.map +1 -1
- package/lib/packlets/ai-assist/imageGenerationClient.js +16 -1
- package/lib/packlets/ai-assist/imageGenerationClient.js.map +1 -1
- package/lib/packlets/ai-assist/listModelsClient.d.ts +12 -0
- package/lib/packlets/ai-assist/listModelsClient.d.ts.map +1 -1
- package/lib/packlets/ai-assist/listModelsClient.js +19 -1
- package/lib/packlets/ai-assist/listModelsClient.js.map +1 -1
- package/lib/packlets/ai-assist/model.d.ts +22 -7
- package/lib/packlets/ai-assist/model.d.ts.map +1 -1
- package/lib/packlets/ai-assist/model.js.map +1 -1
- package/lib/packlets/ai-assist/streamUsageCapability.d.ts +16 -6
- package/lib/packlets/ai-assist/streamUsageCapability.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamUsageCapability.js +16 -6
- package/lib/packlets/ai-assist/streamUsageCapability.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts +2 -1
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.js +6 -2
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts +37 -2
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +21 -6
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/common.d.ts +17 -3
- package/lib/packlets/ai-assist/streamingAdapters/common.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts +2 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js +15 -7
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts +2 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js +10 -7
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/proxy.d.ts +22 -0
- package/lib/packlets/ai-assist/streamingAdapters/proxy.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/proxy.js +62 -4
- package/lib/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -1
- package/lib/packlets/ai-assist/streamingClient.d.ts +5 -0
- package/lib/packlets/ai-assist/streamingClient.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingClient.js +19 -5
- package/lib/packlets/ai-assist/streamingClient.js.map +1 -1
- package/lib/packlets/ai-assist/thinkingOptionsResolver.d.ts +11 -3
- package/lib/packlets/ai-assist/thinkingOptionsResolver.d.ts.map +1 -1
- package/lib/packlets/ai-assist/thinkingOptionsResolver.js +5 -2
- package/lib/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -1
- package/lib/packlets/ai-assist/toolTypes.d.ts +61 -3
- package/lib/packlets/ai-assist/toolTypes.d.ts.map +1 -1
- package/lib/packlets/ai-assist/toolTypes.js.map +1 -1
- package/package.json +7 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openaiChat.js","sourceRoot":"","sources":["../../../../src/packlets/ai-assist/streamingAdapters/openaiChat.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * Streaming adapter for OpenAI Chat Completions (also used for Groq, Mistral,\n * and other Chat-Completions-compatible providers when no tools are\n * requested).\n *\n * @packageDocumentation\n */\n\nimport { type Logging, Result, succeed, type Validator, Validators } from '@fgv/ts-utils';\nimport { type JsonObject } from '@fgv/ts-json-base';\n\nimport { buildMessages, buildOpenAiChatUserContent } from '../chatRequestBuilders';\nimport { bearerAuthHeader } from '../endpoint';\nimport { AiPrompt, type IAiStreamEvent, type IChatMessage } from '../model';\nimport { parseSseEventJson, readSseEvents } from '../sseParser';\nimport { type IResolvedThinkingConfig } from '../thinkingOptionsResolver';\nimport { normalizeOpenAiChatUsage } from '../usageNormalization';\nimport {\n IStreamApiConfig,\n jsonObjectOrNullValidator,\n openSseConnection,\n validateEventPayload\n} from './common';\n\n// ============================================================================\n// Event payload shapes\n// ============================================================================\n\n/**\n * The shape of `choices[0]` in an OpenAI Chat Completions streaming chunk.\n * Both `delta.content` and `finish_reason` are optional — content arrives in\n * intermediate chunks, finish_reason in the terminal chunk. The wire sends\n * `finish_reason: null` (literal null, not absent) on intermediate chunks,\n * so the validator must accept null alongside string.\n *\n * @internal\n */\ninterface IOpenAiChatStreamChoice {\n readonly delta?: { readonly content?: string };\n // eslint-disable-next-line @rushstack/no-new-null\n readonly finish_reason?: string | null;\n}\n\n/**\n * One streaming chunk from the OpenAI Chat Completions endpoint. Always has\n * a `choices` array with one element in the streaming mode.\n *\n * @internal\n */\ninterface IOpenAiChatStreamChunk {\n readonly choices: ReadonlyArray<IOpenAiChatStreamChoice>;\n /**\n * Only present at all when the request carries\n * `stream_options.include_usage: true` (see `callOpenAiChatStream`) — without\n * it OpenAI Chat Completions never emits a usage block while streaming,\n * unlike every other adapter here. When\n * present, the wire sends literal `null` on every chunk except the terminal\n * one, which carries the populated object; `null` must validate (not fail the\n * whole chunk) or every intermediate chunk — including its `delta.content` and\n * `finish_reason` — is dropped along with it.\n */\n // eslint-disable-next-line @rushstack/no-new-null\n readonly usage?: JsonObject | null;\n}\n\n// eslint-disable-next-line @rushstack/no-new-null\nconst stringOrNull: Validator<string | null> = Validators.isA<string | null>(\n 'string-or-null',\n // eslint-disable-next-line @rushstack/no-new-null\n (v: unknown): v is string | null => typeof v === 'string' || v === null\n);\n\nconst openAiChatStreamChoice: Validator<IOpenAiChatStreamChoice> = Validators.object<IOpenAiChatStreamChoice>(\n {\n delta: Validators.object<{ content?: string }>(\n { content: Validators.string },\n { options: { optionalFields: ['content'] } }\n ).optional(),\n finish_reason: stringOrNull.optional()\n },\n { options: { optionalFields: ['delta', 'finish_reason'] } }\n);\n\nconst openAiChatStreamChunk: Validator<IOpenAiChatStreamChunk> = Validators.object<IOpenAiChatStreamChunk>({\n choices: Validators.arrayOf(openAiChatStreamChoice),\n usage: jsonObjectOrNullValidator.optional()\n});\n\n// ============================================================================\n// Stream translator\n// ============================================================================\n\n/**\n * Translates an OpenAI Chat Completions SSE stream into unified events.\n *\n * @internal\n */\nasync function* translateOpenAiChatStream(\n response: Response,\n reportsUsage: boolean\n): AsyncGenerator<IAiStreamEvent> {\n let fullText = '';\n let truncated = false;\n let receivedDone = false;\n let usageRaw: JsonObject | undefined;\n\n try {\n /* c8 ignore next - body is non-null at this point per openSseConnection */\n if (!response.body) return;\n for await (const message of readSseEvents(response.body)) {\n const json = parseSseEventJson(message.data);\n if (json === undefined) {\n // [DONE] sentinel or unparseable; skip\n continue;\n }\n const chunk = validateEventPayload(json, openAiChatStreamChunk);\n // Every intermediate chunk carries literal `null` once `stream_options.include_usage`\n // is set; only the terminal chunk's populated object should update the accumulator.\n // Gated on reportsUsage (same descriptor check that decides whether the request even\n // asked for it, see callOpenAiChatStream) — this adapter is shared by descriptors with\n // no confirmed cache-usage reporting, and an unprompted usage block from one of those\n // carries no cache information (see supportsCacheUsageReporting).\n if (reportsUsage && chunk?.usage !== undefined && chunk.usage !== null) {\n usageRaw = chunk.usage;\n }\n /* c8 ignore next 1 - defensive: chunk?.choices optional chain unreachable after validation */\n const choice = chunk?.choices[0];\n // The `stream_options.include_usage` terminal chunk carries `choices: []` and only\n // `usage` — already captured above, so it is expected (not defensive) to fall through here.\n if (!choice) {\n continue;\n }\n /* c8 ignore next 1 - defensive: choice.delta?.content optional chain unreachable after validation */\n const delta = choice.delta?.content;\n if (typeof delta === 'string' && delta.length > 0) {\n fullText += delta;\n yield { type: 'text-delta', delta };\n }\n const finish = choice.finish_reason;\n if (typeof finish === 'string' && finish.length > 0) {\n truncated = finish === 'length';\n receivedDone = true;\n }\n }\n } catch (err: unknown) /* c8 ignore start - defensive: stream errors are always Error instances */ {\n yield { type: 'error', message: err instanceof Error ? err.message : String(err) };\n return;\n } /* c8 ignore stop */\n\n if (receivedDone) {\n const usage = normalizeOpenAiChatUsage(usageRaw);\n yield { type: 'done', truncated, fullText, ...(usage !== undefined ? { usage } : {}) };\n } else {\n yield { type: 'error', message: 'OpenAI stream ended without a finish_reason' };\n }\n}\n\n// ============================================================================\n// Per-format request caller\n// ============================================================================\n\n/**\n * Issues a streaming Chat Completions request and returns the unified-event\n * iterable on success.\n *\n * @internal\n */\nexport async function callOpenAiChatStream(\n config: IStreamApiConfig,\n prompt: AiPrompt,\n messagesBefore: ReadonlyArray<IChatMessage> | undefined,\n temperature: number | undefined,\n logger?: Logging.ILogger,\n signal?: AbortSignal,\n resolvedThinking?: IResolvedThinkingConfig,\n maxTokens?: number,\n useMaxCompletionTokensField: boolean = false,\n includeStreamUsage: boolean = false\n): Promise<Result<AsyncIterable<IAiStreamEvent>>> {\n const url = `${config.baseUrl}/chat/completions`;\n const messages = buildMessages(prompt.system, buildOpenAiChatUserContent(prompt), {\n head: messagesBefore\n });\n const effort = resolvedThinking?.openAiEffort ?? resolvedThinking?.xaiEffort;\n const supportsReasoning = config.model !== 'grok-4';\n const body: Record<string, unknown> = {\n model: config.model,\n messages,\n stream: true\n };\n // Chat Completions omits the usage block from every streaming response unless asked —\n // unlike the Responses API and Anthropic, which report it unconditionally. Gated per\n // AiAssist.supportsStreamUsageOption: this adapter also carries providers whose tolerance\n // for an unrecognized request field is unverified (a strict self-hosted server can reject\n // the whole request rather than ignore it), so the field is sent only where it's confirmed.\n if (includeStreamUsage) {\n body.stream_options = { include_usage: true };\n }\n if (effort !== undefined && supportsReasoning) {\n body.reasoning_effort = effort;\n }\n // Temperature is sent only when the caller explicitly provided one; omitting it lets the\n // provider apply its own default (current-gen models reject a caller-supplied default). This\n // adapter is reached only via the conflict-guarded streaming entry point — the client-tool path\n // routes OpenAI through the Responses adapter, not chat — so, like callOpenAiCompletion, no\n // effort gate is needed: temperature is present here only when the effort is undefined/'none'.\n if (temperature !== undefined) {\n body.temperature = temperature;\n }\n // Omitted when the caller doesn't set maxTokens. See AiAssist.usesMaxCompletionTokensField for\n // the field-name split between genuine OpenAI and the other openai-format providers.\n if (maxTokens !== undefined) {\n body[useMaxCompletionTokensField ? 'max_completion_tokens' : 'max_tokens'] = maxTokens;\n }\n if (resolvedThinking?.otherParams !== undefined) {\n Object.assign(body, resolvedThinking.otherParams);\n }\n const headers: Record<string, string> = bearerAuthHeader(config.apiKey);\n /* c8 ignore next 1 - optional logger */\n logger?.info(`OpenAI streaming completion: model=${config.model}`);\n const conn = await openSseConnection(url, headers, body, logger, signal);\n return conn.onSuccess((response) => succeed(translateOpenAiChatStream(response, includeStreamUsage)));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"openaiChat.js","sourceRoot":"","sources":["../../../../src/packlets/ai-assist/streamingAdapters/openaiChat.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * Streaming adapter for OpenAI Chat Completions (also used for Groq, Mistral,\n * and other Chat-Completions-compatible providers when no tools are\n * requested).\n *\n * @packageDocumentation\n */\n\nimport { fail, type Logging, Result, succeed, type Validator, Validators } from '@fgv/ts-utils';\nimport { type JsonObject } from '@fgv/ts-json-base';\n\nimport {\n buildMessages,\n buildOpenAiChatSystemContent,\n buildOpenAiChatUserContent\n} from '../chatRequestBuilders';\nimport { type IAiCacheRequest } from '../cacheRequest';\nimport { bearerAuthHeader } from '../endpoint';\nimport { AiPrompt, type IAiStreamEvent, type IChatMessage } from '../model';\nimport { parseSseEventJson, readSseEvents } from '../sseParser';\nimport { type IResolvedThinkingConfig } from '../thinkingOptionsResolver';\nimport { normalizeOpenAiChatUsage } from '../usageNormalization';\nimport {\n IStreamApiConfig,\n jsonObjectOrNullValidator,\n openSseConnection,\n validateEventPayload\n} from './common';\n\n// ============================================================================\n// Event payload shapes\n// ============================================================================\n\n/**\n * The shape of `choices[0]` in an OpenAI Chat Completions streaming chunk.\n * Both `delta.content` and `finish_reason` are optional — content arrives in\n * intermediate chunks, finish_reason in the terminal chunk. The wire sends\n * `finish_reason: null` (literal null, not absent) on intermediate chunks,\n * so the validator must accept null alongside string.\n *\n * @internal\n */\ninterface IOpenAiChatStreamChoice {\n readonly delta?: { readonly content?: string };\n // eslint-disable-next-line @rushstack/no-new-null\n readonly finish_reason?: string | null;\n}\n\n/**\n * One streaming chunk from the OpenAI Chat Completions endpoint. Always has\n * a `choices` array with one element in the streaming mode.\n *\n * @internal\n */\ninterface IOpenAiChatStreamChunk {\n readonly choices: ReadonlyArray<IOpenAiChatStreamChoice>;\n /**\n * Only present at all when the request carries\n * `stream_options.include_usage: true` (see `callOpenAiChatStream`) — without\n * it OpenAI Chat Completions never emits a usage block while streaming,\n * unlike every other adapter here. When\n * present, the wire sends literal `null` on every chunk except the terminal\n * one, which carries the populated object; `null` must validate (not fail the\n * whole chunk) or every intermediate chunk — including its `delta.content` and\n * `finish_reason` — is dropped along with it.\n */\n // eslint-disable-next-line @rushstack/no-new-null\n readonly usage?: JsonObject | null;\n}\n\n// eslint-disable-next-line @rushstack/no-new-null\nconst stringOrNull: Validator<string | null> = Validators.isA<string | null>(\n 'string-or-null',\n // eslint-disable-next-line @rushstack/no-new-null\n (v: unknown): v is string | null => typeof v === 'string' || v === null\n);\n\nconst openAiChatStreamChoice: Validator<IOpenAiChatStreamChoice> = Validators.object<IOpenAiChatStreamChoice>(\n {\n delta: Validators.object<{ content?: string }>(\n { content: Validators.string },\n { options: { optionalFields: ['content'] } }\n ).optional(),\n finish_reason: stringOrNull.optional()\n },\n { options: { optionalFields: ['delta', 'finish_reason'] } }\n);\n\nconst openAiChatStreamChunk: Validator<IOpenAiChatStreamChunk> = Validators.object<IOpenAiChatStreamChunk>({\n choices: Validators.arrayOf(openAiChatStreamChoice),\n usage: jsonObjectOrNullValidator.optional()\n});\n\n// ============================================================================\n// Stream translator\n// ============================================================================\n\n/**\n * Translates an OpenAI Chat Completions SSE stream into unified events.\n *\n * @internal\n */\nasync function* translateOpenAiChatStream(\n response: Response,\n reportsUsage: boolean\n): AsyncGenerator<IAiStreamEvent> {\n let fullText = '';\n let truncated = false;\n let receivedDone = false;\n let usageRaw: JsonObject | undefined;\n\n try {\n /* c8 ignore next - body is non-null at this point per openSseConnection */\n if (!response.body) return;\n for await (const message of readSseEvents(response.body)) {\n const json = parseSseEventJson(message.data);\n if (json === undefined) {\n // [DONE] sentinel or unparseable; skip\n continue;\n }\n const chunk = validateEventPayload(json, openAiChatStreamChunk);\n // Every intermediate chunk carries literal `null` once `stream_options.include_usage`\n // is set; only the terminal chunk's populated object should update the accumulator.\n // Gated on reportsUsage (same descriptor check that decides whether the request even\n // asked for it, see callOpenAiChatStream) — this adapter is shared by descriptors with\n // no confirmed cache-usage reporting, and an unprompted usage block from one of those\n // carries no cache information (see supportsCacheUsageReporting).\n if (reportsUsage && chunk?.usage !== undefined && chunk.usage !== null) {\n usageRaw = chunk.usage;\n }\n /* c8 ignore next 1 - defensive: chunk?.choices optional chain unreachable after validation */\n const choice = chunk?.choices[0];\n // The `stream_options.include_usage` terminal chunk carries `choices: []` and only\n // `usage` — already captured above, so it is expected (not defensive) to fall through here.\n if (!choice) {\n continue;\n }\n /* c8 ignore next 1 - defensive: choice.delta?.content optional chain unreachable after validation */\n const delta = choice.delta?.content;\n if (typeof delta === 'string' && delta.length > 0) {\n fullText += delta;\n yield { type: 'text-delta', delta };\n }\n const finish = choice.finish_reason;\n if (typeof finish === 'string' && finish.length > 0) {\n truncated = finish === 'length';\n receivedDone = true;\n }\n }\n } catch (err: unknown) /* c8 ignore start - defensive: stream errors are always Error instances */ {\n yield { type: 'error', message: err instanceof Error ? err.message : String(err) };\n return;\n } /* c8 ignore stop */\n\n if (receivedDone) {\n const usage = normalizeOpenAiChatUsage(usageRaw);\n yield { type: 'done', truncated, fullText, ...(usage !== undefined ? { usage } : {}) };\n } else {\n yield { type: 'error', message: 'OpenAI stream ended without a finish_reason' };\n }\n}\n\n// ============================================================================\n// Per-format request caller\n// ============================================================================\n\n/**\n * Issues a streaming Chat Completions request and returns the unified-event\n * iterable on success.\n *\n * @internal\n */\nexport async function callOpenAiChatStream(\n config: IStreamApiConfig,\n prompt: AiPrompt,\n messagesBefore: ReadonlyArray<IChatMessage> | undefined,\n temperature: number | undefined,\n logger?: Logging.ILogger,\n signal?: AbortSignal,\n resolvedThinking?: IResolvedThinkingConfig,\n maxTokens?: number,\n useMaxCompletionTokensField: boolean = false,\n includeStreamUsage: boolean = false,\n cache?: IAiCacheRequest,\n cacheKeyHeader?: string\n): Promise<Result<AsyncIterable<IAiStreamEvent>>> {\n const url = `${config.baseUrl}/chat/completions`;\n const systemContentResult = buildOpenAiChatSystemContent(prompt.system, cache);\n if (systemContentResult.isFailure()) {\n return fail(systemContentResult.message);\n }\n const messages = buildMessages(systemContentResult.value, buildOpenAiChatUserContent(prompt), {\n head: messagesBefore\n });\n const effort = resolvedThinking?.openAiEffort ?? resolvedThinking?.xaiEffort;\n const supportsReasoning = config.model !== 'grok-4';\n const body: Record<string, unknown> = {\n model: config.model,\n messages,\n stream: true,\n // Pure additive routing plumbing — see the identical field and gating comment on\n // callOpenAiCompletion's body in completionClient.ts. `cacheKeyHeader` is set when the\n // provider carries the routing key as a header instead (xAI), so the body field is\n // omitted in that case to avoid sending an unrecognized field twice.\n ...(cache?.cacheKey !== undefined && cacheKeyHeader === undefined\n ? // eslint-disable-next-line @typescript-eslint/naming-convention -- wire field name\n { prompt_cache_key: cache.cacheKey }\n : {})\n };\n // Chat Completions omits the usage block from every streaming response unless asked —\n // unlike the Responses API and Anthropic, which report it unconditionally. Gated per\n // AiAssist.supportsStreamUsageOption: this adapter also carries providers whose tolerance\n // for an unrecognized request field is unverified (a strict self-hosted server can reject\n // the whole request rather than ignore it), so the field is sent only where it's confirmed.\n if (includeStreamUsage) {\n body.stream_options = { include_usage: true };\n }\n if (effort !== undefined && supportsReasoning) {\n body.reasoning_effort = effort;\n }\n // Temperature is sent only when the caller explicitly provided one; omitting it lets the\n // provider apply its own default (current-gen models reject a caller-supplied default). This\n // adapter is reached only via the conflict-guarded streaming entry point — the client-tool path\n // routes OpenAI through the Responses adapter, not chat — so, like callOpenAiCompletion, no\n // effort gate is needed: temperature is present here only when the effort is undefined/'none'.\n if (temperature !== undefined) {\n body.temperature = temperature;\n }\n // Omitted when the caller doesn't set maxTokens. See AiAssist.usesMaxCompletionTokensField for\n // the field-name split between genuine OpenAI and the other openai-format providers.\n if (maxTokens !== undefined) {\n body[useMaxCompletionTokensField ? 'max_completion_tokens' : 'max_tokens'] = maxTokens;\n }\n if (resolvedThinking?.otherParams !== undefined) {\n Object.assign(body, resolvedThinking.otherParams);\n }\n const headers: Record<string, string> = bearerAuthHeader(config.apiKey);\n // Sticky-routing key, when the provider carries it as a header rather than a body field —\n // see the identical header on callOpenAiCompletion's request in completionClient.ts.\n if (cache?.cacheKey !== undefined && cacheKeyHeader !== undefined) {\n headers[cacheKeyHeader] = cache.cacheKey;\n }\n /* c8 ignore next 1 - optional logger */\n logger?.info(`OpenAI streaming completion: model=${config.model}`);\n const conn = await openSseConnection(url, headers, body, logger, signal);\n return conn.onSuccess((response) => succeed(translateOpenAiChatStream(response, includeStreamUsage)));\n}\n"]}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { type Logging, Result } from '@fgv/ts-utils';
|
|
13
13
|
import { type JsonObject } from '@fgv/ts-json-base';
|
|
14
|
+
import { type IAiCacheRequest } from '../cacheRequest';
|
|
14
15
|
import { AiPrompt, type AiToolConfig, type IAiStreamEvent, type IChatMessage } from '../model';
|
|
15
16
|
import { type IResolvedThinkingConfig } from '../thinkingOptionsResolver';
|
|
16
17
|
import { IStreamApiConfig } from './common';
|
|
@@ -29,5 +30,5 @@ export interface IAccumulatedFunctionCall {
|
|
|
29
30
|
*
|
|
30
31
|
* @internal
|
|
31
32
|
*/
|
|
32
|
-
export declare function callOpenAiResponsesStream(config: IStreamApiConfig, prompt: AiPrompt, tools: ReadonlyArray<AiToolConfig> | undefined, messagesBefore: ReadonlyArray<IChatMessage> | undefined, temperature: number | undefined, logger?: Logging.ILogger, signal?: AbortSignal, resolvedThinking?: IResolvedThinkingConfig, functionCallMap?: Map<string, IAccumulatedFunctionCall>, continuationMessages?: ReadonlyArray<JsonObject>, maxTokens?: number, reportsUsage?: boolean): Promise<Result<AsyncIterable<IAiStreamEvent>>>;
|
|
33
|
+
export declare function callOpenAiResponsesStream(config: IStreamApiConfig, prompt: AiPrompt, tools: ReadonlyArray<AiToolConfig> | undefined, messagesBefore: ReadonlyArray<IChatMessage> | undefined, temperature: number | undefined, logger?: Logging.ILogger, signal?: AbortSignal, resolvedThinking?: IResolvedThinkingConfig, functionCallMap?: Map<string, IAccumulatedFunctionCall>, continuationMessages?: ReadonlyArray<JsonObject>, maxTokens?: number, reportsUsage?: boolean, cache?: IAiCacheRequest): Promise<Result<AsyncIterable<IAiStreamEvent>>>;
|
|
33
34
|
//# sourceMappingURL=openaiResponses.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openaiResponses.d.ts","sourceRoot":"","sources":["../../../../src/packlets/ai-assist/streamingAdapters/openaiResponses.ts"],"names":[],"mappings":"AAoBA;;;;;;;;;;GAUG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"openaiResponses.d.ts","sourceRoot":"","sources":["../../../../src/packlets/ai-assist/streamingAdapters/openaiResponses.ts"],"names":[],"mappings":"AAoBA;;;;;;;;;;GAUG;AAEH,OAAO,EAAQ,KAAK,OAAO,EAAE,MAAM,EAAuC,MAAM,eAAe,CAAC;AAChG,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAOpD,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD,OAAO,EAAE,QAAQ,EAAE,KAAK,YAAY,EAAE,KAAK,cAAc,EAAE,KAAK,YAAY,EAAE,MAAM,UAAU,CAAC;AAG/F,OAAO,EAAE,KAAK,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAE1E,OAAO,EACL,gBAAgB,EAMjB,MAAM,UAAU,CAAC;AAMlB;;;GAGG;AACH,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;CACpB;AAoZD;;;;;GAKG;AACH,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,gBAAgB,EACxB,MAAM,EAAE,QAAQ,EAChB,KAAK,EAAE,aAAa,CAAC,YAAY,CAAC,YAAK,EACvC,cAAc,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,SAAS,EACvD,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,MAAM,CAAC,EAAE,OAAO,CAAC,OAAO,EACxB,MAAM,CAAC,EAAE,WAAW,EACpB,gBAAgB,CAAC,EAAE,uBAAuB,EAC1C,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,wBAAwB,CAAC,EACvD,oBAAoB,CAAC,EAAE,aAAa,CAAC,UAAU,CAAC,EAChD,SAAS,CAAC,EAAE,MAAM,EAClB,YAAY,GAAE,OAAe,EAC7B,KAAK,CAAC,EAAE,eAAe,GACtB,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,cAAc,CAAC,CAAC,CAAC,CAwDhD"}
|
|
@@ -356,20 +356,23 @@ function translateOpenAiResponsesStream(response, functionCallMap, reportsUsage,
|
|
|
356
356
|
*
|
|
357
357
|
* @internal
|
|
358
358
|
*/
|
|
359
|
-
async function callOpenAiResponsesStream(config, prompt, tools = [], messagesBefore, temperature, logger, signal, resolvedThinking, functionCallMap, continuationMessages, maxTokens, reportsUsage = false) {
|
|
359
|
+
async function callOpenAiResponsesStream(config, prompt, tools = [], messagesBefore, temperature, logger, signal, resolvedThinking, functionCallMap, continuationMessages, maxTokens, reportsUsage = false, cache) {
|
|
360
360
|
var _a;
|
|
361
361
|
const url = `${config.baseUrl}/responses`;
|
|
362
|
-
const
|
|
362
|
+
const systemContentResult = (0, chatRequestBuilders_1.buildOpenAiResponsesSystemContent)(prompt.system, cache);
|
|
363
|
+
if (systemContentResult.isFailure()) {
|
|
364
|
+
return (0, ts_utils_1.fail)(systemContentResult.message);
|
|
365
|
+
}
|
|
366
|
+
const input = (0, chatRequestBuilders_1.buildMessages)(systemContentResult.value, (0, chatRequestBuilders_1.buildOpenAiResponsesUserContent)(prompt), {
|
|
363
367
|
head: messagesBefore,
|
|
364
368
|
rawTail: continuationMessages
|
|
365
369
|
});
|
|
366
370
|
const effort = (_a = resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.openAiEffort) !== null && _a !== void 0 ? _a : resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.xaiEffort;
|
|
367
371
|
const supportsReasoning = config.model !== 'grok-4';
|
|
368
|
-
const body = {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
};
|
|
372
|
+
const body = Object.assign({ model: config.model, input, stream: true }, ((cache === null || cache === void 0 ? void 0 : cache.cacheKey) !== undefined
|
|
373
|
+
? // eslint-disable-next-line @typescript-eslint/naming-convention -- wire field name
|
|
374
|
+
{ prompt_cache_key: cache.cacheKey }
|
|
375
|
+
: {}));
|
|
373
376
|
// `tools` is omitted entirely when none are requested — a Responses-only model routed
|
|
374
377
|
// here for tier/model reasons (not tools) must not send an empty tools array.
|
|
375
378
|
if (tools.length > 0) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openaiResponses.js","sourceRoot":"","sources":["../../../../src/packlets/ai-assist/streamingAdapters/openaiResponses.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * Streaming adapter for the OpenAI / xAI Responses API. This is the format\n * used when server-side tools (e.g. web_search) are requested — Chat\n * Completions doesn't support tool progress events, but the Responses API\n * does.\n *\n * Client-defined tools (`function_call` type) are accumulated per call ID\n * and emitted as `client-tool-call-done` events when complete.\n *\n * @packageDocumentation\n */\n\nimport { type Logging, Result, succeed, type Validator, Validators } from '@fgv/ts-utils';\nimport { type JsonObject } from '@fgv/ts-json-base';\n\nimport { buildMessages, buildOpenAiResponsesUserContent } from '../chatRequestBuilders';\nimport { bearerAuthHeader } from '../endpoint';\nimport { AiPrompt, type AiToolConfig, type IAiStreamEvent, type IChatMessage } from '../model';\nimport { parseSseEventJson, readSseEvents } from '../sseParser';\nimport { toResponsesApiTools } from '../toolFormats';\nimport { type IResolvedThinkingConfig } from '../thinkingOptionsResolver';\nimport { normalizeOpenAiResponsesUsage } from '../usageNormalization';\nimport {\n IStreamApiConfig,\n UNRECOGNIZED_EVENT_WARN_TAG,\n formatUnrecognizedEventPayloadPreview,\n jsonObjectOrNullValidator,\n openSseConnection,\n validateEventPayload\n} from './common';\n\n// ============================================================================\n// Accumulated call state (internal — used by C3 continuation builder)\n// ============================================================================\n\n/**\n * Accumulated state for a single function_call in the OpenAI Responses API stream.\n * @internal\n */\nexport interface IAccumulatedFunctionCall {\n readonly id: string;\n readonly name: string;\n argsBuffer: string;\n}\n\n// ============================================================================\n// Event payload shapes\n// ============================================================================\n\n/**\n * Payload of a `response.output_text.delta` SSE event.\n * @internal\n */\ninterface IResponsesDeltaPayload {\n readonly delta: string;\n}\n\n/**\n * Payload of a `response.output_item.added` SSE event.\n * @internal\n */\ninterface IResponsesOutputItemAddedPayload {\n readonly item: {\n readonly type?: string;\n readonly id?: string;\n readonly name?: string;\n readonly call_id?: string;\n };\n}\n\n/**\n * Payload of a `response.function_call_arguments.delta` SSE event.\n *\n * The live OpenAI / xAI Responses API emits these events keyed by `item_id` (the\n * `fc_*` output-item id), NOT by `call_id` (the `call_*` id used in continuation\n * input items). The adapter correlates `item_id` → `call_id` via the\n * `response.output_item.added` event that introduced the function_call item.\n *\n * @internal\n */\ninterface IResponsesFunctionCallArgsDeltaPayload {\n readonly item_id: string;\n readonly delta: string;\n}\n\n/**\n * Payload of a `response.function_call_arguments.done` SSE event.\n *\n * Keyed by `item_id` like the delta event — see {@link IResponsesFunctionCallArgsDeltaPayload}.\n *\n * @internal\n */\ninterface IResponsesFunctionCallArgsDonePayload {\n readonly item_id: string;\n readonly arguments: string;\n}\n\n/**\n * Payload of a `response.completed` SSE event.\n * @internal\n */\ninterface IResponsesCompletedPayload {\n readonly response: {\n readonly status?: string;\n readonly incomplete_details?: { readonly reason?: string };\n /**\n * `null` when the provider has no usage block for this response (not just\n * absent) — must validate, or the whole `response.completed` payload is\n * rejected and `status`/`incomplete_details` are lost along with it.\n */\n // eslint-disable-next-line @rushstack/no-new-null\n readonly usage?: JsonObject | null;\n };\n}\n\n/**\n * Payload of a `response.failed` or `error` SSE event.\n * @internal\n */\ninterface IResponsesErrorPayload {\n readonly error?: { readonly message?: string };\n readonly message?: string;\n}\n\nconst responsesDeltaPayload: Validator<IResponsesDeltaPayload> = Validators.object<IResponsesDeltaPayload>({\n delta: Validators.string\n});\n\nconst responsesOutputItemInner: Validator<{\n type?: string;\n id?: string;\n name?: string;\n call_id?: string;\n}> = Validators.object<{ type?: string; id?: string; name?: string; call_id?: string }>(\n {\n type: Validators.string.optional(),\n id: Validators.string.optional(),\n name: Validators.string.optional(),\n call_id: Validators.string.optional()\n },\n { options: { optionalFields: ['type', 'id', 'name', 'call_id'] } }\n);\n\nconst responsesOutputItemAddedPayload: Validator<IResponsesOutputItemAddedPayload> =\n Validators.object<IResponsesOutputItemAddedPayload>({ item: responsesOutputItemInner });\n\nconst responsesFunctionCallArgsDeltaPayload: Validator<IResponsesFunctionCallArgsDeltaPayload> =\n Validators.object<IResponsesFunctionCallArgsDeltaPayload>({\n item_id: Validators.string,\n delta: Validators.string\n });\n\nconst responsesFunctionCallArgsDonePayload: Validator<IResponsesFunctionCallArgsDonePayload> =\n Validators.object<IResponsesFunctionCallArgsDonePayload>({\n item_id: Validators.string,\n arguments: Validators.string\n });\n\nconst responsesIncompleteDetails: Validator<{ reason?: string }> = Validators.object<{ reason?: string }>(\n { reason: Validators.string.optional() },\n { options: { optionalFields: ['reason'] } }\n);\n\nconst responsesCompletedPayload: Validator<IResponsesCompletedPayload> =\n Validators.object<IResponsesCompletedPayload>({\n response: Validators.object<{\n status?: string;\n incomplete_details?: { reason?: string };\n // eslint-disable-next-line @rushstack/no-new-null\n usage?: JsonObject | null;\n }>({\n status: Validators.string.optional(),\n incomplete_details: responsesIncompleteDetails.optional(),\n usage: jsonObjectOrNullValidator.optional()\n })\n });\n\nconst responsesErrorInner: Validator<{ message?: string }> = Validators.object<{ message?: string }>(\n { message: Validators.string.optional() },\n { options: { optionalFields: ['message'] } }\n);\n\nconst responsesErrorPayload: Validator<IResponsesErrorPayload> = Validators.object<IResponsesErrorPayload>(\n {\n error: responsesErrorInner.optional(),\n message: Validators.string.optional()\n },\n { options: { optionalFields: ['error', 'message'] } }\n);\n\n// ============================================================================\n// Stream translator\n// ============================================================================\n\n/**\n * Recognized OpenAI / xAI Responses API SSE event names. An event in this set is either\n * handled below or intentionally ignored (lifecycle / reasoning content discarded by design /\n * server-tool channels not yet surfaced). Any event whose name is NOT in this set surfaces\n * a one-time `logger.warn` per stream — making provider drift (new event types from a model\n * update) visible empirically instead of silently no-op'd.\n *\n * Source: official `openai-node` SDK source (`src/resources/responses/responses.ts`)\n * event-type literal-string sweep, plus the xAI Responses superset. Add to this set when a\n * new event type is observed and confirmed safe to ignore.\n *\n * @internal\n */\nconst RECOGNIZED_OPENAI_RESPONSES_EVENTS: ReadonlySet<string> = new Set<string>([\n // ---- handled by the translator ----\n 'response.output_text.delta',\n 'response.web_search_call.in_progress',\n 'response.web_search_call.completed',\n 'response.output_item.added',\n 'response.function_call_arguments.delta',\n 'response.function_call_arguments.done',\n 'response.completed',\n 'response.failed',\n 'error',\n // ---- lifecycle events: intentionally silent ----\n 'response.created',\n 'response.in_progress',\n 'response.queued',\n 'response.incomplete',\n // ---- text content lifecycle: handled implicitly via delta accumulation ----\n 'response.output_text.done',\n 'response.output_text.annotation.added',\n 'response.content_part.added',\n 'response.content_part.done',\n 'response.output_item.done',\n // ---- reasoning content: discarded by design (see ai-assist-thinking-events follow-on) ----\n 'response.reasoning_summary_part.added',\n 'response.reasoning_summary_part.done',\n 'response.reasoning_summary_text.delta',\n 'response.reasoning_summary_text.done',\n 'response.reasoning_text.delta',\n 'response.reasoning_text.done',\n // ---- refusals: caller currently sees these via the model's final text ----\n 'response.refusal.delta',\n 'response.refusal.done',\n // ---- server-tool channels not surfaced as tool-event yet ----\n 'response.web_search_call.searching',\n 'response.file_search_call.in_progress',\n 'response.file_search_call.searching',\n 'response.file_search_call.completed',\n 'response.code_interpreter_call.in_progress',\n 'response.code_interpreter_call.interpreting',\n 'response.code_interpreter_call.completed',\n 'response.code_interpreter_call_code.delta',\n 'response.code_interpreter_call_code.done',\n 'response.image_generation_call.in_progress',\n 'response.image_generation_call.generating',\n 'response.image_generation_call.completed',\n 'response.image_generation_call.partial_image',\n 'response.mcp_call.in_progress',\n 'response.mcp_call.completed',\n 'response.mcp_call.failed',\n 'response.mcp_call_arguments.delta',\n 'response.mcp_call_arguments.done',\n 'response.mcp_list_tools.in_progress',\n 'response.mcp_list_tools.completed',\n 'response.mcp_list_tools.failed',\n 'response.custom_tool_call_input.delta',\n 'response.custom_tool_call_input.done',\n // ---- audio (not used in chat/tool flows here) ----\n 'response.audio.delta',\n 'response.audio.done',\n 'response.audio.transcript.delta',\n 'response.audio.transcript.done'\n]);\n\n/**\n * Translates an OpenAI Responses API SSE stream into unified events.\n *\n * Maintains a per-call-ID accumulation map for client-defined function calls.\n * The map is exposed via the passed-in `functionCallMap` parameter for the\n * C3 continuation builder to read after the stream completes.\n *\n * Unrecognized event names are reported once per stream via `logger?.warn` —\n * see {@link RECOGNIZED_OPENAI_RESPONSES_EVENTS}.\n *\n * @internal\n */\nasync function* translateOpenAiResponsesStream(\n response: Response,\n functionCallMap: Map<string, IAccumulatedFunctionCall>,\n reportsUsage: boolean,\n logger?: Logging.ILogger\n): AsyncGenerator<IAiStreamEvent> {\n let fullText = '';\n let truncated = false;\n let completed = false;\n let incompleteReason: string | undefined;\n let usageRaw: JsonObject | undefined;\n // OpenAI / xAI Responses API emits function_call_arguments.{delta,done} events keyed by\n // `item_id` (the fc_* output-item id). The harness and continuation builder key by\n // `call_id` (the call_* id). This map correlates the two — populated when the\n // function_call item is announced in `response.output_item.added`.\n const itemIdToCallId = new Map<string, string>();\n // Track unrecognized event names we have already warned about, so a hot stream of\n // an unknown event type produces exactly one log line per name per stream.\n const warnedEvents = new Set<string>();\n\n try {\n /* c8 ignore next - body is non-null at this point per openSseConnection */\n if (!response.body) return;\n for await (const message of readSseEvents(response.body)) {\n const eventName = message.event;\n if (eventName === 'response.output_text.delta') {\n const payload = validateEventPayload(parseSseEventJson(message.data), responsesDeltaPayload);\n /* c8 ignore next 1 - defensive: payload?.delta null branch unreachable after validation */\n const delta = payload?.delta;\n if (typeof delta === 'string' && delta.length > 0) {\n fullText += delta;\n yield { type: 'text-delta', delta };\n }\n } else if (eventName === 'response.web_search_call.in_progress') {\n yield { type: 'tool-event', toolType: 'web_search', phase: 'started' };\n } else if (eventName === 'response.web_search_call.completed') {\n yield { type: 'tool-event', toolType: 'web_search', phase: 'completed' };\n } else if (eventName === 'response.output_item.added') {\n const payload = validateEventPayload(\n parseSseEventJson(message.data),\n responsesOutputItemAddedPayload\n );\n /* c8 ignore next 1 - defensive: payload null branch unreachable after validation */\n const item = payload?.item;\n /* c8 ignore next 1 - defensive: falsy call_id/name branches are protocol violations */\n if (item?.type === 'function_call' && item.call_id && item.name) {\n functionCallMap.set(item.call_id, { id: item.call_id, name: item.name, argsBuffer: '' });\n // Reasoning models (and the standard flow) reference this function_call in subsequent\n // arguments.{delta,done} events by `item_id`, never by `call_id`. Record the mapping\n // so the arg-accumulation handlers can resolve the call.\n if (item.id) {\n itemIdToCallId.set(item.id, item.call_id);\n }\n yield { type: 'client-tool-call-start', toolName: item.name, callId: item.call_id };\n }\n } else if (eventName === 'response.function_call_arguments.delta') {\n const payload = validateEventPayload(\n parseSseEventJson(message.data),\n responsesFunctionCallArgsDeltaPayload\n );\n /* c8 ignore next 1 - defensive: payload null branch unreachable after validation */\n const itemId = payload?.item_id;\n if (itemId !== undefined) {\n const callId = itemIdToCallId.get(itemId);\n /* c8 ignore next 1 - defensive: orphan delta (no preceding output_item.added) is a protocol violation */\n const call = callId !== undefined ? functionCallMap.get(callId) : undefined;\n /* c8 ignore next 1 - defensive: call always present and delta always string in valid stream */\n if (call && typeof payload?.delta === 'string') {\n call.argsBuffer += payload.delta;\n }\n }\n } else if (eventName === 'response.function_call_arguments.done') {\n const payload = validateEventPayload(\n parseSseEventJson(message.data),\n responsesFunctionCallArgsDonePayload\n );\n /* c8 ignore next 1 - defensive: payload null branch unreachable after validation */\n const itemId = payload?.item_id;\n if (itemId !== undefined) {\n const callId = itemIdToCallId.get(itemId);\n /* c8 ignore next 1 - defensive: orphan done (no preceding output_item.added) is a protocol violation */\n const call = callId !== undefined ? functionCallMap.get(callId) : undefined;\n if (call && callId !== undefined) {\n /* c8 ignore next 1 - defensive: payload?.arguments null branch unreachable after validation */\n const canonicalArgs = payload?.arguments ?? '{}';\n // Sync the accumulation entry with the canonical arguments from the .done event.\n // Delta events may carry partial/empty payloads; the .done event carries the\n // authoritative final arguments string used by the continuation builder.\n call.argsBuffer = canonicalArgs;\n let args: JsonObject;\n try {\n const parsed = JSON.parse(canonicalArgs) as unknown;\n /* c8 ignore start - defensive: non-object/malformed parse defaults to empty object */\n args =\n parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as JsonObject)\n : {};\n } catch {\n args = {};\n }\n /* c8 ignore stop */\n yield { type: 'client-tool-call-done', toolName: call.name, callId, args };\n }\n }\n } else if (eventName === 'response.completed') {\n const payload = validateEventPayload(parseSseEventJson(message.data), responsesCompletedPayload);\n /* c8 ignore next 1 - defensive: payload null branch unreachable after validation */\n if (payload) {\n truncated = payload.response.status === 'incomplete';\n // Per IAiStreamDone.incompleteReason's contract, the reason is meaningful only\n // when the response was truncated. Move both fields together on every completed\n // event so a stray incomplete_details on a non-incomplete payload never leaks\n // through, and a later (defensive) completed event can't leave a stale reason.\n incompleteReason = truncated ? payload.response.incomplete_details?.reason : undefined;\n // Gated the same way as the non-streaming Responses path (see\n // supportsCacheUsageReporting) — this adapter is shared by descriptors with no\n // confirmed cache-usage reporting.\n usageRaw = reportsUsage ? payload.response.usage ?? undefined : undefined;\n }\n completed = true;\n /* c8 ignore next 1 - defensive: eventName === 'error' alternative not exercised in tests */\n } else if (eventName === 'response.failed' || eventName === 'error') {\n const payload = validateEventPayload(parseSseEventJson(message.data), responsesErrorPayload);\n /* c8 ignore next 1 - defensive: payload?.error and payload?.message null branches unreachable */\n const errMsg = payload?.error?.message ?? payload?.message ?? 'Responses API stream failed';\n yield { type: 'error', message: errMsg };\n return;\n } else if (\n typeof eventName === 'string' &&\n !RECOGNIZED_OPENAI_RESPONSES_EVENTS.has(eventName) &&\n !warnedEvents.has(eventName)\n ) {\n // Empirical drift instrument: an unrecognized SSE event from the Responses API\n // surfaces as a one-time warning per stream. If a provider adds a new event type\n // (or changes an existing event's name), the warning fires the first time the\n // event arrives. Update RECOGNIZED_OPENAI_RESPONSES_EVENTS once the new event is\n // either handled or confirmed safe to ignore.\n warnedEvents.add(eventName);\n logger?.warn(\n `${UNRECOGNIZED_EVENT_WARN_TAG} OpenAI Responses adapter: unrecognized SSE event ` +\n `'${eventName}'. ` +\n `payload preview: ${formatUnrecognizedEventPayloadPreview(message.data)}. ` +\n `This may indicate provider drift — if the new event carries data the adapter ` +\n `should surface, add a handler; otherwise add the name to ` +\n `RECOGNIZED_OPENAI_RESPONSES_EVENTS.`\n );\n }\n }\n } catch (err: unknown) /* c8 ignore start - defensive: stream errors are always Error instances */ {\n yield { type: 'error', message: err instanceof Error ? err.message : String(err) };\n return;\n } /* c8 ignore stop */\n\n if (completed) {\n const usage = normalizeOpenAiResponsesUsage(usageRaw);\n yield { type: 'done', truncated, fullText, incompleteReason, ...(usage !== undefined ? { usage } : {}) };\n } else {\n yield { type: 'error', message: 'Responses API stream ended without a completed event' };\n }\n}\n\n// ============================================================================\n// Per-format request caller\n// ============================================================================\n\n/**\n * Issues a streaming Responses API request (with tools) and returns the\n * unified-event iterable on success.\n *\n * @internal\n */\nexport async function callOpenAiResponsesStream(\n config: IStreamApiConfig,\n prompt: AiPrompt,\n tools: ReadonlyArray<AiToolConfig> = [],\n messagesBefore: ReadonlyArray<IChatMessage> | undefined,\n temperature: number | undefined,\n logger?: Logging.ILogger,\n signal?: AbortSignal,\n resolvedThinking?: IResolvedThinkingConfig,\n functionCallMap?: Map<string, IAccumulatedFunctionCall>,\n continuationMessages?: ReadonlyArray<JsonObject>,\n maxTokens?: number,\n reportsUsage: boolean = false\n): Promise<Result<AsyncIterable<IAiStreamEvent>>> {\n const url = `${config.baseUrl}/responses`;\n const input = buildMessages(prompt.system, buildOpenAiResponsesUserContent(prompt), {\n head: messagesBefore,\n rawTail: continuationMessages\n });\n const effort = resolvedThinking?.openAiEffort ?? resolvedThinking?.xaiEffort;\n const supportsReasoning = config.model !== 'grok-4';\n const body: Record<string, unknown> = {\n model: config.model,\n input,\n stream: true\n };\n // `tools` is omitted entirely when none are requested — a Responses-only model routed\n // here for tier/model reasons (not tools) must not send an empty tools array.\n if (tools.length > 0) {\n body.tools = toResponsesApiTools(tools);\n }\n if (effort !== undefined && supportsReasoning) {\n body.reasoning = { effort };\n }\n // Temperature is sent only when the caller explicitly provided one (see callOpenAiChatStream).\n if (effort === undefined || effort === 'none') {\n if (temperature !== undefined) {\n body.temperature = temperature;\n }\n }\n // Shared by OpenAI and xAI — both route through the Responses API with the same field name.\n if (maxTokens !== undefined) {\n body.max_output_tokens = maxTokens;\n }\n if (resolvedThinking?.otherParams !== undefined) {\n Object.assign(body, resolvedThinking.otherParams);\n }\n const headers: Record<string, string> = bearerAuthHeader(config.apiKey);\n /* c8 ignore next 3 - optional logger */\n logger?.info(\n `OpenAI Responses streaming: model=${config.model}, tools=${tools.map((t) => t.type).join(',')}`\n );\n const callMap = functionCallMap ?? new Map<string, IAccumulatedFunctionCall>();\n const conn = await openSseConnection(url, headers, body, logger, signal);\n return conn.onSuccess((response) =>\n succeed(translateOpenAiResponsesStream(response, callMap, reportsUsage, logger))\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"openaiResponses.js","sourceRoot":"","sources":["../../../../src/packlets/ai-assist/streamingAdapters/openaiResponses.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * Streaming adapter for the OpenAI / xAI Responses API. This is the format\n * used when server-side tools (e.g. web_search) are requested — Chat\n * Completions doesn't support tool progress events, but the Responses API\n * does.\n *\n * Client-defined tools (`function_call` type) are accumulated per call ID\n * and emitted as `client-tool-call-done` events when complete.\n *\n * @packageDocumentation\n */\n\nimport { fail, type Logging, Result, succeed, type Validator, Validators } from '@fgv/ts-utils';\nimport { type JsonObject } from '@fgv/ts-json-base';\n\nimport {\n buildMessages,\n buildOpenAiResponsesSystemContent,\n buildOpenAiResponsesUserContent\n} from '../chatRequestBuilders';\nimport { type IAiCacheRequest } from '../cacheRequest';\nimport { bearerAuthHeader } from '../endpoint';\nimport { AiPrompt, type AiToolConfig, type IAiStreamEvent, type IChatMessage } from '../model';\nimport { parseSseEventJson, readSseEvents } from '../sseParser';\nimport { toResponsesApiTools } from '../toolFormats';\nimport { type IResolvedThinkingConfig } from '../thinkingOptionsResolver';\nimport { normalizeOpenAiResponsesUsage } from '../usageNormalization';\nimport {\n IStreamApiConfig,\n UNRECOGNIZED_EVENT_WARN_TAG,\n formatUnrecognizedEventPayloadPreview,\n jsonObjectOrNullValidator,\n openSseConnection,\n validateEventPayload\n} from './common';\n\n// ============================================================================\n// Accumulated call state (internal — used by C3 continuation builder)\n// ============================================================================\n\n/**\n * Accumulated state for a single function_call in the OpenAI Responses API stream.\n * @internal\n */\nexport interface IAccumulatedFunctionCall {\n readonly id: string;\n readonly name: string;\n argsBuffer: string;\n}\n\n// ============================================================================\n// Event payload shapes\n// ============================================================================\n\n/**\n * Payload of a `response.output_text.delta` SSE event.\n * @internal\n */\ninterface IResponsesDeltaPayload {\n readonly delta: string;\n}\n\n/**\n * Payload of a `response.output_item.added` SSE event.\n * @internal\n */\ninterface IResponsesOutputItemAddedPayload {\n readonly item: {\n readonly type?: string;\n readonly id?: string;\n readonly name?: string;\n readonly call_id?: string;\n };\n}\n\n/**\n * Payload of a `response.function_call_arguments.delta` SSE event.\n *\n * The live OpenAI / xAI Responses API emits these events keyed by `item_id` (the\n * `fc_*` output-item id), NOT by `call_id` (the `call_*` id used in continuation\n * input items). The adapter correlates `item_id` → `call_id` via the\n * `response.output_item.added` event that introduced the function_call item.\n *\n * @internal\n */\ninterface IResponsesFunctionCallArgsDeltaPayload {\n readonly item_id: string;\n readonly delta: string;\n}\n\n/**\n * Payload of a `response.function_call_arguments.done` SSE event.\n *\n * Keyed by `item_id` like the delta event — see {@link IResponsesFunctionCallArgsDeltaPayload}.\n *\n * @internal\n */\ninterface IResponsesFunctionCallArgsDonePayload {\n readonly item_id: string;\n readonly arguments: string;\n}\n\n/**\n * Payload of a `response.completed` SSE event.\n * @internal\n */\ninterface IResponsesCompletedPayload {\n readonly response: {\n readonly status?: string;\n readonly incomplete_details?: { readonly reason?: string };\n /**\n * `null` when the provider has no usage block for this response (not just\n * absent) — must validate, or the whole `response.completed` payload is\n * rejected and `status`/`incomplete_details` are lost along with it.\n */\n // eslint-disable-next-line @rushstack/no-new-null\n readonly usage?: JsonObject | null;\n };\n}\n\n/**\n * Payload of a `response.failed` or `error` SSE event.\n * @internal\n */\ninterface IResponsesErrorPayload {\n readonly error?: { readonly message?: string };\n readonly message?: string;\n}\n\nconst responsesDeltaPayload: Validator<IResponsesDeltaPayload> = Validators.object<IResponsesDeltaPayload>({\n delta: Validators.string\n});\n\nconst responsesOutputItemInner: Validator<{\n type?: string;\n id?: string;\n name?: string;\n call_id?: string;\n}> = Validators.object<{ type?: string; id?: string; name?: string; call_id?: string }>(\n {\n type: Validators.string.optional(),\n id: Validators.string.optional(),\n name: Validators.string.optional(),\n call_id: Validators.string.optional()\n },\n { options: { optionalFields: ['type', 'id', 'name', 'call_id'] } }\n);\n\nconst responsesOutputItemAddedPayload: Validator<IResponsesOutputItemAddedPayload> =\n Validators.object<IResponsesOutputItemAddedPayload>({ item: responsesOutputItemInner });\n\nconst responsesFunctionCallArgsDeltaPayload: Validator<IResponsesFunctionCallArgsDeltaPayload> =\n Validators.object<IResponsesFunctionCallArgsDeltaPayload>({\n item_id: Validators.string,\n delta: Validators.string\n });\n\nconst responsesFunctionCallArgsDonePayload: Validator<IResponsesFunctionCallArgsDonePayload> =\n Validators.object<IResponsesFunctionCallArgsDonePayload>({\n item_id: Validators.string,\n arguments: Validators.string\n });\n\nconst responsesIncompleteDetails: Validator<{ reason?: string }> = Validators.object<{ reason?: string }>(\n { reason: Validators.string.optional() },\n { options: { optionalFields: ['reason'] } }\n);\n\nconst responsesCompletedPayload: Validator<IResponsesCompletedPayload> =\n Validators.object<IResponsesCompletedPayload>({\n response: Validators.object<{\n status?: string;\n incomplete_details?: { reason?: string };\n // eslint-disable-next-line @rushstack/no-new-null\n usage?: JsonObject | null;\n }>({\n status: Validators.string.optional(),\n incomplete_details: responsesIncompleteDetails.optional(),\n usage: jsonObjectOrNullValidator.optional()\n })\n });\n\nconst responsesErrorInner: Validator<{ message?: string }> = Validators.object<{ message?: string }>(\n { message: Validators.string.optional() },\n { options: { optionalFields: ['message'] } }\n);\n\nconst responsesErrorPayload: Validator<IResponsesErrorPayload> = Validators.object<IResponsesErrorPayload>(\n {\n error: responsesErrorInner.optional(),\n message: Validators.string.optional()\n },\n { options: { optionalFields: ['error', 'message'] } }\n);\n\n// ============================================================================\n// Stream translator\n// ============================================================================\n\n/**\n * Recognized OpenAI / xAI Responses API SSE event names. An event in this set is either\n * handled below or intentionally ignored (lifecycle / reasoning content discarded by design /\n * server-tool channels not yet surfaced). Any event whose name is NOT in this set surfaces\n * a one-time `logger.warn` per stream — making provider drift (new event types from a model\n * update) visible empirically instead of silently no-op'd.\n *\n * Source: official `openai-node` SDK source (`src/resources/responses/responses.ts`)\n * event-type literal-string sweep, plus the xAI Responses superset. Add to this set when a\n * new event type is observed and confirmed safe to ignore.\n *\n * @internal\n */\nconst RECOGNIZED_OPENAI_RESPONSES_EVENTS: ReadonlySet<string> = new Set<string>([\n // ---- handled by the translator ----\n 'response.output_text.delta',\n 'response.web_search_call.in_progress',\n 'response.web_search_call.completed',\n 'response.output_item.added',\n 'response.function_call_arguments.delta',\n 'response.function_call_arguments.done',\n 'response.completed',\n 'response.failed',\n 'error',\n // ---- lifecycle events: intentionally silent ----\n 'response.created',\n 'response.in_progress',\n 'response.queued',\n 'response.incomplete',\n // ---- text content lifecycle: handled implicitly via delta accumulation ----\n 'response.output_text.done',\n 'response.output_text.annotation.added',\n 'response.content_part.added',\n 'response.content_part.done',\n 'response.output_item.done',\n // ---- reasoning content: discarded by design (see ai-assist-thinking-events follow-on) ----\n 'response.reasoning_summary_part.added',\n 'response.reasoning_summary_part.done',\n 'response.reasoning_summary_text.delta',\n 'response.reasoning_summary_text.done',\n 'response.reasoning_text.delta',\n 'response.reasoning_text.done',\n // ---- refusals: caller currently sees these via the model's final text ----\n 'response.refusal.delta',\n 'response.refusal.done',\n // ---- server-tool channels not surfaced as tool-event yet ----\n 'response.web_search_call.searching',\n 'response.file_search_call.in_progress',\n 'response.file_search_call.searching',\n 'response.file_search_call.completed',\n 'response.code_interpreter_call.in_progress',\n 'response.code_interpreter_call.interpreting',\n 'response.code_interpreter_call.completed',\n 'response.code_interpreter_call_code.delta',\n 'response.code_interpreter_call_code.done',\n 'response.image_generation_call.in_progress',\n 'response.image_generation_call.generating',\n 'response.image_generation_call.completed',\n 'response.image_generation_call.partial_image',\n 'response.mcp_call.in_progress',\n 'response.mcp_call.completed',\n 'response.mcp_call.failed',\n 'response.mcp_call_arguments.delta',\n 'response.mcp_call_arguments.done',\n 'response.mcp_list_tools.in_progress',\n 'response.mcp_list_tools.completed',\n 'response.mcp_list_tools.failed',\n 'response.custom_tool_call_input.delta',\n 'response.custom_tool_call_input.done',\n // ---- audio (not used in chat/tool flows here) ----\n 'response.audio.delta',\n 'response.audio.done',\n 'response.audio.transcript.delta',\n 'response.audio.transcript.done'\n]);\n\n/**\n * Translates an OpenAI Responses API SSE stream into unified events.\n *\n * Maintains a per-call-ID accumulation map for client-defined function calls.\n * The map is exposed via the passed-in `functionCallMap` parameter for the\n * C3 continuation builder to read after the stream completes.\n *\n * Unrecognized event names are reported once per stream via `logger?.warn` —\n * see {@link RECOGNIZED_OPENAI_RESPONSES_EVENTS}.\n *\n * @internal\n */\nasync function* translateOpenAiResponsesStream(\n response: Response,\n functionCallMap: Map<string, IAccumulatedFunctionCall>,\n reportsUsage: boolean,\n logger?: Logging.ILogger\n): AsyncGenerator<IAiStreamEvent> {\n let fullText = '';\n let truncated = false;\n let completed = false;\n let incompleteReason: string | undefined;\n let usageRaw: JsonObject | undefined;\n // OpenAI / xAI Responses API emits function_call_arguments.{delta,done} events keyed by\n // `item_id` (the fc_* output-item id). The harness and continuation builder key by\n // `call_id` (the call_* id). This map correlates the two — populated when the\n // function_call item is announced in `response.output_item.added`.\n const itemIdToCallId = new Map<string, string>();\n // Track unrecognized event names we have already warned about, so a hot stream of\n // an unknown event type produces exactly one log line per name per stream.\n const warnedEvents = new Set<string>();\n\n try {\n /* c8 ignore next - body is non-null at this point per openSseConnection */\n if (!response.body) return;\n for await (const message of readSseEvents(response.body)) {\n const eventName = message.event;\n if (eventName === 'response.output_text.delta') {\n const payload = validateEventPayload(parseSseEventJson(message.data), responsesDeltaPayload);\n /* c8 ignore next 1 - defensive: payload?.delta null branch unreachable after validation */\n const delta = payload?.delta;\n if (typeof delta === 'string' && delta.length > 0) {\n fullText += delta;\n yield { type: 'text-delta', delta };\n }\n } else if (eventName === 'response.web_search_call.in_progress') {\n yield { type: 'tool-event', toolType: 'web_search', phase: 'started' };\n } else if (eventName === 'response.web_search_call.completed') {\n yield { type: 'tool-event', toolType: 'web_search', phase: 'completed' };\n } else if (eventName === 'response.output_item.added') {\n const payload = validateEventPayload(\n parseSseEventJson(message.data),\n responsesOutputItemAddedPayload\n );\n /* c8 ignore next 1 - defensive: payload null branch unreachable after validation */\n const item = payload?.item;\n /* c8 ignore next 1 - defensive: falsy call_id/name branches are protocol violations */\n if (item?.type === 'function_call' && item.call_id && item.name) {\n functionCallMap.set(item.call_id, { id: item.call_id, name: item.name, argsBuffer: '' });\n // Reasoning models (and the standard flow) reference this function_call in subsequent\n // arguments.{delta,done} events by `item_id`, never by `call_id`. Record the mapping\n // so the arg-accumulation handlers can resolve the call.\n if (item.id) {\n itemIdToCallId.set(item.id, item.call_id);\n }\n yield { type: 'client-tool-call-start', toolName: item.name, callId: item.call_id };\n }\n } else if (eventName === 'response.function_call_arguments.delta') {\n const payload = validateEventPayload(\n parseSseEventJson(message.data),\n responsesFunctionCallArgsDeltaPayload\n );\n /* c8 ignore next 1 - defensive: payload null branch unreachable after validation */\n const itemId = payload?.item_id;\n if (itemId !== undefined) {\n const callId = itemIdToCallId.get(itemId);\n /* c8 ignore next 1 - defensive: orphan delta (no preceding output_item.added) is a protocol violation */\n const call = callId !== undefined ? functionCallMap.get(callId) : undefined;\n /* c8 ignore next 1 - defensive: call always present and delta always string in valid stream */\n if (call && typeof payload?.delta === 'string') {\n call.argsBuffer += payload.delta;\n }\n }\n } else if (eventName === 'response.function_call_arguments.done') {\n const payload = validateEventPayload(\n parseSseEventJson(message.data),\n responsesFunctionCallArgsDonePayload\n );\n /* c8 ignore next 1 - defensive: payload null branch unreachable after validation */\n const itemId = payload?.item_id;\n if (itemId !== undefined) {\n const callId = itemIdToCallId.get(itemId);\n /* c8 ignore next 1 - defensive: orphan done (no preceding output_item.added) is a protocol violation */\n const call = callId !== undefined ? functionCallMap.get(callId) : undefined;\n if (call && callId !== undefined) {\n /* c8 ignore next 1 - defensive: payload?.arguments null branch unreachable after validation */\n const canonicalArgs = payload?.arguments ?? '{}';\n // Sync the accumulation entry with the canonical arguments from the .done event.\n // Delta events may carry partial/empty payloads; the .done event carries the\n // authoritative final arguments string used by the continuation builder.\n call.argsBuffer = canonicalArgs;\n let args: JsonObject;\n try {\n const parsed = JSON.parse(canonicalArgs) as unknown;\n /* c8 ignore start - defensive: non-object/malformed parse defaults to empty object */\n args =\n parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as JsonObject)\n : {};\n } catch {\n args = {};\n }\n /* c8 ignore stop */\n yield { type: 'client-tool-call-done', toolName: call.name, callId, args };\n }\n }\n } else if (eventName === 'response.completed') {\n const payload = validateEventPayload(parseSseEventJson(message.data), responsesCompletedPayload);\n /* c8 ignore next 1 - defensive: payload null branch unreachable after validation */\n if (payload) {\n truncated = payload.response.status === 'incomplete';\n // Per IAiStreamDone.incompleteReason's contract, the reason is meaningful only\n // when the response was truncated. Move both fields together on every completed\n // event so a stray incomplete_details on a non-incomplete payload never leaks\n // through, and a later (defensive) completed event can't leave a stale reason.\n incompleteReason = truncated ? payload.response.incomplete_details?.reason : undefined;\n // Gated the same way as the non-streaming Responses path (see\n // supportsCacheUsageReporting) — this adapter is shared by descriptors with no\n // confirmed cache-usage reporting.\n usageRaw = reportsUsage ? payload.response.usage ?? undefined : undefined;\n }\n completed = true;\n /* c8 ignore next 1 - defensive: eventName === 'error' alternative not exercised in tests */\n } else if (eventName === 'response.failed' || eventName === 'error') {\n const payload = validateEventPayload(parseSseEventJson(message.data), responsesErrorPayload);\n /* c8 ignore next 1 - defensive: payload?.error and payload?.message null branches unreachable */\n const errMsg = payload?.error?.message ?? payload?.message ?? 'Responses API stream failed';\n yield { type: 'error', message: errMsg };\n return;\n } else if (\n typeof eventName === 'string' &&\n !RECOGNIZED_OPENAI_RESPONSES_EVENTS.has(eventName) &&\n !warnedEvents.has(eventName)\n ) {\n // Empirical drift instrument: an unrecognized SSE event from the Responses API\n // surfaces as a one-time warning per stream. If a provider adds a new event type\n // (or changes an existing event's name), the warning fires the first time the\n // event arrives. Update RECOGNIZED_OPENAI_RESPONSES_EVENTS once the new event is\n // either handled or confirmed safe to ignore.\n warnedEvents.add(eventName);\n logger?.warn(\n `${UNRECOGNIZED_EVENT_WARN_TAG} OpenAI Responses adapter: unrecognized SSE event ` +\n `'${eventName}'. ` +\n `payload preview: ${formatUnrecognizedEventPayloadPreview(message.data)}. ` +\n `This may indicate provider drift — if the new event carries data the adapter ` +\n `should surface, add a handler; otherwise add the name to ` +\n `RECOGNIZED_OPENAI_RESPONSES_EVENTS.`\n );\n }\n }\n } catch (err: unknown) /* c8 ignore start - defensive: stream errors are always Error instances */ {\n yield { type: 'error', message: err instanceof Error ? err.message : String(err) };\n return;\n } /* c8 ignore stop */\n\n if (completed) {\n const usage = normalizeOpenAiResponsesUsage(usageRaw);\n yield { type: 'done', truncated, fullText, incompleteReason, ...(usage !== undefined ? { usage } : {}) };\n } else {\n yield { type: 'error', message: 'Responses API stream ended without a completed event' };\n }\n}\n\n// ============================================================================\n// Per-format request caller\n// ============================================================================\n\n/**\n * Issues a streaming Responses API request (with tools) and returns the\n * unified-event iterable on success.\n *\n * @internal\n */\nexport async function callOpenAiResponsesStream(\n config: IStreamApiConfig,\n prompt: AiPrompt,\n tools: ReadonlyArray<AiToolConfig> = [],\n messagesBefore: ReadonlyArray<IChatMessage> | undefined,\n temperature: number | undefined,\n logger?: Logging.ILogger,\n signal?: AbortSignal,\n resolvedThinking?: IResolvedThinkingConfig,\n functionCallMap?: Map<string, IAccumulatedFunctionCall>,\n continuationMessages?: ReadonlyArray<JsonObject>,\n maxTokens?: number,\n reportsUsage: boolean = false,\n cache?: IAiCacheRequest\n): Promise<Result<AsyncIterable<IAiStreamEvent>>> {\n const url = `${config.baseUrl}/responses`;\n const systemContentResult = buildOpenAiResponsesSystemContent(prompt.system, cache);\n if (systemContentResult.isFailure()) {\n return fail(systemContentResult.message);\n }\n const input = buildMessages(systemContentResult.value, buildOpenAiResponsesUserContent(prompt), {\n head: messagesBefore,\n rawTail: continuationMessages\n });\n const effort = resolvedThinking?.openAiEffort ?? resolvedThinking?.xaiEffort;\n const supportsReasoning = config.model !== 'grok-4';\n const body: Record<string, unknown> = {\n model: config.model,\n input,\n stream: true,\n // See the identical field and gating comment on callOpenAiResponsesCompletion's body in\n // completionClient.ts — same routing plumbing, confirmed on both Chat Completions and the\n // Responses API. The Responses API takes the routing key as a body field on every provider\n // that supports it (see supportsPromptCacheRouting), so there is no header variant here.\n ...(cache?.cacheKey !== undefined\n ? // eslint-disable-next-line @typescript-eslint/naming-convention -- wire field name\n { prompt_cache_key: cache.cacheKey }\n : {})\n };\n // `tools` is omitted entirely when none are requested — a Responses-only model routed\n // here for tier/model reasons (not tools) must not send an empty tools array.\n if (tools.length > 0) {\n body.tools = toResponsesApiTools(tools);\n }\n if (effort !== undefined && supportsReasoning) {\n body.reasoning = { effort };\n }\n // Temperature is sent only when the caller explicitly provided one (see callOpenAiChatStream).\n if (effort === undefined || effort === 'none') {\n if (temperature !== undefined) {\n body.temperature = temperature;\n }\n }\n // Shared by OpenAI and xAI — both route through the Responses API with the same field name.\n if (maxTokens !== undefined) {\n body.max_output_tokens = maxTokens;\n }\n if (resolvedThinking?.otherParams !== undefined) {\n Object.assign(body, resolvedThinking.otherParams);\n }\n const headers: Record<string, string> = bearerAuthHeader(config.apiKey);\n /* c8 ignore next 3 - optional logger */\n logger?.info(\n `OpenAI Responses streaming: model=${config.model}, tools=${tools.map((t) => t.type).join(',')}`\n );\n const callMap = functionCallMap ?? new Map<string, IAccumulatedFunctionCall>();\n const conn = await openSseConnection(url, headers, body, logger, signal);\n return conn.onSuccess((response) =>\n succeed(translateOpenAiResponsesStream(response, callMap, reportsUsage, logger))\n );\n}\n"]}
|
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
* directly: each `data:` line is a JSON-serialized {@link AiAssist.IAiStreamEvent},
|
|
5
5
|
* so this adapter only validates the event-type discriminator and forwards.
|
|
6
6
|
*
|
|
7
|
+
* **It carries the four completion-path events, not the whole union.**
|
|
8
|
+
* `text-delta`, `tool-event`, `done` and `error` pass through; the three
|
|
9
|
+
* `client-tool-*` dispatch events are not recognized and are dropped. That is a
|
|
10
|
+
* real limit rather than an oversight: `executeClientToolTurn` has no proxied
|
|
11
|
+
* entry point, so nothing produces those events on this path, and forwarding a
|
|
12
|
+
* vocabulary no caller can reach would be dead code. A proxy implementor reading
|
|
13
|
+
* this contract should not emit them.
|
|
14
|
+
*
|
|
7
15
|
* @packageDocumentation
|
|
8
16
|
*/
|
|
9
17
|
import { Result } from '@fgv/ts-utils';
|
|
@@ -20,9 +28,23 @@ import { IProviderCompletionStreamParams } from './common';
|
|
|
20
28
|
* - Response: `Content-Type: text/event-stream`; body is the unified
|
|
21
29
|
* {@link AiAssist.IAiStreamEvent} JSON-serialized one event per SSE `data:` line
|
|
22
30
|
* (no `event:` line needed since the type discriminator is in the JSON).
|
|
31
|
+
* **Only `text-delta`, `tool-event`, `done` and `error` are carried** — an event
|
|
32
|
+
* of any other type, including the three `client-tool-*` variants, is discarded
|
|
33
|
+
* without error. Client-tool turns are not proxiable; `executeClientToolTurn`
|
|
34
|
+
* has no proxied entry point.
|
|
23
35
|
* - Error response (when the proxy can't even start): JSON `{error: string}`
|
|
24
36
|
* with a non-2xx status, surfaced as `proxy: ${error}`.
|
|
25
37
|
*
|
|
38
|
+
* Two request parameters are handled before the body is composed, identically to
|
|
39
|
+
* `callProxiedCompletion`: `tier` is resolved here and sent as a concrete
|
|
40
|
+
* `modelOverride`, so a proxy needs no `tier` vocabulary; `endpoint` is refused,
|
|
41
|
+
* because a proxy cannot confirm it honored it and reaching the provider's default
|
|
42
|
+
* upstream instead would send the request somewhere the caller excluded. `cache`,
|
|
43
|
+
* by contrast, is forwarded as a plain `body.cache` field exactly as
|
|
44
|
+
* `callProxiedCompletion` forwards it — a proxy that ignores it degrades to the
|
|
45
|
+
* request it would have sent anyway, never a misdirected one, so there is no
|
|
46
|
+
* `endpoint`-style reason to refuse it.
|
|
47
|
+
*
|
|
26
48
|
* The proxy server is responsible for opening the upstream SSE connection,
|
|
27
49
|
* translating provider-native events to the unified vocabulary, and
|
|
28
50
|
* forwarding events as they arrive (no buffering). The library does not
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../../../src/packlets/ai-assist/streamingAdapters/proxy.ts"],"names":[],"mappings":"AAoBA
|
|
1
|
+
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../../../src/packlets/ai-assist/streamingAdapters/proxy.ts"],"names":[],"mappings":"AAoBA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAQ,MAAM,EAAuC,MAAM,eAAe,CAAC;AAGlF,OAAO,EAAE,KAAK,cAAc,EAAwC,MAAM,UAAU,CAAC;AAErF,OAAO,EAAE,+BAA+B,EAA2C,MAAM,UAAU,CAAC;AAmFpG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAsB,2BAA2B,CAC/C,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,+BAA+B,GACtC,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,cAAc,CAAC,CAAC,CAAC,CAmGhD"}
|
|
@@ -46,10 +46,19 @@ exports.callProxiedCompletionStream = callProxiedCompletionStream;
|
|
|
46
46
|
* directly: each `data:` line is a JSON-serialized {@link AiAssist.IAiStreamEvent},
|
|
47
47
|
* so this adapter only validates the event-type discriminator and forwards.
|
|
48
48
|
*
|
|
49
|
+
* **It carries the four completion-path events, not the whole union.**
|
|
50
|
+
* `text-delta`, `tool-event`, `done` and `error` pass through; the three
|
|
51
|
+
* `client-tool-*` dispatch events are not recognized and are dropped. That is a
|
|
52
|
+
* real limit rather than an oversight: `executeClientToolTurn` has no proxied
|
|
53
|
+
* entry point, so nothing produces those events on this path, and forwarding a
|
|
54
|
+
* vocabulary no caller can reach would be dead code. A proxy implementor reading
|
|
55
|
+
* this contract should not emit them.
|
|
56
|
+
*
|
|
49
57
|
* @packageDocumentation
|
|
50
58
|
*/
|
|
51
59
|
const ts_utils_1 = require("@fgv/ts-utils");
|
|
52
60
|
const chatRequestBuilders_1 = require("../chatRequestBuilders");
|
|
61
|
+
const model_1 = require("../model");
|
|
53
62
|
const sseParser_1 = require("../sseParser");
|
|
54
63
|
const common_1 = require("./common");
|
|
55
64
|
const proxyEventTypes = ['text-delta', 'tool-event', 'done', 'error'];
|
|
@@ -84,7 +93,11 @@ function translateProxyStream(response) {
|
|
|
84
93
|
continue;
|
|
85
94
|
}
|
|
86
95
|
const envelope = (0, common_1.validateEventPayload)(json, proxyEventEnvelope);
|
|
87
|
-
|
|
96
|
+
// Reachable, not defensive: any event whose `type` is outside
|
|
97
|
+
// `proxyEventTypes` lands here, which includes the three `client-tool-*`
|
|
98
|
+
// variants a proxy might emit from a vocabulary this path does not carry.
|
|
99
|
+
// It previously claimed to be unreachable defensive code, so the coverage
|
|
100
|
+
// gate reported clean on the one branch that silently discards data.
|
|
88
101
|
if (!envelope) {
|
|
89
102
|
continue;
|
|
90
103
|
}
|
|
@@ -122,9 +135,23 @@ function translateProxyStream(response) {
|
|
|
122
135
|
* - Response: `Content-Type: text/event-stream`; body is the unified
|
|
123
136
|
* {@link AiAssist.IAiStreamEvent} JSON-serialized one event per SSE `data:` line
|
|
124
137
|
* (no `event:` line needed since the type discriminator is in the JSON).
|
|
138
|
+
* **Only `text-delta`, `tool-event`, `done` and `error` are carried** — an event
|
|
139
|
+
* of any other type, including the three `client-tool-*` variants, is discarded
|
|
140
|
+
* without error. Client-tool turns are not proxiable; `executeClientToolTurn`
|
|
141
|
+
* has no proxied entry point.
|
|
125
142
|
* - Error response (when the proxy can't even start): JSON `{error: string}`
|
|
126
143
|
* with a non-2xx status, surfaced as `proxy: ${error}`.
|
|
127
144
|
*
|
|
145
|
+
* Two request parameters are handled before the body is composed, identically to
|
|
146
|
+
* `callProxiedCompletion`: `tier` is resolved here and sent as a concrete
|
|
147
|
+
* `modelOverride`, so a proxy needs no `tier` vocabulary; `endpoint` is refused,
|
|
148
|
+
* because a proxy cannot confirm it honored it and reaching the provider's default
|
|
149
|
+
* upstream instead would send the request somewhere the caller excluded. `cache`,
|
|
150
|
+
* by contrast, is forwarded as a plain `body.cache` field exactly as
|
|
151
|
+
* `callProxiedCompletion` forwards it — a proxy that ignores it degrades to the
|
|
152
|
+
* request it would have sent anyway, never a misdirected one, so there is no
|
|
153
|
+
* `endpoint`-style reason to refuse it.
|
|
154
|
+
*
|
|
128
155
|
* The proxy server is responsible for opening the upstream SSE connection,
|
|
129
156
|
* translating provider-native events to the unified vocabulary, and
|
|
130
157
|
* forwarding events as they arrive (no buffering). The library does not
|
|
@@ -133,7 +160,7 @@ function translateProxyStream(response) {
|
|
|
133
160
|
* @public
|
|
134
161
|
*/
|
|
135
162
|
async function callProxiedCompletionStream(proxyUrl, params) {
|
|
136
|
-
const { descriptor, apiKey, system, messages, temperature, modelOverride, logger, tools, signal, thinking, maxTokens } = params;
|
|
163
|
+
const { descriptor, apiKey, system, messages, temperature, modelOverride, logger, tools, signal, thinking, maxTokens, tier, endpoint, cache } = params;
|
|
137
164
|
// Enforce the same unified-request invariants the direct entry points apply
|
|
138
165
|
// (non-empty messages, trailing user turn) so an invalid request fails fast
|
|
139
166
|
// here rather than diverging at the proxy.
|
|
@@ -144,6 +171,27 @@ async function callProxiedCompletionStream(proxyUrl, params) {
|
|
|
144
171
|
if (splitResult.value.prompt.attachments.length > 0 && !descriptor.acceptsImageInput) {
|
|
145
172
|
return (0, ts_utils_1.fail)(`provider "${descriptor.id}" does not accept image input`);
|
|
146
173
|
}
|
|
174
|
+
// `tier` and `endpoint` are handled exactly as `callProxiedCompletion` handles them,
|
|
175
|
+
// and for the same reasons — see the comments there. Briefly: the tier resolves on
|
|
176
|
+
// this side because the descriptor holding both halves of the resolution is already
|
|
177
|
+
// here, and the concrete model rides the existing `modelOverride` field, so a proxy
|
|
178
|
+
// that predates tiers still honors one. `endpoint` cannot resolve here, has never
|
|
179
|
+
// been sent, and forwarding it would be unverifiable, so it is refused rather than
|
|
180
|
+
// dropped.
|
|
181
|
+
if (endpoint !== undefined) {
|
|
182
|
+
return (0, ts_utils_1.fail)(`callProxiedCompletionStream: endpoint is not supported on the proxied path — a proxy ` +
|
|
183
|
+
`cannot confirm it honored it, and silently reaching the provider's default ` +
|
|
184
|
+
`upstream would send the request somewhere the caller excluded. Use ` +
|
|
185
|
+
`callProviderCompletionStream, or route the proxy itself at the intended upstream.`);
|
|
186
|
+
}
|
|
187
|
+
let effectiveModelOverride = modelOverride;
|
|
188
|
+
if (tier !== undefined) {
|
|
189
|
+
const tierResult = (0, model_1.resolveProviderModel)(descriptor, modelOverride, tier);
|
|
190
|
+
if (tierResult.isFailure()) {
|
|
191
|
+
return (0, ts_utils_1.fail)(tierResult.message);
|
|
192
|
+
}
|
|
193
|
+
effectiveModelOverride = tierResult.value;
|
|
194
|
+
}
|
|
147
195
|
const body = {
|
|
148
196
|
providerId: descriptor.id,
|
|
149
197
|
apiKey,
|
|
@@ -158,8 +206,8 @@ async function callProxiedCompletionStream(proxyUrl, params) {
|
|
|
158
206
|
if (system !== undefined) {
|
|
159
207
|
body.system = system;
|
|
160
208
|
}
|
|
161
|
-
if (
|
|
162
|
-
body.modelOverride =
|
|
209
|
+
if (effectiveModelOverride !== undefined) {
|
|
210
|
+
body.modelOverride = effectiveModelOverride;
|
|
163
211
|
}
|
|
164
212
|
if (tools && tools.length > 0) {
|
|
165
213
|
body.tools = tools;
|
|
@@ -172,6 +220,16 @@ async function callProxiedCompletionStream(proxyUrl, params) {
|
|
|
172
220
|
if (maxTokens !== undefined) {
|
|
173
221
|
body.maxTokens = maxTokens;
|
|
174
222
|
}
|
|
223
|
+
// `IAiCacheRequest` is plain numbers and a string — JSON-serializable as-is, forwarded exactly
|
|
224
|
+
// as `callProxiedCompletion` forwards it (completionClient.ts). This is a considered decision,
|
|
225
|
+
// not a default: unlike `endpoint` above, a proxy that does not understand `cache` degrades to
|
|
226
|
+
// the no-cache request it would have sent anyway — it never sends the prompt anywhere the
|
|
227
|
+
// caller excluded, so there is no correctness reason to refuse it the way #679 refused
|
|
228
|
+
// `endpoint`. No shipped proxy implementation is known to read this field yet; a caller relying
|
|
229
|
+
// on it should confirm their proxy forwards `cache` through to `callProviderCompletionStream`.
|
|
230
|
+
if (cache !== undefined) {
|
|
231
|
+
body.cache = cache;
|
|
232
|
+
}
|
|
175
233
|
/* c8 ignore next 1 - optional logger */
|
|
176
234
|
logger === null || logger === void 0 ? void 0 : logger.info(`AI streaming proxy request: provider=${descriptor.id}, proxy=${proxyUrl}`);
|
|
177
235
|
const url = `${proxyUrl}/api/ai/completion-stream`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../../../src/packlets/ai-assist/streamingAdapters/proxy.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * Streaming adapter for a caller-provided proxy server. Unlike the\n * provider-specific adapters, the proxy speaks our own unified vocabulary\n * directly: each `data:` line is a JSON-serialized {@link AiAssist.IAiStreamEvent},\n * so this adapter only validates the event-type discriminator and forwards.\n *\n * @packageDocumentation\n */\n\nimport { fail, Result, succeed, type Validator, Validators } from '@fgv/ts-utils';\n\nimport { normalizeOutboundMessages, splitChatRequest } from '../chatRequestBuilders';\nimport { type IAiStreamEvent } from '../model';\nimport { parseSseEventJson, readSseEvents } from '../sseParser';\nimport { IProviderCompletionStreamParams, openSseConnection, validateEventPayload } from './common';\n\n// ============================================================================\n// Event payload shape — a tagged-event envelope\n// ============================================================================\n\ntype ProxyEventType = 'text-delta' | 'tool-event' | 'done' | 'error';\n\n/**\n * Minimal envelope used to identify and discriminate proxy events. Once the\n * `type` is recognized, the event is forwarded as-is — the unified shape is\n * the proxy contract, so there's no further per-type validation here.\n *\n * @internal\n */\ninterface IProxyEventEnvelope {\n readonly type: ProxyEventType;\n}\n\nconst proxyEventTypes: ReadonlyArray<ProxyEventType> = ['text-delta', 'tool-event', 'done', 'error'];\n\nconst proxyEventEnvelope: Validator<IProxyEventEnvelope> = Validators.object<IProxyEventEnvelope>({\n type: Validators.enumeratedValue<ProxyEventType>(proxyEventTypes)\n});\n\n// ============================================================================\n// Stream translator\n// ============================================================================\n\n/**\n * Translates a proxied SSE stream back into {@link AiAssist.IAiStreamEvent} objects.\n * Validation is limited to the type discriminator; the proxy is contractually\n * required to emit shape-correct unified events.\n *\n * @internal\n */\nasync function* translateProxyStream(response: Response): AsyncGenerator<IAiStreamEvent> {\n try {\n /* c8 ignore next - body is non-null at this point per openSseConnection */\n if (!response.body) return;\n for await (const message of readSseEvents(response.body)) {\n const json = parseSseEventJson(message.data);\n /* c8 ignore next 3 - defensive: malformed SSE events skipped */\n if (json === undefined) {\n continue;\n }\n const envelope = validateEventPayload(json, proxyEventEnvelope);\n /* c8 ignore next 3 - defensive: SSE events without valid envelope skipped */\n if (!envelope) {\n continue;\n }\n const event = json as IAiStreamEvent;\n yield event;\n if (envelope.type === 'done' || envelope.type === 'error') {\n return;\n }\n }\n } catch (err: unknown) /* c8 ignore start - defensive: stream errors are always Error instances */ {\n yield { type: 'error', message: err instanceof Error ? err.message : String(err) };\n } /* c8 ignore stop */\n}\n\n// ============================================================================\n// Public entry point\n// ============================================================================\n\n/**\n * Calls the streaming chat endpoint on a proxy server instead of calling\n * the provider directly from the browser.\n *\n * @remarks\n * Proxy contract:\n * - Endpoint: `POST ${proxyUrl}/api/ai/completion-stream`\n * - Request body: same JSON as `/api/ai/completion` plus `\"stream\": true`\n * - Response: `Content-Type: text/event-stream`; body is the unified\n * {@link AiAssist.IAiStreamEvent} JSON-serialized one event per SSE `data:` line\n * (no `event:` line needed since the type discriminator is in the JSON).\n * - Error response (when the proxy can't even start): JSON `{error: string}`\n * with a non-2xx status, surfaced as `proxy: ${error}`.\n *\n * The proxy server is responsible for opening the upstream SSE connection,\n * translating provider-native events to the unified vocabulary, and\n * forwarding events as they arrive (no buffering). The library does not\n * ship a proxy implementation.\n *\n * @public\n */\nexport async function callProxiedCompletionStream(\n proxyUrl: string,\n params: IProviderCompletionStreamParams\n): Promise<Result<AsyncIterable<IAiStreamEvent>>> {\n const {\n descriptor,\n apiKey,\n system,\n messages,\n temperature,\n modelOverride,\n logger,\n tools,\n signal,\n thinking,\n maxTokens\n } = params;\n\n // Enforce the same unified-request invariants the direct entry points apply\n // (non-empty messages, trailing user turn) so an invalid request fails fast\n // here rather than diverging at the proxy.\n const splitResult = splitChatRequest(system, messages);\n if (splitResult.isFailure()) {\n return fail(splitResult.message);\n }\n if (splitResult.value.prompt.attachments.length > 0 && !descriptor.acceptsImageInput) {\n return fail(`provider \"${descriptor.id}\" does not accept image input`);\n }\n\n const body: Record<string, unknown> = {\n providerId: descriptor.id,\n apiKey,\n messages: normalizeOutboundMessages(splitResult.value),\n stream: true\n };\n // Temperature is forwarded only when explicitly provided, matching the direct path — the proxy\n // omits it from the upstream request so the provider default applies.\n if (temperature !== undefined) {\n body.temperature = temperature;\n }\n if (system !== undefined) {\n body.system = system;\n }\n if (modelOverride !== undefined) {\n body.modelOverride = modelOverride;\n }\n if (tools && tools.length > 0) {\n body.tools = tools;\n }\n if (thinking !== undefined) {\n body.thinking = thinking;\n }\n // Forwarded only when explicitly provided; the proxy is responsible for mapping it to the\n // correct upstream provider field (see AiAssist.usesMaxCompletionTokensField).\n if (maxTokens !== undefined) {\n body.maxTokens = maxTokens;\n }\n\n /* c8 ignore next 1 - optional logger */\n logger?.info(`AI streaming proxy request: provider=${descriptor.id}, proxy=${proxyUrl}`);\n\n const url = `${proxyUrl}/api/ai/completion-stream`;\n const conn = await openSseConnection(url, {}, body, logger, signal);\n return conn.onSuccess((response) => succeed(translateProxyStream(response)));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../../../src/packlets/ai-assist/streamingAdapters/proxy.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * Streaming adapter for a caller-provided proxy server. Unlike the\n * provider-specific adapters, the proxy speaks our own unified vocabulary\n * directly: each `data:` line is a JSON-serialized {@link AiAssist.IAiStreamEvent},\n * so this adapter only validates the event-type discriminator and forwards.\n *\n * **It carries the four completion-path events, not the whole union.**\n * `text-delta`, `tool-event`, `done` and `error` pass through; the three\n * `client-tool-*` dispatch events are not recognized and are dropped. That is a\n * real limit rather than an oversight: `executeClientToolTurn` has no proxied\n * entry point, so nothing produces those events on this path, and forwarding a\n * vocabulary no caller can reach would be dead code. A proxy implementor reading\n * this contract should not emit them.\n *\n * @packageDocumentation\n */\n\nimport { fail, Result, succeed, type Validator, Validators } from '@fgv/ts-utils';\n\nimport { normalizeOutboundMessages, splitChatRequest } from '../chatRequestBuilders';\nimport { type IAiStreamEvent, type ModelSpec, resolveProviderModel } from '../model';\nimport { parseSseEventJson, readSseEvents } from '../sseParser';\nimport { IProviderCompletionStreamParams, openSseConnection, validateEventPayload } from './common';\n\n// ============================================================================\n// Event payload shape — a tagged-event envelope\n// ============================================================================\n\n/**\n * The event types this adapter recognizes — deliberately the four completion-path\n * events, not all seven members of `IAiStreamEvent`.\n *\n * @remarks\n * The omitted three are the `client-tool-*` dispatch events. `IAiStreamEvent`'s own\n * docstring tells exhaustive switches over that union to be updated in lockstep\n * when those variants were added; this one is a deliberate exception, and saying so\n * here is the difference between a documented boundary and a switch someone forgot.\n * Revisit together with a proxied client-tool entry point, not before — see\n * `docs/FUTURE.md`, \"A browser story for client-tool turns\".\n * @internal\n */\ntype ProxyEventType = 'text-delta' | 'tool-event' | 'done' | 'error';\n\n/**\n * Minimal envelope used to identify and discriminate proxy events. Once the\n * `type` is recognized, the event is forwarded as-is — the unified shape is\n * the proxy contract, so there's no further per-type validation here.\n *\n * @internal\n */\ninterface IProxyEventEnvelope {\n readonly type: ProxyEventType;\n}\n\nconst proxyEventTypes: ReadonlyArray<ProxyEventType> = ['text-delta', 'tool-event', 'done', 'error'];\n\nconst proxyEventEnvelope: Validator<IProxyEventEnvelope> = Validators.object<IProxyEventEnvelope>({\n type: Validators.enumeratedValue<ProxyEventType>(proxyEventTypes)\n});\n\n// ============================================================================\n// Stream translator\n// ============================================================================\n\n/**\n * Translates a proxied SSE stream back into {@link AiAssist.IAiStreamEvent} objects.\n * Validation is limited to the type discriminator; the proxy is contractually\n * required to emit shape-correct unified events.\n *\n * @internal\n */\nasync function* translateProxyStream(response: Response): AsyncGenerator<IAiStreamEvent> {\n try {\n /* c8 ignore next - body is non-null at this point per openSseConnection */\n if (!response.body) return;\n for await (const message of readSseEvents(response.body)) {\n const json = parseSseEventJson(message.data);\n /* c8 ignore next 3 - defensive: malformed SSE events skipped */\n if (json === undefined) {\n continue;\n }\n const envelope = validateEventPayload(json, proxyEventEnvelope);\n // Reachable, not defensive: any event whose `type` is outside\n // `proxyEventTypes` lands here, which includes the three `client-tool-*`\n // variants a proxy might emit from a vocabulary this path does not carry.\n // It previously claimed to be unreachable defensive code, so the coverage\n // gate reported clean on the one branch that silently discards data.\n if (!envelope) {\n continue;\n }\n const event = json as IAiStreamEvent;\n yield event;\n if (envelope.type === 'done' || envelope.type === 'error') {\n return;\n }\n }\n } catch (err: unknown) /* c8 ignore start - defensive: stream errors are always Error instances */ {\n yield { type: 'error', message: err instanceof Error ? err.message : String(err) };\n } /* c8 ignore stop */\n}\n\n// ============================================================================\n// Public entry point\n// ============================================================================\n\n/**\n * Calls the streaming chat endpoint on a proxy server instead of calling\n * the provider directly from the browser.\n *\n * @remarks\n * Proxy contract:\n * - Endpoint: `POST ${proxyUrl}/api/ai/completion-stream`\n * - Request body: same JSON as `/api/ai/completion` plus `\"stream\": true`\n * - Response: `Content-Type: text/event-stream`; body is the unified\n * {@link AiAssist.IAiStreamEvent} JSON-serialized one event per SSE `data:` line\n * (no `event:` line needed since the type discriminator is in the JSON).\n * **Only `text-delta`, `tool-event`, `done` and `error` are carried** — an event\n * of any other type, including the three `client-tool-*` variants, is discarded\n * without error. Client-tool turns are not proxiable; `executeClientToolTurn`\n * has no proxied entry point.\n * - Error response (when the proxy can't even start): JSON `{error: string}`\n * with a non-2xx status, surfaced as `proxy: ${error}`.\n *\n * Two request parameters are handled before the body is composed, identically to\n * `callProxiedCompletion`: `tier` is resolved here and sent as a concrete\n * `modelOverride`, so a proxy needs no `tier` vocabulary; `endpoint` is refused,\n * because a proxy cannot confirm it honored it and reaching the provider's default\n * upstream instead would send the request somewhere the caller excluded. `cache`,\n * by contrast, is forwarded as a plain `body.cache` field exactly as\n * `callProxiedCompletion` forwards it — a proxy that ignores it degrades to the\n * request it would have sent anyway, never a misdirected one, so there is no\n * `endpoint`-style reason to refuse it.\n *\n * The proxy server is responsible for opening the upstream SSE connection,\n * translating provider-native events to the unified vocabulary, and\n * forwarding events as they arrive (no buffering). The library does not\n * ship a proxy implementation.\n *\n * @public\n */\nexport async function callProxiedCompletionStream(\n proxyUrl: string,\n params: IProviderCompletionStreamParams\n): Promise<Result<AsyncIterable<IAiStreamEvent>>> {\n const {\n descriptor,\n apiKey,\n system,\n messages,\n temperature,\n modelOverride,\n logger,\n tools,\n signal,\n thinking,\n maxTokens,\n tier,\n endpoint,\n cache\n } = params;\n\n // Enforce the same unified-request invariants the direct entry points apply\n // (non-empty messages, trailing user turn) so an invalid request fails fast\n // here rather than diverging at the proxy.\n const splitResult = splitChatRequest(system, messages);\n if (splitResult.isFailure()) {\n return fail(splitResult.message);\n }\n if (splitResult.value.prompt.attachments.length > 0 && !descriptor.acceptsImageInput) {\n return fail(`provider \"${descriptor.id}\" does not accept image input`);\n }\n\n // `tier` and `endpoint` are handled exactly as `callProxiedCompletion` handles them,\n // and for the same reasons — see the comments there. Briefly: the tier resolves on\n // this side because the descriptor holding both halves of the resolution is already\n // here, and the concrete model rides the existing `modelOverride` field, so a proxy\n // that predates tiers still honors one. `endpoint` cannot resolve here, has never\n // been sent, and forwarding it would be unverifiable, so it is refused rather than\n // dropped.\n if (endpoint !== undefined) {\n return fail(\n `callProxiedCompletionStream: endpoint is not supported on the proxied path — a proxy ` +\n `cannot confirm it honored it, and silently reaching the provider's default ` +\n `upstream would send the request somewhere the caller excluded. Use ` +\n `callProviderCompletionStream, or route the proxy itself at the intended upstream.`\n );\n }\n\n let effectiveModelOverride: ModelSpec | undefined = modelOverride;\n if (tier !== undefined) {\n const tierResult = resolveProviderModel(descriptor, modelOverride, tier);\n if (tierResult.isFailure()) {\n return fail(tierResult.message);\n }\n effectiveModelOverride = tierResult.value;\n }\n\n const body: Record<string, unknown> = {\n providerId: descriptor.id,\n apiKey,\n messages: normalizeOutboundMessages(splitResult.value),\n stream: true\n };\n // Temperature is forwarded only when explicitly provided, matching the direct path — the proxy\n // omits it from the upstream request so the provider default applies.\n if (temperature !== undefined) {\n body.temperature = temperature;\n }\n if (system !== undefined) {\n body.system = system;\n }\n if (effectiveModelOverride !== undefined) {\n body.modelOverride = effectiveModelOverride;\n }\n if (tools && tools.length > 0) {\n body.tools = tools;\n }\n if (thinking !== undefined) {\n body.thinking = thinking;\n }\n // Forwarded only when explicitly provided; the proxy is responsible for mapping it to the\n // correct upstream provider field (see AiAssist.usesMaxCompletionTokensField).\n if (maxTokens !== undefined) {\n body.maxTokens = maxTokens;\n }\n // `IAiCacheRequest` is plain numbers and a string — JSON-serializable as-is, forwarded exactly\n // as `callProxiedCompletion` forwards it (completionClient.ts). This is a considered decision,\n // not a default: unlike `endpoint` above, a proxy that does not understand `cache` degrades to\n // the no-cache request it would have sent anyway — it never sends the prompt anywhere the\n // caller excluded, so there is no correctness reason to refuse it the way #679 refused\n // `endpoint`. No shipped proxy implementation is known to read this field yet; a caller relying\n // on it should confirm their proxy forwards `cache` through to `callProviderCompletionStream`.\n if (cache !== undefined) {\n body.cache = cache;\n }\n\n /* c8 ignore next 1 - optional logger */\n logger?.info(`AI streaming proxy request: provider=${descriptor.id}, proxy=${proxyUrl}`);\n\n const url = `${proxyUrl}/api/ai/completion-stream`;\n const conn = await openSseConnection(url, {}, body, logger, signal);\n return conn.onSuccess((response) => succeed(translateProxyStream(response)));\n}\n"]}
|
|
@@ -37,6 +37,11 @@ export { executeClientToolTurn, type IExecuteClientToolTurnParams, type IExecute
|
|
|
37
37
|
* `Result.fail` before fetch is invoked. Callers should route through
|
|
38
38
|
* {@link AiAssist.callProxiedCompletionStream} or surface the failure to the user.
|
|
39
39
|
*
|
|
40
|
+
* This guard is applied **here only**. `executeClientToolTurn` is also a
|
|
41
|
+
* streaming entry point and does not pre-flight the flag, so a browser
|
|
42
|
+
* client-tool turn against a `streamingCorsRestricted` provider fails at the
|
|
43
|
+
* fetch with a CORS error rather than with this legible refusal.
|
|
44
|
+
*
|
|
40
45
|
* Connection-time failures (auth, network, non-2xx) surface as the outer
|
|
41
46
|
* `Result.fail`. Once iteration begins, errors mid-stream surface as a
|
|
42
47
|
* terminal error event ({@link AiAssist.IAiStreamError}) followed by the iterable
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"streamingClient.d.ts","sourceRoot":"","sources":["../../../src/packlets/ai-assist/streamingClient.ts"],"names":[],"mappings":"AAoBA;;;;;;;GAOG;AAEH,OAAO,EAAQ,MAAM,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"streamingClient.d.ts","sourceRoot":"","sources":["../../../src/packlets/ai-assist/streamingClient.ts"],"names":[],"mappings":"AAoBA;;;;;;;GAOG;AAEH,OAAO,EAAQ,MAAM,EAAE,MAAM,eAAe,CAAC;AAK7C,OAAO,EACL,KAAK,cAAc,EAMpB,MAAM,SAAS,CAAC;AAQjB,OAAO,EAAE,KAAK,+BAA+B,EAAyB,MAAM,4BAA4B,CAAC;AAWzG,OAAO,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AACxE,YAAY,EAAE,+BAA+B,EAAE,MAAM,4BAA4B,CAAC;AAElF;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EACL,qBAAqB,EACrB,KAAK,4BAA4B,EACjC,KAAK,4BAA4B,EACjC,KAAK,sBAAsB,EAC5B,MAAM,mDAAmD,CAAC;AAE3D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,4BAA4B,CAChD,MAAM,EAAE,+BAA+B,GACtC,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,cAAc,CAAC,CAAC,CAAC,CAsJhD"}
|