From bf51227034bbe0adef3860f9a51164e7543f2bbd Mon Sep 17 00:00:00 2001 From: ducat Date: Sun, 7 Jun 2026 01:23:49 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20ollama=20=E5=85=8D?= =?UTF-8?q?=20key=20=E6=97=A0=E6=B3=95=E5=90=AF=E5=8A=A8=E4=B8=8E=E7=A4=BA?= =?UTF-8?q?=E4=BE=8B=20base=5Furl=20=E7=BC=BA=20/v1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit litellm 构造客户端时无条件要求 api_key 非空(OpenAICompatProvider 未重写 Validate,沿用 BaseProvider 校验,无视 ollama 的 SkipAPIKeyValidation), 与本项目 RequiresAPIKey 约定冲突,ollama 等免 key provider 报 "ollama: API key is required" 无法启动。 - createModelFromConfig: 对 RequiresAPIKey==false 的 provider 在 key 为空时 注入占位 api_key 绕过 litellm 构造校验(ollama 忽略该值) - config.example.jsonc / setup.go / README.md: ollama base_url 补全 /v1 (OpenAI 兼容端点在 /v1 下,省略会打到 /chat/completions 失败) Closes #28 --- README.md | 2 +- config.example.jsonc | 3 ++- internal/bootstrap/models.go | 16 +++++++++++++++- internal/bootstrap/setup.go | 4 ++-- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2e7c65c..05653b8 100644 --- a/README.md +++ b/README.md @@ -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" } } } diff --git a/config.example.jsonc b/config.example.jsonc index 461ec0d..8709f8a 100644 --- a/config.example.jsonc +++ b/config.example.jsonc @@ -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": { diff --git a/internal/bootstrap/models.go b/internal/bootstrap/models.go index 6afb945..f3245a7 100644 --- a/internal/bootstrap/models.go +++ b/internal/bootstrap/models.go @@ -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 { @@ -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), ) diff --git a/internal/bootstrap/setup.go b/internal/bootstrap/setup.go index 5068928..e97dadb 100644 --- a/internal/bootstrap/setup.go +++ b/internal/bootstrap/setup.go @@ -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}, } @@ -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": {