@fgv/ts-extras 5.1.0-34 → 5.1.0-36
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 +54 -108
- package/dist/packlets/ai-assist/apiClient.js.map +1 -1
- package/dist/packlets/ai-assist/chatRequestBuilders.js +55 -42
- package/dist/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
- package/dist/packlets/ai-assist/embeddingClient.js +346 -0
- package/dist/packlets/ai-assist/embeddingClient.js.map +1 -0
- package/dist/packlets/ai-assist/http.js +75 -0
- package/dist/packlets/ai-assist/http.js.map +1 -0
- package/dist/packlets/ai-assist/index.js +3 -2
- package/dist/packlets/ai-assist/index.js.map +1 -1
- package/dist/packlets/ai-assist/jsonCompletion.js +6 -8
- package/dist/packlets/ai-assist/jsonCompletion.js.map +1 -1
- package/dist/packlets/ai-assist/model.js +36 -1
- package/dist/packlets/ai-assist/model.js.map +1 -1
- package/dist/packlets/ai-assist/registry.js +77 -7
- package/dist/packlets/ai-assist/registry.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +30 -5
- package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/proxy.js +15 -8
- package/dist/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -1
- package/dist/packlets/ai-assist/streamingClient.js +11 -5
- package/dist/packlets/ai-assist/streamingClient.js.map +1 -1
- package/dist/ts-extras.d.ts +395 -66
- package/lib/packlets/ai-assist/apiClient.d.ts +24 -34
- package/lib/packlets/ai-assist/apiClient.d.ts.map +1 -1
- package/lib/packlets/ai-assist/apiClient.js +64 -118
- package/lib/packlets/ai-assist/apiClient.js.map +1 -1
- package/lib/packlets/ai-assist/chatRequestBuilders.d.ts +56 -20
- package/lib/packlets/ai-assist/chatRequestBuilders.d.ts.map +1 -1
- package/lib/packlets/ai-assist/chatRequestBuilders.js +55 -40
- package/lib/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
- package/lib/packlets/ai-assist/embeddingClient.d.ts +69 -0
- package/lib/packlets/ai-assist/embeddingClient.d.ts.map +1 -0
- package/lib/packlets/ai-assist/embeddingClient.js +350 -0
- package/lib/packlets/ai-assist/embeddingClient.js.map +1 -0
- package/lib/packlets/ai-assist/http.d.ts +24 -0
- package/lib/packlets/ai-assist/http.d.ts.map +1 -0
- package/lib/packlets/ai-assist/http.js +78 -0
- package/lib/packlets/ai-assist/http.js.map +1 -0
- package/lib/packlets/ai-assist/index.d.ts +3 -2
- package/lib/packlets/ai-assist/index.d.ts.map +1 -1
- package/lib/packlets/ai-assist/index.js +7 -1
- package/lib/packlets/ai-assist/index.js.map +1 -1
- package/lib/packlets/ai-assist/jsonCompletion.d.ts.map +1 -1
- package/lib/packlets/ai-assist/jsonCompletion.js +6 -8
- package/lib/packlets/ai-assist/jsonCompletion.js.map +1 -1
- package/lib/packlets/ai-assist/model.d.ts +226 -12
- package/lib/packlets/ai-assist/model.d.ts.map +1 -1
- package/lib/packlets/ai-assist/model.js +37 -2
- package/lib/packlets/ai-assist/model.js.map +1 -1
- package/lib/packlets/ai-assist/registry.d.ts +23 -1
- package/lib/packlets/ai-assist/registry.d.ts.map +1 -1
- package/lib/packlets/ai-assist/registry.js +79 -7
- package/lib/packlets/ai-assist/registry.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts +34 -11
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +30 -5
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/common.d.ts +8 -11
- package/lib/packlets/ai-assist/streamingAdapters/common.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/proxy.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/proxy.js +14 -7
- package/lib/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -1
- package/lib/packlets/ai-assist/streamingClient.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingClient.js +11 -5
- package/lib/packlets/ai-assist/streamingClient.js.map +1 -1
- package/package.json +7 -7
|
@@ -25,8 +25,49 @@
|
|
|
25
25
|
* @packageDocumentation
|
|
26
26
|
*/
|
|
27
27
|
import { isJsonObject } from '@fgv/ts-json-base';
|
|
28
|
-
import { Converters } from '@fgv/ts-utils';
|
|
29
|
-
import { toDataUrl } from './model';
|
|
28
|
+
import { Converters, fail, succeed } from '@fgv/ts-utils';
|
|
29
|
+
import { AiPrompt, toDataUrl } from './model';
|
|
30
|
+
/**
|
|
31
|
+
* Splits a unified {@link AiAssist.IChatRequest} into `{ prompt, head }` for the
|
|
32
|
+
* per-provider builders. The **last** message is the current `user` turn; the
|
|
33
|
+
* preceding messages are history (`head`). This is the single linearization
|
|
34
|
+
* shared by every turn entry point, so the completion path and the client-tool
|
|
35
|
+
* turn path place history at the identical position relative to the current turn.
|
|
36
|
+
*
|
|
37
|
+
* Fails when `messages` is empty (no current turn) or when the last message is
|
|
38
|
+
* not a `user` turn — relabelling a trailing assistant message as the user turn
|
|
39
|
+
* would be a silent footgun, so it is rejected loudly instead.
|
|
40
|
+
*
|
|
41
|
+
* @internal
|
|
42
|
+
*/
|
|
43
|
+
export function splitChatRequest(system, messages) {
|
|
44
|
+
if (messages.length === 0) {
|
|
45
|
+
return fail('messages must contain at least one entry (the current user turn)');
|
|
46
|
+
}
|
|
47
|
+
const current = messages[messages.length - 1];
|
|
48
|
+
if (current.role !== 'user') {
|
|
49
|
+
return fail(`the last message must be the current user turn (role 'user'); got '${current.role}'`);
|
|
50
|
+
}
|
|
51
|
+
const head = messages.slice(0, messages.length - 1);
|
|
52
|
+
const prompt = new AiPrompt(current.content, system !== null && system !== void 0 ? system : '', current.attachments);
|
|
53
|
+
return succeed({ prompt, head });
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Rebuilds the ordered messages for a proxy wire body so that only the current
|
|
57
|
+
* turn can carry attachments — history (non-current) messages are reduced to
|
|
58
|
+
* `{ role, content }`. The direct per-provider builders already drop attachments
|
|
59
|
+
* on history turns (only the current user turn's attachments are honored), so
|
|
60
|
+
* normalizing here keeps the proxy wire shape consistent with the direct paths
|
|
61
|
+
* and avoids transmitting attachment payloads the upstream provider would ignore.
|
|
62
|
+
*
|
|
63
|
+
* @internal
|
|
64
|
+
*/
|
|
65
|
+
export function normalizeOutboundMessages(split) {
|
|
66
|
+
return [
|
|
67
|
+
...split.head.map((m) => ({ role: m.role, content: m.content })),
|
|
68
|
+
...split.prompt.toRequest().messages
|
|
69
|
+
];
|
|
70
|
+
}
|
|
30
71
|
/**
|
|
31
72
|
* Converter for a rawTail message entry. Narrows a `JsonObject` to
|
|
32
73
|
* `{ role: string; content: string | unknown[] }` at runtime using the
|
|
@@ -74,9 +115,10 @@ const geminiRawTailMessageConverter = Converters.object({
|
|
|
74
115
|
parts: Converters.isA('array', (v) => Array.isArray(v))
|
|
75
116
|
}, { strict: false });
|
|
76
117
|
/**
|
|
77
|
-
* Builds the messages array from prompt + optional head
|
|
78
|
-
* The caller supplies the user content
|
|
79
|
-
* for vision prompts) since the parts shape
|
|
118
|
+
* Builds the messages array from prompt + optional history (`head`) and raw
|
|
119
|
+
* continuation (`rawTail`) messages. The caller supplies the user content
|
|
120
|
+
* (string for text-only, parts array for vision prompts) since the parts shape
|
|
121
|
+
* differs by format.
|
|
80
122
|
*
|
|
81
123
|
* `rawTail` items (OpenAI / xAI Responses `function_call` /
|
|
82
124
|
* `function_call_output` continuation items) are appended verbatim after the
|
|
@@ -89,19 +131,12 @@ const geminiRawTailMessageConverter = Converters.object({
|
|
|
89
131
|
*/
|
|
90
132
|
export function buildMessages(systemPrompt, userContent, options) {
|
|
91
133
|
const messages = [{ role: 'system', content: systemPrompt }];
|
|
92
|
-
/* c8 ignore next 4 - head branch: options?.head short-circuit not reached from current call sites */
|
|
93
134
|
if (options === null || options === void 0 ? void 0 : options.head) {
|
|
94
135
|
for (const msg of options.head) {
|
|
95
136
|
messages.push({ role: msg.role, content: msg.content });
|
|
96
137
|
}
|
|
97
138
|
}
|
|
98
139
|
messages.push({ role: 'user', content: userContent });
|
|
99
|
-
/* c8 ignore next 4 - tail branch: options?.tail short-circuit not reached from current call sites */
|
|
100
|
-
if (options === null || options === void 0 ? void 0 : options.tail) {
|
|
101
|
-
for (const msg of options.tail) {
|
|
102
|
-
messages.push({ role: msg.role, content: msg.content });
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
140
|
// OpenAI / xAI Responses continuation items (function_call /
|
|
106
141
|
// function_call_output) are appended verbatim — their field set differs per
|
|
107
142
|
// item type, so the whole object is preserved rather than projected.
|
|
@@ -184,16 +219,15 @@ export function buildGeminiUserParts(prompt) {
|
|
|
184
219
|
return parts;
|
|
185
220
|
}
|
|
186
221
|
/**
|
|
187
|
-
* Builds the Anthropic messages array, weaving any `head` messages
|
|
188
|
-
* implicit system + the prompt's user message and appending `
|
|
189
|
-
* after. System messages are filtered out (Anthropic uses
|
|
190
|
-
* field).
|
|
222
|
+
* Builds the Anthropic messages array, weaving any `head` history messages
|
|
223
|
+
* between implicit system + the prompt's user message and appending `rawTail`
|
|
224
|
+
* continuation messages after. System messages are filtered out (Anthropic uses
|
|
225
|
+
* a top-level system field).
|
|
191
226
|
*
|
|
192
227
|
* @internal
|
|
193
228
|
*/
|
|
194
229
|
export function buildAnthropicMessages(prompt, options) {
|
|
195
230
|
const messages = [];
|
|
196
|
-
/* c8 ignore next 5 - head branch: options?.head short-circuit not reached from current call sites */
|
|
197
231
|
if (options === null || options === void 0 ? void 0 : options.head) {
|
|
198
232
|
for (const msg of options.head) {
|
|
199
233
|
if (msg.role !== 'system') {
|
|
@@ -202,15 +236,6 @@ export function buildAnthropicMessages(prompt, options) {
|
|
|
202
236
|
}
|
|
203
237
|
}
|
|
204
238
|
messages.push({ role: 'user', content: buildAnthropicUserContent(prompt) });
|
|
205
|
-
/* c8 ignore next 5 - tail branch: options?.tail short-circuit not reached from current call sites */
|
|
206
|
-
if (options === null || options === void 0 ? void 0 : options.tail) {
|
|
207
|
-
for (const msg of options.tail) {
|
|
208
|
-
if (msg.role !== 'system') {
|
|
209
|
-
messages.push({ role: msg.role, content: msg.content });
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
/* c8 ignore next 7 - options?.rawTail optional-chain short-circuit (options=undefined) not reached in unit tests */
|
|
214
239
|
if (options === null || options === void 0 ? void 0 : options.rawTail) {
|
|
215
240
|
for (const msg of options.rawTail) {
|
|
216
241
|
const converted = rawTailMessageConverter.convert(msg);
|
|
@@ -222,16 +247,15 @@ export function buildAnthropicMessages(prompt, options) {
|
|
|
222
247
|
return messages;
|
|
223
248
|
}
|
|
224
249
|
/**
|
|
225
|
-
* Builds the Gemini `contents` array, weaving any `head` messages before
|
|
226
|
-
* prompt's user parts and appending `
|
|
227
|
-
* are filtered out (Gemini uses a top-level systemInstruction
|
|
228
|
-
* assistant roles are mapped to Gemini's `model` role.
|
|
250
|
+
* Builds the Gemini `contents` array, weaving any `head` history messages before
|
|
251
|
+
* the prompt's user parts and appending `rawTail` continuation messages after.
|
|
252
|
+
* System messages are filtered out (Gemini uses a top-level systemInstruction
|
|
253
|
+
* field) and assistant roles are mapped to Gemini's `model` role.
|
|
229
254
|
*
|
|
230
255
|
* @internal
|
|
231
256
|
*/
|
|
232
257
|
export function buildGeminiContents(prompt, options) {
|
|
233
258
|
const contents = [];
|
|
234
|
-
/* c8 ignore next 7 - head branch: options?.head short-circuit not reached from current call sites */
|
|
235
259
|
if (options === null || options === void 0 ? void 0 : options.head) {
|
|
236
260
|
for (const msg of options.head) {
|
|
237
261
|
if (msg.role !== 'system') {
|
|
@@ -243,17 +267,6 @@ export function buildGeminiContents(prompt, options) {
|
|
|
243
267
|
}
|
|
244
268
|
}
|
|
245
269
|
contents.push({ role: 'user', parts: buildGeminiUserParts(prompt) });
|
|
246
|
-
/* c8 ignore next 7 - tail branch: options?.tail short-circuit not reached from current call sites */
|
|
247
|
-
if (options === null || options === void 0 ? void 0 : options.tail) {
|
|
248
|
-
for (const msg of options.tail) {
|
|
249
|
-
if (msg.role !== 'system') {
|
|
250
|
-
contents.push({
|
|
251
|
-
role: msg.role === 'assistant' ? 'model' : msg.role,
|
|
252
|
-
parts: [{ text: msg.content }]
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
270
|
// Gemini continuation turns (model `functionCall` parts + user
|
|
258
271
|
// `functionResponse` parts) are projected to `{ role, parts }`.
|
|
259
272
|
if (options === null || options === void 0 ? void 0 : options.rawTail) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chatRequestBuilders.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/chatRequestBuilders.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * Per-format chat request shape builders. Shared between the synchronous\n * (`apiClient.ts`) and streaming (`streamingClient.ts`) paths so the wire\n * shapes stay consistent.\n *\n * @packageDocumentation\n */\n\nimport { isJsonObject, type JsonObject } from '@fgv/ts-json-base';\nimport { type Converter, Converters } from '@fgv/ts-utils';\n\nimport { AiPrompt, type IAiImageAttachment, type IChatMessage, toDataUrl } from './model';\n\n/**\n * Converter for a rawTail message entry. Narrows a `JsonObject` to\n * `{ role: string; content: string | unknown[] }` at runtime using the\n * Converter pattern. Entries that fail validation are silently skipped — the\n * surrounding function is infallible, and a malformed continuation message is\n * better omitted than transmitted verbatim.\n * @internal\n */\nconst rawTailMessageConverter: Converter<{ role: 'user' | 'assistant'; content: string | unknown[] }> =\n Converters.object<{ role: 'user' | 'assistant'; content: string | unknown[] }>(\n {\n role: Converters.enumeratedValue<'user' | 'assistant'>(['user', 'assistant']),\n content: Converters.oneOf<string | unknown[]>([\n Converters.string,\n Converters.isA('array', (v): v is unknown[] => Array.isArray(v))\n ])\n },\n { strict: false }\n );\n\n/**\n * Converter for an OpenAI / xAI Responses API `rawTail` item. These are\n * provider-native input items (`function_call`, `function_call_output`) whose\n * fields differ per item type, so — unlike the Anthropic `{ role, content }`\n * projection — the whole object is preserved verbatim.\n *\n * The static input is already typed `JsonObject`, so the `isJsonObject` guard\n * is a runtime backstop, not a compile-time narrowing: continuation messages\n * originate from a prior turn's `IAiClientToolContinuation.messages` and a\n * consumer may persist and reload them through untyped JSON before passing them\n * back. The guard preserves the same \"a malformed continuation message is\n * better omitted than transmitted verbatim\" posture as the Anthropic path —\n * non-object entries fail conversion and are skipped by the caller.\n * @internal\n */\nconst openAiRawTailItemConverter: Converter<JsonObject> = Converters.isA<JsonObject>(\n 'JsonObject',\n (v): v is JsonObject => isJsonObject(v)\n);\n\n/**\n * Converter for a Gemini `rawTail` item. Gemini continuation messages are\n * `{ role, parts }` turns (a model turn with `functionCall` parts followed by a\n * user turn with `functionResponse` parts). Narrows a `JsonObject` to\n * `{ role: 'user' | 'model'; parts: Array<Record<string, unknown>> }`; entries\n * that fail validation are skipped by the caller.\n * @internal\n */\nconst geminiRawTailMessageConverter: Converter<{\n role: 'user' | 'model';\n parts: unknown[];\n}> = Converters.object<{ role: 'user' | 'model'; parts: unknown[] }>(\n {\n role: Converters.enumeratedValue<'user' | 'model'>(['user', 'model']),\n // `parts` is preserved verbatim and serialized into the request body, so the\n // element shape is not narrowed here — `Array.isArray` soundly guarantees\n // `unknown[]` (narrowing to `Record<string, unknown>[]` would be an unchecked cast).\n parts: Converters.isA('array', (v): v is unknown[] => Array.isArray(v))\n },\n { strict: false }\n);\n\n/**\n * Optional head/tail messages to weave around the prompt's user message.\n *\n * @internal\n */\nexport interface IBuildMessagesOptions {\n /**\n * Messages inserted between the system prompt and the prompt's user\n * message (e.g. prior conversation history for multi-turn chat).\n */\n readonly head?: ReadonlyArray<IChatMessage>;\n /**\n * Messages appended after the prompt's user message (e.g. assistant\n * + correction turns for the JSON-validation retry loop).\n */\n readonly tail?: ReadonlyArray<IChatMessage>;\n /**\n * Raw JSON objects appended after the prompt's user message. Used to\n * inject provider-specific continuation messages (e.g. Anthropic assistant\n * turns with thinking blocks, OpenAI Responses `function_call` /\n * `function_call_output` items, Gemini `functionCall` / `functionResponse`\n * turns) that cannot be expressed as plain {@link IChatMessage} objects.\n *\n * Each builder applies its own provider-specific shape guard:\n * - {@link buildAnthropicMessages} projects each entry to `{ role, content }`.\n * - {@link buildMessages} (OpenAI / xAI Responses) preserves each item\n * verbatim (item fields differ per `type`), guarding only that it is a\n * JSON object.\n * - {@link buildGeminiContents} projects each entry to `{ role, parts }`.\n *\n * Entries that fail their builder's shape check are silently skipped (the\n * caller is responsible for supplying well-formed continuation messages).\n * Takes precedence over (and is appended after) `tail`.\n */\n readonly rawTail?: ReadonlyArray<JsonObject>;\n}\n\n/**\n * Builds the messages array from prompt + optional head/tail messages.\n * The caller supplies the user content (string for text-only, parts array\n * for vision prompts) since the parts shape differs by format.\n *\n * `rawTail` items (OpenAI / xAI Responses `function_call` /\n * `function_call_output` continuation items) are appended verbatim after the\n * user message — their fields differ per item `type`, so they are preserved\n * rather than projected. The return type is `Array<Record<string, unknown>>`\n * to accommodate both `{ role, content }` messages and these heterogeneous\n * input items.\n *\n * @internal\n */\nexport function buildMessages(\n systemPrompt: string,\n userContent: string | unknown[],\n options?: IBuildMessagesOptions\n): Array<Record<string, unknown>> {\n const messages: Array<Record<string, unknown>> = [{ role: 'system', content: systemPrompt }];\n /* c8 ignore next 4 - head branch: options?.head short-circuit not reached from current call sites */\n if (options?.head) {\n for (const msg of options.head) {\n messages.push({ role: msg.role, content: msg.content });\n }\n }\n messages.push({ role: 'user', content: userContent });\n /* c8 ignore next 4 - tail branch: options?.tail short-circuit not reached from current call sites */\n if (options?.tail) {\n for (const msg of options.tail) {\n messages.push({ role: msg.role, content: msg.content });\n }\n }\n // OpenAI / xAI Responses continuation items (function_call /\n // function_call_output) are appended verbatim — their field set differs per\n // item type, so the whole object is preserved rather than projected.\n if (options?.rawTail) {\n for (const item of options.rawTail) {\n const converted = openAiRawTailItemConverter.convert(item);\n if (converted.isSuccess()) {\n messages.push(converted.value);\n }\n }\n }\n return messages;\n}\n\n/**\n * Builds the user content for OpenAI Chat Completions when attachments are\n * present. Returns a string when there are no attachments.\n *\n * @internal\n */\nexport function buildOpenAiChatUserContent(prompt: AiPrompt): string | unknown[] {\n if (prompt.attachments.length === 0) {\n return prompt.user;\n }\n return [\n { type: 'text', text: prompt.user },\n ...prompt.attachments.map((att: IAiImageAttachment) => ({\n type: 'image_url',\n image_url: {\n url: toDataUrl(att),\n ...(att.detail !== undefined ? { detail: att.detail } : {})\n }\n }))\n ];\n}\n\n/**\n * Builds the user content for OpenAI / xAI Responses API when attachments\n * are present. Responses API uses `input_text` / `input_image` part types,\n * distinct from Chat Completions' `text` / `image_url`.\n *\n * @internal\n */\nexport function buildOpenAiResponsesUserContent(prompt: AiPrompt): string | unknown[] {\n if (prompt.attachments.length === 0) {\n return prompt.user;\n }\n return [\n { type: 'input_text', text: prompt.user },\n ...prompt.attachments.map((att: IAiImageAttachment) => ({\n type: 'input_image',\n image_url: toDataUrl(att),\n ...(att.detail !== undefined ? { detail: att.detail } : {})\n }))\n ];\n}\n\n/**\n * Builds the user-message content for Anthropic when attachments are present.\n *\n * @internal\n */\nexport function buildAnthropicUserContent(prompt: AiPrompt): string | unknown[] {\n if (prompt.attachments.length === 0) {\n return prompt.user;\n }\n return [\n { type: 'text', text: prompt.user },\n ...prompt.attachments.map((att: IAiImageAttachment) => ({\n type: 'image',\n source: {\n type: 'base64',\n media_type: att.mimeType,\n data: att.base64\n }\n }))\n ];\n}\n\n/**\n * Builds the Gemini `parts` array for the user turn, including any image\n * attachments as `inlineData` parts.\n *\n * @internal\n */\nexport function buildGeminiUserParts(prompt: AiPrompt): Array<Record<string, unknown>> {\n const parts: Array<Record<string, unknown>> = [{ text: prompt.user }];\n for (const att of prompt.attachments) {\n parts.push({ inlineData: { mimeType: att.mimeType, data: att.base64 } });\n }\n return parts;\n}\n\n/**\n * Builds the Anthropic messages array, weaving any `head` messages between\n * implicit system + the prompt's user message and appending `tail` messages\n * after. System messages are filtered out (Anthropic uses a top-level system\n * field).\n *\n * @internal\n */\nexport function buildAnthropicMessages(\n prompt: AiPrompt,\n options?: IBuildMessagesOptions\n): Array<{ role: string; content: string | unknown[] }> {\n const messages: Array<{ role: string; content: string | unknown[] }> = [];\n /* c8 ignore next 5 - head branch: options?.head short-circuit not reached from current call sites */\n if (options?.head) {\n for (const msg of options.head) {\n if (msg.role !== 'system') {\n messages.push({ role: msg.role, content: msg.content });\n }\n }\n }\n messages.push({ role: 'user', content: buildAnthropicUserContent(prompt) });\n /* c8 ignore next 5 - tail branch: options?.tail short-circuit not reached from current call sites */\n if (options?.tail) {\n for (const msg of options.tail) {\n if (msg.role !== 'system') {\n messages.push({ role: msg.role, content: msg.content });\n }\n }\n }\n /* c8 ignore next 7 - options?.rawTail optional-chain short-circuit (options=undefined) not reached in unit tests */\n if (options?.rawTail) {\n for (const msg of options.rawTail) {\n const converted = rawTailMessageConverter.convert(msg);\n if (converted.isSuccess()) {\n messages.push(converted.value);\n }\n }\n }\n return messages;\n}\n\n/**\n * Builds the Gemini `contents` array, weaving any `head` messages before the\n * prompt's user parts and appending `tail` messages after. System messages\n * are filtered out (Gemini uses a top-level systemInstruction field) and\n * assistant roles are mapped to Gemini's `model` role.\n *\n * @internal\n */\nexport function buildGeminiContents(\n prompt: AiPrompt,\n options?: IBuildMessagesOptions\n): Array<{ role: string; parts: unknown[] }> {\n const contents: Array<{ role: string; parts: unknown[] }> = [];\n /* c8 ignore next 7 - head branch: options?.head short-circuit not reached from current call sites */\n if (options?.head) {\n for (const msg of options.head) {\n if (msg.role !== 'system') {\n contents.push({\n role: msg.role === 'assistant' ? 'model' : msg.role,\n parts: [{ text: msg.content }]\n });\n }\n }\n }\n contents.push({ role: 'user', parts: buildGeminiUserParts(prompt) });\n /* c8 ignore next 7 - tail branch: options?.tail short-circuit not reached from current call sites */\n if (options?.tail) {\n for (const msg of options.tail) {\n if (msg.role !== 'system') {\n contents.push({\n role: msg.role === 'assistant' ? 'model' : msg.role,\n parts: [{ text: msg.content }]\n });\n }\n }\n }\n // Gemini continuation turns (model `functionCall` parts + user\n // `functionResponse` parts) are projected to `{ role, parts }`.\n if (options?.rawTail) {\n for (const item of options.rawTail) {\n const converted = geminiRawTailMessageConverter.convert(item);\n if (converted.isSuccess()) {\n contents.push(converted.value);\n }\n }\n }\n return contents;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"chatRequestBuilders.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/chatRequestBuilders.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * Per-format chat request shape builders. Shared between the synchronous\n * (`apiClient.ts`) and streaming (`streamingClient.ts`) paths so the wire\n * shapes stay consistent.\n *\n * @packageDocumentation\n */\n\nimport { isJsonObject, type JsonObject } from '@fgv/ts-json-base';\nimport { type Converter, Converters, fail, Result, succeed } from '@fgv/ts-utils';\n\nimport { AiPrompt, type IAiImageAttachment, type IChatMessage, toDataUrl } from './model';\n\n/**\n * The result of splitting an {@link AiAssist.IChatRequest} into the per-builder\n * inputs: the current turn (as an {@link AiPrompt}, carrying system + the final\n * user message + any attachments) and the preceding conversation history.\n * @internal\n */\nexport interface ISplitChatRequest {\n /** The current turn lowered to an {@link AiPrompt} (system + user + attachments). */\n readonly prompt: AiPrompt;\n /** Prior conversation history — every message before the current turn. */\n readonly head: ReadonlyArray<IChatMessage>;\n}\n\n/**\n * Splits a unified {@link AiAssist.IChatRequest} into `{ prompt, head }` for the\n * per-provider builders. The **last** message is the current `user` turn; the\n * preceding messages are history (`head`). This is the single linearization\n * shared by every turn entry point, so the completion path and the client-tool\n * turn path place history at the identical position relative to the current turn.\n *\n * Fails when `messages` is empty (no current turn) or when the last message is\n * not a `user` turn — relabelling a trailing assistant message as the user turn\n * would be a silent footgun, so it is rejected loudly instead.\n *\n * @internal\n */\nexport function splitChatRequest(\n system: string | undefined,\n messages: ReadonlyArray<IChatMessage>\n): Result<ISplitChatRequest> {\n if (messages.length === 0) {\n return fail('messages must contain at least one entry (the current user turn)');\n }\n const current = messages[messages.length - 1];\n if (current.role !== 'user') {\n return fail(`the last message must be the current user turn (role 'user'); got '${current.role}'`);\n }\n const head = messages.slice(0, messages.length - 1);\n const prompt = new AiPrompt(current.content, system ?? '', current.attachments);\n return succeed({ prompt, head });\n}\n\n/**\n * Rebuilds the ordered messages for a proxy wire body so that only the current\n * turn can carry attachments — history (non-current) messages are reduced to\n * `{ role, content }`. The direct per-provider builders already drop attachments\n * on history turns (only the current user turn's attachments are honored), so\n * normalizing here keeps the proxy wire shape consistent with the direct paths\n * and avoids transmitting attachment payloads the upstream provider would ignore.\n *\n * @internal\n */\nexport function normalizeOutboundMessages(split: ISplitChatRequest): IChatMessage[] {\n return [\n ...split.head.map((m) => ({ role: m.role, content: m.content })),\n ...split.prompt.toRequest().messages\n ];\n}\n\n/**\n * Converter for a rawTail message entry. Narrows a `JsonObject` to\n * `{ role: string; content: string | unknown[] }` at runtime using the\n * Converter pattern. Entries that fail validation are silently skipped — the\n * surrounding function is infallible, and a malformed continuation message is\n * better omitted than transmitted verbatim.\n * @internal\n */\nconst rawTailMessageConverter: Converter<{ role: 'user' | 'assistant'; content: string | unknown[] }> =\n Converters.object<{ role: 'user' | 'assistant'; content: string | unknown[] }>(\n {\n role: Converters.enumeratedValue<'user' | 'assistant'>(['user', 'assistant']),\n content: Converters.oneOf<string | unknown[]>([\n Converters.string,\n Converters.isA('array', (v): v is unknown[] => Array.isArray(v))\n ])\n },\n { strict: false }\n );\n\n/**\n * Converter for an OpenAI / xAI Responses API `rawTail` item. These are\n * provider-native input items (`function_call`, `function_call_output`) whose\n * fields differ per item type, so — unlike the Anthropic `{ role, content }`\n * projection — the whole object is preserved verbatim.\n *\n * The static input is already typed `JsonObject`, so the `isJsonObject` guard\n * is a runtime backstop, not a compile-time narrowing: continuation messages\n * originate from a prior turn's `IAiClientToolContinuation.messages` and a\n * consumer may persist and reload them through untyped JSON before passing them\n * back. The guard preserves the same \"a malformed continuation message is\n * better omitted than transmitted verbatim\" posture as the Anthropic path —\n * non-object entries fail conversion and are skipped by the caller.\n * @internal\n */\nconst openAiRawTailItemConverter: Converter<JsonObject> = Converters.isA<JsonObject>(\n 'JsonObject',\n (v): v is JsonObject => isJsonObject(v)\n);\n\n/**\n * Converter for a Gemini `rawTail` item. Gemini continuation messages are\n * `{ role, parts }` turns (a model turn with `functionCall` parts followed by a\n * user turn with `functionResponse` parts). Narrows a `JsonObject` to\n * `{ role: 'user' | 'model'; parts: Array<Record<string, unknown>> }`; entries\n * that fail validation are skipped by the caller.\n * @internal\n */\nconst geminiRawTailMessageConverter: Converter<{\n role: 'user' | 'model';\n parts: unknown[];\n}> = Converters.object<{ role: 'user' | 'model'; parts: unknown[] }>(\n {\n role: Converters.enumeratedValue<'user' | 'model'>(['user', 'model']),\n // `parts` is preserved verbatim and serialized into the request body, so the\n // element shape is not narrowed here — `Array.isArray` soundly guarantees\n // `unknown[]` (narrowing to `Record<string, unknown>[]` would be an unchecked cast).\n parts: Converters.isA('array', (v): v is unknown[] => Array.isArray(v))\n },\n { strict: false }\n);\n\n/**\n * Optional history (`head`) and raw continuation (`rawTail`) messages to weave\n * around the prompt's current user message.\n *\n * @internal\n */\nexport interface IBuildMessagesOptions {\n /**\n * Prior conversation history inserted between the system prompt and the\n * prompt's current user message (multi-turn chat / correction retries). The\n * single ordered linearization is `[system, ...head, user, ...rawTail]`.\n */\n readonly head?: ReadonlyArray<IChatMessage>;\n /**\n * Raw JSON objects appended after the prompt's user message. Used to\n * inject provider-specific continuation messages (e.g. Anthropic assistant\n * turns with thinking blocks, OpenAI Responses `function_call` /\n * `function_call_output` items, Gemini `functionCall` / `functionResponse`\n * turns) that cannot be expressed as plain {@link IChatMessage} objects.\n *\n * Each builder applies its own provider-specific shape guard:\n * - {@link buildAnthropicMessages} projects each entry to `{ role, content }`.\n * - {@link buildMessages} (OpenAI / xAI Responses) preserves each item\n * verbatim (item fields differ per `type`), guarding only that it is a\n * JSON object.\n * - {@link buildGeminiContents} projects each entry to `{ role, parts }`.\n *\n * Entries that fail their builder's shape check are silently skipped (the\n * caller is responsible for supplying well-formed continuation messages).\n * Appended after the current user message.\n */\n readonly rawTail?: ReadonlyArray<JsonObject>;\n}\n\n/**\n * Builds the messages array from prompt + optional history (`head`) and raw\n * continuation (`rawTail`) messages. The caller supplies the user content\n * (string for text-only, parts array for vision prompts) since the parts shape\n * differs by format.\n *\n * `rawTail` items (OpenAI / xAI Responses `function_call` /\n * `function_call_output` continuation items) are appended verbatim after the\n * user message — their fields differ per item `type`, so they are preserved\n * rather than projected. The return type is `Array<Record<string, unknown>>`\n * to accommodate both `{ role, content }` messages and these heterogeneous\n * input items.\n *\n * @internal\n */\nexport function buildMessages(\n systemPrompt: string,\n userContent: string | unknown[],\n options?: IBuildMessagesOptions\n): Array<Record<string, unknown>> {\n const messages: Array<Record<string, unknown>> = [{ role: 'system', content: systemPrompt }];\n if (options?.head) {\n for (const msg of options.head) {\n messages.push({ role: msg.role, content: msg.content });\n }\n }\n messages.push({ role: 'user', content: userContent });\n // OpenAI / xAI Responses continuation items (function_call /\n // function_call_output) are appended verbatim — their field set differs per\n // item type, so the whole object is preserved rather than projected.\n if (options?.rawTail) {\n for (const item of options.rawTail) {\n const converted = openAiRawTailItemConverter.convert(item);\n if (converted.isSuccess()) {\n messages.push(converted.value);\n }\n }\n }\n return messages;\n}\n\n/**\n * Builds the user content for OpenAI Chat Completions when attachments are\n * present. Returns a string when there are no attachments.\n *\n * @internal\n */\nexport function buildOpenAiChatUserContent(prompt: AiPrompt): string | unknown[] {\n if (prompt.attachments.length === 0) {\n return prompt.user;\n }\n return [\n { type: 'text', text: prompt.user },\n ...prompt.attachments.map((att: IAiImageAttachment) => ({\n type: 'image_url',\n image_url: {\n url: toDataUrl(att),\n ...(att.detail !== undefined ? { detail: att.detail } : {})\n }\n }))\n ];\n}\n\n/**\n * Builds the user content for OpenAI / xAI Responses API when attachments\n * are present. Responses API uses `input_text` / `input_image` part types,\n * distinct from Chat Completions' `text` / `image_url`.\n *\n * @internal\n */\nexport function buildOpenAiResponsesUserContent(prompt: AiPrompt): string | unknown[] {\n if (prompt.attachments.length === 0) {\n return prompt.user;\n }\n return [\n { type: 'input_text', text: prompt.user },\n ...prompt.attachments.map((att: IAiImageAttachment) => ({\n type: 'input_image',\n image_url: toDataUrl(att),\n ...(att.detail !== undefined ? { detail: att.detail } : {})\n }))\n ];\n}\n\n/**\n * Builds the user-message content for Anthropic when attachments are present.\n *\n * @internal\n */\nexport function buildAnthropicUserContent(prompt: AiPrompt): string | unknown[] {\n if (prompt.attachments.length === 0) {\n return prompt.user;\n }\n return [\n { type: 'text', text: prompt.user },\n ...prompt.attachments.map((att: IAiImageAttachment) => ({\n type: 'image',\n source: {\n type: 'base64',\n media_type: att.mimeType,\n data: att.base64\n }\n }))\n ];\n}\n\n/**\n * Builds the Gemini `parts` array for the user turn, including any image\n * attachments as `inlineData` parts.\n *\n * @internal\n */\nexport function buildGeminiUserParts(prompt: AiPrompt): Array<Record<string, unknown>> {\n const parts: Array<Record<string, unknown>> = [{ text: prompt.user }];\n for (const att of prompt.attachments) {\n parts.push({ inlineData: { mimeType: att.mimeType, data: att.base64 } });\n }\n return parts;\n}\n\n/**\n * Builds the Anthropic messages array, weaving any `head` history messages\n * between implicit system + the prompt's user message and appending `rawTail`\n * continuation messages after. System messages are filtered out (Anthropic uses\n * a top-level system field).\n *\n * @internal\n */\nexport function buildAnthropicMessages(\n prompt: AiPrompt,\n options?: IBuildMessagesOptions\n): Array<{ role: string; content: string | unknown[] }> {\n const messages: Array<{ role: string; content: string | unknown[] }> = [];\n if (options?.head) {\n for (const msg of options.head) {\n if (msg.role !== 'system') {\n messages.push({ role: msg.role, content: msg.content });\n }\n }\n }\n messages.push({ role: 'user', content: buildAnthropicUserContent(prompt) });\n if (options?.rawTail) {\n for (const msg of options.rawTail) {\n const converted = rawTailMessageConverter.convert(msg);\n if (converted.isSuccess()) {\n messages.push(converted.value);\n }\n }\n }\n return messages;\n}\n\n/**\n * Builds the Gemini `contents` array, weaving any `head` history messages before\n * the prompt's user parts and appending `rawTail` continuation messages after.\n * System messages are filtered out (Gemini uses a top-level systemInstruction\n * field) and assistant roles are mapped to Gemini's `model` role.\n *\n * @internal\n */\nexport function buildGeminiContents(\n prompt: AiPrompt,\n options?: IBuildMessagesOptions\n): Array<{ role: string; parts: unknown[] }> {\n const contents: Array<{ role: string; parts: unknown[] }> = [];\n if (options?.head) {\n for (const msg of options.head) {\n if (msg.role !== 'system') {\n contents.push({\n role: msg.role === 'assistant' ? 'model' : msg.role,\n parts: [{ text: msg.content }]\n });\n }\n }\n }\n contents.push({ role: 'user', parts: buildGeminiUserParts(prompt) });\n // Gemini continuation turns (model `functionCall` parts + user\n // `functionResponse` parts) are projected to `{ role, parts }`.\n if (options?.rawTail) {\n for (const item of options.rawTail) {\n const converted = geminiRawTailMessageConverter.convert(item);\n if (converted.isSuccess()) {\n contents.push(converted.value);\n }\n }\n }\n return contents;\n}\n"]}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
// Copyright (c) 2026 Erik Fortune
|
|
2
|
+
//
|
|
3
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
// of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
// in the Software without restriction, including without limitation the rights
|
|
6
|
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
// copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
// furnished to do so, subject to the following conditions:
|
|
9
|
+
//
|
|
10
|
+
// The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
// copies or substantial portions of the Software.
|
|
12
|
+
//
|
|
13
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
// SOFTWARE.
|
|
20
|
+
/**
|
|
21
|
+
* Cross-provider embedding client for AI assist. Mirrors the completion and
|
|
22
|
+
* image-generation primitives in `apiClient.ts`: a single dispatcher
|
|
23
|
+
* ({@link AiAssist.callProviderEmbedding}) resolves the provider descriptor's
|
|
24
|
+
* embedding capability and routes to the per-format adapter. `text -> vector`,
|
|
25
|
+
* batch in, `number[][]` out.
|
|
26
|
+
*
|
|
27
|
+
* @packageDocumentation
|
|
28
|
+
*/
|
|
29
|
+
import { fail, succeed, Validators } from '@fgv/ts-utils';
|
|
30
|
+
import { resolveModel } from './model';
|
|
31
|
+
import { bearerAuthHeader, resolveEffectiveBaseUrl } from './endpoint';
|
|
32
|
+
import { resolveEmbeddingCapability, supportsEmbedding } from './registry';
|
|
33
|
+
import { fetchJson } from './http';
|
|
34
|
+
const openAiEmbeddingItem = Validators.object({
|
|
35
|
+
index: Validators.number,
|
|
36
|
+
embedding: Validators.arrayOf(Validators.number)
|
|
37
|
+
});
|
|
38
|
+
const openAiEmbeddingUsage = Validators.object({
|
|
39
|
+
prompt_tokens: Validators.number.optional(),
|
|
40
|
+
total_tokens: Validators.number.optional()
|
|
41
|
+
});
|
|
42
|
+
const openAiEmbeddingResponse = Validators.object({
|
|
43
|
+
data: Validators.arrayOf(openAiEmbeddingItem).withConstraint((arr) => arr.length > 0),
|
|
44
|
+
model: Validators.string.optional(),
|
|
45
|
+
usage: openAiEmbeddingUsage.optional()
|
|
46
|
+
});
|
|
47
|
+
// ============================================================================
|
|
48
|
+
// Shared helpers
|
|
49
|
+
// ============================================================================
|
|
50
|
+
/**
|
|
51
|
+
* Normalizes the `input` field (string | string[]) into a concrete string array.
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
function toInputArray(input) {
|
|
55
|
+
return typeof input === 'string' ? [input] : input;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Builds an {@link IAiEmbeddingUsage} from OpenAI-format usage, or `undefined`
|
|
59
|
+
* when no token counts are reported.
|
|
60
|
+
* @internal
|
|
61
|
+
*/
|
|
62
|
+
function toEmbeddingUsage(usage) {
|
|
63
|
+
if (usage === undefined || (usage.prompt_tokens === undefined && usage.total_tokens === undefined)) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
return Object.assign(Object.assign({}, (usage.prompt_tokens !== undefined ? { promptTokens: usage.prompt_tokens } : {})), (usage.total_tokens !== undefined ? { totalTokens: usage.total_tokens } : {}));
|
|
67
|
+
}
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// OpenAI-format adapter
|
|
70
|
+
// ============================================================================
|
|
71
|
+
/**
|
|
72
|
+
* Calls the OpenAI `/v1/embeddings` endpoint. Serves OpenAI, Ollama (via `/v1`),
|
|
73
|
+
* openai-compat self-hosted servers, and Mistral. Sends `dimensions` only when
|
|
74
|
+
* the capability declares `supportsDimensions`; always requests
|
|
75
|
+
* `encoding_format: 'float'`.
|
|
76
|
+
*
|
|
77
|
+
* @remarks
|
|
78
|
+
* `encoding_format: 'float'` is a known assumption (design §14 #5): a strict
|
|
79
|
+
* openai-compat server could in principle reject the field, but mainstream
|
|
80
|
+
* servers accept it and it keeps us off the base64 decode path. Low risk.
|
|
81
|
+
*
|
|
82
|
+
* @internal
|
|
83
|
+
*/
|
|
84
|
+
async function callOpenAiEmbeddings(config, inputs, request, capability, logger, signal) {
|
|
85
|
+
const body = {
|
|
86
|
+
model: config.model,
|
|
87
|
+
input: inputs,
|
|
88
|
+
encoding_format: 'float'
|
|
89
|
+
};
|
|
90
|
+
if (capability.supportsDimensions && request.dimensions !== undefined) {
|
|
91
|
+
body.dimensions = request.dimensions;
|
|
92
|
+
}
|
|
93
|
+
const headers = bearerAuthHeader(config.apiKey);
|
|
94
|
+
/* c8 ignore next 1 - optional logger */
|
|
95
|
+
logger === null || logger === void 0 ? void 0 : logger.info(`AI embedding: format=openai-embeddings, model=${config.model}, inputs=${inputs.length}`);
|
|
96
|
+
const jsonResult = await fetchJson(`${config.baseUrl}/embeddings`, headers, body, logger, signal);
|
|
97
|
+
if (jsonResult.isFailure()) {
|
|
98
|
+
return fail(jsonResult.message);
|
|
99
|
+
}
|
|
100
|
+
return openAiEmbeddingResponse
|
|
101
|
+
.validate(jsonResult.value)
|
|
102
|
+
.withErrorFormat((msg) => `OpenAI embeddings API response: ${msg}`)
|
|
103
|
+
.onSuccess((response) => {
|
|
104
|
+
var _a;
|
|
105
|
+
// The spec allows `data` in any order; align to request order by `index`.
|
|
106
|
+
const ordered = [...response.data].sort((a, b) => a.index - b.index);
|
|
107
|
+
// Validate the response is well-formed against the request before trusting
|
|
108
|
+
// the alignment: a compat server could return an incomplete batch, gapped
|
|
109
|
+
// or duplicate indices, or ragged dimensions — all of which would silently
|
|
110
|
+
// yield misaligned/partial vectors. (inputs is non-empty here; the
|
|
111
|
+
// empty-input case short-circuits before the wire call.)
|
|
112
|
+
if (ordered.length !== inputs.length) {
|
|
113
|
+
return fail(`OpenAI embeddings API response: expected ${inputs.length} embedding(s), got ${ordered.length}`);
|
|
114
|
+
}
|
|
115
|
+
// After sorting, a perfect 0..n-1 sequence has item.index === position at
|
|
116
|
+
// every slot. Any gap OR duplicate shifts a later element off its position
|
|
117
|
+
// (the second copy of value k, or the element after a missing k, lands at
|
|
118
|
+
// position > its index), so this single walk catches both.
|
|
119
|
+
const misindexed = ordered.findIndex((item, i) => item.index !== i);
|
|
120
|
+
if (misindexed !== -1) {
|
|
121
|
+
return fail(`OpenAI embeddings API response: malformed embedding indices ` +
|
|
122
|
+
`(expected 0..${inputs.length - 1}; gap or duplicate at position ${misindexed})`);
|
|
123
|
+
}
|
|
124
|
+
const vectors = ordered.map((item) => item.embedding);
|
|
125
|
+
const dimensions = vectors[0].length;
|
|
126
|
+
const ragged = vectors.findIndex((v) => v.length !== dimensions);
|
|
127
|
+
if (ragged !== -1) {
|
|
128
|
+
return fail(`OpenAI embeddings API response: inconsistent vector dimensionality ` +
|
|
129
|
+
`(vector ${ragged} has length ${vectors[ragged].length}, expected ${dimensions})`);
|
|
130
|
+
}
|
|
131
|
+
const usage = toEmbeddingUsage(response.usage);
|
|
132
|
+
return succeed(Object.assign({ vectors, model: (_a = response.model) !== null && _a !== void 0 ? _a : config.model, dimensions }, (usage !== undefined ? { usage } : {})));
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
const geminiEmbedding = Validators.object({
|
|
136
|
+
values: Validators.arrayOf(Validators.number)
|
|
137
|
+
});
|
|
138
|
+
const geminiEmbeddingResponse = Validators.object({
|
|
139
|
+
embeddings: Validators.arrayOf(geminiEmbedding).withConstraint((arr) => arr.length > 0)
|
|
140
|
+
});
|
|
141
|
+
/**
|
|
142
|
+
* Maps a cross-provider {@link AiAssist.AiEmbeddingTaskType} (kebab-case) to the
|
|
143
|
+
* Gemini wire form (`SCREAMING_SNAKE_CASE`), e.g. `'retrieval-document'` becomes
|
|
144
|
+
* `'RETRIEVAL_DOCUMENT'`.
|
|
145
|
+
* @internal
|
|
146
|
+
*/
|
|
147
|
+
function toGeminiTaskType(taskType) {
|
|
148
|
+
return taskType.replace(/-/g, '_').toUpperCase();
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Strips a leading `models/` from a model id so the URL path and the per-request
|
|
152
|
+
* `model` field can each apply the prefix exactly once.
|
|
153
|
+
* @internal
|
|
154
|
+
*/
|
|
155
|
+
function bareGeminiModel(model) {
|
|
156
|
+
return model.startsWith('models/') ? model.slice('models/'.length) : model;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Calls the Google Gemini `:batchEmbedContents` endpoint. Always uses the batch
|
|
160
|
+
* route (a single input is a one-element batch). Sends `taskType` /
|
|
161
|
+
* `outputDimensionality` only when the capability declares support. Gemini does
|
|
162
|
+
* not report token usage for embeddings, so `usage` is omitted.
|
|
163
|
+
* @internal
|
|
164
|
+
*/
|
|
165
|
+
async function callGeminiEmbeddings(config, inputs, request, capability, logger, signal) {
|
|
166
|
+
const bare = bareGeminiModel(config.model);
|
|
167
|
+
const qualified = `models/${bare}`;
|
|
168
|
+
// `request.taskType` is the open `AiEmbeddingTaskType` union (includes `string & {}`),
|
|
169
|
+
// which `!== undefined` cannot narrow to `string`; resolve it cast-free here.
|
|
170
|
+
const taskType = capability.supportsTaskType && request.taskType !== undefined
|
|
171
|
+
? toGeminiTaskType(request.taskType)
|
|
172
|
+
: undefined;
|
|
173
|
+
const sendDimensions = capability.supportsDimensions && request.dimensions !== undefined;
|
|
174
|
+
const requests = inputs.map((text) => (Object.assign(Object.assign({ model: qualified, content: { parts: [{ text }] } }, (taskType !== undefined ? { taskType } : {})), (sendDimensions ? { outputDimensionality: request.dimensions } : {}))));
|
|
175
|
+
const body = { requests };
|
|
176
|
+
const headers = { 'x-goog-api-key': config.apiKey };
|
|
177
|
+
const url = `${config.baseUrl}/models/${bare}:batchEmbedContents`;
|
|
178
|
+
/* c8 ignore next 1 - optional logger */
|
|
179
|
+
logger === null || logger === void 0 ? void 0 : logger.info(`AI embedding: format=gemini-embeddings, model=${bare}, inputs=${inputs.length}`);
|
|
180
|
+
const jsonResult = await fetchJson(url, headers, body, logger, signal);
|
|
181
|
+
if (jsonResult.isFailure()) {
|
|
182
|
+
return fail(jsonResult.message);
|
|
183
|
+
}
|
|
184
|
+
return geminiEmbeddingResponse
|
|
185
|
+
.validate(jsonResult.value)
|
|
186
|
+
.withErrorFormat((msg) => `Gemini embeddings API response: ${msg}`)
|
|
187
|
+
.onSuccess((response) => {
|
|
188
|
+
// Gemini returns embeddings aligned to request order (no index field), so
|
|
189
|
+
// there is no per-item index to re-sort or gap-check (unlike the OpenAI
|
|
190
|
+
// path). Validate the response is well-formed against the request before
|
|
191
|
+
// trusting the positional alignment: a truncated batch or ragged
|
|
192
|
+
// dimensions would silently yield misaligned/partial vectors. (inputs is
|
|
193
|
+
// non-empty here; the empty-input case short-circuits before the wire call.)
|
|
194
|
+
const vectors = response.embeddings.map((e) => e.values);
|
|
195
|
+
if (vectors.length !== inputs.length) {
|
|
196
|
+
return fail(`Gemini embeddings API response: expected ${inputs.length} embedding(s), got ${vectors.length}`);
|
|
197
|
+
}
|
|
198
|
+
const dimensions = vectors[0].length;
|
|
199
|
+
const ragged = vectors.findIndex((v) => v.length !== dimensions);
|
|
200
|
+
if (ragged !== -1) {
|
|
201
|
+
return fail(`Gemini embeddings API response: inconsistent vector dimensionality ` +
|
|
202
|
+
`(vector ${ragged} has length ${vectors[ragged].length}, expected ${dimensions})`);
|
|
203
|
+
}
|
|
204
|
+
const result = { vectors, model: bare, dimensions };
|
|
205
|
+
return succeed(result);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
// ============================================================================
|
|
209
|
+
// Dispatcher
|
|
210
|
+
// ============================================================================
|
|
211
|
+
/**
|
|
212
|
+
* Notes any caller-supplied embedding knobs that the resolved capability does
|
|
213
|
+
* not honor. Per design §7 these are a no-op (logged, never a failure) so a
|
|
214
|
+
* cross-provider call site can pass `taskType`/`dimensions` once and have them
|
|
215
|
+
* apply only where supported.
|
|
216
|
+
* @internal
|
|
217
|
+
*/
|
|
218
|
+
function noteUnsupportedKnobs(model, request, capability, logger) {
|
|
219
|
+
if (logger === undefined) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (request.taskType !== undefined && !capability.supportsTaskType) {
|
|
223
|
+
logger.info(`AI embedding: model "${model}" ignores taskType (not supported); proceeding without it`);
|
|
224
|
+
}
|
|
225
|
+
if (request.dimensions !== undefined && !capability.supportsDimensions) {
|
|
226
|
+
logger.info(`AI embedding: model "${model}" ignores dimensions (not supported); proceeding without it`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Calls the appropriate embedding API for a given provider. Routes by the
|
|
231
|
+
* `format` of the resolved {@link AiAssist.IAiEmbeddingModelCapability}:
|
|
232
|
+
* `'openai-embeddings'` or `'gemini-embeddings'`.
|
|
233
|
+
*
|
|
234
|
+
* @remarks
|
|
235
|
+
* - Rejects up front when the provider declares no embedding capability, when no
|
|
236
|
+
* embedding model resolves, or when the batch exceeds the capability's
|
|
237
|
+
* `maxBatchSize` (no auto-chunking).
|
|
238
|
+
* - An empty `input` array short-circuits to an empty result with no wire call
|
|
239
|
+
* (most providers HTTP-400 on empty input).
|
|
240
|
+
* - Caller-supplied `dimensions`/`taskType` that the model doesn't support are a
|
|
241
|
+
* no-op (logged), not a failure (design §7).
|
|
242
|
+
*
|
|
243
|
+
* @param params - Request parameters including descriptor, API key, and input.
|
|
244
|
+
* @returns The embedding vectors aligned to input order, or a failure.
|
|
245
|
+
* @public
|
|
246
|
+
*/
|
|
247
|
+
export async function callProviderEmbedding(params) {
|
|
248
|
+
const { descriptor, apiKey, params: request, modelOverride, logger, signal, endpoint } = params;
|
|
249
|
+
if (!supportsEmbedding(descriptor)) {
|
|
250
|
+
return fail(`provider "${descriptor.id}" does not support embeddings`);
|
|
251
|
+
}
|
|
252
|
+
const model = resolveModel(modelOverride !== null && modelOverride !== void 0 ? modelOverride : descriptor.defaultModel, 'embedding');
|
|
253
|
+
if (model.length === 0) {
|
|
254
|
+
return fail(`provider "${descriptor.id}": no embedding model resolved; ` +
|
|
255
|
+
`pass modelOverride or set descriptor.defaultModel ` +
|
|
256
|
+
`(a plain string, or an object with an "embedding" entry)`);
|
|
257
|
+
}
|
|
258
|
+
const capability = resolveEmbeddingCapability(descriptor, model);
|
|
259
|
+
if (capability === undefined) {
|
|
260
|
+
return fail(`provider "${descriptor.id}" does not support embeddings for model "${model}"`);
|
|
261
|
+
}
|
|
262
|
+
const inputs = toInputArray(request.input);
|
|
263
|
+
if (inputs.length === 0) {
|
|
264
|
+
// Short-circuit: empty batch never hits the wire (design §14 #2).
|
|
265
|
+
return succeed({ vectors: [], model, dimensions: 0 });
|
|
266
|
+
}
|
|
267
|
+
if (capability.maxBatchSize !== undefined && inputs.length > capability.maxBatchSize) {
|
|
268
|
+
return fail(`provider "${descriptor.id}": embedding batch of ${inputs.length} exceeds ` +
|
|
269
|
+
`model "${model}" maximum of ${capability.maxBatchSize}`);
|
|
270
|
+
}
|
|
271
|
+
const baseUrlResult = resolveEffectiveBaseUrl(descriptor, endpoint);
|
|
272
|
+
if (baseUrlResult.isFailure()) {
|
|
273
|
+
return fail(baseUrlResult.message);
|
|
274
|
+
}
|
|
275
|
+
noteUnsupportedKnobs(model, request, capability, logger);
|
|
276
|
+
const config = { baseUrl: baseUrlResult.value, apiKey, model };
|
|
277
|
+
return dispatchEmbedding(capability.format, config, inputs, request, capability, logger, signal);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Routes a resolved embedding request to the per-format adapter.
|
|
281
|
+
* @internal
|
|
282
|
+
*/
|
|
283
|
+
function dispatchEmbedding(format, config, inputs, request, capability, logger, signal) {
|
|
284
|
+
switch (format) {
|
|
285
|
+
case 'openai-embeddings':
|
|
286
|
+
return callOpenAiEmbeddings(config, inputs, request, capability, logger, signal);
|
|
287
|
+
case 'gemini-embeddings':
|
|
288
|
+
return callGeminiEmbeddings(config, inputs, request, capability, logger, signal);
|
|
289
|
+
/* c8 ignore next 4 - defensive: exhaustive switch guaranteed by TypeScript */
|
|
290
|
+
default: {
|
|
291
|
+
const _exhaustive = format;
|
|
292
|
+
return Promise.resolve(fail(`unsupported embedding API format: ${String(_exhaustive)}`));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// ============================================================================
|
|
297
|
+
// Proxied embedding
|
|
298
|
+
// ============================================================================
|
|
299
|
+
const proxiedEmbeddingUsage = Validators.object({
|
|
300
|
+
promptTokens: Validators.number.optional(),
|
|
301
|
+
totalTokens: Validators.number.optional()
|
|
302
|
+
});
|
|
303
|
+
const proxiedEmbeddingResponse = Validators.object({
|
|
304
|
+
vectors: Validators.arrayOf(Validators.arrayOf(Validators.number)),
|
|
305
|
+
model: Validators.string,
|
|
306
|
+
dimensions: Validators.number,
|
|
307
|
+
usage: proxiedEmbeddingUsage.optional()
|
|
308
|
+
});
|
|
309
|
+
/**
|
|
310
|
+
* Calls the embedding endpoint on a proxy server instead of calling the provider
|
|
311
|
+
* API directly from the browser. Endpoint: `POST ${proxyUrl}/api/ai/embedding`.
|
|
312
|
+
* Request body: `{ providerId, apiKey, params, modelOverride? }`. The proxy
|
|
313
|
+
* handles descriptor lookup, model/capability resolution, and provider dispatch.
|
|
314
|
+
* Error body `{ error: string }` is surfaced as `proxy: ${error}`.
|
|
315
|
+
*
|
|
316
|
+
* @param proxyUrl - Base URL of the proxy server (e.g. `http://localhost:3001`).
|
|
317
|
+
* @param params - Same parameters as {@link AiAssist.callProviderEmbedding}.
|
|
318
|
+
* @returns The embedding result, or a failure.
|
|
319
|
+
* @public
|
|
320
|
+
*/
|
|
321
|
+
export async function callProxiedEmbedding(proxyUrl, params) {
|
|
322
|
+
const { descriptor, apiKey, params: request, modelOverride, logger, signal } = params;
|
|
323
|
+
const body = {
|
|
324
|
+
providerId: descriptor.id,
|
|
325
|
+
apiKey,
|
|
326
|
+
params: request
|
|
327
|
+
};
|
|
328
|
+
if (modelOverride !== undefined) {
|
|
329
|
+
body.modelOverride = modelOverride;
|
|
330
|
+
}
|
|
331
|
+
/* c8 ignore next 1 - optional logger */
|
|
332
|
+
logger === null || logger === void 0 ? void 0 : logger.info(`AI embedding proxy request: provider=${descriptor.id}, proxy=${proxyUrl}`);
|
|
333
|
+
const url = `${proxyUrl}/api/ai/embedding`;
|
|
334
|
+
const jsonResult = await fetchJson(url, {}, body, logger, signal);
|
|
335
|
+
if (jsonResult.isFailure()) {
|
|
336
|
+
return fail(jsonResult.message);
|
|
337
|
+
}
|
|
338
|
+
const response = jsonResult.value;
|
|
339
|
+
if (typeof response.error === 'string') {
|
|
340
|
+
return fail(`proxy: ${response.error}`);
|
|
341
|
+
}
|
|
342
|
+
return proxiedEmbeddingResponse
|
|
343
|
+
.validate(response)
|
|
344
|
+
.withErrorFormat((msg) => `proxy returned invalid response: ${msg}`);
|
|
345
|
+
}
|
|
346
|
+
//# sourceMappingURL=embeddingClient.js.map
|