mirror of
https://github.com/RightNow-AI/openfang.git
synced 2026-07-30 06:32:17 +00:00
fix: rewrite Copilot driver with OAuth device flow authentication
The Copilot LLM driver was broken - it expected users to provide a GITHUB_TOKEN env var, but no standard token type (PAT, gh CLI token) works with the Copilot token exchange endpoint. Changes: - Full rewrite of copilot.rs with OAuth device flow using Copilot's client ID (Iv1.b507a08c87ecfe98) - Three-layer token chain: ghu_ (8h) -> Copilot API token (30min), with automatic caching and refresh - Dynamic model fetching from Copilot API on daemon startup and on model_not_supported error - Init wizard: TUI auth screen with device code display, live model picker after authentication - set-key command: interactive device flow for github-copilot provider - Doctor: detects Copilot auth via persisted token file - Removed static Copilot model entries (now fetched dynamically) - Simplified driver instantiation (no env vars needed) Tested end-to-end with Copilot Enterprise: auth, token exchange, 43 models fetched, completions working with claude-opus-4.6-1m. Closes #1014 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -2457,6 +2457,18 @@ decay_rate = 0.05
|
||||
}
|
||||
}
|
||||
|
||||
// Check GitHub Copilot auth (separate from env var checks)
|
||||
{
|
||||
let openfang_dir = cli_openfang_home();
|
||||
if openfang_runtime::drivers::copilot::copilot_auth_available(&openfang_dir) {
|
||||
any_key_set = true;
|
||||
if !json {
|
||||
ui::check_ok("GitHub Copilot (authenticated via device flow)");
|
||||
}
|
||||
checks.push(serde_json::json!({"check": "provider", "name": "GitHub Copilot", "status": "ok"}));
|
||||
}
|
||||
}
|
||||
|
||||
if !any_key_set {
|
||||
if !json {
|
||||
println!();
|
||||
@@ -4969,6 +4981,27 @@ fn cmd_config_unset(key: &str) {
|
||||
}
|
||||
|
||||
fn cmd_config_set_key(provider: &str) {
|
||||
// GitHub Copilot uses OAuth device flow, not a simple API key paste.
|
||||
if provider == "github-copilot" || provider == "copilot" {
|
||||
let openfang_dir = cli_openfang_home();
|
||||
let rt = tokio::runtime::Runtime::new().unwrap_or_else(|e| {
|
||||
ui::error(&format!("Failed to create async runtime: {e}"));
|
||||
std::process::exit(1);
|
||||
});
|
||||
match rt.block_on(openfang_runtime::drivers::copilot::run_interactive_setup(&openfang_dir)) {
|
||||
Ok(_) => {
|
||||
ui::success("GitHub Copilot configured successfully");
|
||||
ui::hint("Restart the daemon: openfang stop && openfang start");
|
||||
}
|
||||
Err(e) => {
|
||||
ui::error(&format!("Copilot setup failed: {e}"));
|
||||
ui::hint("Check your Client ID/Secret and try again");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let env_var = provider_to_env_var(provider);
|
||||
|
||||
let key = prompt_input(&format!(" Paste your {provider} API key: "));
|
||||
|
||||
@@ -159,10 +159,10 @@ const PROVIDERS: &[ProviderInfo] = &[
|
||||
ProviderInfo {
|
||||
name: "github-copilot",
|
||||
display: "GitHub Copilot",
|
||||
env_var: "GITHUB_TOKEN",
|
||||
default_model: "gpt-4o",
|
||||
needs_key: true,
|
||||
hint: "via PAT",
|
||||
env_var: "",
|
||||
default_model: "claude-sonnet-4.6",
|
||||
needs_key: false, // Auth handled via OAuth device flow after init
|
||||
hint: "free with subscription",
|
||||
},
|
||||
ProviderInfo {
|
||||
name: "replicate",
|
||||
@@ -257,6 +257,7 @@ enum Step {
|
||||
Welcome,
|
||||
Migration,
|
||||
Provider,
|
||||
CopilotAuth,
|
||||
ApiKey,
|
||||
Model,
|
||||
Routing,
|
||||
@@ -288,6 +289,26 @@ enum KeyTestState {
|
||||
Warn,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
enum CopilotAuthStatus {
|
||||
/// Requesting device code from GitHub.
|
||||
Starting,
|
||||
/// Waiting for user to authorize in browser.
|
||||
WaitingForUser,
|
||||
/// Authorized, fetching models.
|
||||
FetchingModels,
|
||||
/// Done — models loaded.
|
||||
Done,
|
||||
/// Error.
|
||||
Failed(String),
|
||||
}
|
||||
|
||||
enum CopilotAuthEvent {
|
||||
DeviceCode { user_code: String, verification_uri: String },
|
||||
Authenticated,
|
||||
Models(Vec<String>),
|
||||
}
|
||||
|
||||
/// A model entry for list display.
|
||||
struct ModelEntry {
|
||||
id: String,
|
||||
@@ -348,6 +369,11 @@ struct State {
|
||||
daemon_url: String,
|
||||
daemon_error: String,
|
||||
saving_done: bool,
|
||||
|
||||
// Copilot auth
|
||||
copilot_user_code: String,
|
||||
copilot_verification_uri: String,
|
||||
copilot_auth_status: CopilotAuthStatus,
|
||||
save_error: String,
|
||||
}
|
||||
|
||||
@@ -386,6 +412,9 @@ impl State {
|
||||
daemon_error: String::new(),
|
||||
saving_done: false,
|
||||
save_error: String::new(),
|
||||
copilot_user_code: String::new(),
|
||||
copilot_verification_uri: String::new(),
|
||||
copilot_auth_status: CopilotAuthStatus::Starting,
|
||||
};
|
||||
s.build_provider_order();
|
||||
s.provider_list.select(Some(0));
|
||||
@@ -431,6 +460,7 @@ impl State {
|
||||
Step::Welcome => "1 of 7",
|
||||
Step::Migration => "2 of 7",
|
||||
Step::Provider => "3 of 7",
|
||||
Step::CopilotAuth => "4 of 7",
|
||||
Step::ApiKey => "4 of 7",
|
||||
Step::Model => "5 of 7",
|
||||
Step::Routing => "6 of 7",
|
||||
@@ -610,12 +640,50 @@ pub fn run() -> InitResult {
|
||||
let (test_tx, test_rx) = std::sync::mpsc::channel::<bool>();
|
||||
let (migrate_tx, migrate_rx) =
|
||||
std::sync::mpsc::channel::<Result<openfang_migrate::report::MigrationReport, String>>();
|
||||
let (copilot_tx, copilot_rx) =
|
||||
std::sync::mpsc::channel::<Result<CopilotAuthEvent, String>>();
|
||||
|
||||
let result = loop {
|
||||
terminal
|
||||
.draw(|f| draw(f, f.area(), &mut state))
|
||||
.expect("draw failed");
|
||||
|
||||
// Check for Copilot auth events
|
||||
if state.step == Step::CopilotAuth {
|
||||
while let Ok(event) = copilot_rx.try_recv() {
|
||||
match event {
|
||||
Ok(CopilotAuthEvent::DeviceCode { user_code, verification_uri }) => {
|
||||
state.copilot_user_code = user_code;
|
||||
state.copilot_verification_uri = verification_uri;
|
||||
state.copilot_auth_status = CopilotAuthStatus::WaitingForUser;
|
||||
}
|
||||
Ok(CopilotAuthEvent::Authenticated) => {
|
||||
state.copilot_auth_status = CopilotAuthStatus::FetchingModels;
|
||||
}
|
||||
Ok(CopilotAuthEvent::Models(models)) => {
|
||||
state.copilot_auth_status = CopilotAuthStatus::Done;
|
||||
state.model_entries.clear();
|
||||
for model_id in &models {
|
||||
state.model_entries.push(ModelEntry {
|
||||
id: model_id.clone(),
|
||||
display_name: model_id.clone(),
|
||||
tier: "copilot",
|
||||
cost: "free".to_string(),
|
||||
});
|
||||
}
|
||||
if !state.model_entries.is_empty() {
|
||||
state.model_list.select(Some(0));
|
||||
}
|
||||
// Auto-advance to model picker
|
||||
state.step = Step::Model;
|
||||
}
|
||||
Err(e) => {
|
||||
state.copilot_auth_status = CopilotAuthStatus::Failed(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for background key-test result
|
||||
if state.key_test == KeyTestState::Testing {
|
||||
if let Ok(ok) = test_rx.try_recv() {
|
||||
@@ -750,7 +818,79 @@ pub fn run() -> InitResult {
|
||||
state.selected_provider = Some(prov_idx);
|
||||
let p = &PROVIDERS[prov_idx];
|
||||
|
||||
if !p.needs_key {
|
||||
if p.name == "github-copilot" {
|
||||
// Start Copilot device flow in background
|
||||
state.copilot_auth_status = CopilotAuthStatus::Starting;
|
||||
state.api_key_from_env = false;
|
||||
state.step = Step::CopilotAuth;
|
||||
|
||||
// Kick off background auth
|
||||
let copilot_tx = copilot_tx.clone();
|
||||
std::thread::spawn(move || {
|
||||
let openfang_dir = crate::cli_openfang_home();
|
||||
let rt = match tokio::runtime::Runtime::new() {
|
||||
Ok(rt) => rt,
|
||||
Err(e) => {
|
||||
let _ = copilot_tx.send(Err(format!("Runtime error: {e}")));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
rt.block_on(async {
|
||||
let http = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.build()
|
||||
.map_err(|e| format!("HTTP error: {e}"));
|
||||
let http = match http {
|
||||
Ok(h) => h,
|
||||
Err(e) => { let _ = copilot_tx.send(Err(e)); return; }
|
||||
};
|
||||
|
||||
// Step 1: request device code
|
||||
use openfang_runtime::drivers::copilot;
|
||||
let device = match copilot::request_device_code(&http).await {
|
||||
Ok(d) => d,
|
||||
Err(e) => { let _ = copilot_tx.send(Err(e)); return; }
|
||||
};
|
||||
|
||||
// Send device code to TUI for display
|
||||
let _ = copilot_tx.send(Ok(CopilotAuthEvent::DeviceCode {
|
||||
user_code: device.user_code.clone(),
|
||||
verification_uri: device.verification_uri.clone(),
|
||||
}));
|
||||
|
||||
// Browser will be opened by user pressing Enter in TUI
|
||||
|
||||
// Step 2: poll for token
|
||||
let tokens = match copilot::poll_for_token(
|
||||
&http,
|
||||
&device.device_code,
|
||||
device.interval,
|
||||
).await {
|
||||
Ok(t) => t,
|
||||
Err(e) => { let _ = copilot_tx.send(Err(e)); return; }
|
||||
};
|
||||
|
||||
// Save tokens
|
||||
if let Err(e) = tokens.save(&openfang_dir) {
|
||||
let _ = copilot_tx.send(Err(e));
|
||||
return;
|
||||
}
|
||||
|
||||
let _ = copilot_tx.send(Ok(CopilotAuthEvent::Authenticated));
|
||||
|
||||
// Step 3: fetch models
|
||||
let ct = match copilot::exchange_copilot_token(&http, &tokens.access_token).await {
|
||||
Ok(ct) => ct,
|
||||
Err(e) => { let _ = copilot_tx.send(Err(format!("Token exchange: {e}"))); return; }
|
||||
};
|
||||
match copilot::fetch_models(&http, &ct.base_url, &ct.token).await {
|
||||
Ok(models) => { let _ = copilot_tx.send(Ok(CopilotAuthEvent::Models(models))); }
|
||||
Err(e) => { let _ = copilot_tx.send(Err(format!("Model fetch: {e}"))); }
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if !p.needs_key {
|
||||
state.api_key_from_env = false;
|
||||
state.load_models_for_provider();
|
||||
state.step = Step::Model;
|
||||
@@ -769,6 +909,24 @@ pub fn run() -> InitResult {
|
||||
_ => {}
|
||||
},
|
||||
|
||||
Step::CopilotAuth => match key.code {
|
||||
KeyCode::Esc => {
|
||||
if matches!(state.copilot_auth_status, CopilotAuthStatus::Failed(_)) {
|
||||
state.step = Step::Provider;
|
||||
}
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
if matches!(state.copilot_auth_status, CopilotAuthStatus::WaitingForUser) {
|
||||
if !state.copilot_verification_uri.is_empty() {
|
||||
let _ = openfang_runtime::drivers::copilot::open_verification_url(
|
||||
&state.copilot_verification_uri,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
|
||||
Step::ApiKey => {
|
||||
if matches!(state.key_test, KeyTestState::Ok | KeyTestState::Warn) {
|
||||
continue;
|
||||
@@ -1242,6 +1400,7 @@ fn draw(f: &mut Frame, area: Rect, state: &mut State) {
|
||||
Step::Welcome => draw_welcome(f, chunks[3]),
|
||||
Step::Migration => draw_migration(f, chunks[3], state),
|
||||
Step::Provider => draw_provider(f, chunks[3], state),
|
||||
Step::CopilotAuth => draw_copilot_auth(f, chunks[3], state),
|
||||
Step::ApiKey => draw_api_key(f, chunks[3], state),
|
||||
Step::Model => draw_model(f, chunks[3], state),
|
||||
Step::Routing => draw_routing(f, chunks[3], state),
|
||||
@@ -1743,6 +1902,12 @@ fn draw_provider(f: &mut Frame, area: Rect, state: &mut State) {
|
||||
} else {
|
||||
"no API key needed".to_string()
|
||||
}
|
||||
} else if p.name == "github-copilot" {
|
||||
if detected {
|
||||
format!("{} detected", p.env_var)
|
||||
} else {
|
||||
"run set-key after init".to_string()
|
||||
}
|
||||
} else if detected {
|
||||
format!("{} detected", p.env_var)
|
||||
} else if !p.needs_key {
|
||||
@@ -1772,6 +1937,109 @@ fn draw_provider(f: &mut Frame, area: Rect, state: &mut State) {
|
||||
f.render_widget(hints, chunks[2]);
|
||||
}
|
||||
|
||||
fn draw_copilot_auth(f: &mut Frame, area: Rect, state: &mut State) {
|
||||
let chunks = Layout::vertical([
|
||||
Constraint::Length(2), // title
|
||||
Constraint::Length(1), // blank
|
||||
Constraint::Length(1), // status line 1
|
||||
Constraint::Length(1), // status line 2
|
||||
Constraint::Length(1), // blank
|
||||
Constraint::Length(1), // code label
|
||||
Constraint::Length(1), // code value
|
||||
Constraint::Length(1), // blank
|
||||
Constraint::Length(1), // url
|
||||
Constraint::Min(0), // spacer
|
||||
Constraint::Length(1), // hint
|
||||
])
|
||||
.split(area);
|
||||
|
||||
let title = Paragraph::new(Line::from(vec![
|
||||
Span::styled(" GitHub Copilot Authentication", Style::default().fg(theme::ACCENT)),
|
||||
]));
|
||||
f.render_widget(title, chunks[0]);
|
||||
|
||||
let spinner = theme::SPINNER_FRAMES[state.tick % theme::SPINNER_FRAMES.len()];
|
||||
|
||||
match &state.copilot_auth_status {
|
||||
CopilotAuthStatus::Starting => {
|
||||
let line = Paragraph::new(Line::from(vec![
|
||||
Span::raw(" "),
|
||||
Span::styled(spinner, Style::default().fg(theme::ACCENT)),
|
||||
Span::raw(" Requesting device code..."),
|
||||
]));
|
||||
f.render_widget(line, chunks[2]);
|
||||
}
|
||||
CopilotAuthStatus::WaitingForUser => {
|
||||
let line1 = Paragraph::new(Line::from(vec![
|
||||
Span::raw(" "),
|
||||
Span::styled(spinner, Style::default().fg(theme::ACCENT)),
|
||||
Span::raw(" Waiting for authorization..."),
|
||||
]));
|
||||
f.render_widget(line1, chunks[2]);
|
||||
|
||||
let code_label = Paragraph::new(Line::from(vec![
|
||||
Span::raw(" Enter this code:"),
|
||||
]));
|
||||
f.render_widget(code_label, chunks[5]);
|
||||
|
||||
let code_value = Paragraph::new(Line::from(vec![
|
||||
Span::raw(" "),
|
||||
Span::styled(
|
||||
&state.copilot_user_code,
|
||||
Style::default()
|
||||
.fg(theme::GREEN)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
]));
|
||||
f.render_widget(code_value, chunks[6]);
|
||||
|
||||
let url = Paragraph::new(Line::from(vec![
|
||||
Span::raw(" at "),
|
||||
Span::styled(&state.copilot_verification_uri, theme::dim_style()),
|
||||
]));
|
||||
f.render_widget(url, chunks[8]);
|
||||
|
||||
let hint = Paragraph::new(Line::from(vec![
|
||||
Span::styled(" [Enter] Open browser", theme::dim_style()),
|
||||
]));
|
||||
f.render_widget(hint, chunks[10]);
|
||||
}
|
||||
CopilotAuthStatus::FetchingModels => {
|
||||
let line = Paragraph::new(Line::from(vec![
|
||||
Span::styled(" \u{2714} ", Style::default().fg(theme::GREEN)),
|
||||
Span::raw("Authenticated"),
|
||||
]));
|
||||
f.render_widget(line, chunks[2]);
|
||||
|
||||
let line2 = Paragraph::new(Line::from(vec![
|
||||
Span::raw(" "),
|
||||
Span::styled(spinner, Style::default().fg(theme::ACCENT)),
|
||||
Span::raw(" Fetching available models..."),
|
||||
]));
|
||||
f.render_widget(line2, chunks[3]);
|
||||
}
|
||||
CopilotAuthStatus::Done => {
|
||||
let line = Paragraph::new(Line::from(vec![
|
||||
Span::styled(" \u{2714} ", Style::default().fg(theme::GREEN)),
|
||||
Span::raw("Models loaded"),
|
||||
]));
|
||||
f.render_widget(line, chunks[2]);
|
||||
}
|
||||
CopilotAuthStatus::Failed(err) => {
|
||||
let line = Paragraph::new(Line::from(vec![
|
||||
Span::styled(" \u{2718} ", Style::default().fg(theme::RED)),
|
||||
Span::raw(err.as_str()),
|
||||
]));
|
||||
f.render_widget(line, chunks[2]);
|
||||
|
||||
let hint = Paragraph::new(Line::from(vec![
|
||||
Span::styled(" Esc to go back", theme::dim_style()),
|
||||
]));
|
||||
f.render_widget(hint, chunks[10]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_api_key(f: &mut Frame, area: Rect, state: &mut State) {
|
||||
let p = match state.provider() {
|
||||
Some(p) => p,
|
||||
|
||||
@@ -509,6 +509,39 @@ impl OpenFangKernel {
|
||||
Self::boot_with_config(config)
|
||||
}
|
||||
|
||||
/// Fetch live Copilot models by exchanging the persisted token and querying the API.
|
||||
/// Works both inside and outside a tokio runtime.
|
||||
fn fetch_copilot_models(openfang_dir: &Path) -> Result<Vec<String>, String> {
|
||||
use openfang_runtime::drivers::copilot;
|
||||
|
||||
let tokens = copilot::PersistedTokens::load(&openfang_dir.to_path_buf())
|
||||
.ok_or("No persisted Copilot tokens found")?;
|
||||
|
||||
let fetch = async {
|
||||
let http = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.build()
|
||||
.map_err(|e| format!("HTTP client error: {e}"))?;
|
||||
|
||||
let ct = copilot::exchange_copilot_token(&http, &tokens.access_token).await?;
|
||||
copilot::fetch_models(&http, &ct.base_url, &ct.token).await
|
||||
};
|
||||
|
||||
// If we're already inside a tokio runtime (daemon start), use the existing one.
|
||||
// Otherwise (CLI commands), create a new one.
|
||||
if let Ok(handle) = tokio::runtime::Handle::try_current() {
|
||||
std::thread::scope(|s| {
|
||||
s.spawn(|| {
|
||||
handle.block_on(fetch)
|
||||
}).join().unwrap_or(Err("Thread panicked".to_string()))
|
||||
})
|
||||
} else {
|
||||
let rt = tokio::runtime::Runtime::new()
|
||||
.map_err(|e| format!("Failed to create runtime: {e}"))?;
|
||||
rt.block_on(fetch)
|
||||
}
|
||||
}
|
||||
|
||||
/// Boot the kernel with an explicit configuration.
|
||||
pub fn boot_with_config(mut config: KernelConfig) -> KernelResult<Self> {
|
||||
use openfang_types::config::KernelMode;
|
||||
@@ -746,6 +779,21 @@ impl OpenFangKernel {
|
||||
// Load user's custom models from ~/.openfang/custom_models.json
|
||||
let custom_models_path = config.home_dir.join("custom_models.json");
|
||||
model_catalog.load_custom_models(&custom_models_path);
|
||||
|
||||
// Fetch live Copilot models if authenticated
|
||||
if openfang_runtime::drivers::copilot::copilot_auth_available(&config.home_dir) {
|
||||
let copilot_dir = config.home_dir.clone();
|
||||
match Self::fetch_copilot_models(&copilot_dir) {
|
||||
Ok(models) => {
|
||||
info!(count = models.len(), "Fetched live Copilot model catalog");
|
||||
model_catalog.merge_discovered_models("github-copilot", &models);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to fetch Copilot models (will use static catalog): {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let available_count = model_catalog.available_models().len();
|
||||
let total_count = model_catalog.list_models().len();
|
||||
let local_count = model_catalog
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -139,8 +139,8 @@ fn provider_defaults(provider: &str) -> Option<ProviderDefaults> {
|
||||
}),
|
||||
"github-copilot" | "copilot" => Some(ProviderDefaults {
|
||||
base_url: copilot::GITHUB_COPILOT_BASE_URL,
|
||||
api_key_env: "GITHUB_TOKEN",
|
||||
key_required: true,
|
||||
api_key_env: "COPILOT_CLIENT_ID",
|
||||
key_required: false, // Auth handled via OAuth device flow, not simple API key
|
||||
}),
|
||||
"codex" | "openai-codex" => Some(ProviderDefaults {
|
||||
base_url: OPENAI_BASE_URL,
|
||||
@@ -334,27 +334,23 @@ pub fn create_driver(config: &DriverConfig) -> Result<Arc<dyn LlmDriver>, LlmErr
|
||||
)));
|
||||
}
|
||||
|
||||
// GitHub Copilot — wraps OpenAI-compatible driver with automatic token exchange.
|
||||
// The CopilotDriver exchanges the GitHub PAT for a Copilot API token on demand,
|
||||
// caches it, and refreshes when expired.
|
||||
// GitHub Copilot — OAuth device flow + OpenAI-compatible completions.
|
||||
// Authentication is handled automatically via persisted tokens from the device flow.
|
||||
// Run `openfang config set-key github-copilot` to authenticate.
|
||||
if provider == "github-copilot" || provider == "copilot" {
|
||||
let github_token = config
|
||||
.api_key
|
||||
.clone()
|
||||
.or_else(|| std::env::var("GITHUB_TOKEN").ok())
|
||||
.ok_or_else(|| {
|
||||
LlmError::MissingApiKey(
|
||||
"Set GITHUB_TOKEN environment variable for GitHub Copilot".to_string(),
|
||||
)
|
||||
})?;
|
||||
let base_url = config
|
||||
.base_url
|
||||
.clone()
|
||||
.unwrap_or_else(|| copilot::GITHUB_COPILOT_BASE_URL.to_string());
|
||||
return Ok(Arc::new(copilot::CopilotDriver::new(
|
||||
github_token,
|
||||
base_url,
|
||||
)));
|
||||
let openfang_dir = std::env::var("HOME")
|
||||
.or_else(|_| std::env::var("USERPROFILE"))
|
||||
.map(|h| std::path::PathBuf::from(h).join(".openfang"))
|
||||
.unwrap_or_else(|_| std::path::PathBuf::from(".openfang"));
|
||||
|
||||
if !copilot::copilot_auth_available(&openfang_dir) {
|
||||
return Err(LlmError::MissingApiKey(
|
||||
"Copilot not authenticated. Run `openfang config set-key github-copilot` to sign in."
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
return Ok(Arc::new(copilot::CopilotDriver::new(openfang_dir)));
|
||||
}
|
||||
|
||||
// Azure OpenAI — deployment-based URL with `api-key` header
|
||||
|
||||
@@ -75,6 +75,21 @@ impl ModelCatalog {
|
||||
continue;
|
||||
}
|
||||
|
||||
// GitHub Copilot: check for persisted OAuth tokens
|
||||
if provider.id == "github-copilot" || provider.id == "copilot" {
|
||||
let openfang_dir = std::env::var("HOME")
|
||||
.or_else(|_| std::env::var("USERPROFILE"))
|
||||
.map(|h| std::path::PathBuf::from(h).join(".openfang"))
|
||||
.unwrap_or_else(|_| std::path::PathBuf::from(".openfang"));
|
||||
provider.auth_status =
|
||||
if crate::drivers::copilot::copilot_auth_available(&openfang_dir) {
|
||||
AuthStatus::Configured
|
||||
} else {
|
||||
AuthStatus::Missing
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
if !provider.key_required {
|
||||
provider.auth_status = AuthStatus::NotRequired;
|
||||
continue;
|
||||
@@ -961,11 +976,10 @@ fn builtin_aliases() -> HashMap<String, String> {
|
||||
("command-r", "command-r-plus"),
|
||||
("command", "command-a"),
|
||||
// GitHub Copilot aliases
|
||||
("copilot", "copilot/gpt-4o"),
|
||||
("copilot-4o", "copilot/gpt-4o"),
|
||||
("copilot-4", "copilot/gpt-4"),
|
||||
("copilot-gpt4o", "copilot/gpt-4o"),
|
||||
("copilot-gpt4", "copilot/gpt-4"),
|
||||
("copilot", "gpt-4o"),
|
||||
("copilot-4o", "gpt-4o"),
|
||||
("copilot-opus", "claude-opus-4.6"),
|
||||
("copilot-sonnet", "claude-sonnet-4.6"),
|
||||
// Chinese model aliases
|
||||
("qwen", "qwen-plus"),
|
||||
("glm", "glm-5-20250605"),
|
||||
@@ -2904,36 +2918,9 @@ fn builtin_models() -> Vec<ModelCatalogEntry> {
|
||||
aliases: vec![],
|
||||
},
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// GitHub Copilot (2) — free for subscribers
|
||||
// GitHub Copilot — models fetched dynamically at runtime.
|
||||
// No static entries needed; see kernel.rs fetch_copilot_models().
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
ModelCatalogEntry {
|
||||
id: "copilot/gpt-4o".into(),
|
||||
display_name: "GPT-4o (Copilot)".into(),
|
||||
provider: "github-copilot".into(),
|
||||
tier: ModelTier::Smart,
|
||||
context_window: 128_000,
|
||||
max_output_tokens: 4_096,
|
||||
input_cost_per_m: 0.0,
|
||||
output_cost_per_m: 0.0,
|
||||
supports_tools: true,
|
||||
supports_vision: true,
|
||||
supports_streaming: true,
|
||||
aliases: vec!["copilot-gpt4o".into()],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "copilot/gpt-4".into(),
|
||||
display_name: "GPT-4 (Copilot)".into(),
|
||||
provider: "github-copilot".into(),
|
||||
tier: ModelTier::Frontier,
|
||||
context_window: 128_000,
|
||||
max_output_tokens: 4_096,
|
||||
input_cost_per_m: 0.0,
|
||||
output_cost_per_m: 0.0,
|
||||
supports_tools: true,
|
||||
supports_vision: false,
|
||||
supports_streaming: true,
|
||||
aliases: vec!["copilot-gpt4".into()],
|
||||
},
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// Qwen / Alibaba (6)
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user