@layer-ai/core 2.0.32 → 2.0.34

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 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 chatCompletionsRouter } from './routes/v1/chat-completions.js';
6
+ export { default as messagesRouter } from './routes/v1/messages.js';
6
7
  export { default as spendingRouter } from './routes/v1/spending.js';
7
8
  export { default as completeRouter } from './routes/v2/complete.js';
8
9
  export { default as chatRouter } from './routes/v3/chat.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,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AACnF,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAGpE,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAGpE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAG1D,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;AAGnI,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,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;AAC5D,OAAO,EAAE,OAAO,IAAI,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AACnF,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAGpE,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAGpE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAG1D,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;AAGnI,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC"}
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ 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
6
  export { default as chatCompletionsRouter } from './routes/v1/chat-completions.js';
7
+ export { default as messagesRouter } from './routes/v1/messages.js';
7
8
  export { default as spendingRouter } from './routes/v1/spending.js';
8
9
  // v2 routes
9
10
  export { default as completeRouter } from './routes/v2/complete.js';
@@ -0,0 +1,15 @@
1
+ import type { AnthropicMessageCreateParams, AnthropicMessageResponse, AnthropicMessageStreamEvent } from '@layer-ai/sdk';
2
+ import type { LayerRequest, LayerResponse } from '@layer-ai/sdk';
3
+ /**
4
+ * Convert Anthropic Messages API request to LayerRequest
5
+ */
6
+ export declare function convertAnthropicRequestToLayer(anthropicReq: AnthropicMessageCreateParams, gateId: string): LayerRequest;
7
+ /**
8
+ * Convert LayerResponse to Anthropic Messages API response
9
+ */
10
+ export declare function convertLayerResponseToAnthropic(layerResp: LayerResponse): AnthropicMessageResponse;
11
+ /**
12
+ * Convert LayerResponse stream chunks to Anthropic streaming events
13
+ */
14
+ export declare function convertLayerStreamToAnthropicEvents(chunks: AsyncGenerator<LayerResponse>): AsyncGenerator<AnthropicMessageStreamEvent>;
15
+ //# sourceMappingURL=anthropic-conversion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anthropic-conversion.d.ts","sourceRoot":"","sources":["../../src/lib/anthropic-conversion.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,4BAA4B,EAI5B,wBAAwB,EACxB,2BAA2B,EAE5B,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EACV,YAAY,EACZ,aAAa,EAMd,MAAM,eAAe,CAAC;AAyHvB;;GAEG;AACH,wBAAgB,8BAA8B,CAC5C,YAAY,EAAE,4BAA4B,EAC1C,MAAM,EAAE,MAAM,GACb,YAAY,CAkCd;AAkBD;;GAEG;AACH,wBAAgB,+BAA+B,CAC7C,SAAS,EAAE,aAAa,GACvB,wBAAwB,CAoC1B;AAeD;;GAEG;AACH,wBAAuB,mCAAmC,CACxD,MAAM,EAAE,cAAc,CAAC,aAAa,CAAC,GACpC,cAAc,CAAC,2BAA2B,CAAC,CAiJ7C"}
@@ -0,0 +1,331 @@
1
+ import { nanoid } from 'nanoid';
2
+ /**
3
+ * Convert a single Anthropic message to Layer message format
4
+ */
5
+ function convertAnthropicMessageToLayer(msg) {
6
+ // Handle string content (simple case)
7
+ if (typeof msg.content === 'string') {
8
+ return {
9
+ role: msg.role,
10
+ content: msg.content,
11
+ };
12
+ }
13
+ // Handle content blocks
14
+ let textContent = '';
15
+ const images = [];
16
+ const toolCalls = [];
17
+ let toolCallId;
18
+ for (const block of msg.content) {
19
+ if (block.type === 'text') {
20
+ textContent += (textContent ? '\n' : '') + block.text;
21
+ }
22
+ else if (block.type === 'image') {
23
+ if (block.source.type === 'url' && block.source.url) {
24
+ images.push({ url: block.source.url });
25
+ }
26
+ else if (block.source.type === 'base64' && block.source.data) {
27
+ images.push({
28
+ base64: block.source.data,
29
+ mimeType: (block.source.media_type || 'image/jpeg'),
30
+ });
31
+ }
32
+ }
33
+ else if (block.type === 'tool_use') {
34
+ toolCalls.push({
35
+ id: block.id,
36
+ type: 'function',
37
+ function: {
38
+ name: block.name,
39
+ arguments: JSON.stringify(block.input),
40
+ },
41
+ });
42
+ }
43
+ else if (block.type === 'tool_result') {
44
+ toolCallId = block.tool_use_id;
45
+ // Content in tool_result can be string or array
46
+ if (typeof block.content === 'string') {
47
+ textContent = block.content;
48
+ }
49
+ else if (Array.isArray(block.content)) {
50
+ // Extract text from content blocks
51
+ textContent = block.content
52
+ .filter((c) => c.type === 'text')
53
+ .map((c) => c.text)
54
+ .join('\n');
55
+ }
56
+ }
57
+ }
58
+ return {
59
+ role: msg.role,
60
+ content: textContent || undefined,
61
+ images: images.length > 0 ? images : undefined,
62
+ toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
63
+ toolCallId,
64
+ };
65
+ }
66
+ /**
67
+ * Convert Anthropic tools to Layer tools
68
+ */
69
+ function convertAnthropicToolsToLayer(anthropicTools) {
70
+ return anthropicTools.map((tool) => ({
71
+ type: 'function',
72
+ function: {
73
+ name: tool.name,
74
+ description: tool.description,
75
+ parameters: tool.input_schema,
76
+ },
77
+ }));
78
+ }
79
+ /**
80
+ * Convert Anthropic tool_choice to Layer tool_choice
81
+ */
82
+ function convertAnthropicToolChoiceToLayer(anthropicToolChoice) {
83
+ if (!anthropicToolChoice)
84
+ return undefined;
85
+ if (anthropicToolChoice.type === 'auto') {
86
+ return 'auto';
87
+ }
88
+ else if (anthropicToolChoice.type === 'any') {
89
+ return 'required';
90
+ }
91
+ else if (anthropicToolChoice.type === 'tool') {
92
+ return {
93
+ type: 'function',
94
+ function: { name: anthropicToolChoice.name },
95
+ };
96
+ }
97
+ return undefined;
98
+ }
99
+ /**
100
+ * Extract system prompt from Anthropic system parameter
101
+ */
102
+ function extractSystemPrompt(system) {
103
+ if (!system)
104
+ return undefined;
105
+ if (typeof system === 'string') {
106
+ return system;
107
+ }
108
+ // Array of content blocks - concatenate text blocks
109
+ return system
110
+ .filter((block) => block.type === 'text')
111
+ .map((block) => block.text)
112
+ .join('\n');
113
+ }
114
+ /**
115
+ * Convert Anthropic Messages API request to LayerRequest
116
+ */
117
+ export function convertAnthropicRequestToLayer(anthropicReq, gateId) {
118
+ // Extract system prompt
119
+ const systemPrompt = extractSystemPrompt(anthropicReq.system);
120
+ // Convert messages
121
+ const messages = anthropicReq.messages.map((msg) => convertAnthropicMessageToLayer(msg));
122
+ // Convert tools
123
+ const tools = anthropicReq.tools
124
+ ? convertAnthropicToolsToLayer(anthropicReq.tools)
125
+ : undefined;
126
+ // Convert tool_choice
127
+ const toolChoice = convertAnthropicToolChoiceToLayer(anthropicReq.tool_choice);
128
+ return {
129
+ gateId,
130
+ type: 'chat',
131
+ model: anthropicReq.model,
132
+ data: {
133
+ messages,
134
+ systemPrompt,
135
+ tools,
136
+ toolChoice,
137
+ temperature: anthropicReq.temperature,
138
+ maxTokens: anthropicReq.max_tokens,
139
+ topP: anthropicReq.top_p,
140
+ // Note: topK is not supported in Layer's ChatRequest, Anthropic-specific parameter ignored
141
+ stream: anthropicReq.stream,
142
+ stopSequences: anthropicReq.stop_sequences,
143
+ },
144
+ };
145
+ }
146
+ /**
147
+ * Map Layer finish reason to Anthropic stop_reason
148
+ */
149
+ function mapFinishReasonToAnthropic(finishReason) {
150
+ if (finishReason === 'length_limit') {
151
+ return 'max_tokens';
152
+ }
153
+ else if (finishReason === 'tool_call') {
154
+ return 'tool_use';
155
+ }
156
+ else if (finishReason === 'stop_sequence') {
157
+ return 'stop_sequence';
158
+ }
159
+ return 'end_turn';
160
+ }
161
+ /**
162
+ * Convert LayerResponse to Anthropic Messages API response
163
+ */
164
+ export function convertLayerResponseToAnthropic(layerResp) {
165
+ const content = [];
166
+ // Add text content block
167
+ if (layerResp.content) {
168
+ content.push({
169
+ type: 'text',
170
+ text: layerResp.content,
171
+ });
172
+ }
173
+ // Add tool use blocks
174
+ if (layerResp.toolCalls && layerResp.toolCalls.length > 0) {
175
+ for (const toolCall of layerResp.toolCalls) {
176
+ content.push({
177
+ type: 'tool_use',
178
+ id: toolCall.id,
179
+ name: toolCall.function.name,
180
+ input: JSON.parse(toolCall.function.arguments),
181
+ });
182
+ }
183
+ }
184
+ return {
185
+ id: layerResp.id || `msg-${nanoid()}`,
186
+ type: 'message',
187
+ role: 'assistant',
188
+ content,
189
+ model: layerResp.model || 'claude-3-5-sonnet-20241022',
190
+ stop_reason: mapFinishReasonToAnthropic(layerResp.finishReason),
191
+ stop_sequence: null,
192
+ usage: {
193
+ input_tokens: layerResp.usage?.promptTokens || 0,
194
+ output_tokens: layerResp.usage?.completionTokens || 0,
195
+ },
196
+ };
197
+ }
198
+ /**
199
+ * Convert LayerResponse stream chunks to Anthropic streaming events
200
+ */
201
+ export async function* convertLayerStreamToAnthropicEvents(chunks) {
202
+ const state = {
203
+ messageId: `msg-${nanoid()}`,
204
+ model: 'claude-3-5-sonnet-20241022',
205
+ contentBlockIndex: 0,
206
+ hasStartedContentBlock: false,
207
+ currentToolCallInput: '',
208
+ };
209
+ let isFirstChunk = true;
210
+ let inputTokens = 0;
211
+ let outputTokens = 0;
212
+ for await (const chunk of chunks) {
213
+ // Update model from first chunk
214
+ if (chunk.model) {
215
+ state.model = chunk.model;
216
+ }
217
+ // First chunk: send message_start
218
+ if (isFirstChunk) {
219
+ inputTokens = chunk.usage?.promptTokens || 0;
220
+ yield {
221
+ type: 'message_start',
222
+ message: {
223
+ id: state.messageId,
224
+ type: 'message',
225
+ role: 'assistant',
226
+ content: [],
227
+ model: state.model,
228
+ stop_reason: null,
229
+ stop_sequence: null,
230
+ usage: {
231
+ input_tokens: inputTokens,
232
+ output_tokens: 0,
233
+ },
234
+ },
235
+ };
236
+ isFirstChunk = false;
237
+ }
238
+ // Handle text content
239
+ if (chunk.content) {
240
+ if (!state.hasStartedContentBlock) {
241
+ // Start text content block
242
+ yield {
243
+ type: 'content_block_start',
244
+ index: state.contentBlockIndex,
245
+ content_block: {
246
+ type: 'text',
247
+ text: '',
248
+ },
249
+ };
250
+ state.hasStartedContentBlock = true;
251
+ }
252
+ // Send text delta
253
+ yield {
254
+ type: 'content_block_delta',
255
+ index: state.contentBlockIndex,
256
+ delta: {
257
+ type: 'text_delta',
258
+ text: chunk.content,
259
+ },
260
+ };
261
+ }
262
+ // Handle tool calls
263
+ if (chunk.toolCalls && chunk.toolCalls.length > 0) {
264
+ for (const toolCall of chunk.toolCalls) {
265
+ // Close previous content block if needed
266
+ if (state.hasStartedContentBlock) {
267
+ yield {
268
+ type: 'content_block_stop',
269
+ index: state.contentBlockIndex,
270
+ };
271
+ state.contentBlockIndex++;
272
+ state.hasStartedContentBlock = false;
273
+ }
274
+ // Start tool use block
275
+ yield {
276
+ type: 'content_block_start',
277
+ index: state.contentBlockIndex,
278
+ content_block: {
279
+ type: 'tool_use',
280
+ id: toolCall.id,
281
+ name: toolCall.function.name,
282
+ input: {},
283
+ },
284
+ };
285
+ // Send tool input delta
286
+ yield {
287
+ type: 'content_block_delta',
288
+ index: state.contentBlockIndex,
289
+ delta: {
290
+ type: 'input_json_delta',
291
+ partial_json: toolCall.function.arguments,
292
+ },
293
+ };
294
+ // Close tool use block
295
+ yield {
296
+ type: 'content_block_stop',
297
+ index: state.contentBlockIndex,
298
+ };
299
+ state.contentBlockIndex++;
300
+ }
301
+ }
302
+ // Track output tokens
303
+ if (chunk.usage?.completionTokens) {
304
+ outputTokens = chunk.usage.completionTokens;
305
+ }
306
+ // Last chunk: send message_delta and message_stop
307
+ if (chunk.finishReason) {
308
+ // Close any open content block
309
+ if (state.hasStartedContentBlock) {
310
+ yield {
311
+ type: 'content_block_stop',
312
+ index: state.contentBlockIndex,
313
+ };
314
+ }
315
+ // Send message delta with stop reason
316
+ yield {
317
+ type: 'message_delta',
318
+ delta: {
319
+ stop_reason: mapFinishReasonToAnthropic(chunk.finishReason),
320
+ },
321
+ usage: {
322
+ output_tokens: outputTokens,
323
+ },
324
+ };
325
+ // Send message stop
326
+ yield {
327
+ type: 'message_stop',
328
+ };
329
+ }
330
+ }
331
+ }
@@ -20,6 +20,11 @@ declare global {
20
20
  * Authorization: Bearer layer_abc123...
21
21
  */
22
22
  export declare function authenticate(req: Request, res: Response, next: NextFunction): Promise<void>;
23
+ /**
24
+ * Auth middleware that supports both Layer format (Authorization: Bearer) and Anthropic format (x-api-key)
25
+ * Used for the /v1/messages endpoint to support Anthropic SDK clients
26
+ */
27
+ export declare function authenticateAnthropicCompatible(req: Request, res: Response, next: NextFunction): Promise<void>;
23
28
  /**
24
29
  * Optional middleware for endpoints that don't require auth
25
30
  * like the health check public endpoints etc.
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAK1D,oBAAY,QAAQ;IAClB,OAAO,YAAY;IACnB,OAAO,YAAY;CACpB;AAGD,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,MAAM,CAAC,EAAE,MAAM,CAAC;YAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;YAClB,UAAU,CAAC,EAAE,MAAM,CAAC;YACpB,QAAQ,CAAC,EAAE,QAAQ,CAAC;SACrB;KACF;CACF;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,GACjB,OAAO,CAAC,IAAI,CAAC,CA4Jf;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,GACjB,IAAI,CAWN"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAK1D,oBAAY,QAAQ;IAClB,OAAO,YAAY;IACnB,OAAO,YAAY;CACpB;AAGD,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,MAAM,CAAC,EAAE,MAAM,CAAC;YAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;YAClB,UAAU,CAAC,EAAE,MAAM,CAAC;YACpB,QAAQ,CAAC,EAAE,QAAQ,CAAC;SACrB;KACF;CACF;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,GACjB,OAAO,CAAC,IAAI,CAAC,CA4Jf;AAED;;;GAGG;AACH,wBAAsB,+BAA+B,CACnD,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,GACjB,OAAO,CAAC,IAAI,CAAC,CAoBf;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,GACjB,IAAI,CAWN"}
@@ -144,6 +144,30 @@ export async function authenticate(req, res, next) {
144
144
  });
145
145
  }
146
146
  }
147
+ /**
148
+ * Auth middleware that supports both Layer format (Authorization: Bearer) and Anthropic format (x-api-key)
149
+ * Used for the /v1/messages endpoint to support Anthropic SDK clients
150
+ */
151
+ export async function authenticateAnthropicCompatible(req, res, next) {
152
+ try {
153
+ // Check for x-api-key header first (Anthropic SDK format)
154
+ const xApiKey = req.headers['x-api-key'];
155
+ if (xApiKey) {
156
+ // Convert x-api-key to Authorization header format and process
157
+ req.headers.authorization = `Bearer ${xApiKey}`;
158
+ return authenticate(req, res, next);
159
+ }
160
+ // Fall back to standard Authorization header
161
+ return authenticate(req, res, next);
162
+ }
163
+ catch (error) {
164
+ console.error('Authentication error:', error);
165
+ res.status(500).json({
166
+ error: 'internal_error',
167
+ message: 'Authentication failed'
168
+ });
169
+ }
170
+ }
147
171
  /**
148
172
  * Optional middleware for endpoints that don't require auth
149
173
  * like the health check public endpoints etc.
@@ -155,6 +179,6 @@ export function optionalAuth(req, res, next) {
155
179
  next();
156
180
  return;
157
181
  }
158
- // if auth header exists, validate it
182
+ // if auth header exists, validate it
159
183
  authenticate(req, res, next);
160
184
  }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env npx tsx
2
+ export {};
3
+ //# sourceMappingURL=test-anthropic-endpoint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-anthropic-endpoint.d.ts","sourceRoot":"","sources":["../../../src/routes/tests/test-anthropic-endpoint.ts"],"names":[],"mappings":""}
@@ -0,0 +1,346 @@
1
+ #!/usr/bin/env npx tsx
2
+ console.log('='.repeat(80));
3
+ console.log('ANTHROPIC MESSAGES API ENDPOINT TESTS');
4
+ console.log('='.repeat(80));
5
+ console.log('');
6
+ const BASE_URL = process.env.API_URL || 'http://localhost:3001';
7
+ const API_KEY = process.env.LAYER_API_KEY;
8
+ const GATE_ID = process.env.TEST_GATE_ID;
9
+ if (!API_KEY) {
10
+ console.error('❌ Error: LAYER_API_KEY environment variable not set');
11
+ process.exit(1);
12
+ }
13
+ if (!GATE_ID) {
14
+ console.error('❌ Error: TEST_GATE_ID environment variable not set');
15
+ process.exit(1);
16
+ }
17
+ async function testNonStreamingBasic() {
18
+ console.log('Test 1: Non-streaming basic message');
19
+ console.log('-'.repeat(80));
20
+ const request = {
21
+ model: 'claude-3-5-sonnet-20241022',
22
+ max_tokens: 100,
23
+ messages: [
24
+ { role: 'user', content: 'Say "test passed" and nothing else.' }
25
+ ],
26
+ gateId: GATE_ID,
27
+ };
28
+ const response = await fetch(`${BASE_URL}/v1/messages`, {
29
+ method: 'POST',
30
+ headers: {
31
+ 'Content-Type': 'application/json',
32
+ 'Authorization': `Bearer ${API_KEY}`,
33
+ },
34
+ body: JSON.stringify(request),
35
+ });
36
+ if (!response.ok) {
37
+ const error = await response.json();
38
+ throw new Error(`Request failed: ${JSON.stringify(error)}`);
39
+ }
40
+ const data = await response.json();
41
+ console.log(' Response ID:', data.id);
42
+ console.log(' Model:', data.model);
43
+ console.log(' Content:', data.content);
44
+ console.log(' Stop reason:', data.stop_reason);
45
+ console.log(' Usage:', data.usage);
46
+ console.log(' ✅ Non-streaming basic test passed\n');
47
+ }
48
+ async function testNonStreamingWithGateIdInHeader() {
49
+ console.log('Test 2: Non-streaming with gateId in header');
50
+ console.log('-'.repeat(80));
51
+ const request = {
52
+ model: 'claude-3-5-sonnet-20241022',
53
+ max_tokens: 100,
54
+ messages: [
55
+ { role: 'user', content: 'Say "header test passed" and nothing else.' }
56
+ ],
57
+ };
58
+ const response = await fetch(`${BASE_URL}/v1/messages`, {
59
+ method: 'POST',
60
+ headers: {
61
+ 'Content-Type': 'application/json',
62
+ 'Authorization': `Bearer ${API_KEY}`,
63
+ 'X-Layer-Gate-Id': GATE_ID,
64
+ },
65
+ body: JSON.stringify(request),
66
+ });
67
+ if (!response.ok) {
68
+ const error = await response.json();
69
+ throw new Error(`Request failed: ${JSON.stringify(error)}`);
70
+ }
71
+ const data = await response.json();
72
+ const textBlock = data.content.find(block => block.type === 'text');
73
+ console.log(' Content:', textBlock ? textBlock.text : 'No text content');
74
+ console.log(' ✅ Header gateId test passed\n');
75
+ }
76
+ async function testStreamingBasic() {
77
+ console.log('Test 3: Streaming basic message');
78
+ console.log('-'.repeat(80));
79
+ const request = {
80
+ model: 'claude-3-5-sonnet-20241022',
81
+ max_tokens: 100,
82
+ messages: [
83
+ { role: 'user', content: 'Count from 1 to 3, one number per line.' }
84
+ ],
85
+ stream: true,
86
+ gateId: GATE_ID,
87
+ };
88
+ const response = await fetch(`${BASE_URL}/v1/messages`, {
89
+ method: 'POST',
90
+ headers: {
91
+ 'Content-Type': 'application/json',
92
+ 'Authorization': `Bearer ${API_KEY}`,
93
+ },
94
+ body: JSON.stringify(request),
95
+ });
96
+ if (!response.ok) {
97
+ const error = await response.json();
98
+ throw new Error(`Request failed: ${JSON.stringify(error)}`);
99
+ }
100
+ let eventCount = 0;
101
+ let fullContent = '';
102
+ let inputTokens = 0;
103
+ let outputTokens = 0;
104
+ const reader = response.body?.getReader();
105
+ const decoder = new TextDecoder();
106
+ if (!reader) {
107
+ throw new Error('No response body reader');
108
+ }
109
+ while (true) {
110
+ const { done, value } = await reader.read();
111
+ if (done)
112
+ break;
113
+ const text = decoder.decode(value);
114
+ const lines = text.split('\n');
115
+ let currentEvent = '';
116
+ for (const line of lines) {
117
+ if (line.startsWith('event: ')) {
118
+ currentEvent = line.replace('event: ', '').trim();
119
+ }
120
+ else if (line.startsWith('data: ')) {
121
+ const data = line.replace('data: ', '').trim();
122
+ try {
123
+ const event = JSON.parse(data);
124
+ eventCount++;
125
+ if (event.type === 'message_start' && 'message' in event) {
126
+ inputTokens = event.message.usage.input_tokens;
127
+ }
128
+ else if (event.type === 'content_block_delta' && 'delta' in event) {
129
+ if (event.delta.type === 'text_delta' && event.delta.text) {
130
+ fullContent += event.delta.text;
131
+ }
132
+ }
133
+ else if (event.type === 'message_delta' && 'usage' in event) {
134
+ outputTokens = event.usage.output_tokens;
135
+ }
136
+ }
137
+ catch (e) {
138
+ // Skip invalid JSON
139
+ }
140
+ }
141
+ }
142
+ }
143
+ console.log(' Events received:', eventCount);
144
+ console.log(' Full content:', fullContent.trim());
145
+ console.log(' Input tokens:', inputTokens);
146
+ console.log(' Output tokens:', outputTokens);
147
+ console.log(' ✅ Streaming basic test passed\n');
148
+ }
149
+ async function testWithToolCalls() {
150
+ console.log('Test 4: Non-streaming with tool calls');
151
+ console.log('-'.repeat(80));
152
+ const request = {
153
+ model: 'claude-3-5-sonnet-20241022',
154
+ max_tokens: 1024,
155
+ messages: [
156
+ { role: 'user', content: 'What is the weather in Paris?' }
157
+ ],
158
+ tools: [
159
+ {
160
+ name: 'get_weather',
161
+ description: 'Get the current weather for a location',
162
+ input_schema: {
163
+ type: 'object',
164
+ properties: {
165
+ location: {
166
+ type: 'string',
167
+ description: 'The city and state, e.g. Paris, France',
168
+ },
169
+ },
170
+ required: ['location'],
171
+ },
172
+ },
173
+ ],
174
+ tool_choice: { type: 'auto' },
175
+ gateId: GATE_ID,
176
+ };
177
+ const response = await fetch(`${BASE_URL}/v1/messages`, {
178
+ method: 'POST',
179
+ headers: {
180
+ 'Content-Type': 'application/json',
181
+ 'Authorization': `Bearer ${API_KEY}`,
182
+ },
183
+ body: JSON.stringify(request),
184
+ });
185
+ if (!response.ok) {
186
+ const error = await response.json();
187
+ throw new Error(`Request failed: ${JSON.stringify(error)}`);
188
+ }
189
+ const data = await response.json();
190
+ console.log(' Stop reason:', data.stop_reason);
191
+ const toolUseBlocks = data.content.filter(block => block.type === 'tool_use');
192
+ if (toolUseBlocks.length > 0) {
193
+ console.log(' Tool calls:', JSON.stringify(toolUseBlocks, null, 2));
194
+ console.log(' ✅ Tool calls test passed\n');
195
+ }
196
+ else {
197
+ console.log(' ⚠️ No tool calls received (model may have chosen not to use tools)\n');
198
+ }
199
+ }
200
+ async function testWithSystemPrompt() {
201
+ console.log('Test 5: Message with system prompt');
202
+ console.log('-'.repeat(80));
203
+ const request = {
204
+ model: 'claude-3-5-sonnet-20241022',
205
+ max_tokens: 100,
206
+ system: 'You are a pirate. Always respond in pirate speak.',
207
+ messages: [
208
+ { role: 'user', content: 'Hello, how are you?' }
209
+ ],
210
+ gateId: GATE_ID,
211
+ };
212
+ const response = await fetch(`${BASE_URL}/v1/messages`, {
213
+ method: 'POST',
214
+ headers: {
215
+ 'Content-Type': 'application/json',
216
+ 'Authorization': `Bearer ${API_KEY}`,
217
+ },
218
+ body: JSON.stringify(request),
219
+ });
220
+ if (!response.ok) {
221
+ const error = await response.json();
222
+ throw new Error(`Request failed: ${JSON.stringify(error)}`);
223
+ }
224
+ const data = await response.json();
225
+ const textBlock = data.content.find(block => block.type === 'text');
226
+ const content = textBlock ? textBlock.text : 'No text content';
227
+ console.log(' Content:', content);
228
+ console.log(' ✅ System prompt test passed\n');
229
+ }
230
+ async function testMultiTurnConversation() {
231
+ console.log('Test 6: Multi-turn conversation');
232
+ console.log('-'.repeat(80));
233
+ const request = {
234
+ model: 'claude-3-5-sonnet-20241022',
235
+ max_tokens: 100,
236
+ messages: [
237
+ { role: 'user', content: 'My name is Alice.' },
238
+ { role: 'assistant', content: 'Hello Alice! Nice to meet you.' },
239
+ { role: 'user', content: 'What is my name?' }
240
+ ],
241
+ gateId: GATE_ID,
242
+ };
243
+ const response = await fetch(`${BASE_URL}/v1/messages`, {
244
+ method: 'POST',
245
+ headers: {
246
+ 'Content-Type': 'application/json',
247
+ 'Authorization': `Bearer ${API_KEY}`,
248
+ },
249
+ body: JSON.stringify(request),
250
+ });
251
+ if (!response.ok) {
252
+ const error = await response.json();
253
+ throw new Error(`Request failed: ${JSON.stringify(error)}`);
254
+ }
255
+ const data = await response.json();
256
+ const textBlock = data.content.find(block => block.type === 'text');
257
+ const content = textBlock ? textBlock.text : 'No text content';
258
+ console.log(' Content:', content);
259
+ if (content.toLowerCase().includes('alice')) {
260
+ console.log(' ✅ Multi-turn conversation test passed (remembered name)\n');
261
+ }
262
+ else {
263
+ console.log(' ⚠️ Multi-turn conversation test unclear (name not found in response)\n');
264
+ }
265
+ }
266
+ async function testTemperatureParameter() {
267
+ console.log('Test 7: Temperature parameter');
268
+ console.log('-'.repeat(80));
269
+ const request = {
270
+ model: 'claude-3-5-sonnet-20241022',
271
+ max_tokens: 50,
272
+ temperature: 0.1,
273
+ messages: [
274
+ { role: 'user', content: 'Say "temperature test passed"' }
275
+ ],
276
+ gateId: GATE_ID,
277
+ };
278
+ const response = await fetch(`${BASE_URL}/v1/messages`, {
279
+ method: 'POST',
280
+ headers: {
281
+ 'Content-Type': 'application/json',
282
+ 'Authorization': `Bearer ${API_KEY}`,
283
+ },
284
+ body: JSON.stringify(request),
285
+ });
286
+ if (!response.ok) {
287
+ const error = await response.json();
288
+ throw new Error(`Request failed: ${JSON.stringify(error)}`);
289
+ }
290
+ const data = await response.json();
291
+ const textBlock = data.content.find(block => block.type === 'text');
292
+ const content = textBlock ? textBlock.text : 'No text content';
293
+ console.log(' Content:', content);
294
+ console.log(' ✅ Temperature parameter test passed\n');
295
+ }
296
+ async function testErrorHandlingMissingMaxTokens() {
297
+ console.log('Test 8: Error handling - missing max_tokens');
298
+ console.log('-'.repeat(80));
299
+ const request = {
300
+ model: 'claude-3-5-sonnet-20241022',
301
+ messages: [
302
+ { role: 'user', content: 'Hello' }
303
+ ],
304
+ gateId: GATE_ID,
305
+ // Intentionally omit max_tokens
306
+ };
307
+ const response = await fetch(`${BASE_URL}/v1/messages`, {
308
+ method: 'POST',
309
+ headers: {
310
+ 'Content-Type': 'application/json',
311
+ 'Authorization': `Bearer ${API_KEY}`,
312
+ },
313
+ body: JSON.stringify(request),
314
+ });
315
+ if (response.ok) {
316
+ throw new Error('Expected error for missing max_tokens, but request succeeded');
317
+ }
318
+ const error = await response.json();
319
+ console.log(' Error response:', error);
320
+ if (error.type === 'error' && error.error.message.includes('max_tokens')) {
321
+ console.log(' ✅ Error handling test passed (correctly rejected missing max_tokens)\n');
322
+ }
323
+ else {
324
+ console.log(' ⚠️ Error handling test unclear (unexpected error format)\n');
325
+ }
326
+ }
327
+ (async () => {
328
+ try {
329
+ await testNonStreamingBasic();
330
+ await testNonStreamingWithGateIdInHeader();
331
+ await testStreamingBasic();
332
+ await testWithToolCalls();
333
+ await testWithSystemPrompt();
334
+ await testMultiTurnConversation();
335
+ await testTemperatureParameter();
336
+ await testErrorHandlingMissingMaxTokens();
337
+ console.log('='.repeat(80));
338
+ console.log('✅ ALL ANTHROPIC MESSAGES API ENDPOINT TESTS PASSED');
339
+ console.log('='.repeat(80));
340
+ }
341
+ catch (error) {
342
+ console.error('❌ Test failed:', error);
343
+ process.exit(1);
344
+ }
345
+ })();
346
+ export {};
@@ -0,0 +1,4 @@
1
+ import type { Router as RouterType } from 'express';
2
+ declare const router: RouterType;
3
+ export default router;
4
+ //# sourceMappingURL=messages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../../src/routes/v1/messages.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AAcpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AA0TpC,eAAe,MAAM,CAAC"}
@@ -0,0 +1,286 @@
1
+ import { Router } from 'express';
2
+ import { db } from '../../lib/db/postgres.js';
3
+ import { authenticateAnthropicCompatible } from '../../middleware/auth.js';
4
+ import { spendingTracker } from '../../lib/spending-tracker.js';
5
+ import { convertAnthropicRequestToLayer, convertLayerResponseToAnthropic, convertLayerStreamToAnthropicEvents, } from '../../lib/anthropic-conversion.js';
6
+ import { resolveFinalRequest } from '../v3/chat.js';
7
+ import { callAdapter, callAdapterStream } from '../../lib/provider-factory.js';
8
+ const router = Router();
9
+ async function* executeWithRoutingStream(gateConfig, request, userId) {
10
+ yield* callAdapterStream(request, userId);
11
+ }
12
+ async function executeWithRouting(gateConfig, request, userId) {
13
+ const result = await callAdapter(request, userId);
14
+ return { result, modelUsed: request.model };
15
+ }
16
+ router.post('/', authenticateAnthropicCompatible, async (req, res) => {
17
+ const startTime = Date.now();
18
+ if (!req.userId) {
19
+ const error = {
20
+ type: 'error',
21
+ error: {
22
+ type: 'authentication_error',
23
+ message: 'Missing user ID',
24
+ },
25
+ };
26
+ res.status(401).json(error);
27
+ return;
28
+ }
29
+ const userId = req.userId;
30
+ let gateConfig = null;
31
+ let layerRequest = null;
32
+ try {
33
+ const anthropicReq = req.body;
34
+ // Extract gate ID from multiple possible sources
35
+ let gateId = anthropicReq.gateId || req.headers['x-layer-gate-id'];
36
+ // If not found in body or header, try to extract from model field
37
+ if (!gateId && anthropicReq.model) {
38
+ const modelStr = anthropicReq.model;
39
+ // Try to extract UUID from model field (e.g., "layer/82ab7591-..." or "layer:82ab7591-..." or just "82ab7591-...")
40
+ const uuidPattern = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
41
+ const match = modelStr.match(uuidPattern);
42
+ if (match) {
43
+ gateId = match[0];
44
+ }
45
+ }
46
+ if (!gateId) {
47
+ const error = {
48
+ type: 'error',
49
+ error: {
50
+ type: 'invalid_request_error',
51
+ message: 'Missing required field: gateId (provide in request body, X-Layer-Gate-Id header, or as part of model field)',
52
+ },
53
+ };
54
+ res.status(400).json(error);
55
+ return;
56
+ }
57
+ const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(gateId);
58
+ if (!isUUID) {
59
+ const error = {
60
+ type: 'error',
61
+ error: {
62
+ type: 'invalid_request_error',
63
+ message: 'gateId must be a valid UUID',
64
+ },
65
+ };
66
+ res.status(400).json(error);
67
+ return;
68
+ }
69
+ gateConfig = await db.getGateByUserAndId(userId, gateId);
70
+ if (!gateConfig) {
71
+ const error = {
72
+ type: 'error',
73
+ error: {
74
+ type: 'not_found_error',
75
+ message: `Gate with ID "${gateId}" not found`,
76
+ },
77
+ };
78
+ res.status(404).json(error);
79
+ return;
80
+ }
81
+ if (!anthropicReq.messages || !Array.isArray(anthropicReq.messages) || anthropicReq.messages.length === 0) {
82
+ const error = {
83
+ type: 'error',
84
+ error: {
85
+ type: 'invalid_request_error',
86
+ message: 'Missing required field: messages (must be a non-empty array)',
87
+ },
88
+ };
89
+ res.status(400).json(error);
90
+ return;
91
+ }
92
+ if (!anthropicReq.max_tokens) {
93
+ const error = {
94
+ type: 'error',
95
+ error: {
96
+ type: 'invalid_request_error',
97
+ message: 'Missing required field: max_tokens',
98
+ },
99
+ };
100
+ res.status(400).json(error);
101
+ return;
102
+ }
103
+ if (gateConfig.taskType && gateConfig.taskType !== 'chat') {
104
+ console.warn(`[Type Mismatch] Gate "${gateConfig.name}" (${gateConfig.id}) configured for taskType="${gateConfig.taskType}" ` +
105
+ `but received request to /v1/messages endpoint. Processing as chat request.`);
106
+ }
107
+ layerRequest = convertAnthropicRequestToLayer(anthropicReq, gateId);
108
+ const finalRequest = resolveFinalRequest(gateConfig, layerRequest);
109
+ const isStreaming = finalRequest.data && 'stream' in finalRequest.data && finalRequest.data.stream === true;
110
+ if (isStreaming) {
111
+ res.setHeader('Content-Type', 'text/event-stream');
112
+ res.setHeader('Cache-Control', 'no-cache');
113
+ res.setHeader('Connection', 'keep-alive');
114
+ res.setHeader('X-Accel-Buffering', 'no');
115
+ let promptTokens = 0;
116
+ let completionTokens = 0;
117
+ let totalCost = 0;
118
+ let modelUsed = finalRequest.model;
119
+ try {
120
+ const streamGenerator = executeWithRoutingStream(gateConfig, finalRequest, userId);
121
+ for await (const event of convertLayerStreamToAnthropicEvents(streamGenerator)) {
122
+ // Track usage from message_start and message_delta events
123
+ if (event.type === 'message_start' && event.message.usage) {
124
+ promptTokens = event.message.usage.input_tokens;
125
+ }
126
+ if (event.type === 'message_delta' && event.usage) {
127
+ completionTokens = event.usage.output_tokens;
128
+ }
129
+ if (event.type === 'message_start') {
130
+ modelUsed = event.message.model;
131
+ }
132
+ // Write event to stream
133
+ res.write(`event: ${event.type}\n`);
134
+ res.write(`data: ${JSON.stringify(event)}\n\n`);
135
+ }
136
+ res.end();
137
+ const latencyMs = Date.now() - startTime;
138
+ db.logRequest({
139
+ userId,
140
+ gateId: gateConfig.id,
141
+ gateName: gateConfig.name,
142
+ modelRequested: layerRequest.model || gateConfig.model,
143
+ modelUsed: modelUsed,
144
+ promptTokens,
145
+ completionTokens,
146
+ totalTokens: promptTokens + completionTokens,
147
+ costUsd: totalCost,
148
+ latencyMs,
149
+ success: true,
150
+ errorMessage: null,
151
+ userAgent: req.headers['user-agent'] || null,
152
+ ipAddress: req.ip || null,
153
+ requestPayload: {
154
+ gateId: layerRequest.gateId,
155
+ type: layerRequest.type,
156
+ model: layerRequest.model,
157
+ data: layerRequest.data,
158
+ metadata: layerRequest.metadata,
159
+ },
160
+ responsePayload: {
161
+ streamed: true,
162
+ model: modelUsed,
163
+ usage: { promptTokens, completionTokens, totalTokens: promptTokens + completionTokens },
164
+ cost: totalCost,
165
+ },
166
+ }).catch(err => console.error('Failed to log request:', err));
167
+ spendingTracker.trackSpending(userId, totalCost).catch(err => {
168
+ console.error('Failed to track spending:', err);
169
+ });
170
+ }
171
+ catch (streamError) {
172
+ const errorMessage = streamError instanceof Error ? streamError.message : 'Unknown streaming error';
173
+ const anthropicError = {
174
+ type: 'error',
175
+ error: {
176
+ type: 'api_error',
177
+ message: errorMessage,
178
+ },
179
+ };
180
+ res.write(`event: error\n`);
181
+ res.write(`data: ${JSON.stringify(anthropicError)}\n\n`);
182
+ res.end();
183
+ db.logRequest({
184
+ userId,
185
+ gateId: gateConfig.id,
186
+ gateName: gateConfig.name,
187
+ modelRequested: layerRequest.model || gateConfig.model,
188
+ modelUsed: null,
189
+ promptTokens: 0,
190
+ completionTokens: 0,
191
+ totalTokens: 0,
192
+ costUsd: 0,
193
+ latencyMs: Date.now() - startTime,
194
+ success: false,
195
+ errorMessage,
196
+ userAgent: req.headers['user-agent'] || null,
197
+ ipAddress: req.ip || null,
198
+ requestPayload: {
199
+ gateId: layerRequest.gateId,
200
+ type: layerRequest.type,
201
+ model: layerRequest.model,
202
+ data: layerRequest.data,
203
+ metadata: layerRequest.metadata,
204
+ },
205
+ responsePayload: null,
206
+ }).catch(err => console.error('Failed to log request:', err));
207
+ }
208
+ return;
209
+ }
210
+ const { result, modelUsed } = await executeWithRouting(gateConfig, finalRequest, userId);
211
+ const latencyMs = Date.now() - startTime;
212
+ db.logRequest({
213
+ userId,
214
+ gateId: gateConfig.id,
215
+ gateName: gateConfig.name,
216
+ modelRequested: layerRequest.model || gateConfig.model,
217
+ modelUsed: modelUsed,
218
+ promptTokens: result.usage?.promptTokens || 0,
219
+ completionTokens: result.usage?.completionTokens || 0,
220
+ totalTokens: result.usage?.totalTokens || 0,
221
+ costUsd: result.cost || 0,
222
+ latencyMs,
223
+ success: true,
224
+ errorMessage: null,
225
+ userAgent: req.headers['user-agent'] || null,
226
+ ipAddress: req.ip || null,
227
+ requestPayload: {
228
+ gateId: layerRequest.gateId,
229
+ type: layerRequest.type,
230
+ model: layerRequest.model,
231
+ data: layerRequest.data,
232
+ metadata: layerRequest.metadata,
233
+ },
234
+ responsePayload: {
235
+ content: result.content,
236
+ model: result.model,
237
+ usage: result.usage,
238
+ cost: result.cost,
239
+ finishReason: result.finishReason,
240
+ },
241
+ }).catch(err => console.error('Failed to log request:', err));
242
+ spendingTracker.trackSpending(userId, result.cost || 0).catch(err => {
243
+ console.error('Failed to track spending:', err);
244
+ });
245
+ const anthropicResponse = convertLayerResponseToAnthropic(result);
246
+ res.json(anthropicResponse);
247
+ }
248
+ catch (error) {
249
+ const latencyMs = Date.now() - startTime;
250
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
251
+ db.logRequest({
252
+ userId,
253
+ gateId: gateConfig?.id || null,
254
+ gateName: gateConfig?.name || null,
255
+ modelRequested: (layerRequest?.model || gateConfig?.model) || 'unknown',
256
+ modelUsed: null,
257
+ promptTokens: 0,
258
+ completionTokens: 0,
259
+ totalTokens: 0,
260
+ costUsd: 0,
261
+ latencyMs,
262
+ success: false,
263
+ errorMessage,
264
+ userAgent: req.headers['user-agent'] || null,
265
+ ipAddress: req.ip || null,
266
+ requestPayload: layerRequest ? {
267
+ gateId: layerRequest.gateId,
268
+ type: layerRequest.type,
269
+ model: layerRequest.model,
270
+ data: layerRequest.data,
271
+ metadata: layerRequest.metadata,
272
+ } : null,
273
+ responsePayload: null,
274
+ }).catch(err => console.error('Failed to log request:', err));
275
+ console.error('Anthropic messages error:', error);
276
+ const anthropicError = {
277
+ type: 'error',
278
+ error: {
279
+ type: 'api_error',
280
+ message: errorMessage,
281
+ },
282
+ };
283
+ res.status(500).json(anthropicError);
284
+ }
285
+ });
286
+ export default router;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@layer-ai/core",
3
- "version": "2.0.32",
3
+ "version": "2.0.34",
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.9"
39
+ "@layer-ai/sdk": "^2.5.10"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/bcryptjs": "^2.4.6",