@loreai/gateway 0.13.4 → 0.14.1

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,536 +0,0 @@
1
- /**
2
- * OpenAI ↔ Gateway translation layer.
3
- *
4
- * Converts between OpenAI's `/v1/chat/completions` API format and the gateway's
5
- * internal `GatewayRequest`/`GatewayResponse` types.
6
- */
7
- import type {
8
- GatewayContentBlock,
9
- GatewayMessage,
10
- GatewayRequest,
11
- GatewayResponse,
12
- GatewayTool,
13
- } from "./types";
14
- import { extractAuth } from "../auth";
15
-
16
- // ---------------------------------------------------------------------------
17
- // OpenAI → GatewayRequest
18
- // ---------------------------------------------------------------------------
19
-
20
- export function parseOpenAIRequest(
21
- body: unknown,
22
- headers: Record<string, string>,
23
- ): GatewayRequest {
24
- const raw = (body ?? {}) as Record<string, unknown>;
25
-
26
- // Extract known fields
27
- const model = String(raw.model ?? "");
28
- const stream = raw.stream === true;
29
-
30
- // max_tokens defaults to 4096 if not specified
31
- const maxTokens =
32
- typeof raw.max_tokens === "number" ? raw.max_tokens : 4096;
33
-
34
- // Extract extras (temperature, top_p, etc.) for later forwarding
35
- const extras: GatewayRequest["extras"] = {};
36
- if (typeof raw.temperature === "number") {
37
- extras.temperature = raw.temperature;
38
- }
39
- if (typeof raw.top_p === "number") {
40
- extras.top_p = raw.top_p;
41
- }
42
- if (typeof raw.frequency_penalty === "number") {
43
- extras.frequency_penalty = raw.frequency_penalty;
44
- }
45
- if (typeof raw.presence_penalty === "number") {
46
- extras.presence_penalty = raw.presence_penalty;
47
- }
48
- if (typeof raw.user === "string") {
49
- extras.user = raw.user;
50
- }
51
- if (raw.logprobs === true || raw.logprobs === false) {
52
- extras.logprobs = raw.logprobs;
53
- }
54
- if (typeof raw.top_logprobs === "number") {
55
- extras.top_logprobs = raw.top_logprobs;
56
- }
57
-
58
- // Parse messages and extract system prompt
59
- const rawMessages = Array.isArray(raw.messages) ? raw.messages : [];
60
- let system = "";
61
- const messages: GatewayMessage[] = [];
62
-
63
- for (const msg of rawMessages as Array<Record<string, unknown>>) {
64
- const role = msg.role as string;
65
- const content = msg.content;
66
-
67
- if (role === "system") {
68
- // Concatenate multiple system messages with double newline
69
- const text = typeof content === "string" ? content : "";
70
- if (system) {
71
- system += "\n\n" + text;
72
- } else {
73
- system = text;
74
- }
75
- continue;
76
- }
77
-
78
- if (role === "user") {
79
- const blocks = parseUserContent(content, msg.tool_calls as Array<Record<string, unknown>> | undefined);
80
- messages.push({ role: "user", content: blocks });
81
- continue;
82
- }
83
-
84
- if (role === "assistant") {
85
- const blocks = parseAssistantContent(
86
- content,
87
- msg.tool_calls as Array<Record<string, unknown>> | undefined,
88
- );
89
- messages.push({ role: "assistant", content: blocks });
90
- continue;
91
- }
92
-
93
- if (role === "tool") {
94
- // Tool results are already represented in the content of the user message
95
- // that follows them in OpenAI. We process them when we encounter the
96
- // assistant message that generated the tool call.
97
- const toolResultBlocks = parseToolResult(msg);
98
- if (toolResultBlocks.length > 0) {
99
- messages.push({ role: "user", content: toolResultBlocks });
100
- }
101
- continue;
102
- }
103
- }
104
-
105
- // Parse tools
106
- const rawTools = Array.isArray(raw.tools) ? raw.tools : [];
107
- const tools: GatewayTool[] = rawTools.map(
108
- (t: Record<string, unknown>) => {
109
- const func = t.function as Record<string, unknown> | undefined;
110
- return {
111
- name: String(func?.name ?? t.name ?? ""),
112
- description: String(func?.description ?? ""),
113
- inputSchema: (func?.parameters as Record<string, unknown>) ?? {},
114
- };
115
- },
116
- );
117
-
118
- return {
119
- protocol: "openai",
120
- model,
121
- system,
122
- messages,
123
- tools,
124
- stream,
125
- maxTokens,
126
- metadata: {},
127
- rawHeaders: {
128
- ...headers,
129
- "x-api-key": headers["x-api-key"] ?? "",
130
- },
131
- extras,
132
- };
133
- }
134
-
135
- function parseUserContent(
136
- content: unknown,
137
- toolCalls?: Array<Record<string, unknown>>,
138
- ): GatewayContentBlock[] {
139
- const blocks: GatewayContentBlock[] = [];
140
-
141
- if (typeof content === "string" && content) {
142
- blocks.push({ type: "text", text: content });
143
- } else if (Array.isArray(content)) {
144
- for (const item of content as Array<Record<string, unknown>>) {
145
- if (item.type === "text") {
146
- blocks.push({ type: "text", text: String(item.text ?? "") });
147
- } else if (item.type === "tool_use") {
148
- blocks.push({
149
- type: "tool_use",
150
- id: String(item.id ?? ""),
151
- name: String(item.name ?? ""),
152
- input: item.input ?? {},
153
- });
154
- }
155
- }
156
- }
157
-
158
- // Add tool_use blocks from tool_calls field
159
- if (toolCalls) {
160
- for (const tc of toolCalls) {
161
- const fn = tc.function as Record<string, unknown> | undefined;
162
- blocks.push({
163
- type: "tool_use",
164
- id: String(tc.id ?? ""),
165
- name: String(fn?.name ?? ""),
166
- input: fn?.arguments ? JSON.parse(fn.arguments as string) : {},
167
- });
168
- }
169
- }
170
-
171
- return blocks;
172
- }
173
-
174
- function parseAssistantContent(
175
- content: unknown,
176
- toolCalls?: Array<Record<string, unknown>>,
177
- ): GatewayContentBlock[] {
178
- const blocks: GatewayContentBlock[] = [];
179
-
180
- if (typeof content === "string" && content) {
181
- blocks.push({ type: "text", text: content });
182
- } else if (Array.isArray(content)) {
183
- for (const item of content as Array<Record<string, unknown>>) {
184
- if (item.type === "text") {
185
- blocks.push({ type: "text", text: String(item.text ?? "") });
186
- } else if (item.type === "tool_use") {
187
- blocks.push({
188
- type: "tool_use",
189
- id: String(item.id ?? ""),
190
- name: String(item.name ?? ""),
191
- input: item.input ?? {},
192
- });
193
- }
194
- }
195
- }
196
-
197
- // Add tool_use blocks from tool_calls field
198
- if (toolCalls) {
199
- for (const tc of toolCalls) {
200
- const fn = tc.function as Record<string, unknown> | undefined;
201
- blocks.push({
202
- type: "tool_use",
203
- id: String(tc.id ?? ""),
204
- name: String(fn?.name ?? ""),
205
- input: fn?.arguments ? JSON.parse(fn.arguments as string) : {},
206
- });
207
- }
208
- }
209
-
210
- return blocks;
211
- }
212
-
213
- function parseToolResult(msg: Record<string, unknown>): GatewayContentBlock[] {
214
- const blocks: GatewayContentBlock[] = [];
215
- const toolCallId = String(msg.tool_call_id ?? "");
216
- const content = msg.content;
217
-
218
- if (typeof content === "string" && content) {
219
- blocks.push({
220
- type: "tool_result",
221
- toolUseId: toolCallId,
222
- content,
223
- });
224
- } else if (Array.isArray(content)) {
225
- for (const item of content as Array<Record<string, unknown>>) {
226
- if (item.type === "text") {
227
- blocks.push({
228
- type: "tool_result",
229
- toolUseId: toolCallId,
230
- content: String(item.text ?? ""),
231
- });
232
- }
233
- }
234
- }
235
-
236
- return blocks;
237
- }
238
-
239
- // ---------------------------------------------------------------------------
240
- // GatewayResponse → OpenAI response
241
- // ---------------------------------------------------------------------------
242
-
243
- export function buildOpenAIResponse(
244
- resp: GatewayResponse,
245
- wasStreaming: boolean,
246
- ): Response {
247
- if (wasStreaming) {
248
- return buildOpenAIStreamResponse(resp);
249
- }
250
- return buildOpenAINonStreamResponse(resp);
251
- }
252
-
253
- function buildOpenAINonStreamResponse(resp: GatewayResponse): Response {
254
- const chunks: unknown[] = [];
255
- let content = "";
256
- const toolCalls: Array<Record<string, unknown>> = [];
257
-
258
- for (const block of resp.content) {
259
- if (block.type === "text") {
260
- content += block.text;
261
- } else if (block.type === "tool_use") {
262
- toolCalls.push({
263
- id: block.id,
264
- type: "function",
265
- function: {
266
- name: block.name,
267
- arguments: JSON.stringify(block.input),
268
- },
269
- });
270
- }
271
- }
272
-
273
- const message: Record<string, unknown> = {
274
- role: "assistant",
275
- content: content || null,
276
- };
277
-
278
- if (toolCalls.length > 0) {
279
- message.tool_calls = toolCalls;
280
- }
281
-
282
- const response = {
283
- id: resp.id.startsWith("chatcmpl-") ? resp.id : `chatcmpl-${resp.id}`,
284
- object: "chat.completion",
285
- created: Math.floor(Date.now() / 1000),
286
- model: resp.model,
287
- choices: [
288
- {
289
- index: 0,
290
- message,
291
- finish_reason: mapStopReason(resp.stopReason),
292
- logprobs: null,
293
- },
294
- ],
295
- usage: {
296
- prompt_tokens: resp.usage.inputTokens,
297
- completion_tokens: resp.usage.outputTokens,
298
- total_tokens:
299
- resp.usage.inputTokens + resp.usage.outputTokens,
300
- },
301
- };
302
-
303
- return new Response(JSON.stringify(response), {
304
- status: 200,
305
- headers: { "content-type": "application/json" },
306
- });
307
- }
308
-
309
- function mapStopReason(reason: string): string {
310
- switch (reason) {
311
- case "end_turn":
312
- case "stop":
313
- case "stop_sequence":
314
- return "stop";
315
- case "max_tokens":
316
- case "length":
317
- return "length";
318
- case "tool_use":
319
- return "tool_calls";
320
- default:
321
- return "stop";
322
- }
323
- }
324
-
325
- function buildOpenAIStreamResponse(resp: GatewayResponse): Response {
326
- const encoder = new TextEncoder();
327
- let offset = 0;
328
-
329
- const stream = new ReadableStream({
330
- start(controller) {
331
- const baseId = resp.id.startsWith("chatcmpl-")
332
- ? resp.id
333
- : `chatcmpl-${resp.id}`;
334
- const created = Math.floor(Date.now() / 1000);
335
-
336
- function emitChunk(
337
- delta: Record<string, unknown>,
338
- finishReason: string | null,
339
- ) {
340
- const chunk = {
341
- id: baseId,
342
- object: "chat.completion.chunk",
343
- created,
344
- model: resp.model,
345
- choices: [
346
- {
347
- index: 0,
348
- delta,
349
- finish_reason: finishReason,
350
- },
351
- ],
352
- };
353
- controller.enqueue(
354
- encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`),
355
- );
356
- }
357
-
358
- // Emit role in first chunk
359
- emitChunk({ role: "assistant" }, null);
360
-
361
- // Process content blocks
362
- for (const block of resp.content) {
363
- if (block.type === "text") {
364
- // Split text into small chunks to simulate streaming
365
- const text = block.text;
366
- let pos = 0;
367
- while (pos < text.length) {
368
- const chunk = text.slice(pos, pos + 10);
369
- emitChunk({ content: chunk }, null);
370
- pos += 10;
371
- }
372
- } else if (block.type === "tool_use") {
373
- emitChunk(
374
- {
375
- tool_calls: [
376
- {
377
- index: offset,
378
- id: block.id,
379
- type: "function",
380
- function: {
381
- name: block.name,
382
- arguments: JSON.stringify(block.input),
383
- },
384
- },
385
- ],
386
- },
387
- null,
388
- );
389
- offset++;
390
- }
391
- }
392
-
393
- // Emit final chunk with finish reason
394
- emitChunk({}, mapStopReason(resp.stopReason));
395
-
396
- // Send [DONE] marker
397
- controller.enqueue(encoder.encode("data: [DONE]\n\n"));
398
- controller.close();
399
- },
400
- });
401
-
402
- return new Response(stream, {
403
- status: 200,
404
- headers: {
405
- "content-type": "text/event-stream",
406
- "cache-control": "no-cache",
407
- connection: "keep-alive",
408
- },
409
- });
410
- }
411
-
412
- // ---------------------------------------------------------------------------
413
- // GatewayRequest → OpenAI upstream request
414
- // ---------------------------------------------------------------------------
415
-
416
- export function buildOpenAIUpstreamRequest(
417
- req: GatewayRequest,
418
- upstreamBase: string,
419
- ): { url: string; headers: Record<string, string>; body: unknown } {
420
- const headers: Record<string, string> = {
421
- "content-type": "application/json",
422
- };
423
-
424
- // Forward auth from the original request — OpenAI-protocol upstreams
425
- // always use Bearer regardless of the incoming auth scheme.
426
- const cred = extractAuth(req.rawHeaders);
427
- if (cred) {
428
- headers["Authorization"] = `Bearer ${cred.value}`;
429
- }
430
-
431
- const body: Record<string, unknown> = {
432
- model: req.model,
433
- messages: buildOpenAIMessages(req.messages, req.system),
434
- stream: req.stream,
435
- };
436
-
437
- if (req.maxTokens) {
438
- body.max_tokens = req.maxTokens;
439
- }
440
-
441
- // Add tools in OpenAI format
442
- if (req.tools.length > 0) {
443
- body.tools = req.tools.map((t) => ({
444
- type: "function",
445
- function: {
446
- name: t.name,
447
- description: t.description,
448
- parameters: t.inputSchema,
449
- },
450
- }));
451
- }
452
-
453
- // Forward extras
454
- if (req.extras) {
455
- if (req.extras.temperature !== undefined) {
456
- body.temperature = req.extras.temperature;
457
- }
458
- if (req.extras.top_p !== undefined) {
459
- body.top_p = req.extras.top_p;
460
- }
461
- if (req.extras.frequency_penalty !== undefined) {
462
- body.frequency_penalty = req.extras.frequency_penalty;
463
- }
464
- if (req.extras.presence_penalty !== undefined) {
465
- body.presence_penalty = req.extras.presence_penalty;
466
- }
467
- if (req.extras.user !== undefined) {
468
- body.user = req.extras.user;
469
- }
470
- if (req.extras.logprobs !== undefined) {
471
- body.logprobs = req.extras.logprobs;
472
- }
473
- if (req.extras.top_logprobs !== undefined) {
474
- body.top_logprobs = req.extras.top_logprobs;
475
- }
476
- }
477
-
478
- return {
479
- url: `${upstreamBase}/v1/chat/completions`,
480
- headers,
481
- body,
482
- };
483
- }
484
-
485
- function buildOpenAIMessages(
486
- messages: GatewayMessage[],
487
- system: string,
488
- ): Array<Record<string, unknown>> {
489
- const result: Array<Record<string, unknown>> = [];
490
-
491
- // Add system prompt if present
492
- if (system) {
493
- result.push({ role: "system", content: system });
494
- }
495
-
496
- for (const msg of messages) {
497
- const blocks = msg.content;
498
- const role = msg.role;
499
-
500
- // Find text content and tool_use blocks
501
- const textParts: string[] = [];
502
- const toolUses: Array<Record<string, unknown>> = [];
503
-
504
- for (const block of blocks) {
505
- if (block.type === "text") {
506
- textParts.push(block.text);
507
- } else if (block.type === "tool_use") {
508
- toolUses.push({
509
- id: block.id,
510
- type: "function",
511
- function: {
512
- name: block.name,
513
- arguments: JSON.stringify(block.input),
514
- },
515
- });
516
- } else if (block.type === "tool_result") {
517
- // tool_result comes from previous assistant tool_use calls
518
- // It's typically attached to the user message as a tool result
519
- }
520
- }
521
-
522
- const msgRecord: Record<string, unknown> = { role };
523
-
524
- if (textParts.length > 0) {
525
- msgRecord.content = textParts.join("");
526
- }
527
-
528
- if (toolUses.length > 0) {
529
- msgRecord.tool_calls = toolUses;
530
- }
531
-
532
- result.push(msgRecord);
533
- }
534
-
535
- return result;
536
- }
@@ -1,177 +0,0 @@
1
- /**
2
- * Internal representation types for the Lore gateway.
3
- *
4
- * The gateway accepts both Anthropic (`/v1/messages`) and OpenAI
5
- * (`/v1/chat/completions`) protocol requests, normalizes them into these
6
- * types for Lore pipeline processing, then translates back to the original
7
- * protocol for the upstream response.
8
- *
9
- * Design: types are intentionally minimal — only fields that Lore's context
10
- * management (gradient, LTM, distillation) actually reads/writes. Protocol-
11
- * specific fields the gateway doesn't process live in `metadata`.
12
- */
13
-
14
- // ---------------------------------------------------------------------------
15
- // Content blocks — discriminated union on `type`
16
- // ---------------------------------------------------------------------------
17
-
18
- export type GatewayTextBlock = {
19
- type: "text";
20
- text: string;
21
- };
22
-
23
- export type GatewayThinkingBlock = {
24
- type: "thinking";
25
- thinking: string;
26
- /** Anthropic extended thinking signature, opaque bytes. */
27
- signature?: string;
28
- };
29
-
30
- export type GatewayToolUseBlock = {
31
- type: "tool_use";
32
- /** Provider-assigned tool call ID (e.g. `toolu_…` for Anthropic). */
33
- id: string;
34
- name: string;
35
- input: unknown;
36
- };
37
-
38
- export type GatewayToolResultBlock = {
39
- type: "tool_result";
40
- /** ID of the tool_use block this result corresponds to. */
41
- toolUseId: string;
42
- content: string;
43
- isError?: boolean;
44
- };
45
-
46
- export type GatewayContentBlock =
47
- | GatewayTextBlock
48
- | GatewayThinkingBlock
49
- | GatewayToolUseBlock
50
- | GatewayToolResultBlock;
51
-
52
- // ---------------------------------------------------------------------------
53
- // Messages
54
- // ---------------------------------------------------------------------------
55
-
56
- /** Normalized message — system messages are extracted to `GatewayRequest.system`. */
57
- export type GatewayMessage = {
58
- role: "user" | "assistant";
59
- content: GatewayContentBlock[];
60
- };
61
-
62
- // ---------------------------------------------------------------------------
63
- // Tools
64
- // ---------------------------------------------------------------------------
65
-
66
- /** Normalized tool definition. Both protocols use JSON Schema for input. */
67
- export type GatewayTool = {
68
- name: string;
69
- description: string;
70
- inputSchema: Record<string, unknown>;
71
- };
72
-
73
- // ---------------------------------------------------------------------------
74
- // Request — the normalized form after ingress translation
75
- // ---------------------------------------------------------------------------
76
-
77
- export type GatewayProtocol = "anthropic" | "openai";
78
-
79
- /** Normalized request after ingress translation from either protocol. */
80
- export type GatewayRequest = {
81
- /** Which protocol the request arrived as — determines egress translation. */
82
- protocol: GatewayProtocol;
83
- /** Model identifier (e.g. `claude-sonnet-4-20250514`, `gpt-4o`). */
84
- model: string;
85
- /**
86
- * Extracted system prompt.
87
- * - Anthropic: top-level `system` field.
88
- * - OpenAI: first message with `role: "system"`, removed from messages.
89
- */
90
- system: string;
91
- messages: GatewayMessage[];
92
- tools: GatewayTool[];
93
- stream: boolean;
94
- maxTokens: number;
95
- /**
96
- * Protocol-specific parameters the gateway doesn't process but must
97
- * forward to the upstream provider (e.g. `temperature`, `top_p`,
98
- * `stop_sequences`, `tool_choice`).
99
- */
100
- metadata: Record<string, unknown>;
101
- /** Original request headers — passed through for auth, tracing, etc. */
102
- rawHeaders: Record<string, string>;
103
- /**
104
- * Additional OpenAI-compatible parameters preserved for upstream forwarding.
105
- * Populated by `parseOpenAIRequest`.
106
- */
107
- extras?: {
108
- temperature?: number;
109
- top_p?: number;
110
- frequency_penalty?: number;
111
- presence_penalty?: number;
112
- user?: string;
113
- logprobs?: boolean;
114
- top_logprobs?: number;
115
- };
116
- };
117
-
118
- // ---------------------------------------------------------------------------
119
- // Response — accumulated from upstream streaming/non-streaming response
120
- // ---------------------------------------------------------------------------
121
-
122
- export type GatewayUsage = {
123
- inputTokens: number;
124
- outputTokens: number;
125
- /** Anthropic prompt caching — present when cache hits occur. */
126
- cacheReadInputTokens?: number;
127
- /** Anthropic prompt caching — tokens written to cache on this request. */
128
- cacheCreationInputTokens?: number;
129
- };
130
-
131
- /** Accumulated response from the upstream provider. */
132
- export type GatewayResponse = {
133
- id: string;
134
- model: string;
135
- content: GatewayContentBlock[];
136
- /** Provider stop reason (e.g. `end_turn`, `stop`, `tool_use`, `length`). */
137
- stopReason: string;
138
- usage: GatewayUsage;
139
- };
140
-
141
- // ---------------------------------------------------------------------------
142
- // Pending recall state (cross-request, gateway recall interception)
143
- // ---------------------------------------------------------------------------
144
-
145
- /** Pending recall result stored between requests (Case 2: mixed tools). */
146
- export type PendingRecall = {
147
- /** tool_use ID from the suppressed block. */
148
- toolUseId: string;
149
- /** The original recall input (for conversation history reconstruction). */
150
- input: { query: string; scope?: string };
151
- /** Position (content block index) in the original assistant message. */
152
- position: number;
153
- /** Executed recall result (formatted markdown). */
154
- result: string;
155
- /** Timestamp for TTL-based cleanup. */
156
- timestamp: number;
157
- };
158
-
159
- // ---------------------------------------------------------------------------
160
- // Session state — per-session tracking for Lore pipeline integration
161
- // ---------------------------------------------------------------------------
162
-
163
- /** Per-session state tracked by the gateway for Lore pipeline decisions. */
164
- export type SessionState = {
165
- sessionID: string;
166
- projectPath: string;
167
- /** SHA-256 fingerprint of the first user message — used for session correlation. */
168
- fingerprint: string;
169
- /** Unix timestamp (ms) of the last request in this session. */
170
- lastRequestTime: number;
171
- /** Total user+assistant messages seen in this session. */
172
- messageCount: number;
173
- /** Turns since last curation run — triggers background curation. */
174
- turnsSinceCuration: number;
175
- /** Pending recall result from previous turn (Case 2: mixed tool interception). */
176
- pendingRecall?: PendingRecall;
177
- };