自用的「Gemini 使用 Google 搜索」的 Cloudflare Worker 代码。

前言: 是 GCP 里的那个 Gemini,Google AI Studio 那个好像还没开放。

因为 Google AI Studio 只需要填个 Key 就能直接用,所以用 GCP Gemini 的应该没多少,不过 Vertex AI 的东西灰度测试比较早,这几天才公测的 Imagen 3 大概两个月前就已经可以在 GCP 里直接用很成熟的 API 了。

_(√ ζ ε:)_ 我用 gcp 的 Gemini 是因为 studio 的老是被道德审核打断,想着 gcp 的会不会好点,然并卵。不过可以选距离机子近的区,响应快点。

然后集成 Google 搜索并不是免费的:

问一次 $0.035,价格大概 Opus 的一半。但国内 GCP 老用户人均秽土转生大师,所以价格什么的倒也是无所谓。

对于 习惯 Google 搜索 的用户来说这个提升是巨大的,但对于觉得 百度、360 搜索 等也差不了多少的用户,NextChat 一键配置的 DuckDuckGo 搜索插件显然是更好的选择。

然后这是 Cloudflare Worker 的代码:

点击展开
const PROJECT_ID = '(这里填你自己的)';
const CLIENT_ID = '(这里填你自己的)';
const CLIENT_SECRET = '(这里填你自己的)';
const REFRESH_TOKEN = '(这里填你自己的)';

// ↑ 上述部分从 https://linux.do/t/topic/118702 这个喂饭教程里拿。

// ↓ 这个是客户端(如 NextChat)访问接口的 Key,自定。
const API_KEY = 'sk-8848decameterBOOM'

const TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token';

let accessToken = '';
let tokenExpiry = 0;

// 获取 access_token
async function getAccessToken() {
  if (Date.now() / 1000 < tokenExpiry - 60) {
    return accessToken;
  }

  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();
  accessToken = data.access_token;
  tokenExpiry = Date.now() / 1000 + data.expires_in;
  return accessToken;
}

/*
northamerica-northeast1 (蒙特利尔)
southamerica-east1 (圣保罗)
us-central1 (爱荷华)
us-east1 (南卡罗来纳)
us-east4 (北弗吉尼亚)
us-east5 (哥伦布)
us-south1 (达拉斯)
us-west1 (俄勒冈)
us-west4 (拉斯维加斯)
europe-central2 (华沙)
europe-north1 (芬兰)
europe-southwest1 (马德里)
europe-west1 (比利时)
europe-west2 (伦敦)
europe-west3 (法兰克福)
europe-west4 (荷兰)
europe-west6 (苏黎世)
europe-west8 (米兰)
europe-west9 (巴黎)
asia-east1 (台湾)
asia-east2 (香港)
asia-northeast1 (东京)
asia-northeast3 (首尔)
asia-south1 (孟买)
asia-southeast1 (新加坡)
australia-southeast1 (悉尼)
me-central1 (Doha)
me-central2 (达曼)
me-west1 (特拉维夫)
*/

const AVAILABLE_LOCATIONS = [
  'us-central1',
  'us-south1',
  'us-west1',
  'us-west4',
];

// 选择区域
function getLocation(model) {
  if (model === 'gemini-experimental') {
    return 'us-central1';
  } else {
    const randomIndex = Math.floor(Math.random() * AVAILABLE_LOCATIONS.length);
    return AVAILABLE_LOCATIONS[randomIndex];
  }
}

// 构建 API URL
function constructApiUrl(location, model, isStream) {
  const _type = isStream ? 'streamGenerateContent' : 'generateContent';
  return `https://${location}-aiplatform.googleapis.com/v1/projects/${PROJECT_ID}/locations/${location}/publishers/google/models/${model}:${_type}`;
}

// 处理请求
async function handleRequest(request) {
  if (request.method === 'OPTIONS') {
    return handleOptions();
  }
  // 检查API_KEY
  const url = new URL(request.url);
  const path = url.pathname;

  const apiKey = url.searchParams.get('key');
  const authHeader = request.headers.get('Authorization');
  const expectedAuthHeader = `Bearer ${API_KEY}`;

  let type = 'generateContent';
  // 找到路径中的冒号后的内容
  const colonIndex = path.lastIndexOf(':');
  if (colonIndex === -1) {
    // 如果找不到冒号,则默认为 streamGenerateContent (NextChat)
    type = 'streamGenerateContent';
  } else {   
    // 截取冒号后的内容
    const typeString = path.substring(colonIndex + 1);

    // 判断类型
    if (typeString.includes('streamGenerateContent')) {
      type = 'streamGenerateContent';
    }
  }
  const isStream = type === 'streamGenerateContent';

  if (apiKey && apiKey === API_KEY){
      // ai-studio
  } else if (authHeader && authHeader === expectedAuthHeader) {
      // vertex-ai
  } else {
    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();

  // 从 URL 中获取模型名称
  let model = url.searchParams.get('model');
  if (!model) {
    model = 'gemini-1.5-pro'; // 默认模型
  }

  const location = getLocation(model);
  const apiUrl = constructApiUrl(location, model, isStream);

  let requestBody = await request.json();

  // 判断 URL 中是否包含 'search' 键
  if (url.searchParams.has('search')) {
    function findLastUserInput(contents) {
      for (let i = contents.length - 1; i >= 0; i--) {
        const content = contents[i];
        if (content.role === 'user' && content.parts && content.parts.length > 0) {
          return content.parts[0].text;
        }
      }
      return null;
    }

    // 查找最后的用户输入
    const lastUserInput = findLastUserInput(requestBody.contents);

    // 如果找到最后的用户输入并且不是以星号开始,则添加 Google 搜索检索工具
    if (lastUserInput && !lastUserInput.startsWith('*')) {
      requestBody.tools = [{
        "googleSearchRetrieval": {}
      }];
    } else {
      // 移除行首星号。懒得写例外了自己注意规则就好
      const lastContent = requestBody.contents[requestBody.contents.length - 1];
      if (lastContent.role === 'user') {
        lastContent.parts[0].text = lastContent.parts[0].text.substring(1).trim();
      }
    }
  }

  // TODO:: Change safetySettings

  /**
   *     "safetySettings": [
        {
            "category": "HARM_CATEGORY_HATE_SPEECH",
            "threshold": "BLOCK_ONLY_HIGH"
        },
        {
            "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
            "threshold": "BLOCK_ONLY_HIGH"
        },
        {
            "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
            "threshold": "BLOCK_ONLY_HIGH"
        },
        {
            "category": "HARM_CATEGORY_HARASSMENT",
            "threshold": "BLOCK_ONLY_HIGH"
        }
    ]
   * 
   */

  const modifiedHeaders = new Headers(request.headers);
  modifiedHeaders.set('Authorization', `Bearer ${accessToken}`);
  modifiedHeaders.set('Content-Type', 'application/json; charset=utf-8');

  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);
  }
}

顶部 GCP 验证部分佬已经写了完整的喂饭教程,就不赘述了:


使用说明:

① NextChat 模型服务商 那里选的是 Google 不是 OpenAI,Key 填的是你自己设置的 API_KEY。需要统一 OpenAI 的话 One/New API 自转。

② 当 URL 包含 search 键时默认开启搜索。例如:https://gcp-gemini.workder.com?model=gemini-1.5-pro-002&search,没有此参数则不开启搜索。对话 用 * 开头的话 则本次请求屏蔽搜索使用常态 Gemini。

③ 由于 NextChat 的 Gemini 渠道 POST 的请求不包含模型参数 (NextChat 官方也是通过 URL 判断) 所以自定义模型需要在 URL 中带上 model= 参数。不指定的话默认使用 gemini-1.5-pro

  • 支持的 models 包含不限于

    • gemini-1.5-pro
    • gemini-experimental
    • gemini-1.5-flash
    • gemini-pro
    • gemini-pro-vision
    • ……
    • gemini-1.5-pro-001
    • gemini-1.5-pro-002
    • gemini-1.5-pro-preview-0514
    • gemini-1.5-pro-preview-0409
    • gemini-1.5-flash-001
    • gemini-1.5-flash-002
    • gemini-1.5-flash-preview-0514
    • gemini-1.0-pro-vision-001

……

:bili_040: :bili_040: :bili_040:
比起 Google AI Studio 相当麻烦,但我想用 Google 聚合搜索。以上。


:bili_040: :bili_040: :bili_040:
嗯?又顶上来了,那就补张图。实时搜索的信息聚合 Very 方便。

105 Likes

感谢大佬分享

好好Mark一下

:cow:的,希望多分享一些这样的干活。

有点厉害,不会C,也要支持~~

请教下NextChat设置里面的API 版本应该填什么?我现在留空,NextChat 里面测试得到这个错:

{
  "error": {
    "message": "The model `gemini-1.5-pro` does not exist or you do not have access to it.",
    "type": "invalid_request_error",
    "param": null,
    "code": "model_not_found"
  }
}

我默认 v1,随便填,因为 Google 模型请求里并不包含这个值。你这个报错看起来像 One API 的?是的话去 One API 渠道里加上 gemini-1.5-pro。。

谢谢,不过我没有使用One API, 我的next chat是直接作为一个cloudflare pages部署的,环境变量本来设置的是访问azure的,好奇怪

主要设置这两个 (下面那个不适用于 Vertex AI 设什么都无所谓没作用的),然后 F12 去看 POST 和 响应。你上面那个报错不是 Worker 的预设回复不知道是哪里的。如果 Worker 出错一般是提示空请求之类不会有模型不存在之类提示。

奇怪,为什么我的GCP gemini api还能用


tieba_025我也用上了

1 Like

还有个好处,hk 的小鸡可以直接用

是的,不过它的配额是分散每个区的不像 Studio 是全区归一的 gcp 的放沉浸式并发要特别处理,单区配额不是很高扛不住高并发 但日用随便刷管够

翻译我一般用cohere

cohere 始终矮御三家一头,我免费账户一个月也只有 1000 请求额度。gemini-flash 1 分钟就有 2000 次配额,加上本身就是最快的模型之一,全页翻译刷刷就翻完了

:tieba_025: :tieba_025:
当然是嫖pro版key,志扬的站也可以

1 Like