Upgrade error handling (#6747)

Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
Douwe Osinga
2026-01-28 15:50:29 -05:00
committed by GitHub
parent 2bc368948b
commit 87737f81a4
8 changed files with 458 additions and 409 deletions
@@ -1,5 +1,6 @@
use crate::routes::errors::ErrorResponse;
use crate::state::AppState;
use axum::{extract::State, http::StatusCode, routing::post, Json, Router};
use axum::{extract::State, routing::post, Json, Router};
use goose::permission::permission_confirmation::PrincipalType;
use goose::permission::{Permission, PermissionConfirmation};
use serde::{Deserialize, Serialize};
@@ -34,7 +35,7 @@ fn default_principal_type() -> PrincipalType {
pub async fn confirm_tool_action(
State(state): State<Arc<AppState>>,
Json(request): Json<ConfirmToolActionRequest>,
) -> Result<Json<Value>, StatusCode> {
) -> Result<Json<Value>, ErrorResponse> {
let agent = state.get_agent_for_route(request.session_id).await?;
let permission = match request.action.as_str() {
"always_allow" => Permission::AlwaysAllow,
@@ -72,6 +73,7 @@ mod tests {
mod integration_tests {
use super::*;
use axum::{body::Body, http::Request};
use http::StatusCode;
use tower::ServiceExt;
#[tokio::test(flavor = "multi_thread")]
+91 -61
View File
@@ -2,6 +2,7 @@
///
/// This module provides endpoints for audio transcription using OpenAI's Whisper API.
/// The OpenAI API key must be configured in the backend for this to work.
use crate::routes::errors::ErrorResponse;
use crate::state::AppState;
use axum::{
http::StatusCode,
@@ -44,18 +45,22 @@ struct WhisperResponse {
fn validate_audio_input(
audio: &str,
mime_type: &str,
) -> Result<(Vec<u8>, &'static str), StatusCode> {
) -> Result<(Vec<u8>, &'static str), ErrorResponse> {
// Decode the base64 audio data
let audio_bytes = BASE64.decode(audio).map_err(|_| StatusCode::BAD_REQUEST)?;
let audio_bytes = BASE64
.decode(audio)
.map_err(|_| ErrorResponse::bad_request("Invalid base64 audio data"))?;
// Check file size
if audio_bytes.len() > MAX_AUDIO_SIZE_BYTES {
tracing::warn!(
"Audio file too large: {} bytes (max: {} bytes)",
audio_bytes.len(),
MAX_AUDIO_SIZE_BYTES
);
return Err(StatusCode::PAYLOAD_TOO_LARGE);
return Err(ErrorResponse {
message: format!(
"Audio file too large: {} bytes (max: {} bytes)",
audio_bytes.len(),
MAX_AUDIO_SIZE_BYTES
),
status: StatusCode::PAYLOAD_TOO_LARGE,
});
}
// Determine file extension based on MIME type
@@ -68,19 +73,28 @@ fn validate_audio_input(
"audio/m4a" => "m4a",
"audio/wav" => "wav",
"audio/x-wav" => "wav",
_ => return Err(StatusCode::UNSUPPORTED_MEDIA_TYPE),
_ => {
return Err(ErrorResponse {
message: format!("Unsupported audio format: {}", mime_type),
status: StatusCode::UNSUPPORTED_MEDIA_TYPE,
})
}
};
Ok((audio_bytes, file_extension))
}
/// Get OpenAI configuration (API key and host)
fn get_openai_config() -> Result<(String, String), StatusCode> {
fn get_openai_config() -> Result<(String, String), ErrorResponse> {
let config = goose::config::Config::global();
let api_key: String = config.get_secret("OPENAI_API_KEY").map_err(|e| {
tracing::error!("Failed to get OpenAI API key: {:?}", e);
StatusCode::PRECONDITION_FAILED
let api_key: String = config.get_secret("OPENAI_API_KEY").map_err(|e| match e {
goose::config::ConfigError::NotFound(_) => ErrorResponse {
message: "OpenAI API key not configured. Please set OPENAI_API_KEY in settings."
.to_string(),
status: StatusCode::PRECONDITION_FAILED,
},
_ => ErrorResponse::internal(format!("Failed to get OpenAI API key: {:?}", e)),
})?;
let openai_host = match config.get("OPENAI_HOST", false) {
@@ -101,7 +115,7 @@ async fn send_openai_request(
mime_type: &str,
api_key: &str,
openai_host: &str,
) -> Result<WhisperResponse, StatusCode> {
) -> Result<WhisperResponse, ErrorResponse> {
tracing::info!("Using OpenAI host: {}", openai_host);
tracing::info!(
"Audio file size: {} bytes, extension: {}, mime_type: {}",
@@ -115,8 +129,7 @@ async fn send_openai_request(
.file_name(format!("audio.{}", file_extension))
.mime_str(mime_type)
.map_err(|e| {
tracing::error!("Failed to create multipart part: {:?}", e);
StatusCode::INTERNAL_SERVER_ERROR
ErrorResponse::internal(format!("Failed to create multipart part: {:?}", e))
})?;
let form = reqwest::multipart::Form::new()
@@ -130,10 +143,7 @@ async fn send_openai_request(
let client = Client::builder()
.timeout(Duration::from_secs(OPENAI_TIMEOUT_SECONDS))
.build()
.map_err(|e| {
tracing::error!("Failed to create HTTP client: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
.map_err(|e| ErrorResponse::internal(format!("Failed to create HTTP client: {}", e)))?;
tracing::info!(
"Sending request to OpenAI: {}/v1/audio/transcriptions",
@@ -148,14 +158,18 @@ async fn send_openai_request(
.await
.map_err(|e| {
if e.is_timeout() {
tracing::error!(
"OpenAI API request timed out after {}s",
OPENAI_TIMEOUT_SECONDS
);
StatusCode::GATEWAY_TIMEOUT
ErrorResponse {
message: format!(
"OpenAI API request timed out after {}s",
OPENAI_TIMEOUT_SECONDS
),
status: StatusCode::GATEWAY_TIMEOUT,
}
} else {
tracing::error!("Failed to send request to OpenAI: {}", e);
StatusCode::SERVICE_UNAVAILABLE
ErrorResponse {
message: format!("Failed to send request to OpenAI: {}", e),
status: StatusCode::SERVICE_UNAVAILABLE,
}
}
})?;
@@ -171,20 +185,27 @@ async fn send_openai_request(
// Check for specific error codes
if status == 401 {
tracing::error!("OpenAI API key appears to be invalid or unauthorized");
return Err(StatusCode::UNAUTHORIZED);
return Err(ErrorResponse {
message: "OpenAI API key appears to be invalid or unauthorized".to_string(),
status: StatusCode::UNAUTHORIZED,
});
} else if status == 429 {
tracing::error!("OpenAI API quota or rate limit exceeded");
return Err(StatusCode::TOO_MANY_REQUESTS);
return Err(ErrorResponse {
message: "OpenAI API quota or rate limit exceeded".to_string(),
status: StatusCode::TOO_MANY_REQUESTS,
});
}
return Err(StatusCode::BAD_GATEWAY);
return Err(ErrorResponse {
message: format!("OpenAI API error: {}", error_text),
status: StatusCode::BAD_GATEWAY,
});
}
let whisper_response: WhisperResponse = response.json().await.map_err(|e| {
tracing::error!("Failed to parse OpenAI response: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
let whisper_response: WhisperResponse = response
.json()
.await
.map_err(|e| ErrorResponse::internal(format!("Failed to parse OpenAI response: {}", e)))?;
Ok(whisper_response)
}
@@ -208,7 +229,7 @@ async fn send_openai_request(
/// - 503: Service Unavailable (network error)
async fn transcribe_handler(
Json(request): Json<TranscribeRequest>,
) -> Result<Json<TranscribeResponse>, StatusCode> {
) -> Result<Json<TranscribeResponse>, ErrorResponse> {
let (audio_bytes, file_extension) = validate_audio_input(&request.audio, &request.mime_type)?;
let (api_key, openai_host) = get_openai_config()?;
@@ -232,7 +253,7 @@ async fn transcribe_handler(
/// Requires an ElevenLabs API key with speech-to-text access.
async fn transcribe_elevenlabs_handler(
Json(request): Json<TranscribeElevenLabsRequest>,
) -> Result<Json<TranscribeResponse>, StatusCode> {
) -> Result<Json<TranscribeResponse>, ErrorResponse> {
let (audio_bytes, file_extension) = validate_audio_input(&request.audio, &request.mime_type)?;
// Get the ElevenLabs API key from config (after input validation)
@@ -266,17 +287,17 @@ async fn transcribe_elevenlabs_handler(
key
}
None => {
tracing::error!(
return Err(ErrorResponse::bad_request(format!(
"ElevenLabs API key is not a string, found: {:?}",
value
);
return Err(StatusCode::PRECONDITION_FAILED);
)));
}
}
}
Err(_) => {
tracing::error!("No ElevenLabs API key found in configuration");
return Err(StatusCode::PRECONDITION_FAILED);
return Err(ErrorResponse::bad_request(
"No ElevenLabs API key found in configuration",
));
}
}
}
@@ -286,7 +307,7 @@ async fn transcribe_elevenlabs_handler(
let part = reqwest::multipart::Part::bytes(audio_bytes)
.file_name(format!("audio.{}", file_extension))
.mime_str(&request.mime_type)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
.map_err(|_| ErrorResponse::internal("Failed to create multipart part"))?;
let form = reqwest::multipart::Form::new()
.part("file", part) // Changed from "audio" to "file"
@@ -298,10 +319,7 @@ async fn transcribe_elevenlabs_handler(
let client = Client::builder()
.timeout(Duration::from_secs(OPENAI_TIMEOUT_SECONDS))
.build()
.map_err(|e| {
tracing::error!("Failed to create HTTP client: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
.map_err(|e| ErrorResponse::internal(format!("Failed to create HTTP client: {}", e)))?;
let response = client
.post("https://api.elevenlabs.io/v1/speech-to-text")
@@ -311,14 +329,18 @@ async fn transcribe_elevenlabs_handler(
.await
.map_err(|e| {
if e.is_timeout() {
tracing::error!(
"ElevenLabs API request timed out after {}s",
OPENAI_TIMEOUT_SECONDS
);
StatusCode::GATEWAY_TIMEOUT
ErrorResponse {
message: format!(
"ElevenLabs API request timed out after {}s",
OPENAI_TIMEOUT_SECONDS
),
status: StatusCode::GATEWAY_TIMEOUT,
}
} else {
tracing::error!("Failed to send request to ElevenLabs: {}", e);
StatusCode::SERVICE_UNAVAILABLE
ErrorResponse {
message: format!("Failed to send request to ElevenLabs: {}", e),
status: StatusCode::SERVICE_UNAVAILABLE,
}
}
})?;
@@ -329,12 +351,21 @@ async fn transcribe_elevenlabs_handler(
// Check for specific error codes
if error_text.contains("Unauthorized") || error_text.contains("Invalid API key") {
return Err(StatusCode::UNAUTHORIZED);
return Err(ErrorResponse {
message: "ElevenLabs API key is invalid or unauthorized".to_string(),
status: StatusCode::UNAUTHORIZED,
});
} else if error_text.contains("quota") || error_text.contains("limit") {
return Err(StatusCode::PAYMENT_REQUIRED);
return Err(ErrorResponse {
message: "ElevenLabs API quota or rate limit exceeded".to_string(),
status: StatusCode::PAYMENT_REQUIRED,
});
}
return Err(StatusCode::BAD_GATEWAY);
return Err(ErrorResponse {
message: format!("ElevenLabs API error: {}", error_text),
status: StatusCode::BAD_GATEWAY,
});
}
// Parse ElevenLabs response
@@ -347,8 +378,7 @@ async fn transcribe_elevenlabs_handler(
}
let elevenlabs_response: ElevenLabsResponse = response.json().await.map_err(|e| {
tracing::error!("Failed to parse ElevenLabs response: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
ErrorResponse::internal(format!("Failed to parse ElevenLabs response: {}", e))
})?;
Ok(Json(TranscribeResponse {
@@ -359,7 +389,7 @@ async fn transcribe_elevenlabs_handler(
/// Check if dictation providers are configured
///
/// Returns configuration status for dictation providers
async fn check_dictation_config() -> Result<Json<serde_json::Value>, StatusCode> {
async fn check_dictation_config() -> Result<Json<serde_json::Value>, ErrorResponse> {
let config = goose::config::Config::global();
// Check if ElevenLabs API key is configured
@@ -1,3 +1,4 @@
use crate::routes::errors::ErrorResponse;
use crate::routes::utils::check_provider_configured;
use crate::state::AppState;
use axum::routing::put;
@@ -15,13 +16,11 @@ use goose::providers::auto_detect::detect_provider_from_api_key;
use goose::providers::base::{ProviderMetadata, ProviderType};
use goose::providers::canonical::maybe_get_canonical_model;
use goose::providers::create_with_default_model;
use goose::providers::errors::ProviderError;
use goose::providers::providers as get_providers;
use goose::{
agents::execute_commands, agents::ExtensionConfig, config::permission::PermissionLevel,
slash_commands,
};
use http::StatusCode;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_yaml;
@@ -163,14 +162,10 @@ pub struct DetectProviderResponse {
)]
pub async fn upsert_config(
Json(query): Json<UpsertConfigQuery>,
) -> Result<Json<Value>, StatusCode> {
) -> Result<Json<Value>, ErrorResponse> {
let config = Config::global();
let result = config.set(&query.key, &query.value, query.is_secret);
match result {
Ok(_) => Ok(Json(Value::String(format!("Upserted key {}", query.key)))),
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
config.set(&query.key, &query.value, query.is_secret)?;
Ok(Json(Value::String(format!("Upserted key {}", query.key))))
}
#[utoipa::path(
@@ -183,19 +178,18 @@ pub async fn upsert_config(
(status = 500, description = "Internal server error")
)
)]
pub async fn remove_config(Json(query): Json<ConfigKeyQuery>) -> Result<Json<String>, StatusCode> {
pub async fn remove_config(
Json(query): Json<ConfigKeyQuery>,
) -> Result<Json<String>, ErrorResponse> {
let config = Config::global();
let result = if query.is_secret {
config.delete_secret(&query.key)
if query.is_secret {
config.delete_secret(&query.key)?;
} else {
config.delete(&query.key)
};
match result {
Ok(_) => Ok(Json(format!("Removed key {}", query.key))),
Err(_) => Err(StatusCode::NOT_FOUND),
config.delete(&query.key)?;
}
Ok(Json(format!("Removed key {}", query.key)))
}
const SECRET_MASK_SHOW_LEN: usize = 8;
@@ -232,12 +226,12 @@ fn is_valid_provider_name(provider_name: &str) -> bool {
)]
pub async fn read_config(
Json(query): Json<ConfigKeyQuery>,
) -> Result<Json<ConfigValueResponse>, StatusCode> {
) -> Result<Json<ConfigValueResponse>, ErrorResponse> {
if query.key == "model-limits" {
let limits = ModelConfig::get_all_model_limits();
return Ok(Json(ConfigValueResponse::Value(
serde_json::to_value(limits).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
)));
return Ok(Json(ConfigValueResponse::Value(serde_json::to_value(
limits,
)?)));
}
let config = Config::global();
@@ -253,9 +247,7 @@ pub async fn read_config(
}
}
Err(ConfigError::NotFound(_)) => ConfigValueResponse::Value(Value::Null),
Err(_) => {
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
Err(e) => return Err(e.into()),
};
Ok(Json(response_value))
}
@@ -268,7 +260,7 @@ pub async fn read_config(
(status = 500, description = "Internal server error")
)
)]
pub async fn get_extensions() -> Result<Json<ExtensionResponse>, StatusCode> {
pub async fn get_extensions() -> Result<Json<ExtensionResponse>, ErrorResponse> {
let extensions = goose::config::get_all_extensions();
let warnings = goose::config::get_warnings();
Ok(Json(ExtensionResponse {
@@ -290,7 +282,7 @@ pub async fn get_extensions() -> Result<Json<ExtensionResponse>, StatusCode> {
)]
pub async fn add_extension(
Json(extension_query): Json<ExtensionQuery>,
) -> Result<Json<String>, StatusCode> {
) -> Result<Json<String>, ErrorResponse> {
let extensions = goose::config::get_all_extensions();
let key = goose::config::extensions::name_to_key(&extension_query.name);
@@ -317,7 +309,7 @@ pub async fn add_extension(
(status = 500, description = "Internal server error")
)
)]
pub async fn remove_extension(Path(name): Path<String>) -> Result<Json<String>, StatusCode> {
pub async fn remove_extension(Path(name): Path<String>) -> Result<Json<String>, ErrorResponse> {
let key = goose::config::extensions::name_to_key(&name);
goose::config::remove_extension(&key);
Ok(Json(format!("Removed extension {}", name)))
@@ -330,13 +322,11 @@ pub async fn remove_extension(Path(name): Path<String>) -> Result<Json<String>,
(status = 200, description = "All configuration values retrieved successfully", body = ConfigResponse)
)
)]
pub async fn read_all_config() -> Result<Json<ConfigResponse>, StatusCode> {
pub async fn read_all_config() -> Result<Json<ConfigResponse>, ErrorResponse> {
let config = Config::global();
let values = config
.all_values()
.map_err(|_| StatusCode::UNPROCESSABLE_ENTITY)?;
.map_err(|e| ErrorResponse::unprocessable(e.to_string()))?;
Ok(Json(ConfigResponse { config: values }))
}
@@ -347,7 +337,7 @@ pub async fn read_all_config() -> Result<Json<ConfigResponse>, StatusCode> {
(status = 200, description = "All configuration values retrieved successfully", body = [ProviderDetails])
)
)]
pub async fn providers() -> Result<Json<Vec<ProviderDetails>>, StatusCode> {
pub async fn providers() -> Result<Json<Vec<ProviderDetails>>, ErrorResponse> {
let providers = get_providers().await;
let providers_response: Vec<ProviderDetails> = providers
.into_iter()
@@ -381,7 +371,7 @@ pub async fn providers() -> Result<Json<Vec<ProviderDetails>>, StatusCode> {
)]
pub async fn get_provider_models(
Path(name): Path<String>,
) -> Result<Json<Vec<String>>, StatusCode> {
) -> Result<Json<Vec<String>>, ErrorResponse> {
let loaded_provider = goose::config::declarative_providers::load_provider(name.as_str()).ok();
// TODO(Douwe): support a get models url for custom providers
if let Some(loaded_provider) = loaded_provider {
@@ -395,49 +385,29 @@ pub async fn get_provider_models(
));
}
let all = get_providers()
.await
.into_iter()
//.map(|(m, p)| m)
.collect::<Vec<_>>();
let all = get_providers().await.into_iter().collect::<Vec<_>>();
let Some((metadata, provider_type)) = all.into_iter().find(|(m, _)| m.name == name) else {
return Err(StatusCode::BAD_REQUEST);
return Err(ErrorResponse::bad_request(format!(
"Unknown provider: {}",
name
)));
};
if !check_provider_configured(&metadata, provider_type) {
return Err(StatusCode::BAD_REQUEST);
return Err(ErrorResponse::bad_request(format!(
"Provider '{}' is not configured",
name
)));
}
let model_config =
ModelConfig::new(&metadata.default_model).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let provider = goose::providers::create(&name, model_config)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let model_config = ModelConfig::new(&metadata.default_model)?;
let provider = goose::providers::create(&name, model_config).await?;
let models_result = provider.fetch_recommended_models().await;
match models_result {
Ok(Some(models)) => Ok(Json(models)),
Ok(None) => Ok(Json(Vec::new())),
Err(provider_error) => {
let status_code = match provider_error {
// Permanent misconfigurations - client should fix configuration
ProviderError::Authentication(_) => StatusCode::BAD_REQUEST,
ProviderError::UsageError(_) => StatusCode::BAD_REQUEST,
// Transient errors - client should retry later
ProviderError::RateLimitExceeded { .. } => StatusCode::TOO_MANY_REQUESTS,
// All other errors - internal server error
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
tracing::warn!(
"Provider {} failed to fetch models: {}",
name,
provider_error
);
Err(status_code)
}
Err(provider_error) => Err(provider_error.into()),
}
}
@@ -448,7 +418,7 @@ pub async fn get_provider_models(
(status = 200, description = "Slash commands retrieved successfully", body = SlashCommandsResponse)
)
)]
pub async fn get_slash_commands() -> Result<Json<SlashCommandsResponse>, StatusCode> {
pub async fn get_slash_commands() -> Result<Json<SlashCommandsResponse>, ErrorResponse> {
let mut commands: Vec<_> = slash_commands::list_commands()
.iter()
.map(|command| SlashCommand {
@@ -501,9 +471,14 @@ pub struct PricingQuery {
)]
pub async fn get_pricing(
Json(query): Json<PricingQuery>,
) -> Result<Json<PricingResponse>, StatusCode> {
) -> Result<Json<PricingResponse>, ErrorResponse> {
let canonical_model =
maybe_get_canonical_model(&query.provider, &query.model).ok_or(StatusCode::NOT_FOUND)?;
maybe_get_canonical_model(&query.provider, &query.model).ok_or_else(|| {
ErrorResponse::not_found(format!(
"Model '{}/{}' not found",
query.provider, query.model
))
})?;
let mut pricing_data = Vec::new();
@@ -535,7 +510,7 @@ pub async fn get_pricing(
(status = 500, description = "Internal server error")
)
)]
pub async fn init_config() -> Result<Json<String>, StatusCode> {
pub async fn init_config() -> Result<Json<String>, ErrorResponse> {
let config = Config::global();
if config.exists() {
@@ -544,10 +519,10 @@ pub async fn init_config() -> Result<Json<String>, StatusCode> {
// Use the shared function to load init-config.yaml
match goose::config::base::load_init_config_from_workspace() {
Ok(init_values) => match config.initialize_if_empty(init_values) {
Ok(_) => Ok(Json("Config initialized successfully".to_string())),
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
},
Ok(init_values) => {
config.initialize_if_empty(init_values)?;
Ok(Json("Config initialized successfully".to_string()))
}
Err(_) => Ok(Json(
"No init-config.yaml found, using default configuration".to_string(),
)),
@@ -565,7 +540,7 @@ pub async fn init_config() -> Result<Json<String>, StatusCode> {
)]
pub async fn upsert_permissions(
Json(query): Json<UpsertPermissionsQuery>,
) -> Result<Json<String>, StatusCode> {
) -> Result<Json<String>, ErrorResponse> {
let permission_manager = goose::config::PermissionManager::instance();
for tool_permission in &query.tool_permissions {
@@ -589,7 +564,7 @@ pub async fn upsert_permissions(
)]
pub async fn detect_provider(
Json(detect_request): Json<DetectProviderRequest>,
) -> Result<Json<DetectProviderResponse>, StatusCode> {
) -> Result<Json<DetectProviderResponse>, ErrorResponse> {
let api_key = detect_request.api_key.trim();
match detect_provider_from_api_key(api_key).await {
@@ -597,7 +572,9 @@ pub async fn detect_provider(
provider_name,
models,
})),
None => Err(StatusCode::NOT_FOUND),
None => Err(ErrorResponse::not_found(
"Could not detect provider from the provided API key",
)),
}
}
@@ -609,25 +586,23 @@ pub async fn detect_provider(
(status = 500, description = "Internal server error")
)
)]
pub async fn backup_config() -> Result<Json<String>, StatusCode> {
pub async fn backup_config() -> Result<Json<String>, ErrorResponse> {
let config_path = Paths::config_dir().join("config.yaml");
if config_path.exists() {
let file_name = config_path
.file_name()
.ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
let mut backup_name = file_name.to_os_string();
backup_name.push(".bak");
let backup = config_path.with_file_name(backup_name);
match std::fs::copy(&config_path, &backup) {
Ok(_) => Ok(Json(format!("Copied {:?} to {:?}", config_path, backup))),
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
} else {
Err(StatusCode::INTERNAL_SERVER_ERROR)
if !config_path.exists() {
return Err(ErrorResponse::not_found("Config file does not exist"));
}
let file_name = config_path
.file_name()
.ok_or_else(|| ErrorResponse::internal("Invalid config file path"))?;
let mut backup_name = file_name.to_os_string();
backup_name.push(".bak");
let backup = config_path.with_file_name(backup_name);
std::fs::copy(&config_path, &backup)?;
Ok(Json(format!("Copied {:?} to {:?}", config_path, backup)))
}
#[utoipa::path(
@@ -638,27 +613,21 @@ pub async fn backup_config() -> Result<Json<String>, StatusCode> {
(status = 500, description = "Internal server error")
)
)]
pub async fn recover_config() -> Result<Json<String>, StatusCode> {
pub async fn recover_config() -> Result<Json<String>, ErrorResponse> {
let config = Config::global();
// Force a reload which will trigger recovery if needed
match config.all_values() {
Ok(values) => {
let recovered_keys: Vec<String> = values.keys().cloned().collect();
if recovered_keys.is_empty() {
Ok(Json("Config recovery completed, but no data was recoverable. Starting with empty configuration.".to_string()))
} else {
Ok(Json(format!(
"Config recovery completed. Recovered {} keys: {}",
recovered_keys.len(),
recovered_keys.join(", ")
)))
}
}
Err(e) => {
tracing::error!("Config recovery failed: {}", e);
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
let values = config.all_values()?;
let recovered_keys: Vec<String> = values.keys().cloned().collect();
if recovered_keys.is_empty() {
Ok(Json("Config recovery completed, but no data was recoverable. Starting with empty configuration.".to_string()))
} else {
Ok(Json(format!(
"Config recovery completed. Recovered {} keys: {}",
recovered_keys.len(),
recovered_keys.join(", ")
)))
}
}
@@ -670,26 +639,18 @@ pub async fn recover_config() -> Result<Json<String>, StatusCode> {
(status = 422, description = "Config file is corrupted")
)
)]
pub async fn validate_config() -> Result<Json<String>, StatusCode> {
pub async fn validate_config() -> Result<Json<String>, ErrorResponse> {
let config_path = Paths::config_dir().join("config.yaml");
if !config_path.exists() {
return Ok(Json("Config file does not exist".to_string()));
}
match std::fs::read_to_string(&config_path) {
Ok(content) => match serde_yaml::from_str::<serde_yaml::Value>(&content) {
Ok(_) => Ok(Json("Config file is valid".to_string())),
Err(e) => {
tracing::warn!("Config validation failed: {}", e);
Err(StatusCode::UNPROCESSABLE_ENTITY)
}
},
Err(e) => {
tracing::error!("Failed to read config file: {}", e);
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
}
let content = std::fs::read_to_string(&config_path)?;
serde_yaml::from_str::<serde_yaml::Value>(&content)
.map_err(|e| ErrorResponse::unprocessable(format!("Config file is corrupted: {}", e)))?;
Ok(Json("Config file is valid".to_string()))
}
#[utoipa::path(
post,
@@ -703,7 +664,7 @@ pub async fn validate_config() -> Result<Json<String>, StatusCode> {
)]
pub async fn create_custom_provider(
Json(request): Json<UpdateCustomProviderRequest>,
) -> Result<Json<String>, StatusCode> {
) -> Result<Json<String>, ErrorResponse> {
let config = goose::config::declarative_providers::create_custom_provider(
goose::config::declarative_providers::CreateCustomProviderParams {
engine: request.engine,
@@ -715,12 +676,9 @@ pub async fn create_custom_provider(
headers: request.headers,
requires_auth: request.requires_auth,
},
)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
)?;
if let Err(e) = goose::providers::refresh_custom_providers().await {
tracing::warn!("Failed to refresh custom providers after creation: {}", e);
}
goose::providers::refresh_custom_providers().await?;
Ok(Json(format!("Custom provider added - ID: {}", config.id())))
}
@@ -736,9 +694,11 @@ pub async fn create_custom_provider(
)]
pub async fn get_custom_provider(
Path(id): Path<String>,
) -> Result<Json<LoadedProvider>, StatusCode> {
) -> Result<Json<LoadedProvider>, ErrorResponse> {
let loaded_provider = goose::config::declarative_providers::load_provider(id.as_str())
.map_err(|_| StatusCode::NOT_FOUND)?;
.map_err(|e| {
ErrorResponse::not_found(format!("Custom provider '{}' not found: {}", id, e))
})?;
Ok(Json(loaded_provider))
}
@@ -752,13 +712,10 @@ pub async fn get_custom_provider(
(status = 500, description = "Internal server error")
)
)]
pub async fn remove_custom_provider(Path(id): Path<String>) -> Result<Json<String>, StatusCode> {
goose::config::declarative_providers::remove_custom_provider(&id)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
pub async fn remove_custom_provider(Path(id): Path<String>) -> Result<Json<String>, ErrorResponse> {
goose::config::declarative_providers::remove_custom_provider(&id)?;
if let Err(e) = goose::providers::refresh_custom_providers().await {
tracing::warn!("Failed to refresh custom providers after deletion: {}", e);
}
goose::providers::refresh_custom_providers().await?;
Ok(Json(format!("Removed custom provider: {}", id)))
}
@@ -776,7 +733,7 @@ pub async fn remove_custom_provider(Path(id): Path<String>) -> Result<Json<Strin
pub async fn update_custom_provider(
Path(id): Path<String>,
Json(request): Json<UpdateCustomProviderRequest>,
) -> Result<Json<String>, StatusCode> {
) -> Result<Json<String>, ErrorResponse> {
goose::config::declarative_providers::update_custom_provider(
goose::config::declarative_providers::UpdateCustomProviderParams {
id: id.clone(),
@@ -789,12 +746,9 @@ pub async fn update_custom_provider(
headers: request.headers,
requires_auth: request.requires_auth,
},
)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
)?;
if let Err(e) = goose::providers::refresh_custom_providers().await {
tracing::warn!("Failed to refresh custom providers after update: {}", e);
}
goose::providers::refresh_custom_providers().await?;
Ok(Json(format!("Updated custom provider: {}", id)))
}
@@ -806,10 +760,10 @@ pub async fn update_custom_provider(
)]
pub async fn check_provider(
Json(CheckProviderRequest { provider }): Json<CheckProviderRequest>,
) -> Result<(), (StatusCode, String)> {
create_with_default_model(&provider)
.await
.map_err(|err| (StatusCode::BAD_REQUEST, err.to_string()))?;
) -> Result<(), ErrorResponse> {
create_with_default_model(&provider).await.map_err(|err| {
ErrorResponse::bad_request(format!("Provider '{}' check failed: {}", provider, err))
})?;
Ok(())
}
@@ -820,17 +774,22 @@ pub async fn check_provider(
)]
pub async fn set_config_provider(
Json(SetProviderRequest { provider, model }): Json<SetProviderRequest>,
) -> Result<(), (StatusCode, String)> {
) -> Result<(), ErrorResponse> {
create_with_default_model(&provider)
.await
.and_then(|_| {
let config = Config::global();
config
.set_goose_provider(provider)
.and_then(|_| config.set_goose_model(model))
.set_goose_provider(provider.clone())
.and_then(|_| config.set_goose_model(model.clone()))
.map_err(|e| anyhow::anyhow!(e))
})
.map_err(|err| (StatusCode::BAD_REQUEST, err.to_string()))?;
.map_err(|err| {
ErrorResponse::bad_request(format!(
"Failed to set provider to '{}' with model '{}': {}",
provider, model, err
))
})?;
Ok(())
}
@@ -847,37 +806,39 @@ pub async fn set_config_provider(
)]
pub async fn configure_provider_oauth(
Path(provider_name): Path<String>,
) -> Result<Json<String>, (StatusCode, String)> {
) -> Result<Json<String>, ErrorResponse> {
use goose::model::ModelConfig;
use goose::providers::create;
if !is_valid_provider_name(&provider_name) {
return Err((StatusCode::BAD_REQUEST, "Invalid provider name".to_string()));
return Err(ErrorResponse::bad_request(format!(
"Invalid provider name: '{}'",
provider_name
)));
}
let temp_model =
ModelConfig::new("temp").map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
let temp_model = ModelConfig::new("temp").map_err(|e| {
ErrorResponse::bad_request(format!("Failed to create temporary model config: {}", e))
})?;
let provider = create(&provider_name, temp_model).await.map_err(|e| {
(
StatusCode::BAD_REQUEST,
format!("Failed to create provider: {}", e),
)
ErrorResponse::bad_request(format!(
"Failed to create provider '{}': {}",
provider_name, e
))
})?;
provider.configure_oauth().await.map_err(|e| {
(
StatusCode::BAD_REQUEST,
format!("OAuth configuration failed: {}", e),
)
ErrorResponse::bad_request(format!(
"OAuth configuration failed for provider '{}': {}",
provider_name, e
))
})?;
// Mark the provider as configured after successful OAuth
let configured_marker = format!("{}_configured", provider_name);
let config = goose::config::Config::global();
config
.set_param(&configured_marker, true)
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
config.set_param(&configured_marker, true)?;
Ok(Json("OAuth configuration completed".to_string()))
}
+85 -1
View File
@@ -4,6 +4,8 @@ use axum::{
Json,
};
use goose::config::ConfigError;
use goose::model::ConfigError as ModelConfigError;
use goose::providers::errors::ProviderError;
use serde::Serialize;
use utoipa::ToSchema;
@@ -21,6 +23,27 @@ impl ErrorResponse {
status: StatusCode::INTERNAL_SERVER_ERROR,
}
}
pub(crate) fn bad_request(message: impl Into<String>) -> Self {
Self {
message: message.into(),
status: StatusCode::BAD_REQUEST,
}
}
pub(crate) fn not_found(message: impl Into<String>) -> Self {
Self {
message: message.into(),
status: StatusCode::NOT_FOUND,
}
}
pub(crate) fn unprocessable(message: impl Into<String>) -> Self {
Self {
message: message.into(),
status: StatusCode::UNPROCESSABLE_ENTITY,
}
}
}
impl IntoResponse for ErrorResponse {
@@ -41,6 +64,67 @@ impl From<anyhow::Error> for ErrorResponse {
impl From<ConfigError> for ErrorResponse {
fn from(err: ConfigError) -> Self {
Self::internal(err.to_string())
match err {
ConfigError::NotFound(key) => Self::not_found(format!("Config key not found: {}", key)),
_ => Self::internal(err.to_string()),
}
}
}
impl From<ModelConfigError> for ErrorResponse {
fn from(err: ModelConfigError) -> Self {
Self::internal(format!("Model configuration error: {}", err))
}
}
impl From<StatusCode> for ErrorResponse {
fn from(status: StatusCode) -> Self {
let message = status.canonical_reason().unwrap_or("Unknown error");
Self {
message: message.to_string(),
status,
}
}
}
impl From<std::io::Error> for ErrorResponse {
fn from(err: std::io::Error) -> Self {
Self::internal(format!("IO error: {}", err))
}
}
impl From<serde_json::Error> for ErrorResponse {
fn from(err: serde_json::Error) -> Self {
Self::internal(format!("JSON serialization error: {}", err))
}
}
impl From<serde_yaml::Error> for ErrorResponse {
fn from(err: serde_yaml::Error) -> Self {
Self::unprocessable(format!("YAML parsing error: {}", err))
}
}
impl From<ProviderError> for ErrorResponse {
fn from(err: ProviderError) -> Self {
let (status, message) = match err {
ProviderError::Authentication(_) => (
StatusCode::BAD_REQUEST,
format!("Authentication failed: {}", err),
),
ProviderError::UsageError(_) => {
(StatusCode::BAD_REQUEST, format!("Usage error: {}", err))
}
ProviderError::RateLimitExceeded { .. } => (
StatusCode::TOO_MANY_REQUESTS,
format!("Rate limit exceeded: {}", err),
),
_ => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Provider error: {}", err),
),
};
Self { message, status }
}
}
+10 -11
View File
@@ -1,3 +1,4 @@
use crate::routes::errors::ErrorResponse;
use axum::{
extract::Path,
routing::{delete, get, put},
@@ -6,7 +7,6 @@ use axum::{
use goose::prompt_template::{
get_template, list_templates, reset_template, save_template, Template,
};
use http::StatusCode;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
@@ -54,8 +54,9 @@ pub async fn get_prompts() -> Json<PromptsListResponse> {
)]
pub async fn get_prompt(
Path(name): Path<String>,
) -> Result<Json<PromptContentResponse>, StatusCode> {
let template = get_template(&name).ok_or(StatusCode::NOT_FOUND)?;
) -> Result<Json<PromptContentResponse>, ErrorResponse> {
let template = get_template(&name)
.ok_or_else(|| ErrorResponse::not_found(format!("Prompt template '{}' not found", name)))?;
let content = template
.user_content
@@ -86,13 +87,12 @@ pub async fn get_prompt(
pub async fn save_prompt(
Path(name): Path<String>,
Json(request): Json<SavePromptRequest>,
) -> Result<Json<String>, StatusCode> {
) -> Result<Json<String>, ErrorResponse> {
save_template(&name, &request.content).map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
StatusCode::NOT_FOUND
ErrorResponse::not_found(format!("Prompt template '{}' not found", name))
} else {
tracing::error!("Failed to save prompt {}: {}", name, e);
StatusCode::INTERNAL_SERVER_ERROR
ErrorResponse::internal(format!("Failed to save prompt '{}': {}", name, e))
}
})?;
@@ -111,13 +111,12 @@ pub async fn save_prompt(
(status = 500, description = "Failed to reset prompt")
)
)]
pub async fn reset_prompt(Path(name): Path<String>) -> Result<Json<String>, StatusCode> {
pub async fn reset_prompt(Path(name): Path<String>) -> Result<Json<String>, ErrorResponse> {
reset_template(&name).map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
StatusCode::NOT_FOUND
ErrorResponse::not_found(format!("Prompt template '{}' not found", name))
} else {
tracing::error!("Failed to reset prompt {}: {}", name, e);
StatusCode::INTERNAL_SERVER_ERROR
ErrorResponse::internal(format!("Failed to reset prompt '{}': {}", name, e))
}
})?;
+5 -2
View File
@@ -1,7 +1,10 @@
use crate::routes::errors::ErrorResponse;
use crate::state::AppState;
#[cfg(test)]
use axum::http::StatusCode;
use axum::{
extract::{DefaultBodyLimit, State},
http::{self, StatusCode},
http::{self},
response::IntoResponse,
routing::post,
Json, Router,
@@ -202,7 +205,7 @@ async fn stream_event(
pub async fn reply(
State(state): State<Arc<AppState>>,
Json(request): Json<ChatRequest>,
) -> Result<SseResponse, StatusCode> {
) -> Result<SseResponse, ErrorResponse> {
let session_start = std::time::Instant::now();
tracing::info!(
+118 -123
View File
@@ -8,6 +8,7 @@ use axum::{
};
use serde::{Deserialize, Serialize};
use crate::routes::errors::ErrorResponse;
use crate::state::AppState;
use goose::scheduler::ScheduledJob;
@@ -87,13 +88,9 @@ pub struct SessionDisplayInfo {
async fn create_schedule(
State(state): State<Arc<AppState>>,
Json(req): Json<CreateScheduleRequest>,
) -> Result<Json<ScheduledJob>, StatusCode> {
) -> Result<Json<ScheduledJob>, ErrorResponse> {
let scheduler = state.scheduler();
tracing::info!(
"Server: Calling scheduler.add_scheduled_job() for job '{}'",
req.id
);
let job = ScheduledJob {
id: req.id,
source: req.recipe_source,
@@ -107,15 +104,18 @@ async fn create_schedule(
scheduler
.add_scheduled_job(job.clone(), true)
.await
.map_err(|e| {
eprintln!("Error creating schedule: {:?}", e); // Log error
match e {
goose::scheduler::SchedulerError::JobNotFound(_) => StatusCode::NOT_FOUND,
goose::scheduler::SchedulerError::CronParseError(_) => StatusCode::BAD_REQUEST,
goose::scheduler::SchedulerError::RecipeLoadError(_) => StatusCode::BAD_REQUEST,
goose::scheduler::SchedulerError::JobIdExists(_) => StatusCode::CONFLICT,
_ => StatusCode::INTERNAL_SERVER_ERROR,
.map_err(|e| match e {
goose::scheduler::SchedulerError::CronParseError(msg) => {
ErrorResponse::bad_request(format!("Invalid cron expression: {}", msg))
}
goose::scheduler::SchedulerError::RecipeLoadError(msg) => {
ErrorResponse::bad_request(format!("Recipe load error: {}", msg))
}
goose::scheduler::SchedulerError::JobIdExists(msg) => ErrorResponse {
message: format!("Job ID already exists: {}", msg),
status: StatusCode::CONFLICT,
},
_ => ErrorResponse::internal(format!("Error creating schedule: {}", e)),
})?;
Ok(Json(job))
}
@@ -132,10 +132,9 @@ async fn create_schedule(
#[axum::debug_handler]
async fn list_schedules(
State(state): State<Arc<AppState>>,
) -> Result<Json<ListSchedulesResponse>, StatusCode> {
) -> Result<Json<ListSchedulesResponse>, ErrorResponse> {
let scheduler = state.scheduler();
tracing::info!("Server: Calling scheduler.list_scheduled_jobs()");
let jobs = scheduler.list_scheduled_jobs().await;
Ok(Json(ListSchedulesResponse { jobs }))
}
@@ -157,17 +156,16 @@ async fn list_schedules(
async fn delete_schedule(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<StatusCode, StatusCode> {
) -> Result<StatusCode, ErrorResponse> {
let scheduler = state.scheduler();
scheduler
.remove_scheduled_job(&id, true)
.await
.map_err(|e| {
eprintln!("Error deleting schedule '{}': {:?}", id, e);
match e {
goose::scheduler::SchedulerError::JobNotFound(_) => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
.map_err(|e| match e {
goose::scheduler::SchedulerError::JobNotFound(msg) => {
ErrorResponse::not_found(format!("Schedule not found: {}", msg))
}
_ => ErrorResponse::internal(format!("Error deleting schedule: {}", e)),
})?;
Ok(StatusCode::NO_CONTENT)
}
@@ -189,7 +187,7 @@ async fn delete_schedule(
async fn run_now_handler(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<Json<RunNowResponse>, StatusCode> {
) -> Result<Json<RunNowResponse>, ErrorResponse> {
let scheduler = state.scheduler();
let (recipe_display_name, recipe_version_opt) = if let Some(job) = scheduler
@@ -238,28 +236,31 @@ async fn run_now_handler(
"Recipe execution started"
);
tracing::info!("Server: Calling scheduler.run_now() for job '{}'", id);
match scheduler.run_now(&id).await {
Ok(session_id) => Ok(Json(RunNowResponse { session_id })),
Err(e) => {
eprintln!("Error running schedule '{}' now: {:?}", id, e);
match e {
goose::scheduler::SchedulerError::JobNotFound(_) => Err(StatusCode::NOT_FOUND),
goose::scheduler::SchedulerError::AnyhowError(ref err) => {
// Check if this is a cancellation error
if err.to_string().contains("was successfully cancelled") {
// Return a special session_id to indicate cancellation
Ok(Json(RunNowResponse {
session_id: "CANCELLED".to_string(),
}))
} else {
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
Err(e) => match e {
goose::scheduler::SchedulerError::JobNotFound(msg) => Err(ErrorResponse::not_found(
format!("Schedule not found: {}", msg),
)),
goose::scheduler::SchedulerError::AnyhowError(ref err) => {
// Check if this is a cancellation error
if err.to_string().contains("was successfully cancelled") {
// Return a special session_id to indicate cancellation
Ok(Json(RunNowResponse {
session_id: "CANCELLED".to_string(),
}))
} else {
Err(ErrorResponse::internal(format!(
"Error running schedule: {}",
err
)))
}
_ => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
}
_ => Err(ErrorResponse::internal(format!(
"Error running schedule: {}",
e
))),
},
}
}
@@ -281,41 +282,32 @@ async fn sessions_handler(
State(state): State<Arc<AppState>>,
Path(schedule_id_param): Path<String>, // Renamed to avoid confusion with session_id
Query(query_params): Query<SessionsQuery>,
) -> Result<Json<Vec<SessionDisplayInfo>>, StatusCode> {
) -> Result<Json<Vec<SessionDisplayInfo>>, ErrorResponse> {
let scheduler = state.scheduler();
match scheduler
let session_tuples = scheduler
.sessions(&schedule_id_param, query_params.limit)
.await
{
Ok(session_tuples) => {
let mut display_infos = Vec::new();
for (session_name, session) in session_tuples {
display_infos.push(SessionDisplayInfo {
id: session_name.clone(),
name: session.name,
created_at: session.created_at.to_rfc3339(),
working_dir: session.working_dir.to_string_lossy().into_owned(),
schedule_id: session.schedule_id,
message_count: session.message_count,
total_tokens: session.total_tokens,
input_tokens: session.input_tokens,
output_tokens: session.output_tokens,
accumulated_total_tokens: session.accumulated_total_tokens,
accumulated_input_tokens: session.accumulated_input_tokens,
accumulated_output_tokens: session.accumulated_output_tokens,
});
}
Ok(Json(display_infos))
}
Err(e) => {
eprintln!(
"Error fetching sessions for schedule '{}': {:?}",
schedule_id_param, e
);
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
.map_err(|e| ErrorResponse::internal(format!("Error fetching sessions: {}", e)))?;
let mut display_infos = Vec::new();
for (session_name, session) in session_tuples {
display_infos.push(SessionDisplayInfo {
id: session_name.clone(),
name: session.name,
created_at: session.created_at.to_rfc3339(),
working_dir: session.working_dir.to_string_lossy().into_owned(),
schedule_id: session.schedule_id,
message_count: session.message_count,
total_tokens: session.total_tokens,
input_tokens: session.input_tokens,
output_tokens: session.output_tokens,
accumulated_total_tokens: session.accumulated_total_tokens,
accumulated_input_tokens: session.accumulated_input_tokens,
accumulated_output_tokens: session.accumulated_output_tokens,
});
}
Ok(Json(display_infos))
}
#[utoipa::path(
@@ -336,16 +328,17 @@ async fn sessions_handler(
async fn pause_schedule(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<StatusCode, StatusCode> {
) -> Result<StatusCode, ErrorResponse> {
let scheduler = state.scheduler();
scheduler.pause_schedule(&id).await.map_err(|e| {
eprintln!("Error pausing schedule '{}': {:?}", id, e);
match e {
goose::scheduler::SchedulerError::JobNotFound(_) => StatusCode::NOT_FOUND,
goose::scheduler::SchedulerError::AnyhowError(_) => StatusCode::BAD_REQUEST,
_ => StatusCode::INTERNAL_SERVER_ERROR,
scheduler.pause_schedule(&id).await.map_err(|e| match e {
goose::scheduler::SchedulerError::JobNotFound(msg) => {
ErrorResponse::not_found(format!("Schedule not found: {}", msg))
}
goose::scheduler::SchedulerError::AnyhowError(err) => {
ErrorResponse::bad_request(format!("Cannot pause schedule: {}", err))
}
_ => ErrorResponse::internal(format!("Error pausing schedule: {}", e)),
})?;
Ok(StatusCode::NO_CONTENT)
}
@@ -367,15 +360,14 @@ async fn pause_schedule(
async fn unpause_schedule(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<StatusCode, StatusCode> {
) -> Result<StatusCode, ErrorResponse> {
let scheduler = state.scheduler();
scheduler.unpause_schedule(&id).await.map_err(|e| {
eprintln!("Error unpausing schedule '{}': {:?}", id, e);
match e {
goose::scheduler::SchedulerError::JobNotFound(_) => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
scheduler.unpause_schedule(&id).await.map_err(|e| match e {
goose::scheduler::SchedulerError::JobNotFound(msg) => {
ErrorResponse::not_found(format!("Schedule not found: {}", msg))
}
_ => ErrorResponse::internal(format!("Error unpausing schedule: {}", e)),
})?;
Ok(StatusCode::NO_CONTENT)
}
@@ -400,27 +392,30 @@ async fn update_schedule(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
Json(req): Json<UpdateScheduleRequest>,
) -> Result<Json<ScheduledJob>, StatusCode> {
) -> Result<Json<ScheduledJob>, ErrorResponse> {
let scheduler = state.scheduler();
scheduler
.update_schedule(&id, req.cron)
.await
.map_err(|e| {
eprintln!("Error updating schedule '{}': {:?}", id, e);
match e {
goose::scheduler::SchedulerError::JobNotFound(_) => StatusCode::NOT_FOUND,
goose::scheduler::SchedulerError::AnyhowError(_) => StatusCode::BAD_REQUEST,
goose::scheduler::SchedulerError::CronParseError(_) => StatusCode::BAD_REQUEST,
_ => StatusCode::INTERNAL_SERVER_ERROR,
.map_err(|e| match e {
goose::scheduler::SchedulerError::JobNotFound(msg) => {
ErrorResponse::not_found(format!("Schedule not found: {}", msg))
}
goose::scheduler::SchedulerError::AnyhowError(err) => {
ErrorResponse::bad_request(format!("Cannot update schedule: {}", err))
}
goose::scheduler::SchedulerError::CronParseError(msg) => {
ErrorResponse::bad_request(format!("Invalid cron expression: {}", msg))
}
_ => ErrorResponse::internal(format!("Error updating schedule: {}", e)),
})?;
let jobs = scheduler.list_scheduled_jobs().await;
let updated_job = jobs
.into_iter()
.find(|job| job.id == id)
.ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
.ok_or_else(|| ErrorResponse::internal("Schedule not found after update"))?;
Ok(Json(updated_job))
}
@@ -437,16 +432,17 @@ async fn update_schedule(
pub async fn kill_running_job(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<Json<KillJobResponse>, StatusCode> {
) -> Result<Json<KillJobResponse>, ErrorResponse> {
let scheduler = state.scheduler();
scheduler.kill_running_job(&id).await.map_err(|e| {
eprintln!("Error killing running job '{}': {:?}", id, e);
match e {
goose::scheduler::SchedulerError::JobNotFound(_) => StatusCode::NOT_FOUND,
goose::scheduler::SchedulerError::AnyhowError(_) => StatusCode::BAD_REQUEST,
_ => StatusCode::INTERNAL_SERVER_ERROR,
scheduler.kill_running_job(&id).await.map_err(|e| match e {
goose::scheduler::SchedulerError::JobNotFound(msg) => {
ErrorResponse::not_found(format!("Job not found: {}", msg))
}
goose::scheduler::SchedulerError::AnyhowError(err) => {
ErrorResponse::bad_request(format!("Cannot kill job: {}", err))
}
_ => ErrorResponse::internal(format!("Error killing job: {}", e)),
})?;
Ok(Json(KillJobResponse {
@@ -471,33 +467,32 @@ pub async fn kill_running_job(
pub async fn inspect_running_job(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<Json<InspectJobResponse>, StatusCode> {
) -> Result<Json<InspectJobResponse>, ErrorResponse> {
let scheduler = state.scheduler();
match scheduler.get_running_job_info(&id).await {
Ok(info) => {
if let Some((session_id, start_time)) = info {
let duration = chrono::Utc::now().signed_duration_since(start_time);
Ok(Json(InspectJobResponse {
session_id: Some(session_id),
process_start_time: Some(start_time.to_rfc3339()),
running_duration_seconds: Some(duration.num_seconds()),
}))
} else {
Ok(Json(InspectJobResponse {
session_id: None,
process_start_time: None,
running_duration_seconds: None,
}))
let info = scheduler
.get_running_job_info(&id)
.await
.map_err(|e| match e {
goose::scheduler::SchedulerError::JobNotFound(msg) => {
ErrorResponse::not_found(format!("Job not found: {}", msg))
}
}
Err(e) => {
eprintln!("Error inspecting running job '{}': {:?}", id, e);
match e {
goose::scheduler::SchedulerError::JobNotFound(_) => Err(StatusCode::NOT_FOUND),
_ => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
}
_ => ErrorResponse::internal(format!("Error inspecting job: {}", e)),
})?;
if let Some((session_id, start_time)) = info {
let duration = chrono::Utc::now().signed_duration_since(start_time);
Ok(Json(InspectJobResponse {
session_id: Some(session_id),
process_start_time: Some(start_time.to_rfc3339()),
running_duration_seconds: Some(duration.num_seconds()),
}))
} else {
Ok(Json(InspectJobResponse {
session_id: None,
process_start_time: None,
running_duration_seconds: None,
}))
}
}
+16 -41
View File
@@ -1,5 +1,6 @@
use crate::routes::errors::ErrorResponse;
use crate::state::AppState;
use axum::{http::StatusCode, routing::post, Json, Router};
use axum::{routing::post, Json, Router};
use goose::config::signup_openrouter::OpenRouterAuth;
use goose::config::signup_tetrate::{configure_tetrate, TetrateAuth};
use goose::config::{configure_openrouter, Config};
@@ -27,43 +28,30 @@ pub fn routes(state: Arc<AppState>) -> Router {
(status = 200, body=SetupResponse)
),
)]
async fn start_openrouter_setup() -> Result<Json<SetupResponse>, StatusCode> {
tracing::info!("Starting OpenRouter setup flow");
let mut auth_flow = OpenRouterAuth::new().map_err(|e| {
tracing::error!("Failed to initialize auth flow: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
tracing::info!("Auth flow initialized, starting complete_flow");
async fn start_openrouter_setup() -> Result<Json<SetupResponse>, ErrorResponse> {
let mut auth_flow = OpenRouterAuth::new()
.map_err(|e| ErrorResponse::internal(format!("Failed to initialize auth flow: {}", e)))?;
match auth_flow.complete_flow().await {
Ok(api_key) => {
tracing::info!("Got API key, configuring OpenRouter...");
let config = Config::global();
if let Err(e) = configure_openrouter(config, api_key) {
tracing::error!("Failed to configure OpenRouter: {}", e);
return Ok(Json(SetupResponse {
success: false,
message: format!("Failed to configure OpenRouter: {}", e),
}));
}
tracing::info!("OpenRouter setup completed successfully");
Ok(Json(SetupResponse {
success: true,
message: "OpenRouter setup completed successfully".to_string(),
}))
}
Err(e) => {
tracing::error!("OpenRouter setup failed: {}", e);
Ok(Json(SetupResponse {
success: false,
message: format!("Setup failed: {}", e),
}))
}
Err(e) => Ok(Json(SetupResponse {
success: false,
message: format!("Setup failed: {}", e),
})),
}
}
@@ -74,42 +62,29 @@ async fn start_openrouter_setup() -> Result<Json<SetupResponse>, StatusCode> {
(status = 200, body=SetupResponse)
),
)]
async fn start_tetrate_setup() -> Result<Json<SetupResponse>, StatusCode> {
tracing::info!("Starting Tetrate Agent Router Service setup flow");
let mut auth_flow = TetrateAuth::new().map_err(|e| {
tracing::error!("Failed to initialize auth flow: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
tracing::info!("Auth flow initialized, starting complete_flow");
async fn start_tetrate_setup() -> Result<Json<SetupResponse>, ErrorResponse> {
let mut auth_flow = TetrateAuth::new()
.map_err(|e| ErrorResponse::internal(format!("Failed to initialize auth flow: {}", e)))?;
match auth_flow.complete_flow().await {
Ok(api_key) => {
tracing::info!("Got API key, configuring Tetrate Agent Router Service...");
let config = Config::global();
if let Err(e) = configure_tetrate(config, api_key) {
tracing::error!("Failed to configure Tetrate Agent Router Service: {}", e);
return Ok(Json(SetupResponse {
success: false,
message: format!("Failed to configure Tetrate Agent Router Service: {}", e),
}));
}
tracing::info!("Tetrate Agent Router Service setup completed successfully");
Ok(Json(SetupResponse {
success: true,
message: "Tetrate Agent Router Service setup completed successfully".to_string(),
}))
}
Err(e) => {
tracing::error!("Tetrate Agent Router Service setup failed: {}", e);
Ok(Json(SetupResponse {
success: false,
message: format!("Setup failed: {}", e),
}))
}
Err(e) => Ok(Json(SetupResponse {
success: false,
message: format!("Setup failed: {}", e),
})),
}
}