dsh-plugin-subscription-oauth 0.16.3 → 0.17.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.
Files changed (43) hide show
  1. package/README.dev.md +1 -1
  2. package/README.md +7 -6
  3. package/README.zh.dev.md +1 -1
  4. package/README.zh.md +7 -6
  5. package/lib/client/FastModeButton.d.ts +0 -1
  6. package/lib/client/FastModeButton.js +48 -11
  7. package/lib/client.js +32 -9
  8. package/lib/client.js.map +1 -1
  9. package/lib/index.js +21 -0
  10. package/lib/protocol.d.ts +9 -0
  11. package/lib/providers/antigravity.d.ts +34 -4
  12. package/lib/providers/antigravity.js +100 -28
  13. package/lib/providers/ark-provider.d.ts +10 -0
  14. package/lib/providers/ark-provider.js +16 -2
  15. package/lib/providers/ark.d.ts +104 -10
  16. package/lib/providers/ark.js +186 -48
  17. package/lib/providers/catalog.d.ts +92 -0
  18. package/lib/providers/catalog.js +191 -0
  19. package/lib/providers/common.d.ts +14 -0
  20. package/lib/providers/common.js +23 -0
  21. package/lib/providers/glm-provider.d.ts +10 -0
  22. package/lib/providers/glm-provider.js +16 -2
  23. package/lib/providers/glm.d.ts +100 -16
  24. package/lib/providers/glm.js +182 -85
  25. package/lib/providers/grok.d.ts +114 -27
  26. package/lib/providers/grok.js +330 -129
  27. package/lib/providers/kimi-provider.d.ts +10 -0
  28. package/lib/providers/kimi-provider.js +16 -4
  29. package/lib/providers/kimi.d.ts +122 -32
  30. package/lib/providers/kimi.js +243 -122
  31. package/lib/providers/mimo-provider.d.ts +14 -1
  32. package/lib/providers/mimo-provider.js +17 -6
  33. package/lib/providers/mimo.d.ts +164 -19
  34. package/lib/providers/mimo.js +318 -19
  35. package/lib/providers/model-cache.d.ts +57 -0
  36. package/lib/providers/model-cache.js +105 -0
  37. package/lib/providers/responses-stream.d.ts +8 -0
  38. package/lib/providers/responses-stream.js +21 -5
  39. package/lib/providers/workbuddy-provider.d.ts +17 -0
  40. package/lib/providers/workbuddy-provider.js +14 -6
  41. package/lib/providers/workbuddy.d.ts +140 -13
  42. package/lib/providers/workbuddy.js +501 -59
  43. package/package.json +1 -1
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The last model listing each route read from its own endpoint, kept on disk.
3
+ *
4
+ * A live listing is the authority on which models a subscription serves and what
5
+ * each one is: its display name, context window, input modalities and reasoning
6
+ * ladder. Asking for it on every picker query would spend a request per refresh and
7
+ * still show nothing while the endpoint is unreachable, so the answer is read once,
8
+ * cached here, and reused as the route's catalog until the next discovery.
9
+ *
10
+ * That cache replaces the model table this build used to ship by hand: the same
11
+ * curated list, except it is now the endpoint's own answer rather than a roster that
12
+ * goes stale between releases. A hardcoded table cannot learn that a model was
13
+ * released, renamed or retired; a cached live read can, and it is read from disk
14
+ * rather than the network on the path that matters.
15
+ *
16
+ * The endpoint's own payload is stored verbatim rather than a pre-digested catalog, so
17
+ * the route re-derives everything it publishes — picker rows, fast variants, context
18
+ * variants — from the same parse it uses on a fresh read. A later build that reads more
19
+ * of a payload therefore benefits from a cache written by an earlier one.
20
+ *
21
+ * @module dsh-plugin-subscription-oauth/providers/model-cache
22
+ */
23
+ /**
24
+ * Keep each route's last live listing on disk.
25
+ *
26
+ * Reads are tolerant: a missing, unreadable, unparsable or stale-version file yields
27
+ * no cache rather than an error, because a cache miss must only mean "ask the
28
+ * endpoint", never a broken picker. Writes are atomic, so a crash mid-write cannot
29
+ * leave a half-written document behind.
30
+ */
31
+ export declare class ModelCatalogCache {
32
+ private readonly file;
33
+ /**
34
+ * @param file - absolute path of the cache document.
35
+ */
36
+ constructor(file: string);
37
+ /**
38
+ * Whatever one route last cached.
39
+ *
40
+ * The payload is opaque here: a route writes what it needs to rebuild its catalog
41
+ * (Kimi stores the decoded rows of `/models`; a route that derives fast variants from
42
+ * a second listing stores both). It re-validates on read.
43
+ *
44
+ * @param provider - provider route key.
45
+ * @returns the cached payload, or undefined when there is no usable cache.
46
+ */
47
+ read(provider: string): Promise<unknown>;
48
+ /**
49
+ * Record what the endpoint just reported for one route.
50
+ *
51
+ * @param provider - provider route key.
52
+ * @param payload - what that route needs to rebuild its catalog.
53
+ */
54
+ write(provider: string, payload: unknown): Promise<void>;
55
+ /** Load the document, or undefined when it is absent or unusable. */
56
+ private load;
57
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * The last model listing each route read from its own endpoint, kept on disk.
3
+ *
4
+ * A live listing is the authority on which models a subscription serves and what
5
+ * each one is: its display name, context window, input modalities and reasoning
6
+ * ladder. Asking for it on every picker query would spend a request per refresh and
7
+ * still show nothing while the endpoint is unreachable, so the answer is read once,
8
+ * cached here, and reused as the route's catalog until the next discovery.
9
+ *
10
+ * That cache replaces the model table this build used to ship by hand: the same
11
+ * curated list, except it is now the endpoint's own answer rather than a roster that
12
+ * goes stale between releases. A hardcoded table cannot learn that a model was
13
+ * released, renamed or retired; a cached live read can, and it is read from disk
14
+ * rather than the network on the path that matters.
15
+ *
16
+ * The endpoint's own payload is stored verbatim rather than a pre-digested catalog, so
17
+ * the route re-derives everything it publishes — picker rows, fast variants, context
18
+ * variants — from the same parse it uses on a fresh read. A later build that reads more
19
+ * of a payload therefore benefits from a cache written by an earlier one.
20
+ *
21
+ * @module dsh-plugin-subscription-oauth/providers/model-cache
22
+ */
23
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
24
+ import path from 'node:path';
25
+ /**
26
+ * On-disk shape version.
27
+ *
28
+ * Bumped when a route's parse of a cached payload changes meaning, so a document
29
+ * written by an older understanding is discarded rather than misread.
30
+ */
31
+ const CACHE_VERSION = 1;
32
+ /**
33
+ * Keep each route's last live listing on disk.
34
+ *
35
+ * Reads are tolerant: a missing, unreadable, unparsable or stale-version file yields
36
+ * no cache rather than an error, because a cache miss must only mean "ask the
37
+ * endpoint", never a broken picker. Writes are atomic, so a crash mid-write cannot
38
+ * leave a half-written document behind.
39
+ */
40
+ export class ModelCatalogCache {
41
+ file;
42
+ /**
43
+ * @param file - absolute path of the cache document.
44
+ */
45
+ constructor(file) {
46
+ this.file = file;
47
+ }
48
+ /**
49
+ * Whatever one route last cached.
50
+ *
51
+ * The payload is opaque here: a route writes what it needs to rebuild its catalog
52
+ * (Kimi stores the decoded rows of `/models`; a route that derives fast variants from
53
+ * a second listing stores both). It re-validates on read.
54
+ *
55
+ * @param provider - provider route key.
56
+ * @returns the cached payload, or undefined when there is no usable cache.
57
+ */
58
+ async read(provider) {
59
+ const document = await this.load();
60
+ const cached = document?.providers[provider];
61
+ if (cached === undefined || cached.payload === undefined || cached.payload === null)
62
+ return undefined;
63
+ if (Array.isArray(cached.payload) && cached.payload.length === 0)
64
+ return undefined;
65
+ return cached.payload;
66
+ }
67
+ /**
68
+ * Record what the endpoint just reported for one route.
69
+ *
70
+ * @param provider - provider route key.
71
+ * @param payload - what that route needs to rebuild its catalog.
72
+ */
73
+ async write(provider, payload) {
74
+ if (payload === undefined || payload === null)
75
+ return;
76
+ if (Array.isArray(payload) && payload.length === 0)
77
+ return;
78
+ const document = await this.load() ?? { version: CACHE_VERSION, providers: {} };
79
+ document.providers[provider] = { fetchedAt: Date.now(), payload };
80
+ await mkdir(path.dirname(this.file), { recursive: true });
81
+ // Write beside the target and rename, so a reader never sees a partial document.
82
+ const temporary = `${this.file}.${process.pid}.tmp`;
83
+ await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, 'utf8');
84
+ await rename(temporary, this.file);
85
+ }
86
+ /** Load the document, or undefined when it is absent or unusable. */
87
+ async load() {
88
+ try {
89
+ const parsed = JSON.parse(await readFile(this.file, 'utf8'));
90
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
91
+ return undefined;
92
+ const record = parsed;
93
+ if (record['version'] !== CACHE_VERSION)
94
+ return undefined;
95
+ const providers = record['providers'];
96
+ if (typeof providers !== 'object' || providers === null || Array.isArray(providers))
97
+ return undefined;
98
+ return { version: CACHE_VERSION, providers: providers };
99
+ }
100
+ catch {
101
+ // Absent, unreadable or corrupt: a miss, not a failure.
102
+ return undefined;
103
+ }
104
+ }
105
+ }
@@ -57,6 +57,14 @@ export interface ResponsesStreamInput<S extends SessionBase> {
57
57
  models?: () => Promise<readonly string[]>;
58
58
  /** Called when a call proved the stored credential unusable. */
59
59
  onCredentialRejected?: (account: string) => void;
60
+ /**
61
+ * Called when the endpoint answered a request by naming the model id to use instead.
62
+ *
63
+ * The request is retried with that id, so the turn still succeeds; the caller records
64
+ * the rename so the next turn goes straight to the current id instead of learning it
65
+ * again from a refused request.
66
+ */
67
+ onModelRenamed?: (from: string, to: string) => void;
60
68
  /** Rewrite a thrown failure before it is mapped, e.g. to name the fix. */
61
69
  describeFailure?(error: unknown): unknown;
62
70
  /** Called when a model in the chain was skipped, with the reason. */
@@ -14,7 +14,7 @@
14
14
  import { EMPTY_RESPONSE_CODE, LlmError } from '@deepseek-ai/dsh-llm';
15
15
  import { allowanceScheduler } from '../auth/allowance-scheduler.js';
16
16
  import { streamResponses } from '../translate/responses.js';
17
- import { httpLlmError, idleWatchdog, mapFetchFailure } from './common.js';
17
+ import { httpLlmError, idleWatchdog, mapFetchFailure, unknownModelReplacement } from './common.js';
18
18
  /**
19
19
  * Minimum beat between dispatch attempts on one route, so a refused account is not
20
20
  * re-hit at once and a sweep across the pool does not become a burst. opencodex keeps
@@ -161,10 +161,26 @@ export async function* streamProviderResponses(options, input) {
161
161
  continue;
162
162
  let response = await request(options, model, token.accessToken, watchdog.signal);
163
163
  if (response.status === 401 || response.status === 403) {
164
- token = await tokens.resolve(token.account, true);
165
- response = await request(options, model, token.accessToken, watchdog.signal);
166
- if (response.status === 401 || response.status === 403) {
167
- input.onCredentialRejected?.(token.account);
164
+ // Two different failures share this status: a credential the provider no
165
+ // longer accepts, and a model id it does not know. Only the first is answered
166
+ // by a token refresh — treating a model rename that way would refresh a
167
+ // healthy token and, worse, mark the account as needing a new sign-in. The
168
+ // body says which it is, so it is read before deciding, and an unknown model
169
+ // is retried once under the id the provider named.
170
+ const refusal = await httpLlmError(response, label);
171
+ const replacement = unknownModelReplacement(refusal.message);
172
+ if (replacement !== undefined) {
173
+ lastError = refusal;
174
+ input.onModelRenamed?.(model, replacement);
175
+ input.onFallback?.(model, replacement, reasonOf(refusal));
176
+ response = await request(options, replacement, token.accessToken, watchdog.signal);
177
+ }
178
+ else {
179
+ token = await tokens.resolve(token.account, true);
180
+ response = await request(options, model, token.accessToken, watchdog.signal);
181
+ if (response.status === 401 || response.status === 403) {
182
+ input.onCredentialRejected?.(token.account);
183
+ }
168
184
  }
169
185
  }
170
186
  // A spent allowance moves to the next account (when the provider supports
@@ -13,8 +13,25 @@ export interface WorkBuddyProviderOptions {
13
13
  credentialFile: string;
14
14
  /** Abandon a stream after this much silence. */
15
15
  streamIdleTimeoutMs: number;
16
+ /**
17
+ * Whether WorkBuddy's own console listing is consulted for the catalog.
18
+ *
19
+ * The listing is the authority on which models an account serves; switching this off
20
+ * leaves the seed table in place and reads nothing.
21
+ */
22
+ discovery?: boolean;
16
23
  /** Model the picker preselects. */
17
24
  defaultModel?: string;
25
+ /**
26
+ * WorkBuddy's last listing, from this plugin's cache.
27
+ *
28
+ * The cached table is what the picker shows before any request: the console's own answer
29
+ * from the last read, so a release does not need a plugin update and a refresh does not
30
+ * need a request.
31
+ */
32
+ cachedListing?: () => Promise<unknown>;
33
+ /** Record what a live listing just produced, replacing the cached table. */
34
+ saveListing?: (payload: unknown) => Promise<void>;
18
35
  /** Shared model-visibility preferences. */
19
36
  preferences: ModelPreferences;
20
37
  /** Invoked when the catalog or its visibility changed. */
@@ -11,7 +11,7 @@ import { LOGIN_CANCELLED_MESSAGE } from '../auth/device-flow.js';
11
11
  import path from 'node:path';
12
12
  import { readSpendLedger } from './workbuddy-credits.js';
13
13
  import { finishWorkBuddySignIn, parseWorkBuddySession, readWorkBuddyUsage, refreshWorkBuddyToken, startWorkBuddySignIn, workbuddyRefreshable, workbuddyStale, claimWorkBuddyCheckin, listWorkBuddyTasks, acceptWorkBuddyTasks, claimWorkBuddyTask, WORKBUDDY_EARLY_MS, } from '../auth/workbuddy-flow.js';
14
- import { WorkBuddyAdapter, WORKBUDDY_CODE_URL, WORKBUDDY_CATALOG, WORKBUDDY_DEFAULT_MODEL, WORKBUDDY_PROVIDER_ID, WORKBUDDY_PROVIDER_NAME, } from './workbuddy.js';
14
+ import { WorkBuddyAdapter, WORKBUDDY_CODE_URL, WORKBUDDY_DEFAULT_MODEL, WORKBUDDY_FALLBACK_CONTEXT_WINDOW, WORKBUDDY_PROVIDER_ID, WORKBUDDY_PROVIDER_NAME, } from './workbuddy.js';
15
15
  import { ProviderController } from '../auth/provider-controller.js';
16
16
  import { proxiedFetch } from '../http.js';
17
17
  /** Wait, honouring cancellation. */
@@ -133,12 +133,17 @@ export function createWorkBuddyProvider(options) {
133
133
  });
134
134
  const spendFile = path.join(path.dirname(options.credentialFile), 'workbuddy-spend.json');
135
135
  const adapter = new WorkBuddyAdapter({
136
+ // Consulting the console's listing is what keeps the catalog from going stale: a model
137
+ // released, renamed or retired server-side appears without a plugin update.
138
+ discovery: options.discovery ?? true,
136
139
  streamIdleTimeoutMs: options.streamIdleTimeoutMs,
137
140
  tokens,
138
141
  spendFile,
139
142
  ...options.onWarn === undefined ? {} : { onWarn: options.onWarn },
140
143
  hiddenModels: () => options.preferences.hiddenIds(WORKBUDDY_PROVIDER_ID),
141
144
  userModels: () => options.preferences.extraIds(WORKBUDDY_PROVIDER_ID),
145
+ ...options.cachedListing === undefined ? {} : { cachedListing: options.cachedListing },
146
+ ...options.saveListing === undefined ? {} : { saveListing: options.saveListing },
142
147
  fetchFn,
143
148
  });
144
149
  /** The stored session's fields as the token a request or meter signs with. */
@@ -157,13 +162,17 @@ export function createWorkBuddyProvider(options) {
157
162
  displayName: WORKBUDDY_PROVIDER_NAME,
158
163
  codeUrl: WORKBUDDY_CODE_URL,
159
164
  credentialFile: options.credentialFile,
160
- // WorkBuddy ships a fixed catalog; there is no discovery endpoint.
161
- discovery: false,
165
+ // WorkBuddy's own console route lists the models an account may use; it is read once
166
+ // and cached, and a picker query never asks the endpoint.
167
+ discovery: options.discovery ?? true,
162
168
  defaultModel: options.defaultModel ?? WORKBUDDY_DEFAULT_MODEL,
163
169
  catalog: () => adapter.catalogEntries(),
164
170
  catalogOwn: () => adapter.ownCatalogEntries(),
165
- fallbackContextWindow: WORKBUDDY_CATALOG[0]?.contextWindow ?? 0,
166
- discover: () => Promise.resolve(undefined),
171
+ fallbackContextWindow: WORKBUDDY_FALLBACK_CONTEXT_WINDOW,
172
+ // The token is handed over rather than the session it came from; the adapter reads the
173
+ // session back, so each account is attributed to its own enterprise.
174
+ discover: (accessToken) => adapter.fetchCatalog(accessToken),
175
+ applyDiscovery: (ids) => { adapter.discover(ids); },
167
176
  usage: async (_accessToken, account) => {
168
177
  const held = await store.get(account);
169
178
  if (held === undefined)
@@ -178,7 +187,6 @@ export function createWorkBuddyProvider(options) {
178
187
  { key: 'endpoint', value: 'https://copilot.tencent.com' },
179
188
  { key: 'credentialFile', value: options.credentialFile },
180
189
  ],
181
- applyDiscovery: () => { },
182
190
  };
183
191
  const controller = new ProviderController({
184
192
  spec,
@@ -3,8 +3,15 @@
3
3
  *
4
4
  * WorkBuddy chats are chat completions at `https://copilot.tencent.com/v2/chat/completions`,
5
5
  * streamed only, and the API refuses a request whose User-Agent is not
6
- * `WorkBuddy/<version>`. The route ships the China build's catalog (Hy, GLM, Kimi,
7
- * DeepSeek, MiniMax). Adapted from magpie's `workbuddy` for this plugin's contracts.
6
+ * `WorkBuddy/<version>`. The model table this route publishes is the console's own
7
+ * listing — `GET /console/enterprises/<enterpriseId || personal>/models`, the route
8
+ * WorkBuddy's web console reads — fetched once and kept in this plugin's cache (see
9
+ * {@link WorkBuddyAdapter.hydrate} and {@link WorkBuddyAdapter.fetchCatalog}).
10
+ * {@link WORKBUDDY_CATALOG} is the cold-start seed used until a listing has been read,
11
+ * the fallback when a read fails, and the metadata overlay for a row the listing
12
+ * describes only partly, so a model released, renamed or retired server-side reaches the
13
+ * picker without a plugin update and without a request per refresh.
14
+ * Adapted from magpie's `workbuddy` for this plugin's contracts.
8
15
  *
9
16
  * @module dsh-plugin-subscription-oauth/providers/workbuddy
10
17
  */
@@ -19,8 +26,28 @@ export declare const WORKBUDDY_PROVIDER_ID = "workbuddy";
19
26
  export declare const WORKBUDDY_PROVIDER_NAME = "WorkBuddy";
20
27
  /** Where the Settings page sends someone to get an account. */
21
28
  export declare const WORKBUDDY_CODE_URL = "https://copilot.tencent.com";
22
- /** The model the picker preselects. */
23
- export declare const WORKBUDDY_DEFAULT_MODEL = "hy4-preview-f";
29
+ /**
30
+ * The model the picker preselects.
31
+ *
32
+ * The console's own default, and a row its listing actually serves, so the page's
33
+ * "recommended" marker never names a model the picker cannot offer. The previous value
34
+ * (`hy4-preview-f`) was a seed-table spelling the live listing does not serve — it offers
35
+ * `hy4-preview` — which is the staleness this route no longer keeps in its catalog.
36
+ */
37
+ export declare const WORKBUDDY_DEFAULT_MODEL = "auto";
38
+ /** Context advertised for a listed id neither the listing nor the seed table describes. */
39
+ export declare const WORKBUDDY_FALLBACK_CONTEXT_WINDOW = 168000;
40
+ /**
41
+ * The console route that lists the models one account may use.
42
+ *
43
+ * The scope segment is the enterprise the session is attributed to, or `personal` for an
44
+ * account that states none. This is the route WorkBuddy's own console reads; it is not
45
+ * one of the plugin's `/v2` API routes, which is why probing those found nothing.
46
+ *
47
+ * @param enterpriseId - the session's enterprise, when it has one.
48
+ * @returns the absolute listing URL.
49
+ */
50
+ export declare function workbuddyModelsUrl(enterpriseId?: string): string;
24
51
  /** One signed-in WorkBuddy session, as stored. */
25
52
  export interface WorkBuddyStored {
26
53
  accessToken: string;
@@ -34,15 +61,35 @@ export interface WorkBuddyStored {
34
61
  tokenType?: string;
35
62
  refreshExpiresAt?: number;
36
63
  }
37
- /** The catalog the route serves. */
64
+ /**
65
+ * The seed table the route falls back to when no listing has been read (see
66
+ * {@link CN_MODELS}).
67
+ *
68
+ * A table this build ships, never the roster: WorkBuddy's own listing decides which
69
+ * models exist, and each row's own figures decide what they are. This is only the
70
+ * cold-start membership and the overlay for what a listing leaves unsaid.
71
+ */
38
72
  export declare const WORKBUDDY_CATALOG: readonly CatalogEntry[];
39
73
  /** Constructor dependencies for {@link WorkBuddyAdapter}. */
40
74
  export interface WorkBuddyAdapterOptions {
75
+ /** Whether the console's live model listing is consulted. */
76
+ discovery: boolean;
41
77
  streamIdleTimeoutMs: number;
42
78
  tokens: TokenManager<WorkBuddySession>;
43
79
  onWarn?: (message: string) => void;
44
80
  hiddenModels?: () => Promise<ReadonlySet<string>>;
45
81
  userModels?: () => Promise<readonly string[]>;
82
+ /**
83
+ * The listing WorkBuddy's console answered last, as this plugin's cache kept it.
84
+ *
85
+ * Read once per session, before the first catalog query, so the picker offers the real
86
+ * models without asking the console anything. A route that fetched once and cached the
87
+ * answer has no reason to fetch again on every refresh; the Settings "refresh models"
88
+ * action is what re-reads it.
89
+ */
90
+ cachedListing?: () => Promise<unknown>;
91
+ /** Record what a live listing just produced, replacing the cached table. */
92
+ saveListing?: (payload: unknown) => Promise<void>;
46
93
  /** Where per-account credit spends accumulate; beside the credential file. */
47
94
  spendFile?: string;
48
95
  fetchFn?: FetchFn;
@@ -51,19 +98,101 @@ export interface WorkBuddyAdapterOptions {
51
98
  export declare class WorkBuddyAdapter extends LlmAdapter {
52
99
  private readonly options;
53
100
  private readonly fetchFn;
101
+ /**
102
+ * The catalog this route serves: entries derived from WorkBuddy's own listing, either
103
+ * the one just read or the one the cache kept from the last read.
104
+ *
105
+ * Undefined means neither source has produced a listing yet, so the seed table
106
+ * ({@link WORKBUDDY_CATALOG}) is what the picker shows.
107
+ */
108
+ private live;
109
+ /** Whether the cached listing has been read; it is read at most once per session. */
110
+ private hydrated;
54
111
  constructor(options: WorkBuddyAdapterOptions);
55
112
  providerInfo(provider: string): LlmProviderInfo;
113
+ /**
114
+ * Adopt one listing as this route's catalog.
115
+ *
116
+ * Everything the picker shows is derived here from WorkBuddy's own answer: which models
117
+ * exist (the account's `cli` agent when the listing states one), in the order that
118
+ * listing states them, each with the name, window, modalities and reasoning rung its row
119
+ * states. The seed table is consulted only for what a row leaves unsaid, so a live
120
+ * figure is never overridden by a shipped guess.
121
+ *
122
+ * @param payload - the decoded listing, or the `{models, agents}` object a cache kept.
123
+ * @returns the entries to publish, in picker order.
124
+ */
125
+ private adoptListing;
126
+ /**
127
+ * Read the cached listing, once, so the picker works without asking the console.
128
+ *
129
+ * Called before the first catalog query. A read failure is a cache miss, never a
130
+ * failure: the route falls back to its seed table until a listing is fetched, so a
131
+ * broken cache can never empty the picker.
132
+ */
133
+ hydrate(): Promise<void>;
56
134
  listModels(_provider: string): Promise<readonly LlmModelInfo[]>;
57
135
  private hidden;
136
+ /**
137
+ * The catalog to present: the models WorkBuddy's listing named, in the order its own
138
+ * console states them, each with the metadata that listing stated.
139
+ *
140
+ * The seed table is the membership only while no listing has been read, and the metadata
141
+ * overlay for a row the listing describes partly. It is never allowed to add a row back:
142
+ * a model the console stopped serving must leave the picker without a plugin update,
143
+ * which is the whole point of reading the listing instead of shipping a roster.
144
+ */
145
+ private shippedCatalog;
58
146
  /** The catalog the Settings list reads. */
59
147
  catalogEntries(): readonly CatalogEntry[];
60
148
  /** The entries the route itself serves, without hand-added extras. */
61
149
  ownCatalogEntries(): readonly CatalogEntry[];
150
+ /**
151
+ * Adopt the ids a caller reports as the served set, without a listing.
152
+ *
153
+ * {@link fetchCatalog} already derived the entries from the listing it read, and the
154
+ * controller reports those same ids back here; the entries it recorded are kept, so a
155
+ * live name, window or rung is not replaced by the seed table's. A caller that only has
156
+ * ids (a test, another discovery path) gets the seed table's metadata where it knows the
157
+ * id and conservative entries otherwise.
158
+ *
159
+ * @param ids - model ids the caller reports.
160
+ */
161
+ discover(ids: readonly string[]): void;
62
162
  resolveModel(_provider: string, model: string): Promise<LlmResolvedModelInfo>;
163
+ /**
164
+ * Read WorkBuddy's own console listing and adopt it as this route's catalog.
165
+ *
166
+ * This is the one place a model table is produced, and it is WorkBuddy's own answer: the
167
+ * account's `cli` agent decides which models are available, and each row states its own
168
+ * name, window, modalities and reasoning rung (see {@link adoptListing}). The payload is
169
+ * also handed to {@link WorkBuddyAdapterOptions.saveListing}, so the next session starts
170
+ * from this table instead of asking again — the Settings "refresh models" action is what
171
+ * re-reads it, and a picker query never does.
172
+ *
173
+ * A failed or empty read keeps the catalog the route already had, and says so once.
174
+ *
175
+ * @param accessToken - bearer for the account.
176
+ * @param enterpriseId - the enterprise to read; taken from the stored session when omitted.
177
+ * @param signal - optional cancellation.
178
+ * @returns the model ids now served, or undefined when the listing is unavailable.
179
+ */
180
+ fetchCatalog(accessToken: string, enterpriseId?: string, signal?: AbortSignal): Promise<readonly string[] | undefined>;
181
+ /**
182
+ * The stored session one access token belongs to.
183
+ *
184
+ * Discovery is handed the account's token rather than the session it came from, and the
185
+ * console route is attributed by that session's own fields, so the session is read back
186
+ * here. The first account whose stored token matches wins; a token that matches none
187
+ * falls back to the route's current session. A read failure is never fatal — the listing
188
+ * is still read with the token the caller passed.
189
+ *
190
+ * @param accessToken - the token discovery was called with.
191
+ * @returns the session's fields as a signable token, or undefined when none was readable.
192
+ */
193
+ private sessionFor;
63
194
  providerRetryPolicy(): undefined;
64
195
  stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
65
- /** The current turn's billed credit, read off the last usage frame. */
66
- private turnCredit;
67
196
  /** The stored session's fields as the token the request signs with. */
68
197
  private tokenOf;
69
198
  private requestBody;
@@ -72,21 +201,19 @@ export declare class WorkBuddyAdapter extends LlmAdapter {
72
201
  *
73
202
  * A tool name may hold only letters, digits, `_`, `-`; another character is refused, and
74
203
  * a schema with a union at the parameter root or a `\0` escape is turned away by the
75
- * model. Each unsafe name is made safe and the safe→original map kept on the adapter so
76
- * the model's calls can be renamed back in the answer. Adapted from magpie's fitTools.
204
+ * model. Each unsafe name is made safe and the safe→original map returned to the caller
205
+ * so the model's calls can be renamed back in the answer. Adapted from magpie's fitTools.
77
206
  *
78
207
  * @param tools - the declared tool schemas, when any.
208
+ * @param history - the request's assistant/tool turns, renamed in place to the safe names.
79
209
  * @returns the mended wire tools, or undefined when there are none.
80
210
  */
81
211
  private fittedTools;
82
- /** The safe→original tool names to restore in the answer. */
83
- private safeToolNames;
84
- /** The assistant/tool history of the request being built, for renaming past calls. */
85
- private historyMessages;
86
212
  /**
87
213
  * Restore the original tool names the model was given, in one parsed chunk.
88
214
  *
89
215
  * @param value - a decoded SSE payload.
216
+ * @param names - the safe→original map the request that produced this stream mended.
90
217
  * @returns whether any name changed.
91
218
  */
92
219
  private namedBack;