feat: 扩展 cache creation token 细分统计与 effective_input_tokens 计费逻辑

- 新增 cache_creation_ephemeral_5m/1h_input_tokens 字段,区分不同 TTL 的缓存写入 token
- 引入 effective_input_tokens(扣除 cache read 后的有效输入 token),暴露给 usage 接口
- billing 规则生成器支持 5m/1h ephemeral cache 独立定价与分级计费
- usage_mapper 增加 Claude/Anthropic 格式映射,修复 OpenAI responses 格式字段兼容性
- 迁移逻辑增强:支持 checksum 容错、applied/pending 数量日志、逐步执行信息输出
- executor 抽离 LocalExecutionRequestOutcome 类型,统一 sync/stream 路径返回语义
- provider-transport auth 层新增 complete passthrough headers 构建逻辑
- 前端 usage 类型全面补充 effective_input_tokens、cache_creation_tokens、total_input_context 字段
This commit is contained in:
fawney19
2026-04-10 17:44:55 +08:00
parent 5014e2f5fd
commit 010ab127e2
64 changed files with 4217 additions and 477 deletions

View File

@@ -184,6 +184,7 @@ pub fn apply_codex_openai_cli_special_headers(
if let Some(account_id) = extract_codex_account_id(decrypted_auth_config_raw) {
provider_request_headers.insert("chatgpt-account-id".to_string(), account_id);
}
if !provider_request_headers
.get("x-client-request-id")
.map(|value| !value.trim().is_empty())
@@ -200,6 +201,7 @@ pub fn apply_codex_openai_cli_special_headers(
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty());
let Some(short_id) = prompt_cache_key.and_then(build_short_codex_header_id) else {
return;
};
@@ -213,6 +215,7 @@ pub fn apply_codex_openai_cli_special_headers(
.eq_ignore_ascii_case("openai:compact")
&& !header_map_has_non_empty_value(original_headers, "conversation_id")
{
provider_request_headers.insert("conversation_id".to_string(), short_id);
let session_id = provider_request_headers.get("session_id").unwrap();
provider_request_headers.insert("conversation_id".to_string(), session_id.clone());
}
}

View File

@@ -124,8 +124,18 @@ pub fn build_cross_format_openai_cli_request_body(
#[cfg(test)]
mod tests {
use super::build_local_openai_cli_request_body;
use super::{build_cross_format_openai_cli_request_body, build_local_openai_chat_request_body};
use serde_json::json;
use serde_json::{json, Value};
fn object_keys(value: &Value) -> Vec<&str> {
value
.as_object()
.expect("json object")
.keys()
.map(String::as_str)
.collect()
}
#[test]
fn builds_openai_chat_cross_format_request_body_from_openai_cli_source() {
@@ -148,6 +158,29 @@ mod tests {
assert_eq!(provider_request_body["messages"][0]["content"], "hello");
}
#[test]
fn local_openai_cli_request_body_preserves_original_field_order() {
let body_json: Value = serde_json::from_str(
r#"{
"model": "gpt-5",
"include": ["reasoning.encrypted_content"],
"input": [],
"instructions": "Keep order"
}"#,
)
.expect("request json should parse");
let provider_request_body =
build_local_openai_cli_request_body(&body_json, "gpt-5-upstream", false)
.expect("openai cli body should build");
assert_eq!(
object_keys(&provider_request_body),
vec!["model", "include", "input", "instructions"]
);
assert_eq!(provider_request_body["model"], "gpt-5-upstream");
}
#[test]
fn builds_streaming_local_openai_chat_request_body_with_include_usage() {
let body_json = json!({