@fgv/ts-extras 5.1.0-54 → 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 (166) 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/structuredOutput.js +84 -7
  40. package/dist/packlets/ai-assist/structuredOutput.js.map +1 -1
  41. package/dist/packlets/ai-assist/structuredOutputTypes.js.map +1 -1
  42. package/dist/packlets/ai-assist/thinkingOptionsResolver.js +18 -3
  43. package/dist/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -1
  44. package/dist/packlets/ai-assist/toolFormats.js +80 -0
  45. package/dist/packlets/ai-assist/toolFormats.js.map +1 -1
  46. package/dist/packlets/ai-assist/toolTypes.js +21 -0
  47. package/dist/packlets/ai-assist/toolTypes.js.map +1 -0
  48. package/dist/packlets/ai-assist/usageNormalization.js +178 -0
  49. package/dist/packlets/ai-assist/usageNormalization.js.map +1 -0
  50. package/dist/packlets/ai-assist/usageTypes.js +21 -0
  51. package/dist/packlets/ai-assist/usageTypes.js.map +1 -0
  52. package/dist/packlets/mustache/index.js.map +1 -1
  53. package/dist/packlets/mustache/interfaces.js.map +1 -1
  54. package/dist/packlets/mustache/mustacheTemplate.js +91 -0
  55. package/dist/packlets/mustache/mustacheTemplate.js.map +1 -1
  56. package/dist/ts-extras.d.ts +577 -30
  57. package/lib/packlets/ai-assist/cacheRequest.d.ts +100 -0
  58. package/lib/packlets/ai-assist/cacheRequest.d.ts.map +1 -0
  59. package/lib/packlets/ai-assist/cacheRequest.js +123 -0
  60. package/lib/packlets/ai-assist/cacheRequest.js.map +1 -0
  61. package/lib/packlets/ai-assist/chatRequestBuilders.d.ts +34 -4
  62. package/lib/packlets/ai-assist/chatRequestBuilders.d.ts.map +1 -1
  63. package/lib/packlets/ai-assist/chatRequestBuilders.js +73 -5
  64. package/lib/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
  65. package/lib/packlets/ai-assist/completionClient.d.ts +6 -0
  66. package/lib/packlets/ai-assist/completionClient.d.ts.map +1 -1
  67. package/lib/packlets/ai-assist/completionClient.js +121 -44
  68. package/lib/packlets/ai-assist/completionClient.js.map +1 -1
  69. package/lib/packlets/ai-assist/imageGenerationClient.d.ts.map +1 -1
  70. package/lib/packlets/ai-assist/imageGenerationClient.js +1 -1
  71. package/lib/packlets/ai-assist/imageGenerationClient.js.map +1 -1
  72. package/lib/packlets/ai-assist/index.d.ts +7 -2
  73. package/lib/packlets/ai-assist/index.d.ts.map +1 -1
  74. package/lib/packlets/ai-assist/index.js +11 -1
  75. package/lib/packlets/ai-assist/index.js.map +1 -1
  76. package/lib/packlets/ai-assist/model.d.ts +59 -288
  77. package/lib/packlets/ai-assist/model.d.ts.map +1 -1
  78. package/lib/packlets/ai-assist/model.js.map +1 -1
  79. package/lib/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.d.ts +124 -0
  80. package/lib/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.d.ts.map +1 -0
  81. package/lib/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.js +208 -0
  82. package/lib/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.js.map +1 -0
  83. package/lib/packlets/ai-assist/promptCacheObservabilityHarness.d.ts +77 -0
  84. package/lib/packlets/ai-assist/promptCacheObservabilityHarness.d.ts.map +1 -0
  85. package/lib/packlets/ai-assist/promptCacheObservabilityHarness.js +143 -0
  86. package/lib/packlets/ai-assist/promptCacheObservabilityHarness.js.map +1 -0
  87. package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.d.ts +84 -0
  88. package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.d.ts.map +1 -0
  89. package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.js +163 -0
  90. package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.js.map +1 -0
  91. package/lib/packlets/ai-assist/promptCacheUsageMath.d.ts +105 -0
  92. package/lib/packlets/ai-assist/promptCacheUsageMath.d.ts.map +1 -0
  93. package/lib/packlets/ai-assist/promptCacheUsageMath.js +130 -0
  94. package/lib/packlets/ai-assist/promptCacheUsageMath.js.map +1 -0
  95. package/lib/packlets/ai-assist/registry.d.ts.map +1 -1
  96. package/lib/packlets/ai-assist/registry.js +9 -11
  97. package/lib/packlets/ai-assist/registry.js.map +1 -1
  98. package/lib/packlets/ai-assist/streamUsageCapability.d.ts +119 -0
  99. package/lib/packlets/ai-assist/streamUsageCapability.d.ts.map +1 -0
  100. package/lib/packlets/ai-assist/streamUsageCapability.js +130 -0
  101. package/lib/packlets/ai-assist/streamUsageCapability.js.map +1 -0
  102. package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts.map +1 -1
  103. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js +23 -4
  104. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
  105. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts +21 -12
  106. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts.map +1 -1
  107. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +64 -34
  108. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -1
  109. package/lib/packlets/ai-assist/streamingAdapters/common.d.ts +21 -0
  110. package/lib/packlets/ai-assist/streamingAdapters/common.d.ts.map +1 -1
  111. package/lib/packlets/ai-assist/streamingAdapters/common.js +25 -1
  112. package/lib/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
  113. package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts.map +1 -1
  114. package/lib/packlets/ai-assist/streamingAdapters/gemini.js +12 -5
  115. package/lib/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
  116. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts +1 -1
  117. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts.map +1 -1
  118. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js +33 -7
  119. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -1
  120. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts +1 -1
  121. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts.map +1 -1
  122. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js +21 -13
  123. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
  124. package/lib/packlets/ai-assist/streamingClient.d.ts.map +1 -1
  125. package/lib/packlets/ai-assist/streamingClient.js +3 -2
  126. package/lib/packlets/ai-assist/streamingClient.js.map +1 -1
  127. package/lib/packlets/ai-assist/structuredOutput.d.ts +32 -0
  128. package/lib/packlets/ai-assist/structuredOutput.d.ts.map +1 -1
  129. package/lib/packlets/ai-assist/structuredOutput.js +85 -7
  130. package/lib/packlets/ai-assist/structuredOutput.js.map +1 -1
  131. package/lib/packlets/ai-assist/structuredOutputTypes.d.ts +30 -0
  132. package/lib/packlets/ai-assist/structuredOutputTypes.d.ts.map +1 -1
  133. package/lib/packlets/ai-assist/structuredOutputTypes.js.map +1 -1
  134. package/lib/packlets/ai-assist/thinkingOptionsResolver.d.ts.map +1 -1
  135. package/lib/packlets/ai-assist/thinkingOptionsResolver.js +18 -3
  136. package/lib/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -1
  137. package/lib/packlets/ai-assist/toolFormats.d.ts +55 -1
  138. package/lib/packlets/ai-assist/toolFormats.d.ts.map +1 -1
  139. package/lib/packlets/ai-assist/toolFormats.js +82 -0
  140. package/lib/packlets/ai-assist/toolFormats.js.map +1 -1
  141. package/lib/packlets/ai-assist/toolTypes.d.ts +328 -0
  142. package/lib/packlets/ai-assist/toolTypes.d.ts.map +1 -0
  143. package/lib/packlets/ai-assist/toolTypes.js +22 -0
  144. package/lib/packlets/ai-assist/toolTypes.js.map +1 -0
  145. package/lib/packlets/ai-assist/usageNormalization.d.ts +58 -0
  146. package/lib/packlets/ai-assist/usageNormalization.d.ts.map +1 -0
  147. package/lib/packlets/ai-assist/usageNormalization.js +184 -0
  148. package/lib/packlets/ai-assist/usageNormalization.js.map +1 -0
  149. package/lib/packlets/ai-assist/usageTypes.d.ts +84 -0
  150. package/lib/packlets/ai-assist/usageTypes.d.ts.map +1 -0
  151. package/lib/packlets/ai-assist/usageTypes.js +22 -0
  152. package/lib/packlets/ai-assist/usageTypes.js.map +1 -0
  153. package/lib/packlets/mustache/index.d.ts +1 -1
  154. package/lib/packlets/mustache/index.d.ts.map +1 -1
  155. package/lib/packlets/mustache/index.js.map +1 -1
  156. package/lib/packlets/mustache/interfaces.d.ts +30 -0
  157. package/lib/packlets/mustache/interfaces.d.ts.map +1 -1
  158. package/lib/packlets/mustache/interfaces.js.map +1 -1
  159. package/lib/packlets/mustache/mustacheTemplate.d.ts +56 -1
  160. package/lib/packlets/mustache/mustacheTemplate.d.ts.map +1 -1
  161. package/lib/packlets/mustache/mustacheTemplate.js +91 -0
  162. package/lib/packlets/mustache/mustacheTemplate.js.map +1 -1
  163. package/package.json +16 -16
  164. package/perf/promptCacheAnthropicBreakpoint.js +135 -0
  165. package/perf/promptCacheObservability.js +107 -0
  166. 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: Validators.arrayOf(geminiPart).withConstraint((arr) => arr.length > 0)
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 messages = buildMessages(prompt.system, buildOpenAiChatUserContent(prompt), {
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 input = buildMessages(prompt.system, buildOpenAiResponsesUserContent(prompt), {
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 body = Object.assign({ model: config.model, system: prompt.system, messages,
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
- return succeed({
348
- // ALL parts, not `parts[0]`. Gemini may split one reply across several
349
- // text parts, and reading only the first silently discards the rest —
350
- // yielding a truncated document that often still parses, which is the
351
- // worst way to be wrong. The streaming adapter has always concatenated
352
- // (`fullText += part.text`); this path did not, so the same response gave
353
- // different text depending on which one you called.
354
- content: candidate.content.parts.map((part) => part.text).join(''),
355
- truncated: candidate.finishReason === 'MAX_TOKENS',
356
- structuredOutput: structured.enforcement
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`;