@layer-ai/core 0.9.2 → 2.0.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.
Files changed (38) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +3 -0
  4. package/dist/routes/v1/gates.js +2 -2
  5. package/dist/routes/v2/complete.d.ts.map +1 -1
  6. package/dist/routes/v2/complete.js +127 -56
  7. package/dist/routes/v2/tests/test-byok-completion.js +1 -1
  8. package/dist/routes/v2/tests/test-complete-anthropic.js +4 -4
  9. package/dist/routes/v2/tests/test-complete-openai.js +7 -7
  10. package/dist/routes/v2/tests/test-complete-routing.js +5 -5
  11. package/dist/routes/v3/chat.d.ts +4 -0
  12. package/dist/routes/v3/chat.d.ts.map +1 -0
  13. package/dist/routes/v3/chat.js +203 -0
  14. package/dist/routes/v3/completions/chat.d.ts +4 -0
  15. package/dist/routes/v3/completions/chat.d.ts.map +1 -0
  16. package/dist/routes/v3/completions/chat.js +178 -0
  17. package/dist/routes/v3/completions/embed.d.ts +4 -0
  18. package/dist/routes/v3/completions/embed.d.ts.map +1 -0
  19. package/dist/routes/v3/completions/embed.js +94 -0
  20. package/dist/routes/v3/completions/image.d.ts +4 -0
  21. package/dist/routes/v3/completions/image.d.ts.map +1 -0
  22. package/dist/routes/v3/completions/image.js +155 -0
  23. package/dist/routes/v3/completions/ocr.d.ts +4 -0
  24. package/dist/routes/v3/completions/ocr.d.ts.map +1 -0
  25. package/dist/routes/v3/completions/ocr.js +94 -0
  26. package/dist/routes/v3/completions/tts.d.ts +4 -0
  27. package/dist/routes/v3/completions/tts.d.ts.map +1 -0
  28. package/dist/routes/v3/completions/tts.js +94 -0
  29. package/dist/routes/v3/completions/video.d.ts +4 -0
  30. package/dist/routes/v3/completions/video.d.ts.map +1 -0
  31. package/dist/routes/v3/completions/video.js +94 -0
  32. package/dist/services/providers/base-adapter.d.ts.map +1 -1
  33. package/dist/services/providers/base-adapter.js +5 -2
  34. package/dist/services/providers/tests/test-anthropic-adapter.js +4 -4
  35. package/dist/services/providers/tests/test-google-adapter.js +9 -9
  36. package/dist/services/providers/tests/test-mistral-adapter.js +11 -11
  37. package/dist/services/providers/tests/test-openai-adapter.js +8 -8
  38. package/package.json +2 -2
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export { default as gatesRouter } from './routes/v1/gates.js';
3
3
  export { default as keysRouter } from './routes/v1/keys.js';
4
4
  export { default as logsRouter } from './routes/v1/logs.js';
5
5
  export { default as completeRouter } from './routes/v2/complete.js';
6
+ export { default as chatRouter } from './routes/v3/chat.js';
6
7
  export { authenticate } from './middleware/auth.js';
7
8
  export type {} from './middleware/auth.js';
8
9
  export { db } from './lib/db/postgres.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAGpE,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAG3C,OAAO,EAAE,EAAE,EAAE,MAAM,sBAAsB,CAAC;AAC1C,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAGrD,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC9E,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAGnD,eAAO,MAAM,gBAAgB,GAAU,QAAQ,MAAM,KAAG,OAAO,CAAC,MAAM,CAGrE,CAAC;AAEF,eAAO,MAAM,wBAAwB,GAAU,QAAQ,MAAM,KAAG,OAAO,CAAC,IAAI,CAG3E,CAAC;AAGF,cAAc,6BAA6B,CAAC;AAG5C,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,QAAQ,EAAE,WAAW,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAG5D,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAGpE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAG5D,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAG3C,OAAO,EAAE,EAAE,EAAE,MAAM,sBAAsB,CAAC;AAC1C,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAGrD,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC9E,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAGnD,eAAO,MAAM,gBAAgB,GAAU,QAAQ,MAAM,KAAG,OAAO,CAAC,MAAM,CAGrE,CAAC;AAEF,eAAO,MAAM,wBAAwB,GAAU,QAAQ,MAAM,KAAG,OAAO,CAAC,IAAI,CAG3E,CAAC;AAGF,cAAc,6BAA6B,CAAC;AAG5C,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,QAAQ,EAAE,WAAW,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC"}
package/dist/index.js CHANGED
@@ -3,7 +3,10 @@ export { default as authRouter } from './routes/v1/auth.js';
3
3
  export { default as gatesRouter } from './routes/v1/gates.js';
4
4
  export { default as keysRouter } from './routes/v1/keys.js';
5
5
  export { default as logsRouter } from './routes/v1/logs.js';
6
+ // v2 routes
6
7
  export { default as completeRouter } from './routes/v2/complete.js';
8
+ // v3 routes
9
+ export { default as chatRouter } from './routes/v3/chat.js';
7
10
  // Middleware
8
11
  export { authenticate } from './middleware/auth.js';
9
12
  // Database
@@ -327,7 +327,7 @@ router.post('/test', async (req, res) => {
327
327
  try {
328
328
  const request = {
329
329
  type: 'chat',
330
- gate: finalGate.name || 'test-gate',
330
+ gateId: finalGate.id,
331
331
  model: finalGate.model,
332
332
  data: {
333
333
  messages,
@@ -363,7 +363,7 @@ router.post('/test', async (req, res) => {
363
363
  try {
364
364
  const request = {
365
365
  type: 'chat',
366
- gate: finalGate.name || 'test-gate',
366
+ gateId: finalGate.id,
367
367
  model: fallbackModel,
368
368
  data: {
369
369
  messages,
@@ -1 +1 @@
1
- {"version":3,"file":"complete.d.ts","sourceRoot":"","sources":["../../../src/routes/v2/complete.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AASpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AAqQpC,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"complete.d.ts","sourceRoot":"","sources":["../../../src/routes/v2/complete.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AASpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AAkVpC,eAAe,MAAM,CAAC"}
@@ -1,6 +1,5 @@
1
1
  import { Router } from 'express';
2
2
  import { db } from '../../lib/db/postgres.js';
3
- import { cache } from '../../lib/db/redis.js';
4
3
  import { authenticate } from '../../middleware/auth.js';
5
4
  import { callAdapter, normalizeModelId } from '../../lib/provider-factory.js';
6
5
  import { OverrideField } from '@layer-ai/sdk';
@@ -13,31 +12,7 @@ function isOverrideAllowed(allowOverrides, field) {
13
12
  return false;
14
13
  return allowOverrides[field] ?? false;
15
14
  }
16
- async function getGateConfig(userId, gateIdentifier) {
17
- const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(gateIdentifier);
18
- let gateConfig = null;
19
- if (isUUID) {
20
- gateConfig = await cache.getGateById(userId, gateIdentifier);
21
- if (!gateConfig) {
22
- gateConfig = await db.getGateByUserAndId(userId, gateIdentifier);
23
- if (gateConfig) {
24
- await cache.setGate(userId, gateConfig.name, gateConfig);
25
- }
26
- }
27
- }
28
- else {
29
- gateConfig = await cache.getGate(userId, gateIdentifier);
30
- if (!gateConfig) {
31
- gateConfig = await db.getGateByUserAndName(userId, gateIdentifier);
32
- if (gateConfig) {
33
- await cache.setGate(userId, gateIdentifier, gateConfig);
34
- }
35
- }
36
- }
37
- return gateConfig;
38
- }
39
15
  function resolveFinalRequest(gateConfig, request) {
40
- const finalRequest = { ...request };
41
16
  let finalModel = gateConfig.model;
42
17
  if (request.model && isOverrideAllowed(gateConfig.allowOverrides, OverrideField.Model)) {
43
18
  try {
@@ -47,33 +22,85 @@ function resolveFinalRequest(gateConfig, request) {
47
22
  finalModel = gateConfig.model;
48
23
  }
49
24
  }
50
- finalRequest.model = normalizeModelId(finalModel);
51
- if (request.type === 'chat') {
52
- const chatData = { ...request.data };
53
- if (!chatData.systemPrompt && gateConfig.systemPrompt) {
54
- chatData.systemPrompt = gateConfig.systemPrompt;
25
+ // Use discriminated union to handle each request type
26
+ switch (request.type) {
27
+ case 'chat': {
28
+ const chatData = { ...request.data };
29
+ if (!chatData.systemPrompt && gateConfig.systemPrompt) {
30
+ chatData.systemPrompt = gateConfig.systemPrompt;
31
+ }
32
+ if (chatData.temperature === undefined && gateConfig.temperature !== undefined) {
33
+ chatData.temperature = gateConfig.temperature;
34
+ }
35
+ else if (chatData.temperature !== undefined && !isOverrideAllowed(gateConfig.allowOverrides, OverrideField.Temperature)) {
36
+ chatData.temperature = gateConfig.temperature;
37
+ }
38
+ if (chatData.maxTokens === undefined && gateConfig.maxTokens !== undefined) {
39
+ chatData.maxTokens = gateConfig.maxTokens;
40
+ }
41
+ else if (chatData.maxTokens !== undefined && !isOverrideAllowed(gateConfig.allowOverrides, OverrideField.MaxTokens)) {
42
+ chatData.maxTokens = gateConfig.maxTokens;
43
+ }
44
+ if (chatData.topP === undefined && gateConfig.topP !== undefined) {
45
+ chatData.topP = gateConfig.topP;
46
+ }
47
+ else if (chatData.topP !== undefined && !isOverrideAllowed(gateConfig.allowOverrides, OverrideField.TopP)) {
48
+ chatData.topP = gateConfig.topP;
49
+ }
50
+ return {
51
+ ...request,
52
+ model: normalizeModelId(finalModel),
53
+ data: chatData,
54
+ };
55
55
  }
56
- if (chatData.temperature === undefined && gateConfig.temperature !== undefined) {
57
- chatData.temperature = gateConfig.temperature;
56
+ case 'image': {
57
+ // TODO: Future enhancement - intelligently apply gate-level defaults
58
+ // Potential features:
59
+ // - Apply systemPrompt by intelligently merging with user prompt
60
+ // - Support defaults for size, quality, style, count
61
+ // - Make this opt-in with a gate setting (e.g., applySystemPromptToAllTasks)
62
+ return {
63
+ ...request,
64
+ model: normalizeModelId(finalModel),
65
+ };
58
66
  }
59
- else if (chatData.temperature !== undefined && !isOverrideAllowed(gateConfig.allowOverrides, OverrideField.Temperature)) {
60
- chatData.temperature = gateConfig.temperature;
67
+ case 'video': {
68
+ // TODO: Future enhancement - intelligently apply gate-level defaults
69
+ // Similar to image generation, could support systemPrompt integration
70
+ // and video-specific defaults (duration, size, fps)
71
+ return {
72
+ ...request,
73
+ model: normalizeModelId(finalModel),
74
+ };
61
75
  }
62
- if (chatData.maxTokens === undefined && gateConfig.maxTokens !== undefined) {
63
- chatData.maxTokens = gateConfig.maxTokens;
76
+ case 'embeddings': {
77
+ // Embeddings are deterministic and don't typically need gate-level defaults
78
+ // beyond model selection (already handled)
79
+ return {
80
+ ...request,
81
+ model: normalizeModelId(finalModel),
82
+ };
64
83
  }
65
- else if (chatData.maxTokens !== undefined && !isOverrideAllowed(gateConfig.allowOverrides, OverrideField.MaxTokens)) {
66
- chatData.maxTokens = gateConfig.maxTokens;
84
+ case 'tts': {
85
+ // TODO: Future enhancement - support defaults for voice, speed, format
86
+ return {
87
+ ...request,
88
+ model: normalizeModelId(finalModel),
89
+ };
67
90
  }
68
- if (chatData.topP === undefined && gateConfig.topP !== undefined) {
69
- chatData.topP = gateConfig.topP;
91
+ case 'ocr': {
92
+ // TODO: Future enhancement - support defaults for tableFormat, includeImageBase64, etc.
93
+ return {
94
+ ...request,
95
+ model: normalizeModelId(finalModel),
96
+ };
70
97
  }
71
- else if (chatData.topP !== undefined && !isOverrideAllowed(gateConfig.allowOverrides, OverrideField.TopP)) {
72
- chatData.topP = gateConfig.topP;
98
+ default: {
99
+ // This will cause a TypeScript error if we miss a case
100
+ const exhaustiveCheck = request;
101
+ throw new Error(`Unhandled request type: ${exhaustiveCheck.type}`);
73
102
  }
74
- finalRequest.data = chatData;
75
103
  }
76
- return finalRequest;
77
104
  }
78
105
  function getModelsToTry(gateConfig, primaryModel) {
79
106
  const modelsToTry = [primaryModel];
@@ -141,28 +168,72 @@ router.post('/', authenticate, async (req, res) => {
141
168
  let request = null;
142
169
  try {
143
170
  const rawRequest = req.body;
144
- if (!rawRequest.gate) {
145
- res.status(400).json({ error: 'bad_request', message: 'Missing required field: gate' });
171
+ if (!rawRequest.gateId) {
172
+ res.status(400).json({ error: 'bad_request', message: 'Missing required field: gateId' });
173
+ return;
174
+ }
175
+ const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(rawRequest.gateId);
176
+ if (!isUUID) {
177
+ res.status(400).json({ error: 'bad_request', message: 'gateId must be a valid UUID. Gate names are no longer supported.' });
146
178
  return;
147
179
  }
148
- gateConfig = await getGateConfig(userId, rawRequest.gate);
180
+ gateConfig = await db.getGateByUserAndId(userId, rawRequest.gateId);
149
181
  if (!gateConfig) {
150
- res.status(404).json({ error: 'not_found', message: `Gate "${rawRequest.gate}" not found` });
182
+ res.status(404).json({ error: 'not_found', message: `Gate with ID "${rawRequest.gateId}" not found` });
151
183
  return;
152
184
  }
153
- const requestType = rawRequest.type || gateConfig.taskType || 'chat';
185
+ // Default to gate's taskType, allow override via request.type
186
+ const requestType = rawRequest.type || gateConfig.taskType;
187
+ // Warn if request type doesn't match gate's taskType (possible misconfiguration)
188
+ if (rawRequest.type && gateConfig.taskType && rawRequest.type !== gateConfig.taskType) {
189
+ console.warn(`[Type Mismatch] Gate "${gateConfig.name}" (${gateConfig.id}) configured for taskType="${gateConfig.taskType}" ` +
190
+ `but received request with type="${rawRequest.type}". Using request type as override.`);
191
+ }
154
192
  request = {
155
- gate: rawRequest.gate,
193
+ gateId: rawRequest.gateId,
156
194
  type: requestType,
157
195
  data: rawRequest.data,
158
196
  model: rawRequest.model,
159
197
  metadata: rawRequest.metadata
160
198
  };
161
- if (request.type === 'chat') {
162
- if (!request.data.messages || !Array.isArray(request.data.messages) || request.data.messages.length === 0) {
163
- res.status(400).json({ error: 'bad_request', message: 'Missing required field: data.messages' });
164
- return;
165
- }
199
+ // Validate required fields based on request type
200
+ switch (request.type) {
201
+ case 'chat':
202
+ if (!request.data.messages || !Array.isArray(request.data.messages) || request.data.messages.length === 0) {
203
+ res.status(400).json({ error: 'bad_request', message: 'Missing required field: data.messages' });
204
+ return;
205
+ }
206
+ break;
207
+ case 'image':
208
+ if (!request.data.prompt) {
209
+ res.status(400).json({ error: 'bad_request', message: 'Missing required field: data.prompt' });
210
+ return;
211
+ }
212
+ break;
213
+ case 'video':
214
+ if (!request.data.prompt) {
215
+ res.status(400).json({ error: 'bad_request', message: 'Missing required field: data.prompt' });
216
+ return;
217
+ }
218
+ break;
219
+ case 'embeddings':
220
+ if (!request.data.input) {
221
+ res.status(400).json({ error: 'bad_request', message: 'Missing required field: data.input' });
222
+ return;
223
+ }
224
+ break;
225
+ case 'tts':
226
+ if (!request.data.input) {
227
+ res.status(400).json({ error: 'bad_request', message: 'Missing required field: data.input' });
228
+ return;
229
+ }
230
+ break;
231
+ case 'ocr':
232
+ if (!request.data.documentUrl && !request.data.imageUrl && !request.data.base64) {
233
+ res.status(400).json({ error: 'bad_request', message: 'Missing required field: data must contain one of documentUrl, imageUrl, or base64' });
234
+ return;
235
+ }
236
+ break;
166
237
  }
167
238
  const finalRequest = resolveFinalRequest(gateConfig, request);
168
239
  const { result, modelUsed } = await executeWithRouting(gateConfig, finalRequest, userId);
@@ -171,7 +242,7 @@ router.post('/', authenticate, async (req, res) => {
171
242
  db.logRequest({
172
243
  userId,
173
244
  gateId: gateConfig.id,
174
- gateName: request.gate,
245
+ gateName: gateConfig.name,
175
246
  modelRequested: request.model || gateConfig.model,
176
247
  modelUsed: modelUsed,
177
248
  promptTokens: result.usage?.promptTokens || 0,
@@ -12,7 +12,7 @@ import { GoogleAdapter } from '../../../services/providers/google-adapter.js';
12
12
  // Test user ID from the database
13
13
  const TEST_USER_ID = 'ebd64998-465d-4211-ad67-87b4e01ad0da';
14
14
  const SIMPLE_REQUEST = {
15
- gate: 'byok-test',
15
+ gateId: 'byok-test',
16
16
  model: 'gpt-4o-mini',
17
17
  type: 'chat',
18
18
  data: {
@@ -3,7 +3,7 @@
3
3
  async function testBasicChat() {
4
4
  console.log('Test 1: Basic chat completion with Anthropic\n');
5
5
  const request = {
6
- gate: 'test-gate',
6
+ gateId: 'test-gate',
7
7
  model: 'claude-sonnet-4-5-20250929',
8
8
  type: 'chat',
9
9
  data: {
@@ -27,7 +27,7 @@ async function testBasicChat() {
27
27
  async function testVision() {
28
28
  console.log('\n\nTest 2: Vision with Anthropic (not supported in v1)\n');
29
29
  const request = {
30
- gate: 'test-gate',
30
+ gateId: 'test-gate',
31
31
  model: 'claude-sonnet-4-5-20250929',
32
32
  type: 'chat',
33
33
  data: {
@@ -49,7 +49,7 @@ async function testVision() {
49
49
  async function testToolCalls() {
50
50
  console.log('\n\nTest 3: Tool calls with Anthropic (not supported in v1)\n');
51
51
  const request = {
52
- gate: 'test-gate',
52
+ gateId: 'test-gate',
53
53
  model: 'claude-sonnet-4-5-20250929',
54
54
  type: 'chat',
55
55
  data: {
@@ -86,7 +86,7 @@ async function testToolCalls() {
86
86
  async function testSystemPrompt() {
87
87
  console.log('\n\nTest 4: System prompt and advanced params\n');
88
88
  const request = {
89
- gate: 'test-gate',
89
+ gateId: 'test-gate',
90
90
  model: 'claude-sonnet-4-5-20250929',
91
91
  type: 'chat',
92
92
  data: {
@@ -3,7 +3,7 @@
3
3
  async function testBasicChat() {
4
4
  console.log('Test 1: Basic chat completion with OpenAI\n');
5
5
  const request = {
6
- gate: 'test-gate',
6
+ gateId: 'test-gate',
7
7
  model: 'gpt-4o-mini',
8
8
  type: 'chat',
9
9
  data: {
@@ -26,7 +26,7 @@ async function testBasicChat() {
26
26
  async function testVision() {
27
27
  console.log('\n\nTest 2: Vision with GPT-4o\n');
28
28
  const request = {
29
- gate: 'test-gate',
29
+ gateId: 'test-gate',
30
30
  model: 'gpt-4o',
31
31
  type: 'chat',
32
32
  data: {
@@ -49,7 +49,7 @@ async function testVision() {
49
49
  async function testToolCalls() {
50
50
  console.log('\n\nTest 3: Tool calls with GPT-4\n');
51
51
  const request = {
52
- gate: 'test-gate',
52
+ gateId: 'test-gate',
53
53
  model: 'gpt-4o-mini',
54
54
  type: 'chat',
55
55
  data: {
@@ -85,7 +85,7 @@ async function testToolCalls() {
85
85
  async function testImageGeneration() {
86
86
  console.log('\n\nTest 4: Image generation with DALL-E\n');
87
87
  const request = {
88
- gate: 'test-gate',
88
+ gateId: 'test-gate',
89
89
  model: 'dall-e-3',
90
90
  type: 'image',
91
91
  data: {
@@ -102,7 +102,7 @@ async function testImageGeneration() {
102
102
  async function testEmbeddings() {
103
103
  console.log('\n\nTest 5: Text embeddings\n');
104
104
  const request = {
105
- gate: 'test-gate',
105
+ gateId: 'test-gate',
106
106
  model: 'text-embedding-3-small',
107
107
  type: 'embeddings',
108
108
  data: {
@@ -117,7 +117,7 @@ async function testEmbeddings() {
117
117
  async function testTextToSpeech() {
118
118
  console.log('\n\nTest 6: Text-to-speech\n');
119
119
  const request = {
120
- gate: 'test-gate',
120
+ gateId: 'test-gate',
121
121
  model: 'tts-1',
122
122
  type: 'tts',
123
123
  data: {
@@ -133,7 +133,7 @@ async function testTextToSpeech() {
133
133
  async function testResponseFormat() {
134
134
  console.log('\n\nTest 7: JSON response format\n');
135
135
  const request = {
136
- gate: 'test-gate',
136
+ gateId: 'test-gate',
137
137
  model: 'gpt-4o-mini',
138
138
  type: 'chat',
139
139
  data: {
@@ -3,7 +3,7 @@
3
3
  async function testFallbackRouting() {
4
4
  console.log('Test 1: Fallback routing (Anthropic -> OpenAI)\n');
5
5
  const request = {
6
- gate: 'test-gate-with-fallback',
6
+ gateId: 'test-gate-with-fallback',
7
7
  model: 'claude-sonnet-4-5-20250929', // Primary model
8
8
  type: 'chat',
9
9
  data: {
@@ -27,7 +27,7 @@ async function testFallbackRouting() {
27
27
  async function testRoundRobinRouting() {
28
28
  console.log('\n\nTest 2: Round-robin routing across providers\n');
29
29
  const request = {
30
- gate: 'test-gate-with-round-robin',
30
+ gateId: 'test-gate-with-round-robin',
31
31
  model: 'claude-sonnet-4-5-20250929',
32
32
  type: 'chat',
33
33
  data: {
@@ -50,7 +50,7 @@ async function testRoundRobinRouting() {
50
50
  async function testCrossProviderFallback() {
51
51
  console.log('\n\nTest 3: Cross-provider fallback with vision\n');
52
52
  const request = {
53
- gate: 'test-gate-vision-fallback',
53
+ gateId: 'test-gate-vision-fallback',
54
54
  model: 'claude-sonnet-4-5-20250929',
55
55
  type: 'chat',
56
56
  data: {
@@ -78,7 +78,7 @@ async function testCrossProviderFallback() {
78
78
  async function testToolCallsFallback() {
79
79
  console.log('\n\nTest 4: Fallback routing with tool calls\n');
80
80
  const request = {
81
- gate: 'test-gate-tools-fallback',
81
+ gateId: 'test-gate-tools-fallback',
82
82
  model: 'gpt-4o-mini',
83
83
  type: 'chat',
84
84
  data: {
@@ -117,7 +117,7 @@ async function testToolCallsFallback() {
117
117
  async function testCostOptimization() {
118
118
  console.log('\n\nTest 5: Cost optimization with round-robin\n');
119
119
  const request = {
120
- gate: 'test-gate-cost-optimization',
120
+ gateId: 'test-gate-cost-optimization',
121
121
  model: 'gpt-4o-mini',
122
122
  type: 'chat',
123
123
  data: {
@@ -0,0 +1,4 @@
1
+ import type { Router as RouterType } from 'express';
2
+ declare const router: RouterType;
3
+ export default router;
4
+ //# sourceMappingURL=chat.d.ts.map
@@ -0,0 +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;AAOpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AAqPpC,eAAe,MAAM,CAAC"}
@@ -0,0 +1,203 @@
1
+ import { Router } from 'express';
2
+ import { db } from '../../lib/db/postgres.js';
3
+ import { authenticate } from '../../middleware/auth.js';
4
+ import { callAdapter, normalizeModelId } from '../../lib/provider-factory.js';
5
+ import { OverrideField } from '@layer-ai/sdk';
6
+ const router = Router();
7
+ // MARK:- Helper Functions
8
+ function isOverrideAllowed(allowOverrides, field) {
9
+ if (allowOverrides === undefined || allowOverrides === null || allowOverrides === true)
10
+ return true;
11
+ if (allowOverrides === false)
12
+ return false;
13
+ return allowOverrides[field] ?? false;
14
+ }
15
+ function resolveFinalRequest(gateConfig, request) {
16
+ let finalModel = gateConfig.model;
17
+ if (request.model && isOverrideAllowed(gateConfig.allowOverrides, OverrideField.Model)) {
18
+ try {
19
+ finalModel = normalizeModelId(request.model);
20
+ }
21
+ catch {
22
+ finalModel = gateConfig.model;
23
+ }
24
+ }
25
+ // Since this is v3/chat endpoint, we know the data is ChatRequest
26
+ const chatData = { ...request.data };
27
+ if (!chatData.systemPrompt && gateConfig.systemPrompt) {
28
+ chatData.systemPrompt = gateConfig.systemPrompt;
29
+ }
30
+ if (chatData.temperature === undefined && gateConfig.temperature !== undefined) {
31
+ chatData.temperature = gateConfig.temperature;
32
+ }
33
+ else if (chatData.temperature !== undefined && !isOverrideAllowed(gateConfig.allowOverrides, OverrideField.Temperature)) {
34
+ chatData.temperature = gateConfig.temperature;
35
+ }
36
+ if (chatData.maxTokens === undefined && gateConfig.maxTokens !== undefined) {
37
+ chatData.maxTokens = gateConfig.maxTokens;
38
+ }
39
+ else if (chatData.maxTokens !== undefined && !isOverrideAllowed(gateConfig.allowOverrides, OverrideField.MaxTokens)) {
40
+ chatData.maxTokens = gateConfig.maxTokens;
41
+ }
42
+ if (chatData.topP === undefined && gateConfig.topP !== undefined) {
43
+ chatData.topP = gateConfig.topP;
44
+ }
45
+ else if (chatData.topP !== undefined && !isOverrideAllowed(gateConfig.allowOverrides, OverrideField.TopP)) {
46
+ chatData.topP = gateConfig.topP;
47
+ }
48
+ return {
49
+ ...request,
50
+ type: 'chat',
51
+ model: normalizeModelId(finalModel),
52
+ data: chatData,
53
+ };
54
+ }
55
+ function getModelsToTry(gateConfig, primaryModel) {
56
+ const modelsToTry = [primaryModel];
57
+ if (gateConfig.routingStrategy === 'fallback' && gateConfig.fallbackModels?.length) {
58
+ modelsToTry.push(...gateConfig.fallbackModels);
59
+ }
60
+ return modelsToTry;
61
+ }
62
+ async function executeWithFallback(request, modelsToTry, userId) {
63
+ let result = null;
64
+ let lastError = null;
65
+ let modelUsed = request.model;
66
+ for (const modelToTry of modelsToTry) {
67
+ try {
68
+ const modelRequest = { ...request, model: modelToTry };
69
+ result = await callAdapter(modelRequest, userId);
70
+ modelUsed = modelToTry;
71
+ break;
72
+ }
73
+ catch (error) {
74
+ lastError = error;
75
+ console.log(`Model ${modelToTry} failed, trying next fallback...`, error instanceof Error ? error.message : error);
76
+ continue;
77
+ }
78
+ }
79
+ if (!result) {
80
+ throw lastError || new Error('All models failed');
81
+ }
82
+ return { result, modelUsed };
83
+ }
84
+ async function executeWithRoundRobin(gateConfig, request, userId) {
85
+ if (!gateConfig.fallbackModels?.length) {
86
+ const result = await callAdapter(request, userId);
87
+ return { result, modelUsed: request.model };
88
+ }
89
+ const allModels = [gateConfig.model, ...gateConfig.fallbackModels];
90
+ const modelIndex = Math.floor(Math.random() * allModels.length);
91
+ const selectedModel = allModels[modelIndex];
92
+ const modelRequest = { ...request, model: selectedModel };
93
+ const result = await callAdapter(modelRequest, userId);
94
+ return { result, modelUsed: selectedModel };
95
+ }
96
+ async function executeWithRouting(gateConfig, request, userId) {
97
+ const modelsToTry = getModelsToTry(gateConfig, request.model);
98
+ switch (gateConfig.routingStrategy) {
99
+ case 'fallback':
100
+ return await executeWithFallback(request, modelsToTry, userId);
101
+ case 'round-robin':
102
+ return await executeWithRoundRobin(gateConfig, request, userId);
103
+ case 'single':
104
+ default:
105
+ const result = await callAdapter(request, userId);
106
+ return { result, modelUsed: request.model };
107
+ }
108
+ }
109
+ // MARK:- Route Handler
110
+ router.post('/', authenticate, async (req, res) => {
111
+ const startTime = Date.now();
112
+ if (!req.userId) {
113
+ res.status(401).json({ error: 'unauthorized', message: 'Missing user ID' });
114
+ return;
115
+ }
116
+ const userId = req.userId;
117
+ let gateConfig = null;
118
+ let request = null;
119
+ try {
120
+ const rawRequest = req.body;
121
+ if (!rawRequest.gateId) {
122
+ res.status(400).json({ error: 'bad_request', message: 'Missing required field: gateId' });
123
+ return;
124
+ }
125
+ const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(rawRequest.gateId);
126
+ if (!isUUID) {
127
+ res.status(400).json({ error: 'bad_request', message: 'gateId must be a valid UUID' });
128
+ return;
129
+ }
130
+ gateConfig = await db.getGateByUserAndId(userId, rawRequest.gateId);
131
+ if (!gateConfig) {
132
+ res.status(404).json({ error: 'not_found', message: `Gate with ID "${rawRequest.gateId}" not found` });
133
+ return;
134
+ }
135
+ // Validate chat-specific fields
136
+ if (!rawRequest.data?.messages || !Array.isArray(rawRequest.data.messages) || rawRequest.data.messages.length === 0) {
137
+ res.status(400).json({ error: 'bad_request', message: 'Missing required field: data.messages (must be a non-empty array)' });
138
+ return;
139
+ }
140
+ // Warn if gate is configured for a different task type
141
+ if (gateConfig.taskType && gateConfig.taskType !== 'chat') {
142
+ console.warn(`[Type Mismatch] Gate "${gateConfig.name}" (${gateConfig.id}) configured for taskType="${gateConfig.taskType}" ` +
143
+ `but received request to /v3/chat endpoint. Processing as chat request.`);
144
+ }
145
+ request = {
146
+ gateId: rawRequest.gateId,
147
+ type: 'chat',
148
+ data: rawRequest.data,
149
+ model: rawRequest.model,
150
+ metadata: rawRequest.metadata
151
+ };
152
+ const finalRequest = resolveFinalRequest(gateConfig, request);
153
+ const { result, modelUsed } = await executeWithRouting(gateConfig, finalRequest, userId);
154
+ const latencyMs = Date.now() - startTime;
155
+ // Log request to database
156
+ db.logRequest({
157
+ userId,
158
+ gateId: gateConfig.id,
159
+ gateName: gateConfig.name,
160
+ modelRequested: request.model || gateConfig.model,
161
+ modelUsed: modelUsed,
162
+ promptTokens: result.usage?.promptTokens || 0,
163
+ completionTokens: result.usage?.completionTokens || 0,
164
+ totalTokens: result.usage?.totalTokens || 0,
165
+ costUsd: result.cost || 0,
166
+ latencyMs,
167
+ success: true,
168
+ errorMessage: null,
169
+ userAgent: req.headers['user-agent'] || null,
170
+ ipAddress: req.ip || null,
171
+ }).catch(err => console.error('Failed to log request:', err));
172
+ // Return LayerResponse with additional metadata
173
+ const response = {
174
+ ...result,
175
+ model: modelUsed,
176
+ latencyMs,
177
+ };
178
+ res.json(response);
179
+ }
180
+ catch (error) {
181
+ const latencyMs = Date.now() - startTime;
182
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
183
+ db.logRequest({
184
+ userId,
185
+ gateId: gateConfig?.id || null,
186
+ gateName: req.body?.gate || null,
187
+ modelRequested: (request?.model || gateConfig?.model) || 'unknown',
188
+ modelUsed: null,
189
+ promptTokens: 0,
190
+ completionTokens: 0,
191
+ totalTokens: 0,
192
+ costUsd: 0,
193
+ latencyMs,
194
+ success: false,
195
+ errorMessage,
196
+ userAgent: req.headers['user-agent'] || null,
197
+ ipAddress: req.ip || null,
198
+ }).catch(err => console.error('Failed to log request:', err));
199
+ console.error('Chat completion error:', error);
200
+ res.status(500).json({ error: 'internal_error', message: errorMessage });
201
+ }
202
+ });
203
+ export default router;
@@ -0,0 +1,4 @@
1
+ import type { Router as RouterType } from 'express';
2
+ declare const router: RouterType;
3
+ export default router;
4
+ //# sourceMappingURL=chat.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../../../src/routes/v3/completions/chat.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AAOpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AAoNpC,eAAe,MAAM,CAAC"}