Skip to content

Latest commit

 

History

History
303 lines (235 loc) · 8.57 KB

File metadata and controls

303 lines (235 loc) · 8.57 KB

扩展 Provider

本文档说明如何为 NeoCode 添加新的 Provider。

架构概览

NeoCode 的 provider 架构采用集中式配置 + 驱动复用的设计:

两层分离

  • 配置层internal/config/builtin_providers.go):集中管理所有 provider 的元数据

    • Provider 名称、Driver 名称
    • Base URL、默认模型、API Key 环境变量名
  • 驱动层internal/provider/openai/ 等):负责实际的 API 协议实现

    • 请求构造、响应解析
    • 流式输出、Tool Call 处理
    • 动态模型发现(如 GET /models)与结果归一化

驱动复用

一个驱动可被多个 provider 复用。当前所有内置 provider 都复用 openai 驱动:

// internal/config/builtin_providers.go
OpenAIProvider()   // Driver: "openai"
GeminiProvider()   // Driver: "openai" (OpenAI-compatible API)
OpenLLProvider()   // Driver: "openai" (OpenAI-compatible API)

方式一:添加 OpenAI 兼容 Provider(推荐)

适用于 OpenAI 兼容接口(大多数第三方服务)。只需在配置层添加,无需编写新驱动。

步骤:添加 DeepSeek

1. 在 internal/config/builtin_providers.go 添加常量和配置:

const (
    // ... 现有常量 ...

    DeepSeekName             = "deepseek"
    DeepSeekDefaultBaseURL   = "https://api.deepseek.com/v1"
    DeepSeekDefaultModel     = "deepseek-chat"
    DeepSeekDefaultAPIKeyEnv = "DEEPSEEK_API_KEY"
)

// DeepSeekProvider 返回 DeepSeek provider 的默认配置。
func DeepSeekProvider() ProviderConfig {
    return ProviderConfig{
        Name:      DeepSeekName,
        Driver:    "openai",                    // 复用 openai 驱动
        BaseURL:   DeepSeekDefaultBaseURL,
        Model:     DeepSeekDefaultModel,
        APIKeyEnv: DeepSeekDefaultAPIKeyEnv,
    }
}

2. 在 DefaultProviders() 中注册:

func DefaultProviders() []ProviderConfig {
    return []ProviderConfig{
        OpenAIProvider(),
        GeminiProvider(),
        OpenLLProvider(),
        DeepSeekProvider(),  // 新增
    }
}

3. 设置环境变量并测试:

export DEEPSEEK_API_KEY="your-api-key"
go run ./cmd/neocode

优点

  • ✅ 无需编写新代码,只需配置
  • ✅ 自动继承 openai 驱动的所有功能(流式、Tool Call)
  • ✅ 配置集中管理,易于维护
  • ✅ 用户无需修改 YAML 文件

方式二:实现新驱动

适用于协议不兼容的厂商(如 Anthropic、Google 原生 API)。

步骤:添加 Anthropic

1. 在 internal/provider/anthropic/anthropic.go 实现驱动:

package anthropic

import (
    "context"
    "errors"
    "net/http"
    "strings"
    "time"

    "neo-code/internal/config"
    domain "neo-code/internal/provider"
)

const (
    Name           = "anthropic"
    DefaultBaseURL = "https://api.anthropic.com/v1"
)

// Provider 实现 domain.Provider 接口
type Provider struct {
    cfg    config.ResolvedProviderConfig
    client *http.Client
}

// New 构造函数
func New(cfg config.ResolvedProviderConfig) (*Provider, error) {
    if strings.TrimSpace(cfg.APIKey) == "" {
        return nil, errors.New("anthropic provider: api key is empty")
    }
    return &Provider{
        cfg:    cfg,
        client: &http.Client{Timeout: 60 * time.Second},
    }, nil
}

// Chat 实现流式对话接口
func (p *Provider) Chat(ctx context.Context, req domain.ChatRequest, events chan<- domain.StreamEvent) (domain.ChatResponse, error) {
    // 1. 将 domain.ChatRequest 转换为 Anthropic API 格式
    // 2. 调用 Anthropic API(流式 SSE)
    // 3. 解析响应,推送 StreamEventTextDelta / StreamEventToolCallStart
    // 4. 返回 domain.ChatResponse
}

// Driver 返回驱动定义
func Driver() domain.DriverDefinition {
    return domain.DriverDefinition{
        Name: Name,
        Build: func(ctx context.Context, cfg config.ResolvedProviderConfig) (domain.Provider, error) {
            return New(cfg)
        },
    }
}

2. 在 internal/provider/builtin/builtin.go 注册驱动:

import (
    "neo-code/internal/provider/anthropic"
    "neo-code/internal/provider/openai"
)

func Register(registry *provider.Registry) error {
    if registry == nil {
        return errors.New("builtin provider registry is nil")
    }
    if err := registry.Register(openai.Driver()); err != nil {
        return err
    }
    return registry.Register(anthropic.Driver())  // 新增
}

3. 在 internal/config/builtin_providers.go 添加配置:

const (
    // ... 现有常量 ...

    AnthropicName             = "anthropic"
    AnthropicDefaultBaseURL   = "https://api.anthropic.com/v1"
    AnthropicDefaultModel     = "claude-sonnet-4-20250514"
    AnthropicDefaultAPIKeyEnv = "ANTHROPIC_API_KEY"
)

func AnthropicProvider() ProviderConfig {
    return ProviderConfig{
        Name:      AnthropicName,
        Driver:    "anthropic",               // 使用新的 anthropic 驱动
        BaseURL:   AnthropicDefaultBaseURL,
        Model:     AnthropicDefaultModel,
        APIKeyEnv: AnthropicDefaultAPIKeyEnv,
    }
}

func DefaultProviders() []ProviderConfig {
    return []ProviderConfig{
        OpenAIProvider(),
        GeminiProvider(),
        OpenLLProvider(),
        AnthropicProvider(),  // 新增
    }
}

关键接口与类型

核心接口

类型 位置 说明
Provider internal/provider/types.go 核心接口,定义 Chat 方法
DriverDefinition internal/provider/registry.go 驱动定义:Name + Build 构造函数
Registry internal/provider/registry.go 驱动注册中心

数据结构

类型 位置 说明
ChatRequest internal/provider/types.go 请求:ModelSystemPromptMessagesTools
ChatResponse internal/provider/types.go 响应:MessageFinishReasonUsage
StreamEvent internal/provider/types.go 流式事件:TextDeltaToolCallStart
ProviderConfig internal/config/model.go 配置:NameDriverBaseURLModelAPIKeyEnv

设计约束

必须遵守

配置集中管理

  • 所有内置 provider 配置统一在 internal/config/builtin_providers.go
  • 不再为每个 provider 创建独立的包

API Key 安全

  • 只从环境变量读取,不写入 config.yaml
  • 不硬编码在源码中

驱动职责清晰

  • 驱动只负责协议构造与响应解析
  • 不持有 provider 元数据;模型目录由 driver 发现,缓存与合并由 service 处理

架构分层

  • 厂商差异收敛在 internal/provider/
  • runtimetui 等上层模块只依赖统一的 Provider 接口
  • base_url 不在 TUI 中展示给用户

最佳实践

  1. 优先复用现有驱动

    • 大多数 OpenAI 兼容服务无需编写新驱动
    • 只需在配置层添加即可
  2. 配置即代码

    • provider 配置随代码版本发布
    • 用户无需手动配置 providers 列表
  3. 测试覆盖

    • 新驱动必须添加完整的单元测试
    • 使用 httptest.NewServer 模拟 HTTP 调用
    • 不使用真实 API Key

示例:当前内置 Provider

// internal/config/builtin_providers.go

func DefaultProviders() []ProviderConfig {
    return []ProviderConfig{
        OpenAIProvider(),  // OpenAI 官方 API
        GeminiProvider(),  // Google Gemini (OpenAI-compatible)
        OpenLLProvider(),  // OpenLL 服务 (OpenAI-compatible)
        QiniuProvider(),   // 七牛云推理服务 (OpenAI-compatible)
    }
}

所有内置 provider 都通过代码集中注册。模型选择器展示的候选模型由默认模型、动态发现结果和本地缓存共同组成。

custom provider 模型元数据补齐

对于复用 openaicompat 驱动的 custom provider,如果上游 GET /models 不能返回可靠的上下文窗口信息,可以在:

~/.neocode/providers/<provider-name>/provider.yaml

中显式声明 models

name: company-gateway
driver: openaicompat
api_key_env: COMPANY_GATEWAY_API_KEY
models:
  - id: deepseek-coder
    name: DeepSeek Coder
    context_window: 131072
    max_output_tokens: 8192
openai_compatible:
  base_url: https://llm.example.com/v1
  api_style: chat_completions

约束如下:

  • models[].id 必须非空。
  • models[].context_windowmodels[].max_output_tokens 如果显式配置,必须大于 0
  • 同一个 provider.yaml 中重复的模型 id 会在加载阶段直接报错。
  • 这些元数据会进入统一的 model catalog 合并链路,优先级仍为“配置模型元数据优先于 discovery/default”。