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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ output/novel/meta/simulation_profile.json
"model": "qwen3:latest",
"providers": {
"ollama": {
"base_url": "http://localhost:11434"
"base_url": "http://localhost:11434/v1"
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion config.example.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
},
"ollama": {
// ollama / bedrock / 显式 type 的自定义代理可以省略 api_key
"base_url": "http://localhost:11434",
// base_url 需带 /v1(ollama 的 OpenAI 兼容端点在 /v1 下)
"base_url": "http://localhost:11434/v1",
"models": ["qwen3:14b"]
},
"my-proxy": {
Expand Down
16 changes: 15 additions & 1 deletion internal/bootstrap/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ import (
// 仍小于 RequestTimeout 10 分钟,网络真死时仍能兜底。
const streamIdleTimeout = 5 * time.Minute

// keylessAPIKeyPlaceholder 是给免鉴权 provider 注入的占位 api_key。
// litellm 在创建客户端时会无条件要求 api_key 非空(连 ollama 这类本地、
// 免鉴权 provider 也不例外),这与本项目 RequiresAPIKey 的约定冲突。
// 对约定为免 key 的 provider 注入该占位值即可通过 litellm 的构造校验;
// ollama 会忽略 Authorization 头,免鉴权代理同理,不影响实际请求。
const keylessAPIKeyPlaceholder = "no-key"

// FailoverEvent 表示一次显式 provider 切换。
// Reason 为短标签(rate_limit / timeout / stream_idle / network),用于结构化日志。
type FailoverEvent struct {
Expand Down Expand Up @@ -270,8 +277,15 @@ func createModelFromConfig(providerKey, model string, pc ProviderConfig, cache m
return nil, fmt.Errorf("解析 provider 类型失败: %w", err)
}

apiKey := pc.APIKey
if apiKey == "" && !pc.RequiresAPIKey(providerKey) {
// 免鉴权 provider(ollama / 显式 type 的代理等)允许不填 api_key,
// 但 litellm 构造客户端时仍要求非空,这里注入占位值绕过该校验。
apiKey = keylessAPIKeyPlaceholder
}

m, err := llm.NewModel(providerType, model,
llm.WithAPIKey(pc.APIKey),
llm.WithAPIKey(apiKey),
llm.WithBaseURL(pc.BaseURL),
llm.WithStreamIdleTimeout(streamIdleTimeout),
)
Expand Down
4 changes: 2 additions & 2 deletions internal/bootstrap/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ var setupProviders = []setupProvider{
{name: "qwen", label: "Qwen"},
{name: "glm", label: "GLM"},
{name: "grok", label: "Grok"},
{name: "ollama", label: "Ollama", baseURL: "http://localhost:11434", apiKeyOptional: true},
{name: "ollama", label: "Ollama", baseURL: "http://localhost:11434/v1", apiKeyOptional: true},
{name: "bedrock", label: "Bedrock", apiKeyOptional: true},
{name: "custom", label: "Custom Proxy", needType: true, apiKeyOptional: true},
}
Expand Down Expand Up @@ -183,7 +183,7 @@ func saveExampleConfig() {
"models": ["gemini-2.5-flash", "gemini-2.5-pro"]
},
"ollama": {
"base_url": "http://localhost:11434",
"base_url": "http://localhost:11434/v1",
"models": ["qwen3:14b"]
},
"bedrock": {
Expand Down