use super::api::{LlmCallOptions, ThinkingConfig};
use super::capabilities::WireDialect;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DispatchProvenance {
pub provider: Option<String>,
pub model: Option<String>,
pub wire_format: Option<String>,
pub thinking: Option<String>,
pub tool_format: Option<String>,
}
impl DispatchProvenance {
pub const INHERITED_FROM_PRIMARY: &'static str = "inherited_from_primary";
pub const OPERATOR_PIN: &'static str = "operator_pin";
pub const ESCALATION_OVERRIDE: &'static str = "escalation_override";
pub const PIPELINE_INPUT: &'static str = "pipeline_input";
pub const CATALOG_DEFAULT: &'static str = "catalog_default";
pub fn from_vm_value(value: &crate::value::VmValue) -> Option<Self> {
let dict = value.as_dict()?;
let field = |key: &str| -> Option<String> {
dict.get(key)
.map(|v| v.as_str_cow().into_owned())
.filter(|s| !s.is_empty())
};
Some(Self {
provider: field("provider"),
model: field("model"),
wire_format: field("wire_format"),
thinking: field("thinking"),
tool_format: field("tool_format"),
})
}
fn origin_or_unknown(value: &Option<String>) -> &str {
value.as_deref().unwrap_or("unknown")
}
fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"provider": Self::origin_or_unknown(&self.provider),
"model": Self::origin_or_unknown(&self.model),
"wire_format": Self::origin_or_unknown(&self.wire_format),
"thinking": Self::origin_or_unknown(&self.thinking),
"tool_format": Self::origin_or_unknown(&self.tool_format),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum DispatchOutcome {
Served {
completion_tokens: i64,
content_len: usize,
},
EmptyCompletionTransientRecovered {
completion_tokens: i64,
content_len: usize,
empty_retries: usize,
},
EmptyCompletionTerminal { completion_tokens: i64 },
UsageLimit,
ProviderError { class: String },
}
impl DispatchOutcome {
pub(crate) fn from_result(result: &super::api::LlmResult, empty_retries: usize) -> Self {
let content_len = result.text.len();
let committed_nothing = result.text.is_empty()
&& result.tool_calls.is_empty()
&& result
.thinking
.as_deref()
.map(str::is_empty)
.unwrap_or(true);
if committed_nothing && result.output_tokens > 0 {
return DispatchOutcome::EmptyCompletionTerminal {
completion_tokens: result.output_tokens,
};
}
if empty_retries > 0 {
return DispatchOutcome::EmptyCompletionTransientRecovered {
completion_tokens: result.output_tokens,
content_len,
empty_retries,
};
}
DispatchOutcome::Served {
completion_tokens: result.output_tokens,
content_len,
}
}
pub(crate) fn from_error_message(message: &str) -> Self {
let lower = message.to_lowercase();
if lower.contains("completion_tokens=")
&& (lower.contains("delivered no content")
|| (lower.contains("no dispatchable tool call or answer")
&& lower.contains("upstream contract violation")))
{
return DispatchOutcome::EmptyCompletionTerminal {
completion_tokens: 0,
};
}
if lower.contains("rate limit")
|| lower.contains("quota")
|| lower.contains("usage limit")
|| lower.contains("429")
{
return DispatchOutcome::UsageLimit;
}
DispatchOutcome::ProviderError {
class: provider_error_class(&lower),
}
}
pub(crate) fn label(&self) -> &'static str {
match self {
DispatchOutcome::Served { .. } => "served",
DispatchOutcome::EmptyCompletionTransientRecovered { .. } => {
"empty_completion_transient_recovered"
}
DispatchOutcome::EmptyCompletionTerminal { .. } => "empty_completion_terminal",
DispatchOutcome::UsageLimit => "usage_limit",
DispatchOutcome::ProviderError { .. } => "provider_error",
}
}
fn to_json(&self) -> serde_json::Value {
match self {
DispatchOutcome::Served {
completion_tokens,
content_len,
} => serde_json::json!({
"kind": "served",
"completion_tokens": completion_tokens,
"content_len": content_len,
}),
DispatchOutcome::EmptyCompletionTransientRecovered {
completion_tokens,
content_len,
empty_retries,
} => serde_json::json!({
"kind": "empty_completion_transient_recovered",
"completion_tokens": completion_tokens,
"content_len": content_len,
"empty_retries": empty_retries,
}),
DispatchOutcome::EmptyCompletionTerminal { completion_tokens } => serde_json::json!({
"kind": "empty_completion_terminal",
"completion_tokens": completion_tokens,
"content_len": 0,
}),
DispatchOutcome::UsageLimit => serde_json::json!({
"kind": "usage_limit",
}),
DispatchOutcome::ProviderError { class } => serde_json::json!({
"kind": "provider_error",
"class": class,
}),
}
}
}
fn provider_error_class(lower: &str) -> String {
for (needle, class) in [
("api error", "api_error"),
("timed out", "timeout"),
("timeout", "timeout"),
("connection", "connection"),
("missing content array", "malformed_response"),
("authentication", "auth"),
("unauthorized", "auth"),
("401", "auth"),
("not found", "not_found"),
("404", "not_found"),
("overloaded", "overloaded"),
("500", "server_error"),
("502", "server_error"),
("503", "server_error"),
] {
if lower.contains(needle) {
return class.to_string();
}
}
"unknown".to_string()
}
pub fn wire_format_for(provider: &str, model: &str) -> &'static str {
match super::capabilities::lookup(provider, model).message_wire_format {
WireDialect::Anthropic => "anthropic_native",
WireDialect::OpenAiCompat => "openai_compat",
WireDialect::Ollama => "ollama",
WireDialect::Gemini => "gemini",
}
}
fn base_url_host(provider: &str) -> String {
let base_url = super::helpers::ResolvedProvider::resolve(provider).base_url;
base_url
.split("://")
.nth(1)
.and_then(|rest| rest.split('/').next())
.map(str::to_string)
.unwrap_or(base_url)
}
fn thinking_json(thinking: &ThinkingConfig) -> serde_json::Value {
match thinking {
ThinkingConfig::Disabled => serde_json::json!({"mode": "off", "enabled": false}),
ThinkingConfig::Enabled { budget_tokens } => serde_json::json!({
"mode": "enabled",
"enabled": true,
"budget_tokens": budget_tokens,
}),
ThinkingConfig::Adaptive => serde_json::json!({"mode": "adaptive", "enabled": true}),
ThinkingConfig::Effort { level } => serde_json::json!({
"mode": "effort",
"level": level.as_str(),
"enabled": !thinking.is_disabled(),
}),
}
}
pub(crate) fn build_record(
iteration: usize,
call_id: &str,
span_id: Option<u64>,
timestamp: String,
opts: &LlmCallOptions,
effective_tool_format: &str,
outcome: &DispatchOutcome,
) -> serde_json::Value {
let provenance = opts.dispatch_provenance.clone().unwrap_or_default();
serde_json::json!({
"type": "resolved_dispatch",
"iteration": iteration,
"call_id": call_id,
"span_id": span_id,
"timestamp": timestamp,
"provider": opts.provider,
"model": opts.model,
"wire_format": wire_format_for(&opts.provider, &opts.model),
"thinking": thinking_json(&opts.thinking),
"tool_format": effective_tool_format,
"base_url_host": base_url_host(&opts.provider),
"provenance": provenance.to_json(),
"outcome": outcome.to_json(),
"outcome_kind": outcome.label(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wire_format_native_for_anthropic_claude() {
assert_eq!(
wire_format_for("anthropic", "claude-sonnet-4-6"),
"anthropic_native"
);
}
#[test]
fn wire_format_compat_for_openai_style() {
assert_eq!(wire_format_for("openai", "gpt-4o"), "openai_compat");
}
#[test]
fn wire_format_preserves_native_non_openai_dialects() {
assert_eq!(wire_format_for("gemini", "gemini-2.5-pro"), "gemini");
assert_eq!(wire_format_for("ollama", "llama3.2"), "ollama");
}
#[test]
fn outcome_empty_completion_terminal_from_billed_no_content() {
let msg = "anthropic-native model anthropic:claude-sonnet-4-6 reported \
completion_tokens=8 but delivered no content, reasoning, or tool calls";
assert!(matches!(
DispatchOutcome::from_error_message(msg),
DispatchOutcome::EmptyCompletionTerminal {
completion_tokens: 0
}
));
}
#[test]
fn transient_recovered_is_not_served_empty() {
let recovered = DispatchOutcome::EmptyCompletionTransientRecovered {
completion_tokens: 487,
content_len: 1666,
empty_retries: 3,
};
assert_eq!(recovered.label(), "empty_completion_transient_recovered");
assert!(!matches!(
recovered,
DispatchOutcome::EmptyCompletionTerminal { .. }
));
}
#[test]
fn outcome_usage_limit_from_quota() {
assert_eq!(
DispatchOutcome::from_error_message("provider returned 429 rate limit exceeded"),
DispatchOutcome::UsageLimit
);
}
#[test]
fn outcome_provider_error_class() {
match DispatchOutcome::from_error_message("anthropic API error: overloaded") {
DispatchOutcome::ProviderError { class } => assert_eq!(class, "api_error"),
other => panic!("expected provider_error, got {other:?}"),
}
}
#[test]
fn provenance_inherited_marker_is_stable() {
assert_eq!(
DispatchProvenance::INHERITED_FROM_PRIMARY,
"inherited_from_primary"
);
let prov = DispatchProvenance {
provider: Some(DispatchProvenance::INHERITED_FROM_PRIMARY.to_string()),
..Default::default()
};
let json = prov.to_json();
assert_eq!(json["provider"], "inherited_from_primary");
assert_eq!(json["model"], "unknown");
}
}