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:
1 parent
ff0fb56c67
commit
a02b555e66
1458 files changed
+10729
-21750
No files matched your search
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user