前言: 是 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
……
比起 Google AI Studio 相当麻烦,但我想用 Google 聚合搜索。以上。
嗯?又顶上来了,那就补张图。实时搜索的信息聚合 Very 方便。