Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion desktop/macos/Backend-Rust/src/models/chat_completions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,6 @@ pub struct AnthropicRequest {
// String or array-of-content-blocks. We emit the block form with a
// cache_control breakpoint to cache the static tools+system prefix.
#[serde(skip_serializing_if = "Option::is_none")]
pub system: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f64>,
pub stream: bool,
Expand All @@ -192,6 +191,34 @@ pub struct AnthropicMessage {
pub content: serde_json::Value,
}

/// Anthropic content block type (system prompt blocks are always "text").
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AnthropicContentBlockType {
Text,
}

/// Anthropic cache control type (currently only "ephemeral" is supported).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AnthropicCacheControlType {
Ephemeral,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct AnthropicSystemContentBlock {
#[serde(rename = "type")]
pub block_type: AnthropicContentBlockType,
pub text: String,
pub cache_control: AnthropicCacheControl,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct AnthropicCacheControl {
#[serde(rename = "type")]
pub cache_type: AnthropicCacheControlType,
}
Comment on lines +208 to +220

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The block_type and cache_type fields are plain String, so a typo (e.g. "Ephemeral" or "Text") compiles cleanly but produces a 400 from Anthropic at runtime. Since these fields are discriminants with a fixed, known set of valid values, typed enums would catch mistakes at compile time with zero runtime cost.

Suggested change
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct AnthropicSystemContentBlock {
#[serde(rename = "type")]
pub block_type: String,
pub text: String,
pub cache_control: AnthropicCacheControl,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct AnthropicCacheControl {
#[serde(rename = "type")]
pub cache_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum AnthropicContentBlockType {
Text,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum AnthropicCacheControlType {
Ephemeral,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct AnthropicSystemContentBlock {
#[serde(rename = "type")]
pub block_type: AnthropicContentBlockType,
pub text: String,
pub cache_control: AnthropicCacheControl,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct AnthropicCacheControl {
#[serde(rename = "type")]
pub cache_type: AnthropicCacheControlType,
}


#[derive(Debug, Clone, Serialize)]
pub struct AnthropicTool {
pub name: String,
Expand Down
143 changes: 134 additions & 9 deletions desktop/macos/Backend-Rust/src/routes/chat_completions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,23 @@ fn translate_request(
model: upstream_model.to_string(),
max_tokens,
messages: anthropic_messages,
system,
<<<<<<< HEAD

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Resolve the leftover merge conflict markers

When this commit is checked out, the Rust source still contains merge-conflict markers in the AnthropicRequest initializer. Any build of the desktop backend that parses this file fails immediately, so the prompt-caching change cannot ship until the conflict is resolved and the intended system serialization is restored.

Useful? React with 👍 / 👎.

=======
system: system_prompt.and_then(|text| {
let text = text.trim().to_string();
if text.is_empty() {
None
} else {
Some(vec![AnthropicSystemContentBlock {
block_type: AnthropicContentBlockType::Text,
text,
cache_control: AnthropicCacheControl {
cache_type: AnthropicCacheControlType::Ephemeral,
},
}])
}
}),
>>>>>>> 608ebd12c3 (refactor: replace raw String types with typed enums for Anthropic block/cache types)
temperature: req.temperature,
stream: req.stream,
tools: if is_tool_choice_none { None } else { anthropic_tools },
Expand Down Expand Up @@ -1269,10 +1285,19 @@ mod tests {

let result = translate_request(&req, "claude-sonnet-4-6").unwrap();
assert_eq!(result.model, "claude-sonnet-4-6");
// system is now an ephemeral-cached content-block array, not a bare string.
let system = result.system.as_ref().expect("system block should be present");
assert_eq!(system[0]["text"], "You are helpful.");
assert_eq!(system[0]["cache_control"]["type"], "ephemeral");
<<<<<<< HEAD
=======
assert_eq!(
result.system,
Some(vec![AnthropicSystemContentBlock {
block_type: AnthropicContentBlockType::Text,
text: "You are helpful.".to_string(),
cache_control: AnthropicCacheControl {
cache_type: AnthropicCacheControlType::Ephemeral,
},
}])
);
>>>>>>> 608ebd12c3 (refactor: replace raw String types with typed enums for Anthropic block/cache types)
assert_eq!(result.messages.len(), 1); // only user message, system extracted
assert_eq!(result.messages[0].role, "user");
assert_eq!(result.max_tokens, 1024);
Expand Down Expand Up @@ -1431,13 +1456,113 @@ mod tests {
};

let result = translate_request(&req, "claude-sonnet-4-6").unwrap();
let system = result.system.as_ref().expect("system block should be present");
assert_eq!(system[0]["text"], "You are terse.");
assert_eq!(system[0]["cache_control"]["type"], "ephemeral");
assert_eq!(result.messages.len(), 1, "developer msg must be extracted, not forwarded");
assert_eq!(result.messages[0].role, "user");
}

#[test]
fn test_translate_request_system_prompt_uses_cache_control_blocks() {
let req = ChatCompletionRequest {
model: "omi-sonnet".to_string(),
messages: vec![
ChatMessage {
role: "system".to_string(),
content: Some(json!("You are helpful.")),
name: None,
tool_calls: None,
tool_call_id: None,
},
ChatMessage {
role: "user".to_string(),
content: Some(json!("Hello")),
name: None,
tool_calls: None,
tool_call_id: None,
},
],
stream: false,
temperature: None,
max_tokens: None,
max_completion_tokens: None,
tools: None,
tool_choice: None,
};

let result = translate_request(&req, "claude-sonnet-4-6").unwrap();
let json = serde_json::to_value(&result).unwrap();

assert_eq!(
json["system"],
json!([{
"type": "text",
"text": "You are helpful.",
"cache_control": {"type": "ephemeral"}
}])
);
}

#[test]
fn test_translate_request_without_system_prompt_omits_system() {
let req = ChatCompletionRequest {
model: "omi-sonnet".to_string(),
messages: vec![ChatMessage {
role: "user".to_string(),
content: Some(json!("Hello")),
name: None,
tool_calls: None,
tool_call_id: None,
}],
stream: false,
temperature: None,
max_tokens: None,
max_completion_tokens: None,
tools: None,
tool_choice: None,
};

let result = translate_request(&req, "claude-sonnet-4-6").unwrap();
let json = serde_json::to_value(&result).unwrap();

assert!(result.system.is_none());
assert!(json.get("system").is_none());
}

#[test]
fn test_translate_request_empty_system_prompt_omits_system() {
// Empty or whitespace-only system prompts must NOT be sent as cached blocks
// (Anthropic rejects empty cached text blocks with 400).
for content in [Some(json!("")), Some(json!(" ")), None] {
let req = ChatCompletionRequest {
model: "omi-sonnet".to_string(),
messages: vec![ChatMessage {
role: "system".to_string(),
content: content.clone(),
name: None,
tool_calls: None,
tool_call_id: None,
}, ChatMessage {
role: "user".to_string(),
content: Some(json!("Hello")),
name: None,
tool_calls: None,
tool_call_id: None,
}],
stream: false,
temperature: None,
max_tokens: None,
max_completion_tokens: None,
tools: None,
tool_choice: None,
};

let result = translate_request(&req, "claude-sonnet-4-6").unwrap();
assert!(
result.system.is_none(),
"empty/whitespace system prompt must omit system field, got: {:?}",
result.system
);
}
}

#[test]
fn test_translate_request_max_completion_tokens_preferred() {
// OpenAI renamed `max_tokens` → `max_completion_tokens` for reasoning
Expand Down
Loading