Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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