@layer-ai/core 0.1.10 → 0.2.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.
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/routes/complete.d.ts.map +1 -1
- package/dist/routes/complete.js +24 -4
- package/dist/routes/gates.d.ts.map +1 -1
- package/dist/routes/gates.js +8 -13
- package/dist/routes/v2/complete.d.ts +4 -0
- package/dist/routes/v2/complete.d.ts.map +1 -0
- package/dist/routes/v2/complete.js +214 -0
- package/dist/routes/v2/tests/test-complete-anthropic.d.ts +2 -0
- package/dist/routes/v2/tests/test-complete-anthropic.d.ts.map +1 -0
- package/dist/routes/v2/tests/test-complete-anthropic.js +132 -0
- package/dist/routes/v2/tests/test-complete-openai.d.ts +2 -0
- package/dist/routes/v2/tests/test-complete-openai.d.ts.map +1 -0
- package/dist/routes/v2/tests/test-complete-openai.js +178 -0
- package/dist/routes/v2/tests/test-complete-routing.d.ts +2 -0
- package/dist/routes/v2/tests/test-complete-routing.d.ts.map +1 -0
- package/dist/routes/v2/tests/test-complete-routing.js +192 -0
- package/dist/services/providers/anthropic-adapter.d.ts +12 -0
- package/dist/services/providers/anthropic-adapter.d.ts.map +1 -0
- package/dist/services/providers/anthropic-adapter.js +203 -0
- package/dist/services/providers/base-adapter.d.ts +1 -1
- package/dist/services/providers/base-adapter.d.ts.map +1 -1
- package/dist/services/providers/base-adapter.js +1 -1
- package/dist/services/providers/openai-adapter.d.ts +2 -2
- package/dist/services/providers/openai-adapter.d.ts.map +1 -1
- package/dist/services/providers/openai-adapter.js +15 -3
- package/dist/services/providers/tests/test-anthropic-adapter.d.ts +2 -0
- package/dist/services/providers/tests/test-anthropic-adapter.d.ts.map +1 -0
- package/dist/services/providers/tests/test-anthropic-adapter.js +104 -0
- package/dist/services/providers/tests/test-openai-adapter.d.ts +2 -0
- package/dist/services/providers/tests/test-openai-adapter.d.ts.map +1 -0
- package/dist/services/providers/tests/test-openai-adapter.js +118 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { default as gatesRouter } from './routes/gates.js';
|
|
|
3
3
|
export { default as keysRouter } from './routes/keys.js';
|
|
4
4
|
export { default as logsRouter } from './routes/logs.js';
|
|
5
5
|
export { default as completeRouter } from './routes/complete.js';
|
|
6
|
+
export { default as completeV2Router } from './routes/v2/complete.js';
|
|
6
7
|
export { authenticate } from './middleware/auth.js';
|
|
7
8
|
export { db } from './lib/db/postgres.js';
|
|
8
9
|
export { default as redis } from './lib/db/redis.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACjE,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAGtE,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAGpD,OAAO,EAAE,EAAE,EAAE,MAAM,sBAAsB,CAAC;AAC1C,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAGrD,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,OAAO,EAAE,gBAAgB,IAAI,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAC5F,OAAO,EAAE,gBAAgB,IAAI,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAClG,OAAO,EAAE,gBAAgB,IAAI,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAC5F,YAAY,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ export { default as gatesRouter } from './routes/gates.js';
|
|
|
4
4
|
export { default as keysRouter } from './routes/keys.js';
|
|
5
5
|
export { default as logsRouter } from './routes/logs.js';
|
|
6
6
|
export { default as completeRouter } from './routes/complete.js';
|
|
7
|
+
export { default as completeV2Router } from './routes/v2/complete.js';
|
|
7
8
|
// Middleware
|
|
8
9
|
export { authenticate } from './middleware/auth.js';
|
|
9
10
|
// Database
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"complete.d.ts","sourceRoot":"","sources":["../../src/routes/complete.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AAWpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"complete.d.ts","sourceRoot":"","sources":["../../src/routes/complete.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AAWpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AAgSpC,eAAe,MAAM,CAAC"}
|
package/dist/routes/complete.js
CHANGED
|
@@ -3,7 +3,7 @@ import { db } from '../lib/db/postgres.js';
|
|
|
3
3
|
import { cache } from '../lib/db/redis.js';
|
|
4
4
|
import { authenticate } from '../middleware/auth.js';
|
|
5
5
|
import { OpenAIAdapter } from '../services/providers/openai-adapter.js';
|
|
6
|
-
import
|
|
6
|
+
import { AnthropicAdapter } from '../services/providers/anthropic-adapter.js';
|
|
7
7
|
import * as google from '../services/providers/google.js';
|
|
8
8
|
import { MODEL_REGISTRY, OverrideField } from '@layer-ai/sdk';
|
|
9
9
|
const router = Router();
|
|
@@ -54,7 +54,7 @@ function resolveFinalParams(gateConfig, requestParams) {
|
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
56
|
* MIGRATION IN PROGRESS: Moving to normalized adapter pattern.
|
|
57
|
-
* OpenAI now
|
|
57
|
+
* OpenAI and Anthropic now use the new adapter. Other providers will follow.
|
|
58
58
|
* This temporary conversion layer will be removed after all providers are migrated.
|
|
59
59
|
*/
|
|
60
60
|
async function callProvider(params) {
|
|
@@ -82,8 +82,28 @@ async function callProvider(params) {
|
|
|
82
82
|
costUsd: layerResponse.cost || 0,
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
|
-
case 'anthropic':
|
|
86
|
-
|
|
85
|
+
case 'anthropic': {
|
|
86
|
+
const adapter = new AnthropicAdapter();
|
|
87
|
+
const layerResponse = await adapter.call({
|
|
88
|
+
gate: 'internal',
|
|
89
|
+
model: params.model,
|
|
90
|
+
type: 'chat',
|
|
91
|
+
data: {
|
|
92
|
+
messages: params.messages,
|
|
93
|
+
systemPrompt: params.systemPrompt,
|
|
94
|
+
temperature: params.temperature,
|
|
95
|
+
maxTokens: params.maxTokens,
|
|
96
|
+
topP: params.topP,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
content: layerResponse.content || '',
|
|
101
|
+
promptTokens: layerResponse.usage?.promptTokens || 0,
|
|
102
|
+
completionTokens: layerResponse.usage?.completionTokens || 0,
|
|
103
|
+
totalTokens: layerResponse.usage?.totalTokens || 0,
|
|
104
|
+
costUsd: layerResponse.cost || 0,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
87
107
|
case 'google':
|
|
88
108
|
return await google.createCompletion(params);
|
|
89
109
|
default:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gates.d.ts","sourceRoot":"","sources":["../../src/routes/gates.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AAOpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"gates.d.ts","sourceRoot":"","sources":["../../src/routes/gates.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AAOpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AAwSpC,eAAe,MAAM,CAAC"}
|
package/dist/routes/gates.js
CHANGED
|
@@ -228,30 +228,25 @@ router.delete('/:id', async (req, res) => {
|
|
|
228
228
|
res.status(500).json({ error: 'internal_error', message: 'Failed to delete gate' });
|
|
229
229
|
}
|
|
230
230
|
});
|
|
231
|
-
//
|
|
232
|
-
router.
|
|
231
|
+
// POST /suggestions - Get AI-powered model suggestions for a gate
|
|
232
|
+
router.post('/suggestions', async (req, res) => {
|
|
233
233
|
if (!req.userId) {
|
|
234
234
|
res.status(401).json({ error: 'unauthorized', message: 'Missing user ID' });
|
|
235
235
|
return;
|
|
236
236
|
}
|
|
237
237
|
try {
|
|
238
|
-
const {
|
|
239
|
-
|
|
240
|
-
if (!gate) {
|
|
241
|
-
res.status(404).json({ error: 'not_found', message: 'Gate not found' });
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
if (!gate.description) {
|
|
238
|
+
const { description, costWeight, latencyWeight, qualityWeight } = req.body;
|
|
239
|
+
if (!description) {
|
|
245
240
|
res.status(400).json({ error: 'bad_request', message: 'Gate must have a description for AI recommendations' });
|
|
246
241
|
return;
|
|
247
242
|
}
|
|
248
243
|
const userPreferences = {
|
|
249
|
-
costWeight:
|
|
250
|
-
latencyWeight:
|
|
251
|
-
qualityWeight:
|
|
244
|
+
costWeight: parseFloat(costWeight ?? '0.33'),
|
|
245
|
+
latencyWeight: parseFloat(latencyWeight ?? '0.33'),
|
|
246
|
+
qualityWeight: parseFloat(qualityWeight ?? '0.34'),
|
|
252
247
|
};
|
|
253
248
|
const { analyzeTask } = await import('../services/task-analysis.js');
|
|
254
|
-
const suggestions = await analyzeTask(
|
|
249
|
+
const suggestions = await analyzeTask(description, userPreferences);
|
|
255
250
|
res.json(suggestions);
|
|
256
251
|
}
|
|
257
252
|
catch (error) {
|
|
@@ -0,0 +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;AAUpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AAgQpC,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { db } from '../../lib/db/postgres.js';
|
|
3
|
+
import { cache } from '../../lib/db/redis.js';
|
|
4
|
+
import { authenticate } from '../../middleware/auth.js';
|
|
5
|
+
import { OpenAIAdapter } from '../../services/providers/openai-adapter.js';
|
|
6
|
+
import { AnthropicAdapter } from '../../services/providers/anthropic-adapter.js';
|
|
7
|
+
import { MODEL_REGISTRY, OverrideField } from '@layer-ai/sdk';
|
|
8
|
+
const router = Router();
|
|
9
|
+
// MARK:- Helper Functions
|
|
10
|
+
function isOverrideAllowed(allowOverrides, field) {
|
|
11
|
+
if (allowOverrides === undefined || allowOverrides === null || allowOverrides === true)
|
|
12
|
+
return true;
|
|
13
|
+
if (allowOverrides === false)
|
|
14
|
+
return false;
|
|
15
|
+
return allowOverrides[field] ?? false;
|
|
16
|
+
}
|
|
17
|
+
async function getGateConfig(userId, gateName) {
|
|
18
|
+
let gateConfig = await cache.getGate(userId, gateName);
|
|
19
|
+
if (!gateConfig) {
|
|
20
|
+
gateConfig = await db.getGateByUserAndName(userId, gateName);
|
|
21
|
+
if (gateConfig) {
|
|
22
|
+
await cache.setGate(userId, gateName, gateConfig);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return gateConfig;
|
|
26
|
+
}
|
|
27
|
+
function resolveFinalRequest(gateConfig, request) {
|
|
28
|
+
const finalRequest = { ...request };
|
|
29
|
+
let finalModel = gateConfig.model;
|
|
30
|
+
if (request.model && isOverrideAllowed(gateConfig.allowOverrides, OverrideField.Model) && MODEL_REGISTRY[request.model]) {
|
|
31
|
+
finalModel = request.model;
|
|
32
|
+
}
|
|
33
|
+
finalRequest.model = finalModel;
|
|
34
|
+
if (request.type === 'chat') {
|
|
35
|
+
const chatData = { ...request.data };
|
|
36
|
+
if (!chatData.systemPrompt && gateConfig.systemPrompt) {
|
|
37
|
+
chatData.systemPrompt = gateConfig.systemPrompt;
|
|
38
|
+
}
|
|
39
|
+
if (chatData.temperature === undefined && gateConfig.temperature !== undefined) {
|
|
40
|
+
chatData.temperature = gateConfig.temperature;
|
|
41
|
+
}
|
|
42
|
+
else if (chatData.temperature !== undefined && !isOverrideAllowed(gateConfig.allowOverrides, OverrideField.Temperature)) {
|
|
43
|
+
chatData.temperature = gateConfig.temperature;
|
|
44
|
+
}
|
|
45
|
+
if (chatData.maxTokens === undefined && gateConfig.maxTokens !== undefined) {
|
|
46
|
+
chatData.maxTokens = gateConfig.maxTokens;
|
|
47
|
+
}
|
|
48
|
+
else if (chatData.maxTokens !== undefined && !isOverrideAllowed(gateConfig.allowOverrides, OverrideField.MaxTokens)) {
|
|
49
|
+
chatData.maxTokens = gateConfig.maxTokens;
|
|
50
|
+
}
|
|
51
|
+
if (chatData.topP === undefined && gateConfig.topP !== undefined) {
|
|
52
|
+
chatData.topP = gateConfig.topP;
|
|
53
|
+
}
|
|
54
|
+
else if (chatData.topP !== undefined && !isOverrideAllowed(gateConfig.allowOverrides, OverrideField.TopP)) {
|
|
55
|
+
chatData.topP = gateConfig.topP;
|
|
56
|
+
}
|
|
57
|
+
finalRequest.data = chatData;
|
|
58
|
+
}
|
|
59
|
+
return finalRequest;
|
|
60
|
+
}
|
|
61
|
+
async function callProvider(request) {
|
|
62
|
+
const provider = MODEL_REGISTRY[request.model].provider;
|
|
63
|
+
switch (provider) {
|
|
64
|
+
case 'openai': {
|
|
65
|
+
const adapter = new OpenAIAdapter();
|
|
66
|
+
return await adapter.call(request);
|
|
67
|
+
}
|
|
68
|
+
case 'anthropic': {
|
|
69
|
+
const adapter = new AnthropicAdapter();
|
|
70
|
+
return await adapter.call(request);
|
|
71
|
+
}
|
|
72
|
+
case 'google':
|
|
73
|
+
// TODO: Migrate Google to use adapter pattern
|
|
74
|
+
throw new Error('Google provider not yet migrated to v2 API');
|
|
75
|
+
default:
|
|
76
|
+
throw new Error(`Unknown provider: ${provider}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function getModelsToTry(gateConfig, primaryModel) {
|
|
80
|
+
const modelsToTry = [primaryModel];
|
|
81
|
+
if (gateConfig.routingStrategy === 'fallback' && gateConfig.fallbackModels?.length) {
|
|
82
|
+
modelsToTry.push(...gateConfig.fallbackModels);
|
|
83
|
+
}
|
|
84
|
+
return modelsToTry;
|
|
85
|
+
}
|
|
86
|
+
async function executeWithFallback(request, modelsToTry) {
|
|
87
|
+
let result = null;
|
|
88
|
+
let lastError = null;
|
|
89
|
+
let modelUsed = request.model;
|
|
90
|
+
for (const modelToTry of modelsToTry) {
|
|
91
|
+
try {
|
|
92
|
+
const modelRequest = { ...request, model: modelToTry };
|
|
93
|
+
result = await callProvider(modelRequest);
|
|
94
|
+
modelUsed = modelToTry;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
lastError = error;
|
|
99
|
+
console.log(`Model ${modelToTry} failed, trying next fallback...`, error instanceof Error ? error.message : error);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (!result) {
|
|
104
|
+
throw lastError || new Error('All models failed');
|
|
105
|
+
}
|
|
106
|
+
return { result, modelUsed };
|
|
107
|
+
}
|
|
108
|
+
async function executeWithRoundRobin(gateConfig, request) {
|
|
109
|
+
if (!gateConfig.fallbackModels?.length) {
|
|
110
|
+
const result = await callProvider(request);
|
|
111
|
+
return { result, modelUsed: request.model };
|
|
112
|
+
}
|
|
113
|
+
const allModels = [gateConfig.model, ...gateConfig.fallbackModels];
|
|
114
|
+
const modelIndex = Math.floor(Math.random() * allModels.length);
|
|
115
|
+
const selectedModel = allModels[modelIndex];
|
|
116
|
+
const modelRequest = { ...request, model: selectedModel };
|
|
117
|
+
const result = await callProvider(modelRequest);
|
|
118
|
+
return { result, modelUsed: selectedModel };
|
|
119
|
+
}
|
|
120
|
+
async function executeWithRouting(gateConfig, request) {
|
|
121
|
+
const modelsToTry = getModelsToTry(gateConfig, request.model);
|
|
122
|
+
switch (gateConfig.routingStrategy) {
|
|
123
|
+
case 'fallback':
|
|
124
|
+
return await executeWithFallback(request, modelsToTry);
|
|
125
|
+
case 'round-robin':
|
|
126
|
+
return await executeWithRoundRobin(gateConfig, request);
|
|
127
|
+
case 'single':
|
|
128
|
+
default:
|
|
129
|
+
const result = await callProvider(request);
|
|
130
|
+
return { result, modelUsed: request.model };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// MARK:- Route Handler
|
|
134
|
+
router.post('/', authenticate, async (req, res) => {
|
|
135
|
+
const startTime = Date.now();
|
|
136
|
+
if (!req.userId) {
|
|
137
|
+
res.status(401).json({ error: 'unauthorized', message: 'Missing user ID' });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const userId = req.userId;
|
|
141
|
+
try {
|
|
142
|
+
const request = req.body;
|
|
143
|
+
if (!request.gate) {
|
|
144
|
+
res.status(400).json({ error: 'bad_request', message: 'Missing required field: gate' });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (!request.type) {
|
|
148
|
+
res.status(400).json({ error: 'bad_request', message: 'Missing required field: type' });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// Validate chat-specific requirements
|
|
152
|
+
if (request.type === 'chat') {
|
|
153
|
+
if (!request.data.messages || !Array.isArray(request.data.messages) || request.data.messages.length === 0) {
|
|
154
|
+
res.status(400).json({ error: 'bad_request', message: 'Missing required field: data.messages' });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const gateConfig = await getGateConfig(userId, request.gate);
|
|
159
|
+
if (!gateConfig) {
|
|
160
|
+
res.status(404).json({ error: 'not_found', message: `Gate "${request.gate}" not found` });
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const finalRequest = resolveFinalRequest(gateConfig, request);
|
|
164
|
+
const { result, modelUsed } = await executeWithRouting(gateConfig, finalRequest);
|
|
165
|
+
const latencyMs = Date.now() - startTime;
|
|
166
|
+
// Log request to database
|
|
167
|
+
db.logRequest({
|
|
168
|
+
userId,
|
|
169
|
+
gateId: gateConfig.id,
|
|
170
|
+
gateName: request.gate,
|
|
171
|
+
modelRequested: request.model || gateConfig.model,
|
|
172
|
+
modelUsed: modelUsed,
|
|
173
|
+
promptTokens: result.usage?.promptTokens || 0,
|
|
174
|
+
completionTokens: result.usage?.completionTokens || 0,
|
|
175
|
+
totalTokens: result.usage?.totalTokens || 0,
|
|
176
|
+
costUsd: result.cost || 0,
|
|
177
|
+
latencyMs,
|
|
178
|
+
success: true,
|
|
179
|
+
errorMessage: null,
|
|
180
|
+
userAgent: req.headers['user-agent'] || null,
|
|
181
|
+
ipAddress: req.ip || null,
|
|
182
|
+
}).catch(err => console.error('Failed to log request:', err));
|
|
183
|
+
// Return LayerResponse with additional metadata
|
|
184
|
+
const response = {
|
|
185
|
+
...result,
|
|
186
|
+
model: modelUsed,
|
|
187
|
+
latencyMs,
|
|
188
|
+
};
|
|
189
|
+
res.json(response);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
const latencyMs = Date.now() - startTime;
|
|
193
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
194
|
+
db.logRequest({
|
|
195
|
+
userId,
|
|
196
|
+
gateId: null,
|
|
197
|
+
gateName: req.body?.gate || null,
|
|
198
|
+
modelRequested: null,
|
|
199
|
+
modelUsed: null,
|
|
200
|
+
promptTokens: 0,
|
|
201
|
+
completionTokens: 0,
|
|
202
|
+
totalTokens: 0,
|
|
203
|
+
costUsd: 0,
|
|
204
|
+
latencyMs,
|
|
205
|
+
success: false,
|
|
206
|
+
errorMessage,
|
|
207
|
+
userAgent: req.headers['user-agent'] || null,
|
|
208
|
+
ipAddress: req.ip || null,
|
|
209
|
+
}).catch(err => console.error('Failed to log request:', err));
|
|
210
|
+
console.error('Completion error:', error);
|
|
211
|
+
res.status(500).json({ error: 'internal_error', message: errorMessage });
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
export default router;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-complete-anthropic.d.ts","sourceRoot":"","sources":["../../../../src/routes/v2/tests/test-complete-anthropic.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Test v2 complete route with Anthropic adapter
|
|
2
|
+
// This demonstrates the full LayerRequest format with all features
|
|
3
|
+
async function testBasicChat() {
|
|
4
|
+
console.log('Test 1: Basic chat completion with Anthropic\n');
|
|
5
|
+
const request = {
|
|
6
|
+
gate: 'test-gate',
|
|
7
|
+
model: 'claude-sonnet-4-5-20250929',
|
|
8
|
+
type: 'chat',
|
|
9
|
+
data: {
|
|
10
|
+
messages: [
|
|
11
|
+
{ role: 'user', content: 'Say "Hello from v2 API" and nothing else.' }
|
|
12
|
+
],
|
|
13
|
+
temperature: 0.7,
|
|
14
|
+
maxTokens: 20,
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
console.log('Request:', JSON.stringify(request, null, 2));
|
|
18
|
+
console.log('\nExpected response includes:');
|
|
19
|
+
console.log('- content: string');
|
|
20
|
+
console.log('- model: string');
|
|
21
|
+
console.log('- finishReason: "completed" | "length_limit" | "tool_call" | "filtered" | "error"');
|
|
22
|
+
console.log('- usage: { promptTokens, completionTokens, totalTokens }');
|
|
23
|
+
console.log('- cost: number');
|
|
24
|
+
console.log('- latencyMs: number');
|
|
25
|
+
console.log('- raw: original provider response');
|
|
26
|
+
}
|
|
27
|
+
async function testVision() {
|
|
28
|
+
console.log('\n\nTest 2: Vision with Anthropic (not supported in v1)\n');
|
|
29
|
+
const request = {
|
|
30
|
+
gate: 'test-gate',
|
|
31
|
+
model: 'claude-sonnet-4-5-20250929',
|
|
32
|
+
type: 'chat',
|
|
33
|
+
data: {
|
|
34
|
+
messages: [
|
|
35
|
+
{
|
|
36
|
+
role: 'user',
|
|
37
|
+
content: 'What color is the sky in this image?',
|
|
38
|
+
images: [{
|
|
39
|
+
url: 'https://images.unsplash.com/photo-1765202659641-9ad9facfe5cf?q=80&w=1364&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
|
|
40
|
+
}]
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
maxTokens: 50,
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
console.log('Request includes image URL in message');
|
|
47
|
+
console.log('This feature is only available in v2 API');
|
|
48
|
+
}
|
|
49
|
+
async function testToolCalls() {
|
|
50
|
+
console.log('\n\nTest 3: Tool calls with Anthropic (not supported in v1)\n');
|
|
51
|
+
const request = {
|
|
52
|
+
gate: 'test-gate',
|
|
53
|
+
model: 'claude-sonnet-4-5-20250929',
|
|
54
|
+
type: 'chat',
|
|
55
|
+
data: {
|
|
56
|
+
messages: [
|
|
57
|
+
{ role: 'user', content: 'What is the weather in San Francisco?' }
|
|
58
|
+
],
|
|
59
|
+
tools: [
|
|
60
|
+
{
|
|
61
|
+
type: 'function',
|
|
62
|
+
function: {
|
|
63
|
+
name: 'get_weather',
|
|
64
|
+
description: 'Get the current weather for a location',
|
|
65
|
+
parameters: {
|
|
66
|
+
type: 'object',
|
|
67
|
+
properties: {
|
|
68
|
+
location: {
|
|
69
|
+
type: 'string',
|
|
70
|
+
description: 'The city and state, e.g. San Francisco, CA',
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
required: ['location'],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
toolChoice: 'auto',
|
|
79
|
+
maxTokens: 100,
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
console.log('Request includes tools array and toolChoice');
|
|
83
|
+
console.log('Response will include toolCalls array if Claude wants to call a tool');
|
|
84
|
+
console.log('This feature is only available in v2 API');
|
|
85
|
+
}
|
|
86
|
+
async function testSystemPrompt() {
|
|
87
|
+
console.log('\n\nTest 4: System prompt and advanced params\n');
|
|
88
|
+
const request = {
|
|
89
|
+
gate: 'test-gate',
|
|
90
|
+
model: 'claude-sonnet-4-5-20250929',
|
|
91
|
+
type: 'chat',
|
|
92
|
+
data: {
|
|
93
|
+
messages: [
|
|
94
|
+
{ role: 'user', content: 'Write a haiku about coding' }
|
|
95
|
+
],
|
|
96
|
+
systemPrompt: 'You are a poetic AI that loves to write haikus.',
|
|
97
|
+
temperature: 1.0,
|
|
98
|
+
topP: 0.9,
|
|
99
|
+
maxTokens: 100,
|
|
100
|
+
stopSequences: ['END'],
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
console.log('Request includes:');
|
|
104
|
+
console.log('- systemPrompt: custom system instruction');
|
|
105
|
+
console.log('- temperature: controls randomness');
|
|
106
|
+
console.log('- topP: nucleus sampling');
|
|
107
|
+
console.log('- stopSequences: custom stop sequences');
|
|
108
|
+
console.log('All these features are available in v2');
|
|
109
|
+
}
|
|
110
|
+
async function runTests() {
|
|
111
|
+
console.log('='.repeat(60));
|
|
112
|
+
console.log('V2 Complete Route - Feature Showcase');
|
|
113
|
+
console.log('='.repeat(60));
|
|
114
|
+
console.log('\nTo test manually:');
|
|
115
|
+
console.log('1. Start server: pnpm --filter @layer-ai/api dev');
|
|
116
|
+
console.log('2. Create test gate in database with Claude model');
|
|
117
|
+
console.log('3. POST to /v2/complete with LayerRequest format');
|
|
118
|
+
console.log('='.repeat(60));
|
|
119
|
+
await testBasicChat();
|
|
120
|
+
await testVision();
|
|
121
|
+
await testToolCalls();
|
|
122
|
+
await testSystemPrompt();
|
|
123
|
+
console.log('\n' + '='.repeat(60));
|
|
124
|
+
console.log('Key differences from v1:');
|
|
125
|
+
console.log('- v1: Simple CompletionRequest (messages, temp, maxTokens, topP)');
|
|
126
|
+
console.log('- v2: Full LayerRequest (all above + tools, images, stopSeqs, etc)');
|
|
127
|
+
console.log('- v1: Returns CompletionResponse (content, usage)');
|
|
128
|
+
console.log('- v2: Returns LayerResponse (content, usage, toolCalls, cost, etc)');
|
|
129
|
+
console.log('='.repeat(60));
|
|
130
|
+
}
|
|
131
|
+
runTests();
|
|
132
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-complete-openai.d.ts","sourceRoot":"","sources":["../../../../src/routes/v2/tests/test-complete-openai.ts"],"names":[],"mappings":""}
|