blob: 0d8a3049ed9df8b31a11c892254f2a02979dc5e0 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use crate::{ModelError, ProviderFailure, ProviderFailureReason};
use maka_provider_protocol::output::ProviderError;
use serde_json::Value;
#[derive(Default)]
pub(super) struct Evidence {
pub observed: bool,
pub unsafe_replay: bool,
}
impl Evidence {
pub fn interrupted(
&self,
reason: ProviderFailureReason,
message: impl Into<String>,
) -> ModelError {
ModelError::Provider(ProviderFailure::new(
reason,
message,
!self.unsafe_replay,
None,
))
}
pub fn transport(&self, error: ModelError) -> ModelError {
match error {
ModelError::Provider(failure) => ModelError::Provider(ProviderFailure::new(
failure.reason(),
failure.message(),
!self.unsafe_replay,
failure.retry_after().map(|d| d.as_millis() as u64),
)),
error => error,
}
}
pub fn provider(
&self,
error: ProviderError,
anthropic: bool,
retry_after: Option<u64>,
) -> ModelError {
let mut records = vec![&error.data];
if let Some(inner) = error.data.get("error") {
records.push(inner);
}
let identifiers = records
.iter()
.flat_map(|record| [record.get("code"), record.get("type")])
.flatten()
.filter_map(Value::as_str)
.chain(error.kind.as_deref())
.chain(error.code.as_ref().and_then(Value::as_str))
.map(str::to_ascii_lowercase)
.collect::<Vec<_>>();
let has = |values: &[&str]| identifiers.iter().any(|id| values.contains(&id.as_str()));
let too_long = anthropic
&& error.status == Some(400)
&& records.iter().any(|record| {
record.get("type").and_then(Value::as_str) == Some("invalid_request_error")
&& record
.get("message")
.and_then(Value::as_str)
.is_some_and(prompt_too_long)
});
if has(&[
"context_length_exceeded",
"model_context_window_exceeded",
"request_too_large",
]) || too_long
{
return ModelError::ContextOverflow {
observed_output: self.observed,
};
}
let permanent = matches!(error.status, Some(401..=403))
|| has(&[
"insufficient_quota",
"insufficient_balance",
"quota_exceeded",
"freeusagelimiterror",
"authentication_error",
"permission_error",
"invalid_api_key",
]);
let reason = if permanent {
None
} else if has(&["resource-exhausted"]) {
Some(ProviderFailureReason::ProviderUnavailable)
} else if error.status == Some(429) || has(&["rate_limit_error"]) {
Some(ProviderFailureReason::RateLimit)
} else if matches!(error.status, Some(408 | 500..=599))
|| has(&["server_error", "overloaded_error"])
{
Some(ProviderFailureReason::ProviderUnavailable)
} else {
None
};
if let Some(reason) = reason {
return ModelError::Provider(ProviderFailure::new(
reason,
error.message,
!self.unsafe_replay,
retry_after,
));
}
if let Some(status) = error
.status
.filter(|s| (100..600).contains(s) && !(200..300).contains(s))
{
return ModelError::Http { status };
}
ModelError::Adapter(error.message)
}
}
fn prompt_too_long(message: &str) -> bool {
let Some(body) = message.strip_prefix("prompt is too long: ") else {
return false;
};
let Some(body) = body.strip_suffix(" maximum") else {
return false;
};
let Some((input, maximum)) = body.split_once(" tokens > ") else {
return false;
};
let parse = |s: &str| {
(!s.is_empty() && s.len() <= 16 && s.bytes().all(|b| b.is_ascii_digit()))
.then(|| s.parse::<u64>().ok())
.flatten()
.filter(|n| *n <= 9_007_199_254_740_991)
};
matches!((parse(input), parse(maximum)), (Some(input), Some(maximum)) if maximum > 0 && input > maximum)
}
pub(super) fn http_error(status: u16, bytes: &[u8]) -> ProviderError {
let data: Value = if bytes.len() <= 64 * 1024 {
serde_json::from_slice(bytes).unwrap_or(Value::Null)
} else {
Value::Null
};
let record = data.get("error").unwrap_or(&data);
ProviderError {
message: record
.get("message")
.and_then(Value::as_str)
.map(str::to_owned)
.unwrap_or_else(|| format!("model HTTP request failed (status {status})")),
kind: record
.get("type")
.and_then(Value::as_str)
.map(str::to_owned),
code: record.get("code").cloned(),
status: Some(status),
retryable: None,
data,
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn classify(status: u16, value: Value, anthropic: bool) -> ModelError {
Evidence::default().provider(
http_error(status, &serde_json::to_vec(&value).unwrap()),
anthropic,
None,
)
}
#[test]
fn overflow_requires_bounded_structured_provider_evidence() {
for value in [
json!({"code":"context_length_exceeded"}),
json!({"error":{"code":"context_length_exceeded"}}),
json!({"type":"request_too_large"}),
json!({"code":"model_context_window_exceeded"}),
] {
assert!(matches!(
classify(400, value, false),
ModelError::ContextOverflow {
observed_output: false
}
));
}
for value in [
json!({"message":"context_length_exceeded"}),
json!({"code":"rate_limit_error","message":"context overflow"}),
json!({"prompt":{"code":"context_length_exceeded"}}),
] {
assert!(!matches!(
classify(400, value, false),
ModelError::ContextOverflow { .. }
));
}
assert!(matches!(
classify(413, json!({}), false),
ModelError::Http { status: 413 }
));
let mut body = vec![b' '; 65537];
body.extend_from_slice(br#"{"error":{"code":"context_length_exceeded"}}"#);
assert!(matches!(
Evidence::default().provider(http_error(400, &body), false, None),
ModelError::Http { status: 400 }
));
assert!(matches!(
Evidence::default().provider(http_error(400, b"not JSON"), false, None),
ModelError::Http { status: 400 }
));
}
#[test]
fn anthropic_overflow_relation_is_exact_and_route_scoped() {
let value = |message| json!({"error":{"type":"invalid_request_error","message":message}});
let valid = "prompt is too long: 200001 tokens > 200000 maximum";
assert!(matches!(
classify(400, value(valid), true),
ModelError::ContextOverflow { .. }
));
assert!(matches!(
classify(400, value(valid), false),
ModelError::Http { .. }
));
assert!(matches!(
classify(413, value(valid), true),
ModelError::Http { .. }
));
for message in [
"prompt is too long",
"prompt is too long: 1 tokens > 2 maximum",
"quota: prompt is too long: 3 tokens > 2 maximum",
"prompt is too long: 3 tokens > 2 maximum; output limit",
"prompt is too long: 9999999999999999 tokens > 2 maximum",
"prompt is too long: 3 tokens > 0 maximum",
] {
assert!(
matches!(classify(400, value(message), true), ModelError::Http { .. }),
"{message}"
);
}
}
#[test]
fn http_status_is_private_and_permanent_failures_override_retry_signals() {
for status in [100, 199, 300, 400, 401, 402, 403, 404] {
let error = classify(status, json!({"error":{"message":"PRIVATE_BODY"}}), false);
assert!(matches!(error, ModelError::Http { status: actual } if actual == status));
assert!(!error.to_string().contains("PRIVATE_BODY"));
}
for status in [401, 402, 403, 429] {
assert!(matches!(
classify(
status,
json!({"error":{"code":"insufficient_quota"}}),
false
),
ModelError::Http { .. }
));
}
for (status, reason) in [
(429, ProviderFailureReason::RateLimit),
(408, ProviderFailureReason::ProviderUnavailable),
(503, ProviderFailureReason::ProviderUnavailable),
] {
let ModelError::Provider(failure) =
Evidence::default().provider(http_error(status, b"{}"), false, Some(1234))
else {
panic!("typed provider failure")
};
assert_eq!(failure.reason(), reason);
assert!(failure.replay_safe());
assert_eq!(failure.retry_after().unwrap().as_millis(), 1234);
}
}
}