我追蹤自己在三個專案中的人工智慧支出。三月份,成長最快的支出專案並非 Claude 或 GPT 呼叫,而是我的 Cursor 帳戶、Copilot 帳戶以及我透過個人命令列介面 (CLI) 呼叫的 Anthropic API。三個訂閱,三個計量單位,同樣的 Opus 代幣被計費兩次,因為 Cursor 將相同的上下文發送到它自己的後端,而我已經將這些上下文直接傳遞給了 Anthropic。
這些封裝程序不會宣傳這一點。路由程式碼並非他們的產品。他們的產品是無需考慮哪個模型處理哪個提示的便利性。您需要為編排成本支付利潤,這部分利潤已包含在席位價格中。
我厭倦了支付這筆費用,所以我自己寫了一個路由器。它只有 200 行 TypeScript 程式碼。四月我的帳單比三月低了 41%,而工作量卻大致相同。
| 模型 | 輸入 $/M 代幣 | 輸出 $/M 代幣 | 最適合 |
|-------|------------------|-------------------|----------|
| Haiku 4.5 | 0.80 | 4.00 | 尋找、分類、拼字錯誤修復 |
| Sonnet 4.6 | 3.00 | 15.00 | 預設編碼、重構、程式碼審查 |
| Opus 4.7 | 5.00 | 25.00 | 多步驟規劃、架構 |
| GPT-5 mini | 0.50 | 2.00 | 低成本分類、嵌入準備 |
節省 41% 的成本源自於一點:停止讓 Sonnet 處理那些 Haiku 可以用十分之一的成本完成的任務。大多數程式碼查詢實際上是偽裝成問題的查找操作。要根據意圖而非習慣來選擇路由。
每個包裝器都做著同樣的交易。它們為你選擇一個模型,加入一個你無法編輯的系統提示,並佔用一個你無法查看的上下文視窗。作為回報,你無需思考。
不思考的代價體現在兩方面:
此封裝程式會呼叫符合其服務等級協定 (SLA) 的最昂貴型號,因為這樣才能使簡報效果看起來不錯。
包裝器會就其代表您發送的上下文資訊向您收費,包括其自身的系統提示和工具定義。
我記錄了 Cursor 在 Anthropic 控制面板上 30 天的使用情況。 Cursor 平均每回合聊天發送 8400 個輸入標記。而我直接呼叫 API 處理相同聊天平均只發送 1900 個標記。這 6500 個標記的差值包含了 Cursor 的框架、索引上下文以及代理腳手架等資訊。雖然很有用,但並非免費。
自己建置路由器時,你可以選擇要傳送哪些資料。這就是關鍵所在。
文件在這裡。把它拖到專案中,提供你的 API 金鑰,它就會根據你控制的規則,為每個請求選擇一個模型。
// router.ts
import Anthropic from "@anthropic-ai/sdk";
import OpenAI from "openai";
type Intent = "trivial" | "code" | "plan" | "embed";
interface RouteRule {
match: (prompt: string) => boolean;
intent: Intent;
}
interface ModelConfig {
provider: "anthropic" | "openai";
model: string;
maxTokens: number;
}
const ROUTES: Record<Intent, ModelConfig> = {
trivial: { provider: "anthropic", model: "claude-haiku-4-5-20251001", maxTokens: 1024 },
code: { provider: "anthropic", model: "claude-sonnet-4-6", maxTokens: 4096 },
plan: { provider: "anthropic", model: "claude-opus-4-7", maxTokens: 8192 },
embed: { provider: "openai", model: "gpt-5-mini", maxTokens: 512 },
};
const RULES: RouteRule[] = [
{ intent: "trivial", match: (p) => p.length < 200 && /\?$/.test(p.trim()) },
{ intent: "trivial", match: (p) => /^(what is|define|fix typo|rename)/i.test(p) },
{ intent: "plan", match: (p) => /(refactor|design|architect|migrate|plan)/i.test(p) },
{ intent: "code", match: (p) => /(```|function |class |const |let )/i.test(p) },
{ intent: "embed", match: (p) => p.startsWith("CLASSIFY:") },
];
function pickIntent(prompt: string): Intent {
for (const rule of RULES) {
if (rule.match(prompt)) return rule.intent;
}
return "code";
}
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
export interface RouteResult {
text: string;
model: string;
inputTokens: number;
outputTokens: number;
costUsd: number;
}
const PRICING: Record<string, { in: number; out: number }> = {
"claude-haiku-4-5-20251001": { in: 0.8, out: 4 },
"claude-sonnet-4-6": { in: 3, out: 15 },
"claude-opus-4-7": { in: 5, out: 25 },
"gpt-5-mini": { in: 0.5, out: 2 },
};
function priceCall(model: string, inTok: number, outTok: number): number {
const p = PRICING[model];
if (!p) return 0;
return (inTok * p.in + outTok * p.out) / 1_000_000;
}
export async function route(prompt: string): Promise<RouteResult> {
const intent = pickIntent(prompt);
const cfg = ROUTES[intent];
if (cfg.provider === "anthropic") {
const r = await anthropic.messages.create({
model: cfg.model,
max_tokens: cfg.maxTokens,
messages: [{ role: "user", content: prompt }],
});
const text = r.content
.filter((b) => b.type === "text")
.map((b) => (b as { text: string }).text)
.join("");
return {
text,
model: cfg.model,
inputTokens: r.usage.input_tokens,
outputTokens: r.usage.output_tokens,
costUsd: priceCall(cfg.model, r.usage.input_tokens, r.usage.output_tokens),
};
}
const r = await openai.chat.completions.create({
model: cfg.model,
max_tokens: cfg.maxTokens,
messages: [{ role: "user", content: prompt }],
});
const usage = r.usage ?? { prompt_tokens: 0, completion_tokens: 0 };
return {
text: r.choices[0]?.message?.content ?? "",
model: cfg.model,
inputTokens: usage.prompt_tokens,
outputTokens: usage.completion_tokens,
costUsd: priceCall(cfg.model, usage.prompt_tokens, usage.completion_tokens),
};
}
就是這樣。兩個服務提供者,四個意圖,五條規則,以及一個費用計算器。使用方法如下:
import { route } from "./router";
const out = await route("rename this function from getUser to fetchUser");
console.log(out.model, out.costUsd.toFixed(5));
// claude-haiku-4-5-20251001 0.00012
這些規則故意設計得很愚蠢。長度加上正規表示式大概能正確處理 70% 的路由決策。對於剩下的 30%,可以用前綴覆蓋:
await route("[force:opus] design a permissions model for ...");
加入一行程式碼到pickIntent中讀取前綴。為了保持範例簡潔,我省略了這行程式碼。
最簡單的做法是向一個低成本模型發送一個小型分類器呼叫,讓它來選擇路由。這聽起來很聰明,但實際上成本更高,因為每個請求現在都會消耗兩次 API 呼叫。 pickIntent 的開銷必須為零。
五個正規表示式規則就能滿足我大部分的工作需求:
簡短且以問號結尾:瑣碎的
以「是什麼」、「定義」、「修正拼字錯誤」、「重新命名」開頭:瑣碎
包含「重構」、「設計」、「架構」、「遷移」、「計劃」:計劃
包含程式碼區塊或函數關鍵字:程式碼
以「CLASSIFY:」前綴開頭:嵌入(廉價分類器)
預設使用程式碼。從簡單路徑錯誤地跳到程式碼路徑,單次請求的成本可能增加 4 倍。從程式碼路徑錯誤地跳到作品路徑,成本增加 1.6 倍。這兩種情況都不算災難性的。需要避免的錯誤是向 Haiku 發送它無法處理的多步驟計劃,這意味著預設路徑應該保守一些。
我還記錄了每一次錯誤。兩週後,我得到了一個簡短的 CSV 文件,內容是「此提示被路由到 X,但應該路由到 Y」。我新增了兩個正規表示式規則,錯誤率從 8% 降到了 2% 以下。
三月帳單顯示沒有路由器:
| 來源 | 通話 | 支出 |
|--------|-------|-------|
| 遊標座位 | 不適用 | 20 美元 |
副駕駛座 | 不適用 | 10 美元 |
| Anthropico 直銷 | 4,200 | 87 美元 |
OpenAI Direct | 800 | 14 美元 |
總計| | 131 美元|
四月份的帳單,包含路由器(取消了 Cursor,僅保留了用於 IDE 內聯的 Copilot):
| 來源 | 通話 | 支出 |
|--------|-------|-------|
| 遊標座位 | 不適用 | 0 美元 |
副駕駛座 | 不適用 | 10 美元 |
| Anthropic 透過路由器 | 5,100 | 54 美元 |
|透過路由器 OpenAI | 1,400 | 1,400 13 美元 |
總計| | 77 美元|
總呼叫量增加了 30%,但每次通話的成本卻降低了 41%。路由器將 62% 的呼叫轉移到了 Haiku 上,從而分擔了原本由 Sonnet 處理的工作負載。每次通話的平均成本從 0.024 美元降至 0.013 美元。
遊標取消功能節省了大部分開支。路由功能節省了更多開支,這些開支雖然數額較小,但卻會不斷累積。兩者都源自於同一個理念:包裝器隱藏了你可以自己做出的更好的決定。
這不是代理框架。它不進行串流。它不會重試。它不會緩存。它不處理速率限制。它不使用任何工具。它不了解你的程式碼庫。
加入這些功能都需要工作。串流媒體需要兩處改動。使用 Anthropic 提示快取進行快取需要在每次呼叫時加入額外的頭部資訊。使用指數退避重試需要 20 行程式碼。工具的使用需要寫一些你本來就要寫的架構基礎。
如果你需要所有這些功能,那就使用一個真正的框架。如果你想避免為 80% 的呼叫支付編排費用,上面這 200 行程式碼就能做到。剩下的部分可以根據你實際遇到的問題逐步加入。
之所以需要封裝器,是因為路由 AI 呼叫很麻煩。它也是你在自己的程式碼中能掌控的最有價值的東西。上面那 200 行程式碼並非護城河,它們只是你週二下午的日常工作。寫它們的理由是:你無法改進你看不見的帳單。
您目前低價機型和高價機型的通話比例是多少?如果您不清楚,這是首先要解決的問題。在連接路由器之前,先建立成本記錄系統。結果會令您大吃一驚。
GDS KS · thegdsks.com · 建置Glincker · 在 X 上追蹤@thegdsks
編排稅是人工智慧帳單中不會顯示在定價頁面上的部分。
原文出處:https://dev.to/thegdsks/i-built-a-200-line-ai-router-in-typescript-my-monthly-bill-dropped-41-23ok