diff --git a/src/ai_client.py b/src/ai_client.py index a1f21851..ffdeea87 100644 --- a/src/ai_client.py +++ b/src/ai_client.py @@ -3537,3 +3537,13 @@ register(VendorCapabilities(vendor='deepseek', model='deepseek-v3', conte register(VendorCapabilities(vendor='deepseek', model='deepseek-reasoner', context_window=32768, cost_input_per_mtok=0.55, cost_output_per_mtok=2.19, reasoning=True, structured_output=True)) register(VendorCapabilities(vendor='deepseek', model='deepseek-r1', context_window=32768, cost_input_per_mtok=0.55, cost_output_per_mtok=2.19, reasoning=True, structured_output=True)) #endregion: Vendor Capabilities + +#region: Vendor State (moved from src/vendor_state.py) +@dataclass(frozen=True) +class VendorMetric: + key: str + label: str + value: str + state: str + tooltip: str +#endregion: Vendor State diff --git a/src/gui_2.py b/src/gui_2.py index 6bf14e4b..14448360 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -97,6 +97,7 @@ filedialog = _LazyModule("tkinter", "filedialog") # was: from tkinter import fi Tk = _LazyModule("tkinter", "Tk") # was: from tkinter import Tk from src import ai_client +from src.ai_client import VendorMetric from src import aggregate from src import api_hooks from src import app_controller @@ -5628,8 +5629,7 @@ def render_vendor_state(app: App) -> None: | Last Error | (none) | info | +---------------------------------------------------------+ """ - from src.vendor_state import get_vendor_state - metrics = get_vendor_state(app) + metrics = _get_vendor_state_metrics(app) if imgui.begin_table("vendor_state", 3, imgui.TableFlags_.row_bg | imgui.TableFlags_.borders): imgui.table_setup_column("Metric", imgui.TableColumnFlags_.width_fixed, 180) imgui.table_setup_column("Value", imgui.TableColumnFlags_.width_stretch) @@ -8704,3 +8704,70 @@ def render_palette_modal(app: Any, commands: List[Any]) -> None: imgui.end_child() imgui.end() #endregion: Command Palette Modal + +#region: Vendor State Metrics (moved from src/vendor_state.py; VendorMetric dataclass lives in src/ai_client.py) +def _get_vendor_state_metrics(app: Any) -> list[Any]: + out: list[Any] = [] + out.append(VendorMetric( + key = "provider_model", + label = "Provider / Model", + value = f"{app.current_provider} / {app.current_model}", + state = "info", + tooltip = "The vendor and model that will handle the next request." + )) + ctrl = getattr(app, "controller", None) + tt = getattr(ctrl, "token_tracker", None) if ctrl else None + if tt and getattr(tt, "limit", 0): + pct = 100.0 * getattr(tt, "used", 0) / tt.limit + state = "warn" if pct > 75 else "ok" + out.append(VendorMetric( + key = "context_window", + label = "Context Window", + value = f"{tt.used:,} / {tt.limit:,} ({pct:.0f}%)", + state = state, + tooltip = "Used vs total context window for the current session." + )) + else: + out.append(VendorMetric( + key = "context_window", label="Context Window", value="—", state="info", + tooltip = "No token tracker attached for the current provider." + )) + if tt is not None: + hits = getattr(tt, "cache_hits", 0) + miss = getattr(tt, "cache_misses", 0) + total = hits + miss + rate = (100.0 * hits / total) if total else 0.0 + out.append(VendorMetric( + key = "cache", label="Cache Hit Rate", + value = f"{rate:.0f}% ({hits:,}/{total:,})", + state = "ok" if rate > 50 else "info", + tooltip = "Server-side prompt cache hit rate for the current session." + )) + else: + out.append(VendorMetric( + key = "cache", label="Cache Hit Rate", value="—", state="info", + tooltip = "No token tracker attached for the current provider." + )) + quota = (getattr(ctrl, "vendor_quota", {}) or {}) if ctrl else {} + pct_left = quota.get("remaining_pct") + if pct_left is None: + out.append(VendorMetric( + key = "quota", label="Vendor Quota", value="—", state="info", + tooltip = "Vendor did not report quota for the current billing period." + )) + else: + out.append(VendorMetric( + key = "quota", label="Vendor Quota", + value = f"{pct_left}% remaining", + state = "ok" if pct_left > 25 else "warn", + tooltip = "Approximate quota remaining for the current billing period." + )) + err = getattr(ctrl, "last_error", None) if ctrl else None + out.append(VendorMetric( + key = "last_error", label="Last Error", + value = err.get("class", "none") if err else "none", + state = "error" if err else "ok", + tooltip = err.get("message", "No error since session start.") if err else "No error since session start." + )) + return out +#endregion: Vendor State Metrics diff --git a/src/vendor_state.py b/src/vendor_state.py deleted file mode 100644 index d1d404cc..00000000 --- a/src/vendor_state.py +++ /dev/null @@ -1,81 +0,0 @@ -from dataclasses import dataclass - - -@dataclass(frozen=True) -class VendorMetric: - """Atomic vendor-state metric. - [C: src/gui_2.py:render_vendor_state] - """ - key: str - label: str - value: str - state: str - tooltip: str - -def get_vendor_state(app) -> list[VendorMetric]: - """Aggregate per-vendor session state for the Operations Hub Vendor State tab. - [C: src/gui_2.py:render_vendor_state] - """ - out: list[VendorMetric] = [] - out.append(VendorMetric( - key = "provider_model", - label = "Provider / Model", - value = f"{app.current_provider} / {app.current_model}", - state = "info", - tooltip = "The vendor and model that will handle the next request." - )) - ctrl = getattr(app, "controller", None) - tt = getattr(ctrl, "token_tracker", None) if ctrl else None - if tt and getattr(tt, "limit", 0): - pct = 100.0 * getattr(tt, "used", 0) / tt.limit - state = "warn" if pct > 75 else "ok" - out.append(VendorMetric( - key = "context_window", - label = "Context Window", - value = f"{tt.used:,} / {tt.limit:,} ({pct:.0f}%)", - state = state, - tooltip = "Used vs total context window for the current session." - )) - else: - out.append(VendorMetric( - key = "context_window", label="Context Window", value="—", state="info", - tooltip = "No token tracker attached for the current provider." - )) - if tt is not None: - hits = getattr(tt, "cache_hits", 0) - miss = getattr(tt, "cache_misses", 0) - total = hits + miss - rate = (100.0 * hits / total) if total else 0.0 - out.append(VendorMetric( - key = "cache", label="Cache Hit Rate", - value = f"{rate:.0f}% ({hits:,}/{total:,})", - state = "ok" if rate > 50 else "info", - tooltip = "Server-side prompt cache hit rate for the current session." - )) - else: - out.append(VendorMetric( - key = "cache", label="Cache Hit Rate", value="—", state="info", - tooltip = "No token tracker attached for the current provider." - )) - quota = (getattr(ctrl, "vendor_quota", {}) or {}) if ctrl else {} - pct_left = quota.get("remaining_pct") - if pct_left is None: - out.append(VendorMetric( - key = "quota", label="Vendor Quota", value="—", state="info", - tooltip = "Vendor did not report quota for the current billing period." - )) - else: - out.append(VendorMetric( - key = "quota", label="Vendor Quota", - value = f"{pct_left}% remaining", - state = "ok" if pct_left > 25 else "warn", - tooltip = "Approximate quota remaining for the current billing period." - )) - err = getattr(ctrl, "last_error", None) if ctrl else None - out.append(VendorMetric( - key = "last_error", label="Last Error", - value = err.get("class", "none") if err else "none", - state = "error" if err else "ok", - tooltip = err.get("message", "No error since session start.") if err else "No error since session start." - )) - return out diff --git a/tests/test_vendor_state.py b/tests/test_vendor_state.py index c7a187ec..c93ed252 100644 --- a/tests/test_vendor_state.py +++ b/tests/test_vendor_state.py @@ -1,4 +1,5 @@ -from src.vendor_state import get_vendor_state, VendorMetric +from src.gui_2 import _get_vendor_state_metrics as get_vendor_state +from src.ai_client import VendorMetric class _StubTT: def __init__(self, used=0, limit=0, cache_hits=0, cache_misses=0):