From 10e34418a42e752fe119aa59436d57fa1ea6785f Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Tue, 22 Sep 2026 12:54:08 +0900 Subject: [PATCH 1/3] feat(logs): record the upstream-served model and flag wire/served mismatches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit is-gpt-nerfed-style auditability: the log already carried the routed model and, on some paths, the upstream-served one, but the GUI rendered resolvedModel alone and a virtual-model rewrite could overwrite the served value before it was ever compared. Record the upstream-served model on a dedicated servedModel field (openai-model header and response body), keep the exact wire id on wireModel when a route/virtual rewrite diverges it from the client-facing model, and render 'wire → served' in the Logs model column when they differ. --- gui/src/i18n/de.ts | 3 +++ gui/src/i18n/en.ts | 3 +++ gui/src/i18n/fr.ts | 3 +++ gui/src/i18n/ja.ts | 3 +++ gui/src/i18n/ko.ts | 3 +++ gui/src/i18n/ru.ts | 3 +++ gui/src/i18n/tr.ts | 3 +++ gui/src/i18n/vi.ts | 3 +++ gui/src/i18n/zh-TW.ts | 3 +++ gui/src/i18n/zh.ts | 3 +++ gui/src/pages/Logs.tsx | 10 ++++++-- gui/src/pages/logs-model-title.ts | 4 ++++ gui/tests/logs-model-title.test.ts | 7 ++++++ src/providers/openai-virtual-models.ts | 1 + src/server/request-log.ts | 25 ++++++++++++++++---- src/server/responses/compact.ts | 1 + src/server/responses/core-codex-account.ts | 1 + src/server/responses/core-normalize.ts | 1 + src/server/responses/passthrough-delivery.ts | 5 +++- src/usage/log.ts | 6 +++++ 20 files changed, 83 insertions(+), 8 deletions(-) diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index b28662b265a..445fb42ff0c 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -942,6 +942,9 @@ export const de: Record = { "logs.col.duration": "Dauer", "logs.modelTooltip.model": "Modell", "logs.modelTooltip.resolvedModel": "aufgelöstes Modell", + "logs.modelTooltip.servedModel": "bereitgestelltes Modell", + "logs.modelTooltip.wireModel": "Wire-Modell", + "logs.modelRerouteTitle": "Upstream hat ein anderes Modell bereitgestellt als angefordert", "logs.modelTooltip.requestedTier": "angeforderte Stufe", "logs.modelTooltip.configuredTier": "konfigurierte Stufe", "logs.modelTooltip.responseTier": "Antwortstufe", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index f3f03e21c79..ece920fa3f9 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -991,6 +991,9 @@ export const en = { "logs.col.duration": "Duration", "logs.modelTooltip.model": "model", "logs.modelTooltip.resolvedModel": "resolved model", + "logs.modelTooltip.servedModel": "served model", + "logs.modelTooltip.wireModel": "wire model", + "logs.modelRerouteTitle": "Upstream served a different model than requested", "logs.modelTooltip.requestedTier": "requested tier", "logs.modelTooltip.configuredTier": "configured tier", "logs.modelTooltip.responseTier": "response tier", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 34525f7e4a1..199587344bb 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -972,6 +972,9 @@ export const fr: Record = { "logs.col.duration": "Durée", "logs.modelTooltip.model": "modèle", "logs.modelTooltip.resolvedModel": "modèle résolu", + "logs.modelTooltip.servedModel": "modèle servi", + "logs.modelTooltip.wireModel": "modèle wire", + "logs.modelRerouteTitle": "Le modèle servi en amont diffère du modèle demandé", "logs.modelTooltip.requestedTier": "niveau demandé", "logs.modelTooltip.configuredTier": "niveau configuré", "logs.modelTooltip.responseTier": "niveau de réponse", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index dcf3ce28623..d657bebd1fc 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -902,6 +902,9 @@ export const ja: Record = { "logs.col.duration": "所要時間", "logs.modelTooltip.model": "モデル", "logs.modelTooltip.resolvedModel": "解決後モデル", + "logs.modelTooltip.servedModel": "提供モデル", + "logs.modelTooltip.wireModel": "ワイヤーモデル", + "logs.modelRerouteTitle": "アップストリームが要求されたモデルと異なるモデルを提供しました", "logs.modelTooltip.requestedTier": "要求ティア", "logs.modelTooltip.configuredTier": "設定ティア", "logs.modelTooltip.responseTier": "応答ティア", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 68c11214149..12488b4b82b 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -973,6 +973,9 @@ export const ko: Record = { "logs.col.duration": "소요 시간", "logs.modelTooltip.model": "모델", "logs.modelTooltip.resolvedModel": "해석된 모델", + "logs.modelTooltip.servedModel": "제공된 모델", + "logs.modelTooltip.wireModel": "와이어 모델", + "logs.modelRerouteTitle": "업스트림이 요청한 모델과 다른 모델을 제공했습니다", "logs.modelTooltip.requestedTier": "요청 티어", "logs.modelTooltip.configuredTier": "설정 티어", "logs.modelTooltip.responseTier": "응답 티어", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 966c173fe0a..e79962eaa13 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -959,6 +959,9 @@ export const ru: Record = { "logs.col.duration": "Длительность", "logs.modelTooltip.model": "модель", "logs.modelTooltip.resolvedModel": "разрешённая модель", + "logs.modelTooltip.servedModel": "обслуженная модель", + "logs.modelTooltip.wireModel": "модель на проводе", + "logs.modelRerouteTitle": "Выше по потоку обслужена другая модель, чем запрошено", "logs.modelTooltip.requestedTier": "запрошенный уровень", "logs.modelTooltip.configuredTier": "настроенный уровень", "logs.modelTooltip.responseTier": "уровень ответа", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 4f1053581f2..74a8e50350a 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -978,6 +978,9 @@ export const tr: Record = { "logs.col.duration": "Süre", "logs.modelTooltip.model": "model", "logs.modelTooltip.resolvedModel": "çözümlenen model", + "logs.modelTooltip.servedModel": "sunulan model", + "logs.modelTooltip.wireModel": "wire model", + "logs.modelRerouteTitle": "Yukarı akış, istenenden farklı bir model sundu", "logs.modelTooltip.requestedTier": "istenen katman", "logs.modelTooltip.configuredTier": "yapılandırılan katman", "logs.modelTooltip.responseTier": "yanıt katmanı", diff --git a/gui/src/i18n/vi.ts b/gui/src/i18n/vi.ts index 8cb83da7079..1b596404e66 100644 --- a/gui/src/i18n/vi.ts +++ b/gui/src/i18n/vi.ts @@ -965,6 +965,9 @@ export const vi: Record = { "logs.col.duration": "Thời lượng", "logs.modelTooltip.model": "model", "logs.modelTooltip.resolvedModel": "model đã giải quyết (resolved model)", + "logs.modelTooltip.servedModel": "model được phục vụ", + "logs.modelTooltip.wireModel": "model wire", + "logs.modelRerouteTitle": "Upstream đã phục vụ một model khác với model được yêu cầu", "logs.modelTooltip.requestedTier": "bậc (tier) được yêu cầu", "logs.modelTooltip.configuredTier": "bậc (tier) cấu hình", "logs.modelTooltip.responseTier": "bậc (tier) phản hồi", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index c3482290e58..5bbd61e50c4 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -749,6 +749,9 @@ export const zhTW: Record = { "logs.col.duration": "耗時", "logs.modelTooltip.model": "模型", "logs.modelTooltip.resolvedModel": "解析後模型", + "logs.modelTooltip.servedModel": "實際服務模型", + "logs.modelTooltip.wireModel": "線路上模型", + "logs.modelRerouteTitle": "上游實際服務的模型與請求的模型不同", "logs.modelTooltip.requestedTier": "請求層級", "logs.modelTooltip.configuredTier": "設定層級", "logs.modelTooltip.responseTier": "回應層級", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 7655f8b09e0..377ed2a3578 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -954,6 +954,9 @@ export const zh: Record = { "logs.col.duration": "耗时", "logs.modelTooltip.model": "模型", "logs.modelTooltip.resolvedModel": "解析后模型", + "logs.modelTooltip.servedModel": "实际服务模型", + "logs.modelTooltip.wireModel": "线路上模型", + "logs.modelRerouteTitle": "上游实际服务的模型与请求的模型不同", "logs.modelTooltip.requestedTier": "请求层级", "logs.modelTooltip.configuredTier": "配置层级", "logs.modelTooltip.responseTier": "响应层级", diff --git a/gui/src/pages/Logs.tsx b/gui/src/pages/Logs.tsx index 9f55f37e45d..fed9f4676bb 100644 --- a/gui/src/pages/Logs.tsx +++ b/gui/src/pages/Logs.tsx @@ -179,6 +179,8 @@ export interface LogEntry extends LogFailureAttribution { // cannot say whether Fast was granted on a backend whose echo is not authoritative. tierOutcome?: ModelTitleTierOutcome; resolvedModel?: string; + servedModel?: string; + wireModel?: string; modelSupportsServiceTier?: boolean; status: number; durationMs: number; @@ -954,7 +956,11 @@ export default function Logs({ apiBase }: { apiBase: string }) { - {modelLabel(log.resolvedModel ?? log.model)} + {log.servedModel && log.servedModel !== (log.wireModel ?? log.model) ? ( + {modelLabel(log.wireModel ?? log.model)}{" → "}{modelLabel(log.servedModel)} + ) : ( + {modelLabel(log.servedModel ?? log.resolvedModel ?? log.model)} + )} {log.shadowCallRewrittenFrom && ( )} - {t("logs.col.model")}{modelLabel(detail.resolvedModel ?? detail.model)} + {t("logs.col.model")}{modelLabel(detail.servedModel ?? detail.resolvedModel ?? detail.model)} {t("logs.col.provider")}{formatProviderDisplayName(detail.provider, t)} {(detail.requestedEffort || detail.effectiveEffort) && ( <>{t("logs.col.effort")}{effortLabel(detail)}{reasoningWire ? ` (${reasoningWire})` : ""} diff --git a/gui/src/pages/logs-model-title.ts b/gui/src/pages/logs-model-title.ts index 8bcf3141197..34fc345c31c 100644 --- a/gui/src/pages/logs-model-title.ts +++ b/gui/src/pages/logs-model-title.ts @@ -8,6 +8,8 @@ export interface ModelTitleTierOutcome { export interface ModelTitleEntry { model: string; resolvedModel?: string; + servedModel?: string; + wireModel?: string; requestedServiceTier?: string; configuredServiceTier?: string; responseServiceTier?: string; @@ -40,6 +42,8 @@ export function modelTitle(log: ModelTitleEntry, t: TFn): string { const details = [ `${t("logs.modelTooltip.model")}=${log.model}`, log.resolvedModel ? `${t("logs.modelTooltip.resolvedModel")}=${log.resolvedModel}` : undefined, + log.servedModel ? `${t("logs.modelTooltip.servedModel")}=${log.servedModel}` : undefined, + log.wireModel ? `${t("logs.modelTooltip.wireModel")}=${log.wireModel}` : undefined, log.requestedServiceTier ? `${t("logs.modelTooltip.requestedTier")}=${log.requestedServiceTier}` : undefined, log.configuredServiceTier ? `${t("logs.modelTooltip.configuredTier")}=${log.configuredServiceTier}` : undefined, log.responseServiceTier diff --git a/gui/tests/logs-model-title.test.ts b/gui/tests/logs-model-title.test.ts index 009433d334e..f26d55527f5 100644 --- a/gui/tests/logs-model-title.test.ts +++ b/gui/tests/logs-model-title.test.ts @@ -5,6 +5,7 @@ import { modelTitle, type ModelTitleEntry } from "../src/pages/logs-model-title" const labels: Partial> = { "logs.modelTooltip.model": "模型", "logs.modelTooltip.resolvedModel": "解析后模型", + "logs.modelTooltip.servedModel": "实际服务模型", "logs.modelTooltip.requestedTier": "请求层级", "logs.modelTooltip.configuredTier": "配置层级", "logs.modelTooltip.responseTier": "响应层级", @@ -35,3 +36,9 @@ test("model diagnostics localize every label and use one Unicode middle dot betw test("model diagnostics do not include an extra Latin capital A with circumflex", () => { expect(modelTitle(entry({ resolvedModel: "gpt-5.6-sol" }), t)).not.toContain("\u00C2"); }); + +test("model diagnostics surface the upstream-served model when it differs from the wire model", () => { + expect(modelTitle(entry({ model: "gpt-5.6-sol", servedModel: "gpt-5.6-luna" }), t)).toBe( + "模型=gpt-5.6-sol · 实际服务模型=gpt-5.6-luna", + ); +}); diff --git a/src/providers/openai-virtual-models.ts b/src/providers/openai-virtual-models.ts index 5aabe83907b..e85403949eb 100644 --- a/src/providers/openai-virtual-models.ts +++ b/src/providers/openai-virtual-models.ts @@ -99,6 +99,7 @@ export function applyOpenAiVirtualModel( logCtx.model = resolution.selectedModelId; logCtx.resolvedModel = resolution.wireModelId; + logCtx.wireModel = resolution.wireModelId; route.modelId = resolution.wireModelId; captureOpenAiVirtualWirePolicy(route, resolution, inboundWire); parsed.modelId = resolution.wireModelId; diff --git a/src/server/request-log.ts b/src/server/request-log.ts index ac1ad98270d..62cd3113e7e 100644 --- a/src/server/request-log.ts +++ b/src/server/request-log.ts @@ -168,6 +168,12 @@ export interface RequestLogContext { /** Final-attempt tier summary; attempt rows remain the accounting source of truth. */ tierOutcome?: AttemptTierOutcome; resolvedModel?: string; + /** Model the upstream actually served (openai-model header or response body), recorded even + * when a route/virtual resolution owns resolvedModel, so an upstream reroute stays visible. */ + servedModel?: string; + /** The exact model id sent upstream; recorded when a route/virtual rewrite makes it differ + * from the client-facing `model`, so a served-model mismatch can be judged against the wire. */ + wireModel?: string; /** Internal: client-facing response metadata must not replace the physical routed model. */ preserveResolvedModelFromRoute?: boolean; usage?: OcxUsage; @@ -295,6 +301,10 @@ export interface RequestLogEntry { responseServiceTier?: string; tierOutcome?: AttemptTierOutcome; resolvedModel?: string; + /** Model the upstream actually served (openai-model header or response body). */ + servedModel?: string; + /** The exact model id sent upstream when it differs from the client-facing `model`. */ + wireModel?: string; status: number; durationMs: number; errorCode?: string; @@ -445,6 +455,8 @@ export function requestLogEntryFromPersistedUsage(entry: PersistedUsageEntry): R ...(entry.responseServiceTier ? { responseServiceTier: entry.responseServiceTier } : {}), ...(entry.tierOutcome ? { tierOutcome: entry.tierOutcome } : {}), ...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}), + ...(entry.servedModel ? { servedModel: entry.servedModel } : {}), + ...(entry.wireModel ? { wireModel: entry.wireModel } : {}), status: entry.status, durationMs: entry.durationMs, ...(entry.errorCode ? { errorCode: entry.errorCode } : {}), @@ -598,6 +610,8 @@ export function addRequestLog(entry: RequestLogEntry) { : {}), ...(entry.conversationId ? { conversationId: entry.conversationId } : {}), ...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}), + ...(entry.servedModel ? { servedModel: entry.servedModel } : {}), + ...(entry.wireModel ? { wireModel: entry.wireModel } : {}), ...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}), ...(entry.requestedAlias ? { requestedAlias: entry.requestedAlias } : {}), ...(entry.shadowCallRewrittenFrom @@ -882,11 +896,10 @@ export function applyResponseLogMetadata(logCtx: RequestLogContext, payload: unk : payload; if (!source || typeof source !== "object") return; const model = (source as { model?: unknown }).model; - if ( - !logCtx.preserveResolvedModelFromRoute - && typeof model === "string" - && model.trim() - ) logCtx.resolvedModel = model; + if (typeof model === "string" && model.trim()) { + logCtx.servedModel = model; + if (!logCtx.preserveResolvedModelFromRoute) logCtx.resolvedModel = model; + } const serviceTier = (source as { service_tier?: unknown }).service_tier; if (typeof serviceTier === "string" && serviceTier.trim()) { const sanitized = sanitizeLogMetadataString(serviceTier); @@ -1520,6 +1533,8 @@ export function addFinalRequestLog( ? { tierOutcome: attempts?.at(-1)?.tierOutcome ?? { ...logCtx.tierOutcome! } } : {}), ...(logCtx.resolvedModel ? { resolvedModel: logCtx.resolvedModel } : {}), + ...(logCtx.servedModel ? { servedModel: logCtx.servedModel } : {}), + ...(logCtx.wireModel ? { wireModel: logCtx.wireModel } : {}), status: effectiveStatus, durationMs, ...(logCtx.firstOutputMs !== undefined ? { firstOutputMs: logCtx.firstOutputMs } : {}), diff --git a/src/server/responses/compact.ts b/src/server/responses/compact.ts index 89e40123789..1db534f32ac 100644 --- a/src/server/responses/compact.ts +++ b/src/server/responses/compact.ts @@ -709,6 +709,7 @@ export async function handleResponsesCompact( captureOpenAiVirtualWirePolicy(route, virtual); logCtx.model = virtual.selectedModelId; logCtx.resolvedModel = virtual.wireModelId; + logCtx.wireModel = virtual.wireModelId; } else { logCtx.resolvedModel = route.modelId; } diff --git a/src/server/responses/core-codex-account.ts b/src/server/responses/core-codex-account.ts index 99796846d8f..a32b788c50d 100644 --- a/src/server/responses/core-codex-account.ts +++ b/src/server/responses/core-codex-account.ts @@ -444,6 +444,7 @@ export function applyCodexAccountGatedWireNormalization(parsed: OcxParsedRequest if (logCtx) { logCtx.preserveResolvedModelFromRoute = true; delete logCtx.resolvedModel; + logCtx.wireModel = wireModel; } parsed.modelId = wireModel; if (!parsed._rawBody || typeof parsed._rawBody !== "object") return; diff --git a/src/server/responses/core-normalize.ts b/src/server/responses/core-normalize.ts index 7da5e9d4028..db508ad3f2c 100644 --- a/src/server/responses/core-normalize.ts +++ b/src/server/responses/core-normalize.ts @@ -195,6 +195,7 @@ export async function applyFinalRouteRequestNormalization(args: { if (parsed._responseModelId !== undefined && parsed._responseModelId !== parsed.modelId) { logCtx.resolvedModel = route.modelId; + logCtx.wireModel = route.modelId; logCtx.preserveResolvedModelFromRoute = true; } diff --git a/src/server/responses/passthrough-delivery.ts b/src/server/responses/passthrough-delivery.ts index 969cd84e589..bd3358eef87 100644 --- a/src/server/responses/passthrough-delivery.ts +++ b/src/server/responses/passthrough-delivery.ts @@ -207,7 +207,10 @@ export async function deliverPassthroughResponse( const headers = sanitizePassthroughHeaders(upstreamResponse.headers, codexSafetyBufferingOptions); const resolvedModel = headers.get("openai-model")?.trim(); - if (resolvedModel && !logCtx.preserveResolvedModelFromRoute) logCtx.resolvedModel = resolvedModel; + if (resolvedModel) { + logCtx.servedModel = resolvedModel; + if (!logCtx.preserveResolvedModelFromRoute) logCtx.resolvedModel = resolvedModel; + } if (isUsageDebugEnabled()) { const upstreamContentType = upstreamResponse.headers.get("content-type"); if (upstreamContentType) logCtx.usageDebugContentType = upstreamContentType; diff --git a/src/usage/log.ts b/src/usage/log.ts index 6955db2073a..499c2c4bde6 100644 --- a/src/usage/log.ts +++ b/src/usage/log.ts @@ -284,6 +284,10 @@ export interface PersistedUsageEntry { /** Best-effort chat/session correlation for Logs grouping (#330). */ conversationId?: string; resolvedModel?: string; + /** Model the upstream actually served (openai-model header or response body). */ + servedModel?: string; + /** The exact model id sent upstream when it differs from the client-facing `model`. */ + wireModel?: string; requestedModel?: string; /** Original bare helper model when the opt-in shadow-call route rewrote this request. */ shadowCallRewrittenFrom?: string; @@ -856,6 +860,8 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry { ? { conversationId: entry.conversationId.trim().slice(0, 128) } : {}), ...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}), + ...(entry.servedModel ? { servedModel: entry.servedModel } : {}), + ...(entry.wireModel ? { wireModel: entry.wireModel } : {}), ...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}), ...(shadowCallRewrittenFrom ? { shadowCallRewrittenFrom } : {}), ...(typeof entry.requestedEffort === "string" && entry.requestedEffort From 37de7b0504f3469d340aa39906cc59a13efb05d6 Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Tue, 22 Sep 2026 13:28:50 +0900 Subject: [PATCH 2/3] fix(i18n): describe the wire-model comparison in reroute strings --- gui/src/i18n/de.ts | 2 +- gui/src/i18n/en.ts | 2 +- gui/src/i18n/fr.ts | 2 +- gui/src/i18n/ja.ts | 2 +- gui/src/i18n/ko.ts | 2 +- gui/src/i18n/ru.ts | 6 +++--- gui/src/i18n/tr.ts | 2 +- gui/src/i18n/vi.ts | 2 +- gui/src/i18n/zh-TW.ts | 2 +- gui/src/i18n/zh.ts | 2 +- 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 445fb42ff0c..c447332fd3b 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -944,7 +944,7 @@ export const de: Record = { "logs.modelTooltip.resolvedModel": "aufgelöstes Modell", "logs.modelTooltip.servedModel": "bereitgestelltes Modell", "logs.modelTooltip.wireModel": "Wire-Modell", - "logs.modelRerouteTitle": "Upstream hat ein anderes Modell bereitgestellt als angefordert", + "logs.modelRerouteTitle": "Upstream hat ein anderes Modell bereitgestellt als gesendet", "logs.modelTooltip.requestedTier": "angeforderte Stufe", "logs.modelTooltip.configuredTier": "konfigurierte Stufe", "logs.modelTooltip.responseTier": "Antwortstufe", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index ece920fa3f9..103aef9c6b9 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -993,7 +993,7 @@ export const en = { "logs.modelTooltip.resolvedModel": "resolved model", "logs.modelTooltip.servedModel": "served model", "logs.modelTooltip.wireModel": "wire model", - "logs.modelRerouteTitle": "Upstream served a different model than requested", + "logs.modelRerouteTitle": "Upstream served a different model than sent", "logs.modelTooltip.requestedTier": "requested tier", "logs.modelTooltip.configuredTier": "configured tier", "logs.modelTooltip.responseTier": "response tier", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 199587344bb..9e497307270 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -974,7 +974,7 @@ export const fr: Record = { "logs.modelTooltip.resolvedModel": "modèle résolu", "logs.modelTooltip.servedModel": "modèle servi", "logs.modelTooltip.wireModel": "modèle wire", - "logs.modelRerouteTitle": "Le modèle servi en amont diffère du modèle demandé", + "logs.modelRerouteTitle": "Le modèle servi en amont diffère du modèle envoyé", "logs.modelTooltip.requestedTier": "niveau demandé", "logs.modelTooltip.configuredTier": "niveau configuré", "logs.modelTooltip.responseTier": "niveau de réponse", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index d657bebd1fc..1aa32611552 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -904,7 +904,7 @@ export const ja: Record = { "logs.modelTooltip.resolvedModel": "解決後モデル", "logs.modelTooltip.servedModel": "提供モデル", "logs.modelTooltip.wireModel": "ワイヤーモデル", - "logs.modelRerouteTitle": "アップストリームが要求されたモデルと異なるモデルを提供しました", + "logs.modelRerouteTitle": "アップストリームが送信したモデルと異なるモデルを提供しました", "logs.modelTooltip.requestedTier": "要求ティア", "logs.modelTooltip.configuredTier": "設定ティア", "logs.modelTooltip.responseTier": "応答ティア", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 12488b4b82b..3efb431a2eb 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -975,7 +975,7 @@ export const ko: Record = { "logs.modelTooltip.resolvedModel": "해석된 모델", "logs.modelTooltip.servedModel": "제공된 모델", "logs.modelTooltip.wireModel": "와이어 모델", - "logs.modelRerouteTitle": "업스트림이 요청한 모델과 다른 모델을 제공했습니다", + "logs.modelRerouteTitle": "업스트림이 보낸 모델과 다른 모델을 제공했습니다", "logs.modelTooltip.requestedTier": "요청 티어", "logs.modelTooltip.configuredTier": "설정 티어", "logs.modelTooltip.responseTier": "응답 티어", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index e79962eaa13..2b074bb7841 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -959,9 +959,9 @@ export const ru: Record = { "logs.col.duration": "Длительность", "logs.modelTooltip.model": "модель", "logs.modelTooltip.resolvedModel": "разрешённая модель", - "logs.modelTooltip.servedModel": "обслуженная модель", - "logs.modelTooltip.wireModel": "модель на проводе", - "logs.modelRerouteTitle": "Выше по потоку обслужена другая модель, чем запрошено", + "logs.modelTooltip.servedModel": "фактически обслуженная модель", + "logs.modelTooltip.wireModel": "модель, отправленная выше по потоку", + "logs.modelRerouteTitle": "Выше по потоку обслужена другая модель, чем отправлено", "logs.modelTooltip.requestedTier": "запрошенный уровень", "logs.modelTooltip.configuredTier": "настроенный уровень", "logs.modelTooltip.responseTier": "уровень ответа", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 74a8e50350a..634c11c402a 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -980,7 +980,7 @@ export const tr: Record = { "logs.modelTooltip.resolvedModel": "çözümlenen model", "logs.modelTooltip.servedModel": "sunulan model", "logs.modelTooltip.wireModel": "wire model", - "logs.modelRerouteTitle": "Yukarı akış, istenenden farklı bir model sundu", + "logs.modelRerouteTitle": "Yukarı akış, gönderilenden farklı bir model sundu", "logs.modelTooltip.requestedTier": "istenen katman", "logs.modelTooltip.configuredTier": "yapılandırılan katman", "logs.modelTooltip.responseTier": "yanıt katmanı", diff --git a/gui/src/i18n/vi.ts b/gui/src/i18n/vi.ts index 1b596404e66..ca530dad179 100644 --- a/gui/src/i18n/vi.ts +++ b/gui/src/i18n/vi.ts @@ -967,7 +967,7 @@ export const vi: Record = { "logs.modelTooltip.resolvedModel": "model đã giải quyết (resolved model)", "logs.modelTooltip.servedModel": "model được phục vụ", "logs.modelTooltip.wireModel": "model wire", - "logs.modelRerouteTitle": "Upstream đã phục vụ một model khác với model được yêu cầu", + "logs.modelRerouteTitle": "Upstream đã phục vụ một model khác với model đã gửi", "logs.modelTooltip.requestedTier": "bậc (tier) được yêu cầu", "logs.modelTooltip.configuredTier": "bậc (tier) cấu hình", "logs.modelTooltip.responseTier": "bậc (tier) phản hồi", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index 5bbd61e50c4..194207e7194 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -751,7 +751,7 @@ export const zhTW: Record = { "logs.modelTooltip.resolvedModel": "解析後模型", "logs.modelTooltip.servedModel": "實際服務模型", "logs.modelTooltip.wireModel": "線路上模型", - "logs.modelRerouteTitle": "上游實際服務的模型與請求的模型不同", + "logs.modelRerouteTitle": "上游實際服務的模型與發送的模型不同", "logs.modelTooltip.requestedTier": "請求層級", "logs.modelTooltip.configuredTier": "設定層級", "logs.modelTooltip.responseTier": "回應層級", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 377ed2a3578..bb7fed19f03 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -956,7 +956,7 @@ export const zh: Record = { "logs.modelTooltip.resolvedModel": "解析后模型", "logs.modelTooltip.servedModel": "实际服务模型", "logs.modelTooltip.wireModel": "线路上模型", - "logs.modelRerouteTitle": "上游实际服务的模型与请求的模型不同", + "logs.modelRerouteTitle": "上游实际服务的模型与发送的模型不同", "logs.modelTooltip.requestedTier": "请求层级", "logs.modelTooltip.configuredTier": "配置层级", "logs.modelTooltip.responseTier": "响应层级", From 3d989e311505282b046a4df7dfcadc332e4a8c11 Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:21:11 +0900 Subject: [PATCH 3/3] fix(logs): keep the full model tooltip on rerouted rows The reroute span's own title masked the cell's modelTitle diagnostics on exactly the rows where wire/served details matter most. Share one isModelRerouted predicate, fold the reroute notice into the tooltip, reuse the wire -> served arrow in the detail dialog, and cover the no-reroute case. --- gui/src/pages/Logs.tsx | 28 ++++++++++++++++++++-------- gui/src/pages/logs-model-title.ts | 11 +++++++++++ gui/tests/logs-model-title.test.ts | 10 +++++++++- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/gui/src/pages/Logs.tsx b/gui/src/pages/Logs.tsx index fed9f4676bb..eea73ee89c2 100644 --- a/gui/src/pages/Logs.tsx +++ b/gui/src/pages/Logs.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { useVirtualizer } from "@tanstack/react-virtual"; import { useI18n, LOCALES, type TFn } from "../i18n/shared"; import { formatProviderDisplayName } from "../provider-icons"; @@ -18,7 +18,7 @@ import { DEFAULT_LOG_FILTER_STATE, extractLogFilterOptions, filterLogs, hasActiv import type { LogsTab } from "./logs-tab-keydown"; import { logsTabKeyDown, readTabFromHash, selectLogsTab } from "./logs-tab-keydown"; -import { modelTitle, type ModelTitleTierOutcome } from "./logs-model-title"; +import { isModelRerouted, modelTitle, type ModelTitleTierOutcome } from "./logs-model-title"; import { speedLabel } from "./logs-speed-label"; import { formatEstimatedUsd, formatEstimatedUsdValue, summarizeEstimatedCosts } from "./logs-cost-format"; import { cacheSplit, isCursorUsageProvider, tokensTitle } from "./logs-token-title"; @@ -278,6 +278,22 @@ function reasoningWireLabel(log: ReasoningLogFields): string | undefined { return `${log.reasoningWireField}=${log.reasoningWireValue}`; } +// Mirrors the effort column's "requested → effective" form: on a reroute both +// sides keep their own model icon, so the wire id and the upstream-served id +// stay visible at a glance instead of collapsing into one branded label. +function servedModelLabel(log: { model: string; resolvedModel?: string; servedModel?: string; wireModel?: string }): ReactNode { + if (isModelRerouted(log)) { + return ( + <> + {modelLabel(log.wireModel ?? log.model)} + {" \u2192 "} + {modelLabel(log.servedModel!)} + + ); + } + return modelLabel(log.servedModel ?? log.resolvedModel ?? log.model); +} + function formatTokPerSecond(result: TokPerSecondResult | undefined, localeTag?: string): string { if (!result || result.kind === "unavailable" || !Number.isFinite(result.value) || result.value <= 0) return "\u2014"; const digits = result.value >= 100 ? 0 : 1; @@ -956,11 +972,7 @@ export default function Logs({ apiBase }: { apiBase: string }) { - {log.servedModel && log.servedModel !== (log.wireModel ?? log.model) ? ( - {modelLabel(log.wireModel ?? log.model)}{" → "}{modelLabel(log.servedModel)} - ) : ( - {modelLabel(log.servedModel ?? log.resolvedModel ?? log.model)} - )} + {servedModelLabel(log)} {log.shadowCallRewrittenFrom && ( )} - {t("logs.col.model")}{modelLabel(detail.servedModel ?? detail.resolvedModel ?? detail.model)} + {t("logs.col.model")}{servedModelLabel(detail)} {t("logs.col.provider")}{formatProviderDisplayName(detail.provider, t)} {(detail.requestedEffort || detail.effectiveEffort) && ( <>{t("logs.col.effort")}{effortLabel(detail)}{reasoningWire ? ` (${reasoningWire})` : ""} diff --git a/gui/src/pages/logs-model-title.ts b/gui/src/pages/logs-model-title.ts index 34fc345c31c..ea906da8a87 100644 --- a/gui/src/pages/logs-model-title.ts +++ b/gui/src/pages/logs-model-title.ts @@ -17,6 +17,16 @@ export interface ModelTitleEntry { tierOutcome?: ModelTitleTierOutcome; } +/** + * A reroute means the upstream answered a different model than the one on the + * wire. The wire id is the rewritten request when a route/virtual rewrite + * diverged it from the client-facing model, and the client-facing model + * otherwise. + */ +export function isModelRerouted(log: Pick): boolean { + return log.servedModel !== undefined && log.servedModel !== (log.wireModel ?? log.model); +} + /** * #2455: the echoed tier alone does not say whether Fast was granted. The ChatGPT * backend answers `default` on turns it in fact scheduled as priority, so its echo is @@ -40,6 +50,7 @@ function tierConfirmationSuffix(outcome: ModelTitleEntry["tierOutcome"], t: TFn) export function modelTitle(log: ModelTitleEntry, t: TFn): string { const details = [ + isModelRerouted(log) ? t("logs.modelRerouteTitle") : undefined, `${t("logs.modelTooltip.model")}=${log.model}`, log.resolvedModel ? `${t("logs.modelTooltip.resolvedModel")}=${log.resolvedModel}` : undefined, log.servedModel ? `${t("logs.modelTooltip.servedModel")}=${log.servedModel}` : undefined, diff --git a/gui/tests/logs-model-title.test.ts b/gui/tests/logs-model-title.test.ts index f26d55527f5..26a1df7c5e4 100644 --- a/gui/tests/logs-model-title.test.ts +++ b/gui/tests/logs-model-title.test.ts @@ -6,6 +6,8 @@ const labels: Partial> = { "logs.modelTooltip.model": "模型", "logs.modelTooltip.resolvedModel": "解析后模型", "logs.modelTooltip.servedModel": "实际服务模型", + "logs.modelTooltip.wireModel": "线上模型", + "logs.modelRerouteTitle": "上游提供的模型与请求不同", "logs.modelTooltip.requestedTier": "请求层级", "logs.modelTooltip.configuredTier": "配置层级", "logs.modelTooltip.responseTier": "响应层级", @@ -39,6 +41,12 @@ test("model diagnostics do not include an extra Latin capital A with circumflex" test("model diagnostics surface the upstream-served model when it differs from the wire model", () => { expect(modelTitle(entry({ model: "gpt-5.6-sol", servedModel: "gpt-5.6-luna" }), t)).toBe( - "模型=gpt-5.6-sol · 实际服务模型=gpt-5.6-luna", + "上游提供的模型与请求不同 · 模型=gpt-5.6-sol · 实际服务模型=gpt-5.6-luna", + ); +}); + +test("model diagnostics omit the reroute notice when the served model matches the wire model", () => { + expect(modelTitle(entry({ model: "gpt-5.6-sol", wireModel: "gpt-5.6-sol", servedModel: "gpt-5.6-sol" }), t)).toBe( + "模型=gpt-5.6-sol · 实际服务模型=gpt-5.6-sol · 线上模型=gpt-5.6-sol", ); });