后端扩展
后端扩展是部署方在网关启动时加载的可信 Python 模块。它可以新增一种 adapter kind、决定谁能使用云端 Agent,或者上报 provider 的配额。本页写给编写扩展的开发者;如果只是改配置,见发行版定制。
加载扩展
BACKEND_EXTENSIONS 是一个可选的列表,用逗号分隔,列出受信任的本地 Python 模块名,默认为空。每个模块都必须提供一个不带参数的同步函数 register()。在每个进程里,网关对每个模块只导入、注册一次,时机是加载 dotenv 之后、构造运行时路由或读取它们的注册表之前。导入或注册一旦失败,启动就会中止;网关不会悄悄换用别的 adapter。
扩展通过 serving.servers.registry.register_adapter_factory(kind, factory, *, override=False) 注册工厂。工厂接收一个配置字典,返回一个 adapter;这一步发生在应用内置的 provider 默认值之前。如果某个 kind 已经被别的扩展注册过,再注册会报错。要替换内置 kind,必须传 override=True,并且会记一条日志。注册过的 kind 同时会成为保留的 provider 标签。启动时调用的 register() 可以赶在使用方运行之前,往现有的运行时设置字典和 provider 元数据字典里填充内容;但只能原地修改这些共享字典,不能整个替换掉。
部署方必须保证这些模块能被导入,比如放在只读挂载的 overlay 里。这里执行的是可信的服务端代码,不是用户提供的配置:不要从请求里推导模块名,也不要让管理表单来选模块。加载器不会拉取远程模块,也不会按请求加载代码。所有扩展代码都要在启动时导入,之后不要再延迟导入或热重载代码;改了挂载的扩展,要重启后端。最小的工厂示例见部署本地 adapter。
云端 Agent 访问权限
角色层级由网关固定,账号的当前状态也由网关查询;谁能使用、谁能管理部署自己的云端 Agent,则由部署方决定。受信任的扩展在 register() 里调用 serving.agent_access.register_agent_access_policy(policy),注册一个同步回调。网关先确认用户存在、状态为 active,再把一个只读映射传给回调,里面只有 user_id 和 role 两项。回调拿不到用户资料、凭据,也拿不到数据库连接。
回调返回一个 list[str] 或 tuple[str, ...],逐项列出授予的权限:agent.use 表示可以使用 Agent,agent.admin 表示可以管理 Agent。管理 Agent 需要同时具备这两项权限。未知的、重复的或不是字符串的权限都算无效,网关也不会替你做类型转换。注册第二个策略会报错。没有注册任何策略时,所有角色都无权访问 Agent。角色怎样对应到权限,网关不做任何针对具体部署的规定。
独立部署的 Agent 以现有的 GATEWAY_GRANT_DISPATCH_TOKEN 作为 bearer token,调用 GET /internal/users/{user_id}/agent-access。成功的响应只有 user_id、allowed 和 permissions 三个字段;当且仅当权限里有 agent.use 时,allowed 才为 true。权限按 agent.use、agent.admin 的顺序返回。策略明确拒绝时,返回的是 HTTP 200,内容为 allowed: false 和 permissions: []。这个端点每次请求都会读网关的运行数据存储、重新执行策略,不缓存权限判定。账号信息最多可能滞后 60 秒:经由本网关进程做的修改马上就能看到;直接改数据库、或者由其他进程做的修改,要等缓存的账号信息过期后才能看到。
账号不存在或未激活时,网关在调用回调之前就返回 HTTP 403,错误类型为 subject_unavailable。回调抛出异常或返回无效结果时,返回 HTTP 503,错误类型为 agent_access_unavailable,不附带策略细节。两种情况都使用网关标准的 {"error": {"type": ..., "message": ...}} 错误格式。请求没有带有效的 dispatch token 时返回 401;网关关闭了内部 API 时返回 404。
配额上报
管理控制台的 Providers → Quotas 标签页和 RouteWise 的配额感知路由,都从配额来源读取用量。网关本身不带任何配额来源:由后端扩展为每个 provider 注册一个,读取该 provider 的用量 API,或者你自己的计量服务。整个机制不依赖特定的供应商,也不需要网站登录或 cookie。
扩展的 register() 调用 serving.admin.provider_quotas.register_quota_fetcher(provider, display_name, fetch)。调用方会以 fetch(operational_store, services) 的形式 await fetch,它为每个已配置的 key 返回一个 ProviderQuotaResult;自身出错时也要转成结果返回,而不是抛出异常。管理端点调用时会传入存储和服务对象;RouteWise 目前传的是 (None, None),所以 fetcher 拿不到这两个对象时也必须能正常工作。注册在启动时执行,只导入模块并不会注册。fetcher 只查询用量,不会发送用户的推理请求,也不会改变路由的推理协议。
每个结果包含若干 ProviderQuotaUsage 行,字段有 label、used、limit、unit,以及可选、带时区的 reset_at。来源靠部署自己定义的 provider 标识和路由对应起来,这个标识不是写死的厂商枚举。quota_source.provider、usage_label 和 unit 必须与返回的某个结果及其中的某一行用量完全一致。上报的应该是这个配额池共享的账号和时间窗口;彼此独立的账号要放在不同的来源里。不要把互不相关的 key 的用量加在一起,也不要把查询失败报成零用量。
UI 可以显示百分比、货币和其他单位,但 RouteWise 的配额准入每次扣减一个请求,所以需要一个与之兼容、按次计数的来源。光有百分比,算不出还能发多少请求。窗口边界和重置时间由来源决定;quota.limit 只用来和来源报告的上限做交叉核对,不能代替来源的实测值。一次快照都没成功过的来源,会一直处于未就绪状态。刷新失败不会凭空造出新的余额;已有的池会沿用上一次成功的快照,加上本地累计的增量。
运行本地配额来源
完整的示例扩展从仓库自带的假 provider 读取 /usage。它在内存里维护的每日计数器仅用于演示:进程重启或到 UTC 零点时就会清零,不能用作生产环境的计量核算。它不使用任何真实账号或凭据,只有通过 BACKEND_EXTENSIONS 显式选中时才会加载。
配好开发环境后,打开三个终端,都在仓库根目录下执行下面的命令。先启动一个模拟的配额 provider:
uv run python distributions/example/fixtures/fake-openai-provider/server.py \
--port 18353 --response-text ROUTED_TO_QUOTA --quota-limit 100
再启动一个回退路由,它更慢,而且带价格(示例里的价格是虚构的):
uv run python distributions/example/fixtures/fake-openai-provider/server.py \
--port 18352 --response-text ROUTED_TO_FALLBACK --ttft-delay-ms 400
最后用配额注册表启动网关,这份注册表只有显式指定时才会用到。先把演示 token 存进一个变量,下面调用管理接口时复用它:
export DEMO_ADMIN_TOKEN=local-quota-demo-only
PYTHONPATH=.:apps/backend \
PYTHON_DOTENV_DISABLED=1 \
BACKEND_EXTENSIONS=distributions.example.quota_extension \
EXAMPLE_QUOTA_BASE_URL=http://127.0.0.1:18353 \
MODELS_CONFIG_PATH=config/examples/models.routewise.quota.yaml \
ROUTING_CONFIG_PATH=config/examples/routing.minimal.yaml \
DB_ENABLED=false USER_AUTH_ENABLED=false ADMIN_TOKEN="${DEMO_ADMIN_TOKEN}" \
JWT_SECRET_KEY=local-quota-demo-signing-secret-not-for-production \
uv run uvicorn serving.servers.app:app --no-proxy-headers --host 127.0.0.1 --port 18080
这些设置关闭了 dotenv 加载和用户认证,用的管理 token 和 JWT 签名密钥都是公开的、仅供演示:只能监听回环地址,也绝不能用在真实部署里。即使用演示 token 认证,管理端点也需要这个签名密钥。读取控制台用的同一份结果:
curl http://127.0.0.1:18080/admin/provider-quotas \
-H "Authorization: Bearer ${DEMO_ADMIN_TOKEN}"
发几个请求来校准成本包络,并等第一轮探测和快照(每五秒一次)跑完:
curl http://127.0.0.1:18080/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"quota-demo","messages":[{"role":"user","content":"hi"}]}'
从响应内容可以看出选中的是哪个模拟服务。一开始的请求走回退路由,直到配额路由既有了用量快照,又有了成本包络的依据。成功受理的聊天请求(包括主动探测)会消耗模拟配额;健康检查、模型发现和读取 /usage 不会。要模拟额度耗尽,只需用 --quota-limit 100 --quota-used 100 重启配额模拟服务;下一次快照刷新后,请求会继续走回退路由。用完后按 Ctrl+C 停掉各个本地进程。
要在登录后的 Web 控制台/管理控制台里看到这张卡片,请在本地的全栈演示后端上配置同一个扩展和模型注册表,然后打开 Providers → Quotas。配额端点必须能从该后端访问到:在 Compose 里,127.0.0.1 指的是后端容器,而不是宿主机。全栈演示及其认证见快速开始。没有返回任何结果时,Quotas 标签页仍然可见,并链接回本页;Overview、Keys、Availability 和 Performance 这几个标签页不需要任何配额来源也能正常使用。
对接你自己的数据源时,复制这个扩展,把其中的本地 HTTP 查询和字段映射换成你获得授权的数据源。保持结果契约不变,给网络请求加上超时,显式处理失败,并确保凭据不出现在响应和日志中。至于能以什么方式查询 provider 的用量,仍以该 provider 自己的条款为准。