Skip to content
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ NeoCode 是一个在终端中运行的 AI 编码助手,采用 ReAct(Reason-A

- **流式输出** — 实时展示模型思考过程
- **工具系统** — 文件操作、代码执行、搜索等内置工具
- ** Provider 支持** — OpenAI、Gemini 等主流模型,易于扩展
- **内建 Provider 支持** — OpenAI、Gemini、OpenLL、Qiniu,模型列表支持动态发现与缓存
- **终端原生体验** — 基于 Bubble Tea 的现代化 TUI

## 一键安装
Expand Down Expand Up @@ -66,7 +66,7 @@ go run ./cmd/neocode

使用 slash 命令快速切换配置:

- `/provider` — 切换模型提供商
- `/provider` — 切换内建模型提供商
- `/model` — 切换模型

## 架构概览
Expand All @@ -89,8 +89,8 @@ go run ./cmd/neocode
核心模块职责:

- **`internal/config`** — 配置管理、环境变量、YAML 加载
- **`internal/context`** — 多 section system prompt 组装、消息裁剪与上下文构建
- **`internal/provider`** — LLM 提供商抽象,抹平厂商差异
- **`internal/context`** — system prompt、消息裁剪与上下文构建
- **`internal/provider`** — 内建 LLM Provider 抽象、模型元数据归一化、动态发现与 catalog 缓存
- **`internal/runtime`** — ReAct 主循环、事件流、会话管理
- **`internal/tools`** — 工具注册表与具体工具实现
- **`internal/tui`** — 终端 UI、交互体验、事件桥接
Expand All @@ -107,8 +107,8 @@ go run ./cmd/neocode
├── internal
│ ├── app # 应用装配
│ ├── config # 配置管理
│ ├── context # system prompt 组装与上下文构建
│ ├── provider # Provider 抽象与实现
│ ├── context # 上下文构建
│ ├── provider # Provider 抽象、模型发现与元数据缓存
│ ├── runtime # ReAct 循环与事件流
│ ├── tools # 工具系统
│ └── tui # 终端 UI
Expand Down
24 changes: 6 additions & 18 deletions docs/guides/adding-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ NeoCode 的 provider 架构采用**集中式配置 + 驱动复用**的设计:

- **配置层**(`internal/config/builtin_providers.go`):集中管理所有 provider 的元数据
- Provider 名称、Driver 名称
- Base URL、默认模型、模型列表
- API Key 环境变量名
- Base URL、默认模型、API Key 环境变量名

- **驱动层**(`internal/provider/openai/` 等):负责实际的 API 协议实现
- 请求构造、响应解析
- 流式输出、Tool Call 处理
- 动态模型发现(如 `GET /models`)与结果归一化

### 驱动复用

Expand Down Expand Up @@ -46,19 +46,13 @@ const (
DeepSeekDefaultAPIKeyEnv = "DEEPSEEK_API_KEY"
)

var deepSeekModels = []string{
DeepSeekDefaultModel,
"deepseek-coder",
}

// DeepSeekProvider 返回 DeepSeek provider 的默认配置。
func DeepSeekProvider() ProviderConfig {
return ProviderConfig{
Name: DeepSeekName,
Driver: "openai", // 复用 openai 驱动
BaseURL: DeepSeekDefaultBaseURL,
Model: DeepSeekDefaultModel,
Models: append([]string(nil), deepSeekModels...),
APIKeyEnv: DeepSeekDefaultAPIKeyEnv,
}
}
Expand Down Expand Up @@ -182,19 +176,12 @@ const (
AnthropicDefaultAPIKeyEnv = "ANTHROPIC_API_KEY"
)

var anthropicModels = []string{
AnthropicDefaultModel,
"claude-opus-4-20250514",
"claude-3-5-sonnet-20241022",
}

func AnthropicProvider() ProviderConfig {
return ProviderConfig{
Name: AnthropicName,
Driver: "anthropic", // 使用新的 anthropic 驱动
BaseURL: AnthropicDefaultBaseURL,
Model: AnthropicDefaultModel,
Models: append([]string(nil), anthropicModels...),
APIKeyEnv: AnthropicDefaultAPIKeyEnv,
}
}
Expand Down Expand Up @@ -226,7 +213,7 @@ func DefaultProviders() []ProviderConfig {
| `ChatRequest` | `internal/provider/types.go` | 请求:`Model`、`SystemPrompt`、`Messages`、`Tools` |
| `ChatResponse` | `internal/provider/types.go` | 响应:`Message`、`FinishReason`、`Usage` |
| `StreamEvent` | `internal/provider/types.go` | 流式事件:`TextDelta`、`ToolCallStart` |
| `ProviderConfig` | `internal/config/model.go` | 配置:`Name`、`Driver`、`BaseURL`、`Model`、`Models`、`APIKeyEnv` |
| `ProviderConfig` | `internal/config/model.go` | 配置:`Name`、`Driver`、`BaseURL`、`Model`、`APIKeyEnv` |

## 设计约束

Expand All @@ -242,7 +229,7 @@ func DefaultProviders() []ProviderConfig {

✅ **驱动职责清晰**
- 驱动只负责协议构造与响应解析
- 不持有模型列表、base_url 等配置信息
- 不持有 provider 元数据;模型目录由 driver 发现,缓存与合并由 service 处理

✅ **架构分层**
- 厂商差异收敛在 `internal/provider/` 内
Expand Down Expand Up @@ -274,9 +261,10 @@ func DefaultProviders() []ProviderConfig {
OpenAIProvider(), // OpenAI 官方 API
GeminiProvider(), // Google Gemini (OpenAI-compatible)
OpenLLProvider(), // OpenLL 服务 (OpenAI-compatible)
QiniuProvider(), // 七牛云推理服务 (OpenAI-compatible)
}
}
```

所有内置 provider 都复用 `openai` 驱动,配置集中在 `builtin_providers.go`
所有内置 provider 都通过代码集中注册。模型选择器展示的候选模型由默认模型、动态发现结果和本地缓存共同组成

11 changes: 9 additions & 2 deletions docs/guides/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ NeoCode 采用"内置 Provider 优先"的策略:
- `config.yaml` 不再持久化完整 `providers` 列表
- 只保存当前选择状态和通用运行配置
- 运行时的 `providers` 完全来自代码内置定义
- 用户不能通过 YAML 注入新的 provider

✅ **安全第一**
- API Key 只从环境变量读取,永不写入 YAML
Expand All @@ -28,6 +29,7 @@ NeoCode 采用"内置 Provider 优先"的策略:
| `openai` | `openai` | OpenAI 官方 API |
| `gemini` | `openai` | Google Gemini (OpenAI-compatible API) |
| `openll` | `openai` | OpenLL 服务 (OpenAI-compatible API) |
| `qiniu` | `openai` | 七牛云推理服务 (OpenAI-compatible API) |

所有内置 provider 都复用 `openai` 驱动,支持流式输出和 Tool Call。

Expand All @@ -39,6 +41,7 @@ NeoCode 采用"内置 Provider 优先"的策略:
- ✅ 未来代码新增 provider 时,用户无需修改 YAML
- ✅ 老配置文件中的 `providers` / `provider_overrides` 会在加载时被清理
- ✅ 配置文件始终保持简洁,只包含必要的运行时状态
- ✅ 如需新增 provider,只能通过代码扩展内建列表完成

## 配置文件

Expand Down Expand Up @@ -106,6 +109,7 @@ tools:
| `openai` | `OPENAI_API_KEY` | [OpenAI Platform](https://platform.openai.com/api-keys) |
| `gemini` | `GEMINI_API_KEY` | [Google AI Studio](https://aistudio.google.com/apikey) |
| `openll` | `AI_API_KEY` | OpenLL 服务提供商 |
| `qiniu` | `QINIU_API_KEY` | 七牛云推理服务 |

### 环境变量管理

Expand Down Expand Up @@ -156,6 +160,7 @@ NeoCode 提供以下 slash 命令用于快速切换配置:
❯ openai
gemini
openll
qiniu
```

### /model - 模型选择器
Expand All @@ -166,6 +171,8 @@ NeoCode 提供以下 slash 命令用于快速切换配置:

打开当前 provider 的模型选择器。

模型列表会合并当前 provider 的默认模型、服务端动态发现结果和本地缓存结果。

**界面示例**(选择 openai provider 后):
```
? Select a model:
Expand Down Expand Up @@ -208,9 +215,9 @@ NeoCode 提供以下 slash 命令用于快速切换配置:
2. 立即持久化到 `config.yaml`
3. 下次启动自动恢复选择状态

## 添加自定义 Provider
## 扩展内建 Provider

如需添加自定义 provider(如企业内部服务),请参考:
如需添加新的内建 provider(包括企业内部服务),请参考:

👉 [adding-providers.md](./adding-providers.md)

Expand Down
16 changes: 10 additions & 6 deletions internal/app/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,26 @@ import (
func NewProgram(ctx context.Context) (*tea.Program, error) {
loader := config.NewLoader("", builtin.DefaultConfig())
manager := config.NewManager(loader)
cfg, err := manager.Load(ctx)
if err != nil {
if _, err := manager.Load(ctx); err != nil {
return nil, err
}

toolRegistry := buildToolRegistry(cfg)
toolManager, err := buildToolManager(toolRegistry)
providerRegistry, err := builtin.NewRegistry()
if err != nil {
return nil, err
}
providerService := provider.NewService(manager, providerRegistry, nil)
if _, err := providerService.EnsureSelection(ctx); err != nil {
return nil, err
}

providerRegistry, err := builtin.NewRegistry()
cfg := manager.Get()

toolRegistry := buildToolRegistry(cfg)
toolManager, err := buildToolManager(toolRegistry)
if err != nil {
return nil, err
}
providerService := provider.NewService(manager, providerRegistry)

sessionStore := agentruntime.NewSessionStore(loader.BaseDir())
runtimeSvc := agentruntime.NewWithFactory(
Expand Down
46 changes: 46 additions & 0 deletions internal/app/bootstrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ import (
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"

"neo-code/internal/config"
"neo-code/internal/tools"
)

func TestNewProgram(t *testing.T) {
disableBuiltinProviderAPIKeys(t)

home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)
Expand All @@ -32,6 +35,41 @@ func TestNewProgram(t *testing.T) {
}
}

func TestNewProgramNormalizesInvalidCurrentModelOnStartup(t *testing.T) {
disableBuiltinProviderAPIKeys(t)

home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)

configDir := filepath.Join(home, ".neocode")
if err := os.MkdirAll(configDir, 0o755); err != nil {
t.Fatalf("mkdir config dir: %v", err)
}

configPath := filepath.Join(configDir, "config.yaml")
raw := []byte("selected_provider: openai\ncurrent_model: unsupported-current\nworkdir: .\nshell: powershell\n")
if err := os.WriteFile(configPath, raw, 0o644); err != nil {
t.Fatalf("write config: %v", err)
}

program, err := NewProgram(context.Background())
if err != nil {
t.Fatalf("NewProgram() error = %v", err)
}
if program == nil {
t.Fatalf("expected tea program")
}

data, err := os.ReadFile(configPath)
if err != nil {
t.Fatalf("read config: %v", err)
}
if !strings.Contains(string(data), "current_model: "+config.OpenAIDefaultModel) {
t.Fatalf("expected startup normalization to rewrite current_model, got:\n%s", string(data))
}
}

func TestBuildToolRegistryUsesWebFetchConfig(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -126,3 +164,11 @@ func (s stubToolForBootstrap) Schema() map[string]any { return map[string]any{"t
func (s stubToolForBootstrap) Execute(ctx context.Context, call tools.ToolCallInput) (tools.ToolResult, error) {
return tools.ToolResult{Name: s.name, Content: s.content}, nil
}

func disableBuiltinProviderAPIKeys(t *testing.T) {
t.Helper()
t.Setenv(config.OpenAIDefaultAPIKeyEnv, "")
t.Setenv(config.GeminiDefaultAPIKeyEnv, "")
t.Setenv(config.OpenLLDefaultAPIKeyEnv, "")
t.Setenv(config.QiniuDefaultAPIKeyEnv, "")
}
37 changes: 5 additions & 32 deletions internal/config/builtin_providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,78 +22,51 @@ const (
QiniuDefaultAPIKeyEnv = "QINIU_API_KEY"
)

var openAIModels = []string{
OpenAIDefaultModel,
"gpt-4o",
"gpt-5.4",
"gpt-5.3-codex",
}

var geminiModels = []string{
GeminiDefaultModel,
"gemini-2.5-pro",
"gemini-2.0-flash",
}

var openLLModels = []string{
OpenLLDefaultModel,
"gpt-5.3-codex",
"gpt-5.3-turbo",
}

var qiniuModels = []string{
QiniuDefaultModel,
"openai/gpt-5.2-codex",
}

// OpenAIProvider 返回 OpenAI provider 的默认配置。
// OpenAIProvider returns the builtin OpenAI provider definition.
func OpenAIProvider() ProviderConfig {
return ProviderConfig{
Name: OpenAIName,
Driver: "openai",
BaseURL: OpenAIDefaultBaseURL,
Model: OpenAIDefaultModel,
Models: append([]string(nil), openAIModels...),
APIKeyEnv: OpenAIDefaultAPIKeyEnv,
}
}

// GeminiProvider 返回 Gemini provider 的默认配置。
// GeminiProvider returns the builtin Gemini provider definition.
func GeminiProvider() ProviderConfig {
return ProviderConfig{
Name: GeminiName,
Driver: "openai",
BaseURL: GeminiDefaultBaseURL,
Model: GeminiDefaultModel,
Models: append([]string(nil), geminiModels...),
APIKeyEnv: GeminiDefaultAPIKeyEnv,
}
}

// OpenLLProvider 返回 OpenLL provider 的默认配置。
// OpenLLProvider returns the builtin OpenLL provider definition.
func OpenLLProvider() ProviderConfig {
return ProviderConfig{
Name: OpenLLName,
Driver: "openai",
BaseURL: OpenLLDefaultBaseURL,
Model: OpenLLDefaultModel,
Models: append([]string(nil), openLLModels...),
APIKeyEnv: OpenLLDefaultAPIKeyEnv,
}
}

// QiniuProvider returns the builtin Qiniu provider definition.
func QiniuProvider() ProviderConfig {
return ProviderConfig{
Name: QiniuName,
Driver: "openai",
BaseURL: QiniuDefaultBaseURL,
Model: QiniuDefaultModel,
Models: append([]string(nil), qiniuModels...),
APIKeyEnv: QiniuDefaultAPIKeyEnv,
}
}

// DefaultProviders 返回所有内置 provider 配置列表。
// DefaultProviders returns all builtin provider definitions.
func DefaultProviders() []ProviderConfig {
return []ProviderConfig{
OpenAIProvider(),
Expand Down
Loading
Loading