@fgv/ts-extras 5.1.0-18 → 5.1.0-19

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.
@@ -92,6 +92,102 @@ async function fetchJson(url, headers, body, logger, signal) {
92
92
  }
93
93
  return (0, ts_utils_1.succeed)(json);
94
94
  }
95
+ /**
96
+ * Makes a multipart/form-data POST request and returns the parsed JSON, or a
97
+ * failure. The Content-Type header (with boundary) is set automatically by
98
+ * `fetch` from the `FormData` body — callers must NOT pass it explicitly.
99
+ * @internal
100
+ */
101
+ async function fetchMultipart(url, headers, body, logger, signal) {
102
+ /* c8 ignore next 1 - optional logger */
103
+ logger === null || logger === void 0 ? void 0 : logger.detail(`AI API request: POST ${url} (multipart)`);
104
+ let response;
105
+ try {
106
+ response = await fetch(url, {
107
+ method: 'POST',
108
+ headers,
109
+ body,
110
+ signal
111
+ });
112
+ }
113
+ catch (err) {
114
+ const detail = err instanceof Error ? err.message : String(err);
115
+ /* c8 ignore next 1 - optional logger */
116
+ logger === null || logger === void 0 ? void 0 : logger.error(`AI API request failed: ${detail}`);
117
+ return (0, ts_utils_1.fail)(`AI API request failed: ${detail}`);
118
+ }
119
+ if (!response.ok) {
120
+ const errorText = await response.text().catch(() => 'unknown error');
121
+ /* c8 ignore next 1 - optional logger */
122
+ logger === null || logger === void 0 ? void 0 : logger.error(`AI API returned ${response.status}: ${errorText}`);
123
+ return (0, ts_utils_1.fail)(`AI API returned ${response.status}: ${errorText}`);
124
+ }
125
+ /* c8 ignore next 1 - optional logger */
126
+ logger === null || logger === void 0 ? void 0 : logger.detail(`AI API response: ${response.status}`);
127
+ let json;
128
+ try {
129
+ json = await response.json();
130
+ }
131
+ catch (_a) {
132
+ /* c8 ignore next 1 - optional logger */
133
+ logger === null || logger === void 0 ? void 0 : logger.error('AI API returned invalid JSON response');
134
+ return (0, ts_utils_1.fail)('AI API returned invalid JSON response');
135
+ }
136
+ if (!(0, ts_json_base_1.isJsonObject)(json)) {
137
+ /* c8 ignore next 1 - optional logger */
138
+ logger === null || logger === void 0 ? void 0 : logger.error('AI API returned non-object JSON response');
139
+ return (0, ts_utils_1.fail)('AI API returned non-object JSON response');
140
+ }
141
+ return (0, ts_utils_1.succeed)(json);
142
+ }
143
+ /**
144
+ * Decodes a base64-encoded image attachment into a `Blob` suitable for use as
145
+ * a multipart file field. On Node hands the `Buffer` straight to `Blob`
146
+ * (Buffer extends Uint8Array) to skip an intermediate copy; falls back to
147
+ * `atob` in browsers. Inputs come from `FileReader` or prior provider
148
+ * responses, which are trusted to be valid. Note that Node's
149
+ * `Buffer.from(..., 'base64')` silently strips invalid characters rather
150
+ * than throwing, so failures are only observable in the browser path.
151
+ * @internal
152
+ */
153
+ function attachmentToBlob(attachment) {
154
+ if (typeof Buffer !== 'undefined') {
155
+ return (0, ts_utils_1.succeed)(new Blob([Buffer.from(attachment.base64, 'base64')], { type: attachment.mimeType }));
156
+ }
157
+ /* c8 ignore start - Browser-only fallback cannot be tested in Node.js environment */
158
+ try {
159
+ const binary = atob(attachment.base64);
160
+ const bytes = new Uint8Array(binary.length);
161
+ for (let i = 0; i < binary.length; i++) {
162
+ bytes[i] = binary.charCodeAt(i);
163
+ }
164
+ return (0, ts_utils_1.succeed)(new Blob([bytes], { type: attachment.mimeType }));
165
+ }
166
+ catch (e) {
167
+ const message = e instanceof Error ? e.message : String(e);
168
+ return (0, ts_utils_1.fail)(`Invalid base64: ${message}`);
169
+ }
170
+ /* c8 ignore stop */
171
+ }
172
+ /**
173
+ * Maps a MIME type to a sensible file extension for multipart filenames.
174
+ * @internal
175
+ */
176
+ function extensionForMimeType(mimeType) {
177
+ switch (mimeType) {
178
+ case 'image/png':
179
+ return 'png';
180
+ case 'image/jpeg':
181
+ case 'image/jpg':
182
+ return 'jpg';
183
+ case 'image/webp':
184
+ return 'webp';
185
+ case 'image/gif':
186
+ return 'gif';
187
+ default:
188
+ return 'bin';
189
+ }
190
+ }
95
191
  /**
96
192
  * Makes an HTTP GET request and returns the parsed JSON, or a failure.
97
193
  * @internal
@@ -465,6 +561,24 @@ const imagenPrediction = ts_utils_1.Validators.object({
465
561
  const imagenResponse = ts_utils_1.Validators.object({
466
562
  predictions: ts_utils_1.Validators.arrayOf(imagenPrediction).withConstraint((arr) => arr.length > 0)
467
563
  });
564
+ const geminiImageInlineData = ts_utils_1.Validators.object({
565
+ mimeType: ts_utils_1.Validators.string,
566
+ data: ts_utils_1.Validators.string
567
+ });
568
+ const geminiImageOutPart = ts_utils_1.Validators.object({
569
+ text: ts_utils_1.Validators.string.optional(),
570
+ inlineData: geminiImageInlineData.optional()
571
+ });
572
+ const geminiImageOutContent = ts_utils_1.Validators.object({
573
+ parts: ts_utils_1.Validators.arrayOf(geminiImageOutPart).withConstraint((arr) => arr.length > 0)
574
+ });
575
+ const geminiImageOutCandidate = ts_utils_1.Validators.object({
576
+ content: geminiImageOutContent,
577
+ finishReason: ts_utils_1.Validators.string.optional()
578
+ });
579
+ const geminiImageOutResponse = ts_utils_1.Validators.object({
580
+ candidates: ts_utils_1.Validators.arrayOf(geminiImageOutCandidate).withConstraint((arr) => arr.length > 0)
581
+ });
468
582
  // ---- Proxied image generation response ----
469
583
  const proxiedGeneratedImage = ts_utils_1.Validators.object({
470
584
  mimeType: ts_utils_1.Validators.string,
@@ -490,16 +604,42 @@ const proxiedListModelsResponse = ts_utils_1.Validators.object({
490
604
  * formats — the request shape is the same; the only difference is whether the
491
605
  * `size` field is honored (OpenAI: yes, xAI: ignored at the provider).
492
606
  *
607
+ * When `request.referenceImages` is non-empty, routes to `/images/edits`
608
+ * (multipart) instead of `/images/generations` (JSON). Per-model edit support
609
+ * is not validated here (e.g. dall-e-3 does not support edits) — the
610
+ * provider's 400 surfaces through the failure path.
611
+ *
493
612
  * @internal
494
613
  */
495
614
  async function callOpenAiImageGeneration(config, request, defaultMimeType, logger, signal) {
496
- var _a, _b;
497
- const url = `${config.baseUrl}/images/generations`;
615
+ var _a, _b, _c;
616
+ const opts = (_a = request.options) !== null && _a !== void 0 ? _a : {};
617
+ const refs = (_b = request.referenceImages) !== null && _b !== void 0 ? _b : [];
618
+ const headers = {
619
+ Authorization: `Bearer ${config.apiKey}`
620
+ };
621
+ const n = (_c = opts.count) !== null && _c !== void 0 ? _c : 1;
622
+ const fetched = refs.length > 0
623
+ ? await callOpenAiImagesEdits(config, request, headers, n, refs, logger, signal)
624
+ : await callOpenAiImagesGenerations(config, request, headers, n, logger, signal);
625
+ return fetched.onSuccess((json) => openAiImageResponse
626
+ .validate(json)
627
+ .withErrorFormat((msg) => `OpenAI images API response: ${msg}`)
628
+ .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 } : {}))))
630
+ })));
631
+ }
632
+ /**
633
+ * Builds and posts the JSON `/images/generations` request (no refs).
634
+ * @internal
635
+ */
636
+ function callOpenAiImagesGenerations(config, request, headers, n, logger, signal) {
637
+ var _a;
498
638
  const opts = (_a = request.options) !== null && _a !== void 0 ? _a : {};
499
639
  const body = {
500
640
  model: config.model,
501
641
  prompt: request.prompt,
502
- n: (_b = opts.count) !== null && _b !== void 0 ? _b : 1,
642
+ n,
503
643
  response_format: 'b64_json'
504
644
  };
505
645
  if (opts.size !== undefined) {
@@ -511,22 +651,86 @@ async function callOpenAiImageGeneration(config, request, defaultMimeType, logge
511
651
  if (opts.seed !== undefined) {
512
652
  body.seed = opts.seed;
513
653
  }
654
+ /* c8 ignore next 1 - optional logger */
655
+ logger === null || logger === void 0 ? void 0 : logger.info(`Image generation: model=${config.model}, n=${n}`);
656
+ return fetchJson(`${config.baseUrl}/images/generations`, headers, body, logger, signal);
657
+ }
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;
664
+ const blobsResult = (0, ts_utils_1.mapResults)(refs.map((ref, i) => attachmentToBlob(ref).withErrorFormat((msg) => `reference image ${i}: ${msg}`)));
665
+ /* c8 ignore next 3 - decode failure unreachable via Node's Buffer.from (silently strips invalid input) */
666
+ if (blobsResult.isFailure()) {
667
+ return (0, ts_utils_1.fail)(blobsResult.message);
668
+ }
669
+ const opts = (_a = request.options) !== null && _a !== void 0 ? _a : {};
670
+ const form = new FormData();
671
+ form.append('model', config.model);
672
+ 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);
677
+ }
678
+ if (opts.quality !== undefined) {
679
+ form.append('quality', opts.quality);
680
+ }
681
+ if (opts.seed !== undefined) {
682
+ form.append('seed', String(opts.seed));
683
+ }
684
+ blobsResult.value.forEach((blob, i) => {
685
+ form.append('image[]', blob, `ref-${i}.${extensionForMimeType(refs[i].mimeType)}`);
686
+ });
687
+ /* 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}`);
689
+ return fetchMultipart(`${config.baseUrl}/images/edits`, headers, form, logger, signal);
690
+ }
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
+ var _a;
700
+ const url = `${config.baseUrl}/models/${config.model}:generateContent`;
701
+ const refs = (_a = request.referenceImages) !== null && _a !== void 0 ? _a : [];
702
+ const parts = [{ text: request.prompt }];
703
+ for (const ref of refs) {
704
+ parts.push({ inlineData: { mimeType: ref.mimeType, data: ref.base64 } });
705
+ }
706
+ const body = {
707
+ contents: [{ role: 'user', parts }]
708
+ };
514
709
  const headers = {
515
- Authorization: `Bearer ${config.apiKey}`
710
+ 'x-goog-api-key': config.apiKey
516
711
  };
517
712
  /* c8 ignore next 1 - optional logger */
518
- logger === null || logger === void 0 ? void 0 : logger.info(`Image generation: model=${config.model}, n=${body.n}`);
519
- const jsonResult = await fetchJson(url, headers, body, logger, signal);
520
- if (jsonResult.isFailure()) {
521
- return (0, ts_utils_1.fail)(jsonResult.message);
522
- }
523
- return openAiImageResponse
524
- .validate(jsonResult.value)
525
- .withErrorFormat((msg) => `OpenAI images API response: ${msg}`)
713
+ logger === null || logger === void 0 ? void 0 : logger.info(`Gemini image-out: model=${config.model}, refs=${refs.length}`);
714
+ return (await fetchJson(url, headers, body, logger, signal)).onSuccess((json) => geminiImageOutResponse
715
+ .validate(json)
716
+ .withErrorFormat((msg) => `Gemini image API response: ${msg}`)
526
717
  .onSuccess((response) => {
527
- const images = response.data.map((item) => (Object.assign({ mimeType: defaultMimeType, base64: item.b64_json }, (item.revised_prompt !== undefined ? { revisedPrompt: item.revised_prompt } : {}))));
718
+ const images = [];
719
+ for (const candidate of response.candidates) {
720
+ for (const part of candidate.content.parts) {
721
+ if (part.inlineData) {
722
+ images.push({
723
+ mimeType: part.inlineData.mimeType,
724
+ base64: part.inlineData.data
725
+ });
726
+ }
727
+ }
728
+ }
729
+ if (images.length === 0) {
730
+ return (0, ts_utils_1.fail)('Gemini image API response: no image parts in response');
731
+ }
528
732
  return (0, ts_utils_1.succeed)({ images });
529
- });
733
+ }));
530
734
  }
531
735
  /**
532
736
  * Calls the Gemini Imagen `:predict` endpoint.
@@ -581,45 +785,61 @@ async function callImagenGeneration(config, request, logger, signal) {
581
785
  /**
582
786
  * Calls the appropriate image-generation API for a given provider.
583
787
  *
584
- * Routes based on `descriptor.imageApiFormat`:
788
+ * Resolves a {@link IAiImageModelCapability} from
789
+ * {@link IAiProviderDescriptor.imageGeneration} for the requested model and
790
+ * routes by its `format`:
585
791
  * - `'openai-images'` for OpenAI (DALL-E, gpt-image-1)
586
792
  * - `'xai-images'` for xAI Grok image models
587
- * - `'gemini-imagen'` for Google Imagen
793
+ * - `'gemini-imagen'` for Google Imagen `:predict`
794
+ * - `'gemini-image-out'` for Gemini chat-style image output (Nano Banana)
588
795
  *
589
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`.
590
799
  *
591
800
  * @param params - Request parameters including descriptor, API key, and prompt
592
801
  * @returns The generated images, or a failure
593
802
  * @public
594
803
  */
595
804
  async function callProviderImageGeneration(params) {
805
+ var _a, _b;
596
806
  const { descriptor, apiKey, params: request, modelOverride, logger, signal } = params;
597
- if (descriptor.imageApiFormat === undefined) {
807
+ if (!(0, registry_1.supportsImageGeneration)(descriptor)) {
598
808
  return (0, ts_utils_1.fail)(`provider "${descriptor.id}" does not support image generation`);
599
809
  }
600
810
  if (!descriptor.baseUrl) {
601
811
  return (0, ts_utils_1.fail)(`provider "${descriptor.id}" has no API endpoint configured`);
602
812
  }
813
+ const model = (0, model_1.resolveModel)(modelOverride !== null && modelOverride !== void 0 ? modelOverride : descriptor.defaultModel, 'image');
814
+ const capability = (0, registry_1.resolveImageCapability)(descriptor, model);
815
+ if (capability === undefined) {
816
+ return (0, ts_utils_1.fail)(`provider "${descriptor.id}" does not support image generation for model "${model}"`);
817
+ }
818
+ if (((_b = (_a = request.referenceImages) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 0 && !capability.acceptsImageReferenceInput) {
819
+ return (0, ts_utils_1.fail)(`model "${model}" does not support reference images`);
820
+ }
603
821
  const config = {
604
822
  baseUrl: descriptor.baseUrl,
605
823
  apiKey,
606
- model: (0, model_1.resolveModel)(modelOverride !== null && modelOverride !== void 0 ? modelOverride : descriptor.defaultModel, 'image')
824
+ model
607
825
  };
608
826
  /* c8 ignore next 6 - optional logger diagnostic output */
609
827
  if (logger) {
610
- logger.info(`AI image generation: provider=${descriptor.id}, format=${descriptor.imageApiFormat}, ` +
828
+ logger.info(`AI image generation: provider=${descriptor.id}, format=${capability.format}, ` +
611
829
  `model=${config.model}`);
612
830
  }
613
- switch (descriptor.imageApiFormat) {
831
+ switch (capability.format) {
614
832
  case 'openai-images':
615
833
  return callOpenAiImageGeneration(config, request, 'image/png', logger, signal);
616
834
  case 'xai-images':
617
835
  return callOpenAiImageGeneration(config, request, 'image/jpeg', logger, signal);
618
836
  case 'gemini-imagen':
619
837
  return callImagenGeneration(config, request, logger, signal);
838
+ case 'gemini-image-out':
839
+ return callGeminiImageOutGeneration(config, request, logger, signal);
620
840
  /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */
621
841
  default: {
622
- const _exhaustive = descriptor.imageApiFormat;
842
+ const _exhaustive = capability.format;
623
843
  return (0, ts_utils_1.fail)(`unsupported image API format: ${String(_exhaustive)}`);
624
844
  }
625
845
  }
@@ -969,7 +1189,10 @@ async function callProxiedCompletion(proxyUrl, params) {
969
1189
  * - Error response body: `{error: string}` (surfaced as `proxy: ${error}`)
970
1190
  *
971
1191
  * The proxy server is responsible for descriptor lookup, model resolution,
972
- * provider dispatch, and response normalization.
1192
+ * provider dispatch, and response normalization. When `params.referenceImages`
1193
+ * is present, the proxy is also responsible for repackaging it into the
1194
+ * upstream wire format (e.g. multipart/form-data for OpenAI `/images/edits`,
1195
+ * `inlineData` parts for Gemini `:generateContent`).
973
1196
  *
974
1197
  * @param proxyUrl - Base URL of the proxy server (e.g. `http://localhost:3001`)
975
1198
  * @param params - Same parameters as {@link callProviderImageGeneration}
@@ -2,8 +2,8 @@
2
2
  * AI assist packlet - provider registry, prompt class, settings, and API client.
3
3
  * @packageDocumentation
4
4
  */
5
- export { AiPrompt, type AiModelCapability, type AiProviderId, type AiServerToolType, type AiServerToolConfig, type IAiWebSearchToolConfig, type IAiToolEnablement, type IAiCompletionResponse, type IChatMessage, type AiApiFormat, type AiImageApiFormat, type IAiProviderDescriptor, type IAiAssistProviderConfig, type IAiAssistSettings, DEFAULT_AI_ASSIST, type IAiAssistKeyStore, type IAiImageAttachment, type IAiImageData, type IAiImageGenerationOptions, type IAiImageGenerationParams, type IAiGeneratedImage, type IAiImageGenerationResponse, type IAiModelCapabilityRule, type IAiModelCapabilityConfig, type IAiModelInfo, type IAiStreamEvent, type IAiStreamTextDelta, type IAiStreamToolEvent, type IAiStreamDone, type IAiStreamError, type ModelSpec, type ModelSpecKey, type IModelSpecMap, allModelSpecKeys, MODEL_SPEC_BASE_KEY, resolveModel, toDataUrl } from './model';
6
- export { allProviderIds, getProviderDescriptors, getProviderDescriptor, DEFAULT_MODEL_CAPABILITY_CONFIG } from './registry';
5
+ export { AiPrompt, type AiModelCapability, type AiProviderId, type AiServerToolType, type AiServerToolConfig, type IAiWebSearchToolConfig, type IAiToolEnablement, type IAiCompletionResponse, type IChatMessage, type AiApiFormat, type AiImageApiFormat, type IAiImageModelCapability, type IAiProviderDescriptor, type IAiAssistProviderConfig, type IAiAssistSettings, DEFAULT_AI_ASSIST, type IAiAssistKeyStore, type IAiImageAttachment, type IAiImageData, type IAiImageGenerationOptions, type IAiImageGenerationParams, type IAiGeneratedImage, type IAiImageGenerationResponse, type IAiModelCapabilityRule, type IAiModelCapabilityConfig, type IAiModelInfo, type IAiStreamEvent, type IAiStreamTextDelta, type IAiStreamToolEvent, type IAiStreamDone, type IAiStreamError, type ModelSpec, type ModelSpecKey, type IModelSpecMap, allModelSpecKeys, MODEL_SPEC_BASE_KEY, resolveModel, toDataUrl } from './model';
6
+ export { allProviderIds, getProviderDescriptors, getProviderDescriptor, resolveImageCapability, supportsImageGeneration, DEFAULT_MODEL_CAPABILITY_CONFIG } from './registry';
7
7
  export { callProviderCompletion, callProxiedCompletion, callProviderImageGeneration, callProxiedImageGeneration, callProviderListModels, callProxiedListModels, type IProviderCompletionParams, type IProviderImageGenerationParams, type IProviderListModelsParams } from './apiClient';
8
8
  export { callProviderCompletionStream, callProxiedCompletionStream, type IProviderCompletionStreamParams } from './streamingClient';
9
9
  export { aiProviderId, aiServerToolType, aiWebSearchToolConfig, aiServerToolConfig, aiToolEnablement, aiAssistProviderConfig, aiAssistSettings, modelSpecKey, modelSpec } from './converters';
@@ -4,7 +4,7 @@
4
4
  * @packageDocumentation
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.resolveEffectiveTools = exports.modelSpec = exports.modelSpecKey = exports.aiAssistSettings = exports.aiAssistProviderConfig = exports.aiToolEnablement = exports.aiServerToolConfig = exports.aiWebSearchToolConfig = exports.aiServerToolType = exports.aiProviderId = exports.callProxiedCompletionStream = exports.callProviderCompletionStream = exports.callProxiedListModels = exports.callProviderListModels = exports.callProxiedImageGeneration = exports.callProviderImageGeneration = exports.callProxiedCompletion = exports.callProviderCompletion = exports.DEFAULT_MODEL_CAPABILITY_CONFIG = exports.getProviderDescriptor = exports.getProviderDescriptors = exports.allProviderIds = exports.toDataUrl = exports.resolveModel = exports.MODEL_SPEC_BASE_KEY = exports.allModelSpecKeys = exports.DEFAULT_AI_ASSIST = exports.AiPrompt = void 0;
7
+ exports.resolveEffectiveTools = exports.modelSpec = exports.modelSpecKey = exports.aiAssistSettings = exports.aiAssistProviderConfig = exports.aiToolEnablement = exports.aiServerToolConfig = exports.aiWebSearchToolConfig = exports.aiServerToolType = exports.aiProviderId = exports.callProxiedCompletionStream = exports.callProviderCompletionStream = exports.callProxiedListModels = exports.callProviderListModels = exports.callProxiedImageGeneration = exports.callProviderImageGeneration = exports.callProxiedCompletion = exports.callProviderCompletion = exports.DEFAULT_MODEL_CAPABILITY_CONFIG = exports.supportsImageGeneration = exports.resolveImageCapability = exports.getProviderDescriptor = exports.getProviderDescriptors = exports.allProviderIds = exports.toDataUrl = exports.resolveModel = exports.MODEL_SPEC_BASE_KEY = exports.allModelSpecKeys = exports.DEFAULT_AI_ASSIST = exports.AiPrompt = void 0;
8
8
  var model_1 = require("./model");
9
9
  Object.defineProperty(exports, "AiPrompt", { enumerable: true, get: function () { return model_1.AiPrompt; } });
10
10
  Object.defineProperty(exports, "DEFAULT_AI_ASSIST", { enumerable: true, get: function () { return model_1.DEFAULT_AI_ASSIST; } });
@@ -16,6 +16,8 @@ var registry_1 = require("./registry");
16
16
  Object.defineProperty(exports, "allProviderIds", { enumerable: true, get: function () { return registry_1.allProviderIds; } });
17
17
  Object.defineProperty(exports, "getProviderDescriptors", { enumerable: true, get: function () { return registry_1.getProviderDescriptors; } });
18
18
  Object.defineProperty(exports, "getProviderDescriptor", { enumerable: true, get: function () { return registry_1.getProviderDescriptor; } });
19
+ Object.defineProperty(exports, "resolveImageCapability", { enumerable: true, get: function () { return registry_1.resolveImageCapability; } });
20
+ Object.defineProperty(exports, "supportsImageGeneration", { enumerable: true, get: function () { return registry_1.supportsImageGeneration; } });
19
21
  Object.defineProperty(exports, "DEFAULT_MODEL_CAPABILITY_CONFIG", { enumerable: true, get: function () { return registry_1.DEFAULT_MODEL_CAPABILITY_CONFIG; } });
20
22
  var apiClient_1 = require("./apiClient");
21
23
  Object.defineProperty(exports, "callProviderCompletion", { enumerable: true, get: function () { return apiClient_1.callProviderCompletion; } });
@@ -194,9 +194,20 @@ export type AiProviderId = 'copy-paste' | 'xai-grok' | 'openai' | 'anthropic' |
194
194
  export type AiApiFormat = 'openai' | 'anthropic' | 'gemini';
195
195
  /**
196
196
  * API format categories for image-generation provider routing.
197
+ *
198
+ * @remarks
199
+ * - `'openai-images'` — OpenAI Images API. Routes to `/images/generations`
200
+ * (text-only) or `/images/edits` (when reference images are present).
201
+ * - `'xai-images'` — xAI Images API. Same wire shape as OpenAI but text-only;
202
+ * no reference-image support on grok-2-image.
203
+ * - `'gemini-imagen'` — Google Imagen `:predict` endpoint. Text-only.
204
+ * - `'gemini-image-out'` — Google Gemini chat-style `:generateContent`
205
+ * endpoint that returns image parts (Gemini 2.5 Flash Image / "Nano
206
+ * Banana"). Accepts reference images.
207
+ *
197
208
  * @public
198
209
  */
199
- export type AiImageApiFormat = 'openai-images' | 'gemini-imagen' | 'xai-images';
210
+ export type AiImageApiFormat = 'openai-images' | 'gemini-imagen' | 'xai-images' | 'gemini-image-out';
200
211
  /**
201
212
  * Result of an AI provider completion call.
202
213
  * @public
@@ -309,15 +320,55 @@ export interface IAiProviderDescriptor {
309
320
  */
310
321
  readonly acceptsImageInput: boolean;
311
322
  /**
312
- * Which image-generation API format this provider uses, or undefined if it
313
- * does not support image generation.
323
+ * Image-generation capabilities, scoped to model id prefixes. Empty or
324
+ * undefined means the provider does not support image generation.
314
325
  *
315
326
  * @remarks
327
+ * The dispatcher matches the resolved model id against each rule's
328
+ * `modelPrefix` and selects the longest match (see
329
+ * {@link AiAssist.resolveImageCapability}). An empty `modelPrefix` is the
330
+ * catch-all and matches every model id.
331
+ *
332
+ * Multiple entries support providers that host more than one image-API
333
+ * surface under one baseUrl. Google Gemini is the canonical case: the
334
+ * `imagen-*` family is predict-only via `:predict`, while
335
+ * `gemini-2.5-flash-image` uses chat-style `:generateContent` and accepts
336
+ * reference images. Listing both lets callers pick the right model and the
337
+ * dispatcher routes accordingly.
338
+ *
316
339
  * Image-model selection reuses the existing `image` {@link ModelSpecKey}.
317
- * Providers with `imageApiFormat` set should declare a model in
340
+ * Providers that declare `imageGeneration` should declare a model in
318
341
  * `defaultModel.image`, e.g. `{ base: 'gpt-4o', image: 'dall-e-3' }`.
319
342
  */
320
- readonly imageApiFormat?: AiImageApiFormat;
343
+ readonly imageGeneration?: ReadonlyArray<IAiImageModelCapability>;
344
+ }
345
+ /**
346
+ * Image-generation capability for a model family within a provider. Used as
347
+ * an entry in {@link IAiProviderDescriptor.imageGeneration}.
348
+ *
349
+ * @public
350
+ */
351
+ export interface IAiImageModelCapability {
352
+ /**
353
+ * Prefix matched against the resolved image model id. The empty string is
354
+ * the catch-all and matches every model. When multiple rules' prefixes
355
+ * match a model id, the longest prefix wins; ties are broken by
356
+ * first-encountered.
357
+ */
358
+ readonly modelPrefix: string;
359
+ /** API format used to dispatch requests for matching models. */
360
+ readonly format: AiImageApiFormat;
361
+ /**
362
+ * Whether matching models accept reference images via
363
+ * {@link AiAssist.IAiImageGenerationParams.referenceImages}. When false or
364
+ * undefined, calls that include reference images are rejected up front.
365
+ *
366
+ * @remarks
367
+ * Per-model constraints beyond ref support (e.g. dall-e-3 ignores edits)
368
+ * are not validated here and surface as provider 400s, consistent with the
369
+ * existing image-generation policy.
370
+ */
371
+ readonly acceptsImageReferenceInput?: boolean;
321
372
  }
322
373
  /**
323
374
  * Options for image generation requests.
@@ -366,6 +417,16 @@ export interface IAiImageGenerationParams {
366
417
  readonly prompt: string;
367
418
  /** Optional generation options. */
368
419
  readonly options?: IAiImageGenerationOptions;
420
+ /**
421
+ * Optional reference images. When present, the provider will use them as
422
+ * visual context (e.g. to preserve a character's appearance across multiple
423
+ * generations). The dispatcher resolves the
424
+ * {@link AiAssist.IAiImageModelCapability} for the requested model and
425
+ * rejects the call up front if `acceptsImageReferenceInput` is not set on
426
+ * the matching capability. An empty array is treated identically to
427
+ * `undefined`.
428
+ */
429
+ readonly referenceImages?: ReadonlyArray<IAiImageAttachment>;
369
430
  }
370
431
  /**
371
432
  * A single generated image.
@@ -3,7 +3,7 @@
3
3
  * @packageDocumentation
4
4
  */
5
5
  import { Result } from '@fgv/ts-utils';
6
- import { type AiProviderId, type IAiModelCapabilityConfig, type IAiProviderDescriptor } from './model';
6
+ import { type AiProviderId, type IAiImageModelCapability, type IAiModelCapabilityConfig, type IAiProviderDescriptor } from './model';
7
7
  /**
8
8
  * All valid provider ID values, in the same order as the registry.
9
9
  * @public
@@ -22,6 +22,30 @@ export declare function getProviderDescriptors(): ReadonlyArray<IAiProviderDescr
22
22
  * @public
23
23
  */
24
24
  export declare function getProviderDescriptor(id: string): Result<IAiProviderDescriptor>;
25
+ /**
26
+ * Whether a provider declares any image-generation capability at all.
27
+ *
28
+ * @param descriptor - The provider descriptor
29
+ * @returns `true` when {@link IAiProviderDescriptor.imageGeneration} has at
30
+ * least one entry; `false` otherwise.
31
+ * @public
32
+ */
33
+ export declare function supportsImageGeneration(descriptor: IAiProviderDescriptor): boolean;
34
+ /**
35
+ * Resolve the image-generation capability that applies to a given model id
36
+ * for a provider. Returns the entry from
37
+ * {@link IAiProviderDescriptor.imageGeneration} whose `modelPrefix` is the
38
+ * longest prefix of `modelId`. Ties are broken by first-encountered, so rule
39
+ * order does not matter for correctness — only for tie-breaking among rules
40
+ * with identical-length prefixes (an unusual case).
41
+ *
42
+ * @param descriptor - The provider descriptor
43
+ * @param modelId - The resolved image model id
44
+ * @returns The matching capability, or `undefined` when no rule matches or
45
+ * the provider declares no image-generation capabilities.
46
+ * @public
47
+ */
48
+ export declare function resolveImageCapability(descriptor: IAiProviderDescriptor, modelId: string): IAiImageModelCapability | undefined;
25
49
  /**
26
50
  * Default capability config used by `callProviderListModels` when callers
27
51
  * don't supply their own. Patterns are intentionally narrow — false
@@ -22,6 +22,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
22
22
  exports.DEFAULT_MODEL_CAPABILITY_CONFIG = exports.allProviderIds = void 0;
23
23
  exports.getProviderDescriptors = getProviderDescriptors;
24
24
  exports.getProviderDescriptor = getProviderDescriptor;
25
+ exports.supportsImageGeneration = supportsImageGeneration;
26
+ exports.resolveImageCapability = resolveImageCapability;
25
27
  /**
26
28
  * Centralized provider registry — single source of truth for all AI provider metadata.
27
29
  * @packageDocumentation
@@ -68,12 +70,17 @@ const BUILTIN_PROVIDERS = [
68
70
  needsSecret: true,
69
71
  apiFormat: 'gemini',
70
72
  baseUrl: 'https://generativelanguage.googleapis.com/v1beta',
71
- defaultModel: { base: 'gemini-2.5-flash', image: 'imagen-3.0-generate-002' },
73
+ defaultModel: { base: 'gemini-2.5-flash', image: 'gemini-2.5-flash-image' },
72
74
  supportedTools: ['web_search'],
73
75
  corsRestricted: false,
74
76
  streamingCorsRestricted: false,
75
77
  acceptsImageInput: true,
76
- imageApiFormat: 'gemini-imagen'
78
+ imageGeneration: [
79
+ // imagen-* models are predict-only and do not accept reference images;
80
+ // everything else uses chat-style :generateContent with refs.
81
+ { modelPrefix: 'imagen-', format: 'gemini-imagen' },
82
+ { modelPrefix: '', format: 'gemini-image-out', acceptsImageReferenceInput: true }
83
+ ]
77
84
  },
78
85
  {
79
86
  id: 'groq',
@@ -113,7 +120,14 @@ const BUILTIN_PROVIDERS = [
113
120
  corsRestricted: false,
114
121
  streamingCorsRestricted: false,
115
122
  acceptsImageInput: true,
116
- imageApiFormat: 'openai-images'
123
+ imageGeneration: [
124
+ // gpt-image-1 supports /images/edits with reference images. dall-e-3
125
+ // (the default image model) does not, so the catch-all rule omits
126
+ // acceptsImageReferenceInput; callers selecting dall-e-3 with refs hit
127
+ // the up-front rejection rather than a provider 400.
128
+ { modelPrefix: 'gpt-image-', format: 'openai-images', acceptsImageReferenceInput: true },
129
+ { modelPrefix: '', format: 'openai-images' }
130
+ ]
117
131
  },
118
132
  {
119
133
  id: 'xai-grok',
@@ -131,7 +145,7 @@ const BUILTIN_PROVIDERS = [
131
145
  corsRestricted: true,
132
146
  streamingCorsRestricted: true,
133
147
  acceptsImageInput: true,
134
- imageApiFormat: 'xai-images'
148
+ imageGeneration: [{ modelPrefix: '', format: 'xai-images' }]
135
149
  }
136
150
  ];
137
151
  /**
@@ -168,6 +182,38 @@ function getProviderDescriptor(id) {
168
182
  }
169
183
  return (0, ts_utils_1.succeed)(descriptor);
170
184
  }
185
+ /**
186
+ * Whether a provider declares any image-generation capability at all.
187
+ *
188
+ * @param descriptor - The provider descriptor
189
+ * @returns `true` when {@link IAiProviderDescriptor.imageGeneration} has at
190
+ * least one entry; `false` otherwise.
191
+ * @public
192
+ */
193
+ function supportsImageGeneration(descriptor) {
194
+ var _a, _b;
195
+ return ((_b = (_a = descriptor.imageGeneration) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 0;
196
+ }
197
+ /**
198
+ * Resolve the image-generation capability that applies to a given model id
199
+ * for a provider. Returns the entry from
200
+ * {@link IAiProviderDescriptor.imageGeneration} whose `modelPrefix` is the
201
+ * longest prefix of `modelId`. Ties are broken by first-encountered, so rule
202
+ * order does not matter for correctness — only for tie-breaking among rules
203
+ * with identical-length prefixes (an unusual case).
204
+ *
205
+ * @param descriptor - The provider descriptor
206
+ * @param modelId - The resolved image model id
207
+ * @returns The matching capability, or `undefined` when no rule matches or
208
+ * the provider declares no image-generation capabilities.
209
+ * @public
210
+ */
211
+ function resolveImageCapability(descriptor, modelId) {
212
+ var _a;
213
+ return ((_a = descriptor.imageGeneration) !== null && _a !== void 0 ? _a : [])
214
+ .filter((cap) => modelId.startsWith(cap.modelPrefix))
215
+ .reduce((best, cap) => (best && best.modelPrefix.length >= cap.modelPrefix.length ? best : cap), undefined);
216
+ }
171
217
  // ============================================================================
172
218
  // Default model capability config
173
219
  // ============================================================================
@@ -196,6 +242,7 @@ exports.DEFAULT_MODEL_CAPABILITY_CONFIG = {
196
242
  ],
197
243
  'google-gemini': [
198
244
  { idPattern: /^imagen/, capabilities: ['image-generation'] },
245
+ { idPattern: /^gemini-.*-image/, capabilities: ['image-generation'] },
199
246
  { idPattern: /^gemini-/, capabilities: ['chat', 'tools', 'vision'] }
200
247
  ],
201
248
  anthropic: [{ idPattern: /^claude-/, capabilities: ['chat', 'tools', 'vision'] }],