anthropic-gateway 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UpstreamResponseError = void 0;
4
+ exports.convertResponseToAnthropic = convertResponseToAnthropic;
5
+ exports.createErrorResponse = createErrorResponse;
6
+ /** An upstream response was structurally unusable for Anthropic conversion. */
7
+ class UpstreamResponseError extends Error {
8
+ status = 502;
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = 'UpstreamResponseError';
12
+ }
13
+ }
14
+ exports.UpstreamResponseError = UpstreamResponseError;
15
+ /**
16
+ * Convert OpenAI Chat Completion response to Anthropic Messages format
17
+ */
18
+ function convertResponseToAnthropic(openaiResponse, originalModelRequested) {
19
+ const choice = openaiResponse.choices?.[0];
20
+ if (!choice?.message) {
21
+ throw new UpstreamResponseError('Upstream response did not include a completion choice');
22
+ }
23
+ const message = choice.message;
24
+ // Build content blocks
25
+ const content = [];
26
+ // Add text content if present
27
+ if (message.content) {
28
+ content.push({
29
+ type: 'text',
30
+ text: message.content,
31
+ });
32
+ }
33
+ // Add tool use blocks if present
34
+ if (message.tool_calls && message.tool_calls.length > 0) {
35
+ for (const toolCall of message.tool_calls) {
36
+ content.push(convertToolCallToToolUse(toolCall));
37
+ }
38
+ }
39
+ // Map finish reason
40
+ const stopReason = mapFinishReason(choice.finish_reason);
41
+ // Build usage
42
+ const upstreamUsage = openaiResponse.usage;
43
+ const usage = {
44
+ input_tokens: upstreamUsage?.prompt_tokens ?? 0,
45
+ output_tokens: upstreamUsage?.completion_tokens ?? 0,
46
+ cache_read_input_tokens: upstreamUsage?.prompt_tokens_details?.cached_tokens,
47
+ };
48
+ return {
49
+ id: `msg_${openaiResponse.id}`,
50
+ type: 'message',
51
+ role: 'assistant',
52
+ content,
53
+ model: originalModelRequested,
54
+ stop_reason: stopReason,
55
+ stop_sequence: null,
56
+ usage,
57
+ };
58
+ }
59
+ /**
60
+ * Convert OpenAI tool call to Anthropic tool_use block
61
+ */
62
+ function convertToolCallToToolUse(toolCall) {
63
+ let input;
64
+ try {
65
+ input = JSON.parse(toolCall.function.arguments);
66
+ }
67
+ catch {
68
+ input = { raw: toolCall.function.arguments };
69
+ }
70
+ return {
71
+ type: 'tool_use',
72
+ id: toolCall.id,
73
+ name: toolCall.function.name,
74
+ input,
75
+ };
76
+ }
77
+ /**
78
+ * Map OpenAI finish_reason to Anthropic stop_reason
79
+ */
80
+ function mapFinishReason(finishReason) {
81
+ if (!finishReason)
82
+ return null;
83
+ switch (finishReason) {
84
+ case 'stop':
85
+ return 'end_turn';
86
+ case 'length':
87
+ return 'max_tokens';
88
+ case 'tool_calls':
89
+ return 'tool_use';
90
+ case 'content_filter':
91
+ return 'end_turn'; // Map to end_turn as closest equivalent
92
+ default:
93
+ return 'end_turn';
94
+ }
95
+ }
96
+ /**
97
+ * Create an error response in Anthropic format
98
+ */
99
+ function createErrorResponse(error, statusCode = 500) {
100
+ return {
101
+ error: {
102
+ type: mapErrorType(statusCode),
103
+ message: error.message,
104
+ },
105
+ status: statusCode,
106
+ };
107
+ }
108
+ function mapErrorType(statusCode) {
109
+ switch (statusCode) {
110
+ case 400:
111
+ return 'invalid_request_error';
112
+ case 401:
113
+ return 'authentication_error';
114
+ case 403:
115
+ return 'permission_error';
116
+ case 404:
117
+ return 'not_found_error';
118
+ case 429:
119
+ return 'rate_limit_error';
120
+ case 500:
121
+ default:
122
+ return 'api_error';
123
+ }
124
+ }
125
+ //# sourceMappingURL=response.js.map
@@ -0,0 +1,308 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BoundedToolIdRegistry = void 0;
4
+ exports.streamOpenAIToAnthropic = streamOpenAIToAnthropic;
5
+ const tools_1 = require("./tools");
6
+ const tokenUsage_1 = require("../utils/tokenUsage");
7
+ const errorLog_1 = require("../utils/errorLog");
8
+ // Global counter and bounded registry for unique tool IDs within this process.
9
+ let toolIdCounter = 0;
10
+ class BoundedToolIdRegistry {
11
+ maxEntries;
12
+ pruneCount;
13
+ ids = new Set();
14
+ constructor(maxEntries = 10000, pruneCount = 5000) {
15
+ this.maxEntries = maxEntries;
16
+ this.pruneCount = pruneCount;
17
+ }
18
+ has(id) {
19
+ return this.ids.has(id);
20
+ }
21
+ /** Reserve an ID and return whether it had not been seen before. */
22
+ claim(id) {
23
+ if (this.ids.has(id)) {
24
+ return false;
25
+ }
26
+ this.ids.add(id);
27
+ if (this.ids.size > this.maxEntries) {
28
+ const idsToRemove = Math.min(this.pruneCount, this.ids.size);
29
+ const iterator = this.ids.values();
30
+ for (let i = 0; i < idsToRemove; i++) {
31
+ const oldest = iterator.next().value;
32
+ if (oldest === undefined)
33
+ break;
34
+ this.ids.delete(oldest);
35
+ }
36
+ }
37
+ return true;
38
+ }
39
+ get size() {
40
+ return this.ids.size;
41
+ }
42
+ }
43
+ exports.BoundedToolIdRegistry = BoundedToolIdRegistry;
44
+ const usedToolIds = new BoundedToolIdRegistry();
45
+ function generateUniqueToolId() {
46
+ let id;
47
+ do {
48
+ toolIdCounter++;
49
+ const timestamp = Date.now().toString(36);
50
+ const counter = toolIdCounter.toString(36).padStart(4, '0');
51
+ const random = Math.random().toString(36).substring(2, 10);
52
+ id = `call_${timestamp}_${counter}_${random}`;
53
+ } while (usedToolIds.has(id));
54
+ usedToolIds.claim(id);
55
+ return id;
56
+ }
57
+ /**
58
+ * Transform OpenAI streaming response to Anthropic SSE format
59
+ */
60
+ async function streamOpenAIToAnthropic(openaiStream, reply, originalModel, provider = '') {
61
+ const state = {
62
+ messageId: `msg_${Date.now().toString(36)}`,
63
+ model: originalModel,
64
+ responseModel: '',
65
+ provider,
66
+ contentBlockIndex: 0,
67
+ currentToolCalls: new Map(),
68
+ inputTokens: 0,
69
+ outputTokens: 0,
70
+ cachedInputTokens: 0,
71
+ hasStarted: false,
72
+ textContent: '',
73
+ textBlockOpen: false,
74
+ };
75
+ // Access the underlying Node.js response for SSE streaming
76
+ const raw = reply.raw;
77
+ // Set SSE headers
78
+ raw.setHeader('Content-Type', 'text/event-stream');
79
+ raw.setHeader('Cache-Control', 'no-cache');
80
+ raw.setHeader('Connection', 'keep-alive');
81
+ raw.setHeader('X-Accel-Buffering', 'no');
82
+ try {
83
+ for await (const chunk of openaiStream) {
84
+ processChunk(chunk, state, raw);
85
+ }
86
+ // Send final events
87
+ finishStream(state, raw);
88
+ }
89
+ catch (error) {
90
+ sendErrorEvent(normalizeStreamingError(error), state, raw);
91
+ }
92
+ }
93
+ function normalizeStreamingError(error) {
94
+ return error instanceof Error
95
+ ? error
96
+ : new Error(typeof error === 'string' ? error : 'Unknown streaming failure');
97
+ }
98
+ function processChunk(chunk, state, raw) {
99
+ // Update usage if present
100
+ if (chunk.usage) {
101
+ state.inputTokens = chunk.usage.prompt_tokens;
102
+ state.outputTokens = chunk.usage.completion_tokens;
103
+ state.cachedInputTokens = chunk.usage.prompt_tokens_details?.cached_tokens ?? 0;
104
+ }
105
+ // Capture response model from chunk
106
+ if (chunk.model && !state.responseModel) {
107
+ state.responseModel = chunk.model;
108
+ }
109
+ const choice = chunk.choices[0];
110
+ if (!choice)
111
+ return;
112
+ // Send message_start on first chunk
113
+ if (!state.hasStarted) {
114
+ sendMessageStart(state, raw);
115
+ state.hasStarted = true;
116
+ }
117
+ const delta = choice.delta;
118
+ // Handle text content
119
+ if (delta.content) {
120
+ if (!state.textBlockOpen) {
121
+ sendContentBlockStart(state.contentBlockIndex, 'text', '', raw);
122
+ state.textBlockOpen = true;
123
+ }
124
+ state.textContent += delta.content;
125
+ sendTextDelta(state.contentBlockIndex, delta.content, raw);
126
+ }
127
+ // Handle tool calls
128
+ if (delta.tool_calls) {
129
+ for (const toolCall of delta.tool_calls) {
130
+ processToolCallDelta(toolCall, state, raw);
131
+ }
132
+ }
133
+ // Handle finish reason
134
+ if (choice.finish_reason) {
135
+ if (state.textBlockOpen) {
136
+ sendContentBlockStop(state.contentBlockIndex, raw);
137
+ state.textBlockOpen = false;
138
+ state.textContent = '';
139
+ state.contentBlockIndex++;
140
+ }
141
+ for (const toolCall of state.currentToolCalls.values()) {
142
+ sendContentBlockStop(toolCall.blockIndex, raw);
143
+ }
144
+ }
145
+ }
146
+ function processToolCallDelta(toolCall, state, raw) {
147
+ const index = toolCall.index;
148
+ // Check if this is a new tool call
149
+ if (!state.currentToolCalls.has(index)) {
150
+ if (state.textBlockOpen) {
151
+ sendContentBlockStop(state.contentBlockIndex, raw);
152
+ state.textBlockOpen = false;
153
+ state.textContent = '';
154
+ state.contentBlockIndex++;
155
+ }
156
+ // IMPORTANT: Use the original OpenAI tool ID to maintain consistency
157
+ // This ID must match when tool results are sent back
158
+ // If OpenAI doesn't provide an ID, generate a guaranteed unique one
159
+ let toolId;
160
+ if (toolCall.id && usedToolIds.claim(toolCall.id)) {
161
+ toolId = toolCall.id;
162
+ }
163
+ else {
164
+ toolId = generateUniqueToolId();
165
+ }
166
+ const blockIndex = state.contentBlockIndex + index;
167
+ const newToolCall = {
168
+ id: toolId,
169
+ name: toolCall.function?.name || '',
170
+ arguments: '',
171
+ blockIndex,
172
+ };
173
+ state.currentToolCalls.set(index, newToolCall);
174
+ sendContentBlockStart(blockIndex, 'tool_use', newToolCall.name, raw, newToolCall.id);
175
+ }
176
+ // Update tool call data
177
+ const currentCall = state.currentToolCalls.get(index);
178
+ if (toolCall.function?.name) {
179
+ currentCall.name = toolCall.function.name;
180
+ }
181
+ if (toolCall.function?.arguments) {
182
+ currentCall.arguments += toolCall.function.arguments;
183
+ sendInputJsonDelta(currentCall.blockIndex, toolCall.function.arguments, raw);
184
+ }
185
+ }
186
+ function sendMessageStart(state, raw) {
187
+ const event = {
188
+ type: 'message_start',
189
+ message: {
190
+ id: state.messageId,
191
+ type: 'message',
192
+ role: 'assistant',
193
+ content: [],
194
+ model: state.model,
195
+ stop_reason: null,
196
+ stop_sequence: null,
197
+ usage: {
198
+ input_tokens: state.inputTokens,
199
+ output_tokens: state.outputTokens,
200
+ cache_read_input_tokens: state.cachedInputTokens,
201
+ },
202
+ },
203
+ };
204
+ sendSSE(event, raw);
205
+ }
206
+ function sendContentBlockStart(index, type, textOrName, raw, id) {
207
+ let contentBlock;
208
+ if (type === 'text') {
209
+ contentBlock = { type: 'text', text: '' };
210
+ }
211
+ else {
212
+ contentBlock = {
213
+ type: 'tool_use',
214
+ id: id || (0, tools_1.generateToolUseId)(),
215
+ name: textOrName,
216
+ input: {},
217
+ };
218
+ }
219
+ const event = {
220
+ type: 'content_block_start',
221
+ index,
222
+ content_block: contentBlock,
223
+ };
224
+ sendSSE(event, raw);
225
+ }
226
+ function sendTextDelta(index, text, raw) {
227
+ const event = {
228
+ type: 'content_block_delta',
229
+ index,
230
+ delta: {
231
+ type: 'text_delta',
232
+ text,
233
+ },
234
+ };
235
+ sendSSE(event, raw);
236
+ }
237
+ function sendInputJsonDelta(index, partialJson, raw) {
238
+ const event = {
239
+ type: 'content_block_delta',
240
+ index,
241
+ delta: {
242
+ type: 'input_json_delta',
243
+ partial_json: partialJson,
244
+ },
245
+ };
246
+ sendSSE(event, raw);
247
+ }
248
+ function sendContentBlockStop(index, raw) {
249
+ const event = {
250
+ type: 'content_block_stop',
251
+ index,
252
+ };
253
+ sendSSE(event, raw);
254
+ }
255
+ function finishStream(state, raw) {
256
+ // Determine stop reason
257
+ const hasToolCalls = state.currentToolCalls.size > 0;
258
+ const stopReason = hasToolCalls ? 'tool_use' : 'end_turn';
259
+ // Record token usage
260
+ (0, tokenUsage_1.recordUsage)({
261
+ provider: state.provider,
262
+ modelName: state.model,
263
+ model: state.responseModel || undefined,
264
+ inputTokens: state.inputTokens,
265
+ outputTokens: state.outputTokens,
266
+ cachedInputTokens: state.cachedInputTokens || undefined,
267
+ streaming: true,
268
+ });
269
+ // Send message_delta
270
+ const deltaEvent = {
271
+ type: 'message_delta',
272
+ delta: {
273
+ stop_reason: stopReason,
274
+ stop_sequence: null,
275
+ },
276
+ usage: {
277
+ output_tokens: state.outputTokens,
278
+ cache_read_input_tokens: state.cachedInputTokens,
279
+ },
280
+ };
281
+ sendSSE(deltaEvent, raw);
282
+ // Send message_stop
283
+ sendSSE({ type: 'message_stop' }, raw);
284
+ raw.end();
285
+ }
286
+ function sendErrorEvent(error, state, raw) {
287
+ // Record error to file
288
+ (0, errorLog_1.recordError)(error, {
289
+ requestId: state.messageId,
290
+ provider: state.provider,
291
+ modelName: state.model,
292
+ streaming: true,
293
+ });
294
+ const event = {
295
+ type: 'error',
296
+ error: {
297
+ type: 'api_error',
298
+ message: error.message,
299
+ },
300
+ };
301
+ sendSSE(event, raw);
302
+ raw.end();
303
+ }
304
+ function sendSSE(data, raw) {
305
+ raw.write(`event: ${data.type}\n`);
306
+ raw.write(`data: ${JSON.stringify(data)}\n\n`);
307
+ }
308
+ //# sourceMappingURL=streaming.js.map
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.convertToolsToOpenAI = convertToolsToOpenAI;
4
+ exports.convertToolChoiceToOpenAI = convertToolChoiceToOpenAI;
5
+ exports.generateToolUseId = generateToolUseId;
6
+ /**
7
+ * Convert Anthropic tool definitions to OpenAI function format
8
+ */
9
+ function convertToolsToOpenAI(tools) {
10
+ return tools.map(tool => ({
11
+ type: 'function',
12
+ function: {
13
+ name: tool.name,
14
+ description: tool.description,
15
+ parameters: tool.input_schema,
16
+ },
17
+ }));
18
+ }
19
+ /**
20
+ * Convert Anthropic tool choice to OpenAI format
21
+ */
22
+ function convertToolChoiceToOpenAI(toolChoice) {
23
+ switch (toolChoice.type) {
24
+ case 'auto':
25
+ return 'auto';
26
+ case 'any':
27
+ return 'required'; // OpenAI's equivalent - forces tool use
28
+ case 'tool':
29
+ if (toolChoice.name) {
30
+ return {
31
+ type: 'function',
32
+ function: { name: toolChoice.name },
33
+ };
34
+ }
35
+ return 'auto';
36
+ default:
37
+ return 'auto';
38
+ }
39
+ }
40
+ /**
41
+ * Generate a unique tool use ID in Anthropic format
42
+ */
43
+ function generateToolUseId() {
44
+ const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
45
+ let result = 'toolu_';
46
+ for (let i = 0; i < 24; i++) {
47
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
48
+ }
49
+ return result;
50
+ }
51
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateXmlToolInstructions = generateXmlToolInstructions;
4
+ exports.hasXmlToolInstructions = hasXmlToolInstructions;
5
+ /**
6
+ * Escape special XML characters in a string
7
+ */
8
+ function escapeXml(str) {
9
+ return str
10
+ .replace(/&/g, '&amp;')
11
+ .replace(/</g, '&lt;')
12
+ .replace(/>/g, '&gt;')
13
+ .replace(/"/g, '&quot;')
14
+ .replace(/'/g, '&apos;');
15
+ }
16
+ /**
17
+ * Generate XML tool instructions to inject into system prompt.
18
+ * This enables models without native function calling to use tools via XML output.
19
+ */
20
+ function generateXmlToolInstructions(tools) {
21
+ if (!tools || tools.length === 0) {
22
+ return '';
23
+ }
24
+ const toolDefinitions = tools.map(t => {
25
+ const schemaJson = JSON.stringify(t.input_schema, null, 2);
26
+ return `- **${t.name}**: ${escapeXml(t.description)}
27
+ Parameters: ${schemaJson}`;
28
+ }).join('\n\n');
29
+ return `
30
+ # TOOL CALLING FORMAT
31
+
32
+ You are required to use tools to fetch information or perform actions.
33
+ To invoke a tool, you MUST use the following EXACT XML format.
34
+ ANY deviation from this format will cause the tool call to fail.
35
+
36
+ <tool_code name="TOOL_NAME">
37
+ {"argument_name": "value"}
38
+ </tool_code>
39
+
40
+ ## CRITICAL EXECUTION RULES:
41
+ 1. **NO Markdown**: Do NOT wrap the XML in \`\`\`xml or \`\`\` code blocks. Output the raw XML tags directly.
42
+ 2. **Valid JSON**: The content between the tags MUST be valid, parseable JSON.
43
+ - Use double quotes for keys and string values.
44
+ - No trailing commas.
45
+ - No comments using // or /*.
46
+ 3. **Exact Name Match**: The \`name\` attribute MUST match a tool name from the "Available Tools" list exactly (case-sensitive).
47
+ 4. **No Nested Content**: The JSON parameters must be the direct child of \`tool_code\`. Do not nest another \`tool\` or \`function\` tag inside.
48
+ 5. **Thinking**: If you need to think or explain your reasoning, do so in text BEFORE the \`<tool_code>\` block. Do NOT put thoughts inside the tool code.
49
+ 6. **Multiple Tools**: You may call multiple tools in sequence by outputting multiple \`<tool_code>\` blocks.
50
+ 7. **Tool Outputs**: Tool results will be provided to you in the following format:
51
+ <tool_output>
52
+ {result_json_or_text}
53
+ </tool_output>
54
+
55
+ ## EXAMPLE (Correct):
56
+ Thinking: I need to read the file.
57
+ <tool_code name="Read">
58
+ {"file_path": "src/utils.ts"}
59
+ </tool_code>
60
+
61
+ ## EXAMPLES (Incorrect - DO NOT USE):
62
+ Wrapped in code blocks:
63
+ \`\`\`xml
64
+ <tool_code name="Read">...</tool_code>
65
+ \`\`\`
66
+
67
+ Nested tags:
68
+ <tool_code><tool name="Read">...</tool></tool_code>
69
+
70
+ Invalid JSON (keys not quoted):
71
+ <tool_code name="Read">
72
+ {file_path: "src/utils.ts"}
73
+ </tool_code>
74
+
75
+ ## Available Tools:
76
+
77
+ ${toolDefinitions}
78
+ `;
79
+ }
80
+ /**
81
+ * Check if a system prompt already contains XML tool instructions
82
+ */
83
+ function hasXmlToolInstructions(systemPrompt) {
84
+ return systemPrompt.includes('# TOOL CALLING FORMAT') &&
85
+ systemPrompt.includes('<tool_code');
86
+ }
87
+ //# sourceMappingURL=xmlPrompt.js.map