@byokit/openclaw 0.25.0 → 0.26.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 +24 -0
- package/README.md +15 -7
- package/dist/accounts.d.ts +6 -0
- package/dist/accounts.js +47 -3
- package/dist/auth-status.d.ts +13 -3
- package/dist/auth-status.js +33 -10
- package/dist/bridge.js +3 -3
- package/dist/constants.d.ts +1 -2
- package/dist/constants.js +8 -8
- package/dist/detect.d.ts +30 -0
- package/dist/detect.js +39 -0
- package/dist/keys.js +22 -14
- package/dist/kit.d.ts +2 -0
- package/dist/kit.js +38 -8
- package/dist/signin.d.ts +2 -0
- package/dist/signin.js +12 -2
- package/package.json +2 -2
- package/plugin/catalog.js +13 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.26.1 (2026-10-21)
|
|
6
|
+
|
|
7
|
+
- FIX: a key sign-in through `@byokit/openclaw` no longer fires one slow `openclaw.setup.detect` per reader: the
|
|
8
|
+
readiness read, the account list and a sign-in share one detection per agent (one probe in flight, its answer reused
|
|
9
|
+
briefly, and every sign-in, sign-out, key change and account change drops it), and the pin's own status and the
|
|
10
|
+
detection now run together. The app's poll after a sign-in costs one probe instead of fifteen.
|
|
11
|
+
- FIX: a key lane lists the models the provider's live catalog offers, so a model the picker showed (MiniMax Flash)
|
|
12
|
+
stays listed and pickable instead of being refused as "not included" while the engine's own list still lags the
|
|
13
|
+
registration. The kit keeps no model list of its own: the rows are the ones it registered from the live catalog.
|
|
14
|
+
- FIX: an OpenRouter key sign-in now registers the provider's live list beside the model the person chose, so the
|
|
15
|
+
member keeps every model the vendor lists to pick from (the MiniMax models, `minimax/minimax-m3.1-flash` and its
|
|
16
|
+
free variant, among them) and `openrouter/auto` stays beside them. The engine's catalog stops at a registered list,
|
|
17
|
+
so an add that registered the chosen model alone left the member with that one row and nothing else to pick from.
|
|
18
|
+
The rows are deduped by the id the engine keeps, so the Google preview pair is one row and the list is never
|
|
19
|
+
refused, and the chosen model is registered on its own as well, so a list the engine refuses never costs the member
|
|
20
|
+
their own choice.
|
|
21
|
+
|
|
22
|
+
## 0.26.0 (2026-10-20)
|
|
23
|
+
|
|
24
|
+
- Dependency update: pins @byokit/accounts 0.30.0.
|
|
25
|
+
|
|
26
|
+
- FEAT: every key route now lists its own models: `kit.models(member, { authChoice })` answers the provider plugin's catalog joined by the vendor's own `/models` (not only OpenCode Go, OpenRouter and MiniMax), and `addKey({ ..., model })` takes any full `provider/model` id on any key route. An id the catalog lags is taken with the route's own capabilities instead of being refused; only a route with no catalog at all refuses a model.
|
|
27
|
+
- The bridge's Codex CLI route now names its two model rows itself instead of reading them from `@byokit/accounts`' catalogue, which no longer keeps model tiers; the rows the engine lists are unchanged.
|
|
28
|
+
|
|
5
29
|
## 0.25.0 (2026-10-20)
|
|
6
30
|
|
|
7
31
|
- Dependency update: pins @byokit/accounts 0.29.0.
|
package/README.md
CHANGED
|
@@ -159,10 +159,10 @@ other choice that takes no key (a subscription such as `openai`, a provider with
|
|
|
159
159
|
throws its cause before anything is sent.
|
|
160
160
|
`reason` is `auth` (provider refused the key) or `billing` (the provider's billing refused it; the kit words it and never passes
|
|
161
161
|
provider text): both are `invalid`, and an unchanged key must not be retried. `empty` is a blank key, rejected locally before any
|
|
162
|
-
provider call, also `invalid`. `model` is an unsupported route or
|
|
162
|
+
provider call, also `invalid`. `model` is an unsupported route or a model on a route with no catalog (`not_included`), or a model the engine did not
|
|
163
163
|
register (`retry`). `network` is a timeout or transport failure; `inference` is a provider reply in an unexpected format, an unknown
|
|
164
164
|
probe failure, or a typed request-validation refusal; `engine` is a control or preparation failure; `busy` is a rate limit or a
|
|
165
|
-
restarting engine. `not_included` covers an unsupported route,
|
|
165
|
+
restarting engine. `not_included` covers an unsupported route, a chosen model on a route with no catalog and a probe reporting the route unavailable;
|
|
166
166
|
every other failure is `retry`. Migrate from the old string result with `result.status === 'ok'`, not `result === 'ok'`.
|
|
167
167
|
An OpenCode Go plan login can act for several workspaces, and the Go gateway refuses its every call until one is named
|
|
168
168
|
(`403 Workspace selection is required`). Pass the workspace as `orgId` when you have it; it rides the probe and every run as
|
|
@@ -185,16 +185,24 @@ live catalog. Before a key exists it returns `[]` (a failed catalog probe with n
|
|
|
185
185
|
empty; with one it is an error) and runs no engine discovery. Only available key-lane
|
|
186
186
|
models can be saved; the ordinary member's sign-ins never supply the list. Pass a full catalog id, e.g.
|
|
187
187
|
`model: 'opencode-go/muse-spark-1.3-contributor'`, to `addKey` to save it as the lane's default after activation.
|
|
188
|
-
|
|
188
|
+
Every key route lists and takes models: its catalog is the provider plugin's own, joined by the vendor's own `/models`
|
|
189
|
+
list, and the kit keeps no provider or model list. An OpenRouter key is
|
|
189
190
|
pay-as-you-go with no plan, so it takes any model its own live catalog lists, named `openrouter/<id>`, e.g.
|
|
190
191
|
`model: 'openrouter/deepseek/deepseek-v4.1-flash-1017'`; an id already beginning with its own provider keeps one
|
|
191
192
|
prefix (`openrouter/auto`). A Google preview id is named and saved as the canonical id the engine keeps
|
|
192
193
|
(`openrouter/google/gemini-3.1-flash-lite-preview` becomes `openrouter/google/gemini-3.1-flash-lite`); the add
|
|
193
|
-
registers that one model with the engine
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
registers that one model with the engine (every provider but OpenCode Go and MiniMax, whose whole lists are registered).
|
|
195
|
+
An OpenRouter key add registers the provider's live list beside the chosen model, so the member keeps every model the
|
|
196
|
+
vendor lists to pick from (the MiniMax models, `minimax/minimax-m3.1-flash` and its free variant, among them) and
|
|
197
|
+
`openrouter/auto` stays beside them; the engine's catalog stops at a registered list, so registering the chosen model
|
|
198
|
+
alone would leave the member with that one row. The rows are deduped by the id the engine keeps, so the Google preview
|
|
199
|
+
pair is one row and the list is never refused.
|
|
200
|
+
A vendor's list can lag a model it already runs, so an id the catalog lacks is taken, not refused: it rides the route's
|
|
201
|
+
own models (the richest row's api, base URL and context), unpriced, and the app sees it missing from `models()` to warn.
|
|
202
|
+
A route with no catalog rows (its plugin has none) lists nothing, and a `model` there returns
|
|
203
|
+
`{ status: 'not_included', reason: 'model' }` and clears the replacement.
|
|
196
204
|
Before success, `addKey` waits for catalog registration and prepares the new key agent's full-catalog owner once.
|
|
197
|
-
|
|
205
|
+
A chosen model must also be in the engine's registered `models.providers[provider].models`
|
|
198
206
|
list before anything is sealed; otherwise the add clears the attempt and returns `{ status: 'retry', reason: 'model' }`.
|
|
199
207
|
Preparation failure clears the attempted key and returns `{ status: 'retry', reason: 'engine' }`; ordinary runs stay unchanged.
|
|
200
208
|
With no choice, today's starter stays.
|
package/dist/accounts.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { createLocks } from './locks.ts';
|
|
2
|
+
import type { Detector } from './detect.ts';
|
|
2
3
|
import { type RouteView } from './routes.ts';
|
|
3
4
|
import type { Account, AccountId, AccountPick, Defaults, GatewayTransport, Member, ModelInfo, MoveResult, Room, RunSelection, SignInOptions, SignInView } from './types.ts';
|
|
4
5
|
export declare const kitId: (provider: string, id: string) => string;
|
|
6
|
+
/** The id the engine keeps for a row of `provider` (5.6): a Google preview id (a row of a provider that carries
|
|
7
|
+
* Google's models) and a bare id of a provider that names it. Every other id is the vendor's own, kept as it is. */
|
|
8
|
+
export declare const engineModelId: (provider: string, id: string) => string;
|
|
5
9
|
export declare const engineId: (provider: string, model: string, rows: readonly unknown[] | undefined) => string;
|
|
6
10
|
export declare const modelsOf: (provider: string, answer: unknown) => ModelInfo[];
|
|
7
11
|
export declare const modelsOfAccount: (row: Pick<Account, 'provider' | 'state'>, answer: unknown) => ModelInfo[];
|
|
@@ -73,6 +77,8 @@ export declare function createAccounts(ctx: {
|
|
|
73
77
|
}>;
|
|
74
78
|
log?: (line: string) => void;
|
|
75
79
|
reseal?: () => Promise<void>;
|
|
80
|
+
detect?: Detector;
|
|
81
|
+
catalog?: (provider: string) => ModelInfo[];
|
|
76
82
|
}): {
|
|
77
83
|
list(member: Member): Promise<Account[]>;
|
|
78
84
|
add(member: Member, o: AddOptions, on: (v: SignInView) => void): Promise<AccountSignIn>;
|
package/dist/accounts.js
CHANGED
|
@@ -10,6 +10,7 @@ import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync
|
|
|
10
10
|
import { randomBytes, randomUUID } from 'node:crypto';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import { DatabaseSync } from 'node:sqlite';
|
|
13
|
+
import { detection } from './auth-status.js';
|
|
13
14
|
import { providerOf, resolveSelection } from './pick.js';
|
|
14
15
|
import { agentOf, memberOf as baseMemberOf } from './attribution.js';
|
|
15
16
|
import { KEY_PREFIX, MEMBER_ID, NEW_MEMBER_ID, keyAgentId, untilListed, untilUnlisted } from './members.js';
|
|
@@ -19,6 +20,35 @@ const record = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
|
19
20
|
// A provider-local engine model id as the kit's `provider/model` reference; an id already prefixed with its own
|
|
20
21
|
// provider is left alone.
|
|
21
22
|
export const kitId = (provider, id) => providerOf(id) === provider.toLowerCase() ? id : `${provider}/${id}`;
|
|
23
|
+
// The id the pinned engine keeps for a catalog row: it rewrites a row id when it writes the config. A Google
|
|
24
|
+
// preview id becomes the id Google calls it (`google/gemini-3.1-flash-lite-preview` becomes
|
|
25
|
+
// `google/gemini-3.1-flash-lite`), and a bare id of a provider that names it carries that provider. The kit names
|
|
26
|
+
// every row by the id the engine keeps, so the engine's list and the kit's never disagree, and two rows the engine
|
|
27
|
+
// would keep under one id are one row: a list with both is refused ("Ambiguous duplicate ID").
|
|
28
|
+
const googlePreviewIds = {
|
|
29
|
+
'gemini-3-pro': 'gemini-3.1-pro-preview',
|
|
30
|
+
'gemini-3-pro-preview': 'gemini-3.1-pro-preview',
|
|
31
|
+
'gemini-3.1-pro': 'gemini-3.1-pro-preview',
|
|
32
|
+
'gemini-3-flash': 'gemini-3-flash-preview',
|
|
33
|
+
'gemini-3.1-flash': 'gemini-3-flash-preview',
|
|
34
|
+
'gemini-3.1-flash-preview': 'gemini-3-flash-preview',
|
|
35
|
+
'gemini-3.1-flash-lite-preview': 'gemini-3.1-flash-lite',
|
|
36
|
+
'gemma-4-26b': 'gemma-4-26b-a4b-it',
|
|
37
|
+
};
|
|
38
|
+
/** The id the engine keeps for a row of `provider` (5.6): a Google preview id (a row of a provider that carries
|
|
39
|
+
* Google's models) and a bare id of a provider that names it. Every other id is the vendor's own, kept as it is. */
|
|
40
|
+
export const engineModelId = (provider, id) => {
|
|
41
|
+
const at = id.indexOf('/');
|
|
42
|
+
const maker = at < 0 ? '' : id.slice(0, at);
|
|
43
|
+
const model = at < 0 ? id : id.slice(at + 1);
|
|
44
|
+
if (maker === 'google') {
|
|
45
|
+
const kept = googlePreviewIds[model] ?? model;
|
|
46
|
+
return kept === model ? id : `${maker}/${kept}`;
|
|
47
|
+
}
|
|
48
|
+
if (provider === 'openrouter' && at < 0)
|
|
49
|
+
return `${provider}/${id}`;
|
|
50
|
+
return id;
|
|
51
|
+
};
|
|
22
52
|
// The engine's provider-local id for a kit `provider/model` reference, resolved against that provider's rows.
|
|
23
53
|
export const engineId = (provider, model, rows) => {
|
|
24
54
|
for (const row of rows ?? []) {
|
|
@@ -538,8 +568,9 @@ export function createAccounts(ctx) {
|
|
|
538
568
|
return { rows, dirty };
|
|
539
569
|
};
|
|
540
570
|
// A detected Claude Code login has no profile in the kit: it stays in Claude Code. The engine's detection says whether
|
|
541
|
-
// it is there, and a detected one is the member's signed-in claude-cli profile for this read.
|
|
542
|
-
|
|
571
|
+
// it is there, and a detected one is the member's signed-in claude-cli profile for this read. The shared detection
|
|
572
|
+
// answers it for every reader (one probe per agent, reused briefly), so a listing and a readiness read cost one probe.
|
|
573
|
+
const claudeDetected = (agentId) => detection(ctx.request, agentId, ctx.detect)
|
|
543
574
|
.then((r) => r.candidates?.some((c) => c.kind === 'claude-cli' && c.credentials === true) === true, () => false);
|
|
544
575
|
const gather = async (agentId) => {
|
|
545
576
|
const [roster, profiles, claude] = await Promise.all([rosterOf(), profilesOf(agentId), claudeDetected(agentId)]);
|
|
@@ -669,8 +700,21 @@ export function createAccounts(ctx) {
|
|
|
669
700
|
const { row, entry } = await entryOf(member, id);
|
|
670
701
|
return readModels(row, entry);
|
|
671
702
|
}
|
|
703
|
+
// The account's models: the engine's own rows, joined with the catalog rows the kit registered for that provider
|
|
704
|
+
// (the provider's live list). The engine's list can lag the registration, and a model the picker offered must
|
|
705
|
+
// stay pickable: the kit registered it, so the engine can run it. The engine's own row wins where both name it.
|
|
706
|
+
const withCatalog = (row, own) => {
|
|
707
|
+
const registered = ctx.catalog?.(row.provider) ?? [];
|
|
708
|
+
if (!registered.length)
|
|
709
|
+
return own;
|
|
710
|
+
const byId = new Map(own.map((m) => [m.id, m]));
|
|
711
|
+
for (const model of registered)
|
|
712
|
+
if (!byId.has(model.id))
|
|
713
|
+
byId.set(model.id, model);
|
|
714
|
+
return [...byId.values()];
|
|
715
|
+
};
|
|
672
716
|
// The shared hold covers the read, so no kit read of the agent is in flight while it is removed.
|
|
673
|
-
const readModels = (row, entry) => ctx.locks.shared(entry.agent, async () => modelsOfAccount(row, await ctx.request('models.list', { agentId: entry.agent, view: 'all' })));
|
|
717
|
+
const readModels = (row, entry) => ctx.locks.shared(entry.agent, async () => withCatalog(row, modelsOfAccount(row, await ctx.request('models.list', { agentId: entry.agent, view: 'all' }))));
|
|
674
718
|
async function room(member, id, demand) {
|
|
675
719
|
check(member);
|
|
676
720
|
await ctx.ensure(member);
|
package/dist/auth-status.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Detection, Detector } from './detect.ts';
|
|
1
2
|
import type { GatewayTransport } from './types.ts';
|
|
2
3
|
export type AuthStatus = {
|
|
3
4
|
providers?: (string | {
|
|
@@ -14,14 +15,23 @@ export type AuthStatus = {
|
|
|
14
15
|
message?: unknown;
|
|
15
16
|
};
|
|
16
17
|
};
|
|
18
|
+
/** The engine's setup detection, through the shared detector when the caller has one: one probe per agent, reused
|
|
19
|
+
* briefly, so a poll that reads readiness and the account list does not pay for the probe twice. A failed probe reads
|
|
20
|
+
* as no detection: the caller decides what an unknown answer means. */
|
|
21
|
+
export declare const detection: (request: GatewayTransport['request'], agentId: string, detect?: Detector, o?: {
|
|
22
|
+
fresh?: boolean;
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
}) => Promise<Detection>;
|
|
17
26
|
/** Native Claude login stays in Claude Code. The pin's profile status does not include this synthetic auth;
|
|
18
|
-
* setup detection asks the CLI itself, without handing credentials to the kit.
|
|
19
|
-
|
|
27
|
+
* setup detection asks the CLI itself, without handing credentials to the kit. The detection and the pin's own status
|
|
28
|
+
* are independent reads, so they run together rather than one after the other. */
|
|
29
|
+
export declare function authStatus(request: GatewayTransport['request'], agentId: string, refresh?: boolean, native?: boolean, detect?: Detector): Promise<AuthStatus>;
|
|
20
30
|
/** Admission snapshots, never credentials: per kit/agent/native route, at most 30s and never past reported expiry.
|
|
21
31
|
* Witness only app-owned file metadata (absence included); changed/unreadable state and auth/lifecycle mutations recheck.
|
|
22
32
|
* Remote revocation still comes from the real run's signed-out error (a local status read cannot prove it either).
|
|
23
33
|
*/
|
|
24
|
-
export declare function createAuthStatus(root: string, request: GatewayTransport['request'], codex?: () => Promise<boolean
|
|
34
|
+
export declare function createAuthStatus(root: string, request: GatewayTransport['request'], codex?: () => Promise<boolean>, detect?: Detector): {
|
|
25
35
|
read: (agentId: string, refresh?: boolean, native?: boolean, reuse?: boolean) => Promise<AuthStatus>;
|
|
26
36
|
invalidate: () => void;
|
|
27
37
|
};
|
package/dist/auth-status.js
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
import { statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { signedInProviders, USABLE } from './runs.js';
|
|
4
|
+
/** The engine's setup detection, through the shared detector when the caller has one: one probe per agent, reused
|
|
5
|
+
* briefly, so a poll that reads readiness and the account list does not pay for the probe twice. A failed probe reads
|
|
6
|
+
* as no detection: the caller decides what an unknown answer means. */
|
|
7
|
+
export const detection = async (request, agentId, detect, o) => {
|
|
8
|
+
if (detect)
|
|
9
|
+
return detect.detect(agentId, { fresh: o?.fresh, timeoutMs: o?.timeoutMs, ...(o?.signal ? { signal: o.signal } : {}) });
|
|
10
|
+
return await request('openclaw.setup.detect', { agentId }, { timeoutMs: o?.timeoutMs ?? 20_000, ...(o?.signal ? { signal: o.signal } : {}) });
|
|
11
|
+
};
|
|
4
12
|
/** Native Claude login stays in Claude Code. The pin's profile status does not include this synthetic auth;
|
|
5
|
-
* setup detection asks the CLI itself, without handing credentials to the kit.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
13
|
+
* setup detection asks the CLI itself, without handing credentials to the kit. The detection and the pin's own status
|
|
14
|
+
* are independent reads, so they run together rather than one after the other. */
|
|
15
|
+
export async function authStatus(request, agentId, refresh = false, native = false, detect) {
|
|
16
|
+
const [status, detected] = await Promise.all([
|
|
17
|
+
request('models.authStatus', { agentId, ...(refresh ? { refresh: true } : {}) }, { timeoutMs: 20_000 }),
|
|
18
|
+
native ? detection(request, agentId, detect).catch(() => ({})) : undefined,
|
|
19
|
+
]);
|
|
20
|
+
return native ? nativeStatus(status, detected) : status;
|
|
9
21
|
}
|
|
10
|
-
|
|
11
|
-
const detected = await request('openclaw.setup.detect', { agentId }, { timeoutMs: 20_000 }).catch(() => ({}));
|
|
22
|
+
function nativeStatus(status, detected) {
|
|
12
23
|
const rows = (status.providers ?? []).filter((row) => (typeof row === 'string' ? row : row.provider) !== 'claude-cli');
|
|
13
|
-
if (detected
|
|
24
|
+
if (detected?.candidates?.some((candidate) => candidate.kind === 'claude-cli' && candidate.credentials === true)) {
|
|
14
25
|
return { providers: [...rows, { provider: 'claude-cli', status: 'ok' }] };
|
|
15
26
|
}
|
|
16
27
|
return { ...status, providers: rows };
|
|
@@ -19,8 +30,9 @@ async function nativeStatus(request, agentId, status) {
|
|
|
19
30
|
* Witness only app-owned file metadata (absence included); changed/unreadable state and auth/lifecycle mutations recheck.
|
|
20
31
|
* Remote revocation still comes from the real run's signed-out error (a local status read cannot prove it either).
|
|
21
32
|
*/
|
|
22
|
-
export function createAuthStatus(root, request, codex) {
|
|
33
|
+
export function createAuthStatus(root, request, codex, detect) {
|
|
23
34
|
const snapshots = new Map();
|
|
35
|
+
const witnesses = new Map();
|
|
24
36
|
let generation = 0;
|
|
25
37
|
const invalidate = () => { generation++; snapshots.clear(); };
|
|
26
38
|
const witness = (agentId) => {
|
|
@@ -54,13 +66,24 @@ export function createAuthStatus(root, request, codex) {
|
|
|
54
66
|
return cached.status;
|
|
55
67
|
snapshots.delete(key);
|
|
56
68
|
snapshots.delete(`${agentId}:false`);
|
|
57
|
-
|
|
69
|
+
// The detection answers about the same state this witness covers (the engine's own logins), so a changed witness
|
|
70
|
+
// drops it: the next reader asks the engine again instead of reading an earlier answer.
|
|
71
|
+
const seen = witnesses.get(agentId);
|
|
72
|
+
if (before !== undefined)
|
|
73
|
+
witnesses.set(agentId, before);
|
|
74
|
+
if (before !== undefined && seen !== undefined && seen !== before)
|
|
75
|
+
detect?.invalidate(agentId);
|
|
76
|
+
// The pin's own status and the setup detection are independent reads: they run together, not one after the other.
|
|
77
|
+
const [profiles, detected] = await Promise.all([
|
|
78
|
+
authStatus(request, agentId, refresh),
|
|
79
|
+
native ? detection(request, agentId, detect).catch(() => undefined) : undefined,
|
|
80
|
+
]);
|
|
58
81
|
if (codex) {
|
|
59
82
|
profiles.providers = (profiles.providers ?? []).filter(row => (typeof row === 'string' ? row : row.provider) !== 'openai-codex');
|
|
60
83
|
if (await codex())
|
|
61
84
|
profiles.providers.push({ provider: 'openai-codex', status: 'ok' });
|
|
62
85
|
}
|
|
63
|
-
const status = native ?
|
|
86
|
+
const status = native ? nativeStatus(profiles, detected) : profiles;
|
|
64
87
|
if (before !== undefined && epoch === generation && witness(agentId) === before) {
|
|
65
88
|
for (const [route, answer] of (native ? [[false, profiles], [true, status]] : [[false, profiles]])) {
|
|
66
89
|
const usable = signedInProviders(answer);
|
package/dist/bridge.js
CHANGED
|
@@ -3,7 +3,6 @@ import { randomBytes } from 'node:crypto';
|
|
|
3
3
|
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { createServer } from 'node:net';
|
|
5
5
|
import { rm } from 'node:fs/promises';
|
|
6
|
-
import { provider } from '@byokit/accounts';
|
|
7
6
|
import { words } from './words.js';
|
|
8
7
|
import { HelperError } from './helpers.js';
|
|
9
8
|
/** Newline-framed JSON, one request per connection; anything larger is not a gate request. */
|
|
@@ -40,8 +39,9 @@ export function writePlugin(dir, o) {
|
|
|
40
39
|
name: 'BYOKit bridge',
|
|
41
40
|
activation: { onStartup: true },
|
|
42
41
|
cliBackends: ['openai-codex'],
|
|
43
|
-
// The
|
|
44
|
-
|
|
42
|
+
// The engine lists a plugin route's rows only from here. Named exception: the Codex CLI's own list (its app-server
|
|
43
|
+
// `model/list`) is not read yet, so the route names these two ids; a recorded real `model/list` reply lifts it.
|
|
44
|
+
modelCatalog: { providers: { 'openai-codex': { models: ['gpt-6-sol', 'gpt-6-luna'].map((id) => ({ id, name: id })) } }, discovery: { 'openai-codex': 'static' } },
|
|
45
45
|
contracts: { tools: o.tools.map((t) => t.name), ...(o.browser ? { agentToolResultMiddleware: ['openclaw', 'codex'] } : {}) },
|
|
46
46
|
configSchema: { type: 'object', additionalProperties: false },
|
|
47
47
|
};
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export declare const ENGINE_VERSION = "2026.8.35";
|
|
2
2
|
export declare const PROTOCOL_VERSION = 4;
|
|
3
|
-
export declare const LIVE_CATALOGS: string[];
|
|
4
3
|
export declare const REGISTERED_CATALOGS: string[];
|
|
5
|
-
export declare const
|
|
4
|
+
export declare const LIVE_LIST_ADDS: string[];
|
|
6
5
|
export declare const OPERATOR_SCOPES: readonly ['operator.read', 'operator.write', 'operator.admin', 'operator.approvals', 'operator.questions', 'operator.pairing', 'operator.talk'];
|
package/dist/constants.js
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
// `needs-update`, never silently run.
|
|
3
3
|
export const ENGINE_VERSION = '2026.8.35';
|
|
4
4
|
export const PROTOCOL_VERSION = 4;
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
// Live catalogs the kit registers whole. The engine canonicalizes Google preview ids on config write, so OpenRouter's
|
|
9
|
-
// list collides with itself (`google/x-preview` and `google/x`); an OpenRouter add registers only the chosen model.
|
|
5
|
+
// Live catalogs (5.6) the kit registers whole; every other key route registers its chosen model alone. The engine
|
|
6
|
+
// canonicalizes Google preview ids on config write, so the rows are deduped by the id it keeps (`engineModelId`)
|
|
7
|
+
// before they are written.
|
|
10
8
|
export const REGISTERED_CATALOGS = ['opencode-go', 'minimax'];
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
9
|
+
// The providers whose key add registers the provider's live list beside the chosen model, so the member keeps every
|
|
10
|
+
// model the vendor lists to pick from (the chosen model id sits beside the provider's own default, `openrouter/auto`).
|
|
11
|
+
// The engine's catalog stops at a registered list, so an add that registered the chosen model alone would leave the
|
|
12
|
+
// member with that one row and nothing else to pick.
|
|
13
|
+
export const LIVE_LIST_ADDS = ['openrouter'];
|
|
14
14
|
export const OPERATOR_SCOPES = [
|
|
15
15
|
'operator.read',
|
|
16
16
|
'operator.write',
|
package/dist/detect.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { GatewayTransport } from './types.ts';
|
|
2
|
+
export type Detection = {
|
|
3
|
+
candidates?: {
|
|
4
|
+
kind?: string;
|
|
5
|
+
credentials?: boolean;
|
|
6
|
+
}[];
|
|
7
|
+
};
|
|
8
|
+
export type DetectOptions = {
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
fresh?: boolean;
|
|
12
|
+
};
|
|
13
|
+
export type Detector = {
|
|
14
|
+
/** The engine's setup detection for one agent. `fresh` skips the cache and the in-flight probe: a sign-in that
|
|
15
|
+
* verifies what the person just did must read the engine, not an earlier answer. */
|
|
16
|
+
detect(agentId: string, o?: DetectOptions): Promise<Detection>;
|
|
17
|
+
/** Drops the cached answer (one agent, or every agent). */
|
|
18
|
+
invalidate(agentId?: string): void;
|
|
19
|
+
};
|
|
20
|
+
/** How long a detection answer is reused. The answer is a CLI probe the kit cannot witness (the login lives in the
|
|
21
|
+
* person's own home), so the window bounds how long an outside sign-in stays unseen; every sign-in, sign-out and
|
|
22
|
+
* account change the kit sees drops it at once. */
|
|
23
|
+
export declare const DETECT_TTL_MS = 10000;
|
|
24
|
+
/**
|
|
25
|
+
* The shared detection. One probe per agent at a time, its answer reused briefly, a failure never cached (the next
|
|
26
|
+
* reader tries again) and a fresh read never served from the cache.
|
|
27
|
+
*/
|
|
28
|
+
export declare function createDetect(request: GatewayTransport['request'], o?: {
|
|
29
|
+
ttlMs?: number;
|
|
30
|
+
}): Detector;
|
package/dist/detect.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** How long a detection answer is reused. The answer is a CLI probe the kit cannot witness (the login lives in the
|
|
2
|
+
* person's own home), so the window bounds how long an outside sign-in stays unseen; every sign-in, sign-out and
|
|
3
|
+
* account change the kit sees drops it at once. */
|
|
4
|
+
export const DETECT_TTL_MS = 10_000;
|
|
5
|
+
/**
|
|
6
|
+
* The shared detection. One probe per agent at a time, its answer reused briefly, a failure never cached (the next
|
|
7
|
+
* reader tries again) and a fresh read never served from the cache.
|
|
8
|
+
*/
|
|
9
|
+
export function createDetect(request, o) {
|
|
10
|
+
const ttl = o?.ttlMs ?? DETECT_TTL_MS;
|
|
11
|
+
const cache = new Map();
|
|
12
|
+
const inflight = new Map();
|
|
13
|
+
const detect = (agentId, options = {}) => {
|
|
14
|
+
const hit = cache.get(agentId);
|
|
15
|
+
if (!options.fresh && hit && hit.until > Date.now())
|
|
16
|
+
return Promise.resolve(hit.value);
|
|
17
|
+
const running = options.fresh ? undefined : inflight.get(agentId);
|
|
18
|
+
if (running)
|
|
19
|
+
return running;
|
|
20
|
+
const call = request('openclaw.setup.detect', { agentId }, { timeoutMs: options.timeoutMs ?? 20_000, ...(options.signal ? { signal: options.signal } : {}) })
|
|
21
|
+
.then((value) => { cache.set(agentId, { until: Date.now() + ttl, value }); return value; })
|
|
22
|
+
.finally(() => { if (inflight.get(agentId) === call)
|
|
23
|
+
inflight.delete(agentId); });
|
|
24
|
+
inflight.set(agentId, call);
|
|
25
|
+
return call;
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
detect,
|
|
29
|
+
invalidate: (agentId) => {
|
|
30
|
+
if (agentId === undefined) {
|
|
31
|
+
cache.clear();
|
|
32
|
+
inflight.clear();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
cache.delete(agentId);
|
|
36
|
+
inflight.delete(agentId);
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
package/dist/keys.js
CHANGED
|
@@ -4,8 +4,8 @@ import { openCodeWorkspaces } from '@byokit/accounts/opencode-go';
|
|
|
4
4
|
import { activate } from './activation.js';
|
|
5
5
|
import { keyAgentId, MEMBER_ID, KEY_PREFIX, untilListed } from './members.js';
|
|
6
6
|
import { providerOf } from './pick.js';
|
|
7
|
-
import { kitId, modelsOf } from './accounts.js';
|
|
8
|
-
import {
|
|
7
|
+
import { kitId, modelsOf, engineModelId } from './accounts.js';
|
|
8
|
+
import { LIVE_LIST_ADDS } from './constants.js';
|
|
9
9
|
export function createKeys(ctx) {
|
|
10
10
|
// Serialize key replacement and key runs, so an in-flight run never sees a half-installed replacement.
|
|
11
11
|
const tails = new Map();
|
|
@@ -58,12 +58,14 @@ export function createKeys(ctx) {
|
|
|
58
58
|
return ['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'EAI_AGAIN', 'ETIMEDOUT', 'CLIENT_TIMEOUT', 'NETWORK_ERROR'].includes(e?.code ?? '')
|
|
59
59
|
|| e?.name === 'TimeoutError' ? 'network' : 'engine';
|
|
60
60
|
};
|
|
61
|
-
// The
|
|
62
|
-
|
|
63
|
-
? id.slice(0, -'-preview'.length) : id;
|
|
61
|
+
// The kit's name for a model: the provider and the row id the engine keeps (a model reference already carries its
|
|
62
|
+
// provider, so the row id is never prefixed twice).
|
|
64
63
|
const canonicalModel = (model) => {
|
|
65
64
|
const at = model.indexOf('/');
|
|
66
|
-
|
|
65
|
+
if (at < 0)
|
|
66
|
+
return model;
|
|
67
|
+
const provider = model.slice(0, at);
|
|
68
|
+
return kitId(provider, engineModelId(provider, model.slice(at + 1)));
|
|
67
69
|
};
|
|
68
70
|
// The live catalog's rows, as the engine registers them (provider-local ids, deduped by the id it keeps).
|
|
69
71
|
const catalog = async (member, provider) => {
|
|
@@ -74,7 +76,7 @@ export function createKeys(ctx) {
|
|
|
74
76
|
throw new Error('Model catalog is unavailable.');
|
|
75
77
|
return [];
|
|
76
78
|
}
|
|
77
|
-
const rows = (answer.models ?? []).map((m) => ({ ...m, id:
|
|
79
|
+
const rows = (answer.models ?? []).map((m) => ({ ...m, id: engineModelId(provider, String(m.id)) }));
|
|
78
80
|
return [...new Map(rows.map((m) => [String(m.id), m])).values()];
|
|
79
81
|
};
|
|
80
82
|
// The key route a choice names: its own, or the only key route of the provider whose other sign-in it names, billed
|
|
@@ -96,7 +98,7 @@ export function createKeys(ctx) {
|
|
|
96
98
|
// The vendor's list can lag a model that runs: an id it lacks rides the route's own models (the richest row's api,
|
|
97
99
|
// base URL and context, every input modality any row takes), unpriced. Nothing is added to an empty list.
|
|
98
100
|
const unlisted = (provider, rows, model) => {
|
|
99
|
-
if (!rows.length || rows.some((m) => kitId(provider, String(m.id)) === model))
|
|
101
|
+
if (!rows.length || !model.startsWith(`${provider}/`) || rows.some((m) => kitId(provider, String(m.id)) === model))
|
|
100
102
|
return [];
|
|
101
103
|
const base = rows.reduce((a, b) => (Number(b.contextWindow) || 0) > (Number(a.contextWindow) || 0) ? b : a);
|
|
102
104
|
const input = ['text', 'image'].filter((i) => rows.some((m) => Array.isArray(m.input) && m.input.includes(i)));
|
|
@@ -108,7 +110,7 @@ export function createKeys(ctx) {
|
|
|
108
110
|
if (!check(member))
|
|
109
111
|
throw new Error('Choose your own member.');
|
|
110
112
|
const route = keyRoute(o.authChoice);
|
|
111
|
-
if (route.readiness !== 'ready'
|
|
113
|
+
if (route.readiness !== 'ready')
|
|
112
114
|
return [];
|
|
113
115
|
try {
|
|
114
116
|
return listed(route.provider, await catalog(member, route.provider));
|
|
@@ -138,7 +140,7 @@ export function createKeys(ctx) {
|
|
|
138
140
|
if (!o.apiKey.trim())
|
|
139
141
|
return failure('invalid', 'empty');
|
|
140
142
|
const route = keyRoute(o.authChoice);
|
|
141
|
-
if (route.readiness !== 'ready'
|
|
143
|
+
if (route.readiness !== 'ready')
|
|
142
144
|
return failure('not_included', 'model');
|
|
143
145
|
const model = o.model === undefined ? undefined : canonicalModel(o.model);
|
|
144
146
|
// A workspace names a Go login's own workspace only; another route takes no header, and a
|
|
@@ -227,7 +229,7 @@ export function createKeys(ctx) {
|
|
|
227
229
|
if (result.late)
|
|
228
230
|
return abandon(failure('retry', 'busy'));
|
|
229
231
|
const rows = model === undefined ? [] : await catalog(member, route.provider);
|
|
230
|
-
const lagging = model !== undefined
|
|
232
|
+
const lagging = model !== undefined ? unlisted(route.provider, rows, model) : [];
|
|
231
233
|
rows.push(...lagging);
|
|
232
234
|
if (model !== undefined && !listed(route.provider, rows).some((m) => m.id === model && m.available))
|
|
233
235
|
return abandon(failure('not_included', 'model'));
|
|
@@ -240,10 +242,16 @@ export function createKeys(ctx) {
|
|
|
240
242
|
}
|
|
241
243
|
if (ac.signal.aborted)
|
|
242
244
|
return abandon(cancelled());
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
+
// The provider's live list, for a provider whose add registers it (the engine's catalog stops at a
|
|
246
|
+
// registered list, so the chosen model alone would leave the member with that one row to pick): the
|
|
247
|
+
// chosen model id sits beside the provider's own default, `openrouter/auto`.
|
|
248
|
+
if (model !== undefined && LIVE_LIST_ADDS.includes(route.provider))
|
|
249
|
+
await ctx.registerModels(route.provider, rows).catch(() => { });
|
|
250
|
+
// The chosen model is registered on its own, whatever the provider's catalog is: a list the engine
|
|
251
|
+
// refuses (two rows it would keep under one id) must never cost the member's own choice.
|
|
252
|
+
if (model !== undefined)
|
|
245
253
|
await ctx.registerModels(route.provider, rows.filter((m) => kitId(route.provider, String(m.id)) === model)).catch(() => { });
|
|
246
|
-
if (model !== undefined &&
|
|
254
|
+
if (model !== undefined && !(await registered(route.provider, model)))
|
|
247
255
|
return abandon(failure('retry', 'model'));
|
|
248
256
|
if (!(await control(member, 'seal', undefined, model)).ok)
|
|
249
257
|
return abandon(failure('retry', 'engine'));
|
package/dist/kit.d.ts
CHANGED
|
@@ -73,6 +73,7 @@ export declare class OpenClawKit {
|
|
|
73
73
|
private readonly delegations;
|
|
74
74
|
private readonly keys;
|
|
75
75
|
private readonly admission;
|
|
76
|
+
private readonly detect;
|
|
76
77
|
private readonly approvalsCtl;
|
|
77
78
|
private listeners;
|
|
78
79
|
private off;
|
|
@@ -88,6 +89,7 @@ export declare class OpenClawKit {
|
|
|
88
89
|
private engineExit?;
|
|
89
90
|
private swept;
|
|
90
91
|
private catalogs;
|
|
92
|
+
private readonly registered;
|
|
91
93
|
browser?: BrowserHost;
|
|
92
94
|
private browserHost?;
|
|
93
95
|
private readonly brokers;
|
package/dist/kit.js
CHANGED
|
@@ -15,12 +15,13 @@ import { AuthStoreSealSizeError } from './auth-store.js';
|
|
|
15
15
|
import { gatewayTransport } from './transport.js';
|
|
16
16
|
import { createMembers } from './members.js';
|
|
17
17
|
import { createHelpers, HELPER_TAIL, HelperError } from './helpers.js';
|
|
18
|
-
import { createAccounts, memberOfAccount } from './accounts.js';
|
|
18
|
+
import { createAccounts, memberOfAccount, modelsOf, engineModelId } from './accounts.js';
|
|
19
19
|
import { createLocks } from './locks.js';
|
|
20
20
|
import { keyAgentId, KEY_PREFIX, MEMBER_ID } from './members.js';
|
|
21
21
|
import { confirmRetainedLogin as confirmLogin, migrateRetainedLogin as migrateLogin } from './migrate.js';
|
|
22
22
|
import { createRuns, RunReplay, signedInProviders } from './runs.js';
|
|
23
23
|
import { authStatus, createAuthStatus } from './auth-status.js';
|
|
24
|
+
import { createDetect } from './detect.js';
|
|
24
25
|
import { outputSchema } from './output.js';
|
|
25
26
|
import { words } from './words.js';
|
|
26
27
|
import { createKeys } from './keys.js';
|
|
@@ -457,6 +458,9 @@ export class OpenClawKit {
|
|
|
457
458
|
delegations;
|
|
458
459
|
keys;
|
|
459
460
|
admission;
|
|
461
|
+
// One setup detection for every reader (readiness, the account list, a sign-in): the engine's probe asks the
|
|
462
|
+
// installed CLI itself and takes seconds, so a poll that reads both pays for it once, not once per reader.
|
|
463
|
+
detect;
|
|
460
464
|
approvalsCtl;
|
|
461
465
|
listeners = new Set();
|
|
462
466
|
off = [];
|
|
@@ -472,6 +476,8 @@ export class OpenClawKit {
|
|
|
472
476
|
engineExit;
|
|
473
477
|
swept = false;
|
|
474
478
|
catalogs = Promise.resolve();
|
|
479
|
+
// The catalog rows the kit registered, per provider: the provider's live list, as the engine can run it.
|
|
480
|
+
registered = new Map();
|
|
475
481
|
browser;
|
|
476
482
|
browserHost;
|
|
477
483
|
brokers = new Map();
|
|
@@ -679,6 +685,9 @@ export class OpenClawKit {
|
|
|
679
685
|
throw new Error(`invalid tool name: ${t.name}`);
|
|
680
686
|
}
|
|
681
687
|
this.o = { ...o, stateDir: canonicalStateDir(o.stateDir) };
|
|
688
|
+
// The shared detection, above the request path: the witness check and the engine's probe run only when a probe is
|
|
689
|
+
// really made, so a cached answer costs nothing.
|
|
690
|
+
this.detect = createDetect((method, params, options) => this.request()(method, params, options));
|
|
682
691
|
this.engine = new Engine({ ...o, pluginId: o.plugin?.id ?? 'byokit', tools: [...o.tools ?? [], ...(o.browser ? [{
|
|
683
692
|
name: 'request_sign_in', description: 'Ask the person to sign in to the current browser site.',
|
|
684
693
|
parameters: { type: 'object', additionalProperties: false, properties: {
|
|
@@ -688,7 +697,7 @@ export class OpenClawKit {
|
|
|
688
697
|
...(o.browser ? { browserConfig: () => this.browserConfig() } : {}),
|
|
689
698
|
gateBuiltins: o.gateBuiltins !== false, spawnEngine: o.spawnEngine !== false,
|
|
690
699
|
onState: (s) => this.setState(s), onExit: () => this.closed('engine exited') });
|
|
691
|
-
this.admission = createAuthStatus(this.engine.root, (method, params, options) => this.request()(method, params, options), () => this.engine.installedCodexSignedIn());
|
|
700
|
+
this.admission = createAuthStatus(this.engine.root, (method, params, options) => this.request()(method, params, options), () => this.engine.installedCodexSignedIn(), this.detect);
|
|
692
701
|
this.keys = createKeys({ root: this.engine.root,
|
|
693
702
|
prepareOwner: (agentId) => this.prepareKeyOwner(agentId),
|
|
694
703
|
indexLane: (member, route) => this.indexKeyLane(member, route),
|
|
@@ -760,8 +769,10 @@ export class OpenClawKit {
|
|
|
760
769
|
slot.bridge = this.bridge;
|
|
761
770
|
}
|
|
762
771
|
setState(s) {
|
|
763
|
-
if (s.phase !== 'ready')
|
|
772
|
+
if (s.phase !== 'ready') {
|
|
764
773
|
this.admission?.invalidate();
|
|
774
|
+
this.detect?.invalidate();
|
|
775
|
+
}
|
|
765
776
|
this.current = { ...s, ...(this.engine.patchSet !== undefined ? { patchSet: this.engine.patchSet } : {}) };
|
|
766
777
|
this.o.onState?.(this.current);
|
|
767
778
|
for (const fn of this.phaseListeners)
|
|
@@ -850,7 +861,8 @@ export class OpenClawKit {
|
|
|
850
861
|
this.accountsCtl = createAccounts({ request: (method, params, o) => this.request()(method, params, o), root: this.engine.root,
|
|
851
862
|
ensure: (member) => this.ensureMember(member), locks: this.locks,
|
|
852
863
|
signIn: (agentId, o, on, fresh) => startSignIn({ ...this.signInCtx(), ensure: async () => ({ agentId }), fresh }, agentId, o, on),
|
|
853
|
-
routes: this.routeMap, log: this.o.log, reseal: () => this.engine.authStore.reseal()
|
|
864
|
+
routes: this.routeMap, log: this.o.log, reseal: () => this.engine.authStore.reseal(), detect: this.detect,
|
|
865
|
+
catalog: (provider) => this.registered.get(provider) ?? [] });
|
|
854
866
|
this.helpersCtl = createHelpers({ root: this.engine.root, list: (member) => this.accounts(member) });
|
|
855
867
|
// The fail-closed tool bridge feeds the kit's single Approval surface (5.9, O5).
|
|
856
868
|
await Promise.race([this.bridge.start(), exit]);
|
|
@@ -1038,11 +1050,23 @@ export class OpenClawKit {
|
|
|
1038
1050
|
await this.start();
|
|
1039
1051
|
return this.request()(method, params, options);
|
|
1040
1052
|
}
|
|
1053
|
+
// An account change (a sealed, cleared or adopted account) moves the detection too: a Claude Code login the
|
|
1054
|
+
// person just made is an account the kit now knows about. A read of the accounts changes nothing.
|
|
1055
|
+
if (method === 'byokit.accounts') {
|
|
1056
|
+
const action = params?.action;
|
|
1057
|
+
if (action !== 'list' && action !== 'ready')
|
|
1058
|
+
this.detect.invalidate();
|
|
1059
|
+
}
|
|
1041
1060
|
const changesAuth = /^(models\.authLogout|openclaw\.setup\.(auth\.start|activate)|wizard\.(next|cancel)|config\.(apply|patch|set)|agents\.(create|update|delete)|secrets\.(reload|store\.(set|delete))|byokit\.keys)$/.test(method);
|
|
1042
1061
|
if (!changesAuth || (method === 'byokit.keys' && params?.action === 'ready'))
|
|
1043
1062
|
return transport.request(method, params, options);
|
|
1044
1063
|
this.admission.invalidate();
|
|
1045
|
-
|
|
1064
|
+
// A sign-in, a sign-out or a key change moves the detection too: its answer is about this engine's logins.
|
|
1065
|
+
this.detect.invalidate();
|
|
1066
|
+
return Promise.resolve().then(() => transport.request(method, params, options)).finally(() => {
|
|
1067
|
+
this.admission.invalidate();
|
|
1068
|
+
this.detect.invalidate();
|
|
1069
|
+
});
|
|
1046
1070
|
};
|
|
1047
1071
|
}
|
|
1048
1072
|
call(method, params, o) {
|
|
@@ -1117,6 +1141,7 @@ export class OpenClawKit {
|
|
|
1117
1141
|
return { request: (method, params, o) => this.request()(method, params, o),
|
|
1118
1142
|
// Public readiness/sign-in reads stay live; their fresh result can prepare the next run's admission.
|
|
1119
1143
|
authStatus: (agentId, refresh, native) => this.admission.read(agentId, refresh, native, false),
|
|
1144
|
+
detect: this.detect,
|
|
1120
1145
|
ensure: (member) => this.ensureMember(member), callbackPort: this.o.callbackPort ?? 1455,
|
|
1121
1146
|
nativeClaudeLogin: () => this.engine.nativeClaudeLogin(),
|
|
1122
1147
|
resetNativeClaudeLogin: () => this.engine.resetNativeClaudeLogin(),
|
|
@@ -1361,14 +1386,19 @@ export class OpenClawKit {
|
|
|
1361
1386
|
this.o.log?.(`catalog sync ${provider}: failed`); });
|
|
1362
1387
|
}
|
|
1363
1388
|
}
|
|
1364
|
-
// Merges catalog rows into the registered list by id
|
|
1389
|
+
// Merges catalog rows into the registered list by the id the engine keeps (it rewrites a Google preview id when it
|
|
1390
|
+
// writes the config, so two rows it would keep under one id are one row and never a refused write); an unchanged
|
|
1391
|
+
// list is never rewritten.
|
|
1365
1392
|
async registerModels(provider, rows) {
|
|
1366
1393
|
const { config } = await this.request()('config.get');
|
|
1367
1394
|
const registered = config?.models?.providers?.[provider]?.models ?? [];
|
|
1368
|
-
const byId = new Map(registered.map((m) =>
|
|
1395
|
+
const byId = new Map(registered.map((m) => { const id = engineModelId(provider, m.id); return [id, { ...m, id }]; }));
|
|
1369
1396
|
for (const m of rows)
|
|
1370
|
-
byId.set(m.id, m);
|
|
1397
|
+
byId.set(engineModelId(provider, m.id), { ...m, id: engineModelId(provider, m.id) });
|
|
1371
1398
|
const models = [...byId.values()];
|
|
1399
|
+
// What the kit registered is what the engine can run for that provider: the account list joins it, so a model
|
|
1400
|
+
// the live catalog offers is pickable even while the engine's own list still lags the registration.
|
|
1401
|
+
this.registered.set(provider, modelsOf(provider, { models: models.map((m) => ({ ...m, provider })) }));
|
|
1372
1402
|
if (!isDeepStrictEqual(models, registered))
|
|
1373
1403
|
await this.patchConfig({ models: { providers: { [provider]: { models } } } });
|
|
1374
1404
|
}
|
package/dist/signin.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { remoteCliHost } from '@byokit/accounts/cli';
|
|
2
2
|
import { authStatus } from './auth-status.ts';
|
|
3
|
+
import type { Detector } from './detect.ts';
|
|
3
4
|
import type { CliLoginCause, GatewayTransport, Member, SignInOptions, SignInView } from './types.ts';
|
|
4
5
|
export type SignInCtx = {
|
|
5
6
|
request: GatewayTransport['request'];
|
|
6
7
|
authStatus?: (agentId: string, refresh: boolean, native: boolean) => ReturnType<typeof authStatus>;
|
|
8
|
+
detect?: Detector;
|
|
7
9
|
ensure(member: Member): Promise<{
|
|
8
10
|
agentId: string;
|
|
9
11
|
}>;
|
package/dist/signin.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { createServer } from 'node:http';
|
|
5
5
|
import { activate, LATE_RESTART } from './activation.js';
|
|
6
|
-
import { authStatus } from './auth-status.js';
|
|
6
|
+
import { authStatus, detection } from './auth-status.js';
|
|
7
7
|
import { routes } from './routes.js';
|
|
8
8
|
import { signedInProviders } from './runs.js';
|
|
9
9
|
import { words } from './words.js';
|
|
@@ -134,6 +134,9 @@ export function signIn(ctx, member, o, on) {
|
|
|
134
134
|
returned = undefined;
|
|
135
135
|
pasteIn = undefined;
|
|
136
136
|
await closeCallback();
|
|
137
|
+
// A drive moves the login state, so the shared detection is dropped: the next reader asks the engine.
|
|
138
|
+
if (agentId !== undefined)
|
|
139
|
+
ctx.detect?.invalidate(agentId);
|
|
137
140
|
if (sensitive && view.state === 'failed' && view.why === 'failed')
|
|
138
141
|
view = { ...view, error: secretFailure };
|
|
139
142
|
on(view);
|
|
@@ -222,6 +225,10 @@ export function signIn(ctx, member, o, on) {
|
|
|
222
225
|
return { state: 'failed', via, why: 'busy' };
|
|
223
226
|
}
|
|
224
227
|
({ agentId } = await ctx.ensure(member));
|
|
228
|
+
// A drive moves the login state, so the shared detection is dropped before it reads one: the drive's own decision
|
|
229
|
+
// (is a Claude Code login there?) must be the engine's answer now, not an earlier one.
|
|
230
|
+
if (agentId !== undefined)
|
|
231
|
+
ctx.detect?.invalidate(agentId);
|
|
225
232
|
if (reconfirm && ctx.onDisconnect) {
|
|
226
233
|
before = profiles(await authStatus(ctx.request, agentId).catch(() => undefined), false);
|
|
227
234
|
if (signal.aborted)
|
|
@@ -235,8 +242,11 @@ export function signIn(ctx, member, o, on) {
|
|
|
235
242
|
settleStart();
|
|
236
243
|
let apiKey;
|
|
237
244
|
if (o.authChoice === 'anthropic-cli') {
|
|
245
|
+
// The drive reads the engine, never a reused answer: what it decides (is a Claude Code login there? did the
|
|
246
|
+
// one the person just made land?) must be the engine's answer now.
|
|
247
|
+
const probe = agentId;
|
|
238
248
|
const detected = async () => {
|
|
239
|
-
const value = await ctx.request
|
|
249
|
+
const value = await detection(ctx.request, probe, ctx.detect, { fresh: true, signal, timeoutMs: STATUS_MS });
|
|
240
250
|
return value.candidates?.some(candidate => candidate.kind === 'claude-cli' && candidate.credentials === true);
|
|
241
251
|
};
|
|
242
252
|
native = ctx.nativeClaudeLogin?.();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byokit/openclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.1",
|
|
4
4
|
"description": "The OpenClaw runtime kit: the pinned engine's full operator surface with plain-words helpers for members, sign-in, runs and approvals.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"prepack": "tsc -b && node ../../scripts/fix-words-dts.cjs"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@byokit/accounts": "0.
|
|
63
|
+
"@byokit/accounts": "0.30.0",
|
|
64
64
|
"@byokit/pair": "0.10.4",
|
|
65
65
|
"@byokit/relay": "0.8.2",
|
|
66
66
|
"@byokit/discover": "0.8.4",
|
package/plugin/catalog.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// byokit.catalog (5.6 live provider catalogs): the provider plugin's own live model list, with each model's api and
|
|
2
|
-
// baseUrl from its catalog data, so the kit can register models embedded runs
|
|
3
|
-
// catalog loader stops at a registered list; the provider's catalog hook does not.
|
|
2
|
+
// baseUrl from its catalog data, joined by the vendor's own model list, so the kit can register models embedded runs
|
|
3
|
+
// cannot resolve otherwise. The engine's catalog loader stops at a registered list; the provider's catalog hook does not.
|
|
4
4
|
export function registerCatalog(api) {
|
|
5
5
|
api.registerGatewayMethod('byokit.catalog', async ({ params, respond }) => {
|
|
6
6
|
try {
|
|
@@ -11,7 +11,8 @@ export function registerCatalog(api) {
|
|
|
11
11
|
const { resolveApiKeyForProvider } = await import('openclaw/plugin-sdk/provider-auth-runtime');
|
|
12
12
|
const config = getRuntimeConfig();
|
|
13
13
|
const hook = resolvePluginProviders({ config, env: process.env }).find((p) => p.id === provider)?.catalog;
|
|
14
|
-
if (!provider
|
|
14
|
+
if (!provider) return respond(true, { ok: false, reason: 'failed' });
|
|
15
|
+
if (typeof hook?.run !== 'function') return respond(true, { ok: true, models: [] }); // a provider with no catalog lists nothing
|
|
15
16
|
// The first agent holding the provider's key; every account of one provider sees the same catalog.
|
|
16
17
|
let unreadable = false;
|
|
17
18
|
for (const agentId of new Set([...Object.keys(config.agents?.entries ?? {}), 'main'])) {
|
|
@@ -38,13 +39,16 @@ export function registerCatalog(api) {
|
|
|
38
39
|
const models = [...live.models, ...(pinned?.models ?? []).filter((m) => !liveIds.has(m.id))]
|
|
39
40
|
.map(({ provider: _p, apiKey: _k, headers: _h, ...model }) =>
|
|
40
41
|
(endpoint.has(model.api) ? { ...model, baseUrl: endpoint.get(model.api) } : model));
|
|
41
|
-
//
|
|
42
|
-
// row's api, base URL and capabilities (
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
// A plugin catalog can be a fixed list (MiniMax's): the vendor's own `/models` lists newer ids, which join it with
|
|
43
|
+
// the richest row's api, base URL and capabilities (a vendor list carries ids only). Same rule as
|
|
44
|
+
// @byokit/accounts' live lists; another api, or a failed fetch, keeps the plugin's rows.
|
|
45
|
+
const base = models.reduce((a, b) => (b.contextWindow ?? 0) > (a.contextWindow ?? 0) ? b : a);
|
|
46
|
+
const openai = String(base.api).startsWith('openai-');
|
|
47
|
+
if (base.baseUrl && (openai || base.api === 'anthropic-messages')) {
|
|
45
48
|
const input = ['text', 'image'].filter((i) => models.some((m) => m.input?.includes(i)));
|
|
46
|
-
const
|
|
47
|
-
|
|
49
|
+
const bearer = openai || /^minimax/.test(provider);
|
|
50
|
+
const listed = await fetch(`${String(base.baseUrl).replace(/\/$/, '')}${openai ? '/models' : '/v1/models'}`, { signal: AbortSignal.timeout(10_000),
|
|
51
|
+
headers: bearer ? { authorization: `Bearer ${apiKey}`, 'anthropic-version': '2023-06-01' } : { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' } })
|
|
48
52
|
.then((r) => r.ok ? r.json() : null, () => null);
|
|
49
53
|
const have = new Set(models.map((m) => m.id));
|
|
50
54
|
for (const m of Array.isArray(listed?.data) ? listed.data : [])
|