@fgv/ts-extras 5.1.0-34 → 5.1.0-35
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 +22 -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 +349 -51
- 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 +194 -2
- 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 +21 -7
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +22 -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
|
@@ -6,23 +6,58 @@
|
|
|
6
6
|
* @packageDocumentation
|
|
7
7
|
*/
|
|
8
8
|
import { type JsonObject } from '@fgv/ts-json-base';
|
|
9
|
+
import { Result } from '@fgv/ts-utils';
|
|
9
10
|
import { AiPrompt, type IChatMessage } from './model';
|
|
10
11
|
/**
|
|
11
|
-
*
|
|
12
|
+
* The result of splitting an {@link AiAssist.IChatRequest} into the per-builder
|
|
13
|
+
* inputs: the current turn (as an {@link AiPrompt}, carrying system + the final
|
|
14
|
+
* user message + any attachments) and the preceding conversation history.
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export interface ISplitChatRequest {
|
|
18
|
+
/** The current turn lowered to an {@link AiPrompt} (system + user + attachments). */
|
|
19
|
+
readonly prompt: AiPrompt;
|
|
20
|
+
/** Prior conversation history — every message before the current turn. */
|
|
21
|
+
readonly head: ReadonlyArray<IChatMessage>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Splits a unified {@link AiAssist.IChatRequest} into `{ prompt, head }` for the
|
|
25
|
+
* per-provider builders. The **last** message is the current `user` turn; the
|
|
26
|
+
* preceding messages are history (`head`). This is the single linearization
|
|
27
|
+
* shared by every turn entry point, so the completion path and the client-tool
|
|
28
|
+
* turn path place history at the identical position relative to the current turn.
|
|
29
|
+
*
|
|
30
|
+
* Fails when `messages` is empty (no current turn) or when the last message is
|
|
31
|
+
* not a `user` turn — relabelling a trailing assistant message as the user turn
|
|
32
|
+
* would be a silent footgun, so it is rejected loudly instead.
|
|
33
|
+
*
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
export declare function splitChatRequest(system: string | undefined, messages: ReadonlyArray<IChatMessage>): Result<ISplitChatRequest>;
|
|
37
|
+
/**
|
|
38
|
+
* Rebuilds the ordered messages for a proxy wire body so that only the current
|
|
39
|
+
* turn can carry attachments — history (non-current) messages are reduced to
|
|
40
|
+
* `{ role, content }`. The direct per-provider builders already drop attachments
|
|
41
|
+
* on history turns (only the current user turn's attachments are honored), so
|
|
42
|
+
* normalizing here keeps the proxy wire shape consistent with the direct paths
|
|
43
|
+
* and avoids transmitting attachment payloads the upstream provider would ignore.
|
|
44
|
+
*
|
|
45
|
+
* @internal
|
|
46
|
+
*/
|
|
47
|
+
export declare function normalizeOutboundMessages(split: ISplitChatRequest): IChatMessage[];
|
|
48
|
+
/**
|
|
49
|
+
* Optional history (`head`) and raw continuation (`rawTail`) messages to weave
|
|
50
|
+
* around the prompt's current user message.
|
|
12
51
|
*
|
|
13
52
|
* @internal
|
|
14
53
|
*/
|
|
15
54
|
export interface IBuildMessagesOptions {
|
|
16
55
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
56
|
+
* Prior conversation history inserted between the system prompt and the
|
|
57
|
+
* prompt's current user message (multi-turn chat / correction retries). The
|
|
58
|
+
* single ordered linearization is `[system, ...head, user, ...rawTail]`.
|
|
19
59
|
*/
|
|
20
60
|
readonly head?: ReadonlyArray<IChatMessage>;
|
|
21
|
-
/**
|
|
22
|
-
* Messages appended after the prompt's user message (e.g. assistant
|
|
23
|
-
* + correction turns for the JSON-validation retry loop).
|
|
24
|
-
*/
|
|
25
|
-
readonly tail?: ReadonlyArray<IChatMessage>;
|
|
26
61
|
/**
|
|
27
62
|
* Raw JSON objects appended after the prompt's user message. Used to
|
|
28
63
|
* inject provider-specific continuation messages (e.g. Anthropic assistant
|
|
@@ -39,14 +74,15 @@ export interface IBuildMessagesOptions {
|
|
|
39
74
|
*
|
|
40
75
|
* Entries that fail their builder's shape check are silently skipped (the
|
|
41
76
|
* caller is responsible for supplying well-formed continuation messages).
|
|
42
|
-
*
|
|
77
|
+
* Appended after the current user message.
|
|
43
78
|
*/
|
|
44
79
|
readonly rawTail?: ReadonlyArray<JsonObject>;
|
|
45
80
|
}
|
|
46
81
|
/**
|
|
47
|
-
* Builds the messages array from prompt + optional head
|
|
48
|
-
* The caller supplies the user content
|
|
49
|
-
* for vision prompts) since the parts shape
|
|
82
|
+
* Builds the messages array from prompt + optional history (`head`) and raw
|
|
83
|
+
* continuation (`rawTail`) messages. The caller supplies the user content
|
|
84
|
+
* (string for text-only, parts array for vision prompts) since the parts shape
|
|
85
|
+
* differs by format.
|
|
50
86
|
*
|
|
51
87
|
* `rawTail` items (OpenAI / xAI Responses `function_call` /
|
|
52
88
|
* `function_call_output` continuation items) are appended verbatim after the
|
|
@@ -87,10 +123,10 @@ export declare function buildAnthropicUserContent(prompt: AiPrompt): string | un
|
|
|
87
123
|
*/
|
|
88
124
|
export declare function buildGeminiUserParts(prompt: AiPrompt): Array<Record<string, unknown>>;
|
|
89
125
|
/**
|
|
90
|
-
* Builds the Anthropic messages array, weaving any `head` messages
|
|
91
|
-
* implicit system + the prompt's user message and appending `
|
|
92
|
-
* after. System messages are filtered out (Anthropic uses
|
|
93
|
-
* field).
|
|
126
|
+
* Builds the Anthropic messages array, weaving any `head` history messages
|
|
127
|
+
* between implicit system + the prompt's user message and appending `rawTail`
|
|
128
|
+
* continuation messages after. System messages are filtered out (Anthropic uses
|
|
129
|
+
* a top-level system field).
|
|
94
130
|
*
|
|
95
131
|
* @internal
|
|
96
132
|
*/
|
|
@@ -99,10 +135,10 @@ export declare function buildAnthropicMessages(prompt: AiPrompt, options?: IBuil
|
|
|
99
135
|
content: string | unknown[];
|
|
100
136
|
}>;
|
|
101
137
|
/**
|
|
102
|
-
* Builds the Gemini `contents` array, weaving any `head` messages before
|
|
103
|
-
* prompt's user parts and appending `
|
|
104
|
-
* are filtered out (Gemini uses a top-level systemInstruction
|
|
105
|
-
* assistant roles are mapped to Gemini's `model` role.
|
|
138
|
+
* Builds the Gemini `contents` array, weaving any `head` history messages before
|
|
139
|
+
* the prompt's user parts and appending `rawTail` continuation messages after.
|
|
140
|
+
* System messages are filtered out (Gemini uses a top-level systemInstruction
|
|
141
|
+
* field) and assistant roles are mapped to Gemini's `model` role.
|
|
106
142
|
*
|
|
107
143
|
* @internal
|
|
108
144
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chatRequestBuilders.d.ts","sourceRoot":"","sources":["../../../src/packlets/ai-assist/chatRequestBuilders.ts"],"names":[],"mappings":"AAoBA;;;;;;GAMG;AAEH,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"chatRequestBuilders.d.ts","sourceRoot":"","sources":["../../../src/packlets/ai-assist/chatRequestBuilders.ts"],"names":[],"mappings":"AAoBA;;;;;;GAMG;AAEH,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAAoC,MAAM,EAAW,MAAM,eAAe,CAAC;AAElF,OAAO,EAAE,QAAQ,EAA2B,KAAK,YAAY,EAAa,MAAM,SAAS,CAAC;AAE1F;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,qFAAqF;IACrF,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC1B,0EAA0E;IAC1E,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;CAC5C;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,QAAQ,EAAE,aAAa,CAAC,YAAY,CAAC,GACpC,MAAM,CAAC,iBAAiB,CAAC,CAW3B;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,iBAAiB,GAAG,YAAY,EAAE,CAKlF;AAgED;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;IAC5C;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;CAC9C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAC3B,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,GAAG,OAAO,EAAE,EAC/B,OAAO,CAAC,EAAE,qBAAqB,GAC9B,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAoBhC;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,GAAG,OAAO,EAAE,CAc/E;AAED;;;;;;GAMG;AACH,wBAAgB,+BAA+B,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,GAAG,OAAO,EAAE,CAYpF;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,GAAG,OAAO,EAAE,CAe9E;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAMrF;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,QAAQ,EAChB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,GAAG,OAAO,EAAE,CAAA;CAAE,CAAC,CAmBtD;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,QAAQ,EAChB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,EAAE,CAAA;CAAE,CAAC,CAwB3C"}
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
20
|
// SOFTWARE.
|
|
21
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.splitChatRequest = splitChatRequest;
|
|
23
|
+
exports.normalizeOutboundMessages = normalizeOutboundMessages;
|
|
22
24
|
exports.buildMessages = buildMessages;
|
|
23
25
|
exports.buildOpenAiChatUserContent = buildOpenAiChatUserContent;
|
|
24
26
|
exports.buildOpenAiResponsesUserContent = buildOpenAiResponsesUserContent;
|
|
@@ -36,6 +38,47 @@ exports.buildGeminiContents = buildGeminiContents;
|
|
|
36
38
|
const ts_json_base_1 = require("@fgv/ts-json-base");
|
|
37
39
|
const ts_utils_1 = require("@fgv/ts-utils");
|
|
38
40
|
const model_1 = require("./model");
|
|
41
|
+
/**
|
|
42
|
+
* Splits a unified {@link AiAssist.IChatRequest} into `{ prompt, head }` for the
|
|
43
|
+
* per-provider builders. The **last** message is the current `user` turn; the
|
|
44
|
+
* preceding messages are history (`head`). This is the single linearization
|
|
45
|
+
* shared by every turn entry point, so the completion path and the client-tool
|
|
46
|
+
* turn path place history at the identical position relative to the current turn.
|
|
47
|
+
*
|
|
48
|
+
* Fails when `messages` is empty (no current turn) or when the last message is
|
|
49
|
+
* not a `user` turn — relabelling a trailing assistant message as the user turn
|
|
50
|
+
* would be a silent footgun, so it is rejected loudly instead.
|
|
51
|
+
*
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
function splitChatRequest(system, messages) {
|
|
55
|
+
if (messages.length === 0) {
|
|
56
|
+
return (0, ts_utils_1.fail)('messages must contain at least one entry (the current user turn)');
|
|
57
|
+
}
|
|
58
|
+
const current = messages[messages.length - 1];
|
|
59
|
+
if (current.role !== 'user') {
|
|
60
|
+
return (0, ts_utils_1.fail)(`the last message must be the current user turn (role 'user'); got '${current.role}'`);
|
|
61
|
+
}
|
|
62
|
+
const head = messages.slice(0, messages.length - 1);
|
|
63
|
+
const prompt = new model_1.AiPrompt(current.content, system !== null && system !== void 0 ? system : '', current.attachments);
|
|
64
|
+
return (0, ts_utils_1.succeed)({ prompt, head });
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Rebuilds the ordered messages for a proxy wire body so that only the current
|
|
68
|
+
* turn can carry attachments — history (non-current) messages are reduced to
|
|
69
|
+
* `{ role, content }`. The direct per-provider builders already drop attachments
|
|
70
|
+
* on history turns (only the current user turn's attachments are honored), so
|
|
71
|
+
* normalizing here keeps the proxy wire shape consistent with the direct paths
|
|
72
|
+
* and avoids transmitting attachment payloads the upstream provider would ignore.
|
|
73
|
+
*
|
|
74
|
+
* @internal
|
|
75
|
+
*/
|
|
76
|
+
function normalizeOutboundMessages(split) {
|
|
77
|
+
return [
|
|
78
|
+
...split.head.map((m) => ({ role: m.role, content: m.content })),
|
|
79
|
+
...split.prompt.toRequest().messages
|
|
80
|
+
];
|
|
81
|
+
}
|
|
39
82
|
/**
|
|
40
83
|
* Converter for a rawTail message entry. Narrows a `JsonObject` to
|
|
41
84
|
* `{ role: string; content: string | unknown[] }` at runtime using the
|
|
@@ -83,9 +126,10 @@ const geminiRawTailMessageConverter = ts_utils_1.Converters.object({
|
|
|
83
126
|
parts: ts_utils_1.Converters.isA('array', (v) => Array.isArray(v))
|
|
84
127
|
}, { strict: false });
|
|
85
128
|
/**
|
|
86
|
-
* Builds the messages array from prompt + optional head
|
|
87
|
-
* The caller supplies the user content
|
|
88
|
-
* for vision prompts) since the parts shape
|
|
129
|
+
* Builds the messages array from prompt + optional history (`head`) and raw
|
|
130
|
+
* continuation (`rawTail`) messages. The caller supplies the user content
|
|
131
|
+
* (string for text-only, parts array for vision prompts) since the parts shape
|
|
132
|
+
* differs by format.
|
|
89
133
|
*
|
|
90
134
|
* `rawTail` items (OpenAI / xAI Responses `function_call` /
|
|
91
135
|
* `function_call_output` continuation items) are appended verbatim after the
|
|
@@ -98,19 +142,12 @@ const geminiRawTailMessageConverter = ts_utils_1.Converters.object({
|
|
|
98
142
|
*/
|
|
99
143
|
function buildMessages(systemPrompt, userContent, options) {
|
|
100
144
|
const messages = [{ role: 'system', content: systemPrompt }];
|
|
101
|
-
/* c8 ignore next 4 - head branch: options?.head short-circuit not reached from current call sites */
|
|
102
145
|
if (options === null || options === void 0 ? void 0 : options.head) {
|
|
103
146
|
for (const msg of options.head) {
|
|
104
147
|
messages.push({ role: msg.role, content: msg.content });
|
|
105
148
|
}
|
|
106
149
|
}
|
|
107
150
|
messages.push({ role: 'user', content: userContent });
|
|
108
|
-
/* c8 ignore next 4 - tail branch: options?.tail short-circuit not reached from current call sites */
|
|
109
|
-
if (options === null || options === void 0 ? void 0 : options.tail) {
|
|
110
|
-
for (const msg of options.tail) {
|
|
111
|
-
messages.push({ role: msg.role, content: msg.content });
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
151
|
// OpenAI / xAI Responses continuation items (function_call /
|
|
115
152
|
// function_call_output) are appended verbatim — their field set differs per
|
|
116
153
|
// item type, so the whole object is preserved rather than projected.
|
|
@@ -193,16 +230,15 @@ function buildGeminiUserParts(prompt) {
|
|
|
193
230
|
return parts;
|
|
194
231
|
}
|
|
195
232
|
/**
|
|
196
|
-
* Builds the Anthropic messages array, weaving any `head` messages
|
|
197
|
-
* implicit system + the prompt's user message and appending `
|
|
198
|
-
* after. System messages are filtered out (Anthropic uses
|
|
199
|
-
* field).
|
|
233
|
+
* Builds the Anthropic messages array, weaving any `head` history messages
|
|
234
|
+
* between implicit system + the prompt's user message and appending `rawTail`
|
|
235
|
+
* continuation messages after. System messages are filtered out (Anthropic uses
|
|
236
|
+
* a top-level system field).
|
|
200
237
|
*
|
|
201
238
|
* @internal
|
|
202
239
|
*/
|
|
203
240
|
function buildAnthropicMessages(prompt, options) {
|
|
204
241
|
const messages = [];
|
|
205
|
-
/* c8 ignore next 5 - head branch: options?.head short-circuit not reached from current call sites */
|
|
206
242
|
if (options === null || options === void 0 ? void 0 : options.head) {
|
|
207
243
|
for (const msg of options.head) {
|
|
208
244
|
if (msg.role !== 'system') {
|
|
@@ -211,15 +247,6 @@ function buildAnthropicMessages(prompt, options) {
|
|
|
211
247
|
}
|
|
212
248
|
}
|
|
213
249
|
messages.push({ role: 'user', content: buildAnthropicUserContent(prompt) });
|
|
214
|
-
/* c8 ignore next 5 - tail branch: options?.tail short-circuit not reached from current call sites */
|
|
215
|
-
if (options === null || options === void 0 ? void 0 : options.tail) {
|
|
216
|
-
for (const msg of options.tail) {
|
|
217
|
-
if (msg.role !== 'system') {
|
|
218
|
-
messages.push({ role: msg.role, content: msg.content });
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
/* c8 ignore next 7 - options?.rawTail optional-chain short-circuit (options=undefined) not reached in unit tests */
|
|
223
250
|
if (options === null || options === void 0 ? void 0 : options.rawTail) {
|
|
224
251
|
for (const msg of options.rawTail) {
|
|
225
252
|
const converted = rawTailMessageConverter.convert(msg);
|
|
@@ -231,16 +258,15 @@ function buildAnthropicMessages(prompt, options) {
|
|
|
231
258
|
return messages;
|
|
232
259
|
}
|
|
233
260
|
/**
|
|
234
|
-
* Builds the Gemini `contents` array, weaving any `head` messages before
|
|
235
|
-
* prompt's user parts and appending `
|
|
236
|
-
* are filtered out (Gemini uses a top-level systemInstruction
|
|
237
|
-
* assistant roles are mapped to Gemini's `model` role.
|
|
261
|
+
* Builds the Gemini `contents` array, weaving any `head` history messages before
|
|
262
|
+
* the prompt's user parts and appending `rawTail` continuation messages after.
|
|
263
|
+
* System messages are filtered out (Gemini uses a top-level systemInstruction
|
|
264
|
+
* field) and assistant roles are mapped to Gemini's `model` role.
|
|
238
265
|
*
|
|
239
266
|
* @internal
|
|
240
267
|
*/
|
|
241
268
|
function buildGeminiContents(prompt, options) {
|
|
242
269
|
const contents = [];
|
|
243
|
-
/* c8 ignore next 7 - head branch: options?.head short-circuit not reached from current call sites */
|
|
244
270
|
if (options === null || options === void 0 ? void 0 : options.head) {
|
|
245
271
|
for (const msg of options.head) {
|
|
246
272
|
if (msg.role !== 'system') {
|
|
@@ -252,17 +278,6 @@ function buildGeminiContents(prompt, options) {
|
|
|
252
278
|
}
|
|
253
279
|
}
|
|
254
280
|
contents.push({ role: 'user', parts: buildGeminiUserParts(prompt) });
|
|
255
|
-
/* c8 ignore next 7 - tail branch: options?.tail short-circuit not reached from current call sites */
|
|
256
|
-
if (options === null || options === void 0 ? void 0 : options.tail) {
|
|
257
|
-
for (const msg of options.tail) {
|
|
258
|
-
if (msg.role !== 'system') {
|
|
259
|
-
contents.push({
|
|
260
|
-
role: msg.role === 'assistant' ? 'model' : msg.role,
|
|
261
|
-
parts: [{ text: msg.content }]
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
281
|
// Gemini continuation turns (model `functionCall` parts + user
|
|
267
282
|
// `functionResponse` parts) are projected to `{ role, parts }`.
|
|
268
283
|
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,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-provider embedding client for AI assist. Mirrors the completion and
|
|
3
|
+
* image-generation primitives in `apiClient.ts`: a single dispatcher
|
|
4
|
+
* ({@link AiAssist.callProviderEmbedding}) resolves the provider descriptor's
|
|
5
|
+
* embedding capability and routes to the per-format adapter. `text -> vector`,
|
|
6
|
+
* batch in, `number[][]` out.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
import { type Logging, Result } from '@fgv/ts-utils';
|
|
11
|
+
import { type IAiEmbeddingParams, type IAiEmbeddingResult, type IAiProviderDescriptor, type ModelSpec } from './model';
|
|
12
|
+
/**
|
|
13
|
+
* Parameters for a provider embedding request. Mirrors
|
|
14
|
+
* {@link AiAssist.IProviderImageGenerationParams}.
|
|
15
|
+
* @public
|
|
16
|
+
*/
|
|
17
|
+
export interface IProviderEmbeddingParams {
|
|
18
|
+
/** The provider descriptor. */
|
|
19
|
+
readonly descriptor: IAiProviderDescriptor;
|
|
20
|
+
/** API key for authentication (empty string for keyless self-hosted providers). */
|
|
21
|
+
readonly apiKey: string;
|
|
22
|
+
/** The embedding request (input + optional knobs). */
|
|
23
|
+
readonly params: IAiEmbeddingParams;
|
|
24
|
+
/**
|
|
25
|
+
* Optional model override — string or context-aware map. Uses
|
|
26
|
+
* `descriptor.defaultModel.embedding` otherwise. Self-hosted providers
|
|
27
|
+
* (`ollama`, `openai-compat`) have no default and require this.
|
|
28
|
+
*/
|
|
29
|
+
readonly modelOverride?: ModelSpec;
|
|
30
|
+
/** Optional logger for request/response observability. */
|
|
31
|
+
readonly logger?: Logging.ILogger;
|
|
32
|
+
/** Optional abort signal for cancelling the in-flight request. */
|
|
33
|
+
readonly signal?: AbortSignal;
|
|
34
|
+
/** Optional override of the descriptor's base URL; the `/embeddings` suffix is appended unchanged. */
|
|
35
|
+
readonly endpoint?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Calls the appropriate embedding API for a given provider. Routes by the
|
|
39
|
+
* `format` of the resolved {@link AiAssist.IAiEmbeddingModelCapability}:
|
|
40
|
+
* `'openai-embeddings'` or `'gemini-embeddings'`.
|
|
41
|
+
*
|
|
42
|
+
* @remarks
|
|
43
|
+
* - Rejects up front when the provider declares no embedding capability, when no
|
|
44
|
+
* embedding model resolves, or when the batch exceeds the capability's
|
|
45
|
+
* `maxBatchSize` (no auto-chunking).
|
|
46
|
+
* - An empty `input` array short-circuits to an empty result with no wire call
|
|
47
|
+
* (most providers HTTP-400 on empty input).
|
|
48
|
+
* - Caller-supplied `dimensions`/`taskType` that the model doesn't support are a
|
|
49
|
+
* no-op (logged), not a failure (design §7).
|
|
50
|
+
*
|
|
51
|
+
* @param params - Request parameters including descriptor, API key, and input.
|
|
52
|
+
* @returns The embedding vectors aligned to input order, or a failure.
|
|
53
|
+
* @public
|
|
54
|
+
*/
|
|
55
|
+
export declare function callProviderEmbedding(params: IProviderEmbeddingParams): Promise<Result<IAiEmbeddingResult>>;
|
|
56
|
+
/**
|
|
57
|
+
* Calls the embedding endpoint on a proxy server instead of calling the provider
|
|
58
|
+
* API directly from the browser. Endpoint: `POST ${proxyUrl}/api/ai/embedding`.
|
|
59
|
+
* Request body: `{ providerId, apiKey, params, modelOverride? }`. The proxy
|
|
60
|
+
* handles descriptor lookup, model/capability resolution, and provider dispatch.
|
|
61
|
+
* Error body `{ error: string }` is surfaced as `proxy: ${error}`.
|
|
62
|
+
*
|
|
63
|
+
* @param proxyUrl - Base URL of the proxy server (e.g. `http://localhost:3001`).
|
|
64
|
+
* @param params - Same parameters as {@link AiAssist.callProviderEmbedding}.
|
|
65
|
+
* @returns The embedding result, or a failure.
|
|
66
|
+
* @public
|
|
67
|
+
*/
|
|
68
|
+
export declare function callProxiedEmbedding(proxyUrl: string, params: IProviderEmbeddingParams): Promise<Result<IAiEmbeddingResult>>;
|
|
69
|
+
//# sourceMappingURL=embeddingClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embeddingClient.d.ts","sourceRoot":"","sources":["../../../src/packlets/ai-assist/embeddingClient.ts"],"names":[],"mappings":"AAoBA;;;;;;;;GAQG;AAEH,OAAO,EAAQ,KAAK,OAAO,EAAE,MAAM,EAAuC,MAAM,eAAe,CAAC;AAEhG,OAAO,EAGL,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EAEvB,KAAK,qBAAqB,EAC1B,KAAK,SAAS,EAEf,MAAM,SAAS,CAAC;AASjB;;;;GAIG;AACH,MAAM,WAAW,wBAAwB;IACvC,+BAA+B;IAC/B,QAAQ,CAAC,UAAU,EAAE,qBAAqB,CAAC;IAC3C,mFAAmF;IACnF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sDAAsD;IACtD,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;IACpC;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,CAAC;IACnC,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC;IAClC,kEAAkE;IAClE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B,sGAAsG;IACtG,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAoSD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,wBAAwB,GAC/B,OAAO,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC,CA2CrC;AA2CD;;;;;;;;;;;GAWG;AACH,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,wBAAwB,GAC/B,OAAO,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC,CA6BrC"}
|