本教程方案,能 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渠道中
更新:
如果提示获取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);
}
}