使用cf worker,sub2api一个key调用全部模型

2026-04-29 09:392阅读0评论SEO资讯
  • 内容介绍
  • 文章标签
  • 相关推荐
问题描述:

sub2api的性能很好,但是路由规则比较死板,一个key只能对应一种协议,我喜欢在cc里面同时用gpt和glm,所以搓了了一个 cf worker,进行自动路由,一个Key可以调用多个模型。

使用方法:
Screenshot| 100%2560×4686 552 KB

/** * Cloudflare Worker: model-based API key router for sub2api. * * Features: * - Authenticate clients with a dedicated Worker API key. * - Select upstream sub2api key by request model. * - Stream upstream responses through without buffering. * - No retries. * * Required env vars: * - WORKER_API_KEY: client-facing secret for this Worker. * - SUB2API_BASE_URL: e.g. https://sub2api.example.com * - MODEL_KEY_RULES_JSON: JSON array like: * [ * {"pattern":"gpt-*","key_env":"SUB2API_KEY_OPENAI"}, * {"pattern":"glm-*","key_env":"SUB2API_KEY_GLM"} * ] * - DEFAULT_KEY_ENV: env var name used for endpoints without model * (e.g. "SUB2API_KEY_OPENAI") * * Each key_env in MODEL_KEY_RULES_JSON must exist as an env var containing * a real sub2api API key. Example: * - SUB2API_KEY_OPENAI=sk-xxx * - SUB2API_KEY_GLM=sk-yyy */ const HOP_BY_HOP_HEADERS = new Set([ 'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailer', 'transfer-encoding', 'upgrade', ]); export default { async fetch(request, env) { if (!isAuthorized(request, env.WORKER_API_KEY)) { return jsonError(401, 'unauthorized', 'Invalid worker API key'); } if (!env.SUB2API_BASE_URL || !env.MODEL_KEY_RULES_JSON) { return jsonError(500, 'server_error', 'Worker is not configured'); } const rules = parseRules(env.MODEL_KEY_RULES_JSON); if (rules.length === 0) { return jsonError(500, 'server_error', 'No model routing rules configured (check MODEL_KEY_RULES_JSON format/type)'); } const routing = await resolveRouting(request, env, rules); if (routing.errorResponse) return routing.errorResponse; const upstreamUrl = buildUpstreamURL(request.url, env.SUB2API_BASE_URL); const upstreamHeaders = buildUpstreamHeaders(request.headers, routing.upstreamApiKey); const upstreamBody = shouldForwardBody(request.method) ? request.body : null; const websocketUpgrade = isWebSocketUpgrade(request); const upstreamResp = await fetch(upstreamUrl, { method: request.method, headers: upstreamHeaders, body: upstreamBody, redirect: 'manual', }); if (websocketUpgrade) { if (upstreamResp.status === 101 && upstreamResp.webSocket) { return new Response(null, { status: 101, webSocket: upstreamResp.webSocket, headers: sanitizeResponseHeaders(upstreamResp.headers), }); } return jsonError(502, 'websocket_upgrade_failed', 'Upstream did not accept websocket upgrade'); } const responseHeaders = sanitizeResponseHeaders(upstreamResp.headers); return new Response(upstreamResp.body, { status: upstreamResp.status, headers: responseHeaders, }); }, }; async function resolveRouting(request, env, rules) { const contentType = (request.headers.get('content-type') || '').toLowerCase(); const defaultKeyEnvName = String(env.DEFAULT_KEY_ENV || '').trim(); // Prefer model-based routing when body is JSON and has model. if (isJSONContentType(contentType) && shouldForwardBody(request.method)) { let payload; try { payload = await request.clone().json(); } catch { return { errorResponse: jsonError(400, 'invalid_json', 'Request body must be valid JSON'), }; } const model = extractModel(payload); if (model) { const matchedRule = matchRule(model, rules); if (!matchedRule) { return { errorResponse: jsonError(400, 'model_not_matched', `Model not matched by any rule: ${model}`), }; } const upstreamApiKey = env[matchedRule.key_env]; if (!upstreamApiKey) { return { errorResponse: jsonError(500, 'server_error', `Missing upstream key env: ${matchedRule.key_env}`), }; } return { upstreamApiKey }; } } // Endpoints without model (e.g. list models) use DEFAULT_KEY_ENV. if (!defaultKeyEnvName) { return { errorResponse: jsonError(400, 'missing_default_key_env', 'DEFAULT_KEY_ENV is required for non-model endpoints'), }; } const upstreamApiKey = env[defaultKeyEnvName]; if (!upstreamApiKey) { return { errorResponse: jsonError(500, 'server_error', `Missing upstream key env: ${defaultKeyEnvName}`), }; } return { upstreamApiKey }; } function isAuthorized(request, workerApiKey) { if (!workerApiKey) return false; const auth = request.headers.get('authorization') || ''; const token = auth.startsWith('Bearer ') ? auth.slice('Bearer '.length).trim() : ''; return token !== '' && token === workerApiKey; } function parseRules(raw) { let parsed = raw; // Cloudflare variable "JSON" type may already provide object/array values. if (typeof raw === 'string') { const text = raw.trim(); if (text === '') return []; try { parsed = JSON.parse(text); } catch { return []; } } // Support optional object wrapper: { rules: [...] } if (!Array.isArray(parsed) && parsed && typeof parsed === 'object' && Array.isArray(parsed.rules)) { parsed = parsed.rules; } if (!Array.isArray(parsed)) return []; return parsed .map((r) => ({ pattern: String(r?.pattern || '').trim(), key_env: String(r?.key_env || '').trim(), })) .filter((r) => r.pattern !== '' && r.key_env !== ''); } function extractModel(payload) { if (!payload || typeof payload !== 'object') return ''; const model = payload.model; return typeof model === 'string' ? model.trim() : ''; } function matchRule(model, rules) { const modelLower = model.toLowerCase(); // 1) Exact match first for (const rule of rules) { const p = rule.pattern.toLowerCase(); if (!p.includes('*') && modelLower === p) { return rule; } } // 2) Prefix wildcard match (e.g. gpt-*) for (const rule of rules) { const p = rule.pattern.toLowerCase(); if (p.endsWith('*')) { const prefix = p.slice(0, -1); if (prefix && modelLower.startsWith(prefix)) { return rule; } } } return null; } function shouldForwardBody(method) { const m = (method || '').toUpperCase(); return !(m === 'GET' || m === 'HEAD'); } function isWebSocketUpgrade(request) { const upgrade = request.headers.get('upgrade') || ''; return upgrade.toLowerCase() === 'websocket'; } function isJSONContentType(contentType) { return contentType.includes('application/json'); } function buildUpstreamURL(requestURL, upstreamBaseURL) { const incoming = new URL(requestURL); const base = new URL(upstreamBaseURL); base.pathname = joinPath(base.pathname, incoming.pathname); base.search = incoming.search; return base.toString(); } function joinPath(basePath, incomingPath) { const left = (basePath || '').replace(/\/+$/, ''); const right = (incomingPath || '').replace(/^\/+/, ''); if (!left) return `/${right}`; if (!right) return left; return `${left}/${right}`; } function buildUpstreamHeaders(incomingHeaders, upstreamApiKey) { const h = new Headers(); for (const [k, v] of incomingHeaders.entries()) { const lower = k.toLowerCase(); if (HOP_BY_HOP_HEADERS.has(lower)) continue; if (lower === 'authorization') continue; if (lower === 'host') continue; h.set(k, v); } h.set('authorization', `Bearer ${upstreamApiKey}`); return h; } function sanitizeResponseHeaders(upstreamHeaders) { const out = new Headers(); for (const [k, v] of upstreamHeaders.entries()) { if (!HOP_BY_HOP_HEADERS.has(k.toLowerCase())) { out.set(k, v); } } return out; } function jsonError(status, code, message) { return new Response(JSON.stringify({ error: { code, message } }), { status, headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store', }, }); } 网友解答:


--【壹】--:

sub2api的性能很好,但是路由规则比较死板,一个key只能对应一种协议,我喜欢在cc里面同时用gpt和glm,所以搓了了一个 cf worker,进行自动路由,一个Key可以调用多个模型。

使用方法:
Screenshot| 100%2560×4686 552 KB

/** * Cloudflare Worker: model-based API key router for sub2api. * * Features: * - Authenticate clients with a dedicated Worker API key. * - Select upstream sub2api key by request model. * - Stream upstream responses through without buffering. * - No retries. * * Required env vars: * - WORKER_API_KEY: client-facing secret for this Worker. * - SUB2API_BASE_URL: e.g. https://sub2api.example.com * - MODEL_KEY_RULES_JSON: JSON array like: * [ * {"pattern":"gpt-*","key_env":"SUB2API_KEY_OPENAI"}, * {"pattern":"glm-*","key_env":"SUB2API_KEY_GLM"} * ] * - DEFAULT_KEY_ENV: env var name used for endpoints without model * (e.g. "SUB2API_KEY_OPENAI") * * Each key_env in MODEL_KEY_RULES_JSON must exist as an env var containing * a real sub2api API key. Example: * - SUB2API_KEY_OPENAI=sk-xxx * - SUB2API_KEY_GLM=sk-yyy */ const HOP_BY_HOP_HEADERS = new Set([ 'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailer', 'transfer-encoding', 'upgrade', ]); export default { async fetch(request, env) { if (!isAuthorized(request, env.WORKER_API_KEY)) { return jsonError(401, 'unauthorized', 'Invalid worker API key'); } if (!env.SUB2API_BASE_URL || !env.MODEL_KEY_RULES_JSON) { return jsonError(500, 'server_error', 'Worker is not configured'); } const rules = parseRules(env.MODEL_KEY_RULES_JSON); if (rules.length === 0) { return jsonError(500, 'server_error', 'No model routing rules configured (check MODEL_KEY_RULES_JSON format/type)'); } const routing = await resolveRouting(request, env, rules); if (routing.errorResponse) return routing.errorResponse; const upstreamUrl = buildUpstreamURL(request.url, env.SUB2API_BASE_URL); const upstreamHeaders = buildUpstreamHeaders(request.headers, routing.upstreamApiKey); const upstreamBody = shouldForwardBody(request.method) ? request.body : null; const websocketUpgrade = isWebSocketUpgrade(request); const upstreamResp = await fetch(upstreamUrl, { method: request.method, headers: upstreamHeaders, body: upstreamBody, redirect: 'manual', }); if (websocketUpgrade) { if (upstreamResp.status === 101 && upstreamResp.webSocket) { return new Response(null, { status: 101, webSocket: upstreamResp.webSocket, headers: sanitizeResponseHeaders(upstreamResp.headers), }); } return jsonError(502, 'websocket_upgrade_failed', 'Upstream did not accept websocket upgrade'); } const responseHeaders = sanitizeResponseHeaders(upstreamResp.headers); return new Response(upstreamResp.body, { status: upstreamResp.status, headers: responseHeaders, }); }, }; async function resolveRouting(request, env, rules) { const contentType = (request.headers.get('content-type') || '').toLowerCase(); const defaultKeyEnvName = String(env.DEFAULT_KEY_ENV || '').trim(); // Prefer model-based routing when body is JSON and has model. if (isJSONContentType(contentType) && shouldForwardBody(request.method)) { let payload; try { payload = await request.clone().json(); } catch { return { errorResponse: jsonError(400, 'invalid_json', 'Request body must be valid JSON'), }; } const model = extractModel(payload); if (model) { const matchedRule = matchRule(model, rules); if (!matchedRule) { return { errorResponse: jsonError(400, 'model_not_matched', `Model not matched by any rule: ${model}`), }; } const upstreamApiKey = env[matchedRule.key_env]; if (!upstreamApiKey) { return { errorResponse: jsonError(500, 'server_error', `Missing upstream key env: ${matchedRule.key_env}`), }; } return { upstreamApiKey }; } } // Endpoints without model (e.g. list models) use DEFAULT_KEY_ENV. if (!defaultKeyEnvName) { return { errorResponse: jsonError(400, 'missing_default_key_env', 'DEFAULT_KEY_ENV is required for non-model endpoints'), }; } const upstreamApiKey = env[defaultKeyEnvName]; if (!upstreamApiKey) { return { errorResponse: jsonError(500, 'server_error', `Missing upstream key env: ${defaultKeyEnvName}`), }; } return { upstreamApiKey }; } function isAuthorized(request, workerApiKey) { if (!workerApiKey) return false; const auth = request.headers.get('authorization') || ''; const token = auth.startsWith('Bearer ') ? auth.slice('Bearer '.length).trim() : ''; return token !== '' && token === workerApiKey; } function parseRules(raw) { let parsed = raw; // Cloudflare variable "JSON" type may already provide object/array values. if (typeof raw === 'string') { const text = raw.trim(); if (text === '') return []; try { parsed = JSON.parse(text); } catch { return []; } } // Support optional object wrapper: { rules: [...] } if (!Array.isArray(parsed) && parsed && typeof parsed === 'object' && Array.isArray(parsed.rules)) { parsed = parsed.rules; } if (!Array.isArray(parsed)) return []; return parsed .map((r) => ({ pattern: String(r?.pattern || '').trim(), key_env: String(r?.key_env || '').trim(), })) .filter((r) => r.pattern !== '' && r.key_env !== ''); } function extractModel(payload) { if (!payload || typeof payload !== 'object') return ''; const model = payload.model; return typeof model === 'string' ? model.trim() : ''; } function matchRule(model, rules) { const modelLower = model.toLowerCase(); // 1) Exact match first for (const rule of rules) { const p = rule.pattern.toLowerCase(); if (!p.includes('*') && modelLower === p) { return rule; } } // 2) Prefix wildcard match (e.g. gpt-*) for (const rule of rules) { const p = rule.pattern.toLowerCase(); if (p.endsWith('*')) { const prefix = p.slice(0, -1); if (prefix && modelLower.startsWith(prefix)) { return rule; } } } return null; } function shouldForwardBody(method) { const m = (method || '').toUpperCase(); return !(m === 'GET' || m === 'HEAD'); } function isWebSocketUpgrade(request) { const upgrade = request.headers.get('upgrade') || ''; return upgrade.toLowerCase() === 'websocket'; } function isJSONContentType(contentType) { return contentType.includes('application/json'); } function buildUpstreamURL(requestURL, upstreamBaseURL) { const incoming = new URL(requestURL); const base = new URL(upstreamBaseURL); base.pathname = joinPath(base.pathname, incoming.pathname); base.search = incoming.search; return base.toString(); } function joinPath(basePath, incomingPath) { const left = (basePath || '').replace(/\/+$/, ''); const right = (incomingPath || '').replace(/^\/+/, ''); if (!left) return `/${right}`; if (!right) return left; return `${left}/${right}`; } function buildUpstreamHeaders(incomingHeaders, upstreamApiKey) { const h = new Headers(); for (const [k, v] of incomingHeaders.entries()) { const lower = k.toLowerCase(); if (HOP_BY_HOP_HEADERS.has(lower)) continue; if (lower === 'authorization') continue; if (lower === 'host') continue; h.set(k, v); } h.set('authorization', `Bearer ${upstreamApiKey}`); return h; } function sanitizeResponseHeaders(upstreamHeaders) { const out = new Headers(); for (const [k, v] of upstreamHeaders.entries()) { if (!HOP_BY_HOP_HEADERS.has(k.toLowerCase())) { out.set(k, v); } } return out; } function jsonError(status, code, message) { return new Response(JSON.stringify({ error: { code, message } }), { status, headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store', }, }); }

标签:人工智能
问题描述:

sub2api的性能很好,但是路由规则比较死板,一个key只能对应一种协议,我喜欢在cc里面同时用gpt和glm,所以搓了了一个 cf worker,进行自动路由,一个Key可以调用多个模型。

使用方法:
Screenshot| 100%2560×4686 552 KB

/** * Cloudflare Worker: model-based API key router for sub2api. * * Features: * - Authenticate clients with a dedicated Worker API key. * - Select upstream sub2api key by request model. * - Stream upstream responses through without buffering. * - No retries. * * Required env vars: * - WORKER_API_KEY: client-facing secret for this Worker. * - SUB2API_BASE_URL: e.g. https://sub2api.example.com * - MODEL_KEY_RULES_JSON: JSON array like: * [ * {"pattern":"gpt-*","key_env":"SUB2API_KEY_OPENAI"}, * {"pattern":"glm-*","key_env":"SUB2API_KEY_GLM"} * ] * - DEFAULT_KEY_ENV: env var name used for endpoints without model * (e.g. "SUB2API_KEY_OPENAI") * * Each key_env in MODEL_KEY_RULES_JSON must exist as an env var containing * a real sub2api API key. Example: * - SUB2API_KEY_OPENAI=sk-xxx * - SUB2API_KEY_GLM=sk-yyy */ const HOP_BY_HOP_HEADERS = new Set([ 'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailer', 'transfer-encoding', 'upgrade', ]); export default { async fetch(request, env) { if (!isAuthorized(request, env.WORKER_API_KEY)) { return jsonError(401, 'unauthorized', 'Invalid worker API key'); } if (!env.SUB2API_BASE_URL || !env.MODEL_KEY_RULES_JSON) { return jsonError(500, 'server_error', 'Worker is not configured'); } const rules = parseRules(env.MODEL_KEY_RULES_JSON); if (rules.length === 0) { return jsonError(500, 'server_error', 'No model routing rules configured (check MODEL_KEY_RULES_JSON format/type)'); } const routing = await resolveRouting(request, env, rules); if (routing.errorResponse) return routing.errorResponse; const upstreamUrl = buildUpstreamURL(request.url, env.SUB2API_BASE_URL); const upstreamHeaders = buildUpstreamHeaders(request.headers, routing.upstreamApiKey); const upstreamBody = shouldForwardBody(request.method) ? request.body : null; const websocketUpgrade = isWebSocketUpgrade(request); const upstreamResp = await fetch(upstreamUrl, { method: request.method, headers: upstreamHeaders, body: upstreamBody, redirect: 'manual', }); if (websocketUpgrade) { if (upstreamResp.status === 101 && upstreamResp.webSocket) { return new Response(null, { status: 101, webSocket: upstreamResp.webSocket, headers: sanitizeResponseHeaders(upstreamResp.headers), }); } return jsonError(502, 'websocket_upgrade_failed', 'Upstream did not accept websocket upgrade'); } const responseHeaders = sanitizeResponseHeaders(upstreamResp.headers); return new Response(upstreamResp.body, { status: upstreamResp.status, headers: responseHeaders, }); }, }; async function resolveRouting(request, env, rules) { const contentType = (request.headers.get('content-type') || '').toLowerCase(); const defaultKeyEnvName = String(env.DEFAULT_KEY_ENV || '').trim(); // Prefer model-based routing when body is JSON and has model. if (isJSONContentType(contentType) && shouldForwardBody(request.method)) { let payload; try { payload = await request.clone().json(); } catch { return { errorResponse: jsonError(400, 'invalid_json', 'Request body must be valid JSON'), }; } const model = extractModel(payload); if (model) { const matchedRule = matchRule(model, rules); if (!matchedRule) { return { errorResponse: jsonError(400, 'model_not_matched', `Model not matched by any rule: ${model}`), }; } const upstreamApiKey = env[matchedRule.key_env]; if (!upstreamApiKey) { return { errorResponse: jsonError(500, 'server_error', `Missing upstream key env: ${matchedRule.key_env}`), }; } return { upstreamApiKey }; } } // Endpoints without model (e.g. list models) use DEFAULT_KEY_ENV. if (!defaultKeyEnvName) { return { errorResponse: jsonError(400, 'missing_default_key_env', 'DEFAULT_KEY_ENV is required for non-model endpoints'), }; } const upstreamApiKey = env[defaultKeyEnvName]; if (!upstreamApiKey) { return { errorResponse: jsonError(500, 'server_error', `Missing upstream key env: ${defaultKeyEnvName}`), }; } return { upstreamApiKey }; } function isAuthorized(request, workerApiKey) { if (!workerApiKey) return false; const auth = request.headers.get('authorization') || ''; const token = auth.startsWith('Bearer ') ? auth.slice('Bearer '.length).trim() : ''; return token !== '' && token === workerApiKey; } function parseRules(raw) { let parsed = raw; // Cloudflare variable "JSON" type may already provide object/array values. if (typeof raw === 'string') { const text = raw.trim(); if (text === '') return []; try { parsed = JSON.parse(text); } catch { return []; } } // Support optional object wrapper: { rules: [...] } if (!Array.isArray(parsed) && parsed && typeof parsed === 'object' && Array.isArray(parsed.rules)) { parsed = parsed.rules; } if (!Array.isArray(parsed)) return []; return parsed .map((r) => ({ pattern: String(r?.pattern || '').trim(), key_env: String(r?.key_env || '').trim(), })) .filter((r) => r.pattern !== '' && r.key_env !== ''); } function extractModel(payload) { if (!payload || typeof payload !== 'object') return ''; const model = payload.model; return typeof model === 'string' ? model.trim() : ''; } function matchRule(model, rules) { const modelLower = model.toLowerCase(); // 1) Exact match first for (const rule of rules) { const p = rule.pattern.toLowerCase(); if (!p.includes('*') && modelLower === p) { return rule; } } // 2) Prefix wildcard match (e.g. gpt-*) for (const rule of rules) { const p = rule.pattern.toLowerCase(); if (p.endsWith('*')) { const prefix = p.slice(0, -1); if (prefix && modelLower.startsWith(prefix)) { return rule; } } } return null; } function shouldForwardBody(method) { const m = (method || '').toUpperCase(); return !(m === 'GET' || m === 'HEAD'); } function isWebSocketUpgrade(request) { const upgrade = request.headers.get('upgrade') || ''; return upgrade.toLowerCase() === 'websocket'; } function isJSONContentType(contentType) { return contentType.includes('application/json'); } function buildUpstreamURL(requestURL, upstreamBaseURL) { const incoming = new URL(requestURL); const base = new URL(upstreamBaseURL); base.pathname = joinPath(base.pathname, incoming.pathname); base.search = incoming.search; return base.toString(); } function joinPath(basePath, incomingPath) { const left = (basePath || '').replace(/\/+$/, ''); const right = (incomingPath || '').replace(/^\/+/, ''); if (!left) return `/${right}`; if (!right) return left; return `${left}/${right}`; } function buildUpstreamHeaders(incomingHeaders, upstreamApiKey) { const h = new Headers(); for (const [k, v] of incomingHeaders.entries()) { const lower = k.toLowerCase(); if (HOP_BY_HOP_HEADERS.has(lower)) continue; if (lower === 'authorization') continue; if (lower === 'host') continue; h.set(k, v); } h.set('authorization', `Bearer ${upstreamApiKey}`); return h; } function sanitizeResponseHeaders(upstreamHeaders) { const out = new Headers(); for (const [k, v] of upstreamHeaders.entries()) { if (!HOP_BY_HOP_HEADERS.has(k.toLowerCase())) { out.set(k, v); } } return out; } function jsonError(status, code, message) { return new Response(JSON.stringify({ error: { code, message } }), { status, headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store', }, }); } 网友解答:


--【壹】--:

sub2api的性能很好,但是路由规则比较死板,一个key只能对应一种协议,我喜欢在cc里面同时用gpt和glm,所以搓了了一个 cf worker,进行自动路由,一个Key可以调用多个模型。

使用方法:
Screenshot| 100%2560×4686 552 KB

/** * Cloudflare Worker: model-based API key router for sub2api. * * Features: * - Authenticate clients with a dedicated Worker API key. * - Select upstream sub2api key by request model. * - Stream upstream responses through without buffering. * - No retries. * * Required env vars: * - WORKER_API_KEY: client-facing secret for this Worker. * - SUB2API_BASE_URL: e.g. https://sub2api.example.com * - MODEL_KEY_RULES_JSON: JSON array like: * [ * {"pattern":"gpt-*","key_env":"SUB2API_KEY_OPENAI"}, * {"pattern":"glm-*","key_env":"SUB2API_KEY_GLM"} * ] * - DEFAULT_KEY_ENV: env var name used for endpoints without model * (e.g. "SUB2API_KEY_OPENAI") * * Each key_env in MODEL_KEY_RULES_JSON must exist as an env var containing * a real sub2api API key. Example: * - SUB2API_KEY_OPENAI=sk-xxx * - SUB2API_KEY_GLM=sk-yyy */ const HOP_BY_HOP_HEADERS = new Set([ 'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailer', 'transfer-encoding', 'upgrade', ]); export default { async fetch(request, env) { if (!isAuthorized(request, env.WORKER_API_KEY)) { return jsonError(401, 'unauthorized', 'Invalid worker API key'); } if (!env.SUB2API_BASE_URL || !env.MODEL_KEY_RULES_JSON) { return jsonError(500, 'server_error', 'Worker is not configured'); } const rules = parseRules(env.MODEL_KEY_RULES_JSON); if (rules.length === 0) { return jsonError(500, 'server_error', 'No model routing rules configured (check MODEL_KEY_RULES_JSON format/type)'); } const routing = await resolveRouting(request, env, rules); if (routing.errorResponse) return routing.errorResponse; const upstreamUrl = buildUpstreamURL(request.url, env.SUB2API_BASE_URL); const upstreamHeaders = buildUpstreamHeaders(request.headers, routing.upstreamApiKey); const upstreamBody = shouldForwardBody(request.method) ? request.body : null; const websocketUpgrade = isWebSocketUpgrade(request); const upstreamResp = await fetch(upstreamUrl, { method: request.method, headers: upstreamHeaders, body: upstreamBody, redirect: 'manual', }); if (websocketUpgrade) { if (upstreamResp.status === 101 && upstreamResp.webSocket) { return new Response(null, { status: 101, webSocket: upstreamResp.webSocket, headers: sanitizeResponseHeaders(upstreamResp.headers), }); } return jsonError(502, 'websocket_upgrade_failed', 'Upstream did not accept websocket upgrade'); } const responseHeaders = sanitizeResponseHeaders(upstreamResp.headers); return new Response(upstreamResp.body, { status: upstreamResp.status, headers: responseHeaders, }); }, }; async function resolveRouting(request, env, rules) { const contentType = (request.headers.get('content-type') || '').toLowerCase(); const defaultKeyEnvName = String(env.DEFAULT_KEY_ENV || '').trim(); // Prefer model-based routing when body is JSON and has model. if (isJSONContentType(contentType) && shouldForwardBody(request.method)) { let payload; try { payload = await request.clone().json(); } catch { return { errorResponse: jsonError(400, 'invalid_json', 'Request body must be valid JSON'), }; } const model = extractModel(payload); if (model) { const matchedRule = matchRule(model, rules); if (!matchedRule) { return { errorResponse: jsonError(400, 'model_not_matched', `Model not matched by any rule: ${model}`), }; } const upstreamApiKey = env[matchedRule.key_env]; if (!upstreamApiKey) { return { errorResponse: jsonError(500, 'server_error', `Missing upstream key env: ${matchedRule.key_env}`), }; } return { upstreamApiKey }; } } // Endpoints without model (e.g. list models) use DEFAULT_KEY_ENV. if (!defaultKeyEnvName) { return { errorResponse: jsonError(400, 'missing_default_key_env', 'DEFAULT_KEY_ENV is required for non-model endpoints'), }; } const upstreamApiKey = env[defaultKeyEnvName]; if (!upstreamApiKey) { return { errorResponse: jsonError(500, 'server_error', `Missing upstream key env: ${defaultKeyEnvName}`), }; } return { upstreamApiKey }; } function isAuthorized(request, workerApiKey) { if (!workerApiKey) return false; const auth = request.headers.get('authorization') || ''; const token = auth.startsWith('Bearer ') ? auth.slice('Bearer '.length).trim() : ''; return token !== '' && token === workerApiKey; } function parseRules(raw) { let parsed = raw; // Cloudflare variable "JSON" type may already provide object/array values. if (typeof raw === 'string') { const text = raw.trim(); if (text === '') return []; try { parsed = JSON.parse(text); } catch { return []; } } // Support optional object wrapper: { rules: [...] } if (!Array.isArray(parsed) && parsed && typeof parsed === 'object' && Array.isArray(parsed.rules)) { parsed = parsed.rules; } if (!Array.isArray(parsed)) return []; return parsed .map((r) => ({ pattern: String(r?.pattern || '').trim(), key_env: String(r?.key_env || '').trim(), })) .filter((r) => r.pattern !== '' && r.key_env !== ''); } function extractModel(payload) { if (!payload || typeof payload !== 'object') return ''; const model = payload.model; return typeof model === 'string' ? model.trim() : ''; } function matchRule(model, rules) { const modelLower = model.toLowerCase(); // 1) Exact match first for (const rule of rules) { const p = rule.pattern.toLowerCase(); if (!p.includes('*') && modelLower === p) { return rule; } } // 2) Prefix wildcard match (e.g. gpt-*) for (const rule of rules) { const p = rule.pattern.toLowerCase(); if (p.endsWith('*')) { const prefix = p.slice(0, -1); if (prefix && modelLower.startsWith(prefix)) { return rule; } } } return null; } function shouldForwardBody(method) { const m = (method || '').toUpperCase(); return !(m === 'GET' || m === 'HEAD'); } function isWebSocketUpgrade(request) { const upgrade = request.headers.get('upgrade') || ''; return upgrade.toLowerCase() === 'websocket'; } function isJSONContentType(contentType) { return contentType.includes('application/json'); } function buildUpstreamURL(requestURL, upstreamBaseURL) { const incoming = new URL(requestURL); const base = new URL(upstreamBaseURL); base.pathname = joinPath(base.pathname, incoming.pathname); base.search = incoming.search; return base.toString(); } function joinPath(basePath, incomingPath) { const left = (basePath || '').replace(/\/+$/, ''); const right = (incomingPath || '').replace(/^\/+/, ''); if (!left) return `/${right}`; if (!right) return left; return `${left}/${right}`; } function buildUpstreamHeaders(incomingHeaders, upstreamApiKey) { const h = new Headers(); for (const [k, v] of incomingHeaders.entries()) { const lower = k.toLowerCase(); if (HOP_BY_HOP_HEADERS.has(lower)) continue; if (lower === 'authorization') continue; if (lower === 'host') continue; h.set(k, v); } h.set('authorization', `Bearer ${upstreamApiKey}`); return h; } function sanitizeResponseHeaders(upstreamHeaders) { const out = new Headers(); for (const [k, v] of upstreamHeaders.entries()) { if (!HOP_BY_HOP_HEADERS.has(k.toLowerCase())) { out.set(k, v); } } return out; } function jsonError(status, code, message) { return new Response(JSON.stringify({ error: { code, message } }), { status, headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store', }, }); }

标签:人工智能