Skip to content
面向多 LLM 接入的模型管理平台设计
模型管理平台位于应用和多个模型供应商(Provider)之间。它接收来自应用的模型调用请求,选择目标模型,转发给上游供应商,并把响应返回给调用方。平台还负责 API Key 的签发与校验、上游供应商凭据的安全托管、模型配置的集中管理,以及调用量、Token 用量和成本的统一记录。这类系统在不同的项目里也被称为 LLM Gateway 或 AI Gateway。
1. 模型管理平台概述
1.1 要解决的核心问题
直接集成多个 LLM Provider 时,应用层需要重复处理下面几类问题:
- 每个 Provider 的 API 协议不同,包括鉴权方式、请求路径、请求体和错误结构。
- 上游 API Key 分散在业务代码、配置文件和环境变量中,难以统一轮换和审计。多个 Provider 并存时,有可能出现用 A 的密钥请求 B 的问题[3]。
- 每个 Provider 对 Token 计费、限流响应和错误码都有自己的定义,统一监控需要先做一层归一化。
- 模型切换或灰度时,如果模型名直接散落在业务代码里,改动成本高。
模型管理平台把这些问题集中到网关层,应用只需要调用统一的模型接口。
1.2 与普通 API 网关的差异
普通 API 网关关注 HTTP 路由、认证、限流和负载均衡,请求转发往往不关心请求体语义。模型管理平台除了这些能力,还要关注:
model字段的映射:请求中的模型 ID 与 Provider 侧模型名可以不同。- 协议转换:把统一请求格式转成不同 Provider 的请求体。
- Token 用量:每个请求的输入和输出 Token 数量是计费和监控的基础。
- 流式响应:SSE(Server-Sent Events)的转发与中断。
- 成本核算:不同模型单价不同,需要根据 Token 用量估计成本。
因此,模型管理平台可以看成是在通用 API 网关上增加了一层面向 LLM 语义的适配层。
2. 总体架构与核心模块
2.1 模块划分
平台内部的逻辑模块可以划分为:
- 接入/路由层:接收
POST /v1/chat/completions等统一请求,完成鉴权、配额检查、模型路由和协议转换。 - 配置中心:保存 Provider 配置、模型配置、路由规则和租户配额。
- 密钥管理服务:生成和校验调用方 API Key,加密存储上游凭据。
- 监控/观测模块:记录请求日志、指标、链路追踪和审计日志。
- 存储:关系数据库保存配置与调用日志;Redis 保存配额计数和配置变更消息;KMS 保存主密钥。
逻辑模块之间可以按规模和团队边界决定是否独立部署。本文中的示例以单个服务内的模块划分来描述。
2.2 一次请求的流转
一次成功的调用会经过以下步骤:
- 调用方把平台签发的 API Key 放在
Authorization: Bearer <key>请求头中访问网关。 - 网关校验 Key,得到租户、应用、权限范围(scope)。
- 配额模块预检租户或应用的今日用量。
- 路由模块根据请求中的模型 ID、租户等级等条件选择目标模型。
- 配置中心取出目标模型和 Provider 配置,密钥服务解密该 Provider 的上游凭据。
- 对应该 Provider 的 Adapter 把统一请求转成上游格式并发送。
- 响应转换回统一格式,同时记录状态码、延迟、Token 用量和估算成本。
内部可以用下面的类型表示入口请求:
ts
interface GatewayRequest {
requestId: string;
tenantId: string;
appId: string;
apiKeyId: string;
model: string; // 网关内部模型 ID
messages: ChatMessage[];
stream: boolean;
}后续章节以该类型为基础展开说明。
3. API Key 生命周期管理
3.1 调用方 Key 与上游凭据分离
平台中需要区分两类凭据。
调用方 API Key:由平台签发,给应用调用网关时使用。它绑定租户、应用和权限范围,可以吊销和轮换。
上游凭据:用户在模型供应商控制台创建的 API Key,交给平台保管。平台在所有请求中代替调用方持有和使用这些凭据。
两类凭据必须分离。如果调用方直接持有上游凭据,上游 Key 就会散落在多处,平台无法统一审计和轮换。crawl4ai 的案例说明了这种错配的典型表现:调用方同时配置了 Gemini 模型和 OpenAI API Key,结果请求被 OpenAI 拒绝[3]。使用模型管理平台后,Gemini 模型配置中绑定 Gemini 的凭据,调用方 API Key 只是平台身份凭证,不再与具体供应商相关。
3.2 密钥的生成与加密存储
调用方 API Key
调用方 Key 的格式可以自行定义。常见做法是“前缀 + 随机串”,例如 sk-8f3d...。随机部分应使用密码学安全随机数生成器。数据库不保存 Key 明文,保存 SHA-256 哈希。
ts
import { createHash, randomBytes } from 'node:crypto';
function generateCallerKey(prefix = 'sk-'): { key: string; hash: string } {
const raw = randomBytes(32).toString('base64url');
const key = `${prefix}${raw}`;
const hash = createHash('sha256').update(key).digest('hex');
return { key, hash };
}调用方创建 Key 时,接口只返回一次明文 key。之后请求鉴权都通过数据库里的 hash 做匹配。
上游凭据
上游凭据需要转发给 Provider,所以必须能还原出明文。平台使用对称加密保存,示例使用 AES-256-GCM。GCM 模式会同时输出认证标签,用于校验密文是否被篡改。
ts
import { createCipheriv, randomBytes } from 'node:crypto';
function encryptCredential(plaintext: string, masterKey: Buffer) {
const iv = randomBytes(12);
const cipher = createCipheriv('aes-256-gcm', masterKey, iv);
const ciphertext = Buffer.concat([
cipher.update(plaintext, 'utf8'),
cipher.final(),
]);
const authTag = cipher.getAuthTag();
return {
iv: iv.toString('hex'),
ciphertext: ciphertext.toString('hex'),
authTag: authTag.toString('hex'),
};
}主密钥从 KMS 或进程环境变量读取,不写入数据库。加密后的密文、IV 和认证标签一起存入 provider_credentials 表。
注意:调用方 Key 与上游凭据的存储目标不同。前者只允许单向哈希,后者需要可逆加密。不要把两者混用。
3.3 鉴权、租户隔离与配额控制
鉴权流程
网关从请求头中提取调用方 Key,计算 SHA-256 后查询 api_keys 表。Key 不存在、已吊销或已过期时返回 401。OpenAI 的错误码文档把鉴权失败细分为 Invalid Authentication、Incorrect API key provided 等情况,平台可以在错误响应的 code 字段保留类似的细分语义[1]。
鉴权通过后,请求上下文包含以下主体:
ts
interface KeyPrincipal {
keyId: string;
tenantId: string;
appId: string;
scopes: string[];
}细粒度权限
调用方 Key 可以附加一组 scope。网关在处理具体操作时检查所需 scope。下面是一个示例,llm:chat.stream 表示带流式能力的模型调用权限,仅用于演示 scope 的命名方式:
ts
const REQUIRED_SCOPE: Record<string, string> = {
'/v1/chat/completions': 'llm:chat',
'/v1/embeddings': 'llm:embed',
};
function authorize(principal: KeyPrincipal, operation: string): boolean {
const required = REQUIRED_SCOPE[operation];
return principal.scopes.includes('*') || principal.scopes.includes(required);
}scope 可以按“资源:动作”组织,例如 llm:chat.stream、config:read。管理后台与模型调用 API 使用不同的 scope,避免同一个 Key 既能调用模型又能改配置。
租户隔离
tenantId 是数据隔离的基本边界。调用方 Key 只属于一个租户;路由规则、模型配置和用量统计都要按 tenantId 过滤。数据库查询始终带租户条件,避免跨租户读取配置。
配额控制
配额可以按请求次数、Token 数或金额计算。一个常见方案是按天累计 Token 用量。每次请求前做预检,请求结束后用实际 Token 数扣减。
ts
async function precheckQuota(tenantId: string, estimateTokens: number): Promise<boolean> {
const used = Number(await redis.get(`quota:${tenantId}:tokens:${today()}`) ?? 0);
return used + estimateTokens <= tenantQuota(tenantId);
}配额不足时返回 HTTP 429,并可以在响应头中附带 Retry-After(单位:秒),提示客户端等待时长。
注意:预检和实际扣减之间存在窗口,配额计数不是强一致。对成本敏感的场景,应该在请求前预留配额,请求后按实际用量修正。
3.4 轮换与吊销
吊销调用方 Key 时,把 api_keys.status 置为 revoked,并向缓存发送失效事件。3.3 的鉴权逻辑已经检查 status 字段,因此 revoke 后的 Key 与过期 Key 一样返回 401,避免暴露 Key 的存在性。
轮换调用方 Key 包括三个步骤:
- 签发新 Key 给调用方。
- 调用方完成切换。
- 吊销旧 Key。
上游凭据的轮换通常采用主备方式。在 Provider 控制台创建新 Key 后,把新 Key 写入配置的 primary 字段,旧 Key 保留在 secondary 字段。网关优先使用 primary;如果上游返回认证错误,且错误码明确表示凭据无效,可以使用 secondary 重试一次。确认新 Key 稳定后,清除 secondary。
ts
async function rotateProviderCredential(
config: ProviderConfig,
newApiKey: string,
vault: CredentialVault,
) {
config.secondary = config.primary;
config.primary = vault.encrypt(newApiKey);
await saveProviderConfig(config);
}密钥的创建、吊销和轮换都需要写入审计日志。
4. 模型配置抽象与多 Provider 接入
4.1 模型配置
网关内部定义一个统一的模型 ID。业务代码只传这个 ID,底层对应的 Provider 和上游模型名由配置决定。
ts
interface ModelConfig {
modelId: string; // 网关内统一 ID,例如 'gpt-4o-mini'
provider: string; // 'openai' | 'anthropic' | 'gemini' 等
upstreamModel: string; // Provider 侧真实模型名
capabilities: string[]; // 如 ['chat', 'stream', 'tool_calling']
contextWindow: number;
defaultParams: Record<string, unknown>;
inputCostPer1K: number;
outputCostPer1K: number;
}modelId 与 upstreamModel 分离带来的好处是:模型升级、切换供应商或按租户分配不同模型时,只需要修改配置,不需要改业务代码。
4.2 Provider Adapter
每个 Provider 对应一个 Adapter。Adapter 负责把统一请求转换成 Provider 请求体,以及把 Provider 响应转换为统一响应。
ts
interface ChatDriver {
chat(req: ChatRequest, cred: ProviderCredential): Promise<ChatResponse>;
chatStream(req: ChatRequest, cred: ProviderCredential): AsyncIterable<ChatStreamChunk>;
}下面是 OpenAI 兼容 API 的 Adapter 骨架。cred.endpoint 是类似 https://api.openai.com/v1 的基础地址:
ts
class OpenAICompatDriver implements ChatDriver {
async chat(req: ChatRequest, cred: ProviderCredential): Promise<ChatResponse> {
const resp = await fetch(`${cred.endpoint}/chat/completions`, {
method: 'POST',
headers: {
'content-type': 'application/json',
'authorization': `Bearer ${cred.apiKey}`,
},
body: JSON.stringify(this.toOpenAIReq(req)),
});
if (!resp.ok) {
throw await ProviderError.fromResponse(resp);
}
return this.toChatResponse(await resp.json());
}
private toOpenAIReq(req: ChatRequest): unknown {
// 将内部消息格式转换为 OpenAI chat.completions 请求体
}
private toChatResponse(data: unknown): ChatResponse {
// 将 OpenAI 响应转换为统一 ChatResponse
}
}Adapter 之间的差异主要在:
- 请求路径;
- 鉴权头格式;
- 参数名映射,例如
max_tokens/maxOutputTokens; - 消息格式,例如 system prompt 和 tool calling 的结构;
- 错误响应解析。
参数默认值放在 ModelConfig.defaultParams 中。Adapter 读取该对象,把键名转换成对应 Provider 的键名。
注意:统一抽象不追求抹平所有 Provider 能力差异。对于 Provider 独有的参数,可以在 ChatRequest 中增加 rawParams 字段,由具体 Adapter 选择透传或忽略。
5. 路由策略与降级
5.1 路由规则
路由模块根据请求上下文选择目标模型。规则可以用条件表达式描述,按优先级从上到下匹配。
ts
interface RouteRule {
ruleId: string;
priority: number;
match: {
tenantId?: string[];
modelId?: string[]; // 固定模型直接路由
capability?: string[]; // 需要支持流式或工具调用
maxCostPer1K?: number; // 成本上限
};
targetModelId: string;
fallbackModelIds: string[];
}匹配逻辑按 priority 排序。例如,付费租户先路由到高能力模型,免费租户路由到低成本模型;请求要求 tool_calling 时,跳过不支持该能力的模型。
ts
function matchRule(rule: RouteRule, req: GatewayRequest): boolean {
if (!rule.enabled) return false;
if (rule.match.tenantId && !rule.match.tenantId.includes(req.tenantId)) return false;
if (rule.match.modelId && !rule.match.modelId.includes(req.model)) return false;
// capability 检查需要结合 ModelConfig
return true;
}
function resolveModel(req: GatewayRequest, rules: RouteRule[]): string {
const matched = rules
.sort((a, b) => a.priority - b.priority)
.find((r) => matchRule(r, req));
return matched ? matched.targetModelId : req.model;
}5.2 重试
重试只适合瞬时错误。可以重试的状态码包括 429、5xx 和网络错误;400、401、403 以及上下文超长等请求类错误不应重试。
ts
function isRetryableError(err: unknown): boolean {
if (err instanceof ProviderError) {
return err.statusCode === 429 || err.statusCode >= 500;
}
return true; // 网络异常
}
async function withRetry<T>(fn: () => Promise<T>, retries = 2): Promise<T> {
for (let attempt = 0; ; attempt++) {
try {
return await fn();
} catch (err) {
if (attempt >= retries || !isRetryableError(err)) {
throw err;
}
await sleep(2 ** attempt * 100 + Math.random() * 50);
}
}
}指数退避中加入随机抖动,可以避免多个客户端同时重试造成上游压力。
流式请求的重试需要额外小心。如果连接在响应首字节之前断开,可以重试;一旦上游已经开始返回内容,客户端可能已经收到部分数据,此时重试会产生重复内容。网关可以在流式模式下放弃自动重试,或只在流尚未开始之前重试。
5.3 熔断
重试针对单个请求,熔断针对 Provider 的持续不健康状态。熔断器记录连续失败次数,超过阈值后进入打开状态,后续请求直接快速失败,不等待上游超时。
ts
class CircuitBreaker {
private state: 'closed' | 'open' | 'half_open' = 'closed';
private failureCount = 0;
private openedAt = 0;
constructor(
private readonly failureThreshold = 5,
private readonly cooldownMs = 30_000,
) {}
allowRequest(): boolean {
if (this.state === 'closed') {
return true;
}
if (this.state === 'open') {
if (Date.now() - this.openedAt >= this.cooldownMs) {
this.state = 'half_open';
return true;
}
return false;
}
return true; // half_open:放行单个探测请求
}
onSuccess(): void {
this.failureCount = 0;
this.state = 'closed';
}
onFailure(): void {
this.failureCount += 1;
if (this.state === 'half_open' || this.failureCount >= this.failureThreshold) {
this.state = 'open';
this.openedAt = Date.now();
}
}
}熔断器状态应该按“路由目标”维护,也就是 Provider + 模型维度。open 状态下,网关直接把请求交给 fallback 链中的下一个模型;half_open 状态只放行少量探测请求,成功一次后恢复 closed,失败则继续 open。
5.4 降级链
RouteRule.fallbackModelIds 定义一个降级链。例如 gpt-4o 失败后尝试 gpt-4o-mini,再失败后尝试 gemini-2.0-flash。降级发生的位置在路由阶段:当前目标不可用(熔断打开、错误率超标或配额耗尽)时,按顺序选择下一个可用模型。
路由、重试、熔断和降级的关系可以概括为:
- 路由决定“调用哪个模型”;
- 熔断决定“这个模型当前是否可用”;
- 重试解决“单次请求的瞬时失败”;
- 降级解决“一个模型或 Provider 整体不可用”。
6. 调用监控与可观测性
6.1 统一调用记录
每次请求结束后,网关生成一条调用记录。统一的字段设计可以让监控、计费和排障使用同一份数据。
ts
interface CallRecord {
requestId: string;
traceId: string;
tenantId: string;
appId: string;
apiKeyId: string;
modelId: string;
modelConfigVersion: number;
provider: string;
statusCode: number;
latencyMs: number;
promptTokens: number;
completionTokens: number;
estimatedCostUsd: number;
}核心指标包括:
- 请求量:QPS、按租户/模型维度统计;
- 延迟:总耗时、首 Token 延迟(TTFT);
- Token 用量:输入、输出、总 Token;
- 错误率:按状态码和错误类型聚合;
- 成本:由 Token 用量和模型单价估算。
流式请求的总延迟要到流结束时才能确定。网关需要分别记录“首 Token 延迟”和“总耗时”,避免流式长连接把延迟平均值拉高。
6.2 OpenTelemetry 与链路追踪
OpenTelemetry 社区提供了 gen_ai 语义约定,推荐用统一的属性名记录模型调用[4]。示例:
ts
span.setAttribute('gen_ai.request.model', req.model);
span.setAttribute('gen_ai.provider.name', providerName);
span.setAttribute('gen_ai.usage.input_tokens', usage.promptTokens);
span.setAttribute('gen_ai.usage.output_tokens', usage.completionTokens);这些属性名仍在演进,具体以所用 SDK 版本支持的语义约定为准。网关可以在每个请求中生成一个 span,并把 traceId 写入 CallRecord。网关在发出上游 HTTP 请求时,把当前 trace context 注入到出站请求,这样在追踪系统中可以看到应用、网关、Provider 三段 span 的完整调用链。
6.3 日志脱敏
请求日志不能记录明文 API Key。收到请求时,把 Authorization 请求头中的 Key 替换为固定占位符;响应日志同样不能包含上游 API Key。
ts
function redactHeaders(headers: Record<string, string>): Record<string, string> {
const out = { ...headers };
if (out.authorization) {
out.authorization = 'Bearer filtered';
}
return out;
}错误对象中携带的上游响应体也要先经过脱敏再落日志。日志中只保留错误码、错误类型和可公开的消息。
6.4 成本估算
估算成本的基本方法是:
text
estimatedCost = promptTokens / 1000 * inputCostPer1K
+ completionTokens / 1000 * outputCostPer1K模型单价从 ModelConfig 读取。不同供应商的计费项可能不同(缓存命中 Token、图片输入、批量 API 折扣等),因此该值是运营估算,不能替代供应商账单。
开源同类项目中,LiteLLM、Portkey Gateway 等也提供代理、密钥托管和用量统计能力。实现之前对照它们暴露的指标和成本计算方式,可以检查自己的监控维度是否完整。
7. 配置动态生效与数据模型
7.1 数据模型
平台的核心表包括:
tenants/apps:租户和应用;api_keys:调用方 Key 的哈希、scope、状态;provider_credentials:上游 Provider 配置和加密后的凭据;model_configs:模型定义;route_rules:路由和降级规则;request_logs:调用记录;audit_logs:审计记录。
以下是 model_configs 和 request_logs 的示例(以 PostgreSQL 为例):
sql
CREATE TABLE model_configs (
model_id VARCHAR(128) PRIMARY KEY,
provider VARCHAR(64) NOT NULL,
upstream_model VARCHAR(128) NOT NULL,
capabilities JSONB NOT NULL,
context_window INTEGER NOT NULL,
default_params JSONB NOT NULL,
input_cost_per_1k NUMERIC(10, 6) NOT NULL,
output_cost_per_1k NUMERIC(10, 6) NOT NULL,
config_version INTEGER NOT NULL,
enabled BOOLEAN NOT NULL
);
CREATE TABLE request_logs (
request_id VARCHAR(64) PRIMARY KEY,
trace_id VARCHAR(64),
tenant_id VARCHAR(64) NOT NULL,
app_id VARCHAR(64) NOT NULL,
api_key_id VARCHAR(64),
model_id VARCHAR(128) NOT NULL,
model_config_version INTEGER,
provider VARCHAR(64) NOT NULL,
status_code INTEGER,
latency_ms INTEGER,
prompt_tokens INTEGER,
completion_tokens INTEGER,
estimated_cost_usd NUMERIC(12, 6),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);request_logs 中的 model_config_version 用于追溯请求发生时使用的模型配置版本。当路由规则或模型参数改变后,历史数据仍然可以还原到准确的配置上下文。
7.2 内存注册表与热更新
网关不会在每次请求时直接查询数据库。配置在启动时加载到内存,更新通过配置变更事件触发重载。
ts
class ModelRegistry {
private models = new Map<string, ModelConfig>();
private version = 0;
get(modelId: string): ModelConfig | undefined {
return this.models.get(modelId);
}
buildSnapshot(rows: ModelConfig[], version: number): void {
const next = new Map(rows.map((r) => [r.modelId, r]));
this.models = next;
this.version = version;
}
}buildSnapshot 用新 Map 替换旧 Map,读取方不会在更新过程中看到半份配置。配置中心(如 etcd、Nacos 或 Redis Pub/Sub)在数据变更时发送事件,网关监听事件后重新加载:
ts
class ConfigService {
constructor(
private readonly registry: ModelRegistry,
private readonly pubsub: PubSub,
) {}
async start(): Promise<void> {
await this.load();
this.pubsub.subscribe('model-config:updated', () => this.load());
}
private async load(): Promise<void> {
const snapshot = await loadModelConfigsFromDB();
this.registry.buildSnapshot(snapshot.rows, snapshot.version);
}
}配置变更和密钥吊销的生效要求不同。模型配置可以接受秒级延迟;吊销的 Key 需要立即失效,因此吊销消息要走单独的缓存失效通道,而不是依赖配置重载。
8. 安全设计与审计
8.1 访问控制
模型调用接口和管理接口要分开。管理接口(修改模型配置、路由规则、查看日志)使用独立的身份体系,执行 RBAC,而不是使用调用方 API Key 授权。
调用方 API Key 的可执行权限由 scopes 控制。建议最小化分配,例如只给应用 llm:chat 权限,不给 config:read。
8.2 审计日志
审计日志记录管理行为和密钥生命周期事件:
ts
interface AuditEvent {
actorId: string;
action:
| 'api_key.created'
| 'api_key.revoked'
| 'credential.rotated'
| 'model_config.updated'
| 'route_rule.updated';
target: string;
detail: Record<string, unknown>;
createdAt: Date;
}需要记录的事件包括:创建和吊销调用方 Key、轮换上游凭据、修改模型配置、修改路由规则。审计日志不能修改,建议使用追加写入。
8.3 密钥与网络
上游凭据解密后的明文只应存在于密钥服务或网关进程内存中,不写入日志和异常消息。网络传输使用 TLS。数据库中的上游凭据已经加密,即使数据库泄露,也无法直接得到可用明文。
9. OpenAI 兼容层与协议适配
9.1 兼容层的作用
OpenAI 的 API 规范在开源生态中已经成为事实上的标准接口。许多 SDK 和工具链原生支持 OpenAI 客户端,可以通过配置 baseURL 指向网关。兼容层的目标是让这些工具不改代码就能接入平台。
兼容层提供的入口至少包括:
POST /v1/chat/completionsPOST /v1/embeddings
网关在入口处完成鉴权和路由后,把请求转成内部统一格式,再由具体 Adapter 转发到上游。也就是说,OpenAI 兼容层只是接入层协议,本身并不作为 Provider 之一。
9.2 错误响应格式对齐
OpenAI 兼容接口的错误响应必须放在根级 error 对象中:
json
{
"error": {
"message": "The request is invalid.",
"type": "invalid_request_error",
"param": null,
"code": "invalid_model"
}
}vLLM 曾在错误响应中把 message 放在顶层,导致 OpenAI 官方 npm 包的 error.message 无法读取[2]。这说明了兼容性的关键在于字段结构,而不仅是 URL 路径一致。
9.3 Provider 错误映射
不同 Provider 的错误响应字段和语义不同。Adapter 解析上游错误后,统一转换为内部错误类型:
ts
interface GatewayError {
statusCode: number;
code: string;
message: string;
retryable: boolean;
}retryable 用于路由层的重试判断。状态码 429 和 5xx 通常为 retryable: true;4xx 请求类错误为 false。OpenAI 错误码把鉴权失败细分为多种场景[1],兼容层可以在 code 字段保留这些细分值,方便调用方做错误处理。
10. 注意事项与边界
10.1 成本估算不是账单
模型的计费规则可能包含免费额度、缓存 Token 折扣、批量 API 折扣、按字符或按图片计费。基于 Token 数和单价计算的成本只能作为内部参考,不能直接用于财务对账。
10.2 限流与重试
网关的配额限制和 Provider 的自身限流是两层。即使网关没有限流,上游也可能返回 429。不同 Provider 对限流响应头的命名和值单位不同,Adapter 需要分别解析,统一转换成网关的 Retry-After 语义,而不是假设所有 Provider 都返回相同字段。
10.3 流式请求的语义
流式响应要求网关具备流量控制能力。不要在转发前完整缓存上游响应,否则首 Token 延迟等于上游总耗时。流式请求的中断、客户端断开和上游异常需要分别处理。
10.4 数据合规
请求体会被发送到第三方模型供应商。平台不能自动判断哪些数据允许出境或是否可用于训练。租户协议需要明确数据目的地和保存策略,平台侧至少要在管理界面显示每个模型对应的 Provider 和端点。
10.5 不包含范围
本文讨论的是模型管理平台的接入层设计。模型训练、微调、评估、底层推理优化、Kubernetes/SRE 部署手册以及客户端 SDK 的 UI 交互设计不在范围内。平台如果要支持模型版本管理,应当与模型注册表系统对接,而不是在网关中维护模型权重和版本仓。
