From 1b01f0edf4e0a70a0c98aaa5174a124a2a54901d Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 2 Jun 2026 14:50:37 +0200 Subject: [PATCH] Honour explicit OpenRouter --model when it equals the adapter default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The adapter compared self._model to _DEFAULT_MODEL ("anthropic/claude-sonnet-4") to decide whether to honour the constructor's model. When a caller passes that exact value via --model, the comparison treats it as "not specified" and falls through to RunConfig.model_name, which defaults to "gpt-4". So every llm-connect call started with --provider openrouter --model anthropic/claude-sonnet-4 actually landed on OpenAI's gpt-4 — and on gpt-4 OpenAI's structured-output response_format requires a model with schema support that gpt-4 lacks, returning 400. The CUST-WP-0045 canary hit this for hours; the smoke probes that worked were the ones with no json_schema, where gpt-4 returned fine. Track _explicit_model separately so a constructor or LLMConfig that matches the default is still treated as a real intent. Co-Authored-By: Claude Opus 4.7 --- llm_connect/openrouter.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/llm_connect/openrouter.py b/llm_connect/openrouter.py index 9a7c283..623c5a8 100644 --- a/llm_connect/openrouter.py +++ b/llm_connect/openrouter.py @@ -37,6 +37,14 @@ class OpenRouterAdapter(LLMAdapter): max_retries: Optional[int] = None, ): self._config = config or LLMConfig() + # Track whether the model was explicitly supplied (constructor or + # LLMConfig). Comparing self._model to _DEFAULT_MODEL is not enough — + # callers who pass --model anthropic/claude-sonnet-4 happen to match + # the default and would otherwise be misrouted to RunConfig.model_name + # (which defaults to "gpt-4" — quietly sending every call to OpenAI's + # gpt-4 model, which is what broke the activity-core CUST-WP-0045 + # canary on 2026-06-02). + self._explicit_model = model is not None or self._config.model is not None self._model = model or self._config.model or _DEFAULT_MODEL self._api_base = (api_base or self._config.api_base).rstrip("/") self._system_prompt = system_prompt @@ -56,7 +64,14 @@ class OpenRouterAdapter(LLMAdapter): def execute_prompt(self, prompt: str, config: RunConfig) -> LLMResponse: self._preflight_budget(config) - model = self._model if self._model != _DEFAULT_MODEL else (config.model_name or self._model) + # Explicit constructor/LLMConfig model wins; only fall back to the + # per-call RunConfig.model_name when the adapter wasn't told what to + # use. RunConfig.model_name defaults to "gpt-4", so falling back + # unconditionally would silently misroute callers. + if self._explicit_model: + model = self._model + else: + model = config.model_name or self._model messages: list[Dict[str, str]] = [] if self._system_prompt: