Files
TrueGrowth/openspec/changes/add-provider-protocol-routing/design.md

180 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## Context
当前代码已经具备以下基础能力:
- `settings-manager` 可以管理 `ProviderProfile / ProviderCatalog / ModelRef / InvocationPreset`
- `runtime-model-discovery` 可以按 `profileId` 隔离发现和选择模型
- 图片和视频侧已经存在若干专用 adapter分别处理 Flux、MJ、Kling、Seedance 等协议变体
但当前结构仍存在三个核心缺口:
1. `resolveInvocationRoute` 只解析凭证和模型,不解析协议与请求体策略
2. `runtime-model-discovery` 仍以统一 `/models + Bearer` 的假设执行发现
3. `model-adapters` 仍然按模型 / 厂商 / 标签进行猜测式匹配,无法表达“同名模型在不同 profile 下走不同协议”的事实
这会导致下列问题:
- 同名模型在不同上游中发生协议冲突
- 同协议不同请求体的差异只能堆在 adapter 内的条件分支中,难以扩展
- 文本链路继续绕过统一路由层,无法和图片 / 视频共享同一套供应商协议规划
## Goals / Non-Goals
- Goals:
- 让运行时调用基于 `profileId + modelId + operation` 选择协议绑定
- 支持同一模型 ID 在不同供应商下走不同协议
- 支持同一协议下的不同请求体 schema
- 统一文本、图片、视频三类调用的运行时规划流程
- 尽量降低用户配置成本,优先自动推断协议绑定,仅在歧义时暴露高级覆盖项
- Non-Goals:
- 本次不实现跨供应商自动故障切换
- 本次不实现按价格 / 延迟 / 健康度自动择优调度
- 本次不为所有第三方平台实现完全自动化、零误差的协议识别
## Decisions
- Decision: 区分“模型标识”和“可执行绑定”
- `modelId` 表示用户看到的模型
- `ProviderModelBinding` 表示该模型在特定供应商下针对特定操作的调用方式
- 绑定的主键必须至少包含 `profileId + modelId + operation`
- Decision: 引入 `InvocationPlanner`
- 输入:`routeType + ModelRef + optional override`
- 输出:`InvocationPlan`
- Planner 负责在多个绑定中选择当前最合适的协议,而不是让 UI 或 service 直接拼 URL
- Decision: 将适配器从“按模型注册”迁移为“按协议注册”
- 现有 Flux / MJ / Kling / Seedance 等实现可以保留,但角色改为 `ProtocolAdapter`
- `ProtocolAdapter` 负责请求体构建、响应解析、可选轮询策略
- Decision: 将鉴权和基础 URL 处理下沉到 `ProviderTransport`
- 统一处理 `bearer / header / query / custom` 等认证方式
- 统一处理额外 Header、查询参数和供应商级发现接口
- 避免每个 adapter 再重复拼装认证信息
- Decision: discovery 必须保留原始元数据和绑定推断结果
- 不能只把远端模型压成扁平 `ModelConfig`
- 必须保留 `raw``capabilityHints``bindingCandidates`
- 否则后续运行时无法稳定判断同名模型的协议归属
- Decision: 同名模型不得按裸 `modelId` 全局去重
- 选择器和运行时缓存应使用 `selectionKey = profileId::modelId`
- 运行时绑定必须保留 `profileId`
## Proposed Data Model
```ts
interface ResolvedProviderContext {
profileId: string;
profileName: string;
providerType: string;
baseUrl: string;
apiKey: string;
authType: 'bearer' | 'header' | 'query' | 'custom';
extraHeaders?: Record<string, string>;
}
interface ProviderModelBinding {
id: string;
profileId: string;
modelId: string;
operation: 'text' | 'image' | 'video';
protocol:
| 'openai.chat.completions'
| 'openai.images.generations'
| 'openai.async.video'
| 'google.generateContent'
| 'mj.imagine'
| 'flux.task'
| 'kling.video'
| 'seedance.task';
requestSchema: string;
responseSchema: string;
submitPath: string;
pollPathTemplate?: string;
priority: number;
confidence: 'high' | 'medium' | 'low';
source: 'discovered' | 'template' | 'manual';
}
interface DiscoveredProviderModel {
profileId: string;
modelId: string;
selectionKey: string;
raw: unknown;
capabilityHints: {
supportsText: boolean;
supportsImage: boolean;
supportsVideo: boolean;
};
bindings: ProviderModelBinding[];
}
interface InvocationPlan {
provider: ResolvedProviderContext;
modelRef: { profileId: string; modelId: string };
binding: ProviderModelBinding;
}
```
## Execution Flow
1. 用户在设置中保存 `ProviderProfile`
2. `ProviderTransport` 根据 `providerType/baseUrl` 选择供应商模板或探测策略
3. discovery 同步远端模型,并推断出每个模型的 `bindings[]`
4. 用户在运行时选择 `ModelRef(profileId + modelId)`
5. `InvocationPlanner` 根据 `operation` 找到最高优先级的可执行 binding
6. `ProtocolAdapterRegistry``binding.protocol` 取得协议适配器
7. 适配器根据 `requestSchema` 构建请求,并通过 `ProviderTransport` 发送
8. 如为异步任务,则由对应 `PollingStrategy` 轮询并标准化结果
## Binding Inference Strategy
- OpenAI 兼容供应商:
- 优先使用 `/v1/models` 返回的字段、模型 ID 和供应商模板推断绑定
- 默认优先生成 `openai.chat.completions / openai.images.generations / openai.async.video`
- Gemini 官方供应商:
- 不再强依赖 `/models`
- 允许由 provider template 直接提供 `google.generateContent` 绑定
- 自定义供应商:
- 若 discovery 无法高置信度推断协议,则保留低置信度绑定
- 仅在需要时暴露高级覆盖入口
## Request Schema Strategy
同一协议下的请求体差异不再通过大量 `if modelId.includes(...)` 解决,而是显式建模:
- `openai.image.basic-json`
- `openai.video.form.input-reference`
- `google.gemini.generate-content.image`
- `kling.video.text2video`
- `kling.video.image2video`
- `seedance.video.first-last-frame`
这样新增模型时,优先复用已有协议,仅为其指派合适的 `requestSchema`
## Migration Plan
1. 新增 provider protocol routing 类型与 planner不改 UI
2. 将现有图片 / 视频 adapter 包装为 `ProtocolAdapter`
3. 将文本链路迁移到同一 planner 和 protocol registry
4. 扩展 discovery 存储,保留 `bindings[]`
5. 最后再清理旧的“按模型猜 adapter”逻辑
## Risks / Trade-offs
- 风险: 架构抽象增加初始复杂度
- Mitigation: 分阶段迁移,先保留现有 adapter 实现,只调整其注册方式
- 风险: 自动推断协议可能存在误判
- Mitigation: 为 binding 引入 `confidence``source`,仅在低置信度场景允许高级覆盖
- 风险: 文本 / 图片 / 视频同时迁移会影响面较大
- Mitigation: 先完成 planner 和 image/video 迁移,再迁移 text
## Open Questions
- 是否需要将用户手动覆盖的 binding 保存在 `ProviderCatalog`,还是单独存为 `ProviderBindingOverrides`
- `authType` 是否需要从当前的 `bearer/header` 扩展为更通用的 `query/custom`
- 官方 Gemini 等不提供 OpenAI 风格 `/models` 时,设置页是否需要显式展示“供应商模板已接管模型能力”