@layer-ai/core 2.0.14 → 2.0.16

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.
@@ -24,4 +24,11 @@ export declare function getProviderForModel(model: SupportedModel): Provider;
24
24
  * @param userId - Optional user ID for BYOK key resolution
25
25
  */
26
26
  export declare function callAdapter(request: LayerRequest, userId?: string): Promise<LayerResponse>;
27
+ /**
28
+ * Calls the appropriate provider adapter for streaming responses.
29
+ * This is the streaming version of callAdapter.
30
+ * @param request - The Layer request to execute
31
+ * @param userId - Optional user ID for BYOK key resolution
32
+ */
33
+ export declare function callAdapterStream(request: LayerRequest, userId?: string): AsyncIterable<LayerResponse>;
27
34
  //# sourceMappingURL=provider-factory.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"provider-factory.d.ts","sourceRoot":"","sources":["../../src/lib/provider-factory.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAEjF,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAG7E,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,QAAQ,EAAE,CAAC;AAa9C;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CAmBhE;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,cAAc,GAAG,QAAQ,CAMnE;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAWhG"}
1
+ {"version":3,"file":"provider-factory.d.ts","sourceRoot":"","sources":["../../src/lib/provider-factory.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAEjF,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAG7E,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,QAAQ,EAAE,CAAC;AAa9C;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CAmBhE;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,cAAc,GAAG,QAAQ,CAMnE;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAWhG;AAED;;;;;GAKG;AACH,wBAAuB,iBAAiB,CAAC,OAAO,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,aAAa,CAAC,aAAa,CAAC,CAgB7G"}
@@ -71,3 +71,22 @@ export async function callAdapter(request, userId) {
71
71
  const adapter = new AdapterClass();
72
72
  return await adapter.call(request, userId);
73
73
  }
74
+ /**
75
+ * Calls the appropriate provider adapter for streaming responses.
76
+ * This is the streaming version of callAdapter.
77
+ * @param request - The Layer request to execute
78
+ * @param userId - Optional user ID for BYOK key resolution
79
+ */
80
+ export async function* callAdapterStream(request, userId) {
81
+ const normalizedModel = normalizeModelId(request.model);
82
+ const provider = getProviderForModel(normalizedModel);
83
+ const AdapterClass = PROVIDER_ADAPTERS[provider];
84
+ if (!AdapterClass) {
85
+ throw new Error(`No adapter found for provider: ${provider}`);
86
+ }
87
+ const adapter = new AdapterClass();
88
+ if (!adapter.callStream) {
89
+ throw new Error(`Streaming not supported for provider: ${provider}`);
90
+ }
91
+ yield* adapter.callStream(request, userId);
92
+ }
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * Test streaming at the chat route level
4
+ * Tests routing strategies with streaming support
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=test-chat-streaming.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-chat-streaming.d.ts","sourceRoot":"","sources":["../../../src/routes/tests/test-chat-streaming.ts"],"names":[],"mappings":";AACA;;;GAGG"}
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * Test streaming at the chat route level
4
+ * Tests routing strategies with streaming support
5
+ */
6
+ import { callAdapterStream } from '../../lib/provider-factory.js';
7
+ console.log('='.repeat(80));
8
+ console.log('STREAMING - CHAT ROUTE LEVEL TESTS');
9
+ console.log('='.repeat(80));
10
+ console.log('');
11
+ // Simplified versions of the routing functions from chat.ts
12
+ async function* executeWithFallbackStream(request, modelsToTry) {
13
+ let lastError = null;
14
+ for (const modelToTry of modelsToTry) {
15
+ try {
16
+ const modelRequest = { ...request, model: modelToTry };
17
+ yield* callAdapterStream(modelRequest);
18
+ return; // Success!
19
+ }
20
+ catch (error) {
21
+ lastError = error;
22
+ console.log(` Model ${modelToTry} failed, trying next fallback...`);
23
+ continue;
24
+ }
25
+ }
26
+ throw lastError || new Error('All models failed during streaming');
27
+ }
28
+ async function* executeWithRoundRobinStream(gateConfig, request) {
29
+ let selectedModel = request.model;
30
+ if (gateConfig.fallbackModels?.length) {
31
+ const allModels = [gateConfig.model, ...gateConfig.fallbackModels];
32
+ const modelIndex = Math.floor(Math.random() * allModels.length);
33
+ selectedModel = allModels[modelIndex];
34
+ console.log(` Selected model via round-robin: ${selectedModel}`);
35
+ }
36
+ const modelRequest = { ...request, model: selectedModel };
37
+ yield* callAdapterStream(modelRequest);
38
+ }
39
+ // Test 1: Single routing with streaming
40
+ async function testSingleRouting() {
41
+ console.log('Test 1: Single Routing with Streaming');
42
+ console.log('-'.repeat(80));
43
+ const request = {
44
+ gateId: 'test-gate',
45
+ model: 'gpt-4o',
46
+ type: 'chat',
47
+ data: {
48
+ messages: [
49
+ { role: 'user', content: 'Say "test passed" and nothing else.' }
50
+ ],
51
+ maxTokens: 10,
52
+ stream: true,
53
+ }
54
+ };
55
+ let chunkCount = 0;
56
+ let fullContent = '';
57
+ for await (const chunk of callAdapterStream(request)) {
58
+ chunkCount++;
59
+ if (chunk.content) {
60
+ fullContent += chunk.content;
61
+ }
62
+ }
63
+ console.log(` Chunks received: ${chunkCount}`);
64
+ console.log(` Content: ${fullContent.trim()}`);
65
+ console.log(' ✅ Single routing test passed\n');
66
+ }
67
+ // Test 2: Fallback routing with streaming
68
+ async function testFallbackRouting() {
69
+ console.log('Test 2: Fallback Routing with Streaming');
70
+ console.log('-'.repeat(80));
71
+ const request = {
72
+ gateId: 'test-gate',
73
+ model: 'invalid-model-1', // This will fail
74
+ type: 'chat',
75
+ data: {
76
+ messages: [
77
+ { role: 'user', content: 'Say "fallback worked" and nothing else.' }
78
+ ],
79
+ maxTokens: 10,
80
+ stream: true,
81
+ }
82
+ };
83
+ const modelsToTry = [
84
+ 'invalid-model-1', // Should fail
85
+ 'invalid-model-2', // Should fail
86
+ 'gpt-4o', // Should succeed
87
+ ];
88
+ let chunkCount = 0;
89
+ let fullContent = '';
90
+ let succeeded = false;
91
+ try {
92
+ for await (const chunk of executeWithFallbackStream(request, modelsToTry)) {
93
+ chunkCount++;
94
+ if (chunk.content) {
95
+ fullContent += chunk.content;
96
+ }
97
+ }
98
+ succeeded = true;
99
+ }
100
+ catch (error) {
101
+ console.error(' ❌ Fallback failed:', error instanceof Error ? error.message : error);
102
+ }
103
+ if (succeeded) {
104
+ console.log(` Chunks received: ${chunkCount}`);
105
+ console.log(` Content: ${fullContent.trim()}`);
106
+ console.log(' ✅ Fallback routing test passed\n');
107
+ }
108
+ }
109
+ // Test 3: Round-robin routing with streaming
110
+ async function testRoundRobinRouting() {
111
+ console.log('Test 3: Round-Robin Routing with Streaming');
112
+ console.log('-'.repeat(80));
113
+ const gateConfig = {
114
+ id: 'test-gate-id',
115
+ name: 'Test Gate',
116
+ model: 'gpt-4o',
117
+ taskType: 'chat',
118
+ routingStrategy: 'round-robin',
119
+ fallbackModels: ['gpt-4o', 'gpt-3.5-turbo'],
120
+ userId: 'test-user',
121
+ createdAt: new Date(),
122
+ updatedAt: new Date(),
123
+ };
124
+ const request = {
125
+ gateId: 'test-gate',
126
+ model: 'gpt-4o',
127
+ type: 'chat',
128
+ data: {
129
+ messages: [
130
+ { role: 'user', content: 'Say "round-robin worked" and nothing else.' }
131
+ ],
132
+ maxTokens: 10,
133
+ stream: true,
134
+ }
135
+ };
136
+ let chunkCount = 0;
137
+ let fullContent = '';
138
+ for await (const chunk of executeWithRoundRobinStream(gateConfig, request)) {
139
+ chunkCount++;
140
+ if (chunk.content) {
141
+ fullContent += chunk.content;
142
+ }
143
+ }
144
+ console.log(` Chunks received: ${chunkCount}`);
145
+ console.log(` Content: ${fullContent.trim()}`);
146
+ console.log(' ✅ Round-robin routing test passed\n');
147
+ }
148
+ // Test 4: Streaming with tool calls
149
+ async function testStreamingWithTools() {
150
+ console.log('Test 4: Streaming with Tool Calls');
151
+ console.log('-'.repeat(80));
152
+ const request = {
153
+ gateId: 'test-gate',
154
+ model: 'gpt-4o',
155
+ type: 'chat',
156
+ data: {
157
+ messages: [
158
+ { role: 'user', content: 'What is the weather in Paris?' }
159
+ ],
160
+ tools: [
161
+ {
162
+ type: 'function',
163
+ function: {
164
+ name: 'get_weather',
165
+ description: 'Get weather for a location',
166
+ parameters: {
167
+ type: 'object',
168
+ properties: {
169
+ location: { type: 'string' },
170
+ },
171
+ required: ['location'],
172
+ },
173
+ },
174
+ },
175
+ ],
176
+ stream: true,
177
+ }
178
+ };
179
+ let toolCallsFound = false;
180
+ let finishReason = null;
181
+ for await (const chunk of callAdapterStream(request)) {
182
+ if (chunk.toolCalls && chunk.toolCalls.length > 0) {
183
+ toolCallsFound = true;
184
+ }
185
+ if (chunk.finishReason) {
186
+ finishReason = chunk.finishReason;
187
+ }
188
+ }
189
+ console.log(` Tool calls found: ${toolCallsFound}`);
190
+ console.log(` Finish reason: ${finishReason}`);
191
+ if (toolCallsFound && finishReason === 'tool_call') {
192
+ console.log(' ✅ Tool calls streaming test passed\n');
193
+ }
194
+ else {
195
+ console.log(' ⚠️ Tool calls may not have been invoked (model chose not to use tools)\n');
196
+ }
197
+ }
198
+ // Run all tests
199
+ (async () => {
200
+ try {
201
+ await testSingleRouting();
202
+ await testFallbackRouting();
203
+ await testRoundRobinRouting();
204
+ await testStreamingWithTools();
205
+ console.log('='.repeat(80));
206
+ console.log('✅ ALL STREAMING ROUTE TESTS PASSED');
207
+ console.log('='.repeat(80));
208
+ }
209
+ catch (error) {
210
+ console.error('❌ Test failed:', error);
211
+ process.exit(1);
212
+ }
213
+ })();
@@ -1 +1 @@
1
- {"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../../src/routes/v3/chat.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AAIpD,OAAO,KAAK,EAAE,YAAY,EAAiB,IAAI,EAA+C,MAAM,eAAe,CAAC;AAGpH,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AAiBpC,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,IAAI,EAChB,OAAO,EAAE,YAAY,GACpB,YAAY,CAkFd;AA6MD,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../../src/routes/v3/chat.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AAIpD,OAAO,KAAK,EAAE,YAAY,EAAiB,IAAI,EAA+C,MAAM,eAAe,CAAC;AAGpH,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AAiBpC,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,IAAI,EAChB,OAAO,EAAE,YAAY,GACpB,YAAY,CAiFd;AAgWD,eAAe,MAAM,CAAC"}
@@ -1,7 +1,7 @@
1
1
  import { Router } from 'express';
2
2
  import { db } from '../../lib/db/postgres.js';
3
3
  import { authenticate } from '../../middleware/auth.js';
4
- import { callAdapter, normalizeModelId, getProviderForModel, PROVIDER } from '../../lib/provider-factory.js';
4
+ import { callAdapter, callAdapterStream, normalizeModelId, getProviderForModel, PROVIDER } from '../../lib/provider-factory.js';
5
5
  import { OverrideField } from '@layer-ai/sdk';
6
6
  const router = Router();
7
7
  // MARK:- Helper Functions
@@ -22,7 +22,6 @@ export function resolveFinalRequest(gateConfig, request) {
22
22
  finalModel = gateConfig.model;
23
23
  }
24
24
  }
25
- // Since this is v3/chat endpoint, we know the data is ChatRequest
26
25
  const chatData = { ...request.data };
27
26
  if (!chatData.systemPrompt && gateConfig.systemPrompt) {
28
27
  chatData.systemPrompt = gateConfig.systemPrompt;
@@ -141,6 +140,48 @@ async function executeWithRouting(gateConfig, request, userId) {
141
140
  return { result, modelUsed: request.model };
142
141
  }
143
142
  }
143
+ async function* executeWithFallbackStream(request, modelsToTry, userId) {
144
+ let lastError = null;
145
+ for (const modelToTry of modelsToTry) {
146
+ try {
147
+ const modelRequest = { ...request, model: modelToTry };
148
+ yield* callAdapterStream(modelRequest, userId);
149
+ return;
150
+ }
151
+ catch (error) {
152
+ lastError = error;
153
+ console.log(`Model ${modelToTry} failed during streaming, trying next fallback...`, error instanceof Error ? error.message : error);
154
+ continue;
155
+ }
156
+ }
157
+ throw lastError || new Error('All models failed during streaming');
158
+ }
159
+ async function* executeWithRoundRobinStream(gateConfig, request, userId) {
160
+ if (!gateConfig.fallbackModels?.length) {
161
+ yield* callAdapterStream(request, userId);
162
+ return;
163
+ }
164
+ const allModels = [gateConfig.model, ...gateConfig.fallbackModels];
165
+ const modelIndex = Math.floor(Math.random() * allModels.length);
166
+ const selectedModel = allModels[modelIndex];
167
+ const modelRequest = { ...request, model: selectedModel };
168
+ yield* callAdapterStream(modelRequest, userId);
169
+ }
170
+ async function* executeWithRoutingStream(gateConfig, request, userId) {
171
+ const modelsToTry = getModelsToTry(gateConfig, request.model);
172
+ switch (gateConfig.routingStrategy) {
173
+ case 'fallback':
174
+ yield* executeWithFallbackStream(request, modelsToTry, userId);
175
+ break;
176
+ case 'round-robin':
177
+ yield* executeWithRoundRobinStream(gateConfig, request, userId);
178
+ break;
179
+ case 'single':
180
+ default:
181
+ yield* callAdapterStream(request, userId);
182
+ break;
183
+ }
184
+ }
144
185
  // MARK:- Route Handler
145
186
  router.post('/', authenticate, async (req, res) => {
146
187
  const startTime = Date.now();
@@ -185,9 +226,96 @@ router.post('/', authenticate, async (req, res) => {
185
226
  metadata: rawRequest.metadata
186
227
  };
187
228
  const finalRequest = resolveFinalRequest(gateConfig, request);
229
+ const isStreaming = finalRequest.data && 'stream' in finalRequest.data && finalRequest.data.stream === true;
230
+ if (isStreaming) {
231
+ res.setHeader('Content-Type', 'text/event-stream');
232
+ res.setHeader('Cache-Control', 'no-cache');
233
+ res.setHeader('Connection', 'keep-alive');
234
+ res.setHeader('X-Accel-Buffering', 'no');
235
+ let promptTokens = 0;
236
+ let completionTokens = 0;
237
+ let totalCost = 0;
238
+ let modelUsed = finalRequest.model;
239
+ try {
240
+ for await (const chunk of executeWithRoutingStream(gateConfig, finalRequest, userId)) {
241
+ if (chunk.usage) {
242
+ promptTokens = chunk.usage.promptTokens || 0;
243
+ completionTokens = chunk.usage.completionTokens || 0;
244
+ }
245
+ if (chunk.cost) {
246
+ totalCost = chunk.cost;
247
+ }
248
+ if (chunk.model) {
249
+ modelUsed = chunk.model;
250
+ }
251
+ res.write(`data: ${JSON.stringify(chunk)}\n\n`);
252
+ }
253
+ res.write(`data: [DONE]\n\n`);
254
+ res.end();
255
+ const latencyMs = Date.now() - startTime;
256
+ db.logRequest({
257
+ userId,
258
+ gateId: gateConfig.id,
259
+ gateName: gateConfig.name,
260
+ modelRequested: request.model || gateConfig.model,
261
+ modelUsed: modelUsed,
262
+ promptTokens,
263
+ completionTokens,
264
+ totalTokens: promptTokens + completionTokens,
265
+ costUsd: totalCost,
266
+ latencyMs,
267
+ success: true,
268
+ errorMessage: null,
269
+ userAgent: req.headers['user-agent'] || null,
270
+ ipAddress: req.ip || null,
271
+ requestPayload: {
272
+ gateId: request.gateId,
273
+ type: request.type,
274
+ model: request.model,
275
+ data: request.data,
276
+ metadata: request.metadata,
277
+ },
278
+ responsePayload: {
279
+ streamed: true,
280
+ model: modelUsed,
281
+ usage: { promptTokens, completionTokens, totalTokens: promptTokens + completionTokens },
282
+ cost: totalCost,
283
+ },
284
+ }).catch(err => console.error('Failed to log request:', err));
285
+ }
286
+ catch (streamError) {
287
+ const errorMessage = streamError instanceof Error ? streamError.message : 'Unknown streaming error';
288
+ res.write(`data: ${JSON.stringify({ error: 'stream_error', message: errorMessage })}\n\n`);
289
+ res.end();
290
+ db.logRequest({
291
+ userId,
292
+ gateId: gateConfig.id,
293
+ gateName: gateConfig.name,
294
+ modelRequested: request.model || gateConfig.model,
295
+ modelUsed: null,
296
+ promptTokens: 0,
297
+ completionTokens: 0,
298
+ totalTokens: 0,
299
+ costUsd: 0,
300
+ latencyMs: Date.now() - startTime,
301
+ success: false,
302
+ errorMessage,
303
+ userAgent: req.headers['user-agent'] || null,
304
+ ipAddress: req.ip || null,
305
+ requestPayload: {
306
+ gateId: request.gateId,
307
+ type: request.type,
308
+ model: request.model,
309
+ data: request.data,
310
+ metadata: request.metadata,
311
+ },
312
+ responsePayload: null,
313
+ }).catch(err => console.error('Failed to log request:', err));
314
+ }
315
+ return;
316
+ }
188
317
  const { result, modelUsed } = await executeWithRouting(gateConfig, finalRequest, userId);
189
318
  const latencyMs = Date.now() - startTime;
190
- // Log request to database
191
319
  db.logRequest({
192
320
  userId,
193
321
  gateId: gateConfig.id,
@@ -218,7 +346,6 @@ router.post('/', authenticate, async (req, res) => {
218
346
  finishReason: result.finishReason,
219
347
  },
220
348
  }).catch(err => console.error('Failed to log request:', err));
221
- // Return LayerResponse with additional metadata
222
349
  const response = {
223
350
  ...result,
224
351
  model: modelUsed,
@@ -17,6 +17,7 @@ export declare abstract class BaseProviderAdapter {
17
17
  protected imageMimeTypeMappings?: Record<ImageMimeType, string>;
18
18
  protected encodingFormatMappings?: Record<EncodingFormat, string>;
19
19
  abstract call(request: LayerRequest, userId?: string): Promise<LayerResponse>;
20
+ callStream?(request: LayerRequest, userId?: string): AsyncIterable<LayerResponse>;
20
21
  protected mapRole(role: Role): string;
21
22
  protected mapImageDetail(detail: ImageDetail): string | undefined;
22
23
  protected mapImageSize(size: ImageSize): string | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"base-adapter.d.ts","sourceRoot":"","sources":["../../../src/services/providers/base-adapter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"base-adapter.d.ts","sourceRoot":"","sources":["../../../src/services/providers/base-adapter.ts"],"names":[],"mappings":"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"}
@@ -12,7 +12,9 @@ export declare class OpenAIAdapter extends BaseProviderAdapter {
12
12
  protected videoSizeMappings: Record<VideoSize, string>;
13
13
  protected audioFormatMappings: Record<AudioFormat, string>;
14
14
  call(request: LayerRequest, userId?: string): Promise<LayerResponse>;
15
+ callStream(request: LayerRequest, userId?: string): AsyncIterable<LayerResponse>;
15
16
  private handleChat;
17
+ private handleChatStream;
16
18
  private handleImageGeneration;
17
19
  private handleEmbeddings;
18
20
  private handleTextToSpeech;
@@ -1 +1 @@
1
- {"version":3,"file":"openai-adapter.d.ts","sourceRoot":"","sources":["../../../src/services/providers/openai-adapter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EACL,YAAY,EACZ,aAAa,EACb,IAAI,EACJ,WAAW,EACX,SAAS,EACT,YAAY,EACZ,UAAU,EACV,SAAS,EACT,WAAW,EACX,YAAY,EACb,MAAM,eAAe,CAAC;AACvB,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAoB1E,qBAAa,aAAc,SAAQ,mBAAmB;IACpD,SAAS,CAAC,QAAQ,EAAE,QAAQ,CAAmB;IAE/C,SAAS,CAAC,YAAY,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAQ1C;IAEF,SAAS,CAAC,mBAAmB,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAIxD;IAEF,SAAS,CAAC,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAK1D;IAEF,SAAS,CAAC,iBAAiB,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAQpD;IAEF,SAAS,CAAC,oBAAoB,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAG1D;IAEF,SAAS,CAAC,kBAAkB,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAGtD;IAEF,SAAS,CAAC,iBAAiB,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAKpD;IAEF,SAAS,CAAC,mBAAmB,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAOxD;IAEI,IAAI,CAAC,OAAO,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;YAoB5D,UAAU;YAoHV,qBAAqB;YAuCrB,gBAAgB;YAkChB,kBAAkB;CA+BjC"}
1
+ {"version":3,"file":"openai-adapter.d.ts","sourceRoot":"","sources":["../../../src/services/providers/openai-adapter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EACL,YAAY,EACZ,aAAa,EACb,IAAI,EACJ,WAAW,EACX,SAAS,EACT,YAAY,EACZ,UAAU,EACV,SAAS,EACT,WAAW,EACX,YAAY,EACb,MAAM,eAAe,CAAC;AACvB,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAkB1E,qBAAa,aAAc,SAAQ,mBAAmB;IACpD,SAAS,CAAC,QAAQ,EAAE,QAAQ,CAAmB;IAE/C,SAAS,CAAC,YAAY,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAQ1C;IAEF,SAAS,CAAC,mBAAmB,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAIxD;IAEF,SAAS,CAAC,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAK1D;IAEF,SAAS,CAAC,iBAAiB,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAQpD;IAEF,SAAS,CAAC,oBAAoB,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAG1D;IAEF,SAAS,CAAC,kBAAkB,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAGtD;IAEF,SAAS,CAAC,iBAAiB,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAKpD;IAEF,SAAS,CAAC,mBAAmB,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAOxD;IAEI,IAAI,CAAC,OAAO,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAmBnE,UAAU,CAAC,OAAO,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,aAAa,CAAC,aAAa,CAAC;YAYzE,UAAU;YA+GT,gBAAgB;YAyIjB,qBAAqB;YAsCrB,gBAAgB;YAkChB,kBAAkB;CA+BjC"}
@@ -4,11 +4,9 @@ import { PROVIDER } from "../../lib/provider-constants.js";
4
4
  import { resolveApiKey } from '../../lib/key-resolver.js';
5
5
  let openai = null;
6
6
  function getOpenAIClient(apiKey) {
7
- // If custom API key provided, create new client
8
7
  if (apiKey) {
9
8
  return new OpenAI({ apiKey });
10
9
  }
11
- // Otherwise use singleton with platform key
12
10
  if (!openai) {
13
11
  openai = new OpenAI({
14
12
  apiKey: process.env.OPENAI_API_KEY,
@@ -73,7 +71,6 @@ export class OpenAIAdapter extends BaseProviderAdapter {
73
71
  };
74
72
  }
75
73
  async call(request, userId) {
76
- // Resolve API key (BYOK → Platform key)
77
74
  const resolved = await resolveApiKey(this.provider, userId, process.env.OPENAI_API_KEY);
78
75
  switch (request.type) {
79
76
  case 'chat':
@@ -90,6 +87,16 @@ export class OpenAIAdapter extends BaseProviderAdapter {
90
87
  throw new Error(`Unknown modality: ${request.type}`);
91
88
  }
92
89
  }
90
+ async *callStream(request, userId) {
91
+ const resolved = await resolveApiKey(this.provider, userId, process.env.OPENAI_API_KEY);
92
+ switch (request.type) {
93
+ case 'chat':
94
+ yield* this.handleChatStream(request, resolved.key, resolved.usedPlatformKey);
95
+ break;
96
+ default:
97
+ throw new Error(`Streaming not supported for type: ${request.type}`);
98
+ }
99
+ }
93
100
  async handleChat(request, apiKey, usedPlatformKey) {
94
101
  const startTime = Date.now();
95
102
  const client = getOpenAIClient(apiKey);
@@ -103,7 +110,6 @@ export class OpenAIAdapter extends BaseProviderAdapter {
103
110
  }
104
111
  for (const msg of chat.messages) {
105
112
  const role = this.mapRole(msg.role);
106
- // Handle vision messages (content + images)
107
113
  if (msg.images && msg.images.length > 0) {
108
114
  const content = [];
109
115
  if (msg.content) {
@@ -121,7 +127,6 @@ export class OpenAIAdapter extends BaseProviderAdapter {
121
127
  }
122
128
  messages.push({ role: role, content });
123
129
  }
124
- // Handle tool responses (mutually exclusive)
125
130
  else if (msg.toolCallId) {
126
131
  messages.push({
127
132
  role: 'tool',
@@ -129,7 +134,6 @@ export class OpenAIAdapter extends BaseProviderAdapter {
129
134
  tool_call_id: msg.toolCallId,
130
135
  });
131
136
  }
132
- // Handle assistant messages with tool calls (can have content + tool_calls)
133
137
  else if (msg.toolCalls) {
134
138
  messages.push({
135
139
  role: 'assistant',
@@ -137,7 +141,6 @@ export class OpenAIAdapter extends BaseProviderAdapter {
137
141
  tool_calls: msg.toolCalls,
138
142
  });
139
143
  }
140
- // Handle regular text messages
141
144
  else {
142
145
  messages.push({
143
146
  role,
@@ -161,7 +164,6 @@ export class OpenAIAdapter extends BaseProviderAdapter {
161
164
  tools: chat.tools,
162
165
  ...(chat.toolChoice && { tool_choice: chat.toolChoice }),
163
166
  }),
164
- // Structured output support: convert camelCase to snake_case
165
167
  ...(chat.responseFormat && {
166
168
  response_format: (typeof chat.responseFormat === 'string'
167
169
  ? { type: chat.responseFormat }
@@ -191,6 +193,119 @@ export class OpenAIAdapter extends BaseProviderAdapter {
191
193
  raw: response,
192
194
  };
193
195
  }
196
+ async *handleChatStream(request, apiKey, usedPlatformKey) {
197
+ const startTime = Date.now();
198
+ const client = getOpenAIClient(apiKey);
199
+ const { data: chat, model } = request;
200
+ if (!model) {
201
+ throw new Error('Model is required for chat completion');
202
+ }
203
+ const messages = [];
204
+ if (chat.systemPrompt) {
205
+ messages.push({ role: 'system', content: chat.systemPrompt });
206
+ }
207
+ for (const msg of chat.messages) {
208
+ const role = this.mapRole(msg.role);
209
+ if (msg.images && msg.images.length > 0) {
210
+ const content = [];
211
+ if (msg.content) {
212
+ content.push({ type: 'text', text: msg.content });
213
+ }
214
+ for (const image of msg.images) {
215
+ const imageUrl = image.url || `data:${image.mimeType || 'image/jpeg'};base64,${image.base64}`;
216
+ content.push({
217
+ type: 'image_url',
218
+ image_url: {
219
+ url: imageUrl,
220
+ ...(image.detail && { detail: this.mapImageDetail(image.detail) }),
221
+ },
222
+ });
223
+ }
224
+ messages.push({ role: role, content });
225
+ }
226
+ else if (msg.toolCallId) {
227
+ messages.push({
228
+ role: 'tool',
229
+ content: msg.content || '',
230
+ tool_call_id: msg.toolCallId,
231
+ });
232
+ }
233
+ else if (msg.toolCalls) {
234
+ messages.push({
235
+ role: 'assistant',
236
+ content: msg.content || null,
237
+ tool_calls: msg.toolCalls,
238
+ });
239
+ }
240
+ else {
241
+ messages.push({
242
+ role,
243
+ content: msg.content || '',
244
+ ...(role === 'function' && msg.name && { name: msg.name }),
245
+ });
246
+ }
247
+ }
248
+ const openaiRequest = {
249
+ model: model,
250
+ messages,
251
+ stream: true,
252
+ ...(chat.temperature !== undefined && { temperature: chat.temperature }),
253
+ ...(chat.maxTokens !== undefined && { max_completion_tokens: chat.maxTokens }),
254
+ ...(chat.topP !== undefined && { top_p: chat.topP }),
255
+ ...(chat.stopSequences !== undefined && { stop: chat.stopSequences }),
256
+ ...(chat.frequencyPenalty !== undefined && { frequency_penalty: chat.frequencyPenalty }),
257
+ ...(chat.presencePenalty !== undefined && { presence_penalty: chat.presencePenalty }),
258
+ ...(chat.seed !== undefined && { seed: chat.seed }),
259
+ ...(chat.tools && {
260
+ tools: chat.tools,
261
+ ...(chat.toolChoice && { tool_choice: chat.toolChoice }),
262
+ }),
263
+ ...(chat.responseFormat && {
264
+ response_format: (typeof chat.responseFormat === 'string'
265
+ ? { type: chat.responseFormat }
266
+ : chat.responseFormat),
267
+ }),
268
+ };
269
+ const stream = await client.chat.completions.create(openaiRequest);
270
+ let promptTokens = 0;
271
+ let completionTokens = 0;
272
+ let fullContent = '';
273
+ for await (const chunk of stream) {
274
+ const delta = chunk.choices[0]?.delta;
275
+ const finishReason = chunk.choices[0]?.finish_reason;
276
+ if (delta?.content) {
277
+ fullContent += delta.content;
278
+ }
279
+ if (chunk.usage) {
280
+ promptTokens = chunk.usage.prompt_tokens || 0;
281
+ completionTokens = chunk.usage.completion_tokens || 0;
282
+ }
283
+ yield {
284
+ content: delta?.content || undefined,
285
+ toolCalls: delta?.tool_calls,
286
+ model: chunk.model || model,
287
+ finishReason: finishReason ? this.mapFinishReason(finishReason) : undefined,
288
+ rawFinishReason: finishReason || undefined,
289
+ stream: true,
290
+ };
291
+ }
292
+ const cost = this.calculateCost(model, promptTokens, completionTokens);
293
+ const latencyMs = Date.now() - startTime;
294
+ yield {
295
+ content: '',
296
+ model: model,
297
+ usage: {
298
+ promptTokens,
299
+ completionTokens,
300
+ totalTokens: promptTokens + completionTokens,
301
+ },
302
+ cost,
303
+ latencyMs,
304
+ usedPlatformKey,
305
+ stream: true,
306
+ finishReason: 'completed',
307
+ };
308
+ }
194
309
  async handleImageGeneration(request, apiKey, usedPlatformKey) {
195
310
  const startTime = Date.now();
196
311
  const client = getOpenAIClient(apiKey);
@@ -206,7 +321,6 @@ export class OpenAIAdapter extends BaseProviderAdapter {
206
321
  ...(image.count && { n: image.count }),
207
322
  ...(image.style && { style: this.mapImageStyle(image.style) }),
208
323
  });
209
- // Calculate cost based on quality, size, and count
210
324
  const cost = this.calculateImageCost(model, image.quality || 'standard', image.size || '1024x1024', image.count || 1);
211
325
  return {
212
326
  images: (response.data || []).map(img => ({
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=test-openai-streaming.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-openai-streaming.d.ts","sourceRoot":"","sources":["../../../../src/services/providers/tests/test-openai-streaming.ts"],"names":[],"mappings":""}
@@ -0,0 +1,139 @@
1
+ import { OpenAIAdapter } from '../openai-adapter.js';
2
+ const adapter = new OpenAIAdapter();
3
+ async function testChatStreamingBasic() {
4
+ console.log('Testing basic chat streaming...');
5
+ const request = {
6
+ gateId: 'test-gate',
7
+ model: 'gpt-4o-mini',
8
+ type: 'chat',
9
+ data: {
10
+ messages: [
11
+ { role: 'user', content: 'Count from 1 to 5 slowly, one number per line.' }
12
+ ],
13
+ temperature: 0.7,
14
+ maxTokens: 50,
15
+ stream: true,
16
+ }
17
+ };
18
+ if (!adapter.callStream) {
19
+ throw new Error('callStream method not available');
20
+ }
21
+ let chunkCount = 0;
22
+ let fullContent = '';
23
+ let finalUsage = null;
24
+ let finalCost = null;
25
+ console.log('\nStreaming chunks:');
26
+ console.log('---');
27
+ for await (const chunk of adapter.callStream(request)) {
28
+ chunkCount++;
29
+ if (chunk.content) {
30
+ process.stdout.write(chunk.content);
31
+ fullContent += chunk.content;
32
+ }
33
+ if (chunk.usage) {
34
+ finalUsage = chunk.usage;
35
+ }
36
+ if (chunk.cost) {
37
+ finalCost = chunk.cost;
38
+ }
39
+ }
40
+ console.log('\n---\n');
41
+ console.log('Total chunks received:', chunkCount);
42
+ console.log('Full content:', fullContent);
43
+ console.log('Final usage:', finalUsage);
44
+ console.log('Final cost:', finalCost);
45
+ console.log('✅ Basic streaming test passed\n');
46
+ }
47
+ async function testChatStreamingWithToolCalls() {
48
+ console.log('Testing chat streaming with tool calls...');
49
+ const request = {
50
+ gateId: 'test-gate',
51
+ model: 'gpt-4o-mini',
52
+ type: 'chat',
53
+ data: {
54
+ messages: [
55
+ { role: 'user', content: 'What is the weather in San Francisco?' }
56
+ ],
57
+ tools: [
58
+ {
59
+ type: 'function',
60
+ function: {
61
+ name: 'get_weather',
62
+ description: 'Get the current weather for a location',
63
+ parameters: {
64
+ type: 'object',
65
+ properties: {
66
+ location: {
67
+ type: 'string',
68
+ description: 'The city and state, e.g. San Francisco, CA',
69
+ },
70
+ },
71
+ required: ['location'],
72
+ },
73
+ },
74
+ },
75
+ ],
76
+ toolChoice: 'auto',
77
+ stream: true,
78
+ }
79
+ };
80
+ if (!adapter.callStream) {
81
+ throw new Error('callStream method not available');
82
+ }
83
+ let toolCallsReceived = false;
84
+ for await (const chunk of adapter.callStream(request)) {
85
+ if (chunk.toolCalls && chunk.toolCalls.length > 0) {
86
+ console.log('Tool calls received:', JSON.stringify(chunk.toolCalls, null, 2));
87
+ toolCallsReceived = true;
88
+ }
89
+ if (chunk.finishReason === 'tool_call') {
90
+ console.log('Finish reason: tool_call');
91
+ }
92
+ }
93
+ if (!toolCallsReceived) {
94
+ console.warn('⚠️ No tool calls received (model may have chosen not to use tools)');
95
+ }
96
+ else {
97
+ console.log('✅ Tool calls streaming test passed\n');
98
+ }
99
+ }
100
+ async function testChatStreamingError() {
101
+ console.log('Testing streaming with invalid model (error handling)...');
102
+ const request = {
103
+ gateId: 'test-gate',
104
+ model: 'invalid-model-name-that-does-not-exist',
105
+ type: 'chat',
106
+ data: {
107
+ messages: [
108
+ { role: 'user', content: 'Hello' }
109
+ ],
110
+ stream: true,
111
+ }
112
+ };
113
+ if (!adapter.callStream) {
114
+ throw new Error('callStream method not available');
115
+ }
116
+ try {
117
+ for await (const chunk of adapter.callStream(request)) {
118
+ console.log('Received chunk:', chunk);
119
+ }
120
+ console.error('❌ Should have thrown an error for invalid model');
121
+ }
122
+ catch (error) {
123
+ console.log('✅ Correctly threw error:', error instanceof Error ? error.message : error);
124
+ console.log('✅ Error handling test passed\n');
125
+ }
126
+ }
127
+ // Run all tests
128
+ (async () => {
129
+ try {
130
+ await testChatStreamingBasic();
131
+ await testChatStreamingWithToolCalls();
132
+ await testChatStreamingError();
133
+ console.log('✅ All streaming tests completed successfully!');
134
+ }
135
+ catch (error) {
136
+ console.error('❌ Test failed:', error);
137
+ process.exit(1);
138
+ }
139
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@layer-ai/core",
3
- "version": "2.0.14",
3
+ "version": "2.0.16",
4
4
  "description": "Core API routes and services for Layer AI",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -36,7 +36,7 @@
36
36
  "nanoid": "^5.0.4",
37
37
  "openai": "^4.24.0",
38
38
  "pg": "^8.11.3",
39
- "@layer-ai/sdk": "^2.5.5"
39
+ "@layer-ai/sdk": "^2.5.6"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/bcryptjs": "^2.4.6",