Spaces:
Running
Running
const express = require('express'); | |
const fetch = require('node-fetch'); | |
const zlib = require('zlib'); | |
const { promisify } = require('util'); | |
const stream = require('stream'); | |
const gunzip = promisify(zlib.gunzip); | |
const pipeline = promisify(stream.pipeline); | |
const PROJECT_ID = process.env.PROJECT_ID; | |
const CLIENT_ID = process.env.CLIENT_ID; | |
const CLIENT_SECRET = process.env.CLIENT_SECRET; | |
const REFRESH_TOKEN = process.env.REFRESH_TOKEN; | |
const API_KEY = process.env.API_KEY; | |
const TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token'; | |
let tokenCache = { | |
accessToken: '', | |
expiry: 0, | |
refreshPromise: null | |
}; | |
function logRequest(req, status, message) { | |
const timestamp = new Date().toISOString(); | |
const method = req.method; | |
const url = req.originalUrl; | |
const ip = req.ip; | |
console.log(`[${timestamp}] ${method} ${url} - Status: ${status}, IP: ${ip}, Message: ${message}`); | |
} | |
async function getAccessToken() { | |
const now = Date.now() / 1000; | |
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'; | |
} | |
function constructApiUrl(location, model) { | |
return `https://${location}-aiplatform.googleapis.com/v1/projects/${PROJECT_ID}/locations/${location}/publishers/anthropic/models/${model}:streamRawPredict`; | |
} | |
function formatModelName(model) { | |
if (model === 'claude-3-5-sonnet-20240620') { | |
return 'claude-3-5-sonnet@20240620'; | |
} | |
return model; | |
} | |
async function handleRequest(req, res) { | |
if (req.method === 'OPTIONS') { | |
handleOptions(res); | |
logRequest(req, 204, 'CORS preflight request'); | |
return; | |
} | |
const apiKey = req.headers['x-api-key']; | |
if (apiKey !== API_KEY) { | |
res.status(403).json({ | |
type: "error", | |
error: { | |
type: "permission_error", | |
message: "Your API key does not have permission to use the specified resource." | |
} | |
}); | |
logRequest(req, 403, 'Invalid API key'); | |
return; | |
} | |
const accessToken = await getAccessToken(); | |
const location = getLocation(); | |
let requestBody = req.body; | |
let model = requestBody.model || 'claude-3-5-sonnet@20240620'; | |
model = formatModelName(model); | |
const apiUrl = constructApiUrl(location, model); | |
if (requestBody.anthropic_version) { | |
delete requestBody.anthropic_version; | |
} | |
if (requestBody.model) { | |
delete requestBody.model; | |
} | |
requestBody.anthropic_version = "vertex-2023-10-16"; | |
try { | |
const response = await fetch(apiUrl, { | |
method: 'POST', | |
headers: { | |
'Authorization': `Bearer ${accessToken}`, | |
'Content-Type': 'application/json; charset=utf-8', | |
'Accept-Encoding': 'gzip, deflate' | |
}, | |
body: JSON.stringify(requestBody), | |
compress: false | |
}); | |
res.status(response.status); | |
for (const [key, value] of response.headers.entries()) { | |
if (key.toLowerCase() !== 'content-encoding' && key.toLowerCase() !== 'content-length') { | |
res.setHeader(key, value); | |
} | |
} | |
res.setHeader('Access-Control-Allow-Origin', '*'); | |
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS'); | |
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-api-key, anthropic-version, model'); | |
const contentType = response.headers.get('content-type'); | |
const contentEncoding = response.headers.get('content-encoding'); | |
if (contentType && contentType.includes('text/event-stream')) { | |
// 处理 SSE | |
res.setHeader('Content-Type', 'text/event-stream'); | |
res.setHeader('Cache-Control', 'no-cache'); | |
res.setHeader('Connection', 'keep-alive'); | |
const gunzipStream = zlib.createGunzip(); | |
const transformStream = new stream.Transform({ | |
transform(chunk, encoding, callback) { | |
this.push(chunk); | |
callback(); | |
} | |
}); | |
pipeline(response.body, gunzipStream, transformStream) | |
.then(() => { | |
console.log('Stream processing completed'); | |
}) | |
.catch((err) => { | |
console.error('Stream processing error:', err); | |
}); | |
transformStream.pipe(res); | |
} else { | |
// 非流式响应的处理 | |
const buffer = await response.buffer(); | |
let data; | |
if (contentEncoding === 'gzip') { | |
try { | |
data = await gunzip(buffer); | |
} catch (error) { | |
console.error('Gunzip error:', error); | |
throw new Error('Failed to decompress the response'); | |
} | |
} else { | |
data = buffer; | |
} | |
res.send(data); | |
} | |
logRequest(req, response.status, `Request forwarded successfully for model: ${model}`); | |
} catch (error) { | |
console.error('Request error:', error); | |
res.status(500).json({ | |
type: "error", | |
error: { | |
type: "internal_server_error", | |
message: "An unexpected error occurred while processing your request." | |
} | |
}); | |
logRequest(req, 500, `Error: ${error.message}`); | |
} | |
} | |
function handleOptions(res) { | |
res.status(204); | |
res.setHeader('Access-Control-Allow-Origin', '*'); | |
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS'); | |
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-api-key, anthropic-version, model'); | |
res.end(); | |
} | |
const app = express(); | |
app.use(express.json()); | |
// 根路由处理 | |
app.get('/', (req, res) => { | |
res.status(200).send('GCP VertexAI For Claude Proxy'); | |
}); | |
app.all('/ai/v1/messages', handleRequest); | |
const PORT = 8080; | |
app.listen(PORT, () => { | |
console.log(`Server is running on port ${PORT}`); | |
}); | |