@askalf/dario 6.19.0 → 6.19.2
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 +1 -1
- package/dist/account-sync.d.ts +174 -0
- package/dist/account-sync.js +351 -0
- package/dist/accounts.d.ts +40 -1
- package/dist/accounts.js +76 -1
- package/dist/cc-template-data.json +9 -7
- package/dist/cli.js +51 -2
- package/dist/live-fingerprint.d.ts +1 -1
- package/dist/live-fingerprint.js +1 -1
- package/dist/proxy.d.ts +8 -0
- package/dist/proxy.js +84 -6
- package/docs/admin-api.md +8 -0
- package/docs/configuration.md +1 -0
- package/docs/docker.md +35 -4
- package/docs/multi-instance.md +29 -2
- package/package.json +1 -1
package/docs/docker.md
CHANGED
|
@@ -169,6 +169,21 @@ readinessProbe:
|
|
|
169
169
|
> [admin API](./admin-api.md). Provision before relying on readiness, or use a
|
|
170
170
|
> long-window `startupProbe` around the bootstrap.
|
|
171
171
|
|
|
172
|
+
A Service routes only to ready pods, so the admin API of a pod that is not
|
|
173
|
+
ready yet cannot be reached through it. Two ways in that leave the probes as
|
|
174
|
+
they are:
|
|
175
|
+
|
|
176
|
+
- `kubectl port-forward pod/<pod> 3456:3456` connects to the pod itself,
|
|
177
|
+
ready or not, and the admin calls above work against `localhost:3456`.
|
|
178
|
+
- A second Service for the admin calls with `publishNotReadyAddresses: true`
|
|
179
|
+
keeps routing to pods that are not ready. Keep it cluster-internal: it
|
|
180
|
+
reaches a pod before it has an account, which is the point.
|
|
181
|
+
|
|
182
|
+
With more than one replica, `--pool-shared-accounts` (below) removes the
|
|
183
|
+
bootstrap from every pod after the first: a pod that starts with an empty
|
|
184
|
+
accounts directory loads its peers' accounts before the pool starts, and
|
|
185
|
+
reports ready as soon as they do.
|
|
186
|
+
|
|
172
187
|
## Kubernetes example
|
|
173
188
|
|
|
174
189
|
```yaml
|
|
@@ -202,7 +217,7 @@ spec:
|
|
|
202
217
|
volumeMounts:
|
|
203
218
|
- { name: config, mountPath: /home/dario/.dario }
|
|
204
219
|
livenessProbe:
|
|
205
|
-
httpGet: { path: /
|
|
220
|
+
httpGet: { path: /livez, port: 3456 }
|
|
206
221
|
periodSeconds: 30
|
|
207
222
|
readinessProbe:
|
|
208
223
|
httpGet: { path: /health, port: 3456 }
|
|
@@ -224,9 +239,25 @@ spec:
|
|
|
224
239
|
ports: [{ port: 3456, targetPort: 3456 }]
|
|
225
240
|
```
|
|
226
241
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
242
|
+
Keep `replicas: 1` unless the replicas share a refresh lock: OAuth refresh
|
|
243
|
+
tokens are single use, and two pods holding the same account without one
|
|
244
|
+
race to refresh it. For more than one replica, run the
|
|
245
|
+
[refresh lock](./multi-instance.md#turning-the-refresh-lock-on) and, so a new
|
|
246
|
+
pod starts with the others' accounts and an account added or removed on one
|
|
247
|
+
pod reaches every pod,
|
|
248
|
+
[`--pool-shared-accounts`](./multi-instance.md#sharing-the-account-list):
|
|
249
|
+
|
|
250
|
+
```yaml
|
|
251
|
+
env:
|
|
252
|
+
- { name: DARIO_REFRESH_LOCK_URL, value: "http://dario-refresh-lock:8080" }
|
|
253
|
+
- { name: DARIO_REFRESH_LOCK_TOKEN, valueFrom: { secretKeyRef: { name: dario-lock, key: token } } }
|
|
254
|
+
- { name: DARIO_POOL_SHARED_ACCOUNTS, value: "1" }
|
|
255
|
+
- { name: DARIO_POOL_SHARED_STATE, value: "1" }
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
The registry holds every seat's credentials, so the lock service and its Redis
|
|
259
|
+
need the same care as the accounts volume; see
|
|
260
|
+
[Sharing the account list](./multi-instance.md#sharing-the-account-list).
|
|
230
261
|
|
|
231
262
|
## Image updates
|
|
232
263
|
|
package/docs/multi-instance.md
CHANGED
|
@@ -117,6 +117,34 @@ The proxy says what it is doing at startup (`Pool shared state: on (instance …
|
|
|
117
117
|
|
|
118
118
|
It fails open like the lock: an unreachable service is one log line per outage and each instance carries on with its own state until the service answers again.
|
|
119
119
|
|
|
120
|
+
## Sharing the account list
|
|
121
|
+
|
|
122
|
+
Without it, each instance serves the accounts in its own `~/.dario/accounts/`. With `--pool-shared-accounts` the instances keep one account list in the same lock service: a replica that starts with an empty accounts directory (a second pod on a fresh volume) starts with its peers' accounts, and an account added or removed on one instance is added or removed on all of them.
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
DARIO_REFRESH_LOCK_URL=http://<lock-host>:8080
|
|
126
|
+
DARIO_REFRESH_LOCK_TOKEN=<shared secret>
|
|
127
|
+
DARIO_POOL_SHARED_ACCOUNTS=1 # or: dario proxy --pool-shared-accounts
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Set it on every instance. It works with or without `--pool-shared-state`.
|
|
131
|
+
|
|
132
|
+
- **At startup**, before the pool loads, an instance reads the registry and merges it with its accounts directory. Per alias the newer record wins: a newer login first, then, for the same login, the later expiry. Accounts only this instance holds are published; accounts only the registry holds are written to this instance's accounts directory. The pool then loads from that directory as usual, so `/health` is ready as soon as a peer has an account.
|
|
133
|
+
- **Every 15 seconds** after that it merges again, and updates the running pool too: a new account joins it, newer credentials replace older ones, and a removed account leaves it. A `dario accounts add`, an admin API login or a token refresh on one instance reaches the others within one sync. An admin API login or removal is published straight away, and the other instances apply it on their next sync.
|
|
134
|
+
- **A removal leaves a marker** in the registry. The marker beats every login made before the removal, so an instance that was offline and still holds the old file deletes it instead of publishing it back. That holds for the `login` account taken from `credentials.json` at startup too: an instance whose login was removed starts without it instead of serving it. A login made after the removal beats the marker, and the account comes back everywhere.
|
|
135
|
+
- **`dario accounts remove <alias>`** publishes the removal first and deletes the file only once the service has taken it. If the service cannot be reached it removes nothing and says so, since the other instances would put the account straight back. A login made after the removal stays, in the registry or in the file here, and the command says so; run it again to remove that login too. If the file here can't be deleted, the command says so and exits non-zero, and this instance keeps serving the account until the file goes.
|
|
136
|
+
|
|
137
|
+
The proxy says it is on at startup (`Pool shared accounts: on (via …, syncing every 15s; fails open)`), and `GET /accounts` carries a `sharedAccounts` block: the last sync and the last good one, how many accounts and removal markers the registry holds, the files this instance saved and removed from it, the accounts and removals it published, and errors. Each change is logged: `added account "work" (wo***@example.com) from the registry`, `removed account "spare"; it was removed on another instance`, `published account "home" to the registry`.
|
|
138
|
+
|
|
139
|
+
Before turning it on:
|
|
140
|
+
|
|
141
|
+
- **The registry holds every seat's credentials, refresh tokens included, and they do not expire there.** That is what lets a new replica start with no files. Guard the lock service like the accounts directory itself: a real random `DARIO_REFRESH_LOCK_TOKEN`, the service on a private network, and a Redis that nothing else uses. Anyone who can read that Redis or the Durable Object can use every subscription in the pool.
|
|
142
|
+
- **Aliases are one namespace across all instances.** Two instances that each log a different subscription in under the same alias (`login`, most often) keep only the newer login, and the instance whose account was replaced logs it loudly: `account "login" here was ... and is now ...; aliases are shared across instances, so give each subscription its own alias`. Give each subscription its own alias.
|
|
143
|
+
- **Clocks have to agree.** The newer login and removal rules compare wall-clock times taken on different machines. Run NTP; Kubernetes nodes do by default.
|
|
144
|
+
- **Claude seats only.** ChatGPT accounts stay per instance, and the switch is off under `--no-claude-auth`.
|
|
145
|
+
- **It fails open.** With the service unreachable, each instance serves the accounts on its own disk and logs once per outage. A removal made while it is down is published when it answers again; if the instance restarts before then, that removal is lost and the account comes back from the registry, so remove it again.
|
|
146
|
+
- **Upgrade the lock service.** The `/accounts/*` endpoints are new: rebuild the Redis image or redeploy the Worker. An older service answers 404, which an instance logs as the service being unreachable and then serves its own disk, so nothing breaks, but nothing is shared either.
|
|
147
|
+
|
|
120
148
|
## Proving it works on your own setup
|
|
121
149
|
|
|
122
150
|
Do not take the above on trust. `test/integration/dual-instance-race.mjs` runs the real scenario: **two genuinely separate `node` processes**, each with its own isolated `~/.dario`, sharing nothing but the lock service, both racing to refresh the same account at the same instant.
|
|
@@ -142,9 +170,8 @@ Anthropic's token endpoint is the one mocked part, and only that — hammering t
|
|
|
142
170
|
|
|
143
171
|
## If you need true HA
|
|
144
172
|
|
|
145
|
-
Everything above is as far as dario goes today: shared credentials through the lock,
|
|
173
|
+
Everything above is as far as dario goes today: shared credentials through the lock, shared rate-limit readings and sticky bindings through `--pool-shared-state`, and a shared account list through `--pool-shared-accounts`, all eventually consistent and all failing open. Without `--pool-shared-accounts` the account list is per instance too, and copying the account files to every instance is safe with the refresh lock on: the copies share one refresh token, and whichever instance refreshes first hands the result to the others ([#1733](https://github.com/askalf/dario/issues/1733)). Without the lock, copies race exactly as described in section 1. Three things are still per instance:
|
|
146
174
|
|
|
147
|
-
- **The account list.** Each instance loads `~/.dario/accounts/` from its own disk, and a login through the admin API reloads only the instance that served it. Copying the account files to every instance is safe with the refresh lock on: the copies share one refresh token, and whichever instance refreshes first hands the result to the others ([#1733](https://github.com/askalf/dario/issues/1733)). Without the lock, copies race exactly as described in section 1.
|
|
148
175
|
- **ChatGPT seats.** The Codex accounts have their own refresh flow and usage windows; neither the lock nor the shared state covers them.
|
|
149
176
|
- **The ledger.** Each instance writes its own file, so `dario usage` and `/analytics` show that instance's share of the spend.
|
|
150
177
|
- **The overage guard's halt**, which each instance tracks on its own.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "6.19.
|
|
3
|
+
"version": "6.19.2",
|
|
4
4
|
"description": "Use your Claude and ChatGPT subscriptions in Cursor, Cline, Aider, Claude Code and the Agent SDK at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint: either plan answers either wire shape, opt-in failover when one hits its limit, and a ledger of what each plan absorbed.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|