mirror of
https://github.com/morgan9e/llmproxy
synced 2026-04-13 16:04:06 +09:00
308 lines
8.9 KiB
JavaScript
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',
|
|
},
|
|
});
|
|
} |