# NousResearch/hermes-agent · Kimi custom endpoint auth and header fixes

## Open now

- Nothing open on GitHub.

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.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #6754. Verify it addresses your case.
- **Recommended:** read PR #6754 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #5796: \[Bug\]: Sync auxiliary vision calls fail with 403 on kimi-coding custom endpoint due to missing User-Agent header

## Prior & competing PRs

- **#6754: fix: fallback header preservation, kimi credential pool, TUI stale model display**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `agent/auxiliary_client.py`, `agent/credential_pool.py`, `cli.py`, `hermes_cli/auth.py`, `run_agent.py`
  - fix hunks:
  - `agent/auxiliary_client.py`

    ```diff
    @@ -702,7 +702,7 @@ def _resolve_api_key_provider() -> Tuple[Optional[OpenAI], Optional[str]]:
                 logger.debug("Auxiliary text client: %s (%s) via pool", pconfig.name, model)
                 extra = {}
                 if "api.kimi.com" in base_url.lower():
    -                extra["default_headers"] = {"User-Agent": "KimiCLI/1.0"}
    +                extra["default_headers"] = {"User-Agent": "KimiCLI/1.3"}
                 elif "api.githubcopilot.com" in base_url.lower():
                     from hermes_cli.models import copilot_default_headers
     
    ```

    ```diff
    @@ -721,7 +721,7 @@ def _resolve_api_key_provider() -> Tuple[Optional[OpenAI], Optional[str]]:
             logger.debug("Auxiliary text client: %s (%s)", pconfig.name, model)
             extra = {}
             if "api.kimi.com" in base_url.lower():
    -            extra["default_headers"] = {"User-Agent": "KimiCLI/1.0"}
    +            extra["default_headers"] = {"User-Agent": "KimiCLI/1.3"}
             elif "api.githubcopilot.com" in base_url.lower():
                 from hermes_cli.models import copilot_default_headers
     
    ```

    ```diff
    @@ -1195,7 +1195,7 @@ def _to_async_client(sync_client, model: str):
     
             async_kwargs["default_headers"] = copilot_default_headers()
         elif "api.kimi.com" in base_lower:
    -        async_kwargs["default_headers"] = {"User-Agent": "KimiCLI/1.0"}
    +        async_kwargs["default_headers"] = {"User-Agent": "KimiCLI/1.3"}
         return AsyncOpenAI(**async_kwargs), model
     
     
    ```

    ```diff
    @@ -1315,7 +1315,13 @@ def resolve_provider_client(
                     )
                     return None, None
                 final_model = model or _read_main_model() or "gpt-4o-mini"
    -            client = OpenAI(api_key=[redacted:secret-assignment] base_url=custom_base)
    +            extra = {}
    +            if "api.kimi.com" in custom_base.lower():
    +                extra["default_headers"] = {"User-Agent": "KimiCLI/1.3"}
    +            elif "api.githubcopilot.com" in custom_base.lower():
    +                from hermes_cli.models import copilot_default_headers
    +                extra["default_headers"] = copilot_default_headers()
    +            client = OpenAI(api_key=[redacted:secret-assignment] base_url=custom_base, **extra)
                 return (_to_async_client(client, final_model) if async_mode
                         else (client, final_model))
             # Try custom first, then codex, then API-key providers
    ```

    ```diff
    @@ -1394,7 +1400,7 @@ def resolve_provider_client(
             # Provider-specific headers
             headers = {}
             if "api.kimi.com" in base_url.lower():
    -            headers["User-Agent"] = "KimiCLI/1.0"
    +            headers["User-Agent"] = "KimiCLI/1.3"
             elif "api.githubcopilot.com" in base_url.lower():
                 from hermes_cli.models import copilot_default_headers
     
    ```
  - `agent/credential_pool.py`

    ```diff
    @@ -18,12 +18,14 @@
     from hermes_cli.auth import (
         CODEX_ACCESS_TOKEN_REFRESH_SKEW_SECONDS,
         DEFAULT_AGENT_KEY_MIN_TTL_SECONDS,
    +    KIMI_CODE_BASE_URL,
         PROVIDER_REGISTRY,
         _codex_access_token_is_expiring,
         _decode_jwt_claims,
         _import_codex_cli_tokens,
         _load_auth_store,
         _load_provider_state,
    +    _resolve_kimi_base_url,
         _resolve_zai_base_url,
         read_credential_pool,
         write_credential_pool,
    ```

    ```diff
    @@ -1084,7 +1086,9 @@ def _seed_from_env(provider: str, entries: List[PooledCredential]) -> Tuple[bool
             active_sources.add(source)
             auth_type = AUTH_TYPE_OAUTH if provider == "anthropic" and not token.startswith("sk-ant-api") else AUTH_TYPE_API_KEY
             base_url = env_url or pconfig.inference_base_url
    -        if provider == "zai":
    +        if provider == "kimi-coding":
    +            base_url = _resolve_kimi_base_url(token, pconfig.inference_base_url, env_url)
    +        elif provider == "zai":
                 base_url = _resolve_zai_base_url(token, pconfig.inference_base_url, env_url)
             changed |= _upsert_entry(
                 entries,
    ```
  - `cli.py`

    ```diff
    @@ -1603,7 +1603,12 @@ def _build_context_bar(self, percent_used: Optional[int], width: int = 10) -> st
             return f"[{('█' * filled) + ('░' * max(0, width - filled))}]"
     
         def _get_status_bar_snapshot(self) -> Dict[str, Any]:
    -        model_name = self.model or "unknown"
    +        # Prefer the agent's model name — it updates on fallback.
    +        # self.model reflects the originally configured model and never
    +        # changes mid-session, so the TUI would show a stale name after
    +        # _try_activate_fallback() switches provider/model.
    +        agent = getattr(self, "agent", None)
    +        model_name = (getattr(agent, "model", None) or self.model or "unknown")
             model_short = model_name.split("/")[-1] if "/" in model_name else model_name
             if model_short.endswith(".gguf"):
                 model_short = model_short[:-5]
    ```

    ```diff
    @@ -1629,7 +1634,6 @@ def _get_status_bar_snapshot(self) -> Dict[str, Any]:
                 "compressions": 0,
             }
     
    -        agent = getattr(self, "agent", None)
             if not agent:
                 return snapshot
     
    ```
  - `hermes_cli/auth.py`

    ```diff
    @@ -250,7 +250,7 @@ class ProviderConfig:
     # Kimi Code Endpoint Detection
     # =============================================================================
     
    -# Kimi Code (platform.kimi.ai) issues keys prefixed "sk-kimi-" that only work
    +# Kimi Code (kimi.com/code) issues keys prefixed "sk-kimi-" that only work
     # on api.kimi.com/coding/v1.  Legacy keys from platform.moonshot.ai work on
     # api.moonshot.ai/v1 (the default).  Auto-detect when user hasn't set
     # KIMI_BASE_URL explicitly.
    ```
  - `run_agent.py`

    ```diff
    @@ -4968,9 +4968,21 @@ def _try_activate_fallback(self) -> bool:
                     # Swap OpenAI client and config in-place
                     self.api_key = [redacted:secret-assignment]
                     self.client = fb_client
    +                # Preserve provider-specific headers that
    +                # resolve_provider_client() may have baked into
    +                # fb_client via the default_headers kwarg.  The OpenAI
    +                # SDK stores these in _custom_headers.  Without this,
    +                # subsequent request-client rebuilds (via
    +                # _create_request_openai_client) drop the headers,
    +                # causing 403s from providers like Kimi Coding that
    +                # require a User-Agent sentinel.
    +                fb_headers = getattr(fb_client, "_custom_headers", None)
    +                if not fb_headers:
    +                    fb_headers = getattr(fb_client, "default_headers", None)
                     self._client_kwargs = {
                         "api_key": [redacted:secret-assignment]
                         "base_url": fb_base_url,
    +                    **({"default_headers": dict(fb_headers)} if fb_headers else {}),
                     }
     
                 # Re-evaluate prompt caching for the new provider/model
    ```
- **#5797: fix(auxiliary_client): inject KimiCLI User-Agent for custom endpoint sync clients**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#6076`, `#6754`

## Fix status

- Merged to main in PR #6754 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-04-09`. This confirmation is old and may be out of date (a revert/reopen would not have invalidated it here); re-verify.
- Subsystem: `agent`
