# SciRouter 接口文档(完整版) > SciRouter 是科研垂域大模型的统一入口:一个与 OpenAI 兼容的 API,接入多家厂商的科研模型,统一计费与调度。 本文件把 https://www.scirouter.cn/docs 下的 10 页文档合成一份,供 AI 编程助手读完后实现接入。内容最后更新于 2026-09-23;每一页前面标了它的来源地址与更新日期。 ## 接入要点 - **base URL**:`https://api.scirouter.cn/v1`,OpenAI 兼容。只有四个接口:`GET /models`、`GET /models/{model_id}`、`POST /chat/completions`、`POST /embeddings`;旧的 `/completions`、Responses、图片、语音等接口不存在。 - **认证**:`Authorization: Bearer `,只认这一种写法。Key 以 `sk-sr-` 开头;从环境变量(如 `SCIROUTER_API_KEY`)读,不要写进代码,也不要放进浏览器、App 等前端。 - **模型 id**:形如 `厂商/模型名`,以 `GET /models` 的返回为准;那个列表只含当前 Key 能调用的模型。 - **错误**:OpenAI 形状 `{"error": {"message", "type", "code", "param", "request_id", "details"}}`。按小写的 `error.code` 分支,不要按 `message`。 - **该重试的**:429 `rate_limited`、502 `upstream_error`、503 `model_unavailable`、504 `upstream_timeout`、409 `idempotency_conflict`(处理中)——指数退避,遵守 `Retry-After`。其余 4xx 不要重试;另外两种 429(`key_quota_exceeded`、`project_budget_exceeded`)要等到下个月或调额度。 - **流式**:跳过以 `:` 开头的注释行(心跳);`choices` 可能是空数组;出错时是一个 `{"error": …}` 分片,之后断开;**没收到 `data: [DONE]` 的流一律按失败处理**,哪怕 HTTP 状态是 200。 - **截断**:`finish_reason` 为 `length` 表示回答被 `max_tokens` 截断。`max_tokens` 超过模型或套餐上限会 400(`details.limit` 给出上限),不会被悄悄截短。 - **幂等**:非流式请求带 `Idempotency-Key` 头,网络抖动后用同一个键重试不会重复扣费。 - **报障**:记录响应头 `X-Request-Id`(错误体里也有 `request_id`)。 --- # 快速开始 > 五分钟内发出第一个请求。 来源:https://www.scirouter.cn/docs/quickstart · 更新于 2026-09-23 SciRouter 的接口与 OpenAI 兼容。如果你的代码已经在调 OpenAI, 改一个 `base_url` 就能用。 ## 拿一个 API Key 在[控制台的 API Keys 页面](https://www.scirouter.cn/console/keys)新建一个。 密钥**只在创建时显示一次**,我们只存它的哈希。关掉那个窗口之后就再也拿不到了—— 请当场复制并存进你的密钥管理工具或环境变量。 ## 发出第一个请求 ```bash curl https://api.scirouter.cn/v1/chat/completions \ -H "Authorization: Bearer $SCIROUTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "bio/protein-72b", "messages": [{"role": "user", "content": "帮我判断这段序列的二级结构倾向"}] }' ``` Python: ```python import os from openai import OpenAI client = OpenAI( api_key=os.environ["SCIROUTER_API_KEY"], base_url="https://api.scirouter.cn/v1", ) response = client.chat.completions.create( model="bio/protein-72b", messages=[{"role": "user", "content": "帮我判断这段序列的二级结构倾向"}], ) print(response.choices[0].message.content) ``` ## 先在 Playground 里试 不想写代码就先试一下的话,[Playground](https://www.scirouter.cn/playground) 里可以直接调参数、 看用量与费用,并把当前这次调用复制成可运行的代码。 Playground **不需要 API Key**——它走的是登录会话,密钥不会出现在任何网页里。 ## 接下来 - [模型中心](https://www.scirouter.cn/models)看有哪些模型、各自擅长什么。 - 用 LangChain、LlamaIndex、Vercel AI SDK 或 Node.js,见[SDK 与框架](https://www.scirouter.cn/docs/sdks)。 - 流式、工具调用、`max_tokens` 超限怎么处理,见[对话补全](https://www.scirouter.cn/docs/chat-completions)。 - 让 AI 编程助手替你写接入代码:[给 AI 助手](https://www.scirouter.cn/docs/llms)里有整份文档的一键复制。 - 出错了先看[错误码](https://www.scirouter.cn/docs/errors)。 --- # 认证与 API Key > 怎么带 Key、一把 Key 能管住什么、泄露了怎么办。 来源:https://www.scirouter.cn/docs/authentication · 更新于 2026-09-23 所有 `/v1` 接口都用 API Key 认证,放在 `Authorization` 请求头里: ```http Authorization: Bearer <你的 API Key> ``` - **只认这一种写法。** Cookie、查询参数、`api-key` 头都不认。 浏览器里即使登录着 SciRouter,会话 Cookie 也不会被当成调用凭据——网关收到 `/v1` 请求时会直接丢掉 Cookie 头, 所以别的网页没法借你的登录态替你调模型、花你的额度。 - Key 的明文以 `sk-sr-` 开头,共 46 位。格式不对的请求直接 401 `unauthenticated`。 ## 新建时可以设什么 在[控制台的 API Keys 页面](https://www.scirouter.cn/console/keys)新建。每一项都是这把 Key 自己的边界: | 设置 | 作用 | 越界时 | |---|---|---| | 名称 | 给你自己看;建好后不能改 | — | | 允许的模型 | 白名单,留空表示不限 | 403 `model_not_allowed`;`/v1/models` 也只列白名单里的 | | 月度额度上限 | 这把 Key 每月最多花多少,不填表示不限 | 429 `key_quota_exceeded`,下个月恢复 | | 过期时间 | 到期自动失效,不填表示不过期 | 401 `key_expired` | | 归属项目 | 团队工作区里按项目归账;项目可以设月预算 | 429 `project_budget_exceeded` | 明文**只在创建时显示一次**,我们只存它的哈希。关掉那个窗口之后就再也拿不到了—— 请当场复制,存进密钥管理工具或环境变量。 ## 放进环境变量,不要写进代码 ```bash export SCIROUTER_API_KEY="<创建时复制的那串>" ``` 官方 OpenAI SDK(Python 与 Node)会读 `OPENAI_API_KEY` 与 `OPENAI_BASE_URL` 两个环境变量。 已有的代码不想改一行的话,把这两个变量指向 SciRouter 即可: ```bash export OPENAI_API_KEY="$SCIROUTER_API_KEY" export OPENAI_BASE_URL="https://api.scirouter.cn/v1" ``` ## 不要放进前端 `/v1` 的 CORS 对所有来源开放(不带凭证),技术上浏览器可以直接调—— 但那等于把 Key 发给每一个打开网页的人,任何人都能从开发者工具里拿走它。 浏览器、App、小程序里的调用,请经你自己的后端转发。 [Playground](https://www.scirouter.cn/playground) 和站内对话是例外:它们走的是登录会话,不经过 API Key。 ## 一个用途一把 Key 按服务、环境、实验分开建,好处是三样: - 泄露时只吊销那一把,别的服务不受影响; - 用量与日志按 Key 分开看,谁花的钱一目了然; - 白名单和额度上限可以按用途设——跑批量评测的那把设一个月度上限,失控时最多花掉这么多。 同一把 Key 下还想再细分(比如十个实验共用一把),用请求体里的 `metadata` 贴标签,见「可观测性」。 ## 泄露了怎么办 在控制台**吊销**它:立即生效、不可撤销,正在用它的服务会开始收到 401 `key_revoked`。 然后新建一把换上。 没有「暂停」开关。一把已经泄露的 Key 不存在「先停一下观察观察」——停用期间它仍然是一把有效的钥匙。 ## 一次调用会经过哪些检查 按顺序: 1. **认证**:Key 存在、没吊销、没过期; 2. **授权**:模型在 Key 的白名单里,也在你的套餐里; 3. **额度**:Key 的月度上限、套餐的日上限、项目的月预算; 4. **限频**:每分钟请求数、每分钟 token 数(按估算)、并发数,Key 与账号各算一份; 5. **预扣**:按估算成本先冻结一笔额度,调用结束后按实际用量结算,多退少补。 第 5 步意味着:余额很少时,一个 `max_tokens` 设得很大的请求可能因为**预扣不够**而被拒(402 `insufficient_quota`,`details.required_quota` 是这次要冻结的额度), 哪怕它实际只会用掉一点。调小 `max_tokens` 或充值即可。 每一步的余量都能看到:限频余量在响应头 `x-ratelimit-*` 里,额度在控制台——见「可观测性」。 --- # SDK 与框架 > Python、Node.js、LangChain、LlamaIndex、Vercel AI SDK 的接法,以及我们没有的那些接口。 来源:https://www.scirouter.cn/docs/sdks · 更新于 2026-09-23 接口与 OpenAI 兼容,所以**任何能改 base URL 的 OpenAI 客户端都能直接用**。要改的只有两处: | | 值 | |---|---| | base URL | `https://api.scirouter.cn/v1` | | API Key | 控制台新建的那一把(见「认证与 API Key」) | 下面是常见写法。模型 id 换成你在[模型中心](https://www.scirouter.cn/models)挑好的那个。 ## Python(openai) ```bash pip install openai ``` ```python import os from openai import OpenAI client = OpenAI( api_key=os.environ["SCIROUTER_API_KEY"], base_url="https://api.scirouter.cn/v1", ) # 非流式 resp = client.chat.completions.create( model="bio/protein-72b", messages=[{"role": "user", "content": "这段序列的二级结构倾向?"}], ) print(resp.choices[0].message.content) # 流式 stream = client.chat.completions.create( model="bio/protein-72b", messages=[{"role": "user", "content": "这段序列的二级结构倾向?"}], stream=True, ) for chunk in stream: # 末尾的用量分片与 scirouter 分片 choices 为空,先判再取 if chunk.choices and chunk.choices[0].delta.content: print(chunk.choices[0].delta.content, end="", flush=True) ``` 拿响应头(报障用的 `X-Request-Id`)和扩展字段: ```python raw = client.chat.completions.with_raw_response.create( model="bio/protein-72b", messages=[{"role": "user", "content": "…"}], ) print(raw.headers.get("X-Request-Id")) resp = raw.parse() print(resp.model_dump().get("scirouter")) ``` ## Node.js / TypeScript(openai) ```bash npm install openai ``` ```ts import OpenAI from 'openai' const client = new OpenAI({ apiKey: process.env.SCIROUTER_API_KEY, baseURL: 'https://api.scirouter.cn/v1', }) // 非流式,同时拿到响应头 const { data, response } = await client.chat.completions .create({ model: 'bio/protein-72b', messages: [{ role: 'user', content: '这段序列的二级结构倾向?' }], }) .withResponse() console.log(data.choices[0].message.content, response.headers.get('x-request-id')) // 流式 const stream = await client.chat.completions.create({ model: 'bio/protein-72b', messages: [{ role: 'user', content: '这段序列的二级结构倾向?' }], stream: true, }) for await (const chunk of stream) { process.stdout.write(chunk.choices[0]?.delta?.content ?? '') } ``` **不要在浏览器里这样用**——Key 会暴露给每一个访问者。前端请调你自己的后端。 ## LangChain(Python) ```bash pip install langchain-openai ``` ```python import os from langchain_openai import ChatOpenAI llm = ChatOpenAI( model="bio/protein-72b", base_url="https://api.scirouter.cn/v1", api_key=os.environ["SCIROUTER_API_KEY"], ) print(llm.invoke("这段序列的二级结构倾向?").content) ``` ## LlamaIndex ```bash pip install llama-index-llms-openai-like ``` ```python import os from llama_index.llms.openai_like import OpenAILike llm = OpenAILike( model="bio/protein-72b", api_base="https://api.scirouter.cn/v1", api_key=os.environ["SCIROUTER_API_KEY"], is_chat_model=True, ) print(llm.complete("这段序列的二级结构倾向?")) ``` `is_chat_model=True` 不能省:不写的话它会去调旧的 `/completions` 接口,我们没有这个接口。 ## Vercel AI SDK ```bash npm install ai @ai-sdk/openai-compatible ``` ```ts import { createOpenAICompatible } from '@ai-sdk/openai-compatible' import { generateText } from 'ai' const scirouter = createOpenAICompatible({ name: 'scirouter', baseURL: 'https://api.scirouter.cn/v1', apiKey: process.env.SCIROUTER_API_KEY, }) const { text } = await generateText({ model: scirouter('bio/protein-72b'), prompt: '这段序列的二级结构倾向?', }) ``` ## 图形客户端 各种桌面聊天客户端、IDE 插件里选「OpenAI 兼容」一类的服务商,填 API 地址与 Key 即可。 注意有的客户端要你填到 `/v1` 为止,有的会自己补上 `/v1`——填错会得到 404,换另一种写法再试。 ## 没有 SDK 的语言 直接发 HTTP。请求就是「快速开始」里那条 curl:`POST https://api.scirouter.cn/v1/chat/completions`, `Authorization: Bearer `,JSON 请求体。流式按 SSE 读,要点见「对话补全」。 ## 我们只有这几个接口 `/v1` 下现在是:`GET /models`、`GET /models/{id}`、`POST /chat/completions`、`POST /embeddings`。 旧的 `/completions`、图片、语音、Assistants、Responses 等接口**没有**,调用会得到 404。 框架默认走这些接口时(例如某些版本默认用 Responses API),请把它切到 Chat Completions。 ## 重试与超时 - 官方 SDK 默认会对 429 与 5xx 自动重试两次,并遵守 `Retry-After`——这正是我们希望的行为,不用再包一层; - 推理类模型一次回答可能要几分钟,请把超时设长,或者用流式; - 哪些错误值得重试、哪些不值得,见「错误码」。 --- # 对话补全 > /v1/chat/completions 的每个字段我们怎么处理、流式要注意的四件事、工具调用与中途断开怎么计费。 来源:https://www.scirouter.cn/docs/chat-completions · 更新于 2026-09-23 ```http POST https://api.scirouter.cn/v1/chat/completions ``` 请求与响应都是 OpenAI Chat Completions 的形状。这一页只写**我们怎么处理**这些字段, 字段本身的语义以 OpenAI 的文档为准。 ## 请求体 ```json { "model": "bio/protein-72b", "messages": [ { "role": "system", "content": "你是一名结构生物学助手。" }, { "role": "user", "content": "帮我判断这段序列的二级结构倾向:MKTAYIAKQR…" } ], "max_tokens": 1024, "temperature": 0.3, "stream": false } ``` | 字段 | 必填 | 我们怎么处理 | |---|---|---| | `model` | 是 | 模型 id,见「模型列表」。不存在或已下线 404 `model_not_found`,白名单外 403 `model_not_allowed` | | `messages` | 是 | 不能为空数组。原样转给上游 | | `max_tokens` | 否 | 必须是正整数;**超过模型或套餐的单次上限直接 400**,`details.limit` 给出上限,不会替你悄悄截短。不填时取两者中较小的那个 | | `max_completion_tokens` | 否 | `max_tokens` 的新名字,两个都给时以 `max_tokens` 为准 | | `temperature` / `top_p` | 否 | 原样转给上游 | | `stop` | 否 | 字符串或字符串数组,其他类型 400 | | `stream` | 否 | 见下文「流式」 | | `stream_options.include_usage` | 否 | 流式时在末尾多发一个只带 `usage` 的分片 | | `tools` / `tool_choice` | 否 | 见下文「工具调用」 | | `metadata` / `user` | 否 | 你的归因标签,**不转给上游**,见「可观测性」 | | 其他字段 | 否 | 原样转给上游;厂商专有参数的写法见「多厂商差异」 | 请求体上限默认 4 MB,超过返回 413 `payload_too_large`。 ## 非流式响应 ```json { "id": "chatcmpl-…", "object": "chat.completion", "created": 1758585600, "model": "bio/protein-72b", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "这段序列……" }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 42, "completion_tokens": 318, "total_tokens": 360 }, "scirouter": { "request_id": "req_…", "attempts": 1, "…": "…" } } ``` `scirouter` 字段与 `X-SciRouter-*` 响应头见「可观测性」。 **`finish_reason` 是 `length` 时,回答被 `max_tokens` 截断了。** 它看起来可能很完整—— 一段结构数据、一段代码被截在中间时,解析不报错也不代表内容是全的。批处理请按它判断,而不是看内容长短。 ## 流式 带 `"stream": true`,响应是 `text/event-stream`: ```text : ping data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant","content":""}}]} data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"这段序列"}}]} data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]} data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[],"usage":{"prompt_tokens":42,"completion_tokens":318,"total_tokens":360}} data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[],"scirouter":{"request_id":"req_…","attempts":1}} data: [DONE] ``` 读流时要处理好四件事: 1. **以 `:` 开头的行是注释,跳过。** 排队或上游很久没吐字时,网关每隔一段时间发一行 `: ping`, 防止中间的代理把空闲连接掐掉。官方 SDK 会自动跳过。 2. **`choices` 可能是空数组。** 末尾的用量分片(要了 `include_usage` 才有)和 `scirouter` 分片都是这样, 直接取 `choices[0]` 会越界。 3. **收到 `data: [DONE]` 才算正常结束。** 出错时流里会出现一个 `{"error": {...}}` 分片,之后直接断开、不发 `[DONE]`; 没收到 `[DONE]` 就结束的流一律按失败处理,形状见「错误码」。 4. **错误分片可能出现在第一个内容分片之前。** 心跳一旦发出,HTTP 状态码就已经是 200 了—— 这之后的任何错误(例如排队太久超时)都只能以错误分片出现。只看状态码会把它们全部当成成功。 故障转移只发生在**第一个字节之前**:上游一旦开始吐字,中途失败不会换线路重来(那会让你收到两段拼起来的回答), 而是按上一条给你一个错误分片。 ## 工具调用 `tools` / `tool_choice` 按 OpenAI 的格式写,原样转给上游;模型返回的 `tool_calls` 也原样交还给你。 带 `tools` 的请求**只会发往支持工具调用的线路**。一个模型的所有线路都不支持时,返回 503 `model_unavailable`, `details.reason` 说明原因——这种 503 重试不会好,请换模型。 ## 中途断开与计费 - 非流式请求失败不计费; - 流式请求在吐出一部分之后失败,按**已经产生的用量**计费(上游给了用量就按它,没给就按已转发的文本估算); - 你这边主动断开连接时,网关会同时取消上游,按断开前已产生的部分计费,**结果不保存**—— 所以带 `Idempotency-Key` 重试时会重新调用,见「缓存与幂等」。 ## 相关 - 所有错误码与哪些该重试:「错误码」 - 重复请求直接命中、重试不重复扣费:「缓存与幂等」 - 每次调用打给了谁、慢在哪:「可观测性」 --- # 向量 > /v1/embeddings:输入的写法、默认开着的缓存,以及批量向量化的建议。 来源:https://www.scirouter.cn/docs/embeddings · 更新于 2026-09-23 ```http POST https://api.scirouter.cn/v1/embeddings ``` 把文本变成向量,用于检索、聚类、去重。请求与响应都是 OpenAI Embeddings 的形状。 ## 请求 ```bash curl https://api.scirouter.cn/v1/embeddings \ -H "Authorization: Bearer $SCIROUTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "<向量模型的 id>", "input": ["铁电材料的畴壁迁移", "钙钛矿太阳能电池的稳定性"] }' ``` Python: ```python resp = client.embeddings.create( model="<向量模型的 id>", input=["铁电材料的畴壁迁移", "钙钛矿太阳能电池的稳定性"], ) vectors = [item.embedding for item in resp.data] ``` | 字段 | 必填 | 说明 | |---|---|---| | `model` | 是 | 向量模型的 id,在[模型中心](https://www.scirouter.cn/models)或 `/v1/models` 里找 | | `input` | 是 | 一个字符串,或字符串数组(一次向量化多条) | | `metadata` / `user` | 否 | 归因标签,不转给上游,见「可观测性」 | | 其他字段 | 否 | 原样转给上游(例如 `dimensions`、`encoding_format`,上游支持才生效) | ## 响应 ```json { "object": "list", "data": [ { "object": "embedding", "index": 0, "embedding": [0.0123, -0.0456, …] }, { "object": "embedding", "index": 1, "embedding": [0.0789, 0.0012, …] } ], "model": "<向量模型的 id>", "usage": { "prompt_tokens": 24, "total_tokens": 24 }, "scirouter": { "request_id": "req_…", "cache": "miss", "…": "…" } } ``` `data` 按 `index` 与输入一一对应。按输入 token 计费。 ## 默认走缓存 **向量接口默认开着响应缓存**:同样的模型、同样的输入必然得到同样的向量,第二次起直接返回、**不计费**。 批量向量化一批会重复出现的文本时,这能省下不少。 不想要时带请求头 `X-SciRouter-Cache: disabled`。细节见「缓存与幂等」。 ## 批量的建议 - 一次传一个数组比逐条调用快,也更不容易撞上每分钟请求数的限频; - 但单次请求体不能超过 4 MB(413 `payload_too_large`),上游对单批条数也常有上限—— 超长的列表请切成几百条一批; - 批量任务带上 `Idempotency-Key`,网络抖动后重试不会重复扣费(见「缓存与幂等」)。 --- # 模型列表 > 列出这把 Key 能调用的模型、模型 id 的格式,以及扩展字段的含义。 来源:https://www.scirouter.cn/docs/models · 更新于 2026-09-23 ```http GET https://api.scirouter.cn/v1/models GET https://api.scirouter.cn/v1/models/{model_id} ``` 和 OpenAI 的模型接口同一个形状,另附一个 `scirouter` 扩展字段。 ## 模型 id 形如 `厂商/模型名`,例如 `bio/protein-72b`: - 前一段是**接入这条线路的厂商**,只含小写字母、数字、`.`、`_`、`-`; - 后一段是模型名,可以含大写字母; - 调用时 `model` 字段原样填这一整串。 **不要从展示名推 id**,以列表接口返回的为准。 ## 列出能调用的模型 ```bash curl https://api.scirouter.cn/v1/models \ -H "Authorization: Bearer $SCIROUTER_API_KEY" ``` ```json { "object": "list", "data": [ { "id": "bio/protein-72b", "object": "model", "created": 1757894400, "owned_by": "bio", "scirouter": { "domain": "life-science", "display_name": "Protein 72B", "status": "available", "pricing": [ { "dimension": "input_tokens", "dimension_label": "输入", "unit": "1M tokens", "ratio_micro": 100 } ] } } ] } ``` **只列这把 Key 此刻能调用的模型**:已下线的不列,Key 白名单与套餐之外的不列。 所以同一个账号下两把 Key 拿到的列表可能不一样——这是对的,列表回答的是「用这把 Key 能调谁」。 `scirouter` 扩展字段: | 字段 | 含义 | |---|---| | `domain` | 所属科研领域:`life-science`、`medicine`、`chemistry`、`materials`、`physics`、`math`、`earth`、`agriculture`、`environment`、`engineering`、`cs`、`general` | | `display_name` | 展示名,给人看的 | | `status` | `available`(正常)/ `degraded`(部分线路异常,仍可调用)/ `maintenance`(维护中) | | `pricing` | 计价维度列表。维度不止输入 / 输出两种,见「多厂商差异」;换算成价格以模型详情页和控制台账单为准 | 官方 SDK 会忽略它不认识的 `scirouter` 字段,`client.models.list()` 照常可用。 ## 查一个模型 ```bash curl https://api.scirouter.cn/v1/models/bio/protein-72b \ -H "Authorization: Bearer $SCIROUTER_API_KEY" ``` id 里的 `/` 直接写在路径里即可,写成 `bio%2Fprotein-72b` 也认。 **看不到的模型一律 404 `model_not_found`**,不区分「不存在」和「这把 Key 无权调用」—— 后者如果返回 403,等于告诉任何拿到 Key 的人「平台上有这个模型」。 而调用接口对白名单外的模型返回的是 403 `model_not_allowed`:那时你已经明确点名要它了,告诉你「不许」才有用。 ## 挑模型 列表适合程序读。要**比较**模型——能力、上下文长度、价格、近期可用率——去[模型中心](https://www.scirouter.cn/models), 可以按领域筛选,最多五个并排对比。 --- # 错误码 > 每个错误码该怎么处理,以及哪些值得重试。 来源:https://www.scirouter.cn/docs/errors · 更新于 2026-09-17 错误是 OpenAI 兼容的形状,另附 `request_id`: ```json { "error": { "message": "请求过于频繁,请稍后再试", "type": "rate_limit_error", "code": "rate_limited", "request_id": "req_88a1", "details": { "retry_after_seconds": 12 } } } ``` - `code` 是小写的,按它分支,不要按 `message` 分支——`message` 是给人看的,会改; - 参数错误时 `param` 指出是哪个字段; - `details` 是附加信息(例如 `retry_after_seconds`),官方 SDK 会忽略它,不影响兼容。 **报障时请带上 `request_id`**(响应头 `X-Request-Id` 里也有)。它是我们能定位到那一次具体调用的唯一线索, 没有它只能靠时间范围模糊查。控制台请求日志里按它一搜就是那一条,详情页的「复制报障信息」会把要用的线索一次拼齐——见「可观测性」。 ## 该重试的 | 状态码 | code | 怎么处理 | |---|---|---| | 429 | `rate_limited` | 按响应头 `Retry-After` 等待后重试。不要固定间隔猛重试。 | | 502 | `upstream_error` | 上游返回了错误,指数退避重试。我们内部已经转移过了。 | | 503 | `model_unavailable` | 这个模型暂时没有可用的上游。有 `Retry-After` 就按它等,没有就指数退避。 | | 504 | `upstream_timeout` | 同 502。如果持续出现,考虑调小 `max_tokens`。 | | 409 | `idempotency_conflict` | 带了 `Idempotency-Key`、而同一个键的请求还在处理中:等一会儿再试,并设重试上限(见下表同名的一行)。 | ## 不该重试的 | 状态码 | code | 怎么处理 | |---|---|---| | 400 | `validation_failed` | 请求体有问题,重试多少次都一样。看 `message` 与 `param`。上游拒绝了你的参数时也是这个码。 | | 401 | `unauthenticated` | 没带 Key、格式不对,或 Key 不存在。**不要自动重试**,检查环境变量。 | | 401 | `key_revoked` / `key_expired` | Key 已被吊销或过期。去控制台换一把。 | | 401 | `account_suspended` | 账号已被停用。联系我们,不要重试。 | | 402 | `insufficient_quota` | 配额不足。充值后再试,或联系管理员。 | | 403 | `model_not_allowed` | 这个 Key 的模型白名单里没有你请求的模型。 | | 404 | `model_not_found` | 模型 id 写错了,或该模型已下线。 | | 409 | `idempotency_conflict` | 同一个 `Idempotency-Key` 用在了**不同的请求体**上:多半是代码把键复用错了。和上表「处理中」是同一个 `code`,只有 `message` 不同——有限次重试后仍是 409,就按这一行处理。 | | 413 | `payload_too_large` | 请求体超过上限。拆小再发。 | | 429 | `key_quota_exceeded` | 这把 Key 的月度消费上限用完了,到下个月或在控制台调高上限。 | | 429 | `project_budget_exceeded` | 项目的月预算用完了,`Retry-After` 指向下个月一号。 | 同是 429,`rate_limited` 过几秒就好,另外两个要等到下个月——按 `code` 分支,别只看状态码。 ## 关于重试 我们在网关侧已经做了一层故障转移:一个上游接口超时或返回 5xx 时, 会自动转到下一个可用接口(次数有上限,且总耗时有预算)。 所以**你收到 5xx 时,说明我们这边已经试过不止一次了**。 你自己那层重试请用指数退避,不要立刻重发——那只会让已经拥塞的上游更拥塞。 非流式请求重试时带上同一个 `Idempotency-Key`,就不会因为「其实第一次已经成功了」而被扣两次费—— 前提是第一次没有被你这边先断开(断开的请求服务端会取消、不保存,见「缓存与幂等」)。 ## 流式请求里的错误 流式响应可能在**已经吐出一部分内容之后**才出错。这时候 HTTP 状态码已经是 200 了, 错误以一个带 `error` 字段的分片出现在流里,之后流直接结束,**不会再有 `data: [DONE]`**: ``` data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"这段序列的"}}]} data: {"error":{"message":"上游模型服务响应超时","type":"upstream_error","code":"upstream_timeout","request_id":"req_88a1"}} ``` **请处理这种情况**。只看 HTTP 状态码的话,你会把一段被截断的回答当成完整的; 没收到 `[DONE]` 就结束的流,一律按失败处理。 --- # 缓存与幂等 > 省钱的两层缓存怎么用,以及重试时怎样不被重复扣费。 来源:https://www.scirouter.cn/docs/caching · 更新于 2026-09-17 SciRouter 上有两层缓存,省的东西不一样: | | 上游提示词缓存 | SciRouter 响应缓存 | |---|---|---| | 缓存在哪 | 模型厂商那边 | 我们的网关 | | 命中条件 | 请求**开头一段**和之前相同 | 整个请求和之前**完全**相同 | | 省什么 | 命中部分按更低的「缓存命中价」计,仍然会推理 | 不调用模型,**不计费** | | 要你做什么 | 多数情况什么都不用做(单轮批量调用见下) | 对话请求带一个请求头 | ## 上游提示词缓存 同一段长前缀(系统提示、参考文档、工具定义)反复发送时,上游会把它缓存起来, 命中的部分按模型详情页里的**缓存命中价**计,通常远低于输入价。 有的上游第一次写入缓存时按**缓存写入价**计,略高于输入价,之后几轮命中就省回来了。 网关替你做了能做的: - 需要显式标记缓存位置的上游:输入够长(约 1000 token 起)**且是多轮对话**(system 之外至少两条消息)时, 网关自动在工具定义、系统提示、上一轮末尾和最后一块上打标记; - 支持缓存分组键的上游:网关替你算好分组键再发过去(默认按 API Key 隔离,你在请求里给了 `prompt_cache_key` 就按你的键,仍按账号隔离)。 用量里 `usage.prompt_tokens_details.cached_tokens` 是命中的数量。 **单轮批量调用要自己标。** 一批请求共用同一段很长的系统提示、但每条只有一个问题时,网关不会自动打标记 (它判断不出这段前缀还会不会再用)。在想缓存的那一段内容上加 `cache_control`,网关会原样转给上游: ```json { "model": "bio/protein-72b", "messages": [ { "role": "system", "content": [ { "type": "text", "text": "(很长的评测规则与参考资料……)", "cache_control": { "type": "ephemeral" } } ] }, { "role": "user", "content": "第 17 题:……" } ] } ``` 同一个请求里最多保留 4 个标记(多了留最后 4 个);不需要标记的上游会忽略这个字段。 想让它多命中,你只需要做一件事:**把不变的内容放在前面,变化的内容放在后面**。 把当前时间、随机 id 之类塞进系统提示的开头,会让每次请求的前缀都不一样,一次也命中不了。 模型没有单独标缓存价时,这部分按输入价计——不会因为没标价就不收,也不会多收。 ## SciRouter 响应缓存 对话请求带上 `X-SciRouter-Cache: enabled`,完全相同的请求第二次起直接返回上一次的结果: ```bash curl https://api.scirouter.cn/v1/chat/completions \ -H "Authorization: Bearer $SCIROUTER_API_KEY" \ -H "Content-Type: application/json" \ -H "X-SciRouter-Cache: enabled" \ -d '{ "model": "bio/protein-72b", "temperature": 0, "messages": [{"role": "user", "content": "列出这段序列里所有的跨膜区段"}] }' ``` Python(OpenAI SDK): ```python response = client.chat.completions.create( model="bio/protein-72b", temperature=0, messages=[{"role": "user", "content": "列出这段序列里所有的跨膜区段"}], extra_headers={"X-SciRouter-Cache": "enabled"}, ) ``` **向量接口(`/embeddings`)默认就走缓存**——同样的输入必然是同样的向量。 不想要时带 `X-SciRouter-Cache: disabled`。 适合用它的场景:批量评测反复跑同一套题、向量化一批会重复出现的文本、开发时反复调试同一个请求。 **不适合**要求每次重新生成的场景——命中时你拿到的是同一份回答,一字不差。 带了这个头就会缓存,不看 `temperature`:温度不为 0 时,之后每次拿到的都是第一次那一份随机结果。 ### 请求头 | 请求头 | 取值 | 作用 | |---|---|---| | `X-SciRouter-Cache` | `enabled` / `disabled` | 开启或关闭这次请求的缓存 | | `X-SciRouter-Cache-TTL` | 正整数(秒) | 这次结果保存多久。对话默认 1 小时、向量默认 24 小时;超过 24 小时按 24 小时算 | | `Cache-Control` | `no-cache` | 不读旧结果,调用模型后用新结果覆盖缓存 | | `Cache-Control` | `no-store` | 这次既不读也不写 | `X-SciRouter-Cache` 不是这两个值、`X-SciRouter-Cache-TTL` 不是正整数时直接返回 400。 `Cache-Control` 里其它的值(`max-age` 之类)会被忽略。 ### 怎么知道命中了 响应头 `X-SciRouter-Cache`: - `HIT` —— 来自缓存,没有调用模型,不计费; - `MISS` —— 没命中,正常调用并计费,成功的结果会存下来; - `BYPASS` —— 本该走缓存,但这次请求要求不读(`disabled`、`no-cache`、`no-store`),或者请求形状不能缓存(见下); - 没有这个头 —— 这次请求不涉及缓存。例外:流式请求排队很久、心跳已经先把响应头发出去时, 这个头来不及带上;以请求日志里的「缓存」标记为准。 非流式响应的 `scirouter.cache` 字段是同样的结论(小写)。 控制台的[请求日志](https://www.scirouter.cn/console/logs)里,命中的记录带「缓存」标记,token 与费用都是 0,也不计入用量统计。 ### 什么算「完全相同」 按**规范化之后**的请求体比较: - 键的顺序、空白、`1` 与 `1.0` 这类写法差异不影响; - `stream`、`stream_options`、`user`、`metadata`、`store` 不参与比较—— **流式和非流式共用一份缓存**,非流式存下的结果也能以流式回放; - `max_completion_tokens` 与 `max_tokens` 视为同一个参数,`n: 1` 与不写相同; - 其余任何字段不同(换了模型、改了一个标点、`temperature` 不同)都是另一个请求。 平台调整了这个模型背后的上游(换了接口或模型版本)之后,旧结果自动不再命中。 ### 什么不会被缓存 - 要多个候选(`n` 大于 1)或要逐 token 概率(`logprobs` / `top_logprobs`)的请求:回放给不出这些,响应头是 `BYPASS`; - 失败的调用,以及被截断(`finish_reason` 为 `length`)或内容为空的回答; - 太大的结果(对话超过 256 KB、向量超过 2 MB)。 ### 隔离与计费细节 - 缓存**按 API Key 隔离**:同一账号下的两把 Key 互相看不到对方的缓存。 - 命中也占每分钟请求数(RPM),不占每分钟 token 数与并发。 - 只对 `/v1` 的调用生效;Playground 与 Chat 不走响应缓存(在那里点「重新生成」,就该真的重新生成)。 ## 幂等:重试不重复扣费 网络抖动时你多半会重试。为了不让一次重试变成两次计费,给请求带一个你自己生成的 `Idempotency-Key`: ```python import uuid key = str(uuid.uuid4()) # 同一个逻辑请求的所有重试用同一个值 response = client.chat.completions.create( model="bio/protein-72b", messages=[{"role": "user", "content": "帮我判断这段序列的二级结构倾向"}], extra_headers={"Idempotency-Key": key}, ) ``` `/chat/completions` 与 `/embeddings` 都支持,规则是: - 键最长 128 个字符,**按 API Key 隔离**,保留 24 小时; - 同一个键、同一个请求体:直接返回第一次的响应,带响应头 `Idempotent-Replayed: true`,不再调用模型、不再计费; - 第一次还没处理完时,同一个键再来:返回 409 `idempotency_conflict`; - 同一个键、不同的请求体:也是 409 `idempotency_conflict`——这多半是你的代码把键复用到了别的请求上。 两种 409 的 `code` 相同,只有 `message` 不同;确定是重试同一个请求时,按退避等一会儿再试、并设上限; - 只记住成功(2xx)的响应。失败的请求可以用同一个键修正后重试; - **你这边先断开的请求不算数**:客户端超时断开时,服务端会取消这次调用、不保存结果, 之后同一个键的重试会重新调用模型、照常计费。所以客户端超时要设得比模型的正常耗时长。 **流式请求的幂等只管「进行中」**:流还没结束时,同一个键的重试会得到 409; 流结束之后,同一个键会重新执行一次(照常计费)。流式响应没法完整重放,这是有意的取舍。 幂等和响应缓存是两回事:幂等只认你给的键,保证「同一个请求只算一次」; 响应缓存认的是请求内容,让「不同时间发出的相同请求」共用结果。两个可以一起用。 --- # 可观测性 > 每次调用打给了谁、试了几次、慢在哪、离限额还有多远、这个实验花了多少——从响应头、响应体到控制台的日志、用量与导出。 来源:https://www.scirouter.cn/docs/observability · 更新于 2026-09-22 每一次调用,SciRouter 都会把「实际发生了什么」交还给你:响应头与响应体里给当下这一次的事实, 控制台的请求日志里给完整的时间线与尝试明细。**全部是 OpenAI 形状之外的附加项**,官方 SDK 会忽略它不认识的字段,你什么都不用改就能拿到。 ## 响应头 非流式响应带这一组: | 头 | 含义 | |---|---| | `X-Request-Id` | 这次调用的编号。**报障时带上它**,它是我们定位到那一次具体调用的唯一线索 | | `X-SciRouter-Model` | 实际服务的模型 id | | `X-SciRouter-Provider` | 实际服务这次调用的供应商 id。同一模型可能挂在多家供应商的线路上,失败时会自动转移 | | `X-SciRouter-Attempts` | 上游尝试总数(含跳过的)。`2` 及以上说明发生过自动转移;费用只按最终成功的那次结算 | | `X-SciRouter-Upstream-Request-Id` | 供应商返回的请求 id(有才给)。拿供应商工单时用它对账 | | `X-SciRouter-Cost-Quota` | 本次消耗的配额。流式响应给不出准确值,所以**只有非流式有** | | `X-SciRouter-Cache` | `HIT` / `MISS` / `BYPASS`,见「缓存与幂等」 | 限额余量与 OpenAI 同名,你的 SDK 里已有的退避逻辑直接认: | 头 | 含义 | |---|---| | `x-ratelimit-limit-requests` / `x-ratelimit-remaining-requests` / `x-ratelimit-reset-requests` | 每分钟请求数的上限 / 余量 / 到当前窗口结束还有多久(如 `42s`) | | `x-ratelimit-limit-tokens` / `x-ratelimit-remaining-tokens` / `x-ratelimit-reset-tokens` | 每分钟 token 数,同上 | 余量按你的 Key 与账号两级里更小的那一级给。**没有限额时这组头不出现**(不会给你一个 0 让你误以为该退避了)。 流式响应的限额头在第一个分片之前就写好了;被限频的 429 响应仍带 `Retry-After` 与 `error.details.retry_after_seconds`。 浏览器里跨源调用时这些头都在 `Access-Control-Expose-Headers` 里,`fetch` 读得到。 ## 响应体里的 `scirouter` 字段 非流式响应的顶层多一个 `scirouter` 对象: ```json { "id": "chatcmpl-…", "choices": [ … ], "usage": { … }, "scirouter": { "request_id": "req_…", "model": "deepseek/deepseek-r1", "provider": "p_…", "cost_quota": 18240, "cache": "miss", "attempts": 2, "upstream_request_id": "…", "timings": { "queue_wait_ms": 830, "ttfb_ms": 412, "total_ms": 5688 } } } ``` - `timings.queue_wait_ms`:在供应商渠道的容量队列里等了多久(没排队为 0)——这一段慢**不是模型慢**,是厂商给的容量满了; - `timings.ttfb_ms`:成功那次尝试从发出到收到上游首字节;没走到上游时为 null; - `timings.total_ms`:端到端总耗时。 **流式响应**在 `data: [DONE]` 之前多一个 `choices` 为空的分片,带同一个 `scirouter` 字段(形状和 OpenAI 自己的最后一个 usage 分片一样,SDK 都认): ```text data: {"id":"chatcmpl-…","object":"chat.completion.chunk","created":…,"model":"…","choices":[{"delta":{"content":"…"},…}]} data: {"id":"chatcmpl-…","object":"chat.completion.chunk","created":…,"model":"…","choices":[],"scirouter":{"request_id":"req_…","attempts":1,…}} data: [DONE] ``` 上游一个分片都没吐就结束的流不会有这个分片。 ## 控制台里的请求日志 `/console/logs` 能按 `request_id` 直接查一条,也能按结束原因、来源、慢请求、缓存命中、错误码、自定义时间区间(最长 90 天)筛。 每条记录点开是**调用详情**: - **时间线**:准入 → 排队 → 路由 → 每次尝试 → 上游首字节 → 推流 → 结算,各段多久一目了然; 两个首字节(你看到的 / 上游给出的)各一根标记线,差值就是花在 SciRouter 侧的时间; - **路由**:实际供应商、每次尝试的结果与耗时; - **诊断提示**:只解释已有的事实——被 `max_tokens` 截断、主要耗时在排队、发生过转移、用量为估算、客户端中途断开; - **复制报障信息**:一键把 `request_id`、时间、模型、错误码、供应商请求 id 拼成一段,贴给我们即可。 ## 归因:给调用贴标签 一个 Key 下跑十个实验,账单只能按 Key 分。在请求体里带 OpenAI 原生的 `metadata` 字段(字符串键值对), 每次调用就带上了你自己的归因标签: ```python client.chat.completions.create( model="deepseek/deepseek-r1", messages=[{"role": "user", "content": "…"}], metadata={"exp": "abc", "run": "12"}, user="researcher-42", ) ``` ```ts await client.chat.completions.create({ model: 'deepseek/deepseek-r1', messages: [{ role: 'user', content: '…' }], metadata: { exp: 'abc', run: '12' }, user: 'researcher-42', }) ``` - 上限:**最多 8 个键**,键只能是 1–32 位的小写字母、数字、`_`、`.`、`-`,值最长 64 个字符、不能含换行;超限 400。 - `metadata` 与 `user` **都不会转发给模型供应商**——它们是给你自己看的,落在控制台日志里。 - 日志页可以按标签筛(`?tag=exp:abc`,点一下记录上的标签即可),用量页可以按标签分组看花费、错误率、截断率(窗口 ≤ 30 天)。 - 站内对话(Chat)的每次调用自动带 `chat_session` 标签,值是会话 id:日志页能从一条消息跳到那次调用,也能筛出一场对话的全部调用。 ## 一段时间:用量页的维度 `/console/usage` 可以按**模型 / Key / 项目 / 来源 / 状态 / 标签**分组,看请求数、Tokens、配额、错误率、截断率, 按模型时还有 P50 / P95 延迟;开「与上期对比」会把上一个等长窗口画在同一张图上。窗口有近 24 小时 / 7 天 / 30 天 / 90 天,或自定义区间(最长 90 天)。 两条口径:**请求数不含响应缓存命中**(命中单独计数,它们没有调用模型、不计费);**延迟分位只在按模型分组时给**—— 分位数不能跨模型合并,合并出来的数字没有定义,所以其它分组那一列写的是「按模型查看」,不是 0。 ## 此刻:离限额还有多远 控制台概览页的「实时」卡每 10 秒刷新一次:每分钟请求 / 每分钟 Token 当前窗口用了多少(与 `x-ratelimit-*` 头同一组读数)、 并发在途几个、正在跑的是哪几个请求、近 1 小时错了些什么。接口是 `GET /console/limits/live`,可选 `?keyId=` 多看一级某个 Key 的读数。 ## 导出 日志页的「导出当前筛选」按当前筛选把记录导成 CSV(UTF-8 BOM,Excel 直接开)或 JSON Lines,**单次上限 50 000 行**, 超过会先告诉你命中了多少条、让你缩小范围,不会导出一半。文件里**没有提示词与返回片段**,也没有请求参数; 时间列按你账号设置的显示时区,列名里写着时区(如 `created_at (Asia/Shanghai)`)。 ## 出事主动说:调用告警 账号页「通知」里有五个调用告警,默认开着、选了渠道就会发(每 5 分钟扫一次,冷却见括号): | 通知项 | 触发 | 冷却 | |---|---|---| | 错误率飙升 | 某把 Key 近 15 分钟错误(不含限频)超过 20% 且样本 ≥ 20 | 每把 Key 1 小时 | | 延迟劣化 | 某模型近 15 分钟 P95 比近 7 天基线高出一倍以上且样本 ≥ 20 | 每个模型 1 小时 | | 频繁被限频 | 近 15 分钟 429 ≥ 30 次 | 1 小时 | | 配额即将耗尽 | 按近 24 小时的消耗速度,剩余配额撑不过 3 天 | 24 小时 | | 回答频繁被截断 | 某模型近 1 小时被 `max_tokens` 截断的回答超过 30% 且样本 ≥ 20 | 每个模型 24 小时 | 正文只有数字与名字,没有链接(提醒里的链接是钓鱼的样子);每条都写明该去控制台哪一页看。 ## 接进你自己的系统:通用 webhook 通知渠道除了企业微信 / 飞书 / Bark,还可以是你自己的 https 地址(公网;本机与内网地址会被拒绝)。建好渠道时会**显示一次签名密钥**(`whsec_…`),之后读不回,换密钥要删掉重建。 每条提醒是一次 JSON POST: ```json { "id": "whd_01HZX…", "event": "error_rate_spike", "scope": "user", "subject": "错误率飙升", "text": "…纯文本…", "markdown": "…Markdown…", "data": { "key": "生产", "percent": "35", "requests": "40", "errors": "14", "topCode": "UPSTREAM_ERROR", "window": "15 分钟", "site": "SciRouter", "time": "2026-09-22 10:30" }, "site": "SciRouter", "time": "2026-09-22T02:30:00Z" } ``` 头:`X-SciRouter-Event`(通知项 id;试发是 `test`)、`X-SciRouter-Delivery`(投递 id,重试时不变,据此去重)、 `X-SciRouter-Signature: t=,v1=`。验签三行: ```python t, v1 = parse("X-SciRouter-Signature") # "t=1726990000,v1=…" expected = hmac_sha256(secret, f"{t}.{raw_body}").hexdigest() ok = hmac.compare_digest(expected, v1) and abs(time.time() - int(t)) < 300 ``` 你的接收端返回任意 2xx 即成功;5 秒没回或非 2xx 会再试 3 次(0.5 / 1 / 2 秒后)。**连续失败 10 次渠道会自动停用**, 原因写在渠道的「上次失败」里,修好地址后重新启用即可。`data` 里的键与通知模板的变量同名,后台改模板不影响它。 ## 接进你的监控:Prometheus 自部署时网关的 `/metrics` 给出调用指标(只给内网抓,对外 404): `scirouter_request_duration_seconds{model,source,status}`、`scirouter_request_ttfb_seconds{model,source}`、 `scirouter_tokens_total{model,direction}`、`scirouter_attempts_total{provider,outcome}`、 `scirouter_ratelimited_total{scope}`、`scirouter_finish_reason_total{model,reason}`。 标签只到模型 / 供应商 / 来源 / 状态,不含用户与 Key;仓库里带一份 Grafana 面板(`scripts/grafana/scirouter.json`)。 按用户、按 Key 看请用控制台的用量页与日志页。 ## 几个容易读错的地方 - `finish_reason: "length"` 是**被 max_tokens 截断**,不是模型故障。需要完整回答就调大 `max_tokens`。 - `attempts` 数的是尝试,不是失败:`2` 可能是一次失败 + 一次成功,也可能是一条渠道满了被跳过 + 一次成功。 - 首字节有两个口径。SDK 里量到的首字节含网络与边缘那一段;`timings.ttfb_ms` 是网关到上游的那一段。判断「模型快不快」看后者。 - 响应缓存命中(`cache: "hit"`)的调用没有上游、不计费,`attempts` 为 0,`timings.ttfb_ms` 为 null。 ## 隐私 请求日志里的提示词与返回片段按你在账号设置里的保留期保存,超期只剩统计列;日志不会存到浏览器里。 供应商只给名称,不给接口地址与线路信息。 --- # 多厂商差异 > 统一入口已经把协议差异吃掉了;这一页只讲你确实需要知道的那部分。 来源:https://www.scirouter.cn/docs/vendors · 更新于 2026-09-14 平台上的模型来自不同厂商,各家的原生协议不完全一样。 **但你基本不需要关心这件事**:经 SciRouter 的统一入口调用时,一律是 OpenAI 格式。 协议差异是我们的问题,不是你的问题。 这一页讲的是那少数几处你确实会碰到的差异。 ## 兼容度标识 每个模型的详情页顶部有一行兼容度标识,三种: - **完全兼容** —— 现有的 OpenAI 代码改一个 `base_url` 就能用。 - **兼容,专有参数需放 extra_body** —— 标准字段照常用;这家厂商特有的参数 放进 `extra_body` 透传。 - **需专用调用方式** —— 经我们的统一入口**仍然是 OpenAI 格式**; 只有直连厂商时才需要它的原生协议。 ## 厂商专有参数 标准字段(`temperature`、`top_p`、`max_tokens`…)对所有模型都一样。 厂商特有的参数走 `extra_body`: ```python response = client.chat.completions.create( model="bio/protein-72b", messages=[{"role": "user", "content": "..."}], temperature=0.7, extra_body={ "beam_width": 4, }, ) ``` **哪些参数可用、各自的取值范围,以模型详情页的参数面板为准。** 那个面板是由后端下发的能力描述符渲染的,永远和实际可用的参数一致—— 文档里抄一份必然会过期。 不支持的参数会被**忽略并在响应里说明**,不会静默丢弃。 ## 计价维度不止两个 不要假设「输入价 + 输出价」两个数字。各家口径差别很大: - 缓存命中的输入单独计价(通常便宜一个数量级); - 推理(thinking)token 单独计价; - 图片按张、音频按秒、有些按每次请求收固定费; - 阶梯价与最小计费单位。 用量响应里的 `usage` 是**按维度返回**的,维度名由服务端给。 你的对账代码请按「遍历维度」写,不要硬编码 `input_tokens` / `output_tokens` 这两个键——接入新厂商时会出现你没见过的维度。 ## 流式 所有模型都支持流式(`stream: true`),分片格式与 OpenAI 一致。 一处差异:部分模型会在内容之前先发一个 `selection` 分片,说明**实际命中了哪个模型** (Auto 模式下)。不认识的分片类型请忽略而不是报错——我们会继续往里加。