@timo972/cc-router 0.10.0 → 0.10.1

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.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,32 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ---
12
+
13
+ ## [0.10.1] — 2026-08-20
14
+
15
+ ### Fixed
16
+
17
+ - A passed-through upstream 5xx on the Anthropic path is logged and counted.
18
+ The proxy is byte-transparent and only special-cased 401/429/529, so a
19
+ plain upstream 500 left no trace: an overnight Anthropic 500 stopped an
20
+ unattended Claude session while the daemon log showed nothing and the
21
+ stats reported a clean night. It now produces an `[ERROR]` line, an
22
+ activity entry (`upstream-error`), and error counts — with no cooldown,
23
+ since a plain 5xx says nothing about the account's capacity and can even
24
+ be request-specific.
25
+ - A client-cancelled stream abort is no longer logged as an error on the
26
+ OpenAI path. The Codex CLI aborts streams routinely, and each abort
27
+ rejects the relay's body read — the log printed an `[ERROR] ... relay
28
+ failed` line for every one (eight hours of them in one overnight
29
+ session) while the stats correctly classified them as cancellations. The
30
+ log line now waits for the cancellation check, so it fires only for real
31
+ relay failures.
32
+
33
+ ---
34
+
35
+ ## [0.10.0] — 2026-08-19
36
+
11
37
  ### Added
12
38
 
13
39
  - OpenAI/Codex account usage is fetched proactively, so `cc-router status`
@@ -52,6 +78,29 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
52
78
  one row when the selection steps past its bottom or top edge, stays put while
53
79
  the selection moves inside it, and re-clamps when new entries push the
54
80
  selected row (which is timestamp-anchored) out of the stored window.
81
+ - OpenAI/Codex sticky session routing: sessions pin to one account for prompt-cache
82
+ locality (`session_id` → `x-claude-code-session-id` → `prompt_cache_key`), with
83
+ load- and headroom-aware selection for new sessions.
84
+ - Codex usage tracking from `x-codex-*` response headers: default 5h/weekly windows
85
+ plus dynamically discovered model-scoped metered buckets, credits, and plan.
86
+ - Scoped cooldowns on upstream failures: bucket-scoped via `x-codex-active-limit`,
87
+ account-global otherwise; local 429/503 responses when no account is eligible.
88
+ - Dashboard: OpenAI accounts now show 5h/weekly bars, per-bucket rows, credits,
89
+ plan, request/error/in-flight/session counts, and cooldown state.
90
+ - Unprefixed `gpt-*` models route to OpenAI. The Codex CLI writes the bare slug
91
+ from its own registry — `model = "gpt-5.6-sol"` in `config.toml`, or whatever
92
+ its `/model` picker selects — and an unprefixed name went to the Claude path,
93
+ where `/v1/responses` answers `501 Not Implemented`. No configuration could
94
+ redirect it, because `openAIAliases` is only consulted for names that are
95
+ already prefixed; those aliases now apply to the bare form as well. Every
96
+ other unprefixed model still routes to Claude.
97
+
98
+ ### Changed
99
+
100
+ - OpenAI account records persist `scopes`, `sessionLimitPercent`, and
101
+ `weeklyLimitPercent`.
102
+ - The stateless OpenAI round-robin picker was removed in favor of
103
+ `OpenAITokenPool`.
55
104
 
56
105
  ### Fixed
57
106
 
@@ -143,39 +192,6 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
143
192
  on `CC_ROUTER_DAEMON`, which the LaunchAgent and systemd unit never set —
144
193
  they set `CC_ROUTER_SERVICE` — so every service-managed instance left no PID
145
194
  behind and took the weaker port-based stop path.
146
-
147
- ---
148
-
149
- ## [0.10.0] — 2026-08-18
150
-
151
- ### Added
152
-
153
- - OpenAI/Codex sticky session routing: sessions pin to one account for prompt-cache
154
- locality (`session_id` → `x-claude-code-session-id` → `prompt_cache_key`), with
155
- load- and headroom-aware selection for new sessions.
156
- - Codex usage tracking from `x-codex-*` response headers: default 5h/weekly windows
157
- plus dynamically discovered model-scoped metered buckets, credits, and plan.
158
- - Scoped cooldowns on upstream failures: bucket-scoped via `x-codex-active-limit`,
159
- account-global otherwise; local 429/503 responses when no account is eligible.
160
- - Dashboard: OpenAI accounts now show 5h/weekly bars, per-bucket rows, credits,
161
- plan, request/error/in-flight/session counts, and cooldown state.
162
- - Unprefixed `gpt-*` models route to OpenAI. The Codex CLI writes the bare slug
163
- from its own registry — `model = "gpt-5.6-sol"` in `config.toml`, or whatever
164
- its `/model` picker selects — and an unprefixed name went to the Claude path,
165
- where `/v1/responses` answers `501 Not Implemented`. No configuration could
166
- redirect it, because `openAIAliases` is only consulted for names that are
167
- already prefixed; those aliases now apply to the bare form as well. Every
168
- other unprefixed model still routes to Claude.
169
-
170
- ### Changed
171
-
172
- - OpenAI account records persist `scopes`, `sessionLimitPercent`, and
173
- `weeklyLimitPercent`.
174
- - The stateless OpenAI round-robin picker was removed in favor of
175
- `OpenAITokenPool`.
176
-
177
- ### Fixed
178
-
179
195
  - A refresh token the OAuth server rejects as terminally expired
180
196
  (`400 invalid_grant`) is no longer retried forever. Every rejection was
181
197
  treated as transient, so the five-minute refresh loop kept re-POSTing a token
@@ -304,6 +304,7 @@ export async function runOpenAIIngress(opts) {
304
304
  };
305
305
  let finalStatus = upstream.status;
306
306
  let relayFailed = false;
307
+ let relayFailureMessage = "";
307
308
  const relayReport = { upstreamReportedFailure: false };
308
309
  try {
309
310
  const result = await relay(upstream, res, entry, relayReport);
@@ -316,7 +317,7 @@ export async function runOpenAIIngress(opts) {
316
317
  // can do is tear the connection down.
317
318
  relayFailed = true;
318
319
  const message = error instanceof Error ? error.message : String(error);
319
- logError(account.id, 502, `openai response relay failed: ${message}`);
320
+ relayFailureMessage = message;
320
321
  // The recorded status is what this request *became*, which is a failure
321
322
  // whether or not another HTTP response can still be sent. Leaving it at
322
323
  // the upstream's 200 in the headers-already-sent case produced an
@@ -346,6 +347,15 @@ export async function runOpenAIIngress(opts) {
346
347
  // upstream announce a failure. Only the truncation is the disconnect's to
347
348
  // explain away.
348
349
  const clientCancelled = clientGone.signal.aborted && !relayReport.upstreamReportedFailure;
350
+ // Logged only now, after the cancellation classification: the Codex CLI
351
+ // aborts streams routinely (a superseded turn, Ctrl-C, a pane closing),
352
+ // and each abort rejects the relay's body read with "This operation was
353
+ // aborted". Logging that from the catch block printed an [ERROR] line for
354
+ // every one — eight hours of them in one unattended overnight session —
355
+ // while the stats correctly ignored them. The log now matches the stats.
356
+ if (relayFailed && !clientCancelled) {
357
+ logError(account.id, 502, `openai response relay failed: ${relayFailureMessage}`);
358
+ }
349
359
  // Activity/stats must reflect what the client actually received, not just
350
360
  // the raw upstream signal: the non-streaming collector can synthesize a
351
361
  // local 502 from an upstream 200 whose SSE stream ended in
@@ -1037,6 +1037,25 @@ export async function startServer(opts = {}) {
1037
1037
  : "service-overloaded";
1038
1038
  logError(account.id, 529, "Service overloaded — cooldown 30s");
1039
1039
  }
1040
+ else if (status >= 500) {
1041
+ // Any other upstream 5xx passes through byte-transparent — but it
1042
+ // must not pass through the DIAGNOSTICS silently: an overnight
1043
+ // Anthropic 500 stopped an unattended Claude session while this
1044
+ // log showed nothing and the stats reported a clean night. The
1045
+ // question "did the proxy or the upstream fail?" was only
1046
+ // answerable by cross-referencing the client's own transcript.
1047
+ // Unlike 429/529 this takes no cooldown: a plain 5xx says nothing
1048
+ // about the account's capacity and can even be request-specific,
1049
+ // so cooling the account down would punish it for upstream's (or
1050
+ // the request's) problem.
1051
+ stats.totalErrors++;
1052
+ account.errorCount++;
1053
+ pendingLog.type = "error";
1054
+ pendingLog.details = route
1055
+ ? routeFailureDetails(route, "upstream-error")
1056
+ : "upstream-error";
1057
+ logError(account.id, status, "Upstream server error (passed through)");
1058
+ }
1040
1059
  // ── Capture rate limit utilization from response headers ────────────
1041
1060
  applyRateLimitHeaders(account, proxyRes.headers);
1042
1061
  const entry = pendingLog;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timo972/cc-router",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "Cache-aware session router for Claude Max OAuth tokens — use multiple Claude Max accounts with Claude Code",
5
5
  "type": "module",
6
6
  "bin": {