From 6fce6ab3f90c2db39bf976135dd93aed7950ce03 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 1 Sep 2026 04:26:50 +0900 Subject: [PATCH 1/3] feat(gui): let Logs be filtered by model or provider #3070: an operator running custom providers watched their OpenAI monthly window shrink and could not find which turns were responsible. The window is the ChatGPT account's own, not a sum of proxy logs, so the only way to explain it is to isolate the rows that used an account-gated native model -- and the dashboard had no control that could. ?model= filtering landed for the CLI and the API in b68edc077, but that commit does not touch gui/src/pages/Logs.tsx. The page's one adjacent control, intercepted-helpers, keys on shadowCallRewrittenFrom, which a plain account-gated turn does not carry. So the rows that explain the bill were exactly the rows no filter could reach. A search field now matches model, resolvedModel and provider, case-insensitively, as a substring. resolvedModel is matched as well as model because they differ precisely when routing redirected the turn, which is the case worth finding: matching only the requested id would hide the redirect that caused the charge. The helper lives in its own module because Logs.tsx must export components only (react-refresh/only-export-components), which is the same reason logs-surface-filter.ts exists. Strings added to all nine locale catalogs; i18n-locales parity is green. --- gui/src/i18n/de.ts | 4 ++- gui/src/i18n/en.ts | 4 ++- gui/src/i18n/fr.ts | 4 ++- gui/src/i18n/ja.ts | 4 ++- gui/src/i18n/ko.ts | 4 ++- gui/src/i18n/ru.ts | 4 ++- gui/src/i18n/tr.ts | 4 ++- gui/src/i18n/zh-TW.ts | 4 ++- gui/src/i18n/zh.ts | 4 ++- gui/src/pages/Logs.tsx | 14 ++++++++ gui/src/pages/logs-model-filter.ts | 36 ++++++++++++++++++++ gui/tests/logs-model-filter.test.ts | 51 +++++++++++++++++++++++++++++ 12 files changed, 128 insertions(+), 9 deletions(-) create mode 100644 gui/src/pages/logs-model-filter.ts create mode 100644 gui/tests/logs-model-filter.test.ts diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 5278690349..61b46f2223 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -656,7 +656,9 @@ export const de: Record = { "logs.badge.interceptedHelperTitle": "Abgefangene Helfer-Anfrage", "logs.filter.conversation.label": "Konversation", "logs.filter.conversation.placeholder": "Konversations-ID einfügen", - "logs.filter.conversation.clear": "Löschen", + "logs.filter.conversation.clear": "Löschen", + "logs.filter.model.label": "Modell", + "logs.filter.model.placeholder": "Modell oder Anbieter filtern", "logs.filter.conversation.apply": "Logs filtern", "logs.conversation.totals": "{requests} Anfragen · {tokens} Tokens · {cost}", "logs.conversation.scope": "Summen gelten nur für den aktuell geladenen Logs-Ring.", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index ab6b652c69..fb25f211fb 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -689,7 +689,9 @@ export const en = { "logs.badge.interceptedHelperTitle": "Intercepted helper request", "logs.filter.conversation.label": "Conversation", "logs.filter.conversation.placeholder": "Paste conversation id", - "logs.filter.conversation.clear": "Clear", + "logs.filter.conversation.clear": "Clear", + "logs.filter.model.label": "Model", + "logs.filter.model.placeholder": "Filter by model or provider", "logs.filter.conversation.apply": "Filter logs", "logs.conversation.totals": "{requests} requests · {tokens} tokens · {cost}", "logs.conversation.scope": "Totals cover the currently loaded Logs ring only.", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 5b1809f394..16a6ab693d 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -670,7 +670,9 @@ export const fr: Record = { "logs.badge.interceptedHelperTitle": "Requête d'assistant interceptée", "logs.filter.conversation.label": "Conversation", "logs.filter.conversation.placeholder": "Coller l’ID de conversation", - "logs.filter.conversation.clear": "Effacer", + "logs.filter.conversation.clear": "Effacer", + "logs.filter.model.label": "Modèle", + "logs.filter.model.placeholder": "Filtrer par modèle ou fournisseur", "logs.filter.conversation.apply": "Filtrer les journaux", "logs.conversation.totals": "{requests} requêtes · {tokens} jetons · {cost}", "logs.conversation.scope": "Les totaux couvrent uniquement le tampon circulaire des journaux actuellement chargé.", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index d1bfb6dfcd..df16c0562e 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -632,7 +632,9 @@ export const ja: Record = { "logs.badge.interceptedHelperTitle": "インターセプトされたヘルパー要求", "logs.filter.conversation.label": "会話", "logs.filter.conversation.placeholder": "会話 ID を貼り付け", - "logs.filter.conversation.clear": "クリア", + "logs.filter.conversation.clear": "クリア", + "logs.filter.model.label": "モデル", + "logs.filter.model.placeholder": "モデルまたはプロバイダーで絞り込む", "logs.filter.conversation.apply": "ログを絞り込み", "logs.conversation.totals": "{requests} 件 · {tokens} トークン · {cost}", "logs.conversation.scope": "合計は現在読み込まれている Logs リングのみです。", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 532b798018..dfa0803dd9 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -675,7 +675,9 @@ export const ko: Record = { "logs.badge.interceptedHelperTitle": "가로챈 헬퍼 요청", "logs.filter.conversation.label": "대화", "logs.filter.conversation.placeholder": "대화 ID 붙여넣기", - "logs.filter.conversation.clear": "지우기", + "logs.filter.conversation.clear": "지우기", + "logs.filter.model.label": "모델", + "logs.filter.model.placeholder": "모델 또는 공급자로 거르기", "logs.filter.conversation.apply": "로그 필터", "logs.conversation.totals": "{requests}건 요청 · {tokens} 토큰 · {cost}", "logs.conversation.scope": "합계는 현재 로드된 Logs 링만 포함합니다.", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 28f30646bd..36153cd22f 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -673,7 +673,9 @@ export const ru: Record = { "logs.badge.interceptedHelperTitle": "Перехваченный запрос помощника", "logs.filter.conversation.label": "Диалог", "logs.filter.conversation.placeholder": "Вставьте ID диалога", - "logs.filter.conversation.clear": "Сбросить", + "logs.filter.conversation.clear": "Сбросить", + "logs.filter.model.label": "Модель", + "logs.filter.model.placeholder": "Фильтр по модели или провайдеру", "logs.filter.conversation.apply": "Фильтровать логи", "logs.conversation.totals": "{requests} запросов · {tokens} токенов · {cost}", "logs.conversation.scope": "Итоги только по загруженному кольцу Logs.", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 232900f51e..f567faa70d 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -680,7 +680,9 @@ export const tr: Record = { "logs.badge.interceptedHelperTitle": "Yakalanan yardimci istegi", "logs.filter.conversation.label": "Sohbet", "logs.filter.conversation.placeholder": "Sohbet ID'sini yapıştırın", - "logs.filter.conversation.clear": "Temizle", + "logs.filter.conversation.clear": "Temizle", + "logs.filter.model.label": "Model", + "logs.filter.model.placeholder": "Modele veya sağlayıcıya göre filtrele", "logs.filter.conversation.apply": "Günlükleri filtrele", "logs.conversation.totals": "{requests} istek · {tokens} jeton · {cost}", "logs.conversation.scope": "Toplamlar yalnızca yüklü günlükleri kapsar.", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index b491a7bc94..616fa1044c 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -527,7 +527,9 @@ export const zhTW: Record = { "logs.badge.interceptedHelperTitle": "已攔截的輔助請求", "logs.filter.conversation.label": "對話", "logs.filter.conversation.placeholder": "貼上對話 ID", - "logs.filter.conversation.clear": "清除", + "logs.filter.conversation.clear": "清除", + "logs.filter.model.label": "模型", + "logs.filter.model.placeholder": "依模型或供應商篩選", "logs.filter.conversation.apply": "篩選日誌", "logs.conversation.totals": "{requests} 次請求 · {tokens} tokens · {cost}", "logs.conversation.scope": "合計僅涵蓋目前已載入的 Logs 環形緩衝。", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 189a61fa20..733ce1728e 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -668,7 +668,9 @@ export const zh: Record = { "logs.badge.interceptedHelperTitle": "已拦截的辅助请求", "logs.filter.conversation.label": "会话", "logs.filter.conversation.placeholder": "粘贴会话 ID", - "logs.filter.conversation.clear": "清除", + "logs.filter.conversation.clear": "清除", + "logs.filter.model.label": "模型", + "logs.filter.model.placeholder": "按模型或供应商筛选", "logs.filter.conversation.apply": "筛选日志", "logs.conversation.totals": "{requests} 次请求 · {tokens} tokens · {cost}", "logs.conversation.scope": "合计仅覆盖当前已加载的 Logs 环形缓冲。", diff --git a/gui/src/pages/Logs.tsx b/gui/src/pages/Logs.tsx index e7384a96b2..07aec2b12a 100644 --- a/gui/src/pages/Logs.tsx +++ b/gui/src/pages/Logs.tsx @@ -21,6 +21,7 @@ import { formatEstimatedUsd, formatEstimatedUsdValue, summarizeEstimatedCosts } import { cacheSplit, isCursorUsageProvider, tokensTitle } from "./logs-token-title"; import type { LogSurface, LogSurfaceFilter } from "./logs-surface-filter"; import { logMatchesSurface } from "./logs-surface-filter"; +import { logMatchesModelQuery } from "./logs-model-filter"; import { sanitizeLogEntryRouteDecision, validCachedRouteDecision, @@ -372,6 +373,7 @@ export default function Logs({ apiBase }: { apiBase: string }) { const [surfaceFilter, setSurfaceFilter] = useState("all"); const [interceptedHelpersOnly, setInterceptedHelpersOnly] = useState(false); const [conversationFilter, setConversationFilter] = useState(""); + const [modelFilter, setModelFilter] = useState(""); const [conversationQueryHash, setConversationQueryHash] = useState(); const scrollContainerRef = useRef(null); const logRetryRef = useRef<{ key: string; failures: number; nextAttemptAt: number; error: unknown }>( @@ -511,6 +513,7 @@ export default function Logs({ apiBase }: { apiBase: string }) { const filteredLogs = logs.filter(log => ( logMatchesSurface(log, surfaceFilter) && (!interceptedHelpersOnly || Boolean(log.shadowCallRewrittenFrom)) + && logMatchesModelQuery(log, modelFilter) && (!conversationQuery || matchesLogConversationId(log.conversationId, conversationQuery, conversationQueryHash)) )); const conversationTotals = conversationQuery ? summarizeFilteredLogs(filteredLogs) : null; @@ -630,6 +633,17 @@ export default function Logs({ apiBase }: { apiBase: string }) { aria-label={t("logs.filter.conversation.label")} /> + {conversationQuery && (