feat(providers): add GitHub Copilot subscription OAuth (device flow + copilot-token re-mint)
28th platform `github-copilot` (uses_oauth, ChatCompletions wire). Two-stage auth: RFC-8628 GitHub device flow (client Iv1.b507a08c87ecfe98, scope read:user, errors in a 200 body) mints the DURABLE github token; a GET api.github.com/copilot_internal/ v2/token exchange re-mints the SHORT-LIVED copilot token. Persisted as key=copilot token, refresh_token=github token, expires_at=copilot expiry; the "refresh" is a copilot-token re-mint (not a refresh_token grant), dispatched via OAuthTokenBody::GithubCopilotExchange in the generic refresher. VS Code editor-identity headers on /models + /chat/completions, gated on SamplerConfig.github_copilot / PlatformId::sends_copilot_editor_headers() so every other ChatCompletions provider stays byte-identical. Live /models filtered (parse_github_copilot_listing) to the openai-completions-served models: keep iff model_picker_enabled && policy.state!="disabled" && tool_calls!=false AND not a claude-4.x/5.x (messages) or gpt-5/oswe/mai- (responses-only) id — those need per-model wire routing (documented debt), excluded rather than mis-routed. Inherits the leak-safe pooled routing (scope oauth/github-copilot); its bearer/ refresh/api_key never touch the Kimi token (regression test added). Fail-fast on an out-of-range copilot expires_at (would otherwise silently 401 mid-session). Adversarial security review: GO, no CRITICAL/HIGH. Known limitation: Pi's per-model policy-enablement POST is not ported (documented in AGENTS.md).
This commit is contained in:
@@ -147,6 +147,43 @@ mod tests {
|
||||
.expect("claude-pro-max carries an OAuthConfig")
|
||||
}
|
||||
|
||||
fn copilot_oauth() -> &'static kigi_models::OAuthConfig {
|
||||
kigi_models::PlatformId::GithubCopilot
|
||||
.oauth()
|
||||
.expect("github-copilot carries an OAuthConfig")
|
||||
}
|
||||
|
||||
/// A `github-copilot/<model>` turn resolves to the process-global pooled
|
||||
/// github-copilot manager (its OWN `oauth/github-copilot` scope), NEVER the
|
||||
/// primary Kimi manager — the same leak-safe routing as xai-grok /
|
||||
/// claude-pro-max, and a DISTINCT pool entry from either.
|
||||
#[tokio::test]
|
||||
async fn github_copilot_model_resolves_to_its_own_manager_not_kimi() {
|
||||
let (_kd, kimi) = primary_with_token("kimi-tok");
|
||||
let home = tempfile::tempdir().unwrap();
|
||||
let resolved = manager_for_model(home.path(), "github-copilot/gpt-4.1", Some(&kimi))
|
||||
.expect("github-copilot model resolves to its pooled manager");
|
||||
assert!(
|
||||
!Arc::ptr_eq(&resolved, &kimi),
|
||||
"github-copilot must NOT resolve to the Kimi manager"
|
||||
);
|
||||
assert!(
|
||||
Arc::ptr_eq(&resolved, &global_manager_for(home.path(), copilot_oauth())),
|
||||
"github-copilot must resolve to its OWN process-global pooled manager"
|
||||
);
|
||||
assert!(
|
||||
!Arc::ptr_eq(&resolved, &global_manager_for(home.path(), claude_oauth())),
|
||||
"github-copilot and claude-pro-max must not share a pooled manager"
|
||||
);
|
||||
// Fail-fast: even with a Kimi primary, a copilot turn never yields the
|
||||
// Kimi bearer — it draws from the copilot pool (its own token, or None).
|
||||
assert_ne!(
|
||||
session_key_for_model(home.path(), "github-copilot/gpt-4.1", Some(&kimi)),
|
||||
Some("kimi-tok".to_string()),
|
||||
"a github-copilot model must never receive the primary Kimi token"
|
||||
);
|
||||
}
|
||||
|
||||
/// A `claude-pro-max/<model>` turn resolves to the process-global pooled
|
||||
/// claude-pro-max manager (its OWN `oauth/claude-pro-max` scope), NEVER the
|
||||
/// primary Kimi manager — the same leak-safe routing as xai-grok, and a
|
||||
|
||||
Reference in New Issue
Block a user