@fgv/ts-extras 5.1.0-55 → 5.1.0-56
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 +95 -0
- package/dist/packlets/ai-assist/cacheRequest.js +118 -0
- package/dist/packlets/ai-assist/cacheRequest.js.map +1 -0
- package/dist/packlets/ai-assist/chatRequestBuilders.js +70 -5
- package/dist/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
- package/dist/packlets/ai-assist/completionClient.js +122 -45
- package/dist/packlets/ai-assist/completionClient.js.map +1 -1
- package/dist/packlets/ai-assist/imageGenerationClient.js +1 -1
- package/dist/packlets/ai-assist/imageGenerationClient.js.map +1 -1
- package/dist/packlets/ai-assist/index.js +3 -1
- package/dist/packlets/ai-assist/index.js.map +1 -1
- package/dist/packlets/ai-assist/model.js.map +1 -1
- package/dist/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.js +200 -0
- package/dist/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.js.map +1 -0
- package/dist/packlets/ai-assist/promptCacheObservabilityHarness.js +137 -0
- package/dist/packlets/ai-assist/promptCacheObservabilityHarness.js.map +1 -0
- package/dist/packlets/ai-assist/promptCacheRoutingAbHarness.js +156 -0
- package/dist/packlets/ai-assist/promptCacheRoutingAbHarness.js.map +1 -0
- package/dist/packlets/ai-assist/promptCacheUsageMath.js +124 -0
- package/dist/packlets/ai-assist/promptCacheUsageMath.js.map +1 -0
- package/dist/packlets/ai-assist/registry.js +9 -11
- package/dist/packlets/ai-assist/registry.js.map +1 -1
- package/dist/packlets/ai-assist/streamUsageCapability.js +124 -0
- package/dist/packlets/ai-assist/streamUsageCapability.js.map +1 -0
- package/dist/packlets/ai-assist/streamingAdapters/anthropic.js +24 -5
- package/dist/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +64 -33
- package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/common.js +25 -1
- package/dist/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/gemini.js +12 -5
- package/dist/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js +34 -8
- package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js +22 -14
- package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
- package/dist/packlets/ai-assist/streamingClient.js +3 -2
- package/dist/packlets/ai-assist/streamingClient.js.map +1 -1
- package/dist/packlets/ai-assist/thinkingOptionsResolver.js +18 -3
- package/dist/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -1
- package/dist/packlets/ai-assist/toolFormats.js +80 -0
- package/dist/packlets/ai-assist/toolFormats.js.map +1 -1
- package/dist/packlets/ai-assist/toolTypes.js +21 -0
- package/dist/packlets/ai-assist/toolTypes.js.map +1 -0
- package/dist/packlets/ai-assist/usageNormalization.js +178 -0
- package/dist/packlets/ai-assist/usageNormalization.js.map +1 -0
- package/dist/packlets/ai-assist/usageTypes.js +21 -0
- package/dist/packlets/ai-assist/usageTypes.js.map +1 -0
- package/dist/packlets/mustache/index.js.map +1 -1
- package/dist/packlets/mustache/interfaces.js.map +1 -1
- package/dist/packlets/mustache/mustacheTemplate.js +91 -0
- package/dist/packlets/mustache/mustacheTemplate.js.map +1 -1
- package/dist/ts-extras.d.ts +547 -30
- package/lib/packlets/ai-assist/cacheRequest.d.ts +100 -0
- package/lib/packlets/ai-assist/cacheRequest.d.ts.map +1 -0
- package/lib/packlets/ai-assist/cacheRequest.js +123 -0
- package/lib/packlets/ai-assist/cacheRequest.js.map +1 -0
- package/lib/packlets/ai-assist/chatRequestBuilders.d.ts +34 -4
- package/lib/packlets/ai-assist/chatRequestBuilders.d.ts.map +1 -1
- package/lib/packlets/ai-assist/chatRequestBuilders.js +73 -5
- package/lib/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
- package/lib/packlets/ai-assist/completionClient.d.ts +6 -0
- package/lib/packlets/ai-assist/completionClient.d.ts.map +1 -1
- package/lib/packlets/ai-assist/completionClient.js +121 -44
- package/lib/packlets/ai-assist/completionClient.js.map +1 -1
- package/lib/packlets/ai-assist/imageGenerationClient.d.ts.map +1 -1
- package/lib/packlets/ai-assist/imageGenerationClient.js +1 -1
- package/lib/packlets/ai-assist/imageGenerationClient.js.map +1 -1
- package/lib/packlets/ai-assist/index.d.ts +7 -2
- package/lib/packlets/ai-assist/index.d.ts.map +1 -1
- package/lib/packlets/ai-assist/index.js +11 -1
- package/lib/packlets/ai-assist/index.js.map +1 -1
- package/lib/packlets/ai-assist/model.d.ts +59 -288
- 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/promptCacheAnthropicBreakpointHarness.d.ts +124 -0
- package/lib/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.d.ts.map +1 -0
- package/lib/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.js +208 -0
- package/lib/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.js.map +1 -0
- package/lib/packlets/ai-assist/promptCacheObservabilityHarness.d.ts +77 -0
- package/lib/packlets/ai-assist/promptCacheObservabilityHarness.d.ts.map +1 -0
- package/lib/packlets/ai-assist/promptCacheObservabilityHarness.js +143 -0
- package/lib/packlets/ai-assist/promptCacheObservabilityHarness.js.map +1 -0
- package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.d.ts +84 -0
- package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.d.ts.map +1 -0
- package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.js +163 -0
- package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.js.map +1 -0
- package/lib/packlets/ai-assist/promptCacheUsageMath.d.ts +105 -0
- package/lib/packlets/ai-assist/promptCacheUsageMath.d.ts.map +1 -0
- package/lib/packlets/ai-assist/promptCacheUsageMath.js +130 -0
- package/lib/packlets/ai-assist/promptCacheUsageMath.js.map +1 -0
- package/lib/packlets/ai-assist/registry.d.ts.map +1 -1
- package/lib/packlets/ai-assist/registry.js +9 -11
- package/lib/packlets/ai-assist/registry.js.map +1 -1
- package/lib/packlets/ai-assist/streamUsageCapability.d.ts +119 -0
- package/lib/packlets/ai-assist/streamUsageCapability.d.ts.map +1 -0
- package/lib/packlets/ai-assist/streamUsageCapability.js +130 -0
- package/lib/packlets/ai-assist/streamUsageCapability.js.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.js +23 -4
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts +21 -12
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +64 -34
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/common.d.ts +21 -0
- package/lib/packlets/ai-assist/streamingAdapters/common.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/common.js +25 -1
- package/lib/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/gemini.js +12 -5
- package/lib/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js +33 -7
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js +21 -13
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
- package/lib/packlets/ai-assist/streamingClient.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingClient.js +3 -2
- package/lib/packlets/ai-assist/streamingClient.js.map +1 -1
- package/lib/packlets/ai-assist/thinkingOptionsResolver.d.ts.map +1 -1
- package/lib/packlets/ai-assist/thinkingOptionsResolver.js +18 -3
- package/lib/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -1
- package/lib/packlets/ai-assist/toolFormats.d.ts +55 -1
- package/lib/packlets/ai-assist/toolFormats.d.ts.map +1 -1
- package/lib/packlets/ai-assist/toolFormats.js +82 -0
- package/lib/packlets/ai-assist/toolFormats.js.map +1 -1
- package/lib/packlets/ai-assist/toolTypes.d.ts +328 -0
- package/lib/packlets/ai-assist/toolTypes.d.ts.map +1 -0
- package/lib/packlets/ai-assist/toolTypes.js +22 -0
- package/lib/packlets/ai-assist/toolTypes.js.map +1 -0
- package/lib/packlets/ai-assist/usageNormalization.d.ts +58 -0
- package/lib/packlets/ai-assist/usageNormalization.d.ts.map +1 -0
- package/lib/packlets/ai-assist/usageNormalization.js +184 -0
- package/lib/packlets/ai-assist/usageNormalization.js.map +1 -0
- package/lib/packlets/ai-assist/usageTypes.d.ts +84 -0
- package/lib/packlets/ai-assist/usageTypes.d.ts.map +1 -0
- package/lib/packlets/ai-assist/usageTypes.js +22 -0
- package/lib/packlets/ai-assist/usageTypes.js.map +1 -0
- package/lib/packlets/mustache/index.d.ts +1 -1
- package/lib/packlets/mustache/index.d.ts.map +1 -1
- package/lib/packlets/mustache/index.js.map +1 -1
- package/lib/packlets/mustache/interfaces.d.ts +30 -0
- package/lib/packlets/mustache/interfaces.d.ts.map +1 -1
- package/lib/packlets/mustache/interfaces.js.map +1 -1
- package/lib/packlets/mustache/mustacheTemplate.d.ts +56 -1
- package/lib/packlets/mustache/mustacheTemplate.d.ts.map +1 -1
- package/lib/packlets/mustache/mustacheTemplate.js +91 -0
- package/lib/packlets/mustache/mustacheTemplate.js.map +1 -1
- package/package.json +16 -16
- package/perf/promptCacheAnthropicBreakpoint.js +135 -0
- package/perf/promptCacheObservability.js +107 -0
- package/perf/promptCacheRoutingAb.js +105 -0
|
@@ -49,13 +49,12 @@ var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _ar
|
|
|
49
49
|
* @packageDocumentation
|
|
50
50
|
*/
|
|
51
51
|
import { succeed, Validators } from '@fgv/ts-utils';
|
|
52
|
-
import { isJsonObject } from '@fgv/ts-json-base';
|
|
53
52
|
import { buildGeminiContents } from '../chatRequestBuilders';
|
|
54
53
|
import { geminiAuthHeader } from '../endpoint';
|
|
55
54
|
import { parseSseEventJson, readSseEvents } from '../sseParser';
|
|
56
55
|
import { toGeminiTools } from '../toolFormats';
|
|
57
|
-
import {
|
|
58
|
-
|
|
56
|
+
import { normalizeGeminiUsage } from '../usageNormalization';
|
|
57
|
+
import { jsonObjectValidator, openSseConnection, validateEventPayload } from './common';
|
|
59
58
|
const geminiFunctionCallInner = Validators.object({
|
|
60
59
|
name: Validators.string.optional(),
|
|
61
60
|
args: jsonObjectValidator.optional()
|
|
@@ -71,7 +70,8 @@ const geminiStreamCandidate = Validators.object({
|
|
|
71
70
|
finishReason: Validators.string.optional()
|
|
72
71
|
}, { options: { optionalFields: ['content', 'finishReason'] } });
|
|
73
72
|
const geminiStreamChunk = Validators.object({
|
|
74
|
-
candidates: Validators.arrayOf(geminiStreamCandidate)
|
|
73
|
+
candidates: Validators.arrayOf(geminiStreamCandidate),
|
|
74
|
+
usageMetadata: jsonObjectValidator.optional()
|
|
75
75
|
});
|
|
76
76
|
// ============================================================================
|
|
77
77
|
// Stream translator
|
|
@@ -88,6 +88,9 @@ function translateGeminiStream(response, functionCalls) {
|
|
|
88
88
|
let fullText = '';
|
|
89
89
|
let truncated = false;
|
|
90
90
|
let receivedFinishReason = false;
|
|
91
|
+
// Gemini repeats usageMetadata on every chunk with running totals; the last chunk received
|
|
92
|
+
// carries the final figures, so each new value simply overwrites the previous one.
|
|
93
|
+
let usageRaw;
|
|
91
94
|
try {
|
|
92
95
|
/* c8 ignore next - body is non-null at this point per openSseConnection */
|
|
93
96
|
if (!response.body)
|
|
@@ -103,6 +106,9 @@ function translateGeminiStream(response, functionCalls) {
|
|
|
103
106
|
continue;
|
|
104
107
|
}
|
|
105
108
|
const chunk = validateEventPayload(json, geminiStreamChunk);
|
|
109
|
+
if ((chunk === null || chunk === void 0 ? void 0 : chunk.usageMetadata) !== undefined) {
|
|
110
|
+
usageRaw = chunk.usageMetadata;
|
|
111
|
+
}
|
|
106
112
|
/* c8 ignore next 1 - defensive: chunk?.candidates optional chain unreachable after validation */
|
|
107
113
|
const candidate = chunk === null || chunk === void 0 ? void 0 : chunk.candidates[0];
|
|
108
114
|
/* c8 ignore next 3 - defensive: SSE events without candidates skipped */
|
|
@@ -150,7 +156,8 @@ function translateGeminiStream(response, functionCalls) {
|
|
|
150
156
|
return yield __await(void 0);
|
|
151
157
|
} /* c8 ignore stop */
|
|
152
158
|
if (receivedFinishReason) {
|
|
153
|
-
|
|
159
|
+
const usage = normalizeGeminiUsage(usageRaw);
|
|
160
|
+
yield yield __await(Object.assign({ type: 'done', truncated, fullText }, (usage !== undefined ? { usage } : {})));
|
|
154
161
|
}
|
|
155
162
|
else {
|
|
156
163
|
yield yield __await({ type: 'error', message: 'Gemini stream ended without a finishReason' });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gemini.js","sourceRoot":"","sources":["../../../../src/packlets/ai-assist/streamingAdapters/gemini.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 Gemini's `streamGenerateContent` endpoint. Gemini\n * emits no explicit tool-progress events even when `google_search` is\n * enabled — grounding metadata arrives attached to text chunks — so this\n * adapter never yields `tool-event`s.\n *\n * Client-defined tools (`functionCall` parts) are emitted as\n * `client-tool-call-done` immediately (no delta accumulation needed for Gemini).\n *\n * @packageDocumentation\n */\n\nimport { type Logging, Result, succeed, type Validator, Validators } from '@fgv/ts-utils';\nimport { isJsonObject, type JsonObject } from '@fgv/ts-json-base';\n\nimport { buildGeminiContents } from '../chatRequestBuilders';\nimport { geminiAuthHeader } from '../endpoint';\nimport { AiPrompt, type AiToolConfig, type IAiStreamEvent, type IChatMessage } from '../model';\nimport { parseSseEventJson, readSseEvents } from '../sseParser';\nimport { toGeminiTools } from '../toolFormats';\nimport { type IResolvedThinkingConfig } from '../thinkingOptionsResolver';\nimport { IStreamApiConfig, openSseConnection, validateEventPayload } from './common';\n\n// ============================================================================\n// Accumulated call state (internal — used by C3 continuation builder)\n// ============================================================================\n\n/**\n * An accumulated function call from a Gemini stream. Gemini does not assign\n * call IDs; correlation is by tool name. Arguments arrive complete in the\n * `functionCall` part (no delta accumulation).\n * @internal\n */\nexport interface IAccumulatedGeminiFunctionCall {\n readonly name: string;\n readonly args: JsonObject;\n /**\n * The opaque thought signature Gemini stamps on a `functionCall` part when\n * thinking is enabled. It must be echoed back, verbatim, on the continuation's\n * model turn alongside the `functionCall`, or Gemini rejects the follow-up with\n * \"Function call is missing a thought_signature in functionCall parts\".\n * Absent when thinking is disabled. See\n * https://ai.google.dev/gemini-api/docs/thought-signatures.\n */\n readonly thoughtSignature?: string;\n}\n\n// ============================================================================\n// Event payload shapes\n// ============================================================================\n\n/**\n * One `parts[]` element in a Gemini streaming chunk. Text parts and\n * functionCall parts are both surfaced.\n * @internal\n */\ninterface IGeminiStreamPart {\n readonly text?: string;\n readonly functionCall?: { readonly name?: string; readonly args?: JsonObject };\n /**\n * The opaque thought signature attached to a `functionCall` part when thinking\n * is enabled. A sibling of `functionCall`/`text` at the part level (base64\n * string). Captured for replay on the continuation's model turn.\n */\n readonly thoughtSignature?: string;\n}\n\n/**\n * One `candidates[]` element in a Gemini streaming chunk.\n * @internal\n */\ninterface IGeminiStreamCandidate {\n readonly content?: { readonly parts?: ReadonlyArray<IGeminiStreamPart> };\n readonly finishReason?: string;\n}\n\n/**\n * One streaming chunk from `streamGenerateContent?alt=sse`.\n * @internal\n */\ninterface IGeminiStreamChunk {\n readonly candidates: ReadonlyArray<IGeminiStreamCandidate>;\n}\n\nconst jsonObjectValidator: Validator<JsonObject> = Validators.isA<JsonObject>(\n 'JsonObject',\n (v): v is JsonObject => isJsonObject(v)\n);\n\nconst geminiFunctionCallInner: Validator<{ name?: string; args?: JsonObject }> = Validators.object<{\n name?: string;\n args?: JsonObject;\n}>(\n {\n name: Validators.string.optional(),\n args: jsonObjectValidator.optional()\n },\n { options: { optionalFields: ['name', 'args'] } }\n);\n\nconst geminiStreamPart: Validator<IGeminiStreamPart> = Validators.object<IGeminiStreamPart>(\n {\n text: Validators.string.optional(),\n functionCall: geminiFunctionCallInner.optional(),\n thoughtSignature: Validators.string.optional()\n },\n { options: { optionalFields: ['text', 'functionCall', 'thoughtSignature'] } }\n);\n\nconst geminiStreamContent: Validator<{ parts?: ReadonlyArray<IGeminiStreamPart> }> = Validators.object<{\n parts?: ReadonlyArray<IGeminiStreamPart>;\n}>({ parts: Validators.arrayOf(geminiStreamPart).optional() }, { options: { optionalFields: ['parts'] } });\n\nconst geminiStreamCandidate: Validator<IGeminiStreamCandidate> = Validators.object<IGeminiStreamCandidate>(\n {\n content: geminiStreamContent.optional(),\n finishReason: Validators.string.optional()\n },\n { options: { optionalFields: ['content', 'finishReason'] } }\n);\n\nconst geminiStreamChunk: Validator<IGeminiStreamChunk> = Validators.object<IGeminiStreamChunk>({\n candidates: Validators.arrayOf(geminiStreamCandidate)\n});\n\n// ============================================================================\n// Stream translator\n// ============================================================================\n\n/**\n * Translates a Gemini streamGenerateContent SSE stream into unified events.\n *\n * @internal\n */\nasync function* translateGeminiStream(\n response: Response,\n functionCalls: IAccumulatedGeminiFunctionCall[]\n): AsyncGenerator<IAiStreamEvent> {\n let fullText = '';\n let truncated = false;\n let receivedFinishReason = false;\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 /* c8 ignore next 3 - defensive: malformed SSE events skipped */\n if (json === undefined) {\n continue;\n }\n const chunk = validateEventPayload(json, geminiStreamChunk);\n /* c8 ignore next 1 - defensive: chunk?.candidates optional chain unreachable after validation */\n const candidate = chunk?.candidates[0];\n /* c8 ignore next 3 - defensive: SSE events without candidates skipped */\n if (!candidate) {\n continue;\n }\n /* c8 ignore next 1 - defensive: candidate.content?.parts null branch unreachable after validation */\n const parts = candidate.content?.parts;\n if (parts) {\n for (const part of parts) {\n if (typeof part.text === 'string' && part.text.length > 0) {\n fullText += part.text;\n yield { type: 'text-delta', delta: part.text };\n /* c8 ignore next 1 - defensive: functionCall parts without a `name` are silently dropped (cannot construct a continuation builder entry without one) */\n } else if (part.functionCall?.name) {\n const { name, args } = part.functionCall;\n /* c8 ignore next 1 - defensive: Gemini always sends args; {} fallback unreachable in practice */\n const callArgs = args ?? {};\n // Capture the part-level thoughtSignature (sibling of functionCall) so the\n // continuation builder can replay it on the model turn. Reads as undefined\n // when thinking is disabled; the builder omits the key in that case.\n functionCalls.push({ name, args: callArgs, thoughtSignature: part.thoughtSignature });\n yield { type: 'client-tool-call-done', toolName: name, args: callArgs };\n }\n }\n }\n const finishReason = candidate.finishReason;\n if (typeof finishReason === 'string' && finishReason.length > 0) {\n truncated = finishReason === 'MAX_TOKENS';\n receivedFinishReason = 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 (receivedFinishReason) {\n yield { type: 'done', truncated, fullText };\n } else {\n yield { type: 'error', message: 'Gemini stream ended without a finishReason' };\n }\n}\n\n// ============================================================================\n// Per-format request caller\n// ============================================================================\n\n/**\n * Issues a streaming Gemini request and returns the unified-event iterable\n * on success.\n *\n * @internal\n */\nexport async function callGeminiStream(\n config: IStreamApiConfig,\n prompt: AiPrompt,\n messagesBefore: ReadonlyArray<IChatMessage> | undefined,\n temperature: number | undefined,\n tools: ReadonlyArray<AiToolConfig> | undefined,\n logger?: Logging.ILogger,\n signal?: AbortSignal,\n resolvedThinking?: IResolvedThinkingConfig,\n functionCalls?: IAccumulatedGeminiFunctionCall[],\n continuationMessages?: ReadonlyArray<JsonObject>,\n maxTokens?: number\n): Promise<Result<AsyncIterable<IAiStreamEvent>>> {\n const url = `${config.baseUrl}/models/${config.model}:streamGenerateContent?alt=sse`;\n const contents = buildGeminiContents(prompt, {\n head: messagesBefore,\n rawTail: continuationMessages\n });\n // Temperature is sent only when explicitly provided; otherwise Gemini's default applies.\n const generationConfig: Record<string, unknown> = {};\n if (temperature !== undefined) {\n generationConfig.temperature = temperature;\n }\n if (maxTokens !== undefined) {\n generationConfig.maxOutputTokens = maxTokens;\n }\n if (resolvedThinking?.geminiThinkingBudget !== undefined) {\n generationConfig.thinkingConfig = { thinkingBudget: resolvedThinking.geminiThinkingBudget };\n }\n if (resolvedThinking?.otherParams !== undefined) {\n Object.assign(generationConfig, resolvedThinking.otherParams);\n }\n const body: Record<string, unknown> = {\n systemInstruction: { parts: [{ text: prompt.system }] },\n contents,\n generationConfig\n };\n /* c8 ignore next 3 - tools branch not exercised in streaming tests */\n if (tools && tools.length > 0) {\n body.tools = toGeminiTools(tools);\n }\n const headers: Record<string, string> = geminiAuthHeader(config.apiKey);\n /* c8 ignore next 4 - optional logger diagnostic output */\n if (logger) {\n const toolTypes = tools && tools.length > 0 ? tools.map((t) => t.type).join(',') : 'none';\n logger.info(`Gemini streaming: model=${config.model}, tools=${toolTypes}`);\n }\n const calls = functionCalls ?? [];\n const conn = await openSseConnection(url, headers, body, logger, signal);\n return conn.onSuccess((response) => succeed(translateGeminiStream(response, calls)));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"gemini.js","sourceRoot":"","sources":["../../../../src/packlets/ai-assist/streamingAdapters/gemini.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 Gemini's `streamGenerateContent` endpoint. Gemini\n * emits no explicit tool-progress events even when `google_search` is\n * enabled — grounding metadata arrives attached to text chunks — so this\n * adapter never yields `tool-event`s.\n *\n * Client-defined tools (`functionCall` parts) are emitted as\n * `client-tool-call-done` immediately (no delta accumulation needed for Gemini).\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 { buildGeminiContents } from '../chatRequestBuilders';\nimport { geminiAuthHeader } from '../endpoint';\nimport { AiPrompt, type AiToolConfig, type IAiStreamEvent, type IChatMessage } from '../model';\nimport { parseSseEventJson, readSseEvents } from '../sseParser';\nimport { toGeminiTools } from '../toolFormats';\nimport { type IResolvedThinkingConfig } from '../thinkingOptionsResolver';\nimport { normalizeGeminiUsage } from '../usageNormalization';\nimport { IStreamApiConfig, jsonObjectValidator, openSseConnection, validateEventPayload } from './common';\n\n// ============================================================================\n// Accumulated call state (internal — used by C3 continuation builder)\n// ============================================================================\n\n/**\n * An accumulated function call from a Gemini stream. Gemini does not assign\n * call IDs; correlation is by tool name. Arguments arrive complete in the\n * `functionCall` part (no delta accumulation).\n * @internal\n */\nexport interface IAccumulatedGeminiFunctionCall {\n readonly name: string;\n readonly args: JsonObject;\n /**\n * The opaque thought signature Gemini stamps on a `functionCall` part when\n * thinking is enabled. It must be echoed back, verbatim, on the continuation's\n * model turn alongside the `functionCall`, or Gemini rejects the follow-up with\n * \"Function call is missing a thought_signature in functionCall parts\".\n * Absent when thinking is disabled. See\n * https://ai.google.dev/gemini-api/docs/thought-signatures.\n */\n readonly thoughtSignature?: string;\n}\n\n// ============================================================================\n// Event payload shapes\n// ============================================================================\n\n/**\n * One `parts[]` element in a Gemini streaming chunk. Text parts and\n * functionCall parts are both surfaced.\n * @internal\n */\ninterface IGeminiStreamPart {\n readonly text?: string;\n readonly functionCall?: { readonly name?: string; readonly args?: JsonObject };\n /**\n * The opaque thought signature attached to a `functionCall` part when thinking\n * is enabled. A sibling of `functionCall`/`text` at the part level (base64\n * string). Captured for replay on the continuation's model turn.\n */\n readonly thoughtSignature?: string;\n}\n\n/**\n * One `candidates[]` element in a Gemini streaming chunk.\n * @internal\n */\ninterface IGeminiStreamCandidate {\n readonly content?: { readonly parts?: ReadonlyArray<IGeminiStreamPart> };\n readonly finishReason?: string;\n}\n\n/**\n * One streaming chunk from `streamGenerateContent?alt=sse`.\n * @internal\n */\ninterface IGeminiStreamChunk {\n readonly candidates: ReadonlyArray<IGeminiStreamCandidate>;\n /** Present on every chunk with running totals; the last chunk's value is the final one. */\n readonly usageMetadata?: JsonObject;\n}\n\nconst geminiFunctionCallInner: Validator<{ name?: string; args?: JsonObject }> = Validators.object<{\n name?: string;\n args?: JsonObject;\n}>(\n {\n name: Validators.string.optional(),\n args: jsonObjectValidator.optional()\n },\n { options: { optionalFields: ['name', 'args'] } }\n);\n\nconst geminiStreamPart: Validator<IGeminiStreamPart> = Validators.object<IGeminiStreamPart>(\n {\n text: Validators.string.optional(),\n functionCall: geminiFunctionCallInner.optional(),\n thoughtSignature: Validators.string.optional()\n },\n { options: { optionalFields: ['text', 'functionCall', 'thoughtSignature'] } }\n);\n\nconst geminiStreamContent: Validator<{ parts?: ReadonlyArray<IGeminiStreamPart> }> = Validators.object<{\n parts?: ReadonlyArray<IGeminiStreamPart>;\n}>({ parts: Validators.arrayOf(geminiStreamPart).optional() }, { options: { optionalFields: ['parts'] } });\n\nconst geminiStreamCandidate: Validator<IGeminiStreamCandidate> = Validators.object<IGeminiStreamCandidate>(\n {\n content: geminiStreamContent.optional(),\n finishReason: Validators.string.optional()\n },\n { options: { optionalFields: ['content', 'finishReason'] } }\n);\n\nconst geminiStreamChunk: Validator<IGeminiStreamChunk> = Validators.object<IGeminiStreamChunk>({\n candidates: Validators.arrayOf(geminiStreamCandidate),\n usageMetadata: jsonObjectValidator.optional()\n});\n\n// ============================================================================\n// Stream translator\n// ============================================================================\n\n/**\n * Translates a Gemini streamGenerateContent SSE stream into unified events.\n *\n * @internal\n */\nasync function* translateGeminiStream(\n response: Response,\n functionCalls: IAccumulatedGeminiFunctionCall[]\n): AsyncGenerator<IAiStreamEvent> {\n let fullText = '';\n let truncated = false;\n let receivedFinishReason = false;\n // Gemini repeats usageMetadata on every chunk with running totals; the last chunk received\n // carries the final figures, so each new value simply overwrites the previous one.\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 /* c8 ignore next 3 - defensive: malformed SSE events skipped */\n if (json === undefined) {\n continue;\n }\n const chunk = validateEventPayload(json, geminiStreamChunk);\n if (chunk?.usageMetadata !== undefined) {\n usageRaw = chunk.usageMetadata;\n }\n /* c8 ignore next 1 - defensive: chunk?.candidates optional chain unreachable after validation */\n const candidate = chunk?.candidates[0];\n /* c8 ignore next 3 - defensive: SSE events without candidates skipped */\n if (!candidate) {\n continue;\n }\n /* c8 ignore next 1 - defensive: candidate.content?.parts null branch unreachable after validation */\n const parts = candidate.content?.parts;\n if (parts) {\n for (const part of parts) {\n if (typeof part.text === 'string' && part.text.length > 0) {\n fullText += part.text;\n yield { type: 'text-delta', delta: part.text };\n /* c8 ignore next 1 - defensive: functionCall parts without a `name` are silently dropped (cannot construct a continuation builder entry without one) */\n } else if (part.functionCall?.name) {\n const { name, args } = part.functionCall;\n /* c8 ignore next 1 - defensive: Gemini always sends args; {} fallback unreachable in practice */\n const callArgs = args ?? {};\n // Capture the part-level thoughtSignature (sibling of functionCall) so the\n // continuation builder can replay it on the model turn. Reads as undefined\n // when thinking is disabled; the builder omits the key in that case.\n functionCalls.push({ name, args: callArgs, thoughtSignature: part.thoughtSignature });\n yield { type: 'client-tool-call-done', toolName: name, args: callArgs };\n }\n }\n }\n const finishReason = candidate.finishReason;\n if (typeof finishReason === 'string' && finishReason.length > 0) {\n truncated = finishReason === 'MAX_TOKENS';\n receivedFinishReason = 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 (receivedFinishReason) {\n const usage = normalizeGeminiUsage(usageRaw);\n yield { type: 'done', truncated, fullText, ...(usage !== undefined ? { usage } : {}) };\n } else {\n yield { type: 'error', message: 'Gemini stream ended without a finishReason' };\n }\n}\n\n// ============================================================================\n// Per-format request caller\n// ============================================================================\n\n/**\n * Issues a streaming Gemini request and returns the unified-event iterable\n * on success.\n *\n * @internal\n */\nexport async function callGeminiStream(\n config: IStreamApiConfig,\n prompt: AiPrompt,\n messagesBefore: ReadonlyArray<IChatMessage> | undefined,\n temperature: number | undefined,\n tools: ReadonlyArray<AiToolConfig> | undefined,\n logger?: Logging.ILogger,\n signal?: AbortSignal,\n resolvedThinking?: IResolvedThinkingConfig,\n functionCalls?: IAccumulatedGeminiFunctionCall[],\n continuationMessages?: ReadonlyArray<JsonObject>,\n maxTokens?: number\n): Promise<Result<AsyncIterable<IAiStreamEvent>>> {\n const url = `${config.baseUrl}/models/${config.model}:streamGenerateContent?alt=sse`;\n const contents = buildGeminiContents(prompt, {\n head: messagesBefore,\n rawTail: continuationMessages\n });\n // Temperature is sent only when explicitly provided; otherwise Gemini's default applies.\n const generationConfig: Record<string, unknown> = {};\n if (temperature !== undefined) {\n generationConfig.temperature = temperature;\n }\n if (maxTokens !== undefined) {\n generationConfig.maxOutputTokens = maxTokens;\n }\n if (resolvedThinking?.geminiThinkingBudget !== undefined) {\n generationConfig.thinkingConfig = { thinkingBudget: resolvedThinking.geminiThinkingBudget };\n }\n if (resolvedThinking?.otherParams !== undefined) {\n Object.assign(generationConfig, resolvedThinking.otherParams);\n }\n const body: Record<string, unknown> = {\n systemInstruction: { parts: [{ text: prompt.system }] },\n contents,\n generationConfig\n };\n /* c8 ignore next 3 - tools branch not exercised in streaming tests */\n if (tools && tools.length > 0) {\n body.tools = toGeminiTools(tools);\n }\n const headers: Record<string, string> = geminiAuthHeader(config.apiKey);\n /* c8 ignore next 4 - optional logger diagnostic output */\n if (logger) {\n const toolTypes = tools && tools.length > 0 ? tools.map((t) => t.type).join(',') : 'none';\n logger.info(`Gemini streaming: model=${config.model}, tools=${toolTypes}`);\n }\n const calls = functionCalls ?? [];\n const conn = await openSseConnection(url, headers, body, logger, signal);\n return conn.onSuccess((response) => succeed(translateGeminiStream(response, calls)));\n}\n"]}
|
|
@@ -48,7 +48,8 @@ import { succeed, Validators } from '@fgv/ts-utils';
|
|
|
48
48
|
import { buildMessages, buildOpenAiChatUserContent } from '../chatRequestBuilders';
|
|
49
49
|
import { bearerAuthHeader } from '../endpoint';
|
|
50
50
|
import { parseSseEventJson, readSseEvents } from '../sseParser';
|
|
51
|
-
import {
|
|
51
|
+
import { normalizeOpenAiChatUsage } from '../usageNormalization';
|
|
52
|
+
import { jsonObjectOrNullValidator, openSseConnection, validateEventPayload } from './common';
|
|
52
53
|
// eslint-disable-next-line @rushstack/no-new-null
|
|
53
54
|
const stringOrNull = Validators.isA('string-or-null',
|
|
54
55
|
// eslint-disable-next-line @rushstack/no-new-null
|
|
@@ -58,7 +59,8 @@ const openAiChatStreamChoice = Validators.object({
|
|
|
58
59
|
finish_reason: stringOrNull.optional()
|
|
59
60
|
}, { options: { optionalFields: ['delta', 'finish_reason'] } });
|
|
60
61
|
const openAiChatStreamChunk = Validators.object({
|
|
61
|
-
choices: Validators.arrayOf(openAiChatStreamChoice)
|
|
62
|
+
choices: Validators.arrayOf(openAiChatStreamChoice),
|
|
63
|
+
usage: jsonObjectOrNullValidator.optional()
|
|
62
64
|
});
|
|
63
65
|
// ============================================================================
|
|
64
66
|
// Stream translator
|
|
@@ -68,13 +70,14 @@ const openAiChatStreamChunk = Validators.object({
|
|
|
68
70
|
*
|
|
69
71
|
* @internal
|
|
70
72
|
*/
|
|
71
|
-
function translateOpenAiChatStream(response) {
|
|
73
|
+
function translateOpenAiChatStream(response, reportsUsage) {
|
|
72
74
|
return __asyncGenerator(this, arguments, function* translateOpenAiChatStream_1() {
|
|
73
75
|
var _a, e_1, _b, _c;
|
|
74
76
|
var _d;
|
|
75
77
|
let fullText = '';
|
|
76
78
|
let truncated = false;
|
|
77
79
|
let receivedDone = false;
|
|
80
|
+
let usageRaw;
|
|
78
81
|
try {
|
|
79
82
|
/* c8 ignore next - body is non-null at this point per openSseConnection */
|
|
80
83
|
if (!response.body)
|
|
@@ -90,9 +93,19 @@ function translateOpenAiChatStream(response) {
|
|
|
90
93
|
continue;
|
|
91
94
|
}
|
|
92
95
|
const chunk = validateEventPayload(json, openAiChatStreamChunk);
|
|
96
|
+
// Every intermediate chunk carries literal `null` once `stream_options.include_usage`
|
|
97
|
+
// is set; only the terminal chunk's populated object should update the accumulator.
|
|
98
|
+
// Gated on reportsUsage (same descriptor check that decides whether the request even
|
|
99
|
+
// asked for it, see callOpenAiChatStream) — this adapter is shared by descriptors with
|
|
100
|
+
// no confirmed cache-usage reporting, and an unprompted usage block from one of those
|
|
101
|
+
// carries no cache information (see supportsCacheUsageReporting).
|
|
102
|
+
if (reportsUsage && (chunk === null || chunk === void 0 ? void 0 : chunk.usage) !== undefined && chunk.usage !== null) {
|
|
103
|
+
usageRaw = chunk.usage;
|
|
104
|
+
}
|
|
93
105
|
/* c8 ignore next 1 - defensive: chunk?.choices optional chain unreachable after validation */
|
|
94
106
|
const choice = chunk === null || chunk === void 0 ? void 0 : chunk.choices[0];
|
|
95
|
-
|
|
107
|
+
// The `stream_options.include_usage` terminal chunk carries `choices: []` and only
|
|
108
|
+
// `usage` — already captured above, so it is expected (not defensive) to fall through here.
|
|
96
109
|
if (!choice) {
|
|
97
110
|
continue;
|
|
98
111
|
}
|
|
@@ -122,7 +135,8 @@ function translateOpenAiChatStream(response) {
|
|
|
122
135
|
return yield __await(void 0);
|
|
123
136
|
} /* c8 ignore stop */
|
|
124
137
|
if (receivedDone) {
|
|
125
|
-
|
|
138
|
+
const usage = normalizeOpenAiChatUsage(usageRaw);
|
|
139
|
+
yield yield __await(Object.assign({ type: 'done', truncated, fullText }, (usage !== undefined ? { usage } : {})));
|
|
126
140
|
}
|
|
127
141
|
else {
|
|
128
142
|
yield yield __await({ type: 'error', message: 'OpenAI stream ended without a finish_reason' });
|
|
@@ -138,7 +152,7 @@ function translateOpenAiChatStream(response) {
|
|
|
138
152
|
*
|
|
139
153
|
* @internal
|
|
140
154
|
*/
|
|
141
|
-
export async function callOpenAiChatStream(config, prompt, messagesBefore, temperature, logger, signal, resolvedThinking, maxTokens, useMaxCompletionTokensField = false) {
|
|
155
|
+
export async function callOpenAiChatStream(config, prompt, messagesBefore, temperature, logger, signal, resolvedThinking, maxTokens, useMaxCompletionTokensField = false, includeStreamUsage = false) {
|
|
142
156
|
var _a;
|
|
143
157
|
const url = `${config.baseUrl}/chat/completions`;
|
|
144
158
|
const messages = buildMessages(prompt.system, buildOpenAiChatUserContent(prompt), {
|
|
@@ -146,7 +160,19 @@ export async function callOpenAiChatStream(config, prompt, messagesBefore, tempe
|
|
|
146
160
|
});
|
|
147
161
|
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;
|
|
148
162
|
const supportsReasoning = config.model !== 'grok-4';
|
|
149
|
-
const body = {
|
|
163
|
+
const body = {
|
|
164
|
+
model: config.model,
|
|
165
|
+
messages,
|
|
166
|
+
stream: true
|
|
167
|
+
};
|
|
168
|
+
// Chat Completions omits the usage block from every streaming response unless asked —
|
|
169
|
+
// unlike the Responses API and Anthropic, which report it unconditionally. Gated per
|
|
170
|
+
// AiAssist.supportsStreamUsageOption: this adapter also carries providers whose tolerance
|
|
171
|
+
// for an unrecognized request field is unverified (a strict self-hosted server can reject
|
|
172
|
+
// the whole request rather than ignore it), so the field is sent only where it's confirmed.
|
|
173
|
+
if (includeStreamUsage) {
|
|
174
|
+
body.stream_options = { include_usage: true };
|
|
175
|
+
}
|
|
150
176
|
if (effort !== undefined && supportsReasoning) {
|
|
151
177
|
body.reasoning_effort = effort;
|
|
152
178
|
}
|
|
@@ -170,6 +196,6 @@ export async function callOpenAiChatStream(config, prompt, messagesBefore, tempe
|
|
|
170
196
|
/* c8 ignore next 1 - optional logger */
|
|
171
197
|
logger === null || logger === void 0 ? void 0 : logger.info(`OpenAI streaming completion: model=${config.model}`);
|
|
172
198
|
const conn = await openSseConnection(url, headers, body, logger, signal);
|
|
173
|
-
return conn.onSuccess((response) => succeed(translateOpenAiChatStream(response)));
|
|
199
|
+
return conn.onSuccess((response) => succeed(translateOpenAiChatStream(response, includeStreamUsage)));
|
|
174
200
|
}
|
|
175
201
|
//# sourceMappingURL=openaiChat.js.map
|
|
@@ -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';\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 { IStreamApiConfig, openSseConnection, validateEventPayload } 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\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});\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(response: Response): AsyncGenerator<IAiStreamEvent> {\n let fullText = '';\n let truncated = false;\n let receivedDone = false;\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 /* c8 ignore next 1 - defensive: chunk?.choices optional chain unreachable after validation */\n const choice = chunk?.choices[0];\n /* c8 ignore next 3 - defensive: SSE events without choices are skipped */\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 yield { type: 'done', truncated, fullText };\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): 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> = { model: config.model, messages, stream: true };\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)));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"openaiChat.js","sourceRoot":"","sources":["../../../../src/packlets/ai-assist/streamingAdapters/openaiChat.ts"],"names":[],"mappings":"AAAA,kCAAkC;AAClC,EAAE;AACF,+EAA+E;AAC/E,gFAAgF;AAChF,+EAA+E;AAC/E,4EAA4E;AAC5E,wEAAwE;AACxE,2DAA2D;AAC3D,EAAE;AACF,iFAAiF;AACjF,kDAAkD;AAClD,EAAE;AACF,6EAA6E;AAC7E,2EAA2E;AAC3E,8EAA8E;AAC9E,yEAAyE;AACzE,gFAAgF;AAChF,gFAAgF;AAChF,YAAY;;;;;;;;;;;;;;;;;;;;;AAEZ;;;;;;GAMG;AAEH,OAAO,EAAwB,OAAO,EAAkB,UAAU,EAAE,MAAM,eAAe,CAAC;AAG1F,OAAO,EAAE,aAAa,EAAE,0BAA0B,EAAE,MAAM,wBAAwB,CAAC;AACnF,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAEhE,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,EAEL,yBAAyB,EACzB,iBAAiB,EACjB,oBAAoB,EACrB,MAAM,UAAU,CAAC;AA2ClB,kDAAkD;AAClD,MAAM,YAAY,GAA6B,UAAU,CAAC,GAAG,CAC3D,gBAAgB;AAChB,kDAAkD;AAClD,CAAC,CAAU,EAAsB,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,CACxE,CAAC;AAEF,MAAM,sBAAsB,GAAuC,UAAU,CAAC,MAAM,CAClF;IACE,KAAK,EAAE,UAAU,CAAC,MAAM,CACtB,EAAE,OAAO,EAAE,UAAU,CAAC,MAAM,EAAE,EAC9B,EAAE,OAAO,EAAE,EAAE,cAAc,EAAE,CAAC,SAAS,CAAC,EAAE,EAAE,CAC7C,CAAC,QAAQ,EAAE;IACZ,aAAa,EAAE,YAAY,CAAC,QAAQ,EAAE;CACvC,EACD,EAAE,OAAO,EAAE,EAAE,cAAc,EAAE,CAAC,OAAO,EAAE,eAAe,CAAC,EAAE,EAAE,CAC5D,CAAC;AAEF,MAAM,qBAAqB,GAAsC,UAAU,CAAC,MAAM,CAAyB;IACzG,OAAO,EAAE,UAAU,CAAC,OAAO,CAAC,sBAAsB,CAAC;IACnD,KAAK,EAAE,yBAAyB,CAAC,QAAQ,EAAE;CAC5C,CAAC,CAAC;AAEH,+EAA+E;AAC/E,oBAAoB;AACpB,+EAA+E;AAE/E;;;;GAIG;AACH,SAAgB,yBAAyB,CACvC,QAAkB,EAClB,YAAqB;;;;QAErB,IAAI,QAAQ,GAAG,EAAE,CAAC;QAClB,IAAI,SAAS,GAAG,KAAK,CAAC;QACtB,IAAI,YAAY,GAAG,KAAK,CAAC;QACzB,IAAI,QAAgC,CAAC;QAErC,IAAI,CAAC;YACH,2EAA2E;YAC3E,IAAI,CAAC,QAAQ,CAAC,IAAI;gBAAE,6BAAO;;gBAC3B,KAA4B,eAAA,KAAA,cAAA,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAA,IAAA,+DAAE,CAAC;oBAA/B,cAA4B;oBAA5B,WAA4B;oBAA7C,MAAM,OAAO,KAAA,CAAA;oBACtB,MAAM,IAAI,GAAG,iBAAiB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;oBAC7C,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;wBACvB,uCAAuC;wBACvC,SAAS;oBACX,CAAC;oBACD,MAAM,KAAK,GAAG,oBAAoB,CAAC,IAAI,EAAE,qBAAqB,CAAC,CAAC;oBAChE,sFAAsF;oBACtF,oFAAoF;oBACpF,qFAAqF;oBACrF,uFAAuF;oBACvF,sFAAsF;oBACtF,kEAAkE;oBAClE,IAAI,YAAY,IAAI,CAAA,KAAK,aAAL,KAAK,uBAAL,KAAK,CAAE,KAAK,MAAK,SAAS,IAAI,KAAK,CAAC,KAAK,KAAK,IAAI,EAAE,CAAC;wBACvE,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC;oBACzB,CAAC;oBACD,8FAA8F;oBAC9F,MAAM,MAAM,GAAG,KAAK,aAAL,KAAK,uBAAL,KAAK,CAAE,OAAO,CAAC,CAAC,CAAC,CAAC;oBACjC,mFAAmF;oBACnF,4FAA4F;oBAC5F,IAAI,CAAC,MAAM,EAAE,CAAC;wBACZ,SAAS;oBACX,CAAC;oBACD,qGAAqG;oBACrG,MAAM,KAAK,GAAG,MAAA,MAAM,CAAC,KAAK,0CAAE,OAAO,CAAC;oBACpC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;wBAClD,QAAQ,IAAI,KAAK,CAAC;wBAClB,oBAAM,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,CAAA,CAAC;oBACtC,CAAC;oBACD,MAAM,MAAM,GAAG,MAAM,CAAC,aAAa,CAAC;oBACpC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;wBACpD,SAAS,GAAG,MAAM,KAAK,QAAQ,CAAC;wBAChC,YAAY,GAAG,IAAI,CAAC;oBACtB,CAAC;gBACH,CAAC;;;;;;;;;QACH,CAAC;QAAC,OAAO,GAAY,EAAE,2EAA2E,CAAC,CAAC;YAClG,oBAAM,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAA,CAAC;YACnF,6BAAO;QACT,CAAC,CAAC,oBAAoB;QAEtB,IAAI,YAAY,EAAE,CAAC;YACjB,MAAM,KAAK,GAAG,wBAAwB,CAAC,QAAQ,CAAC,CAAC;YACjD,oCAAQ,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,IAAK,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;QACzF,CAAC;aAAM,CAAC;YACN,oBAAM,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,6CAA6C,EAAE,CAAA,CAAC;QAClF,CAAC;IACH,CAAC;CAAA;AAED,+EAA+E;AAC/E,4BAA4B;AAC5B,+EAA+E;AAE/E;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,MAAwB,EACxB,MAAgB,EAChB,cAAuD,EACvD,WAA+B,EAC/B,MAAwB,EACxB,MAAoB,EACpB,gBAA0C,EAC1C,SAAkB,EAClB,8BAAuC,KAAK,EAC5C,qBAA8B,KAAK;;IAEnC,MAAM,GAAG,GAAG,GAAG,MAAM,CAAC,OAAO,mBAAmB,CAAC;IACjD,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,0BAA0B,CAAC,MAAM,CAAC,EAAE;QAChF,IAAI,EAAE,cAAc;KACrB,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,MAAA,gBAAgB,aAAhB,gBAAgB,uBAAhB,gBAAgB,CAAE,YAAY,mCAAI,gBAAgB,aAAhB,gBAAgB,uBAAhB,gBAAgB,CAAE,SAAS,CAAC;IAC7E,MAAM,iBAAiB,GAAG,MAAM,CAAC,KAAK,KAAK,QAAQ,CAAC;IACpD,MAAM,IAAI,GAA4B;QACpC,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,QAAQ;QACR,MAAM,EAAE,IAAI;KACb,CAAC;IACF,sFAAsF;IACtF,qFAAqF;IACrF,0FAA0F;IAC1F,0FAA0F;IAC1F,4FAA4F;IAC5F,IAAI,kBAAkB,EAAE,CAAC;QACvB,IAAI,CAAC,cAAc,GAAG,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;IAChD,CAAC;IACD,IAAI,MAAM,KAAK,SAAS,IAAI,iBAAiB,EAAE,CAAC;QAC9C,IAAI,CAAC,gBAAgB,GAAG,MAAM,CAAC;IACjC,CAAC;IACD,yFAAyF;IACzF,6FAA6F;IAC7F,gGAAgG;IAChG,4FAA4F;IAC5F,+FAA+F;IAC/F,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC9B,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;IACjC,CAAC;IACD,+FAA+F;IAC/F,qFAAqF;IACrF,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC5B,IAAI,CAAC,2BAA2B,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,CAAC,YAAY,CAAC,GAAG,SAAS,CAAC;IACzF,CAAC;IACD,IAAI,CAAA,gBAAgB,aAAhB,gBAAgB,uBAAhB,gBAAgB,CAAE,WAAW,MAAK,SAAS,EAAE,CAAC;QAChD,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,gBAAgB,CAAC,WAAW,CAAC,CAAC;IACpD,CAAC;IACD,MAAM,OAAO,GAA2B,gBAAgB,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACxE,wCAAwC;IACxC,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,IAAI,CAAC,sCAAsC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;IACnE,MAAM,IAAI,GAAG,MAAM,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACzE,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,OAAO,CAAC,yBAAyB,CAAC,QAAQ,EAAE,kBAAkB,CAAC,CAAC,CAAC,CAAC;AACxG,CAAC","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"]}
|
|
@@ -53,7 +53,8 @@ import { buildMessages, buildOpenAiResponsesUserContent } from '../chatRequestBu
|
|
|
53
53
|
import { bearerAuthHeader } from '../endpoint';
|
|
54
54
|
import { parseSseEventJson, readSseEvents } from '../sseParser';
|
|
55
55
|
import { toResponsesApiTools } from '../toolFormats';
|
|
56
|
-
import {
|
|
56
|
+
import { normalizeOpenAiResponsesUsage } from '../usageNormalization';
|
|
57
|
+
import { UNRECOGNIZED_EVENT_WARN_TAG, formatUnrecognizedEventPayloadPreview, jsonObjectOrNullValidator, openSseConnection, validateEventPayload } from './common';
|
|
57
58
|
const responsesDeltaPayload = Validators.object({
|
|
58
59
|
delta: Validators.string
|
|
59
60
|
});
|
|
@@ -76,8 +77,9 @@ const responsesIncompleteDetails = Validators.object({ reason: Validators.string
|
|
|
76
77
|
const responsesCompletedPayload = Validators.object({
|
|
77
78
|
response: Validators.object({
|
|
78
79
|
status: Validators.string.optional(),
|
|
79
|
-
incomplete_details: responsesIncompleteDetails.optional()
|
|
80
|
-
|
|
80
|
+
incomplete_details: responsesIncompleteDetails.optional(),
|
|
81
|
+
usage: jsonObjectOrNullValidator.optional()
|
|
82
|
+
})
|
|
81
83
|
});
|
|
82
84
|
const responsesErrorInner = Validators.object({ message: Validators.string.optional() }, { options: { optionalFields: ['message'] } });
|
|
83
85
|
const responsesErrorPayload = Validators.object({
|
|
@@ -174,14 +176,15 @@ const RECOGNIZED_OPENAI_RESPONSES_EVENTS = new Set([
|
|
|
174
176
|
*
|
|
175
177
|
* @internal
|
|
176
178
|
*/
|
|
177
|
-
function translateOpenAiResponsesStream(response, functionCallMap, logger) {
|
|
179
|
+
function translateOpenAiResponsesStream(response, functionCallMap, reportsUsage, logger) {
|
|
178
180
|
return __asyncGenerator(this, arguments, function* translateOpenAiResponsesStream_1() {
|
|
179
181
|
var _a, e_1, _b, _c;
|
|
180
|
-
var _d, _e, _f, _g, _h;
|
|
182
|
+
var _d, _e, _f, _g, _h, _j;
|
|
181
183
|
let fullText = '';
|
|
182
184
|
let truncated = false;
|
|
183
185
|
let completed = false;
|
|
184
186
|
let incompleteReason;
|
|
187
|
+
let usageRaw;
|
|
185
188
|
// OpenAI / xAI Responses API emits function_call_arguments.{delta,done} events keyed by
|
|
186
189
|
// `item_id` (the fc_* output-item id). The harness and continuation builder key by
|
|
187
190
|
// `call_id` (the call_* id). This map correlates the two — populated when the
|
|
@@ -195,9 +198,9 @@ function translateOpenAiResponsesStream(response, functionCallMap, logger) {
|
|
|
195
198
|
if (!response.body)
|
|
196
199
|
return yield __await(void 0);
|
|
197
200
|
try {
|
|
198
|
-
for (var
|
|
199
|
-
_c =
|
|
200
|
-
|
|
201
|
+
for (var _k = true, _l = __asyncValues(readSseEvents(response.body)), _m; _m = yield __await(_l.next()), _a = _m.done, !_a; _k = true) {
|
|
202
|
+
_c = _m.value;
|
|
203
|
+
_k = false;
|
|
201
204
|
const message = _c;
|
|
202
205
|
const eventName = message.event;
|
|
203
206
|
if (eventName === 'response.output_text.delta') {
|
|
@@ -269,7 +272,7 @@ function translateOpenAiResponsesStream(response, functionCallMap, logger) {
|
|
|
269
272
|
? parsed
|
|
270
273
|
: {};
|
|
271
274
|
}
|
|
272
|
-
catch (
|
|
275
|
+
catch (_o) {
|
|
273
276
|
args = {};
|
|
274
277
|
}
|
|
275
278
|
/* c8 ignore stop */
|
|
@@ -287,6 +290,10 @@ function translateOpenAiResponsesStream(response, functionCallMap, logger) {
|
|
|
287
290
|
// event so a stray incomplete_details on a non-incomplete payload never leaks
|
|
288
291
|
// through, and a later (defensive) completed event can't leave a stale reason.
|
|
289
292
|
incompleteReason = truncated ? (_e = payload.response.incomplete_details) === null || _e === void 0 ? void 0 : _e.reason : undefined;
|
|
293
|
+
// Gated the same way as the non-streaming Responses path (see
|
|
294
|
+
// supportsCacheUsageReporting) — this adapter is shared by descriptors with no
|
|
295
|
+
// confirmed cache-usage reporting.
|
|
296
|
+
usageRaw = reportsUsage ? (_f = payload.response.usage) !== null && _f !== void 0 ? _f : undefined : undefined;
|
|
290
297
|
}
|
|
291
298
|
completed = true;
|
|
292
299
|
/* c8 ignore next 1 - defensive: eventName === 'error' alternative not exercised in tests */
|
|
@@ -294,7 +301,7 @@ function translateOpenAiResponsesStream(response, functionCallMap, logger) {
|
|
|
294
301
|
else if (eventName === 'response.failed' || eventName === 'error') {
|
|
295
302
|
const payload = validateEventPayload(parseSseEventJson(message.data), responsesErrorPayload);
|
|
296
303
|
/* c8 ignore next 1 - defensive: payload?.error and payload?.message null branches unreachable */
|
|
297
|
-
const errMsg = (
|
|
304
|
+
const errMsg = (_j = (_h = (_g = payload === null || payload === void 0 ? void 0 : payload.error) === null || _g === void 0 ? void 0 : _g.message) !== null && _h !== void 0 ? _h : payload === null || payload === void 0 ? void 0 : payload.message) !== null && _j !== void 0 ? _j : 'Responses API stream failed';
|
|
298
305
|
yield yield __await({ type: 'error', message: errMsg });
|
|
299
306
|
return yield __await(void 0);
|
|
300
307
|
}
|
|
@@ -319,7 +326,7 @@ function translateOpenAiResponsesStream(response, functionCallMap, logger) {
|
|
|
319
326
|
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
320
327
|
finally {
|
|
321
328
|
try {
|
|
322
|
-
if (!
|
|
329
|
+
if (!_k && !_a && (_b = _l.return)) yield __await(_b.call(_l));
|
|
323
330
|
}
|
|
324
331
|
finally { if (e_1) throw e_1.error; }
|
|
325
332
|
}
|
|
@@ -329,7 +336,8 @@ function translateOpenAiResponsesStream(response, functionCallMap, logger) {
|
|
|
329
336
|
return yield __await(void 0);
|
|
330
337
|
} /* c8 ignore stop */
|
|
331
338
|
if (completed) {
|
|
332
|
-
|
|
339
|
+
const usage = normalizeOpenAiResponsesUsage(usageRaw);
|
|
340
|
+
yield yield __await(Object.assign({ type: 'done', truncated, fullText, incompleteReason }, (usage !== undefined ? { usage } : {})));
|
|
333
341
|
}
|
|
334
342
|
else {
|
|
335
343
|
yield yield __await({ type: 'error', message: 'Responses API stream ended without a completed event' });
|
|
@@ -345,7 +353,7 @@ function translateOpenAiResponsesStream(response, functionCallMap, logger) {
|
|
|
345
353
|
*
|
|
346
354
|
* @internal
|
|
347
355
|
*/
|
|
348
|
-
export async function callOpenAiResponsesStream(config, prompt, tools = [], messagesBefore, temperature, logger, signal, resolvedThinking, functionCallMap, continuationMessages, maxTokens) {
|
|
356
|
+
export async function callOpenAiResponsesStream(config, prompt, tools = [], messagesBefore, temperature, logger, signal, resolvedThinking, functionCallMap, continuationMessages, maxTokens, reportsUsage = false) {
|
|
349
357
|
var _a;
|
|
350
358
|
const url = `${config.baseUrl}/responses`;
|
|
351
359
|
const input = buildMessages(prompt.system, buildOpenAiResponsesUserContent(prompt), {
|
|
@@ -385,6 +393,6 @@ export async function callOpenAiResponsesStream(config, prompt, tools = [], mess
|
|
|
385
393
|
logger === null || logger === void 0 ? void 0 : logger.info(`OpenAI Responses streaming: model=${config.model}, tools=${tools.map((t) => t.type).join(',')}`);
|
|
386
394
|
const callMap = functionCallMap !== null && functionCallMap !== void 0 ? functionCallMap : new Map();
|
|
387
395
|
const conn = await openSseConnection(url, headers, body, logger, signal);
|
|
388
|
-
return conn.onSuccess((response) => succeed(translateOpenAiResponsesStream(response, callMap, logger)));
|
|
396
|
+
return conn.onSuccess((response) => succeed(translateOpenAiResponsesStream(response, callMap, reportsUsage, logger)));
|
|
389
397
|
}
|
|
390
398
|
//# sourceMappingURL=openaiResponses.js.map
|