为什么应用不该扛推理账单
今天几乎每个在做的应用都想在某处接 LLM:聊天框、摘要按钮、干活的 agent。但一加上,你就继承了账单问题。如果你自己发供应商 key,用户的每次请求都落在你的发票上——一个热情用户就能把账单撑得比他们付给你的还大,于是你开始计量、限流、防欺诈,跑你本不想跑的推理。
常见逃生口是让每人粘贴一把裸的 OpenAI 或 Anthropic key。能用,但体验很差:用户要管他们不懂的密钥,你要存能做远超调模型的长期凭证,一次日志失误就会泄露。两者都不是你真正想要的:让用户付自己的推理,并且永远别碰他们的 key。
这正是 "Sign in with AnyRouter" 给你的。AnyRouter 是应用围绕搭建的地基——网关已经有用户账户、他们的额度或免费档,以及一份目录里的全部模型。加一个登录按钮,用户批准一次,应用就收到一把临时、仅推理、绑定该用户的 key。之后每次请求记在他们账户上,不是你的。你交付产品;AnyRouter 处理模型账单、供应商故障切换和审计日志。
流程怎么走
这是带 PKCE 的标准 OAuth 2.1 authorization-code 流程。你的应用是 public client——没有要保护的 client secret,所以纯浏览器前端也能跑。用户被跳到 AnyRouter 同意页,批准仅推理范围,你的回调用返回的 code 换一把短命 sk-ar-v1-* key。
sequenceDiagram
participant App as Your app
participant AR as AnyRouter
participant User
App->>AR: (once) POST /oauth/register {app_type:"signin"}
AR-->>App: client_id (public, safe to embed)
User->>App: clicks "Sign in with AnyRouter"
App->>AR: GET /oauth/authorize?client_id&redirect_uri&code_challenge
AR->>User: consent screen — your app name + inference scope
User->>AR: Approve
AR-->>App: redirect_uri?code=...
App->>AR: POST /oauth/token {code, code_verifier}
AR-->>App: access_token = sk-ar-v1-* (expires in 30d)
App->>AR: POST /chat/completions (Bearer sk-ar-v1-*)
Note over AR: billed to the user's account下面每个端点都在 https://anyrouter.dev/api/v1/mcp/oauth/* 下——与 AnyRouter 给 MCP 和企业托管访问用的同一套统一 OAuth 网关。注册一次,再对每个用户跑 authorize/token。
第 1 步 — 注册应用一次
注册是开放的:注册应用不需要 AnyRouter 账户,也没有 client secret。发送应用名、精确的 redirect URI(仅 https,开发可用 localhost),以及 app_type: "signin"。
curl https://anyrouter.dev/api/v1/mcp/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "My AI App",
"redirect_uris": ["https://myapp.example/callback"],
"app_type": "signin",
"origin_url": "https://myapp.example"
}'响应里有你的 client_id。它是公开的——可以放心嵌进前端。Signin 应用会打上轻量管理审核标记,并出现在 AnyRouter 管理控制台;滥用应用由此被暂停,且从不挡住用户登录。
第 2 步 — 把用户送到同意页
点击按钮时,在浏览器里生成 PKCE verifier 和 S256 challenge,存起来(外加防 CSRF 的 state),再跳到 authorize 端点。
<button id="signin">Sign in with AnyRouter</button>
<script>
const CLIENT_ID = "your_client_id"
const REDIRECT_URI = "https://myapp.example/callback"
const b64url = (b) =>
btoa(String.fromCharCode(...new Uint8Array(b)))
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
document.getElementById("signin").onclick = async () => {
const verifier = b64url(crypto.getRandomValues(new Uint8Array(32)))
const challenge = b64url(
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)),
)
const state = b64url(crypto.getRandomValues(new Uint8Array(16)))
sessionStorage.setItem("ar_verifier", verifier)
sessionStorage.setItem("ar_state", state)
const url = new URL("https://anyrouter.dev/api/v1/mcp/oauth/authorize")
url.search = new URLSearchParams({
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: "code",
code_challenge: challenge,
code_challenge_method: "S256",
state,
}).toString()
location.href = url.toString()
}
</script>用户落到 AnyRouter 同意页——有 Clerk 门禁,未登录会先登自己的 AnyRouter 账户——看到你的应用名和精确请求的 scope,然后批准。AnyRouter 再 302 回你的 redirect_uri,带 ?code=…&state=…。
第 3 步 — 用 code 换 key
在回调页核对 state 与你存的一致,再把 code 和 PKCE verifier POST 到 token 端点。它同时接受 form-encoded 和 JSON。
const params = new URLSearchParams(location.search)
if (params.get("state") !== sessionStorage.getItem("ar_state")) {
throw new Error("State mismatch — restart sign-in")
}
const res = await fetch("https://anyrouter.dev/api/v1/mcp/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "authorization_code",
code: params.get("code"),
redirect_uri: "https://myapp.example/callback",
client_id: "your_client_id",
code_verifier: sessionStorage.getItem("ar_verifier"),
}),
})
const token = await res.json()
// {
// "access_token": "sk-ar-v1-...",
// "token_type": "Bearer",
// "scope": "inference read:profile",
// "expires_in": 2592000
// }
saveTokenForThisUser(token.access_token)每个用户存一把 token。从应用角度看,那把 sk-ar-v1-* 就是普通 AnyRouter key——但它只能跑推理、只绑定这个用户,并且会过期(默认 30 天)。
第 4 步 — 以该用户身份跑推理
把 token 当任意 AnyRouter API key 用。每次请求都按已登录用户的余额计量——他们的额度,或他们所在的免费档。你看不到模型账单。
const res = await fetch("https://anyrouter.dev/api/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "google/gemini-3.5-flash",
messages: [{ role: "user", content: "Hello from my app!" }],
}),
})因为它是真正的 AnyRouter key,你白得整套网关:目录里任意模型按 id、供应商被限流时自动故障切换,以及同一端点上的 OpenAI、Anthropic、Responses 方言。要渲染 "Signed in as …" 徽章,read:profile scope 允许调用 GET /api/v1/me 取显示名、头像和套餐。
用户看到和控制什么
同意页故意很窄。登录 token 只带两个 scope,再无其他——多要无效,AnyRouter 会夹回这两个:
| Scope | 允许什么 |
|---|---|
| inference | 调用 /chat/completions、/messages、/responses、/embeddings — 记到用户 |
| read:profile | GET /api/v1/me — 显示名、头像、套餐 |
登录 token **不能**做的事才是重点:不能创建或吊销 API key,不能读用量或账单,不能改任何账户设置,也不能继承管理员权限。没有任何管理访问——一把被盗 token 最坏也只是在过期前花掉该用户自己有上限的推理预算。
用户在另一头仍掌控。从 **Dashboard → Connected apps** 随时可撤销你的应用,这会使你为该用户拿到的每把 token 失效。过期和撤销对你的代码表现一样——401。v1 没有 refresh token:把任何 401 当作「重新认证」,静默再跑一遍 authorize。若用户仍登录着 AnyRouter,几秒内就会过同意页并带回新 token。
建在地基上,不要绕开它
重点是推理不再是你的问题。你不配 key,不垫模型账单,不存任何人的供应商密钥,也不做计量系统。用户带上已有账户;AnyRouter 处理钱和模型。你的应用只调用一个端点。
完整端点参考、scope,以及从头到尾的浏览器片段。
阅读 Sign in with AnyRouter 指南相关文章
Use AnyRouter with the Vercel AI SDK
@anyr/ai-sdk-provider wraps the AnyRouter gateway as a standard Vercel AI SDK provider. Call createAnyRouter(), pass a provider/model id, and generateText, streamText, and generateObject work unchanged — switching models is a one-string edit.
One API for every LLM
One OpenAI-compatible endpoint reaches every model in the catalog — 170+ models addressed as provider/model, with automatic failover across upstreams, routing presets, and streaming. Point your existing client at AnyRouter and change a model with a string, not a rewrite.