@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 +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 +25 -1
- package/dist/routes/tests/test-anthropic-endpoint.d.ts +3 -0
- package/dist/routes/tests/test-anthropic-endpoint.d.ts.map +1 -0
- package/dist/routes/tests/test-anthropic-endpoint.js +346 -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 +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { default as gatesRouter } from './routes/v1/gates.js';
|
|
|
3
3
|
export { default as keysRouter } from './routes/v1/keys.js';
|
|
4
4
|
export { default as logsRouter } from './routes/v1/logs.js';
|
|
5
5
|
export { default as 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
|
+
}
|
|
@@ -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"}
|
package/dist/middleware/auth.js
CHANGED
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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.
|
|
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.
|
|
39
|
+
"@layer-ai/sdk": "^2.5.10"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/bcryptjs": "^2.4.6",
|