feat: new onboarding flow (#7266)
This commit is contained in:
@@ -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,
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
@@ -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(
|
||||
|
||||
@@ -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};
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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 {
|
||||
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);
|
||||
|
||||
@@ -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",
|
||||
])
|
||||
}
|
||||
|
||||
fn from_env(
|
||||
|
||||
@@ -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 {
|
||||
@@ -208,6 +211,7 @@ impl ProviderMetadata {
|
||||
.collect(),
|
||||
model_doc_link: model_doc_link.to_string(),
|
||||
config_keys,
|
||||
setup_steps: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,6 +232,7 @@ impl ProviderMetadata {
|
||||
known_models: models,
|
||||
model_doc_link: model_doc_link.to_string(),
|
||||
config_keys,
|
||||
setup_steps: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -20,6 +20,7 @@ use super::{
|
||||
lead_worker::LeadWorkerProvider,
|
||||
litellm::LiteLLMProvider,
|
||||
local_inference::LocalInferenceProvider,
|
||||
nanogpt::NanoGptProvider,
|
||||
ollama::OllamaProvider,
|
||||
openai::OpenAiProvider,
|
||||
openrouter::OpenRouterProvider,
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
use super::api_client::{ApiClient, AuthMethod};
|
||||
use super::base::{ConfigKey, MessageStream, Provider, ProviderDef, ProviderMetadata};
|
||||
use super::errors::ProviderError;
|
||||
use super::openai_compatible::{handle_status_openai_compat, stream_openai_compat};
|
||||
use super::retry::ProviderRetry;
|
||||
use super::utils::{ImageFormat, RequestLog};
|
||||
use crate::conversation::message::Message;
|
||||
use crate::model::ModelConfig;
|
||||
use crate::providers::formats::openai::create_request;
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use futures::future::BoxFuture;
|
||||
use rmcp::model::Tool;
|
||||
|
||||
const NANOGPT_PROVIDER_NAME: &str = "nano-gpt";
|
||||
pub const NANOGPT_API_HOST: &str = "https://nano-gpt.com/api/v1";
|
||||
pub const NANOGPT_SUBSCRIPTION_HOST: &str = "https://nano-gpt.com/api/subscription/v1";
|
||||
pub const NANOGPT_DEFAULT_MODEL: &str = "anthropic/claude-sonnet-4.6";
|
||||
pub const NANOGPT_DOC_URL: &str = "https://docs.nano-gpt.com/";
|
||||
const NANOGPT_API_KEY: &str = "NANOGPT_API_KEY";
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct NanoGptProvider {
|
||||
#[serde(skip)]
|
||||
api_client: ApiClient,
|
||||
model: ModelConfig,
|
||||
#[serde(skip)]
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl NanoGptProvider {
|
||||
async fn check_subscription(api_key: &str) -> bool {
|
||||
let client = match ApiClient::new(
|
||||
NANOGPT_SUBSCRIPTION_HOST.to_string(),
|
||||
AuthMethod::BearerToken(api_key.to_string()),
|
||||
) {
|
||||
Ok(c) => c,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
match client.response_get(None, "usage").await {
|
||||
Ok(resp) => resp
|
||||
.json::<serde_json::Value>()
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|json| json.get("active")?.as_bool())
|
||||
.unwrap_or(false),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_env(model: ModelConfig) -> Result<Self> {
|
||||
let config = crate::config::Config::global();
|
||||
let api_key: String = config.get_secret(NANOGPT_API_KEY)?;
|
||||
|
||||
let is_subscription = Self::check_subscription(&api_key).await;
|
||||
let host = if is_subscription {
|
||||
tracing::debug!("NanoGPT subscription active, using subscription endpoint");
|
||||
NANOGPT_SUBSCRIPTION_HOST.to_string()
|
||||
} else {
|
||||
tracing::debug!("NanoGPT using pay-as-you-go endpoint");
|
||||
NANOGPT_API_HOST.to_string()
|
||||
};
|
||||
|
||||
let api_client = ApiClient::new(host, AuthMethod::BearerToken(api_key))?;
|
||||
|
||||
Ok(Self {
|
||||
api_client,
|
||||
model,
|
||||
name: NANOGPT_PROVIDER_NAME.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ProviderDef for NanoGptProvider {
|
||||
type Provider = Self;
|
||||
|
||||
fn metadata() -> ProviderMetadata {
|
||||
ProviderMetadata::new(
|
||||
NANOGPT_PROVIDER_NAME,
|
||||
"NanoGPT",
|
||||
"Access multiple AI models through NanoGPT's unified API",
|
||||
NANOGPT_DEFAULT_MODEL,
|
||||
vec![NANOGPT_DEFAULT_MODEL],
|
||||
NANOGPT_DOC_URL,
|
||||
vec![ConfigKey::new(NANOGPT_API_KEY, true, true, None, true)],
|
||||
)
|
||||
}
|
||||
|
||||
fn from_env(
|
||||
model: ModelConfig,
|
||||
_extensions: Vec<crate::config::ExtensionConfig>,
|
||||
) -> BoxFuture<'static, Result<Self::Provider>> {
|
||||
Box::pin(Self::from_env(model))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Provider for NanoGptProvider {
|
||||
fn get_name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn get_model_config(&self) -> ModelConfig {
|
||||
self.model.clone()
|
||||
}
|
||||
|
||||
async fn fetch_supported_models(&self) -> Result<Vec<String>, ProviderError> {
|
||||
let response = self
|
||||
.api_client
|
||||
.request(None, "models?detailed=true")
|
||||
.response_get()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ProviderError::RequestFailed(format!(
|
||||
"Failed to fetch models from NanoGPT API: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
let json: serde_json::Value = response.json().await.map_err(|e| {
|
||||
ProviderError::RequestFailed(format!(
|
||||
"Failed to parse NanoGPT models API response as JSON: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
if let Some(err_obj) = json.get("error") {
|
||||
let msg = err_obj
|
||||
.get("message")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown error");
|
||||
return Err(ProviderError::RequestFailed(format!(
|
||||
"NanoGPT API returned an error: {}",
|
||||
msg
|
||||
)));
|
||||
}
|
||||
|
||||
let data = json.get("data").and_then(|v| v.as_array()).ok_or_else(|| {
|
||||
ProviderError::RequestFailed("Missing 'data' field in JSON response".into())
|
||||
})?;
|
||||
|
||||
let mut models: Vec<String> = data
|
||||
.iter()
|
||||
.filter_map(|model| {
|
||||
let id = model.get("id").and_then(|v| v.as_str())?;
|
||||
let supports_tool_calling = model
|
||||
.get("capabilities")
|
||||
.and_then(|c| c.get("tool_calling"))
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
if supports_tool_calling {
|
||||
Some(id.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
models.sort();
|
||||
Ok(models)
|
||||
}
|
||||
|
||||
async fn stream(
|
||||
&self,
|
||||
model_config: &ModelConfig,
|
||||
session_id: &str,
|
||||
system: &str,
|
||||
messages: &[Message],
|
||||
tools: &[Tool],
|
||||
) -> Result<MessageStream, ProviderError> {
|
||||
let payload = create_request(
|
||||
model_config,
|
||||
system,
|
||||
messages,
|
||||
tools,
|
||||
&ImageFormat::OpenAi,
|
||||
true,
|
||||
)?;
|
||||
|
||||
let mut log = RequestLog::start(model_config, &payload)?;
|
||||
|
||||
let response = self
|
||||
.with_retry(|| async {
|
||||
let resp = self
|
||||
.api_client
|
||||
.response_post(Some(session_id), "chat/completions", &payload)
|
||||
.await?;
|
||||
handle_status_openai_compat(resp).await
|
||||
})
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
let _ = log.error(e);
|
||||
})?;
|
||||
|
||||
stream_openai_compat(response, log)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_metadata() {
|
||||
let metadata = NanoGptProvider::metadata();
|
||||
assert_eq!(metadata.name, "nano-gpt");
|
||||
assert_eq!(metadata.default_model, "anthropic/claude-sonnet-4.6");
|
||||
assert_eq!(metadata.config_keys[0].name, NANOGPT_API_KEY);
|
||||
assert!(metadata.config_keys[0].required);
|
||||
assert!(metadata.config_keys[0].secret);
|
||||
}
|
||||
}
|
||||
@@ -338,6 +338,12 @@ impl ProviderDef for OpenAiProvider {
|
||||
ConfigKey::new("OPENAI_TIMEOUT", false, false, Some("600"), false),
|
||||
],
|
||||
)
|
||||
.with_setup_steps(vec![
|
||||
"Go to https://platform.openai.com and sign up or log in",
|
||||
"Navigate to API Keys in the left sidebar",
|
||||
"Click 'Create new secret key'",
|
||||
"Copy the key and paste it above",
|
||||
])
|
||||
}
|
||||
|
||||
fn from_env(
|
||||
|
||||
@@ -168,6 +168,11 @@ impl ProviderDef for OpenRouterProvider {
|
||||
),
|
||||
],
|
||||
)
|
||||
.with_setup_steps(vec![
|
||||
"Go to https://openrouter.ai/settings/keys",
|
||||
"Click 'Create' or use an existing API key",
|
||||
"Copy the key and paste it above",
|
||||
])
|
||||
}
|
||||
|
||||
fn from_env(
|
||||
|
||||
@@ -139,6 +139,7 @@ impl ProviderRegistry {
|
||||
known_models,
|
||||
model_doc_link: base_metadata.model_doc_link,
|
||||
config_keys,
|
||||
setup_steps: vec![],
|
||||
};
|
||||
|
||||
self.entries.insert(
|
||||
|
||||
@@ -373,6 +373,7 @@ mod tests {
|
||||
known_models: vec![],
|
||||
model_doc_link: "".to_string(),
|
||||
config_keys: vec![],
|
||||
setup_steps: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -191,6 +191,7 @@ impl ProviderDef for MockCompactionProvider {
|
||||
known_models: vec![],
|
||||
model_doc_link: "".to_string(),
|
||||
config_keys: vec![],
|
||||
setup_steps: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user