@fgv/ts-extras 5.1.0-55 → 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 +96 -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 +72 -7
- package/dist/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
- package/dist/packlets/ai-assist/completionClient.js +195 -58
- 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 +17 -2
- 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/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/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 +134 -0
- package/dist/packlets/ai-assist/streamUsageCapability.js.map +1 -0
- package/dist/packlets/ai-assist/streamingAdapters/anthropic.js +32 -9
- package/dist/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +81 -35
- 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 +45 -11
- package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js +33 -22
- 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 -5
- package/dist/packlets/ai-assist/streamingClient.js.map +1 -1
- package/dist/packlets/ai-assist/thinkingOptionsResolver.js +23 -5
- 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 +764 -48
- 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 +36 -6
- package/lib/packlets/ai-assist/chatRequestBuilders.d.ts.map +1 -1
- package/lib/packlets/ai-assist/chatRequestBuilders.js +75 -7
- package/lib/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
- package/lib/packlets/ai-assist/completionClient.d.ts +19 -0
- package/lib/packlets/ai-assist/completionClient.d.ts.map +1 -1
- package/lib/packlets/ai-assist/completionClient.js +194 -57
- 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 +17 -2
- 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/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 +81 -295
- 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 +129 -0
- package/lib/packlets/ai-assist/streamUsageCapability.d.ts.map +1 -0
- package/lib/packlets/ai-assist/streamUsageCapability.js +140 -0
- package/lib/packlets/ai-assist/streamUsageCapability.js.map +1 -0
- 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 +29 -6
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts +58 -14
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +81 -36
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/common.d.ts +38 -3
- 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 +2 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js +42 -8
- 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 +30 -19
- 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 +20 -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 +23 -5
- 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 +386 -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
|
@@ -43,10 +43,19 @@ var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _ar
|
|
|
43
43
|
* directly: each `data:` line is a JSON-serialized {@link AiAssist.IAiStreamEvent},
|
|
44
44
|
* so this adapter only validates the event-type discriminator and forwards.
|
|
45
45
|
*
|
|
46
|
+
* **It carries the four completion-path events, not the whole union.**
|
|
47
|
+
* `text-delta`, `tool-event`, `done` and `error` pass through; the three
|
|
48
|
+
* `client-tool-*` dispatch events are not recognized and are dropped. That is a
|
|
49
|
+
* real limit rather than an oversight: `executeClientToolTurn` has no proxied
|
|
50
|
+
* entry point, so nothing produces those events on this path, and forwarding a
|
|
51
|
+
* vocabulary no caller can reach would be dead code. A proxy implementor reading
|
|
52
|
+
* this contract should not emit them.
|
|
53
|
+
*
|
|
46
54
|
* @packageDocumentation
|
|
47
55
|
*/
|
|
48
56
|
import { fail, succeed, Validators } from '@fgv/ts-utils';
|
|
49
57
|
import { normalizeOutboundMessages, splitChatRequest } from '../chatRequestBuilders';
|
|
58
|
+
import { resolveProviderModel } from '../model';
|
|
50
59
|
import { parseSseEventJson, readSseEvents } from '../sseParser';
|
|
51
60
|
import { openSseConnection, validateEventPayload } from './common';
|
|
52
61
|
const proxyEventTypes = ['text-delta', 'tool-event', 'done', 'error'];
|
|
@@ -81,7 +90,11 @@ function translateProxyStream(response) {
|
|
|
81
90
|
continue;
|
|
82
91
|
}
|
|
83
92
|
const envelope = validateEventPayload(json, proxyEventEnvelope);
|
|
84
|
-
|
|
93
|
+
// Reachable, not defensive: any event whose `type` is outside
|
|
94
|
+
// `proxyEventTypes` lands here, which includes the three `client-tool-*`
|
|
95
|
+
// variants a proxy might emit from a vocabulary this path does not carry.
|
|
96
|
+
// It previously claimed to be unreachable defensive code, so the coverage
|
|
97
|
+
// gate reported clean on the one branch that silently discards data.
|
|
85
98
|
if (!envelope) {
|
|
86
99
|
continue;
|
|
87
100
|
}
|
|
@@ -119,9 +132,23 @@ function translateProxyStream(response) {
|
|
|
119
132
|
* - Response: `Content-Type: text/event-stream`; body is the unified
|
|
120
133
|
* {@link AiAssist.IAiStreamEvent} JSON-serialized one event per SSE `data:` line
|
|
121
134
|
* (no `event:` line needed since the type discriminator is in the JSON).
|
|
135
|
+
* **Only `text-delta`, `tool-event`, `done` and `error` are carried** — an event
|
|
136
|
+
* of any other type, including the three `client-tool-*` variants, is discarded
|
|
137
|
+
* without error. Client-tool turns are not proxiable; `executeClientToolTurn`
|
|
138
|
+
* has no proxied entry point.
|
|
122
139
|
* - Error response (when the proxy can't even start): JSON `{error: string}`
|
|
123
140
|
* with a non-2xx status, surfaced as `proxy: ${error}`.
|
|
124
141
|
*
|
|
142
|
+
* Two request parameters are handled before the body is composed, identically to
|
|
143
|
+
* `callProxiedCompletion`: `tier` is resolved here and sent as a concrete
|
|
144
|
+
* `modelOverride`, so a proxy needs no `tier` vocabulary; `endpoint` is refused,
|
|
145
|
+
* because a proxy cannot confirm it honored it and reaching the provider's default
|
|
146
|
+
* upstream instead would send the request somewhere the caller excluded. `cache`,
|
|
147
|
+
* by contrast, is forwarded as a plain `body.cache` field exactly as
|
|
148
|
+
* `callProxiedCompletion` forwards it — a proxy that ignores it degrades to the
|
|
149
|
+
* request it would have sent anyway, never a misdirected one, so there is no
|
|
150
|
+
* `endpoint`-style reason to refuse it.
|
|
151
|
+
*
|
|
125
152
|
* The proxy server is responsible for opening the upstream SSE connection,
|
|
126
153
|
* translating provider-native events to the unified vocabulary, and
|
|
127
154
|
* forwarding events as they arrive (no buffering). The library does not
|
|
@@ -130,7 +157,7 @@ function translateProxyStream(response) {
|
|
|
130
157
|
* @public
|
|
131
158
|
*/
|
|
132
159
|
export async function callProxiedCompletionStream(proxyUrl, params) {
|
|
133
|
-
const { descriptor, apiKey, system, messages, temperature, modelOverride, logger, tools, signal, thinking, maxTokens } = params;
|
|
160
|
+
const { descriptor, apiKey, system, messages, temperature, modelOverride, logger, tools, signal, thinking, maxTokens, tier, endpoint, cache } = params;
|
|
134
161
|
// Enforce the same unified-request invariants the direct entry points apply
|
|
135
162
|
// (non-empty messages, trailing user turn) so an invalid request fails fast
|
|
136
163
|
// here rather than diverging at the proxy.
|
|
@@ -141,6 +168,27 @@ export async function callProxiedCompletionStream(proxyUrl, params) {
|
|
|
141
168
|
if (splitResult.value.prompt.attachments.length > 0 && !descriptor.acceptsImageInput) {
|
|
142
169
|
return fail(`provider "${descriptor.id}" does not accept image input`);
|
|
143
170
|
}
|
|
171
|
+
// `tier` and `endpoint` are handled exactly as `callProxiedCompletion` handles them,
|
|
172
|
+
// and for the same reasons — see the comments there. Briefly: the tier resolves on
|
|
173
|
+
// this side because the descriptor holding both halves of the resolution is already
|
|
174
|
+
// here, and the concrete model rides the existing `modelOverride` field, so a proxy
|
|
175
|
+
// that predates tiers still honors one. `endpoint` cannot resolve here, has never
|
|
176
|
+
// been sent, and forwarding it would be unverifiable, so it is refused rather than
|
|
177
|
+
// dropped.
|
|
178
|
+
if (endpoint !== undefined) {
|
|
179
|
+
return fail(`callProxiedCompletionStream: endpoint is not supported on the proxied path — a proxy ` +
|
|
180
|
+
`cannot confirm it honored it, and silently reaching the provider's default ` +
|
|
181
|
+
`upstream would send the request somewhere the caller excluded. Use ` +
|
|
182
|
+
`callProviderCompletionStream, or route the proxy itself at the intended upstream.`);
|
|
183
|
+
}
|
|
184
|
+
let effectiveModelOverride = modelOverride;
|
|
185
|
+
if (tier !== undefined) {
|
|
186
|
+
const tierResult = resolveProviderModel(descriptor, modelOverride, tier);
|
|
187
|
+
if (tierResult.isFailure()) {
|
|
188
|
+
return fail(tierResult.message);
|
|
189
|
+
}
|
|
190
|
+
effectiveModelOverride = tierResult.value;
|
|
191
|
+
}
|
|
144
192
|
const body = {
|
|
145
193
|
providerId: descriptor.id,
|
|
146
194
|
apiKey,
|
|
@@ -155,8 +203,8 @@ export async function callProxiedCompletionStream(proxyUrl, params) {
|
|
|
155
203
|
if (system !== undefined) {
|
|
156
204
|
body.system = system;
|
|
157
205
|
}
|
|
158
|
-
if (
|
|
159
|
-
body.modelOverride =
|
|
206
|
+
if (effectiveModelOverride !== undefined) {
|
|
207
|
+
body.modelOverride = effectiveModelOverride;
|
|
160
208
|
}
|
|
161
209
|
if (tools && tools.length > 0) {
|
|
162
210
|
body.tools = tools;
|
|
@@ -169,6 +217,16 @@ export async function callProxiedCompletionStream(proxyUrl, params) {
|
|
|
169
217
|
if (maxTokens !== undefined) {
|
|
170
218
|
body.maxTokens = maxTokens;
|
|
171
219
|
}
|
|
220
|
+
// `IAiCacheRequest` is plain numbers and a string — JSON-serializable as-is, forwarded exactly
|
|
221
|
+
// as `callProxiedCompletion` forwards it (completionClient.ts). This is a considered decision,
|
|
222
|
+
// not a default: unlike `endpoint` above, a proxy that does not understand `cache` degrades to
|
|
223
|
+
// the no-cache request it would have sent anyway — it never sends the prompt anywhere the
|
|
224
|
+
// caller excluded, so there is no correctness reason to refuse it the way #679 refused
|
|
225
|
+
// `endpoint`. No shipped proxy implementation is known to read this field yet; a caller relying
|
|
226
|
+
// on it should confirm their proxy forwards `cache` through to `callProviderCompletionStream`.
|
|
227
|
+
if (cache !== undefined) {
|
|
228
|
+
body.cache = cache;
|
|
229
|
+
}
|
|
172
230
|
/* c8 ignore next 1 - optional logger */
|
|
173
231
|
logger === null || logger === void 0 ? void 0 : logger.info(`AI streaming proxy request: provider=${descriptor.id}, proxy=${proxyUrl}`);
|
|
174
232
|
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":"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;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,IAAI,EAAU,OAAO,EAAkB,UAAU,EAAE,MAAM,eAAe,CAAC;AAElF,OAAO,EAAE,yBAAyB,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AACrF,OAAO,EAAuC,oBAAoB,EAAE,MAAM,UAAU,CAAC;AACrF,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAChE,OAAO,EAAmC,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAgCpG,MAAM,eAAe,GAAkC,CAAC,YAAY,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;AAErG,MAAM,kBAAkB,GAAmC,UAAU,CAAC,MAAM,CAAsB;IAChG,IAAI,EAAE,UAAU,CAAC,eAAe,CAAiB,eAAe,CAAC;CAClE,CAAC,CAAC;AAEH,+EAA+E;AAC/E,oBAAoB;AACpB,+EAA+E;AAE/E;;;;;;GAMG;AACH,SAAgB,oBAAoB,CAAC,QAAkB;;;QACrD,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,gEAAgE;oBAChE,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;wBACvB,SAAS;oBACX,CAAC;oBACD,MAAM,QAAQ,GAAG,oBAAoB,CAAC,IAAI,EAAE,kBAAkB,CAAC,CAAC;oBAChE,8DAA8D;oBAC9D,yEAAyE;oBACzE,0EAA0E;oBAC1E,0EAA0E;oBAC1E,qEAAqE;oBACrE,IAAI,CAAC,QAAQ,EAAE,CAAC;wBACd,SAAS;oBACX,CAAC;oBACD,MAAM,KAAK,GAAG,IAAsB,CAAC;oBACrC,oBAAM,KAAK,CAAA,CAAC;oBACZ,IAAI,QAAQ,CAAC,IAAI,KAAK,MAAM,IAAI,QAAQ,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;wBAC1D,6BAAO;oBACT,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;QACrF,CAAC,CAAC,oBAAoB;IACxB,CAAC;CAAA;AAED,+EAA+E;AAC/E,qBAAqB;AACrB,+EAA+E;AAE/E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAC/C,QAAgB,EAChB,MAAuC;IAEvC,MAAM,EACJ,UAAU,EACV,MAAM,EACN,MAAM,EACN,QAAQ,EACR,WAAW,EACX,aAAa,EACb,MAAM,EACN,KAAK,EACL,MAAM,EACN,QAAQ,EACR,SAAS,EACT,IAAI,EACJ,QAAQ,EACR,KAAK,EACN,GAAG,MAAM,CAAC;IAEX,4EAA4E;IAC5E,4EAA4E;IAC5E,2CAA2C;IAC3C,MAAM,WAAW,GAAG,gBAAgB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACvD,IAAI,WAAW,CAAC,SAAS,EAAE,EAAE,CAAC;QAC5B,OAAO,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IACnC,CAAC;IACD,IAAI,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,iBAAiB,EAAE,CAAC;QACrF,OAAO,IAAI,CAAC,aAAa,UAAU,CAAC,EAAE,+BAA+B,CAAC,CAAC;IACzE,CAAC;IAED,qFAAqF;IACrF,mFAAmF;IACnF,oFAAoF;IACpF,oFAAoF;IACpF,kFAAkF;IAClF,mFAAmF;IACnF,WAAW;IACX,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,OAAO,IAAI,CACT,uFAAuF;YACrF,6EAA6E;YAC7E,qEAAqE;YACrE,mFAAmF,CACtF,CAAC;IACJ,CAAC;IAED,IAAI,sBAAsB,GAA0B,aAAa,CAAC;IAClE,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,MAAM,UAAU,GAAG,oBAAoB,CAAC,UAAU,EAAE,aAAa,EAAE,IAAI,CAAC,CAAC;QACzE,IAAI,UAAU,CAAC,SAAS,EAAE,EAAE,CAAC;YAC3B,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;QAClC,CAAC;QACD,sBAAsB,GAAG,UAAU,CAAC,KAAK,CAAC;IAC5C,CAAC;IAED,MAAM,IAAI,GAA4B;QACpC,UAAU,EAAE,UAAU,CAAC,EAAE;QACzB,MAAM;QACN,QAAQ,EAAE,yBAAyB,CAAC,WAAW,CAAC,KAAK,CAAC;QACtD,MAAM,EAAE,IAAI;KACb,CAAC;IACF,+FAA+F;IAC/F,sEAAsE;IACtE,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC9B,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;IACjC,CAAC;IACD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IACD,IAAI,sBAAsB,KAAK,SAAS,EAAE,CAAC;QACzC,IAAI,CAAC,aAAa,GAAG,sBAAsB,CAAC;IAC9C,CAAC;IACD,IAAI,KAAK,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC9B,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACrB,CAAC;IACD,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;IACD,0FAA0F;IAC1F,+EAA+E;IAC/E,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC5B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC7B,CAAC;IACD,+FAA+F;IAC/F,+FAA+F;IAC/F,+FAA+F;IAC/F,0FAA0F;IAC1F,uFAAuF;IACvF,gGAAgG;IAChG,+FAA+F;IAC/F,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACrB,CAAC;IAED,wCAAwC;IACxC,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,IAAI,CAAC,wCAAwC,UAAU,CAAC,EAAE,WAAW,QAAQ,EAAE,CAAC,CAAC;IAEzF,MAAM,GAAG,GAAG,GAAG,QAAQ,2BAA2B,CAAC;IACnD,MAAM,IAAI,GAAG,MAAM,iBAAiB,CAAC,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACpE,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,OAAO,CAAC,oBAAoB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;AAC/E,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 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"]}
|
|
@@ -29,6 +29,7 @@ import { fail } from '@fgv/ts-utils';
|
|
|
29
29
|
import { splitChatRequest } from './chatRequestBuilders';
|
|
30
30
|
import { resolveEffectiveBaseUrl } from './endpoint';
|
|
31
31
|
import { isAdaptiveThinkingModel, isResponsesOnlyModel, resolveProviderModel, usesMaxCompletionTokensField } from './model';
|
|
32
|
+
import { supportsCacheUsageReporting, supportsPromptCacheBreakpoints, supportsPromptCacheRouting, supportsStreamUsageOption } from './streamUsageCapability';
|
|
32
33
|
import { callAnthropicStream } from './streamingAdapters/anthropic';
|
|
33
34
|
import { callGeminiStream } from './streamingAdapters/gemini';
|
|
34
35
|
import { callOpenAiChatStream } from './streamingAdapters/openaiChat';
|
|
@@ -62,6 +63,11 @@ export { executeClientToolTurn } from './streamingAdapters/clientToolContinuatio
|
|
|
62
63
|
* `Result.fail` before fetch is invoked. Callers should route through
|
|
63
64
|
* {@link AiAssist.callProxiedCompletionStream} or surface the failure to the user.
|
|
64
65
|
*
|
|
66
|
+
* This guard is applied **here only**. `executeClientToolTurn` is also a
|
|
67
|
+
* streaming entry point and does not pre-flight the flag, so a browser
|
|
68
|
+
* client-tool turn against a `streamingCorsRestricted` provider fails at the
|
|
69
|
+
* fetch with a CORS error rather than with this legible refusal.
|
|
70
|
+
*
|
|
65
71
|
* Connection-time failures (auth, network, non-2xx) surface as the outer
|
|
66
72
|
* `Result.fail`. Once iteration begins, errors mid-stream surface as a
|
|
67
73
|
* terminal error event ({@link AiAssist.IAiStreamError}) followed by the iterable
|
|
@@ -72,7 +78,7 @@ export { executeClientToolTurn } from './streamingAdapters/clientToolContinuatio
|
|
|
72
78
|
* @public
|
|
73
79
|
*/
|
|
74
80
|
export async function callProviderCompletionStream(params) {
|
|
75
|
-
const { descriptor, apiKey, system, messages, temperature, modelOverride, tier, logger, tools, signal, endpoint, thinking, maxTokens } = params;
|
|
81
|
+
const { descriptor, apiKey, system, messages, temperature, modelOverride, tier, logger, tools, signal, endpoint, thinking, maxTokens, cache } = params;
|
|
76
82
|
const splitResult = splitChatRequest(system, messages);
|
|
77
83
|
if (splitResult.isFailure()) {
|
|
78
84
|
return fail(splitResult.message);
|
|
@@ -119,15 +125,24 @@ export async function callProviderCompletionStream(params) {
|
|
|
119
125
|
model
|
|
120
126
|
};
|
|
121
127
|
switch (descriptor.apiFormat) {
|
|
122
|
-
case 'openai':
|
|
128
|
+
case 'openai': {
|
|
129
|
+
// Same gating as callProviderCompletion's 'openai' case in completionClient.ts — see the
|
|
130
|
+
// comment there for why breakpoints and the routing key are gated independently.
|
|
131
|
+
const routing = supportsPromptCacheRouting(descriptor);
|
|
132
|
+
const breakpoints = supportsPromptCacheBreakpoints(descriptor) ? cache === null || cache === void 0 ? void 0 : cache.systemBreakpoints : undefined;
|
|
133
|
+
const routedKey = routing !== undefined ? cache === null || cache === void 0 ? void 0 : cache.cacheKey : undefined;
|
|
134
|
+
const gatedCache = breakpoints !== undefined || routedKey !== undefined
|
|
135
|
+
? { systemBreakpoints: breakpoints, cacheKey: routedKey }
|
|
136
|
+
: undefined;
|
|
123
137
|
// Responses-API-only models (e.g. gpt-5.5-pro) 400 on /chat/completions, so they route
|
|
124
138
|
// to the Responses stream even with no tools requested — same path the tools case uses.
|
|
125
139
|
if (hasTools || isResponsesOnlyModel(descriptor, config.model)) {
|
|
126
|
-
return callOpenAiResponsesStream(config, prompt, tools, head, temperature, logger, signal, resolvedThinking, undefined, undefined, maxTokens);
|
|
140
|
+
return callOpenAiResponsesStream(config, prompt, tools, head, temperature, logger, signal, resolvedThinking, undefined, undefined, maxTokens, supportsCacheUsageReporting(descriptor), gatedCache);
|
|
127
141
|
}
|
|
128
|
-
return callOpenAiChatStream(config, prompt, head, temperature, logger, signal, resolvedThinking, maxTokens, usesMaxCompletionTokensField(descriptor));
|
|
142
|
+
return callOpenAiChatStream(config, prompt, head, temperature, logger, signal, resolvedThinking, maxTokens, usesMaxCompletionTokensField(descriptor), supportsStreamUsageOption(descriptor), gatedCache, routing === null || routing === void 0 ? void 0 : routing.chatCompletionsHeader);
|
|
143
|
+
}
|
|
129
144
|
case 'anthropic':
|
|
130
|
-
return callAnthropicStream(config, prompt, head, temperature, tools, logger, signal, resolvedThinking, undefined, undefined, isAdaptiveThinkingModel(descriptor, config.model), maxTokens);
|
|
145
|
+
return callAnthropicStream(config, prompt, head, temperature, tools, logger, signal, resolvedThinking, undefined, undefined, isAdaptiveThinkingModel(descriptor, config.model), maxTokens, cache);
|
|
131
146
|
case 'gemini':
|
|
132
147
|
return callGeminiStream(config, prompt, head, temperature, tools, logger, signal, resolvedThinking, undefined, undefined, maxTokens);
|
|
133
148
|
/* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"streamingClient.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/streamingClient.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 chat completion client. Public façade over the per-format\n * adapters under `streamingAdapters/`: provider dispatch and pre-flight\n * validation live here; format-specific request/translation logic and the\n * typed event-payload validators live with each adapter.\n *\n * @packageDocumentation\n */\n\nimport { fail, Result } from '@fgv/ts-utils';\n\nimport { splitChatRequest } from './chatRequestBuilders';\nimport { resolveEffectiveBaseUrl } from './endpoint';\nimport {\n type IAiStreamEvent,\n type ModelSpecKey,\n isAdaptiveThinkingModel,\n isResponsesOnlyModel,\n resolveProviderModel,\n usesMaxCompletionTokensField\n} from './model';\nimport { callAnthropicStream } from './streamingAdapters/anthropic';\nimport { type IProviderCompletionStreamParams, type IStreamApiConfig } from './streamingAdapters/common';\nimport { callGeminiStream } from './streamingAdapters/gemini';\nimport { callOpenAiChatStream } from './streamingAdapters/openaiChat';\nimport { callOpenAiResponsesStream } from './streamingAdapters/openaiResponses';\nimport {\n checkTemperatureConflict,\n mergeThinkingConfig,\n providerDiscriminatorForId,\n type IResolvedThinkingConfig\n} from './thinkingOptionsResolver';\n\nexport { callProxiedCompletionStream } from './streamingAdapters/proxy';\nexport type { IProviderCompletionStreamParams } from './streamingAdapters/common';\n\n/**\n * Re-export `executeClientToolTurn` from the continuation builder so callers\n * can import it directly from the streaming-client surface.\n *\n * @remarks\n * **Why `clientTools` does not flow through `callProviderCompletionStream`:**\n * Client tools require a stateful per-stream accumulation buffer (for thinking\n * blocks, tool-use args, Gemini functionCall accumulation) that is owned by the\n * continuation builder's execution context. Passing `clientTools` through the\n * generic `callProviderCompletionStream` entry would require the caller to manage\n * the accumulation buffer externally. `executeClientToolTurn` is therefore the\n * canonical (and only) call path for client tools in Phase C. Callers that need\n * both server tools and client tools in the same request should use\n * `executeClientToolTurn` with both `tools` (server) and `clientTools` present;\n * `callProviderCompletionStream` is for server-tools-only or no-tools requests.\n */\n/* c8 ignore next 5 - barrel re-export; function is covered by clientToolContinuationBuilder tests */\nexport {\n executeClientToolTurn,\n type IExecuteClientToolTurnParams,\n type IExecuteClientToolTurnResult,\n type IToolExecutionDecision\n} from './streamingAdapters/clientToolContinuationBuilder';\n\n/**\n * Calls the appropriate streaming chat completion API for a given provider.\n *\n * @remarks\n * Pre-flight rejection: when `descriptor.streamingCorsRestricted === true`\n * and the call isn't being routed through a proxy, this returns\n * `Result.fail` before fetch is invoked. Callers should route through\n * {@link AiAssist.callProxiedCompletionStream} or surface the failure to the user.\n *\n * Connection-time failures (auth, network, non-2xx) surface as the outer\n * `Result.fail`. Once iteration begins, errors mid-stream surface as a\n * terminal error event ({@link AiAssist.IAiStreamError}) followed by the iterable\n * ending. The final successful event is {@link AiAssist.IAiStreamDone}.\n *\n * @param params - Request parameters including descriptor, API key, prompt, and optional tools\n * @returns A streaming iterable of unified events, or a Result.fail\n * @public\n */\nexport async function callProviderCompletionStream(\n params: IProviderCompletionStreamParams\n): Promise<Result<AsyncIterable<IAiStreamEvent>>> {\n const {\n descriptor,\n apiKey,\n system,\n messages,\n temperature,\n modelOverride,\n tier,\n logger,\n tools,\n signal,\n endpoint,\n thinking,\n maxTokens\n } = params;\n\n const splitResult = splitChatRequest(system, messages);\n if (splitResult.isFailure()) {\n return fail(splitResult.message);\n }\n const { prompt, head } = splitResult.value;\n\n const baseUrlResult = resolveEffectiveBaseUrl(descriptor, endpoint);\n if (baseUrlResult.isFailure()) {\n return fail(baseUrlResult.message);\n }\n if (descriptor.streamingCorsRestricted) {\n return fail(`provider \"${descriptor.id}\" requires a proxy for streaming; none is configured`);\n }\n if (prompt.attachments.length > 0 && !descriptor.acceptsImageInput) {\n return fail(`provider \"${descriptor.id}\" does not accept image input`);\n }\n\n const hasTools = tools !== undefined && tools.length > 0;\n const discriminator = providerDiscriminatorForId(descriptor.id);\n // The quality tier is the only completion-model selector; thinking and tools\n // are orthogonal request params/capabilities and never pick a model.\n const modelContext: ModelSpecKey | undefined = tier;\n\n const modelResult = resolveProviderModel(descriptor, modelOverride, modelContext);\n if (modelResult.isFailure()) {\n return fail(modelResult.message);\n }\n const model = modelResult.value;\n\n let resolvedThinking: IResolvedThinkingConfig | undefined;\n if (thinking !== undefined) {\n if (discriminator !== undefined) {\n const mergeResult = mergeThinkingConfig(thinking, model, discriminator);\n /* c8 ignore next 3 - mergeThinkingConfig always succeeds; defensive guard */\n if (mergeResult.isFailure()) {\n return fail(mergeResult.message);\n }\n resolvedThinking = mergeResult.value;\n const conflictResult = checkTemperatureConflict(resolvedThinking, discriminator, temperature);\n if (conflictResult.isFailure()) {\n return fail(conflictResult.message);\n }\n }\n }\n\n const config: IStreamApiConfig = {\n baseUrl: baseUrlResult.value,\n apiKey,\n model\n };\n\n switch (descriptor.apiFormat) {\n case 'openai':\n // Responses-API-only models (e.g. gpt-5.5-pro) 400 on /chat/completions, so they route\n // to the Responses stream even with no tools requested — same path the tools case uses.\n if (hasTools || isResponsesOnlyModel(descriptor, config.model)) {\n return callOpenAiResponsesStream(\n config,\n prompt,\n tools,\n head,\n temperature,\n logger,\n signal,\n resolvedThinking,\n undefined,\n undefined,\n maxTokens\n );\n }\n return callOpenAiChatStream(\n config,\n prompt,\n head,\n temperature,\n logger,\n signal,\n resolvedThinking,\n maxTokens,\n usesMaxCompletionTokensField(descriptor)\n );\n case 'anthropic':\n return callAnthropicStream(\n config,\n prompt,\n head,\n temperature,\n tools,\n logger,\n signal,\n resolvedThinking,\n undefined,\n undefined,\n isAdaptiveThinkingModel(descriptor, config.model),\n maxTokens\n );\n case 'gemini':\n return callGeminiStream(\n config,\n prompt,\n head,\n temperature,\n tools,\n logger,\n signal,\n resolvedThinking,\n undefined,\n undefined,\n maxTokens\n );\n /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */\n default: {\n const _exhaustive: never = descriptor.apiFormat;\n return fail(`unsupported API format: ${String(_exhaustive)}`);\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"streamingClient.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/streamingClient.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 chat completion client. Public façade over the per-format\n * adapters under `streamingAdapters/`: provider dispatch and pre-flight\n * validation live here; format-specific request/translation logic and the\n * typed event-payload validators live with each adapter.\n *\n * @packageDocumentation\n */\n\nimport { fail, Result } from '@fgv/ts-utils';\n\nimport { type IAiCacheRequest } from './cacheRequest';\nimport { splitChatRequest } from './chatRequestBuilders';\nimport { resolveEffectiveBaseUrl } from './endpoint';\nimport {\n type IAiStreamEvent,\n type ModelSpecKey,\n isAdaptiveThinkingModel,\n isResponsesOnlyModel,\n resolveProviderModel,\n usesMaxCompletionTokensField\n} from './model';\nimport {\n supportsCacheUsageReporting,\n supportsPromptCacheBreakpoints,\n supportsPromptCacheRouting,\n supportsStreamUsageOption\n} from './streamUsageCapability';\nimport { callAnthropicStream } from './streamingAdapters/anthropic';\nimport { type IProviderCompletionStreamParams, type IStreamApiConfig } from './streamingAdapters/common';\nimport { callGeminiStream } from './streamingAdapters/gemini';\nimport { callOpenAiChatStream } from './streamingAdapters/openaiChat';\nimport { callOpenAiResponsesStream } from './streamingAdapters/openaiResponses';\nimport {\n checkTemperatureConflict,\n mergeThinkingConfig,\n providerDiscriminatorForId,\n type IResolvedThinkingConfig\n} from './thinkingOptionsResolver';\n\nexport { callProxiedCompletionStream } from './streamingAdapters/proxy';\nexport type { IProviderCompletionStreamParams } from './streamingAdapters/common';\n\n/**\n * Re-export `executeClientToolTurn` from the continuation builder so callers\n * can import it directly from the streaming-client surface.\n *\n * @remarks\n * **Why `clientTools` does not flow through `callProviderCompletionStream`:**\n * Client tools require a stateful per-stream accumulation buffer (for thinking\n * blocks, tool-use args, Gemini functionCall accumulation) that is owned by the\n * continuation builder's execution context. Passing `clientTools` through the\n * generic `callProviderCompletionStream` entry would require the caller to manage\n * the accumulation buffer externally. `executeClientToolTurn` is therefore the\n * canonical (and only) call path for client tools in Phase C. Callers that need\n * both server tools and client tools in the same request should use\n * `executeClientToolTurn` with both `tools` (server) and `clientTools` present;\n * `callProviderCompletionStream` is for server-tools-only or no-tools requests.\n */\n/* c8 ignore next 5 - barrel re-export; function is covered by clientToolContinuationBuilder tests */\nexport {\n executeClientToolTurn,\n type IExecuteClientToolTurnParams,\n type IExecuteClientToolTurnResult,\n type IToolExecutionDecision\n} from './streamingAdapters/clientToolContinuationBuilder';\n\n/**\n * Calls the appropriate streaming chat completion API for a given provider.\n *\n * @remarks\n * Pre-flight rejection: when `descriptor.streamingCorsRestricted === true`\n * and the call isn't being routed through a proxy, this returns\n * `Result.fail` before fetch is invoked. Callers should route through\n * {@link AiAssist.callProxiedCompletionStream} or surface the failure to the user.\n *\n * This guard is applied **here only**. `executeClientToolTurn` is also a\n * streaming entry point and does not pre-flight the flag, so a browser\n * client-tool turn against a `streamingCorsRestricted` provider fails at the\n * fetch with a CORS error rather than with this legible refusal.\n *\n * Connection-time failures (auth, network, non-2xx) surface as the outer\n * `Result.fail`. Once iteration begins, errors mid-stream surface as a\n * terminal error event ({@link AiAssist.IAiStreamError}) followed by the iterable\n * ending. The final successful event is {@link AiAssist.IAiStreamDone}.\n *\n * @param params - Request parameters including descriptor, API key, prompt, and optional tools\n * @returns A streaming iterable of unified events, or a Result.fail\n * @public\n */\nexport async function callProviderCompletionStream(\n params: IProviderCompletionStreamParams\n): Promise<Result<AsyncIterable<IAiStreamEvent>>> {\n const {\n descriptor,\n apiKey,\n system,\n messages,\n temperature,\n modelOverride,\n tier,\n logger,\n tools,\n signal,\n endpoint,\n thinking,\n maxTokens,\n cache\n } = params;\n\n const splitResult = splitChatRequest(system, messages);\n if (splitResult.isFailure()) {\n return fail(splitResult.message);\n }\n const { prompt, head } = splitResult.value;\n\n const baseUrlResult = resolveEffectiveBaseUrl(descriptor, endpoint);\n if (baseUrlResult.isFailure()) {\n return fail(baseUrlResult.message);\n }\n if (descriptor.streamingCorsRestricted) {\n return fail(`provider \"${descriptor.id}\" requires a proxy for streaming; none is configured`);\n }\n if (prompt.attachments.length > 0 && !descriptor.acceptsImageInput) {\n return fail(`provider \"${descriptor.id}\" does not accept image input`);\n }\n\n const hasTools = tools !== undefined && tools.length > 0;\n const discriminator = providerDiscriminatorForId(descriptor.id);\n // The quality tier is the only completion-model selector; thinking and tools\n // are orthogonal request params/capabilities and never pick a model.\n const modelContext: ModelSpecKey | undefined = tier;\n\n const modelResult = resolveProviderModel(descriptor, modelOverride, modelContext);\n if (modelResult.isFailure()) {\n return fail(modelResult.message);\n }\n const model = modelResult.value;\n\n let resolvedThinking: IResolvedThinkingConfig | undefined;\n if (thinking !== undefined) {\n if (discriminator !== undefined) {\n const mergeResult = mergeThinkingConfig(thinking, model, discriminator);\n /* c8 ignore next 3 - mergeThinkingConfig always succeeds; defensive guard */\n if (mergeResult.isFailure()) {\n return fail(mergeResult.message);\n }\n resolvedThinking = mergeResult.value;\n const conflictResult = checkTemperatureConflict(resolvedThinking, discriminator, temperature);\n if (conflictResult.isFailure()) {\n return fail(conflictResult.message);\n }\n }\n }\n\n const config: IStreamApiConfig = {\n baseUrl: baseUrlResult.value,\n apiKey,\n model\n };\n\n switch (descriptor.apiFormat) {\n case 'openai': {\n // Same gating as callProviderCompletion's 'openai' case in completionClient.ts — see the\n // comment there for why breakpoints and the routing key are gated independently.\n const routing = supportsPromptCacheRouting(descriptor);\n const breakpoints = supportsPromptCacheBreakpoints(descriptor) ? cache?.systemBreakpoints : undefined;\n const routedKey = routing !== undefined ? cache?.cacheKey : undefined;\n const gatedCache: IAiCacheRequest | undefined =\n breakpoints !== undefined || routedKey !== undefined\n ? { systemBreakpoints: breakpoints, cacheKey: routedKey }\n : undefined;\n // Responses-API-only models (e.g. gpt-5.5-pro) 400 on /chat/completions, so they route\n // to the Responses stream even with no tools requested — same path the tools case uses.\n if (hasTools || isResponsesOnlyModel(descriptor, config.model)) {\n return callOpenAiResponsesStream(\n config,\n prompt,\n tools,\n head,\n temperature,\n logger,\n signal,\n resolvedThinking,\n undefined,\n undefined,\n maxTokens,\n supportsCacheUsageReporting(descriptor),\n gatedCache\n );\n }\n return callOpenAiChatStream(\n config,\n prompt,\n head,\n temperature,\n logger,\n signal,\n resolvedThinking,\n maxTokens,\n usesMaxCompletionTokensField(descriptor),\n supportsStreamUsageOption(descriptor),\n gatedCache,\n routing?.chatCompletionsHeader\n );\n }\n case 'anthropic':\n return callAnthropicStream(\n config,\n prompt,\n head,\n temperature,\n tools,\n logger,\n signal,\n resolvedThinking,\n undefined,\n undefined,\n isAdaptiveThinkingModel(descriptor, config.model),\n maxTokens,\n cache\n );\n case 'gemini':\n return callGeminiStream(\n config,\n prompt,\n head,\n temperature,\n tools,\n logger,\n signal,\n resolvedThinking,\n undefined,\n undefined,\n maxTokens\n );\n /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */\n default: {\n const _exhaustive: never = descriptor.apiFormat;\n return fail(`unsupported API format: ${String(_exhaustive)}`);\n }\n }\n}\n"]}
|
|
@@ -41,7 +41,10 @@ export function providerDiscriminatorForId(providerId) {
|
|
|
41
41
|
// Common-subset mapping
|
|
42
42
|
// ============================================================================
|
|
43
43
|
/**
|
|
44
|
-
* Maps generic effort to Anthropic wire effort.
|
|
44
|
+
* Maps generic effort to Anthropic wire effort. Anthropic has no 'none' value in its
|
|
45
|
+
* own vocabulary — the caller (tier 1 of `mergeThinkingConfig`) handles `'none'` by
|
|
46
|
+
* skipping this mapping entirely, so `anthropicEffort` stays unset and the emit site
|
|
47
|
+
* omits the `thinking` wire param. @internal
|
|
45
48
|
*/
|
|
46
49
|
function genericEffortToAnthropic(effort) {
|
|
47
50
|
return effort; // 1:1 mapping for the common subset
|
|
@@ -52,8 +55,11 @@ function genericEffortToAnthropic(effort) {
|
|
|
52
55
|
*
|
|
53
56
|
* Policy: low = 2048, medium = 8192, high = 24000, max = 32000. The lower three
|
|
54
57
|
* align with the Anthropic-published minimum-meaningful budget, a mid-range
|
|
55
|
-
* default, and a "deep thinking" allotment respectively. `max`
|
|
56
|
-
*
|
|
58
|
+
* default, and a "deep thinking" allotment respectively. `max` is the deepest
|
|
59
|
+
* allotment and stays within typical model limits.
|
|
60
|
+
*
|
|
61
|
+
* Only reached for models outside the Claude 5 family — the adaptive-thinking path
|
|
62
|
+
* sends no budget at all.
|
|
57
63
|
*
|
|
58
64
|
* @public
|
|
59
65
|
*/
|
|
@@ -76,10 +82,16 @@ function genericEffortToOpenAi(effort) {
|
|
|
76
82
|
return effort; // 1:1 mapping for the common subset
|
|
77
83
|
}
|
|
78
84
|
/**
|
|
79
|
-
* Maps generic effort to Gemini thinkingBudget.
|
|
85
|
+
* Maps generic effort to Gemini thinkingBudget. `'none'` maps to `0` — Gemini's own
|
|
86
|
+
* off value on the wire, though `IGeminiThinkingConfig.thinkingBudget` documents that `0`
|
|
87
|
+
* is valid only on Flash/Flash-Lite and errors on Pro; this mapping is not model-aware and
|
|
88
|
+
* inherits that same caveat (not a new gap — an explicit `providers` block could already
|
|
89
|
+
* request `thinkingBudget: 0` on any model). @internal
|
|
80
90
|
*/
|
|
81
91
|
function genericEffortToGemini(effort) {
|
|
82
92
|
switch (effort) {
|
|
93
|
+
case 'none':
|
|
94
|
+
return 0;
|
|
83
95
|
case 'low':
|
|
84
96
|
return 1024;
|
|
85
97
|
case 'medium':
|
|
@@ -170,7 +182,13 @@ export function mergeThinkingConfig(config, resolvedModel, discriminator) {
|
|
|
170
182
|
if (config.effort !== undefined) {
|
|
171
183
|
switch (discriminator) {
|
|
172
184
|
case 'anthropic':
|
|
173
|
-
|
|
185
|
+
// Anthropic has no 'none' value: "off" means no `thinking` wire param at all,
|
|
186
|
+
// so 'none' leaves `anthropicEffort` unset rather than mapping to a value. That
|
|
187
|
+
// also satisfies checkTemperatureConflict's anthropic gate (which fails only when
|
|
188
|
+
// anthropicEffort is set), so temperature survives without any special-casing there.
|
|
189
|
+
if (config.effort !== 'none') {
|
|
190
|
+
resolved = Object.assign(Object.assign({}, resolved), { anthropicEffort: genericEffortToAnthropic(config.effort) });
|
|
191
|
+
}
|
|
174
192
|
break;
|
|
175
193
|
case 'openai':
|
|
176
194
|
resolved = Object.assign(Object.assign({}, resolved), { openAiEffort: genericEffortToOpenAi(config.effort) });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"thinkingOptionsResolver.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/thinkingOptionsResolver.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 * Merge logic and runtime validation for thinking/reasoning options.\n * @packageDocumentation\n */\n\nimport { type JsonObject } from '@fgv/ts-json-base';\nimport { fail, Result, succeed } from '@fgv/ts-utils';\n\nimport type {\n IThinkingConfig,\n IThinkingProviderConfig,\n IAnthropicThinkingConfig,\n IOpenAiThinkingConfig,\n IXAiThinkingConfig\n} from './model';\n\n// ============================================================================\n// Provider discriminator\n// ============================================================================\n\n/**\n * Coarse provider family used to discriminate thinking config blocks.\n * Maps from AiProviderId to the IThinkingProviderConfig `provider` discriminator.\n * @internal\n */\nexport type ThinkingProviderDiscriminator = 'anthropic' | 'openai' | 'google' | 'xai';\n\n/**\n * Maps an AiProviderId (registry key) to the coarse family discriminator used\n * in IThinkingProviderConfig. Returns undefined for providers without thinking support.\n * @internal\n */\nexport function providerDiscriminatorForId(providerId: string): ThinkingProviderDiscriminator | undefined {\n switch (providerId) {\n case 'anthropic':\n return 'anthropic';\n case 'openai':\n return 'openai';\n case 'google-gemini':\n return 'google';\n case 'xai-grok':\n return 'xai';\n default:\n return undefined;\n }\n}\n\n// ============================================================================\n// Resolved wire shape\n// ============================================================================\n\n/**\n * Resolved thinking wire parameters for a specific provider, after merging\n * all applicable config blocks. Ready for provider-specific wire encoding.\n *\n * Callers that pre-resolve thinking config outside of the standard streaming\n * helpers (e.g. `executeClientToolTurn`) accept this type via the\n * `resolvedThinking` parameter and pass it directly to the adapter layer.\n * @public\n */\nexport interface IResolvedThinkingConfig {\n /** Anthropic: effort level; emit-site converts to `thinking.budget_tokens` via `anthropicEffortToBudgetTokens`. */\n readonly anthropicEffort?: IAnthropicThinkingConfig['effort'];\n /** OpenAI Chat: reasoning_effort value; OpenAI Responses: reasoning.effort */\n readonly openAiEffort?: IOpenAiThinkingConfig['effort'];\n /** Gemini: generationConfig.thinkingConfig.thinkingBudget */\n readonly geminiThinkingBudget?: number;\n /** xAI: reasoning_effort value (omit for grok-4) */\n readonly xaiEffort?: IXAiThinkingConfig['effort'];\n /** Other/passthrough: merged verbatim into wire request */\n readonly otherParams?: JsonObject;\n}\n\n// ============================================================================\n// Common-subset mapping\n// ============================================================================\n\n/**\n * Maps generic effort to Anthropic wire effort. @internal\n */\nfunction genericEffortToAnthropic(effort: 'low' | 'medium' | 'high'): IAnthropicThinkingConfig['effort'] {\n return effort; // 1:1 mapping for the common subset\n}\n\n/**\n * Maps Anthropic effort level to the `thinking.budget_tokens` integer that the\n * Anthropic API requires when `thinking.type === 'enabled'`.\n *\n * Policy: low = 2048, medium = 8192, high = 24000, max = 32000. The lower three\n * align with the Anthropic-published minimum-meaningful budget, a mid-range\n * default, and a \"deep thinking\" allotment respectively. `max` targets Opus 4.6's\n * deepest budget and stays within typical model limits.\n *\n * @public\n */\nexport function anthropicEffortToBudgetTokens(\n effort: NonNullable<IAnthropicThinkingConfig['effort']>\n): number {\n switch (effort) {\n case 'low':\n return 2048;\n case 'medium':\n return 8192;\n case 'high':\n return 24000;\n case 'max':\n return 32000;\n }\n}\n\n/**\n * Maps generic effort to OpenAI wire effort. @internal\n */\nfunction genericEffortToOpenAi(effort: 'low' | 'medium' | 'high'): IOpenAiThinkingConfig['effort'] {\n return effort; // 1:1 mapping for the common subset\n}\n\n/**\n * Maps generic effort to Gemini thinkingBudget. @internal\n */\nfunction genericEffortToGemini(effort: 'low' | 'medium' | 'high'): number {\n switch (effort) {\n case 'low':\n return 1024;\n case 'medium':\n return 4096;\n case 'high':\n return 8192;\n }\n}\n\n/**\n * Maps generic effort to xAI reasoning_effort. @internal\n */\nfunction genericEffortToXai(effort: 'low' | 'medium' | 'high'): IXAiThinkingConfig['effort'] {\n return effort; // 1:1 mapping for the common subset\n}\n\n// ============================================================================\n// Block applicability\n// ============================================================================\n\n/**\n * Returns true when a provider config block applies to the given resolved model\n * and provider discriminator.\n *\n * Applicability rules:\n * - provider must match the coarse discriminator\n * - if models array is present, resolved model must match (exact or base-name prefix)\n * - if models array is absent, the block is provider-generic (applies to all)\n *\n * Prefix matching supports versioned IDs: `'claude-sonnet-4-5'` matches resolved\n * `'claude-sonnet-4-5-20250929'`. An entry matches when it equals the resolved model\n * or when the resolved model starts with the entry followed by a `-`.\n *\n * 'other' blocks require models to be present (enforced by the type).\n * @internal\n */\nfunction modelNameMatches(resolvedModel: string, name: string): boolean {\n return resolvedModel === name || resolvedModel.startsWith(`${name}-`);\n}\n\nfunction blockApplies(\n block: IThinkingProviderConfig,\n resolvedModel: string,\n discriminator: ThinkingProviderDiscriminator\n): boolean {\n if (block.provider !== discriminator && block.provider !== 'other') {\n return false;\n }\n if (block.provider === 'other') {\n return block.models.some((name) => modelNameMatches(resolvedModel, name));\n }\n if (block.models !== undefined) {\n return (block.models as ReadonlyArray<string>).some((name) => modelNameMatches(resolvedModel, name));\n }\n return true; // provider-generic block\n}\n\n/**\n * Returns true when a block is model-specific (has a models array).\n * Used to partition blocks into merge tiers.\n * @internal\n */\nfunction isModelSpecific(block: IThinkingProviderConfig): boolean {\n if (block.provider === 'other') {\n return true; // other blocks always require models\n }\n return block.models !== undefined;\n}\n\n// ============================================================================\n// Merge function\n// ============================================================================\n\n/**\n * Resolves the effective thinking wire parameters for a specific resolved model\n * by merging all applicable config blocks in precedence order.\n *\n * Precedence (later tier wins; within tier, later declaration wins):\n * 1. Generic effort (top-level IThinkingConfig.effort) → common-subset mapping\n * 2. Provider-generic blocks (matching provider, no models filter)\n * 3. Model-specific blocks (matching provider + models array includes resolved model)\n * 4. Other blocks (provider: 'other', models includes resolved model) — same tier as 3\n *\n * Blocks whose provider does not match are silently skipped.\n *\n * Note: when the resolved OpenAI effort is `'none'`, reasoning is disabled and\n * temperature is accepted; see {@link IOpenAiThinkingConfig.effort} for the full\n * hybrid-mode semantics.\n *\n * @param config - The caller's IThinkingConfig\n * @param resolvedModel - The concrete model string after registry resolution\n * @param discriminator - Coarse provider family\n * @returns Merged effective config for wire encoding\n * @internal\n */\nexport function mergeThinkingConfig(\n config: IThinkingConfig,\n resolvedModel: string,\n discriminator: ThinkingProviderDiscriminator\n): Result<IResolvedThinkingConfig> {\n let resolved: IResolvedThinkingConfig = {};\n\n // Tier 1: generic effort → common-subset mapping\n if (config.effort !== undefined) {\n switch (discriminator) {\n case 'anthropic':\n resolved = { ...resolved, anthropicEffort: genericEffortToAnthropic(config.effort) };\n break;\n case 'openai':\n resolved = { ...resolved, openAiEffort: genericEffortToOpenAi(config.effort) };\n break;\n case 'google':\n resolved = { ...resolved, geminiThinkingBudget: genericEffortToGemini(config.effort) };\n break;\n case 'xai':\n resolved = { ...resolved, xaiEffort: genericEffortToXai(config.effort) };\n break;\n }\n }\n\n if (!config.providers) {\n return succeed(resolved);\n }\n\n // Partition into tiers 2 and 3+4\n const applicableBlocks = config.providers.filter((b) => blockApplies(b, resolvedModel, discriminator));\n const genericBlocks = applicableBlocks.filter((b) => !isModelSpecific(b));\n const specificBlocks = applicableBlocks.filter((b) => isModelSpecific(b));\n\n // Tier 2: provider-generic blocks (declaration order; later wins)\n for (const block of genericBlocks) {\n resolved = applyBlock(resolved, block, discriminator);\n }\n\n // Tier 3+4: model-specific + other blocks (declaration order; later wins)\n for (const block of specificBlocks) {\n resolved = applyBlock(resolved, block, discriminator);\n }\n\n return succeed(resolved);\n}\n\n/**\n * Applies a single config block to the accumulated resolved config.\n * @internal\n */\nfunction applyBlock(\n current: IResolvedThinkingConfig,\n block: IThinkingProviderConfig,\n discriminator: ThinkingProviderDiscriminator\n): IResolvedThinkingConfig {\n if (block.provider === 'other') {\n const merged =\n current.otherParams !== undefined ? { ...current.otherParams, ...block.config } : block.config;\n return { ...current, otherParams: merged };\n }\n\n switch (discriminator) {\n case 'anthropic':\n if (block.provider === 'anthropic') {\n if (block.config.effort !== undefined) {\n return { ...current, anthropicEffort: block.config.effort };\n }\n }\n break;\n case 'openai':\n if (block.provider === 'openai') {\n if (block.config.effort !== undefined) {\n return { ...current, openAiEffort: block.config.effort };\n }\n }\n break;\n case 'google':\n if (block.provider === 'google') {\n const updated: IResolvedThinkingConfig = { ...current };\n if (block.config.thinkingBudget !== undefined) {\n return { ...updated, geminiThinkingBudget: block.config.thinkingBudget };\n }\n return updated;\n }\n /* c8 ignore next - blockApplies guarantees provider match; unreachable for google */\n break;\n case 'xai':\n if (block.provider === 'xai') {\n if (block.config.effort !== undefined) {\n return { ...current, xaiEffort: block.config.effort };\n }\n }\n break;\n }\n return current;\n}\n\n// ============================================================================\n// Temperature conflict check\n// ============================================================================\n\n/**\n * Returns a Result.fail if temperature conflicts with thinking mode for the\n * given provider, otherwise succeed(undefined).\n *\n * Per D4: temperature + thinking = Result.fail for Anthropic, OpenAI (when\n * effective effort is non-null and non-'none'), and xAI (conservative default\n * pending live verification). Gemini accepts temperature alongside thinking.\n *\n * @internal\n */\nexport function checkTemperatureConflict(\n resolved: IResolvedThinkingConfig,\n discriminator: ThinkingProviderDiscriminator,\n temperature: number | undefined\n): Result<undefined> {\n if (temperature === undefined) {\n return succeed(undefined);\n }\n\n switch (discriminator) {\n case 'anthropic':\n if (resolved.anthropicEffort !== undefined) {\n return fail(\n 'thinking mode is not compatible with temperature on provider anthropic: remove temperature or disable thinking'\n );\n }\n break;\n case 'openai':\n // 'none' disables reasoning; temperature is accepted in that case\n if (resolved.openAiEffort !== undefined && resolved.openAiEffort !== 'none') {\n return fail(\n 'thinking mode is not compatible with temperature on provider openai: remove temperature or disable thinking'\n );\n }\n break;\n case 'xai':\n // Conservative default: fail if xAI effort is active (per D8 — live verification pending)\n if (resolved.xaiEffort !== undefined && resolved.xaiEffort !== 'none') {\n return fail(\n 'thinking mode is not compatible with temperature on provider xai: remove temperature or disable thinking'\n );\n }\n break;\n case 'google':\n // Gemini accepts temperature alongside thinkingConfig — no conflict\n break;\n }\n return succeed(undefined);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"thinkingOptionsResolver.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/thinkingOptionsResolver.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 * Merge logic and runtime validation for thinking/reasoning options.\n * @packageDocumentation\n */\n\nimport { type JsonObject } from '@fgv/ts-json-base';\nimport { fail, Result, succeed } from '@fgv/ts-utils';\n\nimport type {\n IThinkingConfig,\n IThinkingProviderConfig,\n IAnthropicThinkingConfig,\n IOpenAiThinkingConfig,\n IXAiThinkingConfig\n} from './model';\n\n// ============================================================================\n// Provider discriminator\n// ============================================================================\n\n/**\n * Coarse provider family used to discriminate thinking config blocks.\n * Maps from AiProviderId to the IThinkingProviderConfig `provider` discriminator.\n * @internal\n */\nexport type ThinkingProviderDiscriminator = 'anthropic' | 'openai' | 'google' | 'xai';\n\n/**\n * Maps an AiProviderId (registry key) to the coarse family discriminator used\n * in IThinkingProviderConfig. Returns undefined for providers without thinking support.\n * @internal\n */\nexport function providerDiscriminatorForId(providerId: string): ThinkingProviderDiscriminator | undefined {\n switch (providerId) {\n case 'anthropic':\n return 'anthropic';\n case 'openai':\n return 'openai';\n case 'google-gemini':\n return 'google';\n case 'xai-grok':\n return 'xai';\n default:\n return undefined;\n }\n}\n\n// ============================================================================\n// Resolved wire shape\n// ============================================================================\n\n/**\n * Resolved thinking wire parameters for a specific provider, after merging\n * all applicable config blocks. Ready for provider-specific wire encoding.\n *\n * Callers that pre-resolve thinking config outside of the standard streaming\n * helpers (e.g. `executeClientToolTurn`) accept this type via the\n * `resolvedThinking` parameter and pass it directly to the adapter layer.\n * @public\n */\nexport interface IResolvedThinkingConfig {\n /**\n * Anthropic: effort level. The emit-site picks one of two wire shapes — on the\n * Claude 5 family it sends the effort string verbatim in `output_config` with no\n * budget; on older models it converts via `anthropicEffortToBudgetTokens`. See\n * {@link AiAssist.IAnthropicThinkingConfig.effort}.\n */\n readonly anthropicEffort?: IAnthropicThinkingConfig['effort'];\n /** OpenAI Chat: reasoning_effort value; OpenAI Responses: reasoning.effort */\n readonly openAiEffort?: IOpenAiThinkingConfig['effort'];\n /** Gemini: generationConfig.thinkingConfig.thinkingBudget */\n readonly geminiThinkingBudget?: number;\n /** xAI: reasoning_effort value (omit for grok-4) */\n readonly xaiEffort?: IXAiThinkingConfig['effort'];\n /** Other/passthrough: merged verbatim into wire request */\n readonly otherParams?: JsonObject;\n}\n\n// ============================================================================\n// Common-subset mapping\n// ============================================================================\n\n/**\n * Maps generic effort to Anthropic wire effort. Anthropic has no 'none' value in its\n * own vocabulary — the caller (tier 1 of `mergeThinkingConfig`) handles `'none'` by\n * skipping this mapping entirely, so `anthropicEffort` stays unset and the emit site\n * omits the `thinking` wire param. @internal\n */\nfunction genericEffortToAnthropic(effort: 'low' | 'medium' | 'high'): IAnthropicThinkingConfig['effort'] {\n return effort; // 1:1 mapping for the common subset\n}\n\n/**\n * Maps Anthropic effort level to the `thinking.budget_tokens` integer that the\n * Anthropic API requires when `thinking.type === 'enabled'`.\n *\n * Policy: low = 2048, medium = 8192, high = 24000, max = 32000. The lower three\n * align with the Anthropic-published minimum-meaningful budget, a mid-range\n * default, and a \"deep thinking\" allotment respectively. `max` is the deepest\n * allotment and stays within typical model limits.\n *\n * Only reached for models outside the Claude 5 family — the adaptive-thinking path\n * sends no budget at all.\n *\n * @public\n */\nexport function anthropicEffortToBudgetTokens(\n effort: NonNullable<IAnthropicThinkingConfig['effort']>\n): number {\n switch (effort) {\n case 'low':\n return 2048;\n case 'medium':\n return 8192;\n case 'high':\n return 24000;\n case 'max':\n return 32000;\n }\n}\n\n/**\n * Maps generic effort to OpenAI wire effort. @internal\n */\nfunction genericEffortToOpenAi(effort: 'none' | 'low' | 'medium' | 'high'): IOpenAiThinkingConfig['effort'] {\n return effort; // 1:1 mapping for the common subset\n}\n\n/**\n * Maps generic effort to Gemini thinkingBudget. `'none'` maps to `0` — Gemini's own\n * off value on the wire, though `IGeminiThinkingConfig.thinkingBudget` documents that `0`\n * is valid only on Flash/Flash-Lite and errors on Pro; this mapping is not model-aware and\n * inherits that same caveat (not a new gap — an explicit `providers` block could already\n * request `thinkingBudget: 0` on any model). @internal\n */\nfunction genericEffortToGemini(effort: 'none' | 'low' | 'medium' | 'high'): number {\n switch (effort) {\n case 'none':\n return 0;\n case 'low':\n return 1024;\n case 'medium':\n return 4096;\n case 'high':\n return 8192;\n }\n}\n\n/**\n * Maps generic effort to xAI reasoning_effort. @internal\n */\nfunction genericEffortToXai(effort: 'none' | 'low' | 'medium' | 'high'): IXAiThinkingConfig['effort'] {\n return effort; // 1:1 mapping for the common subset\n}\n\n// ============================================================================\n// Block applicability\n// ============================================================================\n\n/**\n * Returns true when a provider config block applies to the given resolved model\n * and provider discriminator.\n *\n * Applicability rules:\n * - provider must match the coarse discriminator\n * - if models array is present, resolved model must match (exact or base-name prefix)\n * - if models array is absent, the block is provider-generic (applies to all)\n *\n * Prefix matching supports versioned IDs: `'claude-sonnet-4-5'` matches resolved\n * `'claude-sonnet-4-5-20250929'`. An entry matches when it equals the resolved model\n * or when the resolved model starts with the entry followed by a `-`.\n *\n * 'other' blocks require models to be present (enforced by the type).\n * @internal\n */\nfunction modelNameMatches(resolvedModel: string, name: string): boolean {\n return resolvedModel === name || resolvedModel.startsWith(`${name}-`);\n}\n\nfunction blockApplies(\n block: IThinkingProviderConfig,\n resolvedModel: string,\n discriminator: ThinkingProviderDiscriminator\n): boolean {\n if (block.provider !== discriminator && block.provider !== 'other') {\n return false;\n }\n if (block.provider === 'other') {\n return block.models.some((name) => modelNameMatches(resolvedModel, name));\n }\n if (block.models !== undefined) {\n return (block.models as ReadonlyArray<string>).some((name) => modelNameMatches(resolvedModel, name));\n }\n return true; // provider-generic block\n}\n\n/**\n * Returns true when a block is model-specific (has a models array).\n * Used to partition blocks into merge tiers.\n * @internal\n */\nfunction isModelSpecific(block: IThinkingProviderConfig): boolean {\n if (block.provider === 'other') {\n return true; // other blocks always require models\n }\n return block.models !== undefined;\n}\n\n// ============================================================================\n// Merge function\n// ============================================================================\n\n/**\n * Resolves the effective thinking wire parameters for a specific resolved model\n * by merging all applicable config blocks in precedence order.\n *\n * Precedence (later tier wins; within tier, later declaration wins):\n * 1. Generic effort (top-level IThinkingConfig.effort) → common-subset mapping\n * 2. Provider-generic blocks (matching provider, no models filter)\n * 3. Model-specific blocks (matching provider + models array includes resolved model)\n * 4. Other blocks (provider: 'other', models includes resolved model) — same tier as 3\n *\n * Blocks whose provider does not match are silently skipped.\n *\n * Note: when the resolved OpenAI effort is `'none'`, reasoning is disabled and\n * temperature is accepted; see {@link IOpenAiThinkingConfig.effort} for the full\n * hybrid-mode semantics.\n *\n * @param config - The caller's IThinkingConfig\n * @param resolvedModel - The concrete model string after registry resolution\n * @param discriminator - Coarse provider family\n * @returns Merged effective config for wire encoding\n * @internal\n */\nexport function mergeThinkingConfig(\n config: IThinkingConfig,\n resolvedModel: string,\n discriminator: ThinkingProviderDiscriminator\n): Result<IResolvedThinkingConfig> {\n let resolved: IResolvedThinkingConfig = {};\n\n // Tier 1: generic effort → common-subset mapping\n if (config.effort !== undefined) {\n switch (discriminator) {\n case 'anthropic':\n // Anthropic has no 'none' value: \"off\" means no `thinking` wire param at all,\n // so 'none' leaves `anthropicEffort` unset rather than mapping to a value. That\n // also satisfies checkTemperatureConflict's anthropic gate (which fails only when\n // anthropicEffort is set), so temperature survives without any special-casing there.\n if (config.effort !== 'none') {\n resolved = { ...resolved, anthropicEffort: genericEffortToAnthropic(config.effort) };\n }\n break;\n case 'openai':\n resolved = { ...resolved, openAiEffort: genericEffortToOpenAi(config.effort) };\n break;\n case 'google':\n resolved = { ...resolved, geminiThinkingBudget: genericEffortToGemini(config.effort) };\n break;\n case 'xai':\n resolved = { ...resolved, xaiEffort: genericEffortToXai(config.effort) };\n break;\n }\n }\n\n if (!config.providers) {\n return succeed(resolved);\n }\n\n // Partition into tiers 2 and 3+4\n const applicableBlocks = config.providers.filter((b) => blockApplies(b, resolvedModel, discriminator));\n const genericBlocks = applicableBlocks.filter((b) => !isModelSpecific(b));\n const specificBlocks = applicableBlocks.filter((b) => isModelSpecific(b));\n\n // Tier 2: provider-generic blocks (declaration order; later wins)\n for (const block of genericBlocks) {\n resolved = applyBlock(resolved, block, discriminator);\n }\n\n // Tier 3+4: model-specific + other blocks (declaration order; later wins)\n for (const block of specificBlocks) {\n resolved = applyBlock(resolved, block, discriminator);\n }\n\n return succeed(resolved);\n}\n\n/**\n * Applies a single config block to the accumulated resolved config.\n * @internal\n */\nfunction applyBlock(\n current: IResolvedThinkingConfig,\n block: IThinkingProviderConfig,\n discriminator: ThinkingProviderDiscriminator\n): IResolvedThinkingConfig {\n if (block.provider === 'other') {\n const merged =\n current.otherParams !== undefined ? { ...current.otherParams, ...block.config } : block.config;\n return { ...current, otherParams: merged };\n }\n\n switch (discriminator) {\n case 'anthropic':\n if (block.provider === 'anthropic') {\n if (block.config.effort !== undefined) {\n return { ...current, anthropicEffort: block.config.effort };\n }\n }\n break;\n case 'openai':\n if (block.provider === 'openai') {\n if (block.config.effort !== undefined) {\n return { ...current, openAiEffort: block.config.effort };\n }\n }\n break;\n case 'google':\n if (block.provider === 'google') {\n const updated: IResolvedThinkingConfig = { ...current };\n if (block.config.thinkingBudget !== undefined) {\n return { ...updated, geminiThinkingBudget: block.config.thinkingBudget };\n }\n return updated;\n }\n /* c8 ignore next - blockApplies guarantees provider match; unreachable for google */\n break;\n case 'xai':\n if (block.provider === 'xai') {\n if (block.config.effort !== undefined) {\n return { ...current, xaiEffort: block.config.effort };\n }\n }\n break;\n }\n return current;\n}\n\n// ============================================================================\n// Temperature conflict check\n// ============================================================================\n\n/**\n * Returns a Result.fail if temperature conflicts with thinking mode for the\n * given provider, otherwise succeed(undefined).\n *\n * Per D4: temperature + thinking = Result.fail for Anthropic, OpenAI (when\n * effective effort is non-null and non-'none'), and xAI (conservative default\n * pending live verification). Gemini accepts temperature alongside thinking.\n *\n * @internal\n */\nexport function checkTemperatureConflict(\n resolved: IResolvedThinkingConfig,\n discriminator: ThinkingProviderDiscriminator,\n temperature: number | undefined\n): Result<undefined> {\n if (temperature === undefined) {\n return succeed(undefined);\n }\n\n switch (discriminator) {\n case 'anthropic':\n if (resolved.anthropicEffort !== undefined) {\n return fail(\n 'thinking mode is not compatible with temperature on provider anthropic: remove temperature or disable thinking'\n );\n }\n break;\n case 'openai':\n // 'none' disables reasoning; temperature is accepted in that case\n if (resolved.openAiEffort !== undefined && resolved.openAiEffort !== 'none') {\n return fail(\n 'thinking mode is not compatible with temperature on provider openai: remove temperature or disable thinking'\n );\n }\n break;\n case 'xai':\n // Conservative default: fail if xAI effort is active (per D8 — live verification pending)\n if (resolved.xaiEffort !== undefined && resolved.xaiEffort !== 'none') {\n return fail(\n 'thinking mode is not compatible with temperature on provider xai: remove temperature or disable thinking'\n );\n }\n break;\n case 'google':\n // Gemini accepts temperature alongside thinkingConfig — no conflict\n break;\n }\n return succeed(undefined);\n}\n"]}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
18
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
19
|
// SOFTWARE.
|
|
20
|
+
import { fail, succeed } from '@fgv/ts-utils';
|
|
20
21
|
// ============================================================================
|
|
21
22
|
// Tool resolution
|
|
22
23
|
// ============================================================================
|
|
@@ -46,6 +47,85 @@ export function resolveEffectiveTools(descriptor, settingsTools, perCallTools) {
|
|
|
46
47
|
.filter((e) => e.enabled && supported.has(e.type))
|
|
47
48
|
.map((e) => { var _a; return (_a = e.config) !== null && _a !== void 0 ? _a : { type: e.type }; });
|
|
48
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* The default {@link AiAssist.AiToolConflictPolicy} applied when a caller passes none.
|
|
52
|
+
*
|
|
53
|
+
* @remarks
|
|
54
|
+
* Keeping the client tools is the right default because they are what the host
|
|
55
|
+
* registered explicitly for *this* turn, while a built-in server tool is a
|
|
56
|
+
* per-provider nicety the same host would get on some providers and not others.
|
|
57
|
+
* Dropping the nicety degrades a turn; dropping the client tools changes what the
|
|
58
|
+
* turn can do.
|
|
59
|
+
* @public
|
|
60
|
+
*/
|
|
61
|
+
export const defaultToolConflictPolicy = 'drop-server-tools';
|
|
62
|
+
/**
|
|
63
|
+
* Applies a provider's declared server-tool/client-tool exclusions to one request's
|
|
64
|
+
* tools.
|
|
65
|
+
*
|
|
66
|
+
* @remarks
|
|
67
|
+
* The generic half of a rule that used to be a provider-identity branch. The
|
|
68
|
+
* constraint is declared on the descriptor
|
|
69
|
+
* (`IAiProviderDescriptor.serverToolsExclusiveWithClientTools`); this reads it
|
|
70
|
+
* and applies `policy`, so no consumer needs to know *which* provider has the limit
|
|
71
|
+
* or *what* the limit is. A descriptor that declares nothing, or a request with no
|
|
72
|
+
* client tools, or one with no server tools, passes through untouched with an empty
|
|
73
|
+
* report — the exclusion is about the *combination*, and either half alone is legal.
|
|
74
|
+
*
|
|
75
|
+
* Sibling to {@link AiAssist.resolveEffectiveTools} rather than an extension of it:
|
|
76
|
+
* that function filters server tools against what the provider supports at all and
|
|
77
|
+
* is used by every completion path, most of which have no client tools to weigh.
|
|
78
|
+
*
|
|
79
|
+
* @param descriptor - The provider descriptor carrying the declared exclusions.
|
|
80
|
+
* @param serverTools - The server tools the caller wants to send, if any.
|
|
81
|
+
* @param clientTools - The client tools the caller wants to send.
|
|
82
|
+
* @param policy - What to do about a conflict. Defaults to
|
|
83
|
+
* {@link AiAssist.defaultToolConflictPolicy}.
|
|
84
|
+
* @returns `Success` with the surviving tools and a report, or `Failure` naming the
|
|
85
|
+
* conflict when `policy` is `'fail'`.
|
|
86
|
+
* @public
|
|
87
|
+
*/
|
|
88
|
+
export function resolveToolConflicts(descriptor, serverTools, clientTools, policy = defaultToolConflictPolicy) {
|
|
89
|
+
var _a;
|
|
90
|
+
const present = serverTools !== null && serverTools !== void 0 ? serverTools : [];
|
|
91
|
+
const exclusive = new Set((_a = descriptor.serverToolsExclusiveWithClientTools) !== null && _a !== void 0 ? _a : []);
|
|
92
|
+
const conflicting = clientTools.length > 0 ? present.filter((t) => exclusive.has(t.type)).map((t) => t.type) : [];
|
|
93
|
+
if (conflicting.length === 0) {
|
|
94
|
+
return succeed({
|
|
95
|
+
serverTools: present,
|
|
96
|
+
clientTools,
|
|
97
|
+
report: { policy, droppedServerTools: [], droppedClientTools: [] }
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
// Name every conflicting tool, not just the first: a host reading the message is
|
|
101
|
+
// deciding what to send instead, and a partial list invites a second round.
|
|
102
|
+
const named = conflicting.join(', ');
|
|
103
|
+
switch (policy) {
|
|
104
|
+
case 'fail':
|
|
105
|
+
return fail(`provider "${descriptor.id}" cannot combine server tool(s) [${named}] with client (function) tools in the same request; send one or the other`);
|
|
106
|
+
case 'prefer-server-tools':
|
|
107
|
+
return succeed({
|
|
108
|
+
serverTools: present,
|
|
109
|
+
clientTools: [],
|
|
110
|
+
report: {
|
|
111
|
+
policy,
|
|
112
|
+
droppedServerTools: [],
|
|
113
|
+
droppedClientTools: clientTools.map((t) => t.config.name)
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
case 'drop-server-tools':
|
|
117
|
+
return succeed({
|
|
118
|
+
serverTools: present.filter((t) => !exclusive.has(t.type)),
|
|
119
|
+
clientTools,
|
|
120
|
+
report: { policy, droppedServerTools: conflicting, droppedClientTools: [] }
|
|
121
|
+
});
|
|
122
|
+
/* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */
|
|
123
|
+
default: {
|
|
124
|
+
const _exhaustive = policy;
|
|
125
|
+
return fail(`unknown tool conflict policy: ${JSON.stringify(_exhaustive)}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
49
129
|
// ============================================================================
|
|
50
130
|
// OpenAI / xAI Responses API format
|
|
51
131
|
// ============================================================================
|