mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
fix: surface Codex ultra reasoning usage
Restore the official Ultra preset from the proactive multi-agent marker while preserving the max effort sent upstream. Show subagent requests with an inline icon in both usage tables and keep their wire effort visible. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -102,6 +102,7 @@ struct DashboardUsageRecordData {
|
||||
client_ip: Option<String>,
|
||||
user_agent: Option<String>,
|
||||
reasoning_effort: Option<String>,
|
||||
reasoning_preset: Option<String>,
|
||||
compact: bool,
|
||||
request_kind: Option<String>,
|
||||
subagent_kind: Option<String>,
|
||||
@@ -517,6 +518,7 @@ fn dashboard_usage_record_data(
|
||||
let user_agent = metadata_string(&record.metadata, &["userAgent", "user_agent"]);
|
||||
let reasoning_effort =
|
||||
metadata_string(&record.metadata, &["reasoningEffort", "reasoning_effort"]);
|
||||
let reasoning_preset = metadata_string(&record.metadata, &["reasoningPreset"]);
|
||||
let compact = record
|
||||
.metadata
|
||||
.get("compact")
|
||||
@@ -541,6 +543,7 @@ fn dashboard_usage_record_data(
|
||||
client_ip,
|
||||
user_agent,
|
||||
reasoning_effort,
|
||||
reasoning_preset,
|
||||
compact,
|
||||
request_kind,
|
||||
subagent_kind,
|
||||
|
||||
@@ -88,6 +88,7 @@ pub(crate) struct UsageRecordData {
|
||||
client_ip: Option<String>,
|
||||
user_agent: Option<String>,
|
||||
reasoning_effort: Option<String>,
|
||||
reasoning_preset: Option<String>,
|
||||
compact: bool,
|
||||
request_kind: Option<String>,
|
||||
subagent_kind: Option<String>,
|
||||
@@ -442,6 +443,7 @@ fn usage_record_data(
|
||||
let user_agent = metadata_string(&record.metadata, &["userAgent", "user_agent"]);
|
||||
let reasoning_effort =
|
||||
metadata_string(&record.metadata, &["reasoningEffort", "reasoning_effort"]);
|
||||
let reasoning_preset = metadata_string(&record.metadata, &["reasoningPreset"]);
|
||||
let compact = record
|
||||
.metadata
|
||||
.get("compact")
|
||||
@@ -463,6 +465,7 @@ fn usage_record_data(
|
||||
client_ip,
|
||||
user_agent,
|
||||
reasoning_effort,
|
||||
reasoning_preset,
|
||||
compact,
|
||||
request_kind,
|
||||
subagent_kind,
|
||||
|
||||
@@ -424,6 +424,7 @@ fn response_request_summary(
|
||||
"compact": semantics.compact,
|
||||
"requestKind": semantics.request_kind,
|
||||
"subagentKind": semantics.subagent_kind,
|
||||
"reasoningPreset": semantics.reasoning_preset,
|
||||
"transport": backend_transport_name(transport),
|
||||
"inputType": json_value_kind(input),
|
||||
"inputItemsCount": input.and_then(Value::as_array).map(Vec::len),
|
||||
|
||||
@@ -439,6 +439,7 @@ pub(crate) fn enrich_response_request_semantics(
|
||||
object.insert("compact".to_string(), Value::Bool(semantics.compact));
|
||||
insert_trimmed_string(object, "requestKind", semantics.request_kind.as_deref());
|
||||
insert_trimmed_string(object, "subagentKind", semantics.subagent_kind.as_deref());
|
||||
insert_trimmed_string(object, "reasoningPreset", semantics.reasoning_preset);
|
||||
}
|
||||
|
||||
pub(crate) fn reasoning_effort_from_request(request: &CodexResponsesRequest) -> Option<&str> {
|
||||
|
||||
@@ -11,6 +11,10 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
const MULTI_AGENT_MODE_OPEN_TAG: &str = "<multi_agent_mode>";
|
||||
const MULTI_AGENT_MODE_CLOSE_TAG: &str = "</multi_agent_mode>";
|
||||
const PROACTIVE_MULTI_AGENT_MODE_PREFIX: &str = "Proactive multi-agent delegation is active.";
|
||||
|
||||
/// Codex Responses 上游请求体。
|
||||
///
|
||||
/// 发往上游的 Responses 请求。`body` 持有客户端原始 JSON object,逐字段(含顺序、
|
||||
@@ -97,6 +101,8 @@ pub struct CodexRequestSemantics {
|
||||
pub request_kind: Option<String>,
|
||||
/// Codex turn metadata 中的子代理类型。
|
||||
pub subagent_kind: Option<String>,
|
||||
/// Codex 客户端选择的推理预设。
|
||||
pub reasoning_preset: Option<&'static str>,
|
||||
/// 请求是否为远端压缩。
|
||||
pub compact: bool,
|
||||
}
|
||||
@@ -696,7 +702,7 @@ impl CodexResponsesRequest {
|
||||
self.body.get("client_metadata")
|
||||
}
|
||||
|
||||
/// 提取 Codex 请求类型、子代理类型与压缩语义。
|
||||
/// 提取 Codex 请求类型、子代理类型、推理预设与压缩语义。
|
||||
pub fn semantics(&self) -> CodexRequestSemantics {
|
||||
let turn_metadata = self
|
||||
.turn_metadata
|
||||
@@ -717,6 +723,16 @@ impl CodexResponsesRequest {
|
||||
.and_then(|metadata| metadata.get("subagent_kind"))
|
||||
.and_then(Value::as_str)
|
||||
.and_then(non_empty_owned_string);
|
||||
let reasoning_preset = (subagent_kind.is_none()
|
||||
&& self
|
||||
.reasoning()
|
||||
.and_then(|value| value.get("effort"))
|
||||
.and_then(Value::as_str)
|
||||
== Some("max")
|
||||
&& self
|
||||
.latest_multi_agent_mode()
|
||||
.is_some_and(|mode| mode.starts_with(PROACTIVE_MULTI_AGENT_MODE_PREFIX)))
|
||||
.then_some("ultra");
|
||||
let compact = request_kind.as_deref() == Some("compaction")
|
||||
|| self
|
||||
.input()
|
||||
@@ -726,10 +742,19 @@ impl CodexResponsesRequest {
|
||||
CodexRequestSemantics {
|
||||
request_kind,
|
||||
subagent_kind,
|
||||
reasoning_preset,
|
||||
compact,
|
||||
}
|
||||
}
|
||||
|
||||
fn latest_multi_agent_mode(&self) -> Option<&str> {
|
||||
latest_multi_agent_mode(self.input()).or_else(|| {
|
||||
self.local_replay_input
|
||||
.as_deref()
|
||||
.and_then(|input| latest_multi_agent_mode(input.as_slice()))
|
||||
})
|
||||
}
|
||||
|
||||
/// 设置 / 合并 client metadata。
|
||||
pub fn set_client_metadata(&mut self, client_metadata: Option<Value>) {
|
||||
match client_metadata {
|
||||
@@ -772,3 +797,25 @@ fn non_empty_owned_string(value: &str) -> Option<String> {
|
||||
let value = value.trim();
|
||||
(!value.is_empty()).then(|| value.to_string())
|
||||
}
|
||||
|
||||
fn latest_multi_agent_mode(input: &[Value]) -> Option<&str> {
|
||||
input.iter().rev().find_map(|item| {
|
||||
if item.get("role").and_then(Value::as_str) != Some("developer") {
|
||||
return None;
|
||||
}
|
||||
item.get("content")?
|
||||
.as_array()?
|
||||
.iter()
|
||||
.rev()
|
||||
.filter_map(|content| content.get("text").and_then(Value::as_str))
|
||||
.find_map(multi_agent_mode_from_text)
|
||||
})
|
||||
}
|
||||
|
||||
fn multi_agent_mode_from_text(text: &str) -> Option<&str> {
|
||||
let close = text.rfind(MULTI_AGENT_MODE_CLOSE_TAG)?;
|
||||
let before_close = &text[..close];
|
||||
let open = before_close.rfind(MULTI_AGENT_MODE_OPEN_TAG)?;
|
||||
let body = &before_close[open + MULTI_AGENT_MODE_OPEN_TAG.len()..];
|
||||
Some(body.trim())
|
||||
}
|
||||
|
||||
@@ -403,10 +403,12 @@ async fn dashboard_summary_should_return_backend_formatted_time_fields() {
|
||||
)
|
||||
.await;
|
||||
let record_at = Utc::now() - Duration::seconds(1);
|
||||
store
|
||||
.append(&usage_record_with_tokens(record_at, 10))
|
||||
.await
|
||||
.unwrap();
|
||||
let mut record = usage_record_with_tokens(record_at, 10);
|
||||
record.metadata = json!({
|
||||
"reasoningEffort": "max",
|
||||
"reasoningPreset": "ultra"
|
||||
});
|
||||
store.append(&record).await.unwrap();
|
||||
|
||||
let body = dashboard_summary(app).await;
|
||||
|
||||
@@ -414,6 +416,9 @@ async fn dashboard_summary_should_return_backend_formatted_time_fields() {
|
||||
body["data"]["usageRecords"][0]["createdAtDisplay"],
|
||||
china_datetime(&record_at)
|
||||
);
|
||||
assert_eq!(body["data"]["usageRecords"][0]["reasoningEffort"], "max");
|
||||
assert_eq!(body["data"]["usageRecords"][0]["reasoningPreset"], "ultra");
|
||||
assert!(body["data"]["usageRecords"][0]["subagentKind"].is_null());
|
||||
assert!(body["data"]["usageRecords"][0].get("metadata").is_none());
|
||||
}
|
||||
|
||||
|
||||
@@ -46,10 +46,18 @@ async fn usage_route_returns_success_facts_without_legacy_level() {
|
||||
record.metadata = json!({
|
||||
"compact": true,
|
||||
"requestKind": "compaction",
|
||||
"reasoningEffort": "max",
|
||||
"reasoningPreset": "ultra"
|
||||
});
|
||||
usage.append(&record).await.unwrap();
|
||||
let mut subagent = success_record("subagent completed");
|
||||
subagent.id = "usage_route_subagent".to_string();
|
||||
subagent.client_api_key_id = Some("key_42".to_string());
|
||||
subagent.metadata = json!({
|
||||
"subagentKind": "thread_spawn",
|
||||
"reasoningEffort": "max"
|
||||
});
|
||||
usage.append(&record).await.unwrap();
|
||||
usage.append(&subagent).await.unwrap();
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
@@ -65,16 +73,28 @@ async fn usage_route_returns_success_facts_without_legacy_level() {
|
||||
let body = response_json(response).await;
|
||||
assert_eq!(body["data"]["page"]["page"], 1);
|
||||
assert_eq!(body["data"]["page"]["pageSize"], 50);
|
||||
assert_eq!(body["data"]["page"]["total"], 1);
|
||||
assert_eq!(body["data"]["page"]["total"], 2);
|
||||
assert!(body["data"]["page"].get("nextCursor").is_none());
|
||||
let item = &body["data"]["items"][0];
|
||||
let items = body["data"]["items"].as_array().unwrap();
|
||||
let item = items
|
||||
.iter()
|
||||
.find(|item| item["id"] == "usage_route_success")
|
||||
.unwrap();
|
||||
assert_eq!(item["id"], "usage_route_success");
|
||||
assert_eq!(item["clientApiKeyId"], "key_42");
|
||||
assert_eq!(item["statusCode"], 200);
|
||||
assert_eq!(item["compact"], true);
|
||||
assert_eq!(item["requestKind"], "compaction");
|
||||
assert_eq!(item["subagentKind"], "thread_spawn");
|
||||
assert!(item["subagentKind"].is_null());
|
||||
assert_eq!(item["reasoningEffort"], "max");
|
||||
assert_eq!(item["reasoningPreset"], "ultra");
|
||||
let subagent = items
|
||||
.iter()
|
||||
.find(|item| item["id"] == "usage_route_subagent")
|
||||
.unwrap();
|
||||
assert_eq!(subagent["subagentKind"], "thread_spawn");
|
||||
assert_eq!(subagent["reasoningEffort"], "max");
|
||||
assert!(subagent["reasoningPreset"].is_null());
|
||||
assert!(item.get("level").is_none());
|
||||
}
|
||||
|
||||
|
||||
@@ -138,7 +138,15 @@ async fn responses_should_use_imported_account_record_usage_cookie_and_usage_rec
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"model": "gpt-5.5",
|
||||
"input": [],
|
||||
"reasoning": {"effort": "max"},
|
||||
"input": [{
|
||||
"type": "message",
|
||||
"role": "developer",
|
||||
"content": [{
|
||||
"type": "input_text",
|
||||
"text": "<multi_agent_mode>Proactive multi-agent delegation is active.</multi_agent_mode>"
|
||||
}]
|
||||
}],
|
||||
"stream": false,
|
||||
"use_websocket": false
|
||||
})
|
||||
@@ -177,8 +185,9 @@ async fn responses_should_use_imported_account_record_usage_cookie_and_usage_rec
|
||||
assert_eq!(event.account_id.as_deref(), Some("acct_chat"));
|
||||
assert_eq!(event.status_code, Some(200));
|
||||
assert_eq!(event.response_id.as_deref(), Some("resp_usage"));
|
||||
assert_eq!(event.response_id.as_deref(), Some("resp_usage"));
|
||||
assert_eq!(metadata["stream"], false);
|
||||
assert_eq!(metadata["reasoningEffort"], "max");
|
||||
assert_eq!(metadata["reasoningPreset"], "ultra");
|
||||
assert_eq!(event.input_tokens, Some(7));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use super::*;
|
||||
@@ -447,6 +449,120 @@ fn responses_request_semantics_should_detect_compaction_trigger_without_metadata
|
||||
assert!(request.semantics().compact);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn responses_request_semantics_should_restore_ultra_from_proactive_mode() {
|
||||
let body = json!({
|
||||
"model": "gpt-5.6-sol",
|
||||
"reasoning": {"effort": "max"},
|
||||
"input": [developer_multi_agent_mode(
|
||||
"Proactive multi-agent delegation is active. Use sub-agents when useful.",
|
||||
)]
|
||||
});
|
||||
|
||||
let request = build_codex_request(body.as_object().unwrap().clone(), &HeaderMap::new(), None);
|
||||
|
||||
assert_eq!(request.semantics().reasoning_preset, Some("ultra"));
|
||||
assert_eq!(request.reasoning().unwrap()["effort"], "max");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn responses_request_semantics_should_use_latest_multi_agent_mode() {
|
||||
let body = json!({
|
||||
"model": "gpt-5.6-sol",
|
||||
"reasoning": {"effort": "max"},
|
||||
"input": [
|
||||
developer_multi_agent_mode("Proactive multi-agent delegation is active."),
|
||||
developer_multi_agent_mode("Do not spawn sub-agents unless explicitly requested."),
|
||||
]
|
||||
});
|
||||
|
||||
let request = build_codex_request(body.as_object().unwrap().clone(), &HeaderMap::new(), None);
|
||||
|
||||
assert_eq!(request.semantics().reasoning_preset, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn responses_request_semantics_should_ignore_user_multi_agent_mode_marker() {
|
||||
let body = json!({
|
||||
"model": "gpt-5.6-sol",
|
||||
"reasoning": {"effort": "max"},
|
||||
"input": [{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [{
|
||||
"type": "input_text",
|
||||
"text": "<multi_agent_mode>Proactive multi-agent delegation is active.</multi_agent_mode>",
|
||||
}],
|
||||
}]
|
||||
});
|
||||
|
||||
let request = build_codex_request(body.as_object().unwrap().clone(), &HeaderMap::new(), None);
|
||||
|
||||
assert_eq!(request.semantics().reasoning_preset, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn responses_request_semantics_should_require_max_for_ultra() {
|
||||
let body = json!({
|
||||
"model": "gpt-5.6-sol",
|
||||
"reasoning": {"effort": "high"},
|
||||
"input": [developer_multi_agent_mode(
|
||||
"Proactive multi-agent delegation is active.",
|
||||
)]
|
||||
});
|
||||
|
||||
let request = build_codex_request(body.as_object().unwrap().clone(), &HeaderMap::new(), None);
|
||||
|
||||
assert_eq!(request.semantics().reasoning_preset, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn responses_request_semantics_should_not_mark_subagent_as_ultra() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
"x-codex-turn-metadata",
|
||||
r#"{"subagent_kind":"thread_spawn"}"#.parse().unwrap(),
|
||||
);
|
||||
let body = json!({
|
||||
"model": "gpt-5.6-sol",
|
||||
"reasoning": {"effort": "max"},
|
||||
"input": [developer_multi_agent_mode(
|
||||
"Proactive multi-agent delegation is active.",
|
||||
)]
|
||||
});
|
||||
|
||||
let request = build_codex_request(body.as_object().unwrap().clone(), &headers, None);
|
||||
|
||||
assert_eq!(request.semantics().reasoning_preset, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn responses_request_semantics_should_read_mode_from_local_replay_history() {
|
||||
let body = json!({
|
||||
"model": "gpt-5.6-sol",
|
||||
"reasoning": {"effort": "max"},
|
||||
"input": [{"type": "message", "role": "user", "content": []}]
|
||||
});
|
||||
let mut request =
|
||||
build_codex_request(body.as_object().unwrap().clone(), &HeaderMap::new(), None);
|
||||
request.local_replay_input = Some(Arc::new(vec![developer_multi_agent_mode(
|
||||
"Proactive multi-agent delegation is active.",
|
||||
)]));
|
||||
|
||||
assert_eq!(request.semantics().reasoning_preset, Some("ultra"));
|
||||
}
|
||||
|
||||
fn developer_multi_agent_mode(mode: &str) -> Value {
|
||||
json!({
|
||||
"type": "message",
|
||||
"role": "developer",
|
||||
"content": [{
|
||||
"type": "input_text",
|
||||
"text": format!("<multi_agent_mode>{mode}</multi_agent_mode>"),
|
||||
}],
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_streaming_response_with_previous_response_should_require_websocket() {
|
||||
let body = json!({
|
||||
|
||||
@@ -6,7 +6,6 @@ import BaseTable from '@/components/base/BaseTable/index.vue'
|
||||
import {
|
||||
usageAccountText,
|
||||
usageIsCompact,
|
||||
usageReasoningEffort,
|
||||
usageRecordColumns,
|
||||
usageRecordType,
|
||||
usageRecordTypeClass,
|
||||
@@ -14,6 +13,7 @@ import {
|
||||
import UsageClientIpCell from '@/views/usage/components/UsageClientIpCell.vue'
|
||||
import UsageBillingCell from '@/views/usage/components/UsageBillingCell.vue'
|
||||
import UsageModelCell from '@/views/usage/components/UsageModelCell.vue'
|
||||
import UsageReasoningEffortCell from '@/views/usage/components/UsageReasoningEffortCell.vue'
|
||||
import UsageTokenCell from '@/views/usage/components/UsageTokenCell.vue'
|
||||
|
||||
defineProps<{
|
||||
@@ -57,9 +57,7 @@ const dashboardUsageRecordColumns = usageRecordColumns.filter((column) => column
|
||||
</template>
|
||||
|
||||
<template #reasoningEffort="{ row }">
|
||||
<span class="whitespace-nowrap text-[12px] font-[720] text-(--cp-text-primary)">
|
||||
{{ usageReasoningEffort(row) }}
|
||||
</span>
|
||||
<UsageReasoningEffortCell :record="row" />
|
||||
</template>
|
||||
|
||||
<template #route="{ row }">
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { Bot } from '@lucide/vue'
|
||||
|
||||
import { usageIsSubagent, usageReasoningEffort } from '../constants'
|
||||
|
||||
defineProps<{
|
||||
record: any
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
class="inline-flex items-center gap-1 whitespace-nowrap text-[12px] font-[720] text-(--cp-text-primary)"
|
||||
>
|
||||
<span>{{ usageReasoningEffort(record) }}</span>
|
||||
<span
|
||||
v-if="usageIsSubagent(record)"
|
||||
class="inline-flex shrink-0 text-(--cp-info-text)"
|
||||
title="子代理请求"
|
||||
aria-label="子代理请求"
|
||||
>
|
||||
<Bot class="size-3.25" stroke-width="2.2" />
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
@@ -243,7 +243,16 @@ export function usageUserAgent(record: any) {
|
||||
}
|
||||
|
||||
export function usageReasoningEffort(record: any) {
|
||||
return record?.reasoningEffort || record?.metadata?.reasoningEffort || '—'
|
||||
const reasoningEffort = record?.reasoningEffort || record?.metadata?.reasoningEffort || '—'
|
||||
if (usageIsSubagent(record)) {
|
||||
return reasoningEffort
|
||||
}
|
||||
|
||||
return record?.reasoningPreset || record?.metadata?.reasoningPreset || reasoningEffort
|
||||
}
|
||||
|
||||
export function usageIsSubagent(record: any) {
|
||||
return Boolean(record?.subagentKind)
|
||||
}
|
||||
|
||||
export function usageIsCompact(record: any) {
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
usageTimeRangeOptions,
|
||||
usageAccountText,
|
||||
usageIsCompact,
|
||||
usageReasoningEffort,
|
||||
usageRecordColumns,
|
||||
usageRecordType,
|
||||
usageRecordTypeClass,
|
||||
@@ -27,6 +26,7 @@ import UsageInsightsGrid from './components/UsageInsightsGrid.vue'
|
||||
import UsageModelCell from './components/UsageModelCell.vue'
|
||||
import OpsErrorPanel from './components/OpsErrorPanel.vue'
|
||||
import UsageRecordDetailModal from './components/UsageRecordDetailModal.vue'
|
||||
import UsageReasoningEffortCell from './components/UsageReasoningEffortCell.vue'
|
||||
import UsageSummaryCards from './components/UsageSummaryCards.vue'
|
||||
import UsageTokenCell from './components/UsageTokenCell.vue'
|
||||
|
||||
@@ -184,9 +184,7 @@ watch(timeRange, () => {
|
||||
</template>
|
||||
|
||||
<template #reasoningEffort="{ row }">
|
||||
<span class="whitespace-nowrap text-[12px] font-[720] text-(--cp-text-primary)">
|
||||
{{ usageReasoningEffort(row) }}
|
||||
</span>
|
||||
<UsageReasoningEffortCell :record="row" />
|
||||
</template>
|
||||
|
||||
<template #route="{ row }">
|
||||
|
||||
Reference in New Issue
Block a user