@loreai/gateway 0.14.0 → 0.15.0

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.
@@ -1,310 +0,0 @@
1
- /**
2
- * Converts raw Anthropic API messages (as they appear in request/response
3
- * bodies) into `@loreai/core`'s `LoreMessageWithParts` shape for temporal
4
- * storage and gradient transform.
5
- *
6
- * Follows the same tool-pairing pattern used by the Pi adapter
7
- * (`packages/pi/src/adapter.ts`): tool_use blocks on assistant messages are
8
- * initially stored as "pending", then `resolveToolResults` walks the
9
- * conversation to merge matching tool_result blocks from subsequent user
10
- * messages into "completed" state.
11
- */
12
- import { createHash, randomUUID } from "crypto";
13
- import { isToolPart } from "@loreai/core";
14
- import type {
15
- LoreAssistantMessage,
16
- LoreMessageWithParts,
17
- LorePart,
18
- LoreReasoningPart,
19
- LoreTextPart,
20
- LoreToolPart,
21
- LoreUserMessage,
22
- } from "@loreai/core";
23
- import type {
24
- GatewayContentBlock,
25
- GatewayMessage,
26
- GatewayUsage,
27
- } from "./translate/types";
28
-
29
- // ---------------------------------------------------------------------------
30
- // Deterministic ID generation
31
- // ---------------------------------------------------------------------------
32
-
33
- /**
34
- * Generate a deterministic UUID-like ID from message content.
35
- * Same message at the same position produces the same ID across requests,
36
- * which is critical for gradient prefix fingerprinting and cache-bust detection.
37
- */
38
- function deterministicID(role: string, index: number, content: GatewayContentBlock[]): string {
39
- const h = createHash("sha256");
40
- h.update(`${role}:${index}:`);
41
- for (const block of content) {
42
- switch (block.type) {
43
- case "text":
44
- h.update(`text:${block.text}`);
45
- break;
46
- case "thinking":
47
- h.update(`thinking:${block.thinking}`);
48
- break;
49
- case "tool_use":
50
- h.update(`tool_use:${block.id}:${block.name}:${JSON.stringify(block.input)}`);
51
- break;
52
- case "tool_result":
53
- h.update(`tool_result:${block.toolUseId}:${block.content}`);
54
- break;
55
- }
56
- }
57
- return h.digest("hex").slice(0, 32);
58
- }
59
-
60
- /**
61
- * Generate a deterministic ID for a part within a message.
62
- * Uses the message ID + part index for stability.
63
- */
64
- function deterministicPartID(messageID: string, partIndex: number): string {
65
- const h = createHash("sha256");
66
- h.update(`${messageID}:part:${partIndex}`);
67
- return h.digest("hex").slice(0, 32);
68
- }
69
-
70
- // ---------------------------------------------------------------------------
71
- // Part conversion helpers
72
- // ---------------------------------------------------------------------------
73
-
74
- function contentBlockToPart(
75
- block: GatewayContentBlock,
76
- sessionID: string,
77
- messageID: string,
78
- partIndex: number,
79
- ): LorePart {
80
- const now = Date.now();
81
- const id = deterministicPartID(messageID, partIndex);
82
- switch (block.type) {
83
- case "text":
84
- return {
85
- id,
86
- sessionID,
87
- messageID,
88
- type: "text",
89
- text: block.text,
90
- time: { start: now, end: now },
91
- } satisfies LoreTextPart;
92
-
93
- case "thinking":
94
- return {
95
- id,
96
- sessionID,
97
- messageID,
98
- type: "reasoning",
99
- text: block.thinking,
100
- ...(block.signature != null
101
- ? { signature: block.signature }
102
- : undefined),
103
- } satisfies LoreReasoningPart;
104
-
105
- case "tool_use":
106
- return {
107
- id,
108
- sessionID,
109
- messageID,
110
- type: "tool",
111
- tool: block.name,
112
- callID: block.id,
113
- state: { status: "pending", input: block.input },
114
- } satisfies LoreToolPart;
115
-
116
- case "tool_result":
117
- return {
118
- id,
119
- sessionID,
120
- messageID,
121
- type: "tool",
122
- tool: "result",
123
- callID: block.toolUseId,
124
- state: {
125
- status: "completed",
126
- input: null,
127
- output: block.content,
128
- time: { start: now, end: now },
129
- },
130
- } satisfies LoreToolPart;
131
- }
132
- }
133
-
134
- // ---------------------------------------------------------------------------
135
- // 1. gatewayMessagesToLore
136
- // ---------------------------------------------------------------------------
137
-
138
- /**
139
- * Convert an array of gateway messages to Lore's message-with-parts format.
140
- *
141
- * User messages get minimal metadata (we don't know the model at message
142
- * level). Assistant messages get zeroed-out token counts — call
143
- * `updateAssistantMessageTokens` after accumulating the API response to
144
- * fill them in.
145
- */
146
- export function gatewayMessagesToLore(
147
- messages: GatewayMessage[],
148
- sessionID: string,
149
- ): LoreMessageWithParts[] {
150
- const out: LoreMessageWithParts[] = [];
151
- const now = Date.now();
152
-
153
- for (let i = 0; i < messages.length; i++) {
154
- const m = messages[i];
155
- const id = deterministicID(m.role, i, m.content);
156
- const parts: LorePart[] = m.content.map((block, pi) =>
157
- contentBlockToPart(block, sessionID, id, pi),
158
- );
159
-
160
- if (m.role === "user") {
161
- const info: LoreUserMessage = {
162
- id,
163
- sessionID,
164
- role: "user",
165
- time: { created: now },
166
- agent: "gateway",
167
- model: { providerID: "anthropic", modelID: "unknown" },
168
- };
169
- out.push({ info, parts });
170
- } else {
171
- const info: LoreAssistantMessage = {
172
- id,
173
- sessionID,
174
- role: "assistant",
175
- time: { created: now },
176
- parentID: "",
177
- modelID: "unknown",
178
- providerID: "anthropic",
179
- mode: "gateway",
180
- path: { cwd: "", root: "" },
181
- cost: 0,
182
- tokens: {
183
- input: 0,
184
- output: 0,
185
- reasoning: 0,
186
- cache: { read: 0, write: 0 },
187
- },
188
- };
189
- out.push({ info, parts });
190
- }
191
- }
192
-
193
- return out;
194
- }
195
-
196
- // ---------------------------------------------------------------------------
197
- // 2. updateAssistantMessageTokens
198
- // ---------------------------------------------------------------------------
199
-
200
- /**
201
- * Update an assistant message's token counts from the API response usage data.
202
- * Mutates in place — call after the upstream response is fully accumulated.
203
- */
204
- export function updateAssistantMessageTokens(
205
- msg: LoreMessageWithParts,
206
- usage: GatewayUsage,
207
- model: string,
208
- ): void {
209
- const info = msg.info;
210
- if (info.role !== "assistant") return;
211
-
212
- info.tokens.input = usage.inputTokens;
213
- info.tokens.output = usage.outputTokens;
214
- info.tokens.cache.read = usage.cacheReadInputTokens ?? 0;
215
- info.tokens.cache.write = usage.cacheCreationInputTokens ?? 0;
216
- info.modelID = model;
217
- }
218
-
219
- // ---------------------------------------------------------------------------
220
- // 3. resolveToolResults
221
- // ---------------------------------------------------------------------------
222
-
223
- /**
224
- * Walk through the messages and match tool_result blocks (on user messages)
225
- * with their corresponding tool_use blocks (on preceding assistant messages).
226
- *
227
- * When a tool_use was initially stored as "pending", update it to "completed"
228
- * with the output from the matching tool_result. This mirrors the tool-pairing
229
- * pattern used in the Pi adapter (`piMessagesToLore`).
230
- *
231
- * After resolving, strips all `tool: "result"` parts from user messages.
232
- * Their data is now redundant (merged into the assistant's completed tool part).
233
- * This prevents orphaned `tool_result` blocks when gradient evicts the assistant
234
- * message but keeps the following user message — the Anthropic API requires every
235
- * `tool_result` to reference a `tool_use` on the immediately preceding assistant.
236
- *
237
- * Mutates messages in place.
238
- */
239
- export function resolveToolResults(messages: LoreMessageWithParts[]): void {
240
- // --- Pass 1: Index all tool_result parts by callID ---
241
- const resultsByCallID = new Map<
242
- string,
243
- { output: string; isError: boolean }
244
- >();
245
-
246
- for (const msg of messages) {
247
- for (const part of msg.parts) {
248
- if (
249
- isToolPart(part) &&
250
- part.tool === "result" &&
251
- part.state.status === "completed"
252
- ) {
253
- resultsByCallID.set(part.callID, {
254
- output: part.state.output,
255
- isError: false,
256
- });
257
- }
258
- }
259
- }
260
-
261
- // --- Pass 2: Resolve pending tool_use → completed where a result exists ---
262
- const now = Date.now();
263
- for (const msg of messages) {
264
- for (const part of msg.parts) {
265
- if (
266
- isToolPart(part) &&
267
- part.tool !== "result" &&
268
- part.state.status === "pending"
269
- ) {
270
- const result = resultsByCallID.get(part.callID);
271
- if (result) {
272
- part.state = {
273
- status: "completed",
274
- input: part.state.input,
275
- output: result.output,
276
- time: { start: now, end: now },
277
- };
278
- }
279
- }
280
- }
281
- }
282
-
283
- // --- Pass 3: Strip redundant tool_result parts from user messages ---
284
- // After resolving, tool_result data lives on the assistant's completed
285
- // tool part. Keeping it on user messages creates orphaned tool_result
286
- // blocks when gradient evicts the assistant but keeps the user.
287
- // loreMessagesToGateway() reconstructs tool_result blocks from the
288
- // assistant's completed/error tool parts, so no data is lost.
289
- for (const msg of messages) {
290
- if (msg.info.role !== "user") continue;
291
- const before = msg.parts.length;
292
- msg.parts = msg.parts.filter(
293
- (p) => !(isToolPart(p) && p.tool === "result"),
294
- );
295
- // If stripping left the user message with no content parts,
296
- // add a placeholder text part so the message survives API conversion.
297
- if (msg.parts.length === 0 && before > 0) {
298
- msg.parts = [
299
- {
300
- id: randomUUID(),
301
- sessionID: "",
302
- messageID: msg.info.id,
303
- type: "text" as const,
304
- text: "[tool results provided]",
305
- time: { start: 0, end: 0 },
306
- } satisfies LoreTextPart,
307
- ];
308
- }
309
- }
310
- }