{"repo":"NousResearch/hermes-agent","public_id":"5796","display_label":"Kimi custom endpoint auth and header fixes","summary":"These members address authentication and header issues with Kimi/KimiCLI custom endpoints, including missing User-Agent headers causing 403 errors, credential pool handling, and related API support additions. A TUI stale model display fix is also bundled in.","subsystem":"agent","counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":6754,"merged_at":"2026-04-09T18:11:25+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"stale","last_verified_at":"2026-04-09T18:11:25+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":5796,"pr_number":6754,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-04-09T18:11:25+00:00"},{"issue_number":6075,"pr_number":6754,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-04-09T18:11:25+00:00"},{"issue_number":6398,"pr_number":6754,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-04-09T18:11:25+00:00"}],"members":[{"kind":"issue","number":5796,"title":"[Bug]: Sync auxiliary vision calls fail with 403 on kimi-coding custom endpoint due to missing User-Agent header","url":null,"state":"closed"},{"kind":"pr","number":5797,"title":"fix(auxiliary_client): inject KimiCLI User-Agent for custom endpoint sync clients","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#6076","number":6076},{"kind":"pr","value":"#6754","number":6754}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":6754,"title":"fix: fallback header preservation, kimi credential pool, TUI stale model display","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/auxiliary_client.py","agent/credential_pool.py","cli.py","hermes_cli/auth.py","run_agent.py"],"fix_hunks":[{"file":"agent/auxiliary_client.py","hunks":["@@ -702,7 +702,7 @@ def _resolve_api_key_provider() -> Tuple[Optional[OpenAI], Optional[str]]:\n             logger.debug(\"Auxiliary text client: %s (%s) via pool\", pconfig.name, model)\n             extra = {}\n             if \"api.kimi.com\" in base_url.lower():\n-                extra[\"default_headers\"] = {\"User-Agent\": \"KimiCLI/1.0\"}\n+                extra[\"default_headers\"] = {\"User-Agent\": \"KimiCLI/1.3\"}\n             elif \"api.githubcopilot.com\" in base_url.lower():\n                 from hermes_cli.models import copilot_default_headers\n ","@@ -721,7 +721,7 @@ def _resolve_api_key_provider() -> Tuple[Optional[OpenAI], Optional[str]]:\n         logger.debug(\"Auxiliary text client: %s (%s)\", pconfig.name, model)\n         extra = {}\n         if \"api.kimi.com\" in base_url.lower():\n-            extra[\"default_headers\"] = {\"User-Agent\": \"KimiCLI/1.0\"}\n+            extra[\"default_headers\"] = {\"User-Agent\": \"KimiCLI/1.3\"}\n         elif \"api.githubcopilot.com\" in base_url.lower():\n             from hermes_cli.models import copilot_default_headers\n ","@@ -1195,7 +1195,7 @@ def _to_async_client(sync_client, model: str):\n \n         async_kwargs[\"default_headers\"] = copilot_default_headers()\n     elif \"api.kimi.com\" in base_lower:\n-        async_kwargs[\"default_headers\"] = {\"User-Agent\": \"KimiCLI/1.0\"}\n+        async_kwargs[\"default_headers\"] = {\"User-Agent\": \"KimiCLI/1.3\"}\n     return AsyncOpenAI(**async_kwargs), model\n \n ","@@ -1315,7 +1315,13 @@ def resolve_provider_client(\n                 )\n                 return None, None\n             final_model = model or _read_main_model() or \"gpt-4o-mini\"\n-            client = OpenAI(api_key=[redacted:secret-assignment] base_url=custom_base)\n+            extra = {}\n+            if \"api.kimi.com\" in custom_base.lower():\n+                extra[\"default_headers\"] = {\"User-Agent\": \"KimiCLI/1.3\"}\n+            elif \"api.githubcopilot.com\" in custom_base.lower():\n+                from hermes_cli.models import copilot_default_headers\n+                extra[\"default_headers\"] = copilot_default_headers()\n+            client = OpenAI(api_key=[redacted:secret-assignment] base_url=custom_base, **extra)\n             return (_to_async_client(client, final_model) if async_mode\n                     else (client, final_model))\n         # Try custom first, then codex, then API-key providers","@@ -1394,7 +1400,7 @@ def resolve_provider_client(\n         # Provider-specific headers\n         headers = {}\n         if \"api.kimi.com\" in base_url.lower():\n-            headers[\"User-Agent\"] = \"KimiCLI/1.0\"\n+            headers[\"User-Agent\"] = \"KimiCLI/1.3\"\n         elif \"api.githubcopilot.com\" in base_url.lower():\n             from hermes_cli.models import copilot_default_headers\n "]},{"file":"agent/credential_pool.py","hunks":["@@ -18,12 +18,14 @@\n from hermes_cli.auth import (\n     CODEX_ACCESS_TOKEN_REFRESH_SKEW_SECONDS,\n     DEFAULT_AGENT_KEY_MIN_TTL_SECONDS,\n+    KIMI_CODE_BASE_URL,\n     PROVIDER_REGISTRY,\n     _codex_access_token_is_expiring,\n     _decode_jwt_claims,\n     _import_codex_cli_tokens,\n     _load_auth_store,\n     _load_provider_state,\n+    _resolve_kimi_base_url,\n     _resolve_zai_base_url,\n     read_credential_pool,\n     write_credential_pool,","@@ -1084,7 +1086,9 @@ def _seed_from_env(provider: str, entries: List[PooledCredential]) -> Tuple[bool\n         active_sources.add(source)\n         auth_type = AUTH_TYPE_OAUTH if provider == \"anthropic\" and not token.startswith(\"sk-ant-api\") else AUTH_TYPE_API_KEY\n         base_url = env_url or pconfig.inference_base_url\n-        if provider == \"zai\":\n+        if provider == \"kimi-coding\":\n+            base_url = _resolve_kimi_base_url(token, pconfig.inference_base_url, env_url)\n+        elif provider == \"zai\":\n             base_url = _resolve_zai_base_url(token, pconfig.inference_base_url, env_url)\n         changed |= _upsert_entry(\n             entries,"]},{"file":"cli.py","hunks":["@@ -1603,7 +1603,12 @@ def _build_context_bar(self, percent_used: Optional[int], width: int = 10) -> st\n         return f\"[{('█' * filled) + ('░' * max(0, width - filled))}]\"\n \n     def _get_status_bar_snapshot(self) -> Dict[str, Any]:\n-        model_name = self.model or \"unknown\"\n+        # Prefer the agent's model name — it updates on fallback.\n+        # self.model reflects the originally configured model and never\n+        # changes mid-session, so the TUI would show a stale name after\n+        # _try_activate_fallback() switches provider/model.\n+        agent = getattr(self, \"agent\", None)\n+        model_name = (getattr(agent, \"model\", None) or self.model or \"unknown\")\n         model_short = model_name.split(\"/\")[-1] if \"/\" in model_name else model_name\n         if model_short.endswith(\".gguf\"):\n             model_short = model_short[:-5]","@@ -1629,7 +1634,6 @@ def _get_status_bar_snapshot(self) -> Dict[str, Any]:\n             \"compressions\": 0,\n         }\n \n-        agent = getattr(self, \"agent\", None)\n         if not agent:\n             return snapshot\n "]},{"file":"hermes_cli/auth.py","hunks":["@@ -250,7 +250,7 @@ class ProviderConfig:\n # Kimi Code Endpoint Detection\n # =============================================================================\n \n-# Kimi Code (platform.kimi.ai) issues keys prefixed \"sk-kimi-\" that only work\n+# Kimi Code (kimi.com/code) issues keys prefixed \"sk-kimi-\" that only work\n # on api.kimi.com/coding/v1.  Legacy keys from platform.moonshot.ai work on\n # api.moonshot.ai/v1 (the default).  Auto-detect when user hasn't set\n # KIMI_BASE_URL explicitly."]},{"file":"run_agent.py","hunks":["@@ -4968,9 +4968,21 @@ def _try_activate_fallback(self) -> bool:\n                 # Swap OpenAI client and config in-place\n                 self.api_key = [redacted:secret-assignment]\n                 self.client = fb_client\n+                # Preserve provider-specific headers that\n+                # resolve_provider_client() may have baked into\n+                # fb_client via the default_headers kwarg.  The OpenAI\n+                # SDK stores these in _custom_headers.  Without this,\n+                # subsequent request-client rebuilds (via\n+                # _create_request_openai_client) drop the headers,\n+                # causing 403s from providers like Kimi Coding that\n+                # require a User-Agent sentinel.\n+                fb_headers = getattr(fb_client, \"_custom_headers\", None)\n+                if not fb_headers:\n+                    fb_headers = getattr(fb_client, \"default_headers\", None)\n                 self._client_kwargs = {\n                     \"api_key\": [redacted:secret-assignment]\n                     \"base_url\": fb_base_url,\n+                    **({\"default_headers\": dict(fb_headers)} if fb_headers else {}),\n                 }\n \n             # Re-evaluate prompt caching for the new provider/model\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}