fix(providers): route Copilot enterprise tokens via proxy endpoint#355
fix(providers): route Copilot enterprise tokens via proxy endpoint#355
Conversation
) Enterprise Copilot tokens receive a `proxy-ep` hostname in the token exchange response. Requests sent to the individual endpoint return HTTP 421. This change parses `proxy-ep`, persists it alongside the cached API token, and routes all API calls through the enterprise proxy when present. The proxy also requires `stream: true` for chat completions, so `complete()` transparently streams and collects when using an enterprise endpoint. Closes #352
Greptile SummaryThis PR adds enterprise GitHub Copilot account support by parsing the Key changes:
Issues found:
Confidence Score: 3/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant C as Caller
participant P as GitHubCopilotProvider
participant GH as GitHub API (token exchange)
participant IND as api.individual.githubcopilot.com
participant ENT as proxy.enterprise.githubcopilot.com
C->>P: complete(messages, tools)
P->>P: get_copilot_auth()
P->>GH: GET /copilot_internal/v2/token
GH-->>P: { token, expires_at, proxy-ep? }
P->>P: copilot_auth_from_parts(token, proxy_ep?)
P->>P: persist token + proxy_ep in account_id cache
alt is_enterprise == true
P->>ENT: POST /chat/completions (stream: true)
ENT-->>P: SSE stream
P->>P: collect_streamed_completion()
P->>P: stream_events_to_completion(events)
P-->>C: CompletionResponse
else is_enterprise == false
P->>IND: POST /chat/completions
IND-->>P: JSON response
P-->>C: CompletionResponse
end
Last reviewed commit: 3bb64be |
| match process_openai_sse_line(data, &mut state) { | ||
| SseLineResult::Done | SseLineResult::Events(_) | SseLineResult::Skip => {}, | ||
| } |
There was a problem hiding this comment.
Trailing-buffer events silently dropped
When the byte stream ends without a [DONE] line (or when [DONE] arrives in the final chunk without a trailing newline), process_openai_sse_line can return SseLineResult::Events(evts) for the remaining buffer content. The current match arm ignores those events instead of accumulating them, so any text or tool-call deltas in the tail are silently discarded.
Compare the parallel logic in stream_with_tools (lines 867–881), which correctly yields the events:
match process_openai_sse_line(data, &mut state) {
SseLineResult::Done | SseLineResult::Events(_) | SseLineResult::Skip => {},
}Should be:
| match process_openai_sse_line(data, &mut state) { | |
| SseLineResult::Done | SseLineResult::Events(_) | SseLineResult::Skip => {}, | |
| } | |
| match process_openai_sse_line(data, &mut state) { | |
| SseLineResult::Done => { | |
| events.extend(finalize_stream(&mut state)); | |
| return Ok(stream_events_to_completion(events)); | |
| }, | |
| SseLineResult::Events(evts) => events.extend(evts), | |
| SseLineResult::Skip => {}, | |
| } |
| refresh_token: None, | ||
| id_token: None, |
There was a problem hiding this comment.
account_id field repurposed for proxy hostname
account_id is being used here to persist the enterprise proxy_ep hostname — a field whose name implies it stores an account identifier. While this works as a persistence mechanism, it creates an implicit convention that future readers may not expect. Consider at least a short inline comment noting this dual purpose, or (if the OAuthTokens struct can be modified) adding a dedicated field.
| refresh_token: None, | |
| id_token: None, | |
| // NOTE: account_id is repurposed here to persist the enterprise | |
| // proxy-ep hostname so it can be recovered from the token cache. | |
| account_id: copilot_resp.proxy_ep.clone(), |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Merging this PR will not alter performance
Comparing Footnotes
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Summary
proxy-epfrom Copilot token exchange response and persist it alongside the cached API tokenproxy-epis presentstream: trueforcomplete()on enterprise proxy (non-streaming returns 400), transparently collecting the SSE stream into aCompletionResponseapi.individual.githubcopilot.comunchangedCloses #352
Validation
Completed
cargo +nightly-2025-11-30 fmt --all -- --checkcargo check -p moltis-providers -p moltis-gatewaycargo test -p moltis-providers --features provider-github-copilot -- copilot(23/23 pass)Remaining
./scripts/local-validate.sh <PR_NUMBER>Manual QA
GET {proxy-ep}/modelsreturns 200)complete()path works (tool-calling flows use this)🤖 Generated with Claude Code