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

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 (149) 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/converters.js +24 -4
  36. package/dist/packlets/crypto-utils/converters.js.map +1 -1
  37. package/dist/packlets/crypto-utils/hpkeProvider.js +333 -0
  38. package/dist/packlets/crypto-utils/hpkeProvider.js.map +1 -0
  39. package/dist/packlets/crypto-utils/index.browser.js +3 -0
  40. package/dist/packlets/crypto-utils/index.browser.js.map +1 -1
  41. package/dist/packlets/crypto-utils/index.js +2 -0
  42. package/dist/packlets/crypto-utils/index.js.map +1 -1
  43. package/dist/packlets/crypto-utils/keystore/converters.js +2 -2
  44. package/dist/packlets/crypto-utils/keystore/converters.js.map +1 -1
  45. package/dist/packlets/crypto-utils/keystore/keyStore.js +108 -2
  46. package/dist/packlets/crypto-utils/keystore/keyStore.js.map +1 -1
  47. package/dist/packlets/crypto-utils/keystore/model.js.map +1 -1
  48. package/dist/packlets/crypto-utils/model.js +21 -0
  49. package/dist/packlets/crypto-utils/model.js.map +1 -1
  50. package/dist/packlets/crypto-utils/nodeCryptoProvider.js +74 -0
  51. package/dist/packlets/crypto-utils/nodeCryptoProvider.js.map +1 -1
  52. package/dist/ts-extras.d.ts +1029 -137
  53. package/dist/tsdoc-metadata.json +1 -1
  54. package/lib/index.browser.d.ts +2 -2
  55. package/lib/index.browser.d.ts.map +1 -1
  56. package/lib/index.browser.js +4 -3
  57. package/lib/index.browser.js.map +1 -1
  58. package/lib/packlets/ai-assist/apiClient.d.ts +29 -85
  59. package/lib/packlets/ai-assist/apiClient.d.ts.map +1 -1
  60. package/lib/packlets/ai-assist/apiClient.js +300 -213
  61. package/lib/packlets/ai-assist/apiClient.js.map +1 -1
  62. package/lib/packlets/ai-assist/chatRequestBuilders.d.ts.map +1 -1
  63. package/lib/packlets/ai-assist/chatRequestBuilders.js +6 -0
  64. package/lib/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
  65. package/lib/packlets/ai-assist/imageOptionsResolver.d.ts +74 -0
  66. package/lib/packlets/ai-assist/imageOptionsResolver.d.ts.map +1 -0
  67. package/lib/packlets/ai-assist/imageOptionsResolver.js +216 -0
  68. package/lib/packlets/ai-assist/imageOptionsResolver.js.map +1 -0
  69. package/lib/packlets/ai-assist/index.d.ts +2 -1
  70. package/lib/packlets/ai-assist/index.d.ts.map +1 -1
  71. package/lib/packlets/ai-assist/index.js +4 -1
  72. package/lib/packlets/ai-assist/index.js.map +1 -1
  73. package/lib/packlets/ai-assist/model.d.ts +410 -35
  74. package/lib/packlets/ai-assist/model.d.ts.map +1 -1
  75. package/lib/packlets/ai-assist/model.js +1 -1
  76. package/lib/packlets/ai-assist/model.js.map +1 -1
  77. package/lib/packlets/ai-assist/registry.d.ts.map +1 -1
  78. package/lib/packlets/ai-assist/registry.js +120 -22
  79. package/lib/packlets/ai-assist/registry.js.map +1 -1
  80. package/lib/packlets/ai-assist/sseParser.d.ts.map +1 -1
  81. package/lib/packlets/ai-assist/sseParser.js +1 -0
  82. package/lib/packlets/ai-assist/sseParser.js.map +1 -1
  83. package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts +2 -1
  84. package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts.map +1 -1
  85. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js +17 -12
  86. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
  87. package/lib/packlets/ai-assist/streamingAdapters/common.d.ts +5 -1
  88. package/lib/packlets/ai-assist/streamingAdapters/common.d.ts.map +1 -1
  89. package/lib/packlets/ai-assist/streamingAdapters/common.js +2 -0
  90. package/lib/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
  91. package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts +2 -1
  92. package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts.map +1 -1
  93. package/lib/packlets/ai-assist/streamingAdapters/gemini.js +17 -5
  94. package/lib/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
  95. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts +2 -1
  96. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts.map +1 -1
  97. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js +19 -4
  98. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -1
  99. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts +2 -1
  100. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts.map +1 -1
  101. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js +20 -5
  102. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
  103. package/lib/packlets/ai-assist/streamingAdapters/proxy.d.ts.map +1 -1
  104. package/lib/packlets/ai-assist/streamingAdapters/proxy.js +9 -3
  105. package/lib/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -1
  106. package/lib/packlets/ai-assist/streamingClient.d.ts.map +1 -1
  107. package/lib/packlets/ai-assist/streamingClient.js +28 -6
  108. package/lib/packlets/ai-assist/streamingClient.js.map +1 -1
  109. package/lib/packlets/ai-assist/thinkingOptionsResolver.d.ts +71 -0
  110. package/lib/packlets/ai-assist/thinkingOptionsResolver.d.ts.map +1 -0
  111. package/lib/packlets/ai-assist/thinkingOptionsResolver.js +270 -0
  112. package/lib/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -0
  113. package/lib/packlets/conversion/converters.d.ts.map +1 -1
  114. package/lib/packlets/conversion/converters.js +1 -0
  115. package/lib/packlets/conversion/converters.js.map +1 -1
  116. package/lib/packlets/crypto-utils/converters.d.ts +12 -1
  117. package/lib/packlets/crypto-utils/converters.d.ts.map +1 -1
  118. package/lib/packlets/crypto-utils/converters.js +25 -5
  119. package/lib/packlets/crypto-utils/converters.js.map +1 -1
  120. package/lib/packlets/crypto-utils/hpkeProvider.d.ts +142 -0
  121. package/lib/packlets/crypto-utils/hpkeProvider.d.ts.map +1 -0
  122. package/lib/packlets/crypto-utils/hpkeProvider.js +337 -0
  123. package/lib/packlets/crypto-utils/hpkeProvider.js.map +1 -0
  124. package/lib/packlets/crypto-utils/index.browser.d.ts +1 -0
  125. package/lib/packlets/crypto-utils/index.browser.d.ts.map +1 -1
  126. package/lib/packlets/crypto-utils/index.browser.js +5 -1
  127. package/lib/packlets/crypto-utils/index.browser.js.map +1 -1
  128. package/lib/packlets/crypto-utils/index.d.ts +1 -0
  129. package/lib/packlets/crypto-utils/index.d.ts.map +1 -1
  130. package/lib/packlets/crypto-utils/index.js +4 -1
  131. package/lib/packlets/crypto-utils/index.js.map +1 -1
  132. package/lib/packlets/crypto-utils/keystore/converters.js +1 -1
  133. package/lib/packlets/crypto-utils/keystore/converters.js.map +1 -1
  134. package/lib/packlets/crypto-utils/keystore/keyStore.d.ts +32 -2
  135. package/lib/packlets/crypto-utils/keystore/keyStore.d.ts.map +1 -1
  136. package/lib/packlets/crypto-utils/keystore/keyStore.js +116 -10
  137. package/lib/packlets/crypto-utils/keystore/keyStore.js.map +1 -1
  138. package/lib/packlets/crypto-utils/keystore/model.d.ts +21 -3
  139. package/lib/packlets/crypto-utils/keystore/model.d.ts.map +1 -1
  140. package/lib/packlets/crypto-utils/keystore/model.js.map +1 -1
  141. package/lib/packlets/crypto-utils/model.d.ts +165 -9
  142. package/lib/packlets/crypto-utils/model.d.ts.map +1 -1
  143. package/lib/packlets/crypto-utils/model.js +22 -1
  144. package/lib/packlets/crypto-utils/model.js.map +1 -1
  145. package/lib/packlets/crypto-utils/nodeCryptoProvider.d.ts +39 -0
  146. package/lib/packlets/crypto-utils/nodeCryptoProvider.d.ts.map +1 -1
  147. package/lib/packlets/crypto-utils/nodeCryptoProvider.js +74 -0
  148. package/lib/packlets/crypto-utils/nodeCryptoProvider.js.map +1 -1
  149. package/package.json +13 -13
@@ -40,9 +40,11 @@ exports.callProxiedImageGeneration = callProxiedImageGeneration;
40
40
  const ts_json_base_1 = require("@fgv/ts-json-base");
41
41
  const ts_utils_1 = require("@fgv/ts-utils");
42
42
  const model_1 = require("./model");
43
+ const thinkingOptionsResolver_1 = require("./thinkingOptionsResolver");
43
44
  const chatRequestBuilders_1 = require("./chatRequestBuilders");
44
45
  const endpoint_1 = require("./endpoint");
45
46
  const registry_1 = require("./registry");
47
+ const imageOptionsResolver_1 = require("./imageOptionsResolver");
46
48
  const toolFormats_1 = require("./toolFormats");
47
49
  // ============================================================================
48
50
  // Shared helpers
@@ -112,6 +114,7 @@ async function fetchMultipart(url, headers, body, logger, signal) {
112
114
  });
113
115
  }
114
116
  catch (err) {
117
+ /* c8 ignore next 1 - defensive: fetch errors are always Error instances in practice */
115
118
  const detail = err instanceof Error ? err.message : String(err);
116
119
  /* c8 ignore next 1 - optional logger */
117
120
  logger === null || logger === void 0 ? void 0 : logger.error(`AI API request failed: ${detail}`);
@@ -129,13 +132,12 @@ async function fetchMultipart(url, headers, body, logger, signal) {
129
132
  try {
130
133
  json = await response.json();
131
134
  }
132
- catch (_a) {
133
- /* c8 ignore next 1 - optional logger */
135
+ catch /* c8 ignore start - defensive: response.json() failure on a 2xx */ (_a) {
134
136
  logger === null || logger === void 0 ? void 0 : logger.error('AI API returned invalid JSON response');
135
137
  return (0, ts_utils_1.fail)('AI API returned invalid JSON response');
136
- }
138
+ } /* c8 ignore stop */
139
+ /* c8 ignore next 5 - defensive: provider returning non-object JSON on a 2xx */
137
140
  if (!(0, ts_json_base_1.isJsonObject)(json)) {
138
- /* c8 ignore next 1 - optional logger */
139
141
  logger === null || logger === void 0 ? void 0 : logger.error('AI API returned non-object JSON response');
140
142
  return (0, ts_utils_1.fail)('AI API returned non-object JSON response');
141
143
  }
@@ -201,6 +203,7 @@ async function fetchGetJson(url, headers, logger, signal) {
201
203
  response = await fetch(url, { method: 'GET', headers, signal });
202
204
  }
203
205
  catch (err) {
206
+ /* c8 ignore next 1 - defensive: fetch errors are always Error instances in practice */
204
207
  const detail = err instanceof Error ? err.message : String(err);
205
208
  /* c8 ignore next 1 - optional logger */
206
209
  logger === null || logger === void 0 ? void 0 : logger.error(`AI API request failed: ${detail}`);
@@ -218,13 +221,12 @@ async function fetchGetJson(url, headers, logger, signal) {
218
221
  try {
219
222
  json = await response.json();
220
223
  }
221
- catch (_a) {
222
- /* c8 ignore next 1 - optional logger */
224
+ catch /* c8 ignore start - defensive: response.json() failure on a 2xx */ (_a) {
223
225
  logger === null || logger === void 0 ? void 0 : logger.error('AI API returned invalid JSON response');
224
226
  return (0, ts_utils_1.fail)('AI API returned invalid JSON response');
225
- }
227
+ } /* c8 ignore stop */
228
+ /* c8 ignore next 5 - defensive: provider returning non-object JSON on a 2xx */
226
229
  if (!(0, ts_json_base_1.isJsonObject)(json)) {
227
- /* c8 ignore next 1 - optional logger */
228
230
  logger === null || logger === void 0 ? void 0 : logger.error('AI API returned non-object JSON response');
229
231
  return (0, ts_utils_1.fail)('AI API returned non-object JSON response');
230
232
  }
@@ -254,13 +256,6 @@ const responsesApiResponse = ts_utils_1.Validators.object({
254
256
  output: ts_utils_1.Validators.arrayOf(responsesApiOutputItem).withConstraint((arr) => arr.length > 0),
255
257
  status: ts_utils_1.Validators.string
256
258
  });
257
- const anthropicContentBlock = ts_utils_1.Validators.object({
258
- text: ts_utils_1.Validators.string
259
- });
260
- const anthropicResponse = ts_utils_1.Validators.object({
261
- content: ts_utils_1.Validators.arrayOf(anthropicContentBlock).withConstraint((arr) => arr.length > 0),
262
- stop_reason: ts_utils_1.Validators.string
263
- });
264
259
  const geminiPart = ts_utils_1.Validators.object({
265
260
  text: ts_utils_1.Validators.string
266
261
  });
@@ -282,12 +277,17 @@ const geminiResponse = ts_utils_1.Validators.object({
282
277
  * Works for xAI Grok, OpenAI, Groq, and Mistral.
283
278
  * @internal
284
279
  */
285
- async function callOpenAiCompletion(config, prompt, additionalMessages, temperature = 0.7, logger, signal) {
280
+ async function callOpenAiCompletion(config, prompt, additionalMessages, temperature = 0.7, logger, signal, resolvedThinking) {
281
+ var _a;
286
282
  const url = `${config.baseUrl}/chat/completions`;
287
283
  const messages = (0, chatRequestBuilders_1.buildMessages)(prompt.system, (0, chatRequestBuilders_1.buildOpenAiChatUserContent)(prompt), {
288
284
  tail: additionalMessages
289
285
  });
290
- const body = { model: config.model, messages, temperature };
286
+ 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;
287
+ const body = Object.assign(Object.assign({ model: config.model, messages }, (effort === undefined || effort === 'none' ? { temperature } : {})), (effort !== undefined && config.model !== 'grok-4' ? { reasoning_effort: effort } : {}));
288
+ if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.otherParams) !== undefined) {
289
+ Object.assign(body, resolvedThinking.otherParams);
290
+ }
291
291
  const headers = (0, endpoint_1.bearerAuthHeader)(config.apiKey);
292
292
  /* c8 ignore next 1 - optional logger */
293
293
  logger === null || logger === void 0 ? void 0 : logger.info(`OpenAI completion: model=${config.model}`);
@@ -330,17 +330,17 @@ function extractResponsesApiText(output) {
330
330
  * Used when tools are configured for an openai-format provider.
331
331
  * @internal
332
332
  */
333
- async function callOpenAiResponsesCompletion(config, prompt, tools, additionalMessages, temperature = 0.7, logger, signal) {
333
+ async function callOpenAiResponsesCompletion(config, prompt, tools, additionalMessages, temperature = 0.7, logger, signal, resolvedThinking) {
334
+ var _a;
334
335
  const url = `${config.baseUrl}/responses`;
335
336
  const input = (0, chatRequestBuilders_1.buildMessages)(prompt.system, (0, chatRequestBuilders_1.buildOpenAiResponsesUserContent)(prompt), {
336
337
  tail: additionalMessages
337
338
  });
338
- const body = {
339
- model: config.model,
340
- input,
341
- tools: (0, toolFormats_1.toResponsesApiTools)(tools),
342
- temperature
343
- };
339
+ 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;
340
+ const body = Object.assign(Object.assign({ model: config.model, input, tools: (0, toolFormats_1.toResponsesApiTools)(tools) }, (effort === undefined || effort === 'none' ? { temperature } : {})), (effort !== undefined && config.model !== 'grok-4' ? { reasoning: { effort } } : {}));
341
+ if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.otherParams) !== undefined) {
342
+ Object.assign(body, resolvedThinking.otherParams);
343
+ }
344
344
  const headers = (0, endpoint_1.bearerAuthHeader)(config.apiKey);
345
345
  /* c8 ignore next 1 - optional logger */
346
346
  logger === null || logger === void 0 ? void 0 : logger.info(`OpenAI Responses API: model=${config.model}, tools=${tools.map((t) => t.type).join(',')}`);
@@ -383,23 +383,18 @@ function extractAnthropicText(content) {
383
383
  }
384
384
  return (0, ts_utils_1.succeed)(textParts.join(''));
385
385
  }
386
- /**
387
- * Calls the Anthropic Messages API.
388
- * When tools are configured, includes them in the request and handles
389
- * mixed content block responses.
390
- * @internal
391
- */
392
- async function callAnthropicCompletion(config, prompt, additionalMessages, temperature = 0.7, logger, tools, signal) {
386
+ /** Calls the Anthropic Messages API with optional tool support. @internal */
387
+ async function callAnthropicCompletion(config, prompt, additionalMessages, temperature = 0.7, logger, tools, signal, resolvedThinking) {
393
388
  const url = `${config.baseUrl}/messages`;
394
- // Anthropic uses system as a top-level field, not in messages
395
389
  const messages = (0, chatRequestBuilders_1.buildAnthropicMessages)(prompt, { tail: additionalMessages });
396
- const body = {
397
- model: config.model,
398
- system: prompt.system,
399
- messages,
400
- max_tokens: 4096,
401
- temperature
402
- };
390
+ 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 } : {}));
391
+ if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.anthropicEffort) !== undefined) {
392
+ body.thinking = { type: 'enabled' };
393
+ body.output_config = { effort: resolvedThinking.anthropicEffort };
394
+ }
395
+ if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.otherParams) !== undefined) {
396
+ Object.assign(body, resolvedThinking.otherParams);
397
+ }
403
398
  if (tools && tools.length > 0) {
404
399
  body.tools = (0, toolFormats_1.toAnthropicTools)(tools);
405
400
  /* c8 ignore next 3 - optional logger diagnostic output */
@@ -418,28 +413,18 @@ async function callAnthropicCompletion(config, prompt, additionalMessages, tempe
418
413
  if (jsonResult.isFailure()) {
419
414
  return (0, ts_utils_1.fail)(jsonResult.message);
420
415
  }
421
- // When tools are used, the response content is a mixed array of block types.
422
- // We need to extract text from all text blocks.
423
- if (tools && tools.length > 0) {
424
- const rawContent = jsonResult.value.content;
425
- const stopReason = jsonResult.value.stop_reason;
426
- if (!Array.isArray(rawContent)) {
427
- return (0, ts_utils_1.fail)('Anthropic API response: content is not an array');
428
- }
429
- return extractAnthropicText(rawContent).onSuccess((text) => (0, ts_utils_1.succeed)({
430
- content: text,
431
- truncated: stopReason === 'max_tokens'
432
- }));
416
+ const rawContent = jsonResult.value.content;
417
+ const stopReason = jsonResult.value.stop_reason;
418
+ if (!Array.isArray(rawContent)) {
419
+ return (0, ts_utils_1.fail)('Anthropic API response: content is not an array');
433
420
  }
434
- return anthropicResponse
435
- .validate(jsonResult.value)
436
- .withErrorFormat((msg) => `Anthropic API response: ${msg}`)
437
- .onSuccess((response) => {
438
- return (0, ts_utils_1.succeed)({
439
- content: response.content[0].text,
440
- truncated: response.stop_reason === 'max_tokens'
441
- });
442
- });
421
+ if (typeof stopReason !== 'string') {
422
+ return (0, ts_utils_1.fail)('Anthropic API response: stop_reason is missing or not a string');
423
+ }
424
+ return extractAnthropicText(rawContent).onSuccess((text) => (0, ts_utils_1.succeed)({
425
+ content: text,
426
+ truncated: stopReason === 'max_tokens'
427
+ }));
443
428
  }
444
429
  // ============================================================================
445
430
  // Google Gemini adapter
@@ -449,14 +434,20 @@ async function callAnthropicCompletion(config, prompt, additionalMessages, tempe
449
434
  * When tools are configured, includes Google Search grounding.
450
435
  * @internal
451
436
  */
452
- async function callGeminiCompletion(config, prompt, additionalMessages, temperature = 0.7, logger, tools, signal) {
437
+ async function callGeminiCompletion(config, prompt, additionalMessages, temperature = 0.7, logger, tools, signal, resolvedThinking) {
453
438
  const url = `${config.baseUrl}/models/${config.model}:generateContent`;
454
- // Gemini uses 'contents' with 'parts', and 'model' role instead of 'assistant'
455
439
  const contents = (0, chatRequestBuilders_1.buildGeminiContents)(prompt, { tail: additionalMessages });
440
+ const generationConfig = { temperature };
441
+ if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.geminiThinkingBudget) !== undefined) {
442
+ generationConfig.thinkingConfig = { thinkingBudget: resolvedThinking.geminiThinkingBudget };
443
+ }
444
+ if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.otherParams) !== undefined) {
445
+ Object.assign(generationConfig, resolvedThinking.otherParams);
446
+ }
456
447
  const body = {
457
448
  systemInstruction: { parts: [{ text: prompt.system }] },
458
449
  contents,
459
- generationConfig: { temperature }
450
+ generationConfig
460
451
  };
461
452
  if (tools && tools.length > 0) {
462
453
  body.tools = (0, toolFormats_1.toGeminiTools)(tools);
@@ -490,23 +481,15 @@ async function callGeminiCompletion(config, prompt, additionalMessages, temperat
490
481
  // ============================================================================
491
482
  /**
492
483
  * Calls the appropriate chat completion API for a given provider.
493
- *
494
- * Routes based on the provider descriptor's `apiFormat` field:
495
- * - `'openai'` for xAI, OpenAI, Groq, Mistral
496
- * - `'anthropic'` for Anthropic Claude
497
- * - `'gemini'` for Google Gemini
498
- *
499
- * When tools are provided and the provider supports them:
500
- * - OpenAI-format providers switch to the Responses API
501
- * - Anthropic includes tools in the Messages API request
502
- * - Gemini includes Google Search grounding
503
- *
484
+ * Routes by `apiFormat`: `'openai'` (xAI/OpenAI/Groq/Mistral — switches to Responses API when
485
+ * tools are set), `'anthropic'`, or `'gemini'`.
504
486
  * @param params - Request parameters including descriptor, API key, prompt, and optional tools
505
487
  * @returns The completion response with content and truncation status, or a failure
506
488
  * @public
507
489
  */
508
490
  async function callProviderCompletion(params) {
509
- const { descriptor, apiKey, prompt, additionalMessages, temperature = 0.7, modelOverride, logger, tools, signal, endpoint } = params;
491
+ var _a;
492
+ const { descriptor, apiKey, prompt, additionalMessages, temperature, modelOverride, logger, tools, signal, endpoint, thinking } = params;
510
493
  const baseUrlResult = (0, endpoint_1.resolveEffectiveBaseUrl)(descriptor, endpoint);
511
494
  if (baseUrlResult.isFailure()) {
512
495
  return (0, ts_utils_1.fail)(baseUrlResult.message);
@@ -515,11 +498,31 @@ async function callProviderCompletion(params) {
515
498
  return (0, ts_utils_1.fail)(`provider "${descriptor.id}" does not accept image input`);
516
499
  }
517
500
  const hasTools = tools !== undefined && tools.length > 0;
518
- const modelContext = hasTools ? 'tools' : undefined;
501
+ const discriminator = (0, thinkingOptionsResolver_1.providerDiscriminatorForId)(descriptor.id);
502
+ const hasThinkingConfig = discriminator !== undefined &&
503
+ ((thinking === null || thinking === void 0 ? void 0 : thinking.effort) !== undefined ||
504
+ ((_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);
505
+ const modelContext = hasThinkingConfig ? 'thinking' : hasTools ? 'tools' : undefined;
519
506
  const model = (0, model_1.resolveModel)(modelOverride !== null && modelOverride !== void 0 ? modelOverride : descriptor.defaultModel, modelContext);
520
507
  if (model.length === 0) {
521
508
  return (0, ts_utils_1.fail)(`provider "${descriptor.id}": no model resolved; pass modelOverride or set descriptor.defaultModel`);
522
509
  }
510
+ let resolvedThinking;
511
+ if (thinking !== undefined) {
512
+ if (discriminator !== undefined) {
513
+ const mergeResult = (0, thinkingOptionsResolver_1.mergeThinkingConfig)(thinking, model, discriminator);
514
+ /* c8 ignore next 3 - mergeThinkingConfig always succeeds; defensive guard */
515
+ if (mergeResult.isFailure()) {
516
+ return (0, ts_utils_1.fail)(mergeResult.message);
517
+ }
518
+ resolvedThinking = mergeResult.value;
519
+ const conflictResult = (0, thinkingOptionsResolver_1.checkTemperatureConflict)(resolvedThinking, discriminator, temperature);
520
+ if (conflictResult.isFailure()) {
521
+ return (0, ts_utils_1.fail)(conflictResult.message);
522
+ }
523
+ }
524
+ }
525
+ const effectiveTemperature = temperature !== null && temperature !== void 0 ? temperature : 0.7;
523
526
  const config = {
524
527
  baseUrl: baseUrlResult.value,
525
528
  apiKey,
@@ -535,13 +538,13 @@ async function callProviderCompletion(params) {
535
538
  switch (descriptor.apiFormat) {
536
539
  case 'openai':
537
540
  if (hasTools) {
538
- return callOpenAiResponsesCompletion(config, prompt, tools, additionalMessages, temperature, logger, signal);
541
+ return callOpenAiResponsesCompletion(config, prompt, tools, additionalMessages, effectiveTemperature, logger, signal, resolvedThinking);
539
542
  }
540
- return callOpenAiCompletion(config, prompt, additionalMessages, temperature, logger, signal);
543
+ return callOpenAiCompletion(config, prompt, additionalMessages, effectiveTemperature, logger, signal, resolvedThinking);
541
544
  case 'anthropic':
542
- return callAnthropicCompletion(config, prompt, additionalMessages, temperature, logger, tools, signal);
545
+ return callAnthropicCompletion(config, prompt, additionalMessages, effectiveTemperature, logger, tools, signal, resolvedThinking);
543
546
  case 'gemini':
544
- return callGeminiCompletion(config, prompt, additionalMessages, temperature, logger, tools, signal);
547
+ return callGeminiCompletion(config, prompt, additionalMessages, effectiveTemperature, logger, tools, signal, resolvedThinking);
545
548
  /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */
546
549
  default: {
547
550
  const _exhaustive = descriptor.apiFormat;
@@ -592,7 +595,13 @@ const proxiedImageGenerationResponse = ts_utils_1.Validators.object({
592
595
  });
593
596
  const proxiedListModelsEntry = ts_utils_1.Validators.object({
594
597
  id: ts_utils_1.Validators.string,
595
- capabilities: ts_utils_1.Validators.arrayOf(ts_utils_1.Validators.enumeratedValue(['chat', 'tools', 'vision', 'image-generation'])),
598
+ capabilities: ts_utils_1.Validators.arrayOf(ts_utils_1.Validators.enumeratedValue([
599
+ 'chat',
600
+ 'tools',
601
+ 'vision',
602
+ 'image-generation',
603
+ 'thinking'
604
+ ])),
596
605
  displayName: ts_utils_1.Validators.string.optional()
597
606
  });
598
607
  const proxiedListModelsResponse = ts_utils_1.Validators.object({
@@ -601,101 +610,160 @@ const proxiedListModelsResponse = ts_utils_1.Validators.object({
601
610
  // ============================================================================
602
611
  // Image generation — adapters
603
612
  // ============================================================================
604
- /**
605
- * Calls the OpenAI Images API. Used for both `openai-images` and `xai-images`
606
- * formats — the request shape is the same; the only difference is whether the
607
- * `size` field is honored (OpenAI: yes, xAI: ignored at the provider).
608
- *
609
- * When `request.referenceImages` is non-empty, routes to `/images/edits`
610
- * (multipart) instead of `/images/generations` (JSON). Per-model edit support
611
- * is not validated here (e.g. dall-e-3 does not support edits) — the
612
- * provider's 400 surfaces through the failure path.
613
- *
614
- * @internal
615
- */
616
- async function callOpenAiImageGeneration(config, request, defaultMimeType, logger, signal) {
617
- var _a, _b, _c;
618
- const opts = (_a = request.options) !== null && _a !== void 0 ? _a : {};
619
- const refs = (_b = request.referenceImages) !== null && _b !== void 0 ? _b : [];
613
+ /** Routes to /images/generations or /images/edits; handles outputParamStyle. @internal */
614
+ async function callOpenAiImageGeneration(config, request, capability, resolved, logger, signal) {
615
+ var _a, _b;
616
+ const refs = (_a = request.referenceImages) !== null && _a !== void 0 ? _a : [];
620
617
  const headers = (0, endpoint_1.bearerAuthHeader)(config.apiKey);
621
- const n = (_c = opts.count) !== null && _c !== void 0 ? _c : 1;
618
+ const effectiveMimeType = resolved.outputFormat !== undefined
619
+ ? `image/${resolved.outputFormat}`
620
+ : (_b = capability.defaultOutputMimeType) !== null && _b !== void 0 ? _b : 'image/png';
622
621
  const fetched = refs.length > 0
623
- ? await callOpenAiImagesEdits(config, request, headers, n, refs, logger, signal)
624
- : await callOpenAiImagesGenerations(config, request, headers, n, logger, signal);
622
+ ? await callOpenAiImagesEdits(config, capability, request, headers, resolved, logger, signal)
623
+ : await callOpenAiImagesGenerations(config, request, headers, resolved, capability, logger, signal);
625
624
  return fetched.onSuccess((json) => openAiImageResponse
626
625
  .validate(json)
627
626
  .withErrorFormat((msg) => `OpenAI images API response: ${msg}`)
628
627
  .onSuccess((response) => (0, ts_utils_1.succeed)({
629
- images: response.data.map((item) => (Object.assign({ mimeType: defaultMimeType, base64: item.b64_json }, (item.revised_prompt !== undefined ? { revisedPrompt: item.revised_prompt } : {}))))
628
+ images: response.data.map((item) => (Object.assign({ mimeType: effectiveMimeType, base64: item.b64_json }, (item.revised_prompt !== undefined ? { revisedPrompt: item.revised_prompt } : {}))))
630
629
  })));
631
630
  }
632
- /**
633
- * Builds and posts the JSON `/images/generations` request (no refs).
634
- * @internal
635
- */
636
- function callOpenAiImagesGenerations(config, request, headers, n, logger, signal) {
631
+ /** Builds the JSON /images/generations request; handles outputParamStyle. @internal */
632
+ function callOpenAiImagesGenerations(config, request, headers, resolved, capability, logger, signal) {
637
633
  var _a;
638
- const opts = (_a = request.options) !== null && _a !== void 0 ? _a : {};
639
634
  const body = {
640
635
  model: config.model,
641
636
  prompt: request.prompt,
642
- n,
643
- response_format: 'b64_json'
637
+ n: resolved.n
644
638
  };
645
- if (opts.size !== undefined) {
646
- body.size = opts.size;
639
+ // Output format param — conditional on model capability
640
+ if (capability.outputParamStyle === 'response-format') {
641
+ body.response_format = 'b64_json';
642
+ }
643
+ else if (capability.outputParamStyle === 'output-format') {
644
+ body.output_format = (_a = resolved.outputFormat) !== null && _a !== void 0 ? _a : 'png';
645
+ }
646
+ if (resolved.size !== undefined) {
647
+ body.size = resolved.size;
648
+ }
649
+ if (capability.supportsQualityParam && resolved.quality !== undefined) {
650
+ body.quality = resolved.quality;
647
651
  }
648
- if (opts.quality !== undefined) {
649
- body.quality = opts.quality;
652
+ if (resolved.seed !== undefined) {
653
+ body.seed = resolved.seed;
650
654
  }
651
- if (opts.seed !== undefined) {
652
- body.seed = opts.seed;
655
+ if (resolved.style !== undefined) {
656
+ body.style = resolved.style;
657
+ }
658
+ if (resolved.background !== undefined) {
659
+ body.background = resolved.background;
660
+ }
661
+ if (resolved.moderation !== undefined) {
662
+ body.moderation = resolved.moderation;
663
+ }
664
+ if (resolved.outputCompression !== undefined) {
665
+ body.output_compression = resolved.outputCompression;
666
+ }
667
+ if (resolved.otherParams !== undefined) {
668
+ Object.assign(body, resolved.otherParams);
653
669
  }
654
670
  /* c8 ignore next 1 - optional logger */
655
- logger === null || logger === void 0 ? void 0 : logger.info(`Image generation: model=${config.model}, n=${n}`);
671
+ logger === null || logger === void 0 ? void 0 : logger.info(`Image generation: model=${config.model}, n=${resolved.n}`);
656
672
  return fetchJson(`${config.baseUrl}/images/generations`, headers, body, logger, signal);
657
673
  }
658
- /**
659
- * Builds and posts the multipart `/images/edits` request (with refs).
660
- * @internal
661
- */
662
- async function callOpenAiImagesEdits(config, request, headers, n, refs, logger, signal) {
663
- var _a;
674
+ /** Builds the multipart /images/edits request with ref images. @internal */
675
+ async function callOpenAiImagesEdits(config, capability, request, headers, resolved, logger, signal) {
676
+ const refs = request.referenceImages; // callers verify refs.length > 0 before calling this function
664
677
  const blobsResult = (0, ts_utils_1.mapResults)(refs.map((ref, i) => attachmentToBlob(ref).withErrorFormat((msg) => `reference image ${i}: ${msg}`)));
665
678
  /* c8 ignore next 3 - decode failure unreachable via Node's Buffer.from (silently strips invalid input) */
666
679
  if (blobsResult.isFailure()) {
667
680
  return (0, ts_utils_1.fail)(blobsResult.message);
668
681
  }
669
- const opts = (_a = request.options) !== null && _a !== void 0 ? _a : {};
670
682
  const form = new FormData();
671
683
  form.append('model', config.model);
672
684
  form.append('prompt', request.prompt);
673
- form.append('n', String(n));
674
- form.append('response_format', 'b64_json');
675
- if (opts.size !== undefined) {
676
- form.append('size', opts.size);
685
+ form.append('n', String(resolved.n));
686
+ if (capability.outputParamStyle !== 'output-format') {
687
+ form.append('response_format', 'b64_json');
677
688
  }
678
- if (opts.quality !== undefined) {
679
- form.append('quality', opts.quality);
680
- }
681
- if (opts.seed !== undefined) {
682
- form.append('seed', String(opts.seed));
689
+ if (resolved.size !== undefined) {
690
+ form.append('size', resolved.size);
683
691
  }
684
692
  blobsResult.value.forEach((blob, i) => {
685
693
  form.append('image[]', blob, `ref-${i}.${extensionForMimeType(refs[i].mimeType)}`);
686
694
  });
687
695
  /* c8 ignore next 1 - optional logger */
688
- logger === null || logger === void 0 ? void 0 : logger.info(`Image edit: model=${config.model}, n=${n}, refs=${refs.length}`);
696
+ logger === null || logger === void 0 ? void 0 : logger.info(`Image edit: model=${config.model}, n=${resolved.n}, refs=${refs.length}`);
689
697
  return fetchMultipart(`${config.baseUrl}/images/edits`, headers, form, logger, signal);
690
698
  }
691
- /**
692
- * Calls Gemini's chat-style `:generateContent` endpoint for image output
693
- * (Gemini 2.5 Flash Image / "Nano Banana"). Accepts reference images, which
694
- * are passed as `inlineData` parts alongside the text prompt.
695
- *
696
- * @internal
697
- */
698
- async function callGeminiImageOutGeneration(config, request, logger, signal) {
699
+ /** Calls xAI /images/edits with JSON body (not multipart); up to 3 source images. @internal */
700
+ async function callXaiImagesEdits(config, request, resolved, logger, signal) {
701
+ var _a;
702
+ /* c8 ignore next 1 - defensive: referenceImages always defined when this function is called */
703
+ const refs = (_a = request.referenceImages) !== null && _a !== void 0 ? _a : [];
704
+ if (refs.length > 3) {
705
+ return (0, ts_utils_1.fail)(`xAI image edits supports at most 3 reference images; got ${refs.length}`);
706
+ }
707
+ const images = refs.map((ref) => ({
708
+ type: 'image_url',
709
+ url: `data:${ref.mimeType};base64,${ref.base64}`
710
+ }));
711
+ const body = {
712
+ model: config.model,
713
+ prompt: request.prompt,
714
+ n: resolved.n,
715
+ response_format: 'b64_json',
716
+ image: images
717
+ };
718
+ if (resolved.aspectRatio !== undefined) {
719
+ body.aspect_ratio = resolved.aspectRatio;
720
+ }
721
+ if (resolved.resolution !== undefined) {
722
+ body.resolution = resolved.resolution;
723
+ }
724
+ if (resolved.otherParams !== undefined) {
725
+ Object.assign(body, resolved.otherParams);
726
+ }
727
+ /* c8 ignore next 1 - optional logger */
728
+ logger === null || logger === void 0 ? void 0 : logger.info(`xAI image edit: model=${config.model}, n=${resolved.n}, refs=${refs.length}`);
729
+ return fetchJson(`${config.baseUrl}/images/edits`, (0, endpoint_1.bearerAuthHeader)(config.apiKey), body, logger, signal);
730
+ }
731
+ /** Calls xAI /images/generations; uses aspect_ratio instead of size. @internal */
732
+ async function callXaiImageGeneration(config, request, capability, resolved, logger, signal) {
733
+ const headers = (0, endpoint_1.bearerAuthHeader)(config.apiKey);
734
+ const body = {
735
+ model: config.model,
736
+ prompt: request.prompt,
737
+ n: resolved.n,
738
+ response_format: 'b64_json'
739
+ };
740
+ if (resolved.aspectRatio !== undefined) {
741
+ body.aspect_ratio = resolved.aspectRatio;
742
+ }
743
+ if (resolved.resolution !== undefined) {
744
+ body.resolution = resolved.resolution;
745
+ }
746
+ if (resolved.otherParams !== undefined) {
747
+ Object.assign(body, resolved.otherParams);
748
+ }
749
+ /* c8 ignore next 1 - optional logger */
750
+ logger === null || logger === void 0 ? void 0 : logger.info(`xAI image generation: model=${config.model}, n=${resolved.n}`);
751
+ const fetched = await fetchJson(`${config.baseUrl}/images/generations`, headers, body, logger, signal);
752
+ return fetched.onSuccess((json) => openAiImageResponse
753
+ .validate(json)
754
+ .withErrorFormat((msg) => `xAI images API response: ${msg}`)
755
+ .onSuccess((response) => (0, ts_utils_1.succeed)({
756
+ images: response.data.map((item) => {
757
+ var _a;
758
+ return ({
759
+ mimeType: (_a = capability.defaultOutputMimeType) !== null && _a !== void 0 ? _a : 'image/jpeg',
760
+ base64: item.b64_json
761
+ });
762
+ })
763
+ })));
764
+ }
765
+ /** Calls Gemini :generateContent for image output; accepts ref images as inlineData. @internal */
766
+ async function callGeminiImageOutGeneration(config, request, resolved, logger, signal) {
699
767
  var _a;
700
768
  const url = `${config.baseUrl}/models/${config.model}:generateContent`;
701
769
  const refs = (_a = request.referenceImages) !== null && _a !== void 0 ? _a : [];
@@ -703,9 +771,17 @@ async function callGeminiImageOutGeneration(config, request, logger, signal) {
703
771
  for (const ref of refs) {
704
772
  parts.push({ inlineData: { mimeType: ref.mimeType, data: ref.base64 } });
705
773
  }
706
- const body = {
707
- contents: [{ role: 'user', parts }]
708
- };
774
+ const generationConfig = {};
775
+ if (resolved.geminiAspectRatio !== undefined) {
776
+ generationConfig.imageConfig = { aspectRatio: resolved.geminiAspectRatio };
777
+ }
778
+ if (resolved.otherParams !== undefined) {
779
+ Object.assign(generationConfig, resolved.otherParams);
780
+ }
781
+ const body = { contents: [{ role: 'user', parts }] };
782
+ if (Object.keys(generationConfig).length > 0) {
783
+ body.generationConfig = generationConfig;
784
+ }
709
785
  const headers = {
710
786
  'x-goog-api-key': config.apiKey
711
787
  };
@@ -732,33 +808,48 @@ async function callGeminiImageOutGeneration(config, request, logger, signal) {
732
808
  return (0, ts_utils_1.succeed)({ images });
733
809
  }));
734
810
  }
735
- /**
736
- * Calls the Gemini Imagen `:predict` endpoint.
737
- * @internal
738
- */
739
- async function callImagenGeneration(config, request, logger, signal) {
740
- var _a, _b, _c, _d;
811
+ /** Calls the Gemini Imagen :predict endpoint with Imagen 4 params. @internal */
812
+ async function callImagenGeneration(config, request, resolved, logger, signal) {
741
813
  const url = `${config.baseUrl}/models/${config.model}:predict`;
742
- const opts = (_a = request.options) !== null && _a !== void 0 ? _a : {};
743
814
  const parameters = {
744
- sampleCount: (_b = opts.count) !== null && _b !== void 0 ? _b : 1
815
+ sampleCount: resolved.n
745
816
  };
746
- if (((_c = opts.imagen) === null || _c === void 0 ? void 0 : _c.aspectRatio) !== undefined) {
747
- parameters.aspectRatio = opts.imagen.aspectRatio;
817
+ if (resolved.imagenAspectRatio !== undefined) {
818
+ parameters.aspectRatio = resolved.imagenAspectRatio;
748
819
  }
749
- if (((_d = opts.imagen) === null || _d === void 0 ? void 0 : _d.negativePrompt) !== undefined) {
750
- parameters.negativePrompt = opts.imagen.negativePrompt;
820
+ if (resolved.imageSize !== undefined) {
821
+ parameters.imageSize = resolved.imageSize;
751
822
  }
752
- if (opts.seed !== undefined) {
753
- parameters.seed = opts.seed;
823
+ if (resolved.addWatermark !== undefined) {
824
+ parameters.addWatermark = resolved.addWatermark;
825
+ }
826
+ if (resolved.enhancePrompt !== undefined) {
827
+ parameters.enhancePrompt = resolved.enhancePrompt;
828
+ }
829
+ if (resolved.imagenOutputMimeType !== undefined || resolved.imagenOutputCompressionQuality !== undefined) {
830
+ const outputOptions = {};
831
+ if (resolved.imagenOutputMimeType !== undefined) {
832
+ outputOptions.mimeType = resolved.imagenOutputMimeType;
833
+ }
834
+ if (resolved.imagenOutputCompressionQuality !== undefined) {
835
+ outputOptions.compressionQuality = resolved.imagenOutputCompressionQuality;
836
+ }
837
+ parameters.outputOptions = outputOptions;
838
+ }
839
+ if (resolved.personGeneration !== undefined) {
840
+ parameters.personGeneration = resolved.personGeneration;
841
+ }
842
+ if (resolved.seed !== undefined) {
843
+ parameters.seed = resolved.seed;
844
+ }
845
+ if (resolved.otherParams !== undefined) {
846
+ Object.assign(parameters, resolved.otherParams);
754
847
  }
755
848
  const body = {
756
849
  instances: [{ prompt: request.prompt }],
757
850
  parameters
758
851
  };
759
- const headers = {
760
- 'x-goog-api-key': config.apiKey
761
- };
852
+ const headers = { 'x-goog-api-key': config.apiKey };
762
853
  /* c8 ignore next 1 - optional logger */
763
854
  logger === null || logger === void 0 ? void 0 : logger.info(`Imagen generation: model=${config.model}, n=${parameters.sampleCount}`);
764
855
  const jsonResult = await fetchJson(url, headers, body, logger, signal);
@@ -784,25 +875,16 @@ async function callImagenGeneration(config, request, logger, signal) {
784
875
  // ============================================================================
785
876
  /**
786
877
  * Calls the appropriate image-generation API for a given provider.
787
- *
788
- * Resolves a {@link IAiImageModelCapability} from
789
- * {@link IAiProviderDescriptor.imageGeneration} for the requested model and
790
- * routes by its `format`:
791
- * - `'openai-images'` for OpenAI (DALL-E, gpt-image-1)
792
- * - `'xai-images'` for xAI Grok image models
793
- * - `'gemini-imagen'` for Google Imagen `:predict`
794
- * - `'gemini-image-out'` for Gemini chat-style image output (Nano Banana)
795
- *
796
- * Image-model selection reuses the existing `'image'` {@link ModelSpecKey}.
797
- * When `request.referenceImages` is non-empty, the call is rejected up front
798
- * unless the resolved capability declares `acceptsImageReferenceInput`.
799
- *
878
+ * Routes by the `format` field of the resolved {@link IAiImageModelCapability}:
879
+ * `'openai-images'`, `'xai-images'`, `'xai-images-edits'`, `'gemini-imagen'`,
880
+ * or `'gemini-image-out'`. Rejects up front if `referenceImages` is set but the
881
+ * capability does not declare `acceptsImageReferenceInput`.
800
882
  * @param params - Request parameters including descriptor, API key, and prompt
801
883
  * @returns The generated images, or a failure
802
884
  * @public
803
885
  */
804
886
  async function callProviderImageGeneration(params) {
805
- var _a, _b;
887
+ var _a, _b, _c;
806
888
  const { descriptor, apiKey, params: request, modelOverride, logger, signal, endpoint } = params;
807
889
  if (!(0, registry_1.supportsImageGeneration)(descriptor)) {
808
890
  return (0, ts_utils_1.fail)(`provider "${descriptor.id}" does not support image generation`);
@@ -824,6 +906,11 @@ async function callProviderImageGeneration(params) {
824
906
  if (((_b = (_a = request.referenceImages) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 0 && !capability.acceptsImageReferenceInput) {
825
907
  return (0, ts_utils_1.fail)(`model "${model}" does not support reference images`);
826
908
  }
909
+ const resolved = (0, imageOptionsResolver_1.resolveImageOptions)(model, capability, request.options);
910
+ const validationResult = (0, imageOptionsResolver_1.validateResolvedOptions)(model, capability, resolved);
911
+ if (validationResult.isFailure()) {
912
+ return (0, ts_utils_1.fail)(validationResult.message);
913
+ }
827
914
  const config = {
828
915
  baseUrl: baseUrlResult.value,
829
916
  apiKey,
@@ -836,13 +923,32 @@ async function callProviderImageGeneration(params) {
836
923
  }
837
924
  switch (capability.format) {
838
925
  case 'openai-images':
839
- return callOpenAiImageGeneration(config, request, 'image/png', logger, signal);
926
+ return callOpenAiImageGeneration(config, request, capability, resolved, logger, signal);
840
927
  case 'xai-images':
841
- return callOpenAiImageGeneration(config, request, 'image/jpeg', logger, signal);
928
+ return callXaiImageGeneration(config, request, capability, resolved, logger, signal);
929
+ case 'xai-images-edits': {
930
+ const refs = (_c = request.referenceImages) !== null && _c !== void 0 ? _c : [];
931
+ if (refs.length > 0) {
932
+ const editsResult = await callXaiImagesEdits(config, request, resolved, logger, signal);
933
+ return editsResult.onSuccess((json) => openAiImageResponse
934
+ .validate(json)
935
+ .withErrorFormat((msg) => `xAI images API response: ${msg}`)
936
+ .onSuccess((response) => (0, ts_utils_1.succeed)({
937
+ images: response.data.map((item) => {
938
+ var _a;
939
+ return ({
940
+ mimeType: (_a = capability.defaultOutputMimeType) !== null && _a !== void 0 ? _a : 'image/jpeg',
941
+ base64: item.b64_json
942
+ });
943
+ })
944
+ })));
945
+ }
946
+ return callXaiImageGeneration(config, request, capability, resolved, logger, signal);
947
+ }
842
948
  case 'gemini-imagen':
843
- return callImagenGeneration(config, request, logger, signal);
949
+ return callImagenGeneration(config, request, resolved, logger, signal);
844
950
  case 'gemini-image-out':
845
- return callGeminiImageOutGeneration(config, request, logger, signal);
951
+ return callGeminiImageOutGeneration(config, request, resolved, logger, signal);
846
952
  /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */
847
953
  default: {
848
954
  const _exhaustive = capability.format;
@@ -896,6 +1002,7 @@ function geminiMethodsToCapabilities(methods) {
896
1002
  * @internal
897
1003
  */
898
1004
  function geminiBareId(name) {
1005
+ /* c8 ignore next 1 - defensive: Gemini API always returns names prefixed with 'models/' */
899
1006
  return name.startsWith('models/') ? name.substring('models/'.length) : name;
900
1007
  }
901
1008
  /**
@@ -1028,12 +1135,8 @@ async function callGeminiListModels(config, providerId, capabilityConfig, logger
1028
1135
  // List models — dispatcher
1029
1136
  // ============================================================================
1030
1137
  /**
1031
- * Lists models available from a provider, with capabilities resolved from
1032
- * native provider info (where supplied) and a configurable rule set.
1033
- *
1034
- * Routes based on `descriptor.apiFormat` — listing reuses the existing
1035
- * format dispatch and does not require a separate descriptor field.
1036
- *
1138
+ * Lists models available from a provider, routing by `descriptor.apiFormat`.
1139
+ * Capabilities are resolved from native provider info and a configurable rule set.
1037
1140
  * @param params - Request parameters including descriptor, API key, and optional capability filter
1038
1141
  * @returns The resolved model list, or a failure
1039
1142
  * @public
@@ -1080,18 +1183,9 @@ async function callProviderListModels(params) {
1080
1183
  // ============================================================================
1081
1184
  /**
1082
1185
  * Calls the model-listing endpoint on a proxy server.
1083
- *
1084
- * @remarks
1085
- * Proxy contract:
1086
- * - Endpoint: `POST ${proxyUrl}/api/ai/list-models`
1087
- * - Request body: `{providerId, apiKey, capability?}`. Capability config is
1088
- * not forwarded — the proxy applies its own (typically the same default
1089
- * the library ships).
1090
- * - Success response body: an `IAiModelInfo[]` (under key `models`) where
1091
- * `capabilities` is serialized as a string array (not Set, which doesn't
1092
- * round-trip through JSON).
1093
- * - Error response body: `{error: string}`, surfaced as `proxy: ${error}`.
1094
- *
1186
+ * Endpoint: `POST ${proxyUrl}/api/ai/list-models`. Capability config is not
1187
+ * forwarded. `capabilities` is serialized as a string array. Error body
1188
+ * `{error: string}` is surfaced as `proxy: ${error}`.
1095
1189
  * @public
1096
1190
  */
1097
1191
  async function callProxiedListModels(proxyUrl, params) {
@@ -1139,7 +1233,7 @@ async function callProxiedListModels(proxyUrl, params) {
1139
1233
  * @public
1140
1234
  */
1141
1235
  async function callProxiedCompletion(proxyUrl, params) {
1142
- const { descriptor, apiKey, prompt, additionalMessages, temperature, modelOverride, logger, tools, signal } = params;
1236
+ const { descriptor, apiKey, prompt, additionalMessages, temperature, modelOverride, logger, tools, signal, thinking } = params;
1143
1237
  const promptBody = { system: prompt.system, user: prompt.user };
1144
1238
  if (prompt.attachments.length > 0) {
1145
1239
  promptBody.attachments = prompt.attachments;
@@ -1159,6 +1253,9 @@ async function callProxiedCompletion(proxyUrl, params) {
1159
1253
  if (tools && tools.length > 0) {
1160
1254
  body.tools = tools;
1161
1255
  }
1256
+ if (thinking !== undefined) {
1257
+ body.thinking = thinking;
1258
+ }
1162
1259
  /* c8 ignore next 1 - optional logger */
1163
1260
  logger === null || logger === void 0 ? void 0 : logger.info(`AI proxy request: provider=${descriptor.id}, proxy=${proxyUrl}`);
1164
1261
  const url = `${proxyUrl}/api/ai/completion`;
@@ -1166,7 +1263,6 @@ async function callProxiedCompletion(proxyUrl, params) {
1166
1263
  if (jsonResult.isFailure()) {
1167
1264
  return (0, ts_utils_1.fail)(jsonResult.message);
1168
1265
  }
1169
- // Check for error response from proxy
1170
1266
  const response = jsonResult.value;
1171
1267
  if (typeof response.error === 'string') {
1172
1268
  return (0, ts_utils_1.fail)(`proxy: ${response.error}`);
@@ -1185,20 +1281,11 @@ async function callProxiedCompletion(proxyUrl, params) {
1185
1281
  /**
1186
1282
  * Calls the image-generation endpoint on a proxy server instead of calling
1187
1283
  * the provider API directly from the browser.
1188
- *
1189
- * @remarks
1190
- * The proxy contract:
1191
- * - Endpoint: `POST ${proxyUrl}/api/ai/image-generation`
1192
- * - Request body: `{providerId, apiKey, params, modelOverride?}`
1193
- * - Success response body: an {@link IAiImageGenerationResponse}
1194
- * - Error response body: `{error: string}` (surfaced as `proxy: ${error}`)
1195
- *
1196
- * The proxy server is responsible for descriptor lookup, model resolution,
1197
- * provider dispatch, and response normalization. When `params.referenceImages`
1198
- * is present, the proxy is also responsible for repackaging it into the
1199
- * upstream wire format (e.g. multipart/form-data for OpenAI `/images/edits`,
1200
- * `inlineData` parts for Gemini `:generateContent`).
1201
- *
1284
+ * Endpoint: `POST ${proxyUrl}/api/ai/image-generation`. Request body:
1285
+ * `{providerId, apiKey, params, modelOverride?}`. The proxy handles descriptor
1286
+ * lookup, model resolution, provider dispatch, and response normalization
1287
+ * (including repackaging `referenceImages` for the upstream wire format).
1288
+ * Error body `{error: string}` is surfaced as `proxy: ${error}`.
1202
1289
  * @param proxyUrl - Base URL of the proxy server (e.g. `http://localhost:3001`)
1203
1290
  * @param params - Same parameters as {@link callProviderImageGeneration}
1204
1291
  * @returns The generated images, or a failure