@fgv/ts-extras 5.1.0-47 → 5.1.0-49

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 (83) hide show
  1. package/LICENSE +1 -1
  2. package/dist/packlets/ai-assist/completionClient.js +435 -0
  3. package/dist/packlets/ai-assist/completionClient.js.map +1 -0
  4. package/dist/packlets/ai-assist/embeddingClient.js +2 -2
  5. package/dist/packlets/ai-assist/embeddingClient.js.map +1 -1
  6. package/dist/packlets/ai-assist/endpoint.js +29 -0
  7. package/dist/packlets/ai-assist/endpoint.js.map +1 -1
  8. package/dist/packlets/ai-assist/http.js +48 -19
  9. package/dist/packlets/ai-assist/http.js.map +1 -1
  10. package/dist/packlets/ai-assist/imageGenerationClient.js +454 -0
  11. package/dist/packlets/ai-assist/imageGenerationClient.js.map +1 -0
  12. package/dist/packlets/ai-assist/index.js +3 -1
  13. package/dist/packlets/ai-assist/index.js.map +1 -1
  14. package/dist/packlets/ai-assist/jsonCompletion.js +1 -1
  15. package/dist/packlets/ai-assist/jsonCompletion.js.map +1 -1
  16. package/dist/packlets/ai-assist/listModelsClient.js +294 -0
  17. package/dist/packlets/ai-assist/listModelsClient.js.map +1 -0
  18. package/dist/packlets/ai-assist/streamingAdapters/anthropic.js +2 -5
  19. package/dist/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
  20. package/dist/packlets/ai-assist/streamingAdapters/gemini.js +2 -1
  21. package/dist/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
  22. package/dist/packlets/crypto-utils/encryptedFile.js +37 -0
  23. package/dist/packlets/crypto-utils/encryptedFile.js.map +1 -1
  24. package/dist/packlets/crypto-utils/index.js +1 -1
  25. package/dist/packlets/crypto-utils/index.js.map +1 -1
  26. package/dist/packlets/zip-file-tree/zipFileTreeAccessors.js +32 -0
  27. package/dist/packlets/zip-file-tree/zipFileTreeAccessors.js.map +1 -1
  28. package/dist/ts-extras.d.ts +59 -4
  29. package/lib/packlets/ai-assist/completionClient.d.ts +74 -0
  30. package/lib/packlets/ai-assist/completionClient.d.ts.map +1 -0
  31. package/lib/packlets/ai-assist/completionClient.js +439 -0
  32. package/lib/packlets/ai-assist/completionClient.js.map +1 -0
  33. package/lib/packlets/ai-assist/embeddingClient.js +1 -1
  34. package/lib/packlets/ai-assist/embeddingClient.js.map +1 -1
  35. package/lib/packlets/ai-assist/endpoint.d.ts +21 -0
  36. package/lib/packlets/ai-assist/endpoint.d.ts.map +1 -1
  37. package/lib/packlets/ai-assist/endpoint.js +31 -0
  38. package/lib/packlets/ai-assist/endpoint.js.map +1 -1
  39. package/lib/packlets/ai-assist/http.d.ts +12 -2
  40. package/lib/packlets/ai-assist/http.d.ts.map +1 -1
  41. package/lib/packlets/ai-assist/http.js +49 -18
  42. package/lib/packlets/ai-assist/http.js.map +1 -1
  43. package/lib/packlets/ai-assist/imageGenerationClient.d.ts +46 -0
  44. package/lib/packlets/ai-assist/imageGenerationClient.d.ts.map +1 -0
  45. package/lib/packlets/ai-assist/imageGenerationClient.js +458 -0
  46. package/lib/packlets/ai-assist/imageGenerationClient.js.map +1 -0
  47. package/lib/packlets/ai-assist/index.d.ts +3 -1
  48. package/lib/packlets/ai-assist/index.d.ts.map +1 -1
  49. package/lib/packlets/ai-assist/index.js +9 -7
  50. package/lib/packlets/ai-assist/index.js.map +1 -1
  51. package/lib/packlets/ai-assist/jsonCompletion.d.ts +1 -1
  52. package/lib/packlets/ai-assist/jsonCompletion.d.ts.map +1 -1
  53. package/lib/packlets/ai-assist/jsonCompletion.js +2 -2
  54. package/lib/packlets/ai-assist/jsonCompletion.js.map +1 -1
  55. package/lib/packlets/ai-assist/listModelsClient.d.ts +46 -0
  56. package/lib/packlets/ai-assist/listModelsClient.d.ts.map +1 -0
  57. package/lib/packlets/ai-assist/listModelsClient.js +298 -0
  58. package/lib/packlets/ai-assist/listModelsClient.js.map +1 -0
  59. package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts.map +1 -1
  60. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js +2 -5
  61. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
  62. package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts.map +1 -1
  63. package/lib/packlets/ai-assist/streamingAdapters/gemini.js +2 -1
  64. package/lib/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
  65. package/lib/packlets/crypto-utils/encryptedFile.d.ts +25 -0
  66. package/lib/packlets/crypto-utils/encryptedFile.d.ts.map +1 -1
  67. package/lib/packlets/crypto-utils/encryptedFile.js +38 -0
  68. package/lib/packlets/crypto-utils/encryptedFile.js.map +1 -1
  69. package/lib/packlets/crypto-utils/index.d.ts +1 -1
  70. package/lib/packlets/crypto-utils/index.d.ts.map +1 -1
  71. package/lib/packlets/crypto-utils/index.js +2 -1
  72. package/lib/packlets/crypto-utils/index.js.map +1 -1
  73. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.d.ts +30 -2
  74. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.d.ts.map +1 -1
  75. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.js +32 -0
  76. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.js.map +1 -1
  77. package/package.json +7 -7
  78. package/dist/packlets/ai-assist/apiClient.js +0 -1236
  79. package/dist/packlets/ai-assist/apiClient.js.map +0 -1
  80. package/lib/packlets/ai-assist/apiClient.d.ts +0 -152
  81. package/lib/packlets/ai-assist/apiClient.d.ts.map +0 -1
  82. package/lib/packlets/ai-assist/apiClient.js +0 -1244
  83. package/lib/packlets/ai-assist/apiClient.js.map +0 -1
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2020 Erik Fortune
3
+ Copyright (c) 2026 Erik Fortune
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -0,0 +1,435 @@
1
+ // Copyright (c) 2026 Erik Fortune
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+ //
10
+ // The above copyright notice and this permission notice shall be included in all
11
+ // copies or substantial portions of the Software.
12
+ //
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ // SOFTWARE.
20
+ import { fail, succeed, Validators } from '@fgv/ts-utils';
21
+ import { DEFAULT_ANTHROPIC_MAX_TOKENS, isAdaptiveThinkingModel, isResponsesOnlyModel, resolveProviderModel, usesMaxCompletionTokensField } from './model';
22
+ import { anthropicEffortToBudgetTokens, checkTemperatureConflict, mergeThinkingConfig, providerDiscriminatorForId } from './thinkingOptionsResolver';
23
+ import { buildAnthropicMessages, buildGeminiContents, buildMessages, buildOpenAiChatUserContent, buildOpenAiResponsesUserContent, normalizeOutboundMessages, splitChatRequest } from './chatRequestBuilders';
24
+ import { anthropicAuthHeaders, bearerAuthHeader, geminiAuthHeader, resolveEffectiveBaseUrl } from './endpoint';
25
+ import { fetchJson } from './http';
26
+ import { toAnthropicTools, toGeminiTools, toResponsesApiTools } from './toolFormats';
27
+ const openAiMessage = Validators.object({
28
+ content: Validators.string
29
+ });
30
+ const openAiChoice = Validators.object({
31
+ message: openAiMessage,
32
+ finish_reason: Validators.string
33
+ });
34
+ const openAiResponse = Validators.object({
35
+ choices: Validators.arrayOf(openAiChoice).withConstraint((arr) => arr.length > 0)
36
+ });
37
+ const responsesApiOutputText = Validators.object({
38
+ type: Validators.literal('output_text'),
39
+ text: Validators.string
40
+ });
41
+ const responsesApiMessage = Validators.object({
42
+ type: Validators.literal('message'),
43
+ role: Validators.string,
44
+ content: Validators.arrayOf(responsesApiOutputText).withConstraint((arr) => arr.length > 0)
45
+ });
46
+ const responsesApiOutputItem = Validators.isA('object', (v) => typeof v === 'object' && v !== null);
47
+ const responsesApiResponse = Validators.object({
48
+ output: Validators.arrayOf(responsesApiOutputItem).withConstraint((arr) => arr.length > 0),
49
+ status: Validators.string
50
+ });
51
+ const geminiPart = Validators.object({
52
+ text: Validators.string
53
+ });
54
+ const geminiContent = Validators.object({
55
+ parts: Validators.arrayOf(geminiPart).withConstraint((arr) => arr.length > 0)
56
+ });
57
+ const geminiCandidate = Validators.object({
58
+ content: geminiContent,
59
+ finishReason: Validators.string
60
+ });
61
+ const geminiResponse = Validators.object({
62
+ candidates: Validators.arrayOf(geminiCandidate).withConstraint((arr) => arr.length > 0)
63
+ });
64
+ // ============================================================================
65
+ // OpenAI-compatible client (Chat Completions — no tools)
66
+ // ============================================================================
67
+ /**
68
+ * Calls an OpenAI-compatible chat completion endpoint.
69
+ * Works for xAI Grok, OpenAI, Groq, and Mistral.
70
+ * @internal
71
+ */
72
+ async function callOpenAiCompletion(config, prompt, head, temperature, logger, signal, resolvedThinking, maxTokens, useMaxCompletionTokensField = false) {
73
+ var _a;
74
+ const url = `${config.baseUrl}/chat/completions`;
75
+ const messages = buildMessages(prompt.system, buildOpenAiChatUserContent(prompt), {
76
+ head
77
+ });
78
+ 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;
79
+ const maxTokensField = useMaxCompletionTokensField ? 'max_completion_tokens' : 'max_tokens';
80
+ 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 } : {}));
81
+ if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.otherParams) !== undefined) {
82
+ Object.assign(body, resolvedThinking.otherParams);
83
+ }
84
+ const headers = bearerAuthHeader(config.apiKey);
85
+ /* c8 ignore next 1 - optional logger */
86
+ logger === null || logger === void 0 ? void 0 : logger.info(`OpenAI completion: model=${config.model}`);
87
+ const jsonResult = await fetchJson(url, headers, body, logger, signal);
88
+ if (jsonResult.isFailure()) {
89
+ return fail(jsonResult.message);
90
+ }
91
+ return openAiResponse
92
+ .validate(jsonResult.value)
93
+ .withErrorFormat((msg) => `OpenAI API response: ${msg}`)
94
+ .onSuccess((response) => {
95
+ const choice = response.choices[0];
96
+ return succeed({
97
+ content: choice.message.content,
98
+ truncated: choice.finish_reason === 'length'
99
+ });
100
+ });
101
+ }
102
+ // ============================================================================
103
+ // OpenAI/xAI Responses API (with tools)
104
+ // ============================================================================
105
+ /**
106
+ * Extracts text content from a Responses API output array.
107
+ * Finds the first message-type output item and concatenates its text content blocks.
108
+ * @internal
109
+ */
110
+ function extractResponsesApiText(output) {
111
+ for (const item of output) {
112
+ if (item.type === 'message') {
113
+ const messageResult = responsesApiMessage.validate(item);
114
+ if (messageResult.isSuccess()) {
115
+ return succeed(messageResult.value.content.map((c) => c.text).join(''));
116
+ }
117
+ }
118
+ }
119
+ return fail('Responses API output contained no message with text content');
120
+ }
121
+ /**
122
+ * Calls the xAI/OpenAI Responses API with server-side tools.
123
+ * Used when tools are configured for an openai-format provider.
124
+ * @internal
125
+ */
126
+ async function callOpenAiResponsesCompletion(config, prompt, tools = [], head, temperature, logger, signal, resolvedThinking, maxTokens) {
127
+ var _a;
128
+ const url = `${config.baseUrl}/responses`;
129
+ const input = buildMessages(prompt.system, buildOpenAiResponsesUserContent(prompt), {
130
+ head
131
+ });
132
+ 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;
133
+ 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 } } : {}));
134
+ // Shared by OpenAI and xAI — both route through the Responses API with the same field name.
135
+ if (maxTokens !== undefined) {
136
+ body.max_output_tokens = maxTokens;
137
+ }
138
+ if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.otherParams) !== undefined) {
139
+ Object.assign(body, resolvedThinking.otherParams);
140
+ }
141
+ const headers = bearerAuthHeader(config.apiKey);
142
+ /* c8 ignore next 1 - optional logger */
143
+ logger === null || logger === void 0 ? void 0 : logger.info(`OpenAI Responses API: model=${config.model}, tools=${tools.map((t) => t.type).join(',')}`);
144
+ const jsonResult = await fetchJson(url, headers, body, logger, signal);
145
+ if (jsonResult.isFailure()) {
146
+ return fail(jsonResult.message);
147
+ }
148
+ return responsesApiResponse
149
+ .validate(jsonResult.value)
150
+ .withErrorFormat((msg) => `Responses API response: ${msg}`)
151
+ .onSuccess((response) => {
152
+ return extractResponsesApiText(response.output).onSuccess((text) => succeed({
153
+ content: text,
154
+ truncated: response.status === 'incomplete'
155
+ }));
156
+ });
157
+ }
158
+ // ============================================================================
159
+ // Anthropic adapter
160
+ // ============================================================================
161
+ /**
162
+ * Extracts text content from Anthropic response content blocks.
163
+ * When tools are used, the content array contains mixed block types
164
+ * (text, server_tool_use, web_search_tool_result). We extract and
165
+ * concatenate only the text blocks.
166
+ * @internal
167
+ */
168
+ function extractAnthropicText(content) {
169
+ const textParts = [];
170
+ for (const block of content) {
171
+ if (typeof block === 'object' && block !== null && 'type' in block) {
172
+ const typed = block;
173
+ if (typed.type === 'text' && typeof typed.text === 'string') {
174
+ textParts.push(typed.text);
175
+ }
176
+ }
177
+ }
178
+ if (textParts.length === 0) {
179
+ return fail('Anthropic response contained no text content blocks');
180
+ }
181
+ return succeed(textParts.join(''));
182
+ }
183
+ /** Calls the Anthropic Messages API with optional tool support. @internal */
184
+ async function callAnthropicCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking, useAdaptiveThinking = false, maxTokens) {
185
+ const url = `${config.baseUrl}/messages`;
186
+ const messages = buildAnthropicMessages(prompt, { head });
187
+ const body = Object.assign({ model: config.model, system: prompt.system, messages,
188
+ // Anthropic's Messages API requires max_tokens on every request — see
189
+ // AiAssist.DEFAULT_ANTHROPIC_MAX_TOKENS for why only this provider defaults it.
190
+ max_tokens: maxTokens !== null && maxTokens !== void 0 ? maxTokens : DEFAULT_ANTHROPIC_MAX_TOKENS }, (temperature !== undefined ? { temperature } : {}));
191
+ const effort = resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.anthropicEffort;
192
+ if (effort !== undefined) {
193
+ if (useAdaptiveThinking) {
194
+ // Claude 5 family: adaptive thinking — no budget_tokens; effort moves to the
195
+ // top-level output_config block. See AiAssist.isAdaptiveThinkingModel.
196
+ body.thinking = { type: 'adaptive' };
197
+ body.output_config = { effort };
198
+ }
199
+ else {
200
+ body.thinking = { type: 'enabled', budget_tokens: anthropicEffortToBudgetTokens(effort) };
201
+ }
202
+ }
203
+ if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.otherParams) !== undefined) {
204
+ Object.assign(body, resolvedThinking.otherParams);
205
+ }
206
+ if (tools && tools.length > 0) {
207
+ body.tools = toAnthropicTools(tools);
208
+ /* c8 ignore next 3 - optional logger diagnostic output */
209
+ logger === null || logger === void 0 ? void 0 : logger.info(`Anthropic completion: model=${config.model}, tools=${tools.map((t) => t.type).join(',')}`);
210
+ }
211
+ else {
212
+ /* c8 ignore next 1 - optional logger */
213
+ logger === null || logger === void 0 ? void 0 : logger.info(`Anthropic completion: model=${config.model}`);
214
+ }
215
+ const headers = anthropicAuthHeaders(config.apiKey);
216
+ const jsonResult = await fetchJson(url, headers, body, logger, signal);
217
+ if (jsonResult.isFailure()) {
218
+ return fail(jsonResult.message);
219
+ }
220
+ const rawContent = jsonResult.value.content;
221
+ const stopReason = jsonResult.value.stop_reason;
222
+ if (!Array.isArray(rawContent)) {
223
+ return fail('Anthropic API response: content is not an array');
224
+ }
225
+ if (typeof stopReason !== 'string') {
226
+ return fail('Anthropic API response: stop_reason is missing or not a string');
227
+ }
228
+ return extractAnthropicText(rawContent).onSuccess((text) => succeed({
229
+ content: text,
230
+ truncated: stopReason === 'max_tokens'
231
+ }));
232
+ }
233
+ // ============================================================================
234
+ // Google Gemini adapter
235
+ // ============================================================================
236
+ /**
237
+ * Calls the Google Gemini generateContent API.
238
+ * When tools are configured, includes Google Search grounding.
239
+ * @internal
240
+ */
241
+ async function callGeminiCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking, maxTokens) {
242
+ const url = `${config.baseUrl}/models/${config.model}:generateContent`;
243
+ const contents = buildGeminiContents(prompt, { head });
244
+ // Temperature is sent only when explicitly provided; otherwise Gemini's default applies.
245
+ const generationConfig = {};
246
+ if (temperature !== undefined) {
247
+ generationConfig.temperature = temperature;
248
+ }
249
+ if (maxTokens !== undefined) {
250
+ generationConfig.maxOutputTokens = maxTokens;
251
+ }
252
+ if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.geminiThinkingBudget) !== undefined) {
253
+ generationConfig.thinkingConfig = { thinkingBudget: resolvedThinking.geminiThinkingBudget };
254
+ }
255
+ if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.otherParams) !== undefined) {
256
+ Object.assign(generationConfig, resolvedThinking.otherParams);
257
+ }
258
+ const body = {
259
+ systemInstruction: { parts: [{ text: prompt.system }] },
260
+ contents,
261
+ generationConfig
262
+ };
263
+ if (tools && tools.length > 0) {
264
+ body.tools = toGeminiTools(tools);
265
+ /* c8 ignore next 1 - optional logger */
266
+ logger === null || logger === void 0 ? void 0 : logger.info(`Gemini completion: model=${config.model}, tools=${tools.map((t) => t.type).join(',')}`);
267
+ }
268
+ else {
269
+ /* c8 ignore next 1 - optional logger */
270
+ logger === null || logger === void 0 ? void 0 : logger.info(`Gemini completion: model=${config.model}`);
271
+ }
272
+ const headers = geminiAuthHeader(config.apiKey);
273
+ const jsonResult = await fetchJson(url, headers, body, logger, signal);
274
+ if (jsonResult.isFailure()) {
275
+ return fail(jsonResult.message);
276
+ }
277
+ return geminiResponse
278
+ .validate(jsonResult.value)
279
+ .withErrorFormat((msg) => `Gemini API response: ${msg}`)
280
+ .onSuccess((response) => {
281
+ const candidate = response.candidates[0];
282
+ return succeed({
283
+ content: candidate.content.parts[0].text,
284
+ truncated: candidate.finishReason === 'MAX_TOKENS'
285
+ });
286
+ });
287
+ }
288
+ // ============================================================================
289
+ // Provider dispatcher
290
+ // ============================================================================
291
+ /**
292
+ * Calls the appropriate chat completion API for a given provider. Routes by
293
+ * `apiFormat`: `'openai'` (xAI/OpenAI/Groq/Mistral — switches to Responses API
294
+ * when tools are set), `'anthropic'`, or `'gemini'`.
295
+ * @public
296
+ */
297
+ export async function callProviderCompletion(params) {
298
+ const { descriptor, apiKey, system, messages, temperature, modelOverride, tier, logger, tools, signal, endpoint, thinking, maxTokens } = params;
299
+ const splitResult = splitChatRequest(system, messages);
300
+ if (splitResult.isFailure()) {
301
+ return fail(splitResult.message);
302
+ }
303
+ const { prompt, head } = splitResult.value;
304
+ const baseUrlResult = resolveEffectiveBaseUrl(descriptor, endpoint);
305
+ if (baseUrlResult.isFailure()) {
306
+ return fail(baseUrlResult.message);
307
+ }
308
+ if (prompt.attachments.length > 0 && !descriptor.acceptsImageInput) {
309
+ return fail(`provider "${descriptor.id}" does not accept image input`);
310
+ }
311
+ const hasTools = tools !== undefined && tools.length > 0;
312
+ const discriminator = providerDiscriminatorForId(descriptor.id);
313
+ // The quality tier is the only completion-model selector; thinking and tools
314
+ // are orthogonal request params/capabilities and never pick a model.
315
+ const modelContext = tier;
316
+ const modelResult = resolveProviderModel(descriptor, modelOverride, modelContext);
317
+ if (modelResult.isFailure()) {
318
+ return fail(modelResult.message);
319
+ }
320
+ const model = modelResult.value;
321
+ let resolvedThinking;
322
+ if (thinking !== undefined) {
323
+ if (discriminator !== undefined) {
324
+ const mergeResult = mergeThinkingConfig(thinking, model, discriminator);
325
+ /* c8 ignore next 3 - mergeThinkingConfig always succeeds; defensive guard */
326
+ if (mergeResult.isFailure()) {
327
+ return fail(mergeResult.message);
328
+ }
329
+ resolvedThinking = mergeResult.value;
330
+ const conflictResult = checkTemperatureConflict(resolvedThinking, discriminator, temperature);
331
+ if (conflictResult.isFailure()) {
332
+ return fail(conflictResult.message);
333
+ }
334
+ }
335
+ }
336
+ const config = {
337
+ baseUrl: baseUrlResult.value,
338
+ apiKey,
339
+ model
340
+ };
341
+ /* c8 ignore next 8 - optional logger diagnostic output */
342
+ if (logger) {
343
+ const toolTypes = hasTools ? tools.map((t) => t.type).join(',') : 'none';
344
+ const supported = descriptor.supportedTools.length > 0 ? descriptor.supportedTools.join(',') : 'none';
345
+ logger.info(`AI completion: provider=${descriptor.id}, format=${descriptor.apiFormat}, model=${config.model}, ` +
346
+ `tools=${toolTypes}, supported=${supported}`);
347
+ }
348
+ switch (descriptor.apiFormat) {
349
+ case 'openai':
350
+ // Responses-API-only models (e.g. gpt-5.5-pro) 400 on /chat/completions, so they route
351
+ // to the Responses path even with no tools requested — same path the tools case uses.
352
+ if (hasTools || isResponsesOnlyModel(descriptor, config.model)) {
353
+ return callOpenAiResponsesCompletion(config, prompt, tools, head, temperature, logger, signal, resolvedThinking, maxTokens);
354
+ }
355
+ return callOpenAiCompletion(config, prompt, head, temperature, logger, signal, resolvedThinking, maxTokens, usesMaxCompletionTokensField(descriptor));
356
+ case 'anthropic':
357
+ return callAnthropicCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking, isAdaptiveThinkingModel(descriptor, config.model), maxTokens);
358
+ case 'gemini':
359
+ return callGeminiCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking, maxTokens);
360
+ /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */
361
+ default: {
362
+ const _exhaustive = descriptor.apiFormat;
363
+ return fail(`unsupported API format: ${String(_exhaustive)}`);
364
+ }
365
+ }
366
+ }
367
+ // ============================================================================
368
+ // Proxied completion (routes through a backend server)
369
+ // ============================================================================
370
+ /**
371
+ * Calls the AI completion endpoint on a proxy server instead of calling the
372
+ * provider API directly from the browser. The proxy handles provider dispatch,
373
+ * CORS, and API key forwarding. The request body serializes the unified
374
+ * {@link AiAssist.IChatRequest} shape (`system?` + `messages`). Enforces the same
375
+ * non-empty / trailing-user-turn and image-input invariants as the direct path.
376
+ * @param proxyUrl - Base URL of the proxy server
377
+ * @param params - Same parameters as {@link callProviderCompletion}
378
+ * @public
379
+ */
380
+ export async function callProxiedCompletion(proxyUrl, params) {
381
+ const { descriptor, apiKey, system, messages, temperature, modelOverride, logger, tools, signal, thinking, maxTokens } = params;
382
+ const splitResult = splitChatRequest(system, messages);
383
+ if (splitResult.isFailure()) {
384
+ return fail(splitResult.message);
385
+ }
386
+ if (splitResult.value.prompt.attachments.length > 0 && !descriptor.acceptsImageInput) {
387
+ return fail(`provider "${descriptor.id}" does not accept image input`);
388
+ }
389
+ const body = {
390
+ providerId: descriptor.id,
391
+ apiKey,
392
+ messages: normalizeOutboundMessages(splitResult.value)
393
+ };
394
+ // Temperature is forwarded only when explicitly provided, matching the direct path — the proxy
395
+ // omits it from the upstream request so the provider default applies.
396
+ if (temperature !== undefined) {
397
+ body.temperature = temperature;
398
+ }
399
+ if (system !== undefined) {
400
+ body.system = system;
401
+ }
402
+ if (modelOverride !== undefined) {
403
+ body.modelOverride = modelOverride;
404
+ }
405
+ if (tools && tools.length > 0) {
406
+ body.tools = tools;
407
+ }
408
+ if (thinking !== undefined) {
409
+ body.thinking = thinking;
410
+ }
411
+ // Forwarded only when explicitly provided; the proxy is responsible for mapping it to the
412
+ // correct upstream provider field (see AiAssist.usesMaxCompletionTokensField).
413
+ if (maxTokens !== undefined) {
414
+ body.maxTokens = maxTokens;
415
+ }
416
+ /* c8 ignore next 1 - optional logger */
417
+ logger === null || logger === void 0 ? void 0 : logger.info(`AI proxy request: provider=${descriptor.id}, proxy=${proxyUrl}`);
418
+ const url = `${proxyUrl}/api/ai/completion`;
419
+ const jsonResult = await fetchJson(url, {}, body, logger, signal);
420
+ if (jsonResult.isFailure()) {
421
+ return fail(jsonResult.message);
422
+ }
423
+ const response = jsonResult.value;
424
+ if (typeof response.error === 'string') {
425
+ return fail(`proxy: ${response.error}`);
426
+ }
427
+ if (typeof response.content !== 'string') {
428
+ return fail('proxy returned invalid response: missing content');
429
+ }
430
+ return succeed({
431
+ content: response.content,
432
+ truncated: response.truncated === true
433
+ });
434
+ }
435
+ //# sourceMappingURL=completionClient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"completionClient.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/completionClient.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * Chat completion clients for AI assist. One provider dispatcher over\n * `IAiProviderDescriptor.apiFormat`, the four adapters it routes to (OpenAI Chat\n * Completions, OpenAI/xAI Responses, Anthropic, Google Gemini), the response\n * validators those adapters use, and the proxied variant of the same modality.\n *\n * @packageDocumentation\n */\n\nimport { type JsonObject } from '@fgv/ts-json-base';\nimport { fail, type Logging, Result, succeed, type Validator, Validators } from '@fgv/ts-utils';\n\nimport {\n AiPrompt,\n type AiServerToolConfig,\n DEFAULT_ANTHROPIC_MAX_TOKENS,\n type IAiCompletionResponse,\n type IAiProviderDescriptor,\n type IChatMessage,\n type IChatRequest,\n type IThinkingConfig,\n type ModelSpec,\n type ModelSpecKey,\n isAdaptiveThinkingModel,\n isResponsesOnlyModel,\n resolveProviderModel,\n usesMaxCompletionTokensField\n} from './model';\nimport {\n anthropicEffortToBudgetTokens,\n checkTemperatureConflict,\n mergeThinkingConfig,\n providerDiscriminatorForId,\n type IResolvedThinkingConfig\n} from './thinkingOptionsResolver';\nimport {\n buildAnthropicMessages,\n buildGeminiContents,\n buildMessages,\n buildOpenAiChatUserContent,\n buildOpenAiResponsesUserContent,\n normalizeOutboundMessages,\n splitChatRequest\n} from './chatRequestBuilders';\nimport {\n anthropicAuthHeaders,\n bearerAuthHeader,\n geminiAuthHeader,\n resolveEffectiveBaseUrl\n} from './endpoint';\nimport { type IAiApiConfig, fetchJson } from './http';\nimport { toAnthropicTools, toGeminiTools, toResponsesApiTools } from './toolFormats';\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Parameters for a provider completion request. Carries the unified\n * {@link AiAssist.IChatRequest} shape (`system?` + ordered `messages`, last =\n * current user turn); history is linearized before the current turn.\n * @public\n */\nexport interface IProviderCompletionParams extends IChatRequest {\n /** The provider descriptor */\n readonly descriptor: IAiProviderDescriptor;\n /** API key for authentication */\n readonly apiKey: string;\n /**\n * Sampling temperature. Sent to the provider only when explicitly provided; omitted otherwise\n * so the provider's own default applies (current-gen models reject a caller-supplied default).\n */\n readonly temperature?: number;\n /** Optional model override — string or context-aware map (uses descriptor.defaultModel otherwise) */\n readonly modelOverride?: ModelSpec;\n /**\n * Optional quality tier selecting which completion model to use. `undefined`\n * selects the `base` tier; `'frontier'` cascades to `advanced` then `base`\n * when a tier is unset for a provider. Orthogonal to `thinking` and `tools`,\n * which never select a model.\n */\n readonly tier?: 'advanced' | 'frontier';\n /** Optional logger for request/response observability. */\n readonly logger?: Logging.ILogger;\n /** Server-side tools to include in the request. Overrides settings-level tool config when provided. */\n readonly tools?: ReadonlyArray<AiServerToolConfig>;\n /** Optional abort signal for cancelling the in-flight request. */\n readonly signal?: AbortSignal;\n /**\n * Optional override of the descriptor's default base URL (scheme + host +\n * optional port + path prefix). The per-route suffix (e.g. `/chat/completions`)\n * is appended unchanged. Must be a well-formed `http`/`https` URL. Auth shape\n * is unchanged: `needsSecret` providers still require an API key.\n */\n readonly endpoint?: string;\n /**\n * Optional thinking/reasoning config. Anthropic, OpenAI, and xAI reject `temperature` when\n * the effective merged effort is non-`'none'`; Gemini always accepts both.\n */\n readonly thinking?: IThinkingConfig;\n /**\n * Optional cap on generated output tokens, mapped to each provider's native field:\n * Anthropic `max_tokens`, OpenAI Chat Completions `max_completion_tokens`, OpenAI/xAI\n * Responses `max_output_tokens`, Gemini `generationConfig.maxOutputTokens`, and the\n * xAI/Groq/Mistral/Ollama/`openai-compat` chat-completions path `max_tokens`. When unset,\n * every provider except Anthropic omits the field and applies its own default; Anthropic's\n * Messages API requires the field, so it falls back to `DEFAULT_ANTHROPIC_MAX_TOKENS`.\n */\n readonly maxTokens?: number;\n}\n\n// ============================================================================\n// Response validators (non-strict — extra API fields preserved for debugging)\n// ============================================================================\n\n// ---- OpenAI Chat Completions format ----\n\n/** @internal */\ninterface IOpenAiMessage {\n content: string;\n}\n/** @internal */\ninterface IOpenAiChoice {\n message: IOpenAiMessage;\n finish_reason: string;\n}\n/** @internal */\ninterface IOpenAiResponse {\n choices: IOpenAiChoice[];\n}\n\nconst openAiMessage: Validator<IOpenAiMessage> = Validators.object<IOpenAiMessage>({\n content: Validators.string\n});\nconst openAiChoice: Validator<IOpenAiChoice> = Validators.object<IOpenAiChoice>({\n message: openAiMessage,\n finish_reason: Validators.string\n});\nconst openAiResponse: Validator<IOpenAiResponse> = Validators.object<IOpenAiResponse>({\n choices: Validators.arrayOf(openAiChoice).withConstraint((arr) => arr.length > 0)\n});\n\n// ---- OpenAI/xAI Responses API format ----\n\n/** @internal */\ninterface IResponsesApiOutputText {\n type: 'output_text';\n text: string;\n}\n/** @internal */\ninterface IResponsesApiMessage {\n type: 'message';\n role: string;\n content: IResponsesApiOutputText[];\n}\n/** @internal */\ninterface IResponsesApiResponse {\n output: Array<Record<string, unknown>>;\n status: string;\n}\n\nconst responsesApiOutputText: Validator<IResponsesApiOutputText> = Validators.object<IResponsesApiOutputText>(\n {\n type: Validators.literal('output_text'),\n text: Validators.string\n }\n);\nconst responsesApiMessage: Validator<IResponsesApiMessage> = Validators.object<IResponsesApiMessage>({\n type: Validators.literal('message'),\n role: Validators.string,\n content: Validators.arrayOf(responsesApiOutputText).withConstraint((arr) => arr.length > 0)\n});\nconst responsesApiOutputItem: Validator<Record<string, unknown>> = Validators.isA(\n 'object',\n (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null\n);\nconst responsesApiResponse: Validator<IResponsesApiResponse> = Validators.object<IResponsesApiResponse>({\n output: Validators.arrayOf(responsesApiOutputItem).withConstraint((arr) => arr.length > 0),\n status: Validators.string\n});\n\n// ---- Gemini format ----\n\n/** @internal */\ninterface IGeminiPart {\n text: string;\n}\n/** @internal */\ninterface IGeminiContent {\n parts: IGeminiPart[];\n}\n/** @internal */\ninterface IGeminiCandidate {\n content: IGeminiContent;\n finishReason: string;\n}\n/** @internal */\ninterface IGeminiResponse {\n candidates: IGeminiCandidate[];\n}\n\nconst geminiPart: Validator<IGeminiPart> = Validators.object<IGeminiPart>({\n text: Validators.string\n});\nconst geminiContent: Validator<IGeminiContent> = Validators.object<IGeminiContent>({\n parts: Validators.arrayOf(geminiPart).withConstraint((arr) => arr.length > 0)\n});\nconst geminiCandidate: Validator<IGeminiCandidate> = Validators.object<IGeminiCandidate>({\n content: geminiContent,\n finishReason: Validators.string\n});\nconst geminiResponse: Validator<IGeminiResponse> = Validators.object<IGeminiResponse>({\n candidates: Validators.arrayOf(geminiCandidate).withConstraint((arr) => arr.length > 0)\n});\n\n// ============================================================================\n// OpenAI-compatible client (Chat Completions — no tools)\n// ============================================================================\n\n/**\n * Calls an OpenAI-compatible chat completion endpoint.\n * Works for xAI Grok, OpenAI, Groq, and Mistral.\n * @internal\n */\nasync function callOpenAiCompletion(\n config: IAiApiConfig,\n prompt: AiPrompt,\n head?: ReadonlyArray<IChatMessage>,\n temperature?: number,\n logger?: Logging.ILogger,\n signal?: AbortSignal,\n resolvedThinking?: IResolvedThinkingConfig,\n maxTokens?: number,\n useMaxCompletionTokensField: boolean = false\n): Promise<Result<IAiCompletionResponse>> {\n const url = `${config.baseUrl}/chat/completions`;\n const messages = buildMessages(prompt.system, buildOpenAiChatUserContent(prompt), {\n head\n });\n const effort = resolvedThinking?.openAiEffort ?? resolvedThinking?.xaiEffort;\n const maxTokensField = useMaxCompletionTokensField ? 'max_completion_tokens' : 'max_tokens';\n const body: Record<string, unknown> = {\n model: config.model,\n messages,\n // Temperature is sent only when the caller explicitly provided one — omitting it lets each\n // provider apply its own default (current-gen models reject a non-default temperature). The\n // completion path already rejects temperature + non-'none' thinking upstream\n // (checkTemperatureConflict), so no effort gate is needed here.\n ...(temperature !== undefined ? { temperature } : {}),\n ...(effort !== undefined && config.model !== 'grok-4' ? { reasoning_effort: effort } : {}),\n // Omitted when the caller doesn't set maxTokens — every non-Anthropic provider applies its\n // own default. See AiAssist.usesMaxCompletionTokensField for the field-name split.\n ...(maxTokens !== undefined ? { [maxTokensField]: maxTokens } : {})\n };\n if (resolvedThinking?.otherParams !== undefined) {\n Object.assign(body, resolvedThinking.otherParams);\n }\n\n const headers: Record<string, string> = bearerAuthHeader(config.apiKey);\n\n /* c8 ignore next 1 - optional logger */\n logger?.info(`OpenAI completion: model=${config.model}`);\n const jsonResult = await fetchJson(url, headers, body, logger, signal);\n if (jsonResult.isFailure()) {\n return fail(jsonResult.message);\n }\n return openAiResponse\n .validate(jsonResult.value)\n .withErrorFormat((msg) => `OpenAI API response: ${msg}`)\n .onSuccess((response) => {\n const choice = response.choices[0];\n return succeed({\n content: choice.message.content,\n truncated: choice.finish_reason === 'length'\n });\n });\n}\n\n// ============================================================================\n// OpenAI/xAI Responses API (with tools)\n// ============================================================================\n\n/**\n * Extracts text content from a Responses API output array.\n * Finds the first message-type output item and concatenates its text content blocks.\n * @internal\n */\nfunction extractResponsesApiText(output: Array<Record<string, unknown>>): Result<string> {\n for (const item of output) {\n if (item.type === 'message') {\n const messageResult = responsesApiMessage.validate(item as JsonObject);\n if (messageResult.isSuccess()) {\n return succeed(messageResult.value.content.map((c) => c.text).join(''));\n }\n }\n }\n return fail('Responses API output contained no message with text content');\n}\n\n/**\n * Calls the xAI/OpenAI Responses API with server-side tools.\n * Used when tools are configured for an openai-format provider.\n * @internal\n */\nasync function callOpenAiResponsesCompletion(\n config: IAiApiConfig,\n prompt: AiPrompt,\n tools: ReadonlyArray<AiServerToolConfig> = [],\n head?: ReadonlyArray<IChatMessage>,\n temperature?: number,\n logger?: Logging.ILogger,\n signal?: AbortSignal,\n resolvedThinking?: IResolvedThinkingConfig,\n maxTokens?: number\n): Promise<Result<IAiCompletionResponse>> {\n const url = `${config.baseUrl}/responses`;\n const input = buildMessages(prompt.system, buildOpenAiResponsesUserContent(prompt), {\n head\n });\n const effort = resolvedThinking?.openAiEffort ?? resolvedThinking?.xaiEffort;\n const body: Record<string, unknown> = {\n model: config.model,\n input,\n // `tools` is omitted entirely when none are requested — a Responses-only model routed\n // here for tier/model reasons (not tools) must not send an empty tools array.\n ...(tools.length > 0 ? { tools: toResponsesApiTools(tools) } : {}),\n // Temperature is sent only when the caller explicitly provided one (see callOpenAiCompletion).\n ...(temperature !== undefined ? { temperature } : {}),\n ...(effort !== undefined && config.model !== 'grok-4' ? { reasoning: { effort } } : {})\n };\n // Shared by OpenAI and xAI — both route through the Responses API with the same field name.\n if (maxTokens !== undefined) {\n body.max_output_tokens = maxTokens;\n }\n if (resolvedThinking?.otherParams !== undefined) {\n Object.assign(body, resolvedThinking.otherParams);\n }\n\n const headers: Record<string, string> = bearerAuthHeader(config.apiKey);\n\n /* c8 ignore next 1 - optional logger */\n logger?.info(`OpenAI Responses API: model=${config.model}, tools=${tools.map((t) => t.type).join(',')}`);\n const jsonResult = await fetchJson(url, headers, body, logger, signal);\n if (jsonResult.isFailure()) {\n return fail(jsonResult.message);\n }\n return responsesApiResponse\n .validate(jsonResult.value)\n .withErrorFormat((msg) => `Responses API response: ${msg}`)\n .onSuccess((response) => {\n return extractResponsesApiText(response.output).onSuccess((text) =>\n succeed({\n content: text,\n truncated: response.status === 'incomplete'\n })\n );\n });\n}\n\n// ============================================================================\n// Anthropic adapter\n// ============================================================================\n\n/**\n * Extracts text content from Anthropic response content blocks.\n * When tools are used, the content array contains mixed block types\n * (text, server_tool_use, web_search_tool_result). We extract and\n * concatenate only the text blocks.\n * @internal\n */\nfunction extractAnthropicText(content: unknown[]): Result<string> {\n const textParts: string[] = [];\n for (const block of content) {\n if (typeof block === 'object' && block !== null && 'type' in block) {\n const typed = block as Record<string, unknown>;\n if (typed.type === 'text' && typeof typed.text === 'string') {\n textParts.push(typed.text);\n }\n }\n }\n if (textParts.length === 0) {\n return fail('Anthropic response contained no text content blocks');\n }\n return succeed(textParts.join(''));\n}\n\n/** Calls the Anthropic Messages API with optional tool support. @internal */\nasync function callAnthropicCompletion(\n config: IAiApiConfig,\n prompt: AiPrompt,\n head?: ReadonlyArray<IChatMessage>,\n temperature?: number,\n logger?: Logging.ILogger,\n tools?: ReadonlyArray<AiServerToolConfig>,\n signal?: AbortSignal,\n resolvedThinking?: IResolvedThinkingConfig,\n useAdaptiveThinking: boolean = false,\n maxTokens?: number\n): Promise<Result<IAiCompletionResponse>> {\n const url = `${config.baseUrl}/messages`;\n const messages = buildAnthropicMessages(prompt, { head });\n const body: Record<string, unknown> = {\n model: config.model,\n system: prompt.system,\n messages,\n // Anthropic's Messages API requires max_tokens on every request — see\n // AiAssist.DEFAULT_ANTHROPIC_MAX_TOKENS for why only this provider defaults it.\n max_tokens: maxTokens ?? DEFAULT_ANTHROPIC_MAX_TOKENS,\n // Temperature is sent only when explicitly provided (Claude-5 rejects any temperature). The\n // completion path rejects temperature + thinking upstream (checkTemperatureConflict), so no\n // effort gate is needed here.\n ...(temperature !== undefined ? { temperature } : {})\n };\n\n const effort = resolvedThinking?.anthropicEffort;\n if (effort !== undefined) {\n if (useAdaptiveThinking) {\n // Claude 5 family: adaptive thinking — no budget_tokens; effort moves to the\n // top-level output_config block. See AiAssist.isAdaptiveThinkingModel.\n body.thinking = { type: 'adaptive' };\n body.output_config = { effort };\n } else {\n body.thinking = { type: 'enabled', budget_tokens: anthropicEffortToBudgetTokens(effort) };\n }\n }\n if (resolvedThinking?.otherParams !== undefined) {\n Object.assign(body, resolvedThinking.otherParams);\n }\n\n if (tools && tools.length > 0) {\n body.tools = toAnthropicTools(tools);\n /* c8 ignore next 3 - optional logger diagnostic output */\n logger?.info(`Anthropic completion: model=${config.model}, tools=${tools.map((t) => t.type).join(',')}`);\n } else {\n /* c8 ignore next 1 - optional logger */\n logger?.info(`Anthropic completion: model=${config.model}`);\n }\n\n const headers: Record<string, string> = anthropicAuthHeaders(config.apiKey);\n\n const jsonResult = await fetchJson(url, headers, body, logger, signal);\n if (jsonResult.isFailure()) {\n return fail(jsonResult.message);\n }\n\n const rawContent = (jsonResult.value as Record<string, unknown>).content;\n const stopReason = (jsonResult.value as Record<string, unknown>).stop_reason;\n if (!Array.isArray(rawContent)) {\n return fail('Anthropic API response: content is not an array');\n }\n if (typeof stopReason !== 'string') {\n return fail('Anthropic API response: stop_reason is missing or not a string');\n }\n return extractAnthropicText(rawContent).onSuccess((text) =>\n succeed({\n content: text,\n truncated: stopReason === 'max_tokens'\n })\n );\n}\n\n// ============================================================================\n// Google Gemini adapter\n// ============================================================================\n\n/**\n * Calls the Google Gemini generateContent API.\n * When tools are configured, includes Google Search grounding.\n * @internal\n */\nasync function callGeminiCompletion(\n config: IAiApiConfig,\n prompt: AiPrompt,\n head?: ReadonlyArray<IChatMessage>,\n temperature?: number,\n logger?: Logging.ILogger,\n tools?: ReadonlyArray<AiServerToolConfig>,\n signal?: AbortSignal,\n resolvedThinking?: IResolvedThinkingConfig,\n maxTokens?: number\n): Promise<Result<IAiCompletionResponse>> {\n const url = `${config.baseUrl}/models/${config.model}:generateContent`;\n const contents = buildGeminiContents(prompt, { head });\n\n // Temperature is sent only when explicitly provided; otherwise Gemini's default applies.\n const generationConfig: Record<string, unknown> = {};\n if (temperature !== undefined) {\n generationConfig.temperature = temperature;\n }\n if (maxTokens !== undefined) {\n generationConfig.maxOutputTokens = maxTokens;\n }\n if (resolvedThinking?.geminiThinkingBudget !== undefined) {\n generationConfig.thinkingConfig = { thinkingBudget: resolvedThinking.geminiThinkingBudget };\n }\n if (resolvedThinking?.otherParams !== undefined) {\n Object.assign(generationConfig, resolvedThinking.otherParams);\n }\n const body: Record<string, unknown> = {\n systemInstruction: { parts: [{ text: prompt.system }] },\n contents,\n generationConfig\n };\n\n if (tools && tools.length > 0) {\n body.tools = toGeminiTools(tools);\n /* c8 ignore next 1 - optional logger */\n logger?.info(`Gemini completion: model=${config.model}, tools=${tools.map((t) => t.type).join(',')}`);\n } else {\n /* c8 ignore next 1 - optional logger */\n logger?.info(`Gemini completion: model=${config.model}`);\n }\n\n const headers: Record<string, string> = geminiAuthHeader(config.apiKey);\n\n const jsonResult = await fetchJson(url, headers, body, logger, signal);\n if (jsonResult.isFailure()) {\n return fail(jsonResult.message);\n }\n return geminiResponse\n .validate(jsonResult.value)\n .withErrorFormat((msg) => `Gemini API response: ${msg}`)\n .onSuccess((response) => {\n const candidate = response.candidates[0];\n return succeed({\n content: candidate.content.parts[0].text,\n truncated: candidate.finishReason === 'MAX_TOKENS'\n });\n });\n}\n\n// ============================================================================\n// Provider dispatcher\n// ============================================================================\n\n/**\n * Calls the appropriate chat completion API for a given provider. Routes by\n * `apiFormat`: `'openai'` (xAI/OpenAI/Groq/Mistral — switches to Responses API\n * when tools are set), `'anthropic'`, or `'gemini'`.\n * @public\n */\nexport async function callProviderCompletion(\n params: IProviderCompletionParams\n): Promise<Result<IAiCompletionResponse>> {\n const {\n descriptor,\n apiKey,\n system,\n messages,\n temperature,\n modelOverride,\n tier,\n logger,\n tools,\n signal,\n endpoint,\n thinking,\n maxTokens\n } = params;\n\n const splitResult = splitChatRequest(system, messages);\n if (splitResult.isFailure()) {\n return fail(splitResult.message);\n }\n const { prompt, head } = splitResult.value;\n\n const baseUrlResult = resolveEffectiveBaseUrl(descriptor, endpoint);\n if (baseUrlResult.isFailure()) {\n return fail(baseUrlResult.message);\n }\n if (prompt.attachments.length > 0 && !descriptor.acceptsImageInput) {\n return fail(`provider \"${descriptor.id}\" does not accept image input`);\n }\n\n const hasTools = tools !== undefined && tools.length > 0;\n const discriminator = providerDiscriminatorForId(descriptor.id);\n // The quality tier is the only completion-model selector; thinking and tools\n // are orthogonal request params/capabilities and never pick a model.\n const modelContext: ModelSpecKey | undefined = tier;\n\n const modelResult = resolveProviderModel(descriptor, modelOverride, modelContext);\n if (modelResult.isFailure()) {\n return fail(modelResult.message);\n }\n const model = modelResult.value;\n\n let resolvedThinking: IResolvedThinkingConfig | undefined;\n if (thinking !== undefined) {\n if (discriminator !== undefined) {\n const mergeResult = mergeThinkingConfig(thinking, model, discriminator);\n /* c8 ignore next 3 - mergeThinkingConfig always succeeds; defensive guard */\n if (mergeResult.isFailure()) {\n return fail(mergeResult.message);\n }\n resolvedThinking = mergeResult.value;\n const conflictResult = checkTemperatureConflict(resolvedThinking, discriminator, temperature);\n if (conflictResult.isFailure()) {\n return fail(conflictResult.message);\n }\n }\n }\n\n const config: IAiApiConfig = {\n baseUrl: baseUrlResult.value,\n apiKey,\n model\n };\n /* c8 ignore next 8 - optional logger diagnostic output */\n if (logger) {\n const toolTypes = hasTools ? tools.map((t) => t.type).join(',') : 'none';\n const supported = descriptor.supportedTools.length > 0 ? descriptor.supportedTools.join(',') : 'none';\n logger.info(\n `AI completion: provider=${descriptor.id}, format=${descriptor.apiFormat}, model=${config.model}, ` +\n `tools=${toolTypes}, supported=${supported}`\n );\n }\n\n switch (descriptor.apiFormat) {\n case 'openai':\n // Responses-API-only models (e.g. gpt-5.5-pro) 400 on /chat/completions, so they route\n // to the Responses path even with no tools requested — same path the tools case uses.\n if (hasTools || isResponsesOnlyModel(descriptor, config.model)) {\n return callOpenAiResponsesCompletion(\n config,\n prompt,\n tools,\n head,\n temperature,\n logger,\n signal,\n resolvedThinking,\n maxTokens\n );\n }\n return callOpenAiCompletion(\n config,\n prompt,\n head,\n temperature,\n logger,\n signal,\n resolvedThinking,\n maxTokens,\n usesMaxCompletionTokensField(descriptor)\n );\n case 'anthropic':\n return callAnthropicCompletion(\n config,\n prompt,\n head,\n temperature,\n logger,\n tools,\n signal,\n resolvedThinking,\n isAdaptiveThinkingModel(descriptor, config.model),\n maxTokens\n );\n case 'gemini':\n return callGeminiCompletion(\n config,\n prompt,\n head,\n temperature,\n logger,\n tools,\n signal,\n resolvedThinking,\n maxTokens\n );\n /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */\n default: {\n const _exhaustive: never = descriptor.apiFormat;\n return fail(`unsupported API format: ${String(_exhaustive)}`);\n }\n }\n}\n\n// ============================================================================\n// Proxied completion (routes through a backend server)\n// ============================================================================\n\n/**\n * Calls the AI completion endpoint on a proxy server instead of calling the\n * provider API directly from the browser. The proxy handles provider dispatch,\n * CORS, and API key forwarding. The request body serializes the unified\n * {@link AiAssist.IChatRequest} shape (`system?` + `messages`). Enforces the same\n * non-empty / trailing-user-turn and image-input invariants as the direct path.\n * @param proxyUrl - Base URL of the proxy server\n * @param params - Same parameters as {@link callProviderCompletion}\n * @public\n */\nexport async function callProxiedCompletion(\n proxyUrl: string,\n params: IProviderCompletionParams\n): Promise<Result<IAiCompletionResponse>> {\n const {\n descriptor,\n apiKey,\n system,\n messages,\n temperature,\n modelOverride,\n logger,\n tools,\n signal,\n thinking,\n maxTokens\n } = params;\n\n const splitResult = splitChatRequest(system, messages);\n if (splitResult.isFailure()) {\n return fail(splitResult.message);\n }\n if (splitResult.value.prompt.attachments.length > 0 && !descriptor.acceptsImageInput) {\n return fail(`provider \"${descriptor.id}\" does not accept image input`);\n }\n\n const body: Record<string, unknown> = {\n providerId: descriptor.id,\n apiKey,\n messages: normalizeOutboundMessages(splitResult.value)\n };\n // Temperature is forwarded only when explicitly provided, matching the direct path — the proxy\n // omits it from the upstream request so the provider default applies.\n if (temperature !== undefined) {\n body.temperature = temperature;\n }\n if (system !== undefined) {\n body.system = system;\n }\n if (modelOverride !== undefined) {\n body.modelOverride = modelOverride;\n }\n if (tools && tools.length > 0) {\n body.tools = tools;\n }\n if (thinking !== undefined) {\n body.thinking = thinking;\n }\n // Forwarded only when explicitly provided; the proxy is responsible for mapping it to the\n // correct upstream provider field (see AiAssist.usesMaxCompletionTokensField).\n if (maxTokens !== undefined) {\n body.maxTokens = maxTokens;\n }\n\n /* c8 ignore next 1 - optional logger */\n logger?.info(`AI proxy request: provider=${descriptor.id}, proxy=${proxyUrl}`);\n const url = `${proxyUrl}/api/ai/completion`;\n const jsonResult = await fetchJson(url, {}, body, logger, signal);\n if (jsonResult.isFailure()) {\n return fail(jsonResult.message);\n }\n\n const response = jsonResult.value as Record<string, unknown>;\n if (typeof response.error === 'string') {\n return fail(`proxy: ${response.error}`);\n }\n\n if (typeof response.content !== 'string') {\n return fail('proxy returned invalid response: missing content');\n }\n\n return succeed({\n content: response.content,\n truncated: response.truncated === true\n });\n}\n"]}
@@ -28,7 +28,7 @@
28
28
  */
29
29
  import { fail, succeed, Validators } from '@fgv/ts-utils';
30
30
  import { resolveProviderModel } from './model';
31
- import { bearerAuthHeader, resolveEffectiveBaseUrl } from './endpoint';
31
+ import { bearerAuthHeader, geminiAuthHeader, resolveEffectiveBaseUrl } from './endpoint';
32
32
  import { resolveEmbeddingCapability, supportsEmbedding } from './registry';
33
33
  import { fetchJson } from './http';
34
34
  const openAiEmbeddingItem = Validators.object({
@@ -173,7 +173,7 @@ async function callGeminiEmbeddings(config, inputs, request, capability, logger,
173
173
  const sendDimensions = capability.supportsDimensions && request.dimensions !== undefined;
174
174
  const requests = inputs.map((text) => (Object.assign(Object.assign({ model: qualified, content: { parts: [{ text }] } }, (taskType !== undefined ? { taskType } : {})), (sendDimensions ? { outputDimensionality: request.dimensions } : {}))));
175
175
  const body = { requests };
176
- const headers = { 'x-goog-api-key': config.apiKey };
176
+ const headers = geminiAuthHeader(config.apiKey);
177
177
  const url = `${config.baseUrl}/models/${bare}:batchEmbedContents`;
178
178
  /* c8 ignore next 1 - optional logger */
179
179
  logger === null || logger === void 0 ? void 0 : logger.info(`AI embedding: format=gemini-embeddings, model=${bare}, inputs=${inputs.length}`);