From bd4ccf1635869d4ba3c668b4acd073c35736f8d3 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 20 May 2026 17:23:14 +0400 Subject: [PATCH] v2.6.0: Usage Dashboard, per-provider tracking, OAuth file picker - Usage Dashboard: visual cards with success rate bars, token stats, latency - Per-model breakdown and error tracking per provider - Proxy records usage-stats.json after every request - Google OAuth: browse for client_secret.json instead of fixed path - Auto-copies selected file to ~/.cache/codex-proxy/ --- CHANGELOG.md | 14 ++ codex-launcher_2.5.1_all.deb | Bin 32020 -> 0 bytes codex-launcher_2.6.0_all.deb | Bin 0 -> 34216 bytes src/codex-launcher-gui | 252 +++++++++++++++++++++++++++++++++-- src/translate-proxy.py | 43 ++++++ 5 files changed, 301 insertions(+), 8 deletions(-) delete mode 100644 codex-launcher_2.5.1_all.deb create mode 100644 codex-launcher_2.6.0_all.deb diff --git a/CHANGELOG.md b/CHANGELOG.md index 8efdc3b..d56be91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## v2.6.0 (2026-05-20) + +- **Usage Dashboard** — per-provider tracking with visual cards + - Request counts, success/failure rates, token usage, latency stats + - Color-coded success rate bars (green/yellow/red) + - Per-model breakdown showing request counts + - Last error and last used timestamp + - Sorted by most-used provider + - Refresh button for live updates +- **Proxy usage tracking** — records every request to `usage-stats.json` +- **Google OAuth**: browse for `client_secret.json` with file picker dialog + - No longer requires copying to a specific path manually + - Auto-copies selected file to `~/.cache/codex-proxy/` + ## v2.5.1 (2026-05-20) - **Adaptive retry for transient errors** (429/502/503) diff --git a/codex-launcher_2.5.1_all.deb b/codex-launcher_2.5.1_all.deb deleted file mode 100644 index 22ad5acf4c8aea9d8294e169569976e33452e406..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32020 zcmaf)Q;aZ55Ukg>ZQHhO+qP}nw*9Yd+qP|c@40-voyjEg(8;`flj^DrA&;Sxu>~KL ziK&sLp&hM}rJbRZCjkKgBRe}M69*F;BNG7u!++=hyE8H{u(7ZZ5d63PPZ>Zl(J??7 z+uOM~+1t>$7&_5;IlKIS``I~}|6e~v3;mA?0J$y(0GV|Nqk*k61I!QuOtSDbyc0r_ zB$Lvy2=Z%rYJ_OO&LOKwANPQTOj~$FLyy&pGydBK84 z%7Oh>1^slpA$;K3mPHulF8@&%2W9?V_>W?+o}&s%BvqLVR&878)9QY=ybw;T;&{re z9+*!?ByMygg_ToHRS}J}kthW)aAe7EvJj=0)Q;in+bb8M8`<)3CWZ{Mq^YcGh!Yt( zQl?BC3h2X0&+_@am$70n*NA{R=;kyOh#&%*WE6|~SB50g$f|!WSG!bZRH{`!DMr8Pe_O@pmwn}1g5#ZzeIx@7guG=M!vUh~t( zlkwJis)OD&(XI^x+g>-O8w^c9dPwL;)pQ-wHA--Y5Fp}&Nl}f}(<_x|+||__9v;m} zZj#W8K}ri><&UMBlQ-!t_zQoH-Wr0xwxw7s2m*sKSa-s3tH8R z{K{y|4i$>hvLL_>Z!y1}2lV(psC#Q}<)43p?WFSeyNUIrL>eCxM+|1C4uFlu=`8 zcM9w^c8>;r~PGKVJTSR5CL% zF#m5d4WRxfRUj51Z$OEPdMky{I4f#=Q-K01X~?DgnsaRU_(L_QL0#U)G|~{fwaUIez7}={d?aanzkW zZpz(UZco}?)0*$PuN8hYJkj^At>R^CsGpJv)_8_`R}sX*}vV4TzEB<{oze#em&WZn=zZ*Ok1(rM|ipV zKKVp*y8C!d-kCCA*|z*?Jgj%+0NjWURJC>rvNoT-NTG(-7%+eae3AFaF2_>Qw2b?kk<) zAN>eBL<-TXAdYco|E>|)my(rLu@~R1<^T1X7qS#p{#5;}KDBLR=5z818cuNdDwN)J znSE~@=&JZvTP_b}s?9y|+WW0LHdtGirpjNZM^Pir;q7Y~*YVRrY~Tg%sc7nWiFzelLYZ)>=6l@Q&T``Gif zz}&&h31D9UHIj$qqvP6dNv@!rqnZXR3~a0x2TbC{!Q_Xtf}1-B+hNZ6>K|YvJ3&Lu zr6TKneH9oOw323$IB;so0imTu1(t&XpglE6i1tea!x=d5aiaz!1$#2LW$Uq{uFaJt zzyDTtrZZxfej55&o5REx7lyos2qz;Up`eluRVD-^45?*ovt8b(If8)e;2c{S6>-RZ(W%0D;XIb6`2Zw0;MDbD;f%Tu|L1+5b=~B5L_8* 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 F(fWwL32FcU diff --git a/codex-launcher_2.6.0_all.deb b/codex-launcher_2.6.0_all.deb new file mode 100644 index 0000000000000000000000000000000000000000..ee62bdfd1fd6585e17b3aebb941320b78905794b GIT binary patch literal 34216 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⋙fSz7BC%4Ei3_kmR!YCHNQFs8^{ zuzLVj)cNT)DLvXqP}RF8>VV-O4{|q7I>u;gBjg3!&_9jt>8}%^0xnR1Dvhl1+WW%i-xaT@%dHIy?dITNbomK6yeJ5BH&@_&)y0rQ+5GrIAh0k&c6t* zH5ud#j#EyDlCWmC`8Ut7pd^Es9GqyB!miOrop&V(o;yIwl~^I6GeHf8TtcFC#oO_; zDx!7t-MU9riWrYcfeo4y`UE&q)dLDiYjyNTkdps~C-4y1z?w*H`4?slf;Vr∓H3f2?&7oBmaizfAto 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 VXpivZ-FwtjB0Avm2)4KTTnhg(0N4Nk literal 0 HcmV?d00001 diff --git a/src/codex-launcher-gui b/src/codex-launcher-gui index 9fd8e7c..19a9099 100755 --- a/src/codex-launcher-gui +++ b/src/codex-launcher-gui @@ -25,6 +25,11 @@ model_catalog_json = "" """ CHANGELOG = [ + ("2.6.0", "2026-05-20", [ + "Usage Dashboard — per-provider request/token/latency tracking", + "Visual cards with success rate bars, model breakdown, error tracking", + "Google OAuth: browse for client_secret.json instead of fixed path", + ]), ("2.5.1", "2026-05-20", [ "Adaptive retry for 429/502/503 errors with exponential backoff", "BGP routes also retry transient errors before failing over", @@ -635,12 +640,15 @@ class LauncherWin(Gtk.Window): # header row hdr = Gtk.Box(spacing=8) vbox.pack_start(hdr, False, False, 0) - lbl = Gtk.Label(label="Codex Launcher v2.5.1") + lbl = Gtk.Label(label="Codex Launcher v2.6.0") lbl.set_use_markup(True) hdr.pack_start(lbl, False, False, 0) changelog_btn = Gtk.Button(label="Changelog") changelog_btn.connect("clicked", lambda b: self._show_changelog()) hdr.pack_end(changelog_btn, False, False, 0) + usage_btn = Gtk.Button(label="Usage") + usage_btn.connect("clicked", lambda b: self._open_usage()) + hdr.pack_end(usage_btn, False, False, 0) bgp_btn = Gtk.Button(label="AI BGP") bgp_btn.connect("clicked", lambda b: self._open_bgp()) hdr.pack_end(bgp_btn, False, False, 0) @@ -942,6 +950,15 @@ class LauncherWin(Gtk.Window): d = Gtk.MessageDialog(self, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, f"Error: {e}") d.run(); d.destroy() + def _open_usage(self): + try: + self._usage_window = UsageWindow(self) + self._usage_window.connect("destroy", lambda *_: setattr(self, "_usage_window", None)) + except Exception as e: + import traceback; traceback.print_exc() + d = Gtk.MessageDialog(self, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, f"Error: {e}") + d.run(); d.destroy() + def _backup_profile(self): chooser = Gtk.FileChooserDialog( title="Backup Codex Profile", @@ -1807,12 +1824,15 @@ class EditEndpointDialog(Gtk.Dialog): def _google_oauth_flow(self): token_path = os.path.expanduser("~/.cache/codex-proxy/google-oauth-token.json") - client_secret_path = os.path.expanduser("~/.cache/codex-proxy/client_secret.json") + default_cs_path = os.path.expanduser("~/.cache/codex-proxy/client_secret.json") + client_secret_path = None - if not os.path.exists(client_secret_path): + if os.path.exists(default_cs_path): + client_secret_path = default_cs_path + else: dlg = Gtk.Dialog(title="Google OAuth Setup", parent=self, modal=True) dlg.add_button("Open Google Console", 1) - dlg.add_button("I have client_secret.json", 2) + dlg.add_button("Browse for client_secret.json", 2) dlg.add_button("Cancel", Gtk.ResponseType.CANCEL) dlg.set_default_size(500, 320) area = dlg.get_content_area() @@ -1828,9 +1848,9 @@ class EditEndpointDialog(Gtk.Dialog): "2. APIs & Services → Credentials\n" " → Create OAuth 2.0 Client ID (Desktop app)\n" "3. Add redirect URI: http://localhost:8085\n" - "4. Download client_secret.json\n" - f"5. Save to: {client_secret_path}\n\n" - "Then click 'I have client_secret.json'" + "4. Download client_secret.json\n\n" + "Then click 'Browse for client_secret.json'\n" + f"It will be auto-copied to: {default_cs_path}" ), xalign=0) area.pack_start(steps, False, False, 4) area.show_all() @@ -1838,7 +1858,32 @@ class EditEndpointDialog(Gtk.Dialog): dlg.destroy() if r == 1: subprocess.Popen(["xdg-open", "https://console.cloud.google.com/apis/credentials"]) - return + return + if r == 2: + chooser = Gtk.FileChooserDialog( + title="Select client_secret.json", + parent=self, + action=Gtk.FileChooserAction.OPEN, + ) + chooser.add_button("Cancel", Gtk.ResponseType.CANCEL) + chooser.add_button("Open", Gtk.ResponseType.OK) + filt = Gtk.FileFilter() + filt.set_name("JSON files") + filt.add_pattern("*.json") + chooser.add_filter(filt) + chooser.set_current_folder(os.path.expanduser("~/Downloads")) + if chooser.run() == Gtk.ResponseType.OK: + src = chooser.get_filename() + chooser.destroy() + if src and os.path.exists(src): + import shutil as _shutil + os.makedirs(os.path.dirname(default_cs_path), exist_ok=True) + _shutil.copy2(src, default_cs_path) + client_secret_path = default_cs_path + else: + chooser.destroy() + if not client_secret_path: + return with open(client_secret_path) as f: cs = json.load(f) @@ -2465,6 +2510,197 @@ class BGPRouteDialog(Gtk.Dialog): self._combo_model.set_active(0) +_USAGE_COLORS = { + "green": "#27ae60", "yellow": "#f39c12", "orange": "#e67e22", + "red": "#e74c3c", "blue": "#3498db", "purple": "#9b59b6", + "dark": "#2c3e50", "light": "#ecf0f1", "mid": "#bdc3c7", +} + +_USAGE_STATS_FILE = HOME / ".cache/codex-proxy/usage-stats.json" + +def _load_usage_stats(): + try: + if _USAGE_STATS_FILE.exists(): + return json.loads(_USAGE_STATS_FILE.read_text()) + except Exception: + pass + return {"providers": {}, "updated": None} + +def _bar_color(pct): + if pct < 0.5: + return _USAGE_COLORS["green"] + if pct < 0.8: + return _USAGE_COLORS["yellow"] + return _USAGE_COLORS["red"] + +class UsageWindow(Gtk.Window): + def __init__(self, parent): + super().__init__(title="Usage Stats") + self.set_transient_for(parent) + self.set_default_size(640, 560) + self.set_position(Gtk.WindowPosition.CENTER) + self._parent = parent + + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) + self.add(vbox) + + header = Gtk.Box(spacing=8) + header.set_margin_start(16) + header.set_margin_end(16) + header.set_margin_top(12) + header.set_margin_bottom(8) + vbox.pack_start(header, False, False, 0) + title = Gtk.Label() + title.set_markup('Usage Dashboard') + header.pack_start(title, False, False, 0) + refresh_btn = Gtk.Button(label="Refresh") + refresh_btn.connect("clicked", lambda b: self._refresh()) + header.pack_end(refresh_btn, False, False, 0) + self._updated_lbl = Gtk.Label() + self._updated_lbl.set_markup('Never') + header.pack_end(self._updated_lbl, False, False, 8) + + sep = Gtk.Separator() + vbox.pack_start(sep, False, False, 0) + + self._cards_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) + sw = Gtk.ScrolledWindow() + sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + sw.add(self._cards_box) + vbox.pack_start(sw, True, True, 0) + + self._refresh() + self.show_all() + + def _refresh(self): + for c in self._cards_box.get_children(): + self._cards_box.remove(c) + stats = _load_usage_stats() + updated = stats.get("updated") + if updated: + self._updated_lbl.set_markup(f'Updated: {updated}') + providers = stats.get("providers", {}) + if not providers: + empty = Gtk.Label() + empty.set_markup('No usage data yet.\nLaunch a session to start tracking.') + empty.set_margin_top(60) + self._cards_box.pack_start(empty, False, False, 0) + self._cards_box.show_all() + return + + sorted_providers = sorted(providers.items(), key=lambda x: x[1].get("total_requests", 0), reverse=True) + for prov_name, prov_data in sorted_providers: + card = self._build_card(prov_name, prov_data) + self._cards_box.pack_start(card, False, False, 0) + self._cards_box.show_all() + + def _build_card(self, name, data): + frame = Gtk.Frame() + frame.set_margin_start(12) + frame.set_margin_end(12) + frame.set_margin_top(4) + frame.set_margin_bottom(4) + style = frame.get_style_context() + style.add_class("card") + + outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) + outer.set_margin_start(12) + outer.set_margin_end(12) + outer.set_margin_top(8) + outer.set_margin_bottom(8) + frame.add(outer) + + top_row = Gtk.Box(spacing=8) + outer.pack_start(top_row, False, False, 0) + + total = data.get("total_requests", 0) + ok = data.get("successes", 0) + fail = data.get("failures", 0) + success_rate = ok / total if total > 0 else 1.0 + + name_lbl = Gtk.Label() + short = name.replace("https://", "").replace("http://", "").split("/")[0] + name_lbl.set_markup(f'{short}') + top_row.pack_start(name_lbl, False, False, 0) + + req_lbl = Gtk.Label() + req_lbl.set_markup(f'{total} requests') + top_row.pack_start(req_lbl, False, False, 8) + + if fail > 0: + err_lbl = Gtk.Label() + err_lbl.set_markup(f'{fail} failed') + top_row.pack_start(err_lbl, False, False, 4) + + last_used = data.get("last_used", "") + if last_used: + lu_lbl = Gtk.Label() + lu_lbl.set_markup(f'{last_used}') + top_row.pack_end(lu_lbl, False, False, 0) + + # Progress bar for success rate + bar = Gtk.ProgressBar() + bar.set_fraction(success_rate) + bar_pct = int(success_rate * 100) + bar.set_text(f"{bar_pct}% success") + bar.set_show_text(True) + bar.set_margin_top(2) + bar.set_margin_bottom(2) + color = _bar_color(1.0 - success_rate) + bar_css = f'progress {{ background-color: {color}; border-radius: 4px; }} trough {{ border-radius: 4px; min-height: 10px; }}' + provider = Gtk.CssProvider() + provider.load_from_data(bar_css.encode()) + bar.get_style_context().add_provider(provider, Gtk.STYLE_PROVIDER_PRIORITY_USER) + outer.pack_start(bar, False, False, 0) + + # Stats row + stats_row = Gtk.Box(spacing=16) + outer.pack_start(stats_row, False, False, 0) + + t_in = data.get("total_tokens_in", 0) + t_out = data.get("total_tokens_out", 0) + dur = data.get("total_duration_s", 0.0) + avg_dur = dur / total if total > 0 else 0 + + for label, value in [ + ("Tokens In", f"{t_in:,}"), + ("Tokens Out", f"{t_out:,}"), + ("Avg Latency", f"{avg_dur:.1f}s"), + ]: + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=1) + l = Gtk.Label() + l.set_markup(f'{label}') + box.pack_start(l, False, False, 0) + v = Gtk.Label() + v.set_markup(f'{value}') + box.pack_start(v, False, False, 0) + stats_row.pack_start(box, False, False, 0) + + # Models breakdown + models = data.get("models", {}) + if len(models) > 0: + model_str = " ".join( + f'{m} ' + f'({md.get("requests",0)})' + for m, md in sorted(models.items(), key=lambda x: x[1].get("requests", 0), reverse=True)[:4] + ) + m_lbl = Gtk.Label() + m_lbl.set_markup(f'Models: {model_str}') + m_lbl.set_line_wrap(True) + m_lbl.set_xalign(0) + outer.pack_start(m_lbl, False, False, 2) + + # Error info + last_err = data.get("last_error") + if last_err: + err_lbl = Gtk.Label() + err_lbl.set_markup(f'Last error: {last_err}') + err_lbl.set_xalign(0) + outer.pack_start(err_lbl, False, False, 0) + + return frame + + def main(): for d in [LOG_DIR, PROXY_CONFIG_DIR]: d.mkdir(parents=True, exist_ok=True) diff --git a/src/translate-proxy.py b/src/translate-proxy.py index 80e4e69..1d2442e 100755 --- a/src/translate-proxy.py +++ b/src/translate-proxy.py @@ -141,6 +141,43 @@ _pool = uuid.uuid4().hex[:8] _response_store = {} _MAX_STORED = 50 +_stats_path = os.path.join(_LOG_DIR, "usage-stats.json") +_stats_lock = threading.Lock() + +def _load_stats(): + try: + if os.path.exists(_stats_path): + return json.load(open(_stats_path)) + except Exception: + pass + return {"providers": {}, "updated": None} + +def _record_usage(provider, model, success, duration_s, tokens_in=0, tokens_out=0, error_type=None): + with _stats_lock: + stats = _load_stats() + p = stats["providers"].setdefault(provider, { + "total_requests": 0, "successes": 0, "failures": 0, + "total_tokens_in": 0, "total_tokens_out": 0, + "total_duration_s": 0.0, "models": {}, "last_used": None, "last_error": None, + }) + p["total_requests"] += 1 + p["total_tokens_in"] += tokens_in + p["total_tokens_out"] += tokens_out + p["total_duration_s"] += duration_s + p["last_used"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) + if success: + p["successes"] += 1 + else: + p["failures"] += 1 + p["last_error"] = error_type or "unknown" + m = p["models"].setdefault(model, {"requests": 0, "tokens_in": 0, "tokens_out": 0}) + m["requests"] += 1 + m["tokens_in"] += tokens_in + m["tokens_out"] += tokens_out + stats["updated"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) + with open(_stats_path, "w") as f: + json.dump(stats, f, indent=2) + def store_response(resp_id, input_data, output_items): if not resp_id: return @@ -1161,6 +1198,10 @@ class Handler(http.server.BaseHTTPRequestHandler): def _forward_oa_compat(self, upstream, stream, model, chat_body, body, input_data, fwd, target): n_items = len(input_data) if isinstance(input_data, list) else 1 + t0 = time.time() + provider = TARGET_URL.split("//")[-1].split("/")[0] + if BGP_ROUTES: + provider = "bgp:" + (BGP_ROUTES[0].get("name", "pool") if BGP_ROUTES else "unknown") if stream: self.send_response(200) @@ -1205,6 +1246,7 @@ class Handler(http.server.BaseHTTPRequestHandler): _log_resp(last_resp_id, last_status, last_output) if last_resp_id and input_data is not None: store_response(last_resp_id, input_data, last_output) + _record_usage(provider, model, success, time.time() - t0, error_type="length" if not success else None) # Auto-retry on finish_reason=length with no content if finish_reason == "length" and not has_content and isinstance(input_data, list) and len(input_data) > 5: @@ -1234,6 +1276,7 @@ class Handler(http.server.BaseHTTPRequestHandler): _log_resp(rid, result.get("status"), result.get("output", [])) if rid and input_data is not None: store_response(rid, input_data, result.get("output", [])) + _record_usage(provider, model, success, time.time() - t0) def _forward_oa_compat_retry(self, req, model, chat_body, body, input_data): try: