openvidia 2.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,814 @@
1
+ Metadata-Version: 2.4
2
+ Name: openvidia
3
+ Version: 2.2.0
4
+ Summary: NVIDIA NIM multi-key proxy with smart rate limiting, cooldown management, and desktop dashboard
5
+ Author: Ciro Autuori
6
+ License-Expression: MIT
7
+ Keywords: nvidia,nim,proxy,rate-limiting,api-gateway,opencode,desktop
8
+ Classifier: Development Status :: 5 - Production/Stable
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Topic :: Software Development :: Libraries
11
+ Requires-Python: >=3.12
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: fastapi>=0.115
15
+ Requires-Dist: uvicorn[standard]>=0.30
16
+ Requires-Dist: httpx[http2]>=0.27
17
+ Requires-Dist: psutil>=6.0
18
+ Requires-Dist: pywebview>=5.0
19
+ Requires-Dist: PyQt6>=6.5; platform_system == "Linux"
20
+ Requires-Dist: PyQt6-WebEngine>=6.5; platform_system == "Linux"
21
+ Requires-Dist: qtpy>=2.4; platform_system == "Linux"
22
+ Requires-Dist: tomlkit>=0.13
23
+ Provides-Extra: redis
24
+ Requires-Dist: redis>=5.0; extra == "redis"
25
+ Dynamic: license-file
26
+
27
+ # OpenVidia
28
+
29
+ ![Python 3.12+](https://img.shields.io/badge/Python-3.12%2B-blue?logo=python&logoColor=white)
30
+ ![License: MIT](https://img.shields.io/badge/License-MIT-yellow)
31
+ [![CI](https://github.com/autcir/openvidia/actions/workflows/ci.yml/badge.svg)](https://github.com/autcir/openvidia/actions/workflows/ci.yml)
32
+ ![Ruff](https://img.shields.io/badge/Ruff-passed-261230?logo=ruff&logoColor=white)
33
+ ![Stars](https://img.shields.io/github/stars/autcir/openvidia?style=social)
34
+ ![Last Commit](https://img.shields.io/github/last-commit/autcir/openvidia)
35
+ [![Release](https://img.shields.io/github/v/release/autcir/openvidia)](https://github.com/autcir/openvidia/releases)
36
+
37
+ **Multi-key proxy for NVIDIA NIM with a native desktop dashboard.**
38
+
39
+ <p align="center">
40
+ <img src="openvidia/web/assets/dashboard.gif" alt="OpenVidia dashboard — pooled keys, ★ Starred model shortlist, live activity log" width="400">
41
+ </p>
42
+
43
+ Pool multiple free-tier API keys behind one endpoint. Automatic rotation, per-key cooldown, sliding-window RPM limiting, and a compact desktop app — no browser needed.
44
+
45
+ Built for [opencode](https://opencode.ai), [Codex CLI](https://github.com/openai/codex), [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Grok](https://x.ai), [Jcode](https://github.com/1jehuang/jcode), and any OpenAI-compatible client.
46
+
47
+ ---
48
+
49
+ ## Quick Start
50
+
51
+ ### Linux (Arch / Ubuntu / Fedora)
52
+
53
+ ```bash
54
+ git clone https://github.com/autcir/openvidia.git
55
+ cd openvidia
56
+ ./install.sh
57
+ ```
58
+
59
+ Or manually:
60
+
61
+ ```bash
62
+ # Install uv (recommended) — https://astral.sh/uv
63
+ curl -LsSf https://astral.sh/uv/install.sh | sh
64
+
65
+ uv sync # install dependencies
66
+ uv run openvidia setup # auto-configure opencode
67
+ uv run openvidia # start proxy + desktop app
68
+ ```
69
+
70
+ With pip:
71
+
72
+ ```bash
73
+ pip install -e .
74
+ openvidia setup
75
+ openvidia
76
+ ```
77
+
78
+ ### macOS
79
+
80
+ ```bash
81
+ brew install python@3.12 pygobject pkg-config
82
+ git clone https://github.com/autcir/openvidia.git
83
+ cd openvidia
84
+ pip install -e .
85
+ openvidia setup
86
+ openvidia
87
+ ```
88
+
89
+ > pywebview on macOS uses system WebKit (native, no extra deps).
90
+ > If you hit a GTK build error, install `pygobject` via Homebrew or just skip it — macOS doesn't need it.
91
+
92
+ ### Windows
93
+
94
+ ```cmd
95
+ git clone https://github.com/autcir/openvidia.git
96
+ cd openvidia
97
+ pip install -e .
98
+ openvidia setup
99
+ openvidia
100
+ ```
101
+
102
+ > pywebview on Windows uses EdgeChromium (WebView2, pre-installed on Windows 10/11).
103
+ > If WebView2 is missing, install it from [Microsoft](https://developer.microsoft.com/en-us/microsoft-edge/webview2/).
104
+
105
+ ---
106
+
107
+ ## How It Works
108
+
109
+ ```
110
+ ┌─────────────────────────────────────────────────────────────┐
111
+ │ OpenVidia (:1919) │
112
+ │ │
113
+ │ ┌─────────────────────────────────────────────────────┐ │
114
+ │ │ Desktop App (pywebview) │ │
115
+ │ │ 310×570 native window — Keys, Presets, Models, │ │
116
+ │ │ Activity log, CLI setup — all in one panel │ │
117
+ │ └─────────────────────────────────────────────────────┘ │
118
+ │ │ │
119
+ │ ┌─────────────────────────────────────────────────────┐ │
120
+ │ │ Proxy Engine (:1919/v1) │ │
121
+ │ │ │ │
122
+ │ │ Request → override model → pick key → forward │ │
123
+ │ │ ↑ ↑ ↑ │ │
124
+ │ │ │ cooldown? RPM < 28? │ │
125
+ │ │ │ skip if yes skip if no │ │
126
+ │ │ │ │ │
127
+ │ │ On 429: read Retry-After → set cooldown → next key │ │
128
+ │ │ On 401/403: cooldown 3600s (dead key) │ │
129
+ │ │ On 400/404: cooldown 60s (deterministic) │ │
130
+ │ │ On 504: cooldown 30s (model bottleneck, not key) │ │
131
+ │ │ On 5xx: cooldown 10s (gateway) / 30s (other) │ │
132
+ │ │ On network error: no cooldown, 0.5s pause │ │
133
+ │ │ 3 consecutive network errors → stop (network down) │ │
134
+ │ └─────────────────────────────────────────────────────┘ │
135
+ │ │ │
136
+ │ NVIDIA NIM API │
137
+ │ integrate.api.nvidia.com/v1 │
138
+ └─────────────────────────────────────────────────────────────┘
139
+ ```
140
+
141
+ ---
142
+
143
+ ## Why?
144
+
145
+ NVIDIA's free NIM tier limits each API key to ~40 RPM. Aggressive bursts trigger a **penalty box** that can lock keys for hours. OpenVidia:
146
+
147
+ - **Pools multiple keys** behind a single endpoint
148
+ - **Rotates automatically** on 429/401/403/5xx — zero manual intervention
149
+ - **Per-key cooldown timers** — on real 429s, trusts `Retry-After` exactly (no multiplication); distinguishes transient worker saturation (`ResourceExhausted`) from true RPM limits and never burns key cooldowns for the former
150
+ - **Network-error circuit breaker** — `ConnectTimeout`/`ConnectError` don't trigger key cooldown (the key is fine). After 3 consecutive network errors across different keys, rotation stops entirely and returns 503 (the network is down, no key can help)
151
+ - **504 model timeouts don't burn keys** — gateway timeouts cool the key for 30s but don't consume the attempt budget, since the model (not the key) is the bottleneck
152
+ - **Sliding-window RPM limiting** — keeps each key under 28 RPM (safe margin below 40); the ceiling self-tunes down from real `Retry-After` headers, so the pool never spends its full budget against a tighter upstream limit
153
+ - **Health checks** — revives keys whose cooldowns have expired, but skips 401/403 invalid keys (dead keys stay dead)
154
+ - **No silent model substitution** — the model you select is the model that answers; if it fails on every key you are told so, never handed output from a different model
155
+ - **Auto-compaction** — summarizes long histories so requests never fail on context overflow ([details](#auto-compaction))
156
+ - **Per-key × per-model scoring** — rotation is ordered by a composite score (success rate + median time to first token) learned per key and per model from live traffic: the best key for the requested model goes first
157
+ - **Multi-endpoint routing** — extra NVIDIA endpoints (`endpoints.json` or `OPENVIDIA_UPSTREAM_ENDPOINTS`), a 60s blacklist on ≥500 errors, automatic recovery
158
+ - **Multi-node sync via Redis (opt-in)** — cooldowns, invalid keys, pool throttling and model circuit breakers shared across instances ([details](#resilience--scale))
159
+ - **Embedding cache** — `/v1/embeddings` hits are served from memory (TTL 300s) without touching upstream quota
160
+ - **Free-tier provider fallback** — when every NVIDIA key is exhausted, requests fail over to OpenAI-compatible providers declared in `providers.json` ([details](#resilience--scale))
161
+ - **Graph engine** — programmatic multi-agent orchestration (Hub, run_agent, generator-verifier loops) on top of the proxy ([details](#graph-engine))
162
+
163
+ ---
164
+
165
+ ## CLI Commands
166
+
167
+ | Command | Description |
168
+ |---------|-------------|
169
+ | `openvidia` | Start proxy in background + open desktop app |
170
+ | `openvidia foreground` | Foreground mode (logs to stdout, no UI) |
171
+ | `openvidia setup` | Auto-configure detected CLIs: opencode, Codex, Grok (Claude Code requires manual env vars) |
172
+
173
+ ---
174
+
175
+ ## CLI Setup Guides
176
+
177
+ > **TL;DR — run `openvidia setup` once.** It auto-detects and configures every CLI you have installed. You only need to read the manual steps below if something doesn't work or you prefer to configure things yourself.
178
+
179
+ Supported clients:
180
+
181
+ | CLI | Protocol | Endpoint | Auto-setup |
182
+ |-----|----------|----------|------------|
183
+ | **opencode** | OpenAI-compatible | `http://localhost:1919/v1` | ✅ `openvidia setup` |
184
+ | **Codex CLI** | OpenAI Responses API | `http://localhost:1919/v1` | ✅ `openvidia setup` |
185
+ | **Claude Code** | Anthropic Messages API | `http://localhost:1919` | ❌ manual env vars |
186
+ | **Grok (xAI)** | OpenAI-compatible | `http://localhost:1919/v1` | ✅ `openvidia setup` |
187
+ | **Jcode** | OpenAI-compatible | `http://localhost:1919/v1` | ✅ `openvidia setup` |
188
+
189
+ ---
190
+
191
+ ### opencode
192
+
193
+ ```bash
194
+ openvidia setup # configures provider + model + compaction + instructions
195
+ opencode # then /model openvidia
196
+ ```
197
+
198
+ Manual (if `setup` didn't find it):
199
+
200
+ ```bash
201
+ # ~/.config/opencode/opencode.json
202
+ {
203
+ "provider": {
204
+ "openvidia": {
205
+ "npm": "@ai-sdk/openai-compatible",
206
+ "options": { "apiKey": "ignored", "baseURL": "http://localhost:1919/v1" },
207
+ "models": { "openvidia": { "name": "OpenVidia", "tools": true } }
208
+ }
209
+ },
210
+ "model": "openvidia/openvidia"
211
+ }
212
+ ```
213
+
214
+ ---
215
+
216
+ ### Codex CLI
217
+
218
+ ```bash
219
+ openvidia setup # writes ~/.codex/config.toml automatically
220
+ codex --model openvidia
221
+ ```
222
+
223
+ Manual (if `setup` didn't find it):
224
+
225
+ ```toml
226
+ # ~/.codex/config.toml
227
+ model = "openvidia"
228
+ model_provider = "openvidia"
229
+
230
+ [model_providers.openvidia]
231
+ name = "OpenVidia"
232
+ base_url = "http://localhost:1919/v1"
233
+ env_key = "OPENVIDIA_API_KEY"
234
+ wire_api = "responses"
235
+ ```
236
+
237
+ ```bash
238
+ export OPENVIDIA_API_KEY=ignored # also added automatically by setup
239
+ ```
240
+
241
+ ---
242
+
243
+ ### Claude Code
244
+
245
+ ```bash
246
+ # Add to ~/.zshrc or ~/.bashrc manually (auto-setup is disabled)
247
+ export ANTHROPIC_BASE_URL=http://localhost:1919
248
+ export ANTHROPIC_API_KEY=ignored
249
+ ```
250
+
251
+ ```bash
252
+ source ~/.zshrc # (or restart your terminal)
253
+ claude --model openvidia
254
+ ```
255
+
256
+ > Auto-setup for Claude Code is intentionally disabled to avoid mutating shell rc files. Set the two environment variables manually as shown above.
257
+
258
+ > The `/v1/messages` endpoint translates Anthropic Messages format ↔ NVIDIA chat/completions bidirectionally — streaming, tool use, and system prompts all work.
259
+ >
260
+ > **Images:** NVIDIA NIM models are text-only. Image blocks (e.g. screenshots) are replaced with `[image omitted: model has no vision]` so the model stays aware of the context and logs a warning in the Activity panel.
261
+
262
+ ---
263
+
264
+ ### Grok (xAI)
265
+
266
+ ```bash
267
+ openvidia setup # writes ~/.grok/config.toml automatically
268
+ grok -m openvidia
269
+ ```
270
+
271
+ Manual (if `setup` didn't find `~/.grok/`):
272
+
273
+ ```toml
274
+ # ~/.grok/config.toml
275
+ [models]
276
+ default = "openvidia"
277
+
278
+ [model.openvidia]
279
+ api_key = "ignored"
280
+ base_url = "http://localhost:1919/v1"
281
+ api_backend = "chat_completions"
282
+ context_window = 128000
283
+ ```
284
+
285
+ ---
286
+
287
+ ### Jcode
288
+
289
+ ```bash
290
+ openvidia setup # writes ~/.jcode/config.toml automatically
291
+ jcode # then /model openvidia to switch
292
+ ```
293
+
294
+ Manual (if `setup` didn't find `~/.jcode/`):
295
+
296
+ ```toml
297
+ # ~/.jcode/config.toml
298
+
299
+ [provider]
300
+ default_provider = "openvidia"
301
+ default_model = "openvidia"
302
+
303
+ [providers.openvidia]
304
+ type = "openai-compatible"
305
+ base_url = "http://localhost:1919/v1"
306
+ auth = "none"
307
+ default_model = "openvidia"
308
+ requires_api_key = false
309
+ model_catalog = true
310
+ stream_idle_timeout_secs = 300
311
+
312
+ [[providers.openvidia.models]]
313
+ id = "openvidia"
314
+ context_window = 202752
315
+ ```
316
+
317
+ > `openvidia setup` adds the `[providers.openvidia]` block and sets it as default — but only when no provider is configured yet. If you already use another provider, it leaves your choice alone and you can switch with `/model openvidia` inside Jcode.
318
+ >
319
+ > Jcode is an open-source coding agent: [github.com/1jehuang/jcode](https://github.com/1jehuang/jcode)
320
+
321
+ ---
322
+
323
+ ### Any OpenAI-compatible client
324
+
325
+ ```python
326
+ from openai import OpenAI
327
+
328
+ client = OpenAI(base_url="http://localhost:1919/v1", api_key="ignored")
329
+ response = client.chat.completions.create(
330
+ model="openvidia",
331
+ messages=[{"role": "user", "content": "Hello!"}]
332
+ )
333
+ ```
334
+
335
+ ```bash
336
+ curl http://localhost:1919/v1/chat/completions \
337
+ -H "Content-Type: application/json" \
338
+ -H "Authorization: Bearer ignored" \
339
+ -d '{"model":"openvidia","messages":[{"role":"user","content":"Hello!"}]}'
340
+ ```
341
+
342
+ Streaming (SSE) is fully supported — tokens flow through unbuffered.
343
+
344
+ ---
345
+
346
+ ## Smart Rate Limiting
347
+
348
+ ### Per-Key Cooldown
349
+
350
+ | HTTP Status | Cooldown | Reason |
351
+ |-------------|----------|---------|
352
+ | **429** (real RPM limit) | `Retry-After` header (trusted as-is, no multiplication), or 45s + jitter | Rate limited — honour NVIDIA's own window exactly |
353
+ | **429** (worker concurrency) | 0 — retried after 0.8s pause | `ResourceExhausted: Worker local total request limit reached` — transient pool saturation, key is healthy |
354
+ | **401 / 403** | 3600s | Dead key — don't waste requests |
355
+ | **400 / 404** | 60s | Deterministic request error — short cooldown, key untouched for rotation. Every key gets the same error, so rotating only burns cooldown budget. |
356
+ | **504** (gateway timeout) | 30s | Model is the bottleneck, not the key — longer cooldown, but doesn't burn the attempt budget |
357
+ | **5xx** (other) | 10s (502/503) / 30s (500) | Server error — retry soon |
358
+ | **Network error** (ConnectTimeout/ConnectError) | 0 — 0.5s pause, no cooldown | Network is down, not the key. After 3 consecutive errors across different keys, rotation stops entirely (503). |
359
+
360
+ > **Retry-After is used as-is.** When NVIDIA provides a `Retry-After` header (e.g. 60s), the cooldown is exactly that value. Earlier versions multiplied it by an adaptive factor (`1.5^N`), which caused a doom loop: at 3 failures a 60s backoff became 135s and all 26 keys locked out longer than the real rate-limit window required.
361
+ >
362
+ > When no `Retry-After` is provided the proxy uses 45s + random jitter (≤10s). Repeated failures get a flat 1.5× multiplier from the second failure onward (no exponential scaling — capped at ~65s max).
363
+
364
+ ### Sliding-Window RPM
365
+
366
+ Each key tracks requests in a rolling 60-second window. If a key has sent **28+ requests** in the last 60s, it is skipped. Only if all keys are simultaneously RPM-saturated or on cooldown does the proxy return 429 to the client.
367
+
368
+ ### Key Rotation Flow
369
+
370
+ ```
371
+ Request arrives
372
+ │
373
+ ├─ For each candidate key (ordered least-loaded first):
374
+ │ ├─ Key on cooldown? → skip (including keys cooled mid-pass)
375
+ │ ├─ Key RPM ≥ 28? → skip
376
+ │ ├─ Send to NVIDIA → 200? ✅ record RPM, return response
377
+ │ │ → 400/404? set 60s cooldown, next key
378
+ │ │ → 429 ResourceExhausted? pause 0.8s, retry (key untouched)
379
+ │ │ → 429 rate-limit? use Retry-After as-is, set cooldown, next key
380
+ │ │ → 401/403? set 3600s cooldown, next key
381
+ │ │ → 504? set 30s cooldown, next key (doesn't burn attempt budget)
382
+ │ │ → 5xx? set 10s (502/503) or 30s (500), next key
383
+ │ │ → network error? 0.5s pause, key untouched (3 consecutive → stop)
384
+ │ └─ (max 5 sends per pass, 3 passes with 1s pause between)
385
+ │
386
+ └─ All candidates exhausted? → 503 naming the model (never a substitute model)
387
+ ```
388
+
389
+ ### Health Check
390
+
391
+ Every 60 seconds:
392
+ 1. Finds keys still on cooldown (skips 401/403 invalid keys — they're dead, not temporarily rate-limited)
393
+ 2. Sends a lightweight `GET /v1/models` probe
394
+ 3. If the key responds OK — clears the cooldown (revived)
395
+ 4. If still failing — leaves the cooldown in place
396
+
397
+ ---
398
+
399
+ ## Auto-Compaction
400
+
401
+ Long conversations eventually exceed the model's context window. Without handling, the upstream returns a `400`, and — since that error is identical on every key — a naive proxy would burn through the whole pool before dying. **OpenVidia never blocks on context overflow.**
402
+
403
+ Before forwarding a request, if the estimated history exceeds a token budget, OpenVidia compacts it:
404
+
405
+ ```
406
+ history > budget?
407
+ │
408
+ ├─ Cached summary still fits?
409
+ │ └─ Serve summary + EVERY later message verbatim. ← steady state,
410
+ │ zero upstream calls. The summary boundary only moves when this
411
+ │ no longer fits, so you pay for a summarize once every N turns.
412
+ │
413
+ ├─ Boundary must advance → summarize the oldest slice on top of the
414
+ │ previous summary (incremental — never the whole history again).
415
+ │ The verbatim tail is sized to FILL the remaining budget.
416
+ │
417
+ └─ Summary not ready before `inline_deadline`?
418
+ └─ Serve now (cached summary + verbatim remainder, or a
419
+ deterministic trim) while the summarize keeps running
420
+ detached and lands in the cache for the next turn.
421
+ ```
422
+
423
+ - **Never blocks the client** — the request waits at most `inline_deadline` seconds, regardless of how slow the upstream is. Compaction latency is bounded by config, not by the provider.
424
+ - **Works for every client** — hooks `/v1/chat/completions` (opencode / Codex), `/v1/responses`, and the `/v1/messages` Anthropic shim (Claude Code).
425
+ - **Cheap** — the steady state costs zero extra calls; only genuinely new content is ever summarized, and concurrent requests on the same conversation share a single summarize.
426
+ - **Safe** — every fallback is bounded, so a request goes through even with the whole key pool rate-limited.
427
+
428
+ Watch it in the **Activity** log: `⧉ compaction: summarized N new msg (covers M) → …`.
429
+
430
+ ### Tuning
431
+
432
+ > **Set `model_budgets` first.** The generic default (80k) is deliberately conservative because NVIDIA NIM does not advertise a context window on `/v1/models`. If your model accepts more, compaction at 80k throws away context you paid nothing for. To find the real number, send an oversized request — the `400` states it exactly:
433
+ >
434
+ > ```
435
+ > This model's maximum context length is 202752 tokens. However, your
436
+ > messages resulted in 320011 tokens.
437
+ > ```
438
+
439
+ Optional — create `~/.config/openvidia/compaction.json` (built-in defaults shown):
440
+
441
+ ```json
442
+ {
443
+ "enabled": true,
444
+ "budget_tokens": 80000,
445
+ "model_budgets": { "your-provider/your-model": 160000 },
446
+ "reserved_tokens": 8000,
447
+ "compact_ratio": 0.6,
448
+ "keep_recent": 8,
449
+ "summary_model": "",
450
+ "summary_max_tokens": 1024,
451
+ "inline_deadline": 6.0,
452
+ "summarize_timeout": 45.0
453
+ }
454
+ ```
455
+
456
+ | Field | Meaning |
457
+ |-------|---------|
458
+ | `enabled` | Turn compaction on/off |
459
+ | `budget_tokens` | Generic trigger for any model without an explicit budget |
460
+ | `model_budgets` | Per-model context window. **The one setting worth tuning** — leave ~20% headroom, the estimator is a ~4 chars/token approximation |
461
+ | `reserved_tokens` | Generation headroom subtracted from the budget |
462
+ | `compact_ratio` | Compact down to this fraction of the budget. Landing exactly on the threshold re-triggers compaction on the very next turn |
463
+ | `keep_recent` | Floor for the trim fallback. On the summary path the verbatim tail is sized to fill the budget instead |
464
+ | `summary_model` | Model used to summarize. Point it at a **fast** model: it runs on a different traffic stream than the one your agent is saturating. Empty = the default model |
465
+ | `summary_max_tokens` | Cap on the generated summary length |
466
+ | `inline_deadline` | Seconds the *client request* will wait for a summary before being served from the fallback ladder |
467
+ | `summarize_timeout` | Upstream cap for the summarize call itself, which continues in the background past the deadline |
468
+
469
+ ---
470
+
471
+ ## Desktop App
472
+
473
+ Native window via [pywebview](https://pywebview.flowrl.com/). Opens at **310×570 px** — a compact utility panel, like a phone in portrait. Resize freely.
474
+
475
+ | Backend | Platform | Engine |
476
+ |---------|----------|--------|
477
+ | **Qt WebEngine** | Linux (KDE/Wayland) | PyQt6-WebEngine (native, best experience) |
478
+ | **GTK WebKit** | Linux (GNOME/X11) | PyGObject + WebKitGTK |
479
+ | **WebKit** | macOS | system WebKit (no extra deps) |
480
+ | **EdgeChromium** | Windows | WebView2 (pre-installed on Win 10/11) |
481
+
482
+ pywebview auto-detects the best available backend.
483
+
484
+ ### Linux desktop integration
485
+
486
+ ```bash
487
+ # .desktop file (auto-installed by install.sh)
488
+ cp openvidia.desktop ~/.local/share/applications/
489
+ # Icon
490
+ cp openvidia/web/assets/logo.png ~/.local/share/icons/hicolor/256x256/apps/openvidia.png
491
+ update-desktop-database ~/.local/share/applications/
492
+ ```
493
+
494
+ ---
495
+
496
+ ## Dashboard Sections
497
+
498
+ | Section | Features |
499
+ |---------|----------|
500
+ | **Status** | Proxy state, active model, start/stop/restart controls |
501
+ | **Stats** | Request count, success rate, rotations, cooldown counter |
502
+ | **Keys** | Per-key status (Active filter default), live cooldown countdown, RPM, success/fail, freshness dots, add/remove/copy |
503
+ | **Models** | Single list — filters: **★ Starred** (default; your quick-switch shortlist) · All · Popular. Search, test ▶, star/unstar. Active model highlighted and pinned to top. |
504
+ | **Thinking** | `auto` / `on` / `off` next to the active model — a hybrid reasoning model emits nothing while it thinks |
505
+ | **Activity** | Real-time SSE log stream with color-coded levels |
506
+ | **CLI Setup** | Copy-paste config for opencode / Codex / Claude / Grok |
507
+
508
+ ### Key Status Indicators
509
+
510
+ | Indicator | Meaning |
511
+ |-----------|---------|
512
+ | 🟢 Green | Key healthy, has successful requests |
513
+ | 🟡 Amber | Key has failures but not on cooldown |
514
+ | ⚪ Gray | Key idle (no requests yet) |
515
+ | 🔴 Red + ⏳ | Key on cooldown — shows countdown + reason |
516
+ | `active` badge | Currently selected key in rotation |
517
+
518
+ ---
519
+
520
+ ## Configuration
521
+
522
+ ### Config directory
523
+
524
+ | Platform | Path |
525
+ |----------|------|
526
+ | **Linux** | `~/.config/openvidia/` |
527
+ | **macOS** | `~/Library/Application Support/openvidia/` |
528
+ | **Windows** | `%APPDATA%\openvidia\` |
529
+
530
+ ### Config files
531
+
532
+ | File | Purpose |
533
+ |------|---------|
534
+ | `keys.json` | API keys (JSON array) |
535
+ | `presets.json` | ★ Starred models — quick-switch shortlist |
536
+ | `active_model` | Currently active model (persists across restarts) |
537
+ | `index` | Key rotation index |
538
+ | `compaction.json` | Auto-compaction tuning (optional — see [Auto-Compaction](#auto-compaction)) |
539
+ | `timeouts.json` | Upstream timeouts (optional — see [Slow models](#slow-models)) |
540
+ | `model_limits.json` | Context windows the proxy learned by itself — never edit by hand |
541
+ | `model_options.json` | Reasoning toggle + the payload used to express it (see [Thinking](#thinking-reasoning-toggle)) |
542
+ | `endpoints.json` | Extra NVIDIA upstream endpoints (optional — see [Resilience & Scale](#resilience--scale)) |
543
+ | `redis_config.json` | Redis URL for multi-node sync (optional — `OPENVIDIA_REDIS_URL` also works) |
544
+ | `providers.json` | Free-tier fallback providers (optional — name, base URL, env var for the key, model map) |
545
+
546
+ Add keys via the dashboard (**Keys** section) or edit `keys.json`:
547
+
548
+ ```json
549
+ ["nvapi-xxx", "nvapi-yyy", "..."]
550
+ ```
551
+
552
+ ### Thinking (reasoning toggle)
553
+
554
+ A hybrid reasoning model emits **nothing at all** while it thinks. That is the
555
+ whole difference between a 2-second and a 160-second first token, and it is
556
+ not something you can see from the outside — the socket just sits there.
557
+
558
+ Three buttons next to the active model in the dashboard: `auto` (send nothing,
559
+ let the model decide), `on`, `off`. The setting is per-model and stored
560
+ server-side, so all four CLIs pick it up without touching their own configs.
561
+
562
+ The **parameter name is configuration, not code**. Providers spell this flag
563
+ differently and rename it every model generation, so `model_options.json`
564
+ carries the payload to merge:
565
+
566
+ ```json
567
+ {
568
+ "thinking": "auto",
569
+ "thinking_off_payload": { "chat_template_kwargs": { "enable_thinking": false } },
570
+ "thinking_on_payload": { "chat_template_kwargs": { "enable_thinking": true } },
571
+ "per_model": { "vendor/model": { "thinking": "off" } }
572
+ }
573
+ ```
574
+
575
+ A future model that wants `{"reasoning_effort": "none"}` instead needs an edit
576
+ here, not a release. The merge only fills what the client did not set, at
577
+ every level of nesting: a CLI that spells the parameter out in its own request
578
+ has made an explicit choice and wins.
579
+
580
+ ---
581
+
582
+ ### Context windows are learned, not configured
583
+
584
+ A model the proxy has never seen must reach full context with **zero**
585
+ configuration — providers add models continuously, and a hand-maintained
586
+ budget table means every new model runs silently truncated until someone
587
+ notices.
588
+
589
+ NVIDIA does not advertise the window on `/v1/models`, but it states it exactly
590
+ when a request exceeds it:
591
+
592
+ ```
593
+ This model's maximum context length is 202752 tokens.
594
+ However, your messages resulted in 320011 tokens.
595
+ ```
596
+
597
+ So the proxy asks once, in the background, caches the answer in
598
+ `model_limits.json`, and also harvests it from any real overflow. Precedence:
599
+ your `model_budgets` override → learned → the conservative default. An unknown
600
+ model is never allowed to overflow while it is being learned.
601
+
602
+ The day your provider ships a new flagship, you select it and it runs at full
603
+ context. Nothing to configure.
604
+
605
+ ---
606
+
607
+ ### No pinned model
608
+
609
+ There is no `DEFAULT_MODEL` constant. The model a request runs on is resolved
610
+ live: your active selection, then the first starred preset, otherwise an error
611
+ saying no model is selected. A hardcoded model name is a liability the day the
612
+ provider retires it, and it silently overrides what you picked.
613
+
614
+ ---
615
+
616
+ ### Slow models
617
+
618
+ A `read` timeout is the wait for the **first byte**, and a reasoning model
619
+ emits nothing at all while it thinks. Measured on the NVIDIA free tier in
620
+ July 2026, the same key, within the same minute (these models have since been
621
+ retired; the pattern still holds for their successors):
622
+
623
+ | Model | Time to first token |
624
+ |-------|---------------------|
625
+ | `deepseek-ai/deepseek-v4-flash` | 2.1s |
626
+ | `deepseek-ai/deepseek-v4-pro` | 12.3s |
627
+ | `minimaxai/minimax-m3` | 44.5s |
628
+ | `z-ai/glm-5.2` | 2–4s (thinking=off) / 162s (thinking=on) |
629
+
630
+ Provider capacity for one model can collapse without warning while the others
631
+ stay fast — so a timeout short enough to feel responsive is also short enough
632
+ to make a slow model **fail on every key in the pool**. OpenVidia therefore
633
+ waits (default 240s) and keeps the SSE stream alive with periodic comments, so
634
+ your CLI can tell "thinking" from "dead". A read timeout never puts a key on
635
+ cooldown: the key connected fine and the upstream accepted the request — it is
636
+ the model that is slow, and cooling keys down for it drains the whole pool.
637
+
638
+ Override in `~/.config/openvidia/timeouts.json`:
639
+
640
+ ```json
641
+ { "connect": 5.0, "read": 240.0, "write": 30.0, "pool": 240.0 }
642
+ ```
643
+
644
+ If a model is too slow to work with, switch model in the dashboard. OpenVidia
645
+ will not silently answer from a different one.
646
+
647
+ ### Rate limit tuning
648
+
649
+ Constants in `openvidia/proxy_state.py`:
650
+
651
+ ```python
652
+ MAX_RPM = 28 # Safe margin below NVIDIA's 40 RPM limit
653
+ RPM_WINDOW = 60.0 # Sliding window in seconds
654
+
655
+ COOLDOWN_DURATIONS = {
656
+ 400: 60.0, # Deterministic client error — key untouched
657
+ 401: 3600.0, # Unauthorized — dead key
658
+ 403: 3600.0, # Forbidden — dead key
659
+ 404: 60.0, # Endpoint issue — short cooldown
660
+ 429: 45.0, # Rate limited (Retry-After overrides when provided)
661
+ }
662
+ # 400/404 are deterministic content errors: the key is left untouched,
663
+ # so rotating on them would only burn cooldown budget.
664
+ DEFAULT_COOLDOWN = 30.0 # Network errors, unknown 5xx
665
+ ```
666
+
667
+ ---
668
+
669
+ ## API Endpoints
670
+
671
+ ### Proxy
672
+
673
+ | Method | Path | Description |
674
+ |--------|------|-------------|
675
+ | `*` | `/v1/{path}` | Forward to NVIDIA NIM (streaming supported) |
676
+ | `POST` | `/v1/responses` | OpenAI Responses API shim (Codex CLI) |
677
+ | `POST` | `/v1/messages` | Anthropic Messages API shim (Claude Code) |
678
+ | `GET` | `/v1/models` | List available models from upstream |
679
+ | `GET` | `/health` | Health check — key count, port, status |
680
+
681
+ ### Dashboard
682
+
683
+ | Method | Path | Description |
684
+ |--------|------|-------------|
685
+ | `GET` | `/api/status` | Proxy running state + cooldown count |
686
+ | `GET` | `/api/stats` | Requests, rotations, success, cooldowns, total RPM |
687
+ | `GET` | `/api/keys/stats` | Per-key: requests, success/fail, cooldown, RPM, reason |
688
+ | `GET` | `/api/keys` | List keys |
689
+ | `POST` | `/api/keys` | Replace all keys |
690
+ | `POST` | `/api/keys/add` | Add a key |
691
+ | `POST` | `/api/keys/remove` | Remove a key |
692
+ | `GET/POST` | `/api/model` | Get/set active model override |
693
+ | `GET/POST` | `/api/presets` | Get/save model presets |
694
+ | `GET/POST` | `/api/thinking` | Get/set the reasoning mode of the active model (`auto` / `on` / `off`) |
695
+ | `GET` | `/api/model-health` | What the proxy learned from live traffic: per-model success rate, median time to first token, gateway timeouts, 429s |
696
+ | `POST` | `/api/test-model` | Test a model directly (bypasses override) |
697
+ | `POST` | `/api/stop` | Stop proxy (returns 503 to clients) |
698
+ | `POST` | `/api/start` | Resume proxy |
699
+ | `POST` | `/api/restart` | Zero-downtime restart (spawn new, kill old) |
700
+ | `GET` | `/api/logs/stream` | SSE log stream (real-time) |
701
+
702
+ ### Ops (control plane — token protected)
703
+
704
+ | Method | Path | Description |
705
+ |--------|------|-------------|
706
+ | `GET` | `/ops/keys` | Per-key status: cooldown, RPM + learned ceiling, per-model score, in-flight |
707
+ | `GET` | `/ops/health` | Pool, per-model health, endpoints, embedding cache, Redis sync, recent logs |
708
+
709
+ ---
710
+
711
+ ## Graph Engine
712
+
713
+ Programmatic multi-agent orchestration on top of the proxy, following the
714
+ [graph-engine coordination patterns](https://www.anthropic.com/engineering/graph-engine-coordination):
715
+ a single `Hub` owns mailboxes and tasks, agents run with hard caps, and workers
716
+ can be spawned, messaged and killed from inside the loop. Runs on any model the
717
+ proxy serves.
718
+
719
+ ```python
720
+ import asyncio
721
+ from openvidia import Hub, Provider, ToolSpec, run_agent
722
+
723
+ async def add(args, ctx):
724
+ return str(args["a"] + args["b"])
725
+
726
+ async def main():
727
+ hub = Hub(default_provider=Provider()) # http://127.0.0.1:1919/v1 — the proxy above
728
+ try:
729
+ result = await run_agent(
730
+ hub, "math",
731
+ system="You are a calculator. Always use the add tool.",
732
+ first_turn="What is 5 + 2?",
733
+ tools=[ToolSpec(
734
+ name="add", description="Sum two integers",
735
+ handler=add,
736
+ parameters={"type": "object", "properties": {
737
+ "a": {"type": "integer"}, "b": {"type": "integer"}},
738
+ "required": ["a", "b"]},
739
+ )],
740
+ )
741
+ print(result)
742
+ finally:
743
+ await hub.close()
744
+ ```
745
+
746
+ Highlights: `run_agent` caps every run with `max_iterations`, `budget_tokens`
747
+ and `timeout_s`; `graph_tools(hub, worker_factory=...)` provides
748
+ `get_status`, `send_message`, `wait_for_message`, `create_subagents` (depth-bound:
749
+ workers cannot spawn workers) and `kill_subagents`; `generate_and_verify` runs
750
+ generator → verifier loops with explicit rubrics until `APPROVED` or
751
+ `max_rounds`; providers are swappable per worker (a `Provider` or a
752
+ `Callable[[str], Provider]`). 10 regression tests ship in `tests/test_graph_engine.py`.
753
+
754
+ ---
755
+
756
+ ## Resilience & Scale
757
+
758
+ Beyond the core pool, OpenVidia is built to survive upstream trouble and to
759
+ scale across machines.
760
+
761
+ - **Multi-endpoint routing** — by default requests go to
762
+ `integrate.api.nvidia.com`; add endpoints via `endpoints.json` or
763
+ `OPENVIDIA_UPSTREAM_ENDPOINTS` (comma-separated). A status ≥500 blacklists an
764
+ endpoint for 60s; a success revives it. Deterministic client errors never
765
+ retry on another endpoint.
766
+ - **Multi-node sync (Redis, opt-in)** — one proxy per machine, shared state:
767
+ cooldowns, invalid keys, pool throttling and model circuit breakers propagate
768
+ over pub/sub so a 429 learned on node A protects node B. Enable with
769
+ `OPENVIDIA_REDIS_URL` or `redis_config.json` (`{"url": "..."}`); install the
770
+ extra with `pip install "openvidia[redis]"`.
771
+ - **Embedding cache** — repeated `/v1/embeddings` requests (same model + input)
772
+ are answered from an in-memory cache (TTL 300s) keyed by SHA-256; hit/miss
773
+ counters are exposed on `/ops/health`.
774
+ - **Free-tier provider fallback** — when every NVIDIA key is exhausted, the
775
+ catch-all fails over to OpenAI-compatible providers listed in
776
+ `providers.json`:
777
+
778
+ ```json
779
+ [
780
+ {
781
+ "name": "glm",
782
+ "base_url": "https://api.example.com/v1",
783
+ "api_key_env": "GLM_API_KEY",
784
+ "models": { "deepseek-ai/deepseek-v4-flash": "glm-5" }
785
+ }
786
+ ]
787
+ ```
788
+
789
+ A provider without its `api_key_env` set in the environment is skipped
790
+ silently; model names are rewritten through the `models` map.
791
+
792
+ ---
793
+
794
+ ## Tech Stack
795
+
796
+ - **[FastAPI](https://fastapi.tiangolo.com/)** — async web framework
797
+ - **[httpx](https://www.python-httpx.org/)** — HTTP/2 client for upstream
798
+ - **[uvicorn](https://www.uvicorn.org/)** — ASGI server
799
+ - **[pywebview](https://pywebview.flowrl.com/)** — native desktop window (Qt/GTK/WebKit/EdgeChromium)
800
+ - **[psutil](https://github.com/giampaolo/psutil)** — cross-platform process management
801
+ - **[tomlkit](https://github.com/sdispater/tomlkit)** — TOML config editing (preserves comments)
802
+ - **Vanilla HTML/CSS/JS** — zero frontend build, no node_modules
803
+ - **Python 3.12+** — single process, no external services
804
+ - **[redis](https://redis.io/)** (optional) — multi-node state sync via `redis-py`
805
+
806
+ ---
807
+
808
+ ## License
809
+
810
+ MIT
811
+
812
+ ---
813
+
814
+ Built by [Ciro Autuori](https://github.com/ciroautuori).