use serde_json::json;
use thiserror::Error;
use crate::card::prompt::{ParameterName, PromptSpec};
use crate::error::WyrdError;
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum PromptError {
#[error("invalid variable name: {name}")]
InvalidVariableName {
name: String,
},
#[error("media placeholder is not isolated: {name}")]
MediaPlaceholderNotIsolated {
name: String,
},
#[error("media placeholder in system message: {name}")]
MediaInSystemMessage {
name: String,
},
#[error("unsupported media kind {kind} for provider {provider}")]
UnsupportedMediaForProvider {
provider: String,
kind: String,
},
#[error("invalid media type: {message}")]
InvalidMediaType {
message: String,
},
#[error("missing media variable: {name}")]
MissingMediaVariable {
name: String,
},
#[error("media path is not a regular file: {path}")]
MediaNotRegularFile {
path: String,
},
#[error("media file too large: {path}")]
MediaTooLarge {
path: String,
size: u64,
limit: u64,
},
#[error("invalid media extension for {kind}: {path}")]
MediaInvalidExtension {
path: String,
kind: String,
},
#[error("media io error: {message}")]
MediaIo {
message: String,
},
#[error("prompt model must be non-empty")]
EmptyModel,
#[error("response_type JsonSchema schema must be a JSON object")]
InvalidResponseSchema,
#[error("prompt card loader extension is unsupported: {extension:?}")]
LoaderBadExtension {
extension: Option<String>,
},
#[error("prompt card loader IO failure at {path}: {message}")]
LoaderIo {
path: String,
message: String,
},
#[error("failed to serialize native prompt request: {message}")]
SerializeRequest {
message: String,
},
#[error("{provider} provider authentication failed: {detail}")]
ProviderAuth {
provider: String,
detail: String,
},
#[error("{provider} provider rate limit")]
ProviderRateLimit {
provider: String,
retry_after_ms: Option<u64>,
},
#[error("{provider} provider upstream failure {status}: {detail}")]
ProviderUpstream {
provider: String,
status: u16,
detail: String,
},
#[error("{provider} provider request timed out")]
ProviderTimeout {
provider: String,
},
#[error("request variant {sent} does not match provider {provider}")]
ProviderMismatch {
sent: String,
provider: String,
},
#[error("model_settings provider mismatch: expected {expected}, got {got}")]
SettingsProviderMismatch {
expected: String,
got: String,
},
#[error("model_settings for {provider} failed to decode: {message}")]
SettingsDecode {
provider: String,
message: String,
},
#[error("missing render variable: {name}")]
MissingVariable {
name: String,
},
#[error("unsupported handoff from {src} to {dst}")]
UnsupportedHandoff {
src: String,
dst: String,
},
#[error("response did not match schema {schema_name}: {message}")]
ResponseDecode {
schema_name: String,
message: String,
},
}
impl PromptError {
pub const fn code(&self) -> &'static str {
match self {
Self::InvalidVariableName { .. } => "WYRD_PROMPT_400_INVALID_VARIABLE_NAME",
Self::MediaPlaceholderNotIsolated { .. } => {
"WYRD_PROMPT_422_MEDIA_PLACEHOLDER_NOT_ISOLATED"
}
Self::MediaInSystemMessage { .. } => "WYRD_PROMPT_400_MEDIA_IN_SYSTEM_MESSAGE",
Self::UnsupportedMediaForProvider { .. } => {
"WYRD_PROMPT_400_UNSUPPORTED_MEDIA_FOR_PROVIDER"
}
Self::InvalidMediaType { .. } => "WYRD_PROMPT_400_INVALID_MEDIA_TYPE",
Self::MissingMediaVariable { .. } => "WYRD_PROMPT_422_MISSING_MEDIA_VARIABLE",
Self::MediaNotRegularFile { .. } => "WYRD_PROMPT_400_MEDIA_NOT_REGULAR_FILE",
Self::MediaTooLarge { .. } => "WYRD_PROMPT_400_MEDIA_TOO_LARGE",
Self::MediaInvalidExtension { .. } => "WYRD_PROMPT_400_MEDIA_INVALID_EXTENSION",
Self::MediaIo { .. } => "WYRD_PROMPT_500_MEDIA_IO",
Self::EmptyModel => "WYRD_PROMPT_400_EMPTY_MODEL",
Self::InvalidResponseSchema => "WYRD_PROMPT_400_INVALID_RESPONSE_SCHEMA",
Self::LoaderBadExtension { .. } => "WYRD_PROMPT_400_LOADER_BAD_EXTENSION",
Self::LoaderIo { .. } => "WYRD_PROMPT_500_LOADER_IO",
Self::SerializeRequest { .. } => "WYRD_PROMPT_500_SERIALIZE_REQUEST",
Self::ProviderAuth { .. } => "WYRD_PROMPT_401_PROVIDER_AUTH",
Self::ProviderRateLimit { .. } => "WYRD_PROMPT_429_PROVIDER_RATE_LIMIT",
Self::ProviderUpstream { .. } => "WYRD_PROMPT_502_PROVIDER_UPSTREAM",
Self::ProviderTimeout { .. } => "WYRD_PROMPT_504_PROVIDER_TIMEOUT",
Self::ProviderMismatch { .. } => "WYRD_PROMPT_400_PROVIDER_MISMATCH",
Self::SettingsProviderMismatch { .. } => "WYRD_PROMPT_400_SETTINGS_PROVIDER_MISMATCH",
Self::SettingsDecode { .. } => "WYRD_PROMPT_400_SETTINGS_DECODE",
Self::MissingVariable { .. } => "WYRD_PROMPT_422_MISSING_VARIABLE",
Self::UnsupportedHandoff { .. } => "WYRD_PROMPT_501_UNSUPPORTED_HANDOFF",
Self::ResponseDecode { .. } => "WYRD_PROMPT_422_RESPONSE_DECODE",
}
}
}
pub fn validate(spec: &PromptSpec) -> Result<(), WyrdError> {
for name in spec
.prompt
.variables
.iter()
.chain(&spec.prompt.media_variables)
{
ParameterName::new(name.clone())?;
}
Ok(())
}
impl From<PromptError> for WyrdError {
fn from(error: PromptError) -> Self {
let message = error.to_string();
match error {
PromptError::InvalidVariableName { name } => WyrdError::PromptInvalidVariableName {
message,
details: json!({ "name": name }),
},
PromptError::MediaPlaceholderNotIsolated { name } => {
WyrdError::PromptMediaPlaceholderNotIsolated {
message,
details: json!({ "name": name }),
}
}
PromptError::MediaInSystemMessage { name } => WyrdError::PromptMediaInSystemMessage {
message,
details: json!({ "name": name }),
},
PromptError::UnsupportedMediaForProvider { provider, kind } => {
WyrdError::PromptUnsupportedMediaForProvider {
message,
details: json!({ "provider": provider, "kind": kind }),
}
}
PromptError::InvalidMediaType { message: source } => {
WyrdError::PromptInvalidMediaType {
message,
details: json!({ "source": source }),
}
}
PromptError::MissingMediaVariable { name } => WyrdError::PromptMissingMediaVariable {
message,
details: json!({ "name": name }),
},
PromptError::MediaNotRegularFile { path } => WyrdError::PromptMediaNotRegularFile {
message,
details: json!({ "path": path }),
},
PromptError::MediaTooLarge { path, size, limit } => WyrdError::PromptMediaTooLarge {
message,
details: json!({ "path": path, "size": size, "limit": limit }),
},
PromptError::MediaInvalidExtension { path, kind } => {
WyrdError::PromptMediaInvalidExtension {
message,
details: json!({ "path": path, "kind": kind }),
}
}
PromptError::MediaIo { message: source } => WyrdError::PromptMediaIo {
message,
details: json!({ "source": source }),
},
PromptError::EmptyModel => WyrdError::PromptEmptyModel {
message,
details: json!({}),
},
PromptError::InvalidResponseSchema => WyrdError::PromptInvalidResponseSchema {
message,
details: json!({}),
},
PromptError::LoaderBadExtension { extension } => WyrdError::PromptLoaderBadExtension {
message,
details: json!({ "extension": extension }),
},
PromptError::LoaderIo {
path,
message: source,
} => WyrdError::PromptLoaderIo {
message,
details: json!({ "path": path, "source": source }),
},
PromptError::SerializeRequest { message: source } => {
WyrdError::PromptSerializeRequest {
message,
details: json!({ "source": source }),
}
}
PromptError::ProviderAuth { provider, detail } => WyrdError::PromptProviderAuth {
message,
details: json!({ "provider": provider, "detail": detail }),
},
PromptError::ProviderRateLimit {
provider,
retry_after_ms,
} => WyrdError::PromptProviderRateLimit {
message,
details: json!({ "provider": provider, "retry_after_ms": retry_after_ms }),
},
PromptError::ProviderUpstream {
provider,
status,
detail,
} => WyrdError::PromptProviderUpstream {
message,
details: json!({ "provider": provider, "status": status, "detail": detail }),
},
PromptError::ProviderTimeout { provider } => WyrdError::PromptProviderTimeout {
message,
details: json!({ "provider": provider }),
},
PromptError::ProviderMismatch { sent, provider } => WyrdError::PromptProviderMismatch {
message,
details: json!({ "sent": sent, "provider": provider }),
},
PromptError::SettingsProviderMismatch { expected, got } => {
WyrdError::PromptSettingsProviderMismatch {
message,
details: json!({ "expected": expected, "got": got }),
}
}
PromptError::SettingsDecode {
provider,
message: source,
} => WyrdError::PromptSettingsDecode {
message,
details: json!({ "provider": provider, "source": source }),
},
PromptError::MissingVariable { name } => WyrdError::PromptMissingVariable {
message,
details: json!({ "name": name }),
},
PromptError::UnsupportedHandoff { src, dst } => WyrdError::PromptUnsupportedHandoff {
message,
details: json!({ "src": src, "dst": dst }),
},
PromptError::ResponseDecode {
schema_name,
message: source,
} => WyrdError::PromptResponseDecode {
message,
details: json!({ "schema_name": schema_name, "source": source }),
},
}
}
}