dsh-plugin-subscription-oauth 0.8.18 → 0.10.0

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.
@@ -0,0 +1,260 @@
1
+ /**
2
+ * The Xiaomi MiMo wire adapter serving the `mimo` route.
3
+ *
4
+ * Model requests are chat completions at the MiMo server's `/route`, carrying the
5
+ * session cookies the sign-on set — not a bearer token. Two models ship: MiMo Pro
6
+ * and MiMo Flash. The open platform's Token Plan spends its own `tp-` key at a
7
+ * separate endpoint; that fallback lives beside the account-level stream here.
8
+ *
9
+ * @module dsh-plugin-subscription-oauth/providers/mimo
10
+ */
11
+ import { LlmAdapter, errorChain, } from '@deepseek-ai/dsh-llm';
12
+ import { httpLlmError, isMissingOrInvalidCredential, mapFetchFailure, idleWatchdog } from './common.js';
13
+ import { proxiedFetch } from '../http.js';
14
+ import { refreshMimoSession } from '../auth/mimo-flow.js';
15
+ /** The `mimo` provider route key. */
16
+ export const MIMO_PROVIDER_ID = 'mimo';
17
+ /** Human name for the route and its cards. */
18
+ export const MIMO_PROVIDER_NAME = 'Xiaomi MiMo';
19
+ /** Where the Settings page sends someone to get an account. */
20
+ export const MIMO_CODE_URL = 'https://platform.xiaomimimo.com';
21
+ /** MiMo context window shared by Pro and Flash. */
22
+ export const MIMO_CONTEXT_WINDOW = 1_000_000;
23
+ /** The output ceiling MiMo advertises. */
24
+ export const MIMO_DEFAULT_MAX_TOKENS = 128_000;
25
+ /** The model the picker preselects. */
26
+ export const MIMO_DEFAULT_MODEL = 'mimo-pro';
27
+ /** Validate one stored value as a MiMo session, or undefined when it is not one. */
28
+ export function parseMimoSession(value) {
29
+ if (typeof value !== 'object' || value === null)
30
+ return undefined;
31
+ const record = value;
32
+ if (typeof record.accessToken !== 'string' || record.accessToken.length === 0)
33
+ return undefined;
34
+ if (typeof record.expiresAt !== 'number')
35
+ return undefined;
36
+ if (typeof record.apiBase !== 'string' || record.apiBase.length === 0)
37
+ return undefined;
38
+ if (typeof record.mimoCreds !== 'string')
39
+ return undefined;
40
+ if (typeof record.mimoCookies !== 'string')
41
+ return undefined;
42
+ return {
43
+ accessToken: record.accessToken,
44
+ expiresAt: record.expiresAt,
45
+ apiBase: record.apiBase,
46
+ mimoCreds: record.mimoCreds,
47
+ mimoCookies: record.mimoCookies,
48
+ ...typeof record.refreshToken === 'string' ? { refreshToken: record.refreshToken } : {},
49
+ ...typeof record.accountId === 'string' ? { accountId: record.accountId } : {},
50
+ ...typeof record.email === 'string' ? { email: record.email } : {},
51
+ ...typeof record.region === 'string' ? { region: record.region } : {},
52
+ };
53
+ }
54
+ /** The two models the MiMo app serves. */
55
+ export const MIMO_MODELS = [
56
+ { id: 'mimo-pro', name: 'MiMo Pro', description: 'Flagship MiMo model', contextWindow: MIMO_CONTEXT_WINDOW },
57
+ { id: 'mimo-flash', name: 'MiMo Flash', description: 'Fast MiMo model', contextWindow: MIMO_CONTEXT_WINDOW },
58
+ ];
59
+ /** The MiMo wire adapter serving the `mimo` route. */
60
+ export class MimoAdapter extends LlmAdapter {
61
+ options;
62
+ fetchFn;
63
+ constructor(options) {
64
+ super();
65
+ this.options = options;
66
+ this.fetchFn = options.fetchFn ?? proxiedFetch;
67
+ }
68
+ providerInfo(provider) {
69
+ return { id: provider, name: MIMO_PROVIDER_NAME };
70
+ }
71
+ async listModels(_provider) {
72
+ if (await this.options.tokens.session() === undefined)
73
+ return [];
74
+ const hidden = await this.hidden();
75
+ return MIMO_MODELS.filter((entry) => !hidden.has(entry.id)).map((entry) => ({
76
+ provider: MIMO_PROVIDER_ID,
77
+ id: entry.id,
78
+ name: entry.name,
79
+ description: entry.description,
80
+ inputModalities: ['text', 'image'],
81
+ }));
82
+ }
83
+ async hidden() {
84
+ if (this.options.hiddenModels === undefined)
85
+ return new Set();
86
+ try {
87
+ return await this.options.hiddenModels();
88
+ }
89
+ catch {
90
+ return new Set();
91
+ }
92
+ }
93
+ async resolveModel(_provider, model) {
94
+ const entry = MIMO_MODELS.find((candidate) => candidate.id === model);
95
+ if (entry === undefined) {
96
+ return {
97
+ provider: MIMO_PROVIDER_ID,
98
+ id: model,
99
+ name: model,
100
+ context: { contextWindow: MIMO_CONTEXT_WINDOW },
101
+ defaultMaxTokens: MIMO_DEFAULT_MAX_TOKENS,
102
+ inputModalities: ['text', 'image'],
103
+ };
104
+ }
105
+ return {
106
+ provider: MIMO_PROVIDER_ID,
107
+ id: entry.id,
108
+ name: entry.name,
109
+ description: entry.description,
110
+ context: { contextWindow: entry.contextWindow },
111
+ defaultMaxTokens: MIMO_DEFAULT_MAX_TOKENS,
112
+ inputModalities: ['text', 'image'],
113
+ };
114
+ }
115
+ providerRetryPolicy() {
116
+ return undefined;
117
+ }
118
+ /** The catalog the Settings list reads: every shipped model. */
119
+ catalogEntries() {
120
+ return MIMO_MODELS;
121
+ }
122
+ /** The ids the route itself serves, without hand-added extras. */
123
+ ownCatalogEntries() {
124
+ return MIMO_MODELS;
125
+ }
126
+ async *stream(options) {
127
+ const held = await this.options.tokens.session();
128
+ if (held === undefined)
129
+ throw await httpLlmError(new Response(null, { status: 401 }), 'Xiaomi MiMo');
130
+ const session = held.session;
131
+ const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
132
+ try {
133
+ const cookies = this.cookieHeader(session);
134
+ const body = this.requestBody(options);
135
+ const response = await this.fetchFn(`${session.apiBase.replace(/\/+$/, '')}/route/chat/completions`, {
136
+ method: 'POST',
137
+ headers: {
138
+ 'Content-Type': 'application/json',
139
+ 'Accept': 'text/event-stream',
140
+ 'Cookie': cookies,
141
+ 'X-Mimo-Source': 'mimocode-cli-free',
142
+ 'User-Agent': 'miNative PC/Normal Windows_NT/10.0.26100 SDKV/1.0.0 DEVT/PC DEVS/Windows APP/miaccount_desktop APPV/0.1.0',
143
+ 'X-Client-Version': '26.929.292248',
144
+ },
145
+ body: JSON.stringify(body),
146
+ signal: watchdog.signal,
147
+ });
148
+ if (!response.ok || response.body === null) {
149
+ throw await httpLlmError(response, 'Xiaomi MiMo');
150
+ }
151
+ yield* this.readStream(response, watchdog);
152
+ }
153
+ catch (error) {
154
+ throw mapFetchFailure('Xiaomi MiMo', error, watchdog, options.signal);
155
+ }
156
+ finally {
157
+ watchdog.stop();
158
+ }
159
+ }
160
+ /**
161
+ * The session cookies as one Cookie header, in a steady order.
162
+ *
163
+ * MiMo is a cookie session, not a bearer: the wire credential is the cookie
164
+ * header built from the stored session's cookie record.
165
+ */
166
+ cookieHeader(session) {
167
+ const first = ['serviceToken', 'userId', 'cUserId'];
168
+ try {
169
+ const cookies = JSON.parse(session.mimoCookies);
170
+ const parts = first.filter((key) => key in cookies).map((key) => `${key}=${cookies[key]}`);
171
+ for (const [key, value] of Object.entries(cookies)) {
172
+ if (!first.includes(key))
173
+ parts.push(`${key}=${value}`);
174
+ }
175
+ return parts.join('; ');
176
+ }
177
+ catch {
178
+ return '';
179
+ }
180
+ }
181
+ requestBody(options) {
182
+ const messages = [];
183
+ if (options.system !== undefined && options.system.trim() !== '') {
184
+ messages.push({ role: 'system', content: options.system });
185
+ }
186
+ for (const message of options.messages) {
187
+ messages.push(this.toChatMessage(message));
188
+ }
189
+ return {
190
+ model: options.model,
191
+ messages,
192
+ ...options.maxTokens === undefined ? {} : { max_tokens: options.maxTokens },
193
+ stream: true,
194
+ };
195
+ }
196
+ toChatMessage(message) {
197
+ const role = message.role === 'assistant' ? 'assistant' : 'user';
198
+ const textParts = [];
199
+ for (const block of message.content) {
200
+ if (block.type === 'text')
201
+ textParts.push(block.text);
202
+ }
203
+ return { role, content: textParts.join('\n') };
204
+ }
205
+ async *readStream(response, watchdog) {
206
+ const reader = response.body.getReader();
207
+ const decoder = new TextDecoder();
208
+ let buffer = '';
209
+ let started = false;
210
+ try {
211
+ while (true) {
212
+ const { done, value } = await reader.read();
213
+ if (done)
214
+ break;
215
+ watchdog.pulse();
216
+ buffer += decoder.decode(value, { stream: true });
217
+ const frames = buffer.split('\n\n');
218
+ buffer = frames.pop() ?? '';
219
+ for (const frame of frames) {
220
+ const data = frame
221
+ .split('\n')
222
+ .filter((line) => line.startsWith('data:'))
223
+ .map((line) => line.slice(5).trim())
224
+ .join('\n');
225
+ if (data === '' || data === '[DONE]')
226
+ continue;
227
+ let chunk;
228
+ try {
229
+ chunk = JSON.parse(data);
230
+ }
231
+ catch {
232
+ continue;
233
+ }
234
+ const delta = chunk.choices?.[0]?.delta?.content;
235
+ if (typeof delta === 'string' && delta !== '') {
236
+ if (!started) {
237
+ yield { type: 'block-start', index: 0, blockType: 'text' };
238
+ started = true;
239
+ }
240
+ yield { type: 'text-delta', index: 0, text: delta };
241
+ }
242
+ if (chunk.usage !== undefined) {
243
+ const input = chunk.usage.prompt_tokens ?? 0;
244
+ const output = chunk.usage.completion_tokens ?? 0;
245
+ yield {
246
+ type: 'usage',
247
+ usage: { inputTokens: input, outputTokens: output, totalTokens: chunk.usage.total_tokens ?? input + output },
248
+ };
249
+ }
250
+ }
251
+ }
252
+ if (started)
253
+ yield { type: 'block-end', index: 0, block: { type: 'text', text: '' } };
254
+ yield { type: 'finish', reason: { kind: 'stop' } };
255
+ }
256
+ finally {
257
+ reader.cancel().catch(() => { });
258
+ }
259
+ }
260
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The `workbuddy` provider route's wiring (WorkBuddy China build):
3
+ * credential store, browser sign-in, token refresh, and its Settings card facts.
4
+ *
5
+ * @module dsh-plugin-subscription-oauth/providers/workbuddy-provider
6
+ */
7
+ import type { ProviderRoute } from '../auth/registry.js';
8
+ import type { ModelPreferences } from '../settings.js';
9
+ import type { FetchFn } from '../http.js';
10
+ /** Everything the WorkBuddy route needs from the plugin configuration. */
11
+ export interface WorkBuddyProviderOptions {
12
+ /** Absolute path of `auth.json`. */
13
+ credentialFile: string;
14
+ /** Abandon a stream after this much silence. */
15
+ streamIdleTimeoutMs: number;
16
+ /** Model the picker preselects. */
17
+ defaultModel?: string;
18
+ /** Shared model-visibility preferences. */
19
+ preferences: ModelPreferences;
20
+ /** Invoked when the catalog or its visibility changed. */
21
+ onCatalogChanged?: () => void;
22
+ onWarn?: (message: string) => void;
23
+ /** Fetch implementation; defaults to the proxy-aware fetch. */
24
+ fetchFn?: FetchFn;
25
+ }
26
+ /**
27
+ * Build the `workbuddy` route: its adapter and its login/lifecycle controller.
28
+ *
29
+ * @param options - resolved paths and shared services.
30
+ * @returns the route, ready to register and to serve over RPC.
31
+ */
32
+ export declare function createWorkBuddyProvider(options: WorkBuddyProviderOptions): ProviderRoute;
@@ -0,0 +1,181 @@
1
+ /**
2
+ * The `workbuddy` provider route's wiring (WorkBuddy China build):
3
+ * credential store, browser sign-in, token refresh, and its Settings card facts.
4
+ *
5
+ * @module dsh-plugin-subscription-oauth/providers/workbuddy-provider
6
+ */
7
+ import { REAUTH_REQUIRED, ProviderError } from '../errors.js';
8
+ import { TokenStore } from '../auth/store.js';
9
+ import { TokenManager } from '../auth/tokens.js';
10
+ import { LOGIN_CANCELLED_MESSAGE } from '../auth/device-flow.js';
11
+ import { finishWorkBuddySignIn, parseWorkBuddySession, refreshWorkBuddyToken, startWorkBuddySignIn, workbuddyRefreshable, workbuddyStale, WORKBUDDY_EARLY_MS, } from '../auth/workbuddy-flow.js';
12
+ import { WorkBuddyAdapter, WORKBUDDY_CODE_URL, WORKBUDDY_CATALOG, WORKBUDDY_DEFAULT_MODEL, WORKBUDDY_PROVIDER_ID, WORKBUDDY_PROVIDER_NAME, } from './workbuddy.js';
13
+ import { ProviderController } from '../auth/provider-controller.js';
14
+ import { proxiedFetch } from '../http.js';
15
+ /** Wait, honouring cancellation. */
16
+ function sleep(ms, signal) {
17
+ return new Promise((resolve, reject) => {
18
+ if (signal.aborted) {
19
+ reject(new Error(LOGIN_CANCELLED_MESSAGE));
20
+ return;
21
+ }
22
+ const timer = setTimeout(() => {
23
+ signal.removeEventListener('abort', onAbort);
24
+ resolve();
25
+ }, ms);
26
+ function onAbort() {
27
+ clearTimeout(timer);
28
+ reject(new Error(LOGIN_CANCELLED_MESSAGE));
29
+ }
30
+ signal.addEventListener('abort', onAbort, { once: true });
31
+ });
32
+ }
33
+ /**
34
+ * Build the `workbuddy` route: its adapter and its login/lifecycle controller.
35
+ *
36
+ * @param options - resolved paths and shared services.
37
+ * @returns the route, ready to register and to serve over RPC.
38
+ */
39
+ export function createWorkBuddyProvider(options) {
40
+ const fetchFn = options.fetchFn ?? proxiedFetch;
41
+ const store = new TokenStore(options.credentialFile, WORKBUDDY_PROVIDER_ID, parseWorkBuddySession);
42
+ const toSession = (token, name) => ({
43
+ accessToken: token.accessToken,
44
+ expiresAt: token.expiresAt,
45
+ ...token.refreshToken === undefined ? {} : { refreshToken: token.refreshToken },
46
+ ...name === '' ? {} : { accountId: name },
47
+ ...token.uid === undefined ? {} : { uid: token.uid },
48
+ ...token.enterpriseId === undefined ? {} : { enterpriseId: token.enterpriseId },
49
+ ...token.domain === undefined ? {} : { domain: token.domain },
50
+ ...token.tokenType === undefined ? {} : { tokenType: token.tokenType },
51
+ ...token.refreshExpiresAt === undefined ? {} : { refreshExpiresAt: token.refreshExpiresAt },
52
+ });
53
+ /** The in-flight attempt, so `pending()` is stable across calls. */
54
+ let current;
55
+ const flow = {
56
+ async start() {
57
+ const start = await startWorkBuddySignIn(fetchFn);
58
+ const controller = new AbortController();
59
+ const session = (async () => {
60
+ while (true) {
61
+ if (controller.signal.aborted)
62
+ throw new Error(LOGIN_CANCELLED_MESSAGE);
63
+ try {
64
+ const { token, name } = await finishWorkBuddySignIn(start, fetchFn);
65
+ return toSession(token, name);
66
+ }
67
+ catch (error) {
68
+ if (controller.signal.aborted)
69
+ throw new Error(LOGIN_CANCELLED_MESSAGE);
70
+ // A "not yet" poll answer is normal; anything else fails the attempt.
71
+ if (error instanceof Error && /timed out/i.test(error.message))
72
+ throw error;
73
+ throw error;
74
+ }
75
+ }
76
+ })();
77
+ const attempt = {
78
+ prompt: {
79
+ url: start.url,
80
+ expiresAt: start.expiresAt,
81
+ mode: 'open-url',
82
+ },
83
+ session,
84
+ cancel: () => { controller.abort(); },
85
+ };
86
+ current = attempt;
87
+ void session.catch(() => undefined).finally(() => {
88
+ if (current === attempt)
89
+ current = undefined;
90
+ });
91
+ return attempt;
92
+ },
93
+ pending() {
94
+ return current;
95
+ },
96
+ };
97
+ const tokens = new TokenManager({
98
+ store,
99
+ displayName: WORKBUDDY_PROVIDER_NAME,
100
+ // Refresh a minute before the hour runs out, as the app does.
101
+ preemptMs: WORKBUDDY_EARLY_MS,
102
+ refresh: async (session) => {
103
+ const token = {
104
+ accessToken: session.accessToken,
105
+ expiresAt: session.expiresAt,
106
+ ...session.refreshToken === undefined ? {} : { refreshToken: session.refreshToken },
107
+ ...session.uid === undefined ? {} : { uid: session.uid },
108
+ ...session.enterpriseId === undefined ? {} : { enterpriseId: session.enterpriseId },
109
+ ...session.domain === undefined ? {} : { domain: session.domain },
110
+ ...session.tokenType === undefined ? {} : { tokenType: session.tokenType },
111
+ ...session.refreshExpiresAt === undefined ? {} : { refreshExpiresAt: session.refreshExpiresAt },
112
+ };
113
+ if (!workbuddyRefreshable(token)) {
114
+ throw new ProviderError(`${WORKBUDDY_PROVIDER_NAME} sign-in has expired; connect the account again`, REAUTH_REQUIRED);
115
+ }
116
+ try {
117
+ const renewed = await refreshWorkBuddyToken(token, fetchFn);
118
+ return { accessToken: renewed.accessToken, expiresAt: renewed.expiresAt };
119
+ }
120
+ catch (error) {
121
+ throw new ProviderError(`${WORKBUDDY_PROVIDER_NAME} token refresh failed: ${error instanceof Error ? error.message : String(error)}`, REAUTH_REQUIRED);
122
+ }
123
+ },
124
+ });
125
+ const adapter = new WorkBuddyAdapter({
126
+ streamIdleTimeoutMs: options.streamIdleTimeoutMs,
127
+ tokens,
128
+ ...options.onWarn === undefined ? {} : { onWarn: options.onWarn },
129
+ hiddenModels: () => options.preferences.hiddenIds(WORKBUDDY_PROVIDER_ID),
130
+ userModels: () => options.preferences.extraIds(WORKBUDDY_PROVIDER_ID),
131
+ fetchFn,
132
+ });
133
+ const spec = {
134
+ id: WORKBUDDY_PROVIDER_ID,
135
+ displayName: WORKBUDDY_PROVIDER_NAME,
136
+ codeUrl: WORKBUDDY_CODE_URL,
137
+ credentialFile: options.credentialFile,
138
+ // WorkBuddy ships a fixed catalog; there is no discovery endpoint.
139
+ discovery: false,
140
+ defaultModel: options.defaultModel ?? WORKBUDDY_DEFAULT_MODEL,
141
+ catalog: () => adapter.catalogEntries(),
142
+ catalogOwn: () => adapter.ownCatalogEntries(),
143
+ fallbackContextWindow: WORKBUDDY_CATALOG[0]?.contextWindow ?? 0,
144
+ discover: () => Promise.resolve(undefined),
145
+ usage: () => Promise.resolve({ windows: [] }),
146
+ details: () => [
147
+ { key: 'provider', value: WORKBUDDY_PROVIDER_NAME },
148
+ { key: 'endpoint', value: 'https://copilot.tencent.com' },
149
+ { key: 'credentialFile', value: options.credentialFile },
150
+ ],
151
+ applyDiscovery: () => { },
152
+ };
153
+ const controller = new ProviderController({
154
+ spec,
155
+ store,
156
+ tokens,
157
+ flow,
158
+ preferences: options.preferences,
159
+ ...options.onCatalogChanged === undefined ? {} : { onCatalogChanged: options.onCatalogChanged },
160
+ ...options.onWarn === undefined ? {} : { onWarn: options.onWarn },
161
+ });
162
+ return {
163
+ id: controller.id,
164
+ displayName: controller.displayName,
165
+ credentialFile: controller.credentialFile,
166
+ adapter,
167
+ status: () => controller.status(),
168
+ login: (account) => controller.login(account),
169
+ cancel: () => controller.cancel(),
170
+ logout: (account) => controller.logout(account),
171
+ setDefault: (account) => controller.setDefault(account),
172
+ refresh: (account) => controller.refresh(account),
173
+ discover: (force) => controller.discover(force),
174
+ setModelVisible: (id, visible) => controller.setModelVisible(id, visible),
175
+ setModelOrder: (ids) => controller.setModelOrder(ids),
176
+ addModel: (id) => controller.addModel(id),
177
+ removeModel: (id) => controller.removeModel(id),
178
+ usage: (force) => controller.usage(force),
179
+ reload: () => controller.reload(),
180
+ };
181
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The WorkBuddy China wire adapter serving the `workbuddy` route.
3
+ *
4
+ * WorkBuddy chats are chat completions at `https://copilot.tencent.com/v2/chat/completions`,
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.
8
+ *
9
+ * @module dsh-plugin-subscription-oauth/providers/workbuddy
10
+ */
11
+ import { LlmAdapter, type GenerateOptions, type LlmModelInfo, type LlmProviderInfo, type LlmResolvedModelInfo, type StreamChunk } from '@deepseek-ai/dsh-llm';
12
+ import { type FetchFn } from '../http.js';
13
+ import type { TokenManager } from '../auth/tokens.js';
14
+ import type { CatalogEntry } from './catalog.js';
15
+ import { type WorkBuddySession } from '../auth/workbuddy-flow.js';
16
+ /** The `workbuddy` provider route key (China build). */
17
+ export declare const WORKBUDDY_PROVIDER_ID = "workbuddy";
18
+ /** Human name for the route and its cards. */
19
+ export declare const WORKBUDDY_PROVIDER_NAME = "WorkBuddy";
20
+ /** Where the Settings page sends someone to get an account. */
21
+ 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";
24
+ /** One signed-in WorkBuddy session, as stored. */
25
+ export interface WorkBuddyStored {
26
+ accessToken: string;
27
+ refreshToken?: string;
28
+ expiresAt: number;
29
+ accountId?: string;
30
+ email?: string;
31
+ uid?: string;
32
+ enterpriseId?: string;
33
+ domain?: string;
34
+ tokenType?: string;
35
+ refreshExpiresAt?: number;
36
+ }
37
+ /** The catalog the route serves. */
38
+ export declare const WORKBUDDY_CATALOG: readonly CatalogEntry[];
39
+ /** Constructor dependencies for {@link WorkBuddyAdapter}. */
40
+ export interface WorkBuddyAdapterOptions {
41
+ streamIdleTimeoutMs: number;
42
+ tokens: TokenManager<WorkBuddySession>;
43
+ onWarn?: (message: string) => void;
44
+ hiddenModels?: () => Promise<ReadonlySet<string>>;
45
+ userModels?: () => Promise<readonly string[]>;
46
+ fetchFn?: FetchFn;
47
+ }
48
+ /** The WorkBuddy China wire adapter serving the `workbuddy` route. */
49
+ export declare class WorkBuddyAdapter extends LlmAdapter {
50
+ private readonly options;
51
+ private readonly fetchFn;
52
+ constructor(options: WorkBuddyAdapterOptions);
53
+ providerInfo(provider: string): LlmProviderInfo;
54
+ listModels(_provider: string): Promise<readonly LlmModelInfo[]>;
55
+ private hidden;
56
+ /** The catalog the Settings list reads. */
57
+ catalogEntries(): readonly CatalogEntry[];
58
+ /** The entries the route itself serves, without hand-added extras. */
59
+ ownCatalogEntries(): readonly CatalogEntry[];
60
+ resolveModel(_provider: string, model: string): Promise<LlmResolvedModelInfo>;
61
+ providerRetryPolicy(): undefined;
62
+ stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
63
+ /** The stored session's fields as the token the request signs with. */
64
+ private tokenOf;
65
+ private requestBody;
66
+ private toChatMessage;
67
+ /** Turn a non-2xx into an LlmError, naming WorkBuddy's specific refusals. */
68
+ private errorOf;
69
+ private readStream;
70
+ }