Skip to content
Merged
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
116 changes: 107 additions & 9 deletions crates/frontend/src/pages/admin_kiro_gateway.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,18 +233,50 @@ fn anthropic_routing_badge(raw: Option<&str>) -> Option<&'static str> {
}
}

const PREFLIGHT_CHANGE_COUNT_KEYS: [&str; 3] =
["tool_use_id_rewrite_count", "normalization_event_count", "tool_normalization_event_count"];

fn routing_diagnostic_preflight_change_count(raw: Option<&str>) -> Option<u64> {
let preflight = raw
.and_then(|value| serde_json::from_str::<serde_json::Value>(value).ok())
.and_then(|value| value.get("preflight").cloned())?;
if !preflight
.get("normalized")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false)
{
return None;
}
if let Some(count) = preflight
.get("change_count")
.and_then(serde_json::Value::as_u64)
{
return (count > 0).then_some(count);
}
let count = PREFLIGHT_CHANGE_COUNT_KEYS
.into_iter()
.filter_map(|key| preflight.get(key).and_then(serde_json::Value::as_u64))
.sum::<u64>();
(count > 0).then_some(count)
}

fn anthropic_routing_summary(raw: Option<&str>) -> Option<String> {
let badge = anthropic_routing_badge(raw)?;
let channel = routing_diagnostic_string(raw, "channel_name");
let probe_kind = routing_diagnostic_string(raw, "probe_kind");
Some(match (channel, probe_kind) {
(Some(channel), Some(probe_kind)) => {
format!("{badge} · channel {channel} · {probe_kind}")
},
(Some(channel), None) => format!("{badge} · channel {channel}"),
(None, Some(probe_kind)) => format!("{badge} · {probe_kind}"),
(None, None) => badge.to_string(),
})
let preflight_changes = routing_diagnostic_preflight_change_count(raw);
let mut parts = vec![badge.to_string()];
if let Some(channel) = channel {
parts.push(format!("channel {channel}"));
}
if let Some(probe_kind) = probe_kind {
parts.push(probe_kind);
}
if let Some(count) = preflight_changes {
let noun = if count == 1 { "change" } else { "changes" };
parts.push(format!("preflight {count} {noun}"));
}
Some(parts.join(" · "))
}

#[derive(Debug, Clone, PartialEq, Eq, Default)]
Expand Down Expand Up @@ -5885,7 +5917,8 @@ mod tests {
use serde_json::json;

use super::{
admin_kiro_key_total_pages, build_kiro_billable_multiplier_override_json,
admin_kiro_key_total_pages, anthropic_routing_summary,
build_kiro_billable_multiplier_override_json,
build_kiro_billable_multiplier_override_patch, build_kiro_cache_policy_override_json,
build_kiro_cache_policy_override_patch, format_compact_bytes,
format_kiro_cache_policy_summary, format_kiro_key_candidate_credit_summary,
Expand Down Expand Up @@ -5919,6 +5952,71 @@ mod tests {
);
}

#[test]
fn anthropic_routing_summary_includes_preflight_change_count() {
let diagnostics = json!({
"upstream_pool": "direct_anthropic",
"channel_name": "channel-a",
"preflight": {
"normalized": true,
"change_count": 2,
"tool_use_id_rewrite_count": 1,
"normalization_event_count": 1,
"tool_normalization_event_count": 0,
"tool_schema_keyword_count": 0
}
})
.to_string();

assert_eq!(
anthropic_routing_summary(Some(&diagnostics)),
Some("Anthropic 直连 · channel channel-a · preflight 2 changes".to_string())
);
}

#[test]
fn anthropic_routing_summary_omits_schema_observation_only_preflight() {
let diagnostics = json!({
"upstream_pool": "direct_anthropic",
"channel_name": "channel-a",
"preflight": {
"normalized": false,
"change_count": 0,
"tool_use_id_rewrite_count": 0,
"normalization_event_count": 0,
"tool_normalization_event_count": 0,
"tool_schema_keyword_count": 2
}
})
.to_string();

assert_eq!(
anthropic_routing_summary(Some(&diagnostics)),
Some("Anthropic 直连 · channel channel-a".to_string())
);
}

#[test]
fn anthropic_routing_summary_omits_legacy_schema_only_preflight() {
let diagnostics = json!({
"upstream_pool": "direct_anthropic",
"channel_name": "channel-a",
"preflight": {
"normalized": true,
"tool_use_id_rewrite_count": 0,
"normalization_event_count": 0,
"tool_normalization_event_count": 0,
"tool_schema_keyword_count": 2
}
})
.to_string();

assert_eq!(
anthropic_routing_summary(Some(&diagnostics)),
Some("Anthropic 直连 · channel channel-a".to_string())
);
}

#[test]
fn kiro_key_route_summary_uses_full_pool_text_when_group_is_empty() {
let summary = kiro_key_route_summary(
Expand Down
1 change: 1 addition & 0 deletions crates/llm-access-kiro/src/anthropic/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Anthropic-compatible Kiro request and stream conversion.

pub mod converter;
pub mod preflight;
pub mod protected_content;
pub mod stream;
pub mod types;
Expand Down
108 changes: 108 additions & 0 deletions crates/llm-access-kiro/src/anthropic/preflight.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//! Shared preflight for Anthropic-compatible requests entering the Kiro
//! surface.
//!
//! This module intentionally stops before Kiro wire conversion. It provides the
//! normalized Anthropic request shape that both Kiro-native dispatch and direct
//! Anthropic upstream dispatch can reuse without duplicating cleanup rules.

use super::{
converter::{
normalize_request, ConversionError, NormalizationEvent, ToolNormalizationEvent,
ToolUseIdRewrite, ToolValidationSummary,
},
types::MessagesRequest,
};

#[derive(Debug)]
pub struct PreprocessedMessagesRequest {
pub request: MessagesRequest,
pub tool_use_id_rewrites: Vec<ToolUseIdRewrite>,
pub normalization_events: Vec<NormalizationEvent>,
pub tool_normalization_events: Vec<ToolNormalizationEvent>,
pub tool_validation_summary: ToolValidationSummary,
}

pub fn preprocess_messages_request(
request: &MessagesRequest,
) -> Result<PreprocessedMessagesRequest, ConversionError> {
let normalized = normalize_request(request)?;
Ok(PreprocessedMessagesRequest {
request: normalized.request,
tool_use_id_rewrites: normalized.tool_use_id_rewrites,
normalization_events: normalized.normalization_events,
tool_normalization_events: normalized.tool_normalization_events,
tool_validation_summary: normalized.tool_validation_summary,
})
}

#[cfg(test)]
mod tests {
use crate::anthropic::{
preflight::preprocess_messages_request,
types::{Message, MessagesRequest},
};

fn request_with_invalid_history_tool_use_id() -> MessagesRequest {
MessagesRequest {
model: "claude-opus-4-8".to_string(),
_max_tokens: 128,
messages: vec![
Message {
role: "user".to_string(),
content: serde_json::json!("Run the tool"),
},
Message {
role: "assistant".to_string(),
content: serde_json::json!([
{
"type": "tool_use",
"id": "toolu.01:bad",
"name": "read_file",
"input": {"path": "/tmp/test.txt"}
}
]),
},
Message {
role: "user".to_string(),
content: serde_json::json!([
{
"type": "tool_result",
"tool_use_id": "toolu.01:bad",
"content": "file content"
}
]),
},
],
stream: false,
system: None,
tools: None,
_tool_choice: None,
thinking: None,
output_config: None,
metadata: None,
}
}

#[test]
fn preflight_normalizes_tool_use_ids_for_shared_kiro_anthropic_surface() {
let preflight = preprocess_messages_request(&request_with_invalid_history_tool_use_id())
.expect("preflight should normalize reusable Kiro Anthropic request shape");

assert_eq!(preflight.tool_use_id_rewrites.len(), 1);
let rewrite = &preflight.tool_use_id_rewrites[0];
assert_eq!(rewrite.original_tool_use_id, "toolu.01:bad");
assert!(rewrite
.rewritten_tool_use_id
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-'));

let assistant_id = preflight.request.messages[1].content[0]["id"]
.as_str()
.expect("assistant tool_use id should remain a string");
let result_id = preflight.request.messages[2].content[0]["tool_use_id"]
.as_str()
.expect("tool_result id should remain a string");
assert_eq!(assistant_id, rewrite.rewritten_tool_use_id);
assert_eq!(result_id, rewrite.rewritten_tool_use_id);
}
}
2 changes: 2 additions & 0 deletions crates/llm-access/src/provider.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
//! Provider-facing HTTP entrypoints for `llm-access`.

mod anthropic_upstream_diagnostics;
mod anthropic_upstream_dispatch;
mod anthropic_upstream_payload;
mod cctest;
mod client;
mod codex_auth;
Expand Down
104 changes: 104 additions & 0 deletions crates/llm-access/src/provider/anthropic_upstream_diagnostics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use llm_access_kiro::anthropic::preflight::PreprocessedMessagesRequest;
use serde_json::json;

const PREFLIGHT_DETAIL_LIMIT: usize = 20;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct DirectAnthropicPreflightStats {
pub(super) change_count: usize,
pub(super) tool_use_id_rewrite_count: usize,
pub(super) normalization_event_count: usize,
pub(super) tool_normalization_event_count: usize,
pub(super) tool_schema_keyword_count: usize,
}

impl DirectAnthropicPreflightStats {
pub(super) fn normalized(&self) -> bool {
self.change_count > 0
}
}

pub(super) fn direct_anthropic_preflight_stats(
preflight: &PreprocessedMessagesRequest,
) -> DirectAnthropicPreflightStats {
let tool_schema_keyword_count = preflight
.tool_validation_summary
.schema_keyword_counts
.values()
.sum();
let change_count = [
preflight.tool_use_id_rewrites.len(),
preflight.normalization_events.len(),
preflight.tool_normalization_events.len(),
]
.into_iter()
.sum();

DirectAnthropicPreflightStats {
change_count,
tool_use_id_rewrite_count: preflight.tool_use_id_rewrites.len(),
normalization_event_count: preflight.normalization_events.len(),
tool_normalization_event_count: preflight.tool_normalization_events.len(),
tool_schema_keyword_count,
}
}

pub(super) fn build_direct_anthropic_routing_diagnostics(
channel_name: &str,
pool_mode: &str,
preflight: &PreprocessedMessagesRequest,
) -> String {
let stats = direct_anthropic_preflight_stats(preflight);
json!({
"upstream_pool": "direct_anthropic",
"channel_name": channel_name,
"pool_mode": pool_mode,
"preflight": {
"normalized": stats.normalized(),
"change_count": stats.change_count,
"tool_use_id_rewrite_count": stats.tool_use_id_rewrite_count,
"normalization_event_count": stats.normalization_event_count,
"tool_normalization_event_count": stats.tool_normalization_event_count,
"tool_schema_keyword_count": stats.tool_schema_keyword_count,
"tool_validation_summary": {
"normalized_tool_description_count": preflight.tool_validation_summary.normalized_tool_description_count,
"empty_tool_name_count": preflight.tool_validation_summary.empty_tool_name_count,
"schema_keyword_counts": &preflight.tool_validation_summary.schema_keyword_counts,
},
"tool_use_id_rewrites": preflight.tool_use_id_rewrites.iter()
.take(PREFLIGHT_DETAIL_LIMIT)
.map(|rewrite| json!({
"original_tool_use_id": &rewrite.original_tool_use_id,
"rewritten_tool_use_id": &rewrite.rewritten_tool_use_id,
"assistant_message_index": rewrite.assistant_message_index,
"content_block_index": rewrite.content_block_index,
"rewritten_tool_result_count": rewrite.rewritten_tool_result_count,
}))
.collect::<Vec<_>>(),
"tool_use_id_rewrites_truncated": preflight.tool_use_id_rewrites.len() > PREFLIGHT_DETAIL_LIMIT,
"normalization_events": preflight.normalization_events.iter()
.take(PREFLIGHT_DETAIL_LIMIT)
.map(|event| json!({
"message_index": event.message_index,
"role": &event.role,
"content_block_index": event.content_block_index,
"block_type": event.block_type.as_ref(),
"action": event.action,
"reason": event.reason,
}))
.collect::<Vec<_>>(),
"normalization_events_truncated": preflight.normalization_events.len() > PREFLIGHT_DETAIL_LIMIT,
"tool_normalization_events": preflight.tool_normalization_events.iter()
.take(PREFLIGHT_DETAIL_LIMIT)
.map(|event| json!({
"tool_index": event.tool_index,
"tool_name": &event.tool_name,
"action": event.action,
"reason": event.reason,
}))
.collect::<Vec<_>>(),
"tool_normalization_events_truncated": preflight.tool_normalization_events.len() > PREFLIGHT_DETAIL_LIMIT,
}
})
.to_string()
}
Loading
Loading