feat(usage): expose request timing details

This commit is contained in:
elky
2026-06-10 09:16:15 +08:00
parent 84f41dae77
commit 8edcbdcb29
41 changed files with 1884 additions and 335 deletions
+106 -100
View File
@@ -470,16 +470,15 @@ fn validate_cross_format_generation_target(
) -> Result<(), FormatError> {
let generation = &request.generation;
match target {
FormatId::OpenAiChat => {
if generation.top_k.is_some() {
return lossy_generation_field(
source,
target,
"top_k",
"OpenAI Chat Completions has no official top_k request field",
);
}
FormatId::OpenAiChat if generation.top_k.is_some() => {
return lossy_generation_field(
source,
target,
"top_k",
"OpenAI Chat Completions has no official top_k request field",
);
}
FormatId::OpenAiChat => {}
FormatId::OpenAiResponses | FormatId::OpenAiResponsesCompact => {
for (field, present, reason) in [
(
@@ -756,65 +755,72 @@ fn request_extension_key_is_cross_format_safe(
if location == "response_format" {
return response_format_extension_key_is_cross_format_safe(namespace, key);
}
match (source, target, namespace, key) {
matches!(
(source, target, namespace, key),
(
FormatId::OpenAiChat,
FormatId::OpenAiResponses | FormatId::OpenAiResponsesCompact,
"openai",
"stream"
| "store"
| "service_tier"
| "safety_identifier"
| "prompt_cache_key"
| "prompt_cache_retention"
| "verbosity",
) => true,
(
| "store"
| "service_tier"
| "safety_identifier"
| "prompt_cache_key"
| "prompt_cache_retention"
| "verbosity",
) | (
FormatId::OpenAiChat,
FormatId::OpenAiResponses | FormatId::OpenAiResponsesCompact,
"openai_responses",
"verbosity",
) => true,
(FormatId::OpenAiChat, FormatId::GeminiGenerateContent, "openai", "web_search_options") => {
true
}
(
) | (
FormatId::OpenAiChat,
FormatId::GeminiGenerateContent,
"openai",
"web_search_options",
) | (
FormatId::OpenAiResponses | FormatId::OpenAiResponsesCompact,
FormatId::OpenAiChat,
"openai_responses" | "openai_cli",
"stream"
| "store"
| "service_tier"
| "safety_identifier"
| "prompt_cache_key"
| "prompt_cache_retention"
| "verbosity",
) => true,
(FormatId::ClaudeMessages, _, "claude", "output_config") => true,
(
FormatId::ClaudeMessages,
FormatId::OpenAiChat | FormatId::GeminiGenerateContent,
"openai",
"web_search_options",
) => true,
(
FormatId::GeminiGenerateContent,
_,
"gemini",
"thinking_config" | "raw_tools" | "raw_tool_config",
) => true,
(
FormatId::GeminiGenerateContent,
FormatId::OpenAiChat,
"gemini",
"builtin_tools" | "grounding",
) => true,
(FormatId::GeminiGenerateContent, FormatId::GeminiGenerateContent, "gemini", _) => true,
(FormatId::GeminiGenerateContent, FormatId::OpenAiChat, "openai", "web_search_options") => {
true
}
_ => false,
}
| "store"
| "service_tier"
| "safety_identifier"
| "prompt_cache_key"
| "prompt_cache_retention"
| "verbosity",
) | (FormatId::ClaudeMessages, _, "claude", "output_config")
| (
FormatId::ClaudeMessages,
FormatId::OpenAiChat | FormatId::GeminiGenerateContent,
"openai",
"web_search_options",
)
| (
FormatId::GeminiGenerateContent,
_,
"gemini",
"thinking_config" | "raw_tools" | "raw_tool_config",
)
| (
FormatId::GeminiGenerateContent,
FormatId::OpenAiChat,
"gemini",
"builtin_tools" | "grounding",
)
| (
FormatId::GeminiGenerateContent,
FormatId::GeminiGenerateContent,
"gemini",
_
)
| (
FormatId::GeminiGenerateContent,
FormatId::OpenAiChat,
"openai",
"web_search_options",
)
)
}
fn tool_extension_key_is_cross_format_safe(
@@ -823,17 +829,17 @@ fn tool_extension_key_is_cross_format_safe(
namespace: &str,
key: &str,
) -> bool {
match (source, target, namespace, key) {
(_, _, "claude", "raw_input_schema") => true,
(_, _, "gemini", "raw_parameters") => true,
(
FormatId::OpenAiResponses | FormatId::OpenAiResponsesCompact,
FormatId::GeminiGenerateContent,
"openai_responses" | "openai_cli",
"type",
) => true,
_ => false,
}
matches!(
(source, target, namespace, key),
(_, _, "claude", "raw_input_schema")
| (_, _, "gemini", "raw_parameters")
| (
FormatId::OpenAiResponses | FormatId::OpenAiResponsesCompact,
FormatId::GeminiGenerateContent,
"openai_responses" | "openai_cli",
"type",
)
)
}
fn thinking_extension_key_is_cross_format_safe(
@@ -842,43 +848,43 @@ fn thinking_extension_key_is_cross_format_safe(
namespace: &str,
key: &str,
) -> bool {
match (source, target, namespace, key) {
matches!(
(source, target, namespace, key),
(
FormatId::OpenAiChat,
FormatId::OpenAiResponses
| FormatId::OpenAiResponsesCompact
| FormatId::ClaudeMessages
| FormatId::GeminiGenerateContent,
| FormatId::OpenAiResponsesCompact
| FormatId::ClaudeMessages
| FormatId::GeminiGenerateContent,
"openai",
"reasoning_effort",
) => true,
(
) | (
FormatId::OpenAiResponses | FormatId::OpenAiResponsesCompact,
FormatId::OpenAiChat | FormatId::ClaudeMessages | FormatId::GeminiGenerateContent,
"openai_responses" | "openai_cli",
"effort",
) => true,
(FormatId::ClaudeMessages, _, "claude", "type" | "budget_tokens" | "output_config") => true,
(
) | (
FormatId::ClaudeMessages,
_,
"claude",
"type" | "budget_tokens" | "output_config",
) | (
FormatId::ClaudeMessages,
FormatId::OpenAiChat | FormatId::OpenAiResponses | FormatId::OpenAiResponsesCompact,
"openai",
"reasoning_effort",
) => true,
(
) | (
FormatId::GeminiGenerateContent,
_,
"gemini",
"thinking_config" | "includeThoughts" | "thinkingBudget" | "thinkingLevel",
) => true,
(
) | (
FormatId::GeminiGenerateContent,
FormatId::OpenAiChat | FormatId::OpenAiResponses | FormatId::OpenAiResponsesCompact,
"openai",
"reasoning_effort",
) => true,
_ => false,
}
)
)
}
fn response_format_extension_key_is_cross_format_safe(namespace: &str, key: &str) -> bool {
@@ -1674,16 +1680,15 @@ fn validate_claude_cross_format_request(body: &Value, target: FormatId) -> Resul
}
match target {
FormatId::OpenAiChat => {
if claude_request_contains_tool_result_content_array(body) {
return Err(FormatError::LossyConversionBlocked {
source_format: FormatId::ClaudeMessages.as_str().to_string(),
target_format: target.as_str().to_string(),
field: "messages[].content[].tool_result.content".to_string(),
reason: "OpenAI Chat tool messages cannot losslessly preserve Claude multi-block tool_result content".to_string(),
});
}
FormatId::OpenAiChat if claude_request_contains_tool_result_content_array(body) => {
return Err(FormatError::LossyConversionBlocked {
source_format: FormatId::ClaudeMessages.as_str().to_string(),
target_format: target.as_str().to_string(),
field: "messages[].content[].tool_result.content".to_string(),
reason: "OpenAI Chat tool messages cannot losslessly preserve Claude multi-block tool_result content".to_string(),
});
}
FormatId::OpenAiChat => {}
FormatId::OpenAiResponses | FormatId::OpenAiResponsesCompact => {
if claude_request_contains_message_thinking_blocks(body) {
return Err(FormatError::LossyConversionBlocked {
@@ -1702,16 +1707,17 @@ fn validate_claude_cross_format_request(body: &Value, target: FormatId) -> Resul
});
}
}
FormatId::GeminiGenerateContent => {
if claude_request_contains_redacted_thinking_blocks(body) {
return Err(FormatError::LossyConversionBlocked {
source_format: FormatId::ClaudeMessages.as_str().to_string(),
target_format: target.as_str().to_string(),
field: "messages[].content[].type".to_string(),
reason: "Gemini request parts cannot losslessly preserve Claude redacted thinking blocks".to_string(),
});
}
FormatId::GeminiGenerateContent
if claude_request_contains_redacted_thinking_blocks(body) =>
{
return Err(FormatError::LossyConversionBlocked {
source_format: FormatId::ClaudeMessages.as_str().to_string(),
target_format: target.as_str().to_string(),
field: "messages[].content[].type".to_string(),
reason: "Gemini request parts cannot losslessly preserve Claude redacted thinking blocks".to_string(),
});
}
FormatId::GeminiGenerateContent => {}
_ => {}
}
Ok(())
@@ -3566,9 +3566,7 @@ mod tests {
#[test]
fn gemini_stream_aggregation_rejects_unknown_parts() {
let body = concat!(
"data: {\"responseId\":\"resp_gem_unknown_123\",\"modelVersion\":\"gemini-2.5-pro\",\"candidates\":[{\"index\":0,\"content\":{\"role\":\"model\",\"parts\":[{\"futurePart\":{\"kept\":true}}]}}]}\n\n",
);
let body = "data: {\"responseId\":\"resp_gem_unknown_123\",\"modelVersion\":\"gemini-2.5-pro\",\"candidates\":[{\"index\":0,\"content\":{\"role\":\"model\",\"parts\":[{\"futurePart\":{\"kept\":true}}]}}]}\n\n";
assert!(
aggregate_gemini_stream_sync_response(body.as_bytes()).is_none(),
@@ -3582,9 +3580,7 @@ mod tests {
"provider_api_format": "gemini:generate_content",
"client_api_format": "openai:chat",
});
let stream_body = concat!(
"data: {\"responseId\":\"resp_gem_unknown_456\",\"modelVersion\":\"gemini-2.5-pro\",\"candidates\":[{\"index\":0,\"content\":{\"role\":\"model\",\"parts\":[{\"futurePart\":{\"kept\":true}}]}}]}\n\n",
);
let stream_body = "data: {\"responseId\":\"resp_gem_unknown_456\",\"modelVersion\":\"gemini-2.5-pro\",\"candidates\":[{\"index\":0,\"content\":{\"role\":\"model\",\"parts\":[{\"futurePart\":{\"kept\":true}}]}}]}\n\n";
let provider_body_json = json!({
"responseId": "resp_gem_unknown_456",
"modelVersion": "gemini-2.5-pro",
@@ -5079,9 +5075,7 @@ mod tests {
"model": "claude-sonnet-4-5",
"mapped_model": "gpt-5",
});
let stream_body = concat!(
"data: {\"id\":\"chatcmpl_unknown_stream\",\"object\":\"chat.completion.chunk\",\"model\":\"gpt-5\",\"choices\":[{\"index\":0,\"delta\":{\"future_delta\":\"x\"}}]}\n\n",
);
let stream_body = "data: {\"id\":\"chatcmpl_unknown_stream\",\"object\":\"chat.completion.chunk\",\"model\":\"gpt-5\",\"choices\":[{\"index\":0,\"delta\":{\"future_delta\":\"x\"}}]}\n\n";
let fallback_body = json!({
"id": "chatcmpl_fallback",
"object": "chat.completion",