@fgv/ts-extras 5.1.0-36 → 5.1.0-39

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 (128) hide show
  1. package/dist/packlets/ai-assist/apiClient.js +66 -111
  2. package/dist/packlets/ai-assist/apiClient.js.map +1 -1
  3. package/dist/packlets/ai-assist/converters.js +16 -3
  4. package/dist/packlets/ai-assist/converters.js.map +1 -1
  5. package/dist/packlets/ai-assist/embeddingClient.js +5 -6
  6. package/dist/packlets/ai-assist/embeddingClient.js.map +1 -1
  7. package/dist/packlets/ai-assist/imageOptionsResolver.js +0 -19
  8. package/dist/packlets/ai-assist/imageOptionsResolver.js.map +1 -1
  9. package/dist/packlets/ai-assist/index.js +2 -2
  10. package/dist/packlets/ai-assist/index.js.map +1 -1
  11. package/dist/packlets/ai-assist/model.js +139 -7
  12. package/dist/packlets/ai-assist/model.js.map +1 -1
  13. package/dist/packlets/ai-assist/registry.js +58 -51
  14. package/dist/packlets/ai-assist/registry.js.map +1 -1
  15. package/dist/packlets/ai-assist/streamingAdapters/anthropic.js +38 -11
  16. package/dist/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
  17. package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +187 -36
  18. package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -1
  19. package/dist/packlets/ai-assist/streamingAdapters/common.js +13 -0
  20. package/dist/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
  21. package/dist/packlets/ai-assist/streamingAdapters/gemini.js +12 -4
  22. package/dist/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
  23. package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js +6 -1
  24. package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -1
  25. package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js +10 -3
  26. package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
  27. package/dist/packlets/ai-assist/streamingAdapters/proxy.js +5 -2
  28. package/dist/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -1
  29. package/dist/packlets/ai-assist/streamingClient.js +16 -16
  30. package/dist/packlets/ai-assist/streamingClient.js.map +1 -1
  31. package/dist/packlets/crypto-utils/converters.js +18 -0
  32. package/dist/packlets/crypto-utils/converters.js.map +1 -1
  33. package/dist/packlets/crypto-utils/index.browser.js +2 -2
  34. package/dist/packlets/crypto-utils/index.browser.js.map +1 -1
  35. package/dist/packlets/crypto-utils/index.js +2 -2
  36. package/dist/packlets/crypto-utils/index.js.map +1 -1
  37. package/dist/packlets/crypto-utils/keystore/keyStore.js +19 -4
  38. package/dist/packlets/crypto-utils/keystore/keyStore.js.map +1 -1
  39. package/dist/packlets/crypto-utils/keystore/model.js.map +1 -1
  40. package/dist/packlets/crypto-utils/model.js.map +1 -1
  41. package/dist/packlets/crypto-utils/spkiHelpers.js +104 -25
  42. package/dist/packlets/crypto-utils/spkiHelpers.js.map +1 -1
  43. package/dist/ts-extras.d.ts +377 -132
  44. package/dist/tsdoc-metadata.json +1 -1
  45. package/lib/packlets/ai-assist/apiClient.d.ts +12 -2
  46. package/lib/packlets/ai-assist/apiClient.d.ts.map +1 -1
  47. package/lib/packlets/ai-assist/apiClient.js +65 -110
  48. package/lib/packlets/ai-assist/apiClient.js.map +1 -1
  49. package/lib/packlets/ai-assist/converters.d.ts +8 -2
  50. package/lib/packlets/ai-assist/converters.d.ts.map +1 -1
  51. package/lib/packlets/ai-assist/converters.js +17 -4
  52. package/lib/packlets/ai-assist/converters.js.map +1 -1
  53. package/lib/packlets/ai-assist/embeddingClient.d.ts.map +1 -1
  54. package/lib/packlets/ai-assist/embeddingClient.js +4 -5
  55. package/lib/packlets/ai-assist/embeddingClient.js.map +1 -1
  56. package/lib/packlets/ai-assist/imageOptionsResolver.d.ts +0 -8
  57. package/lib/packlets/ai-assist/imageOptionsResolver.d.ts.map +1 -1
  58. package/lib/packlets/ai-assist/imageOptionsResolver.js +0 -19
  59. package/lib/packlets/ai-assist/imageOptionsResolver.js.map +1 -1
  60. package/lib/packlets/ai-assist/index.d.ts +3 -3
  61. package/lib/packlets/ai-assist/index.d.ts.map +1 -1
  62. package/lib/packlets/ai-assist/index.js +6 -1
  63. package/lib/packlets/ai-assist/index.js.map +1 -1
  64. package/lib/packlets/ai-assist/model.d.ts +181 -98
  65. package/lib/packlets/ai-assist/model.d.ts.map +1 -1
  66. package/lib/packlets/ai-assist/model.js +143 -8
  67. package/lib/packlets/ai-assist/model.js.map +1 -1
  68. package/lib/packlets/ai-assist/registry.d.ts.map +1 -1
  69. package/lib/packlets/ai-assist/registry.js +58 -51
  70. package/lib/packlets/ai-assist/registry.js.map +1 -1
  71. package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts +1 -1
  72. package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts.map +1 -1
  73. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js +37 -10
  74. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
  75. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts +82 -3
  76. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts.map +1 -1
  77. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +187 -35
  78. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -1
  79. package/lib/packlets/ai-assist/streamingAdapters/common.d.ts +24 -1
  80. package/lib/packlets/ai-assist/streamingAdapters/common.d.ts.map +1 -1
  81. package/lib/packlets/ai-assist/streamingAdapters/common.js +14 -1
  82. package/lib/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
  83. package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts +10 -1
  84. package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts.map +1 -1
  85. package/lib/packlets/ai-assist/streamingAdapters/gemini.js +12 -4
  86. package/lib/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
  87. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts +1 -1
  88. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts.map +1 -1
  89. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js +6 -1
  90. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -1
  91. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts +1 -1
  92. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts.map +1 -1
  93. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js +10 -3
  94. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
  95. package/lib/packlets/ai-assist/streamingAdapters/proxy.d.ts.map +1 -1
  96. package/lib/packlets/ai-assist/streamingAdapters/proxy.js +5 -2
  97. package/lib/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -1
  98. package/lib/packlets/ai-assist/streamingClient.d.ts +1 -1
  99. package/lib/packlets/ai-assist/streamingClient.d.ts.map +1 -1
  100. package/lib/packlets/ai-assist/streamingClient.js +15 -15
  101. package/lib/packlets/ai-assist/streamingClient.js.map +1 -1
  102. package/lib/packlets/crypto-utils/converters.d.ts +9 -1
  103. package/lib/packlets/crypto-utils/converters.d.ts.map +1 -1
  104. package/lib/packlets/crypto-utils/converters.js +19 -1
  105. package/lib/packlets/crypto-utils/converters.js.map +1 -1
  106. package/lib/packlets/crypto-utils/index.browser.d.ts +1 -1
  107. package/lib/packlets/crypto-utils/index.browser.d.ts.map +1 -1
  108. package/lib/packlets/crypto-utils/index.browser.js +6 -2
  109. package/lib/packlets/crypto-utils/index.browser.js.map +1 -1
  110. package/lib/packlets/crypto-utils/index.d.ts +1 -1
  111. package/lib/packlets/crypto-utils/index.d.ts.map +1 -1
  112. package/lib/packlets/crypto-utils/index.js +6 -2
  113. package/lib/packlets/crypto-utils/index.js.map +1 -1
  114. package/lib/packlets/crypto-utils/keystore/keyStore.d.ts +9 -2
  115. package/lib/packlets/crypto-utils/keystore/keyStore.d.ts.map +1 -1
  116. package/lib/packlets/crypto-utils/keystore/keyStore.js +19 -4
  117. package/lib/packlets/crypto-utils/keystore/keyStore.js.map +1 -1
  118. package/lib/packlets/crypto-utils/keystore/model.d.ts +7 -0
  119. package/lib/packlets/crypto-utils/keystore/model.d.ts.map +1 -1
  120. package/lib/packlets/crypto-utils/keystore/model.js.map +1 -1
  121. package/lib/packlets/crypto-utils/model.d.ts +11 -1
  122. package/lib/packlets/crypto-utils/model.d.ts.map +1 -1
  123. package/lib/packlets/crypto-utils/model.js.map +1 -1
  124. package/lib/packlets/crypto-utils/spkiHelpers.d.ts +59 -2
  125. package/lib/packlets/crypto-utils/spkiHelpers.d.ts.map +1 -1
  126. package/lib/packlets/crypto-utils/spkiHelpers.js +108 -25
  127. package/lib/packlets/crypto-utils/spkiHelpers.js.map +1 -1
  128. package/package.json +7 -7
@@ -28,7 +28,7 @@
28
28
  */
29
29
  import { isJsonObject } from '@fgv/ts-json-base';
30
30
  import { fail, mapResults, succeed, Validators } from '@fgv/ts-utils';
31
- import { allModelCapabilities, resolveModel } from './model';
31
+ import { allModelCapabilities, isResponsesOnlyModel, resolveProviderModel } from './model';
32
32
  import { anthropicEffortToBudgetTokens, checkTemperatureConflict, mergeThinkingConfig, providerDiscriminatorForId } from './thinkingOptionsResolver';
33
33
  import { buildAnthropicMessages, buildGeminiContents, buildMessages, buildOpenAiChatUserContent, buildOpenAiResponsesUserContent, normalizeOutboundMessages, splitChatRequest } from './chatRequestBuilders';
34
34
  import { bearerAuthHeader, resolveEffectiveBaseUrl } from './endpoint';
@@ -221,14 +221,14 @@ const geminiResponse = Validators.object({
221
221
  * Works for xAI Grok, OpenAI, Groq, and Mistral.
222
222
  * @internal
223
223
  */
224
- async function callOpenAiCompletion(config, prompt, head, temperature = 0.7, logger, signal, resolvedThinking) {
224
+ async function callOpenAiCompletion(config, prompt, head, temperature, logger, signal, resolvedThinking) {
225
225
  var _a;
226
226
  const url = `${config.baseUrl}/chat/completions`;
227
227
  const messages = buildMessages(prompt.system, buildOpenAiChatUserContent(prompt), {
228
228
  head
229
229
  });
230
230
  const effort = (_a = resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.openAiEffort) !== null && _a !== void 0 ? _a : resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.xaiEffort;
231
- const body = Object.assign(Object.assign({ model: config.model, messages }, (effort === undefined || effort === 'none' ? { temperature } : {})), (effort !== undefined && config.model !== 'grok-4' ? { reasoning_effort: effort } : {}));
231
+ const body = Object.assign(Object.assign({ model: config.model, messages }, (temperature !== undefined ? { temperature } : {})), (effort !== undefined && config.model !== 'grok-4' ? { reasoning_effort: effort } : {}));
232
232
  if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.otherParams) !== undefined) {
233
233
  Object.assign(body, resolvedThinking.otherParams);
234
234
  }
@@ -274,14 +274,14 @@ function extractResponsesApiText(output) {
274
274
  * Used when tools are configured for an openai-format provider.
275
275
  * @internal
276
276
  */
277
- async function callOpenAiResponsesCompletion(config, prompt, tools, head, temperature = 0.7, logger, signal, resolvedThinking) {
277
+ async function callOpenAiResponsesCompletion(config, prompt, tools = [], head, temperature, logger, signal, resolvedThinking) {
278
278
  var _a;
279
279
  const url = `${config.baseUrl}/responses`;
280
280
  const input = buildMessages(prompt.system, buildOpenAiResponsesUserContent(prompt), {
281
281
  head
282
282
  });
283
283
  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;
284
- 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 } } : {}));
284
+ const body = Object.assign(Object.assign(Object.assign({ model: config.model, input }, (tools.length > 0 ? { tools: toResponsesApiTools(tools) } : {})), (temperature !== undefined ? { temperature } : {})), (effort !== undefined && config.model !== 'grok-4' ? { reasoning: { effort } } : {}));
285
285
  if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.otherParams) !== undefined) {
286
286
  Object.assign(body, resolvedThinking.otherParams);
287
287
  }
@@ -328,10 +328,10 @@ function extractAnthropicText(content) {
328
328
  return succeed(textParts.join(''));
329
329
  }
330
330
  /** Calls the Anthropic Messages API with optional tool support. @internal */
331
- async function callAnthropicCompletion(config, prompt, head, temperature = 0.7, logger, tools, signal, resolvedThinking) {
331
+ async function callAnthropicCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking) {
332
332
  const url = `${config.baseUrl}/messages`;
333
333
  const messages = buildAnthropicMessages(prompt, { head });
334
- 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 } : {}));
334
+ const body = Object.assign({ model: config.model, system: prompt.system, messages, max_tokens: 4096 }, (temperature !== undefined ? { temperature } : {}));
335
335
  const effort = resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.anthropicEffort;
336
336
  if (effort !== undefined) {
337
337
  body.thinking = { type: 'enabled', budget_tokens: anthropicEffortToBudgetTokens(effort) };
@@ -378,10 +378,14 @@ async function callAnthropicCompletion(config, prompt, head, temperature = 0.7,
378
378
  * When tools are configured, includes Google Search grounding.
379
379
  * @internal
380
380
  */
381
- async function callGeminiCompletion(config, prompt, head, temperature = 0.7, logger, tools, signal, resolvedThinking) {
381
+ async function callGeminiCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking) {
382
382
  const url = `${config.baseUrl}/models/${config.model}:generateContent`;
383
383
  const contents = buildGeminiContents(prompt, { head });
384
- const generationConfig = { temperature };
384
+ // Temperature is sent only when explicitly provided; otherwise Gemini's default applies.
385
+ const generationConfig = {};
386
+ if (temperature !== undefined) {
387
+ generationConfig.temperature = temperature;
388
+ }
385
389
  if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.geminiThinkingBudget) !== undefined) {
386
390
  generationConfig.thinkingConfig = { thinkingBudget: resolvedThinking.geminiThinkingBudget };
387
391
  }
@@ -430,8 +434,7 @@ async function callGeminiCompletion(config, prompt, head, temperature = 0.7, log
430
434
  * @public
431
435
  */
432
436
  export async function callProviderCompletion(params) {
433
- var _a;
434
- const { descriptor, apiKey, system, messages, temperature, modelOverride, logger, tools, signal, endpoint, thinking } = params;
437
+ const { descriptor, apiKey, system, messages, temperature, modelOverride, tier, logger, tools, signal, endpoint, thinking } = params;
435
438
  const splitResult = splitChatRequest(system, messages);
436
439
  if (splitResult.isFailure()) {
437
440
  return fail(splitResult.message);
@@ -446,14 +449,14 @@ export async function callProviderCompletion(params) {
446
449
  }
447
450
  const hasTools = tools !== undefined && tools.length > 0;
448
451
  const discriminator = providerDiscriminatorForId(descriptor.id);
449
- const hasThinkingConfig = discriminator !== undefined &&
450
- ((thinking === null || thinking === void 0 ? void 0 : thinking.effort) !== undefined ||
451
- ((_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);
452
- const modelContext = hasThinkingConfig ? 'thinking' : hasTools ? 'tools' : undefined;
453
- const model = resolveModel(modelOverride !== null && modelOverride !== void 0 ? modelOverride : descriptor.defaultModel, modelContext);
454
- if (model.length === 0) {
455
- return fail(`provider "${descriptor.id}": no model resolved; pass modelOverride or set descriptor.defaultModel`);
456
- }
452
+ // The quality tier is the only completion-model selector; thinking and tools
453
+ // are orthogonal request params/capabilities and never pick a model.
454
+ const modelContext = tier;
455
+ const modelResult = resolveProviderModel(descriptor, modelOverride, modelContext);
456
+ if (modelResult.isFailure()) {
457
+ return fail(modelResult.message);
458
+ }
459
+ const model = modelResult.value;
457
460
  let resolvedThinking;
458
461
  if (thinking !== undefined) {
459
462
  if (discriminator !== undefined) {
@@ -469,7 +472,6 @@ export async function callProviderCompletion(params) {
469
472
  }
470
473
  }
471
474
  }
472
- const effectiveTemperature = temperature !== null && temperature !== void 0 ? temperature : 0.7;
473
475
  const config = {
474
476
  baseUrl: baseUrlResult.value,
475
477
  apiKey,
@@ -484,14 +486,16 @@ export async function callProviderCompletion(params) {
484
486
  }
485
487
  switch (descriptor.apiFormat) {
486
488
  case 'openai':
487
- if (hasTools) {
488
- return callOpenAiResponsesCompletion(config, prompt, tools, head, effectiveTemperature, logger, signal, resolvedThinking);
489
+ // Responses-API-only models (e.g. gpt-5.5-pro) 400 on /chat/completions, so they route
490
+ // to the Responses path even with no tools requested same path the tools case uses.
491
+ if (hasTools || isResponsesOnlyModel(descriptor, config.model)) {
492
+ return callOpenAiResponsesCompletion(config, prompt, tools, head, temperature, logger, signal, resolvedThinking);
489
493
  }
490
- return callOpenAiCompletion(config, prompt, head, effectiveTemperature, logger, signal, resolvedThinking);
494
+ return callOpenAiCompletion(config, prompt, head, temperature, logger, signal, resolvedThinking);
491
495
  case 'anthropic':
492
- return callAnthropicCompletion(config, prompt, head, effectiveTemperature, logger, tools, signal, resolvedThinking);
496
+ return callAnthropicCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking);
493
497
  case 'gemini':
494
- return callGeminiCompletion(config, prompt, head, effectiveTemperature, logger, tools, signal, resolvedThinking);
498
+ return callGeminiCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking);
495
499
  /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */
496
500
  default: {
497
501
  const _exhaustive = descriptor.apiFormat;
@@ -506,13 +510,6 @@ const openAiImageItem = Validators.object({
506
510
  const openAiImageResponse = Validators.object({
507
511
  data: Validators.arrayOf(openAiImageItem).withConstraint((arr) => arr.length > 0)
508
512
  });
509
- const imagenPrediction = Validators.object({
510
- bytesBase64Encoded: Validators.string,
511
- mimeType: Validators.string.optional()
512
- });
513
- const imagenResponse = Validators.object({
514
- predictions: Validators.arrayOf(imagenPrediction).withConstraint((arr) => arr.length > 0)
515
- });
516
513
  const geminiImageInlineData = Validators.object({
517
514
  mimeType: Validators.string,
518
515
  data: Validators.string
@@ -522,11 +519,12 @@ const geminiImageOutPart = Validators.object({
522
519
  inlineData: geminiImageInlineData.optional()
523
520
  });
524
521
  const geminiImageOutContent = Validators.object({
525
- parts: Validators.arrayOf(geminiImageOutPart).withConstraint((arr) => arr.length > 0)
522
+ parts: Validators.arrayOf(geminiImageOutPart)
526
523
  });
527
524
  const geminiImageOutCandidate = Validators.object({
528
- content: geminiImageOutContent,
529
- finishReason: Validators.string.optional()
525
+ content: geminiImageOutContent.optional(),
526
+ finishReason: Validators.string.optional(),
527
+ finishMessage: Validators.string.optional()
530
528
  });
531
529
  const geminiImageOutResponse = Validators.object({
532
530
  candidates: Validators.arrayOf(geminiImageOutCandidate).withConstraint((arr) => arr.length > 0)
@@ -593,9 +591,6 @@ function callOpenAiImagesGenerations(config, request, headers, resolved, capabil
593
591
  if (resolved.seed !== undefined) {
594
592
  body.seed = resolved.seed;
595
593
  }
596
- if (resolved.style !== undefined) {
597
- body.style = resolved.style;
598
- }
599
594
  if (resolved.background !== undefined) {
600
595
  body.background = resolved.background;
601
596
  }
@@ -703,6 +698,14 @@ async function callXaiImageGeneration(config, request, capability, resolved, log
703
698
  })
704
699
  })));
705
700
  }
701
+ /**
702
+ * Gemini `finishReason` values that indicate a normal terminal completion rather
703
+ * than a refusal. `STOP` is set on every successful generation (and on completions
704
+ * that return a text part instead of an image); `MAX_TOKENS` is a benign truncation.
705
+ * A candidate carrying only one of these is NOT a decline — treating it as one would
706
+ * mislabel an ordinary no-image outcome as a policy refusal. @internal
707
+ */
708
+ const benignGeminiImageFinishReasons = new Set(['STOP', 'MAX_TOKENS']);
706
709
  /** Calls Gemini :generateContent for image output; accepts ref images as inlineData. @internal */
707
710
  async function callGeminiImageOutGeneration(config, request, resolved, logger, signal) {
708
711
  var _a;
@@ -732,9 +735,10 @@ async function callGeminiImageOutGeneration(config, request, resolved, logger, s
732
735
  .validate(json)
733
736
  .withErrorFormat((msg) => `Gemini image API response: ${msg}`)
734
737
  .onSuccess((response) => {
738
+ var _a, _b;
735
739
  const images = [];
736
740
  for (const candidate of response.candidates) {
737
- for (const part of candidate.content.parts) {
741
+ for (const part of (_b = (_a = candidate.content) === null || _a === void 0 ? void 0 : _a.parts) !== null && _b !== void 0 ? _b : []) {
738
742
  if (part.inlineData) {
739
743
  images.push({
740
744
  mimeType: part.inlineData.mimeType,
@@ -744,80 +748,30 @@ async function callGeminiImageOutGeneration(config, request, resolved, logger, s
744
748
  }
745
749
  }
746
750
  if (images.length === 0) {
751
+ // A candidate with no image parts is a *decline* only when it carries a
752
+ // refusal-shaped finishReason — i.e. present and not a benign terminal reason
753
+ // (`STOP`/`MAX_TOKENS`). A normal completion that emitted text-instead-of-image
754
+ // carries `finishReason: 'STOP'` and must fall through to the no-image message.
755
+ const declined = response.candidates.find((candidate) => candidate.finishReason !== undefined &&
756
+ !benignGeminiImageFinishReasons.has(candidate.finishReason));
757
+ if ((declined === null || declined === void 0 ? void 0 : declined.finishReason) !== undefined) {
758
+ // Truthiness (not `!== undefined`) so an empty-string finishMessage is treated
759
+ // as "no message" and produces no dangling ` — ` separator.
760
+ const suffix = declined.finishMessage ? ` — ${declined.finishMessage}` : '';
761
+ return fail(`Gemini image generation declined: ${declined.finishReason}${suffix}`);
762
+ }
747
763
  return fail('Gemini image API response: no image parts in response');
748
764
  }
749
765
  return succeed({ images });
750
766
  }));
751
767
  }
752
- /** Calls the Gemini Imagen :predict endpoint with Imagen 4 params. @internal */
753
- async function callImagenGeneration(config, request, resolved, logger, signal) {
754
- const url = `${config.baseUrl}/models/${config.model}:predict`;
755
- const parameters = {
756
- sampleCount: resolved.n
757
- };
758
- if (resolved.imagenAspectRatio !== undefined) {
759
- parameters.aspectRatio = resolved.imagenAspectRatio;
760
- }
761
- if (resolved.imageSize !== undefined) {
762
- parameters.imageSize = resolved.imageSize;
763
- }
764
- if (resolved.addWatermark !== undefined) {
765
- parameters.addWatermark = resolved.addWatermark;
766
- }
767
- if (resolved.enhancePrompt !== undefined) {
768
- parameters.enhancePrompt = resolved.enhancePrompt;
769
- }
770
- if (resolved.imagenOutputMimeType !== undefined || resolved.imagenOutputCompressionQuality !== undefined) {
771
- const outputOptions = {};
772
- if (resolved.imagenOutputMimeType !== undefined) {
773
- outputOptions.mimeType = resolved.imagenOutputMimeType;
774
- }
775
- if (resolved.imagenOutputCompressionQuality !== undefined) {
776
- outputOptions.compressionQuality = resolved.imagenOutputCompressionQuality;
777
- }
778
- parameters.outputOptions = outputOptions;
779
- }
780
- if (resolved.personGeneration !== undefined) {
781
- parameters.personGeneration = resolved.personGeneration;
782
- }
783
- if (resolved.seed !== undefined) {
784
- parameters.seed = resolved.seed;
785
- }
786
- if (resolved.otherParams !== undefined) {
787
- Object.assign(parameters, resolved.otherParams);
788
- }
789
- const body = {
790
- instances: [{ prompt: request.prompt }],
791
- parameters
792
- };
793
- const headers = { 'x-goog-api-key': config.apiKey };
794
- /* c8 ignore next 1 - optional logger */
795
- logger === null || logger === void 0 ? void 0 : logger.info(`Imagen generation: model=${config.model}, n=${parameters.sampleCount}`);
796
- const jsonResult = await fetchJson(url, headers, body, logger, signal);
797
- if (jsonResult.isFailure()) {
798
- return fail(jsonResult.message);
799
- }
800
- return imagenResponse
801
- .validate(jsonResult.value)
802
- .withErrorFormat((msg) => `Imagen API response: ${msg}`)
803
- .onSuccess((response) => {
804
- const images = response.predictions.map((p) => {
805
- var _a;
806
- return ({
807
- mimeType: (_a = p.mimeType) !== null && _a !== void 0 ? _a : 'image/png',
808
- base64: p.bytesBase64Encoded
809
- });
810
- });
811
- return succeed({ images });
812
- });
813
- }
814
768
  // ============================================================================
815
769
  // Image generation — dispatcher
816
770
  // ============================================================================
817
771
  /**
818
772
  * Calls the appropriate image-generation API for a given provider. Routes by the
819
773
  * `format` field of the resolved {@link IAiImageModelCapability}:
820
- * `'openai-images'`, `'xai-images'`, `'xai-images-edits'`, `'gemini-imagen'`, or
774
+ * `'openai-images'`, `'xai-images'`, `'xai-images-edits'`, or
821
775
  * `'gemini-image-out'`. Rejects up front if `referenceImages` is set but the
822
776
  * capability does not declare `acceptsImageReferenceInput`.
823
777
  * @param params - Request parameters including descriptor, API key, and prompt
@@ -833,12 +787,11 @@ export async function callProviderImageGeneration(params) {
833
787
  if (baseUrlResult.isFailure()) {
834
788
  return fail(baseUrlResult.message);
835
789
  }
836
- const model = resolveModel(modelOverride !== null && modelOverride !== void 0 ? modelOverride : descriptor.defaultModel, 'image');
837
- if (model.length === 0) {
838
- return fail(`provider "${descriptor.id}": no image model resolved; ` +
839
- `pass modelOverride or set descriptor.defaultModel ` +
840
- `(a plain string, or an object with an "image" entry)`);
790
+ const modelResult = resolveProviderModel(descriptor, modelOverride, 'image');
791
+ if (modelResult.isFailure()) {
792
+ return fail(modelResult.message);
841
793
  }
794
+ const model = modelResult.value;
842
795
  const capability = resolveImageCapability(descriptor, model);
843
796
  if (capability === undefined) {
844
797
  return fail(`provider "${descriptor.id}" does not support image generation for model "${model}"`);
@@ -885,8 +838,6 @@ export async function callProviderImageGeneration(params) {
885
838
  }
886
839
  return callXaiImageGeneration(config, request, capability, resolved, logger, signal);
887
840
  }
888
- case 'gemini-imagen':
889
- return callImagenGeneration(config, request, resolved, logger, signal);
890
841
  case 'gemini-image-out':
891
842
  return callGeminiImageOutGeneration(config, request, resolved, logger, signal);
892
843
  /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */
@@ -1180,9 +1131,13 @@ export async function callProxiedCompletion(proxyUrl, params) {
1180
1131
  const body = {
1181
1132
  providerId: descriptor.id,
1182
1133
  apiKey,
1183
- messages: normalizeOutboundMessages(splitResult.value),
1184
- temperature: temperature !== null && temperature !== void 0 ? temperature : 0.7
1134
+ messages: normalizeOutboundMessages(splitResult.value)
1185
1135
  };
1136
+ // Temperature is forwarded only when explicitly provided, matching the direct path — the proxy
1137
+ // omits it from the upstream request so the provider default applies.
1138
+ if (temperature !== undefined) {
1139
+ body.temperature = temperature;
1140
+ }
1186
1141
  if (system !== undefined) {
1187
1142
  body.system = system;
1188
1143
  }