mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-09 12:40:20 +08:00
Merge remote-tracking branch 'origin/pr/619'
This commit is contained in:
@@ -16,8 +16,8 @@ use crate::{
|
||||
claude_generation_config, claude_messages_to_canonical, claude_parallel_tool_calls,
|
||||
claude_system_to_canonical_instructions, claude_thinking_to_canonical,
|
||||
claude_tool_choice_to_canonical, claude_tools_to_canonical,
|
||||
compact_canonical_claude_messages, insert_f64, namespace_extension_object,
|
||||
CanonicalRequest,
|
||||
compact_canonical_claude_messages, insert_f64, mark_claude_messages_request_source,
|
||||
namespace_extension_object, CanonicalRequest,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -85,6 +85,7 @@ pub fn from_raw(body_json: &Value) -> Option<CanonicalRequest> {
|
||||
"output_config",
|
||||
],
|
||||
);
|
||||
mark_claude_messages_request_source(&mut canonical.extensions);
|
||||
if !builtin_tools.is_empty() {
|
||||
canonical_extension_object_mut(&mut canonical.extensions, "claude")
|
||||
.insert("builtin_tools".to_string(), Value::Array(builtin_tools));
|
||||
@@ -115,7 +116,7 @@ pub fn to_raw(
|
||||
);
|
||||
output.insert(
|
||||
"max_tokens".to_string(),
|
||||
Value::from(canonical.generation.max_tokens.unwrap_or(1024)),
|
||||
Value::from(canonical.generation.max_tokens.unwrap_or(8192)),
|
||||
);
|
||||
if let Some(system) = canonical_instructions_to_claude_system(&canonical.instructions) {
|
||||
output.insert("system".to_string(), system);
|
||||
|
||||
@@ -668,6 +668,142 @@ mod tests {
|
||||
assert!(!block_content_json.contains("document body"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claude_request_to_responses_uses_developer_system_and_sub2api_defaults() {
|
||||
let body = json!({
|
||||
"model": "claude-sonnet",
|
||||
"system": [{
|
||||
"type": "text",
|
||||
"text": "Be exact.",
|
||||
"cache_control": {"type": "ephemeral"}
|
||||
}],
|
||||
"messages": [
|
||||
{"role": "user", "content": "hello"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "thinking", "thinking": "private plan", "signature": "sig_hidden"},
|
||||
{"type": "text", "text": "visible answer"},
|
||||
{
|
||||
"type": "tool_use",
|
||||
"id": "toolu_calc",
|
||||
"name": "calc",
|
||||
"input": {"x": 1}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"tools": [
|
||||
{"name": "implicit_empty", "description": "empty"},
|
||||
{"name": "object_empty", "input_schema": {"type": "object"}}
|
||||
],
|
||||
"thinking": {"type": "enabled", "budget_tokens": 4096},
|
||||
"temperature": 0.2,
|
||||
"top_p": 0.9,
|
||||
"max_tokens": 10,
|
||||
});
|
||||
|
||||
let converted = registry::convert_request(
|
||||
"claude:messages",
|
||||
"openai:responses",
|
||||
&body,
|
||||
&FormatContext::default().with_mapped_model("gpt-5.1"),
|
||||
)
|
||||
.expect("responses request");
|
||||
|
||||
assert_eq!(converted["model"], "gpt-5.1");
|
||||
assert!(converted.get("temperature").is_none());
|
||||
assert!(converted.get("top_p").is_none());
|
||||
assert!(converted.get("instructions").is_none());
|
||||
assert_eq!(converted["text"]["verbosity"], "medium");
|
||||
assert_eq!(converted["reasoning"]["effort"], "medium");
|
||||
assert_eq!(converted["reasoning"]["summary"], "auto");
|
||||
assert_eq!(converted["max_output_tokens"], 128);
|
||||
assert_eq!(converted["store"], false);
|
||||
assert_eq!(converted["parallel_tool_calls"], true);
|
||||
assert!(converted["include"]
|
||||
.as_array()
|
||||
.expect("include")
|
||||
.iter()
|
||||
.any(|value| value.as_str() == Some("reasoning.encrypted_content")));
|
||||
|
||||
let input = converted["input"].as_array().expect("responses input");
|
||||
assert_eq!(input[0]["role"], "developer");
|
||||
assert_eq!(input[0]["content"][0]["type"], "input_text");
|
||||
assert_eq!(input[0]["content"][0]["text"], "Be exact.");
|
||||
assert_eq!(
|
||||
input[0]["content"][0]["cache_control"],
|
||||
json!({"type": "ephemeral"})
|
||||
);
|
||||
let input_json = Value::Array(input.clone()).to_string();
|
||||
assert!(input_json.contains("visible answer"));
|
||||
assert!(!input_json.contains("private plan"));
|
||||
assert!(!input_json.contains("sig_hidden"));
|
||||
|
||||
let tools = converted["tools"].as_array().expect("tools");
|
||||
assert_eq!(tools.len(), 2);
|
||||
for tool in tools {
|
||||
assert_eq!(tool["parameters"]["type"], "object");
|
||||
assert!(tool["parameters"]["properties"].is_object());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claude_output_config_effort_controls_responses_reasoning() {
|
||||
let body = json!({
|
||||
"model": "claude-sonnet",
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
"thinking": {"type": "enabled", "budget_tokens": 1024},
|
||||
"output_config": {"effort": "max"},
|
||||
"max_tokens": 128,
|
||||
});
|
||||
|
||||
let converted = registry::convert_request(
|
||||
"claude:messages",
|
||||
"openai:responses",
|
||||
&body,
|
||||
&FormatContext::default(),
|
||||
)
|
||||
.expect("responses request");
|
||||
|
||||
assert_eq!(converted["reasoning"]["effort"], "xhigh");
|
||||
assert_eq!(converted["reasoning"]["summary"], "auto");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn responses_to_claude_defaults_max_tokens_and_omits_false_is_error() {
|
||||
let body = json!({
|
||||
"model": "gpt-5",
|
||||
"input": [
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"call_id": "toolu_ok",
|
||||
"output": "ok",
|
||||
"is_error": false
|
||||
},
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"call_id": "toolu_bad",
|
||||
"output": "bad",
|
||||
"is_error": true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let converted = registry::convert_request(
|
||||
"openai:responses",
|
||||
"claude:messages",
|
||||
&body,
|
||||
&FormatContext::default(),
|
||||
)
|
||||
.expect("claude request");
|
||||
|
||||
assert_eq!(converted["max_tokens"], 8192);
|
||||
let messages_json = converted["messages"].to_string();
|
||||
assert!(!messages_json.contains("\"is_error\":false"));
|
||||
assert!(messages_json.contains("\"is_error\":true"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claude_request_to_responses_splits_tool_result_media_from_output() {
|
||||
let body = json!({
|
||||
|
||||
@@ -6,9 +6,10 @@ use crate::{
|
||||
formats::context::FormatContext,
|
||||
formats::openai::shared::map_thinking_budget_to_openai_reasoning_effort,
|
||||
protocol::canonical::{
|
||||
canonical_response_format_to_openai, canonicalize_tool_arguments, is_claude_tool_result,
|
||||
media_data_or_url, namespace_extension_object, openai_content_text, openai_extensions,
|
||||
openai_response_format_to_canonical, openai_responses_extension,
|
||||
canonical_response_format_to_openai, canonicalize_tool_arguments,
|
||||
is_claude_messages_request, is_claude_system_instruction, is_claude_thinking_block,
|
||||
is_claude_tool_result, media_data_or_url, namespace_extension_object, openai_content_text,
|
||||
openai_extensions, openai_response_format_to_canonical, openai_responses_extension,
|
||||
openai_responses_generation_config, openai_responses_input_to_canonical_messages,
|
||||
openai_responses_tool_choice_to_canonical, openai_responses_tools_to_canonical,
|
||||
CanonicalContentBlock, CanonicalInstruction, CanonicalRequest, CanonicalRole,
|
||||
@@ -137,13 +138,18 @@ pub fn to_raw(
|
||||
output.insert("instructions".to_string(), instructions);
|
||||
}
|
||||
let mut input = canonical_messages_to_responses_input(canonical)?;
|
||||
if let Some(developer_message) =
|
||||
claude_system_instructions_to_responses_developer_message(canonical)
|
||||
{
|
||||
input.insert(0, developer_message);
|
||||
}
|
||||
ensure_json_object_response_input_mentions_json(canonical, instructions.as_ref(), &mut input);
|
||||
output.insert("input".to_string(), Value::Array(input));
|
||||
|
||||
if upstream_is_stream && !compact {
|
||||
output.insert("stream".to_string(), Value::Bool(true));
|
||||
}
|
||||
if let Some(max_tokens) = canonical.generation.max_tokens {
|
||||
if let Some(max_tokens) = responses_max_output_tokens(canonical) {
|
||||
output.insert("max_output_tokens".to_string(), Value::from(max_tokens));
|
||||
}
|
||||
insert_number(&mut output, "temperature", canonical.generation.temperature);
|
||||
@@ -172,11 +178,7 @@ pub fn to_raw(
|
||||
canonical_tool_choice_to_responses(tool_choice),
|
||||
);
|
||||
}
|
||||
if let Some(reasoning) = canonical
|
||||
.thinking
|
||||
.as_ref()
|
||||
.and_then(reasoning_config_to_responses)
|
||||
{
|
||||
if let Some(reasoning) = canonical_reasoning_config_to_responses(canonical) {
|
||||
output.insert("reasoning".to_string(), reasoning);
|
||||
}
|
||||
|
||||
@@ -190,6 +192,7 @@ pub fn to_raw(
|
||||
OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE,
|
||||
&output,
|
||||
));
|
||||
apply_claude_responses_request_defaults(canonical, mapped_model, &mut output);
|
||||
if compact {
|
||||
output.remove("stream");
|
||||
}
|
||||
@@ -201,6 +204,7 @@ fn canonical_instructions_to_responses(canonical: &CanonicalRequest) -> Option<V
|
||||
let text = canonical
|
||||
.instructions
|
||||
.iter()
|
||||
.filter(|instruction| !is_claude_system_instruction(instruction))
|
||||
.map(|instruction| instruction.text.as_str())
|
||||
.filter(|text| !text.trim().is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
@@ -208,6 +212,13 @@ fn canonical_instructions_to_responses(canonical: &CanonicalRequest) -> Option<V
|
||||
if !text.trim().is_empty() {
|
||||
return Some(Value::String(text));
|
||||
}
|
||||
if canonical
|
||||
.instructions
|
||||
.iter()
|
||||
.any(is_claude_system_instruction)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
canonical
|
||||
.system
|
||||
.as_ref()
|
||||
@@ -216,6 +227,41 @@ fn canonical_instructions_to_responses(canonical: &CanonicalRequest) -> Option<V
|
||||
.map(Value::String)
|
||||
}
|
||||
|
||||
fn claude_system_instructions_to_responses_developer_message(
|
||||
canonical: &CanonicalRequest,
|
||||
) -> Option<Value> {
|
||||
let content = canonical
|
||||
.instructions
|
||||
.iter()
|
||||
.filter(|instruction| is_claude_system_instruction(instruction))
|
||||
.filter_map(claude_system_instruction_to_responses_part)
|
||||
.collect::<Vec<_>>();
|
||||
(!content.is_empty()).then(|| {
|
||||
json!({
|
||||
"type": "message",
|
||||
"role": "developer",
|
||||
"content": content,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn claude_system_instruction_to_responses_part(
|
||||
instruction: &CanonicalInstruction,
|
||||
) -> Option<Value> {
|
||||
if instruction.text.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut part = Map::new();
|
||||
part.insert("type".to_string(), Value::String("input_text".to_string()));
|
||||
part.insert("text".to_string(), Value::String(instruction.text.clone()));
|
||||
part.extend(namespace_extension_object(
|
||||
&instruction.extensions,
|
||||
"claude",
|
||||
&part,
|
||||
));
|
||||
Some(Value::Object(part))
|
||||
}
|
||||
|
||||
fn canonical_messages_to_responses_input(canonical: &CanonicalRequest) -> Option<Vec<Value>> {
|
||||
let mut input = Vec::new();
|
||||
for message in &canonical.messages {
|
||||
@@ -270,7 +316,12 @@ fn canonical_messages_to_responses_input(canonical: &CanonicalRequest) -> Option
|
||||
}));
|
||||
}
|
||||
}
|
||||
CanonicalContentBlock::Thinking { text, .. } => {
|
||||
CanonicalContentBlock::Thinking {
|
||||
text, extensions, ..
|
||||
} => {
|
||||
if is_claude_thinking_block(extensions) {
|
||||
continue;
|
||||
}
|
||||
if role == "assistant" && !text.trim().is_empty() {
|
||||
content.push(json!({
|
||||
"type": "output_text",
|
||||
@@ -309,6 +360,49 @@ fn canonical_messages_to_responses_input(canonical: &CanonicalRequest) -> Option
|
||||
Some(input)
|
||||
}
|
||||
|
||||
fn responses_max_output_tokens(canonical: &CanonicalRequest) -> Option<u64> {
|
||||
canonical.generation.max_tokens.map(|max_tokens| {
|
||||
if is_claude_messages_request(&canonical.extensions) && max_tokens < 128 {
|
||||
128
|
||||
} else {
|
||||
max_tokens
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn apply_claude_responses_request_defaults(
|
||||
canonical: &CanonicalRequest,
|
||||
mapped_model: &str,
|
||||
output: &mut Map<String, Value>,
|
||||
) {
|
||||
if !is_claude_messages_request(&canonical.extensions) {
|
||||
return;
|
||||
}
|
||||
if mapped_model
|
||||
.trim()
|
||||
.to_ascii_lowercase()
|
||||
.starts_with("gpt-5")
|
||||
{
|
||||
output.remove("temperature");
|
||||
output.remove("top_p");
|
||||
}
|
||||
output
|
||||
.entry("store".to_string())
|
||||
.or_insert_with(|| Value::Bool(false));
|
||||
output
|
||||
.entry("parallel_tool_calls".to_string())
|
||||
.or_insert_with(|| Value::Bool(true));
|
||||
let include = output
|
||||
.entry("include".to_string())
|
||||
.or_insert_with(|| Value::Array(Vec::new()));
|
||||
if let Some(include) = include.as_array_mut() {
|
||||
let encrypted_content = Value::String("reasoning.encrypted_content".to_string());
|
||||
if !include.iter().any(|value| value == &encrypted_content) {
|
||||
include.push(encrypted_content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_json_object_response_input_mentions_json(
|
||||
canonical: &CanonicalRequest,
|
||||
instructions: Option<&Value>,
|
||||
@@ -463,6 +557,42 @@ fn canonical_tools_to_responses(canonical: &CanonicalRequest) -> Vec<Value> {
|
||||
tools
|
||||
}
|
||||
|
||||
fn canonical_reasoning_config_to_responses(canonical: &CanonicalRequest) -> Option<Value> {
|
||||
let is_claude_request = is_claude_messages_request(&canonical.extensions);
|
||||
if !is_claude_request {
|
||||
return canonical
|
||||
.thinking
|
||||
.as_ref()
|
||||
.and_then(reasoning_config_to_responses);
|
||||
}
|
||||
|
||||
let mut object = canonical
|
||||
.thinking
|
||||
.as_ref()
|
||||
.and_then(|thinking| openai_responses_extension(&thinking.extensions).cloned())
|
||||
.and_then(|value| match value {
|
||||
Value::Object(object) => Some(object),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let effort = canonical
|
||||
.thinking
|
||||
.as_ref()
|
||||
.and_then(|thinking| thinking.extensions.get("claude"))
|
||||
.and_then(|value| value.get("output_config"))
|
||||
.and_then(|value| value.get("effort"))
|
||||
.and_then(Value::as_str)
|
||||
.map(openai_responses_reasoning_effort)
|
||||
.unwrap_or("medium");
|
||||
object
|
||||
.entry("effort".to_string())
|
||||
.or_insert_with(|| Value::String(effort.to_string()));
|
||||
object
|
||||
.entry("summary".to_string())
|
||||
.or_insert_with(|| Value::String("auto".to_string()));
|
||||
Some(Value::Object(object))
|
||||
}
|
||||
|
||||
fn reasoning_config_to_responses(thinking: &CanonicalThinkingConfig) -> Option<Value> {
|
||||
openai_responses_extension(&thinking.extensions)
|
||||
.cloned()
|
||||
@@ -525,6 +655,10 @@ fn canonical_text_config_to_responses(canonical: &CanonicalRequest) -> Option<Va
|
||||
{
|
||||
text.insert("verbosity".to_string(), verbosity);
|
||||
}
|
||||
if is_claude_messages_request(&canonical.extensions) {
|
||||
text.entry("verbosity".to_string())
|
||||
.or_insert_with(|| Value::String("medium".to_string()));
|
||||
}
|
||||
(!text.is_empty()).then_some(Value::Object(text))
|
||||
}
|
||||
|
||||
@@ -556,9 +690,10 @@ fn canonical_tool_to_responses(tool: &CanonicalToolDefinition) -> Value {
|
||||
Value::String(description.clone()),
|
||||
);
|
||||
}
|
||||
if let Some(parameters) = &tool.parameters {
|
||||
out.insert("parameters".to_string(), parameters.clone());
|
||||
}
|
||||
out.insert(
|
||||
"parameters".to_string(),
|
||||
responses_tool_parameters_schema(tool.parameters.as_ref()),
|
||||
);
|
||||
out.extend(namespace_extension_object(
|
||||
&tool.extensions,
|
||||
OPENAI_RESPONSES_EXTENSION_NAMESPACE,
|
||||
@@ -567,6 +702,25 @@ fn canonical_tool_to_responses(tool: &CanonicalToolDefinition) -> Value {
|
||||
Value::Object(out)
|
||||
}
|
||||
|
||||
fn responses_tool_parameters_schema(parameters: Option<&Value>) -> Value {
|
||||
match parameters {
|
||||
Some(Value::Object(schema)) => {
|
||||
let mut schema = schema.clone();
|
||||
if schema
|
||||
.get("type")
|
||||
.and_then(Value::as_str)
|
||||
.is_some_and(|value| value == "object")
|
||||
&& !schema.contains_key("properties")
|
||||
{
|
||||
schema.insert("properties".to_string(), json!({}));
|
||||
}
|
||||
Value::Object(schema)
|
||||
}
|
||||
Some(Value::Null) | None => json!({"type": "object", "properties": {}}),
|
||||
Some(value) => value.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn canonical_tool_choice_to_responses(choice: &CanonicalToolChoice) -> Value {
|
||||
match choice {
|
||||
CanonicalToolChoice::Auto => Value::String("auto".to_string()),
|
||||
|
||||
@@ -1079,6 +1079,33 @@ mod tests {
|
||||
None,
|
||||
)
|
||||
.expect("typed canonical claude route should build");
|
||||
if matches!(
|
||||
provider_api_format,
|
||||
"openai:responses" | "openai:responses:compact"
|
||||
) {
|
||||
assert!(converted.get("instructions").is_none());
|
||||
assert_eq!(converted["input"][0]["role"], "developer");
|
||||
assert_eq!(converted["input"][0]["content"][0]["text"], "Be exact.");
|
||||
assert_eq!(converted["max_output_tokens"], 128);
|
||||
assert_eq!(converted["text"]["verbosity"], "medium");
|
||||
assert_eq!(converted["reasoning"]["effort"], "medium");
|
||||
assert_eq!(converted["reasoning"]["summary"], "auto");
|
||||
if provider_api_format == "openai:responses" {
|
||||
assert_eq!(converted["store"], false);
|
||||
assert!(converted["include"]
|
||||
.as_array()
|
||||
.expect("include")
|
||||
.iter()
|
||||
.any(|value| value.as_str() == Some("reasoning.encrypted_content")));
|
||||
} else {
|
||||
assert!(converted.get("store").is_none());
|
||||
assert!(converted.get("include").is_none());
|
||||
}
|
||||
let input_json = converted["input"].to_string();
|
||||
assert!(!input_json.contains("<thinking>plan</thinking>"));
|
||||
assert!(!input_json.contains("sig_123"));
|
||||
continue;
|
||||
}
|
||||
let legacy =
|
||||
legacy_claude_request_body(&request, provider_api_format, upstream_is_stream);
|
||||
assert_eq!(
|
||||
|
||||
@@ -11,6 +11,9 @@ pub use crate::protocol::stream::{CanonicalStreamEvent, CanonicalStreamFrame};
|
||||
pub(crate) const OPENAI_RESPONSES_EXTENSION_NAMESPACE: &str = "openai_responses";
|
||||
pub(crate) const OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE: &str = "openai_cli";
|
||||
const AETHER_EXTENSION_NAMESPACE: &str = "aether";
|
||||
const CLAUDE_MESSAGES_REQUEST_SOURCE_MARKER: &str = "claude_messages_request";
|
||||
const CLAUDE_SYSTEM_SOURCE_MARKER: &str = "claude_system";
|
||||
const CLAUDE_THINKING_SOURCE_MARKER: &str = "claude_thinking";
|
||||
const CLAUDE_TOOL_RESULT_SOURCE_MARKER: &str = "claude_tool_result";
|
||||
const OPENAI_CHAT_TOOL_RESULT_SOURCE_MARKER: &str = "openai_chat_tool_result";
|
||||
const OPENAI_RESPONSES_TOOL_RESULT_SOURCE_MARKER: &str = "openai_responses_tool_result";
|
||||
@@ -1052,7 +1055,7 @@ pub(crate) fn claude_system_to_canonical_instructions(
|
||||
Some(vec![CanonicalInstruction {
|
||||
role: CanonicalRole::System,
|
||||
text,
|
||||
extensions: BTreeMap::new(),
|
||||
extensions: claude_system_instruction_extensions(BTreeMap::new()),
|
||||
}])
|
||||
}
|
||||
}
|
||||
@@ -1071,7 +1074,10 @@ pub(crate) fn claude_system_to_canonical_instructions(
|
||||
instructions.push(CanonicalInstruction {
|
||||
role: CanonicalRole::System,
|
||||
text: strip_claude_billing_header(text),
|
||||
extensions: claude_extensions(block, &["type", "text"]),
|
||||
extensions: claude_system_instruction_extensions(claude_extensions(
|
||||
block,
|
||||
&["type", "text"],
|
||||
)),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1081,6 +1087,40 @@ pub(crate) fn claude_system_to_canonical_instructions(
|
||||
}
|
||||
}
|
||||
|
||||
fn claude_system_instruction_extensions(
|
||||
mut extensions: BTreeMap<String, Value>,
|
||||
) -> BTreeMap<String, Value> {
|
||||
canonical_extension_object_mut(&mut extensions, AETHER_EXTENSION_NAMESPACE).insert(
|
||||
"source".to_string(),
|
||||
Value::String(CLAUDE_SYSTEM_SOURCE_MARKER.to_string()),
|
||||
);
|
||||
extensions
|
||||
}
|
||||
|
||||
pub(crate) fn mark_claude_messages_request_source(extensions: &mut BTreeMap<String, Value>) {
|
||||
canonical_extension_object_mut(extensions, AETHER_EXTENSION_NAMESPACE).insert(
|
||||
"source".to_string(),
|
||||
Value::String(CLAUDE_MESSAGES_REQUEST_SOURCE_MARKER.to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn is_claude_messages_request(extensions: &BTreeMap<String, Value>) -> bool {
|
||||
extensions
|
||||
.get(AETHER_EXTENSION_NAMESPACE)
|
||||
.and_then(|value| value.get("source"))
|
||||
.and_then(Value::as_str)
|
||||
== Some(CLAUDE_MESSAGES_REQUEST_SOURCE_MARKER)
|
||||
}
|
||||
|
||||
pub(crate) fn is_claude_system_instruction(instruction: &CanonicalInstruction) -> bool {
|
||||
instruction
|
||||
.extensions
|
||||
.get(AETHER_EXTENSION_NAMESPACE)
|
||||
.and_then(|value| value.get("source"))
|
||||
.and_then(Value::as_str)
|
||||
== Some(CLAUDE_SYSTEM_SOURCE_MARKER)
|
||||
}
|
||||
|
||||
pub(crate) fn claude_messages_to_canonical(
|
||||
messages: Option<&Value>,
|
||||
) -> Option<Vec<CanonicalMessage>> {
|
||||
@@ -1174,7 +1214,10 @@ pub(crate) fn claude_block_to_canonical_block(block: &Value) -> Option<Canonical
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned),
|
||||
encrypted_content: None,
|
||||
extensions: claude_extensions(block_object, &["type", "thinking", "text", "signature"]),
|
||||
extensions: claude_thinking_extensions(claude_extensions(
|
||||
block_object,
|
||||
&["type", "thinking", "text", "signature"],
|
||||
)),
|
||||
}),
|
||||
"redacted_thinking" => Some(CanonicalContentBlock::Thinking {
|
||||
text: String::new(),
|
||||
@@ -1183,7 +1226,10 @@ pub(crate) fn claude_block_to_canonical_block(block: &Value) -> Option<Canonical
|
||||
.get("data")
|
||||
.and_then(Value::as_str)
|
||||
.map(ToOwned::to_owned),
|
||||
extensions: claude_extensions(block_object, &["type", "data"]),
|
||||
extensions: claude_thinking_extensions(claude_extensions(
|
||||
block_object,
|
||||
&["type", "data"],
|
||||
)),
|
||||
}),
|
||||
"image" => claude_media_block_to_canonical(block_object, true),
|
||||
"document" => claude_media_block_to_canonical(block_object, false),
|
||||
@@ -2546,6 +2592,22 @@ fn canonical_tool_result_to_openai_chat(block: &CanonicalContentBlock) -> Value
|
||||
Value::Object(output)
|
||||
}
|
||||
|
||||
fn claude_thinking_extensions(mut extensions: BTreeMap<String, Value>) -> BTreeMap<String, Value> {
|
||||
canonical_extension_object_mut(&mut extensions, AETHER_EXTENSION_NAMESPACE).insert(
|
||||
"source".to_string(),
|
||||
Value::String(CLAUDE_THINKING_SOURCE_MARKER.to_string()),
|
||||
);
|
||||
extensions
|
||||
}
|
||||
|
||||
pub(crate) fn is_claude_thinking_block(extensions: &BTreeMap<String, Value>) -> bool {
|
||||
extensions
|
||||
.get(AETHER_EXTENSION_NAMESPACE)
|
||||
.and_then(|value| value.get("source"))
|
||||
.and_then(Value::as_str)
|
||||
== Some(CLAUDE_THINKING_SOURCE_MARKER)
|
||||
}
|
||||
|
||||
pub(crate) fn is_claude_tool_result(extensions: &BTreeMap<String, Value>) -> bool {
|
||||
extensions
|
||||
.get(AETHER_EXTENSION_NAMESPACE)
|
||||
@@ -4108,7 +4170,9 @@ pub(crate) fn canonical_block_to_claude(
|
||||
extensions,
|
||||
),
|
||||
);
|
||||
out.insert("is_error".to_string(), Value::Bool(*is_error));
|
||||
if *is_error {
|
||||
out.insert("is_error".to_string(), Value::Bool(true));
|
||||
}
|
||||
out.extend(namespace_extension_object(extensions, "claude", &out));
|
||||
Some(Some(Value::Object(out)))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user