Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions engine/baml-runtime/src/cli/serve/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ impl BamlError {
message: format!("Unexpected error from BAML: {err:?}"),
},
LLMResponse::LLMFailure(failed) => match &failed.code {
crate::internal::llm_client::ErrorCode::FailedToConnect => Self::ClientError {
message: format!("Failed to connect to the LLM provider: {err:?}"),
},
crate::internal::llm_client::ErrorCode::Other(2) => Self::InternalError {
message: format!("Something went wrong with the LLM client: {err:?}"),
},
Expand Down
11 changes: 11 additions & 0 deletions engine/baml-runtime/src/internal/llm_client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,12 @@ pub struct LLMErrorResponse {

#[derive(Debug, Clone, Serialize, PartialEq)]
pub enum ErrorCode {
// Failed to establish a connection
// Tends to happen when either (1) user enters the wrong connection details
// (e.g. wrong AWS region and therefore URL) or (2) in prod it's been
// chugging along and then the server provider goes hard down.
FailedToConnect,

InvalidAuthentication, // 401
NotSupported, // 403
RateLimited, // 429
Expand All @@ -227,6 +233,7 @@ pub enum ErrorCode {
impl std::fmt::Display for ErrorCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ErrorCode::FailedToConnect => f.write_str("Failed while establishing connection"),
ErrorCode::InvalidAuthentication => f.write_str("InvalidAuthentication (401)"),
ErrorCode::NotSupported => f.write_str("NotSupported (403)"),
ErrorCode::RateLimited => f.write_str("RateLimited (429)"),
Expand Down Expand Up @@ -263,6 +270,10 @@ impl ErrorCode {

pub fn to_u16(&self) -> u16 {
match self {
// FAILED_TO_CONNECT maps to 2 because the internal callsites usually used ErrorCode::Other(2)
// for connection errors. It's unclear if this actually makes its way out to users in any
// meaningful way.
ErrorCode::FailedToConnect => 2,
ErrorCode::InvalidAuthentication => 401,
ErrorCode::NotSupported => 403,
ErrorCode::RateLimited => 429,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1245,29 +1245,13 @@ impl WithChat for AwsClient {
latency: instant_start.elapsed(),
message: format!("{e:#?}"),
code: match e {
SdkError::ConstructionFailure(_) => ErrorCode::Other(2),
SdkError::ConstructionFailure(_) => ErrorCode::FailedToConnect,
SdkError::TimeoutError(_) => ErrorCode::Other(2),
SdkError::DispatchFailure(_) => ErrorCode::Other(2),
SdkError::DispatchFailure(_) => ErrorCode::FailedToConnect,
Comment on lines 1249 to +1250

Choose a reason for hiding this comment

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

correctness: SdkError::TimeoutError(_) and SdkError::Other(2) are mapped to ErrorCode::Other(2) instead of a more specific error, which may cause connection errors to be misclassified and not surfaced as HTTP errors.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In engine/baml-runtime/src/internal/llm_client/primitive/aws/aws_client.rs, lines 1249-1250, update the error mapping so that both `SdkError::TimeoutError(_)` and `SdkError::DispatchFailure(_)` are mapped to `ErrorCode::FailedToConnect` instead of `ErrorCode::Other(2)`. This ensures connection errors are surfaced as HTTP errors. Change:
SdkError::TimeoutError(_) => ErrorCode::Other(2),
SdkError::DispatchFailure(_) => ErrorCode::FailedToConnect,
to:
SdkError::TimeoutError(_) => ErrorCode::FailedToConnect,
SdkError::DispatchFailure(_) => ErrorCode::FailedToConnect,
📝 Committable Code Suggestion

‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
SdkError::TimeoutError(_) => ErrorCode::Other(2),
SdkError::DispatchFailure(_) => ErrorCode::Other(2),
SdkError::DispatchFailure(_) => ErrorCode::FailedToConnect,
SdkError::TimeoutError(_) => ErrorCode::FailedToConnect,
SdkError::DispatchFailure(_) => ErrorCode::FailedToConnect,

SdkError::ResponseError(e) => {
ErrorCode::UnsupportedResponse(e.raw().status().as_u16())
}
SdkError::ServiceError(e) => {
let status = e.raw().status();
match status.as_u16() {
400 => ErrorCode::InvalidAuthentication,
403 => ErrorCode::NotSupported,
429 => ErrorCode::RateLimited,
500 => ErrorCode::ServerError,
503 => ErrorCode::ServiceUnavailable,
_ => {
if status.is_server_error() {
ErrorCode::ServerError
} else {
ErrorCode::Other(status.as_u16())
}
}
}
}
SdkError::ServiceError(e) => ErrorCode::from_u16(e.raw().status().as_u16()),
_ => ErrorCode::Other(2),
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,12 @@ impl WithStreamChat for OpenAIClient {
}

macro_rules! make_openai_client {
($client:ident, $properties:ident, $provider:expr, dynamic) => {
($client:ident, $properties:ident, $provider:expr, dynamic) => {{
let resolve_pdf_urls = if $provider == "openai-responses" {
ResolveMediaUrls::Never
} else {
ResolveMediaUrls::Always
};
Ok(Self {
name: $client.name.clone(),
provider: $provider.into(),
Expand All @@ -506,16 +511,21 @@ macro_rules! make_openai_client {
max_one_system_prompt: false,
resolve_audio_urls: ResolveMediaUrls::Always,
resolve_image_urls: ResolveMediaUrls::Never,
resolve_pdf_urls: ResolveMediaUrls::Never,
resolve_pdf_urls,
resolve_video_urls: ResolveMediaUrls::Never,
allowed_metadata: $properties.allowed_metadata.clone(),
},
properties: $properties,
retry_policy: $client.retry_policy.clone(),
client: create_client()?,
})
};
($client:ident, $properties:ident, $provider:expr) => {
}};
($client:ident, $properties:ident, $provider:expr) => {{
let resolve_pdf_urls = if $provider == "openai-responses" {
ResolveMediaUrls::Never
} else {
ResolveMediaUrls::Always
};
Ok(Self {
name: $client.name().into(),
provider: $provider.into(),
Expand All @@ -533,7 +543,7 @@ macro_rules! make_openai_client {
max_one_system_prompt: false,
resolve_audio_urls: ResolveMediaUrls::Always,
resolve_image_urls: ResolveMediaUrls::Never,
resolve_pdf_urls: ResolveMediaUrls::Never,
resolve_pdf_urls,
resolve_video_urls: ResolveMediaUrls::Never,
allowed_metadata: $properties.allowed_metadata.clone(),
},
Expand All @@ -545,7 +555,7 @@ macro_rules! make_openai_client {
.map(|s| s.to_string()),
client: create_client()?,
})
};
}};
}

impl OpenAIClient {
Expand Down Expand Up @@ -714,8 +724,12 @@ impl ToProviderMessage for OpenAIClient {
match &media.content {
BamlMediaContent::Url(url_content) => {
// For URLs, we need to resolve them to base64 first
anyhow::bail!(
"BAML internal error (openai): Pdf URL are not supported by OpenAI use base64."
content.insert(
payload_key.into(),
json!({
"type": "input_file",
"file_url": url_content.url
}),
);
Comment on lines 724 to 733

Choose a reason for hiding this comment

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

correctness: to_media_message for BamlMediaType::Pdf with BamlMediaContent::Url inserts a nested map with both file and file_url, which does not match OpenAI's expected schema and may cause request failures.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In engine/baml-runtime/src/internal/llm_client/primitive/openai/openai_client.rs, lines 724-733, the `to_media_message` function for `BamlMediaType::Pdf` with `BamlMediaContent::Url` inserts a map with both `type` and `file_url` under the `file` key, which does not match OpenAI's expected schema and may cause request failures. Please update this block so that only `{ "file_url": url_content.url }` is inserted under the `file` key, not a nested map with `type`.
📝 Committable Code Suggestion

‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
match &media.content {
BamlMediaContent::Url(url_content) => {
// For URLs, we need to resolve them to base64 first
anyhow::bail!(
"BAML internal error (openai): Pdf URL are not supported by OpenAI use base64."
content.insert(
payload_key.into(),
json!({
"type": "input_file",
"file_url": url_content.url
}),
);
match &media.content {
BamlMediaContent::Url(url_content) => {
content.insert(
payload_key.into(),
json!({
"file_url": url_content.url
}),
);
}

}
BamlMediaContent::Base64(b64_media) => {
Expand Down
15 changes: 11 additions & 4 deletions engine/baml-runtime/src/internal/llm_client/primitive/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,11 @@ pub(crate) async fn build_and_log_outbound_request(
request_options: client.request_options().clone(),
latency: instant_now.elapsed(),
message: format!("Failed to build request: {e:#?}"),
code: ErrorCode::Other(2),
code: if e.is_connect() {
ErrorCode::FailedToConnect
} else {
ErrorCode::Other(2)
},
}));
}
};
Expand Down Expand Up @@ -253,9 +257,12 @@ pub async fn execute_request(
)
}
},
code: e
.status()
.map_or(ErrorCode::Other(2), ErrorCode::from_status),
code: if e.is_connect() {
ErrorCode::FailedToConnect
} else {
e.status()
.map_or(ErrorCode::Other(2), ErrorCode::from_status)
},
}));
}
};
Expand Down
20 changes: 19 additions & 1 deletion engine/baml-runtime/src/types/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,26 @@ impl FunctionResult {
}

fn format_err(&self, err: &anyhow::Error) -> ExposedError {
// panic!("format_err {:?}", err);
if let Some(exposed_error) = err.downcast_ref::<ExposedError>() {
return exposed_error.clone();
}

if let LLMResponse::LLMFailure(err) = self.llm_response() {
if err.code == ErrorCode::FailedToConnect {
let actual_error = err.message.clone();
return ExposedError::ClientHttpError {
client_name: match self.llm_response() {
LLMResponse::Success(resp) => resp.client.clone(),
LLMResponse::LLMFailure(err) => err.client.clone(),
_ => "unknown".to_string(),
},
message: actual_error,
status_code: ErrorCode::FailedToConnect,
};
}
}

// Capture the actual error to preserve its details
let actual_error = err.to_string();
// TODO: HACK! Figure out why now connection errors dont get converted into ExposedError. Instead of converting to a validation error, check for connection errors here. We probably are missing a lot of other connection failures that should NOT be validation errors.
Expand All @@ -137,9 +154,10 @@ impl FunctionResult {
_ => "unknown".to_string(),
},
message: actual_error,
status_code: ErrorCode::ServiceUnavailable,
status_code: ErrorCode::FailedToConnect,
};
}

ExposedError::ValidationError {
prompt: match self.llm_response() {
LLMResponse::Success(resp) => resp.prompt.to_string(),
Expand Down
1 change: 1 addition & 0 deletions engine/language_client_python/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ impl BamlError {
))
}
baml_runtime::internal::llm_client::ErrorCode::Other(_)
| baml_runtime::internal::llm_client::ErrorCode::FailedToConnect
| baml_runtime::internal::llm_client::ErrorCode::InvalidAuthentication
| baml_runtime::internal::llm_client::ErrorCode::NotSupported
| baml_runtime::internal::llm_client::ErrorCode::RateLimited
Expand Down
1 change: 1 addition & 0 deletions engine/language_client_typescript/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ pub fn from_anyhow_error(err: anyhow::Error) -> napi::Error {
),
),
baml_runtime::internal::llm_client::ErrorCode::Other(_)
| baml_runtime::internal::llm_client::ErrorCode::FailedToConnect
| baml_runtime::internal::llm_client::ErrorCode::InvalidAuthentication
| baml_runtime::internal::llm_client::ErrorCode::NotSupported
| baml_runtime::internal::llm_client::ErrorCode::RateLimited
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ function FnFailRetryExponentialDelay(retries: int, initial_delay_ms: int) -> str
function TestAbortFallbackChain(input: string) -> string {
client AbortTestFallback
prompt #"
This is a test for fallback chain cancellation.
Please fail so we test the fallback behavior.
Tell me a 200-word story about tigers.

Input: {{ input }}
"#
}
Expand Down
Loading