mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-05 20:44:07 +08:00
206 lines
6.3 KiB
Rust
206 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 request_template;
|
|
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);
|
|
}
|
|
}
|