dedff81475
- 可信 code-prototype 父 Run 认领并观察直属美术 delivery,普通失败立即收束,合法安全默认 marker 由 Runtime 确定性执行唯一同合同返工 - 补齐 suppressed 无 child 与父身份链丢失时的 completion 失败关闭边界 - 修复 Windows project.lock delete-pending 竞争并收紧 HTML 内联 JS 语法 fail-open - 同步技术方案、决策记录与实施计划,并完成确定性及真实 Provider 分层验证
1093 lines
32 KiB
Rust
1093 lines
32 KiB
Rust
use std::collections::{BTreeMap, BTreeSet};
|
||
use std::fmt;
|
||
use std::future::Future;
|
||
use std::pin::Pin;
|
||
use std::sync::Arc;
|
||
|
||
use serde::{Deserialize, Serialize};
|
||
use serde_json::{Map, Value};
|
||
|
||
use crate::contract::{
|
||
validate_description, validate_function_name, validate_identifier, validate_metadata,
|
||
};
|
||
|
||
macro_rules! provider_id {
|
||
($name:ident, $label:literal) => {
|
||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
||
#[serde(transparent)]
|
||
pub struct $name(String);
|
||
|
||
impl $name {
|
||
pub fn try_new(value: impl Into<String>) -> Result<Self, ProviderError> {
|
||
let value = value.into();
|
||
validate_identifier(&value, $label).map_err(ProviderError::from_contract)?;
|
||
Ok(Self(value))
|
||
}
|
||
|
||
pub fn as_str(&self) -> &str {
|
||
&self.0
|
||
}
|
||
}
|
||
|
||
impl TryFrom<String> for $name {
|
||
type Error = ProviderError;
|
||
|
||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||
Self::try_new(value)
|
||
}
|
||
}
|
||
|
||
impl<'de> Deserialize<'de> for $name {
|
||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||
where
|
||
D: serde::Deserializer<'de>,
|
||
{
|
||
let value = String::deserialize(deserializer)?;
|
||
Self::try_new(value).map_err(serde::de::Error::custom)
|
||
}
|
||
}
|
||
|
||
impl fmt::Display for $name {
|
||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
formatter.write_str(&self.0)
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
provider_id!(ProviderInstanceId, "provider instance id");
|
||
provider_id!(ProviderProtocolId, "provider protocol id");
|
||
|
||
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
||
#[serde(rename_all = "kebab-case")]
|
||
pub enum ProviderCapability {
|
||
Streaming,
|
||
FunctionTools,
|
||
RequiredToolChoice,
|
||
ImageInput,
|
||
WebSearch,
|
||
ReasoningEffort,
|
||
TextVerbosity,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||
pub struct ProviderDescriptor {
|
||
instance_id: ProviderInstanceId,
|
||
protocol_id: ProviderProtocolId,
|
||
display_name: String,
|
||
capabilities: BTreeSet<ProviderCapability>,
|
||
metadata: Value,
|
||
}
|
||
|
||
impl ProviderDescriptor {
|
||
pub fn try_new(
|
||
instance_id: ProviderInstanceId,
|
||
protocol_id: ProviderProtocolId,
|
||
capabilities: impl IntoIterator<Item = ProviderCapability>,
|
||
) -> Result<Self, ProviderError> {
|
||
let descriptor = Self {
|
||
display_name: instance_id.as_str().to_string(),
|
||
instance_id,
|
||
protocol_id,
|
||
capabilities: capabilities.into_iter().collect(),
|
||
metadata: Value::Object(Map::new()),
|
||
};
|
||
descriptor.validate()?;
|
||
Ok(descriptor)
|
||
}
|
||
|
||
pub fn with_metadata(mut self, metadata: Value) -> Result<Self, ProviderError> {
|
||
validate_metadata(&metadata, "provider metadata").map_err(ProviderError::from_contract)?;
|
||
self.metadata = metadata;
|
||
Ok(self)
|
||
}
|
||
|
||
pub fn with_display_name(
|
||
mut self,
|
||
display_name: impl Into<String>,
|
||
) -> Result<Self, ProviderError> {
|
||
self.display_name = display_name.into();
|
||
self.validate()?;
|
||
Ok(self)
|
||
}
|
||
|
||
pub fn instance_id(&self) -> &ProviderInstanceId {
|
||
&self.instance_id
|
||
}
|
||
|
||
pub fn protocol_id(&self) -> &ProviderProtocolId {
|
||
&self.protocol_id
|
||
}
|
||
|
||
pub fn display_name(&self) -> &str {
|
||
&self.display_name
|
||
}
|
||
|
||
pub fn capabilities(&self) -> &BTreeSet<ProviderCapability> {
|
||
&self.capabilities
|
||
}
|
||
|
||
pub fn supports(&self, capability: ProviderCapability) -> bool {
|
||
self.capabilities.contains(&capability)
|
||
}
|
||
|
||
pub fn metadata(&self) -> &Value {
|
||
&self.metadata
|
||
}
|
||
|
||
fn validate(&self) -> Result<(), ProviderError> {
|
||
validate_description(&self.display_name, "provider display name")
|
||
.map_err(ProviderError::from_contract)?;
|
||
validate_metadata(&self.metadata, "provider metadata")
|
||
.map_err(ProviderError::from_contract)?;
|
||
if self.supports(ProviderCapability::RequiredToolChoice)
|
||
&& !self.supports(ProviderCapability::FunctionTools)
|
||
{
|
||
return Err(ProviderError::invalid(
|
||
"required-tool-choice capability 依赖 function-tools capability",
|
||
));
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||
#[serde(rename_all = "kebab-case")]
|
||
pub enum ProviderRole {
|
||
System,
|
||
User,
|
||
Assistant,
|
||
Tool,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||
#[serde(
|
||
deny_unknown_fields,
|
||
tag = "type",
|
||
rename_all = "kebab-case",
|
||
rename_all_fields = "camelCase"
|
||
)]
|
||
pub enum ProviderContentPart {
|
||
Text {
|
||
text: String,
|
||
},
|
||
Image {
|
||
source: Value,
|
||
},
|
||
ToolResult {
|
||
tool_call_id: String,
|
||
output: Value,
|
||
is_error: bool,
|
||
},
|
||
}
|
||
|
||
impl ProviderContentPart {
|
||
pub fn text(text: impl Into<String>) -> Result<Self, ProviderError> {
|
||
let text = text.into();
|
||
validate_non_empty_text(&text, "message text")?;
|
||
Ok(Self::Text { text })
|
||
}
|
||
|
||
pub fn image(source: Value) -> Result<Self, ProviderError> {
|
||
if !source.is_object() {
|
||
return Err(ProviderError::invalid("image source 必须是 JSON object"));
|
||
}
|
||
Ok(Self::Image { source })
|
||
}
|
||
|
||
pub fn tool_result(
|
||
tool_call_id: impl Into<String>,
|
||
output: Value,
|
||
is_error: bool,
|
||
) -> Result<Self, ProviderError> {
|
||
let tool_call_id = tool_call_id.into();
|
||
validate_identifier(&tool_call_id, "tool call id").map_err(ProviderError::from_contract)?;
|
||
Ok(Self::ToolResult {
|
||
tool_call_id,
|
||
output,
|
||
is_error,
|
||
})
|
||
}
|
||
|
||
fn validate(&self) -> Result<(), ProviderError> {
|
||
match self {
|
||
Self::Text { text } => validate_non_empty_text(text, "message text"),
|
||
Self::Image { source } if !source.is_object() => {
|
||
Err(ProviderError::invalid("image source 必须是 JSON object"))
|
||
}
|
||
Self::ToolResult { tool_call_id, .. } => {
|
||
validate_identifier(tool_call_id, "tool call id")
|
||
.map_err(ProviderError::from_contract)
|
||
}
|
||
Self::Image { .. } => Ok(()),
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||
pub struct ProviderMessage {
|
||
role: ProviderRole,
|
||
content: Vec<ProviderContentPart>,
|
||
}
|
||
|
||
impl ProviderMessage {
|
||
pub fn try_new(
|
||
role: ProviderRole,
|
||
content: impl IntoIterator<Item = ProviderContentPart>,
|
||
) -> Result<Self, ProviderError> {
|
||
let message = Self {
|
||
role,
|
||
content: content.into_iter().collect(),
|
||
};
|
||
message.validate()?;
|
||
Ok(message)
|
||
}
|
||
|
||
pub fn role(&self) -> ProviderRole {
|
||
self.role
|
||
}
|
||
|
||
pub fn content(&self) -> &[ProviderContentPart] {
|
||
&self.content
|
||
}
|
||
|
||
fn validate(&self) -> Result<(), ProviderError> {
|
||
if self.content.is_empty() {
|
||
return Err(ProviderError::invalid("provider message content 不能为空"));
|
||
}
|
||
for part in &self.content {
|
||
part.validate()?;
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||
pub struct ProviderToolDefinition {
|
||
name: String,
|
||
description: String,
|
||
input_schema: Value,
|
||
strict: bool,
|
||
}
|
||
|
||
impl ProviderToolDefinition {
|
||
pub fn try_new(
|
||
name: impl Into<String>,
|
||
description: impl Into<String>,
|
||
input_schema: Value,
|
||
) -> Result<Self, ProviderError> {
|
||
let tool = Self {
|
||
name: name.into(),
|
||
description: description.into(),
|
||
input_schema,
|
||
strict: false,
|
||
};
|
||
tool.validate()?;
|
||
Ok(tool)
|
||
}
|
||
|
||
pub fn name(&self) -> &str {
|
||
&self.name
|
||
}
|
||
|
||
pub fn description(&self) -> &str {
|
||
&self.description
|
||
}
|
||
|
||
pub fn input_schema(&self) -> &Value {
|
||
&self.input_schema
|
||
}
|
||
|
||
pub fn with_strict(mut self, strict: bool) -> Self {
|
||
self.strict = strict;
|
||
self
|
||
}
|
||
|
||
pub fn strict(&self) -> bool {
|
||
self.strict
|
||
}
|
||
|
||
fn validate(&self) -> Result<(), ProviderError> {
|
||
validate_function_name(&self.name).map_err(ProviderError::from_contract)?;
|
||
validate_description(&self.description, "provider tool description")
|
||
.map_err(ProviderError::from_contract)?;
|
||
if self.input_schema.get("type").and_then(Value::as_str) != Some("object") {
|
||
return Err(ProviderError::invalid(
|
||
"provider tool inputSchema.type 必须为 object",
|
||
));
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||
#[serde(tag = "type", content = "name", rename_all = "kebab-case")]
|
||
pub enum ProviderToolChoice {
|
||
Auto,
|
||
None,
|
||
Required,
|
||
Specific(String),
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||
#[serde(rename_all = "kebab-case")]
|
||
pub enum ProviderReasoningEffort {
|
||
Minimal,
|
||
Low,
|
||
Medium,
|
||
High,
|
||
XHigh,
|
||
Max,
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||
#[serde(rename_all = "kebab-case")]
|
||
pub enum ProviderTextVerbosity {
|
||
Low,
|
||
Medium,
|
||
High,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||
pub struct ProviderRequest {
|
||
request_id: String,
|
||
model: Option<String>,
|
||
messages: Vec<ProviderMessage>,
|
||
max_output_tokens: Option<u32>,
|
||
request_timeout_ms: Option<u64>,
|
||
tools: Vec<ProviderToolDefinition>,
|
||
tool_choice: ProviderToolChoice,
|
||
web_search: bool,
|
||
reasoning_effort: Option<ProviderReasoningEffort>,
|
||
text_verbosity: Option<ProviderTextVerbosity>,
|
||
metadata: Value,
|
||
}
|
||
|
||
impl ProviderRequest {
|
||
pub fn try_new(
|
||
request_id: impl Into<String>,
|
||
messages: impl IntoIterator<Item = ProviderMessage>,
|
||
) -> Result<Self, ProviderError> {
|
||
let request = Self {
|
||
request_id: request_id.into(),
|
||
model: None,
|
||
messages: messages.into_iter().collect(),
|
||
max_output_tokens: None,
|
||
request_timeout_ms: None,
|
||
tools: Vec::new(),
|
||
tool_choice: ProviderToolChoice::Auto,
|
||
web_search: false,
|
||
reasoning_effort: None,
|
||
text_verbosity: None,
|
||
metadata: Value::Object(Map::new()),
|
||
};
|
||
request.validate()?;
|
||
Ok(request)
|
||
}
|
||
|
||
pub fn with_tools(
|
||
mut self,
|
||
tools: impl IntoIterator<Item = ProviderToolDefinition>,
|
||
tool_choice: ProviderToolChoice,
|
||
) -> Result<Self, ProviderError> {
|
||
self.tools = tools.into_iter().collect();
|
||
self.tool_choice = tool_choice;
|
||
self.validate()?;
|
||
Ok(self)
|
||
}
|
||
|
||
pub fn with_model(mut self, model: impl Into<String>) -> Result<Self, ProviderError> {
|
||
self.model = Some(model.into());
|
||
self.validate()?;
|
||
Ok(self)
|
||
}
|
||
|
||
pub fn with_max_output_tokens(mut self, max_output_tokens: u32) -> Result<Self, ProviderError> {
|
||
self.max_output_tokens = Some(max_output_tokens);
|
||
self.validate()?;
|
||
Ok(self)
|
||
}
|
||
|
||
pub fn with_request_timeout_ms(
|
||
mut self,
|
||
request_timeout_ms: u64,
|
||
) -> Result<Self, ProviderError> {
|
||
self.request_timeout_ms = Some(request_timeout_ms);
|
||
self.validate()?;
|
||
Ok(self)
|
||
}
|
||
|
||
pub fn with_web_search(mut self, enabled: bool) -> Self {
|
||
self.web_search = enabled;
|
||
self
|
||
}
|
||
|
||
pub fn with_reasoning_effort(mut self, effort: ProviderReasoningEffort) -> Self {
|
||
self.reasoning_effort = Some(effort);
|
||
self
|
||
}
|
||
|
||
pub fn with_text_verbosity(mut self, verbosity: ProviderTextVerbosity) -> Self {
|
||
self.text_verbosity = Some(verbosity);
|
||
self
|
||
}
|
||
|
||
pub fn with_metadata(mut self, metadata: Value) -> Result<Self, ProviderError> {
|
||
validate_metadata(&metadata, "provider request metadata")
|
||
.map_err(ProviderError::from_contract)?;
|
||
self.metadata = metadata;
|
||
Ok(self)
|
||
}
|
||
|
||
pub fn request_id(&self) -> &str {
|
||
&self.request_id
|
||
}
|
||
pub fn model(&self) -> Option<&str> {
|
||
self.model.as_deref()
|
||
}
|
||
pub fn messages(&self) -> &[ProviderMessage] {
|
||
&self.messages
|
||
}
|
||
pub fn max_output_tokens(&self) -> Option<u32> {
|
||
self.max_output_tokens
|
||
}
|
||
pub fn request_timeout_ms(&self) -> Option<u64> {
|
||
self.request_timeout_ms
|
||
}
|
||
pub fn tools(&self) -> &[ProviderToolDefinition] {
|
||
&self.tools
|
||
}
|
||
pub fn tool_choice(&self) -> &ProviderToolChoice {
|
||
&self.tool_choice
|
||
}
|
||
pub fn web_search(&self) -> bool {
|
||
self.web_search
|
||
}
|
||
pub fn reasoning_effort(&self) -> Option<ProviderReasoningEffort> {
|
||
self.reasoning_effort
|
||
}
|
||
pub fn text_verbosity(&self) -> Option<ProviderTextVerbosity> {
|
||
self.text_verbosity
|
||
}
|
||
pub fn metadata(&self) -> &Value {
|
||
&self.metadata
|
||
}
|
||
|
||
pub fn required_capabilities(&self) -> BTreeSet<ProviderCapability> {
|
||
let mut required = BTreeSet::new();
|
||
if !self.tools.is_empty() {
|
||
required.insert(ProviderCapability::FunctionTools);
|
||
}
|
||
if matches!(
|
||
self.tool_choice,
|
||
ProviderToolChoice::Required | ProviderToolChoice::Specific(_)
|
||
) {
|
||
required.insert(ProviderCapability::FunctionTools);
|
||
required.insert(ProviderCapability::RequiredToolChoice);
|
||
}
|
||
if self
|
||
.messages
|
||
.iter()
|
||
.flat_map(|message| message.content.iter())
|
||
.any(|part| matches!(part, ProviderContentPart::ToolResult { .. }))
|
||
{
|
||
required.insert(ProviderCapability::FunctionTools);
|
||
}
|
||
if self
|
||
.messages
|
||
.iter()
|
||
.flat_map(|message| message.content.iter())
|
||
.any(|part| matches!(part, ProviderContentPart::Image { .. }))
|
||
{
|
||
required.insert(ProviderCapability::ImageInput);
|
||
}
|
||
if self.web_search {
|
||
required.insert(ProviderCapability::WebSearch);
|
||
}
|
||
if self.reasoning_effort.is_some() {
|
||
required.insert(ProviderCapability::ReasoningEffort);
|
||
}
|
||
if self.text_verbosity.is_some() {
|
||
required.insert(ProviderCapability::TextVerbosity);
|
||
}
|
||
required
|
||
}
|
||
|
||
fn validate(&self) -> Result<(), ProviderError> {
|
||
validate_identifier(&self.request_id, "provider request id")
|
||
.map_err(ProviderError::from_contract)?;
|
||
if self
|
||
.model
|
||
.as_ref()
|
||
.is_some_and(|model| model.trim().is_empty())
|
||
{
|
||
return Err(ProviderError::invalid("provider request model 不能为空"));
|
||
}
|
||
if self.max_output_tokens == Some(0) {
|
||
return Err(ProviderError::invalid(
|
||
"provider request maxOutputTokens 必须大于 0",
|
||
));
|
||
}
|
||
if self.request_timeout_ms == Some(0) {
|
||
return Err(ProviderError::invalid(
|
||
"provider request requestTimeoutMs 必须大于 0",
|
||
));
|
||
}
|
||
if self.messages.is_empty() {
|
||
return Err(ProviderError::invalid("provider request messages 不能为空"));
|
||
}
|
||
for message in &self.messages {
|
||
message.validate()?;
|
||
}
|
||
let mut tool_names = BTreeSet::new();
|
||
for tool in &self.tools {
|
||
tool.validate()?;
|
||
if !tool_names.insert(tool.name.as_str()) {
|
||
return Err(ProviderError::invalid(format!(
|
||
"provider tool name 重复:{}",
|
||
tool.name
|
||
)));
|
||
}
|
||
}
|
||
match &self.tool_choice {
|
||
ProviderToolChoice::Auto | ProviderToolChoice::None => {}
|
||
ProviderToolChoice::Required if self.tools.is_empty() => {
|
||
return Err(ProviderError::invalid(
|
||
"required tool choice 需要至少一个 function tool",
|
||
));
|
||
}
|
||
ProviderToolChoice::Specific(name) => {
|
||
validate_function_name(name).map_err(ProviderError::from_contract)?;
|
||
if !tool_names.contains(name.as_str()) {
|
||
return Err(ProviderError::invalid(format!(
|
||
"specific tool choice 引用了未知工具:{name}"
|
||
)));
|
||
}
|
||
}
|
||
ProviderToolChoice::Required => {}
|
||
}
|
||
validate_metadata(&self.metadata, "provider request metadata")
|
||
.map_err(ProviderError::from_contract)
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||
pub struct ProviderToolCall {
|
||
id: String,
|
||
name: String,
|
||
arguments: String,
|
||
}
|
||
|
||
impl ProviderToolCall {
|
||
pub fn try_new(
|
||
id: impl Into<String>,
|
||
name: impl Into<String>,
|
||
arguments: impl Into<String>,
|
||
) -> Result<Self, ProviderError> {
|
||
let call = Self {
|
||
id: id.into(),
|
||
name: name.into(),
|
||
arguments: arguments.into(),
|
||
};
|
||
validate_identifier(&call.id, "provider tool call id")
|
||
.map_err(ProviderError::from_contract)?;
|
||
validate_function_name(&call.name).map_err(ProviderError::from_contract)?;
|
||
Ok(call)
|
||
}
|
||
pub fn id(&self) -> &str {
|
||
&self.id
|
||
}
|
||
pub fn name(&self) -> &str {
|
||
&self.name
|
||
}
|
||
pub fn arguments(&self) -> &str {
|
||
&self.arguments
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||
pub struct ProviderUsage {
|
||
input_tokens: u64,
|
||
output_tokens: u64,
|
||
total_tokens: u64,
|
||
}
|
||
|
||
impl ProviderUsage {
|
||
pub fn new(input_tokens: u64, output_tokens: u64, total_tokens: u64) -> Self {
|
||
Self {
|
||
input_tokens,
|
||
output_tokens,
|
||
total_tokens,
|
||
}
|
||
}
|
||
pub fn input_tokens(&self) -> u64 {
|
||
self.input_tokens
|
||
}
|
||
pub fn output_tokens(&self) -> u64 {
|
||
self.output_tokens
|
||
}
|
||
pub fn total_tokens(&self) -> u64 {
|
||
self.total_tokens
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||
pub struct ProviderResponse {
|
||
request_id: String,
|
||
model: String,
|
||
response_id: Option<String>,
|
||
content: Vec<ProviderContentPart>,
|
||
tool_calls: Vec<ProviderToolCall>,
|
||
finish_reason: Option<String>,
|
||
usage: Option<ProviderUsage>,
|
||
}
|
||
|
||
impl ProviderResponse {
|
||
pub fn try_new(
|
||
request_id: impl Into<String>,
|
||
model: impl Into<String>,
|
||
content: impl IntoIterator<Item = ProviderContentPart>,
|
||
tool_calls: impl IntoIterator<Item = ProviderToolCall>,
|
||
) -> Result<Self, ProviderError> {
|
||
let response = Self {
|
||
request_id: request_id.into(),
|
||
model: model.into(),
|
||
response_id: None,
|
||
content: content.into_iter().collect(),
|
||
tool_calls: tool_calls.into_iter().collect(),
|
||
finish_reason: None,
|
||
usage: None,
|
||
};
|
||
validate_identifier(&response.request_id, "provider response request id")
|
||
.map_err(ProviderError::from_contract)?;
|
||
validate_non_empty_text(&response.model, "provider response model")?;
|
||
Ok(response)
|
||
}
|
||
pub fn with_finish_reason(mut self, reason: impl Into<String>) -> Result<Self, ProviderError> {
|
||
let reason = reason.into();
|
||
validate_non_empty_text(&reason, "provider finish reason")?;
|
||
self.finish_reason = Some(reason);
|
||
Ok(self)
|
||
}
|
||
pub fn with_response_id(
|
||
mut self,
|
||
response_id: impl Into<String>,
|
||
) -> Result<Self, ProviderError> {
|
||
let response_id = response_id.into();
|
||
validate_non_empty_text(&response_id, "provider response id")?;
|
||
self.response_id = Some(response_id);
|
||
Ok(self)
|
||
}
|
||
pub fn with_usage(mut self, usage: ProviderUsage) -> Self {
|
||
self.usage = Some(usage);
|
||
self
|
||
}
|
||
pub fn request_id(&self) -> &str {
|
||
&self.request_id
|
||
}
|
||
pub fn model(&self) -> &str {
|
||
&self.model
|
||
}
|
||
pub fn response_id(&self) -> Option<&str> {
|
||
self.response_id.as_deref()
|
||
}
|
||
pub fn content(&self) -> &[ProviderContentPart] {
|
||
&self.content
|
||
}
|
||
pub fn tool_calls(&self) -> &[ProviderToolCall] {
|
||
&self.tool_calls
|
||
}
|
||
pub fn finish_reason(&self) -> Option<&str> {
|
||
self.finish_reason.as_deref()
|
||
}
|
||
pub fn usage(&self) -> Option<ProviderUsage> {
|
||
self.usage
|
||
}
|
||
|
||
fn validate_for_request(
|
||
&self,
|
||
request_id: &str,
|
||
declared_tools: &BTreeSet<String>,
|
||
) -> Result<(), ProviderError> {
|
||
if self.request_id != request_id {
|
||
return Err(ProviderError::invalid(
|
||
"provider response request id 与请求不匹配",
|
||
));
|
||
}
|
||
validate_non_empty_text(&self.model, "provider response model")?;
|
||
if self.content.is_empty() && self.tool_calls.is_empty() {
|
||
return Err(ProviderError::invalid(
|
||
"provider response 必须包含 content 或 toolCalls",
|
||
));
|
||
}
|
||
for part in &self.content {
|
||
part.validate()?;
|
||
}
|
||
let mut call_ids = BTreeSet::new();
|
||
for call in &self.tool_calls {
|
||
validate_identifier(&call.id, "provider tool call id")
|
||
.map_err(ProviderError::from_contract)?;
|
||
validate_function_name(&call.name).map_err(ProviderError::from_contract)?;
|
||
if !call_ids.insert(call.id.as_str()) {
|
||
return Err(ProviderError::invalid(format!(
|
||
"provider tool call id 重复:{}",
|
||
call.id
|
||
)));
|
||
}
|
||
if !declared_tools.contains(&call.name) {
|
||
return Err(ProviderError::invalid(format!(
|
||
"provider response 返回了未声明工具:{}",
|
||
call.name
|
||
)));
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||
#[serde(
|
||
deny_unknown_fields,
|
||
tag = "type",
|
||
rename_all = "kebab-case",
|
||
rename_all_fields = "camelCase"
|
||
)]
|
||
pub enum ProviderStreamEvent {
|
||
TextDelta {
|
||
accumulated_text: String,
|
||
delta_text: String,
|
||
finish_reason: Option<String>,
|
||
},
|
||
ToolCallDelta {
|
||
tool_call_id: String,
|
||
arguments_delta: String,
|
||
},
|
||
Usage {
|
||
usage: ProviderUsage,
|
||
},
|
||
}
|
||
|
||
pub trait ProviderStreamSink {
|
||
fn emit(&mut self, event: ProviderStreamEvent) -> Result<(), ProviderError>;
|
||
}
|
||
|
||
pub type ProviderFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
|
||
pub type ProviderStreamFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
|
||
|
||
pub trait ProviderAdapter: Send + Sync {
|
||
fn descriptor(&self) -> &ProviderDescriptor;
|
||
fn invoke(
|
||
&self,
|
||
request: ProviderRequest,
|
||
) -> ProviderFuture<'_, Result<ProviderResponse, ProviderError>>;
|
||
fn stream<'a>(
|
||
&'a self,
|
||
request: ProviderRequest,
|
||
sink: Box<dyn ProviderStreamSink + 'a>,
|
||
) -> ProviderStreamFuture<'a, Result<ProviderResponse, ProviderError>>;
|
||
}
|
||
|
||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||
pub struct ProviderTarget {
|
||
instance_id: ProviderInstanceId,
|
||
protocol_id: ProviderProtocolId,
|
||
}
|
||
|
||
impl ProviderTarget {
|
||
pub fn new(instance_id: ProviderInstanceId, protocol_id: ProviderProtocolId) -> Self {
|
||
Self {
|
||
instance_id,
|
||
protocol_id,
|
||
}
|
||
}
|
||
pub fn instance_id(&self) -> &ProviderInstanceId {
|
||
&self.instance_id
|
||
}
|
||
pub fn protocol_id(&self) -> &ProviderProtocolId {
|
||
&self.protocol_id
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||
pub enum ProviderErrorKind {
|
||
InvalidContract,
|
||
InvalidConfig,
|
||
InvalidRequest,
|
||
DuplicateInstanceId,
|
||
UnknownInstanceId,
|
||
ProtocolMismatch,
|
||
CapabilityMismatch,
|
||
Timeout,
|
||
Connectivity,
|
||
Upstream,
|
||
StreamUnavailable,
|
||
EmptyResponse,
|
||
Transport,
|
||
Deserialize,
|
||
StreamSink,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||
pub struct ProviderError {
|
||
kind: ProviderErrorKind,
|
||
detail: String,
|
||
attempts: Option<u32>,
|
||
status_code: Option<u16>,
|
||
}
|
||
|
||
impl ProviderError {
|
||
pub fn invalid_config(detail: impl Into<String>) -> Self {
|
||
Self::new(ProviderErrorKind::InvalidConfig, detail)
|
||
}
|
||
pub fn invalid_request(detail: impl Into<String>) -> Self {
|
||
Self::new(ProviderErrorKind::InvalidRequest, detail)
|
||
}
|
||
pub fn timeout(attempts: u32, detail: impl Into<String>) -> Self {
|
||
Self::new(ProviderErrorKind::Timeout, detail).with_attempts(attempts)
|
||
}
|
||
pub fn connectivity(attempts: u32, detail: impl Into<String>) -> Self {
|
||
Self::new(ProviderErrorKind::Connectivity, detail).with_attempts(attempts)
|
||
}
|
||
pub fn upstream(status_code: u16, detail: impl Into<String>) -> Self {
|
||
Self::new(ProviderErrorKind::Upstream, detail).with_status_code(status_code)
|
||
}
|
||
pub fn stream_unavailable(detail: impl Into<String>) -> Self {
|
||
Self::new(ProviderErrorKind::StreamUnavailable, detail)
|
||
}
|
||
pub fn empty_response(detail: impl Into<String>) -> Self {
|
||
Self::new(ProviderErrorKind::EmptyResponse, detail)
|
||
}
|
||
pub fn transport(detail: impl Into<String>) -> Self {
|
||
Self::new(ProviderErrorKind::Transport, detail)
|
||
}
|
||
pub fn deserialize(detail: impl Into<String>) -> Self {
|
||
Self::new(ProviderErrorKind::Deserialize, detail)
|
||
}
|
||
pub fn stream_sink(detail: impl Into<String>) -> Self {
|
||
Self::new(ProviderErrorKind::StreamSink, detail)
|
||
}
|
||
fn new(kind: ProviderErrorKind, detail: impl Into<String>) -> Self {
|
||
Self {
|
||
kind,
|
||
detail: detail.into(),
|
||
attempts: None,
|
||
status_code: None,
|
||
}
|
||
}
|
||
fn with_attempts(mut self, attempts: u32) -> Self {
|
||
self.attempts = Some(attempts);
|
||
self
|
||
}
|
||
fn with_status_code(mut self, status_code: u16) -> Self {
|
||
self.status_code = Some(status_code);
|
||
self
|
||
}
|
||
fn invalid(detail: impl Into<String>) -> Self {
|
||
Self::new(ProviderErrorKind::InvalidContract, detail)
|
||
}
|
||
fn from_contract(error: crate::ContractError) -> Self {
|
||
Self::invalid(error.to_string())
|
||
}
|
||
pub fn kind(&self) -> ProviderErrorKind {
|
||
self.kind
|
||
}
|
||
pub fn detail(&self) -> &str {
|
||
&self.detail
|
||
}
|
||
pub fn attempts(&self) -> Option<u32> {
|
||
self.attempts
|
||
}
|
||
pub fn status_code(&self) -> Option<u16> {
|
||
self.status_code
|
||
}
|
||
}
|
||
|
||
impl fmt::Display for ProviderError {
|
||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
formatter.write_str(&self.detail)
|
||
}
|
||
}
|
||
impl std::error::Error for ProviderError {}
|
||
|
||
struct RegisteredProvider {
|
||
descriptor: ProviderDescriptor,
|
||
adapter: Arc<dyn ProviderAdapter>,
|
||
}
|
||
|
||
#[derive(Default)]
|
||
pub struct ProviderRegistry {
|
||
providers: BTreeMap<ProviderInstanceId, RegisteredProvider>,
|
||
}
|
||
|
||
impl ProviderRegistry {
|
||
pub fn new() -> Self {
|
||
Self::default()
|
||
}
|
||
|
||
pub fn try_new(
|
||
adapters: impl IntoIterator<Item = Arc<dyn ProviderAdapter>>,
|
||
) -> Result<Self, ProviderError> {
|
||
let mut registry = Self::new();
|
||
for adapter in adapters {
|
||
registry.register(adapter)?;
|
||
}
|
||
Ok(registry)
|
||
}
|
||
|
||
pub fn register(&mut self, adapter: Arc<dyn ProviderAdapter>) -> Result<(), ProviderError> {
|
||
let descriptor = adapter.descriptor().clone();
|
||
descriptor.validate()?;
|
||
if self.providers.contains_key(descriptor.instance_id()) {
|
||
return Err(ProviderError::new(
|
||
ProviderErrorKind::DuplicateInstanceId,
|
||
format!("provider instance id 重复:{}", descriptor.instance_id()),
|
||
));
|
||
}
|
||
self.providers.insert(
|
||
descriptor.instance_id.clone(),
|
||
RegisteredProvider {
|
||
descriptor,
|
||
adapter,
|
||
},
|
||
);
|
||
Ok(())
|
||
}
|
||
|
||
pub fn len(&self) -> usize {
|
||
self.providers.len()
|
||
}
|
||
pub fn is_empty(&self) -> bool {
|
||
self.providers.is_empty()
|
||
}
|
||
pub fn descriptor(
|
||
&self,
|
||
instance_id: &ProviderInstanceId,
|
||
) -> Result<&ProviderDescriptor, ProviderError> {
|
||
self.providers
|
||
.get(instance_id)
|
||
.map(|entry| &entry.descriptor)
|
||
.ok_or_else(|| {
|
||
ProviderError::new(
|
||
ProviderErrorKind::UnknownInstanceId,
|
||
format!("未知 provider instance:{instance_id}"),
|
||
)
|
||
})
|
||
}
|
||
pub fn adapter(
|
||
&self,
|
||
instance_id: &ProviderInstanceId,
|
||
) -> Result<Arc<dyn ProviderAdapter>, ProviderError> {
|
||
self.providers
|
||
.get(instance_id)
|
||
.map(|entry| Arc::clone(&entry.adapter))
|
||
.ok_or_else(|| {
|
||
ProviderError::new(
|
||
ProviderErrorKind::UnknownInstanceId,
|
||
format!("未知 provider instance:{instance_id}"),
|
||
)
|
||
})
|
||
}
|
||
|
||
pub fn invoke(
|
||
&self,
|
||
target: &ProviderTarget,
|
||
request: ProviderRequest,
|
||
) -> ProviderFuture<'static, Result<ProviderResponse, ProviderError>> {
|
||
let adapter = match self.resolve(target, &request, false) {
|
||
Ok(adapter) => adapter,
|
||
Err(error) => return Box::pin(async move { Err(error) }),
|
||
};
|
||
let request_id = request.request_id.clone();
|
||
let declared_tools = request
|
||
.tools
|
||
.iter()
|
||
.map(|tool| tool.name.clone())
|
||
.collect::<BTreeSet<_>>();
|
||
Box::pin(async move {
|
||
let response = adapter.invoke(request).await?;
|
||
response.validate_for_request(&request_id, &declared_tools)?;
|
||
Ok(response)
|
||
})
|
||
}
|
||
|
||
pub fn stream<'a>(
|
||
&'a self,
|
||
target: &ProviderTarget,
|
||
request: ProviderRequest,
|
||
sink: Box<dyn ProviderStreamSink + 'a>,
|
||
) -> ProviderStreamFuture<'a, Result<ProviderResponse, ProviderError>> {
|
||
let adapter = match self.resolve(target, &request, true) {
|
||
Ok(adapter) => adapter,
|
||
Err(error) => return Box::pin(async move { Err(error) }),
|
||
};
|
||
let request_id = request.request_id.clone();
|
||
let declared_tools = request
|
||
.tools
|
||
.iter()
|
||
.map(|tool| tool.name.clone())
|
||
.collect::<BTreeSet<_>>();
|
||
Box::pin(async move {
|
||
let response = adapter.stream(request, sink).await?;
|
||
response.validate_for_request(&request_id, &declared_tools)?;
|
||
Ok(response)
|
||
})
|
||
}
|
||
|
||
fn resolve(
|
||
&self,
|
||
target: &ProviderTarget,
|
||
request: &ProviderRequest,
|
||
streaming: bool,
|
||
) -> Result<Arc<dyn ProviderAdapter>, ProviderError> {
|
||
request.validate()?;
|
||
let entry = self.providers.get(target.instance_id()).ok_or_else(|| {
|
||
ProviderError::new(
|
||
ProviderErrorKind::UnknownInstanceId,
|
||
format!("未知 provider instance:{}", target.instance_id()),
|
||
)
|
||
})?;
|
||
if entry.descriptor.protocol_id() != target.protocol_id() {
|
||
return Err(ProviderError::new(
|
||
ProviderErrorKind::ProtocolMismatch,
|
||
format!(
|
||
"provider instance {} 的 protocol 不匹配",
|
||
target.instance_id()
|
||
),
|
||
));
|
||
}
|
||
let mut required = request.required_capabilities();
|
||
if streaming {
|
||
required.insert(ProviderCapability::Streaming);
|
||
}
|
||
let missing = required
|
||
.difference(entry.descriptor.capabilities())
|
||
.copied()
|
||
.collect::<Vec<_>>();
|
||
if !missing.is_empty() {
|
||
return Err(ProviderError::new(
|
||
ProviderErrorKind::CapabilityMismatch,
|
||
format!(
|
||
"provider instance {} 缺少能力:{missing:?}",
|
||
target.instance_id()
|
||
),
|
||
));
|
||
}
|
||
Ok(Arc::clone(&entry.adapter))
|
||
}
|
||
}
|
||
|
||
fn validate_non_empty_text(value: &str, field: &str) -> Result<(), ProviderError> {
|
||
if value.trim().is_empty() {
|
||
Err(ProviderError::invalid(format!("{field} 不能为空")))
|
||
} else {
|
||
Ok(())
|
||
}
|
||
}
|