mirror of
https://github.com/block/goose.git
synced 2026-10-19 01:27:15 +00:00
fix(provider): refresh revoked Copilot tokens when listing models (#12627)
Some checks are pending
Canary / build-cli-linux (push) Blocked by required conditions
Canary / Upload Install Script (push) Waiting to run
Canary / bundle-windows (push) Blocked by required conditions
CI / changes (push) Waiting to run
CI / goose ACP npm packages (push) Blocked by required conditions
CI / Build and Test Rust Project (push) Blocked by required conditions
CI / Test Desktop Updater Install (ubuntu-latest) (push) Blocked by required conditions
CI / Test Desktop Updater Install (windows-latest) (push) Blocked by required conditions
MCP Conformance / Build Conformance Binaries (push) Waiting to run
MCP Conformance / Conformance 2025-11-25 / 0.1.16 (push) Blocked by required conditions
MCP Conformance / Conformance 2025-11-25 / 0.2.0-alpha.10 (push) Blocked by required conditions
MCP Conformance / Conformance 2026-07-28 / 0.2.0-alpha.10 (push) Blocked by required conditions
Create Minor Release PR / check-version-bump-pr (push) Waiting to run
Create Minor Release PR / release (push) Blocked by required conditions
Live Provider Tests / changes (push) Blocked by required conditions
Live Provider Tests / check-fork (push) Waiting to run
Live Provider Tests / Smoke Tests (push) Blocked by required conditions
Live Provider Tests / Smoke Tests (Code Execution) (push) Blocked by required conditions
Live Provider Tests / Compaction Tests (push) Blocked by required conditions
Publish Docker Image / docker (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
Canary / Prepare Version (push) Waiting to run
Canary / bundle-macos-arm64 (push) Blocked by required conditions
Canary / bundle-macos-x64 (push) Blocked by required conditions
Canary / bundle-windows-cuda (push) Blocked by required conditions
Canary / Release (push) Blocked by required conditions
Unused Dependencies / machete (push) Waiting to run
CI / Check Rust Code Format (push) Blocked by required conditions
CI / Build Lean ACP Binary (push) Blocked by required conditions
CI / Check and Test GDK Agent Crates on wasm32 (push) Blocked by required conditions
CI / Check goose-sdk UniFFI (push) Blocked by required conditions
CI / Test Kotlin GDK package (push) Blocked by required conditions
CI / Build and Test TLS Backend (native-tls) (push) Blocked by required conditions
CI / Build and Test TLS Backend (rustls-tls) (push) Blocked by required conditions
CI / Build and Test Roaming Feature (push) Blocked by required conditions
CI / Build Rust Project on Windows (push) Waiting to run
CI / Check MSRV (push) Blocked by required conditions
CI / Lint Rust Code (push) Blocked by required conditions
CI / Check Generated Schemas are Up-to-Date (push) Blocked by required conditions
CI / Check GDK Release Configuration (push) Waiting to run
CI / Check GDK API Reference is Up-to-Date (push) Waiting to run
CI / Test and Lint Electron Desktop App (push) Blocked by required conditions
Live Provider Tests / Build Binary (push) Blocked by required conditions
Some checks are pending
Canary / build-cli-linux (push) Blocked by required conditions
Canary / Upload Install Script (push) Waiting to run
Canary / bundle-windows (push) Blocked by required conditions
CI / changes (push) Waiting to run
CI / goose ACP npm packages (push) Blocked by required conditions
CI / Build and Test Rust Project (push) Blocked by required conditions
CI / Test Desktop Updater Install (ubuntu-latest) (push) Blocked by required conditions
CI / Test Desktop Updater Install (windows-latest) (push) Blocked by required conditions
MCP Conformance / Build Conformance Binaries (push) Waiting to run
MCP Conformance / Conformance 2025-11-25 / 0.1.16 (push) Blocked by required conditions
MCP Conformance / Conformance 2025-11-25 / 0.2.0-alpha.10 (push) Blocked by required conditions
MCP Conformance / Conformance 2026-07-28 / 0.2.0-alpha.10 (push) Blocked by required conditions
Create Minor Release PR / check-version-bump-pr (push) Waiting to run
Create Minor Release PR / release (push) Blocked by required conditions
Live Provider Tests / changes (push) Blocked by required conditions
Live Provider Tests / check-fork (push) Waiting to run
Live Provider Tests / Smoke Tests (push) Blocked by required conditions
Live Provider Tests / Smoke Tests (Code Execution) (push) Blocked by required conditions
Live Provider Tests / Compaction Tests (push) Blocked by required conditions
Publish Docker Image / docker (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
Canary / Prepare Version (push) Waiting to run
Canary / bundle-macos-arm64 (push) Blocked by required conditions
Canary / bundle-macos-x64 (push) Blocked by required conditions
Canary / bundle-windows-cuda (push) Blocked by required conditions
Canary / Release (push) Blocked by required conditions
Unused Dependencies / machete (push) Waiting to run
CI / Check Rust Code Format (push) Blocked by required conditions
CI / Build Lean ACP Binary (push) Blocked by required conditions
CI / Check and Test GDK Agent Crates on wasm32 (push) Blocked by required conditions
CI / Check goose-sdk UniFFI (push) Blocked by required conditions
CI / Test Kotlin GDK package (push) Blocked by required conditions
CI / Build and Test TLS Backend (native-tls) (push) Blocked by required conditions
CI / Build and Test TLS Backend (rustls-tls) (push) Blocked by required conditions
CI / Build and Test Roaming Feature (push) Blocked by required conditions
CI / Build Rust Project on Windows (push) Waiting to run
CI / Check MSRV (push) Blocked by required conditions
CI / Lint Rust Code (push) Blocked by required conditions
CI / Check Generated Schemas are Up-to-Date (push) Blocked by required conditions
CI / Check GDK Release Configuration (push) Waiting to run
CI / Check GDK API Reference is Up-to-Date (push) Waiting to run
CI / Test and Lint Electron Desktop App (push) Blocked by required conditions
Live Provider Tests / Build Binary (push) Blocked by required conditions
This commit is contained in:
parent
caadd5f6bc
commit
b9db895a77
1 changed files with 135 additions and 25 deletions
|
|
@ -17,6 +17,7 @@ use serde::{Deserialize, Serialize};
|
|||
use serde_json::Value;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use url::{Host, Url};
|
||||
|
|
@ -310,26 +311,42 @@ impl GithubCopilotProvider {
|
|||
model_config: &ModelConfig,
|
||||
path: &str,
|
||||
is_user_initiated: bool,
|
||||
payload: &mut Value,
|
||||
payload: &Value,
|
||||
has_images: bool,
|
||||
streaming: bool,
|
||||
) -> Result<Response, ProviderError> {
|
||||
for attempt in 0..2 {
|
||||
let (endpoint, token) = self.get_api_info().await?;
|
||||
let mut headers = self.get_github_headers();
|
||||
if has_images {
|
||||
headers.insert("Copilot-Vision-Request", "true".parse().unwrap());
|
||||
}
|
||||
let initiator = if is_user_initiated { "user" } else { "agent" };
|
||||
headers.insert("X-Initiator", initiator.parse().unwrap());
|
||||
let api_client = self.authenticated_api_client(endpoint, token.clone(), headers)?;
|
||||
let mut headers = self.get_github_headers();
|
||||
if has_images {
|
||||
headers.insert("Copilot-Vision-Request", "true".parse().unwrap());
|
||||
}
|
||||
let initiator = if is_user_initiated { "user" } else { "agent" };
|
||||
headers.insert("X-Initiator", initiator.parse().unwrap());
|
||||
|
||||
let response = api_client
|
||||
self.request_with_token_retry(headers, |api_client| async move {
|
||||
Ok(api_client
|
||||
.request(path)
|
||||
.model_headers(model_config)?
|
||||
.streaming(streaming)
|
||||
.response_post(payload)
|
||||
.await?;
|
||||
.await?)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn request_with_token_retry<F, Fut>(
|
||||
&self,
|
||||
headers: http::HeaderMap,
|
||||
request: F,
|
||||
) -> Result<Response, ProviderError>
|
||||
where
|
||||
F: Fn(ApiClient) -> Fut + Send,
|
||||
Fut: Future<Output = Result<Response, ProviderError>> + Send,
|
||||
{
|
||||
for attempt in 0..2 {
|
||||
let (endpoint, token) = self.get_api_info().await?;
|
||||
let api_client =
|
||||
self.authenticated_api_client(endpoint, token.clone(), headers.clone())?;
|
||||
let response = request(api_client).await?;
|
||||
if matches!(
|
||||
response.status(),
|
||||
reqwest::StatusCode::UNAUTHORIZED | reqwest::StatusCode::FORBIDDEN
|
||||
|
|
@ -492,13 +509,12 @@ impl GithubCopilotProvider {
|
|||
|
||||
let response = self
|
||||
.with_retry(|| async {
|
||||
let mut payload_clone = payload.clone();
|
||||
let resp = self
|
||||
.post(
|
||||
model_config,
|
||||
"responses",
|
||||
is_user_initiated,
|
||||
&mut payload_clone,
|
||||
&payload,
|
||||
has_images,
|
||||
true,
|
||||
)
|
||||
|
|
@ -540,13 +556,12 @@ impl GithubCopilotProvider {
|
|||
|
||||
let response = self
|
||||
.with_retry(|| async {
|
||||
let mut payload_clone = payload.clone();
|
||||
let resp = self
|
||||
.post(
|
||||
model_config,
|
||||
"chat/completions",
|
||||
is_user_initiated,
|
||||
&mut payload_clone,
|
||||
&payload,
|
||||
has_images,
|
||||
true,
|
||||
)
|
||||
|
|
@ -572,12 +587,11 @@ impl GithubCopilotProvider {
|
|||
|
||||
let response = self
|
||||
.with_retry(|| async {
|
||||
let mut payload_clone = payload.clone();
|
||||
self.post(
|
||||
model_config,
|
||||
"chat/completions",
|
||||
is_user_initiated,
|
||||
&mut payload_clone,
|
||||
&payload,
|
||||
has_images,
|
||||
false,
|
||||
)
|
||||
|
|
@ -703,8 +717,6 @@ impl Provider for GithubCopilotProvider {
|
|||
}
|
||||
|
||||
async fn fetch_supported_models(&self) -> Result<Vec<String>, ProviderError> {
|
||||
let (endpoint, token) = self.get_api_info().await?;
|
||||
|
||||
let mut headers = http::HeaderMap::new();
|
||||
headers.insert(http::header::ACCEPT, "application/json".parse().unwrap());
|
||||
headers.insert(
|
||||
|
|
@ -712,9 +724,12 @@ impl Provider for GithubCopilotProvider {
|
|||
"application/json".parse().unwrap(),
|
||||
);
|
||||
headers.insert("Copilot-Integration-Id", "vscode-chat".parse().unwrap());
|
||||
let api_client = self.authenticated_api_client(endpoint, token, headers)?;
|
||||
let response = api_client.response_get("models").await?;
|
||||
|
||||
let response = self
|
||||
.request_with_token_retry(headers, |api_client| async move {
|
||||
Ok(api_client.response_get("models").await?)
|
||||
})
|
||||
.await?;
|
||||
let response = handle_status(response).await?;
|
||||
let json: serde_json::Value = response.json().await?;
|
||||
|
||||
let arr = json.get("data").and_then(|v| v.as_array()).ok_or_else(|| {
|
||||
|
|
@ -983,6 +998,101 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
async fn fetch_supported_models_refreshes_revoked_tokens_with_bounded_retry() {
|
||||
use wiremock::matchers::header;
|
||||
|
||||
for (initial_status, exchange_status, retry_status) in [
|
||||
(401, 200, 200),
|
||||
(403, 200, 200),
|
||||
(403, 200, 401),
|
||||
(401, 200, 403),
|
||||
(403, 403, 200),
|
||||
] {
|
||||
let server = MockServer::start().await;
|
||||
let replacement_server = MockServer::start().await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/models"))
|
||||
.and(header("authorization", "Bearer revoked"))
|
||||
.respond_with(ResponseTemplate::new(initial_status))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/copilot-token"))
|
||||
.and(header("authorization", "bearer github-token"))
|
||||
.respond_with(ResponseTemplate::new(exchange_status).set_body_json(json!({
|
||||
"token": "replacement",
|
||||
"expires_at": 0,
|
||||
"refresh_in": 600,
|
||||
"endpoints": { "api": replacement_server.uri() }
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/models"))
|
||||
.and(header("authorization", "Bearer replacement"))
|
||||
.respond_with(ResponseTemplate::new(retry_status).set_body_json(json!({
|
||||
"data": [{ "id": "gpt-z" }, { "id": "gpt-a" }]
|
||||
})))
|
||||
.expect(if exchange_status == 200 { 1 } else { 0 })
|
||||
.mount(&replacement_server)
|
||||
.await;
|
||||
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let cache = DiskCache {
|
||||
cache_path: directory.path().join("info.json"),
|
||||
};
|
||||
cache
|
||||
.save(&CopilotState {
|
||||
expires_at: Utc::now() + chrono::Duration::minutes(10),
|
||||
info: CopilotTokenInfo {
|
||||
token: "revoked".to_string(),
|
||||
expires_at: 0,
|
||||
refresh_in: 600,
|
||||
endpoints: CopilotTokenEndpoints {
|
||||
api: server.uri(),
|
||||
_extra: HashMap::new(),
|
||||
},
|
||||
_extra: HashMap::new(),
|
||||
},
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let provider = GithubCopilotProvider {
|
||||
client: Client::new(),
|
||||
cache,
|
||||
mu: tokio::sync::Mutex::new(RefCell::new(None)),
|
||||
urls: GithubCopilotUrls {
|
||||
device_code_url: String::new(),
|
||||
access_token_url: String::new(),
|
||||
copilot_token_url: format!("{}/copilot-token", server.uri()),
|
||||
},
|
||||
client_id: DEFAULT_GITHUB_COPILOT_CLIENT_ID.to_string(),
|
||||
name: GITHUB_COPILOT_PROVIDER_NAME.to_string(),
|
||||
tls_config: None,
|
||||
};
|
||||
|
||||
std::env::set_var("GITHUB_COPILOT_TOKEN", "github-token");
|
||||
let result = provider.fetch_supported_models().await;
|
||||
std::env::remove_var("GITHUB_COPILOT_TOKEN");
|
||||
|
||||
if exchange_status == 200 && retry_status == 200 {
|
||||
assert_eq!(result.unwrap(), vec!["gpt-a", "gpt-z"]);
|
||||
assert_eq!(
|
||||
provider.cache.load().await.unwrap().info.token,
|
||||
"replacement"
|
||||
);
|
||||
} else {
|
||||
assert!(matches!(result, Err(ProviderError::Authentication(_))));
|
||||
assert!(provider.mu.lock().await.borrow().is_none());
|
||||
assert!(provider.cache.load().await.is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
async fn revoked_token_is_replaced_and_request_retried_once() {
|
||||
|
|
@ -1055,7 +1165,7 @@ mod tests {
|
|||
&ModelConfig::new("gpt-4.1"),
|
||||
"chat/completions",
|
||||
true,
|
||||
&mut json!({}),
|
||||
&json!({}),
|
||||
false,
|
||||
false,
|
||||
)
|
||||
|
|
@ -1122,7 +1232,7 @@ mod tests {
|
|||
&ModelConfig::new("gpt-5.4"),
|
||||
"responses",
|
||||
true,
|
||||
&mut json!({}),
|
||||
&json!({}),
|
||||
false,
|
||||
true,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue