@fgv/ts-extras 5.1.0-26 → 5.1.0-27

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 (103) hide show
  1. package/dist/index.browser.js +2 -2
  2. package/dist/index.browser.js.map +1 -1
  3. package/dist/packlets/ai-assist/apiClient.js +300 -213
  4. package/dist/packlets/ai-assist/apiClient.js.map +1 -1
  5. package/dist/packlets/ai-assist/chatRequestBuilders.js +6 -0
  6. package/dist/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
  7. package/dist/packlets/ai-assist/imageOptionsResolver.js +212 -0
  8. package/dist/packlets/ai-assist/imageOptionsResolver.js.map +1 -0
  9. package/dist/packlets/ai-assist/index.js +1 -0
  10. package/dist/packlets/ai-assist/index.js.map +1 -1
  11. package/dist/packlets/ai-assist/model.js +1 -1
  12. package/dist/packlets/ai-assist/model.js.map +1 -1
  13. package/dist/packlets/ai-assist/registry.js +120 -22
  14. package/dist/packlets/ai-assist/registry.js.map +1 -1
  15. package/dist/packlets/ai-assist/sseParser.js +1 -0
  16. package/dist/packlets/ai-assist/sseParser.js.map +1 -1
  17. package/dist/packlets/ai-assist/streamingAdapters/anthropic.js +17 -12
  18. package/dist/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
  19. package/dist/packlets/ai-assist/streamingAdapters/common.js +2 -0
  20. package/dist/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
  21. package/dist/packlets/ai-assist/streamingAdapters/gemini.js +17 -5
  22. package/dist/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
  23. package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js +19 -4
  24. package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -1
  25. package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js +20 -5
  26. package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
  27. package/dist/packlets/ai-assist/streamingAdapters/proxy.js +9 -3
  28. package/dist/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -1
  29. package/dist/packlets/ai-assist/streamingClient.js +28 -6
  30. package/dist/packlets/ai-assist/streamingClient.js.map +1 -1
  31. package/dist/packlets/ai-assist/thinkingOptionsResolver.js +265 -0
  32. package/dist/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -0
  33. package/dist/packlets/conversion/converters.js +1 -0
  34. package/dist/packlets/conversion/converters.js.map +1 -1
  35. package/dist/packlets/crypto-utils/keystore/keyStore.js +2 -1
  36. package/dist/packlets/crypto-utils/keystore/keyStore.js.map +1 -1
  37. package/dist/ts-extras.d.ts +595 -119
  38. package/lib/index.browser.d.ts +2 -2
  39. package/lib/index.browser.d.ts.map +1 -1
  40. package/lib/index.browser.js +4 -3
  41. package/lib/index.browser.js.map +1 -1
  42. package/lib/packlets/ai-assist/apiClient.d.ts +29 -85
  43. package/lib/packlets/ai-assist/apiClient.d.ts.map +1 -1
  44. package/lib/packlets/ai-assist/apiClient.js +300 -213
  45. package/lib/packlets/ai-assist/apiClient.js.map +1 -1
  46. package/lib/packlets/ai-assist/chatRequestBuilders.d.ts.map +1 -1
  47. package/lib/packlets/ai-assist/chatRequestBuilders.js +6 -0
  48. package/lib/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
  49. package/lib/packlets/ai-assist/imageOptionsResolver.d.ts +74 -0
  50. package/lib/packlets/ai-assist/imageOptionsResolver.d.ts.map +1 -0
  51. package/lib/packlets/ai-assist/imageOptionsResolver.js +216 -0
  52. package/lib/packlets/ai-assist/imageOptionsResolver.js.map +1 -0
  53. package/lib/packlets/ai-assist/index.d.ts +2 -1
  54. package/lib/packlets/ai-assist/index.d.ts.map +1 -1
  55. package/lib/packlets/ai-assist/index.js +4 -1
  56. package/lib/packlets/ai-assist/index.js.map +1 -1
  57. package/lib/packlets/ai-assist/model.d.ts +410 -35
  58. package/lib/packlets/ai-assist/model.d.ts.map +1 -1
  59. package/lib/packlets/ai-assist/model.js +1 -1
  60. package/lib/packlets/ai-assist/model.js.map +1 -1
  61. package/lib/packlets/ai-assist/registry.d.ts.map +1 -1
  62. package/lib/packlets/ai-assist/registry.js +120 -22
  63. package/lib/packlets/ai-assist/registry.js.map +1 -1
  64. package/lib/packlets/ai-assist/sseParser.d.ts.map +1 -1
  65. package/lib/packlets/ai-assist/sseParser.js +1 -0
  66. package/lib/packlets/ai-assist/sseParser.js.map +1 -1
  67. package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts +2 -1
  68. package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts.map +1 -1
  69. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js +17 -12
  70. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
  71. package/lib/packlets/ai-assist/streamingAdapters/common.d.ts +5 -1
  72. package/lib/packlets/ai-assist/streamingAdapters/common.d.ts.map +1 -1
  73. package/lib/packlets/ai-assist/streamingAdapters/common.js +2 -0
  74. package/lib/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
  75. package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts +2 -1
  76. package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts.map +1 -1
  77. package/lib/packlets/ai-assist/streamingAdapters/gemini.js +17 -5
  78. package/lib/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
  79. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts +2 -1
  80. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts.map +1 -1
  81. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js +19 -4
  82. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -1
  83. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts +2 -1
  84. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts.map +1 -1
  85. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js +20 -5
  86. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
  87. package/lib/packlets/ai-assist/streamingAdapters/proxy.d.ts.map +1 -1
  88. package/lib/packlets/ai-assist/streamingAdapters/proxy.js +9 -3
  89. package/lib/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -1
  90. package/lib/packlets/ai-assist/streamingClient.d.ts.map +1 -1
  91. package/lib/packlets/ai-assist/streamingClient.js +28 -6
  92. package/lib/packlets/ai-assist/streamingClient.js.map +1 -1
  93. package/lib/packlets/ai-assist/thinkingOptionsResolver.d.ts +71 -0
  94. package/lib/packlets/ai-assist/thinkingOptionsResolver.d.ts.map +1 -0
  95. package/lib/packlets/ai-assist/thinkingOptionsResolver.js +270 -0
  96. package/lib/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -0
  97. package/lib/packlets/conversion/converters.d.ts.map +1 -1
  98. package/lib/packlets/conversion/converters.js +1 -0
  99. package/lib/packlets/conversion/converters.js.map +1 -1
  100. package/lib/packlets/crypto-utils/keystore/keyStore.d.ts.map +1 -1
  101. package/lib/packlets/crypto-utils/keystore/keyStore.js +2 -1
  102. package/lib/packlets/crypto-utils/keystore/keyStore.js.map +1 -1
  103. package/package.json +13 -13
@@ -32,9 +32,11 @@
32
32
  import { isJsonObject } from '@fgv/ts-json-base';
33
33
  import { fail, mapResults, succeed, Validators } from '@fgv/ts-utils';
34
34
  import { resolveModel } from './model';
35
+ import { checkTemperatureConflict, mergeThinkingConfig, providerDiscriminatorForId } from './thinkingOptionsResolver';
35
36
  import { buildAnthropicMessages, buildGeminiContents, buildMessages, buildOpenAiChatUserContent, buildOpenAiResponsesUserContent } from './chatRequestBuilders';
36
37
  import { bearerAuthHeader, resolveEffectiveBaseUrl } from './endpoint';
37
38
  import { DEFAULT_MODEL_CAPABILITY_CONFIG, resolveImageCapability, supportsImageGeneration } from './registry';
39
+ import { resolveImageOptions, validateResolvedOptions } from './imageOptionsResolver';
38
40
  import { toAnthropicTools, toGeminiTools, toResponsesApiTools } from './toolFormats';
39
41
  // ============================================================================
40
42
  // Shared helpers
@@ -104,6 +106,7 @@ async function fetchMultipart(url, headers, body, logger, signal) {
104
106
  });
105
107
  }
106
108
  catch (err) {
109
+ /* c8 ignore next 1 - defensive: fetch errors are always Error instances in practice */
107
110
  const detail = err instanceof Error ? err.message : String(err);
108
111
  /* c8 ignore next 1 - optional logger */
109
112
  logger === null || logger === void 0 ? void 0 : logger.error(`AI API request failed: ${detail}`);
@@ -121,13 +124,12 @@ async function fetchMultipart(url, headers, body, logger, signal) {
121
124
  try {
122
125
  json = await response.json();
123
126
  }
124
- catch (_a) {
125
- /* c8 ignore next 1 - optional logger */
127
+ catch /* c8 ignore start - defensive: response.json() failure on a 2xx */ (_a) {
126
128
  logger === null || logger === void 0 ? void 0 : logger.error('AI API returned invalid JSON response');
127
129
  return fail('AI API returned invalid JSON response');
128
- }
130
+ } /* c8 ignore stop */
131
+ /* c8 ignore next 5 - defensive: provider returning non-object JSON on a 2xx */
129
132
  if (!isJsonObject(json)) {
130
- /* c8 ignore next 1 - optional logger */
131
133
  logger === null || logger === void 0 ? void 0 : logger.error('AI API returned non-object JSON response');
132
134
  return fail('AI API returned non-object JSON response');
133
135
  }
@@ -193,6 +195,7 @@ async function fetchGetJson(url, headers, logger, signal) {
193
195
  response = await fetch(url, { method: 'GET', headers, signal });
194
196
  }
195
197
  catch (err) {
198
+ /* c8 ignore next 1 - defensive: fetch errors are always Error instances in practice */
196
199
  const detail = err instanceof Error ? err.message : String(err);
197
200
  /* c8 ignore next 1 - optional logger */
198
201
  logger === null || logger === void 0 ? void 0 : logger.error(`AI API request failed: ${detail}`);
@@ -210,13 +213,12 @@ async function fetchGetJson(url, headers, logger, signal) {
210
213
  try {
211
214
  json = await response.json();
212
215
  }
213
- catch (_a) {
214
- /* c8 ignore next 1 - optional logger */
216
+ catch /* c8 ignore start - defensive: response.json() failure on a 2xx */ (_a) {
215
217
  logger === null || logger === void 0 ? void 0 : logger.error('AI API returned invalid JSON response');
216
218
  return fail('AI API returned invalid JSON response');
217
- }
219
+ } /* c8 ignore stop */
220
+ /* c8 ignore next 5 - defensive: provider returning non-object JSON on a 2xx */
218
221
  if (!isJsonObject(json)) {
219
- /* c8 ignore next 1 - optional logger */
220
222
  logger === null || logger === void 0 ? void 0 : logger.error('AI API returned non-object JSON response');
221
223
  return fail('AI API returned non-object JSON response');
222
224
  }
@@ -246,13 +248,6 @@ const responsesApiResponse = Validators.object({
246
248
  output: Validators.arrayOf(responsesApiOutputItem).withConstraint((arr) => arr.length > 0),
247
249
  status: Validators.string
248
250
  });
249
- const anthropicContentBlock = Validators.object({
250
- text: Validators.string
251
- });
252
- const anthropicResponse = Validators.object({
253
- content: Validators.arrayOf(anthropicContentBlock).withConstraint((arr) => arr.length > 0),
254
- stop_reason: Validators.string
255
- });
256
251
  const geminiPart = Validators.object({
257
252
  text: Validators.string
258
253
  });
@@ -274,12 +269,17 @@ const geminiResponse = Validators.object({
274
269
  * Works for xAI Grok, OpenAI, Groq, and Mistral.
275
270
  * @internal
276
271
  */
277
- async function callOpenAiCompletion(config, prompt, additionalMessages, temperature = 0.7, logger, signal) {
272
+ async function callOpenAiCompletion(config, prompt, additionalMessages, temperature = 0.7, logger, signal, resolvedThinking) {
273
+ var _a;
278
274
  const url = `${config.baseUrl}/chat/completions`;
279
275
  const messages = buildMessages(prompt.system, buildOpenAiChatUserContent(prompt), {
280
276
  tail: additionalMessages
281
277
  });
282
- const body = { model: config.model, messages, temperature };
278
+ 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;
279
+ const body = Object.assign(Object.assign({ model: config.model, messages }, (effort === undefined || effort === 'none' ? { temperature } : {})), (effort !== undefined && config.model !== 'grok-4' ? { reasoning_effort: effort } : {}));
280
+ if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.otherParams) !== undefined) {
281
+ Object.assign(body, resolvedThinking.otherParams);
282
+ }
283
283
  const headers = bearerAuthHeader(config.apiKey);
284
284
  /* c8 ignore next 1 - optional logger */
285
285
  logger === null || logger === void 0 ? void 0 : logger.info(`OpenAI completion: model=${config.model}`);
@@ -322,17 +322,17 @@ function extractResponsesApiText(output) {
322
322
  * Used when tools are configured for an openai-format provider.
323
323
  * @internal
324
324
  */
325
- async function callOpenAiResponsesCompletion(config, prompt, tools, additionalMessages, temperature = 0.7, logger, signal) {
325
+ async function callOpenAiResponsesCompletion(config, prompt, tools, additionalMessages, temperature = 0.7, logger, signal, resolvedThinking) {
326
+ var _a;
326
327
  const url = `${config.baseUrl}/responses`;
327
328
  const input = buildMessages(prompt.system, buildOpenAiResponsesUserContent(prompt), {
328
329
  tail: additionalMessages
329
330
  });
330
- const body = {
331
- model: config.model,
332
- input,
333
- tools: toResponsesApiTools(tools),
334
- temperature
335
- };
331
+ 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;
332
+ const body = Object.assign(Object.assign({ model: config.model, input, tools: toResponsesApiTools(tools) }, (effort === undefined || effort === 'none' ? { temperature } : {})), (effort !== undefined && config.model !== 'grok-4' ? { reasoning: { effort } } : {}));
333
+ if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.otherParams) !== undefined) {
334
+ Object.assign(body, resolvedThinking.otherParams);
335
+ }
336
336
  const headers = bearerAuthHeader(config.apiKey);
337
337
  /* c8 ignore next 1 - optional logger */
338
338
  logger === null || logger === void 0 ? void 0 : logger.info(`OpenAI Responses API: model=${config.model}, tools=${tools.map((t) => t.type).join(',')}`);
@@ -375,23 +375,18 @@ function extractAnthropicText(content) {
375
375
  }
376
376
  return succeed(textParts.join(''));
377
377
  }
378
- /**
379
- * Calls the Anthropic Messages API.
380
- * When tools are configured, includes them in the request and handles
381
- * mixed content block responses.
382
- * @internal
383
- */
384
- async function callAnthropicCompletion(config, prompt, additionalMessages, temperature = 0.7, logger, tools, signal) {
378
+ /** Calls the Anthropic Messages API with optional tool support. @internal */
379
+ async function callAnthropicCompletion(config, prompt, additionalMessages, temperature = 0.7, logger, tools, signal, resolvedThinking) {
385
380
  const url = `${config.baseUrl}/messages`;
386
- // Anthropic uses system as a top-level field, not in messages
387
381
  const messages = buildAnthropicMessages(prompt, { tail: additionalMessages });
388
- const body = {
389
- model: config.model,
390
- system: prompt.system,
391
- messages,
392
- max_tokens: 4096,
393
- temperature
394
- };
382
+ const body = Object.assign({ model: config.model, system: prompt.system, messages, max_tokens: 4096 }, ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.anthropicEffort) === undefined ? { temperature } : {}));
383
+ if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.anthropicEffort) !== undefined) {
384
+ body.thinking = { type: 'enabled' };
385
+ body.output_config = { effort: resolvedThinking.anthropicEffort };
386
+ }
387
+ if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.otherParams) !== undefined) {
388
+ Object.assign(body, resolvedThinking.otherParams);
389
+ }
395
390
  if (tools && tools.length > 0) {
396
391
  body.tools = toAnthropicTools(tools);
397
392
  /* c8 ignore next 3 - optional logger diagnostic output */
@@ -410,28 +405,18 @@ async function callAnthropicCompletion(config, prompt, additionalMessages, tempe
410
405
  if (jsonResult.isFailure()) {
411
406
  return fail(jsonResult.message);
412
407
  }
413
- // When tools are used, the response content is a mixed array of block types.
414
- // We need to extract text from all text blocks.
415
- if (tools && tools.length > 0) {
416
- const rawContent = jsonResult.value.content;
417
- const stopReason = jsonResult.value.stop_reason;
418
- if (!Array.isArray(rawContent)) {
419
- return fail('Anthropic API response: content is not an array');
420
- }
421
- return extractAnthropicText(rawContent).onSuccess((text) => succeed({
422
- content: text,
423
- truncated: stopReason === 'max_tokens'
424
- }));
408
+ const rawContent = jsonResult.value.content;
409
+ const stopReason = jsonResult.value.stop_reason;
410
+ if (!Array.isArray(rawContent)) {
411
+ return fail('Anthropic API response: content is not an array');
425
412
  }
426
- return anthropicResponse
427
- .validate(jsonResult.value)
428
- .withErrorFormat((msg) => `Anthropic API response: ${msg}`)
429
- .onSuccess((response) => {
430
- return succeed({
431
- content: response.content[0].text,
432
- truncated: response.stop_reason === 'max_tokens'
433
- });
434
- });
413
+ if (typeof stopReason !== 'string') {
414
+ return fail('Anthropic API response: stop_reason is missing or not a string');
415
+ }
416
+ return extractAnthropicText(rawContent).onSuccess((text) => succeed({
417
+ content: text,
418
+ truncated: stopReason === 'max_tokens'
419
+ }));
435
420
  }
436
421
  // ============================================================================
437
422
  // Google Gemini adapter
@@ -441,14 +426,20 @@ async function callAnthropicCompletion(config, prompt, additionalMessages, tempe
441
426
  * When tools are configured, includes Google Search grounding.
442
427
  * @internal
443
428
  */
444
- async function callGeminiCompletion(config, prompt, additionalMessages, temperature = 0.7, logger, tools, signal) {
429
+ async function callGeminiCompletion(config, prompt, additionalMessages, temperature = 0.7, logger, tools, signal, resolvedThinking) {
445
430
  const url = `${config.baseUrl}/models/${config.model}:generateContent`;
446
- // Gemini uses 'contents' with 'parts', and 'model' role instead of 'assistant'
447
431
  const contents = buildGeminiContents(prompt, { tail: additionalMessages });
432
+ const generationConfig = { temperature };
433
+ if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.geminiThinkingBudget) !== undefined) {
434
+ generationConfig.thinkingConfig = { thinkingBudget: resolvedThinking.geminiThinkingBudget };
435
+ }
436
+ if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.otherParams) !== undefined) {
437
+ Object.assign(generationConfig, resolvedThinking.otherParams);
438
+ }
448
439
  const body = {
449
440
  systemInstruction: { parts: [{ text: prompt.system }] },
450
441
  contents,
451
- generationConfig: { temperature }
442
+ generationConfig
452
443
  };
453
444
  if (tools && tools.length > 0) {
454
445
  body.tools = toGeminiTools(tools);
@@ -482,23 +473,15 @@ async function callGeminiCompletion(config, prompt, additionalMessages, temperat
482
473
  // ============================================================================
483
474
  /**
484
475
  * Calls the appropriate chat completion API for a given provider.
485
- *
486
- * Routes based on the provider descriptor's `apiFormat` field:
487
- * - `'openai'` for xAI, OpenAI, Groq, Mistral
488
- * - `'anthropic'` for Anthropic Claude
489
- * - `'gemini'` for Google Gemini
490
- *
491
- * When tools are provided and the provider supports them:
492
- * - OpenAI-format providers switch to the Responses API
493
- * - Anthropic includes tools in the Messages API request
494
- * - Gemini includes Google Search grounding
495
- *
476
+ * Routes by `apiFormat`: `'openai'` (xAI/OpenAI/Groq/Mistral — switches to Responses API when
477
+ * tools are set), `'anthropic'`, or `'gemini'`.
496
478
  * @param params - Request parameters including descriptor, API key, prompt, and optional tools
497
479
  * @returns The completion response with content and truncation status, or a failure
498
480
  * @public
499
481
  */
500
482
  export async function callProviderCompletion(params) {
501
- const { descriptor, apiKey, prompt, additionalMessages, temperature = 0.7, modelOverride, logger, tools, signal, endpoint } = params;
483
+ var _a;
484
+ const { descriptor, apiKey, prompt, additionalMessages, temperature, modelOverride, logger, tools, signal, endpoint, thinking } = params;
502
485
  const baseUrlResult = resolveEffectiveBaseUrl(descriptor, endpoint);
503
486
  if (baseUrlResult.isFailure()) {
504
487
  return fail(baseUrlResult.message);
@@ -507,11 +490,31 @@ export async function callProviderCompletion(params) {
507
490
  return fail(`provider "${descriptor.id}" does not accept image input`);
508
491
  }
509
492
  const hasTools = tools !== undefined && tools.length > 0;
510
- const modelContext = hasTools ? 'tools' : undefined;
493
+ const discriminator = providerDiscriminatorForId(descriptor.id);
494
+ const hasThinkingConfig = discriminator !== undefined &&
495
+ ((thinking === null || thinking === void 0 ? void 0 : thinking.effort) !== undefined ||
496
+ ((_a = thinking === null || thinking === void 0 ? void 0 : thinking.providers) === null || _a === void 0 ? void 0 : _a.some((b) => b.provider === 'other' || b.provider === discriminator)) === true);
497
+ const modelContext = hasThinkingConfig ? 'thinking' : hasTools ? 'tools' : undefined;
511
498
  const model = resolveModel(modelOverride !== null && modelOverride !== void 0 ? modelOverride : descriptor.defaultModel, modelContext);
512
499
  if (model.length === 0) {
513
500
  return fail(`provider "${descriptor.id}": no model resolved; pass modelOverride or set descriptor.defaultModel`);
514
501
  }
502
+ let resolvedThinking;
503
+ if (thinking !== undefined) {
504
+ if (discriminator !== undefined) {
505
+ const mergeResult = mergeThinkingConfig(thinking, model, discriminator);
506
+ /* c8 ignore next 3 - mergeThinkingConfig always succeeds; defensive guard */
507
+ if (mergeResult.isFailure()) {
508
+ return fail(mergeResult.message);
509
+ }
510
+ resolvedThinking = mergeResult.value;
511
+ const conflictResult = checkTemperatureConflict(resolvedThinking, discriminator, temperature);
512
+ if (conflictResult.isFailure()) {
513
+ return fail(conflictResult.message);
514
+ }
515
+ }
516
+ }
517
+ const effectiveTemperature = temperature !== null && temperature !== void 0 ? temperature : 0.7;
515
518
  const config = {
516
519
  baseUrl: baseUrlResult.value,
517
520
  apiKey,
@@ -527,13 +530,13 @@ export async function callProviderCompletion(params) {
527
530
  switch (descriptor.apiFormat) {
528
531
  case 'openai':
529
532
  if (hasTools) {
530
- return callOpenAiResponsesCompletion(config, prompt, tools, additionalMessages, temperature, logger, signal);
533
+ return callOpenAiResponsesCompletion(config, prompt, tools, additionalMessages, effectiveTemperature, logger, signal, resolvedThinking);
531
534
  }
532
- return callOpenAiCompletion(config, prompt, additionalMessages, temperature, logger, signal);
535
+ return callOpenAiCompletion(config, prompt, additionalMessages, effectiveTemperature, logger, signal, resolvedThinking);
533
536
  case 'anthropic':
534
- return callAnthropicCompletion(config, prompt, additionalMessages, temperature, logger, tools, signal);
537
+ return callAnthropicCompletion(config, prompt, additionalMessages, effectiveTemperature, logger, tools, signal, resolvedThinking);
535
538
  case 'gemini':
536
- return callGeminiCompletion(config, prompt, additionalMessages, temperature, logger, tools, signal);
539
+ return callGeminiCompletion(config, prompt, additionalMessages, effectiveTemperature, logger, tools, signal, resolvedThinking);
537
540
  /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */
538
541
  default: {
539
542
  const _exhaustive = descriptor.apiFormat;
@@ -584,7 +587,13 @@ const proxiedImageGenerationResponse = Validators.object({
584
587
  });
585
588
  const proxiedListModelsEntry = Validators.object({
586
589
  id: Validators.string,
587
- capabilities: Validators.arrayOf(Validators.enumeratedValue(['chat', 'tools', 'vision', 'image-generation'])),
590
+ capabilities: Validators.arrayOf(Validators.enumeratedValue([
591
+ 'chat',
592
+ 'tools',
593
+ 'vision',
594
+ 'image-generation',
595
+ 'thinking'
596
+ ])),
588
597
  displayName: Validators.string.optional()
589
598
  });
590
599
  const proxiedListModelsResponse = Validators.object({
@@ -593,101 +602,160 @@ const proxiedListModelsResponse = Validators.object({
593
602
  // ============================================================================
594
603
  // Image generation — adapters
595
604
  // ============================================================================
596
- /**
597
- * Calls the OpenAI Images API. Used for both `openai-images` and `xai-images`
598
- * formats — the request shape is the same; the only difference is whether the
599
- * `size` field is honored (OpenAI: yes, xAI: ignored at the provider).
600
- *
601
- * When `request.referenceImages` is non-empty, routes to `/images/edits`
602
- * (multipart) instead of `/images/generations` (JSON). Per-model edit support
603
- * is not validated here (e.g. dall-e-3 does not support edits) — the
604
- * provider's 400 surfaces through the failure path.
605
- *
606
- * @internal
607
- */
608
- async function callOpenAiImageGeneration(config, request, defaultMimeType, logger, signal) {
609
- var _a, _b, _c;
610
- const opts = (_a = request.options) !== null && _a !== void 0 ? _a : {};
611
- const refs = (_b = request.referenceImages) !== null && _b !== void 0 ? _b : [];
605
+ /** Routes to /images/generations or /images/edits; handles outputParamStyle. @internal */
606
+ async function callOpenAiImageGeneration(config, request, capability, resolved, logger, signal) {
607
+ var _a, _b;
608
+ const refs = (_a = request.referenceImages) !== null && _a !== void 0 ? _a : [];
612
609
  const headers = bearerAuthHeader(config.apiKey);
613
- const n = (_c = opts.count) !== null && _c !== void 0 ? _c : 1;
610
+ const effectiveMimeType = resolved.outputFormat !== undefined
611
+ ? `image/${resolved.outputFormat}`
612
+ : (_b = capability.defaultOutputMimeType) !== null && _b !== void 0 ? _b : 'image/png';
614
613
  const fetched = refs.length > 0
615
- ? await callOpenAiImagesEdits(config, request, headers, n, refs, logger, signal)
616
- : await callOpenAiImagesGenerations(config, request, headers, n, logger, signal);
614
+ ? await callOpenAiImagesEdits(config, capability, request, headers, resolved, logger, signal)
615
+ : await callOpenAiImagesGenerations(config, request, headers, resolved, capability, logger, signal);
617
616
  return fetched.onSuccess((json) => openAiImageResponse
618
617
  .validate(json)
619
618
  .withErrorFormat((msg) => `OpenAI images API response: ${msg}`)
620
619
  .onSuccess((response) => succeed({
621
- images: response.data.map((item) => (Object.assign({ mimeType: defaultMimeType, base64: item.b64_json }, (item.revised_prompt !== undefined ? { revisedPrompt: item.revised_prompt } : {}))))
620
+ images: response.data.map((item) => (Object.assign({ mimeType: effectiveMimeType, base64: item.b64_json }, (item.revised_prompt !== undefined ? { revisedPrompt: item.revised_prompt } : {}))))
622
621
  })));
623
622
  }
624
- /**
625
- * Builds and posts the JSON `/images/generations` request (no refs).
626
- * @internal
627
- */
628
- function callOpenAiImagesGenerations(config, request, headers, n, logger, signal) {
623
+ /** Builds the JSON /images/generations request; handles outputParamStyle. @internal */
624
+ function callOpenAiImagesGenerations(config, request, headers, resolved, capability, logger, signal) {
629
625
  var _a;
630
- const opts = (_a = request.options) !== null && _a !== void 0 ? _a : {};
631
626
  const body = {
632
627
  model: config.model,
633
628
  prompt: request.prompt,
634
- n,
635
- response_format: 'b64_json'
629
+ n: resolved.n
636
630
  };
637
- if (opts.size !== undefined) {
638
- body.size = opts.size;
631
+ // Output format param — conditional on model capability
632
+ if (capability.outputParamStyle === 'response-format') {
633
+ body.response_format = 'b64_json';
634
+ }
635
+ else if (capability.outputParamStyle === 'output-format') {
636
+ body.output_format = (_a = resolved.outputFormat) !== null && _a !== void 0 ? _a : 'png';
637
+ }
638
+ if (resolved.size !== undefined) {
639
+ body.size = resolved.size;
640
+ }
641
+ if (capability.supportsQualityParam && resolved.quality !== undefined) {
642
+ body.quality = resolved.quality;
639
643
  }
640
- if (opts.quality !== undefined) {
641
- body.quality = opts.quality;
644
+ if (resolved.seed !== undefined) {
645
+ body.seed = resolved.seed;
642
646
  }
643
- if (opts.seed !== undefined) {
644
- body.seed = opts.seed;
647
+ if (resolved.style !== undefined) {
648
+ body.style = resolved.style;
649
+ }
650
+ if (resolved.background !== undefined) {
651
+ body.background = resolved.background;
652
+ }
653
+ if (resolved.moderation !== undefined) {
654
+ body.moderation = resolved.moderation;
655
+ }
656
+ if (resolved.outputCompression !== undefined) {
657
+ body.output_compression = resolved.outputCompression;
658
+ }
659
+ if (resolved.otherParams !== undefined) {
660
+ Object.assign(body, resolved.otherParams);
645
661
  }
646
662
  /* c8 ignore next 1 - optional logger */
647
- logger === null || logger === void 0 ? void 0 : logger.info(`Image generation: model=${config.model}, n=${n}`);
663
+ logger === null || logger === void 0 ? void 0 : logger.info(`Image generation: model=${config.model}, n=${resolved.n}`);
648
664
  return fetchJson(`${config.baseUrl}/images/generations`, headers, body, logger, signal);
649
665
  }
650
- /**
651
- * Builds and posts the multipart `/images/edits` request (with refs).
652
- * @internal
653
- */
654
- async function callOpenAiImagesEdits(config, request, headers, n, refs, logger, signal) {
655
- var _a;
666
+ /** Builds the multipart /images/edits request with ref images. @internal */
667
+ async function callOpenAiImagesEdits(config, capability, request, headers, resolved, logger, signal) {
668
+ const refs = request.referenceImages; // callers verify refs.length > 0 before calling this function
656
669
  const blobsResult = mapResults(refs.map((ref, i) => attachmentToBlob(ref).withErrorFormat((msg) => `reference image ${i}: ${msg}`)));
657
670
  /* c8 ignore next 3 - decode failure unreachable via Node's Buffer.from (silently strips invalid input) */
658
671
  if (blobsResult.isFailure()) {
659
672
  return fail(blobsResult.message);
660
673
  }
661
- const opts = (_a = request.options) !== null && _a !== void 0 ? _a : {};
662
674
  const form = new FormData();
663
675
  form.append('model', config.model);
664
676
  form.append('prompt', request.prompt);
665
- form.append('n', String(n));
666
- form.append('response_format', 'b64_json');
667
- if (opts.size !== undefined) {
668
- form.append('size', opts.size);
677
+ form.append('n', String(resolved.n));
678
+ if (capability.outputParamStyle !== 'output-format') {
679
+ form.append('response_format', 'b64_json');
669
680
  }
670
- if (opts.quality !== undefined) {
671
- form.append('quality', opts.quality);
672
- }
673
- if (opts.seed !== undefined) {
674
- form.append('seed', String(opts.seed));
681
+ if (resolved.size !== undefined) {
682
+ form.append('size', resolved.size);
675
683
  }
676
684
  blobsResult.value.forEach((blob, i) => {
677
685
  form.append('image[]', blob, `ref-${i}.${extensionForMimeType(refs[i].mimeType)}`);
678
686
  });
679
687
  /* c8 ignore next 1 - optional logger */
680
- logger === null || logger === void 0 ? void 0 : logger.info(`Image edit: model=${config.model}, n=${n}, refs=${refs.length}`);
688
+ logger === null || logger === void 0 ? void 0 : logger.info(`Image edit: model=${config.model}, n=${resolved.n}, refs=${refs.length}`);
681
689
  return fetchMultipart(`${config.baseUrl}/images/edits`, headers, form, logger, signal);
682
690
  }
683
- /**
684
- * Calls Gemini's chat-style `:generateContent` endpoint for image output
685
- * (Gemini 2.5 Flash Image / "Nano Banana"). Accepts reference images, which
686
- * are passed as `inlineData` parts alongside the text prompt.
687
- *
688
- * @internal
689
- */
690
- async function callGeminiImageOutGeneration(config, request, logger, signal) {
691
+ /** Calls xAI /images/edits with JSON body (not multipart); up to 3 source images. @internal */
692
+ async function callXaiImagesEdits(config, request, resolved, logger, signal) {
693
+ var _a;
694
+ /* c8 ignore next 1 - defensive: referenceImages always defined when this function is called */
695
+ const refs = (_a = request.referenceImages) !== null && _a !== void 0 ? _a : [];
696
+ if (refs.length > 3) {
697
+ return fail(`xAI image edits supports at most 3 reference images; got ${refs.length}`);
698
+ }
699
+ const images = refs.map((ref) => ({
700
+ type: 'image_url',
701
+ url: `data:${ref.mimeType};base64,${ref.base64}`
702
+ }));
703
+ const body = {
704
+ model: config.model,
705
+ prompt: request.prompt,
706
+ n: resolved.n,
707
+ response_format: 'b64_json',
708
+ image: images
709
+ };
710
+ if (resolved.aspectRatio !== undefined) {
711
+ body.aspect_ratio = resolved.aspectRatio;
712
+ }
713
+ if (resolved.resolution !== undefined) {
714
+ body.resolution = resolved.resolution;
715
+ }
716
+ if (resolved.otherParams !== undefined) {
717
+ Object.assign(body, resolved.otherParams);
718
+ }
719
+ /* c8 ignore next 1 - optional logger */
720
+ logger === null || logger === void 0 ? void 0 : logger.info(`xAI image edit: model=${config.model}, n=${resolved.n}, refs=${refs.length}`);
721
+ return fetchJson(`${config.baseUrl}/images/edits`, bearerAuthHeader(config.apiKey), body, logger, signal);
722
+ }
723
+ /** Calls xAI /images/generations; uses aspect_ratio instead of size. @internal */
724
+ async function callXaiImageGeneration(config, request, capability, resolved, logger, signal) {
725
+ const headers = bearerAuthHeader(config.apiKey);
726
+ const body = {
727
+ model: config.model,
728
+ prompt: request.prompt,
729
+ n: resolved.n,
730
+ response_format: 'b64_json'
731
+ };
732
+ if (resolved.aspectRatio !== undefined) {
733
+ body.aspect_ratio = resolved.aspectRatio;
734
+ }
735
+ if (resolved.resolution !== undefined) {
736
+ body.resolution = resolved.resolution;
737
+ }
738
+ if (resolved.otherParams !== undefined) {
739
+ Object.assign(body, resolved.otherParams);
740
+ }
741
+ /* c8 ignore next 1 - optional logger */
742
+ logger === null || logger === void 0 ? void 0 : logger.info(`xAI image generation: model=${config.model}, n=${resolved.n}`);
743
+ const fetched = await fetchJson(`${config.baseUrl}/images/generations`, headers, body, logger, signal);
744
+ return fetched.onSuccess((json) => openAiImageResponse
745
+ .validate(json)
746
+ .withErrorFormat((msg) => `xAI images API response: ${msg}`)
747
+ .onSuccess((response) => succeed({
748
+ images: response.data.map((item) => {
749
+ var _a;
750
+ return ({
751
+ mimeType: (_a = capability.defaultOutputMimeType) !== null && _a !== void 0 ? _a : 'image/jpeg',
752
+ base64: item.b64_json
753
+ });
754
+ })
755
+ })));
756
+ }
757
+ /** Calls Gemini :generateContent for image output; accepts ref images as inlineData. @internal */
758
+ async function callGeminiImageOutGeneration(config, request, resolved, logger, signal) {
691
759
  var _a;
692
760
  const url = `${config.baseUrl}/models/${config.model}:generateContent`;
693
761
  const refs = (_a = request.referenceImages) !== null && _a !== void 0 ? _a : [];
@@ -695,9 +763,17 @@ async function callGeminiImageOutGeneration(config, request, logger, signal) {
695
763
  for (const ref of refs) {
696
764
  parts.push({ inlineData: { mimeType: ref.mimeType, data: ref.base64 } });
697
765
  }
698
- const body = {
699
- contents: [{ role: 'user', parts }]
700
- };
766
+ const generationConfig = {};
767
+ if (resolved.geminiAspectRatio !== undefined) {
768
+ generationConfig.imageConfig = { aspectRatio: resolved.geminiAspectRatio };
769
+ }
770
+ if (resolved.otherParams !== undefined) {
771
+ Object.assign(generationConfig, resolved.otherParams);
772
+ }
773
+ const body = { contents: [{ role: 'user', parts }] };
774
+ if (Object.keys(generationConfig).length > 0) {
775
+ body.generationConfig = generationConfig;
776
+ }
701
777
  const headers = {
702
778
  'x-goog-api-key': config.apiKey
703
779
  };
@@ -724,33 +800,48 @@ async function callGeminiImageOutGeneration(config, request, logger, signal) {
724
800
  return succeed({ images });
725
801
  }));
726
802
  }
727
- /**
728
- * Calls the Gemini Imagen `:predict` endpoint.
729
- * @internal
730
- */
731
- async function callImagenGeneration(config, request, logger, signal) {
732
- var _a, _b, _c, _d;
803
+ /** Calls the Gemini Imagen :predict endpoint with Imagen 4 params. @internal */
804
+ async function callImagenGeneration(config, request, resolved, logger, signal) {
733
805
  const url = `${config.baseUrl}/models/${config.model}:predict`;
734
- const opts = (_a = request.options) !== null && _a !== void 0 ? _a : {};
735
806
  const parameters = {
736
- sampleCount: (_b = opts.count) !== null && _b !== void 0 ? _b : 1
807
+ sampleCount: resolved.n
737
808
  };
738
- if (((_c = opts.imagen) === null || _c === void 0 ? void 0 : _c.aspectRatio) !== undefined) {
739
- parameters.aspectRatio = opts.imagen.aspectRatio;
809
+ if (resolved.imagenAspectRatio !== undefined) {
810
+ parameters.aspectRatio = resolved.imagenAspectRatio;
740
811
  }
741
- if (((_d = opts.imagen) === null || _d === void 0 ? void 0 : _d.negativePrompt) !== undefined) {
742
- parameters.negativePrompt = opts.imagen.negativePrompt;
812
+ if (resolved.imageSize !== undefined) {
813
+ parameters.imageSize = resolved.imageSize;
743
814
  }
744
- if (opts.seed !== undefined) {
745
- parameters.seed = opts.seed;
815
+ if (resolved.addWatermark !== undefined) {
816
+ parameters.addWatermark = resolved.addWatermark;
817
+ }
818
+ if (resolved.enhancePrompt !== undefined) {
819
+ parameters.enhancePrompt = resolved.enhancePrompt;
820
+ }
821
+ if (resolved.imagenOutputMimeType !== undefined || resolved.imagenOutputCompressionQuality !== undefined) {
822
+ const outputOptions = {};
823
+ if (resolved.imagenOutputMimeType !== undefined) {
824
+ outputOptions.mimeType = resolved.imagenOutputMimeType;
825
+ }
826
+ if (resolved.imagenOutputCompressionQuality !== undefined) {
827
+ outputOptions.compressionQuality = resolved.imagenOutputCompressionQuality;
828
+ }
829
+ parameters.outputOptions = outputOptions;
830
+ }
831
+ if (resolved.personGeneration !== undefined) {
832
+ parameters.personGeneration = resolved.personGeneration;
833
+ }
834
+ if (resolved.seed !== undefined) {
835
+ parameters.seed = resolved.seed;
836
+ }
837
+ if (resolved.otherParams !== undefined) {
838
+ Object.assign(parameters, resolved.otherParams);
746
839
  }
747
840
  const body = {
748
841
  instances: [{ prompt: request.prompt }],
749
842
  parameters
750
843
  };
751
- const headers = {
752
- 'x-goog-api-key': config.apiKey
753
- };
844
+ const headers = { 'x-goog-api-key': config.apiKey };
754
845
  /* c8 ignore next 1 - optional logger */
755
846
  logger === null || logger === void 0 ? void 0 : logger.info(`Imagen generation: model=${config.model}, n=${parameters.sampleCount}`);
756
847
  const jsonResult = await fetchJson(url, headers, body, logger, signal);
@@ -776,25 +867,16 @@ async function callImagenGeneration(config, request, logger, signal) {
776
867
  // ============================================================================
777
868
  /**
778
869
  * Calls the appropriate image-generation API for a given provider.
779
- *
780
- * Resolves a {@link IAiImageModelCapability} from
781
- * {@link IAiProviderDescriptor.imageGeneration} for the requested model and
782
- * routes by its `format`:
783
- * - `'openai-images'` for OpenAI (DALL-E, gpt-image-1)
784
- * - `'xai-images'` for xAI Grok image models
785
- * - `'gemini-imagen'` for Google Imagen `:predict`
786
- * - `'gemini-image-out'` for Gemini chat-style image output (Nano Banana)
787
- *
788
- * Image-model selection reuses the existing `'image'` {@link ModelSpecKey}.
789
- * When `request.referenceImages` is non-empty, the call is rejected up front
790
- * unless the resolved capability declares `acceptsImageReferenceInput`.
791
- *
870
+ * Routes by the `format` field of the resolved {@link IAiImageModelCapability}:
871
+ * `'openai-images'`, `'xai-images'`, `'xai-images-edits'`, `'gemini-imagen'`,
872
+ * or `'gemini-image-out'`. Rejects up front if `referenceImages` is set but the
873
+ * capability does not declare `acceptsImageReferenceInput`.
792
874
  * @param params - Request parameters including descriptor, API key, and prompt
793
875
  * @returns The generated images, or a failure
794
876
  * @public
795
877
  */
796
878
  export async function callProviderImageGeneration(params) {
797
- var _a, _b;
879
+ var _a, _b, _c;
798
880
  const { descriptor, apiKey, params: request, modelOverride, logger, signal, endpoint } = params;
799
881
  if (!supportsImageGeneration(descriptor)) {
800
882
  return fail(`provider "${descriptor.id}" does not support image generation`);
@@ -816,6 +898,11 @@ export async function callProviderImageGeneration(params) {
816
898
  if (((_b = (_a = request.referenceImages) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 0 && !capability.acceptsImageReferenceInput) {
817
899
  return fail(`model "${model}" does not support reference images`);
818
900
  }
901
+ const resolved = resolveImageOptions(model, capability, request.options);
902
+ const validationResult = validateResolvedOptions(model, capability, resolved);
903
+ if (validationResult.isFailure()) {
904
+ return fail(validationResult.message);
905
+ }
819
906
  const config = {
820
907
  baseUrl: baseUrlResult.value,
821
908
  apiKey,
@@ -828,13 +915,32 @@ export async function callProviderImageGeneration(params) {
828
915
  }
829
916
  switch (capability.format) {
830
917
  case 'openai-images':
831
- return callOpenAiImageGeneration(config, request, 'image/png', logger, signal);
918
+ return callOpenAiImageGeneration(config, request, capability, resolved, logger, signal);
832
919
  case 'xai-images':
833
- return callOpenAiImageGeneration(config, request, 'image/jpeg', logger, signal);
920
+ return callXaiImageGeneration(config, request, capability, resolved, logger, signal);
921
+ case 'xai-images-edits': {
922
+ const refs = (_c = request.referenceImages) !== null && _c !== void 0 ? _c : [];
923
+ if (refs.length > 0) {
924
+ const editsResult = await callXaiImagesEdits(config, request, resolved, logger, signal);
925
+ return editsResult.onSuccess((json) => openAiImageResponse
926
+ .validate(json)
927
+ .withErrorFormat((msg) => `xAI images API response: ${msg}`)
928
+ .onSuccess((response) => succeed({
929
+ images: response.data.map((item) => {
930
+ var _a;
931
+ return ({
932
+ mimeType: (_a = capability.defaultOutputMimeType) !== null && _a !== void 0 ? _a : 'image/jpeg',
933
+ base64: item.b64_json
934
+ });
935
+ })
936
+ })));
937
+ }
938
+ return callXaiImageGeneration(config, request, capability, resolved, logger, signal);
939
+ }
834
940
  case 'gemini-imagen':
835
- return callImagenGeneration(config, request, logger, signal);
941
+ return callImagenGeneration(config, request, resolved, logger, signal);
836
942
  case 'gemini-image-out':
837
- return callGeminiImageOutGeneration(config, request, logger, signal);
943
+ return callGeminiImageOutGeneration(config, request, resolved, logger, signal);
838
944
  /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */
839
945
  default: {
840
946
  const _exhaustive = capability.format;
@@ -888,6 +994,7 @@ function geminiMethodsToCapabilities(methods) {
888
994
  * @internal
889
995
  */
890
996
  function geminiBareId(name) {
997
+ /* c8 ignore next 1 - defensive: Gemini API always returns names prefixed with 'models/' */
891
998
  return name.startsWith('models/') ? name.substring('models/'.length) : name;
892
999
  }
893
1000
  /**
@@ -1020,12 +1127,8 @@ async function callGeminiListModels(config, providerId, capabilityConfig, logger
1020
1127
  // List models — dispatcher
1021
1128
  // ============================================================================
1022
1129
  /**
1023
- * Lists models available from a provider, with capabilities resolved from
1024
- * native provider info (where supplied) and a configurable rule set.
1025
- *
1026
- * Routes based on `descriptor.apiFormat` — listing reuses the existing
1027
- * format dispatch and does not require a separate descriptor field.
1028
- *
1130
+ * Lists models available from a provider, routing by `descriptor.apiFormat`.
1131
+ * Capabilities are resolved from native provider info and a configurable rule set.
1029
1132
  * @param params - Request parameters including descriptor, API key, and optional capability filter
1030
1133
  * @returns The resolved model list, or a failure
1031
1134
  * @public
@@ -1072,18 +1175,9 @@ export async function callProviderListModels(params) {
1072
1175
  // ============================================================================
1073
1176
  /**
1074
1177
  * Calls the model-listing endpoint on a proxy server.
1075
- *
1076
- * @remarks
1077
- * Proxy contract:
1078
- * - Endpoint: `POST ${proxyUrl}/api/ai/list-models`
1079
- * - Request body: `{providerId, apiKey, capability?}`. Capability config is
1080
- * not forwarded — the proxy applies its own (typically the same default
1081
- * the library ships).
1082
- * - Success response body: an `IAiModelInfo[]` (under key `models`) where
1083
- * `capabilities` is serialized as a string array (not Set, which doesn't
1084
- * round-trip through JSON).
1085
- * - Error response body: `{error: string}`, surfaced as `proxy: ${error}`.
1086
- *
1178
+ * Endpoint: `POST ${proxyUrl}/api/ai/list-models`. Capability config is not
1179
+ * forwarded. `capabilities` is serialized as a string array. Error body
1180
+ * `{error: string}` is surfaced as `proxy: ${error}`.
1087
1181
  * @public
1088
1182
  */
1089
1183
  export async function callProxiedListModels(proxyUrl, params) {
@@ -1131,7 +1225,7 @@ export async function callProxiedListModels(proxyUrl, params) {
1131
1225
  * @public
1132
1226
  */
1133
1227
  export async function callProxiedCompletion(proxyUrl, params) {
1134
- const { descriptor, apiKey, prompt, additionalMessages, temperature, modelOverride, logger, tools, signal } = params;
1228
+ const { descriptor, apiKey, prompt, additionalMessages, temperature, modelOverride, logger, tools, signal, thinking } = params;
1135
1229
  const promptBody = { system: prompt.system, user: prompt.user };
1136
1230
  if (prompt.attachments.length > 0) {
1137
1231
  promptBody.attachments = prompt.attachments;
@@ -1151,6 +1245,9 @@ export async function callProxiedCompletion(proxyUrl, params) {
1151
1245
  if (tools && tools.length > 0) {
1152
1246
  body.tools = tools;
1153
1247
  }
1248
+ if (thinking !== undefined) {
1249
+ body.thinking = thinking;
1250
+ }
1154
1251
  /* c8 ignore next 1 - optional logger */
1155
1252
  logger === null || logger === void 0 ? void 0 : logger.info(`AI proxy request: provider=${descriptor.id}, proxy=${proxyUrl}`);
1156
1253
  const url = `${proxyUrl}/api/ai/completion`;
@@ -1158,7 +1255,6 @@ export async function callProxiedCompletion(proxyUrl, params) {
1158
1255
  if (jsonResult.isFailure()) {
1159
1256
  return fail(jsonResult.message);
1160
1257
  }
1161
- // Check for error response from proxy
1162
1258
  const response = jsonResult.value;
1163
1259
  if (typeof response.error === 'string') {
1164
1260
  return fail(`proxy: ${response.error}`);
@@ -1177,20 +1273,11 @@ export async function callProxiedCompletion(proxyUrl, params) {
1177
1273
  /**
1178
1274
  * Calls the image-generation endpoint on a proxy server instead of calling
1179
1275
  * the provider API directly from the browser.
1180
- *
1181
- * @remarks
1182
- * The proxy contract:
1183
- * - Endpoint: `POST ${proxyUrl}/api/ai/image-generation`
1184
- * - Request body: `{providerId, apiKey, params, modelOverride?}`
1185
- * - Success response body: an {@link IAiImageGenerationResponse}
1186
- * - Error response body: `{error: string}` (surfaced as `proxy: ${error}`)
1187
- *
1188
- * The proxy server is responsible for descriptor lookup, model resolution,
1189
- * provider dispatch, and response normalization. When `params.referenceImages`
1190
- * is present, the proxy is also responsible for repackaging it into the
1191
- * upstream wire format (e.g. multipart/form-data for OpenAI `/images/edits`,
1192
- * `inlineData` parts for Gemini `:generateContent`).
1193
- *
1276
+ * Endpoint: `POST ${proxyUrl}/api/ai/image-generation`. Request body:
1277
+ * `{providerId, apiKey, params, modelOverride?}`. The proxy handles descriptor
1278
+ * lookup, model resolution, provider dispatch, and response normalization
1279
+ * (including repackaging `referenceImages` for the upstream wire format).
1280
+ * Error body `{error: string}` is surfaced as `proxy: ${error}`.
1194
1281
  * @param proxyUrl - Base URL of the proxy server (e.g. `http://localhost:3001`)
1195
1282
  * @param params - Same parameters as {@link callProviderImageGeneration}
1196
1283
  * @returns The generated images, or a failure