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