@layer-ai/core 2.0.31 → 2.0.33

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
+ }
@@ -1,10 +1,15 @@
1
1
  import { Request, Response, NextFunction } from 'express';
2
+ export declare enum AuthType {
3
+ API_KEY = "api_key",
4
+ SESSION = "session"
5
+ }
2
6
  declare global {
3
7
  namespace Express {
4
8
  interface Request {
5
9
  userId?: string;
6
10
  apiKeyId?: string;
7
11
  apiKeyHash?: string;
12
+ authType?: AuthType;
8
13
  }
9
14
  }
10
15
  }
@@ -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,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;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,CAiJf;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,wBAAgB,YAAY,CAC1B,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,GACjB,IAAI,CAWN"}
@@ -1,5 +1,11 @@
1
1
  import crypto from 'crypto';
2
2
  import { db } from '../lib/db/postgres.js';
3
+ // Auth type enum
4
+ export var AuthType;
5
+ (function (AuthType) {
6
+ AuthType["API_KEY"] = "api_key";
7
+ AuthType["SESSION"] = "session";
8
+ })(AuthType || (AuthType = {}));
3
9
  /**
4
10
  * Auth middleware for api key validation
5
11
  *
@@ -79,6 +85,7 @@ export async function authenticate(req, res, next) {
79
85
  req.userId = apiKeyRecord.userId;
80
86
  req.apiKeyId = apiKeyRecord.id;
81
87
  req.apiKeyHash = tokenHash;
88
+ req.authType = AuthType.API_KEY;
82
89
  // Update last_used_at timestamp (async, dont await)
83
90
  db.updateApiKeyLastUsed(tokenHash).catch((err) => {
84
91
  console.error('Failed to update API key last_used_at:', err);
@@ -117,6 +124,11 @@ export async function authenticate(req, res, next) {
117
124
  }
118
125
  }
119
126
  req.userId = sessionKey.userId;
127
+ req.authType = AuthType.SESSION;
128
+ // Extend session expiration (sliding window) - async, don't await
129
+ db.query("UPDATE session_keys SET expires_at = NOW() + INTERVAL '24 hours' WHERE key_hash = $1", [tokenHash]).catch((err) => {
130
+ console.error('Failed to extend session expiration:', err);
131
+ });
120
132
  next();
121
133
  return;
122
134
  }
@@ -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 { authenticate } 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('/', authenticate, 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.31",
3
+ "version": "2.0.33",
4
4
  "description": "Core API routes and services for Layer AI",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",