Compare commits

...
3 Commits
Author SHA1 Message Date
zyycnandCodex eff522f2b6 chore: bump version to 2.2.1
Co-authored-by: Codex <noreply@openai.com>
2026-07-13 08:47:46 +08:00
zyycnandCodex 65a175d48c 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>
2026-07-13 08:47:25 +08:00
zyycnandCodex f1872df293 fix: reload after updated service is ready
Track the target update version and poll the version endpoint through process replacement. Reload as soon as the new backend is ready without requiring the browser to observe a brief shutdown window.

Co-authored-by: Codex <noreply@openai.com>
2026-07-13 07:54:18 +08:00
16 changed files with 291 additions and 59 deletions
@@ -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,
+3
View File
@@ -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,
+1
View File
@@ -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),
+1
View File
@@ -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())
}
+9 -4
View File
@@ -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());
}
+24 -4
View File
@@ -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!({
+2 -9
View File
@@ -2,7 +2,6 @@ import request from '../request'
import { API_BASE_URL } from '../constants'
export const SYSTEM_UPDATE_EVENTS_URL = `${API_BASE_URL}/api/admin/system/update-events`
const SYSTEM_HEALTH_URL = `${API_BASE_URL}/healthz`
export interface SystemVersion {
version: string
@@ -47,20 +46,14 @@ export interface SystemOperationStarted {
needRestart?: boolean
}
export function getSystemVersion() {
export function getSystemVersion(options: { timeoutMs?: number } = {}) {
return request<SystemVersion>({
url: '/api/admin/system/version',
method: 'GET',
...(options.timeoutMs ? { timeout: options.timeoutMs } : {}),
})
}
export async function checkSystemHealth() {
const response = await fetch(SYSTEM_HEALTH_URL, { cache: 'no-store' })
if (!response.ok) {
throw new Error('service is not ready')
}
}
export function getSystemUpdateDetail(refresh = false) {
return request<SystemUpdateInfo>({
url: '/api/admin/system/update-detail',
+33 -29
View File
@@ -2,7 +2,6 @@ import { computed, ref, shallowRef } from 'vue'
import {
SYSTEM_UPDATE_EVENTS_URL,
checkSystemHealth,
getSystemVersion,
getSystemUpdateDetail,
performSystemUpdate,
@@ -38,14 +37,14 @@ const updateLogs = ref<SystemUpdateLog[]>([])
const updateStreaming = shallowRef(false)
const updateStreamError = shallowRef('')
const updateEventSource = shallowRef<EventSource | null>(null)
const restartTargetVersion = shallowRef('')
let loadVersionPromise: Promise<SystemVersion> | undefined
let loadSystemPromise: Promise<void> | undefined
const maxUpdateLogs = 200
const restartStopPollAttempts = 50
const restartStopPollIntervalMs = 200
const restartReadyPollAttempts = 60
const restartReadyPollIntervalMs = 1000
const restartReadyTimeoutMs = 60_000
const restartProbeTimeoutMs = 2_000
const restartReadyPollIntervalMs = 500
const hasUpdate = computed(() => Boolean(updateInfo.value?.hasUpdate ?? version.value?.hasUpdate))
const isReleaseBuild = computed(() => updateInfo.value?.buildType === 'release')
@@ -63,6 +62,7 @@ function resetUpdateResult() {
updateError.value = ''
updateSuccess.value = false
needRestart.value = false
restartTargetVersion.value = ''
}
function isUpdateLog(value: unknown): value is SystemUpdateLog {
@@ -206,6 +206,7 @@ async function updateNow(targetVersion: string) {
const result = await performSystemUpdate(confirmedTargetVersion)
updateSuccess.value = true
needRestart.value = result.needRestart
restartTargetVersion.value = result.needRestart ? normalizeVersion(result.targetVersion) : ''
updateInfo.value = {
...currentInfo,
latestVersion: result.targetVersion,
@@ -233,39 +234,36 @@ function delay(ms: number) {
}
async function waitForServiceAndReload() {
let stopped = false
for (let attempt = 0; attempt < restartStopPollAttempts; attempt += 1) {
try {
await checkSystemHealth()
} catch {
stopped = true
break
}
await delay(restartStopPollIntervalMs)
}
const expectedVersion = restartTargetVersion.value
const deadline = Date.now() + restartReadyTimeoutMs
if (!stopped) {
updateError.value = '未检测到服务停止,请检查重启进程状态'
restarting.value = false
return
}
for (let attempt = 0; attempt < restartReadyPollAttempts; attempt += 1) {
while (Date.now() < deadline) {
try {
await checkSystemHealth()
window.location.reload()
return
} catch {
if (attempt < restartReadyPollAttempts - 1) {
await delay(restartReadyPollIntervalMs)
const readyVersion = await getSystemVersion({ timeoutMs: restartProbeTimeoutMs })
if (normalizeVersion(readyVersion.version) === expectedVersion) {
window.location.reload()
return
}
} catch {
// 进程切换期间短暂不可达,继续等待目标版本就绪
}
const remainingMs = deadline - Date.now()
if (remainingMs > 0) {
await delay(Math.min(restartReadyPollIntervalMs, remainingMs))
}
}
updateError.value = '服务重启恢复超时,请检查进程是否已被自重启或外部守护进程拉起'
updateError.value = `服务未在预期时间内启动 v${expectedVersion}`
restarting.value = false
}
function normalizeVersion(value: unknown) {
return String(value ?? '')
.trim()
.replace(/^v/i, '')
}
function apiErrorStatus(error: unknown) {
return error instanceof ApiError ? error.status : 0
}
@@ -273,6 +271,12 @@ function apiErrorStatus(error: unknown) {
async function restartNow() {
if (restarting.value) return
if (!restartTargetVersion.value) {
const error = new Error('缺少待生效的目标版本')
updateError.value = error.message
throw error
}
restarting.value = true
updateError.value = ''
disconnectUpdateEvents()
@@ -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>
+10 -1
View File
@@ -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) {
+2 -4
View File
@@ -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 }">
+1 -1
View File
@@ -1 +1 @@
version: "2.2.0"
version: "2.2.1"