Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
d0debc3
onboarding page to start
lifeizhou-ap Feb 16, 2026
06dfcc9
added primary attribute on configKey in provider
lifeizhou-ap Feb 17, 2026
75386e5
cherry pick primary configkey change
lifeizhou-ap Feb 17, 2026
773d392
apply primary in cli config provider, also enables user to config all…
lifeizhou-ap Feb 17, 2026
c99cbc9
change SAGEMAKER_ENDPOINT_NAME as a required field
lifeizhou-ap Feb 17, 2026
f08a0b1
fmt
lifeizhou-ap Feb 17, 2026
882a2b8
added some fallback
lifeizhou-ap Feb 17, 2026
122221e
added props to show options in provider config component
lifeizhou-ap Feb 17, 2026
6243dd0
onboarding page ui
lifeizhou-ap Feb 18, 2026
992a64a
merge main
lifeizhou-ap Feb 18, 2026
a6e5e17
add instructions
lifeizhou-ap Feb 18, 2026
831091b
set default model for selected provider
lifeizhou-ap Feb 18, 2026
95c9361
crete feature toggle to switch between old/new onboarding
lifeizhou-ap Feb 18, 2026
048059b
add option to add custom provider
lifeizhou-ap Feb 18, 2026
743d49e
changed add custom provider response with provider name only
lifeizhou-ap Feb 18, 2026
46fa106
enable nanogpt device login flow
lifeizhou-ap Feb 19, 2026
535b6b5
added nanogpt provider
lifeizhou-ap Feb 19, 2026
301d4f3
text change
lifeizhou-ap Feb 19, 2026
2e85a79
change font size
lifeizhou-ap Feb 19, 2026
6f03f6f
extract to constant
lifeizhou-ap Feb 19, 2026
7488025
clean up special case for ollama and better error message
lifeizhou-ap Feb 20, 2026
2ce5e27
added privacy and success page
lifeizhou-ap Feb 20, 2026
c504e2a
added tracking events
lifeizhou-ap Feb 20, 2026
affff89
updated the instructions
lifeizhou-ap Feb 23, 2026
d183cf0
error handling
lifeizhou-ap Feb 23, 2026
c1062da
feature toggle off by default
lifeizhou-ap Feb 23, 2026
500f7d6
Merge branch 'main' into lifei/simplify_onboarding_flow
lifeizhou-ap Feb 23, 2026
bec2abf
added local model selection
lifeizhou-ap Feb 23, 2026
898310f
fix test
lifeizhou-ap Feb 23, 2026
3398621
revert testing code
lifeizhou-ap Feb 23, 2026
07443dd
fix test compilation
lifeizhou-ap Feb 23, 2026
0fef532
adddres copilot review comments
lifeizhou-ap Feb 24, 2026
2bb1075
Merge branch 'main' into lifei/simplify_onboarding_flow
lifeizhou-ap Feb 24, 2026
c82bb85
resolve merge conflicts
lifeizhou-ap Feb 24, 2026
c708c26
text change
lifeizhou-ap Feb 24, 2026
ae924c0
error message and error handling
lifeizhou-ap Feb 24, 2026
a152100
track provider selected
lifeizhou-ap Feb 24, 2026
4ec0e6f
used nano-gpt to get canonical model data
lifeizhou-ap Feb 25, 2026
5c959b6
update text position
lifeizhou-ap Feb 25, 2026
5454a49
fixed test
lifeizhou-ap Feb 26, 2026
9696662
Merge branch 'main' into lifei/simplify_onboarding_flow
lifeizhou-ap Mar 16, 2026
45f83d1
updated local model style
lifeizhou-ap Mar 16, 2026
da52745
tracking onboarding events
lifeizhou-ap Mar 16, 2026
9cbe4d8
Merge branch 'main' into lifei/simplify_onboarding_flow
lifeizhou-ap Mar 16, 2026
636d007
updated default model for nanogpt
lifeizhou-ap Mar 16, 2026
51ef2dd
address review comments
lifeizhou-ap Mar 17, 2026
36a51c9
Merge branch 'main' into lifei/simplify_onboarding_flow
lifeizhou-ap Mar 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/goose-server/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,7 @@ derive_utoipa!(Icon as IconSchema);
super::routes::recipe::recipe_to_yaml,
super::routes::setup::start_openrouter_setup,
super::routes::setup::start_tetrate_setup,
super::routes::setup::start_nanogpt_setup,
super::routes::tunnel::start_tunnel,
super::routes::tunnel::stop_tunnel,
super::routes::tunnel::get_tunnel_status,
Expand Down Expand Up @@ -510,6 +511,7 @@ derive_utoipa!(Icon as IconSchema);
goose::providers::catalog::ProviderTemplate,
goose::providers::catalog::ModelTemplate,
goose::providers::catalog::ModelCapabilities,
super::routes::config_management::CreateCustomProviderResponse,
super::routes::config_management::CheckProviderRequest,
super::routes::config_management::SetProviderRequest,
super::routes::config_management::ModelInfoQuery,
Expand Down
13 changes: 10 additions & 3 deletions crates/goose-server/src/routes/config_management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -633,19 +633,24 @@ pub async fn validate_config() -> Result<Json<String>, ErrorResponse> {

Ok(Json("Config file is valid".to_string()))
}
#[derive(Serialize, ToSchema)]
pub struct CreateCustomProviderResponse {
pub provider_name: String,
}

#[utoipa::path(
post,
path = "/config/custom-providers",
request_body = UpdateCustomProviderRequest,
responses(
(status = 200, description = "Custom provider created successfully", body = String),
(status = 200, description = "Custom provider created successfully", body = CreateCustomProviderResponse),
(status = 400, description = "Invalid request"),
(status = 500, description = "Internal server error")
)
)]
pub async fn create_custom_provider(
Json(request): Json<UpdateCustomProviderRequest>,
) -> Result<Json<String>, ErrorResponse> {
) -> Result<Json<CreateCustomProviderResponse>, ErrorResponse> {
let config = goose::config::declarative_providers::create_custom_provider(
goose::config::declarative_providers::CreateCustomProviderParams {
engine: request.engine,
Expand All @@ -663,7 +668,9 @@ pub async fn create_custom_provider(

goose::providers::refresh_custom_providers().await?;

Ok(Json(format!("Custom provider added - ID: {}", config.id())))
Ok(Json(CreateCustomProviderResponse {
provider_name: config.id().to_string(),
}))
}

#[utoipa::path(
Expand Down
37 changes: 35 additions & 2 deletions crates/goose-server/src/routes/setup.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::routes::errors::ErrorResponse;
use crate::state::AppState;
use axum::{routing::post, Json, Router};
use goose::config::signup_nanogpt::{complete_nanogpt_auth, configure_nanogpt};
use goose::config::signup_openrouter::OpenRouterAuth;
use goose::config::signup_tetrate::{configure_tetrate, TetrateAuth};
use goose::config::{configure_openrouter, Config};
Expand All @@ -18,6 +19,7 @@ pub fn routes(state: Arc<AppState>) -> Router {
Router::new()
.route("/handle_openrouter", post(start_openrouter_setup))
.route("/handle_tetrate", post(start_tetrate_setup))
.route("/handle_nanogpt", post(start_nanogpt_setup))
.with_state(state)
}

Expand Down Expand Up @@ -50,7 +52,7 @@ async fn start_openrouter_setup() -> Result<Json<SetupResponse>, ErrorResponse>
}
Err(e) => Ok(Json(SetupResponse {
success: false,
message: format!("Setup failed: {}", e),
message: e.to_string(),
})),
}
}
Expand Down Expand Up @@ -84,7 +86,38 @@ async fn start_tetrate_setup() -> Result<Json<SetupResponse>, ErrorResponse> {
}
Err(e) => Ok(Json(SetupResponse {
success: false,
message: format!("Setup failed: {}", e),
message: e.to_string(),
})),
}
}

#[utoipa::path(
post,
path = "/handle_nanogpt",
responses(
(status = 200, body=SetupResponse)
),
)]
async fn start_nanogpt_setup() -> Result<Json<SetupResponse>, ErrorResponse> {
match complete_nanogpt_auth().await {
Ok(api_key) => {
let config = Config::global();

if let Err(e) = configure_nanogpt(config, api_key) {
return Ok(Json(SetupResponse {
success: false,
message: format!("Failed to configure NanoGPT: {}", e),
}));
}

Ok(Json(SetupResponse {
success: true,
message: "NanoGPT setup completed successfully".to_string(),
}))
}
Err(e) => Ok(Json(SetupResponse {
success: false,
message: e.to_string(),
})),
}
}
2 changes: 2 additions & 0 deletions crates/goose/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod migrations;
pub mod paths;
pub mod permission;
pub mod search_path;
pub mod signup_nanogpt;
pub mod signup_openrouter;
pub mod signup_tetrate;

Expand All @@ -21,6 +22,7 @@ pub use extensions::{
};
pub use goose_mode::GooseMode;
pub use permission::PermissionManager;
pub use signup_nanogpt::configure_nanogpt;
pub use signup_openrouter::configure_openrouter;
pub use signup_tetrate::configure_tetrate;

Expand Down
128 changes: 128 additions & 0 deletions crates/goose/src/config/signup_nanogpt/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
use anyhow::{anyhow, Result};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use tokio::time::{sleep, timeout};

use crate::config::Config;

/// Default model for NanoGPT configuration
pub const NANOGPT_DEFAULT_MODEL: &str = "openai/gpt-4.1-nano";

const NANOGPT_START_URL: &str = "https://nano-gpt.com/api/cli-login/start";
const NANOGPT_POLL_URL: &str = "https://nano-gpt.com/api/cli-login/poll";
const AUTH_TIMEOUT: Duration = Duration::from_secs(180); // 3 minutes
const POLL_INTERVAL: Duration = Duration::from_secs(2);

#[derive(Debug, Serialize)]
struct StartRequest {
client_name: String,
}

#[derive(Debug, Deserialize)]
struct StartResponse {
device_code: String,
verification_uri_complete: String,
}

#[derive(Debug, Serialize)]
struct PollRequest {
device_code: String,
}

#[derive(Debug, Deserialize)]
struct PollResponse {
key: String,
}

async fn poll_for_token(device_code: &str) -> Result<String> {
let client = Client::new();

loop {
sleep(POLL_INTERVAL).await;

let body = PollRequest {
device_code: device_code.to_string(),
};

let response = client.post(NANOGPT_POLL_URL).json(&body).send().await?;
// https://docs.nano-gpt.com/integrations/cli-login#response-codes
match response.status().as_u16() {
200 => {
let poll_resp: PollResponse = response.json().await?;
return Ok(poll_resp.key);
}
202 => {
continue;
}
410 => {
return Err(anyhow!("Device code has expired - please try again"));
}
409 => {
return Err(anyhow!("Device code has already been consumed"));
}
404 => {
return Err(anyhow!("Invalid device code"));
}
429 => {
return Err(anyhow!(
"Too many requests to NanoGPT. Please wait a moment and try again."
));
}
other => {
let error_text = response.text().await.unwrap_or_default();
return Err(anyhow!(
"Unexpected poll response: {} - {}",
other,
error_text
));
}
}
}
}

pub async fn complete_nanogpt_auth() -> Result<String> {
let client = Client::new();
let body = StartRequest {
client_name: "goose".to_string(),
};

let response = client.post(NANOGPT_START_URL).json(&body).send().await?;

if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
return Err(anyhow!(
"Failed to start NanoGPT device flow: {} - {}",
status,
error_text
));
}

let start_resp: StartResponse = response.json().await?;

println!("Opening browser for NanoGPT authentication...");

if let Err(e) = webbrowser::open(&start_resp.verification_uri_complete) {
eprintln!("Failed to open browser automatically: {}", e);
println!(
"Please open this URL manually: {}",
start_resp.verification_uri_complete
);
}

println!("Waiting for NanoGPT authorization...");

match timeout(AUTH_TIMEOUT, poll_for_token(&start_resp.device_code)).await {
Ok(Ok(api_key)) => Ok(api_key),
Ok(Err(e)) => Err(e),
Err(_) => Err(anyhow!("Authentication timed out - please try again")),
}
}

pub fn configure_nanogpt(config: &Config, api_key: String) -> Result<()> {
config.set_secret("NANOGPT_API_KEY", &api_key)?;
config.set_goose_provider("nano-gpt")?;
config.set_goose_model(NANOGPT_DEFAULT_MODEL)?;
Ok(())
}
11 changes: 5 additions & 6 deletions crates/goose/src/posthog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -581,15 +581,14 @@ pub async fn emit_event(
event_name: &str,
mut properties: HashMap<String, serde_json::Value>,
) -> Result<(), String> {
if !is_telemetry_enabled() {
// Only onboarding events are enabled for now. These bypass the telemetry
// check so we can track the funnel before the user makes their choice.
let is_onboarding_event =
event_name.starts_with("onboarding_") || event_name == "telemetry_preference_set";
if !is_onboarding_event {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ideally we'd hold onto the event and then send it (or not) only if the user opts in -- think that's possible?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I think we could. I was thinking about this too originally although the code would be a bit complicated. However, we would like to get the metrics for the onboarding as much as we can as requested by UX.

return Ok(());
}

// Temporarily disabled - only session_started events are sent
let _ = (event_name, &mut properties);
return Ok(());

#[allow(unreachable_code)]
let installation = load_or_create_installation();

insert(&mut properties, "os", std::env::consts::OS);
Expand Down
5 changes: 5 additions & 0 deletions crates/goose/src/providers/anthropic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@ impl ProviderDef for AnthropicProvider {
),
],
)
.with_setup_steps(vec![
"Go to https://platform.claude.com/settings/keys",
"Click 'Create Key'",
"Copy the key and paste it above",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or maybe

https://platform.claude.com/settings/keys

which is what anthropic docs seem to link to

])
}

fn from_env(
Expand Down
11 changes: 11 additions & 0 deletions crates/goose/src/providers/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ pub struct ProviderMetadata {
pub model_doc_link: String,
/// Required configuration keys
pub config_keys: Vec<ConfigKey>,
/// step-by-step instructions for set up providers eg: api key
#[serde(default)]
pub setup_steps: Vec<String>,
}

impl ProviderMetadata {
Expand Down Expand Up @@ -208,6 +211,7 @@ impl ProviderMetadata {
.collect(),
model_doc_link: model_doc_link.to_string(),
config_keys,
setup_steps: vec![],
}
}

Expand All @@ -228,6 +232,7 @@ impl ProviderMetadata {
known_models: models,
model_doc_link: model_doc_link.to_string(),
config_keys,
setup_steps: vec![],
}
}

Expand All @@ -240,8 +245,14 @@ impl ProviderMetadata {
known_models: vec![],
model_doc_link: "".to_string(),
config_keys: vec![],
setup_steps: vec![],
}
}

pub fn with_setup_steps(mut self, steps: Vec<&str>) -> Self {
self.setup_steps = steps.into_iter().map(|s| s.to_string()).collect();
self
}
}

/// Configuration key metadata for provider setup
Expand Down
6 changes: 6 additions & 0 deletions crates/goose/src/providers/google.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ impl ProviderDef for GoogleProvider {
ConfigKey::new("GOOGLE_HOST", false, false, Some(GOOGLE_API_HOST), false),
],
)
.with_setup_steps(vec![
"Go to https://aistudio.google.com and sign in with your Google account",
"Click 'Get API key' on the left sidebar",
"Create a new API key or select an existing one",
"Copy the key and paste it above",
])
}

fn from_env(
Expand Down
2 changes: 2 additions & 0 deletions crates/goose/src/providers/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use super::{
lead_worker::LeadWorkerProvider,
litellm::LiteLLMProvider,
local_inference::LocalInferenceProvider,
nanogpt::NanoGptProvider,
ollama::OllamaProvider,
openai::OpenAiProvider,
openrouter::OpenRouterProvider,
Expand Down Expand Up @@ -65,6 +66,7 @@ async fn init_registry() -> RwLock<ProviderRegistry> {
registry.register::<GithubCopilotProvider>(false);
registry.register::<GoogleProvider>(true);
registry.register::<LiteLLMProvider>(false);
registry.register::<NanoGptProvider>(true);
registry.register::<OllamaProvider>(true);
registry.register::<OpenAiProvider>(true);
registry.register::<OpenRouterProvider>(true);
Expand Down
1 change: 1 addition & 0 deletions crates/goose/src/providers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ mod init;
pub mod lead_worker;
pub mod litellm;
pub mod local_inference;
pub mod nanogpt;
pub mod oauth;
pub mod ollama;
pub mod openai;
Expand Down
Loading
Loading