From ad16dec3dbb62186bd164ca0bbaf4aa4d455eebc Mon Sep 17 00:00:00 2001 From: Zyy Date: Thu, 3 Sep 2026 15:33:18 +0800 Subject: [PATCH] fix(openai): align official client routes Remove unsupported review and model extension routes, preserve review semantics on standard Responses requests, and refine usage observability for review and empty performance data. Co-authored-by: Codex --- README.md | 5 +- .../crates/gateway-api/src/openai/models.rs | 60 ------------- .../gateway-api/src/openai/responses/http.rs | 34 +------ .../gateway-api/src/openai/responses/mod.rs | 2 +- .../src/openai/responses/request.rs | 21 ++--- .../openai/responses/websocket/protocol.rs | 1 - .../crates/gateway-api/src/openai/router.rs | 7 +- .../crates/gateway-api/tests/openai/models.rs | 40 --------- .../tests/openai/responses/http.rs | 88 +------------------ .../crates/gateway-api/tests/openai/router.rs | 50 ++++++++++- .../src/transport/protocol/responses.rs | 7 +- docs/api.md | 6 +- .../usage/components/UsagePerformanceCard.vue | 9 +- .../components/UsageReasoningEffortCell.vue | 14 ++- frontend/src/views/usage/constants.ts | 2 +- frontend/src/views/usage/utils/records.ts | 4 + skills/codex-proxy-rs/SKILL.md | 2 +- .../codex-proxy-rs/references/repo-guide.md | 9 +- 18 files changed, 96 insertions(+), 265 deletions(-) diff --git a/README.md b/README.md index cc1323ab3..20dda7a53 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ | 领域 | 当前能力 | | --- | --- | -| 数据面 | Responses JSON、SSE、WebSocket、review 子代理、Images 生成/编辑与模型目录 | +| 数据面 | Responses JSON、SSE、WebSocket(含 Codex 子代理语义)、Images 生成/编辑与模型目录 | | Provider | 固定 `openai`、`xai` 两个 Provider,各自拥有 OAuth、credential、额度、目录与 transport | | 路由 | Client Key 限定账号分组;按模型能力编译 Provider 候选;支持全局精确模型映射、会话亲和和安全 fallback | | 透明边界 | OpenAI 保留未知请求字段及字段顺序,并原样转发 SSE、WebSocket 与 Images 业务字节;xAI 在 Provider 内转换协议 | @@ -136,12 +136,9 @@ curl http://127.0.0.1:8080/v1/responses \ | --- | --- | | `POST /v1/responses` | JSON 响应或 SSE Responses 流 | | `GET /v1/responses` | Responses WebSocket 升级 | -| `POST /v1/responses/review` | review 子代理请求 | | `POST /v1/images/generations` | OpenAI Images 生成 JSON 透明转发 | | `POST /v1/images/edits` | OpenAI Images 编辑 JSON 透明转发 | | `GET /v1/models` | 当前 Client Key 账号范围内的模型列表 | -| `GET /v1/models/catalog` | Codex 客户端展示用模型目录 | -| `GET /v1/models/{model_id}/info` | Codex 客户端展示用模型详情 | | `GET /v1/models/{model_id}` | OpenAI 兼容模型详情 | 所有 `/v1/*` 路由都需要 Client Key。Images 是 OpenAI Provider 自有端点,不读取模型字段,也不参与 diff --git a/backend/crates/gateway-api/src/openai/models.rs b/backend/crates/gateway-api/src/openai/models.rs index cfd4bb7c0..448881405 100644 --- a/backend/crates/gateway-api/src/openai/models.rs +++ b/backend/crates/gateway-api/src/openai/models.rs @@ -97,50 +97,6 @@ pub(crate) async fn model_detail( .into_response() } -/// `GET /v1/models/catalog`。 -/// -/// 新架构的目录由 core 按客户端 Provider 范围裁剪;这里不读取或重建任一 -/// Provider 的 wire catalog,只恢复 Codex 客户端依赖的稳定展示合同。 -pub(crate) async fn model_catalog(State(state): State, headers: HeaderMap) -> Response { - let service = state.openai(); - let client = match authenticate_client(service, &headers) { - Ok(client) => client, - Err(error) => return client_access_error_response(error), - }; - let models = service - .public_models(&client) - .into_iter() - .map(|model| catalog_model_json(model.as_str())) - .collect::>(); - - (StatusCode::OK, Json(models)).into_response() -} - -/// `GET /v1/models/{model_id}/info`。 -pub(crate) async fn model_info( - State(state): State, - headers: HeaderMap, - Path(model_id): Path, -) -> Response { - let service = state.openai(); - let client = match authenticate_client(service, &headers) { - Ok(client) => client, - Err(error) => return client_access_error_response(error), - }; - let Ok(public_model) = PublicModelId::new(model_id) else { - return model_not_found_response().into_response(); - }; - if !service.contains_public_model(&client, &public_model) { - return model_not_found_response().into_response(); - } - - ( - StatusCode::OK, - Json(catalog_model_json(public_model.as_str())), - ) - .into_response() -} - fn openai_model_json(id: &str) -> Value { json!({ "id": id, @@ -150,22 +106,6 @@ fn openai_model_json(id: &str) -> Value { }) } -fn catalog_model_json(id: &str) -> Value { - json!({ - "id": id, - "displayName": id, - "description": "", - "isDefault": false, - "supportedReasoningEfforts": [], - "defaultReasoningEffort": "", - "inputModalities": ["text"], - "outputModalities": ["text"], - "supportsPersonality": false, - "upgrade": Value::Null, - "source": "gateway", - }) -} - fn codex_model_json(profile: &PublicModelProfile, index: usize) -> Value { let id = profile.model().as_str(); let presentation = profile.presentation(); diff --git a/backend/crates/gateway-api/src/openai/responses/http.rs b/backend/crates/gateway-api/src/openai/responses/http.rs index f5990b5d5..a4f34bf88 100644 --- a/backend/crates/gateway-api/src/openai/responses/http.rs +++ b/backend/crates/gateway-api/src/openai/responses/http.rs @@ -32,7 +32,7 @@ use crate::openai::{ use super::{ OpenAiResponsesEncoder, ProtocolErrorBody, ResponseEncodeError, - request::{decode_request_with_headers, decode_review_request_with_headers}, + request::decode_request_with_headers, }; /// `POST /v1/responses`。 @@ -41,37 +41,13 @@ pub(crate) async fn responses( connect_info: Option>>, headers: HeaderMap, body: Bytes, -) -> Response { - handle_responses(state, connect_info, headers, body, false).await -} - -/// `POST /v1/responses/review`。 -pub(crate) async fn review_responses( - State(state): State, - connect_info: Option>>, - headers: HeaderMap, - body: Bytes, -) -> Response { - handle_responses(state, connect_info, headers, body, true).await -} - -async fn handle_responses( - state: ApiState, - connect_info: Option>>, - headers: HeaderMap, - body: Bytes, - review: bool, ) -> Response { let service = state.openai(); let client = match authenticate_client(service, &headers) { Ok(client) => client, Err(error) => return client_access_error_response(error), }; - let decoded = match if review { - decode_review_request_with_headers(&body, &headers) - } else { - decode_request_with_headers(&body, &headers) - } { + let decoded = match decode_request_with_headers(&body, &headers) { Ok(decoded) => decoded, Err(error) => { return protocol_error_response(StatusCode::BAD_REQUEST, error.protocol_body()); @@ -100,11 +76,7 @@ async fn handle_responses( } else { ClientTransport::HttpJson }, - if review { - "/v1/responses/review" - } else { - "/v1/responses" - }, + "/v1/responses", ) .await { diff --git a/backend/crates/gateway-api/src/openai/responses/mod.rs b/backend/crates/gateway-api/src/openai/responses/mod.rs index ba2223419..daa0aa2de 100644 --- a/backend/crates/gateway-api/src/openai/responses/mod.rs +++ b/backend/crates/gateway-api/src/openai/responses/mod.rs @@ -7,9 +7,9 @@ mod response; pub mod websocket; pub use error::{ProtocolError, ProtocolErrorBody, RequestDecodeError, ResponseEncodeError}; +pub(crate) use http::responses; pub(super) use http::{PendingExecution, request_client_context}; pub use http::{collect_execution_response, stream_execution_response}; -pub(crate) use http::{responses, review_responses}; pub use request::{ ContinuationIntent, DecodedResponsesRequest, OpenAiRequestHeaders, ResponsesRequestMetadata, decode_request_with_headers, diff --git a/backend/crates/gateway-api/src/openai/responses/request.rs b/backend/crates/gateway-api/src/openai/responses/request.rs index f11d09695..132c08e66 100644 --- a/backend/crates/gateway-api/src/openai/responses/request.rs +++ b/backend/crates/gateway-api/src/openai/responses/request.rs @@ -13,7 +13,6 @@ use serde_json::{Map, Value}; use super::error::RequestDecodeError; const OPENAI_PROTOCOL: &str = "openai"; -const REVIEW_SUBAGENT: &str = "review"; const OPENAI_SUBAGENT_KEY: &str = "x-openai-subagent"; const CODEX_TURN_METADATA_KEY: &str = "x-codex-turn-metadata"; const PASSTHROUGH_HEADERS_CONTEXT_KEY: &str = "opaque_request_headers"; @@ -75,8 +74,8 @@ impl OpenAiRequestHeaders { } } - fn apply_subagent(&self, body: &mut Map, forced_subagent: Option<&str>) { - if let Some(subagent) = forced_subagent.or(self.subagent.as_deref()) { + fn apply_subagent(&self, body: &mut Map) { + if let Some(subagent) = self.subagent.as_deref() { inject_subagent_metadata(body, subagent); } } @@ -301,20 +300,11 @@ pub fn decode_request_with_headers( body: &[u8], headers: &HeaderMap, ) -> Result { - decode_request_inner(body, false, &OpenAiRequestHeaders::from_headers(headers)) -} - -/// 解码 `POST /v1/responses/review` 请求并冻结 review subagent 语义。 -pub(super) fn decode_review_request_with_headers( - body: &[u8], - headers: &HeaderMap, -) -> Result { - decode_request_inner(body, true, &OpenAiRequestHeaders::from_headers(headers)) + decode_request_inner(body, &OpenAiRequestHeaders::from_headers(headers)) } pub(super) fn decode_request_inner( body: &[u8], - review: bool, request_headers: &OpenAiRequestHeaders, ) -> Result { let value = @@ -322,13 +312,12 @@ pub(super) fn decode_request_inner( let Value::Object(object) = value else { return Err(RequestDecodeError::ExpectedObject); }; - decode_request_object(object, review, request_headers, RequestDecodeSource::Http) + decode_request_object(object, request_headers, RequestDecodeSource::Http) } /// 解码已解析的顶层 object;按下游传输来源恢复连接级协议上下文。 pub(super) fn decode_request_object( mut object: Map, - review: bool, request_headers: &OpenAiRequestHeaders, source: RequestDecodeSource, ) -> Result { @@ -359,7 +348,7 @@ pub(super) fn decode_request_object( .map(|response_id| ContinuationIntent::PreviousResponseId(response_id.to_owned())) .unwrap_or(ContinuationIntent::None); - request_headers.apply_subagent(&mut object, review.then_some(REVIEW_SUBAGENT)); + request_headers.apply_subagent(&mut object); let frame_turn_metadata = match source { RequestDecodeSource::Http => None, RequestDecodeSource::WebSocketFrame => frame_turn_metadata(&object), diff --git a/backend/crates/gateway-api/src/openai/responses/websocket/protocol.rs b/backend/crates/gateway-api/src/openai/responses/websocket/protocol.rs index 9dbaca187..97243467e 100644 --- a/backend/crates/gateway-api/src/openai/responses/websocket/protocol.rs +++ b/backend/crates/gateway-api/src/openai/responses/websocket/protocol.rs @@ -43,7 +43,6 @@ fn decode_response_create_inner( } super::super::request::decode_request_object( body, - false, request_headers, RequestDecodeSource::WebSocketFrame, ) diff --git a/backend/crates/gateway-api/src/openai/router.rs b/backend/crates/gateway-api/src/openai/router.rs index e9c286b14..475eb03bd 100644 --- a/backend/crates/gateway-api/src/openai/router.rs +++ b/backend/crates/gateway-api/src/openai/router.rs @@ -8,8 +8,8 @@ use axum::{ use super::{ images::{image_edits, image_generations}, - models::{model_catalog, model_detail, model_info, models}, - responses::{responses, responses_websocket, review_responses}, + models::{model_detail, models}, + responses::{responses, responses_websocket}, search::standalone_search, }; @@ -22,10 +22,7 @@ pub(crate) fn router() -> Router { .route("/v1/images/edits", post(image_edits)) .route("/v1/alpha/search", post(standalone_search)) .route("/v1/responses", get(responses_websocket).post(responses)) - .route("/v1/responses/review", post(review_responses)) .route("/v1/models", get(models)) - .route("/v1/models/catalog", get(model_catalog)) - .route("/v1/models/{model_id}/info", get(model_info)) // 官方 OpenAI 模型详情合同使用 path ID;它不属于 Admin API 约束。 .route("/v1/models/{model_id}", get(model_detail)) // OpenAI 数据面正文属于客户端/上游协议;代理不能用私有大小上限提前拒绝 diff --git a/backend/crates/gateway-api/tests/openai/models.rs b/backend/crates/gateway-api/tests/openai/models.rs index 9ff9ad881..1960bc4b2 100644 --- a/backend/crates/gateway-api/tests/openai/models.rs +++ b/backend/crates/gateway-api/tests/openai/models.rs @@ -383,46 +383,6 @@ async fn model_detail_should_hide_unknown_models() { assert_eq!(response.status(), StatusCode::NOT_FOUND); } -#[tokio::test] -async fn model_catalog_should_keep_the_codex_catalog_contract() { - let response = api_router(ModelsExecution::new()) - .await - .oneshot(authorized_request("/v1/models/catalog")) - .await - .expect("catalog response"); - - assert_eq!(response.status(), StatusCode::OK); - let body = to_bytes(response.into_body(), usize::MAX) - .await - .expect("read catalog body"); - let value: serde_json::Value = serde_json::from_slice(&body).expect("catalog JSON"); - assert_eq!(value[0]["id"], "model-a"); - assert_eq!(value[0]["displayName"], "model-a"); - assert_eq!(value[0]["source"], "gateway"); -} - -#[tokio::test] -async fn model_info_should_return_a_single_visible_catalog_entry() { - let response = api_router(ModelsExecution::new()) - .await - .oneshot(authorized_request("/v1/models/model-a/info")) - .await - .expect("model info response"); - - assert_eq!(response.status(), StatusCode::OK); -} - -#[tokio::test] -async fn model_info_should_hide_unknown_models() { - let response = api_router(ModelsExecution::new()) - .await - .oneshot(authorized_request("/v1/models/model-private/info")) - .await - .expect("missing model info response"); - - assert_eq!(response.status(), StatusCode::NOT_FOUND); -} - #[tokio::test] async fn models_should_authenticate_before_applying_the_version_gate() { let response = api_router(ModelsExecution::with_cli_min()) diff --git a/backend/crates/gateway-api/tests/openai/responses/http.rs b/backend/crates/gateway-api/tests/openai/responses/http.rs index 37cffb48a..b6a01bf01 100644 --- a/backend/crates/gateway-api/tests/openai/responses/http.rs +++ b/backend/crates/gateway-api/tests/openai/responses/http.rs @@ -36,7 +36,7 @@ use serde_json::{Value, json}; use gateway_api::openai::responses::{collect_execution_response, stream_execution_response}; use tower::ServiceExt; -use crate::openai::{api_router, authenticated_client, authenticated_client_for_provider}; +use crate::openai::{api_router, authenticated_client_for_provider}; #[derive(Default)] struct Trace { @@ -689,92 +689,6 @@ async fn xai_private_headers_should_not_enter_openai_request_facts() { assert!(captured.prompt_cache_key.is_none()); } -#[tokio::test] -async fn review_route_should_inject_review_subagent_and_record_its_endpoint() { - let observed = Arc::new(Mutex::new(None)); - let execution = Arc::new(ContextCaptureExecution { - observed: Arc::clone(&observed), - client: authenticated_client("sk_context_test"), - }); - let request = Request::post("/v1/responses/review") - .header(AUTHORIZATION, "Bearer sk_context_test") - .header("x-openai-subagent", "compact") - .body(Body::from( - json!({ - "model": "smart-code", - "input": "review this", - "client_metadata": {"existing": "preserved"} - }) - .to_string(), - )) - .expect("review request"); - - let response = api_router(execution) - .await - .oneshot(request) - .await - .expect("review response"); - let captured = observed - .lock() - .expect("review capture lock") - .clone() - .expect("captured review request"); - - assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); - assert_eq!(captured.endpoint, "/v1/responses/review"); - assert_eq!( - captured - .client_metadata - .as_ref() - .and_then(|metadata| metadata.get("x-openai-subagent")), - Some(&json!("review")) - ); - assert_eq!( - captured - .client_metadata - .as_ref() - .and_then(|metadata| metadata.get("existing")), - Some(&json!("preserved")) - ); -} - -#[tokio::test] -async fn review_route_should_not_replace_non_object_client_metadata() { - let observed = Arc::new(Mutex::new(None)); - let execution = Arc::new(ContextCaptureExecution { - observed: Arc::clone(&observed), - client: authenticated_client("sk_context_test"), - }); - let request = Request::post("/v1/responses/review") - .header(AUTHORIZATION, "Bearer sk_context_test") - .body(Body::from( - json!({ - "model": "smart-code", - "input": "review this", - "client_metadata": ["opaque", "client", "value"] - }) - .to_string(), - )) - .expect("review request"); - - let response = api_router(execution) - .await - .oneshot(request) - .await - .expect("review response"); - let captured = observed - .lock() - .expect("review capture lock") - .clone() - .expect("captured review request"); - - assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); - assert_eq!( - captured.client_metadata, - Some(json!(["opaque", "client", "value"])) - ); -} - #[tokio::test] async fn streaming_encodes_first_frame_before_commit_and_http_delivery() { let trace = Arc::new(Trace::default()); diff --git a/backend/crates/gateway-api/tests/openai/router.rs b/backend/crates/gateway-api/tests/openai/router.rs index 79636230b..bbfb81374 100644 --- a/backend/crates/gateway-api/tests/openai/router.rs +++ b/backend/crates/gateway-api/tests/openai/router.rs @@ -1,6 +1,6 @@ use axum::{ body::Body, - http::{Method, Request, StatusCode}, + http::{Method, Request, StatusCode, header::AUTHORIZATION}, }; use tower::ServiceExt; @@ -9,6 +9,54 @@ use super::models::ModelsExecution; const REMOVED_BODY_LIMIT_BYTES: usize = 16 * 1024 * 1024; +#[tokio::test] +async fn removed_responses_review_route_should_not_reach_the_responses_handler() { + let response = api_router_with_origins(ModelsExecution::new(), Vec::new()) + .await + .oneshot( + Request::post("/v1/responses/review") + .header(AUTHORIZATION, "Bearer sk_models_test") + .body(Body::empty()) + .expect("build removed review request"), + ) + .await + .expect("route removed review request"); + + assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED); +} + +#[tokio::test] +async fn removed_models_catalog_extension_should_use_standard_model_lookup() { + let response = api_router_with_origins(ModelsExecution::new(), Vec::new()) + .await + .oneshot( + Request::get("/v1/models/catalog") + .header(AUTHORIZATION, "Bearer sk_models_test") + .body(Body::empty()) + .expect("build removed model catalog request"), + ) + .await + .expect("route removed model catalog request"); + + assert_eq!(response.status(), StatusCode::NOT_FOUND); +} + +#[tokio::test] +async fn removed_model_info_route_should_return_not_found() { + let response = api_router_with_origins(ModelsExecution::new(), Vec::new()) + .await + .oneshot( + Request::get("/v1/models/model-a/info") + .header(AUTHORIZATION, "Bearer sk_models_test") + .body(Body::empty()) + .expect("build removed model info request"), + ) + .await + .expect("route removed model info request"); + + assert_eq!(response.status(), StatusCode::NOT_FOUND); +} + #[tokio::test] async fn responses_body_should_accept_payload_above_the_removed_private_limit() { let router = api_router_with_origins(ModelsExecution::new(), Vec::new()).await; diff --git a/backend/crates/providers/openai/src/transport/protocol/responses.rs b/backend/crates/providers/openai/src/transport/protocol/responses.rs index 2447b25e5..844bf2170 100644 --- a/backend/crates/providers/openai/src/transport/protocol/responses.rs +++ b/backend/crates/providers/openai/src/transport/protocol/responses.rs @@ -649,10 +649,9 @@ impl CodexResponsesRequest { /// 返回请求语义与 child transport 隔离所用的子代理区分值。 /// - /// Codex 原生请求在 turn metadata 中声明 `subagent_kind`;网关的 - /// `/responses/review` 等入口则通过 `client_metadata.x-openai-subagent` - /// 传递给上游。它不拆分根会话的账号首选项;`thread_spawn` 仍用它派生独立 - /// WebSocket/continuation transport identity。 + /// Codex 原生请求在 turn metadata 中声明 `subagent_kind`;兼容客户端也可通过 + /// `client_metadata.x-openai-subagent` 声明同一语义。它不拆分根会话的账号首选项; + /// `thread_spawn` 仍用它派生独立 WebSocket/continuation transport identity。 pub fn subagent_kind(&self) -> Option { self.semantics() .subagent_kind diff --git a/docs/api.md b/docs/api.md index 6d0db2f14..4d9f8399d 100644 --- a/docs/api.md +++ b/docs/api.md @@ -115,15 +115,15 @@ Responses、Images 和 standalone Search HTTP body、WebSocket message 和 frame | --- | --- | --- | | `POST` | `/v1/responses` | OpenAI Responses JSON;`stream=true` 返回 SSE,否则返回完整 JSON | | `GET` | `/v1/responses` | 通过 HTTP Upgrade 建立 Responses WebSocket | -| `POST` | `/v1/responses/review` | 使用同一 Responses 合同发起 review 子代理请求 | | `POST` | `/v1/alpha/search` | Codex standalone web search;JSON 请求与响应正文原样转发 | | `POST` | `/v1/images/generations` | 通过 OpenAI Provider 发起图像生成;JSON 请求与响应正文原样转发 | | `POST` | `/v1/images/edits` | 通过 OpenAI Provider 发起图像编辑;JSON 请求与响应正文原样转发 | | `GET` | `/v1/models` | 返回当前 Client Key 账号范围内各 Provider 的可用公开模型并集;有两种响应形态,见下 | -| `GET` | `/v1/models/catalog` | 返回 Codex 客户端使用的模型目录 | -| `GET` | `/v1/models/{model_id}/info` | 返回 Codex 客户端使用的单模型信息 | | `GET` | `/v1/models/{model_id}` | 返回 OpenAI 兼容的单模型详情 | +Codex 的 review 等子代理请求仍使用 `/v1/responses`,并通过 `x-openai-subagent` 请求头携带子代理类型; +网关不提供独立的子代理请求路径。 + `GET /v1/models` 默认返回 OpenAI 兼容列表 `{"object": "list", "data": [...]}`;请求携带非空 `client_version` query 参数(Codex 客户端)时改为返回 Codex 专用目录合同 `{"models": [...]}`。 diff --git a/frontend/src/views/usage/components/UsagePerformanceCard.vue b/frontend/src/views/usage/components/UsagePerformanceCard.vue index 4963554d7..446ddc86a 100644 --- a/frontend/src/views/usage/components/UsagePerformanceCard.vue +++ b/frontend/src/views/usage/components/UsagePerformanceCard.vue @@ -112,8 +112,11 @@ const summaryMetrics = computed(() => { const hasData = computed( () => !props.loading - && selectedPoints.value.some( - point => point.first != null || point.second != null || point.third != null, + && performancePoints.value.some( + point => + seriesValue(point, 'first') != null + || seriesValue(point, 'second') != null + || seriesValue(point, 'third') != null, ), ) @@ -267,7 +270,7 @@ function formatThroughput(value: number | null) { : activeView === 'throughput' ? '当前范围没有吞吐样本' : activeView === 'scheduling' - ? '部署迁移后才会开始积累调度与容量样本' + ? '当前范围没有调度或容量样本' : '当前范围没有总耗时样本' " class="h-52.5 place-content-center" diff --git a/frontend/src/views/usage/components/UsageReasoningEffortCell.vue b/frontend/src/views/usage/components/UsageReasoningEffortCell.vue index 88f7e8902..1ad3d8480 100644 --- a/frontend/src/views/usage/components/UsageReasoningEffortCell.vue +++ b/frontend/src/views/usage/components/UsageReasoningEffortCell.vue @@ -1,8 +1,8 @@