Files
llmproxy/anthropic.js
2025-07-19 19:10:06 +09:00

308 lines
8.9 KiB
JavaScript

export default {
async fetch(req, env) {
return handleRequest(req, env);
}
}
const CLAUDE_API_KEY = '';
const CLAUDE_BASE_URL = 'https://api.anthropic.com/v1/messages';
const MAX_TOKENS = 4096;
function getAPIKey(headers) {
const authorization = headers.authorization;
if (authorization) {
return authorization.split(' ')[1] || CLAUDE_API_KEY;
}
return CLAUDE_API_KEY;
}
function formatStreamResponseJson(claudeResponse) {
switch (claudeResponse.type) {
case 'message_start':
return {
id: claudeResponse.message.id,
model: claudeResponse.message.model,
inputTokens: claudeResponse.message.usage.input_tokens,
};
case 'content_block_start':
case 'ping':
return null;
case 'content_block_delta':
return {
content: claudeResponse.delta.text,
};
case 'content_block_stop':
return null;
case 'message_delta':
return {
stopReason: claudeResponse.delta.stop_reason,
outputTokens: claudeResponse.usage.output_tokens,
};
case 'message_stop':
return null;
default:
return null;
}
}
function claudeToChatGPTResponse(claudeResponse, metaInfo, stream = false) {
const timestamp = Math.floor(Date.now() / 1000);
const completionTokens = metaInfo.outputTokens || 0;
const promptTokens = metaInfo.inputTokens || 0;
if (metaInfo.stopReason && stream) {
return {
id: metaInfo.id,
object: 'chat.completion.chunk',
created: timestamp,
model: metaInfo.model,
choices: [
{
index: 0,
delta: {},
logprobs: null,
finish_reason: 'stop',
},
],
usage: {
prompt_tokens: promptTokens,
completion_tokens: completionTokens,
total_tokens: promptTokens + completionTokens,
},
};
}
const content = claudeResponse.content;
const result = {
id: metaInfo.id || 'unknown',
created: timestamp,
model: metaInfo.model,
usage: {
prompt_tokens: promptTokens,
completion_tokens: completionTokens,
total_tokens: promptTokens + completionTokens,
},
choices: [
{
index: 0,
finish_reason: metaInfo.stopReason === 'end_turn' ? 'stop' : null,
},
],
};
const message = {
role: 'assistant',
content: content || '',
};
if (!stream) {
result.object = 'chat.completion';
result.choices[0].message = message;
} else {
result.object = 'chat.completion.chunk';
result.choices[0].delta = message;
}
return result;
}
async function streamJsonResponseBodies(response, writable, model) {
const reader = response.body.getReader();
const writer = writable.getWriter();
const encoder = new TextEncoder();
const decoder = new TextDecoder();
let buffer = '';
const metaInfo = {
model,
};
while (true) {
const { done, value } = await reader.read();
if (done) {
writer.write(encoder.encode('data: [DONE]'));
break;
}
const currentText = decoder.decode(value, { stream: true }); // stream: true is important here,fix the bug of incomplete line
buffer += currentText;
console.log('streamJsonResponseBodies\n', buffer);
const regex = /event:\s*.*?\s*\ndata:\s*(.*?)(?=\n\n|\s*$)/gs;
let match;
while ((match = regex.exec(buffer)) !== null) {
try {
const decodedLine = JSON.parse(match[1].trim());
const formatedChunk = formatStreamResponseJson(decodedLine);
if (formatedChunk === null) {
continue;
}
metaInfo.id = formatedChunk.id ?? metaInfo.id;
metaInfo.model = formatedChunk.model ?? metaInfo.model;
metaInfo.inputTokens =
formatedChunk.inputTokens ?? metaInfo.inputTokens;
metaInfo.outputTokens =
formatedChunk.outputTokens ?? metaInfo.outputTokens;
metaInfo.stopReason = formatedChunk.stopReason ?? metaInfo.stopReason;
const transformedLine = claudeToChatGPTResponse(
formatedChunk,
metaInfo,
true
);
writer.write(
encoder.encode(`data: ${JSON.stringify(transformedLine)}\n\n`)
);
} catch (e) {}
buffer = buffer.slice(match.index + match[0].length);
}
}
await writer.close();
}
async function handleRequest(request, env) {
if (request.method === 'GET') {
const path = new URL(request.url).pathname;
if (path === '/v1/models') {
const headers = Object.fromEntries(request.headers);
const apiKey = getAPIKey(headers);
const claudeModels = await fetch('https://api.anthropic.com/v1/models', {
method: 'GET',
headers: {
'x-api-key': apiKey,
'anthropic-version': '2023-06-01',
}
})
var claudeModelsResp = await claudeModels.json();
var claudeModelsList = claudeModelsResp.data.map(model => {
return {
id: model.id,
object: model.type,
owned_by: "Anthropic"
};
});
return new Response(
JSON.stringify({
object: 'list',
data: claudeModelsList,
}),
{
status: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': '*',
'Access-Control-Allow-Headers': '*',
'Access-Control-Allow-Credentials': 'true',
}
}
);
}
return new Response('Not Found', { status: 404 });
} else if (request.method === 'OPTIONS') {
return handleOPTIONS();
} else if (request.method === 'POST') {
const headers = Object.fromEntries(request.headers);
const apiKey = getAPIKey(headers);
if (!apiKey) {
return new Response('Not Allowed', {
status: 403,
});
}
const requestBody = await request.json();
const { model, messages, temperature, stop, stream } = requestBody;
const claudeModel = model;
const systemMessage = messages.find((message) => message.role === 'system');
const claudeRequestBody = {
model: claudeModel,
messages: messages.filter((message) => message.role !== 'system'),
temperature,
max_tokens: MAX_TOKENS,
stop_sequences: stop,
system: systemMessage?.content,
stream,
};
const claudeResponse = await fetch(CLAUDE_BASE_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': apiKey,
'anthropic-version': '2023-06-01',
},
body: JSON.stringify(claudeRequestBody),
});
console.log(request.cf.colo);
console.log({
body: claudeRequestBody,
headers: headers,
});
if (!stream) {
const claudeResponseBody = await claudeResponse.json();
var openAIResponseBody = {}
if (claudeResponseBody.type == "error") {
openAIResponseBody =
{
"error": {
"message": claudeResponseBody.error.message,
"type": claudeResponseBody.error.type,
"param": null,
"code": claudeResponseBody.error.type
}
}
} else {
const formatedResult = {
id: claudeResponseBody.id,
model: claudeResponseBody.model,
inputTokens: claudeResponseBody.usage.input_tokens,
outputTokens: claudeResponseBody.usage.output_tokens,
stopReason: claudeResponseBody.stop_reason,
};
openAIResponseBody = claudeToChatGPTResponse(
{ content: claudeResponseBody.content[0].text },
formatedResult
);
}
if (openAIResponseBody === null) {
return new Response('Error processing Claude response', {
status: 500,
});
}
return new Response(JSON.stringify(openAIResponseBody), {
status: claudeResponse.status,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': '*',
'Access-Control-Allow-Headers': '*',
'Access-Control-Allow-Credentials': 'true',
},
});
} else {
// Implement streaming logic here
const { readable, writable } = new TransformStream();
streamJsonResponseBodies(claudeResponse, writable);
return new Response(readable, {
headers: {
'Content-Type': 'text/event-stream',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': '*',
'Access-Control-Allow-Headers': '*',
'Access-Control-Allow-Credentials': 'true',
},
});
}
} else {
return new Response('Method not allowed', { status: 405 });
}
}
function handleOPTIONS() {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': '*',
'Access-Control-Allow-Headers': '*',
'Access-Control-Allow-Credentials': 'true',
},
});
}