Skip to content

Commit 70b204e

Browse files
xgopilotphantom5099
andcommitted
fix(provider): decouple openaicompat api mode from endpoint path
Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: phantom5099 <245659304+phantom5099@users.noreply.github.com>
1 parent 3bb8d8a commit 70b204e

22 files changed

Lines changed: 513 additions & 300 deletions

docs/guides/adding-providers.md

Lines changed: 74 additions & 254 deletions
Original file line numberDiff line numberDiff line change
@@ -1,304 +1,124 @@
11
# 扩展 Provider
22

3-
本文档说明如何为 NeoCode 添加新的 Provider。
3+
本文档说明在当前架构下如何为 NeoCode 添加/扩展 Provider。
44

5-
## 架构概览
5+
## 架构与边界
66

7-
NeoCode 的 provider 架构采用**集中式配置 + 驱动复用**的设计:
7+
当前主链路:`runtime -> provider -> 协议实现 / SDK 实现`
88

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` 只消费统一流事件,不感知厂商协议细节。
1014

11-
- **配置层**`internal/config/builtin_providers.go`):集中管理所有 provider 的元数据
12-
- Provider 名称、Driver 名称
13-
- Base URL、默认模型、API Key 环境变量名
15+
## 方式一:新增 OpenAI-compatible Provider(推荐)
1416

15-
- **驱动层**`internal/provider/openai/` 等):负责实际的 API 协议实现
16-
- 请求构造、响应解析
17-
- 流式输出、Tool Call 处理
18-
- 动态模型发现(如 `GET /models`)与结果归一化
17+
适用于“接口兼容 OpenAI”的网关或模型服务。
1918

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` 增加内置配置
3820

3921
```go
4022
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"
4727
)
4828

49-
// DeepSeekProvider 返回 DeepSeek provider 的默认配置。
5029
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+
}
5842
}
5943
```
6044

61-
**2. 在 `DefaultProviders()` 中注册:**
45+
### 2. 在 `DefaultProviders()` 注册
6246

6347
```go
6448
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+
}
7156
}
7257
```
7358

74-
**3. 设置环境变量并测试:**
59+
### 3. 配置环境变量并验证
7560

7661
```bash
7762
export DEEPSEEK_API_KEY="your-api-key"
7863
go run ./cmd/neocode
7964
```
8065

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
17567

176-
AnthropicName = "anthropic"
177-
AnthropicDefaultBaseURL = "https://api.anthropic.com/v1"
178-
AnthropicDefaultModel = "claude-sonnet-4-20250514"
179-
AnthropicDefaultAPIKeyEnv = "ANTHROPIC_API_KEY"
180-
)
68+
适用于协议与现有三类驱动都不兼容的厂商。
18169

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>/`
19171

192-
func DefaultProviders() []ProviderConfig {
193-
return []ProviderConfig{
194-
OpenAIProvider(),
195-
GeminiProvider(),
196-
OpenLLProvider(),
197-
AnthropicProvider(), // 新增
198-
}
199-
}
200-
```
72+
实现至少以下内容:
20173

202-
## 关键接口与类型
74+
- `driver.go`:暴露 `Driver()`,返回 `provider.DriverDefinition`
75+
- `provider.go``New(cfg provider.RuntimeConfig)``Generate(...)`
76+
-(可选)`request.go` / `stream.go` / `discovery_*.go`
20377

204-
### 核心接口
78+
### 2. 在 provider 注册处接入
20579

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(沿用现有注册入口模式)。
21181

212-
### 数据结构
82+
### 3. 在 `internal/config/provider.go` 增加内置 provider(如果需要)
21383

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`
22085

221-
## 设计约束
86+
## 自定义 provider.yaml(外部接入)
22287

223-
### 必须遵守
88+
路径:`~/.neocode/providers/<name>/provider.yaml`
22489

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 直连):
28391

28492
```yaml
28593
name: company-gateway
28694
driver: openaicompat
287-
base_url: https://llm.example.com/v1
95+
base_url: https://llm.example.com/v1/text/chatcompletion_v2
28896
api_key_env: COMPANY_GATEWAY_API_KEY
28997
model_source: discover
290-
chat_endpoint_path: /chat/completions
98+
chat_api_mode: responses
99+
chat_endpoint_path: /
291100
discovery_endpoint_path: /models
292-
models:
293-
- id: deepseek-coder
294-
name: DeepSeek Coder
295-
context_window: 131072
296-
max_output_tokens: 8192
297101
```
298102
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+
涉及协议变更时,优先补:
300120

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

Comments
 (0)