@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 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 unknown model (`not_included`), or a live model the engine did not
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, an unknown chosen model and a probe reporting the route unavailable;
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
- Model listing and selection belong to the live-catalog routes, OpenCode Go and OpenRouter. An OpenRouter key is
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. Other key routes list no models, and a `model` on them returns
194
- `not_included`/`model` before activation.
195
- An unknown model returns `{ status: 'not_included', reason: 'model' }` and clears the replacement.
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
- For a live-catalog provider, a chosen model must also be in the engine's registered `models.providers[provider].models`
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.
@@ -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
- const claudeDetected = (agentId) => ctx.request('openclaw.setup.detect', { agentId }, { timeoutMs: 20_000 })
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);
@@ -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
- export declare function authStatus(request: GatewayTransport['request'], agentId: string, refresh?: boolean, native?: boolean): Promise<AuthStatus>;
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
  };
@@ -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
- export async function authStatus(request, agentId, refresh = false, native = false) {
7
- const status = await request('models.authStatus', { agentId, ...(refresh ? { refresh: true } : {}) }, { timeoutMs: 20_000 });
8
- return native ? nativeStatus(request, agentId, status) : status;
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
- async function nativeStatus(request, agentId, status) {
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.candidates?.some((candidate) => candidate.kind === 'claude-cli' && candidate.credentials === true)) {
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
- const profiles = await authStatus(request, agentId, refresh);
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 ? await nativeStatus(request, agentId, profiles) : profiles;
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 Codex CLI route lists the ChatGPT catalogue's models; the engine lists a plugin route's rows only from here.
44
- modelCatalog: { providers: { 'openai-codex': { models: [provider('chatgpt').models.strong, provider('chatgpt').models.fast].map((id) => ({ id, name: id })) } }, discovery: { 'openai-codex': 'static' } },
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
  };
@@ -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 OPEN_CATALOGS: string[];
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
- // Providers whose live catalog lists and checks a key's chosen model (5.6), through the catalog hook. OpenRouter is
6
- // pay-as-you-go: its key takes any model its own catalog lists, with no plan to include it.
7
- export const LIVE_CATALOGS = ['opencode-go', 'openrouter', 'minimax'];
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
- // Pay-as-you-go catalogs whose vendor list can lag a model that already runs: an id the list lacks is warned about, never
12
- // refused, and takes the capabilities of the route's own models.
13
- export const OPEN_CATALOGS = ['minimax'];
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',
@@ -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 { LIVE_CATALOGS, OPEN_CATALOGS, REGISTERED_CATALOGS } from './constants.js';
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 engine writes a Google preview id without its `-preview` suffix, so the kit names the id it keeps.
62
- const canonical = (id) => id.startsWith('google/') && id.endsWith('-preview')
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
- return at < 0 ? model : `${model.slice(0, at)}/${canonical(model.slice(at + 1))}`;
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: canonical(String(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' || !LIVE_CATALOGS.includes(route.provider))
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' || (o.model !== undefined && !LIVE_CATALOGS.includes(route.provider)))
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 && OPEN_CATALOGS.includes(route.provider) ? unlisted(route.provider, rows, model) : [];
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
- // A live catalog the kit does not register whole (OpenRouter) registers the chosen model alone.
244
- if (model !== undefined && (!REGISTERED_CATALOGS.includes(route.provider) || lagging.length))
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 && LIVE_CATALOGS.includes(route.provider) && !(await registered(route.provider, model)))
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
- return Promise.resolve().then(() => transport.request(method, params, options)).finally(() => this.admission.invalidate());
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; an unchanged list is never rewritten.
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) => [m.id, 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('openclaw.setup.detect', { agentId }, { timeoutMs: STATUS_MS, signal });
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.25.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.29.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 cannot resolve otherwise. The engine's
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 || typeof hook?.run !== 'function') return respond(true, { ok: false, reason: 'failed' });
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
- // MiniMax's engine catalog is a fixed list: its vendor `/models` lists newer ids, which join it with the richest
42
- // row's api, base URL and capabilities (the vendor lists ids only). A failed fetch keeps the engine's list.
43
- if (provider === 'minimax' && models.length) {
44
- const base = models.reduce((a, b) => (b.contextWindow ?? 0) > (a.contextWindow ?? 0) ? b : a);
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 listed = await fetch(`${String(base.baseUrl).replace(/\/$/, '')}/v1/models`,
47
- { headers: { authorization: `Bearer ${apiKey}`, 'anthropic-version': '2023-06-01' }, signal: AbortSignal.timeout(10_000) })
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 : [])