@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
|
@@ -17,15 +17,27 @@
|
|
|
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
|
+
/**
|
|
21
|
+
* Chat completion clients for AI assist. One provider dispatcher over
|
|
22
|
+
* `IAiProviderDescriptor.apiFormat`, the four adapters it routes to (OpenAI Chat
|
|
23
|
+
* Completions, OpenAI/xAI Responses, Anthropic, Google Gemini), the response
|
|
24
|
+
* validators those adapters use, and the proxied variant of the same modality.
|
|
25
|
+
*
|
|
26
|
+
* @packageDocumentation
|
|
27
|
+
*/
|
|
28
|
+
import { isJsonObject } from '@fgv/ts-json-base';
|
|
20
29
|
import { captureResult, fail, succeed, Validators } from '@fgv/ts-utils';
|
|
21
30
|
import { DEFAULT_ANTHROPIC_MAX_TOKENS, isAdaptiveThinkingModel, isResponsesOnlyModel, resolveProviderModel, usesMaxCompletionTokensField } from './model';
|
|
31
|
+
import { aiCompletionUsage } from './converters';
|
|
22
32
|
import { anthropicEffortToBudgetTokens, checkTemperatureConflict, mergeThinkingConfig, providerDiscriminatorForId } from './thinkingOptionsResolver';
|
|
23
|
-
import { buildAnthropicMessages, buildGeminiContents, buildMessages, buildOpenAiChatUserContent, buildOpenAiResponsesUserContent, normalizeOutboundMessages, splitChatRequest } from './chatRequestBuilders';
|
|
33
|
+
import { buildAnthropicMessages, buildAnthropicSystem, buildGeminiContents, buildMessages, buildOpenAiChatSystemContent, buildOpenAiChatUserContent, buildOpenAiResponsesSystemContent, buildOpenAiResponsesUserContent, normalizeOutboundMessages, splitChatRequest } from './chatRequestBuilders';
|
|
24
34
|
import { anthropicAuthHeaders, bearerAuthHeader, geminiAuthHeader, resolveEffectiveBaseUrl } from './endpoint';
|
|
25
35
|
import { fetchJson } from './http';
|
|
26
36
|
import { toAnthropicTools, toGeminiTools, toResponsesApiTools } from './toolFormats';
|
|
27
37
|
import { ANTHROPIC_STRUCTURED_OUTPUT_TOOL_NAME, NO_STRUCTURED_OUTPUT, isStructuredOutputEnforcement, resolveStructuredOutput } from './structuredOutput';
|
|
28
38
|
import { resolveStructuredOutputCapability } from './registry';
|
|
39
|
+
import { supportsCacheUsageReporting, supportsPromptCacheBreakpoints, supportsPromptCacheRouting } from './streamUsageCapability';
|
|
40
|
+
import { normalizeAnthropicUsage, normalizeGeminiUsage, normalizeOpenAiChatUsage, normalizeOpenAiResponsesUsage } from './usageNormalization';
|
|
29
41
|
const openAiMessage = Validators.object({
|
|
30
42
|
content: Validators.string
|
|
31
43
|
});
|
|
@@ -51,15 +63,22 @@ const responsesApiResponse = Validators.object({
|
|
|
51
63
|
status: Validators.string
|
|
52
64
|
});
|
|
53
65
|
const geminiPart = Validators.object({
|
|
54
|
-
text: Validators.string
|
|
66
|
+
text: Validators.string.optional()
|
|
55
67
|
});
|
|
56
68
|
const geminiContent = Validators.object({
|
|
57
|
-
parts:
|
|
69
|
+
// No non-empty constraint: `parts: []` is a shape Gemini actually returns, and rejecting it
|
|
70
|
+
// here reports a schema violation for what is really "the model produced no output".
|
|
71
|
+
parts: Validators.arrayOf(geminiPart).optional()
|
|
58
72
|
});
|
|
59
73
|
const geminiCandidate = Validators.object({
|
|
60
|
-
content: geminiContent,
|
|
61
|
-
finishReason: Validators.string
|
|
74
|
+
content: geminiContent.optional(),
|
|
75
|
+
finishReason: Validators.string.optional(),
|
|
76
|
+
finishMessage: Validators.string.optional()
|
|
62
77
|
});
|
|
78
|
+
// Terminal reasons that mean "the model finished normally", as distinct from a refusal. Kept in
|
|
79
|
+
// step with the identical set in `imageGenerationClient.ts`; both exist so an empty response can
|
|
80
|
+
// be reported as the decline it is rather than as an empty string or a parse error.
|
|
81
|
+
const benignGeminiFinishReasons = new Set(['STOP', 'MAX_TOKENS']);
|
|
63
82
|
const geminiResponse = Validators.object({
|
|
64
83
|
candidates: Validators.arrayOf(geminiCandidate).withConstraint((arr) => arr.length > 0)
|
|
65
84
|
});
|
|
@@ -71,36 +90,52 @@ const geminiResponse = Validators.object({
|
|
|
71
90
|
* Works for xAI Grok, OpenAI, Groq, and Mistral.
|
|
72
91
|
* @internal
|
|
73
92
|
*/
|
|
74
|
-
async function callOpenAiCompletion(config, prompt, head, temperature, logger, signal, resolvedThinking, maxTokens, useMaxCompletionTokensField = false, structured = NO_STRUCTURED_OUTPUT) {
|
|
93
|
+
async function callOpenAiCompletion(config, prompt, head, temperature, logger, signal, resolvedThinking, maxTokens, useMaxCompletionTokensField = false, structured = NO_STRUCTURED_OUTPUT, reportsUsage = false, cache, cacheKeyHeader) {
|
|
75
94
|
var _a;
|
|
76
95
|
const url = `${config.baseUrl}/chat/completions`;
|
|
77
|
-
const
|
|
96
|
+
const systemContentResult = buildOpenAiChatSystemContent(prompt.system, cache);
|
|
97
|
+
if (systemContentResult.isFailure()) {
|
|
98
|
+
return fail(systemContentResult.message);
|
|
99
|
+
}
|
|
100
|
+
const messages = buildMessages(systemContentResult.value, buildOpenAiChatUserContent(prompt), {
|
|
78
101
|
head
|
|
79
102
|
});
|
|
80
103
|
const effort = (_a = resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.openAiEffort) !== null && _a !== void 0 ? _a : resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.xaiEffort;
|
|
81
104
|
const maxTokensField = useMaxCompletionTokensField ? 'max_completion_tokens' : 'max_tokens';
|
|
82
|
-
const body = Object.assign(Object.assign(Object.assign({ model: config.model, messages }, (temperature !== undefined ? { temperature } : {})), (effort !== undefined && config.model !== 'grok-4' ? { reasoning_effort: effort } : {})), (maxTokens !== undefined ? { [maxTokensField]: maxTokens } : {}))
|
|
105
|
+
const body = Object.assign(Object.assign(Object.assign(Object.assign({ model: config.model, messages }, (temperature !== undefined ? { temperature } : {})), (effort !== undefined && config.model !== 'grok-4' ? { reasoning_effort: effort } : {})), (maxTokens !== undefined ? { [maxTokensField]: maxTokens } : {})), ((cache === null || cache === void 0 ? void 0 : cache.cacheKey) !== undefined && cacheKeyHeader === undefined
|
|
106
|
+
? // eslint-disable-next-line @typescript-eslint/naming-convention -- wire field name
|
|
107
|
+
{ prompt_cache_key: cache.cacheKey }
|
|
108
|
+
: {}));
|
|
83
109
|
if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.otherParams) !== undefined) {
|
|
84
110
|
Object.assign(body, resolvedThinking.otherParams);
|
|
85
111
|
}
|
|
86
112
|
Object.assign(body, structured.wire);
|
|
87
113
|
const headers = bearerAuthHeader(config.apiKey);
|
|
114
|
+
// Sticky-routing key, when the provider carries it as a header rather than a body field.
|
|
115
|
+
// xAI's prompt cache is per-server, so an identical prefix can still miss if the request is
|
|
116
|
+
// routed to a different box; `x-grok-conv-id` pins it.
|
|
117
|
+
if ((cache === null || cache === void 0 ? void 0 : cache.cacheKey) !== undefined && cacheKeyHeader !== undefined) {
|
|
118
|
+
headers[cacheKeyHeader] = cache.cacheKey;
|
|
119
|
+
}
|
|
88
120
|
/* c8 ignore next 1 - optional logger */
|
|
89
121
|
logger === null || logger === void 0 ? void 0 : logger.info(`OpenAI completion: model=${config.model}`);
|
|
90
122
|
const jsonResult = await fetchJson(url, headers, body, logger, signal);
|
|
91
123
|
if (jsonResult.isFailure()) {
|
|
92
124
|
return fail(jsonResult.message);
|
|
93
125
|
}
|
|
126
|
+
// Only descriptors with confirmed cache-relevant usage reporting are normalized — the
|
|
127
|
+
// adapter is shared by Groq/Mistral/Ollama/openai-compat too, and an ordinary usage block
|
|
128
|
+
// from one of those carries no cache information. See supportsCacheUsageReporting.
|
|
129
|
+
const rawUsage = jsonResult.value.usage;
|
|
130
|
+
const usage = reportsUsage
|
|
131
|
+
? normalizeOpenAiChatUsage(isJsonObject(rawUsage) ? rawUsage : undefined)
|
|
132
|
+
: undefined;
|
|
94
133
|
return openAiResponse
|
|
95
134
|
.validate(jsonResult.value)
|
|
96
135
|
.withErrorFormat((msg) => `OpenAI API response: ${msg}`)
|
|
97
136
|
.onSuccess((response) => {
|
|
98
137
|
const choice = response.choices[0];
|
|
99
|
-
return succeed({
|
|
100
|
-
content: choice.message.content,
|
|
101
|
-
truncated: choice.finish_reason === 'length',
|
|
102
|
-
structuredOutput: structured.enforcement
|
|
103
|
-
});
|
|
138
|
+
return succeed(Object.assign({ content: choice.message.content, truncated: choice.finish_reason === 'length', structuredOutput: structured.enforcement }, (usage !== undefined ? { usage } : {})));
|
|
104
139
|
});
|
|
105
140
|
}
|
|
106
141
|
// ============================================================================
|
|
@@ -127,14 +162,21 @@ function extractResponsesApiText(output) {
|
|
|
127
162
|
* Used when tools are configured for an openai-format provider.
|
|
128
163
|
* @internal
|
|
129
164
|
*/
|
|
130
|
-
async function callOpenAiResponsesCompletion(config, prompt, tools = [], head, temperature, logger, signal, resolvedThinking, maxTokens, structured = NO_STRUCTURED_OUTPUT) {
|
|
165
|
+
async function callOpenAiResponsesCompletion(config, prompt, tools = [], head, temperature, logger, signal, resolvedThinking, maxTokens, structured = NO_STRUCTURED_OUTPUT, reportsUsage = false, cache) {
|
|
131
166
|
var _a;
|
|
132
167
|
const url = `${config.baseUrl}/responses`;
|
|
133
|
-
const
|
|
168
|
+
const systemContentResult = buildOpenAiResponsesSystemContent(prompt.system, cache);
|
|
169
|
+
if (systemContentResult.isFailure()) {
|
|
170
|
+
return fail(systemContentResult.message);
|
|
171
|
+
}
|
|
172
|
+
const input = buildMessages(systemContentResult.value, buildOpenAiResponsesUserContent(prompt), {
|
|
134
173
|
head
|
|
135
174
|
});
|
|
136
175
|
const effort = (_a = resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.openAiEffort) !== null && _a !== void 0 ? _a : resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.xaiEffort;
|
|
137
|
-
const body = Object.assign(Object.assign(Object.assign({ model: config.model, input }, (tools.length > 0 ? { tools: toResponsesApiTools(tools) } : {})), (temperature !== undefined ? { temperature } : {})), (effort !== undefined && config.model !== 'grok-4' ? { reasoning: { effort } } : {}))
|
|
176
|
+
const body = Object.assign(Object.assign(Object.assign(Object.assign({ model: config.model, input }, (tools.length > 0 ? { tools: toResponsesApiTools(tools) } : {})), (temperature !== undefined ? { temperature } : {})), (effort !== undefined && config.model !== 'grok-4' ? { reasoning: { effort } } : {})), ((cache === null || cache === void 0 ? void 0 : cache.cacheKey) !== undefined
|
|
177
|
+
? // eslint-disable-next-line @typescript-eslint/naming-convention -- wire field name
|
|
178
|
+
{ prompt_cache_key: cache.cacheKey }
|
|
179
|
+
: {}));
|
|
138
180
|
// Shared by OpenAI and xAI — both route through the Responses API with the same field name.
|
|
139
181
|
if (maxTokens !== undefined) {
|
|
140
182
|
body.max_output_tokens = maxTokens;
|
|
@@ -150,15 +192,16 @@ async function callOpenAiResponsesCompletion(config, prompt, tools = [], head, t
|
|
|
150
192
|
if (jsonResult.isFailure()) {
|
|
151
193
|
return fail(jsonResult.message);
|
|
152
194
|
}
|
|
195
|
+
// See the identical gate in callOpenAiCompletion above — this route is shared the same way.
|
|
196
|
+
const rawResponsesUsage = jsonResult.value.usage;
|
|
197
|
+
const responsesUsage = reportsUsage
|
|
198
|
+
? normalizeOpenAiResponsesUsage(isJsonObject(rawResponsesUsage) ? rawResponsesUsage : undefined)
|
|
199
|
+
: undefined;
|
|
153
200
|
return responsesApiResponse
|
|
154
201
|
.validate(jsonResult.value)
|
|
155
202
|
.withErrorFormat((msg) => `Responses API response: ${msg}`)
|
|
156
203
|
.onSuccess((response) => {
|
|
157
|
-
return extractResponsesApiText(response.output).onSuccess((text) => succeed({
|
|
158
|
-
content: text,
|
|
159
|
-
truncated: response.status === 'incomplete',
|
|
160
|
-
structuredOutput: structured.enforcement
|
|
161
|
-
}));
|
|
204
|
+
return extractResponsesApiText(response.output).onSuccess((text) => succeed(Object.assign({ content: text, truncated: response.status === 'incomplete', structuredOutput: structured.enforcement }, (responsesUsage !== undefined ? { usage: responsesUsage } : {}))));
|
|
162
205
|
});
|
|
163
206
|
}
|
|
164
207
|
// ============================================================================
|
|
@@ -224,10 +267,14 @@ function extractAnthropicStructuredOutput(content) {
|
|
|
224
267
|
return fail(`Anthropic API response: structured output was forced but no '${ANTHROPIC_STRUCTURED_OUTPUT_TOOL_NAME}' tool_use block was returned`);
|
|
225
268
|
}
|
|
226
269
|
/** Calls the Anthropic Messages API with optional tool support. @internal */
|
|
227
|
-
async function callAnthropicCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking, useAdaptiveThinking = false, maxTokens, structured = NO_STRUCTURED_OUTPUT) {
|
|
270
|
+
async function callAnthropicCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking, useAdaptiveThinking = false, maxTokens, structured = NO_STRUCTURED_OUTPUT, cache) {
|
|
228
271
|
const url = `${config.baseUrl}/messages`;
|
|
229
272
|
const messages = buildAnthropicMessages(prompt, { head });
|
|
230
|
-
const
|
|
273
|
+
const systemResult = buildAnthropicSystem(prompt.system, cache);
|
|
274
|
+
if (systemResult.isFailure()) {
|
|
275
|
+
return fail(systemResult.message);
|
|
276
|
+
}
|
|
277
|
+
const body = Object.assign({ model: config.model, system: systemResult.value, messages,
|
|
231
278
|
// Anthropic's Messages API requires max_tokens on every request — see
|
|
232
279
|
// AiAssist.DEFAULT_ANTHROPIC_MAX_TOKENS for why only this provider defaults it.
|
|
233
280
|
max_tokens: maxTokens !== null && maxTokens !== void 0 ? maxTokens : DEFAULT_ANTHROPIC_MAX_TOKENS }, (temperature !== undefined ? { temperature } : {}));
|
|
@@ -278,6 +325,8 @@ async function callAnthropicCompletion(config, prompt, head, temperature, logger
|
|
|
278
325
|
}
|
|
279
326
|
const rawContent = jsonResult.value.content;
|
|
280
327
|
const stopReason = jsonResult.value.stop_reason;
|
|
328
|
+
const rawAnthropicUsage = jsonResult.value.usage;
|
|
329
|
+
const anthropicUsage = normalizeAnthropicUsage(isJsonObject(rawAnthropicUsage) ? rawAnthropicUsage : undefined);
|
|
281
330
|
if (!Array.isArray(rawContent)) {
|
|
282
331
|
return fail('Anthropic API response: content is not an array');
|
|
283
332
|
}
|
|
@@ -287,11 +336,7 @@ async function callAnthropicCompletion(config, prompt, head, temperature, logger
|
|
|
287
336
|
const extracted = structured.enforcement === 'tool-forced'
|
|
288
337
|
? extractAnthropicStructuredOutput(rawContent)
|
|
289
338
|
: extractAnthropicText(rawContent);
|
|
290
|
-
return extracted.onSuccess((text) => succeed({
|
|
291
|
-
content: text,
|
|
292
|
-
truncated: stopReason === 'max_tokens',
|
|
293
|
-
structuredOutput: structured.enforcement
|
|
294
|
-
}));
|
|
339
|
+
return extracted.onSuccess((text) => succeed(Object.assign({ content: text, truncated: stopReason === 'max_tokens', structuredOutput: structured.enforcement }, (anthropicUsage !== undefined ? { usage: anthropicUsage } : {}))));
|
|
295
340
|
}
|
|
296
341
|
// ============================================================================
|
|
297
342
|
// Google Gemini adapter
|
|
@@ -339,22 +384,35 @@ async function callGeminiCompletion(config, prompt, head, temperature, logger, t
|
|
|
339
384
|
if (jsonResult.isFailure()) {
|
|
340
385
|
return fail(jsonResult.message);
|
|
341
386
|
}
|
|
387
|
+
const rawGeminiUsage = jsonResult.value.usageMetadata;
|
|
388
|
+
const geminiUsage = normalizeGeminiUsage(isJsonObject(rawGeminiUsage) ? rawGeminiUsage : undefined);
|
|
342
389
|
return geminiResponse
|
|
343
390
|
.validate(jsonResult.value)
|
|
344
391
|
.withErrorFormat((msg) => `Gemini API response: ${msg}`)
|
|
345
392
|
.onSuccess((response) => {
|
|
393
|
+
var _a, _b, _c;
|
|
346
394
|
const candidate = response.candidates[0];
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
395
|
+
// ALL parts, not `parts[0]`. Gemini may split one reply across several
|
|
396
|
+
// text parts, and reading only the first silently discards the rest —
|
|
397
|
+
// yielding a truncated document that often still parses, which is the
|
|
398
|
+
// worst way to be wrong. The streaming adapter has always concatenated
|
|
399
|
+
// (`fullText += part.text`); this path did not, so the same response gave
|
|
400
|
+
// different text depending on which one you called.
|
|
401
|
+
//
|
|
402
|
+
// `part.text ?? ''` rather than `part.text`: a reasoning or tool part contributes no text,
|
|
403
|
+
// and joining `undefined` would put the literal string "undefined" into the reply.
|
|
404
|
+
const content = (_c = (_b = (_a = candidate.content) === null || _a === void 0 ? void 0 : _a.parts) === null || _b === void 0 ? void 0 : _b.map((part) => { var _a; return (_a = part.text) !== null && _a !== void 0 ? _a : ''; }).join('')) !== null && _c !== void 0 ? _c : '';
|
|
405
|
+
if (content.length === 0) {
|
|
406
|
+
// Nothing came back. If the candidate says why, say so — an empty string handed to a
|
|
407
|
+
// caller is indistinguishable from a model that legitimately replied with nothing, and
|
|
408
|
+
// sends them looking in the wrong place.
|
|
409
|
+
const reason = candidate.finishReason;
|
|
410
|
+
if (reason !== undefined && !benignGeminiFinishReasons.has(reason)) {
|
|
411
|
+
const suffix = candidate.finishMessage ? ` — ${candidate.finishMessage}` : '';
|
|
412
|
+
return fail(`Gemini completion declined: ${reason}${suffix}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return succeed(Object.assign({ content, truncated: candidate.finishReason === 'MAX_TOKENS', structuredOutput: structured.enforcement }, (geminiUsage !== undefined ? { usage: geminiUsage } : {})));
|
|
358
416
|
});
|
|
359
417
|
}
|
|
360
418
|
// ============================================================================
|
|
@@ -367,7 +425,7 @@ async function callGeminiCompletion(config, prompt, head, temperature, logger, t
|
|
|
367
425
|
* @public
|
|
368
426
|
*/
|
|
369
427
|
export async function callProviderCompletion(params) {
|
|
370
|
-
const { descriptor, apiKey, system, messages, temperature, modelOverride, tier, logger, tools, signal, endpoint, thinking, maxTokens, structuredOutput } = params;
|
|
428
|
+
const { descriptor, apiKey, system, messages, temperature, modelOverride, tier, logger, tools, signal, endpoint, thinking, maxTokens, structuredOutput, cache } = params;
|
|
371
429
|
const splitResult = splitChatRequest(system, messages);
|
|
372
430
|
if (splitResult.isFailure()) {
|
|
373
431
|
return fail(splitResult.message);
|
|
@@ -444,15 +502,30 @@ export async function callProviderCompletion(params) {
|
|
|
444
502
|
`tools=${toolTypes}, supported=${supported}`);
|
|
445
503
|
}
|
|
446
504
|
switch (descriptor.apiFormat) {
|
|
447
|
-
case 'openai':
|
|
505
|
+
case 'openai': {
|
|
448
506
|
// Responses-API-only models (e.g. gpt-5.5-pro) 400 on /chat/completions, so they route
|
|
449
507
|
// to the Responses path even with no tools requested — same path the tools case uses.
|
|
508
|
+
// `cache` carries two independently-supported things, and they are gated separately.
|
|
509
|
+
// `systemBreakpoints` restructures `content` and needs the server to tolerate an
|
|
510
|
+
// unrecognized field inside it (OpenAI only). `cacheKey` is an opaque routing string that
|
|
511
|
+
// makes repeated requests with a shared prefix land on the same cache-holding server —
|
|
512
|
+
// which xAI needs precisely because it does per-server byte-for-byte prefix matching.
|
|
513
|
+
// Gating them together withheld the routing key from the provider that depends on it most.
|
|
514
|
+
// Every descriptor supporting neither (Groq, Mistral, Ollama, openai-compat) still gets the
|
|
515
|
+
// exact request body it would have gotten had the caller passed no `cache` at all.
|
|
516
|
+
const routing = supportsPromptCacheRouting(descriptor);
|
|
517
|
+
const breakpoints = supportsPromptCacheBreakpoints(descriptor) ? cache === null || cache === void 0 ? void 0 : cache.systemBreakpoints : undefined;
|
|
518
|
+
const routedKey = routing !== undefined ? cache === null || cache === void 0 ? void 0 : cache.cacheKey : undefined;
|
|
519
|
+
const gatedCache = breakpoints !== undefined || routedKey !== undefined
|
|
520
|
+
? { systemBreakpoints: breakpoints, cacheKey: routedKey }
|
|
521
|
+
: undefined;
|
|
450
522
|
if (usesResponsesApi) {
|
|
451
|
-
return callOpenAiResponsesCompletion(config, prompt, tools, head, temperature, logger, signal, resolvedThinking, maxTokens, resolvedStructured);
|
|
523
|
+
return callOpenAiResponsesCompletion(config, prompt, tools, head, temperature, logger, signal, resolvedThinking, maxTokens, resolvedStructured, supportsCacheUsageReporting(descriptor), gatedCache);
|
|
452
524
|
}
|
|
453
|
-
return callOpenAiCompletion(config, prompt, head, temperature, logger, signal, resolvedThinking, maxTokens, usesMaxCompletionTokensField(descriptor), resolvedStructured);
|
|
525
|
+
return callOpenAiCompletion(config, prompt, head, temperature, logger, signal, resolvedThinking, maxTokens, usesMaxCompletionTokensField(descriptor), resolvedStructured, supportsCacheUsageReporting(descriptor), gatedCache, routing === null || routing === void 0 ? void 0 : routing.chatCompletionsHeader);
|
|
526
|
+
}
|
|
454
527
|
case 'anthropic':
|
|
455
|
-
return callAnthropicCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking, isAdaptiveThinkingModel(descriptor, config.model), maxTokens, resolvedStructured);
|
|
528
|
+
return callAnthropicCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking, isAdaptiveThinkingModel(descriptor, config.model), maxTokens, resolvedStructured, cache);
|
|
456
529
|
case 'gemini':
|
|
457
530
|
return callGeminiCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking, maxTokens, resolvedStructured);
|
|
458
531
|
/* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */
|
|
@@ -471,12 +544,25 @@ export async function callProviderCompletion(params) {
|
|
|
471
544
|
* CORS, and API key forwarding. The request body serializes the unified
|
|
472
545
|
* {@link AiAssist.IChatRequest} shape (`system?` + `messages`). Enforces the same
|
|
473
546
|
* non-empty / trailing-user-turn and image-input invariants as the direct path.
|
|
547
|
+
*
|
|
548
|
+
* @remarks
|
|
549
|
+
* Two parameters are handled before the body is composed, rather than forwarded.
|
|
550
|
+
* `tier` is resolved here and sent as a concrete `modelOverride`, because both
|
|
551
|
+
* halves of the resolution live on the descriptor the caller already holds — so a
|
|
552
|
+
* proxy needs no `tier` vocabulary, where a `tier` body field it did not understand
|
|
553
|
+
* would be ignored and silently serve a frontier request from the base model.
|
|
554
|
+
* `endpoint` is **refused**: it names where the prompt must go, the proxy is what
|
|
555
|
+
* makes that call, and the field has never been sent to one — so honoring it is
|
|
556
|
+
* unverifiable and ignoring it would reach the provider's default upstream instead
|
|
557
|
+
* of the host the caller pinned. Use `callProviderCompletion`, or point the
|
|
558
|
+
* proxy itself at the intended upstream.
|
|
559
|
+
*
|
|
474
560
|
* @param proxyUrl - Base URL of the proxy server
|
|
475
561
|
* @param params - Same parameters as {@link callProviderCompletion}
|
|
476
562
|
* @public
|
|
477
563
|
*/
|
|
478
564
|
export async function callProxiedCompletion(proxyUrl, params) {
|
|
479
|
-
const { descriptor, apiKey, system, messages, temperature, modelOverride, logger, tools, signal, thinking, maxTokens, structuredOutput } = params;
|
|
565
|
+
const { descriptor, apiKey, system, messages, temperature, modelOverride, logger, tools, signal, thinking, maxTokens, structuredOutput, cache, tier, endpoint } = params;
|
|
480
566
|
const splitResult = splitChatRequest(system, messages);
|
|
481
567
|
if (splitResult.isFailure()) {
|
|
482
568
|
return fail(splitResult.message);
|
|
@@ -484,6 +570,48 @@ export async function callProxiedCompletion(proxyUrl, params) {
|
|
|
484
570
|
if (splitResult.value.prompt.attachments.length > 0 && !descriptor.acceptsImageInput) {
|
|
485
571
|
return fail(`provider "${descriptor.id}" does not accept image input`);
|
|
486
572
|
}
|
|
573
|
+
// `endpoint` names the upstream the request must reach. Unlike `tier` it cannot be
|
|
574
|
+
// resolved on this side, because the proxy is the one making that call — it would
|
|
575
|
+
// have to be forwarded and honored. No proxy honors it: the field has never been
|
|
576
|
+
// sent, so every deployed proxy would ignore it and fall back to the provider's
|
|
577
|
+
// public API.
|
|
578
|
+
//
|
|
579
|
+
// Silently is the problem. A caller names an endpoint to pin *where the prompt
|
|
580
|
+
// goes* — a self-hosted model, a LAN deployment, a residency boundary — so the
|
|
581
|
+
// failure is not a degraded answer, it is the content going somewhere they
|
|
582
|
+
// explicitly said it must not, discoverable only by watching the proxy's traffic.
|
|
583
|
+
// That is the same shape as the structured-output silent-drop this function
|
|
584
|
+
// already refuses to allow, with a worse consequence.
|
|
585
|
+
//
|
|
586
|
+
// So refuse rather than pretend. Honoring `endpoint` over a proxy needs a wire
|
|
587
|
+
// field AND a report-back the caller can verify, the way `structuredOutput` is
|
|
588
|
+
// acknowledged below; until that protocol exists, the direct path is where this
|
|
589
|
+
// parameter works.
|
|
590
|
+
if (endpoint !== undefined) {
|
|
591
|
+
return fail(`callProxiedCompletion: endpoint is not supported on the proxied path — a proxy ` +
|
|
592
|
+
`cannot confirm it honored it, and silently reaching the provider's default ` +
|
|
593
|
+
`upstream would send the request somewhere the caller excluded. Use ` +
|
|
594
|
+
`callProviderCompletion, or route the proxy itself at the intended upstream.`);
|
|
595
|
+
}
|
|
596
|
+
// The quality tier is resolved HERE rather than forwarded, because it can be:
|
|
597
|
+
// the tier walk and the alias map both live on the descriptor, which is
|
|
598
|
+
// client-side, so this side already holds everything the resolution needs.
|
|
599
|
+
// Sending the concrete model through the existing `modelOverride` field means a
|
|
600
|
+
// proxy needs no new vocabulary and an already-deployed one honors the tier
|
|
601
|
+
// without being updated — where a `tier` body field it did not understand would
|
|
602
|
+
// be ignored, silently serving a frontier request from the base model.
|
|
603
|
+
//
|
|
604
|
+
// Only when a tier was actually asked for. With no tier, `modelOverride` passes
|
|
605
|
+
// through exactly as before, including absent — which is what lets a proxy apply
|
|
606
|
+
// its own default.
|
|
607
|
+
let effectiveModelOverride = modelOverride;
|
|
608
|
+
if (tier !== undefined) {
|
|
609
|
+
const tierResult = resolveProviderModel(descriptor, modelOverride, tier);
|
|
610
|
+
if (tierResult.isFailure()) {
|
|
611
|
+
return fail(tierResult.message);
|
|
612
|
+
}
|
|
613
|
+
effectiveModelOverride = tierResult.value;
|
|
614
|
+
}
|
|
487
615
|
const body = {
|
|
488
616
|
providerId: descriptor.id,
|
|
489
617
|
apiKey,
|
|
@@ -497,8 +625,8 @@ export async function callProxiedCompletion(proxyUrl, params) {
|
|
|
497
625
|
if (system !== undefined) {
|
|
498
626
|
body.system = system;
|
|
499
627
|
}
|
|
500
|
-
if (
|
|
501
|
-
body.modelOverride =
|
|
628
|
+
if (effectiveModelOverride !== undefined) {
|
|
629
|
+
body.modelOverride = effectiveModelOverride;
|
|
502
630
|
}
|
|
503
631
|
if (tools && tools.length > 0) {
|
|
504
632
|
body.tools = tools;
|
|
@@ -517,12 +645,19 @@ export async function callProxiedCompletion(proxyUrl, params) {
|
|
|
517
645
|
// `JsonSchema.fromJson(raw)` before calling `callProviderCompletion`.
|
|
518
646
|
body.structuredOutput =
|
|
519
647
|
structuredOutput.mode === 'schema'
|
|
520
|
-
? Object.assign({ mode: 'schema', schema: structuredOutput.schema.toJson() }, (structuredOutput.onUnsupported !== undefined
|
|
648
|
+
? Object.assign(Object.assign({ mode: 'schema', schema: structuredOutput.schema.toJson() }, (structuredOutput.onUnsupported !== undefined
|
|
521
649
|
? { onUnsupported: structuredOutput.onUnsupported }
|
|
650
|
+
: {})), (structuredOutput.adaptOptionalToNullable !== undefined
|
|
651
|
+
? { adaptOptionalToNullable: structuredOutput.adaptOptionalToNullable }
|
|
522
652
|
: {})) : Object.assign({ mode: 'json-object' }, (structuredOutput.onUnsupported !== undefined
|
|
523
653
|
? { onUnsupported: structuredOutput.onUnsupported }
|
|
524
654
|
: {}));
|
|
525
655
|
}
|
|
656
|
+
// `IAiCacheRequest` is plain numbers and a string — JSON-serializable as-is, unlike
|
|
657
|
+
// `structuredOutput`'s schema above, so it needs no wire projection before forwarding.
|
|
658
|
+
if (cache !== undefined) {
|
|
659
|
+
body.cache = cache;
|
|
660
|
+
}
|
|
526
661
|
/* c8 ignore next 1 - optional logger */
|
|
527
662
|
logger === null || logger === void 0 ? void 0 : logger.info(`AI proxy request: provider=${descriptor.id}, proxy=${proxyUrl}`);
|
|
528
663
|
const url = `${proxyUrl}/api/ai/completion`;
|
|
@@ -542,22 +677,24 @@ export async function callProxiedCompletion(proxyUrl, params) {
|
|
|
542
677
|
// rather than a response claiming an enforcement nobody verified — a proxy
|
|
543
678
|
// predating this feature drops the constraint silently, which is the exact
|
|
544
679
|
// failure this surface exists to remove.
|
|
680
|
+
// A proxy relaying `callProviderCompletion`'s result carries an already-normalized
|
|
681
|
+
// IAiCompletionUsage, not a provider wire shape — so this validates that shape
|
|
682
|
+
// rather than reaching for the per-provider normalizers. A proxy that reports
|
|
683
|
+
// nothing, or reports something malformed, yields `undefined`: the documented
|
|
684
|
+
// "no normalized usage exposed", never a partially-trusted object and never a
|
|
685
|
+
// fabricated zero.
|
|
686
|
+
const usage = aiCompletionUsage.convert(response.usage).orDefault();
|
|
687
|
+
// Built once: the two returns differ only in the enforcement they report, and
|
|
688
|
+
// duplicating the shape is how a field added to one and not the other drifts.
|
|
689
|
+
const base = Object.assign({ content: response.content, truncated: response.truncated === true }, (usage !== undefined ? { usage } : {}));
|
|
545
690
|
if (structuredOutput === undefined) {
|
|
546
|
-
return succeed({
|
|
547
|
-
content: response.content,
|
|
548
|
-
truncated: response.truncated === true,
|
|
549
|
-
structuredOutput: 'none'
|
|
550
|
-
});
|
|
691
|
+
return succeed(Object.assign(Object.assign({}, base), { structuredOutput: 'none' }));
|
|
551
692
|
}
|
|
552
693
|
if (!isStructuredOutputEnforcement(response.structuredOutput)) {
|
|
553
694
|
return fail(`proxy did not report which structured-output constraint it applied ` +
|
|
554
695
|
`(got ${JSON.stringify(response.structuredOutput)}); it may predate the feature and have ` +
|
|
555
696
|
`dropped the request silently`);
|
|
556
697
|
}
|
|
557
|
-
return succeed({
|
|
558
|
-
content: response.content,
|
|
559
|
-
truncated: response.truncated === true,
|
|
560
|
-
structuredOutput: response.structuredOutput
|
|
561
|
-
});
|
|
698
|
+
return succeed(Object.assign(Object.assign({}, base), { structuredOutput: response.structuredOutput }));
|
|
562
699
|
}
|
|
563
700
|
//# sourceMappingURL=completionClient.js.map
|