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