Renew Copilot auth token before each LLM API request attempt#1
Renew Copilot auth token before each LLM API request attempt#1
Conversation
Agent-Logs-Url: https://github.com/itkonen/eca/sessions/3348aaf5-3cd1-4629-99de-170ec4e63c9d Co-authored-by: itkonen <37696708+itkonen@users.noreply.github.com>
itkonen
left a comment
There was a problem hiding this comment.
I'm slightly concerned if passing around db*/messenger/metrics is necessary. The changes touch multiple files, and my intuition was that this could be simply handled internally by the token object/class. I'm not at all familiar with the codebase or clojure, so I'm not sure if the current suggestion is optimal. In my language of preference, I would make the token a class that handles it's freshness internally, so that the users of that token would not need to worry about validity.
Agent-Logs-Url: https://github.com/itkonen/eca/sessions/41c45749-ba10-49b5-a449-e2e8e29ec513 Co-authored-by: itkonen <37696708+itkonen@users.noreply.github.com>
Refactored in b984531. Instead of passing The factory |
|
@copilot fix what's breaking the tests |
Agent-Logs-Url: https://github.com/itkonen/eca/sessions/9853614c-f3c8-4487-b99f-bd0d60a3d985 Co-authored-by: itkonen <37696708+itkonen@users.noreply.github.com>
Fixed in 1ddcb61. The failing test |
There was a problem hiding this comment.
Pull request overview
This PR aims to prevent GitHub Copilot auth tokens from expiring mid-workflow by resolving/renewing provider auth before each LLM API request attempt, rather than only at prompt boundaries.
Changes:
- Added a
silent-provider-auth-fnfactory in chat lifecycle to encapsulate silent token renewal + returning current auth. - Extended
llm-api/sync-or-async-prompt!with optional:provider-auth-fnand resolved auth before each retry attempt (sync + async). - Updated chat/rewrite call sites and adjusted rewrite test expectations; guarded
login-done!against nilchat-id.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/eca/llm_api.clj |
Adds :provider-auth-fn and resolves auth per retry attempt before calling prompt!. |
src/eca/features/chat/lifecycle.clj |
Introduces silent-provider-auth-fn closure for silent renewal + auth retrieval. |
src/eca/features/chat.clj |
Switches chat prompt to pass :provider-auth-fn from lifecycle. |
src/eca/features/rewrite.clj |
Switches rewrite prompt to pass an inline :provider-auth-fn. |
src/eca/features/login.clj |
Prevents nil-keyed chat updates by guarding login-done! with when chat-id. |
test/eca/features/rewrite_test.clj |
Updates test to invoke/assert :provider-auth-fn result rather than :provider-auth key. |
CHANGELOG.md |
Notes the token-renewal behavior change under Unreleased. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Agent-Logs-Url: https://github.com/itkonen/eca/sessions/545a12ee-f2c6-4cdb-923d-9e02ab7d0520 Co-authored-by: itkonen <37696708+itkonen@users.noreply.github.com>
Copilot tokens expire during long agentic workflows, causing 401 errors mid-execution. Token renewal was only triggered at prompt start and post-tool-call, leaving retry loops and multi-step sequences unprotected.
Changes
lifecycle.clj: Addsilent-provider-auth-fnfactory — returns a 0-arity closure that silently renews provider auth if expired and returns the current auth. This is the Clojure equivalent of a self-managing token: the closure encapsulates all renewal logic so callers ofsync-or-async-prompt!don't need to know about token validity.llm_api.clj: Add optional:provider-auth-fnparam tosync-or-async-prompt!; before each attempt (sync and async),(resolve-auth)is bound tocurrent-auth— which invokesprovider-auth-fnif provided, otherwise falls back to staticprovider-auth. Removedf.loginrequire fromllm_api.clj— renewal logic stays in lifecycle/callers.login.clj: Guardlogin-done!with(when chat-id ...)— prevents nil-keyed chat entries when renewal is invoked outside a chat context.chat.clj: Pass:provider-auth-fn (lifecycle/silent-provider-auth-fn chat-ctx).rewrite.clj: Pass:provider-auth-fnas an inline closure using the existingf.loginimport.rewrite_test.clj: Updatedprompt-default-model-and-auth-renew-testto invoke(:provider-auth-fn opts)inside the stubbedsync-or-async-prompt!and assert against the return value, replacing the removed:provider-authkey check.llm_api_test.clj: Added four tests covering:provider-auth-fnbehavior — verifying the closure is invoked before the initial attempt and again on each retry, for both sync (:stream false) and async/streaming paths, and that the resolved auth is what gets passed intoprompt!.Note: This fix targets Copilot OAuth tokens specifically — API-key providers are unaffected. 401 errors are classified as
:auth(not retryable), so a failed renewal surfaces immediately rather than burning retry budget.Original prompt
Problem
Users are experiencing 401 "unauthorized: token expired" errors during long agentic workflows with GitHub Copilot. The error occurs mid-workflow but the session remains intact, allowing users to manually continue by sending another message.
Root Cause
Token renewal (
maybe-renew-auth-token) is currently only called at these points:src/eca/features/chat.clj:509)src/eca/features/chat/tool_calls.clj:840)During long workflows, the GitHub Copilot token can expire:
When a user manually sends a message, it triggers token renewal at the start of the prompt handler, which is why manually continuing works.
Solution
Add token renewal checks before each LLM API request attempt in
sync-or-async-prompt!to ensure tokens are refreshed proactively during:This ensures tokens remain valid throughout the entire workflow lifecycle, not just at user-initiated prompt boundaries.
Implementation Details
Add
maybe-renew-auth-token!call insrc/eca/llm_api.cljin thesync-or-async-prompt!function:The renewal should:
db*atom with the new token and expirationThis matches the pattern already used in
src/eca/features/chat/tool_calls.clj:840where token renewal happens before continuing after tool execution.Testing
To verify the fix works:
Related Code
src/eca/features/login.clj:90-95src/eca/llm_providers/copilot.clj:61-68src/eca/features/chat.clj:509,src/eca/features/chat/tool_calls.clj:840The following is the prior conversation context from the user's chat exploration (may be truncated):
User: I'm getting this error, and it tends to occur in the middle of long agentic workflows:
LLM response status: 401 body: unauthorized: token expired
It interrupts the task, but the session remains intact so that I can ask the agent to continue. Could it be that token is checked and refreshed on user input but not on intermediate steps during the process?
The main developer said:
Can you diagnose whats going wrong? I would like my workflows to finish without the interrupting error.
Assistant: Based on my analysis of the code, I can now diagnose the issue you're experiencing.
Problem Diagnosis
You're getting a 401 "unauthorized: token expired" error during long agentic workflows with GitHub Copilot. The issue is that token renewal is only happening at specific points in the workflow, not during intermediate tool execution steps.
Current Token Renewal Points
Looking at the code,
maybe-renew-auth-tokenis called in these places:src/eca/features/chat.clj:509)src/eca/features/chat/tool_calls.clj:840)The Problem
However, during long agentic workflows, the token can expire during tool execution or between tool calls, especially when:
The developer's comment confirms this: GitHub Copilot has limits on how many requests can use
x-initiator: agentbefore needing to consume premium requests withx-initiator: user. When you manually send a message, it's treated asx-initiator: user, which refreshes your quota.Root Cause
The GitHub Copilot token (
api-keyin the auth state) has anexpires-attimestamp. Looking atsrc/eca/llm_providers/copilot.clj:64-68, tokens are obtained viaoauth-renew-tokenand include an expiration time.The token renewal check in
src/eca/features/login.clj:90-95compares the expiration time:But this check only happens at the start of prompts and after tool completion, not during the actual...
This pull request was created from Copilot chat.