Summary
When deploying self-hosted Honcho with an OpenAI-compatible provider (OpenRouter, vLLM, Together, Anyscale, etc.) — i.e. setting LLM_OPENAI_API_KEY to a non-OpenAI key — every dialectic / deriver / summary call fails with:
openai.AuthenticationError: Error code: 401 - {'error': {'message': 'Incorrect API key provided: sk-…',
'type': 'invalid_request_error', 'code': 'invalid_api_key'}, 'status': 401}
The SDK error mentions platform.openai.com because the client is hitting https://api.openai.com/v1 instead of the configured provider.
There is no warning that config.toml placed PROVIDER / MODEL at the wrong nesting level either — the operator silently runs against the hardcoded default model.
Repro
.env
LLM_OPENAI_API_KEY=sk-or-v1-… # OpenRouter key
LLM_VLLM_API_KEY=sk-or-v1-…
LLM_VLLM_BASE_URL=https://openrouter.ai/api/v1
LLM_EMBEDDING_API_KEY=sk-or-v1-…
LLM_EMBEDDING_BASE_URL=https://openrouter.ai/api/v1
config.toml excerpt (legacy flat shape, common in the wild):
[dialectic.levels.minimal]
PROVIDER = "vllm"
MODEL = "z-ai/glm-4.7-flash"
Then call peer.chat() / dialectic.chat(). Result: tenacity.RetryError wrapping AuthenticationError. After retry_attempts=3, the 401 surfaces to clients.
Root cause #1 — default OpenAI client lacks base_url
src/llm/registry.py:98
if settings.LLM.OPENAI_API_KEY:
CLIENTS["openai"] = AsyncOpenAI(
api_key=settings.LLM.OPENAI_API_KEY,
# no base_url → SDK defaults to https://api.openai.com/v1
)
LLMSettings (src/config.py:643) has OPENAI_API_KEY but no OPENAI_BASE_URL field, so any operator value is silently ignored.
Root cause #2 — flat PROVIDER/MODEL in config.toml silently fall back to default model
DialecticLevelSettings expects nested:
[dialectic.levels.minimal.model_config]
transport = "openai"
model = "z-ai/glm-4.7-flash"
But config.toml.example and most copy-pasted configs in the wild use the flat shape. When the parsed model_config is missing, Honcho falls back to _default_dialectic_levels() which hardcodes model="gpt-5.4-mini" — a model that doesn't exist on any provider, so OpenRouter responds 401 User not found.. No warning is logged.
Workaround
- Add
OPENAI_BASE_URL=https://openrouter.ai/api/v1 to .env (the OpenAI Python SDK reads this env var on AsyncOpenAI() instantiation).
- Reformat
config.toml: replace each PROVIDER/MODEL pair with a nested [<section>.model_config] block (transport=, model=).
- Note:
config.toml is baked into the Docker image — docker compose restart won't pick up host-side edits. Use docker cp or mount it as a volume in docker-compose.yml.
Proposed fix
- Add
OPENAI_BASE_URL to LLMSettings and pass it to AsyncOpenAI(base_url=…) in registry.py. Same for EmbeddingSettings.
- Validate
config.toml shape on startup: if a [dialectic.levels.*] (or [deriver], [summary], [dream]) section contains PROVIDER/MODEL at top level (instead of nested model_config), log a WARNING and either auto-migrate or refuse to start.
- Mount
config.toml as a volume in docker-compose.yml.example so operator edits survive restarts.
Environment
- Honcho: latest
main as of May 2026
- Deploy: docker compose self-hosted
- Provider: OpenRouter (also reproduces with vLLM)
- SDK:
openai Python
Summary
When deploying self-hosted Honcho with an OpenAI-compatible provider (OpenRouter, vLLM, Together, Anyscale, etc.) — i.e. setting
LLM_OPENAI_API_KEYto a non-OpenAI key — every dialectic / deriver / summary call fails with:The SDK error mentions
platform.openai.combecause the client is hittinghttps://api.openai.com/v1instead of the configured provider.There is no warning that
config.tomlplacedPROVIDER/MODELat the wrong nesting level either — the operator silently runs against the hardcoded default model.Repro
.envconfig.tomlexcerpt (legacy flat shape, common in the wild):Then call
peer.chat()/dialectic.chat(). Result:tenacity.RetryErrorwrappingAuthenticationError. Afterretry_attempts=3, the 401 surfaces to clients.Root cause #1 — default OpenAI client lacks
base_urlsrc/llm/registry.py:98LLMSettings(src/config.py:643) hasOPENAI_API_KEYbut noOPENAI_BASE_URLfield, so any operator value is silently ignored.Root cause #2 — flat
PROVIDER/MODELinconfig.tomlsilently fall back to default modelDialecticLevelSettingsexpects nested:But
config.toml.exampleand most copy-pasted configs in the wild use the flat shape. When the parsedmodel_configis missing, Honcho falls back to_default_dialectic_levels()which hardcodesmodel="gpt-5.4-mini"— a model that doesn't exist on any provider, so OpenRouter responds401 User not found.. No warning is logged.Workaround
OPENAI_BASE_URL=https://openrouter.ai/api/v1to.env(the OpenAI Python SDK reads this env var onAsyncOpenAI()instantiation).config.toml: replace eachPROVIDER/MODELpair with a nested[<section>.model_config]block (transport=,model=).config.tomlis baked into the Docker image —docker compose restartwon't pick up host-side edits. Usedocker cpor mount it as a volume indocker-compose.yml.Proposed fix
OPENAI_BASE_URLtoLLMSettingsand pass it toAsyncOpenAI(base_url=…)inregistry.py. Same forEmbeddingSettings.config.tomlshape on startup: if a[dialectic.levels.*](or[deriver],[summary],[dream]) section containsPROVIDER/MODELat top level (instead of nestedmodel_config), log aWARNINGand either auto-migrate or refuse to start.config.tomlas a volume indocker-compose.yml.exampleso operator edits survive restarts.Environment
mainas of May 2026openaiPython