@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 +49 -33
- package/dist/proxy/openai-ingress.js +11 -1
- package/dist/proxy/server.js +19 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
package/dist/proxy/server.js
CHANGED
|
@@ -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;
|