本教程方案,能 or 不能 开机的 GCP 都能用
强烈建议使用Gmail后缀 正常账号,非Gmail后缀账号会出现非常多的问题和限制!(如 refreshToken 几个小时就过期了(实测不超过半天 ),cloud shell 权限都没有等问题,这类现象 gmail 后缀账号不会发生)此类账号通过WorkSpace批量生成,质量怎么样自己想吧,而且这种账号说嘎就嘎。Workspace拥有者,即生成你账号的人可以将你的账号无条件关停或修改密码,简单来说就是可以强登你的号,另外Workspace订阅(非老G suite)是收费的,但是可以免费用14天,所以想想你的账号能活多久全凭对方良心)
6 /26 Worker.js 脚本更新: 修复大量请求时,accessToken 小几率出现空值且无法自动刷新的问题
本教程由以下商家赞助 :
首先确保你已经开通了Sonnet的API。(可以看我历史发帖 )
然后按照以下步骤操作:
GCP 部分
点这里打开Google Cloud Shell
执行以下命令
Google cloud shell执行
gcloud auth application-default login
点击链接并完成授权
这里出现一段链接,鼠标点击一下这个链接,打开之后一路允许,随后出现下面这个界面,点一下Copy复制下面的验证码
返回Cloud Shell,粘贴验证码并确认
回去刚刚的界面鼠标右键粘贴,然后回车确认
[验证文件(密钥)] 的保存位置
接着提示验证文件保存在了这个位置
查看验证文件内容
使用cat空格加这个路径进行查看,比如我这里是
cat /tmp/tmp.ABCD/application_default_credentials.json
从里面复制出三个值保留备用,project_id就是项目ID这个也要用到
Cloudflare 部分
创建Cloudflare Workers
随后直接进入cloudflare 创建 Workers (是Worker不是Page请注意)
随后一路继续,随后点击这里的编辑按钮
进去之后根据上一节中的信息替换下面脚本中的内容,直接全部覆盖worker.js里面的内容
编辑Worker脚本
脚本内容
const MODEL = 'claude-3-5-sonnet@20240620'; const PROJECT_ID = '项目ID'; const CLIENT_ID = '填写'; const CLIENT_SECRET = '填写'; const REFRESH_TOKEN = '填写'; // 你只需要从GCP获取并设置上面四个信息 // 这个设置成你想要的密码 // 相当于你账号的密码功能,接口密钥,用于保护你的接口 const API_KEY = 'sk-pass' 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) { return `https://${location}-aiplatform.googleapis.com/v1/projects/${PROJECT_ID}/locations/${location}/publishers/anthropic/models/${MODEL}:streamRawPredict`; } // 处理请求 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(); const apiUrl = constructApiUrl(location); let requestBody = await request.json(); // 删除原始请求中的"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 }); } export default { async fetch(request) { return handleRequest(request); } }
部署Worker
随后重击 Deploy 部署到
部署之后可以测试一下结果,注意最后的API格式遵循原版Anthropic格式
文档: Create a Message - Anthropic
备注:
有效性测试方法:
{"messages":[{"role":"user","content":"hello world!"}],"stream":false,"model":"claude-3-opus-20240229","max_tokens":4000,"temperature":0.5,"top_p":1,"top_k":5}
workers的地址
域名绑定:
NextChat 配置,注意密码和worker.js中一致,图片中因为我改了密码所以和脚本里面的不一样,然后模型选择claude系列中任意模型即可(有3.5就选3.5,没有选3 Sonnet):
效果:
API测试地址,可直接填入NextChat中使用
(建议使用网页版 ):https://claude.jgk-blog.tech (终端地址) 密码:sk-key
感谢 @eggacheb 大佬的反馈,可无需修改oneapi/newapi程序,直接加入现有oneapi/newapi渠道中
在newapi里把worker的地址填到代理那里,渠道选claude,然后就可以用了
[image]
更新:
如果提示获取token阶段出现下面的错误:
ERROR: (gcloud.auth.application-default.login) PERMISSION_DENIED: Service Usage API has not been used in project <项目ID> before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/serviceusage.googleapis.com/overview?project=<项目ID> then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry. This command is authenticated as None using the credentials in /tmp/tmp.xxxx/application_default_credentials.json, specified by the [auth/credential_file_override] property. - '@type': type.googleapis.com/google.rpc.Help links: - description: Google developers console API activation url: https://console.developers.google.com/apis/api/serviceusage.googleapis.com/overview?project=<项目ID> - '@type': type.googleapis.com/google.rpc.ErrorInfo domain: googleapis.com metadata: consumer: projects/<项目ID> service: serviceusage.googleapis.com reason: SERVICE_DISABLED
去这里https://console.cloud.google.com/apis/library/serviceusage.googleapis.com?project=<改成你的项目ID>
开启 Service Usage API
@PedroZ 大佬的修改版本
根据楼主的代码进行了修改,支持多账号,支持请求不同模型
补充链接:GCP Vertex 不通过CloudShell获取 REFRESH_TOKEN + Claude多模型多账号转API - 常规话题 / 精华神贴 - LINUX DO GCP vertex为非付费账号开放claude3权限,claude自由更近一步!GCP 150刀可以用opus了 - 常规话题 / 人工智能 - LINUX DO
最后放一个版本(为共享站源码,自己调试,退休(幻想),我不再回复):
总结
/* 定义当前 Worker 提供的模型,便于区分 */ const MODEL = 'claude-3-5-sonnet@20240620'; const ACCOUNTS = [ { PROJECT_ID: '123', CLIENT_ID: '?', CLIENT_SECRET: '?', REFRESH_TOKEN: '12', } ]; const TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token'; const LOCATIONS = ['europe-west1', 'us-east5']; /* Token管理类 */ class TokenManager { constructor() { /* 初始化每个账户的token缓存 */ this.tokenCaches = ACCOUNTS.map(() => ({ accessToken: '', expiry: 0, refreshPromise: null })); } /* 获取访问令牌 */ async getAccessToken(accountIndex) { const now = Date.now() / 1000; const tokenCache = this.tokenCaches[accountIndex]; const account = ACCOUNTS[accountIndex]; /* 如果令牌有效,直接返回 */ if (tokenCache.accessToken && now < tokenCache.expiry - 120) { return tokenCache.accessToken; } /* 如果正在刷新,等待刷新完成 */ if (tokenCache.refreshPromise) { await tokenCache.refreshPromise; return tokenCache.accessToken; } /* 刷新令牌 */ tokenCache.refreshPromise = this.refreshToken(tokenCache, account, now); await tokenCache.refreshPromise; return tokenCache.accessToken; } /* 刷新令牌 */ async refreshToken(tokenCache, account, now) { try { const response = await fetch(TOKEN_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ client_id: account.CLIENT_ID, client_secret: account.CLIENT_SECRET, refresh_token: account.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; } } } /* 请求处理类 */ class RequestHandler { constructor() { this.tokenManager = new TokenManager(); /* 预计算常用值以提高性能 */ this.accountsLength = ACCOUNTS.length; this.locationsLength = LOCATIONS.length; } /* 获取账户索引 */ getAccountIndex() { const now = new Date(); const secondsInHour = now.getMinutes() * 60 + now.getSeconds(); return Math.floor((secondsInHour / 3600) * this.accountsLength) % this.accountsLength; } /* 获取位置 */ getLocation() { return LOCATIONS[Math.floor(Date.now() / 1000) % this.locationsLength]; } /* 校验 API 密钥 */ validateAPIkey(apiKey) { if (!apiKey) return false; /* if (apiKey !== API_KEY) return false; */ return true; } /* 构造API URL */ constructApiUrl(location, projectId) { return `https://${location}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${location}/publishers/anthropic/models/${MODEL}:streamRawPredict`; } /* 处理请求 */ async handleRequest(request) { if (request.method === 'OPTIONS') { return this.handleOptions(); } const apiKey = request.headers.get('x-api-key'); if (!this.validateAPIkey(apiKey)) { return new Response('Working.', { status: 200, headers: { 'Content-Type': 'text/plain' } }); } // 1. 根据时间抽取一个账号和 vertex-ai API 区域用于响应 const accountIndex = this.getAccountIndex(); const [accessToken, location] = await Promise.all([ this.tokenManager.getAccessToken(accountIndex), Promise.resolve(this.getLocation()) ]); // 2. 构造 cURL 形式的 vertex-ai API 端点地址 const apiUrl = this.constructApiUrl(location, ACCOUNTS[accountIndex].PROJECT_ID); // 3. 根据 vertex-ai 和 anthropic API的差异对用户原始请求进行调整和填充 const [requestBody, modifiedHeaders] = await Promise.all([ this.prepareRequestBody(request), this.prepareHeaders(request.headers, accessToken) ]); const modifiedRequest = new Request(apiUrl, { headers: modifiedHeaders, method: request.method, body: JSON.stringify(requestBody), redirect: 'follow' }); // 4. 发送请求给 vertex-ai,此处可以进一步对结果进行判断 // 用以实现故障转移,但 free-plan 的 cpu 时间不够用 // 可以自己尝试实现 const response = await fetch(modifiedRequest); return this.prepareResponse(response); } /* 准备 GCP vertex-ai API 请求体 */ async prepareRequestBody(request) { const requestBody = await request.json(); delete requestBody.anthropic_version; delete requestBody.model; requestBody.anthropic_version = "vertex-2023-10-16"; return requestBody; } /* 准备 GCP vertex-ai API 请求头 */ prepareHeaders(originalHeaders, accessToken) { const headers = new Headers(originalHeaders); headers.set('Authorization', `Bearer ${accessToken}`); headers.set('Content-Type', 'application/json; charset=utf-8'); headers.delete('anthropic-version'); return headers; } /* 准备响应,追加跨域许可头 */ prepareResponse(response) { const modifiedResponse = new Response(response.body, response); 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; } /* 处理OPTIONS请求 * 预取时添加 Access-Control-Allow 头,避免出现跨域问题 */ handleOptions() { return new Response(null, { status: 204, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization, x-api-key, anthropic-version, model' } }); } } /* 创建请求处理器实例 */ const handler = new RequestHandler(); /* 导出fetch函数 */ export default { async fetch(request) { return handler.handleRequest(request); } }