-
-
Save xmaihh/b49fdfdf2649a8615be645fa51604568 to your computer and use it in GitHub Desktop.
Cloudflare Workers免费部署GCP Claude3.5 Sonnet Vertex无损转Anthropic官方版本API可用NextChat、酒馆等
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
curl https://small-frog-a298.luccid.workers.dev/v1/messages \ | |
--header "x-api-key: sk-xxxx" \ | |
--header "anthropic-version: 2023-06-01" \ | |
--header "content-type: application/json" \ | |
--data \ | |
'{ | |
"model": "claude-3-5-sonnet-20240620", | |
"max_tokens": 1024, | |
"messages": [ | |
{"role": "user", "content": "大家对于新出的 Realme GT6 怎么看"} | |
] | |
}' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const MODEL = 'claude-3-5-sonnet@20240620'; | |
const PROJECT_ID = 'static-resource-428308-c5'; | |
const CLIENT_ID = '6403879322-886qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com'; | |
const CLIENT_SECRET = 'd-FL43Q12q1MQmFpd77hHD0Ty0'; | |
const REFRESH_TOKEN = '1//7e8fdCNhXTHXPCgYIARAAGA4SNwF-L6IrJKht7WmsH-AsOiTM-XIgQKlxxd-0KOrJGqgFYZRkSvj11SOMFzFS8kWwrRwAIqhY9qAts'; | |
//这个作为API的KEY | |
const API_KEY = 'sk-pass' | |
const TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token'; | |
const LOCATIONS = ['europe-west1', 'us-east5']; | |
let currentLocationIndex = 0; | |
let requestCount = 0; | |
async function getAccessToken() { | |
try { | |
const response = await fetch(TOKEN_URL, { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/json' }, | |
body: JSON.stringify({ | |
client_id: CLIENT_ID, | |
client_secret: CLIENT_SECRET, | |
refresh_token: REFRESH_TOKEN, | |
grant_type: 'refresh_token' | |
}) | |
}); | |
if (!response.ok) { | |
const errorDetail = await response.text(); | |
console.error(`HTTP Error: ${response.status}, Error details: ${errorDetail}`); | |
throw new Error(`HTTP Error: ${response.status}, Error details: ${errorDetail}`); | |
} | |
const data = await response.json(); | |
return data.access_token; | |
} catch (error) { | |
console.error('Error obtaining access token:', error); | |
throw new Error('Error obtaining access token'); | |
} | |
} | |
function rotateLocation() { | |
requestCount++; | |
if (requestCount >= 3) { | |
requestCount = 0; | |
currentLocationIndex = (currentLocationIndex + 1) % LOCATIONS.length; | |
} | |
console.log(`Rotating to location: ${LOCATIONS[currentLocationIndex]}, request count: ${requestCount}`); | |
} | |
function getLocation() { | |
return LOCATIONS[currentLocationIndex]; | |
} | |
function constructApiUrl(location) { | |
return `https://${location}-aiplatform.googleapis.com/v1/projects/${PROJECT_ID}/locations/${location}/publishers/anthropic/models/${MODEL}:streamRawPredict`; | |
} | |
addEventListener('fetch', event => { | |
event.respondWith(handleRequest(event.request)); | |
}); | |
async function handleRequest(request) { | |
if (request.method !== 'POST' || !request.url.endsWith('/v1/messages')) { | |
return new Response(JSON.stringify({ | |
type: "error", | |
error: { | |
type: "not_found", | |
message: "The requested resource was not found." | |
} | |
}), { status: 404 }); | |
} | |
const apiKey = request.headers.get('x-api-key'); | |
if (apiKey !== API_KEY) { | |
return new Response(JSON.stringify({ | |
type: "error", | |
error: { | |
type: "permission_error", | |
message: "Invalid API key." | |
} | |
}), { status: 403 }); | |
} | |
try { | |
const accessToken = await getAccessToken(); | |
const location = getLocation(); | |
const apiUrl = constructApiUrl(location); | |
let requestBody = await request.json(); | |
requestBody.anthropic_version = "vertex-2023-10-16"; | |
delete requestBody.model; | |
console.log(`Using location: ${location}, request count: ${requestCount}`); | |
console.log('Request body:', JSON.stringify(requestBody, null, 2)); | |
console.log('API URL:', apiUrl); | |
const response = await fetch(apiUrl, { | |
method: 'POST', | |
headers: { | |
'Authorization': `Bearer ${accessToken}`, | |
'Content-Type': 'application/json; charset=utf-8' | |
}, | |
body: JSON.stringify(requestBody) | |
}); | |
if (!response.ok) { | |
const errorText = await response.text(); | |
console.error('API Error:', errorText); | |
throw new Error(`API request failed with status ${response.status}: ${errorText}`); | |
} | |
if (requestBody.stream) { | |
const { readable, writable } = new TransformStream(); | |
response.body.pipeTo(writable); | |
return new Response(readable, { | |
headers: { | |
'Content-Type': 'text/event-stream', | |
'Cache-Control': 'no-cache', | |
'Connection': 'keep-alive' | |
} | |
}); | |
} else { | |
const data = await response.json(); | |
return new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' } }); | |
} | |
rotateLocation(); | |
} catch (error) { | |
console.error('Error in request:', error); | |
return new Response(JSON.stringify({ | |
type: "error", | |
error: { | |
type: "internal_error", | |
message: "An internal error occurred. Please try again later." | |
} | |
}), { status: 500 }); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 添加GCP ID: | |
const PROJECT_ID = 'static-resource-428308-c5'; | |
const CLIENT_ID = '6403879322-886qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com'; | |
const CLIENT_SECRET = 'd-FL43Q12q1MQmFpd77hHD0Ty0'; | |
const REFRESH_TOKEN = '1//7e8fdCNhXTHXPCgYIARAAGA4SNwF-L6IrJKht7WmsH-AsOiTM-XIgQKlxxd-0KOrJGqgFYZRkSvj11SOMFzFS8kWwrRwAIqhY9qAts'; | |
// 相当于密码的功能,接口密钥 | |
const API_KEY = 'sk-f1e09e889ea64d7e88a2eb1aee64266exxxx' | |
const TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token'; | |
let tokenCache = { | |
accessToken: '', | |
expiry: 0, | |
refreshPromise: null | |
}; | |
/** | |
* | |
6/26 更新: | |
~ 修复请求量大时 accessToken 几率获取失败 | |
导致accessToken为空值问题 | |
*/ | |
async function getAccessToken(){ | |
const now = Date.now() / 1000; | |
// 如果 token 仍然有效,直接返回 | |
if (tokenCache.accessToken && now < tokenCache.expiry - 120) { | |
return tokenCache.accessToken; | |
} | |
// 如果已经有一个刷新操作在进行中,等待它完成 | |
if (tokenCache.refreshPromise) { | |
await tokenCache.refreshPromise; | |
return tokenCache.accessToken; | |
} | |
// 开始新的刷新操作 | |
tokenCache.refreshPromise = (async () => { | |
try { | |
const response = await fetch(TOKEN_URL, { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json' | |
}, | |
body: JSON.stringify({ | |
client_id: CLIENT_ID, | |
client_secret: CLIENT_SECRET, | |
refresh_token: REFRESH_TOKEN, | |
grant_type: 'refresh_token' | |
}) | |
}); | |
const data = await response.json(); | |
tokenCache.accessToken = data.access_token; | |
tokenCache.expiry = now + data.expires_in; | |
} finally { | |
tokenCache.refreshPromise = null; | |
} | |
})(); | |
await tokenCache.refreshPromise; | |
return tokenCache.accessToken; | |
} | |
// 选择区域 | |
function getLocation() { | |
const currentSeconds = new Date().getSeconds(); | |
return currentSeconds < 30 ? 'europe-west1' : 'us-east5'; | |
} | |
// 构建 API URL | |
function constructApiUrl(location, MODEL) { | |
return `https://${location}-aiplatform.googleapis.com/v1/projects/${PROJECT_ID}/locations/${location}/publishers/anthropic/models/${MODEL}:streamRawPredict`; | |
} | |
function convertModelName(modelName) { | |
// 使用更灵活的正则表达式来匹配各种可能的模型名称格式 | |
const match = modelName.match(/^(claude-\d+(?:-\w+)+)-(\d+)$/); | |
if (match) { | |
// 如果匹配成功,返回转换后的格式 | |
return `${match[1]}@${match[2]}`; | |
} | |
// 如果不匹配预期格式,返回原始名称 | |
return modelName; | |
} | |
// 处理请求 | |
async function handleRequest(request) { | |
if (request.method === 'OPTIONS') { | |
return handleOptions(); | |
} | |
// 检查x-api-key | |
const apiKey = request.headers.get('x-api-key'); | |
if (apiKey !== API_KEY) { | |
const errorResponse = new Response(JSON.stringify({ | |
type: "error", | |
error: { | |
type: "permission_error", | |
message: "Your API key does not have permission to use the specified resource." | |
} | |
}), { | |
status: 403, | |
headers: { | |
'Content-Type': 'application/json' | |
} | |
}); | |
errorResponse.headers.set('Access-Control-Allow-Origin', '*'); | |
errorResponse.headers.set('Access-Control-Allow-Methods', 'POST, GET, OPTIONS, DELETE, HEAD'); | |
errorResponse.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-api-key, anthropic-version, model'); | |
return errorResponse; | |
} | |
const accessToken = await getAccessToken(); | |
const location = getLocation(); | |
let requestBody = await request.json(); | |
let MODEL = requestBody.model; | |
MODEL = convertModelName(MODEL); | |
const apiUrl = constructApiUrl(location, MODEL); | |
// 删除原始请求中的"anthropic_version"字段(如果存在) | |
if (requestBody.anthropic_version) { | |
delete requestBody.anthropic_version; | |
} | |
// 删除原始请求中的"model"字段(如果存在) | |
if (requestBody.model) { | |
delete requestBody.model; | |
} | |
// 添加新的"anthropic_version"字段 | |
requestBody.anthropic_version = "vertex-2023-10-16"; | |
const modifiedHeaders = new Headers(request.headers); | |
modifiedHeaders.set('Authorization', `Bearer ${accessToken}`); | |
modifiedHeaders.set('Content-Type', 'application/json; charset=utf-8'); | |
modifiedHeaders.delete('anthropic-version'); | |
const modifiedRequest = new Request(apiUrl, { | |
headers: modifiedHeaders, | |
method: request.method, | |
body: JSON.stringify(requestBody), | |
redirect: 'follow' | |
}); | |
const response = await fetch(modifiedRequest); | |
const modifiedResponse = new Response(response.body, { | |
status: response.status, | |
statusText: response.statusText, | |
headers: response.headers | |
}); | |
modifiedResponse.headers.set('Access-Control-Allow-Origin', '*'); | |
modifiedResponse.headers.set('Access-Control-Allow-Methods', 'POST, GET, OPTIONS'); | |
modifiedResponse.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-api-key, anthropic-version, model'); | |
return modifiedResponse; | |
} | |
function handleOptions() { | |
const headers = new Headers(); | |
headers.set('Access-Control-Allow-Origin', '*'); | |
headers.set('Access-Control-Allow-Methods', 'POST, GET, OPTIONS'); | |
headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-api-key, anthropic-version, model'); | |
return new Response(null, { | |
status: 204, | |
headers: headers | |
}); | |
} | |
addEventListener('fetch', event => { | |
event.respondWith(handleRequest(event.request)) | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment