Skip to content
Closed
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
1 change: 1 addition & 0 deletions crates/libsy-llm-client/tests/observability.rs
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,7 @@ async fn affinity_warns_once_when_request_has_no_usable_identity() -> switchyard
text: "provider reasoning".to_string(),
signature: None,
details: Vec::new(),
provenance: None,
}],
}],
..LlmRequest::default()
Expand Down
1 change: 1 addition & 0 deletions crates/libsy/src/algorithms/advisor_gate/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ fn reasoning_only_turn() -> Response {
text: "thinking about it".to_string(),
signature: None,
details: Vec::new(),
provenance: None,
}],
url_citations: Vec::new(),
stop_reason: None,
Expand Down
2 changes: 2 additions & 0 deletions crates/libsy/src/algorithms/llm_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1576,6 +1576,7 @@ mod tests {
text: "private chain of thought".to_string(),
signature: None,
details: Vec::new(),
provenance: None,
},
ContentBlock::Text {
text: "visible answer".to_string(),
Expand All @@ -1590,6 +1591,7 @@ mod tests {
text: "reasoning-only turn".to_string(),
signature: None,
details: Vec::new(),
provenance: None,
}],
},
Message::text(Role::User, "follow-up"),
Expand Down
1 change: 1 addition & 0 deletions crates/libsy/src/algorithms/util/affinity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,7 @@ mod tests {
text: "Internal provider reasoning.".to_string(),
signature: Some("provider-signature".to_string()),
details: Vec::new(),
provenance: None,
},
],
});
Expand Down
1 change: 1 addition & 0 deletions crates/libsy/src/algorithms/util/llm_judge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,7 @@ mod tests {
text: r#"{"ok":false}"#.to_string(),
signature: None,
details: Vec::new(),
provenance: None,
},
);
}
Expand Down
19 changes: 16 additions & 3 deletions crates/protocol/src/llm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};

use crate::format::FormatId;
use crate::format::{FormatId, WireFormat};

/// Actor role normalized across provider APIs.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
Expand Down Expand Up @@ -91,6 +91,9 @@ pub enum ContentBlock {
/// "reasoning.encrypted", "data": "..." }` object, replayed without modification.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
details: Vec<Value>,
/// Wire format that a trusted codec decoded for private reasoning metadata.
#[serde(skip)]
provenance: Option<FormatId>,
},
/// Image content.
Image {
Expand Down Expand Up @@ -270,8 +273,18 @@ pub struct OutputParams {
pub struct ReasoningParams {
/// Requested reasoning effort or level.
pub effort: Option<String>,
/// Provider reasoning controls without a normalized field.
pub raw: Option<Value>,
/// Raw reasoning controls keyed by the wire format that defined them.
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub raw_by_format: BTreeMap<FormatId, Value>,
}

impl ReasoningParams {
/// Returns raw controls owned by one built-in wire format without allocating a lookup key.
pub fn raw_for(&self, format: WireFormat) -> Option<&Value> {
self.raw_by_format
.iter()
.find_map(|(source, value)| (source.as_str() == format.as_str()).then_some(value))
}
}

/// Provider-specific fields that do not have first-class conversation fields.
Expand Down
3 changes: 3 additions & 0 deletions crates/protocol/src/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,7 @@ impl ResponseAccumulator {
.into_iter()
.filter(|detail| !is_reasoning_id_announcement(detail))
.collect(),
provenance: None,
});
}
if !self.text.is_empty() {
Expand Down Expand Up @@ -685,6 +686,7 @@ mod tests {
text: "think".to_string(),
signature: None,
details: Vec::new(),
provenance: None,
},
ContentBlock::Text {
text: "answer".to_string(),
Expand Down Expand Up @@ -737,6 +739,7 @@ mod tests {
text: "fallback reasoning".to_string(),
signature: None,
details: details.clone(),
provenance: None,
}],
url_citations: Vec::new(),
stop_reason: Some(StopReason::EndTurn),
Expand Down
32 changes: 29 additions & 3 deletions crates/switchyard-server/src/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ pub(crate) fn unsupported_capability(
&& (request.reasoning.effort.is_some()
|| request
.reasoning
.raw
.as_ref()
.is_some_and(|value| !value.is_null())
.raw_by_format
.values()
.any(|value| !value.is_null())
|| ["reasoning", "reasoning_effort", "thinking"]
.iter()
.any(|key| body.get(key).is_some_and(|value| !value.is_null()))
Expand Down Expand Up @@ -131,8 +131,11 @@ fn unsupported_content(

#[cfg(test)]
mod tests {
use std::collections::BTreeMap;

use super::*;
use serde_json::json;
use switchyard_protocol::ReasoningParams;
use switchyard_translation::WireFormat::{AnthropicMessages, OpenAiChat, OpenAiResponses};
use switchyard_translation::{WireFormat, decode_request, encode_request};

Expand Down Expand Up @@ -231,6 +234,29 @@ mod tests {
Ok(())
}

#[test]
fn rejects_source_qualified_reasoning_controls() {
let request = LlmRequest {
reasoning: ReasoningParams {
raw_by_format: BTreeMap::from([(
OpenAiResponses.into(),
json!({"summary": "auto"}),
)]),
..Default::default()
},
..Default::default()
};
let capabilities = ModelCapabilities {
reasoning: Some(false),
..Default::default()
};

assert_eq!(
unsupported_capability(capabilities, &request, &json!({})),
Some("reasoning")
);
}

// Decoded file-ID images and preserved computer screenshots both require vision.
#[test]
fn rejects_file_id_images_and_computer_screenshots() -> TestResult {
Expand Down
Loading
Loading