@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.
- package/dist/packlets/ai-assist/apiClient.js +247 -24
- package/dist/packlets/ai-assist/index.js +1 -1
- package/dist/packlets/ai-assist/registry.js +49 -4
- package/dist/packlets/crypto-utils/nodeCryptoProvider.js +96 -0
- package/dist/ts-extras.d.ts +222 -8
- package/lib/packlets/ai-assist/apiClient.d.ts +11 -3
- package/lib/packlets/ai-assist/apiClient.js +245 -22
- package/lib/packlets/ai-assist/index.d.ts +2 -2
- package/lib/packlets/ai-assist/index.js +3 -1
- package/lib/packlets/ai-assist/model.d.ts +66 -5
- package/lib/packlets/ai-assist/registry.d.ts +25 -1
- package/lib/packlets/ai-assist/registry.js +51 -4
- package/lib/packlets/crypto-utils/model.d.ts +92 -0
- package/lib/packlets/crypto-utils/nodeCryptoProvider.d.ts +20 -1
- package/lib/packlets/crypto-utils/nodeCryptoProvider.js +96 -0
- package/package.json +7 -7
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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(`
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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 =
|
|
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
|
-
*
|
|
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 (
|
|
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
|
|
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=${
|
|
828
|
+
logger.info(`AI image generation: provider=${descriptor.id}, format=${capability.format}, ` +
|
|
611
829
|
`model=${config.model}`);
|
|
612
830
|
}
|
|
613
|
-
switch (
|
|
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 =
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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: '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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'] }],
|