@loreai/gateway 0.14.0 → 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
- }