@rikcodes/teamclaude 1.1.13-rik.4 → 1.1.14-rik.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # TeamClaude
2
2
 
3
- > **Fork notice (rikbrown).** This fork adds four features and two reload fixes on top of
3
+ > **Fork notice (rikbrown).** This fork adds three features on top of
4
4
  > [KarpelesLab/teamclaude](https://github.com/KarpelesLab/teamclaude):
5
5
  >
6
6
  > - **[OpenAI models via a Codex sidecar](docs/openai.md)** (`sidecars` + `customModels`, opt-in):
@@ -11,16 +11,13 @@
11
11
  > equal-priority accounts by the weekly window that governs the requested model, continuously — preempt the
12
12
  > current account when another resets more than `poolHours` sooner, and balance `distributeSessions` within
13
13
  > that pool instead of across all accounts. Spends the quota closest to refreshing first, so a window no
14
- > longer rolls over with quota unspent.
14
+ > longer rolls over with quota unspent. Applied live on config reload.
15
15
  > - **[Burn-rate projection](docs/quota.md#burn-rate-projection)** (`projection`, on by default): sample each
16
16
  > bucket's consumption over a rolling window and tag every account row with whichever window binds
17
17
  > first — `Ses TTL 38m` when it runs out before it resets, `Wk 22% unspent` when the reset arrives
18
18
  > first and that much expires. A readout only: no selection code reads it.
19
- > - **[Session titles](docs/usage.md#session-titles-in-the-activity-log)** (`sessionTitles`, on by default):
20
- > name each activity row after the Claude Code session that sent the request, reading the title
21
- > `/rename` writes and the one Claude Code generates. A session with neither keeps its short id.
22
- > - `soonestWeekly` and `distributeSessions` changes now apply on config reload; upstream applies
23
- > `distributeSessions` only at startup.
19
+ >
20
+ > Setup and use of each: [Fork features](#fork-features).
24
21
  >
25
22
  > Published as [`@rikcodes/teamclaude`](https://www.npmjs.com/package/@rikcodes/teamclaude); self-update
26
23
  > tracks that package, so installs of this fork can never be replaced by an upstream release.
@@ -59,6 +56,11 @@ teamclaude run # in another terminal: Claude Code through the proxy
59
56
 
60
57
  Already logged into Claude Code? `teamclaude import` takes its credentials instead of a fresh OAuth round. API keys, and one email holding accounts in several orgs, are covered in [docs/accounts.md](docs/accounts.md).
61
58
 
59
+ `teamclaude run` does not require a separate Claude Code login for normal API
60
+ requests. When local Claude OAuth is unavailable or expired, proxy credential
61
+ mode supplies a local-only bootstrap key; the proxy replaces that placeholder
62
+ with the selected TeamClaude account credential before forwarding.
63
+
62
64
  ## What it does
63
65
 
64
66
  - Rotates to the next account when the 5h session or 7d weekly bucket reaches the threshold (98% by default), preferring the account whose weekly quota resets soonest.
@@ -104,6 +106,91 @@ Every field, plus environment variables and network tuning: [docs/configuration.
104
106
 
105
107
  Step-by-step lifecycle: [docs/routing.md](docs/routing.md#request-lifecycle).
106
108
 
109
+ ## Fork features
110
+
111
+ How to set up and use the features in this fork. Everything else in this README is upstream behaviour.
112
+
113
+ ### OpenAI models via a Codex sidecar
114
+
115
+ A Claude Code session can use OpenAI models alongside the Claude accounts. They are billed to a ChatGPT Plus/Pro subscription and use their real model names in the same session. TeamClaude does not translate the wire format itself. A local **sidecar** does this: [raine/claude-code-proxy](https://github.com/raine/claude-code-proxy) speaks `/v1/messages` on the front and the Codex Responses API on the back. TeamClaude starts and supervises this process. It sends every `gpt-*` request to the sidecar and keeps every other request on the Claude accounts.
116
+
117
+ **1. Install the sidecar and log it into your ChatGPT account** (one time):
118
+
119
+ ```bash
120
+ brew install raine/claude-code-proxy/claude-code-proxy
121
+ claude-code-proxy codex auth login
122
+ ```
123
+
124
+ **2. Connect it** — add four pieces to `~/.config/teamclaude.json`:
125
+
126
+ ```json
127
+ {
128
+ "sidecars": [
129
+ { "name": "codex", "command": ["claude-code-proxy", "serve", "--no-monitor", "--port", "18765"] }
130
+ ],
131
+ "accounts": [
132
+ { "name": "codex", "type": "oauth", "accessToken": "unused-local-sidecar",
133
+ "upstream": "http://127.0.0.1:18765", "priority": 100 }
134
+ ],
135
+ "routes": [
136
+ { "name": "codex", "match": ["gpt-*"], "accounts": ["codex"] },
137
+ { "name": "anthropic", "match": ["*"], "accounts": ["your-claude-account", "..."] }
138
+ ],
139
+ "customModels": [
140
+ { "model": "gpt-5.6-sol", "label": "GPT-5.6 Sol", "contextTokens": 272000 },
141
+ { "model": "gpt-5.6-terra", "label": "GPT-5.6 Terra", "contextTokens": 272000 },
142
+ { "model": "gpt-5.6-luna", "label": "GPT-5.6 Luna", "contextTokens": 272000 }
143
+ ]
144
+ }
145
+ ```
146
+
147
+ - `sidecars` — the process that TeamClaude owns. It starts with the server, restarts with backoff after a crash, and stops on shutdown. Its pid, restart count and latest stderr lines are in `teamclaude status --json` under `sidecars`.
148
+ - `accounts` — the sidecar as a [third-party backend account](docs/accounts.md#third-party-backend-accounts). The token is a placeholder because the sidecar uses its own Codex login for authentication. `priority: 100` is the convention for third-party backends. The routes determine what reaches it.
149
+ - `routes` — sends `gpt-*` to the sidecar. **Keep the catch-all `*` route.** Without it, the sidecar joins the exhaustion-fallback pool. A spent Claude fleet would then silently send Claude-model requests to the sidecar, which maps `claude-*` names onto GPT models. With the route, a request can reach a GPT model only when it asks for one by name.
150
+ - `customModels` — the rows that make the models visible to Claude Code (next section).
151
+
152
+ **3. Restart the server.** `teamclaude status --json` should show the sidecar as `running`. If it enters a crash loop, `stderrTail` explains why — the usual cause is that the sidecar is not logged in.
153
+
154
+ #### How models get into Claude Code
155
+
156
+ Claude Code offers only models that it knows, and it does not know `gpt-*`. `customModels` closes this gap. Each row contains a model id that the proxy can serve, an optional picker label and description, and the model's context window.
157
+
158
+ At launch, `teamclaude run` — and the `claude` alias, which passes through `run` — gives these rows to Claude Code:
159
+
160
+ | Row field | Where it ends up |
161
+ | --- | --- |
162
+ | `model`, `label`, `description` | A `/model` picker row under the **real** model id (`--settings`), so `/model gpt-5.6-sol` works picked or typed |
163
+ | `model` | A dispatchable subagent named after the model (`--agents`), so "dispatch a `gpt-5.6-terra` subagent" works from a Claude parent |
164
+ | `contextTokens` | `CLAUDE_CODE_MAX_CONTEXT_TOKENS`, set to the largest value across rows, so Claude Code compacts at the real window instead of assuming 200k |
165
+
166
+ For tools that spawn `claude` themselves, `teamclaude env` can set only environment variables. It carries the window and `ANTHROPIC_CUSTOM_MODEL_OPTION` for the **first** row. For GPT subagents under `env`, create `~/.claude/agents/<name>.md` with `model: gpt-5.6-terra` in its frontmatter.
167
+
168
+ Each request is routed by the model name in its body, so one session can freely mix models: use `claude --model gpt-5.6-sol` for a whole session, `/model gpt-5.6-sol` during a session, or a Claude parent that dispatches a GPT subagent.
169
+
170
+ **To add a model:**
171
+
172
+ 1. Check that your sidecar build lists it: `curl -s http://127.0.0.1:18765/v1/models`. The sidecar has its own allow-list and rejects any id that it does not know, regardless of the TeamClaude configuration. Upgrade the sidecar if the id is missing.
173
+ 2. Add a `customModels` row. Codex publishes the window for each model as `context_window` in `~/.codex/models_cache.json`; copy it to `contextTokens`.
174
+ 3. Start a new `teamclaude run` session. The rows are read at launch, so you do not need to restart the server. If you upgraded the sidecar binary, restart the server — or send `SIGTERM` to the sidecar process and let the supervisor restart it with the new binary.
175
+
176
+ Claude Code prints one `[claude-code:unrecognized_model]` line to stderr for each custom model. This is expected; suppressing it would lose the correct context window. The quota bars for the sidecar account show `unknown` unless the sidecar forwards Codex's rate-limit headers — see [Quota](docs/openai.md#quota). Keep the sidecar on loopback, and use **one** ChatGPT subscription for each person. Pooling several subscriptions is the pattern that OpenAI's fraud systems target ([terms of service](docs/openai.md#terms-of-service)).
177
+
178
+ Full details: [docs/openai.md](docs/openai.md).
179
+
180
+ ### Soonest-weekly rotation
181
+
182
+ Default selection is sticky. It re-ranks only when the current account is exhausted, so a weekly window can expire with unused quota. `soonestWeekly` re-ranks continuously. Among equal-priority accounts, the one whose governing weekly window resets soonest is used first. It preempts the current account when another account resets more than `poolHours` sooner.
183
+
184
+ ```json
185
+ "soonestWeekly": { "enabled": true, "poolHours": 12 }
186
+ ```
187
+
188
+ `distributeSessions` works with this setting. New sessions balance across that pool instead of across all equal-priority accounts. Both settings take effect when the configuration reloads (upstream applies `distributeSessions` only at startup). Details: [Routing](docs/routing.md#soonest-weekly-rotation).
189
+
190
+ ### Burn-rate projection
191
+
192
+ This feature is on by default. Each account row shows which window binds first: `Ses TTL 38m` when the session bucket runs out before it resets, or `Wk 22% unspent` when the weekly reset arrives first and that amount of quota expires. This is a readout only; no selection code reads it. Tune or disable it with `projection: { enabled, windowMinutes, wasteFloor }`. Details: [Quota](docs/quota.md#burn-rate-projection).
193
+
107
194
  ## Documentation
108
195
 
109
196
  | Page | Contents |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rikcodes/teamclaude",
3
- "version": "1.1.13-rik.4",
3
+ "version": "1.1.14-rik.1",
4
4
  "description": "Multi-account Claude proxy with automatic quota-based rotation",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -0,0 +1,52 @@
1
+ // The identifier a config account entry carries, and the reason it has to exist.
2
+ //
3
+ // The config list and the AccountManager's are not positionally aligned:
4
+ // resolveAccounts drops every entry without a usable credential, so from the
5
+ // first drop onward a config index and a manager index name different accounts
6
+ // for the life of the process. Nothing on the records recovers the pairing
7
+ // either. Two entries can agree on name, account, organization and credential,
8
+ // and the credential — the one field that might separate two records of one
9
+ // person — is rewritten by the token refresh that makes the pairing matter.
10
+ //
11
+ // So an entry carries an identifier of its own, assigned before anything can
12
+ // read one, never rewritten afterwards, and copied onto the account built from
13
+ // the entry by makeAccount.
14
+
15
+ import { randomUUID } from 'node:crypto';
16
+
17
+ /**
18
+ * A fresh entry id.
19
+ *
20
+ * Minted where an entry and the account built from it are created together — the
21
+ * TUI's add paths — because the two have to agree on an id before anything reads
22
+ * one. An entry created anywhere else reaches memory through loadConfig or an
23
+ * admission from disk, and both of those assign ids, so those paths mint none of
24
+ * their own.
25
+ */
26
+ export function mintAccountId() {
27
+ return randomUUID();
28
+ }
29
+
30
+ /**
31
+ * Give every entry in `accounts` an id that no other entry in the list holds,
32
+ * mutating them in place, and return the list.
33
+ *
34
+ * Entry creation mints an id, so this is for entries that arrive already made:
35
+ * a config written before the field existed, and an entry admitted from disk
36
+ * while the server runs. Calling it as those enter an in-memory list is what
37
+ * makes the uniqueness every lookup assumes true from the first read.
38
+ *
39
+ * A duplicate is re-minted rather than kept. Copying an account section by hand
40
+ * copies its id along with it, and two entries answering to one id collapse
41
+ * onto whichever comes first — the later one would be handed the earlier one's
42
+ * credential, which is the crossing this field exists to prevent.
43
+ */
44
+ export function ensureAccountIds(accounts) {
45
+ const seen = new Set();
46
+ for (const acct of accounts || []) {
47
+ if (!acct) continue;
48
+ if (typeof acct.id !== 'string' || acct.id === '' || seen.has(acct.id)) acct.id = mintAccountId();
49
+ seen.add(acct.id);
50
+ }
51
+ return accounts;
52
+ }