From b52eeee96ff22be89b7c53f5e54f1aec602e0b3b Mon Sep 17 00:00:00 2001 From: Lookoff123 Date: Wed, 2 Sep 2026 15:45:17 +0500 Subject: [PATCH 1/2] chore(aimlapi): drop the PKCE sign-in and keep the declarative provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The upstream review asked for the API-key path only: "could you make it just use the api key one ... Declarative only - should be a much smaller change". Removes the browser sign-in flow and everything that reached it — the signup_aimlapi module with its loopback server and templates, the example, the config module wiring, and the setup-menu entry with its handler. The menu therefore returns to its original ordering as well. What remains is the declarative provider: the definition, its line in expose_declarative_providers!, and the docs row. Authentication is a plain AIMLAPI_API_KEY. The removed flow is kept on the pkce/agent-signin branch, where it stays available for this fork if it is ever wanted again. Co-Authored-By: Claude Opus 5 --- crates/goose-cli/src/commands/configure.rs | 41 --- crates/goose/examples/aimlapi_auth.rs | 39 --- crates/goose/src/config/mod.rs | 2 - crates/goose/src/config/signup_aimlapi/mod.rs | 276 ------------------ .../goose/src/config/signup_aimlapi/server.rs | 139 --------- .../signup_aimlapi/templates/error.html | 50 ---- .../signup_aimlapi/templates/invalid.html | 39 --- .../signup_aimlapi/templates/success.html | 45 --- .../goose/src/config/signup_aimlapi/tests.rs | 74 ----- 9 files changed, 705 deletions(-) delete mode 100644 crates/goose/examples/aimlapi_auth.rs delete mode 100644 crates/goose/src/config/signup_aimlapi/mod.rs delete mode 100644 crates/goose/src/config/signup_aimlapi/server.rs delete mode 100644 crates/goose/src/config/signup_aimlapi/templates/error.html delete mode 100644 crates/goose/src/config/signup_aimlapi/templates/invalid.html delete mode 100644 crates/goose/src/config/signup_aimlapi/templates/success.html delete mode 100644 crates/goose/src/config/signup_aimlapi/tests.rs diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs index 6cf9ec2dda66..a5895fd2b358 100644 --- a/crates/goose-cli/src/commands/configure.rs +++ b/crates/goose-cli/src/commands/configure.rs @@ -238,11 +238,6 @@ async fn handle_first_time_setup(config: &Config) -> anyhow::Result<()> { cliclack::intro(style(" goose-configure ").on_cyan().black())?; let setup_method = cliclack::select("How would you like to set up your provider?") - .item( - "aimlapi", - "AI/ML API Login (Recommended)", - "Sign in with AI/ML API to automatically configure models", - ) .item( "openrouter", "OpenRouter Login (Recommended)", @@ -281,18 +276,6 @@ async fn handle_first_time_setup(config: &Config) -> anyhow::Result<()> { ); } } - "aimlapi" => { - if let Err(e) = handle_aimlapi_auth().await { - let _ = config.clear(); - println!( - " - {} AI/ML API sign-in failed: {} - Please try again or use manual configuration", - style("Error").red().italic(), - e, - ); - } - } "manual" => handle_manual_provider_setup(config).await, _ => unreachable!(), } @@ -1966,30 +1949,6 @@ pub fn configure_max_turns_dialog() -> anyhow::Result<()> { Ok(()) } -/// Handle AI/ML API authentication (authorization code + PKCE) -pub async fn handle_aimlapi_auth() -> anyhow::Result<()> { - use goose::config::{configure_aimlapi, signup_aimlapi::AimlapiAuth}; - - let mut auth_flow = AimlapiAuth::new()?; - let api_key = auth_flow.complete_flow().await?; - println!( - " -Sign-in complete!" - ); - - let config = Config::global(); - - println!( - " -Configuring AI/ML API..." - ); - configure_aimlapi(config, api_key)?; - - println!("AI/ML API configuration complete"); - - Ok(()) -} - /// Handle OpenRouter authentication pub async fn handle_openrouter_auth() -> anyhow::Result<()> { use goose::config::{configure_openrouter, signup_openrouter::OpenRouterAuth}; diff --git a/crates/goose/examples/aimlapi_auth.rs b/crates/goose/examples/aimlapi_auth.rs deleted file mode 100644 index d119738c3d43..000000000000 --- a/crates/goose/examples/aimlapi_auth.rs +++ /dev/null @@ -1,39 +0,0 @@ -// Example of AI/ML API authorization-code + PKCE authentication. -// -// Run with: cargo run --example aimlapi_auth -// -// Requires AIMLAPI_PARTNER_ID. To exercise a non-production environment, also -// set AIMLAPI_APP_URL (the API host) and AIMLAPI_WEB_URL (the consent screen). - -use goose::config::signup_aimlapi::AimlapiAuth; - -#[tokio::main] -async fn main() -> Result<(), Box> { - println!("Testing AI/ML API PKCE flow...\n"); - - let mut auth_flow = AimlapiAuth::new()?; - - println!("Starting authentication flow..."); - println!("This will:"); - println!("1. Register an authorization request carrying the PKCE challenge"); - println!("2. Open your browser to the consent screen"); - println!("3. Wait for the redirect back to the loopback listener"); - println!("4. Exchange the one-time code plus the verifier for an api-key\n"); - - match auth_flow.complete_flow().await { - Ok(api_key) => { - println!("\nAuthentication successful."); - println!( - "API key received: {}...", - &api_key.chars().take(10).collect::() - ); - println!("\nYou can now use this key with the aimlapi provider."); - } - Err(e) => { - eprintln!("\nAuthentication failed: {}", e); - eprintln!("Error details: {:?}", e); - } - } - - Ok(()) -} diff --git a/crates/goose/src/config/mod.rs b/crates/goose/src/config/mod.rs index 94e63ac45df1..fcc331e03494 100644 --- a/crates/goose/src/config/mod.rs +++ b/crates/goose/src/config/mod.rs @@ -7,7 +7,6 @@ pub mod paths; pub mod permission; pub mod providers; pub mod search_path; -pub mod signup_aimlapi; pub mod signup_openrouter; pub mod signup_tetrate; pub mod tls; @@ -23,7 +22,6 @@ pub use extensions::{ }; pub use goose_providers::goose_mode::GooseMode; pub use permission::PermissionManager; -pub use signup_aimlapi::configure_aimlapi; pub use signup_openrouter::configure_openrouter; pub use signup_tetrate::configure_tetrate; diff --git a/crates/goose/src/config/signup_aimlapi/mod.rs b/crates/goose/src/config/signup_aimlapi/mod.rs deleted file mode 100644 index c84e44487726..000000000000 --- a/crates/goose/src/config/signup_aimlapi/mod.rs +++ /dev/null @@ -1,276 +0,0 @@ -pub mod server; - -#[cfg(test)] -mod tests; - -use anyhow::{anyhow, Result}; -use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; -use rand::{distr::Alphanumeric, RngExt}; -use reqwest::Client; -use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; -use std::time::Duration; -use tokio::sync::oneshot; -use tokio::time::timeout; - -use self::server::CallbackResult; - -/// Model the provider is set to after a successful sign-in. Any catalog id -/// works; this one is a broadly capable default the user can change with -/// `/model`. -const AIMLAPI_DEFAULT_MODEL: &str = "anthropic/claude-sonnet-5"; - -/// Account/consent host. Overridable so one build can be pointed at a -/// non-production environment for testing; the default is production and no -/// user ever needs to set it. -const AIMLAPI_APP_URL_ENV: &str = "AIMLAPI_APP_URL"; -pub(crate) const AIMLAPI_APP_URL_DEFAULT: &str = "https://app.aimlapi.com"; - -/// Where the browser consent screen lives (the page the user actually sees). -/// -/// This is sent as `verificationBaseUrl`, and the server hands it straight back -/// with `/agent/authorize` appended. The web app is served under an `/app/` -/// base path, so the base has to carry it: dropping the `/app` yields -/// `https://aimlapi.com/agent/authorize`, which is a 404 and strands the user -/// on the very first step of the flow. -const AIMLAPI_WEB_URL_ENV: &str = "AIMLAPI_WEB_URL"; -pub(crate) const AIMLAPI_WEB_URL_DEFAULT: &str = "https://aimlapi.com/app"; - -/// Partner attribution. AI/ML API expects a registered partner id on the -/// authorization request; it identifies goose as the integration that brought -/// the user, and carries no user data — no account, no prompt, no usage. -/// -/// goose's own registered id ships compiled in, so a normal install needs no -/// configuration. The environment variable exists to point a build at another -/// AI/ML API environment during testing, alongside the two URLs above. -const AIMLAPI_PARTNER_ID_ENV: &str = "AIMLAPI_PARTNER_ID"; -pub(crate) const AIMLAPI_PARTNER_ID_DEFAULT: &str = "part_R2KG8QMDBjtWAubVMgG0GF9L"; - -/// Loopback port the consent screen redirects back to. Chosen from the -/// ephemeral range and fixed, because it has to be registered with the -/// authorization request before the browser opens. -const CALLBACK_PORT: u16 = 53682; - -const AUTH_TIMEOUT: Duration = Duration::from_secs(300); // 5 minutes - -fn env_or(name: &str, default: &str) -> String { - std::env::var(name) - .ok() - .map(|v| v.trim().trim_end_matches('/').to_string()) - .filter(|v| !v.is_empty()) - .unwrap_or_else(|| default.to_string()) -} - -/// Authorization-code + PKCE sign-in for AI/ML API (RFC 7636). -/// -/// Unlike a provider that takes the challenge as a browser query parameter, -/// AI/ML API starts the request server-side: the CLI POSTs the challenge and -/// its loopback redirect, gets back a consent URL, and the browser is sent -/// there. The code that comes back to the loopback listener is exchanged with -/// the verifier for an api-key — the key is minted only at that exchange, and -/// only once. -#[derive(Debug)] -pub struct PkceAuthFlow { - code_verifier: String, - code_challenge: String, - state: String, - request_id: Option, - server_shutdown_tx: Option>, -} - -#[derive(Debug, Serialize)] -struct CreateAuthorizationRequest { - #[serde(rename = "partnerId")] - partner_id: String, - #[serde(rename = "agentName")] - agent_name: String, - #[serde(rename = "codeChallenge")] - code_challenge: String, - #[serde(rename = "codeChallengeMethod")] - code_challenge_method: String, - #[serde(rename = "redirectUri")] - redirect_uri: String, - state: String, - #[serde(rename = "verificationBaseUrl")] - verification_base_url: String, -} - -#[derive(Debug, Deserialize)] -struct CreateAuthorizationResponse { - #[serde(rename = "requestId")] - request_id: String, - #[serde(rename = "verificationUriComplete")] - verification_uri_complete: String, -} - -#[derive(Debug, Serialize)] -struct ExchangeRequest { - code: String, - #[serde(rename = "codeVerifier")] - code_verifier: String, -} - -#[derive(Debug, Deserialize)] -struct ExchangeResponse { - status: String, - #[serde(rename = "apiKey")] - api_key: Option, -} - -impl PkceAuthFlow { - pub fn new() -> Result { - // RFC 7636 §4.1 allows 43..128 unreserved characters. - let code_verifier: String = rand::rng() - .sample_iter(&Alphanumeric) - .take(96) - .map(char::from) - .collect(); - - let mut hasher = Sha256::new(); - hasher.update(&code_verifier); - let code_challenge = URL_SAFE_NO_PAD.encode(hasher.finalize()); - - let state: String = rand::rng() - .sample_iter(&Alphanumeric) - .take(24) - .map(char::from) - .collect(); - - Ok(Self { - code_verifier, - code_challenge, - state, - request_id: None, - server_shutdown_tx: None, - }) - } - - fn redirect_uri() -> String { - format!("http://127.0.0.1:{}/", CALLBACK_PORT) - } - - /// Registers the authorization request and returns the consent URL to open. - async fn start_authorization(&mut self) -> Result { - let app_url = env_or(AIMLAPI_APP_URL_ENV, AIMLAPI_APP_URL_DEFAULT); - let partner_id = env_or(AIMLAPI_PARTNER_ID_ENV, AIMLAPI_PARTNER_ID_DEFAULT); - - let body = CreateAuthorizationRequest { - partner_id, - agent_name: "goose".to_string(), - code_challenge: self.code_challenge.clone(), - code_challenge_method: "S256".to_string(), - redirect_uri: Self::redirect_uri(), - state: self.state.clone(), - verification_base_url: env_or(AIMLAPI_WEB_URL_ENV, AIMLAPI_WEB_URL_DEFAULT), - }; - - let response = Client::new() - .post(format!("{}/v1/agent-auth/authorizations", app_url)) - .json(&body) - .send() - .await?; - - if !response.status().is_success() { - let status = response.status(); - let detail = response.text().await.unwrap_or_default(); - return Err(anyhow!( - "Could not start AI/ML API sign-in: {} - {}", - status, - detail - )); - } - - let created: CreateAuthorizationResponse = response.json().await?; - self.request_id = Some(created.request_id); - Ok(created.verification_uri_complete) - } - - /// Starts the loopback listener and waits for the browser to come back. - async fn wait_for_callback(&mut self) -> Result { - let (code_tx, code_rx) = oneshot::channel::(); - let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>(); - self.server_shutdown_tx = Some(shutdown_tx); - - tokio::spawn(async move { - if let Err(e) = server::run_callback_server(code_tx, shutdown_rx).await { - eprintln!("Callback server error: {}", e); - } - }); - - match timeout(AUTH_TIMEOUT, code_rx).await { - Ok(Ok(result)) => Ok(result), - Ok(Err(_)) => Err(anyhow!("Did not receive an authorization code")), - Err(_) => Err(anyhow!("Sign-in timed out - please try again")), - } - } - - /// Exchanges the one-time code plus the verifier for the api-key. - async fn exchange_code(&self, code: String) -> Result { - let app_url = env_or(AIMLAPI_APP_URL_ENV, AIMLAPI_APP_URL_DEFAULT); - let response = Client::new() - .post(format!("{}/v1/agent-auth/token/code", app_url)) - .json(&ExchangeRequest { - code, - code_verifier: self.code_verifier.clone(), - }) - .send() - .await?; - - if !response.status().is_success() { - let status = response.status(); - let detail = response.text().await.unwrap_or_default(); - return Err(anyhow!("Key exchange failed: {} - {}", status, detail)); - } - - let exchanged: ExchangeResponse = response.json().await?; - match (exchanged.status.as_str(), exchanged.api_key) { - ("approved", Some(key)) => Ok(key), - // The server deliberately does not distinguish an unknown code from - // a bad verifier from a real expiry, so neither does this message. - (status, _) => Err(anyhow!( - "Sign-in did not complete ({}). Please run the sign-in again.", - status - )), - } - } - - /// Full flow: register, open the browser, catch the redirect, exchange. - pub async fn complete_flow(&mut self) -> Result { - let consent_url = self.start_authorization().await?; - - println!("Opening your browser to authorize goose..."); - if let Err(e) = webbrowser::open(&consent_url) { - eprintln!("Could not open the browser automatically: {}", e); - println!("Open this URL manually: {}", consent_url); - } - - println!("Waiting for you to approve in the browser..."); - let callback = self.wait_for_callback().await?; - - // RFC 6749 §10.12: a redirect whose state is not the one we sent did not - // come from the request we started, so its code is not ours to redeem. - if callback.state.as_deref() != Some(self.state.as_str()) { - return Err(anyhow!( - "The sign-in response did not match this request - please try again" - )); - } - - let api_key = self.exchange_code(callback.code).await?; - - if let Some(tx) = self.server_shutdown_tx.take() { - let _ = tx.send(()); - } - - Ok(api_key) - } -} - -pub use self::PkceAuthFlow as AimlapiAuth; - -use crate::config::Config; - -pub fn configure_aimlapi(config: &Config, api_key: String) -> Result<()> { - config.set_secret("AIMLAPI_API_KEY", &api_key)?; - crate::config::set_active_provider(config, "aimlapi", AIMLAPI_DEFAULT_MODEL)?; - Ok(()) -} diff --git a/crates/goose/src/config/signup_aimlapi/server.rs b/crates/goose/src/config/signup_aimlapi/server.rs deleted file mode 100644 index 559f8492be58..000000000000 --- a/crates/goose/src/config/signup_aimlapi/server.rs +++ /dev/null @@ -1,139 +0,0 @@ -use anyhow::Result; -use axum::{ - extract::Query, - http::StatusCode, - response::{Html, IntoResponse}, - routing::get, - Router, -}; -use include_dir::{include_dir, Dir}; -use minijinja::{context, Environment}; -use serde::Deserialize; -use std::net::SocketAddr; -use tokio::sync::oneshot; - -static TEMPLATES_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/config/signup_aimlapi/templates"); - -#[derive(Debug, Deserialize)] -struct CallbackQuery { - code: Option, - state: Option, - error: Option, -} - -/// What the loopback listener hands back to the flow: the one-time code plus -/// the `state` the server echoed, so the caller can prove the redirect belongs -/// to the request it started (RFC 6749 §10.12). -#[derive(Debug)] -pub struct CallbackResult { - pub code: String, - pub state: Option, -} - -/// Run the callback server on 127.0.0.1:53682 -pub async fn run_callback_server( - code_tx: oneshot::Sender, - shutdown_rx: oneshot::Receiver<()>, -) -> Result<()> { - let app = Router::new().route("/", get(handle_callback)); - let addr = SocketAddr::from(([127, 0, 0, 1], 53682)); - let listener = tokio::net::TcpListener::bind(addr).await?; - let state = std::sync::Arc::new(tokio::sync::Mutex::new(Some(code_tx))); - - axum::serve(listener, app.with_state(state.clone()).into_make_service()) - .with_graceful_shutdown(async move { - let _ = shutdown_rx.await; - }) - .await?; - - Ok(()) -} - -async fn handle_callback( - Query(params): Query, - state: axum::extract::State< - std::sync::Arc>>>, - >, -) -> impl IntoResponse { - if let Some(error) = params.error { - let mut env = Environment::new(); - let template_content = TEMPLATES_DIR - .get_file("error.html") - .expect("error.html template not found") - .contents_utf8() - .expect("error.html is not valid UTF-8"); - - env.add_template("error.html", template_content).unwrap(); - let tmpl = env.get_template("error.html").unwrap(); - let rendered = tmpl.render(context! { error => error }).unwrap(); - - return (StatusCode::BAD_REQUEST, Html(rendered)); - } - - if let Some(code) = params.code { - let mut tx_guard = state.lock().await; - if let Some(tx) = tx_guard.take() { - let _ = tx.send(CallbackResult { - code, - state: params.state, - }); - } - - let success_html = TEMPLATES_DIR - .get_file("success.html") - .expect("success.html template not found") - .contents_utf8() - .expect("success.html is not valid UTF-8"); - - return (StatusCode::OK, Html(success_html.to_string())); - } - - let invalid_html = TEMPLATES_DIR - .get_file("invalid.html") - .expect("invalid.html template not found") - .contents_utf8() - .expect("invalid.html is not valid UTF-8"); - - (StatusCode::BAD_REQUEST, Html(invalid_html.to_string())) -} - -#[cfg(test)] -mod tests { - use super::*; - use axum::body::to_bytes; - - async fn error_response(error: &str) -> (StatusCode, String) { - let state = std::sync::Arc::new(tokio::sync::Mutex::new(None)); - let response = handle_callback( - Query(CallbackQuery { - code: None, - state: None, - error: Some(error.to_string()), - }), - axum::extract::State(state), - ) - .await - .into_response(); - let status = response.status(); - let body = to_bytes(response.into_body(), usize::MAX).await.unwrap(); - (status, String::from_utf8(body.to_vec()).unwrap()) - } - - #[tokio::test] - async fn error_response_escapes_html() { - let payload = r#"&"#; - let (status, body) = error_response(payload).await; - - assert_eq!(status, StatusCode::BAD_REQUEST); - assert!(!body.contains(payload)); - assert!(body.contains("<script>")); - assert!(body.contains("&")); - } - - #[tokio::test] - async fn error_response_preserves_plain_text() { - let (_, body) = error_response("authorization denied").await; - - assert!(body.contains("authorization denied")); - } -} diff --git a/crates/goose/src/config/signup_aimlapi/templates/error.html b/crates/goose/src/config/signup_aimlapi/templates/error.html deleted file mode 100644 index b9effc9fba79..000000000000 --- a/crates/goose/src/config/signup_aimlapi/templates/error.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - Authentication Failed - - - -
-

❌ Authentication Failed

-

There was an error during the authentication process.

-
{{ error }}
-

Please close this tab and try again.

-
- - diff --git a/crates/goose/src/config/signup_aimlapi/templates/invalid.html b/crates/goose/src/config/signup_aimlapi/templates/invalid.html deleted file mode 100644 index 6bc9bbee8d5f..000000000000 --- a/crates/goose/src/config/signup_aimlapi/templates/invalid.html +++ /dev/null @@ -1,39 +0,0 @@ - - - - Invalid Request - - - -
-

⚠️ Invalid Request

-

This doesn't appear to be a valid authentication callback.

-

Please close this tab and try the authentication process again.

-
- - diff --git a/crates/goose/src/config/signup_aimlapi/templates/success.html b/crates/goose/src/config/signup_aimlapi/templates/success.html deleted file mode 100644 index 6219cbcd5e42..000000000000 --- a/crates/goose/src/config/signup_aimlapi/templates/success.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - Authentication Successful - - - -
-
-

Authentication Successful!

-

You have successfully authenticated with AI/ML API.

-

You can now close this tab and return to goose.

-
- - diff --git a/crates/goose/src/config/signup_aimlapi/tests.rs b/crates/goose/src/config/signup_aimlapi/tests.rs deleted file mode 100644 index 9974677e5c67..000000000000 --- a/crates/goose/src/config/signup_aimlapi/tests.rs +++ /dev/null @@ -1,74 +0,0 @@ -use crate::config::signup_aimlapi::{ - PkceAuthFlow, AIMLAPI_APP_URL_DEFAULT, AIMLAPI_PARTNER_ID_DEFAULT, AIMLAPI_WEB_URL_DEFAULT, -}; -use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; -use sha2::{Digest, Sha256}; - -#[test] -fn challenge_is_the_s256_hash_of_the_verifier() { - let flow = PkceAuthFlow::new().expect("Failed to create PKCE flow"); - - let mut hasher = Sha256::new(); - hasher.update(flow.code_verifier.as_bytes()); - let expected = URL_SAFE_NO_PAD.encode(hasher.finalize()); - - assert_eq!(flow.code_challenge, expected); -} - -#[test] -fn challenge_and_state_are_url_safe_and_unpadded() { - let flow = PkceAuthFlow::new().expect("Failed to create PKCE flow"); - - for value in [&flow.code_challenge, &flow.state] { - assert!(!value.contains('='), "{value} is padded"); - assert!(!value.contains('+'), "{value} is not url-safe"); - assert!(!value.contains('/'), "{value} is not url-safe"); - } -} - -#[test] -fn each_flow_gets_its_own_verifier_state_and_challenge() { - let a = PkceAuthFlow::new().expect("Failed to create PKCE flow 1"); - let b = PkceAuthFlow::new().expect("Failed to create PKCE flow 2"); - - assert_ne!(a.code_verifier, b.code_verifier); - assert_ne!(a.code_challenge, b.code_challenge); - assert_ne!(a.state, b.state); -} - -#[test] -fn consent_base_keeps_the_app_path() { - // The server appends "/agent/authorize" to whatever base it is handed. The - // web app is served under an "/app/" base path, so dropping it produces - // https://aimlapi.com/agent/authorize — a 404 that strands the user on the - // first step of the flow. This has to survive future tidying of the URL. - assert!( - AIMLAPI_WEB_URL_DEFAULT.ends_with("/app"), - "consent base must carry the /app path, got {AIMLAPI_WEB_URL_DEFAULT}" - ); -} - -#[test] -fn api_and_consent_hosts_are_distinct() { - // The registration/exchange calls go to the API host; only the browser is - // sent to the consent host. Collapsing the two would send API calls to the - // marketing site. - assert_ne!(AIMLAPI_APP_URL_DEFAULT, AIMLAPI_WEB_URL_DEFAULT); - assert!(AIMLAPI_APP_URL_DEFAULT.starts_with("https://")); - assert!(AIMLAPI_WEB_URL_DEFAULT.starts_with("https://")); -} - -#[test] -fn partner_id_matches_the_gateway_pattern() { - // The gateway only attributes ids shaped part_; anything else is - // treated as untagged usage and earns nothing. - let id = AIMLAPI_PARTNER_ID_DEFAULT; - - assert!(id.starts_with("part_"), "{id} lacks the part_ prefix"); - let rest = &id["part_".len()..]; - assert!(!rest.is_empty(), "{id} has an empty body"); - assert!( - rest.chars().all(|c| c.is_ascii_alphanumeric()), - "{id} has non-alphanumeric characters after the prefix" - ); -} From ed7e8c8d3064bbdecfc86f258e40187c3a2e49e4 Mon Sep 17 00:00:00 2001 From: Lookoff123 Date: Wed, 2 Sep 2026 15:51:18 +0500 Subject: [PATCH 2/2] test(aimlapi): match the registry wiring test to its neighbours The test also asserted where "AI/ML API" lands in the sorted provider list. That is not what a wiring test is for, it is brittle against any future provider whose display name sorts nearby, and asserting our own placement in someone else's suite is not ours to do. What is left mirrors test_gondola_provider_registry_wiring exactly: the provider resolves from the registry, and its name, default model and API key config are what the definition declares. Co-Authored-By: Claude Opus 5 --- crates/goose/src/providers/init.rs | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/crates/goose/src/providers/init.rs b/crates/goose/src/providers/init.rs index a879eb33c387..46cbb0d7f071 100644 --- a/crates/goose/src/providers/init.rs +++ b/crates/goose/src/providers/init.rs @@ -344,27 +344,6 @@ mod tests { .config_keys .iter() .any(|key| key.name == "AIMLAPI_API_KEY" && key.secret)); - - // The whole product claim, empirically: sorted with everything else - // registered, "AI/ML API" lands before "Amazon Bedrock" ("AI" < "Am") - // with no special-casing anywhere - same sort ProviderGrid.tsx uses. - let mut names: Vec = providers() - .await - .into_iter() - .map(|(m, _)| m.display_name) - .collect(); - names.sort(); - let aimlapi_pos = names - .iter() - .position(|n| n == "AI/ML API") - .expect("AI/ML API should be in the full provider list"); - let bedrock_pos = names.iter().position(|n| n == "Amazon Bedrock"); - if let Some(bedrock_pos) = bedrock_pos { - assert!( - aimlapi_pos < bedrock_pos, - "AI/ML API should sort before Amazon Bedrock" - ); - } } #[tokio::test]