@fgv/ts-extras 5.1.0-48 → 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 (82) hide show
  1. package/dist/packlets/ai-assist/completionClient.js +435 -0
  2. package/dist/packlets/ai-assist/completionClient.js.map +1 -0
  3. package/dist/packlets/ai-assist/embeddingClient.js +2 -2
  4. package/dist/packlets/ai-assist/embeddingClient.js.map +1 -1
  5. package/dist/packlets/ai-assist/endpoint.js +29 -0
  6. package/dist/packlets/ai-assist/endpoint.js.map +1 -1
  7. package/dist/packlets/ai-assist/http.js +48 -19
  8. package/dist/packlets/ai-assist/http.js.map +1 -1
  9. package/dist/packlets/ai-assist/imageGenerationClient.js +454 -0
  10. package/dist/packlets/ai-assist/imageGenerationClient.js.map +1 -0
  11. package/dist/packlets/ai-assist/index.js +3 -1
  12. package/dist/packlets/ai-assist/index.js.map +1 -1
  13. package/dist/packlets/ai-assist/jsonCompletion.js +1 -1
  14. package/dist/packlets/ai-assist/jsonCompletion.js.map +1 -1
  15. package/dist/packlets/ai-assist/listModelsClient.js +294 -0
  16. package/dist/packlets/ai-assist/listModelsClient.js.map +1 -0
  17. package/dist/packlets/ai-assist/streamingAdapters/anthropic.js +2 -5
  18. package/dist/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
  19. package/dist/packlets/ai-assist/streamingAdapters/gemini.js +2 -1
  20. package/dist/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
  21. package/dist/packlets/crypto-utils/encryptedFile.js +37 -0
  22. package/dist/packlets/crypto-utils/encryptedFile.js.map +1 -1
  23. package/dist/packlets/crypto-utils/index.js +1 -1
  24. package/dist/packlets/crypto-utils/index.js.map +1 -1
  25. package/dist/packlets/zip-file-tree/zipFileTreeAccessors.js +32 -0
  26. package/dist/packlets/zip-file-tree/zipFileTreeAccessors.js.map +1 -1
  27. package/dist/ts-extras.d.ts +59 -4
  28. package/lib/packlets/ai-assist/completionClient.d.ts +74 -0
  29. package/lib/packlets/ai-assist/completionClient.d.ts.map +1 -0
  30. package/lib/packlets/ai-assist/completionClient.js +439 -0
  31. package/lib/packlets/ai-assist/completionClient.js.map +1 -0
  32. package/lib/packlets/ai-assist/embeddingClient.js +1 -1
  33. package/lib/packlets/ai-assist/embeddingClient.js.map +1 -1
  34. package/lib/packlets/ai-assist/endpoint.d.ts +21 -0
  35. package/lib/packlets/ai-assist/endpoint.d.ts.map +1 -1
  36. package/lib/packlets/ai-assist/endpoint.js +31 -0
  37. package/lib/packlets/ai-assist/endpoint.js.map +1 -1
  38. package/lib/packlets/ai-assist/http.d.ts +12 -2
  39. package/lib/packlets/ai-assist/http.d.ts.map +1 -1
  40. package/lib/packlets/ai-assist/http.js +49 -18
  41. package/lib/packlets/ai-assist/http.js.map +1 -1
  42. package/lib/packlets/ai-assist/imageGenerationClient.d.ts +46 -0
  43. package/lib/packlets/ai-assist/imageGenerationClient.d.ts.map +1 -0
  44. package/lib/packlets/ai-assist/imageGenerationClient.js +458 -0
  45. package/lib/packlets/ai-assist/imageGenerationClient.js.map +1 -0
  46. package/lib/packlets/ai-assist/index.d.ts +3 -1
  47. package/lib/packlets/ai-assist/index.d.ts.map +1 -1
  48. package/lib/packlets/ai-assist/index.js +9 -7
  49. package/lib/packlets/ai-assist/index.js.map +1 -1
  50. package/lib/packlets/ai-assist/jsonCompletion.d.ts +1 -1
  51. package/lib/packlets/ai-assist/jsonCompletion.d.ts.map +1 -1
  52. package/lib/packlets/ai-assist/jsonCompletion.js +2 -2
  53. package/lib/packlets/ai-assist/jsonCompletion.js.map +1 -1
  54. package/lib/packlets/ai-assist/listModelsClient.d.ts +46 -0
  55. package/lib/packlets/ai-assist/listModelsClient.d.ts.map +1 -0
  56. package/lib/packlets/ai-assist/listModelsClient.js +298 -0
  57. package/lib/packlets/ai-assist/listModelsClient.js.map +1 -0
  58. package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts.map +1 -1
  59. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js +2 -5
  60. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
  61. package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts.map +1 -1
  62. package/lib/packlets/ai-assist/streamingAdapters/gemini.js +2 -1
  63. package/lib/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
  64. package/lib/packlets/crypto-utils/encryptedFile.d.ts +25 -0
  65. package/lib/packlets/crypto-utils/encryptedFile.d.ts.map +1 -1
  66. package/lib/packlets/crypto-utils/encryptedFile.js +38 -0
  67. package/lib/packlets/crypto-utils/encryptedFile.js.map +1 -1
  68. package/lib/packlets/crypto-utils/index.d.ts +1 -1
  69. package/lib/packlets/crypto-utils/index.d.ts.map +1 -1
  70. package/lib/packlets/crypto-utils/index.js +2 -1
  71. package/lib/packlets/crypto-utils/index.js.map +1 -1
  72. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.d.ts +30 -2
  73. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.d.ts.map +1 -1
  74. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.js +32 -0
  75. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.js.map +1 -1
  76. package/package.json +7 -7
  77. package/dist/packlets/ai-assist/apiClient.js +0 -1236
  78. package/dist/packlets/ai-assist/apiClient.js.map +0 -1
  79. package/lib/packlets/ai-assist/apiClient.d.ts +0 -152
  80. package/lib/packlets/ai-assist/apiClient.d.ts.map +0 -1
  81. package/lib/packlets/ai-assist/apiClient.js +0 -1244
  82. package/lib/packlets/ai-assist/apiClient.js.map +0 -1
@@ -1,1236 +0,0 @@
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
- /**
21
- * Chat completion client for AI assist with support for multiple provider APIs.
22
- * Supports OpenAI-compatible providers (xAI, OpenAI, Groq, Mistral) directly,
23
- * plus adapters for Anthropic and Google Gemini. When server-side tools (e.g.
24
- * web_search) are configured, providers that support them include tool
25
- * configuration in the request and handle tool-augmented responses.
26
- *
27
- * @packageDocumentation
28
- */
29
- import { isJsonObject } from '@fgv/ts-json-base';
30
- import { fail, mapResults, succeed, Validators } from '@fgv/ts-utils';
31
- import { allModelCapabilities, DEFAULT_ANTHROPIC_MAX_TOKENS, isAdaptiveThinkingModel, isResponsesOnlyModel, resolveProviderModel, usesMaxCompletionTokensField } from './model';
32
- import { anthropicEffortToBudgetTokens, checkTemperatureConflict, mergeThinkingConfig, providerDiscriminatorForId } from './thinkingOptionsResolver';
33
- import { buildAnthropicMessages, buildGeminiContents, buildMessages, buildOpenAiChatUserContent, buildOpenAiResponsesUserContent, normalizeOutboundMessages, splitChatRequest } from './chatRequestBuilders';
34
- import { bearerAuthHeader, resolveEffectiveBaseUrl } from './endpoint';
35
- import { fetchJson } from './http';
36
- import { DEFAULT_MODEL_CAPABILITY_CONFIG, resolveImageCapability, supportsImageGeneration } from './registry';
37
- import { resolveImageOptions, validateResolvedOptions } from './imageOptionsResolver';
38
- import { toAnthropicTools, toGeminiTools, toResponsesApiTools } from './toolFormats';
39
- // ============================================================================
40
- // Shared helpers
41
- // ============================================================================
42
- /**
43
- * Makes a multipart/form-data POST request and returns the parsed JSON, or a
44
- * failure. The Content-Type header (with boundary) is set automatically by
45
- * `fetch` from the `FormData` body — callers must NOT pass it explicitly.
46
- * @internal
47
- */
48
- async function fetchMultipart(url, headers, body, logger, signal) {
49
- /* c8 ignore next 1 - optional logger */
50
- logger === null || logger === void 0 ? void 0 : logger.detail(`AI API request: POST ${url} (multipart)`);
51
- let response;
52
- try {
53
- response = await fetch(url, {
54
- method: 'POST',
55
- headers,
56
- body,
57
- signal
58
- });
59
- }
60
- catch (err) {
61
- /* c8 ignore next 1 - defensive: fetch errors are always Error instances in practice */
62
- const detail = err instanceof Error ? err.message : String(err);
63
- /* c8 ignore next 1 - optional logger */
64
- logger === null || logger === void 0 ? void 0 : logger.error(`AI API request failed: ${detail}`);
65
- return fail(`AI API request failed: ${detail}`);
66
- }
67
- if (!response.ok) {
68
- const errorText = await response.text().catch(() => 'unknown error');
69
- /* c8 ignore next 1 - optional logger */
70
- logger === null || logger === void 0 ? void 0 : logger.error(`AI API returned ${response.status}: ${errorText}`);
71
- return fail(`AI API returned ${response.status}: ${errorText}`);
72
- }
73
- /* c8 ignore next 1 - optional logger */
74
- logger === null || logger === void 0 ? void 0 : logger.detail(`AI API response: ${response.status}`);
75
- let json;
76
- try {
77
- json = await response.json();
78
- }
79
- catch /* c8 ignore start - defensive: response.json() failure on a 2xx */ (_a) {
80
- logger === null || logger === void 0 ? void 0 : logger.error('AI API returned invalid JSON response');
81
- return fail('AI API returned invalid JSON response');
82
- } /* c8 ignore stop */
83
- /* c8 ignore next 5 - defensive: provider returning non-object JSON on a 2xx */
84
- if (!isJsonObject(json)) {
85
- logger === null || logger === void 0 ? void 0 : logger.error('AI API returned non-object JSON response');
86
- return fail('AI API returned non-object JSON response');
87
- }
88
- return succeed(json);
89
- }
90
- /**
91
- * Decodes a base64-encoded image attachment into a `Blob` suitable for use as
92
- * a multipart file field. On Node hands the `Buffer` straight to `Blob`
93
- * (Buffer extends Uint8Array) to skip an intermediate copy; falls back to
94
- * `atob` in browsers. Inputs come from `FileReader` or prior provider
95
- * responses, which are trusted to be valid. Note that Node's
96
- * `Buffer.from(..., 'base64')` silently strips invalid characters rather
97
- * than throwing, so failures are only observable in the browser path.
98
- * @internal
99
- */
100
- function attachmentToBlob(attachment) {
101
- if (typeof Buffer !== 'undefined') {
102
- return succeed(new Blob([Buffer.from(attachment.base64, 'base64')], { type: attachment.mimeType }));
103
- }
104
- /* c8 ignore start - Browser-only fallback cannot be tested in Node.js environment */
105
- try {
106
- const binary = atob(attachment.base64);
107
- const bytes = new Uint8Array(binary.length);
108
- for (let i = 0; i < binary.length; i++) {
109
- bytes[i] = binary.charCodeAt(i);
110
- }
111
- return succeed(new Blob([bytes], { type: attachment.mimeType }));
112
- }
113
- catch (e) {
114
- const message = e instanceof Error ? e.message : String(e);
115
- return fail(`Invalid base64: ${message}`);
116
- }
117
- /* c8 ignore stop */
118
- }
119
- /**
120
- * Maps a MIME type to a sensible file extension for multipart filenames.
121
- * @internal
122
- */
123
- function extensionForMimeType(mimeType) {
124
- switch (mimeType) {
125
- case 'image/png':
126
- return 'png';
127
- case 'image/jpeg':
128
- case 'image/jpg':
129
- return 'jpg';
130
- case 'image/webp':
131
- return 'webp';
132
- case 'image/gif':
133
- return 'gif';
134
- default:
135
- return 'bin';
136
- }
137
- }
138
- /**
139
- * Makes an HTTP GET request and returns the parsed JSON, or a failure.
140
- * @internal
141
- */
142
- async function fetchGetJson(url, headers, logger, signal) {
143
- /* c8 ignore next 1 - optional logger */
144
- logger === null || logger === void 0 ? void 0 : logger.detail(`AI API request: GET ${url}`);
145
- let response;
146
- try {
147
- response = await fetch(url, { method: 'GET', headers, signal });
148
- }
149
- catch (err) {
150
- /* c8 ignore next 1 - defensive: fetch errors are always Error instances in practice */
151
- const detail = err instanceof Error ? err.message : String(err);
152
- /* c8 ignore next 1 - optional logger */
153
- logger === null || logger === void 0 ? void 0 : logger.error(`AI API request failed: ${detail}`);
154
- return fail(`AI API request failed: ${detail}`);
155
- }
156
- if (!response.ok) {
157
- const errorText = await response.text().catch(() => 'unknown error');
158
- /* c8 ignore next 1 - optional logger */
159
- logger === null || logger === void 0 ? void 0 : logger.error(`AI API returned ${response.status}: ${errorText}`);
160
- return fail(`AI API returned ${response.status}: ${errorText}`);
161
- }
162
- /* c8 ignore next 1 - optional logger */
163
- logger === null || logger === void 0 ? void 0 : logger.detail(`AI API response: ${response.status}`);
164
- let json;
165
- try {
166
- json = await response.json();
167
- }
168
- catch /* c8 ignore start - defensive: response.json() failure on a 2xx */ (_a) {
169
- logger === null || logger === void 0 ? void 0 : logger.error('AI API returned invalid JSON response');
170
- return fail('AI API returned invalid JSON response');
171
- } /* c8 ignore stop */
172
- /* c8 ignore next 5 - defensive: provider returning non-object JSON on a 2xx */
173
- if (!isJsonObject(json)) {
174
- logger === null || logger === void 0 ? void 0 : logger.error('AI API returned non-object JSON response');
175
- return fail('AI API returned non-object JSON response');
176
- }
177
- return succeed(json);
178
- }
179
- const openAiMessage = Validators.object({
180
- content: Validators.string
181
- });
182
- const openAiChoice = Validators.object({
183
- message: openAiMessage,
184
- finish_reason: Validators.string
185
- });
186
- const openAiResponse = Validators.object({
187
- choices: Validators.arrayOf(openAiChoice).withConstraint((arr) => arr.length > 0)
188
- });
189
- const responsesApiOutputText = Validators.object({
190
- type: Validators.literal('output_text'),
191
- text: Validators.string
192
- });
193
- const responsesApiMessage = Validators.object({
194
- type: Validators.literal('message'),
195
- role: Validators.string,
196
- content: Validators.arrayOf(responsesApiOutputText).withConstraint((arr) => arr.length > 0)
197
- });
198
- const responsesApiOutputItem = Validators.isA('object', (v) => typeof v === 'object' && v !== null);
199
- const responsesApiResponse = Validators.object({
200
- output: Validators.arrayOf(responsesApiOutputItem).withConstraint((arr) => arr.length > 0),
201
- status: Validators.string
202
- });
203
- const geminiPart = Validators.object({
204
- text: Validators.string
205
- });
206
- const geminiContent = Validators.object({
207
- parts: Validators.arrayOf(geminiPart).withConstraint((arr) => arr.length > 0)
208
- });
209
- const geminiCandidate = Validators.object({
210
- content: geminiContent,
211
- finishReason: Validators.string
212
- });
213
- const geminiResponse = Validators.object({
214
- candidates: Validators.arrayOf(geminiCandidate).withConstraint((arr) => arr.length > 0)
215
- });
216
- // ============================================================================
217
- // OpenAI-compatible client (Chat Completions — no tools)
218
- // ============================================================================
219
- /**
220
- * Calls an OpenAI-compatible chat completion endpoint.
221
- * Works for xAI Grok, OpenAI, Groq, and Mistral.
222
- * @internal
223
- */
224
- async function callOpenAiCompletion(config, prompt, head, temperature, logger, signal, resolvedThinking, maxTokens, useMaxCompletionTokensField = false) {
225
- var _a;
226
- const url = `${config.baseUrl}/chat/completions`;
227
- const messages = buildMessages(prompt.system, buildOpenAiChatUserContent(prompt), {
228
- head
229
- });
230
- 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;
231
- const maxTokensField = useMaxCompletionTokensField ? 'max_completion_tokens' : 'max_tokens';
232
- 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 } : {}));
233
- if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.otherParams) !== undefined) {
234
- Object.assign(body, resolvedThinking.otherParams);
235
- }
236
- const headers = bearerAuthHeader(config.apiKey);
237
- /* c8 ignore next 1 - optional logger */
238
- logger === null || logger === void 0 ? void 0 : logger.info(`OpenAI completion: model=${config.model}`);
239
- const jsonResult = await fetchJson(url, headers, body, logger, signal);
240
- if (jsonResult.isFailure()) {
241
- return fail(jsonResult.message);
242
- }
243
- return openAiResponse
244
- .validate(jsonResult.value)
245
- .withErrorFormat((msg) => `OpenAI API response: ${msg}`)
246
- .onSuccess((response) => {
247
- const choice = response.choices[0];
248
- return succeed({
249
- content: choice.message.content,
250
- truncated: choice.finish_reason === 'length'
251
- });
252
- });
253
- }
254
- // ============================================================================
255
- // OpenAI/xAI Responses API (with tools)
256
- // ============================================================================
257
- /**
258
- * Extracts text content from a Responses API output array.
259
- * Finds the first message-type output item and concatenates its text content blocks.
260
- * @internal
261
- */
262
- function extractResponsesApiText(output) {
263
- for (const item of output) {
264
- if (item.type === 'message') {
265
- const messageResult = responsesApiMessage.validate(item);
266
- if (messageResult.isSuccess()) {
267
- return succeed(messageResult.value.content.map((c) => c.text).join(''));
268
- }
269
- }
270
- }
271
- return fail('Responses API output contained no message with text content');
272
- }
273
- /**
274
- * Calls the xAI/OpenAI Responses API with server-side tools.
275
- * Used when tools are configured for an openai-format provider.
276
- * @internal
277
- */
278
- async function callOpenAiResponsesCompletion(config, prompt, tools = [], head, temperature, logger, signal, resolvedThinking, maxTokens) {
279
- var _a;
280
- const url = `${config.baseUrl}/responses`;
281
- const input = buildMessages(prompt.system, buildOpenAiResponsesUserContent(prompt), {
282
- head
283
- });
284
- 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;
285
- 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 } } : {}));
286
- // Shared by OpenAI and xAI — both route through the Responses API with the same field name.
287
- if (maxTokens !== undefined) {
288
- body.max_output_tokens = maxTokens;
289
- }
290
- if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.otherParams) !== undefined) {
291
- Object.assign(body, resolvedThinking.otherParams);
292
- }
293
- const headers = bearerAuthHeader(config.apiKey);
294
- /* c8 ignore next 1 - optional logger */
295
- logger === null || logger === void 0 ? void 0 : logger.info(`OpenAI Responses API: model=${config.model}, tools=${tools.map((t) => t.type).join(',')}`);
296
- const jsonResult = await fetchJson(url, headers, body, logger, signal);
297
- if (jsonResult.isFailure()) {
298
- return fail(jsonResult.message);
299
- }
300
- return responsesApiResponse
301
- .validate(jsonResult.value)
302
- .withErrorFormat((msg) => `Responses API response: ${msg}`)
303
- .onSuccess((response) => {
304
- return extractResponsesApiText(response.output).onSuccess((text) => succeed({
305
- content: text,
306
- truncated: response.status === 'incomplete'
307
- }));
308
- });
309
- }
310
- // ============================================================================
311
- // Anthropic adapter
312
- // ============================================================================
313
- /**
314
- * Extracts text content from Anthropic response content blocks.
315
- * When tools are used, the content array contains mixed block types
316
- * (text, server_tool_use, web_search_tool_result). We extract and
317
- * concatenate only the text blocks.
318
- * @internal
319
- */
320
- function extractAnthropicText(content) {
321
- const textParts = [];
322
- for (const block of content) {
323
- if (typeof block === 'object' && block !== null && 'type' in block) {
324
- const typed = block;
325
- if (typed.type === 'text' && typeof typed.text === 'string') {
326
- textParts.push(typed.text);
327
- }
328
- }
329
- }
330
- if (textParts.length === 0) {
331
- return fail('Anthropic response contained no text content blocks');
332
- }
333
- return succeed(textParts.join(''));
334
- }
335
- /** Calls the Anthropic Messages API with optional tool support. @internal */
336
- async function callAnthropicCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking, useAdaptiveThinking = false, maxTokens) {
337
- const url = `${config.baseUrl}/messages`;
338
- const messages = buildAnthropicMessages(prompt, { head });
339
- const body = Object.assign({ model: config.model, system: prompt.system, messages,
340
- // Anthropic's Messages API requires max_tokens on every request — see
341
- // AiAssist.DEFAULT_ANTHROPIC_MAX_TOKENS for why only this provider defaults it.
342
- max_tokens: maxTokens !== null && maxTokens !== void 0 ? maxTokens : DEFAULT_ANTHROPIC_MAX_TOKENS }, (temperature !== undefined ? { temperature } : {}));
343
- const effort = resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.anthropicEffort;
344
- if (effort !== undefined) {
345
- if (useAdaptiveThinking) {
346
- // Claude 5 family: adaptive thinking — no budget_tokens; effort moves to the
347
- // top-level output_config block. See AiAssist.isAdaptiveThinkingModel.
348
- body.thinking = { type: 'adaptive' };
349
- body.output_config = { effort };
350
- }
351
- else {
352
- body.thinking = { type: 'enabled', budget_tokens: anthropicEffortToBudgetTokens(effort) };
353
- }
354
- }
355
- if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.otherParams) !== undefined) {
356
- Object.assign(body, resolvedThinking.otherParams);
357
- }
358
- if (tools && tools.length > 0) {
359
- body.tools = toAnthropicTools(tools);
360
- /* c8 ignore next 3 - optional logger diagnostic output */
361
- logger === null || logger === void 0 ? void 0 : logger.info(`Anthropic completion: model=${config.model}, tools=${tools.map((t) => t.type).join(',')}`);
362
- }
363
- else {
364
- /* c8 ignore next 1 - optional logger */
365
- logger === null || logger === void 0 ? void 0 : logger.info(`Anthropic completion: model=${config.model}`);
366
- }
367
- const headers = {
368
- 'x-api-key': config.apiKey,
369
- 'anthropic-version': '2023-06-01',
370
- 'anthropic-dangerous-direct-browser-access': 'true'
371
- };
372
- const jsonResult = await fetchJson(url, headers, body, logger, signal);
373
- if (jsonResult.isFailure()) {
374
- return fail(jsonResult.message);
375
- }
376
- const rawContent = jsonResult.value.content;
377
- const stopReason = jsonResult.value.stop_reason;
378
- if (!Array.isArray(rawContent)) {
379
- return fail('Anthropic API response: content is not an array');
380
- }
381
- if (typeof stopReason !== 'string') {
382
- return fail('Anthropic API response: stop_reason is missing or not a string');
383
- }
384
- return extractAnthropicText(rawContent).onSuccess((text) => succeed({
385
- content: text,
386
- truncated: stopReason === 'max_tokens'
387
- }));
388
- }
389
- // ============================================================================
390
- // Google Gemini adapter
391
- // ============================================================================
392
- /**
393
- * Calls the Google Gemini generateContent API.
394
- * When tools are configured, includes Google Search grounding.
395
- * @internal
396
- */
397
- async function callGeminiCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking, maxTokens) {
398
- const url = `${config.baseUrl}/models/${config.model}:generateContent`;
399
- const contents = buildGeminiContents(prompt, { head });
400
- // Temperature is sent only when explicitly provided; otherwise Gemini's default applies.
401
- const generationConfig = {};
402
- if (temperature !== undefined) {
403
- generationConfig.temperature = temperature;
404
- }
405
- if (maxTokens !== undefined) {
406
- generationConfig.maxOutputTokens = maxTokens;
407
- }
408
- if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.geminiThinkingBudget) !== undefined) {
409
- generationConfig.thinkingConfig = { thinkingBudget: resolvedThinking.geminiThinkingBudget };
410
- }
411
- if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.otherParams) !== undefined) {
412
- Object.assign(generationConfig, resolvedThinking.otherParams);
413
- }
414
- const body = {
415
- systemInstruction: { parts: [{ text: prompt.system }] },
416
- contents,
417
- generationConfig
418
- };
419
- if (tools && tools.length > 0) {
420
- body.tools = toGeminiTools(tools);
421
- /* c8 ignore next 1 - optional logger */
422
- logger === null || logger === void 0 ? void 0 : logger.info(`Gemini completion: model=${config.model}, tools=${tools.map((t) => t.type).join(',')}`);
423
- }
424
- else {
425
- /* c8 ignore next 1 - optional logger */
426
- logger === null || logger === void 0 ? void 0 : logger.info(`Gemini completion: model=${config.model}`);
427
- }
428
- const headers = {
429
- 'x-goog-api-key': config.apiKey
430
- };
431
- const jsonResult = await fetchJson(url, headers, body, logger, signal);
432
- if (jsonResult.isFailure()) {
433
- return fail(jsonResult.message);
434
- }
435
- return geminiResponse
436
- .validate(jsonResult.value)
437
- .withErrorFormat((msg) => `Gemini API response: ${msg}`)
438
- .onSuccess((response) => {
439
- const candidate = response.candidates[0];
440
- return succeed({
441
- content: candidate.content.parts[0].text,
442
- truncated: candidate.finishReason === 'MAX_TOKENS'
443
- });
444
- });
445
- }
446
- // ============================================================================
447
- // Provider dispatcher
448
- // ============================================================================
449
- /**
450
- * Calls the appropriate chat completion API for a given provider. Routes by
451
- * `apiFormat`: `'openai'` (xAI/OpenAI/Groq/Mistral — switches to Responses API
452
- * when tools are set), `'anthropic'`, or `'gemini'`.
453
- * @public
454
- */
455
- export async function callProviderCompletion(params) {
456
- const { descriptor, apiKey, system, messages, temperature, modelOverride, tier, logger, tools, signal, endpoint, thinking, maxTokens } = params;
457
- const splitResult = splitChatRequest(system, messages);
458
- if (splitResult.isFailure()) {
459
- return fail(splitResult.message);
460
- }
461
- const { prompt, head } = splitResult.value;
462
- const baseUrlResult = resolveEffectiveBaseUrl(descriptor, endpoint);
463
- if (baseUrlResult.isFailure()) {
464
- return fail(baseUrlResult.message);
465
- }
466
- if (prompt.attachments.length > 0 && !descriptor.acceptsImageInput) {
467
- return fail(`provider "${descriptor.id}" does not accept image input`);
468
- }
469
- const hasTools = tools !== undefined && tools.length > 0;
470
- const discriminator = providerDiscriminatorForId(descriptor.id);
471
- // The quality tier is the only completion-model selector; thinking and tools
472
- // are orthogonal request params/capabilities and never pick a model.
473
- const modelContext = tier;
474
- const modelResult = resolveProviderModel(descriptor, modelOverride, modelContext);
475
- if (modelResult.isFailure()) {
476
- return fail(modelResult.message);
477
- }
478
- const model = modelResult.value;
479
- let resolvedThinking;
480
- if (thinking !== undefined) {
481
- if (discriminator !== undefined) {
482
- const mergeResult = mergeThinkingConfig(thinking, model, discriminator);
483
- /* c8 ignore next 3 - mergeThinkingConfig always succeeds; defensive guard */
484
- if (mergeResult.isFailure()) {
485
- return fail(mergeResult.message);
486
- }
487
- resolvedThinking = mergeResult.value;
488
- const conflictResult = checkTemperatureConflict(resolvedThinking, discriminator, temperature);
489
- if (conflictResult.isFailure()) {
490
- return fail(conflictResult.message);
491
- }
492
- }
493
- }
494
- const config = {
495
- baseUrl: baseUrlResult.value,
496
- apiKey,
497
- model
498
- };
499
- /* c8 ignore next 8 - optional logger diagnostic output */
500
- if (logger) {
501
- const toolTypes = hasTools ? tools.map((t) => t.type).join(',') : 'none';
502
- const supported = descriptor.supportedTools.length > 0 ? descriptor.supportedTools.join(',') : 'none';
503
- logger.info(`AI completion: provider=${descriptor.id}, format=${descriptor.apiFormat}, model=${config.model}, ` +
504
- `tools=${toolTypes}, supported=${supported}`);
505
- }
506
- switch (descriptor.apiFormat) {
507
- case 'openai':
508
- // Responses-API-only models (e.g. gpt-5.5-pro) 400 on /chat/completions, so they route
509
- // to the Responses path even with no tools requested — same path the tools case uses.
510
- if (hasTools || isResponsesOnlyModel(descriptor, config.model)) {
511
- return callOpenAiResponsesCompletion(config, prompt, tools, head, temperature, logger, signal, resolvedThinking, maxTokens);
512
- }
513
- return callOpenAiCompletion(config, prompt, head, temperature, logger, signal, resolvedThinking, maxTokens, usesMaxCompletionTokensField(descriptor));
514
- case 'anthropic':
515
- return callAnthropicCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking, isAdaptiveThinkingModel(descriptor, config.model), maxTokens);
516
- case 'gemini':
517
- return callGeminiCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking, maxTokens);
518
- /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */
519
- default: {
520
- const _exhaustive = descriptor.apiFormat;
521
- return fail(`unsupported API format: ${String(_exhaustive)}`);
522
- }
523
- }
524
- }
525
- const openAiImageItem = Validators.object({
526
- b64_json: Validators.string,
527
- revised_prompt: Validators.string.optional()
528
- });
529
- const openAiImageResponse = Validators.object({
530
- data: Validators.arrayOf(openAiImageItem).withConstraint((arr) => arr.length > 0)
531
- });
532
- const geminiImageInlineData = Validators.object({
533
- mimeType: Validators.string,
534
- data: Validators.string
535
- });
536
- const geminiImageOutPart = Validators.object({
537
- text: Validators.string.optional(),
538
- inlineData: geminiImageInlineData.optional()
539
- });
540
- const geminiImageOutContent = Validators.object({
541
- parts: Validators.arrayOf(geminiImageOutPart)
542
- });
543
- const geminiImageOutCandidate = Validators.object({
544
- content: geminiImageOutContent.optional(),
545
- finishReason: Validators.string.optional(),
546
- finishMessage: Validators.string.optional()
547
- });
548
- const geminiImageOutResponse = Validators.object({
549
- candidates: Validators.arrayOf(geminiImageOutCandidate).withConstraint((arr) => arr.length > 0)
550
- });
551
- // ---- Proxied image generation response ----
552
- const proxiedGeneratedImage = Validators.object({
553
- mimeType: Validators.string,
554
- base64: Validators.string,
555
- revisedPrompt: Validators.string.optional()
556
- });
557
- const proxiedImageGenerationResponse = Validators.object({
558
- images: Validators.arrayOf(proxiedGeneratedImage).withConstraint((arr) => arr.length > 0)
559
- });
560
- const proxiedListModelsEntry = Validators.object({
561
- id: Validators.string,
562
- capabilities: Validators.arrayOf(Validators.enumeratedValue(allModelCapabilities)),
563
- displayName: Validators.string.optional()
564
- });
565
- const proxiedListModelsResponse = Validators.object({
566
- models: Validators.arrayOf(proxiedListModelsEntry)
567
- });
568
- // ============================================================================
569
- // Image generation — adapters
570
- // ============================================================================
571
- /** Routes to /images/generations or /images/edits; handles outputParamStyle. @internal */
572
- async function callOpenAiImageGeneration(config, request, capability, resolved, logger, signal) {
573
- var _a, _b;
574
- const refs = (_a = request.referenceImages) !== null && _a !== void 0 ? _a : [];
575
- const headers = bearerAuthHeader(config.apiKey);
576
- const effectiveMimeType = resolved.outputFormat !== undefined
577
- ? `image/${resolved.outputFormat}`
578
- : (_b = capability.defaultOutputMimeType) !== null && _b !== void 0 ? _b : 'image/png';
579
- const fetched = refs.length > 0
580
- ? await callOpenAiImagesEdits(config, capability, request, headers, resolved, logger, signal)
581
- : await callOpenAiImagesGenerations(config, request, headers, resolved, capability, logger, signal);
582
- return fetched.onSuccess((json) => openAiImageResponse
583
- .validate(json)
584
- .withErrorFormat((msg) => `OpenAI images API response: ${msg}`)
585
- .onSuccess((response) => succeed({
586
- images: response.data.map((item) => (Object.assign({ mimeType: effectiveMimeType, base64: item.b64_json }, (item.revised_prompt !== undefined ? { revisedPrompt: item.revised_prompt } : {}))))
587
- })));
588
- }
589
- /** Builds the JSON /images/generations request; handles outputParamStyle. @internal */
590
- function callOpenAiImagesGenerations(config, request, headers, resolved, capability, logger, signal) {
591
- var _a;
592
- const body = {
593
- model: config.model,
594
- prompt: request.prompt,
595
- n: resolved.n
596
- };
597
- // Output format param — conditional on model capability
598
- if (capability.outputParamStyle === 'response-format') {
599
- body.response_format = 'b64_json';
600
- }
601
- else if (capability.outputParamStyle === 'output-format') {
602
- body.output_format = (_a = resolved.outputFormat) !== null && _a !== void 0 ? _a : 'png';
603
- }
604
- if (resolved.size !== undefined) {
605
- body.size = resolved.size;
606
- }
607
- if (capability.supportsQualityParam && resolved.quality !== undefined) {
608
- body.quality = resolved.quality;
609
- }
610
- if (resolved.seed !== undefined) {
611
- body.seed = resolved.seed;
612
- }
613
- if (resolved.background !== undefined) {
614
- body.background = resolved.background;
615
- }
616
- if (resolved.moderation !== undefined) {
617
- body.moderation = resolved.moderation;
618
- }
619
- if (resolved.outputCompression !== undefined) {
620
- body.output_compression = resolved.outputCompression;
621
- }
622
- if (resolved.otherParams !== undefined) {
623
- Object.assign(body, resolved.otherParams);
624
- }
625
- /* c8 ignore next 1 - optional logger */
626
- logger === null || logger === void 0 ? void 0 : logger.info(`Image generation: model=${config.model}, n=${resolved.n}`);
627
- return fetchJson(`${config.baseUrl}/images/generations`, headers, body, logger, signal);
628
- }
629
- /** Builds the multipart /images/edits request with ref images. @internal */
630
- async function callOpenAiImagesEdits(config, capability, request, headers, resolved, logger, signal) {
631
- const refs = request.referenceImages; // callers verify refs.length > 0 before calling this function
632
- const blobsResult = mapResults(refs.map((ref, i) => attachmentToBlob(ref).withErrorFormat((msg) => `reference image ${i}: ${msg}`)));
633
- /* c8 ignore next 3 - decode failure unreachable via Node's Buffer.from (silently strips invalid input) */
634
- if (blobsResult.isFailure()) {
635
- return fail(blobsResult.message);
636
- }
637
- const form = new FormData();
638
- form.append('model', config.model);
639
- form.append('prompt', request.prompt);
640
- form.append('n', String(resolved.n));
641
- if (capability.outputParamStyle !== 'output-format') {
642
- form.append('response_format', 'b64_json');
643
- }
644
- if (resolved.size !== undefined) {
645
- form.append('size', resolved.size);
646
- }
647
- blobsResult.value.forEach((blob, i) => {
648
- form.append('image[]', blob, `ref-${i}.${extensionForMimeType(refs[i].mimeType)}`);
649
- });
650
- /* c8 ignore next 1 - optional logger */
651
- logger === null || logger === void 0 ? void 0 : logger.info(`Image edit: model=${config.model}, n=${resolved.n}, refs=${refs.length}`);
652
- return fetchMultipart(`${config.baseUrl}/images/edits`, headers, form, logger, signal);
653
- }
654
- /** Calls xAI /images/edits with JSON body (not multipart); up to 3 source images. @internal */
655
- async function callXaiImagesEdits(config, request, resolved, logger, signal) {
656
- var _a;
657
- /* c8 ignore next 1 - defensive: referenceImages always defined when this function is called */
658
- const refs = (_a = request.referenceImages) !== null && _a !== void 0 ? _a : [];
659
- if (refs.length > 3) {
660
- return fail(`xAI image edits supports at most 3 reference images; got ${refs.length}`);
661
- }
662
- const images = refs.map((ref) => ({
663
- type: 'image_url',
664
- url: `data:${ref.mimeType};base64,${ref.base64}`
665
- }));
666
- const body = {
667
- model: config.model,
668
- prompt: request.prompt,
669
- n: resolved.n,
670
- response_format: 'b64_json',
671
- image: images
672
- };
673
- if (resolved.aspectRatio !== undefined) {
674
- body.aspect_ratio = resolved.aspectRatio;
675
- }
676
- if (resolved.resolution !== undefined) {
677
- body.resolution = resolved.resolution;
678
- }
679
- if (resolved.otherParams !== undefined) {
680
- Object.assign(body, resolved.otherParams);
681
- }
682
- /* c8 ignore next 1 - optional logger */
683
- logger === null || logger === void 0 ? void 0 : logger.info(`xAI image edit: model=${config.model}, n=${resolved.n}, refs=${refs.length}`);
684
- return fetchJson(`${config.baseUrl}/images/edits`, bearerAuthHeader(config.apiKey), body, logger, signal);
685
- }
686
- /** Calls xAI /images/generations; uses aspect_ratio instead of size. @internal */
687
- async function callXaiImageGeneration(config, request, capability, resolved, logger, signal) {
688
- const headers = bearerAuthHeader(config.apiKey);
689
- const body = {
690
- model: config.model,
691
- prompt: request.prompt,
692
- n: resolved.n,
693
- response_format: 'b64_json'
694
- };
695
- if (resolved.aspectRatio !== undefined) {
696
- body.aspect_ratio = resolved.aspectRatio;
697
- }
698
- if (resolved.resolution !== undefined) {
699
- body.resolution = resolved.resolution;
700
- }
701
- if (resolved.otherParams !== undefined) {
702
- Object.assign(body, resolved.otherParams);
703
- }
704
- /* c8 ignore next 1 - optional logger */
705
- logger === null || logger === void 0 ? void 0 : logger.info(`xAI image generation: model=${config.model}, n=${resolved.n}`);
706
- const fetched = await fetchJson(`${config.baseUrl}/images/generations`, headers, body, logger, signal);
707
- return fetched.onSuccess((json) => openAiImageResponse
708
- .validate(json)
709
- .withErrorFormat((msg) => `xAI images API response: ${msg}`)
710
- .onSuccess((response) => succeed({
711
- images: response.data.map((item) => {
712
- var _a;
713
- return ({
714
- mimeType: (_a = capability.defaultOutputMimeType) !== null && _a !== void 0 ? _a : 'image/jpeg',
715
- base64: item.b64_json
716
- });
717
- })
718
- })));
719
- }
720
- /**
721
- * Gemini `finishReason` values that indicate a normal terminal completion rather
722
- * than a refusal. `STOP` is set on every successful generation (and on completions
723
- * that return a text part instead of an image); `MAX_TOKENS` is a benign truncation.
724
- * A candidate carrying only one of these is NOT a decline — treating it as one would
725
- * mislabel an ordinary no-image outcome as a policy refusal. @internal
726
- */
727
- const benignGeminiImageFinishReasons = new Set(['STOP', 'MAX_TOKENS']);
728
- /** Calls Gemini :generateContent for image output; accepts ref images as inlineData. @internal */
729
- async function callGeminiImageOutGeneration(config, request, resolved, logger, signal) {
730
- var _a;
731
- const url = `${config.baseUrl}/models/${config.model}:generateContent`;
732
- const refs = (_a = request.referenceImages) !== null && _a !== void 0 ? _a : [];
733
- const parts = [{ text: request.prompt }];
734
- for (const ref of refs) {
735
- parts.push({ inlineData: { mimeType: ref.mimeType, data: ref.base64 } });
736
- }
737
- const generationConfig = {};
738
- if (resolved.geminiAspectRatio !== undefined) {
739
- generationConfig.imageConfig = { aspectRatio: resolved.geminiAspectRatio };
740
- }
741
- if (resolved.otherParams !== undefined) {
742
- Object.assign(generationConfig, resolved.otherParams);
743
- }
744
- const body = { contents: [{ role: 'user', parts }] };
745
- if (Object.keys(generationConfig).length > 0) {
746
- body.generationConfig = generationConfig;
747
- }
748
- const headers = {
749
- 'x-goog-api-key': config.apiKey
750
- };
751
- /* c8 ignore next 1 - optional logger */
752
- logger === null || logger === void 0 ? void 0 : logger.info(`Gemini image-out: model=${config.model}, refs=${refs.length}`);
753
- return (await fetchJson(url, headers, body, logger, signal)).onSuccess((json) => geminiImageOutResponse
754
- .validate(json)
755
- .withErrorFormat((msg) => `Gemini image API response: ${msg}`)
756
- .onSuccess((response) => {
757
- var _a, _b;
758
- const images = [];
759
- for (const candidate of response.candidates) {
760
- for (const part of (_b = (_a = candidate.content) === null || _a === void 0 ? void 0 : _a.parts) !== null && _b !== void 0 ? _b : []) {
761
- if (part.inlineData) {
762
- images.push({
763
- mimeType: part.inlineData.mimeType,
764
- base64: part.inlineData.data
765
- });
766
- }
767
- }
768
- }
769
- if (images.length === 0) {
770
- // A candidate with no image parts is a *decline* only when it carries a
771
- // refusal-shaped finishReason — i.e. present and not a benign terminal reason
772
- // (`STOP`/`MAX_TOKENS`). A normal completion that emitted text-instead-of-image
773
- // carries `finishReason: 'STOP'` and must fall through to the no-image message.
774
- const declined = response.candidates.find((candidate) => candidate.finishReason !== undefined &&
775
- !benignGeminiImageFinishReasons.has(candidate.finishReason));
776
- if ((declined === null || declined === void 0 ? void 0 : declined.finishReason) !== undefined) {
777
- // Truthiness (not `!== undefined`) so an empty-string finishMessage is treated
778
- // as "no message" and produces no dangling ` — ` separator.
779
- const suffix = declined.finishMessage ? ` — ${declined.finishMessage}` : '';
780
- return fail(`Gemini image generation declined: ${declined.finishReason}${suffix}`);
781
- }
782
- return fail('Gemini image API response: no image parts in response');
783
- }
784
- return succeed({ images });
785
- }));
786
- }
787
- // ============================================================================
788
- // Image generation — dispatcher
789
- // ============================================================================
790
- /**
791
- * Calls the appropriate image-generation API for a given provider. Routes by the
792
- * `format` field of the resolved {@link IAiImageModelCapability}:
793
- * `'openai-images'`, `'xai-images'`, `'xai-images-edits'`, or
794
- * `'gemini-image-out'`. Rejects up front if `referenceImages` is set but the
795
- * capability does not declare `acceptsImageReferenceInput`.
796
- * @param params - Request parameters including descriptor, API key, and prompt
797
- * @public
798
- */
799
- export async function callProviderImageGeneration(params) {
800
- var _a, _b, _c;
801
- const { descriptor, apiKey, params: request, modelOverride, logger, signal, endpoint } = params;
802
- if (!supportsImageGeneration(descriptor)) {
803
- return fail(`provider "${descriptor.id}" does not support image generation`);
804
- }
805
- const baseUrlResult = resolveEffectiveBaseUrl(descriptor, endpoint);
806
- if (baseUrlResult.isFailure()) {
807
- return fail(baseUrlResult.message);
808
- }
809
- const modelResult = resolveProviderModel(descriptor, modelOverride, 'image');
810
- if (modelResult.isFailure()) {
811
- return fail(modelResult.message);
812
- }
813
- const model = modelResult.value;
814
- const capability = resolveImageCapability(descriptor, model);
815
- if (capability === undefined) {
816
- return fail(`provider "${descriptor.id}" does not support image generation for model "${model}"`);
817
- }
818
- if (((_b = (_a = request.referenceImages) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 0 && !capability.acceptsImageReferenceInput) {
819
- return fail(`model "${model}" does not support reference images`);
820
- }
821
- const resolved = resolveImageOptions(model, capability, request.options);
822
- const validationResult = validateResolvedOptions(model, capability, resolved);
823
- if (validationResult.isFailure()) {
824
- return fail(validationResult.message);
825
- }
826
- const config = {
827
- baseUrl: baseUrlResult.value,
828
- apiKey,
829
- model
830
- };
831
- /* c8 ignore next 6 - optional logger diagnostic output */
832
- if (logger) {
833
- logger.info(`AI image generation: provider=${descriptor.id}, format=${capability.format}, ` +
834
- `model=${config.model}`);
835
- }
836
- switch (capability.format) {
837
- case 'openai-images':
838
- return callOpenAiImageGeneration(config, request, capability, resolved, logger, signal);
839
- case 'xai-images':
840
- return callXaiImageGeneration(config, request, capability, resolved, logger, signal);
841
- case 'xai-images-edits': {
842
- const refs = (_c = request.referenceImages) !== null && _c !== void 0 ? _c : [];
843
- if (refs.length > 0) {
844
- const editsResult = await callXaiImagesEdits(config, request, resolved, logger, signal);
845
- return editsResult.onSuccess((json) => openAiImageResponse
846
- .validate(json)
847
- .withErrorFormat((msg) => `xAI images API response: ${msg}`)
848
- .onSuccess((response) => succeed({
849
- images: response.data.map((item) => {
850
- var _a;
851
- return ({
852
- mimeType: (_a = capability.defaultOutputMimeType) !== null && _a !== void 0 ? _a : 'image/jpeg',
853
- base64: item.b64_json
854
- });
855
- })
856
- })));
857
- }
858
- return callXaiImageGeneration(config, request, capability, resolved, logger, signal);
859
- }
860
- case 'gemini-image-out':
861
- return callGeminiImageOutGeneration(config, request, resolved, logger, signal);
862
- /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */
863
- default: {
864
- const _exhaustive = capability.format;
865
- return fail(`unsupported image API format: ${String(_exhaustive)}`);
866
- }
867
- }
868
- }
869
- const openAiListEntry = Validators.object({
870
- id: Validators.string
871
- });
872
- const openAiListResponse = Validators.object({
873
- data: Validators.arrayOf(openAiListEntry)
874
- });
875
- const anthropicListEntry = Validators.object({
876
- id: Validators.string,
877
- display_name: Validators.string.optional()
878
- });
879
- const anthropicListResponse = Validators.object({
880
- data: Validators.arrayOf(anthropicListEntry)
881
- });
882
- const geminiListEntry = Validators.object({
883
- name: Validators.string,
884
- displayName: Validators.string.optional(),
885
- supportedGenerationMethods: Validators.arrayOf(Validators.string).optional()
886
- });
887
- const geminiListResponse = Validators.object({
888
- models: Validators.arrayOf(geminiListEntry)
889
- });
890
- // ============================================================================
891
- // List models — capability resolution
892
- // ============================================================================
893
- /**
894
- * Translates Gemini's `supportedGenerationMethods` strings into our abstract
895
- * capability vocabulary. Methods without a mapping are ignored.
896
- * @internal
897
- */
898
- function geminiMethodsToCapabilities(methods) {
899
- const out = [];
900
- for (const m of methods) {
901
- if (m === 'generateContent') {
902
- out.push('chat');
903
- }
904
- else if (m === 'predict') {
905
- out.push('image-generation');
906
- }
907
- }
908
- return out;
909
- }
910
- /**
911
- * Strips the `models/` prefix Gemini includes on listed model names.
912
- * @internal
913
- */
914
- function geminiBareId(name) {
915
- /* c8 ignore next 1 - defensive: Gemini API always returns names prefixed with 'models/' */
916
- return name.startsWith('models/') ? name.substring('models/'.length) : name;
917
- }
918
- /**
919
- * Applies a capability config to a model id. Walks per-provider rules then
920
- * global rules; unions all matching rules' capabilities. Returns the union
921
- * and the first matching `displayName` (if any).
922
- * @internal
923
- */
924
- function applyCapabilityConfig(config, providerId, modelId) {
925
- var _a, _b, _c;
926
- const caps = new Set();
927
- let displayName;
928
- const rulesets = [
929
- (_b = (_a = config.perProvider) === null || _a === void 0 ? void 0 : _a[providerId]) !== null && _b !== void 0 ? _b : [],
930
- (_c = config.global) !== null && _c !== void 0 ? _c : []
931
- ];
932
- for (const rules of rulesets) {
933
- for (const rule of rules) {
934
- rule.idPattern.lastIndex = 0;
935
- if (rule.idPattern.test(modelId)) {
936
- for (const cap of rule.capabilities) {
937
- caps.add(cap);
938
- }
939
- if (displayName === undefined && rule.displayName !== undefined) {
940
- displayName = typeof rule.displayName === 'function' ? rule.displayName(modelId) : rule.displayName;
941
- }
942
- }
943
- }
944
- }
945
- return { capabilities: Array.from(caps), displayName };
946
- }
947
- /**
948
- * Combines provider-native capability info (when supplied) and config-derived
949
- * capability info into a final {@link IAiModelInfo}.
950
- * @internal
951
- */
952
- function buildModelInfo(providerId, id, nativeCapabilities, nativeDisplayName, config) {
953
- const fromConfig = applyCapabilityConfig(config, providerId, id);
954
- const all = new Set([...nativeCapabilities, ...fromConfig.capabilities]);
955
- return Object.assign({ id, capabilities: all }, (nativeDisplayName !== undefined
956
- ? { displayName: nativeDisplayName }
957
- : fromConfig.displayName !== undefined
958
- ? { displayName: fromConfig.displayName }
959
- : {}));
960
- }
961
- // ============================================================================
962
- // List models — adapters
963
- // ============================================================================
964
- /**
965
- * Calls the OpenAI-style `GET /models` endpoint. Used by openai, xai-grok,
966
- * groq, and mistral. Provider supplies no capability info — capabilities are
967
- * derived entirely from the config.
968
- * @internal
969
- */
970
- async function callOpenAiListModels(config, providerId, capabilityConfig, logger, signal) {
971
- const url = `${config.baseUrl}/models`;
972
- const headers = bearerAuthHeader(config.apiKey);
973
- /* c8 ignore next 1 - optional logger */
974
- logger === null || logger === void 0 ? void 0 : logger.info(`List models: provider=${providerId}, format=openai`);
975
- const jsonResult = await fetchGetJson(url, headers, logger, signal);
976
- if (jsonResult.isFailure()) {
977
- return fail(jsonResult.message);
978
- }
979
- return openAiListResponse
980
- .validate(jsonResult.value)
981
- .withErrorFormat((msg) => `OpenAI models API response: ${msg}`)
982
- .onSuccess((response) => {
983
- const models = response.data.map((entry) => buildModelInfo(providerId, entry.id, [], undefined, capabilityConfig));
984
- return succeed(models);
985
- });
986
- }
987
- /**
988
- * Calls the Anthropic `GET /models` endpoint. Provider supplies a
989
- * `display_name` but no native capability info.
990
- * @internal
991
- */
992
- async function callAnthropicListModels(config, providerId, capabilityConfig, logger, signal) {
993
- const url = `${config.baseUrl}/models`;
994
- const headers = {
995
- 'x-api-key': config.apiKey,
996
- 'anthropic-version': '2023-06-01',
997
- 'anthropic-dangerous-direct-browser-access': 'true'
998
- };
999
- /* c8 ignore next 1 - optional logger */
1000
- logger === null || logger === void 0 ? void 0 : logger.info(`List models: provider=${providerId}, format=anthropic`);
1001
- const jsonResult = await fetchGetJson(url, headers, logger, signal);
1002
- if (jsonResult.isFailure()) {
1003
- return fail(jsonResult.message);
1004
- }
1005
- return anthropicListResponse
1006
- .validate(jsonResult.value)
1007
- .withErrorFormat((msg) => `Anthropic models API response: ${msg}`)
1008
- .onSuccess((response) => {
1009
- const models = response.data.map((entry) => buildModelInfo(providerId, entry.id, [], entry.display_name, capabilityConfig));
1010
- return succeed(models);
1011
- });
1012
- }
1013
- /**
1014
- * Calls the Gemini `GET /models` endpoint. Provider supplies both a
1015
- * `displayName` and `supportedGenerationMethods` — translated to native
1016
- * capabilities and unioned with config-derived capabilities.
1017
- * @internal
1018
- */
1019
- async function callGeminiListModels(config, providerId, capabilityConfig, logger, signal) {
1020
- const url = `${config.baseUrl}/models`;
1021
- const headers = {
1022
- 'x-goog-api-key': config.apiKey
1023
- };
1024
- /* c8 ignore next 1 - optional logger */
1025
- logger === null || logger === void 0 ? void 0 : logger.info(`List models: provider=${providerId}, format=gemini`);
1026
- const jsonResult = await fetchGetJson(url, headers, logger, signal);
1027
- if (jsonResult.isFailure()) {
1028
- return fail(jsonResult.message);
1029
- }
1030
- return geminiListResponse
1031
- .validate(jsonResult.value)
1032
- .withErrorFormat((msg) => `Gemini models API response: ${msg}`)
1033
- .onSuccess((response) => {
1034
- const models = response.models.map((entry) => {
1035
- const id = geminiBareId(entry.name);
1036
- const native = entry.supportedGenerationMethods
1037
- ? geminiMethodsToCapabilities(entry.supportedGenerationMethods)
1038
- : [];
1039
- return buildModelInfo(providerId, id, native, entry.displayName, capabilityConfig);
1040
- });
1041
- return succeed(models);
1042
- });
1043
- }
1044
- // ============================================================================
1045
- // List models — dispatcher
1046
- // ============================================================================
1047
- /**
1048
- * Lists models available from a provider, routing by `descriptor.apiFormat`.
1049
- * Capabilities are resolved from native provider info and a configurable rule set.
1050
- * @param params - Request parameters (descriptor, API key, optional capability filter)
1051
- * @public
1052
- */
1053
- export async function callProviderListModels(params) {
1054
- const { descriptor, apiKey, capability, capabilityConfig, logger, signal, endpoint } = params;
1055
- const baseUrlResult = resolveEffectiveBaseUrl(descriptor, endpoint);
1056
- if (baseUrlResult.isFailure()) {
1057
- return fail(baseUrlResult.message);
1058
- }
1059
- const config = {
1060
- baseUrl: baseUrlResult.value,
1061
- apiKey,
1062
- model: '' // unused by listing
1063
- };
1064
- const effectiveConfig = capabilityConfig !== null && capabilityConfig !== void 0 ? capabilityConfig : DEFAULT_MODEL_CAPABILITY_CONFIG;
1065
- let listResult;
1066
- switch (descriptor.apiFormat) {
1067
- case 'openai':
1068
- listResult = await callOpenAiListModels(config, descriptor.id, effectiveConfig, logger, signal);
1069
- break;
1070
- case 'anthropic':
1071
- listResult = await callAnthropicListModels(config, descriptor.id, effectiveConfig, logger, signal);
1072
- break;
1073
- case 'gemini':
1074
- listResult = await callGeminiListModels(config, descriptor.id, effectiveConfig, logger, signal);
1075
- break;
1076
- /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */
1077
- default: {
1078
- const _exhaustive = descriptor.apiFormat;
1079
- return fail(`unsupported API format: ${String(_exhaustive)}`);
1080
- }
1081
- }
1082
- if (listResult.isFailure()) {
1083
- return listResult;
1084
- }
1085
- if (capability === undefined) {
1086
- return listResult;
1087
- }
1088
- return succeed(listResult.value.filter((m) => m.capabilities.has(capability)));
1089
- }
1090
- // ============================================================================
1091
- // Proxied list models
1092
- // ============================================================================
1093
- /**
1094
- * Calls the model-listing endpoint on a proxy server. Endpoint:
1095
- * `POST ${proxyUrl}/api/ai/list-models`. Capability config is not forwarded;
1096
- * `capabilities` is serialized as a string array. Error body `{error: string}`
1097
- * is surfaced as `proxy: ${error}`.
1098
- * @public
1099
- */
1100
- export async function callProxiedListModels(proxyUrl, params) {
1101
- const { descriptor, apiKey, capability, logger, signal } = params;
1102
- const body = {
1103
- providerId: descriptor.id,
1104
- apiKey
1105
- };
1106
- if (capability !== undefined) {
1107
- body.capability = capability;
1108
- }
1109
- /* c8 ignore next 1 - optional logger */
1110
- logger === null || logger === void 0 ? void 0 : logger.info(`AI list-models proxy request: provider=${descriptor.id}, proxy=${proxyUrl}`);
1111
- const url = `${proxyUrl}/api/ai/list-models`;
1112
- const jsonResult = await fetchJson(url, {}, body, logger, signal);
1113
- if (jsonResult.isFailure()) {
1114
- return fail(jsonResult.message);
1115
- }
1116
- const response = jsonResult.value;
1117
- if (typeof response.error === 'string') {
1118
- return fail(`proxy: ${response.error}`);
1119
- }
1120
- return proxiedListModelsResponse
1121
- .validate(response)
1122
- .withErrorFormat((msg) => `proxy returned invalid response: ${msg}`)
1123
- .onSuccess((parsed) => {
1124
- const models = parsed.models.map((m) => (Object.assign({ id: m.id, capabilities: new Set(m.capabilities) }, (m.displayName !== undefined ? { displayName: m.displayName } : {}))));
1125
- return succeed(models);
1126
- });
1127
- }
1128
- // ============================================================================
1129
- // Proxied completion (routes through a backend server)
1130
- // ============================================================================
1131
- /**
1132
- * Calls the AI completion endpoint on a proxy server instead of calling the
1133
- * provider API directly from the browser. The proxy handles provider dispatch,
1134
- * CORS, and API key forwarding. The request body serializes the unified
1135
- * {@link AiAssist.IChatRequest} shape (`system?` + `messages`). Enforces the same
1136
- * non-empty / trailing-user-turn and image-input invariants as the direct path.
1137
- * @param proxyUrl - Base URL of the proxy server
1138
- * @param params - Same parameters as {@link callProviderCompletion}
1139
- * @public
1140
- */
1141
- export async function callProxiedCompletion(proxyUrl, params) {
1142
- const { descriptor, apiKey, system, messages, temperature, modelOverride, logger, tools, signal, thinking, maxTokens } = params;
1143
- const splitResult = splitChatRequest(system, messages);
1144
- if (splitResult.isFailure()) {
1145
- return fail(splitResult.message);
1146
- }
1147
- if (splitResult.value.prompt.attachments.length > 0 && !descriptor.acceptsImageInput) {
1148
- return fail(`provider "${descriptor.id}" does not accept image input`);
1149
- }
1150
- const body = {
1151
- providerId: descriptor.id,
1152
- apiKey,
1153
- messages: normalizeOutboundMessages(splitResult.value)
1154
- };
1155
- // Temperature is forwarded only when explicitly provided, matching the direct path — the proxy
1156
- // omits it from the upstream request so the provider default applies.
1157
- if (temperature !== undefined) {
1158
- body.temperature = temperature;
1159
- }
1160
- if (system !== undefined) {
1161
- body.system = system;
1162
- }
1163
- if (modelOverride !== undefined) {
1164
- body.modelOverride = modelOverride;
1165
- }
1166
- if (tools && tools.length > 0) {
1167
- body.tools = tools;
1168
- }
1169
- if (thinking !== undefined) {
1170
- body.thinking = thinking;
1171
- }
1172
- // Forwarded only when explicitly provided; the proxy is responsible for mapping it to the
1173
- // correct upstream provider field (see AiAssist.usesMaxCompletionTokensField).
1174
- if (maxTokens !== undefined) {
1175
- body.maxTokens = maxTokens;
1176
- }
1177
- /* c8 ignore next 1 - optional logger */
1178
- logger === null || logger === void 0 ? void 0 : logger.info(`AI proxy request: provider=${descriptor.id}, proxy=${proxyUrl}`);
1179
- const url = `${proxyUrl}/api/ai/completion`;
1180
- const jsonResult = await fetchJson(url, {}, body, logger, signal);
1181
- if (jsonResult.isFailure()) {
1182
- return fail(jsonResult.message);
1183
- }
1184
- const response = jsonResult.value;
1185
- if (typeof response.error === 'string') {
1186
- return fail(`proxy: ${response.error}`);
1187
- }
1188
- if (typeof response.content !== 'string') {
1189
- return fail('proxy returned invalid response: missing content');
1190
- }
1191
- return succeed({
1192
- content: response.content,
1193
- truncated: response.truncated === true
1194
- });
1195
- }
1196
- // ============================================================================
1197
- // Proxied image generation
1198
- // ============================================================================
1199
- /**
1200
- * Calls the image-generation endpoint on a proxy server instead of calling
1201
- * the provider API directly from the browser.
1202
- * Endpoint: `POST ${proxyUrl}/api/ai/image-generation`. Request body:
1203
- * `{providerId, apiKey, params, modelOverride?}`. The proxy handles descriptor
1204
- * lookup, model resolution, provider dispatch, and response normalization
1205
- * (including repackaging `referenceImages` for the upstream wire format).
1206
- * Error body `{error: string}` is surfaced as `proxy: ${error}`.
1207
- * @param proxyUrl - Base URL of the proxy server
1208
- * @param params - Same parameters as {@link callProviderImageGeneration}
1209
- * @public
1210
- */
1211
- export async function callProxiedImageGeneration(proxyUrl, params) {
1212
- const { descriptor, apiKey, params: request, modelOverride, logger, signal } = params;
1213
- const body = {
1214
- providerId: descriptor.id,
1215
- apiKey,
1216
- params: request
1217
- };
1218
- if (modelOverride !== undefined) {
1219
- body.modelOverride = modelOverride;
1220
- }
1221
- /* c8 ignore next 1 - optional logger */
1222
- logger === null || logger === void 0 ? void 0 : logger.info(`AI image proxy request: provider=${descriptor.id}, proxy=${proxyUrl}`);
1223
- const url = `${proxyUrl}/api/ai/image-generation`;
1224
- const jsonResult = await fetchJson(url, {}, body, logger, signal);
1225
- if (jsonResult.isFailure()) {
1226
- return fail(jsonResult.message);
1227
- }
1228
- const response = jsonResult.value;
1229
- if (typeof response.error === 'string') {
1230
- return fail(`proxy: ${response.error}`);
1231
- }
1232
- return proxiedImageGenerationResponse
1233
- .validate(response)
1234
- .withErrorFormat((msg) => `proxy returned invalid response: ${msg}`);
1235
- }
1236
- //# sourceMappingURL=apiClient.js.map