01

一个请求的旅程

当你用 Claude Code 写代码时,你的请求经历了怎样的冒险?

想象你在写代码,AI 突然断了

你正用 Claude Code 专心写代码,突然弹出"配额用尽"。你的 $20/月 订阅 额度花完了,代码写了一半,怎么办?

9Router 就是你的"智能总机"——当你的主力 AI 提供商 挂了,它自动帮你切换到备用线路,让你的代码永不停歇。

💡
核心洞察

9Router 的本质是一个"本地 代理服务器"——所有 AI 请求先到它这里,它再帮你转发到合适的 AI 服务商。你只需要改一个地址,就能接入 40+ 家 AI 服务商、100+ 个模型。

一个请求的完整旅程

你在 Claude Code 里输入"帮我写个排序算法",然后发生了什么?点"下一步"来看:

⌨️
Claude Code
🔀
9Router
🤖
AI 提供商
点击"下一步"开始旅程

入口代码长什么样

所有 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 用什么来存储提供商配置和使用数据?

02

核心角色大盘点

认识 9Router 里的五个关键"演员",看看它们怎么分工合作

一出好戏,五个主角

想象一个机场。旅客要飞往不同目的地,但不可能自己开飞机。9Router 就像一个智能机场调度系统,每个模块都有明确的职责:

🚪
API 路由层 src/app/api/

前台接待——检查你的"登机牌"(API Key),识别你要去哪里(模型名),然后把请求交给调度中心

🧠
Chat 处理器 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 的代码组织就像一个井井有条的办公室:

src/ 主应用代码
app/api/ 所有 API 接口路由
sse/ SSE 流式处理核心(调度中心)
lib/ 本地数据库(JSON 文件读写)
open-sse/ 核心引擎(跨环境共享)
executors/ 每个 AI 服务商的专属执行器
translator/ 格式翻译器(OpenAI ↔ Claude ↔ Gemini...)
rtk/ Token 省钱引擎(RTK 压缩 + Caveman 模式)

测验:角色认知

如果你要新增一个 AI 服务商的支持,应该修改哪个模块?

翻译器(translator/)最像现实世界中的什么角色?

03

三层智能降级

当一个 AI 服务挂了,9Router 怎么无缝切换到备用方案?

像发电厂的备用发电机

想象一栋大楼。主电源(订阅)停电了,自动切换到 备用发电机(便宜方案)。发电机也烧完了?太阳能(免费方案)继续供电。

9Router 就是这样工作的——三层降级,永不停机

1
Tier 1:订阅服务(主力)

Claude Code Pro $20/月、Codex Plus 等——你已经在付钱了,优先用完它

2
Tier 2:便宜方案(备用)

GLM-5.1($0.6/百万 token)、MiniMax($0.2/百万 token)——极低成本

3
Tier 3:免费方案(兜底)

Kiro AI(免费无限 Claude 4.5)、OpenCode Free——$0,永远不会断

Combo:你的"套餐组合"

你可以在 9Router 仪表盘里创建"Combo"——给多个模型排个优先级,就像给手机设置"WiFi 优先 → 4G 备用 → 3G 兜底":

📋
Combo 选择器
💳
Tier 1 订阅
💰
Tier 2 便宜
🆓
Tier 3 免费
点击"下一步"观看降级过程

怎么判断"该降级了"?

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 可能还能用。

💡
Aha!

指数退避是分布式系统的黄金法则。Google、Amazon 的服务都在用。下次你告诉 AI "加重试逻辑"时,加上"用指数退避"——这是区分小白和专家的关键词。

测验:降级策略

9Router 的三层降级顺序是?

当 AI 服务商限流时,通常返回什么 HTTP 状态码?

04

省 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 的压缩器执行时崩溃了,会发生什么?

05

格式翻译官

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 格式作为"中间格式":

1

来源格式
(Claude/Gemini/Kiro...)

2

OpenAI 中间格式
(通用标准)

3

目标格式
(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 优化代码时,先问:有没有什么操作是根本不需要执行的?这种"短路思维"是最有效的优化。

测验:翻译系统

9Router 用什么格式作为翻译的中间格式?

什么情况下 9Router 会完全跳过格式翻译?