Skip to content

fix(run): apply Copilot-Integration-Id header on codex_responses credential refresh to fix integrator routing (#63188) - #63204

Closed
webtecnica wants to merge 1 commit into
NousResearch:mainfrom
webtecnica:fix/63188-copilot-request-headers
Closed

fix(run): apply Copilot-Integration-Id header on codex_responses credential refresh to fix integrator routing (#63188)#63204
webtecnica wants to merge 1 commit into
NousResearch:mainfrom
webtecnica:fix/63188-copilot-request-headers

Conversation

@webtecnica

Copy link
Copy Markdown
Contributor

Summary

When model.api_mode = codex_responses, requests to Copilot arrive at GitHub without Copilot-Integration-Id: vscode-chat. GitHub attributes the request to copilot-language-server (smaller model whitelist) and returns 400 for models like claude-opus-4.8 that are available on the user's seat but not on that integrator's whitelist.

The chat_completions path correctly applies copilot_default_headers() via _apply_client_headers_for_base_url, but _try_refresh_codex_client_credentials never called that method after updating the base URL.

Change

Added self._apply_client_headers_for_base_url(str(self.base_url or "")) in _try_refresh_codex_client_credentials after the base_url and api_key are set, mirroring the pattern already present in _try_refresh_copilot_client_credentials and _swap_credential.

Verification

Provider attribution header tests pass.

@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint provider/copilot GitHub Copilot (ACP + Chat) area/auth Authentication, OAuth, credential pools P2 Medium — degraded but workaround exists labels Jul 12, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for tracing the credential-rebuild header path.

Automated hermes-sweeper review found that current main already provides this guarantee:

  • run_agent.py:4431 reapplies _apply_client_headers_for_base_url() in _try_refresh_copilot_client_credentials() before rebuilding the client.
  • agent/conversation_loop.py:2821-2827 routes Copilot 401 recovery through that method; the Codex refresh branch is limited to openai-codex and xai-oauth at agent/conversation_loop.py:2767-2774.
  • tests/run_agent/test_run_agent_codex_responses.py:1270 verifies the rebuilt Copilot client includes Copilot-Integration-Id: vscode-chat.

This behavior was shipped by 2cab8129d12e2ff0e234d71643aaf8697d02062f and is present in v2026.4.30, so this PR is redundant.

@teknium1 teknium1 closed this Jul 15, 2026
@teknium1 teknium1 added the sweeper:implemented-on-main Sweeper: behavior already present on current main label Jul 15, 2026
dstkwll added a commit to dstkwll/hermes-agent that referenced this pull request Jul 24, 2026
…oken 401

Copilot degrades in two related ways that both abort a turn as non-retryable
and only clear on a gateway restart (a cold process re-runs the token exchange):

1. HTTP 400 model_not_available_for_integrator / model_not_supported — a
   raw/degraded token routes to the restricted copilot-language-server
   integrator whose allowlist omits enterprise-only models (e.g.
   claude-opus-4.8). Because it is a 400 (not 401), the existing 401 refresh
   path never fired. Prevented (retry-with-backoff exchange + on-disk JWT
   persistence + header guard at the client chokepoint) and self-healed at
   runtime (single-shot forced re-exchange + client rebuild + retry before
   fallback).

2. HTTP 401 'IDE token expired: unauthorized: token expired' — the short-TTL
   *exchanged* IDE token expires mid-turn. The clean-401 path DID fire and call
   _try_refresh_copilot_client_credentials(), but that method only re-resolved
   the stable raw ghu_ token and rebuilt the client — it never evicted the
   cached exchanged JWT or forced a fresh exchange, so the retry put the SAME
   expired token back on the wire, 401'd again, and the single-shot guard
   aborted the turn. Fix: force a fresh IDE-token exchange (evict cached JWT via
   evict_cached_exchanged_token + re-mint via get_copilot_api_token) before the
   client rebuild, mirroring the merged auxiliary-path recovery (NousResearch#59837) and the
   400 recovery in this same PR. Graceful fallback to the resolved token if the
   exchange endpoint is unreachable; picks up the enterprise base_url on
   re-exchange.

Brings main-loop clean-401 recovery to parity with the merged auxiliary path
(NousResearch#59837), using the newer on-disk-aware evict helper. Companion context: NousResearch#58743
(this PR, expanded), NousResearch#51313, NousResearch#63204 (which assumed the 401 path already
recovered — it reached the method but the method was too weak).

Tests: exchange retry/persist round-trip, restart-blip disk reuse, stale-cred
400 classifier, 400 recovery, and 3 new 401 cases (fresh exchanged token on the
wire; network-blip fallback to resolved token). 58 copilot tests green on
current main.
teknium1 pushed a commit that referenced this pull request Aug 1, 2026
…oken 401

Copilot degrades in two related ways that both abort a turn as non-retryable
and only clear on a gateway restart (a cold process re-runs the token exchange):

1. HTTP 400 model_not_available_for_integrator / model_not_supported — a
   raw/degraded token routes to the restricted copilot-language-server
   integrator whose allowlist omits enterprise-only models (e.g.
   claude-opus-4.8). Because it is a 400 (not 401), the existing 401 refresh
   path never fired. Prevented (retry-with-backoff exchange + on-disk JWT
   persistence + header guard at the client chokepoint) and self-healed at
   runtime (single-shot forced re-exchange + client rebuild + retry before
   fallback).

2. HTTP 401 'IDE token expired: unauthorized: token expired' — the short-TTL
   *exchanged* IDE token expires mid-turn. The clean-401 path DID fire and call
   _try_refresh_copilot_client_credentials(), but that method only re-resolved
   the stable raw ghu_ token and rebuilt the client — it never evicted the
   cached exchanged JWT or forced a fresh exchange, so the retry put the SAME
   expired token back on the wire, 401'd again, and the single-shot guard
   aborted the turn. Fix: force a fresh IDE-token exchange (evict cached JWT via
   evict_cached_exchanged_token + re-mint via get_copilot_api_token) before the
   client rebuild, mirroring the merged auxiliary-path recovery (#59837) and the
   400 recovery in this same PR. Graceful fallback to the resolved token if the
   exchange endpoint is unreachable; picks up the enterprise base_url on
   re-exchange.

Brings main-loop clean-401 recovery to parity with the merged auxiliary path
(#59837), using the newer on-disk-aware evict helper. Companion context: #58743
(this PR, expanded), #51313, #63204 (which assumed the 401 path already
recovered — it reached the method but the method was too weak).

Tests: exchange retry/persist round-trip, restart-blip disk reuse, stale-cred
400 classifier, 400 recovery, and 3 new 401 cases (fresh exchanged token on the
wire; network-blip fallback to resolved token). 58 copilot tests green on
current main.
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…oken 401

Copilot degrades in two related ways that both abort a turn as non-retryable
and only clear on a gateway restart (a cold process re-runs the token exchange):

1. HTTP 400 model_not_available_for_integrator / model_not_supported — a
   raw/degraded token routes to the restricted copilot-language-server
   integrator whose allowlist omits enterprise-only models (e.g.
   claude-opus-4.8). Because it is a 400 (not 401), the existing 401 refresh
   path never fired. Prevented (retry-with-backoff exchange + on-disk JWT
   persistence + header guard at the client chokepoint) and self-healed at
   runtime (single-shot forced re-exchange + client rebuild + retry before
   fallback).

2. HTTP 401 'IDE token expired: unauthorized: token expired' — the short-TTL
   *exchanged* IDE token expires mid-turn. The clean-401 path DID fire and call
   _try_refresh_copilot_client_credentials(), but that method only re-resolved
   the stable raw ghu_ token and rebuilt the client — it never evicted the
   cached exchanged JWT or forced a fresh exchange, so the retry put the SAME
   expired token back on the wire, 401'd again, and the single-shot guard
   aborted the turn. Fix: force a fresh IDE-token exchange (evict cached JWT via
   evict_cached_exchanged_token + re-mint via get_copilot_api_token) before the
   client rebuild, mirroring the merged auxiliary-path recovery (NousResearch#59837) and the
   400 recovery in this same PR. Graceful fallback to the resolved token if the
   exchange endpoint is unreachable; picks up the enterprise base_url on
   re-exchange.

Brings main-loop clean-401 recovery to parity with the merged auxiliary path
(NousResearch#59837), using the newer on-disk-aware evict helper. Companion context: NousResearch#58743
(this PR, expanded), NousResearch#51313, NousResearch#63204 (which assumed the 401 path already
recovered — it reached the method but the method was too weak).

Tests: exchange retry/persist round-trip, restart-blip disk reuse, stale-cred
400 classifier, 400 recovery, and 3 new 401 cases (fresh exchanged token on the
wire; network-blip fallback to resolved token). 58 copilot tests green on
current main.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists provider/copilot GitHub Copilot (ACP + Chat) sweeper:implemented-on-main Sweeper: behavior already present on current main type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants