Skip to content
Open
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/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const OPENAI_OVERFLOW_PHRASES: &[&str] = &[
"please reduce the length of the input",
"exceeds the maximum allowed input length",
"exceeds the maximum allowed length",
"exceeds the available context size",
"is longer than the model's context length",
];

Expand Down
6 changes: 4 additions & 2 deletions crates/libsy-llm-client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2349,12 +2349,14 @@ mod tests {
}

#[tokio::test]
async fn context_overflow_400_is_mapped()
async fn llama_cpp_context_overflow_400_is_mapped()
-> std::result::Result<(), Box<dyn Error + Sync + Send + 'static>> {
let server = MockServer::start().await;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(400).set_body_json(json!({
"error": {"code": "context_length_exceeded", "message": "too big"}
"error": {
"message": "request (6016 tokens) exceeds the available context size (4096 tokens), try increasing it"
}
})))
.mount(&server)
.await;
Expand Down
6 changes: 3 additions & 3 deletions crates/libsy/src/algorithms/advisor_gate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ mod transcript;
mod trigger;
mod turn;

pub(crate) use turn::{GatedTurn, buffer_turn, has_tool_use};

use budget::{ReviewBudget, ScopeKey, budget_scope, stall_key};
use signals::{GateSignalProcessor, GateSignals};
use telemetry::{
Expand All @@ -59,9 +61,7 @@ use telemetry::{
};
use transcript::{VERDICT_PATTERN, Verdict, advisor_reply_text, parse_verdict, review_transcript};
use trigger::TriggerClassifier;
#[cfg(test)]
use turn::has_tool_use;
use turn::{GatedTurn, buffer_turn, reasoning_text, visible_text};
use turn::{reasoning_text, visible_text};

/// APPROVE/REDO reviewer contract sent as the advisor's system prompt.
pub const REVIEWER_SYSTEM_PROMPT: &str =
Expand Down
12 changes: 8 additions & 4 deletions crates/libsy/src/algorithms/advisor_gate/turn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use crate::{LibsyError, Result};
// ── Turn buffering and replay ───────────────────────────────────────────────

/// One fully generated executor turn held while the gate decides.
pub(super) struct GatedTurn {
pub(crate) struct GatedTurn {
/// Buffered provider events for streamed turns, preservation included, so
/// replay re-emits them verbatim (signed thinking and provider extensions
/// survive; folding to an aggregate and re-synthesizing would drop them).
Expand All @@ -29,9 +29,13 @@ pub(super) struct GatedTurn {
}

impl GatedTurn {
pub(crate) fn aggregate(&self) -> &AggLlmResponse {
&self.agg
}

/// Releases the turn to the client: streamed turns replay their buffered
/// events verbatim, buffered turns return the original aggregate.
pub(super) fn into_response(self) -> Response {
pub(crate) fn into_response(self) -> Response {
let llm_response = match self.events {
Some(events) => {
LlmResponse::Stream(Box::pin(futures::stream::iter(events.into_iter().map(Ok))))
Expand All @@ -50,7 +54,7 @@ impl GatedTurn {
/// errors and in-band error chunks — become typed client-call errors exactly
/// as [`LlmResponse::into_agg`] maps them; the client saw nothing yet, so the
/// turn fails whole.
pub(super) async fn buffer_turn(executor: &str, response: Response) -> Result<GatedTurn> {
pub(crate) async fn buffer_turn(executor: &str, response: Response) -> Result<GatedTurn> {
let metadata = response.metadata;
let upstream_headers = response.upstream_headers;
match response.llm_response {
Expand Down Expand Up @@ -103,7 +107,7 @@ pub(super) async fn buffer_turn(executor: &str, response: Response) -> Result<Ga
/// Whether the turn carries tool use on either signal: a `ToolUse` stop
/// reason, or any tool-call block (some OSS servers mislabel tool-call turns
/// as an ordinary stop, so block presence wins).
pub(super) fn has_tool_use(agg: &AggLlmResponse) -> bool {
pub(crate) fn has_tool_use(agg: &AggLlmResponse) -> bool {
agg.outputs.iter().any(|output| {
output.stop_reason == Some(StopReason::ToolUse)
|| output
Expand Down
4 changes: 4 additions & 0 deletions crates/libsy/src/algorithms/vgr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@

use switchyard_protocol::Request;

mod config;
mod decide;
mod readout;
mod runtime;
mod safety;
mod text;

#[cfg(test)]
Expand Down
90 changes: 90 additions & 0 deletions crates/libsy/src/algorithms/vgr/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

//! Operator configuration for a verification-gated route.

use std::time::Duration;

use switchyard_protocol::ModelId;

use super::safety::{BreakerConfig, KillSwitch};
use crate::{LibsyError, Result};

/// Required attestation for live local commits.
pub const ACTIVE_APPROVAL: &str = "prospective-validation-and-canary-approved";

/// Authority granted to VGR decisions.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub enum ServingMode {
/// Skip VGR and always use the capable tier.
#[default]
Off,
/// Serve decisions in an isolated evaluation.
Evaluate,
/// Compute decisions but always serve the capable tier.
Shadow,
/// Serve decisions after explicit operator approval.
Active { approval: String },
}

/// Model targets used by one VGR route.
#[derive(Clone, Debug)]
pub struct Targets {
pub local: ModelId,
pub cloud: ModelId,
/// Local verifier, defaulting to `local`.
pub judge: Option<ModelId>,
/// Optional capable-tier confirmation model.
pub cloud_judge: Option<ModelId>,
}

/// Complete runtime configuration.
#[derive(Clone, Debug)]
pub struct VgrConfig {
pub targets: Targets,
pub mode: ServingMode,
pub deadline: Duration,
pub kill_switch: Option<KillSwitch>,
pub breaker: BreakerConfig,
pub task_typing: bool,
}

impl VgrConfig {
pub fn new(local: ModelId, cloud: ModelId) -> Self {
Self {
targets: Targets {
local,
cloud,
judge: None,
cloud_judge: None,
},
mode: ServingMode::Off,
deadline: Duration::from_secs(30),
kill_switch: None,
breaker: BreakerConfig::default(),
task_typing: true,
}
}

pub(super) fn validate(&self) -> Result<()> {
if self.deadline.is_zero() || self.breaker.threshold == 0 {
return Err(LibsyError::AlgorithmError {
message: "vgr deadline and breaker threshold must be non-zero".into(),
});
}
if let ServingMode::Active { approval } = &self.mode
&& approval != ACTIVE_APPROVAL
{
return Err(LibsyError::AlgorithmError {
message: format!(
"vgr active mode requires approval attestation {ACTIVE_APPROVAL:?}"
),
});
}
Ok(())
}

pub(super) fn judge(&self) -> &ModelId {
self.targets.judge.as_ref().unwrap_or(&self.targets.local)
}
}
58 changes: 58 additions & 0 deletions crates/libsy/src/algorithms/vgr/readout.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

//! Probability-scored verifier readout.

use serde_json::{Value, json};
use switchyard_protocol::{AggLlmResponse, FormatId, Request, WireFormat};

pub(super) const MAX_OUTPUT_TOKENS: u64 = 4;

pub(super) fn request_logprobs(request: &mut Request) {
request.llm_request.reasoning.effort = Some("none".to_string());
request
.llm_request
.extensions
.fields
.insert("logprobs".into(), json!(true));
request
.llm_request
.extensions
.fields
.insert("top_logprobs".into(), json!(8));
}

pub(super) fn p_yes(response: &AggLlmResponse) -> Option<f64> {
let alternatives = response
.preservation
.responses
.get(&FormatId::from(WireFormat::OpenAiChat))?
.pointer("/choices/0/logprobs/content/0/top_logprobs")?
.as_array()?;
let mut yes = 0.0;
let mut no = 0.0;
let mut found = false;
for entry in alternatives {
let token = normalize(entry.get("token")?.as_str()?);
let probability = entry.get("logprob").and_then(Value::as_f64)?.exp();
match token.as_str() {
"yes" | "y" | "true" => {
yes += probability;
found = true;
}
"no" | "n" | "false" => {
no += probability;
found = true;
}
_ => {}
}
}
(found && yes + no > 0.0).then_some(yes / (yes + no))
}

fn normalize(token: &str) -> String {
token
.trim()
.trim_matches(['"', '\'', '.', ','])
.to_lowercase()
}
Loading
Loading