一个请求的旅程
当你用 Claude Code 写代码时,你的请求经历了怎样的冒险?
想象你在写代码,AI 突然断了
你正用 Claude Code 专心写代码,突然弹出"配额用尽"。你的 $20/月 订阅 额度花完了,代码写了一半,怎么办?
9Router 就是你的"智能总机"——当你的主力 AI 提供商 挂了,它自动帮你切换到备用线路,让你的代码永不停歇。
9Router 的本质是一个"本地 代理服务器"——所有 AI 请求先到它这里,它再帮你转发到合适的 AI 服务商。你只需要改一个地址,就能接入 40+ 家 AI 服务商、100+ 个模型。
一个请求的完整旅程
你在 Claude Code 里输入"帮我写个排序算法",然后发生了什么?点"下一步"来看:
入口代码长什么样
所有 AI 请求都从 handleChat 这个函数开始。它就像是 9Router 的前台接待员:
export async function handleChat(request) {
const body = await request.json();
const modelStr = body.model;
const settings = await getSettings();
const modelInfo = getModelInfo(modelStr);
return handleSingleModelChat(body, modelInfo);
}
定义一个"处理聊天"的函数,接收来自客户端的请求
把请求体解析成 JS 对象,拿到用户想用的模型名
提取出模型名称字符串,比如 "cc/claude-opus-4-7"
读取全局设置(是否需要 API Key、使用什么策略等)
解析模型名,得到"提供商 + 模型"的组合信息
把请求交给专门处理单个模型的函数去执行
函数结束,返回响应给客户端
三层架构一览
9Router 由三层组成,各司其职:
API 接口层
Next.js 路由——接收请求、验证身份、解析模型名。就像大厦的入口大堂。
SSE 核心
翻译格式、选择执行器、管理 流式传输。是整个系统的大脑。
持久化层
用 JSON 文件存储配置、凭据和使用记录。轻量级,无需数据库。
测验:你理解了请求旅程吗?
9Router 在整个 AI 编程流程中扮演什么角色?
9Router 用什么来存储提供商配置和使用数据?
核心角色大盘点
认识 9Router 里的五个关键"演员",看看它们怎么分工合作
一出好戏,五个主角
想象一个机场。旅客要飞往不同目的地,但不可能自己开飞机。9Router 就像一个智能机场调度系统,每个模块都有明确的职责:
src/app/api/
前台接待——检查你的"登机牌"(API Key),识别你要去哪里(模型名),然后把请求交给调度中心
chat.js / chatCore.js
调度中心——决定用哪家航空公司(提供商),安排"翻译官"转换格式,协调整个飞行过程
translator/
翻译官——不同 AI 服务商说不同的"语言"(OpenAI 格式、Claude 格式、Gemini 格式),翻译官帮你互相转换
executors/
飞行员——真正飞向目标 AI 服务商的人。不同航司有不同飞行方式(OAuth 认证、API Key 认证...)
localDb.js / usageDb.js
档案室——记住所有配置(你的 API Key、套餐设置),以及每次飞行的记录(用了多少 token、花了多少钱)
看它们互相对话
当你发送一个聊天请求时,五个角色会像微信群聊一样互相配合:
执行器:每个提供商一个
不同的 AI 服务商需要不同的"飞行方式"。9Router 给每个服务商写了一个专门的执行器:
import { BaseExecutor } from "./base.js";
export class KiroExecutor extends BaseExecutor {
constructor() {
super();
this.providerId = "kiro";
}
}
导入"基础飞行员"类,它已经实现了通用的飞行逻辑
Kiro 执行器继承基础类,就像"川航飞行员"继承了通用的飞行技能
构造函数初始化时...
调用父类的基础设置
标记自己是"kiro"(Kiro AI 服务商)的专属执行器
类定义结束
如果要加一个新的 AI 服务商,只需要创建一个继承 BaseExecutor 的新类,实现该服务商特有的认证和 请求头逻辑就行。当前有 15+ 个专用执行器。
代码目录结构
9Router 的代码组织就像一个井井有条的办公室:
测验:角色认知
如果你要新增一个 AI 服务商的支持,应该修改哪个模块?
翻译器(translator/)最像现实世界中的什么角色?
三层智能降级
当一个 AI 服务挂了,9Router 怎么无缝切换到备用方案?
像发电厂的备用发电机
想象一栋大楼。主电源(订阅)停电了,自动切换到 备用发电机(便宜方案)。发电机也烧完了?太阳能(免费方案)继续供电。
9Router 就是这样工作的——三层降级,永不停机:
Claude Code Pro $20/月、Codex Plus 等——你已经在付钱了,优先用完它
GLM-5.1($0.6/百万 token)、MiniMax($0.2/百万 token)——极低成本
Kiro AI(免费无限 Claude 4.5)、OpenCode Free——$0,永远不会断
Combo:你的"套餐组合"
你可以在 9Router 仪表盘里创建"Combo"——给多个模型排个优先级,就像给手机设置"WiFi 优先 → 4G 备用 → 3G 兜底":
怎么判断"该降级了"?
9Router 用一套"错误规则"来判断什么时候该切换到下一个方案:
export function checkFallbackError(status, errorText) {
for (const rule of ERROR_RULES) {
if (rule.text && lowerError.includes(rule.text)) {
return { shouldFallback: true };
}
if (rule.status && rule.status === status) {
return { shouldFallback: true };
}
}
}
定义一个"检查是否该降级"的函数,输入是状态码和错误信息
遍历所有预设的错误规则(从最具体的到最一般的)
如果是文字匹配规则,看错误信息里有没有包含关键字(比如"rate limit")
匹配到了!返回"该降级了"
如果是状态码规则,看 HTTP 状态码是否匹配(比如 429 = 限流)
匹配到了!返回"该降级了"
所有规则都没匹配——默认也降级(宁可多试一个,不要直接失败)
指数退避:越失败越慢
当一个账户被限流时,9Router 不会疯狂重试。它用一个聪明的策略——指数退避:
冷却时间翻倍
第一次失败:等 1 秒。第二次:2 秒。第三次:4 秒。最多等 4 分钟。给服务器喘息的时间。
多账户轮换
同一个提供商可以有多个账户。账户 A 限流了?试账户 B。都限流了?才降级到下一个模型。
模型级锁定
不是整个提供商都挂了才切换——是具体的模型。比如 Claude Opus 限流了,但 Claude Sonnet 可能还能用。
指数退避是分布式系统的黄金法则。Google、Amazon 的服务都在用。下次你告诉 AI "加重试逻辑"时,加上"用指数退避"——这是区分小白和专家的关键词。
测验:降级策略
9Router 的三层降级顺序是?
当 AI 服务商限流时,通常返回什么 HTTP 状态码?
省 Token 的魔法
RTK 和 Caveman——两个让每次请求省 20-65% token 的黑科技
Token 就是钱
每次跟 AI 对话,你发送的每一个字都会被计算成 token。一个典型的编程会话,tool 的输出(git diff、grep 结果、文件列表...)经常占掉 30-50% 的 token 预算。
9Router 内置了两个"省钱引擎":
RTK Token Saver
智能压缩工具输出——省 20-40% 输入 token。自动识别 git diff、grep、find 等输出格式,应用专门的压缩策略。
Caveman Mode
在 system prompt 里注入"原始人指令"——让 AI 用更简短的方式回复。省 最高 65% 输出 token。
RTK 怎么自动识别内容类型?
RTK 不需要你配置——它会"偷看"每段 tool 输出的前 1KB,自动判断内容类型:
export function autoDetectFilter(text) {
const head = text.slice(0, DETECT_WINDOW);
if (RE_GIT_DIFF.test(head)) return gitDiff;
if (RE_GIT_STATUS.test(head)) return gitStatus;
if (first5.some(isGrepLine)) return grep;
if (RE_TREE_GLYPH.test(head)) return tree;
if (text.split("\n").length >= MIN_LINES)
return smartTruncate;
return null;
}
定义"自动检测过滤器"函数,输入是 tool 输出的文本
只看文本的开头部分(就像你瞟一眼文件就知道它是什么类型)
看到"diff --git"?这是 git diff 输出 → 用 gitDiff 压缩器
看到"On branch"?这是 git status 输出 → 用 gitStatus 压缩器
前 5 行里有"文件名:行号:内容"格式?这是 grep 结果
看到树形结构字符(├── └──)?这是 tree 命令输出
行数太多?用智能截断,保留关键部分
都不匹配?返回 null(不压缩,保持原样)
函数结束
安全网:压缩失败也不怕
RTK 有一个关键设计哲学:永远不要因为压缩而破坏请求。看看它的安全机制:
export function safeApply(fn, text) {
try {
const out = fn(text);
if (typeof out !== "string") return text;
return out;
} catch (err) {
console.warn("filter panicked");
return text;
}
}
定义一个"安全执行"函数,包裹任何压缩操作
尝试执行压缩...
调用压缩函数,得到结果
如果结果不是字符串(压缩器出错了)?返回原始文本
压缩成功,返回压缩后的文本
如果压缩过程中抛了异常...
打印一条警告日志(但不崩溃!)
返回原始文本(完全没有损失)
无论发生什么,用户都不会看到错误
"优化类"代码最重要的是:失败时回退到原始行为。这叫优雅降级。你在指导 AI 做优化时,一定要说"失败时保持原样"。
Caveman:让 AI 说"短话"
Caveman 的灵感来自一个爆款项目——"为什么要用很多词,明明几个字就能说清楚"。它在发送请求前,偷偷注入一段"原始人指令":
export function injectCaveman(body, format, level) {
const prompt = CAVEMAN_PROMPTS[level];
switch (format) {
case FORMATS.CLAUDE:
injectClaudeSystem(body, prompt);
return;
default:
injectMessagesSystem(body, prompt);
}
}
定义"注入原始人指令"的函数,输入是请求体、格式和压缩等级
根据等级选择对应的原始人提示词("少说话,多干活")
根据目标 AI 的格式来决定怎么注入...
如果是 Claude 格式...
把原始人指令插入到 Claude 的 system 字段里
返回
其他格式(OpenAI、Gemini 等)...
把指令插入到 messages 数组的 system 消息里
函数结束。AI 回复时就会用更简短的方式回答了
测验:Token 省钱术
RTK 怎么知道该用哪种压缩策略?
如果 RTK 的压缩器执行时崩溃了,会发生什么?
格式翻译官
OpenAI、Claude、Gemini 说的"语言"都不同——9Router 怎么让它们互相听懂?
AI 服务商的"方言"问题
想象你在国际会议上发言。你说中文,但听众里有说英文的、法文的、日文的。你需要一个同声传译。
AI 世界也一样——OpenAI 格式、Claude 格式、Gemini 格式 各不相同:
messages[]
OpenAI 用这个字段存聊天历史
system + messages
Claude 把系统指令单独放,聊天记录放 messages
contents + parts
Gemini 用完全不同的结构组织消息
你的工具(Claude Code)说一种"方言",但你想用的 AI 服务商(Gemini)说另一种。没有翻译,它们根本无法沟通。9Router 的翻译器就是让任意工具和任意服务商都能对话的关键。
巧妙的"两阶段翻译"
9Router 用了一个聪明的策略来减少翻译器的数量。它不需要为每对格式都写一个翻译器——而是用 OpenAI 格式作为"中间格式":
来源格式
(Claude/Gemini/Kiro...)
OpenAI 中间格式
(通用标准)
目标格式
(Claude/Gemini/Kiro...)
这意味着:支持 N 种格式,只需要写 2N 个翻译器(而不是 N×N 个)。从 5 种格式 25 个翻译器 → 10 个翻译器。
翻译器注册表
看看 9Router 是怎么注册和管理翻译器的:
const requestRegistry = new Map();
export function register(from, to, requestFn) {
const key = `${from}:${to}`;
requestRegistry.set(key, requestFn);
}
// 懒加载——用到时才加载翻译器
require("./request/claude-to-openai.js");
require("./request/openai-to-claude.js");
require("./request/gemini-to-openai.js");
用一个 Map(字典)来存储所有注册的请求翻译器
定义注册函数:记录"从 A 格式翻译到 B 格式"用哪个翻译函数
把来源和目标拼成一个唯一键,比如"claude:openai"
把这个键和对应的翻译函数存进字典
注册完成
用懒加载:只有第一次需要翻译时才加载这些文件(提高启动速度)
加载 Claude → OpenAI 翻译器
加载 OpenAI → Claude 翻译器
加载 Gemini → OpenAI 翻译器
捷径:原生直通模式
有时候根本不需要翻译!当你的工具和 AI 服务商说同一种"方言"时,9Router 会跳过所有翻译步骤,直接转发:
const passthrough = isNativePassthrough(
clientTool, provider
);
if (passthrough) {
translatedBody = { ...body, model };
}
检查客户端工具和目标服务商是否同一个"生态系统"
(比如 Cursor 客户端 → Cursor 服务商)
如果是同生态——不需要翻译!
只需要换掉模型名,其他原样转发。省时省力。
跳过了翻译步骤,直接发过去
好的优化不是"让每件事做得更快",而是"跳过不需要做的事"。下次你让 AI 优化代码时,先问:有没有什么操作是根本不需要执行的?这种"短路思维"是最有效的优化。