@fgv/ts-extras 5.1.0-55 → 5.1.0-56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CAPABILITIES.md +95 -0
- package/dist/packlets/ai-assist/cacheRequest.js +118 -0
- package/dist/packlets/ai-assist/cacheRequest.js.map +1 -0
- package/dist/packlets/ai-assist/chatRequestBuilders.js +70 -5
- package/dist/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
- package/dist/packlets/ai-assist/completionClient.js +122 -45
- package/dist/packlets/ai-assist/completionClient.js.map +1 -1
- package/dist/packlets/ai-assist/imageGenerationClient.js +1 -1
- package/dist/packlets/ai-assist/imageGenerationClient.js.map +1 -1
- package/dist/packlets/ai-assist/index.js +3 -1
- package/dist/packlets/ai-assist/index.js.map +1 -1
- package/dist/packlets/ai-assist/model.js.map +1 -1
- package/dist/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.js +200 -0
- package/dist/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.js.map +1 -0
- package/dist/packlets/ai-assist/promptCacheObservabilityHarness.js +137 -0
- package/dist/packlets/ai-assist/promptCacheObservabilityHarness.js.map +1 -0
- package/dist/packlets/ai-assist/promptCacheRoutingAbHarness.js +156 -0
- package/dist/packlets/ai-assist/promptCacheRoutingAbHarness.js.map +1 -0
- package/dist/packlets/ai-assist/promptCacheUsageMath.js +124 -0
- package/dist/packlets/ai-assist/promptCacheUsageMath.js.map +1 -0
- package/dist/packlets/ai-assist/registry.js +9 -11
- package/dist/packlets/ai-assist/registry.js.map +1 -1
- package/dist/packlets/ai-assist/streamUsageCapability.js +124 -0
- package/dist/packlets/ai-assist/streamUsageCapability.js.map +1 -0
- package/dist/packlets/ai-assist/streamingAdapters/anthropic.js +24 -5
- package/dist/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +64 -33
- package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/common.js +25 -1
- package/dist/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/gemini.js +12 -5
- package/dist/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js +34 -8
- package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js +22 -14
- package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
- package/dist/packlets/ai-assist/streamingClient.js +3 -2
- package/dist/packlets/ai-assist/streamingClient.js.map +1 -1
- package/dist/packlets/ai-assist/thinkingOptionsResolver.js +18 -3
- package/dist/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -1
- package/dist/packlets/ai-assist/toolFormats.js +80 -0
- package/dist/packlets/ai-assist/toolFormats.js.map +1 -1
- package/dist/packlets/ai-assist/toolTypes.js +21 -0
- package/dist/packlets/ai-assist/toolTypes.js.map +1 -0
- package/dist/packlets/ai-assist/usageNormalization.js +178 -0
- package/dist/packlets/ai-assist/usageNormalization.js.map +1 -0
- package/dist/packlets/ai-assist/usageTypes.js +21 -0
- package/dist/packlets/ai-assist/usageTypes.js.map +1 -0
- package/dist/packlets/mustache/index.js.map +1 -1
- package/dist/packlets/mustache/interfaces.js.map +1 -1
- package/dist/packlets/mustache/mustacheTemplate.js +91 -0
- package/dist/packlets/mustache/mustacheTemplate.js.map +1 -1
- package/dist/ts-extras.d.ts +547 -30
- package/lib/packlets/ai-assist/cacheRequest.d.ts +100 -0
- package/lib/packlets/ai-assist/cacheRequest.d.ts.map +1 -0
- package/lib/packlets/ai-assist/cacheRequest.js +123 -0
- package/lib/packlets/ai-assist/cacheRequest.js.map +1 -0
- package/lib/packlets/ai-assist/chatRequestBuilders.d.ts +34 -4
- package/lib/packlets/ai-assist/chatRequestBuilders.d.ts.map +1 -1
- package/lib/packlets/ai-assist/chatRequestBuilders.js +73 -5
- package/lib/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
- package/lib/packlets/ai-assist/completionClient.d.ts +6 -0
- package/lib/packlets/ai-assist/completionClient.d.ts.map +1 -1
- package/lib/packlets/ai-assist/completionClient.js +121 -44
- package/lib/packlets/ai-assist/completionClient.js.map +1 -1
- package/lib/packlets/ai-assist/imageGenerationClient.d.ts.map +1 -1
- package/lib/packlets/ai-assist/imageGenerationClient.js +1 -1
- package/lib/packlets/ai-assist/imageGenerationClient.js.map +1 -1
- package/lib/packlets/ai-assist/index.d.ts +7 -2
- package/lib/packlets/ai-assist/index.d.ts.map +1 -1
- package/lib/packlets/ai-assist/index.js +11 -1
- package/lib/packlets/ai-assist/index.js.map +1 -1
- package/lib/packlets/ai-assist/model.d.ts +59 -288
- package/lib/packlets/ai-assist/model.d.ts.map +1 -1
- package/lib/packlets/ai-assist/model.js.map +1 -1
- package/lib/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.d.ts +124 -0
- package/lib/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.d.ts.map +1 -0
- package/lib/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.js +208 -0
- package/lib/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.js.map +1 -0
- package/lib/packlets/ai-assist/promptCacheObservabilityHarness.d.ts +77 -0
- package/lib/packlets/ai-assist/promptCacheObservabilityHarness.d.ts.map +1 -0
- package/lib/packlets/ai-assist/promptCacheObservabilityHarness.js +143 -0
- package/lib/packlets/ai-assist/promptCacheObservabilityHarness.js.map +1 -0
- package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.d.ts +84 -0
- package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.d.ts.map +1 -0
- package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.js +163 -0
- package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.js.map +1 -0
- package/lib/packlets/ai-assist/promptCacheUsageMath.d.ts +105 -0
- package/lib/packlets/ai-assist/promptCacheUsageMath.d.ts.map +1 -0
- package/lib/packlets/ai-assist/promptCacheUsageMath.js +130 -0
- package/lib/packlets/ai-assist/promptCacheUsageMath.js.map +1 -0
- package/lib/packlets/ai-assist/registry.d.ts.map +1 -1
- package/lib/packlets/ai-assist/registry.js +9 -11
- package/lib/packlets/ai-assist/registry.js.map +1 -1
- package/lib/packlets/ai-assist/streamUsageCapability.d.ts +119 -0
- package/lib/packlets/ai-assist/streamUsageCapability.d.ts.map +1 -0
- package/lib/packlets/ai-assist/streamUsageCapability.js +130 -0
- package/lib/packlets/ai-assist/streamUsageCapability.js.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.js +23 -4
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts +21 -12
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +64 -34
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/common.d.ts +21 -0
- package/lib/packlets/ai-assist/streamingAdapters/common.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/common.js +25 -1
- package/lib/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/gemini.js +12 -5
- package/lib/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js +33 -7
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js +21 -13
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
- package/lib/packlets/ai-assist/streamingClient.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingClient.js +3 -2
- package/lib/packlets/ai-assist/streamingClient.js.map +1 -1
- package/lib/packlets/ai-assist/thinkingOptionsResolver.d.ts.map +1 -1
- package/lib/packlets/ai-assist/thinkingOptionsResolver.js +18 -3
- package/lib/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -1
- package/lib/packlets/ai-assist/toolFormats.d.ts +55 -1
- package/lib/packlets/ai-assist/toolFormats.d.ts.map +1 -1
- package/lib/packlets/ai-assist/toolFormats.js +82 -0
- package/lib/packlets/ai-assist/toolFormats.js.map +1 -1
- package/lib/packlets/ai-assist/toolTypes.d.ts +328 -0
- package/lib/packlets/ai-assist/toolTypes.d.ts.map +1 -0
- package/lib/packlets/ai-assist/toolTypes.js +22 -0
- package/lib/packlets/ai-assist/toolTypes.js.map +1 -0
- package/lib/packlets/ai-assist/usageNormalization.d.ts +58 -0
- package/lib/packlets/ai-assist/usageNormalization.d.ts.map +1 -0
- package/lib/packlets/ai-assist/usageNormalization.js +184 -0
- package/lib/packlets/ai-assist/usageNormalization.js.map +1 -0
- package/lib/packlets/ai-assist/usageTypes.d.ts +84 -0
- package/lib/packlets/ai-assist/usageTypes.d.ts.map +1 -0
- package/lib/packlets/ai-assist/usageTypes.js +22 -0
- package/lib/packlets/ai-assist/usageTypes.js.map +1 -0
- package/lib/packlets/mustache/index.d.ts +1 -1
- package/lib/packlets/mustache/index.d.ts.map +1 -1
- package/lib/packlets/mustache/index.js.map +1 -1
- package/lib/packlets/mustache/interfaces.d.ts +30 -0
- package/lib/packlets/mustache/interfaces.d.ts.map +1 -1
- package/lib/packlets/mustache/interfaces.js.map +1 -1
- package/lib/packlets/mustache/mustacheTemplate.d.ts +56 -1
- package/lib/packlets/mustache/mustacheTemplate.d.ts.map +1 -1
- package/lib/packlets/mustache/mustacheTemplate.js +91 -0
- package/lib/packlets/mustache/mustacheTemplate.js.map +1 -1
- package/package.json +16 -16
- package/perf/promptCacheAnthropicBreakpoint.js +135 -0
- package/perf/promptCacheObservability.js +107 -0
- package/perf/promptCacheRoutingAb.js +105 -0
|
@@ -17,15 +17,26 @@
|
|
|
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';
|
|
22
31
|
import { anthropicEffortToBudgetTokens, checkTemperatureConflict, mergeThinkingConfig, providerDiscriminatorForId } from './thinkingOptionsResolver';
|
|
23
|
-
import { buildAnthropicMessages, buildGeminiContents, buildMessages, buildOpenAiChatUserContent, buildOpenAiResponsesUserContent, normalizeOutboundMessages, splitChatRequest } from './chatRequestBuilders';
|
|
32
|
+
import { buildAnthropicMessages, buildAnthropicSystem, buildGeminiContents, buildMessages, buildOpenAiChatSystemContent, buildOpenAiChatUserContent, buildOpenAiResponsesSystemContent, buildOpenAiResponsesUserContent, normalizeOutboundMessages, splitChatRequest } from './chatRequestBuilders';
|
|
24
33
|
import { anthropicAuthHeaders, bearerAuthHeader, geminiAuthHeader, resolveEffectiveBaseUrl } from './endpoint';
|
|
25
34
|
import { fetchJson } from './http';
|
|
26
35
|
import { toAnthropicTools, toGeminiTools, toResponsesApiTools } from './toolFormats';
|
|
27
36
|
import { ANTHROPIC_STRUCTURED_OUTPUT_TOOL_NAME, NO_STRUCTURED_OUTPUT, isStructuredOutputEnforcement, resolveStructuredOutput } from './structuredOutput';
|
|
28
37
|
import { resolveStructuredOutputCapability } from './registry';
|
|
38
|
+
import { supportsCacheUsageReporting, supportsPromptCacheBreakpoints, supportsPromptCacheRouting } from './streamUsageCapability';
|
|
39
|
+
import { normalizeAnthropicUsage, normalizeGeminiUsage, normalizeOpenAiChatUsage, normalizeOpenAiResponsesUsage } from './usageNormalization';
|
|
29
40
|
const openAiMessage = Validators.object({
|
|
30
41
|
content: Validators.string
|
|
31
42
|
});
|
|
@@ -51,15 +62,22 @@ const responsesApiResponse = Validators.object({
|
|
|
51
62
|
status: Validators.string
|
|
52
63
|
});
|
|
53
64
|
const geminiPart = Validators.object({
|
|
54
|
-
text: Validators.string
|
|
65
|
+
text: Validators.string.optional()
|
|
55
66
|
});
|
|
56
67
|
const geminiContent = Validators.object({
|
|
57
|
-
parts:
|
|
68
|
+
// No non-empty constraint: `parts: []` is a shape Gemini actually returns, and rejecting it
|
|
69
|
+
// here reports a schema violation for what is really "the model produced no output".
|
|
70
|
+
parts: Validators.arrayOf(geminiPart).optional()
|
|
58
71
|
});
|
|
59
72
|
const geminiCandidate = Validators.object({
|
|
60
|
-
content: geminiContent,
|
|
61
|
-
finishReason: Validators.string
|
|
73
|
+
content: geminiContent.optional(),
|
|
74
|
+
finishReason: Validators.string.optional(),
|
|
75
|
+
finishMessage: Validators.string.optional()
|
|
62
76
|
});
|
|
77
|
+
// Terminal reasons that mean "the model finished normally", as distinct from a refusal. Kept in
|
|
78
|
+
// step with the identical set in `imageGenerationClient.ts`; both exist so an empty response can
|
|
79
|
+
// be reported as the decline it is rather than as an empty string or a parse error.
|
|
80
|
+
const benignGeminiFinishReasons = new Set(['STOP', 'MAX_TOKENS']);
|
|
63
81
|
const geminiResponse = Validators.object({
|
|
64
82
|
candidates: Validators.arrayOf(geminiCandidate).withConstraint((arr) => arr.length > 0)
|
|
65
83
|
});
|
|
@@ -71,36 +89,52 @@ const geminiResponse = Validators.object({
|
|
|
71
89
|
* Works for xAI Grok, OpenAI, Groq, and Mistral.
|
|
72
90
|
* @internal
|
|
73
91
|
*/
|
|
74
|
-
async function callOpenAiCompletion(config, prompt, head, temperature, logger, signal, resolvedThinking, maxTokens, useMaxCompletionTokensField = false, structured = NO_STRUCTURED_OUTPUT) {
|
|
92
|
+
async function callOpenAiCompletion(config, prompt, head, temperature, logger, signal, resolvedThinking, maxTokens, useMaxCompletionTokensField = false, structured = NO_STRUCTURED_OUTPUT, reportsUsage = false, cache, cacheKeyHeader) {
|
|
75
93
|
var _a;
|
|
76
94
|
const url = `${config.baseUrl}/chat/completions`;
|
|
77
|
-
const
|
|
95
|
+
const systemContentResult = buildOpenAiChatSystemContent(prompt.system, cache);
|
|
96
|
+
if (systemContentResult.isFailure()) {
|
|
97
|
+
return fail(systemContentResult.message);
|
|
98
|
+
}
|
|
99
|
+
const messages = buildMessages(systemContentResult.value, buildOpenAiChatUserContent(prompt), {
|
|
78
100
|
head
|
|
79
101
|
});
|
|
80
102
|
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
103
|
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 } : {}))
|
|
104
|
+
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
|
|
105
|
+
? // eslint-disable-next-line @typescript-eslint/naming-convention -- wire field name
|
|
106
|
+
{ prompt_cache_key: cache.cacheKey }
|
|
107
|
+
: {}));
|
|
83
108
|
if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.otherParams) !== undefined) {
|
|
84
109
|
Object.assign(body, resolvedThinking.otherParams);
|
|
85
110
|
}
|
|
86
111
|
Object.assign(body, structured.wire);
|
|
87
112
|
const headers = bearerAuthHeader(config.apiKey);
|
|
113
|
+
// Sticky-routing key, when the provider carries it as a header rather than a body field.
|
|
114
|
+
// xAI's prompt cache is per-server, so an identical prefix can still miss if the request is
|
|
115
|
+
// routed to a different box; `x-grok-conv-id` pins it.
|
|
116
|
+
if ((cache === null || cache === void 0 ? void 0 : cache.cacheKey) !== undefined && cacheKeyHeader !== undefined) {
|
|
117
|
+
headers[cacheKeyHeader] = cache.cacheKey;
|
|
118
|
+
}
|
|
88
119
|
/* c8 ignore next 1 - optional logger */
|
|
89
120
|
logger === null || logger === void 0 ? void 0 : logger.info(`OpenAI completion: model=${config.model}`);
|
|
90
121
|
const jsonResult = await fetchJson(url, headers, body, logger, signal);
|
|
91
122
|
if (jsonResult.isFailure()) {
|
|
92
123
|
return fail(jsonResult.message);
|
|
93
124
|
}
|
|
125
|
+
// Only descriptors with confirmed cache-relevant usage reporting are normalized — the
|
|
126
|
+
// adapter is shared by Groq/Mistral/Ollama/openai-compat too, and an ordinary usage block
|
|
127
|
+
// from one of those carries no cache information. See supportsCacheUsageReporting.
|
|
128
|
+
const rawUsage = jsonResult.value.usage;
|
|
129
|
+
const usage = reportsUsage
|
|
130
|
+
? normalizeOpenAiChatUsage(isJsonObject(rawUsage) ? rawUsage : undefined)
|
|
131
|
+
: undefined;
|
|
94
132
|
return openAiResponse
|
|
95
133
|
.validate(jsonResult.value)
|
|
96
134
|
.withErrorFormat((msg) => `OpenAI API response: ${msg}`)
|
|
97
135
|
.onSuccess((response) => {
|
|
98
136
|
const choice = response.choices[0];
|
|
99
|
-
return succeed({
|
|
100
|
-
content: choice.message.content,
|
|
101
|
-
truncated: choice.finish_reason === 'length',
|
|
102
|
-
structuredOutput: structured.enforcement
|
|
103
|
-
});
|
|
137
|
+
return succeed(Object.assign({ content: choice.message.content, truncated: choice.finish_reason === 'length', structuredOutput: structured.enforcement }, (usage !== undefined ? { usage } : {})));
|
|
104
138
|
});
|
|
105
139
|
}
|
|
106
140
|
// ============================================================================
|
|
@@ -127,14 +161,21 @@ function extractResponsesApiText(output) {
|
|
|
127
161
|
* Used when tools are configured for an openai-format provider.
|
|
128
162
|
* @internal
|
|
129
163
|
*/
|
|
130
|
-
async function callOpenAiResponsesCompletion(config, prompt, tools = [], head, temperature, logger, signal, resolvedThinking, maxTokens, structured = NO_STRUCTURED_OUTPUT) {
|
|
164
|
+
async function callOpenAiResponsesCompletion(config, prompt, tools = [], head, temperature, logger, signal, resolvedThinking, maxTokens, structured = NO_STRUCTURED_OUTPUT, reportsUsage = false, cache) {
|
|
131
165
|
var _a;
|
|
132
166
|
const url = `${config.baseUrl}/responses`;
|
|
133
|
-
const
|
|
167
|
+
const systemContentResult = buildOpenAiResponsesSystemContent(prompt.system, cache);
|
|
168
|
+
if (systemContentResult.isFailure()) {
|
|
169
|
+
return fail(systemContentResult.message);
|
|
170
|
+
}
|
|
171
|
+
const input = buildMessages(systemContentResult.value, buildOpenAiResponsesUserContent(prompt), {
|
|
134
172
|
head
|
|
135
173
|
});
|
|
136
174
|
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 } } : {}))
|
|
175
|
+
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
|
|
176
|
+
? // eslint-disable-next-line @typescript-eslint/naming-convention -- wire field name
|
|
177
|
+
{ prompt_cache_key: cache.cacheKey }
|
|
178
|
+
: {}));
|
|
138
179
|
// Shared by OpenAI and xAI — both route through the Responses API with the same field name.
|
|
139
180
|
if (maxTokens !== undefined) {
|
|
140
181
|
body.max_output_tokens = maxTokens;
|
|
@@ -150,15 +191,16 @@ async function callOpenAiResponsesCompletion(config, prompt, tools = [], head, t
|
|
|
150
191
|
if (jsonResult.isFailure()) {
|
|
151
192
|
return fail(jsonResult.message);
|
|
152
193
|
}
|
|
194
|
+
// See the identical gate in callOpenAiCompletion above — this route is shared the same way.
|
|
195
|
+
const rawResponsesUsage = jsonResult.value.usage;
|
|
196
|
+
const responsesUsage = reportsUsage
|
|
197
|
+
? normalizeOpenAiResponsesUsage(isJsonObject(rawResponsesUsage) ? rawResponsesUsage : undefined)
|
|
198
|
+
: undefined;
|
|
153
199
|
return responsesApiResponse
|
|
154
200
|
.validate(jsonResult.value)
|
|
155
201
|
.withErrorFormat((msg) => `Responses API response: ${msg}`)
|
|
156
202
|
.onSuccess((response) => {
|
|
157
|
-
return extractResponsesApiText(response.output).onSuccess((text) => succeed({
|
|
158
|
-
content: text,
|
|
159
|
-
truncated: response.status === 'incomplete',
|
|
160
|
-
structuredOutput: structured.enforcement
|
|
161
|
-
}));
|
|
203
|
+
return extractResponsesApiText(response.output).onSuccess((text) => succeed(Object.assign({ content: text, truncated: response.status === 'incomplete', structuredOutput: structured.enforcement }, (responsesUsage !== undefined ? { usage: responsesUsage } : {}))));
|
|
162
204
|
});
|
|
163
205
|
}
|
|
164
206
|
// ============================================================================
|
|
@@ -224,10 +266,14 @@ function extractAnthropicStructuredOutput(content) {
|
|
|
224
266
|
return fail(`Anthropic API response: structured output was forced but no '${ANTHROPIC_STRUCTURED_OUTPUT_TOOL_NAME}' tool_use block was returned`);
|
|
225
267
|
}
|
|
226
268
|
/** 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) {
|
|
269
|
+
async function callAnthropicCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking, useAdaptiveThinking = false, maxTokens, structured = NO_STRUCTURED_OUTPUT, cache) {
|
|
228
270
|
const url = `${config.baseUrl}/messages`;
|
|
229
271
|
const messages = buildAnthropicMessages(prompt, { head });
|
|
230
|
-
const
|
|
272
|
+
const systemResult = buildAnthropicSystem(prompt.system, cache);
|
|
273
|
+
if (systemResult.isFailure()) {
|
|
274
|
+
return fail(systemResult.message);
|
|
275
|
+
}
|
|
276
|
+
const body = Object.assign({ model: config.model, system: systemResult.value, messages,
|
|
231
277
|
// Anthropic's Messages API requires max_tokens on every request — see
|
|
232
278
|
// AiAssist.DEFAULT_ANTHROPIC_MAX_TOKENS for why only this provider defaults it.
|
|
233
279
|
max_tokens: maxTokens !== null && maxTokens !== void 0 ? maxTokens : DEFAULT_ANTHROPIC_MAX_TOKENS }, (temperature !== undefined ? { temperature } : {}));
|
|
@@ -278,6 +324,8 @@ async function callAnthropicCompletion(config, prompt, head, temperature, logger
|
|
|
278
324
|
}
|
|
279
325
|
const rawContent = jsonResult.value.content;
|
|
280
326
|
const stopReason = jsonResult.value.stop_reason;
|
|
327
|
+
const rawAnthropicUsage = jsonResult.value.usage;
|
|
328
|
+
const anthropicUsage = normalizeAnthropicUsage(isJsonObject(rawAnthropicUsage) ? rawAnthropicUsage : undefined);
|
|
281
329
|
if (!Array.isArray(rawContent)) {
|
|
282
330
|
return fail('Anthropic API response: content is not an array');
|
|
283
331
|
}
|
|
@@ -287,11 +335,7 @@ async function callAnthropicCompletion(config, prompt, head, temperature, logger
|
|
|
287
335
|
const extracted = structured.enforcement === 'tool-forced'
|
|
288
336
|
? extractAnthropicStructuredOutput(rawContent)
|
|
289
337
|
: extractAnthropicText(rawContent);
|
|
290
|
-
return extracted.onSuccess((text) => succeed({
|
|
291
|
-
content: text,
|
|
292
|
-
truncated: stopReason === 'max_tokens',
|
|
293
|
-
structuredOutput: structured.enforcement
|
|
294
|
-
}));
|
|
338
|
+
return extracted.onSuccess((text) => succeed(Object.assign({ content: text, truncated: stopReason === 'max_tokens', structuredOutput: structured.enforcement }, (anthropicUsage !== undefined ? { usage: anthropicUsage } : {}))));
|
|
295
339
|
}
|
|
296
340
|
// ============================================================================
|
|
297
341
|
// Google Gemini adapter
|
|
@@ -339,22 +383,35 @@ async function callGeminiCompletion(config, prompt, head, temperature, logger, t
|
|
|
339
383
|
if (jsonResult.isFailure()) {
|
|
340
384
|
return fail(jsonResult.message);
|
|
341
385
|
}
|
|
386
|
+
const rawGeminiUsage = jsonResult.value.usageMetadata;
|
|
387
|
+
const geminiUsage = normalizeGeminiUsage(isJsonObject(rawGeminiUsage) ? rawGeminiUsage : undefined);
|
|
342
388
|
return geminiResponse
|
|
343
389
|
.validate(jsonResult.value)
|
|
344
390
|
.withErrorFormat((msg) => `Gemini API response: ${msg}`)
|
|
345
391
|
.onSuccess((response) => {
|
|
392
|
+
var _a, _b, _c;
|
|
346
393
|
const candidate = response.candidates[0];
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
394
|
+
// ALL parts, not `parts[0]`. Gemini may split one reply across several
|
|
395
|
+
// text parts, and reading only the first silently discards the rest —
|
|
396
|
+
// yielding a truncated document that often still parses, which is the
|
|
397
|
+
// worst way to be wrong. The streaming adapter has always concatenated
|
|
398
|
+
// (`fullText += part.text`); this path did not, so the same response gave
|
|
399
|
+
// different text depending on which one you called.
|
|
400
|
+
//
|
|
401
|
+
// `part.text ?? ''` rather than `part.text`: a reasoning or tool part contributes no text,
|
|
402
|
+
// and joining `undefined` would put the literal string "undefined" into the reply.
|
|
403
|
+
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 : '';
|
|
404
|
+
if (content.length === 0) {
|
|
405
|
+
// Nothing came back. If the candidate says why, say so — an empty string handed to a
|
|
406
|
+
// caller is indistinguishable from a model that legitimately replied with nothing, and
|
|
407
|
+
// sends them looking in the wrong place.
|
|
408
|
+
const reason = candidate.finishReason;
|
|
409
|
+
if (reason !== undefined && !benignGeminiFinishReasons.has(reason)) {
|
|
410
|
+
const suffix = candidate.finishMessage ? ` — ${candidate.finishMessage}` : '';
|
|
411
|
+
return fail(`Gemini completion declined: ${reason}${suffix}`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return succeed(Object.assign({ content, truncated: candidate.finishReason === 'MAX_TOKENS', structuredOutput: structured.enforcement }, (geminiUsage !== undefined ? { usage: geminiUsage } : {})));
|
|
358
415
|
});
|
|
359
416
|
}
|
|
360
417
|
// ============================================================================
|
|
@@ -367,7 +424,7 @@ async function callGeminiCompletion(config, prompt, head, temperature, logger, t
|
|
|
367
424
|
* @public
|
|
368
425
|
*/
|
|
369
426
|
export async function callProviderCompletion(params) {
|
|
370
|
-
const { descriptor, apiKey, system, messages, temperature, modelOverride, tier, logger, tools, signal, endpoint, thinking, maxTokens, structuredOutput } = params;
|
|
427
|
+
const { descriptor, apiKey, system, messages, temperature, modelOverride, tier, logger, tools, signal, endpoint, thinking, maxTokens, structuredOutput, cache } = params;
|
|
371
428
|
const splitResult = splitChatRequest(system, messages);
|
|
372
429
|
if (splitResult.isFailure()) {
|
|
373
430
|
return fail(splitResult.message);
|
|
@@ -444,15 +501,30 @@ export async function callProviderCompletion(params) {
|
|
|
444
501
|
`tools=${toolTypes}, supported=${supported}`);
|
|
445
502
|
}
|
|
446
503
|
switch (descriptor.apiFormat) {
|
|
447
|
-
case 'openai':
|
|
504
|
+
case 'openai': {
|
|
448
505
|
// Responses-API-only models (e.g. gpt-5.5-pro) 400 on /chat/completions, so they route
|
|
449
506
|
// to the Responses path even with no tools requested — same path the tools case uses.
|
|
507
|
+
// `cache` carries two independently-supported things, and they are gated separately.
|
|
508
|
+
// `systemBreakpoints` restructures `content` and needs the server to tolerate an
|
|
509
|
+
// unrecognized field inside it (OpenAI only). `cacheKey` is an opaque routing string that
|
|
510
|
+
// makes repeated requests with a shared prefix land on the same cache-holding server —
|
|
511
|
+
// which xAI needs precisely because it does per-server byte-for-byte prefix matching.
|
|
512
|
+
// Gating them together withheld the routing key from the provider that depends on it most.
|
|
513
|
+
// Every descriptor supporting neither (Groq, Mistral, Ollama, openai-compat) still gets the
|
|
514
|
+
// exact request body it would have gotten had the caller passed no `cache` at all.
|
|
515
|
+
const routing = supportsPromptCacheRouting(descriptor);
|
|
516
|
+
const breakpoints = supportsPromptCacheBreakpoints(descriptor) ? cache === null || cache === void 0 ? void 0 : cache.systemBreakpoints : undefined;
|
|
517
|
+
const routedKey = routing !== undefined ? cache === null || cache === void 0 ? void 0 : cache.cacheKey : undefined;
|
|
518
|
+
const gatedCache = breakpoints !== undefined || routedKey !== undefined
|
|
519
|
+
? { systemBreakpoints: breakpoints, cacheKey: routedKey }
|
|
520
|
+
: undefined;
|
|
450
521
|
if (usesResponsesApi) {
|
|
451
|
-
return callOpenAiResponsesCompletion(config, prompt, tools, head, temperature, logger, signal, resolvedThinking, maxTokens, resolvedStructured);
|
|
522
|
+
return callOpenAiResponsesCompletion(config, prompt, tools, head, temperature, logger, signal, resolvedThinking, maxTokens, resolvedStructured, supportsCacheUsageReporting(descriptor), gatedCache);
|
|
452
523
|
}
|
|
453
|
-
return callOpenAiCompletion(config, prompt, head, temperature, logger, signal, resolvedThinking, maxTokens, usesMaxCompletionTokensField(descriptor), resolvedStructured);
|
|
524
|
+
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);
|
|
525
|
+
}
|
|
454
526
|
case 'anthropic':
|
|
455
|
-
return callAnthropicCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking, isAdaptiveThinkingModel(descriptor, config.model), maxTokens, resolvedStructured);
|
|
527
|
+
return callAnthropicCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking, isAdaptiveThinkingModel(descriptor, config.model), maxTokens, resolvedStructured, cache);
|
|
456
528
|
case 'gemini':
|
|
457
529
|
return callGeminiCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking, maxTokens, resolvedStructured);
|
|
458
530
|
/* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */
|
|
@@ -476,7 +548,7 @@ export async function callProviderCompletion(params) {
|
|
|
476
548
|
* @public
|
|
477
549
|
*/
|
|
478
550
|
export async function callProxiedCompletion(proxyUrl, params) {
|
|
479
|
-
const { descriptor, apiKey, system, messages, temperature, modelOverride, logger, tools, signal, thinking, maxTokens, structuredOutput } = params;
|
|
551
|
+
const { descriptor, apiKey, system, messages, temperature, modelOverride, logger, tools, signal, thinking, maxTokens, structuredOutput, cache } = params;
|
|
480
552
|
const splitResult = splitChatRequest(system, messages);
|
|
481
553
|
if (splitResult.isFailure()) {
|
|
482
554
|
return fail(splitResult.message);
|
|
@@ -523,6 +595,11 @@ export async function callProxiedCompletion(proxyUrl, params) {
|
|
|
523
595
|
? { onUnsupported: structuredOutput.onUnsupported }
|
|
524
596
|
: {}));
|
|
525
597
|
}
|
|
598
|
+
// `IAiCacheRequest` is plain numbers and a string — JSON-serializable as-is, unlike
|
|
599
|
+
// `structuredOutput`'s schema above, so it needs no wire projection before forwarding.
|
|
600
|
+
if (cache !== undefined) {
|
|
601
|
+
body.cache = cache;
|
|
602
|
+
}
|
|
526
603
|
/* c8 ignore next 1 - optional logger */
|
|
527
604
|
logger === null || logger === void 0 ? void 0 : logger.info(`AI proxy request: provider=${descriptor.id}, proxy=${proxyUrl}`);
|
|
528
605
|
const url = `${proxyUrl}/api/ai/completion`;
|