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.
- package/LICENSE +25 -0
- package/README.md +75 -0
- package/cli.js +261 -0
- package/config.example.json +20 -0
- package/lib/server.js +132 -0
- package/package.json +39 -0
- package/vendor/LICENSE.claude-adapter +21 -0
- package/vendor/dist/cli.js +288 -0
- package/vendor/dist/converters/index.js +22 -0
- package/vendor/dist/converters/request.js +349 -0
- package/vendor/dist/converters/response.js +125 -0
- package/vendor/dist/converters/streaming.js +308 -0
- package/vendor/dist/converters/tools.js +51 -0
- package/vendor/dist/converters/xmlPrompt.js +87 -0
- package/vendor/dist/converters/xmlStreaming.js +258 -0
- package/vendor/dist/index.js +32 -0
- package/vendor/dist/server/handlers.js +184 -0
- package/vendor/dist/server/index.js +116 -0
- package/vendor/dist/types/anthropic.js +4 -0
- package/vendor/dist/types/config.js +4 -0
- package/vendor/dist/types/index.js +21 -0
- package/vendor/dist/types/openai.js +4 -0
- package/vendor/dist/utils/config.js +169 -0
- package/vendor/dist/utils/errorLog.js +61 -0
- package/vendor/dist/utils/fileStorage.js +114 -0
- package/vendor/dist/utils/index.js +19 -0
- package/vendor/dist/utils/logger.js +138 -0
- package/vendor/dist/utils/metadata.js +128 -0
- package/vendor/dist/utils/provider.js +14 -0
- package/vendor/dist/utils/tokenUsage.js +32 -0
- package/vendor/dist/utils/ui.js +107 -0
- package/vendor/dist/utils/update.js +112 -0
- package/vendor/dist/utils/validation.js +130 -0
- package/vendor/package.json +6 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// XML Streaming Converter: OpenAI text stream → Anthropic SSE with XML tool call detection
|
|
3
|
+
// Uses buffered approach: accumulates complete tool calls before emitting
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.streamXmlOpenAIToAnthropic = streamXmlOpenAIToAnthropic;
|
|
6
|
+
const tools_1 = require("./tools");
|
|
7
|
+
const tokenUsage_1 = require("../utils/tokenUsage");
|
|
8
|
+
const errorLog_1 = require("../utils/errorLog");
|
|
9
|
+
// Regex patterns
|
|
10
|
+
const THINK_BLOCK_PATTERN = /<think>[\s\S]*?<\/think>/g;
|
|
11
|
+
const TOOL_CODE_PATTERN = /<tool_code\s+name\s*=\s*"([^"]+)"\s*>([\s\S]*?)<\/\s*tool_code\s*>/i;
|
|
12
|
+
const NESTED_TOOL_PATTERN = /<tool\s+name="[^"]*">\s*/g;
|
|
13
|
+
const CLOSE_TOOL_PATTERN = /<\/tool>\s*/g;
|
|
14
|
+
/**
|
|
15
|
+
* Transform OpenAI streaming response (with XML tool calls) to Anthropic SSE format.
|
|
16
|
+
* Uses BUFFERED approach: waits for complete tool calls before emitting.
|
|
17
|
+
*/
|
|
18
|
+
async function streamXmlOpenAIToAnthropic(openaiStream, reply, originalModel, provider = '') {
|
|
19
|
+
const state = {
|
|
20
|
+
messageId: `msg_${Date.now().toString(36)}`,
|
|
21
|
+
model: originalModel,
|
|
22
|
+
responseModel: '',
|
|
23
|
+
provider,
|
|
24
|
+
contentBlockIndex: 0,
|
|
25
|
+
inputTokens: 0,
|
|
26
|
+
outputTokens: 0,
|
|
27
|
+
cachedInputTokens: 0,
|
|
28
|
+
hasStarted: false,
|
|
29
|
+
buffer: '',
|
|
30
|
+
toolCallsEmitted: 0,
|
|
31
|
+
};
|
|
32
|
+
const raw = reply.raw;
|
|
33
|
+
// Set SSE headers
|
|
34
|
+
raw.setHeader('Content-Type', 'text/event-stream');
|
|
35
|
+
raw.setHeader('Cache-Control', 'no-cache');
|
|
36
|
+
raw.setHeader('Connection', 'keep-alive');
|
|
37
|
+
raw.setHeader('X-Accel-Buffering', 'no');
|
|
38
|
+
try {
|
|
39
|
+
for await (const chunk of openaiStream) {
|
|
40
|
+
processChunk(chunk, state, raw);
|
|
41
|
+
}
|
|
42
|
+
// Final flush - emit any remaining text
|
|
43
|
+
flushRemainingContent(state, raw);
|
|
44
|
+
finishStream(state, raw);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
sendErrorEvent(normalizeStreamingError(error), state, raw);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function normalizeStreamingError(error) {
|
|
51
|
+
return error instanceof Error
|
|
52
|
+
? error
|
|
53
|
+
: new Error(typeof error === 'string' ? error : 'Unknown streaming failure');
|
|
54
|
+
}
|
|
55
|
+
function processChunk(chunk, state, raw) {
|
|
56
|
+
// Update usage if present
|
|
57
|
+
if (chunk.usage) {
|
|
58
|
+
state.inputTokens = chunk.usage.prompt_tokens;
|
|
59
|
+
state.outputTokens = chunk.usage.completion_tokens;
|
|
60
|
+
state.cachedInputTokens = chunk.usage.prompt_tokens_details?.cached_tokens ?? 0;
|
|
61
|
+
}
|
|
62
|
+
// Capture response model
|
|
63
|
+
if (chunk.model && !state.responseModel) {
|
|
64
|
+
state.responseModel = chunk.model;
|
|
65
|
+
}
|
|
66
|
+
const choice = chunk.choices[0];
|
|
67
|
+
if (!choice)
|
|
68
|
+
return;
|
|
69
|
+
// Send message_start on first chunk
|
|
70
|
+
if (!state.hasStarted) {
|
|
71
|
+
sendMessageStart(state, raw);
|
|
72
|
+
state.hasStarted = true;
|
|
73
|
+
}
|
|
74
|
+
const textDelta = choice.delta?.content || '';
|
|
75
|
+
if (!textDelta)
|
|
76
|
+
return;
|
|
77
|
+
// Add to buffer
|
|
78
|
+
state.buffer += textDelta;
|
|
79
|
+
// Process buffer for complete tool calls
|
|
80
|
+
processBuffer(state, raw);
|
|
81
|
+
}
|
|
82
|
+
function processBuffer(state, raw) {
|
|
83
|
+
// Keep processing until no more complete tool calls are found
|
|
84
|
+
while (true) {
|
|
85
|
+
// Remove <think> blocks from consideration
|
|
86
|
+
const cleanBuffer = state.buffer.replace(THINK_BLOCK_PATTERN, '');
|
|
87
|
+
// Check for complete tool call
|
|
88
|
+
const toolMatch = cleanBuffer.match(TOOL_CODE_PATTERN);
|
|
89
|
+
if (!toolMatch) {
|
|
90
|
+
// No complete tool call found, exit loop
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
const [fullMatch, toolName, rawArgs] = toolMatch;
|
|
94
|
+
const matchStart = cleanBuffer.indexOf(fullMatch);
|
|
95
|
+
// Get text BEFORE the tool call
|
|
96
|
+
const textBeforeTool = cleanBuffer.substring(0, matchStart);
|
|
97
|
+
const cleanText = textBeforeTool.trim();
|
|
98
|
+
// Emit text block if there's content
|
|
99
|
+
if (cleanText.length > 0) {
|
|
100
|
+
emitTextBlock(cleanText, state, raw);
|
|
101
|
+
}
|
|
102
|
+
// Clean and emit tool use block
|
|
103
|
+
const cleanArgs = cleanToolArgs(rawArgs);
|
|
104
|
+
emitToolUseBlock(toolName, cleanArgs, state, raw);
|
|
105
|
+
// Update buffer: remove everything up to and including the tool call
|
|
106
|
+
// We need to find the position in the ORIGINAL buffer (with think blocks)
|
|
107
|
+
const originalMatchEnd = state.buffer.indexOf('</tool_code>') + '</tool_code>'.length;
|
|
108
|
+
state.buffer = state.buffer.substring(originalMatchEnd);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function flushRemainingContent(state, raw) {
|
|
112
|
+
// Clean remaining buffer
|
|
113
|
+
const cleanBuffer = state.buffer.replace(THINK_BLOCK_PATTERN, '').trim();
|
|
114
|
+
// Get any remaining text
|
|
115
|
+
const remainingText = cleanBuffer.trim();
|
|
116
|
+
if (remainingText.length > 0) {
|
|
117
|
+
emitTextBlock(remainingText, state, raw);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function cleanToolArgs(args) {
|
|
121
|
+
let cleaned = args;
|
|
122
|
+
// Remove nested <tool name="..."> tags
|
|
123
|
+
cleaned = cleaned.replace(NESTED_TOOL_PATTERN, '');
|
|
124
|
+
// Remove </tool> closing tags
|
|
125
|
+
cleaned = cleaned.replace(CLOSE_TOOL_PATTERN, '');
|
|
126
|
+
// Remove any leading ToolName\n pattern
|
|
127
|
+
cleaned = cleaned.replace(/^[A-Za-z_][A-Za-z0-9_]*\s*\n/, '');
|
|
128
|
+
return cleaned.trim();
|
|
129
|
+
}
|
|
130
|
+
function emitTextBlock(text, state, raw) {
|
|
131
|
+
// Start text block
|
|
132
|
+
const startEvent = {
|
|
133
|
+
type: 'content_block_start',
|
|
134
|
+
index: state.contentBlockIndex,
|
|
135
|
+
content_block: { type: 'text', text: '' },
|
|
136
|
+
};
|
|
137
|
+
sendSSE(startEvent, raw);
|
|
138
|
+
// Send text delta
|
|
139
|
+
const deltaEvent = {
|
|
140
|
+
type: 'content_block_delta',
|
|
141
|
+
index: state.contentBlockIndex,
|
|
142
|
+
delta: { type: 'text_delta', text },
|
|
143
|
+
};
|
|
144
|
+
sendSSE(deltaEvent, raw);
|
|
145
|
+
// Stop text block
|
|
146
|
+
const stopEvent = {
|
|
147
|
+
type: 'content_block_stop',
|
|
148
|
+
index: state.contentBlockIndex,
|
|
149
|
+
};
|
|
150
|
+
sendSSE(stopEvent, raw);
|
|
151
|
+
state.contentBlockIndex++;
|
|
152
|
+
}
|
|
153
|
+
function emitToolUseBlock(toolName, args, state, raw) {
|
|
154
|
+
const toolId = (0, tools_1.generateToolUseId)();
|
|
155
|
+
// Start tool_use block
|
|
156
|
+
const startEvent = {
|
|
157
|
+
type: 'content_block_start',
|
|
158
|
+
index: state.contentBlockIndex,
|
|
159
|
+
content_block: {
|
|
160
|
+
type: 'tool_use',
|
|
161
|
+
id: toolId,
|
|
162
|
+
name: toolName,
|
|
163
|
+
input: {},
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
sendSSE(startEvent, raw);
|
|
167
|
+
// Send complete input as single delta
|
|
168
|
+
const deltaEvent = {
|
|
169
|
+
type: 'content_block_delta',
|
|
170
|
+
index: state.contentBlockIndex,
|
|
171
|
+
delta: {
|
|
172
|
+
type: 'input_json_delta',
|
|
173
|
+
partial_json: args,
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
sendSSE(deltaEvent, raw);
|
|
177
|
+
// Stop tool_use block
|
|
178
|
+
const stopEvent = {
|
|
179
|
+
type: 'content_block_stop',
|
|
180
|
+
index: state.contentBlockIndex,
|
|
181
|
+
};
|
|
182
|
+
sendSSE(stopEvent, raw);
|
|
183
|
+
state.contentBlockIndex++;
|
|
184
|
+
state.toolCallsEmitted++;
|
|
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 finishStream(state, raw) {
|
|
207
|
+
// Determine stop reason
|
|
208
|
+
const stopReason = state.toolCallsEmitted > 0 ? 'tool_use' : 'end_turn';
|
|
209
|
+
// Record token usage
|
|
210
|
+
(0, tokenUsage_1.recordUsage)({
|
|
211
|
+
provider: state.provider,
|
|
212
|
+
modelName: state.model,
|
|
213
|
+
model: state.responseModel || undefined,
|
|
214
|
+
inputTokens: state.inputTokens,
|
|
215
|
+
outputTokens: state.outputTokens,
|
|
216
|
+
cachedInputTokens: state.cachedInputTokens || undefined,
|
|
217
|
+
streaming: true
|
|
218
|
+
});
|
|
219
|
+
// Send message_delta
|
|
220
|
+
const deltaEvent = {
|
|
221
|
+
type: 'message_delta',
|
|
222
|
+
delta: {
|
|
223
|
+
stop_reason: stopReason,
|
|
224
|
+
stop_sequence: null,
|
|
225
|
+
},
|
|
226
|
+
usage: {
|
|
227
|
+
output_tokens: state.outputTokens,
|
|
228
|
+
cache_read_input_tokens: state.cachedInputTokens,
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
sendSSE(deltaEvent, raw);
|
|
232
|
+
// Send message_stop
|
|
233
|
+
sendSSE({ type: 'message_stop' }, raw);
|
|
234
|
+
raw.end();
|
|
235
|
+
}
|
|
236
|
+
function sendErrorEvent(error, state, raw) {
|
|
237
|
+
// Record error to file
|
|
238
|
+
(0, errorLog_1.recordError)(error, {
|
|
239
|
+
requestId: state.messageId,
|
|
240
|
+
provider: state.provider,
|
|
241
|
+
modelName: state.model,
|
|
242
|
+
streaming: true
|
|
243
|
+
});
|
|
244
|
+
const event = {
|
|
245
|
+
type: 'error',
|
|
246
|
+
error: {
|
|
247
|
+
type: 'api_error',
|
|
248
|
+
message: error.message,
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
sendSSE(event, raw);
|
|
252
|
+
raw.end();
|
|
253
|
+
}
|
|
254
|
+
function sendSSE(data, raw) {
|
|
255
|
+
raw.write(`event: ${data.type}\n`);
|
|
256
|
+
raw.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
257
|
+
}
|
|
258
|
+
//# sourceMappingURL=xmlStreaming.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.updateClaudeSettings = exports.updateClaudeJson = exports.configExists = exports.preserveProxyAuthToken = exports.ensureProxyAuthToken = exports.saveConfig = exports.loadConfig = exports.findAvailablePort = exports.createServer = void 0;
|
|
18
|
+
// Main library exports
|
|
19
|
+
__exportStar(require("./types"), exports);
|
|
20
|
+
__exportStar(require("./converters"), exports);
|
|
21
|
+
var server_1 = require("./server");
|
|
22
|
+
Object.defineProperty(exports, "createServer", { enumerable: true, get: function () { return server_1.createServer; } });
|
|
23
|
+
Object.defineProperty(exports, "findAvailablePort", { enumerable: true, get: function () { return server_1.findAvailablePort; } });
|
|
24
|
+
var config_1 = require("./utils/config");
|
|
25
|
+
Object.defineProperty(exports, "loadConfig", { enumerable: true, get: function () { return config_1.loadConfig; } });
|
|
26
|
+
Object.defineProperty(exports, "saveConfig", { enumerable: true, get: function () { return config_1.saveConfig; } });
|
|
27
|
+
Object.defineProperty(exports, "ensureProxyAuthToken", { enumerable: true, get: function () { return config_1.ensureProxyAuthToken; } });
|
|
28
|
+
Object.defineProperty(exports, "preserveProxyAuthToken", { enumerable: true, get: function () { return config_1.preserveProxyAuthToken; } });
|
|
29
|
+
Object.defineProperty(exports, "configExists", { enumerable: true, get: function () { return config_1.configExists; } });
|
|
30
|
+
Object.defineProperty(exports, "updateClaudeJson", { enumerable: true, get: function () { return config_1.updateClaudeJson; } });
|
|
31
|
+
Object.defineProperty(exports, "updateClaudeSettings", { enumerable: true, get: function () { return config_1.updateClaudeSettings; } });
|
|
32
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createMessagesHandler = createMessagesHandler;
|
|
7
|
+
const openai_1 = __importDefault(require("openai"));
|
|
8
|
+
const crypto_1 = require("crypto");
|
|
9
|
+
const request_1 = require("../converters/request");
|
|
10
|
+
const provider_1 = require("../utils/provider");
|
|
11
|
+
const response_1 = require("../converters/response");
|
|
12
|
+
const streaming_1 = require("../converters/streaming");
|
|
13
|
+
const xmlStreaming_1 = require("../converters/xmlStreaming");
|
|
14
|
+
const validation_1 = require("../utils/validation");
|
|
15
|
+
const logger_1 = require("../utils/logger");
|
|
16
|
+
const tokenUsage_1 = require("../utils/tokenUsage");
|
|
17
|
+
const errorLog_1 = require("../utils/errorLog");
|
|
18
|
+
// Request ID counter for unique identification
|
|
19
|
+
let requestIdCounter = 0;
|
|
20
|
+
function generateRequestId() {
|
|
21
|
+
requestIdCounter++;
|
|
22
|
+
const timestamp = Date.now().toString(36);
|
|
23
|
+
const counter = requestIdCounter.toString(36).padStart(4, '0');
|
|
24
|
+
return `req_${timestamp}_${counter}`;
|
|
25
|
+
}
|
|
26
|
+
function matchesProxyToken(value, expectedToken) {
|
|
27
|
+
if (typeof value !== 'string') {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const received = Buffer.from(value);
|
|
31
|
+
const expected = Buffer.from(expectedToken);
|
|
32
|
+
return received.length === expected.length && (0, crypto_1.timingSafeEqual)(received, expected);
|
|
33
|
+
}
|
|
34
|
+
function isAuthenticated(request, expectedToken) {
|
|
35
|
+
const authorization = request.headers.authorization;
|
|
36
|
+
if (typeof authorization === 'string' && authorization.startsWith('Bearer ')) {
|
|
37
|
+
return matchesProxyToken(authorization.slice('Bearer '.length), expectedToken);
|
|
38
|
+
}
|
|
39
|
+
return matchesProxyToken(request.headers['x-api-key'], expectedToken);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Handle POST /v1/messages requests
|
|
43
|
+
*/
|
|
44
|
+
function createMessagesHandler(config) {
|
|
45
|
+
const isAzure = (0, provider_1.isAzureOpenAIEndpoint)(config.baseUrl);
|
|
46
|
+
const openai = new openai_1.default({
|
|
47
|
+
baseURL: config.baseUrl,
|
|
48
|
+
apiKey: config.apiKey,
|
|
49
|
+
});
|
|
50
|
+
return async (request, reply) => {
|
|
51
|
+
const requestId = generateRequestId();
|
|
52
|
+
const log = logger_1.logger.withRequestId(requestId);
|
|
53
|
+
// Add request ID to response headers for client tracing
|
|
54
|
+
reply.header('X-Request-Id', requestId);
|
|
55
|
+
if (!isAuthenticated(request, config.proxyAuthToken)) {
|
|
56
|
+
const errorResponse = (0, response_1.createErrorResponse)(new Error('Invalid proxy authentication token'), 401);
|
|
57
|
+
reply.code(401).send({ error: errorResponse.error });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
// Validate request before processing
|
|
62
|
+
const validation = (0, validation_1.validateAnthropicRequest)(request.body);
|
|
63
|
+
if (!validation.valid) {
|
|
64
|
+
const errorMessage = (0, validation_1.formatValidationErrors)(validation.errors);
|
|
65
|
+
log.warn('Invalid request', { errors: validation.errors });
|
|
66
|
+
const errorResponse = (0, response_1.createErrorResponse)(new Error(errorMessage), 400);
|
|
67
|
+
reply.code(400).send({ error: errorResponse.error });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const anthropicRequest = request.body;
|
|
71
|
+
const targetModel = anthropicRequest.model;
|
|
72
|
+
const isStreaming = anthropicRequest.stream ?? false;
|
|
73
|
+
log.info(`→ ${targetModel} [sent]`);
|
|
74
|
+
// Determine tool calling style from config
|
|
75
|
+
const toolStyle = config.toolFormat || 'native';
|
|
76
|
+
// Convert request to OpenAI format
|
|
77
|
+
const openaiRequest = (0, request_1.convertRequestToOpenAI)(anthropicRequest, targetModel, toolStyle, isAzure);
|
|
78
|
+
// Log tool calling mode when tools are present
|
|
79
|
+
if (toolStyle === 'xml' && anthropicRequest.tools?.length) {
|
|
80
|
+
log.info(`Using XML tool calling mode (${anthropicRequest.tools.length} tools)`);
|
|
81
|
+
}
|
|
82
|
+
if (isStreaming) {
|
|
83
|
+
if (toolStyle === 'xml') {
|
|
84
|
+
await handleXmlStreamingRequest(openai, openaiRequest, reply, anthropicRequest.model, config.baseUrl, log);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
await handleStreamingRequest(openai, openaiRequest, reply, anthropicRequest.model, config.baseUrl, log);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
await handleNonStreamingRequest(openai, openaiRequest, reply, anthropicRequest.model, config.baseUrl, log);
|
|
92
|
+
}
|
|
93
|
+
log.info(`← ${targetModel} [received]`);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
const body = request.body;
|
|
97
|
+
handleError(error, reply, log, {
|
|
98
|
+
requestId,
|
|
99
|
+
provider: config.baseUrl,
|
|
100
|
+
modelName: body?.model ?? 'unknown',
|
|
101
|
+
streaming: body?.stream ?? false,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Handle non-streaming API request
|
|
108
|
+
*/
|
|
109
|
+
async function handleNonStreamingRequest(openai, openaiRequest, reply, originalModel, provider, log) {
|
|
110
|
+
log.debug('Making non-streaming request');
|
|
111
|
+
const response = await openai.chat.completions.create({
|
|
112
|
+
...openaiRequest,
|
|
113
|
+
stream: false,
|
|
114
|
+
});
|
|
115
|
+
log.debug('Response received', {
|
|
116
|
+
finishReason: response.choices[0]?.finish_reason,
|
|
117
|
+
usage: response.usage,
|
|
118
|
+
});
|
|
119
|
+
// Record token usage
|
|
120
|
+
if (response.usage) {
|
|
121
|
+
(0, tokenUsage_1.recordUsage)({
|
|
122
|
+
provider,
|
|
123
|
+
modelName: originalModel,
|
|
124
|
+
model: response.model,
|
|
125
|
+
inputTokens: response.usage.prompt_tokens,
|
|
126
|
+
outputTokens: response.usage.completion_tokens,
|
|
127
|
+
cachedInputTokens: response.usage.prompt_tokens_details?.cached_tokens,
|
|
128
|
+
streaming: false,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
const anthropicResponse = (0, response_1.convertResponseToAnthropic)(response, originalModel);
|
|
132
|
+
reply.send(anthropicResponse);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Handle streaming API request
|
|
136
|
+
*/
|
|
137
|
+
async function handleStreamingRequest(openai, openaiRequest, reply, originalModel, provider, log) {
|
|
138
|
+
log.debug('Making streaming request');
|
|
139
|
+
const stream = await openai.chat.completions.create({
|
|
140
|
+
...openaiRequest,
|
|
141
|
+
stream: true,
|
|
142
|
+
});
|
|
143
|
+
reply.hijack();
|
|
144
|
+
await (0, streaming_1.streamOpenAIToAnthropic)(stream, reply, originalModel, provider);
|
|
145
|
+
log.debug('Streaming completed');
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Handle XML streaming API request (for models without native tool calling)
|
|
149
|
+
*/
|
|
150
|
+
async function handleXmlStreamingRequest(openai, openaiRequest, reply, originalModel, provider, log) {
|
|
151
|
+
log.debug('Making XML streaming request (experimental)');
|
|
152
|
+
const stream = await openai.chat.completions.create({
|
|
153
|
+
...openaiRequest,
|
|
154
|
+
stream: true,
|
|
155
|
+
});
|
|
156
|
+
reply.hijack();
|
|
157
|
+
await (0, xmlStreaming_1.streamXmlOpenAIToAnthropic)(stream, reply, originalModel, provider);
|
|
158
|
+
log.debug('XML streaming completed');
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Handle errors and send appropriate response
|
|
162
|
+
*/
|
|
163
|
+
function handleError(caughtError, reply, log, context) {
|
|
164
|
+
const error = caughtError instanceof Error
|
|
165
|
+
? caughtError
|
|
166
|
+
: new Error(typeof caughtError === 'string' ? caughtError : 'Unknown request failure');
|
|
167
|
+
const rawStatus = typeof caughtError === 'object' && caughtError !== null && 'status' in caughtError
|
|
168
|
+
? caughtError.status
|
|
169
|
+
: undefined;
|
|
170
|
+
const statusCode = typeof rawStatus === 'number' &&
|
|
171
|
+
Number.isInteger(rawStatus) &&
|
|
172
|
+
rawStatus >= 400 &&
|
|
173
|
+
rawStatus <= 599
|
|
174
|
+
? rawStatus
|
|
175
|
+
: 500;
|
|
176
|
+
log.error('Request failed', error, { statusCode });
|
|
177
|
+
// Record error to file if context is available
|
|
178
|
+
if (context) {
|
|
179
|
+
(0, errorLog_1.recordError)(error, context);
|
|
180
|
+
}
|
|
181
|
+
const errorResponse = (0, response_1.createErrorResponse)(error, statusCode);
|
|
182
|
+
reply.code(errorResponse.status).send({ error: errorResponse.error });
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=handlers.js.map
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.createServer = createServer;
|
|
40
|
+
exports.findAvailablePort = findAvailablePort;
|
|
41
|
+
// Fastify proxy server setup
|
|
42
|
+
const fastify_1 = __importDefault(require("fastify"));
|
|
43
|
+
const handlers_1 = require("./handlers");
|
|
44
|
+
const logger_1 = require("../utils/logger");
|
|
45
|
+
// Default graceful shutdown timeout in milliseconds
|
|
46
|
+
const DEFAULT_SHUTDOWN_TIMEOUT = 10000;
|
|
47
|
+
/**
|
|
48
|
+
* Create the proxy server with configured routes
|
|
49
|
+
*/
|
|
50
|
+
function createServer(config) {
|
|
51
|
+
if (typeof config.proxyAuthToken !== 'string' || config.proxyAuthToken.length === 0) {
|
|
52
|
+
throw new Error('proxyAuthToken is required to start the Claude Adapter server');
|
|
53
|
+
}
|
|
54
|
+
const app = (0, fastify_1.default)({ logger: false });
|
|
55
|
+
// Health check endpoint
|
|
56
|
+
app.get('/health', async (_request, _reply) => {
|
|
57
|
+
return { status: 'ok', adapter: 'claude-adapter' };
|
|
58
|
+
});
|
|
59
|
+
// Main messages endpoint (matches Anthropic API)
|
|
60
|
+
app.post('/v1/messages', (0, handlers_1.createMessagesHandler)(config));
|
|
61
|
+
return {
|
|
62
|
+
app,
|
|
63
|
+
start: async (port) => {
|
|
64
|
+
try {
|
|
65
|
+
await app.listen({ port, host: '127.0.0.1' });
|
|
66
|
+
const address = app.server.address();
|
|
67
|
+
const actualPort = typeof address === 'object' && address ? address.port : port;
|
|
68
|
+
const url = `http://127.0.0.1:${actualPort}`;
|
|
69
|
+
return url;
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
if (err.code === 'EADDRINUSE') {
|
|
73
|
+
throw new Error(`Port ${port} is already in use. Try a different port.`);
|
|
74
|
+
}
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
stop: async (timeout = DEFAULT_SHUTDOWN_TIMEOUT) => {
|
|
79
|
+
// Create a timeout promise for force shutdown
|
|
80
|
+
let timeoutId;
|
|
81
|
+
const forceShutdown = new Promise((resolve) => {
|
|
82
|
+
timeoutId = setTimeout(() => {
|
|
83
|
+
logger_1.logger.warn('Graceful shutdown timeout exceeded, forcing close');
|
|
84
|
+
resolve();
|
|
85
|
+
}, timeout);
|
|
86
|
+
});
|
|
87
|
+
try {
|
|
88
|
+
// Race between graceful close and timeout
|
|
89
|
+
await Promise.race([app.close(), forceShutdown]);
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
if (timeoutId)
|
|
93
|
+
clearTimeout(timeoutId);
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Find an available port starting from the preferred port
|
|
100
|
+
*/
|
|
101
|
+
async function findAvailablePort(preferredPort) {
|
|
102
|
+
const net = await Promise.resolve().then(() => __importStar(require('net')));
|
|
103
|
+
return new Promise((resolve) => {
|
|
104
|
+
const server = net.createServer();
|
|
105
|
+
server.listen(preferredPort, '127.0.0.1', () => {
|
|
106
|
+
const address = server.address();
|
|
107
|
+
const port = typeof address === 'object' && address ? address.port : preferredPort;
|
|
108
|
+
server.close(() => resolve(port));
|
|
109
|
+
});
|
|
110
|
+
server.on('error', () => {
|
|
111
|
+
// Port is in use, try next port
|
|
112
|
+
resolve(findAvailablePort(preferredPort + 1));
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
// Type exports
|
|
18
|
+
__exportStar(require("./anthropic"), exports);
|
|
19
|
+
__exportStar(require("./openai"), exports);
|
|
20
|
+
__exportStar(require("./config"), exports);
|
|
21
|
+
//# sourceMappingURL=index.js.map
|