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