[别用简单密码] [全功能+Cloudflare Workers] 最简方法免费部署GCP Claude3.5 Sonnet Vertex无损转Anthropic官方版本API + 全套教程 + 可用NextChat、酒馆等

本教程方案,能 or 不能 开机的 GCP 都能用

强烈建议使用Gmail后缀正常账号,非Gmail后缀账号会出现非常多的问题和限制!(如 refreshToken 几个小时就过期了(实测不超过半天),cloud shell 权限都没有等问题,这类现象 gmail 后缀账号不会发生)此类账号通过WorkSpace批量生成,质量怎么样自己想吧,而且这种账号说嘎就嘎。Workspace拥有者,即生成你账号的人可以将你的账号无条件关停或修改密码,简单来说就是可以强登你的号,另外Workspace订阅(非老G suite)是收费的,但是可以免费用14天,所以想想你的账号能活多久全凭对方良心)

6 /26 Worker.js 脚本更新:
修复大量请求时,accessToken 小几率出现空值且无法自动刷新的问题

本教程由以下商家赞助
Cloudflare

Google Cloud

首先确保你已经开通了Sonnet的API。(可以看我历史发帖)

然后按照以下步骤操作:


GCP 部分

  1. 点这里打开Google Cloud Shell

  2. 执行以下命令
    Google cloud shell执行

    gcloud auth application-default login
    

  3. 点击链接并完成授权
    这里出现一段链接,鼠标点击一下这个链接,打开之后一路允许,随后出现下面这个界面,点一下Copy复制下面的验证码

  4. 返回Cloud Shell,粘贴验证码并确认
    回去刚刚的界面鼠标右键粘贴,然后回车确认

  5. [验证文件(密钥)] 的保存位置
    接着提示验证文件保存在了这个位置
    image

  6. 查看验证文件内容
    使用cat空格加这个路径进行查看,比如我这里是

    cat /tmp/tmp.ABCD/application_default_credentials.json
    

    从里面复制出三个值保留备用,project_id就是项目ID这个也要用到


Cloudflare 部分

  1. 创建Cloudflare Workers
    随后直接进入cloudflare 创建 Workers (是Worker不是Page请注意)


    随后一路继续,随后点击这里的编辑按钮

    进去之后根据上一节中的信息替换下面脚本中的内容,直接全部覆盖worker.js里面的内容

  2. 编辑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);
  }
}
  1. 部署Worker
    随后重击 Deploy 部署到 Cloudflare
    image
    部署之后可以测试一下结果,注意最后的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);
  }
}

418 Likes

感谢分享

15 Likes

好啊,赞 :tieba_013:

8 Likes

太强了!!

8 Likes

强啊佬

7 Likes

感谢,用上了 :tieba_013:

8 Likes

感谢分享:smiling_face_with_three_hearts:

7 Likes

先mark

6 Likes


全部功能测试可用,可调temperature等参数

8 Likes

大佬牛批

4 Likes

想要酒馆的一键 :joy:

4 Likes

在newapi里把worker的地址填到代理那里,渠道选claude,然后就可以用了

7 Likes

佬,好强

4 Likes

模型名不能直接设置为claude-3.5-sonnet-20240620吗?

4 Likes

感谢分享

5 Likes

已认可!

4 Likes

没试诶,你试试,不行就重定义模型名字了

4 Likes

名字随便选都行,因为worker.js里面是忽略客户端请求的模型名字的

5 Likes

我去找一下管理大佬

4 Likes

找管理就行

3 Likes