NousResearch/hermes-agent

Kimi custom endpoint auth and header fixes

Cluster 5796 · agent. Data as of

Nothing open on GitHub. Fix merged in #6754 on 9 Apr 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #6754. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this unresolved. That is separate from the fix event.
Still open
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.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #6754 fix: fallback header preservation, kimi credential pool, TUI stale model display

    merged to main

    files: agent/auxiliary_client.py, agent/credential_pool.py, cli.py, hermes_cli/auth.py, run_agent.py

    Show the diff
    • agent/auxiliary_client.py
      @@ -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
       
      @@ -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
       
      @@ -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
       
       
      @@ -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
      @@ -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
      @@ -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,
      @@ -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
      @@ -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]
      @@ -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
      @@ -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
      @@ -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

1 PR closed without merging

From the close comments: merged via another change 1

  • #5797 fix(auxiliary_client): inject KimiCLI User-Agent for custom endpoint sync clients · merged via another change → #6076, #6754
Closed issues · 1
  • Closed#5796[Bug]: Sync auxiliary vision calls fail with 403 on kimi-coding custom endpoint due to missing User-Agent header
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: the share of item 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.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
agent
Merged PRs
#6754
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
9 Apr 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.

For your agent

The same facts, typed.