Files
cursor-byok/server/src/provider/mod.rs
T
leokun d83e14af9a refactor: remove retry_count from ProviderConfig and enhance error handling in tool execution
- Removed the `retry_count` field from `ProviderConfig` as it is no longer needed.
- Introduced `argument_error` field in `ToolCall` to capture errors related to tool arguments.
- Updated various components to handle argument errors more gracefully, including in the `ToolDispatcher` and `ConversationOutput`.
- Enhanced tests to validate the new error handling and ensure proper functionality of tool calls.
2026-09-01 10:14:53 +08:00

205 lines
6.3 KiB
Rust

//! Defines the provider interface and exports provider implementations.
mod anthropic;
mod attempt;
mod event;
mod normalize;
mod openai_chat;
mod openai_responses;
mod recorder;
mod router;
use std::pin::Pin;
use futures_util::Stream;
use tokio_util::sync::CancellationToken;
use crate::{model::ModelInvocation, Result};
pub use anthropic::AnthropicProvider;
pub use event::*;
pub use openai_chat::OpenAiChatProvider;
pub use openai_responses::OpenAiResponsesProvider;
pub use recorder::CallRecorder;
pub use router::{build as build_provider, ProviderRouter};
pub type ProviderStream = Pin<Box<dyn Stream<Item = Result<ModelEvent>> + Send>>;
pub trait Provider: Send + Sync {
fn stream(
&self,
invocation: ModelInvocation,
cancellation: CancellationToken,
) -> ProviderStream;
}
fn map_sse_error(
label: &str,
error: eventsource_stream::EventStreamError<crate::Error>,
) -> crate::Error {
match error {
eventsource_stream::EventStreamError::Transport(error) => error,
eventsource_stream::EventStreamError::Utf8(error) => {
crate::Error::Provider(format!("{label} SSE UTF-8 error: {error}"))
}
eventsource_stream::EventStreamError::Parser(error) => {
crate::Error::Provider(format!("{label} SSE parse error: {error}"))
}
}
}
fn provider_event_error(label: &str, value: &serde_json::Value) -> Option<crate::Error> {
let kind = value.get("type").and_then(serde_json::Value::as_str);
let direct_error = value.get("error").filter(|error| !error.is_null());
if !matches!(kind, Some("error" | "response.failed")) && direct_error.is_none() {
return None;
}
let message = value
.get("message")
.and_then(serde_json::Value::as_str)
.or_else(|| {
value
.pointer("/error/message")
.and_then(serde_json::Value::as_str)
})
.or_else(|| {
value
.pointer("/response/error/message")
.and_then(serde_json::Value::as_str)
})
.or_else(|| direct_error.and_then(serde_json::Value::as_str))
.or_else(|| {
value
.pointer("/response/error")
.and_then(serde_json::Value::as_str)
})
.unwrap_or("provider returned an error event without a message");
Some(crate::Error::Provider(format!("{label} error: {message}")))
}
fn merge_extra_params(body: &mut serde_json::Value, extra: &serde_json::Value) -> Result<()> {
let extra = extra
.as_object()
.ok_or_else(|| crate::Error::Config("model extra params must be an object".into()))?;
let body = body
.as_object_mut()
.ok_or_else(|| crate::Error::Provider("provider request body must be an object".into()))?;
for (name, value) in extra {
if matches!(
name.as_str(),
"model"
| "stream"
| "messages"
| "input"
| "tools"
| "system"
| "instructions"
| "prompt_cache_key"
) {
return Err(crate::Error::Config(format!(
"model extra params cannot replace {name}"
)));
}
body.insert(name.clone(), value.clone());
}
Ok(())
}
fn apply_body_allowlist(
body: &mut serde_json::Value,
allowed: Option<&std::collections::HashSet<String>>,
) -> Result<()> {
let Some(allowed) = allowed else {
return Ok(());
};
body.as_object_mut()
.ok_or_else(|| crate::Error::Provider("provider request body must be an object".into()))?
.retain(|name, _| allowed.contains(name));
Ok(())
}
fn apply_openai_prompt_cache_key(body: &mut serde_json::Value, model_id: &str) -> Result<()> {
if !model_id.to_ascii_lowercase().contains("gpt") {
return Ok(());
}
body.as_object_mut()
.ok_or_else(|| crate::Error::Provider("provider request body must be an object".into()))?
.insert(
"prompt_cache_key".into(),
serde_json::Value::String("cursor-byok".into()),
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sse_transport_errors_are_not_relabelled_as_parse_errors() {
let error = map_sse_error(
"test provider",
eventsource_stream::EventStreamError::Transport(crate::Error::Provider(
"connection closed".into(),
)),
);
let crate::Error::Provider(message) = error else {
panic!("transport error category must be preserved");
};
assert_eq!(message, "connection closed");
}
#[test]
fn provider_error_events_extract_flat_and_nested_messages() {
assert_provider_error(
"OpenAI Responses",
serde_json::json!({
"type": "error",
"message": "Internal error during token generation"
}),
"OpenAI Responses error: Internal error during token generation",
);
assert_provider_error(
"OpenAI Chat",
serde_json::json!({
"error": {"message": "quota exceeded", "type": "server_error"}
}),
"OpenAI Chat error: quota exceeded",
);
assert_provider_error(
"Anthropic",
serde_json::json!({
"type": "error",
"error": {"type": "overloaded_error", "message": "Overloaded"}
}),
"Anthropic error: Overloaded",
);
assert_provider_error(
"OpenAI Responses",
serde_json::json!({
"type": "response.failed",
"response": {"error": {"message": "generation failed"}}
}),
"OpenAI Responses error: generation failed",
);
}
#[test]
fn successful_provider_events_are_not_errors() {
assert!(provider_event_error(
"OpenAI Responses",
&serde_json::json!({"type": "response.completed", "error": null})
)
.is_none());
}
fn assert_provider_error(label: &str, value: serde_json::Value, expected: &str) {
let Some(crate::Error::Provider(message)) = provider_event_error(label, &value) else {
panic!("expected provider error");
};
assert_eq!(message, expected);
}
}