docs(comments): rewrite comments across all crates to the guidelines

Sweep every first-party crate source (1956 .rs files) to the project comment
guidelines: delete redundant restatements, decorative banners, change
narration, and end-of-line comments; keep and tighten the crucial ones
(invariants, bug rationale, SAFETY blocks, ported-source attribution).

No functional code changed. Every edit is proven comment-only against the
prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a
separate doctest-fence check. Where removing a comment made rustfmt or clippy
want to re-lay-out adjacent code, the minimal triggering comment is restored so
code tokens stay byte-identical.

Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy
--workspace --all-targets (0 warnings).

Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for
these guidelines (flags banners, end-of-line comments, change narration, and
commented-out code).
This commit is contained in:
2026-07-23 16:55:39 -04:00
parent ff0fb56c67
commit a02b555e66
1458 changed files with 10729 additions and 21750 deletions
@@ -18,9 +18,7 @@ use crate::types::{
Usage,
};
// ============================================================================
// Core Conversation Types
// ============================================================================
/// A single item in a conversation - the unified internal representation.
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -69,7 +67,7 @@ pub struct SystemItem {
/// without parsing message text.
///
/// Serialized as a lowercase string (e.g. `"auto_continue"`).
/// Unknown variants (from future clients or removed historical tags such as
/// Unknown variants (from future clients or retired historical tags such as
/// `"doom_loop_warning"`) deserialize as [`SyntheticReason::Unknown`]
/// so old clients can still read sessions written by newer versions.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -265,7 +263,6 @@ pub struct AssistantItem {
pub struct ToolResultItem {
/// ID of the tool call this is responding to
pub tool_call_id: String,
/// The result content
pub content: Arc<str>,
/// Inline images associated with this tool result (e.g. from `read_file`
/// on an image/PDF). When non-empty, the API conversion layers embed
@@ -351,9 +348,7 @@ pub enum BackendToolKind {
CodeInterpreter(rs::CodeInterpreterToolCall),
}
// ============================================================================
// Content Parts
// ============================================================================
/// A part of message content - text, image, etc.
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -365,9 +360,7 @@ pub enum ContentPart {
Image { url: Arc<str> },
}
// ============================================================================
// Reasoning Content
// ============================================================================
/// Reasoning/thinking content from the model.
/// Structured to support both plain text (chat completions) and
@@ -449,9 +442,7 @@ impl ReasoningContent {
}
}
// ============================================================================
// Tool Definitions and Calls
// ============================================================================
/// A tool call made by the assistant that the client must execute locally.
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -512,9 +503,7 @@ impl From<ToolDefinition> for ToolSpec {
}
}
// ============================================================================
// Conversation Request
// ============================================================================
/// A complete conversation request that can be sent to either API.
#[derive(Debug, Clone, Default)]
@@ -601,9 +590,7 @@ pub enum ConversationToolChoice {
Function(String),
}
// ============================================================================
// Conversation Response
// ============================================================================
/// Why the model stopped generating.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
@@ -776,7 +763,7 @@ impl ConversationResponse {
/// Reasoning siblings that precede the trailing `Assistant`, in order.
/// Used by streaming consumers and the empty-response retry logic that
/// previously inspected `AssistantItem.reasoning`.
/// once inspected `AssistantItem.reasoning`.
pub fn reasoning_items(&self) -> impl Iterator<Item = &rs::ReasoningItem> {
self.items.iter().filter_map(|item| match item {
ConversationItem::Reasoning(r) => Some(r),
@@ -848,9 +835,7 @@ impl ConversationResponse {
}
}
// ============================================================================
// ConversationItem Constructors
// ============================================================================
impl ConversationItem {
/// Create a system message
@@ -1166,10 +1151,8 @@ impl ConversationItem {
}
}
// ---------------------------------------------------------------------------
// Shared-compaction L1 bridge: `CompactionItem` / `CompactionItemFactory`
// for `ConversationItem`
// ---------------------------------------------------------------------------
//
// Part of the Kigi Compaction unification. Lets the shared, transport-agnostic
// engine in `crates/common/kigi-compaction` operate over kigi's
@@ -1549,9 +1532,7 @@ impl ConversationItem {
}
}
// ============================================================================
// Conversion: ConversationItem <-> ChatRequestMessage
// ============================================================================
impl From<ChatRequestMessage> for ConversationItem {
fn from(msg: ChatRequestMessage) -> Self {
@@ -1686,7 +1667,7 @@ fn sanitize_tool_arguments(id: &str, name: &str, arguments: Arc<str>) -> Arc<str
/// Convert a single non-`Reasoning` [`ConversationItem`] into the
/// chat-completions wire format.
///
/// `Reasoning` is intentionally unsupported: it has no single-item Chat
/// `Reasoning` is deliberately unsupported: it has no single-item Chat
/// Completions equivalent (the wire format only carries `reasoning_content`
/// *on the following assistant message*, which a single item can't see).
/// Callers that need reasoning folded in must either use
@@ -1821,7 +1802,7 @@ pub fn conversation_item_to_chat_message(item: ConversationItem) -> ChatRequestM
/// intervening user turn) are dropped. This is the canonical
/// `ConversationItem` → `ChatRequestMessage` conversion for the
/// chat-completions backend; it attaches reasoning to the right assistant
/// turn (there is intentionally no public single-item conversion, since a
/// turn (there is deliberately no public single-item conversion, since a
/// lone `Reasoning` item has no chat-completions equivalent).
pub fn conversation_to_chat_messages(items: Vec<ConversationItem>) -> Vec<ChatRequestMessage> {
let mut out: Vec<ChatRequestMessage> = Vec::with_capacity(items.len());
@@ -1912,9 +1893,7 @@ pub fn conversation_to_chat_messages(items: Vec<ConversationItem>) -> Vec<ChatRe
out
}
// ============================================================================
// Conversion: ChatResponseMessage -> ConversationItem
// ============================================================================
impl From<ChatResponseMessage> for ConversationItem {
fn from(msg: ChatResponseMessage) -> Self {
@@ -1950,9 +1929,7 @@ impl From<ChatResponseMessage> for ConversationItem {
}
}
// ============================================================================
// Conversion: rs::Response (Responses API) -> ConversationItem
// ============================================================================
/// Convert a Responses API `Response` into a flat ordered list of
/// `ConversationItem`s, mirroring the shape of `response.output`.
@@ -2066,9 +2043,7 @@ pub fn response_to_conversation_items(response: rs::Response) -> Vec<Conversatio
items
}
// ============================================================================
// Conversion: ConversationRequest -> ChatCompletionRequest
// ============================================================================
const STRUCTURED_OUTPUT_SCHEMA_NAME: &str = "structured_output";
@@ -2139,9 +2114,7 @@ impl From<ConversationRequest> for ChatCompletionRequest {
}
}
// ============================================================================
// Conversion: ConversationRequest -> CreateResponse (Responses API)
// ============================================================================
impl From<&ConversationRequest> for rs::CreateResponse {
fn from(req: &ConversationRequest) -> Self {
@@ -2562,9 +2535,7 @@ pub fn extra_raw_tools(hosted_tools: &[HostedTool]) -> Vec<serde_json::Value> {
raw
}
// ============================================================================
// ConversationRequest Builder
// ============================================================================
impl ConversationRequest {
/// Create a new empty conversation request
@@ -2585,31 +2556,26 @@ impl ConversationRequest {
self.items.push(item);
}
/// Set the model
pub fn with_model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
/// Set tools
pub fn with_tools(mut self, tools: Vec<ToolSpec>) -> Self {
self.tools = tools;
self
}
/// Set tool choice
pub fn with_tool_choice(mut self, choice: ConversationToolChoice) -> Self {
self.tool_choice = Some(choice);
self
}
/// Set temperature
pub fn with_temperature(mut self, temperature: f32) -> Self {
self.temperature = Some(temperature);
self
}
/// Set max output tokens
pub fn with_max_output_tokens(mut self, max_tokens: u32) -> Self {
self.max_output_tokens = Some(max_tokens);
self
@@ -2866,9 +2832,7 @@ pub fn transform_conversation_cwd(
}
}
// ============================================================================
// Conversation Repair
// ============================================================================
/// Why a tool call ended up dangling — controls the synthetic-result wording.
///
@@ -2878,7 +2842,7 @@ pub fn transform_conversation_cwd(
/// failure as terminal.
///
/// A `PostProcessingFailed` variant existed in an earlier revision for
/// a mid-turn tool post-processing error path. It is now intentionally
/// a mid-turn tool post-processing error path. It is deliberately
/// absent because that path no longer returns `Err` between
/// `tool_completed` and `push_tool_result` — every error mode is
/// degraded inline. The variant would have shipped without any
@@ -3101,9 +3065,7 @@ pub fn dedup_duplicate_tool_results(conversation: &mut Vec<ConversationItem>) ->
total_removed
}
// ============================================================================
// Anthropic Messages API Conversion
// ============================================================================
/// Convert a ConversationRequest to Anthropic MessagesRequest.
/// Normalize a tool-call id to the `[A-Za-z0-9_-]+` charset both Anthropic
@@ -3138,7 +3100,7 @@ const ANTHROPIC_IMAGE_MEDIA_TYPES: [&str; 4] =
/// `(media_type, data)`.
///
/// `None` for anything Anthropic would reject — non-base64 data URIs
/// (previously leaked as `ImageSource::Url` carrying a `data:` payload:
/// (which once leaked as `ImageSource::Url` carrying a `data:` payload:
/// url sources must be http(s) → 400), media types outside the raster
/// whitelist (`image/svg+xml` → 400), and param-carrying headers
/// (`data:image/webp;name=x;base64,…` yields media type
@@ -3399,7 +3361,7 @@ pub fn build_messages_request(req: &ConversationRequest) -> crate::messages::Mes
ConversationToolChoice::Auto => ToolChoiceParam::Auto,
ConversationToolChoice::Required => ToolChoiceParam::Any,
ConversationToolChoice::Function(name) => ToolChoiceParam::Tool { name: name.clone() },
ConversationToolChoice::None => ToolChoiceParam::Auto, // default
ConversationToolChoice::None => ToolChoiceParam::Auto,
});
let effort = req
@@ -3442,7 +3404,8 @@ pub fn build_messages_request(req: &ConversationRequest) -> crate::messages::Mes
temperature: req.temperature,
top_p: req.top_p,
top_k: None,
stream: None, // Set by caller
// Set by caller
stream: None,
stop_sequences: None,
thinking,
output_config,
@@ -3551,7 +3514,8 @@ impl From<crate::messages::MessagesResponse> for ConversationItem {
}
// Thinking dropped — see doc comment above.
ContentBlock::Thinking { .. } => {}
_ => {} // Image, ToolResult not expected in assistant responses
// Image, ToolResult not expected in assistant responses
_ => {}
}
}
@@ -3565,9 +3529,7 @@ impl From<crate::messages::MessagesResponse> for ConversationItem {
}
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod compaction_item_bridge_tests {
@@ -3764,7 +3726,7 @@ mod tests {
let back: ConversationItem = chat_msg.into();
assert_eq!(back.text_content(), "Hello!");
// Assistant message (reasoning is now a sibling, not a field;
// Assistant message (reasoning is a sibling, not a field;
// single-item conversion produces None for reasoning_content. The
// `conversation_to_chat_messages` helper is what carries reasoning
// through; tested separately).
@@ -4224,9 +4186,7 @@ mod tests {
assert_eq!(b.reasoning_effort, Some(crate::ReasoningEffort::Xhigh));
}
// ============================================================================
// Tool Calls Roundtrip Tests
// ============================================================================
#[test]
fn test_tool_calls_roundtrip_to_chat_request() {
@@ -4325,9 +4285,7 @@ mod tests {
assert_eq!(chat_msg.model_id, Some("kigi-3".to_string()));
}
// ============================================================================
// Responses API Tool Conversion Tests
// ============================================================================
#[test]
fn test_tool_calls_to_responses_api() {
@@ -4444,9 +4402,7 @@ mod tests {
assert_eq!(fco_items[1].call_id, "call_2");
}
// ============================================================================
// Encrypted Reasoning Tests
// ============================================================================
#[test]
fn test_reasoning_content_from_text() {
@@ -4679,7 +4635,8 @@ mod tests {
output: vec![
rs::OutputItem::Reasoning(rs::ReasoningItem {
id: "reasoning_only_enc".to_string(),
summary: vec![], // Empty summary
// Empty summary
summary: vec![],
content: None,
encrypted_content: Some("enc_only_encrypted_no_visible_summary".to_string()),
status: Some(rs::OutputStatus::Completed),
@@ -4753,7 +4710,7 @@ mod tests {
#[test]
fn test_conversation_item_with_sibling_reasoning_serialization() {
// Reasoning is now a sibling variant — round-trip both items
// Reasoning is a sibling variant — round-trip both items
// through serde and confirm they survive.
let reasoning_item = ConversationItem::Reasoning(rs::ReasoningItem {
id: "reasoning_1".to_string(),
@@ -4780,7 +4737,7 @@ mod tests {
}
/// Anthropic image sources: base64 only for whitelisted raster types;
/// url sources http(s) only. Previously a non-base64 `data:` URI rode
/// url sources http(s) only. Historically a non-base64 `data:` URI rode
/// as `ImageSource::Url` (400), `image/svg+xml` passed the media type
/// through (400), and a param-carrying header produced
/// `"image/webp;name=x"` (400). Rejected images degrade to a SHORT
@@ -5090,7 +5047,7 @@ mod tests {
let req = ConversationRequest::from_items(vec![
ConversationItem::system("You are helpful"),
ConversationItem::user("What is 2+2?"),
// Previous reasoning + assistant: reasoning is now a sibling.
// Previous reasoning + assistant: reasoning is a sibling.
ConversationItem::Reasoning(rs::ReasoningItem {
id: "r1".to_string(),
summary: vec![rs::SummaryPart::SummaryText(rs::SummaryTextContent {
@@ -5207,7 +5164,8 @@ mod tests {
// Test that when there's no reasoning, no reasoning item is added
let req = ConversationRequest::from_items(vec![
ConversationItem::user("Hello"),
ConversationItem::assistant("Hi!"), // No reasoning
// No reasoning
ConversationItem::assistant("Hi!"),
]);
let responses_req: rs::CreateResponse = (&req).into();
@@ -5223,9 +5181,7 @@ mod tests {
assert!(reasoning_items.is_empty(), "Should have no reasoning items");
}
// ============================================================================
// ConversationRequest with Tools Tests
// ============================================================================
#[test]
fn test_conversation_request_with_tools_to_chat_completion() {
@@ -5312,9 +5268,7 @@ mod tests {
assert_eq!(def.function.description, Some("Does something".to_string()));
}
// ============================================================================
// ConversationToolChoice Tests
// ============================================================================
fn make_test_tool() -> ToolSpec {
ToolSpec {
@@ -5417,9 +5371,7 @@ mod tests {
assert!(chat_req.tools.is_none());
}
// ============================================================================
// Edge Cases Tests
// ============================================================================
#[test]
fn test_empty_content() {
@@ -5482,7 +5434,8 @@ mod tests {
}];
let user = ConversationItem::user_with_parts(parts);
assert_eq!(user.text_content(), ""); // No text content
// No text content
assert_eq!(user.text_content(), "");
}
#[test]
@@ -6488,18 +6441,23 @@ mod tests {
// Truncating at 4 would land inside the second char — must walk back to 3.
let s = "路径";
assert_eq!(s.len(), 6);
assert_eq!(truncate_bytes(s, 4), ""); // only 3 bytes fit
assert_eq!(truncate_bytes(s, 3), ""); // exact boundary
assert_eq!(truncate_bytes(s, 6), s); // full string
assert_eq!(truncate_bytes(s, 100), s); // larger than string
assert_eq!(truncate_bytes(s, 0), ""); // zero
// only 3 bytes fit
assert_eq!(truncate_bytes(s, 4), "");
// exact boundary
assert_eq!(truncate_bytes(s, 3), "");
assert_eq!(truncate_bytes(s, 6), s);
// larger than string
assert_eq!(truncate_bytes(s, 100), s);
assert_eq!(truncate_bytes(s, 0), "");
// Emoji (4-byte): truncating at 5 must back up to 4.
let e = "🎉!";
assert_eq!(e.len(), 5); // 4 + 1
// 4 + 1
assert_eq!(e.len(), 5);
assert_eq!(truncate_bytes(e, 5), "🎉!");
assert_eq!(truncate_bytes(e, 4), "🎉");
assert_eq!(truncate_bytes(e, 3), ""); // 3 < 4, walks back to 0
// 3 < 4, walks back to 0
assert_eq!(truncate_bytes(e, 3), "");
}
/// sanitize_tool_arguments must not panic when arguments contain non-ASCII
@@ -6508,7 +6466,8 @@ mod tests {
fn test_sanitize_non_ascii_args_preview_does_not_panic() {
// Build a string where the 200-byte boundary lands inside a CJK char.
// Each '文' is 3 bytes → 67 × 3 = 201 bytes; byte 200 is inside the 67th char.
let filler = "".repeat(70); // > 200 bytes
// > 200 bytes
let filler = "".repeat(70);
let bad_args = format!("{{\"old_string\": \"{filler}\"}}");
// The outer JSON is valid but contains non-ASCII; force the warning path
// by making the JSON invalid.
@@ -6542,26 +6501,25 @@ mod tests {
#[test]
fn test_large_tool_result() {
let large_output = "x".repeat(100_000); // 100KB of output
// 100KB of output
let large_output = "x".repeat(100_000);
let tool_result = ConversationItem::tool_result("call_1", &large_output);
let chat_msg = conversation_item_to_chat_message(tool_result);
assert_eq!(chat_msg.text_content().len(), 100_000);
}
// ============================================================================
// conversation_truncate_for_prompt Tests
// ============================================================================
#[test]
fn test_truncate_for_prompt_basic() {
let conversation = vec![
ConversationItem::system("System"),
ConversationItem::user("User 1"), // prompt 0
ConversationItem::user("User 1"),
ConversationItem::assistant("Asst 1"),
ConversationItem::user("User 2"), // prompt 1
ConversationItem::user("User 2"),
ConversationItem::assistant("Asst 2"),
ConversationItem::user("User 3"), // prompt 2
ConversationItem::user("User 3"),
];
// Keep up to and including prompt 0 (first user message)
@@ -6578,7 +6536,7 @@ mod tests {
fn test_truncate_for_prompt_with_tool_calls() {
let conversation = vec![
ConversationItem::system("System"),
ConversationItem::user("User 1"), // prompt 0
ConversationItem::user("User 1"),
ConversationItem::assistant_tool_calls(vec![ToolCall {
id: "call_1".into(),
name: "bash".to_string(),
@@ -6586,7 +6544,7 @@ mod tests {
}]),
ConversationItem::tool_result("call_1", "result"),
ConversationItem::assistant("Done"),
ConversationItem::user("User 2"), // prompt 1
ConversationItem::user("User 2"),
];
// Keep up to prompt 0
@@ -6689,7 +6647,7 @@ mod tests {
ConversationItem::user("<user_info>preamble</user_info>"),
ConversationItem::user("P0"),
ConversationItem::assistant("A0"),
ConversationItem::task_completed("Background task abc completed"), // turn 1
ConversationItem::task_completed("Background task abc completed"),
ConversationItem::assistant("A1"),
ConversationItem::user("P2"),
ConversationItem::assistant("A2"),
@@ -6711,10 +6669,12 @@ mod tests {
ConversationItem::system("System"),
ConversationItem::user("<user_info>preamble</user_info>"),
ConversationItem::user("P0"),
ConversationItem::interjection("also do this"), // mid-turn
// mid-turn
ConversationItem::interjection("also do this"),
ConversationItem::assistant("A0"),
ConversationItem::scheduler_fired("loop fired"), // turn 1
ConversationItem::system_reminder("reminder"), // mid-turn
ConversationItem::scheduler_fired("loop fired"),
// mid-turn
ConversationItem::system_reminder("reminder"),
ConversationItem::assistant("A1"),
ConversationItem::user("P2"),
];
@@ -6854,9 +6814,7 @@ mod tests {
}
}
// ============================================================================
// transform_conversation_cwd Tests
// ============================================================================
#[test]
fn test_transform_cwd_in_system_message() {
@@ -6957,9 +6915,7 @@ mod tests {
}
}
// ============================================================================
// transform_conversation_cwd Tool Call & Edge Case Tests
// ============================================================================
#[test]
fn test_transform_cwd_transforms_tool_call_arguments() {
@@ -7333,9 +7289,7 @@ mod tests {
}
}
// ============================================================================
// ConversationResponse Tests
// ============================================================================
#[test]
fn test_conversation_response_is_empty() {
@@ -7542,7 +7496,8 @@ mod tests {
stop_reason: Some(StopReason::Stop),
usage: None,
cost_usd_ticks: None,
message_chunks_emitted: 0, // only reasoning chunks were streamed
// only reasoning chunks were streamed
message_chunks_emitted: 0,
doom_loop_signals: Vec::new(),
stop_message: None,
};
@@ -7571,9 +7526,7 @@ mod tests {
assert!(response.fallback_text().is_none());
}
// ============================================================================
// StopReason Conversion Tests
// ============================================================================
#[test]
fn test_stop_reason_from_finish_reason() {
@@ -7593,9 +7546,7 @@ mod tests {
);
}
// ============================================================================
// Builder Pattern Tests
// ============================================================================
#[test]
fn test_conversation_request_builder() {
@@ -7654,9 +7605,7 @@ mod tests {
assert_matches!(user, ConversationItem::User(_));
}
// ============================================================================
// Serialization Tests
// ============================================================================
#[test]
fn test_conversation_item_serialization() {
@@ -7705,9 +7654,7 @@ mod tests {
assert_eq!(back.encrypted.as_deref(), Some("enc_data"));
}
// ====================================================================
// repair_dangling_tool_calls tests
// ====================================================================
fn assistant_with_calls(calls: &[(&str, &str)]) -> ConversationItem {
ConversationItem::Assistant(AssistantItem {
@@ -7906,7 +7853,6 @@ mod tests {
assert_eq!(tr.tool_call_id, "c1");
});
assert_matches!(&conv[2], ConversationItem::User(_));
// Second call: nothing to repair
assert_eq!(
repair_dangling_tool_calls(&mut conv, DanglingToolCallReason::UserCancelled),
0
@@ -8064,9 +8010,7 @@ mod tests {
);
}
// ====================================================================
// dedup_duplicate_tool_results tests
// ====================================================================
#[test]
fn test_dedup_no_duplicates() {
@@ -8091,7 +8035,8 @@ mod tests {
ConversationItem::tool_result("c1", "exit: 0\nreal output here"),
];
assert_eq!(dedup_duplicate_tool_results(&mut conv), 1);
assert_eq!(conv.len(), 2); // assistant + 1 tool_result
// assistant + 1 tool_result
assert_eq!(conv.len(), 2);
assert_matches!(&conv[1], ConversationItem::ToolResult(tr) => {
assert_eq!(tr.tool_call_id, "c1");
assert!(tr.content.contains("real output here"));
@@ -8107,7 +8052,8 @@ mod tests {
ConversationItem::tool_result("c1", "real content"),
];
assert_eq!(dedup_duplicate_tool_results(&mut conv), 1);
assert_eq!(conv.len(), 3); // assistant + 2 tool_results
// assistant + 2 tool_results
assert_eq!(conv.len(), 3);
// c1 should be the real content (last occurrence)
let c1_results: Vec<_> = conv
.iter()
@@ -8153,7 +8099,8 @@ mod tests {
ConversationItem::tool_result("c2", "fresh"),
];
assert_eq!(dedup_duplicate_tool_results(&mut conv), 2);
assert_eq!(conv.len(), 5); // 2 assistants + 2 tool_results + 1 user
// 2 assistants + 2 tool_results + 1 user
assert_eq!(conv.len(), 5);
assert_matches!(&conv[1], ConversationItem::ToolResult(tr) => {
assert_eq!(tr.tool_call_id, "c1");
assert_eq!(tr.content.as_ref(), "new");
@@ -8173,12 +8120,11 @@ mod tests {
];
assert_eq!(dedup_duplicate_tool_results(&mut conv), 1);
assert_eq!(conv.len(), 2);
// Second run should be a no-op.
assert_eq!(dedup_duplicate_tool_results(&mut conv), 0);
assert_eq!(conv.len(), 2);
}
// ========== strip_images tests ==========
// strip_images tests
#[test]
fn test_strip_images_removes_user_images() {
@@ -8192,7 +8138,8 @@ mod tests {
// Verify image was replaced with placeholder text
if let ConversationItem::User(user) = &req.items[0] {
assert_eq!(user.content.len(), 2); // original text + replaced image
// original text + replaced image
assert_eq!(user.content.len(), 2);
assert_matches!(&user.content[1], ContentPart::Text { text } => {
assert!(text.contains("image removed"));
});
@@ -8295,7 +8242,7 @@ mod tests {
assert_eq!(stripped, 3);
}
// ── Tool result with images tests ──────────────────────────────────────────
// Tool result with images tests
#[test]
fn test_tool_result_with_images_to_responses_api() {
@@ -8544,7 +8491,7 @@ mod tests {
}
}
// ── SyntheticReason tests ─────────────────────────────────────────────────
// SyntheticReason tests
/// Real user messages must have `synthetic_reason = None`.
#[test]
@@ -8624,9 +8571,7 @@ mod tests {
);
}
// -----------------------------------------------------------------------
// user_meta / CompactionMeta tests
// -----------------------------------------------------------------------
#[test]
fn user_meta_tagged_correctly() {
@@ -8972,7 +8917,6 @@ mod tests {
assert_eq!(StopReason::ContentFilter.as_str(), "content_filter");
}
// ============================================================================
// Reasoning-as-sibling regression tests
//
// These pin the invariants that motivated this refactor:
@@ -8993,7 +8937,6 @@ mod tests {
// 4. `patch_reasoning_text_types` injects the `type: "reasoning_text"`
// discriminator on nested `content[]` items that async-openai's
// derived Serialize omits.
// ============================================================================
#[test]
fn multi_tco_reasoning_items_round_trip_as_siblings() {
@@ -9418,9 +9361,7 @@ mod tests {
);
}
// ========================================================================
// upgrade_legacy_reasoning — legacy in-memory reconstruction
// ========================================================================
//
// Three legacy on-disk shapes that the on-read upgrader must lift to
// sibling Reasoning / BackendToolCall items:
@@ -9791,7 +9732,6 @@ mod tests {
}
}
// ========================================================================
// KV Cache Invariant Tests (adapted to sibling-Reasoning)
//
// These tests enforce prefix stability and correct turn ordering for the
@@ -9811,7 +9751,6 @@ mod tests {
// / `extract_raw_input_items` / `splice_raw_input_items` tests are
// structurally obsolete and are not ported; the invariants they pinned
// are preserved here in a backend-shape-agnostic form.
// ========================================================================
/// Helper: build a sibling Reasoning item with the given id, summary
/// text, and optional encrypted_content. This replaces the old
@@ -9935,7 +9874,7 @@ mod tests {
/// sibling, each reasoning must appear at its own interleaved
/// position -- NOT all bunched at the end. This is the exact bug
/// class caused by the earlier placeholder design,
/// now structurally impossible because reasoning lives in the
/// structurally impossible because reasoning lives in the
/// `Vec<ConversationItem>` directly.
#[test]
fn build_responses_input_multi_turn_reasoning_ordering() {
@@ -83,7 +83,6 @@ impl DoomLoopRecoveryPolicy {
/// Default `max_threshold` (lowest common threshold across the backtest
/// corpus of confirmed loops).
pub const DEFAULT_MAX_THRESHOLD: u32 = 8;
/// Default `max_retries`.
pub const DEFAULT_MAX_RETRIES: u32 = 2;
/// Clamp a configured `max_threshold` into [`Self::MAX_THRESHOLD_RANGE`].
@@ -298,8 +298,8 @@ impl From<serde_json::Error> for SamplingError {
/// This is the only error format the Kimi chat/completions endpoint emits —
/// the same shape the official client parses via the OpenAI SDK
/// (kimi-cli packages/kosong/src/kosong/chat_provider/openai_common.py:83-87
/// maps `openai.APIStatusError` → status + message). The old xAI proxy's
/// flat `{"code": "...", "error": "..."}` format was removed with the proxy.
/// maps `openai.APIStatusError` → status + message). The old xAI proxy and
/// its flat `{"code": "...", "error": "..."}` format are gone.
#[derive(Debug, Deserialize)]
struct ErrorResponse {
error: ErrorBody,
@@ -452,7 +452,7 @@ mod tests {
#[test]
fn event_stream_error_is_retryable() {
// Verify the existing contract hasn't changed — EventStreamError is retryable.
// Verify the existing contract still holds — EventStreamError is retryable.
let err = SamplingError::EventStreamError("connection reset".into());
assert!(err.is_retryable());
}
@@ -2,7 +2,7 @@
//!
//! This crate contains the API-agnostic conversation types, chat completion
//! request/response types, streaming types, and error types used across the
//! xAI agent stack. It intentionally contains **no I/O** (no HTTP clients,
//! xAI agent stack. It deliberately contains **no I/O** (no HTTP clients,
//! no file system access) so it can be depended on by downstream crates
//! (e.g., `kigi-chat-state`) without pulling in the full `kigi-shell`.
@@ -4,9 +4,7 @@
use serde::{Deserialize, Serialize};
// ============================================================================
// Request Types
// ============================================================================
/// POST /v1/messages request body
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
@@ -82,7 +80,8 @@ pub enum SystemParam {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextBlock {
#[serde(rename = "type")]
pub r#type: String, // always "text"
// always "text"
pub r#type: String,
pub text: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheControl>,
@@ -91,7 +90,8 @@ pub struct TextBlock {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheControl {
#[serde(rename = "type")]
pub r#type: String, // "ephemeral"
// "ephemeral"
pub r#type: String,
}
/// Content blocks used in both requests and responses
@@ -189,17 +189,17 @@ pub struct Metadata {
pub user_id: Option<String>,
}
// ============================================================================
// Response Types
// ============================================================================
/// Non-streaming response from POST /v1/messages
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessagesResponse {
pub id: String,
#[serde(rename = "type")]
pub r#type: String, // "message"
pub role: String, // "assistant"
// "message"
pub r#type: String,
// "assistant"
pub role: String,
pub content: Vec<ContentBlock>,
pub model: String,
pub stop_reason: Option<StopReason>,
@@ -235,9 +235,7 @@ pub struct MessagesUsage {
pub cache_read_input_tokens: u32,
}
// ============================================================================
// Streaming Event Types
// ============================================================================
/// Top-level streaming event (SSE `type` field determines variant)
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -2,9 +2,7 @@ use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use std::num::NonZeroU64;
// ============================================================================
// TraceContext — cloneable, type-erased context for request tracing
// ============================================================================
/// Object-safe trait for opaque tracing context attached to requests.
///
@@ -105,7 +103,7 @@ pub struct ChatCompletionRequest {
pub x_kigi_user_id: Option<String>,
/// Optional opaque tracing context (e.g., where to persist the finalized request payload).
/// This is intentionally not serialized or deserialized.
/// This is deliberately not serialized or deserialized.
/// Consumers downcast via `trace.as_ref().unwrap().as_any().downcast_ref::<T>()`.
#[serde(skip)]
pub trace: Option<Box<dyn TraceContext>>,
@@ -670,7 +668,7 @@ pub struct CompletionTokensDetails {
#[serde(default)]
pub rejected_prediction_tokens: u32,
}
// ============ Streaming types ============
// Streaming types
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ChatCompletionChunk {
@@ -1098,7 +1096,6 @@ pub fn adapt_body_for_codex_backend(body: &mut Value) {
body["instructions"] = Value::String(instructions);
}
// 2. Request replayable encrypted reasoning.
let include = body
.as_object_mut()
.map(|obj| obj.entry("include").or_insert_with(|| Value::Array(vec![])));
@@ -1167,7 +1164,7 @@ pub const CLAUDE_CODE_USER_AGENT: &str = "claude-cli/2.1.75";
pub const CLAUDE_CODE_SYSTEM_PREFIX: &str =
"You are Claude Code, Anthropic's official CLI for Claude.";
// ── GitHub Copilot editor-identity headers ──────────────────────────────────
// GitHub Copilot editor-identity headers
// The VS Code Copilot Chat client identity. Copilot's proxy authorizes the
// short-lived copilot token AND validates these editor headers, so they ride
// the `copilot_internal/v2/token` exchange, the `/models` listing, and every
@@ -1189,7 +1186,7 @@ pub const COPILOT_API_VERSION: &str = "2026-06-01";
/// `X-Initiator` value — sent ONLY on inference (`user`, per the spec).
pub const COPILOT_INITIATOR: &str = "user";
// ── ChatGPT/Codex (openai-codex) OAuth-inference headers ─────────────────────
// ChatGPT/Codex (openai-codex) OAuth-inference headers
// The ChatGPT Codex backend authorizes an OAuth bearer AND validates the Codex
// client identity. These ride the `/codex/responses` inference request ONLY.
// openai-codex-GATED: no other Responses provider (API-key `openai`) sends them,
@@ -1487,7 +1484,7 @@ pub struct SamplingConfig {
pub stream_tool_calls: Option<bool>,
}
// ============ Responses API wrapper ============
// Responses API wrapper
/// Wrapper around `async_openai::types::responses::CreateResponse` that adds
/// custom header fields for xAI request tracking, similar to
@@ -1567,7 +1564,7 @@ impl From<crate::rs::CreateResponse> for CreateResponseWrapper {
}
}
// ============ Messages API wrapper ============
// Messages API wrapper
/// Wrapper around `MessagesRequest` that adds custom header fields for xAI
/// request tracking, analogous to `CreateResponseWrapper`.
@@ -2125,7 +2122,7 @@ mod tests {
);
let bad_type = as_map(serde_json::json!({"reasoningEffort": 3}));
assert_eq!(parse_reasoning_effort_meta(Some(&bad_type)), None);
// `ultra` is now a real codex tier; a genuinely-unknown token still None.
// `ultra` is a real codex tier; a genuinely-unknown token still None.
let unknown = as_map(serde_json::json!({"reasoningEffort": "MEGA"}));
assert_eq!(parse_reasoning_effort_meta(Some(&unknown)), None);
}