|
1 | 1 | # 扩展 Provider |
2 | 2 |
|
3 | | -本文档说明如何为 NeoCode 添加新的 Provider。 |
| 3 | +本文档说明在当前架构下如何为 NeoCode 添加/扩展 Provider。 |
4 | 4 |
|
5 | | -## 架构概览 |
| 5 | +## 架构与边界 |
6 | 6 |
|
7 | | -NeoCode 的 provider 架构采用**集中式配置 + 驱动复用**的设计: |
| 7 | +当前主链路:`runtime -> provider -> 协议实现 / SDK 实现`。 |
8 | 8 |
|
9 | | -### 两层分离 |
| 9 | +- `internal/config` 负责 provider 配置装配与校验。 |
| 10 | +- `internal/provider` 根包只保留最小公共契约(`RuntimeConfig`、`ProviderIdentity`、错误分类与少量 helper)。 |
| 11 | +- `internal/provider/openaicompat` 负责 OpenAI-compatible 协议细节(含 `chat/completions` 与 `responses`)。 |
| 12 | +- `internal/provider/gemini`、`internal/provider/anthropic` 是基于官方 SDK 的薄适配器。 |
| 13 | +- `runtime` 只消费统一流事件,不感知厂商协议细节。 |
10 | 14 |
|
11 | | -- **配置层**(`internal/config/builtin_providers.go`):集中管理所有 provider 的元数据 |
12 | | - - Provider 名称、Driver 名称 |
13 | | - - Base URL、默认模型、API Key 环境变量名 |
| 15 | +## 方式一:新增 OpenAI-compatible Provider(推荐) |
14 | 16 |
|
15 | | -- **驱动层**(`internal/provider/openai/` 等):负责实际的 API 协议实现 |
16 | | - - 请求构造、响应解析 |
17 | | - - 流式输出、Tool Call 处理 |
18 | | - - 动态模型发现(如 `GET /models`)与结果归一化 |
| 17 | +适用于“接口兼容 OpenAI”的网关或模型服务。 |
19 | 18 |
|
20 | | -### 驱动复用 |
21 | | - |
22 | | -一个驱动可被多个 provider 复用。当前所有内置 provider 都复用 `openai` 驱动: |
23 | | - |
24 | | -```go |
25 | | -// internal/config/builtin_providers.go |
26 | | -OpenAIProvider() // Driver: "openai" |
27 | | -GeminiProvider() // Driver: "openai" (OpenAI-compatible API) |
28 | | -OpenLLProvider() // Driver: "openai" (OpenAI-compatible API) |
29 | | -``` |
30 | | - |
31 | | -## 方式一:添加 OpenAI 兼容 Provider(推荐) |
32 | | - |
33 | | -适用于 OpenAI 兼容接口(大多数第三方服务)。只需在配置层添加,无需编写新驱动。 |
34 | | - |
35 | | -### 步骤:添加 DeepSeek |
36 | | - |
37 | | -**1. 在 `internal/config/builtin_providers.go` 添加常量和配置:** |
| 19 | +### 1. 在 `internal/config/provider.go` 增加内置配置 |
38 | 20 |
|
39 | 21 | ```go |
40 | 22 | const ( |
41 | | - // ... 现有常量 ... |
42 | | - |
43 | | - DeepSeekName = "deepseek" |
44 | | - DeepSeekDefaultBaseURL = "https://api.deepseek.com/v1" |
45 | | - DeepSeekDefaultModel = "deepseek-chat" |
46 | | - DeepSeekDefaultAPIKeyEnv = "DEEPSEEK_API_KEY" |
| 23 | + DeepSeekName = "deepseek" |
| 24 | + DeepSeekDefaultBaseURL = "https://api.deepseek.com/v1" |
| 25 | + DeepSeekDefaultModel = "deepseek-chat" |
| 26 | + DeepSeekDefaultAPIKeyEnv = "DEEPSEEK_API_KEY" |
47 | 27 | ) |
48 | 28 |
|
49 | | -// DeepSeekProvider 返回 DeepSeek provider 的默认配置。 |
50 | 29 | func DeepSeekProvider() ProviderConfig { |
51 | | - return ProviderConfig{ |
52 | | - Name: DeepSeekName, |
53 | | - Driver: "openai", // 复用 openai 驱动 |
54 | | - BaseURL: DeepSeekDefaultBaseURL, |
55 | | - Model: DeepSeekDefaultModel, |
56 | | - APIKeyEnv: DeepSeekDefaultAPIKeyEnv, |
57 | | - } |
| 30 | + return ProviderConfig{ |
| 31 | + Name: DeepSeekName, |
| 32 | + Driver: provider.DriverOpenAICompat, |
| 33 | + BaseURL: DeepSeekDefaultBaseURL, |
| 34 | + Model: DeepSeekDefaultModel, |
| 35 | + APIKeyEnv: DeepSeekDefaultAPIKeyEnv, |
| 36 | + ModelSource: ModelSourceDiscover, |
| 37 | + ChatAPIMode: provider.ChatAPIModeChatCompletions, |
| 38 | + ChatEndpointPath: "/chat/completions", |
| 39 | + DiscoveryEndpointPath: provider.DiscoveryEndpointPathModels, |
| 40 | + Source: ProviderSourceBuiltin, |
| 41 | + } |
58 | 42 | } |
59 | 43 | ``` |
60 | 44 |
|
61 | | -**2. 在 `DefaultProviders()` 中注册:** |
| 45 | +### 2. 在 `DefaultProviders()` 注册 |
62 | 46 |
|
63 | 47 | ```go |
64 | 48 | func DefaultProviders() []ProviderConfig { |
65 | | - return []ProviderConfig{ |
66 | | - OpenAIProvider(), |
67 | | - GeminiProvider(), |
68 | | - OpenLLProvider(), |
69 | | - DeepSeekProvider(), // 新增 |
70 | | - } |
| 49 | + return []ProviderConfig{ |
| 50 | + OpenAIProvider(), |
| 51 | + GeminiProvider(), |
| 52 | + OpenLLProvider(), |
| 53 | + QiniuProvider(), |
| 54 | + DeepSeekProvider(), |
| 55 | + } |
71 | 56 | } |
72 | 57 | ``` |
73 | 58 |
|
74 | | -**3. 设置环境变量并测试:** |
| 59 | +### 3. 配置环境变量并验证 |
75 | 60 |
|
76 | 61 | ```bash |
77 | 62 | export DEEPSEEK_API_KEY="your-api-key" |
78 | 63 | go run ./cmd/neocode |
79 | 64 | ``` |
80 | 65 |
|
81 | | -### 优点 |
82 | | - |
83 | | -- ✅ 无需编写新代码,只需配置 |
84 | | -- ✅ 自动继承 openai 驱动的所有功能(流式、Tool Call) |
85 | | -- ✅ 配置集中管理,易于维护 |
86 | | -- ✅ 用户无需修改 YAML 文件 |
87 | | - |
88 | | -## 方式二:实现新驱动 |
89 | | - |
90 | | -适用于协议不兼容的厂商(如 Anthropic、Google 原生 API)。 |
91 | | - |
92 | | -### 步骤:添加 Anthropic |
93 | | - |
94 | | -**1. 在 `internal/provider/anthropic/anthropic.go` 实现驱动:** |
95 | | - |
96 | | -```go |
97 | | -package anthropic |
98 | | - |
99 | | -import ( |
100 | | - "context" |
101 | | - "errors" |
102 | | - "net/http" |
103 | | - "strings" |
104 | | - "time" |
105 | | - |
106 | | - "neo-code/internal/config" |
107 | | - domain "neo-code/internal/provider" |
108 | | -) |
109 | | - |
110 | | -const ( |
111 | | - Name = "anthropic" |
112 | | - DefaultBaseURL = "https://api.anthropic.com/v1" |
113 | | -) |
114 | | - |
115 | | -// Provider 实现 domain.Provider 接口 |
116 | | -type Provider struct { |
117 | | - cfg config.ResolvedProviderConfig |
118 | | - client *http.Client |
119 | | -} |
120 | | - |
121 | | -// New 构造函数 |
122 | | -func New(cfg config.ResolvedProviderConfig) (*Provider, error) { |
123 | | - if strings.TrimSpace(cfg.APIKey) == "" { |
124 | | - return nil, errors.New("anthropic provider: api key is empty") |
125 | | - } |
126 | | - return &Provider{ |
127 | | - cfg: cfg, |
128 | | - client: &http.Client{Timeout: 60 * time.Second}, |
129 | | - }, nil |
130 | | -} |
131 | | - |
132 | | -// Chat 实现流式对话接口 |
133 | | -func (p *Provider) Chat(ctx context.Context, req domain.ChatRequest, events chan<- domain.StreamEvent) (domain.ChatResponse, error) { |
134 | | - // 1. 将 domain.ChatRequest 转换为 Anthropic API 格式 |
135 | | - // 2. 调用 Anthropic API(流式 SSE) |
136 | | - // 3. 解析响应,推送 StreamEventTextDelta / StreamEventToolCallStart |
137 | | - // 4. 返回 domain.ChatResponse |
138 | | -} |
139 | | - |
140 | | -// Driver 返回驱动定义 |
141 | | -func Driver() domain.DriverDefinition { |
142 | | - return domain.DriverDefinition{ |
143 | | - Name: Name, |
144 | | - Build: func(ctx context.Context, cfg config.ResolvedProviderConfig) (domain.Provider, error) { |
145 | | - return New(cfg) |
146 | | - }, |
147 | | - } |
148 | | -} |
149 | | -``` |
150 | | - |
151 | | -**2. 在 `internal/provider/builtin/builtin.go` 注册驱动:** |
152 | | - |
153 | | -```go |
154 | | -import ( |
155 | | - "neo-code/internal/provider/anthropic" |
156 | | - "neo-code/internal/provider/openai" |
157 | | -) |
158 | | - |
159 | | -func Register(registry *provider.Registry) error { |
160 | | - if registry == nil { |
161 | | - return errors.New("builtin provider registry is nil") |
162 | | - } |
163 | | - if err := registry.Register(openai.Driver()); err != nil { |
164 | | - return err |
165 | | - } |
166 | | - return registry.Register(anthropic.Driver()) // 新增 |
167 | | -} |
168 | | -``` |
169 | | - |
170 | | -**3. 在 `internal/config/builtin_providers.go` 添加配置:** |
171 | | - |
172 | | -```go |
173 | | -const ( |
174 | | - // ... 现有常量 ... |
| 66 | +## 方式二:实现新 Driver |
175 | 67 |
|
176 | | - AnthropicName = "anthropic" |
177 | | - AnthropicDefaultBaseURL = "https://api.anthropic.com/v1" |
178 | | - AnthropicDefaultModel = "claude-sonnet-4-20250514" |
179 | | - AnthropicDefaultAPIKeyEnv = "ANTHROPIC_API_KEY" |
180 | | -) |
| 68 | +适用于协议与现有三类驱动都不兼容的厂商。 |
181 | 69 |
|
182 | | -func AnthropicProvider() ProviderConfig { |
183 | | - return ProviderConfig{ |
184 | | - Name: AnthropicName, |
185 | | - Driver: "anthropic", // 使用新的 anthropic 驱动 |
186 | | - BaseURL: AnthropicDefaultBaseURL, |
187 | | - Model: AnthropicDefaultModel, |
188 | | - APIKeyEnv: AnthropicDefaultAPIKeyEnv, |
189 | | - } |
190 | | -} |
| 70 | +### 1. 新建 `internal/provider/<driver>/` |
191 | 71 |
|
192 | | -func DefaultProviders() []ProviderConfig { |
193 | | - return []ProviderConfig{ |
194 | | - OpenAIProvider(), |
195 | | - GeminiProvider(), |
196 | | - OpenLLProvider(), |
197 | | - AnthropicProvider(), // 新增 |
198 | | - } |
199 | | -} |
200 | | -``` |
| 72 | +实现至少以下内容: |
201 | 73 |
|
202 | | -## 关键接口与类型 |
| 74 | +- `driver.go`:暴露 `Driver()`,返回 `provider.DriverDefinition` |
| 75 | +- `provider.go`:`New(cfg provider.RuntimeConfig)` 和 `Generate(...)` |
| 76 | +-(可选)`request.go` / `stream.go` / `discovery_*.go` |
203 | 77 |
|
204 | | -### 核心接口 |
| 78 | +### 2. 在 provider 注册处接入 |
205 | 79 |
|
206 | | -| 类型 | 位置 | 说明 | |
207 | | -|------|------|------| |
208 | | -| `Provider` | `internal/provider/types.go` | 核心接口,定义 `Chat` 方法 | |
209 | | -| `DriverDefinition` | `internal/provider/registry.go` | 驱动定义:`Name` + `Build` 构造函数 | |
210 | | -| `Registry` | `internal/provider/registry.go` | 驱动注册中心 | |
| 80 | +将新 `DriverDefinition` 注册到 provider registry(沿用现有注册入口模式)。 |
211 | 81 |
|
212 | | -### 数据结构 |
| 82 | +### 3. 在 `internal/config/provider.go` 增加内置 provider(如果需要) |
213 | 83 |
|
214 | | -| 类型 | 位置 | 说明 | |
215 | | -|------|------|------| |
216 | | -| `ChatRequest` | `internal/provider/types.go` | 请求:`Model`、`SystemPrompt`、`Messages`、`Tools` | |
217 | | -| `ChatResponse` | `internal/provider/types.go` | 响应:`Message`、`FinishReason`、`Usage` | |
218 | | -| `StreamEvent` | `internal/provider/types.go` | 流式事件:`TextDelta`、`ToolCallStart` | |
219 | | -| `ProviderConfig` | `internal/config/model.go` | 配置:`Name`、`Driver`、`BaseURL`、`Model`、`APIKeyEnv` | |
| 84 | +`Driver` 指向新驱动名,并补默认模型与 `api_key_env`。 |
220 | 85 |
|
221 | | -## 设计约束 |
| 86 | +## 自定义 provider.yaml(外部接入) |
222 | 87 |
|
223 | | -### 必须遵守 |
| 88 | +路径:`~/.neocode/providers/<name>/provider.yaml` |
224 | 89 |
|
225 | | -✅ **配置集中管理** |
226 | | -- 所有内置 provider 配置统一在 `internal/config/builtin_providers.go` |
227 | | -- 不再为每个 provider 创建独立的包 |
228 | | - |
229 | | -✅ **API Key 安全** |
230 | | -- 只从环境变量读取,不写入 `config.yaml` |
231 | | -- 不硬编码在源码中 |
232 | | - |
233 | | -✅ **驱动职责清晰** |
234 | | -- 驱动只负责协议构造与响应解析 |
235 | | -- 不持有 provider 元数据;模型目录由 driver 发现,缓存与合并由 service 处理 |
236 | | - |
237 | | -✅ **架构分层** |
238 | | -- 厂商差异收敛在 `internal/provider/` 内 |
239 | | -- `runtime`、`tui` 等上层模块只依赖统一的 `Provider` 接口 |
240 | | -- `base_url` 不在 TUI 中展示给用户 |
241 | | - |
242 | | -### 最佳实践 |
243 | | - |
244 | | -1. **优先复用现有驱动** |
245 | | - - 大多数 OpenAI 兼容服务无需编写新驱动 |
246 | | - - 只需在配置层添加即可 |
247 | | - |
248 | | -2. **配置即代码** |
249 | | - - provider 配置随代码版本发布 |
250 | | - - 用户无需手动配置 providers 列表 |
251 | | - |
252 | | -3. **测试覆盖** |
253 | | - - 新驱动必须添加完整的单元测试 |
254 | | - - 使用 `httptest.NewServer` 模拟 HTTP 调用 |
255 | | - - 不使用真实 API Key |
256 | | - |
257 | | -## 示例:当前内置 Provider |
258 | | - |
259 | | -```go |
260 | | -// internal/config/builtin_providers.go |
261 | | - |
262 | | -func DefaultProviders() []ProviderConfig { |
263 | | - return []ProviderConfig{ |
264 | | - OpenAIProvider(), // OpenAI 官方 API |
265 | | - GeminiProvider(), // Google Gemini (OpenAI-compatible) |
266 | | - OpenLLProvider(), // OpenLL 服务 (OpenAI-compatible) |
267 | | - QiniuProvider(), // 七牛云推理服务 (OpenAI-compatible) |
268 | | - } |
269 | | -} |
270 | | -``` |
271 | | - |
272 | | -所有内置 provider 都通过代码集中注册。模型选择器展示的候选模型由默认模型、动态发现结果和本地缓存共同组成。 |
273 | | - |
274 | | -## custom provider 模型元数据补齐 |
275 | | - |
276 | | -对于复用 `openaicompat` 驱动的 custom provider,如果上游 `GET /models` 不能返回可靠的上下文窗口信息,可以在: |
277 | | - |
278 | | -```text |
279 | | -~/.neocode/providers/<provider-name>/provider.yaml |
280 | | -``` |
281 | | - |
282 | | -中显式声明 `models`: |
| 90 | +示例(OpenAI-compatible + responses 直连): |
283 | 91 |
|
284 | 92 | ```yaml |
285 | 93 | name: company-gateway |
286 | 94 | driver: openaicompat |
287 | | -base_url: https://llm.example.com/v1 |
| 95 | +base_url: https://llm.example.com/v1/text/chatcompletion_v2 |
288 | 96 | api_key_env: COMPANY_GATEWAY_API_KEY |
289 | 97 | model_source: discover |
290 | | -chat_endpoint_path: /chat/completions |
| 98 | +chat_api_mode: responses |
| 99 | +chat_endpoint_path: / |
291 | 100 | discovery_endpoint_path: /models |
292 | | -models: |
293 | | - - id: deepseek-coder |
294 | | - name: DeepSeek Coder |
295 | | - context_window: 131072 |
296 | | - max_output_tokens: 8192 |
297 | 101 | ``` |
298 | 102 |
|
299 | | -约束如下: |
| 103 | +说明: |
| 104 | +
|
| 105 | +- `chat_api_mode` 仅 `openaicompat` 生效,可选值:`chat_completions` / `responses`。 |
| 106 | +- `chat_endpoint_path` 为空或 `/` 表示直连 `base_url`,不会自动补子路径。 |
| 107 | +- `model_source: manual` 时必须提供 `models`,且会忽略 `discovery_endpoint_path`。 |
| 108 | + |
| 109 | +## 测试要求 |
| 110 | + |
| 111 | +新增或修改 provider 后,至少执行: |
| 112 | + |
| 113 | +```bash |
| 114 | +go test ./internal/provider/... |
| 115 | +go test ./internal/config/... |
| 116 | +go test ./internal/runtime/... |
| 117 | +``` |
| 118 | + |
| 119 | +涉及协议变更时,优先补: |
300 | 120 |
|
301 | | -- `models[].id` 必须非空。 |
302 | | -- `models[].context_window` 和 `models[].max_output_tokens` 如果显式配置,必须大于 `0`。 |
303 | | -- 同一个 `provider.yaml` 中重复的模型 `id` 会在加载阶段直接报错。 |
304 | | -- 这些元数据会进入统一的 model catalog 合并链路,优先级仍为“配置模型元数据优先于 discovery/default”。 |
| 121 | +- 请求组装与响应解析 |
| 122 | +- tool call 解析 |
| 123 | +- 流式事件映射 |
| 124 | +- discovery 错误分类与边界 |
0 commit comments