@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 +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/lib/anthropic-conversion.d.ts +15 -0
- package/dist/lib/anthropic-conversion.d.ts.map +1 -0
- package/dist/lib/anthropic-conversion.js +331 -0
- package/dist/middleware/auth.d.ts +5 -0
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +12 -0
- package/dist/routes/v1/messages.d.ts +4 -0
- package/dist/routes/v1/messages.d.ts.map +1 -0
- package/dist/routes/v1/messages.js +286 -0
- package/package.json +1 -1
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';
|
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,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,
|
|
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"}
|
package/dist/middleware/auth.js
CHANGED
|
@@ -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 @@
|
|
|
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;
|