Skip to content
GuidesJuly 19, 2026

Sign in with AnyRouter:让用户带上自己的 LLM 账户

如果应用要跑 LLM API,你只有两个糟糕选项:自己出 key、吃下每个用户的推理账单,或让每人粘贴一把裸的供应商 key。"Sign in with AnyRouter" 是第三条路——用户用自己的 AnyRouter 账户登录,用他们自己的余额跑推理。你拿到一把临时、仅推理、按用户隔离的 key,从不经手模型账单。

为什么应用不该扛推理账单

今天几乎每个在做的应用都想在某处接 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
一次性注册,然后按用户登录,铸造记到该用户账上的 key。

下面每个端点都在 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"
  }'
POST /api/v1/mcp/oauth/register — 返回公开的 client_id。

响应里有你的 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>
登录按钮 — PKCE challenge + 跳转到 /oauth/authorize。

用户落到 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)
POST /api/v1/mcp/oauth/token — 返回该用户的推理 key。

每个用户存一把 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!" }],
  }),
})
POST /api/v1/chat/completions — 记到已登录用户。

因为它是真正的 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:profileGET /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 指南

两分钟发第一笔请求

用自己的 key 免费开始,或充值按 token 计费。Go 每月 $4 额度 — $2/月,或捐一把 provider key 免费开通。

开始使用