Skip to content
Open
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
11 changes: 11 additions & 0 deletions config.example.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@
"headers": { "X-Custom-Client": "my-client" }
}
},
"atlascloud": {
// Atlas Cloud 使用 OpenAI-compatible Chat Completions API。
// 设为默认时,把顶层 provider 改为 "atlascloud",model 改为下列任一模型。
"type": "openai",
"api_key": "atl-xxx",
"base_url": "https://api.atlascloud.ai/v1",
"models": [
{ "name": "qwen/qwen3.5-flash", "context_window": 1000000 },
{ "name": "deepseek-ai/deepseek-v4-pro", "context_window": 1048576 }
]
},
"anthropic": {
"api_key": "sk-ant-xxx",
"models": [{ "name": "claude-sonnet-4" }, { "name": "claude-opus-4" }]
Expand Down
11 changes: 11 additions & 0 deletions internal/bootstrap/config.example.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@
"headers": { "X-Custom-Client": "my-client" }
}
},
"atlascloud": {
// Atlas Cloud 使用 OpenAI-compatible Chat Completions API。
// 设为默认时,把顶层 provider 改为 "atlascloud",model 改为下列任一模型。
"type": "openai",
"api_key": "atl-xxx",
"base_url": "https://api.atlascloud.ai/v1",
"models": [
{ "name": "qwen/qwen3.5-flash", "context_window": 1000000 },
{ "name": "deepseek-ai/deepseek-v4-pro", "context_window": 1048576 }
]
},
"anthropic": {
"api_key": "sk-ant-xxx",
"models": [{ "name": "claude-sonnet-4" }, { "name": "claude-opus-4" }]
Expand Down
48 changes: 48 additions & 0 deletions internal/bootstrap/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,51 @@ func TestValidateBaseRejectsBadStreamIdleTimeout(t *testing.T) {
t.Fatalf("非法 stream_idle_timeout 应拒绝并包装 ErrConfig,得到: %v", err)
}
}

func TestProviderPresetsIncludeAtlasCloudOpenAICompatibleDefaults(t *testing.T) {
var atlas ProviderPreset
for _, preset := range ProviderPresets() {
if preset.Name == "atlascloud" {
atlas = preset
break
}
}
if atlas.Name == "" {
t.Fatal("ProviderPresets should include atlascloud")
}
if atlas.Label != "Atlas Cloud" || atlas.Type != "openai" {
t.Fatalf("atlascloud preset label/type = %q/%q", atlas.Label, atlas.Type)
}
if atlas.BaseURL != "https://api.atlascloud.ai/v1" {
t.Fatalf("atlascloud base url = %q", atlas.BaseURL)
}
if atlas.DefaultModel != "qwen/qwen3.5-flash" {
t.Fatalf("atlascloud default model = %q", atlas.DefaultModel)
}
wantModels := map[string]int{
"qwen/qwen3.5-flash": 1000000,
"deepseek-ai/deepseek-v4-pro": 1048576,
}
if len(atlas.Models) != len(wantModels) {
t.Fatalf("atlascloud models = %#v", atlas.Models)
}
for _, model := range atlas.Models {
if want, ok := wantModels[model.Name]; !ok || model.ContextWindow != want {
t.Fatalf("unexpected atlascloud model entry: %#v", model)
}
}
}

func TestPresetModelsWithSelectionPreservesCatalogAndAddsCustomSelection(t *testing.T) {
preset := []ModelConfig{{Name: "qwen/qwen3.5-flash", ContextWindow: 1000000}}
got := presetModelsWithSelection(preset, "custom-model")
if len(got) != 2 {
t.Fatalf("models = %#v", got)
}
if got[0].Name != "qwen/qwen3.5-flash" || got[0].ContextWindow != 1000000 {
t.Fatalf("preset model was not preserved: %#v", got[0])
}
if got[1].Name != "custom-model" || got[1].ContextWindow != 0 {
t.Fatalf("custom selected model not appended: %#v", got[1])
}
}
56 changes: 52 additions & 4 deletions internal/bootstrap/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ func NeedsSetup() bool {
type setupProvider struct {
name string
label string
providerType string
baseURL string // 预填的 base_url
defaultModel string
models []ModelConfig
needType bool // 自定义代理需要额外问 type 和 base_url
apiKeyOptional bool // true 表示 API Key 允许留空
}
Expand All @@ -44,13 +47,27 @@ type setupProvider struct {
type ProviderPreset struct {
Name string
Label string
Type string
BaseURL string
DefaultModel string
Models []ModelConfig
NeedType bool
APIKeyOptional bool
}

var setupProviders = []setupProvider{
{name: "openrouter", label: "OpenRouter", baseURL: "https://openrouter.ai/api/v1"},
{
name: "atlascloud",
label: "Atlas Cloud",
providerType: "openai",
baseURL: "https://api.atlascloud.ai/v1",
defaultModel: "qwen/qwen3.5-flash",
models: []ModelConfig{
{Name: "qwen/qwen3.5-flash", ContextWindow: 1000000},
{Name: "deepseek-ai/deepseek-v4-pro", ContextWindow: 1048576},
},
},
{name: "anthropic", label: "Anthropic"},
{name: "gemini", label: "Gemini"},
{name: "openai", label: "OpenAI"},
Expand All @@ -68,8 +85,14 @@ func ProviderPresets() []ProviderPreset {
out := make([]ProviderPreset, 0, len(setupProviders))
for _, preset := range setupProviders {
out = append(out, ProviderPreset{
Name: preset.name, Label: preset.label, BaseURL: preset.baseURL,
NeedType: preset.needType, APIKeyOptional: preset.apiKeyOptional,
Name: preset.name,
Label: preset.label,
Type: preset.providerType,
BaseURL: preset.baseURL,
DefaultModel: preset.defaultModel,
Models: append([]ModelConfig(nil), preset.models...),
NeedType: preset.needType,
APIKeyOptional: preset.apiKeyOptional,
})
}
return out
Expand Down Expand Up @@ -105,6 +128,8 @@ func RunSetup() (Config, error) {
return Config{}, err
}
pc.Type = providerType
} else if sp.providerType != "" {
pc.Type = sp.providerType
}

// Step 2: 输入 API Key
Expand Down Expand Up @@ -142,12 +167,16 @@ func RunSetup() (Config, error) {
}

// Step 4: 模型名(必填)
modelName, err := runTextInput("[4/4] 模型名称", "例如:gpt-4o / claude-sonnet-4 / gemini-2.5-pro")
modelHint := "例如:gpt-4o / claude-sonnet-4 / gemini-2.5-pro"
if sp.defaultModel != "" {
modelHint = sp.defaultModel
}
modelName, err := runTextInputWithDefault("[4/4] 模型名称", modelHint, sp.defaultModel)
if err != nil {
return Config{}, err
}
printStepDone("Model", modelName)
pc.Models = []ModelConfig{{Name: modelName}}
pc.Models = presetModelsWithSelection(sp.models, modelName)

cfg := Config{
Provider: providerName,
Expand Down Expand Up @@ -182,6 +211,25 @@ func RunSetup() (Config, error) {
return cfg, nil
}

func presetModelsWithSelection(preset []ModelConfig, selected string) []ModelConfig {
selected = strings.TrimSpace(selected)
out := make([]ModelConfig, 0, len(preset)+1)
seen := make(map[string]bool, len(preset)+1)
for _, model := range preset {
name := strings.TrimSpace(model.Name)
if name == "" || seen[name] {
continue
}
model.Name = name
out = append(out, model)
seen[name] = true
}
if selected != "" && !seen[selected] {
out = append(out, ModelConfig{Name: selected})
}
return out
}

func saveExampleConfig() {
dir, err := configDir()
if err != nil {
Expand Down
3 changes: 2 additions & 1 deletion internal/entry/tui/command_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,9 @@ func (s *modelConfigState) applyProviderChoice(choice configProviderChoice) {
return
}
s.provider = choice.preset.Name
s.providerType = "" // 内置 provider 协议由名称隐含
s.providerType = choice.preset.Type // 为空表示内置 provider 协议由名称隐含
s.baseURL = choice.preset.BaseURL
s.models = append([]bootstrap.ModelConfig(nil), choice.preset.Models...)
s.apiKeyOptional = choice.preset.APIKeyOptional
s.step = configStepHub
}
Expand Down
30 changes: 30 additions & 0 deletions internal/entry/tui/command_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,33 @@ func TestProviderMenuIsTwoLevel(t *testing.T) {
}
}
}

func TestAtlasCloudPresetUsesOpenAICompatibleDefaults(t *testing.T) {
var preset *bootstrap.ProviderPreset
for _, value := range bootstrap.ProviderPresets() {
if value.Name == "atlascloud" {
copyValue := value
preset = &copyValue
break
}
}
if preset == nil {
t.Fatal("atlascloud preset not found")
}

state := &modelConfigState{}
state.applyProviderChoice(configProviderChoice{label: preset.Label, preset: preset})
if state.provider != "atlascloud" || state.providerType != "openai" {
t.Fatalf("provider/type = %q/%q", state.provider, state.providerType)
}
if state.baseURL != "https://api.atlascloud.ai/v1" {
t.Fatalf("baseURL = %q", state.baseURL)
}
if len(state.models) != 2 || state.models[0].Name != "qwen/qwen3.5-flash" {
t.Fatalf("atlascloud models = %#v", state.models)
}
ids := hubFieldIDs(state.hubFields())
if !slices.Contains(ids, "protocol") || !slices.Contains(ids, "api") {
t.Fatalf("atlascloud hub should expose OpenAI protocol and endpoint fields, got %v", ids)
}
}