@fgv/ts-extras 5.1.0-55 → 5.1.0-57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/CAPABILITIES.md +96 -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 +72 -7
  5. package/dist/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
  6. package/dist/packlets/ai-assist/completionClient.js +195 -58
  7. package/dist/packlets/ai-assist/completionClient.js.map +1 -1
  8. package/dist/packlets/ai-assist/converters.js +54 -0
  9. package/dist/packlets/ai-assist/converters.js.map +1 -1
  10. package/dist/packlets/ai-assist/embeddingClient.js +16 -1
  11. package/dist/packlets/ai-assist/embeddingClient.js.map +1 -1
  12. package/dist/packlets/ai-assist/imageGenerationClient.js +17 -2
  13. package/dist/packlets/ai-assist/imageGenerationClient.js.map +1 -1
  14. package/dist/packlets/ai-assist/index.js +3 -1
  15. package/dist/packlets/ai-assist/index.js.map +1 -1
  16. package/dist/packlets/ai-assist/listModelsClient.js +19 -1
  17. package/dist/packlets/ai-assist/listModelsClient.js.map +1 -1
  18. package/dist/packlets/ai-assist/model.js.map +1 -1
  19. package/dist/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.js +200 -0
  20. package/dist/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.js.map +1 -0
  21. package/dist/packlets/ai-assist/promptCacheObservabilityHarness.js +137 -0
  22. package/dist/packlets/ai-assist/promptCacheObservabilityHarness.js.map +1 -0
  23. package/dist/packlets/ai-assist/promptCacheRoutingAbHarness.js +156 -0
  24. package/dist/packlets/ai-assist/promptCacheRoutingAbHarness.js.map +1 -0
  25. package/dist/packlets/ai-assist/promptCacheUsageMath.js +124 -0
  26. package/dist/packlets/ai-assist/promptCacheUsageMath.js.map +1 -0
  27. package/dist/packlets/ai-assist/registry.js +9 -11
  28. package/dist/packlets/ai-assist/registry.js.map +1 -1
  29. package/dist/packlets/ai-assist/streamUsageCapability.js +134 -0
  30. package/dist/packlets/ai-assist/streamUsageCapability.js.map +1 -0
  31. package/dist/packlets/ai-assist/streamingAdapters/anthropic.js +32 -9
  32. package/dist/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
  33. package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +81 -35
  34. package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -1
  35. package/dist/packlets/ai-assist/streamingAdapters/common.js +25 -1
  36. package/dist/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
  37. package/dist/packlets/ai-assist/streamingAdapters/gemini.js +12 -5
  38. package/dist/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
  39. package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js +45 -11
  40. package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -1
  41. package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js +33 -22
  42. package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
  43. package/dist/packlets/ai-assist/streamingAdapters/proxy.js +62 -4
  44. package/dist/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -1
  45. package/dist/packlets/ai-assist/streamingClient.js +20 -5
  46. package/dist/packlets/ai-assist/streamingClient.js.map +1 -1
  47. package/dist/packlets/ai-assist/thinkingOptionsResolver.js +23 -5
  48. package/dist/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -1
  49. package/dist/packlets/ai-assist/toolFormats.js +80 -0
  50. package/dist/packlets/ai-assist/toolFormats.js.map +1 -1
  51. package/dist/packlets/ai-assist/toolTypes.js +21 -0
  52. package/dist/packlets/ai-assist/toolTypes.js.map +1 -0
  53. package/dist/packlets/ai-assist/usageNormalization.js +178 -0
  54. package/dist/packlets/ai-assist/usageNormalization.js.map +1 -0
  55. package/dist/packlets/ai-assist/usageTypes.js +21 -0
  56. package/dist/packlets/ai-assist/usageTypes.js.map +1 -0
  57. package/dist/packlets/mustache/index.js.map +1 -1
  58. package/dist/packlets/mustache/interfaces.js.map +1 -1
  59. package/dist/packlets/mustache/mustacheTemplate.js +91 -0
  60. package/dist/packlets/mustache/mustacheTemplate.js.map +1 -1
  61. package/dist/ts-extras.d.ts +764 -48
  62. package/lib/packlets/ai-assist/cacheRequest.d.ts +100 -0
  63. package/lib/packlets/ai-assist/cacheRequest.d.ts.map +1 -0
  64. package/lib/packlets/ai-assist/cacheRequest.js +123 -0
  65. package/lib/packlets/ai-assist/cacheRequest.js.map +1 -0
  66. package/lib/packlets/ai-assist/chatRequestBuilders.d.ts +36 -6
  67. package/lib/packlets/ai-assist/chatRequestBuilders.d.ts.map +1 -1
  68. package/lib/packlets/ai-assist/chatRequestBuilders.js +75 -7
  69. package/lib/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
  70. package/lib/packlets/ai-assist/completionClient.d.ts +19 -0
  71. package/lib/packlets/ai-assist/completionClient.d.ts.map +1 -1
  72. package/lib/packlets/ai-assist/completionClient.js +194 -57
  73. package/lib/packlets/ai-assist/completionClient.js.map +1 -1
  74. package/lib/packlets/ai-assist/converters.d.ts +46 -0
  75. package/lib/packlets/ai-assist/converters.d.ts.map +1 -1
  76. package/lib/packlets/ai-assist/converters.js +55 -1
  77. package/lib/packlets/ai-assist/converters.js.map +1 -1
  78. package/lib/packlets/ai-assist/embeddingClient.d.ts +9 -0
  79. package/lib/packlets/ai-assist/embeddingClient.d.ts.map +1 -1
  80. package/lib/packlets/ai-assist/embeddingClient.js +16 -1
  81. package/lib/packlets/ai-assist/embeddingClient.js.map +1 -1
  82. package/lib/packlets/ai-assist/imageGenerationClient.d.ts +9 -0
  83. package/lib/packlets/ai-assist/imageGenerationClient.d.ts.map +1 -1
  84. package/lib/packlets/ai-assist/imageGenerationClient.js +17 -2
  85. package/lib/packlets/ai-assist/imageGenerationClient.js.map +1 -1
  86. package/lib/packlets/ai-assist/index.d.ts +7 -2
  87. package/lib/packlets/ai-assist/index.d.ts.map +1 -1
  88. package/lib/packlets/ai-assist/index.js +11 -1
  89. package/lib/packlets/ai-assist/index.js.map +1 -1
  90. package/lib/packlets/ai-assist/listModelsClient.d.ts +12 -0
  91. package/lib/packlets/ai-assist/listModelsClient.d.ts.map +1 -1
  92. package/lib/packlets/ai-assist/listModelsClient.js +19 -1
  93. package/lib/packlets/ai-assist/listModelsClient.js.map +1 -1
  94. package/lib/packlets/ai-assist/model.d.ts +81 -295
  95. package/lib/packlets/ai-assist/model.d.ts.map +1 -1
  96. package/lib/packlets/ai-assist/model.js.map +1 -1
  97. package/lib/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.d.ts +124 -0
  98. package/lib/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.d.ts.map +1 -0
  99. package/lib/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.js +208 -0
  100. package/lib/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.js.map +1 -0
  101. package/lib/packlets/ai-assist/promptCacheObservabilityHarness.d.ts +77 -0
  102. package/lib/packlets/ai-assist/promptCacheObservabilityHarness.d.ts.map +1 -0
  103. package/lib/packlets/ai-assist/promptCacheObservabilityHarness.js +143 -0
  104. package/lib/packlets/ai-assist/promptCacheObservabilityHarness.js.map +1 -0
  105. package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.d.ts +84 -0
  106. package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.d.ts.map +1 -0
  107. package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.js +163 -0
  108. package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.js.map +1 -0
  109. package/lib/packlets/ai-assist/promptCacheUsageMath.d.ts +105 -0
  110. package/lib/packlets/ai-assist/promptCacheUsageMath.d.ts.map +1 -0
  111. package/lib/packlets/ai-assist/promptCacheUsageMath.js +130 -0
  112. package/lib/packlets/ai-assist/promptCacheUsageMath.js.map +1 -0
  113. package/lib/packlets/ai-assist/registry.d.ts.map +1 -1
  114. package/lib/packlets/ai-assist/registry.js +9 -11
  115. package/lib/packlets/ai-assist/registry.js.map +1 -1
  116. package/lib/packlets/ai-assist/streamUsageCapability.d.ts +129 -0
  117. package/lib/packlets/ai-assist/streamUsageCapability.d.ts.map +1 -0
  118. package/lib/packlets/ai-assist/streamUsageCapability.js +140 -0
  119. package/lib/packlets/ai-assist/streamUsageCapability.js.map +1 -0
  120. package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts +2 -1
  121. package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts.map +1 -1
  122. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js +29 -6
  123. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
  124. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts +58 -14
  125. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts.map +1 -1
  126. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +81 -36
  127. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -1
  128. package/lib/packlets/ai-assist/streamingAdapters/common.d.ts +38 -3
  129. package/lib/packlets/ai-assist/streamingAdapters/common.d.ts.map +1 -1
  130. package/lib/packlets/ai-assist/streamingAdapters/common.js +25 -1
  131. package/lib/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
  132. package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts.map +1 -1
  133. package/lib/packlets/ai-assist/streamingAdapters/gemini.js +12 -5
  134. package/lib/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
  135. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts +2 -1
  136. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts.map +1 -1
  137. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js +42 -8
  138. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -1
  139. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts +2 -1
  140. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts.map +1 -1
  141. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js +30 -19
  142. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
  143. package/lib/packlets/ai-assist/streamingAdapters/proxy.d.ts +22 -0
  144. package/lib/packlets/ai-assist/streamingAdapters/proxy.d.ts.map +1 -1
  145. package/lib/packlets/ai-assist/streamingAdapters/proxy.js +62 -4
  146. package/lib/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -1
  147. package/lib/packlets/ai-assist/streamingClient.d.ts +5 -0
  148. package/lib/packlets/ai-assist/streamingClient.d.ts.map +1 -1
  149. package/lib/packlets/ai-assist/streamingClient.js +20 -5
  150. package/lib/packlets/ai-assist/streamingClient.js.map +1 -1
  151. package/lib/packlets/ai-assist/thinkingOptionsResolver.d.ts +11 -3
  152. package/lib/packlets/ai-assist/thinkingOptionsResolver.d.ts.map +1 -1
  153. package/lib/packlets/ai-assist/thinkingOptionsResolver.js +23 -5
  154. package/lib/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -1
  155. package/lib/packlets/ai-assist/toolFormats.d.ts +55 -1
  156. package/lib/packlets/ai-assist/toolFormats.d.ts.map +1 -1
  157. package/lib/packlets/ai-assist/toolFormats.js +82 -0
  158. package/lib/packlets/ai-assist/toolFormats.js.map +1 -1
  159. package/lib/packlets/ai-assist/toolTypes.d.ts +386 -0
  160. package/lib/packlets/ai-assist/toolTypes.d.ts.map +1 -0
  161. package/lib/packlets/ai-assist/toolTypes.js +22 -0
  162. package/lib/packlets/ai-assist/toolTypes.js.map +1 -0
  163. package/lib/packlets/ai-assist/usageNormalization.d.ts +58 -0
  164. package/lib/packlets/ai-assist/usageNormalization.d.ts.map +1 -0
  165. package/lib/packlets/ai-assist/usageNormalization.js +184 -0
  166. package/lib/packlets/ai-assist/usageNormalization.js.map +1 -0
  167. package/lib/packlets/ai-assist/usageTypes.d.ts +84 -0
  168. package/lib/packlets/ai-assist/usageTypes.d.ts.map +1 -0
  169. package/lib/packlets/ai-assist/usageTypes.js +22 -0
  170. package/lib/packlets/ai-assist/usageTypes.js.map +1 -0
  171. package/lib/packlets/mustache/index.d.ts +1 -1
  172. package/lib/packlets/mustache/index.d.ts.map +1 -1
  173. package/lib/packlets/mustache/index.js.map +1 -1
  174. package/lib/packlets/mustache/interfaces.d.ts +30 -0
  175. package/lib/packlets/mustache/interfaces.d.ts.map +1 -1
  176. package/lib/packlets/mustache/interfaces.js.map +1 -1
  177. package/lib/packlets/mustache/mustacheTemplate.d.ts +56 -1
  178. package/lib/packlets/mustache/mustacheTemplate.d.ts.map +1 -1
  179. package/lib/packlets/mustache/mustacheTemplate.js +91 -0
  180. package/lib/packlets/mustache/mustacheTemplate.js.map +1 -1
  181. package/package.json +16 -16
  182. package/perf/promptCacheAnthropicBreakpoint.js +135 -0
  183. package/perf/promptCacheObservability.js +107 -0
  184. package/perf/promptCacheRoutingAb.js +105 -0
@@ -21,8 +21,18 @@
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");
35
+ const converters_1 = require("./converters");
26
36
  const thinkingOptionsResolver_1 = require("./thinkingOptionsResolver");
27
37
  const chatRequestBuilders_1 = require("./chatRequestBuilders");
28
38
  const endpoint_1 = require("./endpoint");
@@ -30,6 +40,8 @@ const http_1 = require("./http");
30
40
  const toolFormats_1 = require("./toolFormats");
31
41
  const structuredOutput_1 = require("./structuredOutput");
32
42
  const registry_1 = require("./registry");
43
+ const streamUsageCapability_1 = require("./streamUsageCapability");
44
+ const usageNormalization_1 = require("./usageNormalization");
33
45
  const openAiMessage = ts_utils_1.Validators.object({
34
46
  content: ts_utils_1.Validators.string
35
47
  });
@@ -55,15 +67,22 @@ const responsesApiResponse = ts_utils_1.Validators.object({
55
67
  status: ts_utils_1.Validators.string
56
68
  });
57
69
  const geminiPart = ts_utils_1.Validators.object({
58
- text: ts_utils_1.Validators.string
70
+ text: ts_utils_1.Validators.string.optional()
59
71
  });
60
72
  const geminiContent = ts_utils_1.Validators.object({
61
- parts: ts_utils_1.Validators.arrayOf(geminiPart).withConstraint((arr) => arr.length > 0)
73
+ // No non-empty constraint: `parts: []` is a shape Gemini actually returns, and rejecting it
74
+ // here reports a schema violation for what is really "the model produced no output".
75
+ parts: ts_utils_1.Validators.arrayOf(geminiPart).optional()
62
76
  });
63
77
  const geminiCandidate = ts_utils_1.Validators.object({
64
- content: geminiContent,
65
- finishReason: ts_utils_1.Validators.string
78
+ content: geminiContent.optional(),
79
+ finishReason: ts_utils_1.Validators.string.optional(),
80
+ finishMessage: ts_utils_1.Validators.string.optional()
66
81
  });
82
+ // Terminal reasons that mean "the model finished normally", as distinct from a refusal. Kept in
83
+ // step with the identical set in `imageGenerationClient.ts`; both exist so an empty response can
84
+ // be reported as the decline it is rather than as an empty string or a parse error.
85
+ const benignGeminiFinishReasons = new Set(['STOP', 'MAX_TOKENS']);
67
86
  const geminiResponse = ts_utils_1.Validators.object({
68
87
  candidates: ts_utils_1.Validators.arrayOf(geminiCandidate).withConstraint((arr) => arr.length > 0)
69
88
  });
@@ -75,36 +94,52 @@ const geminiResponse = ts_utils_1.Validators.object({
75
94
  * Works for xAI Grok, OpenAI, Groq, and Mistral.
76
95
  * @internal
77
96
  */
78
- async function callOpenAiCompletion(config, prompt, head, temperature, logger, signal, resolvedThinking, maxTokens, useMaxCompletionTokensField = false, structured = structuredOutput_1.NO_STRUCTURED_OUTPUT) {
97
+ async function callOpenAiCompletion(config, prompt, head, temperature, logger, signal, resolvedThinking, maxTokens, useMaxCompletionTokensField = false, structured = structuredOutput_1.NO_STRUCTURED_OUTPUT, reportsUsage = false, cache, cacheKeyHeader) {
79
98
  var _a;
80
99
  const url = `${config.baseUrl}/chat/completions`;
81
- const messages = (0, chatRequestBuilders_1.buildMessages)(prompt.system, (0, chatRequestBuilders_1.buildOpenAiChatUserContent)(prompt), {
100
+ const systemContentResult = (0, chatRequestBuilders_1.buildOpenAiChatSystemContent)(prompt.system, cache);
101
+ if (systemContentResult.isFailure()) {
102
+ return (0, ts_utils_1.fail)(systemContentResult.message);
103
+ }
104
+ const messages = (0, chatRequestBuilders_1.buildMessages)(systemContentResult.value, (0, chatRequestBuilders_1.buildOpenAiChatUserContent)(prompt), {
82
105
  head
83
106
  });
84
107
  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
108
  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 } : {}));
109
+ 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
110
+ ? // eslint-disable-next-line @typescript-eslint/naming-convention -- wire field name
111
+ { prompt_cache_key: cache.cacheKey }
112
+ : {}));
87
113
  if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.otherParams) !== undefined) {
88
114
  Object.assign(body, resolvedThinking.otherParams);
89
115
  }
90
116
  Object.assign(body, structured.wire);
91
117
  const headers = (0, endpoint_1.bearerAuthHeader)(config.apiKey);
118
+ // Sticky-routing key, when the provider carries it as a header rather than a body field.
119
+ // xAI's prompt cache is per-server, so an identical prefix can still miss if the request is
120
+ // routed to a different box; `x-grok-conv-id` pins it.
121
+ if ((cache === null || cache === void 0 ? void 0 : cache.cacheKey) !== undefined && cacheKeyHeader !== undefined) {
122
+ headers[cacheKeyHeader] = cache.cacheKey;
123
+ }
92
124
  /* c8 ignore next 1 - optional logger */
93
125
  logger === null || logger === void 0 ? void 0 : logger.info(`OpenAI completion: model=${config.model}`);
94
126
  const jsonResult = await (0, http_1.fetchJson)(url, headers, body, logger, signal);
95
127
  if (jsonResult.isFailure()) {
96
128
  return (0, ts_utils_1.fail)(jsonResult.message);
97
129
  }
130
+ // Only descriptors with confirmed cache-relevant usage reporting are normalized — the
131
+ // adapter is shared by Groq/Mistral/Ollama/openai-compat too, and an ordinary usage block
132
+ // from one of those carries no cache information. See supportsCacheUsageReporting.
133
+ const rawUsage = jsonResult.value.usage;
134
+ const usage = reportsUsage
135
+ ? (0, usageNormalization_1.normalizeOpenAiChatUsage)((0, ts_json_base_1.isJsonObject)(rawUsage) ? rawUsage : undefined)
136
+ : undefined;
98
137
  return openAiResponse
99
138
  .validate(jsonResult.value)
100
139
  .withErrorFormat((msg) => `OpenAI API response: ${msg}`)
101
140
  .onSuccess((response) => {
102
141
  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
- });
142
+ return (0, ts_utils_1.succeed)(Object.assign({ content: choice.message.content, truncated: choice.finish_reason === 'length', structuredOutput: structured.enforcement }, (usage !== undefined ? { usage } : {})));
108
143
  });
109
144
  }
110
145
  // ============================================================================
@@ -131,14 +166,21 @@ function extractResponsesApiText(output) {
131
166
  * Used when tools are configured for an openai-format provider.
132
167
  * @internal
133
168
  */
134
- async function callOpenAiResponsesCompletion(config, prompt, tools = [], head, temperature, logger, signal, resolvedThinking, maxTokens, structured = structuredOutput_1.NO_STRUCTURED_OUTPUT) {
169
+ async function callOpenAiResponsesCompletion(config, prompt, tools = [], head, temperature, logger, signal, resolvedThinking, maxTokens, structured = structuredOutput_1.NO_STRUCTURED_OUTPUT, reportsUsage = false, cache) {
135
170
  var _a;
136
171
  const url = `${config.baseUrl}/responses`;
137
- const input = (0, chatRequestBuilders_1.buildMessages)(prompt.system, (0, chatRequestBuilders_1.buildOpenAiResponsesUserContent)(prompt), {
172
+ const systemContentResult = (0, chatRequestBuilders_1.buildOpenAiResponsesSystemContent)(prompt.system, cache);
173
+ if (systemContentResult.isFailure()) {
174
+ return (0, ts_utils_1.fail)(systemContentResult.message);
175
+ }
176
+ const input = (0, chatRequestBuilders_1.buildMessages)(systemContentResult.value, (0, chatRequestBuilders_1.buildOpenAiResponsesUserContent)(prompt), {
138
177
  head
139
178
  });
140
179
  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 } } : {}));
180
+ 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
181
+ ? // eslint-disable-next-line @typescript-eslint/naming-convention -- wire field name
182
+ { prompt_cache_key: cache.cacheKey }
183
+ : {}));
142
184
  // Shared by OpenAI and xAI — both route through the Responses API with the same field name.
143
185
  if (maxTokens !== undefined) {
144
186
  body.max_output_tokens = maxTokens;
@@ -154,15 +196,16 @@ async function callOpenAiResponsesCompletion(config, prompt, tools = [], head, t
154
196
  if (jsonResult.isFailure()) {
155
197
  return (0, ts_utils_1.fail)(jsonResult.message);
156
198
  }
199
+ // See the identical gate in callOpenAiCompletion above — this route is shared the same way.
200
+ const rawResponsesUsage = jsonResult.value.usage;
201
+ const responsesUsage = reportsUsage
202
+ ? (0, usageNormalization_1.normalizeOpenAiResponsesUsage)((0, ts_json_base_1.isJsonObject)(rawResponsesUsage) ? rawResponsesUsage : undefined)
203
+ : undefined;
157
204
  return responsesApiResponse
158
205
  .validate(jsonResult.value)
159
206
  .withErrorFormat((msg) => `Responses API response: ${msg}`)
160
207
  .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
- }));
208
+ 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
209
  });
167
210
  }
168
211
  // ============================================================================
@@ -228,10 +271,14 @@ function extractAnthropicStructuredOutput(content) {
228
271
  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
272
  }
230
273
  /** 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) {
274
+ async function callAnthropicCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking, useAdaptiveThinking = false, maxTokens, structured = structuredOutput_1.NO_STRUCTURED_OUTPUT, cache) {
232
275
  const url = `${config.baseUrl}/messages`;
233
276
  const messages = (0, chatRequestBuilders_1.buildAnthropicMessages)(prompt, { head });
234
- const body = Object.assign({ model: config.model, system: prompt.system, messages,
277
+ const systemResult = (0, chatRequestBuilders_1.buildAnthropicSystem)(prompt.system, cache);
278
+ if (systemResult.isFailure()) {
279
+ return (0, ts_utils_1.fail)(systemResult.message);
280
+ }
281
+ const body = Object.assign({ model: config.model, system: systemResult.value, messages,
235
282
  // Anthropic's Messages API requires max_tokens on every request — see
236
283
  // AiAssist.DEFAULT_ANTHROPIC_MAX_TOKENS for why only this provider defaults it.
237
284
  max_tokens: maxTokens !== null && maxTokens !== void 0 ? maxTokens : model_1.DEFAULT_ANTHROPIC_MAX_TOKENS }, (temperature !== undefined ? { temperature } : {}));
@@ -282,6 +329,8 @@ async function callAnthropicCompletion(config, prompt, head, temperature, logger
282
329
  }
283
330
  const rawContent = jsonResult.value.content;
284
331
  const stopReason = jsonResult.value.stop_reason;
332
+ const rawAnthropicUsage = jsonResult.value.usage;
333
+ const anthropicUsage = (0, usageNormalization_1.normalizeAnthropicUsage)((0, ts_json_base_1.isJsonObject)(rawAnthropicUsage) ? rawAnthropicUsage : undefined);
285
334
  if (!Array.isArray(rawContent)) {
286
335
  return (0, ts_utils_1.fail)('Anthropic API response: content is not an array');
287
336
  }
@@ -291,11 +340,7 @@ async function callAnthropicCompletion(config, prompt, head, temperature, logger
291
340
  const extracted = structured.enforcement === 'tool-forced'
292
341
  ? extractAnthropicStructuredOutput(rawContent)
293
342
  : 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
- }));
343
+ 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
344
  }
300
345
  // ============================================================================
301
346
  // Google Gemini adapter
@@ -343,22 +388,35 @@ async function callGeminiCompletion(config, prompt, head, temperature, logger, t
343
388
  if (jsonResult.isFailure()) {
344
389
  return (0, ts_utils_1.fail)(jsonResult.message);
345
390
  }
391
+ const rawGeminiUsage = jsonResult.value.usageMetadata;
392
+ const geminiUsage = (0, usageNormalization_1.normalizeGeminiUsage)((0, ts_json_base_1.isJsonObject)(rawGeminiUsage) ? rawGeminiUsage : undefined);
346
393
  return geminiResponse
347
394
  .validate(jsonResult.value)
348
395
  .withErrorFormat((msg) => `Gemini API response: ${msg}`)
349
396
  .onSuccess((response) => {
397
+ var _a, _b, _c;
350
398
  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
- });
399
+ // ALL parts, not `parts[0]`. Gemini may split one reply across several
400
+ // text parts, and reading only the first silently discards the rest —
401
+ // yielding a truncated document that often still parses, which is the
402
+ // worst way to be wrong. The streaming adapter has always concatenated
403
+ // (`fullText += part.text`); this path did not, so the same response gave
404
+ // different text depending on which one you called.
405
+ //
406
+ // `part.text ?? ''` rather than `part.text`: a reasoning or tool part contributes no text,
407
+ // and joining `undefined` would put the literal string "undefined" into the reply.
408
+ 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 : '';
409
+ if (content.length === 0) {
410
+ // Nothing came back. If the candidate says why, say so — an empty string handed to a
411
+ // caller is indistinguishable from a model that legitimately replied with nothing, and
412
+ // sends them looking in the wrong place.
413
+ const reason = candidate.finishReason;
414
+ if (reason !== undefined && !benignGeminiFinishReasons.has(reason)) {
415
+ const suffix = candidate.finishMessage ? ` — ${candidate.finishMessage}` : '';
416
+ return (0, ts_utils_1.fail)(`Gemini completion declined: ${reason}${suffix}`);
417
+ }
418
+ }
419
+ return (0, ts_utils_1.succeed)(Object.assign({ content, truncated: candidate.finishReason === 'MAX_TOKENS', structuredOutput: structured.enforcement }, (geminiUsage !== undefined ? { usage: geminiUsage } : {})));
362
420
  });
363
421
  }
364
422
  // ============================================================================
@@ -371,7 +429,7 @@ async function callGeminiCompletion(config, prompt, head, temperature, logger, t
371
429
  * @public
372
430
  */
373
431
  async function callProviderCompletion(params) {
374
- const { descriptor, apiKey, system, messages, temperature, modelOverride, tier, logger, tools, signal, endpoint, thinking, maxTokens, structuredOutput } = params;
432
+ const { descriptor, apiKey, system, messages, temperature, modelOverride, tier, logger, tools, signal, endpoint, thinking, maxTokens, structuredOutput, cache } = params;
375
433
  const splitResult = (0, chatRequestBuilders_1.splitChatRequest)(system, messages);
376
434
  if (splitResult.isFailure()) {
377
435
  return (0, ts_utils_1.fail)(splitResult.message);
@@ -448,15 +506,30 @@ async function callProviderCompletion(params) {
448
506
  `tools=${toolTypes}, supported=${supported}`);
449
507
  }
450
508
  switch (descriptor.apiFormat) {
451
- case 'openai':
509
+ case 'openai': {
452
510
  // Responses-API-only models (e.g. gpt-5.5-pro) 400 on /chat/completions, so they route
453
511
  // to the Responses path even with no tools requested — same path the tools case uses.
512
+ // `cache` carries two independently-supported things, and they are gated separately.
513
+ // `systemBreakpoints` restructures `content` and needs the server to tolerate an
514
+ // unrecognized field inside it (OpenAI only). `cacheKey` is an opaque routing string that
515
+ // makes repeated requests with a shared prefix land on the same cache-holding server —
516
+ // which xAI needs precisely because it does per-server byte-for-byte prefix matching.
517
+ // Gating them together withheld the routing key from the provider that depends on it most.
518
+ // Every descriptor supporting neither (Groq, Mistral, Ollama, openai-compat) still gets the
519
+ // exact request body it would have gotten had the caller passed no `cache` at all.
520
+ const routing = (0, streamUsageCapability_1.supportsPromptCacheRouting)(descriptor);
521
+ const breakpoints = (0, streamUsageCapability_1.supportsPromptCacheBreakpoints)(descriptor) ? cache === null || cache === void 0 ? void 0 : cache.systemBreakpoints : undefined;
522
+ const routedKey = routing !== undefined ? cache === null || cache === void 0 ? void 0 : cache.cacheKey : undefined;
523
+ const gatedCache = breakpoints !== undefined || routedKey !== undefined
524
+ ? { systemBreakpoints: breakpoints, cacheKey: routedKey }
525
+ : undefined;
454
526
  if (usesResponsesApi) {
455
- return callOpenAiResponsesCompletion(config, prompt, tools, head, temperature, logger, signal, resolvedThinking, maxTokens, resolvedStructured);
527
+ return callOpenAiResponsesCompletion(config, prompt, tools, head, temperature, logger, signal, resolvedThinking, maxTokens, resolvedStructured, (0, streamUsageCapability_1.supportsCacheUsageReporting)(descriptor), gatedCache);
456
528
  }
457
- return callOpenAiCompletion(config, prompt, head, temperature, logger, signal, resolvedThinking, maxTokens, (0, model_1.usesMaxCompletionTokensField)(descriptor), resolvedStructured);
529
+ 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);
530
+ }
458
531
  case 'anthropic':
459
- return callAnthropicCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking, (0, model_1.isAdaptiveThinkingModel)(descriptor, config.model), maxTokens, resolvedStructured);
532
+ return callAnthropicCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking, (0, model_1.isAdaptiveThinkingModel)(descriptor, config.model), maxTokens, resolvedStructured, cache);
460
533
  case 'gemini':
461
534
  return callGeminiCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking, maxTokens, resolvedStructured);
462
535
  /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */
@@ -475,12 +548,25 @@ async function callProviderCompletion(params) {
475
548
  * CORS, and API key forwarding. The request body serializes the unified
476
549
  * {@link AiAssist.IChatRequest} shape (`system?` + `messages`). Enforces the same
477
550
  * non-empty / trailing-user-turn and image-input invariants as the direct path.
551
+ *
552
+ * @remarks
553
+ * Two parameters are handled before the body is composed, rather than forwarded.
554
+ * `tier` is resolved here and sent as a concrete `modelOverride`, because both
555
+ * halves of the resolution live on the descriptor the caller already holds — so a
556
+ * proxy needs no `tier` vocabulary, where a `tier` body field it did not understand
557
+ * would be ignored and silently serve a frontier request from the base model.
558
+ * `endpoint` is **refused**: it names where the prompt must go, the proxy is what
559
+ * makes that call, and the field has never been sent to one — so honoring it is
560
+ * unverifiable and ignoring it would reach the provider's default upstream instead
561
+ * of the host the caller pinned. Use `callProviderCompletion`, or point the
562
+ * proxy itself at the intended upstream.
563
+ *
478
564
  * @param proxyUrl - Base URL of the proxy server
479
565
  * @param params - Same parameters as {@link callProviderCompletion}
480
566
  * @public
481
567
  */
482
568
  async function callProxiedCompletion(proxyUrl, params) {
483
- const { descriptor, apiKey, system, messages, temperature, modelOverride, logger, tools, signal, thinking, maxTokens, structuredOutput } = params;
569
+ const { descriptor, apiKey, system, messages, temperature, modelOverride, logger, tools, signal, thinking, maxTokens, structuredOutput, cache, tier, endpoint } = params;
484
570
  const splitResult = (0, chatRequestBuilders_1.splitChatRequest)(system, messages);
485
571
  if (splitResult.isFailure()) {
486
572
  return (0, ts_utils_1.fail)(splitResult.message);
@@ -488,6 +574,48 @@ async function callProxiedCompletion(proxyUrl, params) {
488
574
  if (splitResult.value.prompt.attachments.length > 0 && !descriptor.acceptsImageInput) {
489
575
  return (0, ts_utils_1.fail)(`provider "${descriptor.id}" does not accept image input`);
490
576
  }
577
+ // `endpoint` names the upstream the request must reach. Unlike `tier` it cannot be
578
+ // resolved on this side, because the proxy is the one making that call — it would
579
+ // have to be forwarded and honored. No proxy honors it: the field has never been
580
+ // sent, so every deployed proxy would ignore it and fall back to the provider's
581
+ // public API.
582
+ //
583
+ // Silently is the problem. A caller names an endpoint to pin *where the prompt
584
+ // goes* — a self-hosted model, a LAN deployment, a residency boundary — so the
585
+ // failure is not a degraded answer, it is the content going somewhere they
586
+ // explicitly said it must not, discoverable only by watching the proxy's traffic.
587
+ // That is the same shape as the structured-output silent-drop this function
588
+ // already refuses to allow, with a worse consequence.
589
+ //
590
+ // So refuse rather than pretend. Honoring `endpoint` over a proxy needs a wire
591
+ // field AND a report-back the caller can verify, the way `structuredOutput` is
592
+ // acknowledged below; until that protocol exists, the direct path is where this
593
+ // parameter works.
594
+ if (endpoint !== undefined) {
595
+ return (0, ts_utils_1.fail)(`callProxiedCompletion: endpoint is not supported on the proxied path — a proxy ` +
596
+ `cannot confirm it honored it, and silently reaching the provider's default ` +
597
+ `upstream would send the request somewhere the caller excluded. Use ` +
598
+ `callProviderCompletion, or route the proxy itself at the intended upstream.`);
599
+ }
600
+ // The quality tier is resolved HERE rather than forwarded, because it can be:
601
+ // the tier walk and the alias map both live on the descriptor, which is
602
+ // client-side, so this side already holds everything the resolution needs.
603
+ // Sending the concrete model through the existing `modelOverride` field means a
604
+ // proxy needs no new vocabulary and an already-deployed one honors the tier
605
+ // without being updated — where a `tier` body field it did not understand would
606
+ // be ignored, silently serving a frontier request from the base model.
607
+ //
608
+ // Only when a tier was actually asked for. With no tier, `modelOverride` passes
609
+ // through exactly as before, including absent — which is what lets a proxy apply
610
+ // its own default.
611
+ let effectiveModelOverride = modelOverride;
612
+ if (tier !== undefined) {
613
+ const tierResult = (0, model_1.resolveProviderModel)(descriptor, modelOverride, tier);
614
+ if (tierResult.isFailure()) {
615
+ return (0, ts_utils_1.fail)(tierResult.message);
616
+ }
617
+ effectiveModelOverride = tierResult.value;
618
+ }
491
619
  const body = {
492
620
  providerId: descriptor.id,
493
621
  apiKey,
@@ -501,8 +629,8 @@ async function callProxiedCompletion(proxyUrl, params) {
501
629
  if (system !== undefined) {
502
630
  body.system = system;
503
631
  }
504
- if (modelOverride !== undefined) {
505
- body.modelOverride = modelOverride;
632
+ if (effectiveModelOverride !== undefined) {
633
+ body.modelOverride = effectiveModelOverride;
506
634
  }
507
635
  if (tools && tools.length > 0) {
508
636
  body.tools = tools;
@@ -521,12 +649,19 @@ async function callProxiedCompletion(proxyUrl, params) {
521
649
  // `JsonSchema.fromJson(raw)` before calling `callProviderCompletion`.
522
650
  body.structuredOutput =
523
651
  structuredOutput.mode === 'schema'
524
- ? Object.assign({ mode: 'schema', schema: structuredOutput.schema.toJson() }, (structuredOutput.onUnsupported !== undefined
652
+ ? Object.assign(Object.assign({ mode: 'schema', schema: structuredOutput.schema.toJson() }, (structuredOutput.onUnsupported !== undefined
525
653
  ? { onUnsupported: structuredOutput.onUnsupported }
654
+ : {})), (structuredOutput.adaptOptionalToNullable !== undefined
655
+ ? { adaptOptionalToNullable: structuredOutput.adaptOptionalToNullable }
526
656
  : {})) : Object.assign({ mode: 'json-object' }, (structuredOutput.onUnsupported !== undefined
527
657
  ? { onUnsupported: structuredOutput.onUnsupported }
528
658
  : {}));
529
659
  }
660
+ // `IAiCacheRequest` is plain numbers and a string — JSON-serializable as-is, unlike
661
+ // `structuredOutput`'s schema above, so it needs no wire projection before forwarding.
662
+ if (cache !== undefined) {
663
+ body.cache = cache;
664
+ }
530
665
  /* c8 ignore next 1 - optional logger */
531
666
  logger === null || logger === void 0 ? void 0 : logger.info(`AI proxy request: provider=${descriptor.id}, proxy=${proxyUrl}`);
532
667
  const url = `${proxyUrl}/api/ai/completion`;
@@ -546,22 +681,24 @@ async function callProxiedCompletion(proxyUrl, params) {
546
681
  // rather than a response claiming an enforcement nobody verified — a proxy
547
682
  // predating this feature drops the constraint silently, which is the exact
548
683
  // failure this surface exists to remove.
684
+ // A proxy relaying `callProviderCompletion`'s result carries an already-normalized
685
+ // IAiCompletionUsage, not a provider wire shape — so this validates that shape
686
+ // rather than reaching for the per-provider normalizers. A proxy that reports
687
+ // nothing, or reports something malformed, yields `undefined`: the documented
688
+ // "no normalized usage exposed", never a partially-trusted object and never a
689
+ // fabricated zero.
690
+ const usage = converters_1.aiCompletionUsage.convert(response.usage).orDefault();
691
+ // Built once: the two returns differ only in the enforcement they report, and
692
+ // duplicating the shape is how a field added to one and not the other drifts.
693
+ const base = Object.assign({ content: response.content, truncated: response.truncated === true }, (usage !== undefined ? { usage } : {}));
549
694
  if (structuredOutput === undefined) {
550
- return (0, ts_utils_1.succeed)({
551
- content: response.content,
552
- truncated: response.truncated === true,
553
- structuredOutput: 'none'
554
- });
695
+ return (0, ts_utils_1.succeed)(Object.assign(Object.assign({}, base), { structuredOutput: 'none' }));
555
696
  }
556
697
  if (!(0, structuredOutput_1.isStructuredOutputEnforcement)(response.structuredOutput)) {
557
698
  return (0, ts_utils_1.fail)(`proxy did not report which structured-output constraint it applied ` +
558
699
  `(got ${JSON.stringify(response.structuredOutput)}); it may predate the feature and have ` +
559
700
  `dropped the request silently`);
560
701
  }
561
- return (0, ts_utils_1.succeed)({
562
- content: response.content,
563
- truncated: response.truncated === true,
564
- structuredOutput: response.structuredOutput
565
- });
702
+ return (0, ts_utils_1.succeed)(Object.assign(Object.assign({}, base), { structuredOutput: response.structuredOutput }));
566
703
  }
567
704
  //# sourceMappingURL=completionClient.js.map