183 lines
8.2 KiB
Markdown
183 lines
8.2 KiB
Markdown
# AI Provider OpenClaw-Style Routing Plan
|
||
|
||
Last updated: 2026-05-20
|
||
|
||
## Summary
|
||
|
||
Planet 的 AI Provider 路由要从“运行时识别特殊 provider / 特殊模型”收敛到 OpenClaw 风格的配置驱动模型:模型引用、协议、鉴权、轻量探测、真实调用和模型级例外都由 provider catalog / preset / runtime metadata 描述,运行时只解释这些元数据,不再散落 `if provider == ... and model == ...` 这类硬编码。
|
||
|
||
这份计划覆盖 Admin Next 的 Provider 配置体验、backend settings API、`aiprovider` 适配服务和未来模型目录同步方式。目标是让 OpenCode Go、MiniMax、DeepSeek、OpenAI-compatible、Anthropic-compatible、Ollama、OpenRouter / One API 类代理都能用同一套规则扩展。
|
||
|
||
## Background
|
||
|
||
当前实现已经完成了两步临时修正:
|
||
|
||
- OpenCode Go 模型目录不再使用普通 Zen free 列表,而是使用 `https://opencode.ai/zen/go/v1/models`。
|
||
- `minimax-m2.7` / `minimax-m2.5` 的协议例外已从 `aiprovider` 运行逻辑移到 `model_provider_apis` 元数据中。
|
||
|
||
但整体还没有完全达到 OpenClaw 式结构。OpenClaw 的关键思想是:
|
||
|
||
- 模型引用使用 `provider/model`,由 provider 前缀确定 runtime provider。
|
||
- provider 插件或 catalog 拥有 `normalizeModelId`、`normalizeTransport`、`normalizeConfig`、`prepareRuntimeAuth`、`createStreamFn` 等 provider 行为。
|
||
- 主推理循环不认识具体模型名,只使用解析后的 provider config、transport 和 request adapter。
|
||
- 上游网关能自己路由时,尽量透传 provider routing metadata,不在本地复制上游逻辑。
|
||
|
||
Planet 不需要完整复制 OpenClaw 插件系统,但需要学习它的边界划分。
|
||
|
||
## Design Principles
|
||
|
||
- Provider catalog 是路由事实来源,runtime 不是。
|
||
- 模型级协议例外必须是 metadata,例如 `model_provider_apis`,不能是 Python set / if 分支。
|
||
- 轻量连通性测试只验证网络、鉴权和模型目录,不发真实 prompt。
|
||
- 真实模型调用只发生在 Playground、AI brief、分析任务等明确需要生成的路径。
|
||
- 保存配置不自动设为默认,不自动触发连接测试;保存、设默认、测试三种按钮职责分离。
|
||
- 目录刷新使用增量合并语义:发现新模型,标记旧模型 stale,不直接删除用户选择或自定义模型。
|
||
- 如果 provider 不提供可靠 `/models`,可以用内置 preset 确认已知模型,但 UI 必须说清楚这是 preset confirmation,不是假装 provider 返回了目录。
|
||
|
||
## Target Data Model
|
||
|
||
Provider preset / runtime config 应逐步收敛为类似结构:
|
||
|
||
```json
|
||
{
|
||
"provider": "opencode-go",
|
||
"label": "OpenCode Go",
|
||
"default_transport": "openai-completions",
|
||
"base_url": "https://opencode.ai/zen/go/v1",
|
||
"auth": {
|
||
"type": "bearer",
|
||
"api_key_env": "OPENCODE_GO_API_KEY"
|
||
},
|
||
"models": [
|
||
{
|
||
"id": "glm-5.1",
|
||
"label": "GLM 5.1",
|
||
"transport": "openai-completions",
|
||
"context_window": null,
|
||
"capabilities": ["text"]
|
||
},
|
||
{
|
||
"id": "minimax-m2.7",
|
||
"label": "MiniMax M2.7",
|
||
"transport": "anthropic-messages",
|
||
"capabilities": ["text", "reasoning"]
|
||
}
|
||
],
|
||
"discovery": {
|
||
"type": "openai-models",
|
||
"url": "https://opencode.ai/zen/go/v1/models",
|
||
"auth": "provider-api-key"
|
||
}
|
||
}
|
||
```
|
||
|
||
Runtime 选择规则:
|
||
|
||
1. 解析 provider。
|
||
2. 解析 model。
|
||
3. 从 `models[].transport` 找模型级 transport。
|
||
4. 若没有模型级 transport,使用 provider `default_transport`。
|
||
5. 将解析结果传给 `aiprovider`。
|
||
6. `aiprovider` 只按 `transport` 组装请求,不认识 provider 专属模型名。
|
||
|
||
## Implementation Plan
|
||
|
||
### Phase 1: Stabilize Current Metadata Path
|
||
|
||
- Keep `model_provider_apis` as the immediate compatibility bridge.
|
||
- Ensure `_provider_defaults()` includes provider metadata such as `model_provider_apis`.
|
||
- Ensure `_runtime_config_from_ai_payload()` sends the resolved metadata through `AIProviderClient`.
|
||
- Ensure `AIProviderClient` forwards metadata to `aiprovider` with a structured header.
|
||
- Ensure `aiprovider.ProviderService` reads model metadata and resolves `provider_api = model_provider_apis[model] ?? provider_api`.
|
||
- Add tests proving `aiprovider` does not contain provider/model-specific literals for routing decisions.
|
||
|
||
### Phase 2: Replace `model_provider_apis` With Structured Model Catalog
|
||
|
||
- Extend `backend/app/services/llm_provider_catalog.py` preset shape with `models_metadata`.
|
||
- Preserve old `models` as a compatibility list for the UI.
|
||
- Add helpers:
|
||
- `get_provider_model_metadata(provider, model)`
|
||
- `resolve_provider_transport(provider_config, model)`
|
||
- `merge_discovered_models(existing, discovered)`
|
||
- Return both `models` and `models_metadata` from refresh endpoints.
|
||
- Admin Next should render model labels, capabilities and transport hints from metadata.
|
||
|
||
### Phase 3: Provider Discovery And Incremental Sync
|
||
|
||
- Add provider discovery descriptors:
|
||
- OpenAI-compatible `/models`
|
||
- Anthropic-compatible no-models / preset-confirmed path
|
||
- Ollama `/api/tags`
|
||
- OpenCode Go `/zen/go/v1/models`
|
||
- OpenRouter / One API passthrough model discovery
|
||
- Add incremental merge behavior:
|
||
- New discovered model: add.
|
||
- Existing discovered model: update `last_seen_at`, metadata.
|
||
- Missing discovered model: mark `stale`, do not delete.
|
||
- User custom model: keep unless explicitly removed.
|
||
- Surface discovery source in Admin Next: `实时发现 / 内置预设 / 用户自定义 / 已过期`.
|
||
|
||
### Phase 4: Transport Adapters
|
||
|
||
- Replace provider-specific request decisions with adapter descriptors:
|
||
- `openai-completions`
|
||
- `anthropic-messages`
|
||
- `ollama-generate`
|
||
- future `openai-responses`
|
||
- future `gemini-generate-content`
|
||
- Each adapter owns:
|
||
- path
|
||
- auth header format
|
||
- request body transform
|
||
- response text extraction
|
||
- reasoning/thinking block extraction
|
||
- models endpoint strategy
|
||
- `ProviderService.analyze()` should select adapter by resolved transport and call the adapter.
|
||
|
||
### Phase 5: Admin Next UX
|
||
|
||
- Model provider page should show:
|
||
- provider status tag
|
||
- default model tag
|
||
- source tag: env / runtime / preset / discovered
|
||
- model list with transport/capability hint
|
||
- separate buttons for save, set default, refresh model catalog, lightweight test
|
||
- The connect plug button remains lightweight.
|
||
- Full generation test lives only in Playground or a clearly named “试运行” action.
|
||
- If lightweight test falls back to preset confirmation, toast must say so explicitly.
|
||
|
||
## TODO
|
||
|
||
- [ ] Add `models_metadata` to provider presets and refresh responses.
|
||
- [ ] Add runtime resolver helper for provider/model transport selection.
|
||
- [ ] Remove any remaining provider/model-specific literals from `aiprovider` runtime routing.
|
||
- [ ] Add tests that `opencode-go/minimax-m2.7` resolves through metadata, not through runtime hardcode.
|
||
- [ ] Add tests for lightweight connectivity:
|
||
- [ ] 401 / 403 fail as auth error.
|
||
- [ ] 404 with known preset model passes as preset-confirmed.
|
||
- [ ] `/models` missing alias passes only when preset contains the alias.
|
||
- [ ] unknown model fails.
|
||
- [ ] Add discovery descriptors for OpenCode Go, OpenAI-compatible, Anthropic-compatible, Ollama, OpenRouter / One API.
|
||
- [ ] Add incremental model catalog merge semantics with stale marking.
|
||
- [ ] Update Admin Next model list to show model source, transport and capability.
|
||
- [ ] Keep save / set default / lightweight test / full test as separate actions.
|
||
- [ ] Document the final provider catalog schema in technical docs after implementation.
|
||
|
||
## Current Acceptance Criteria
|
||
|
||
- No runtime routing branch may depend on concrete model names like `minimax-m2.7`.
|
||
- OpenCode Go model refresh must not use the ordinary Zen free-model endpoint.
|
||
- Lightweight connect must not call `analyze()` or consume generation quota.
|
||
- Saving a provider must not automatically set it as default.
|
||
- Provider UI must distinguish configured key, fallback key, preset model and live-discovered model.
|
||
|
||
## Related Files
|
||
|
||
- `backend/app/services/llm_provider_catalog.py`
|
||
- `backend/app/api/v1/settings.py`
|
||
- `backend/app/services/ai_client.py`
|
||
- `aiprovider/main.py`
|
||
- `aiprovider/provider_service.py`
|
||
- `frontend/src/admin-next/pages/PlainResourcePages.tsx`
|
||
- `docs/plans/admin-next-parity-audit-closeout-plan.md`
|