Compare commits
32 Commits
d89f65ffd1
...
850c7d1e82
2
.gitignore
vendored
2
.gitignore
vendored
@@ -9,3 +9,5 @@ config.toml
|
|||||||
*.swp
|
*.swp
|
||||||
*~
|
*~
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
DEBIAN/
|
||||||
|
usr/
|
||||||
|
|||||||
97
CHANGELOG.md
97
CHANGELOG.md
@@ -1,95 +1,20 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## v3.8.0 (2026-05-22)
|
## v3.9.7 (2026-05-25)
|
||||||
|
|
||||||
**AI Monitoring — Self-Healing Watchdog with 3-Tier Response System**
|
**Codebuff Error Forwarding & Crash Fixes**
|
||||||
|
|
||||||
When the proxy crashes, the upstream dies, or the model gets stuck, Codex stops working. The user has to manually restart everything. AI Monitoring fixes this with an autonomous watchdog that detects, diagnoses, and recovers from failures without user intervention.
|
### Rate Limit Error Forwarding
|
||||||
|
- **Real Codebuff error messages** forwarded to user instead of generic "429 Too Many Requests"
|
||||||
|
- **HTTP 200 + Responses API format** for rate limits — Codex displays the actual Codebuff message (e.g. "Daily session limit reached. Resets in 29m.") instead of retrying
|
||||||
|
- **`retryAfterMs` extraction** from Codebuff 429 responses for accurate cooldown timers
|
||||||
|
- **`_codebuff_start_run`** returns actual error body instead of `None` — shows real Codebuff errors
|
||||||
|
|
||||||
### Three-Tier Response System
|
### Crash Fixes
|
||||||
|
- **BrokenPipeError crash** on "all accounts exhausted" response — wrapped in try/except
|
||||||
|
- **3 SyntaxWarnings** fixed for invalid `\ ` escape sequences in docstrings
|
||||||
|
|
||||||
| Tier | Speed | What | When |
|
## v3.9.6 (2026-05-25)
|
||||||
|------|-------|------|------|
|
|
||||||
| **Tier 1** | < 1s | Rule-based auto-recovery | Known failure patterns (14 rules) |
|
|
||||||
| **Tier 2** | < 100ms | Incident store lookup | We've seen this exact failure before |
|
|
||||||
| **Tier 3** | 2-5s | AI diagnostic agent (configurable model) | Novel failure — no rule or pattern matches |
|
|
||||||
|
|
||||||
### Watchdog Components
|
|
||||||
- **HealthWatcher thread** — pings proxy `/health` every 5 seconds, detects crashes and hangs
|
|
||||||
- **LogAnalyzer thread** — tails `cc-debug.log` for 18 failure signal patterns in real-time
|
|
||||||
- **Tier 1 rule engine** — 14 rules covering: proxy crash restart, port conflict resolution, upstream retry with backoff, schema cache clearing, rate limit handling, stream error recovery
|
|
||||||
- **Tier 2 incident store** — JSON pattern database (`~/.cache/codex-proxy/incident-store.json`) with success rates, learns from every resolved incident
|
|
||||||
- **Tier 3 AI diagnostic agent** — calls a user-configured provider/model (e.g., Gemini Flash, GPT-4o-mini, local Ollama) to diagnose novel failures. Cost: ~$0.10-1.50/month
|
|
||||||
|
|
||||||
### Failure Catalog: 30 Fault Types
|
|
||||||
- **Category A** (7): Proxy crash, port conflict, memory leak, deadlock, SSL error, DNS failure, unhandled exception
|
|
||||||
- **Category B** (10): Rate limit (429), server error (5xx), auth failure (401/403), CC upgrade required, timeout, connection reset, broken pipe, bad request, provider overloaded, Cloudflare block
|
|
||||||
- **Category C** (10): Parser empty, stuck recovery, sanitizer flags, double-wrapped cmd, suspicious cmd, empty cmd, bare JSON token, bash without cmd, DSML name mismatch, stuck model loop
|
|
||||||
- **Category D** (6): Codex process killed, memory explosion, 300s stall, config corruption, context overflow, WebSocket reconnect loop
|
|
||||||
- **Category E** (5): Schema cache corruption, stale PID file, port from old session, OAuth token expired, BGP all routes down
|
|
||||||
|
|
||||||
### Safety Guards
|
|
||||||
- Rate-limited AI calls: max 1 per 60s, max 10/day
|
|
||||||
- Restart cap: max 5 proxy restarts per 10 minutes
|
|
||||||
- Cooldown per pattern (30s → 60s → 300s → alert user)
|
|
||||||
- Monthly AI budget cap (configurable, default $2/month)
|
|
||||||
|
|
||||||
### Enhanced /health Endpoint
|
|
||||||
The proxy's `/health` endpoint now returns `uptime_s`, `memory_mb`, and `requests_total` for watchdog monitoring.
|
|
||||||
|
|
||||||
### GUI Integration
|
|
||||||
- **"AI Monitor" button** in header bar
|
|
||||||
- **AIMonitoringWindow**: ON/OFF toggle, provider URL/model/API key selector, health check interval, auto-restart toggle, incident log viewer
|
|
||||||
- Watchdog starts automatically when enabled
|
|
||||||
- All actions logged to `~/.cache/codex-proxy/monitoring.log`
|
|
||||||
|
|
||||||
### AI Monitoring Design Spec
|
|
||||||
Full design document at `AI-MONITORING-DESIGN.md` — architecture diagrams, decision flow, safety guards, implementation plan.
|
|
||||||
|
|
||||||
## v3.7.0 (2026-05-22)
|
|
||||||
|
|
||||||
**Intelligence Routing — Self-Healing Parser System**
|
|
||||||
|
|
||||||
When the Command Code model produces output in unpredictable or unrecognized formats, the multi-format parser chain (DSML, XML, explore_agent, bash blocks, raw JSON, fallback regex) can return empty. This causes the Codex agent loop to stall — zero tool calls means nothing to execute.
|
|
||||||
|
|
||||||
Intelligence Routing is a **three-layer self-healing system** that ensures the agent loop always continues:
|
|
||||||
|
|
||||||
### Layer 1: Deep URL Extraction (FIX 23)
|
|
||||||
- **Problem**: `<explore_agent>` body contained `messages: [{"content": "https://..."}]` — URLs hidden inside JSON values. Regex couldn't match because it excluded the `"` character that terminates JSON strings.
|
|
||||||
- **Solution**: `_build_explore_cmd()` extracted to module level (was a closure inside `_parse_commandcode_text_tool_calls`). After initial regex fails, tries `json.loads()`, iterates list items, extracts `content` field to find URLs. Added `"` to regex exclusion set.
|
|
||||||
- **Self-tests**: Pattern M, O, O2 verify URL extraction from nested JSON.
|
|
||||||
|
|
||||||
### Layer 2: Escalation Block Handling (FIX 24)
|
|
||||||
- **Problem**: Model produces `<require_escalation>` and `<request_escalation_permission>` blocks when it wants elevated permissions. CC adapter doesn't support escalation — blocks silently dropped → `parsed_tool_calls=0` → stall.
|
|
||||||
- **Solution**: Two handlers:
|
|
||||||
- FIX 24a: Closed-tag blocks — extracts URL if present and runs explore command; otherwise echoes auto-proceed.
|
|
||||||
- FIX 24b: Bare/unclosed tags (`<require_escalation />`) — auto-proceeds with diagnostic echo.
|
|
||||||
- **Self-tests**: Pattern N, N2 verify both closed and bare escalation blocks.
|
|
||||||
|
|
||||||
### Layer 3: Intent-Based Command Synthesis (FIX 25 — THE CORE)
|
|
||||||
- **Problem**: After ALL parsers return empty, the agent loop has zero tool calls. Model may have written plain English ("I need to fetch the README"), partial JSON, or completely unrecognized formats.
|
|
||||||
- **Solution**: 5-heuristic synthesis chain in `cc_stream_to_sse()`, run when `parsed_tool_calls=0` and text has content:
|
|
||||||
1. **URL in text** → `curl` to fetch it
|
|
||||||
2. **File path reference** ("read the file /path/to/X") → `cat` or `ls` that file
|
|
||||||
3. **Shell command in backticks/quotes** → extract and run it
|
|
||||||
4. **"explore"/"fetch"/"investigate"/"repository" intent** + last user URL → `_build_explore_cmd()` with `_last_user_urls` deque
|
|
||||||
5. **"I need to"/"let me"/"please" intent text** → echo diagnostic with the intent
|
|
||||||
- The system NEVER returns empty tool calls when there's text to analyze.
|
|
||||||
- **Self-tests**: Patterns M-O2 cover the full pipeline.
|
|
||||||
|
|
||||||
### Architecture
|
|
||||||
```
|
|
||||||
_parse_commandcode_text_tool_calls() ← Layer 1 + Layer 2
|
|
||||||
cc_stream_to_sse() ← Layer 3 (after parser chain + fallback)
|
|
||||||
_last_user_urls deque (maxlen=20) ← Session-wide URL memory for heuristic 4
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test Coverage
|
|
||||||
- **54 self-test patterns** (up from 41 in v3.6.0)
|
|
||||||
- 13 new tests covering all three Intelligence Routing layers
|
|
||||||
- Tests verify: nested JSON URL extraction, closed/bare escalation blocks, module-level explore command builder
|
|
||||||
|
|
||||||
## v3.6.0 (2026-05-22)
|
|
||||||
|
|
||||||
**Performance & Stability Hardening — Connection Pooling, Stream Idle Timeouts, Retry-After**
|
**Performance & Stability Hardening — Connection Pooling, Stream Idle Timeouts, Retry-After**
|
||||||
|
|
||||||
|
|||||||
123
README.md
123
README.md
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<strong>Run OpenAI Codex CLI & Desktop with <em>any</em> AI provider.</strong><br/>
|
<strong>Run OpenAI Codex CLI & Desktop with <em>any</em> AI provider.</strong><br/>
|
||||||
Google Antigravity • Gemini CLI • OpenCode • Z.AI • Anthropic • Command Code • OpenRouter • Crof.ai • NVIDIA NIM • Kilo.ai • DeepSeek • and more
|
Google Antigravity • Gemini CLI • OpenCode • Z.AI • Anthropic • Command Code • Codebuff • OpenRouter • Crof.ai • NVIDIA NIM • OpenAdapter • Kilo.ai • DeepSeek • and more
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -535,6 +535,67 @@ The launcher generates model catalog JSON with dual field naming to satisfy both
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Gemini Antigravity State Continuity
|
||||||
|
|
||||||
|
Codex Launcher includes special handling for Gemini 3 / Antigravity OAuth:
|
||||||
|
|
||||||
|
- **Thought signature preservation**: Captures `thoughtSignature` from Gemini responses
|
||||||
|
and reattaches them on follow-up requests to maintain tool-call continuity.
|
||||||
|
- **Edit-intent detection**: When follow-up requests contain edit keywords, a tool-use
|
||||||
|
nudge is injected to prevent text-only responses.
|
||||||
|
- **User instruction enforcement**: The latest user message is guaranteed to be the
|
||||||
|
final content turn sent to Gemini, even after compaction.
|
||||||
|
- **Smart compaction**: Old tool outputs capped at 3000 chars, recent 6 at 20000 chars.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Multi-Account Rotation
|
||||||
|
|
||||||
|
Codex Launcher supports **multiple accounts per provider** with automatic rotation
|
||||||
|
when one account is rate-limited.
|
||||||
|
|
||||||
|
### Codebuff (Multiple Accounts)
|
||||||
|
|
||||||
|
Register additional free accounts at [codebuff.com](https://www.codebuff.com), then
|
||||||
|
add them to `~/.config/manicode/credentials.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"default": { "authToken": "token-primary", "email": "you+1@gmail.com" },
|
||||||
|
"accounts": [
|
||||||
|
{ "authToken": "token-secondary", "email": "you+2@gmail.com" },
|
||||||
|
{ "authToken": "token-tertiary", "email": "you+3@gmail.com" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each account gets 5 free requests/day. With 3 accounts = **15 requests/day**.
|
||||||
|
|
||||||
|
### Google OAuth (Multiple Projects)
|
||||||
|
|
||||||
|
Add additional Google Cloud token files:
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.cache/codex-proxy/google-antigravity-oauth-token.json # primary
|
||||||
|
~/.cache/codex-proxy/google-antigravity-oauth-token-1.json # extra project 1
|
||||||
|
~/.cache/codex-proxy/google-antigravity-oauth-token-2.json # extra project 2
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Keys (Comma-Separated)
|
||||||
|
|
||||||
|
For any OpenAI-compatible provider:
|
||||||
|
```json
|
||||||
|
{ "api_key": "sk-key1,sk-key2,sk-key3" }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Account Status Endpoint
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://127.0.0.1:PORT/v1/accounts
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Provider Presets
|
## Provider Presets
|
||||||
|
|
||||||
| Preset | Backend | Base URL |
|
| Preset | Backend | Base URL |
|
||||||
@@ -544,13 +605,28 @@ The launcher generates model catalog JSON with dual field naming to satisfy both
|
|||||||
| OpenCode Zen | OpenAI-compat | `https://opencode.ai/zen/v1` |
|
| OpenCode Zen | OpenAI-compat | `https://opencode.ai/zen/v1` |
|
||||||
| OpenCode Go | OpenAI-compat | `https://opencode.ai/zen/go/v1` |
|
| OpenCode Go | OpenAI-compat | `https://opencode.ai/zen/go/v1` |
|
||||||
| Command Code | Command Code | `https://api.commandcode.ai` |
|
| Command Code | Command Code | `https://api.commandcode.ai` |
|
||||||
|
| **Codebuff** | **Codebuff** | `https://codebuff.com` *(free DeepSeek/Kimi — OAuth login built-in)* |
|
||||||
| Crof.ai | OpenAI-compat | `https://crof.ai/v1` |
|
| Crof.ai | OpenAI-compat | `https://crof.ai/v1` |
|
||||||
|
| OpenAdapter | OpenAI-compat | `https://api.openadapter.in/v1` |
|
||||||
|
| Z.ai Coding | OpenAI-compat | `https://api.z.ai/api/coding/paas/v4` |
|
||||||
| NVIDIA NIM | OpenAI-compat | `https://integrate.api.nvidia.com/v1` |
|
| NVIDIA NIM | OpenAI-compat | `https://integrate.api.nvidia.com/v1` |
|
||||||
| Kilo.ai | OpenAI-compat | `https://api.kilo.ai/api/gateway` |
|
| Kilo.ai | OpenAI-compat | `https://api.kilo.ai/api/gateway` |
|
||||||
| OpenRouter | OpenAI-compat | `https://openrouter.ai/api/v1` |
|
| OpenRouter | OpenAI-compat | `https://openrouter.ai/api/v1` |
|
||||||
| Z.AI | OpenAI-compat | `https://api.z.ai/api/coding/paas/v4` |
|
| Z.AI | OpenAI-compat | `https://api.z.ai/api/coding/paas/v4` |
|
||||||
|
| Google Gemini (API Key) | OpenAI-compat | `https://generativelanguage.googleapis.com/v1beta/openai` |
|
||||||
|
| Google Gemini (OAuth) | Gemini OAuth | `cloudcode-pa.googleapis.com` |
|
||||||
|
| Google Antigravity (OAuth) | Antigravity OAuth | `daily-cloudcode-pa.sandbox.googleapis.com` |
|
||||||
| Custom | Any | User-defined |
|
| Custom | Any | User-defined |
|
||||||
|
|
||||||
|
### Free Models (via Codebuff)
|
||||||
|
Codebuff provides free access to these models — no API key needed:
|
||||||
|
- **DeepSeek V4 Pro** — Smartest model
|
||||||
|
- **DeepSeek V4 Flash** — Most efficient
|
||||||
|
- **Kimi K2.6** — Balanced
|
||||||
|
- **MiniMax M2.7** — Fastest
|
||||||
|
|
||||||
|
*Requires: `codebuff login` via GUI OAuth button, or `npm install -g codebuff && codebuff login` (GitHub OAuth)*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## File Structure
|
## File Structure
|
||||||
@@ -581,6 +657,51 @@ README.md # This file
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Phase 10: Codebuff Integration — Free AI for Everyone (v3.8.1)
|
||||||
|
|
||||||
|
**Problem:** Users want access to powerful models like DeepSeek V4 Pro without paying API fees. Codebuff (by CodebuffAI) offers free access to premium models through their server, but it's a CLI tool — not an API you can plug into Codex Launcher.
|
||||||
|
|
||||||
|
**The insight:** Codebuff's backend is a Next.js app with an OpenAI-compatible `/api/v1/chat/completions` endpoint. It uses agent-run lifecycle management and model-specific routing. If we replicate the agent run protocol in our proxy, we can tap into codebuff's free tier.
|
||||||
|
|
||||||
|
**How Codebuff works internally:**
|
||||||
|
1. User logs in via GitHub OAuth → session token stored in `~/.config/manicode/credentials.json`
|
||||||
|
2. Each request creates an **agent run** via `POST /api/v1/agent-runs`
|
||||||
|
3. Chat completions sent with `codebuff_metadata: {run_id, cost_mode: "free"}`
|
||||||
|
4. Server routes to the correct upstream provider using its own API keys
|
||||||
|
5. Agent run finished when request completes
|
||||||
|
|
||||||
|
**What we built:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Codex Request
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ translate-proxy.py │
|
||||||
|
│ _handle_codebuff() │
|
||||||
|
│ │
|
||||||
|
│ 1. Read token from credentials │
|
||||||
|
│ 2. POST /api/v1/agent-runs │──→ {action: "START", agentId}
|
||||||
|
│ 3. POST /api/v1/chat/completions │──→ {model, messages,
|
||||||
|
│ codebuff_metadata: {
|
||||||
|
│ run_id, cost_mode: "free"}}
|
||||||
|
│ 4. Stream response back to Codex │←── SSE events
|
||||||
|
│ 5. POST /api/v1/agent-runs │──→ {action: "FINISH"}
|
||||||
|
└─────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Free models available:**
|
||||||
|
| Model | Agent ID | Notes |
|
||||||
|
|-------|----------|-------|
|
||||||
|
| DeepSeek V4 Pro | `base2-free-deepseek` | Smartest |
|
||||||
|
| DeepSeek V4 Flash | `base2-free-deepseek-flash` | Most efficient |
|
||||||
|
| Kimi K2.6 | `base2-free-kimi` | Balanced |
|
||||||
|
| MiniMax M2.7 | `base2-free` | Fastest |
|
||||||
|
|
||||||
|
**Bonus fix:** While investigating this, we discovered that `endpoints.json` had been overwritten with only 4 AG X entries, losing all 17+ provider presets. Restored all presets from proxy cache files.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
| Issue | Cause | Fix |
|
| Issue | Cause | Fix |
|
||||||
|
|||||||
5073
codex-launcher-gui
Executable file
5073
codex-launcher-gui
Executable file
File diff suppressed because it is too large
Load Diff
BIN
codex-launcher_3.8.1_all.deb
Normal file
BIN
codex-launcher_3.8.1_all.deb
Normal file
Binary file not shown.
BIN
codex-launcher_3.8.3_all.deb
Normal file
BIN
codex-launcher_3.8.3_all.deb
Normal file
Binary file not shown.
BIN
codex-launcher_3.8.4_all.deb
Normal file
BIN
codex-launcher_3.8.4_all.deb
Normal file
Binary file not shown.
BIN
codex-launcher_3.9.7_all.deb
Normal file
BIN
codex-launcher_3.9.7_all.deb
Normal file
Binary file not shown.
@@ -3,11 +3,11 @@ set -e
|
|||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
if [ -f "$SCRIPT_DIR/codex-launcher_3.8.0_all.deb" ]; then
|
if [ -f "$SCRIPT_DIR/codex-launcher_3.9.7_all.deb" ]; then
|
||||||
echo "Installing codex-launcher_3.8.0_all.deb ..."
|
echo "Installing codex-launcher_3.9.7_all.deb ..."
|
||||||
sudo dpkg -i "$SCRIPT_DIR/codex-launcher_3.8.0_all.deb"
|
sudo dpkg -i "$SCRIPT_DIR/codex-launcher_3.9.7_all.deb"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Installed v3.8.0 via .deb package."
|
echo "Installed v3.9.7 via .deb package."
|
||||||
echo " translate-proxy.py -> /usr/bin/translate-proxy.py"
|
echo " translate-proxy.py -> /usr/bin/translate-proxy.py"
|
||||||
echo " codex-launcher-gui -> /usr/bin/codex-launcher-gui"
|
echo " codex-launcher-gui -> /usr/bin/codex-launcher-gui"
|
||||||
echo " cleanup-codex-stale -> /usr/bin/cleanup-codex-stale.sh"
|
echo " cleanup-codex-stale -> /usr/bin/cleanup-codex-stale.sh"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ gi.require_version("Gtk", "3.0")
|
|||||||
from gi.repository import Gtk, GLib
|
from gi.repository import Gtk, GLib
|
||||||
import subprocess, os, signal, sys, threading, time, json, urllib.request, urllib.parse, urllib.error, tempfile, shutil
|
import subprocess, os, signal, sys, threading, time, json, urllib.request, urllib.parse, urllib.error, tempfile, shutil
|
||||||
import hashlib, socket, ssl, contextlib, re, collections
|
import hashlib, socket, ssl, contextlib, re, collections
|
||||||
import base64, secrets
|
import base64, secrets, uuid, webbrowser
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
HOME = Path.home()
|
HOME = Path.home()
|
||||||
@@ -26,7 +26,57 @@ model_catalog_json = ""
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
CHANGELOG = [
|
CHANGELOG = [
|
||||||
("3.8.0", "2026-05-22", [
|
("3.9.7", "2026-05-25", [
|
||||||
|
"Forward real Codebuff error messages to user (not generic 429)",
|
||||||
|
"Return HTTP 200 with Responses API format for rate limits so Codex displays message",
|
||||||
|
"Extract retryAfterMs from Codebuff 429 responses for accurate cooldown",
|
||||||
|
"RateLimitError carries upstream message through session + chat error paths",
|
||||||
|
"BrokenPipeError crash fix on 'all accounts exhausted' response",
|
||||||
|
"Fix 3 SyntaxWarnings for invalid escape sequences in docstrings",
|
||||||
|
"_codebuff_start_run returns actual error body instead of None",
|
||||||
|
]),
|
||||||
|
("3.9.6", "2026-05-25", [
|
||||||
|
"Fix Gemini follow-up turns returning text-only instead of tool calls",
|
||||||
|
"Enforce latest user instruction as final Gemini content turn",
|
||||||
|
"Edit-intent detection with tool-use nudge for file modification requests",
|
||||||
|
"Debug logging: contents count, latest user text, final content preview",
|
||||||
|
"Thought signature preservation for Gemini 3 tool-call continuity",
|
||||||
|
"thought_signature field on all functionCall parts (snake_case)",
|
||||||
|
"Smart tool output compaction: old=3000, recent=20000 chars",
|
||||||
|
"Follow-through guardrail system instruction for autonomous agent behavior",
|
||||||
|
"Stream hang fix for function-call-only responses",
|
||||||
|
"Multi-account rotation for codebuff, Google OAuth, API keys",
|
||||||
|
"/v1/accounts endpoint for account pool status",
|
||||||
|
]),
|
||||||
|
("3.9.0", "2026-05-24", [
|
||||||
|
"Multi-account rotation for OAuth providers (codebuff, Google, API keys)",
|
||||||
|
"Automatic failover: when one account hits rate limit, next is used",
|
||||||
|
"Codebuff: supports accounts[] array in credentials.json",
|
||||||
|
"Google OAuth: supports multiple token files (google-*-oauth-token-N.json)",
|
||||||
|
"API keys: comma-separated keys rotate on 429 errors",
|
||||||
|
"New /v1/accounts endpoint shows account pool status",
|
||||||
|
"Added x-codebuff-model and x-codebuff-instance-id headers",
|
||||||
|
]),
|
||||||
|
("3.8.4", "2026-05-24", [
|
||||||
|
"FIXED: Codebuff streaming — SSE events now reach Codex client",
|
||||||
|
"Root cause: stream_buffered_events was never called for codebuff",
|
||||||
|
"Codebuff stream uses buffered flushing (30ms / 4KB / urgent)",
|
||||||
|
"Codebuff OAuth — built-in login flow (no external CLI needed)",
|
||||||
|
"Codebuff API: reverse-engineered www.codebuff.com endpoints",
|
||||||
|
"Codebuff session management with instance ID (waiting room)",
|
||||||
|
"Codebuff agent run lifecycle (start/finish) with model routing",
|
||||||
|
"Free DeepSeek V4 Pro, V4 Flash, Kimi K2.6, MiniMax M2.7",
|
||||||
|
"Reasoning mode works with codebuff (thinking tokens supported)",
|
||||||
|
"GUI: Sandbox mode selector (Read-only / Workspace / Full Access)",
|
||||||
|
"GUI: Approval mode selector (Untrusted / On Request / Full Auto)",
|
||||||
|
"GUI: Codebuff Login button in endpoint editor",
|
||||||
|
"Fixed _STATS undefined error in /health endpoint",
|
||||||
|
"Fixed codebuff credential path (reads default account)",
|
||||||
|
]),
|
||||||
|
("3.8.1", "2026-05-24", [
|
||||||
|
"Codebuff integration — free DeepSeek V4 Pro, V4 Flash, Kimi K2.6, MiniMax M2.7",
|
||||||
|
"Codebuff backend: auto agent-run lifecycle, credential detection, model routing",
|
||||||
|
"Restored all provider presets (Command Code, Crof, OpenAdapter, OpenRouter, etc.)",
|
||||||
"AI Monitoring — self-healing watchdog with 3-tier response system",
|
"AI Monitoring — self-healing watchdog with 3-tier response system",
|
||||||
"HealthWatcher: monitors proxy health every 5s, auto-restarts on crash",
|
"HealthWatcher: monitors proxy health every 5s, auto-restarts on crash",
|
||||||
"LogAnalyzer: tails debug logs for 18 failure signal patterns",
|
"LogAnalyzer: tails debug logs for 18 failure signal patterns",
|
||||||
@@ -315,6 +365,15 @@ PROVIDER_PRESETS = {
|
|||||||
"GLM-4-Flash", "GLM-4-FlashX", "GLM-Z1-Flash",
|
"GLM-4-Flash", "GLM-4-FlashX", "GLM-Z1-Flash",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
"Codebuff (Free DeepSeek/Kimi)": {
|
||||||
|
"backend_type": "codebuff",
|
||||||
|
"base_url": "https://codebuff.com",
|
||||||
|
"oauth_provider": "codebuff",
|
||||||
|
"models": [
|
||||||
|
"deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-flash",
|
||||||
|
"moonshotai/kimi-k2.6", "minimax/minimax-m2.7",
|
||||||
|
],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def safe_name(name):
|
def safe_name(name):
|
||||||
@@ -327,6 +386,7 @@ def label_for_backend(backend_type):
|
|||||||
"openai-compat": "OpenAI-compatible",
|
"openai-compat": "OpenAI-compatible",
|
||||||
"anthropic": "Anthropic",
|
"anthropic": "Anthropic",
|
||||||
"command-code": "Command Code",
|
"command-code": "Command Code",
|
||||||
|
"codebuff": "Codebuff (Free AI)",
|
||||||
"native": "Native",
|
"native": "Native",
|
||||||
}.get(backend_type, backend_type)
|
}.get(backend_type, backend_type)
|
||||||
|
|
||||||
@@ -983,6 +1043,11 @@ def safe_cleanup_owned(logfn=None):
|
|||||||
|
|
||||||
def _start_proxy_for(endpoint, logfn):
|
def _start_proxy_for(endpoint, logfn):
|
||||||
global _proxy_proc, _proxy_port
|
global _proxy_proc, _proxy_port
|
||||||
|
# Clear stale Python bytecode cache so proxy picks up latest source changes
|
||||||
|
import shutil
|
||||||
|
pycache = os.path.join(os.path.dirname(os.path.abspath(__file__)), '__pycache__')
|
||||||
|
if os.path.isdir(pycache):
|
||||||
|
shutil.rmtree(pycache, ignore_errors=True)
|
||||||
_stop_proxy()
|
_stop_proxy()
|
||||||
port = _pick_free_port()
|
port = _pick_free_port()
|
||||||
_proxy_port = port
|
_proxy_port = port
|
||||||
@@ -1672,7 +1737,7 @@ class LauncherWin(Gtk.Window):
|
|||||||
# header row
|
# header row
|
||||||
hdr = Gtk.Box(spacing=8)
|
hdr = Gtk.Box(spacing=8)
|
||||||
vbox.pack_start(hdr, False, False, 0)
|
vbox.pack_start(hdr, False, False, 0)
|
||||||
lbl = Gtk.Label(label="<b>Codex Launcher v3.8.0</b>")
|
lbl = Gtk.Label(label="<b>Codex Launcher v3.9.7</b>")
|
||||||
lbl.set_use_markup(True)
|
lbl.set_use_markup(True)
|
||||||
hdr.pack_start(lbl, False, False, 0)
|
hdr.pack_start(lbl, False, False, 0)
|
||||||
changelog_btn = Gtk.Button(label="Changelog")
|
changelog_btn = Gtk.Button(label="Changelog")
|
||||||
@@ -1778,6 +1843,26 @@ class LauncherWin(Gtk.Window):
|
|||||||
self._model_combo = Gtk.ComboBoxText()
|
self._model_combo = Gtk.ComboBoxText()
|
||||||
sel_box.pack_start(self._model_combo, True, True, 0)
|
sel_box.pack_start(self._model_combo, True, True, 0)
|
||||||
|
|
||||||
|
# sandbox mode selector
|
||||||
|
sel_box.pack_start(Gtk.Label(label="Sandbox:"), False, False, 0)
|
||||||
|
self._sandbox_combo = Gtk.ComboBoxText()
|
||||||
|
for v, l in [("read-only", "Read-only"),
|
||||||
|
("workspace-write", "Workspace"),
|
||||||
|
("danger-full-access", "Full Access")]:
|
||||||
|
self._sandbox_combo.append(v, l)
|
||||||
|
self._sandbox_combo.set_active_id("workspace-write")
|
||||||
|
sel_box.pack_start(self._sandbox_combo, True, True, 0)
|
||||||
|
|
||||||
|
# approval mode selector
|
||||||
|
sel_box.pack_start(Gtk.Label(label="Approval:"), False, False, 0)
|
||||||
|
self._approval_combo = Gtk.ComboBoxText()
|
||||||
|
for v, l in [("untrusted", "Untrusted"),
|
||||||
|
("on-request", "On Request"),
|
||||||
|
("never", "Never (Full Auto)")]:
|
||||||
|
self._approval_combo.append(v, l)
|
||||||
|
self._approval_combo.set_active_id("on-request")
|
||||||
|
sel_box.pack_start(self._approval_combo, True, True, 0)
|
||||||
|
|
||||||
# launch buttons
|
# launch buttons
|
||||||
btn_box = Gtk.Box(spacing=8, homogeneous=True)
|
btn_box = Gtk.Box(spacing=8, homogeneous=True)
|
||||||
vbox.pack_start(btn_box, False, False, 8)
|
vbox.pack_start(btn_box, False, False, 8)
|
||||||
@@ -2511,7 +2596,6 @@ class LauncherWin(Gtk.Window):
|
|||||||
"""Launch codex CLI in a terminal with the selected endpoint."""
|
"""Launch codex CLI in a terminal with the selected endpoint."""
|
||||||
self.log(f"Launching Codex CLI with {ep['name']}…")
|
self.log(f"Launching Codex CLI with {ep['name']}…")
|
||||||
|
|
||||||
# Find a terminal emulator
|
|
||||||
terms = [
|
terms = [
|
||||||
("x-terminal-emulator", ["-e"]),
|
("x-terminal-emulator", ["-e"]),
|
||||||
("kgx", ["--"]),
|
("kgx", ["--"]),
|
||||||
@@ -2531,16 +2615,17 @@ class LauncherWin(Gtk.Window):
|
|||||||
self.log("ERROR: no terminal emulator found (tried x-terminal-emulator, kgx, gnome-terminal, konsole, xterm)")
|
self.log("ERROR: no terminal emulator found (tried x-terminal-emulator, kgx, gnome-terminal, konsole, xterm)")
|
||||||
return
|
return
|
||||||
|
|
||||||
# For proxied endpoints, the proxy is already running (from _run)
|
sandbox = self._sandbox_combo.get_active_id() or "workspace-write"
|
||||||
# For native, no proxy needed
|
approval = self._approval_combo.get_active_id() or "on-request"
|
||||||
|
|
||||||
cmd_parts = [term] + term_args
|
cmd_parts = [term] + term_args
|
||||||
|
|
||||||
if ep["backend_type"] == "native":
|
if ep["backend_type"] == "native":
|
||||||
# Just run codex directly — config.toml is already set up
|
cmd_parts.extend(["codex", "-c", f"model={model}",
|
||||||
cmd_parts.extend(["codex", "-c", f"model={model}"])
|
"-s", sandbox, "-a", approval])
|
||||||
else:
|
else:
|
||||||
# Proxy is running, run codex with the profile
|
cmd_parts.extend(["codex", "--profile", ep["name"], "-c", f"model={model}",
|
||||||
cmd_parts.extend(["codex", "--profile", ep["name"], "-c", f"model={model}"])
|
"-s", sandbox, "-a", approval])
|
||||||
|
|
||||||
self.log(f"Running: {' '.join(cmd_parts)}")
|
self.log(f"Running: {' '.join(cmd_parts)}")
|
||||||
self._proc = subprocess.Popen(cmd_parts, preexec_fn=os.setsid)
|
self._proc = subprocess.Popen(cmd_parts, preexec_fn=os.setsid)
|
||||||
@@ -2606,7 +2691,9 @@ class LauncherWin(Gtk.Window):
|
|||||||
self.log("ERROR: no terminal emulator found")
|
self.log("ERROR: no terminal emulator found")
|
||||||
return
|
return
|
||||||
|
|
||||||
cmd_parts = [term] + term_args + ["codex"]
|
sandbox = self._sandbox_combo.get_active_id() or "workspace-write"
|
||||||
|
approval = self._approval_combo.get_active_id() or "on-request"
|
||||||
|
cmd_parts = [term] + term_args + ["codex", "-s", sandbox, "-a", approval]
|
||||||
self.log(f"Running: {' '.join(cmd_parts)}")
|
self.log(f"Running: {' '.join(cmd_parts)}")
|
||||||
self._proc = subprocess.Popen(cmd_parts, preexec_fn=os.setsid)
|
self._proc = subprocess.Popen(cmd_parts, preexec_fn=os.setsid)
|
||||||
pid = self._proc.pid
|
pid = self._proc.pid
|
||||||
@@ -2923,6 +3010,7 @@ class EditEndpointDialog(Gtk.Dialog):
|
|||||||
for val, lab in [("openai-compat", "OpenAI-compatible (needs proxy)"),
|
for val, lab in [("openai-compat", "OpenAI-compatible (needs proxy)"),
|
||||||
("anthropic", "Anthropic (needs proxy)"),
|
("anthropic", "Anthropic (needs proxy)"),
|
||||||
("command-code", "Command Code (needs proxy)"),
|
("command-code", "Command Code (needs proxy)"),
|
||||||
|
("codebuff", "Codebuff - Free DeepSeek/Kimi (needs proxy)"),
|
||||||
("gemini-oauth-cli", "Gemini CLI OAuth (needs proxy)"),
|
("gemini-oauth-cli", "Gemini CLI OAuth (needs proxy)"),
|
||||||
("gemini-oauth-antigravity", "Antigravity OAuth (needs proxy)"),
|
("gemini-oauth-antigravity", "Antigravity OAuth (needs proxy)"),
|
||||||
("native", "Native OpenAI (no proxy)")]:
|
("native", "Native OpenAI (no proxy)")]:
|
||||||
@@ -3084,9 +3172,14 @@ class EditEndpointDialog(Gtk.Dialog):
|
|||||||
def _apply_selected_preset(self, initial=False):
|
def _apply_selected_preset(self, initial=False):
|
||||||
preset_name = self._combo_preset.get_active_text() or "Custom"
|
preset_name = self._combo_preset.get_active_text() or "Custom"
|
||||||
preset = PROVIDER_PRESETS.get(preset_name, PROVIDER_PRESETS["Custom"])
|
preset = PROVIDER_PRESETS.get(preset_name, PROVIDER_PRESETS["Custom"])
|
||||||
is_oauth = bool(preset.get("oauth_provider"))
|
oauth_provider = preset.get("oauth_provider", "")
|
||||||
|
is_oauth = bool(oauth_provider)
|
||||||
self._oauth_btn.set_visible(is_oauth)
|
self._oauth_btn.set_visible(is_oauth)
|
||||||
if is_oauth:
|
if oauth_provider == "codebuff":
|
||||||
|
self._oauth_btn.set_label("Codebuff Login")
|
||||||
|
self._entry_key.set_placeholder_text("Auto-filled by codebuff login")
|
||||||
|
elif is_oauth:
|
||||||
|
self._oauth_btn.set_label("OAuth Login")
|
||||||
self._entry_key.set_placeholder_text("Auto-filled by OAuth")
|
self._entry_key.set_placeholder_text("Auto-filled by OAuth")
|
||||||
else:
|
else:
|
||||||
self._entry_key.set_placeholder_text("")
|
self._entry_key.set_placeholder_text("")
|
||||||
@@ -3117,7 +3210,9 @@ class EditEndpointDialog(Gtk.Dialog):
|
|||||||
preset_name = self._combo_preset.get_active_text() or "Custom"
|
preset_name = self._combo_preset.get_active_text() or "Custom"
|
||||||
preset = PROVIDER_PRESETS.get(preset_name, {})
|
preset = PROVIDER_PRESETS.get(preset_name, {})
|
||||||
provider = preset.get("oauth_provider", "")
|
provider = preset.get("oauth_provider", "")
|
||||||
if (provider or "").startswith("google"):
|
if provider == "codebuff":
|
||||||
|
self._codebuff_oauth_flow()
|
||||||
|
elif (provider or "").startswith("google"):
|
||||||
self._google_oauth_flow(provider)
|
self._google_oauth_flow(provider)
|
||||||
|
|
||||||
def _google_oauth_flow(self, oauth_provider="google-cli"):
|
def _google_oauth_flow(self, oauth_provider="google-cli"):
|
||||||
@@ -3393,6 +3488,117 @@ class EditEndpointDialog(Gtk.Dialog):
|
|||||||
dlg.connect("response", lambda d, r: d.destroy())
|
dlg.connect("response", lambda d, r: d.destroy())
|
||||||
dlg.run()
|
dlg.run()
|
||||||
|
|
||||||
|
def _codebuff_oauth_flow(self):
|
||||||
|
dlg = Gtk.Dialog(title="Codebuff Login", parent=self, modal=True)
|
||||||
|
dlg.add_button("Cancel", Gtk.ResponseType.CANCEL)
|
||||||
|
dlg.set_default_size(500, 240)
|
||||||
|
area = dlg.get_content_area()
|
||||||
|
area.set_margin_start(16)
|
||||||
|
area.set_margin_end(16)
|
||||||
|
area.set_margin_top(12)
|
||||||
|
area.set_margin_bottom(12)
|
||||||
|
area.set_spacing(8)
|
||||||
|
|
||||||
|
area.pack_start(Gtk.Label(label="<b>Sign in with GitHub via Codebuff</b>", use_markup=True, xalign=0), False, False, 0)
|
||||||
|
|
||||||
|
self._oauth_status = Gtk.Label(label="Requesting login URL…", xalign=0)
|
||||||
|
self._oauth_status.set_line_wrap(True)
|
||||||
|
self._oauth_status.set_max_width_chars(60)
|
||||||
|
area.pack_start(self._oauth_status, False, False, 4)
|
||||||
|
|
||||||
|
link_lbl = Gtk.Label(xalign=0)
|
||||||
|
link_lbl.set_line_wrap(True)
|
||||||
|
link_lbl.set_max_width_chars(60)
|
||||||
|
area.pack_start(link_lbl, False, False, 4)
|
||||||
|
|
||||||
|
spinner = Gtk.Spinner()
|
||||||
|
spinner.start()
|
||||||
|
area.pack_start(spinner, False, False, 8)
|
||||||
|
|
||||||
|
area.show_all()
|
||||||
|
link_lbl.set_visible(False)
|
||||||
|
|
||||||
|
self._fb_oauth_result = {"success": False, "user": None, "error": None}
|
||||||
|
|
||||||
|
def _codebuff_auth_thread():
|
||||||
|
try:
|
||||||
|
fingerprint_id = str(uuid.uuid4())
|
||||||
|
auth_url = "https://codebuff.com/api/auth/cli/code"
|
||||||
|
body = json.dumps({"fingerprintId": fingerprint_id}).encode()
|
||||||
|
req = urllib.request.Request(auth_url, data=body,
|
||||||
|
headers={"Content-Type": "application/json", "User-Agent": "codex-launcher/3.9.7"})
|
||||||
|
resp = urllib.request.urlopen(req, timeout=30)
|
||||||
|
data = json.loads(resp.read())
|
||||||
|
login_url = data.get("loginUrl", "") or data.get("login_url", "")
|
||||||
|
fingerprint_hash = data.get("fingerprintHash", "") or data.get("fingerprint_hash", "")
|
||||||
|
expires_at = data.get("expiresAt", 0) or data.get("expires_at", 0)
|
||||||
|
if not login_url:
|
||||||
|
self._fb_oauth_result["error"] = "Server returned no login URL"
|
||||||
|
GLib.idle_add(self._codebuff_oauth_done, dlg, spinner)
|
||||||
|
return
|
||||||
|
|
||||||
|
def _set_link():
|
||||||
|
self._oauth_status.set_text("Open this URL in your browser to log in:")
|
||||||
|
link_lbl.set_markup(f'<a href="{login_url}">{login_url}</a>')
|
||||||
|
link_lbl.set_visible(True)
|
||||||
|
GLib.idle_add(_set_link)
|
||||||
|
|
||||||
|
webbrowser.open(login_url)
|
||||||
|
|
||||||
|
poll_url = f"https://codebuff.com/api/auth/cli/status?fingerprintId={urllib.parse.quote(fingerprint_id)}&fingerprintHash={urllib.parse.quote(fingerprint_hash)}&expiresAt={expires_at}"
|
||||||
|
deadline = time.time() + 300
|
||||||
|
while time.time() < deadline:
|
||||||
|
time.sleep(2)
|
||||||
|
try:
|
||||||
|
poll_req = urllib.request.Request(poll_url,
|
||||||
|
headers={"User-Agent": "codex-launcher/3.9.7"})
|
||||||
|
poll_resp = urllib.request.urlopen(poll_req, timeout=10)
|
||||||
|
poll_data = json.loads(poll_resp.read())
|
||||||
|
user = poll_data.get("user")
|
||||||
|
if user and user.get("authToken"):
|
||||||
|
self._fb_oauth_result["success"] = True
|
||||||
|
self._fb_oauth_result["user"] = user
|
||||||
|
GLib.idle_add(self._codebuff_oauth_done, dlg, spinner)
|
||||||
|
return
|
||||||
|
except urllib.error.HTTPError:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._fb_oauth_result["error"] = "Login timed out after 5 minutes."
|
||||||
|
GLib.idle_add(self._codebuff_oauth_done, dlg, spinner)
|
||||||
|
except Exception as e:
|
||||||
|
self._fb_oauth_result["error"] = str(e)[:200]
|
||||||
|
GLib.idle_add(self._codebuff_oauth_done, dlg, spinner)
|
||||||
|
|
||||||
|
threading.Thread(target=_codebuff_auth_thread, daemon=True).start()
|
||||||
|
dlg.connect("response", lambda d, r: d.destroy())
|
||||||
|
dlg.run()
|
||||||
|
|
||||||
|
def _codebuff_oauth_done(self, dlg, spinner):
|
||||||
|
spinner.stop()
|
||||||
|
if self._fb_oauth_result["success"] and self._fb_oauth_result["user"]:
|
||||||
|
user = self._fb_oauth_result["user"]
|
||||||
|
creds_path = os.path.expanduser("~/.config/manicode/credentials.json")
|
||||||
|
os.makedirs(os.path.dirname(creds_path), exist_ok=True)
|
||||||
|
creds = {"default": {
|
||||||
|
"id": user.get("id", ""),
|
||||||
|
"name": user.get("name", ""),
|
||||||
|
"email": user.get("email", ""),
|
||||||
|
"authToken": user.get("authToken", ""),
|
||||||
|
"fingerprintId": user.get("fingerprintId", ""),
|
||||||
|
"fingerprintHash": user.get("fingerprintHash", ""),
|
||||||
|
}}
|
||||||
|
with open(creds_path, "w") as f:
|
||||||
|
json.dump(creds, f, indent=2)
|
||||||
|
os.chmod(creds_path, 0o600)
|
||||||
|
self._entry_key.set_text(user.get("authToken", ""))
|
||||||
|
self._oauth_status.set_markup('<span foreground="#27ae60" weight="bold">Authorization successful! Credentials saved.</span>')
|
||||||
|
dlg.set_title("Codebuff Login – Success")
|
||||||
|
GLib.timeout_add(1500, lambda: dlg.response(Gtk.ResponseType.OK))
|
||||||
|
else:
|
||||||
|
self._oauth_status.set_markup(f'<span foreground="#e74c3c">{self._fb_oauth_result["error"] or "Login failed."}</span>')
|
||||||
|
GLib.timeout_add(3000, lambda: dlg.response(Gtk.ResponseType.CANCEL))
|
||||||
|
|
||||||
def _oauth_success(self, dlg, access_token, spinner):
|
def _oauth_success(self, dlg, access_token, spinner):
|
||||||
spinner.stop()
|
spinner.stop()
|
||||||
self._entry_key.set_text(access_token)
|
self._entry_key.set_text(access_token)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
6067
translate-proxy.py
Executable file
6067
translate-proxy.py
Executable file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user