dsh-plugin-subscription-oauth 0.16.2 → 0.17.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.
- package/README.dev.md +1 -1
- package/README.md +7 -6
- package/README.zh.dev.md +1 -1
- package/README.zh.md +7 -6
- package/lib/auth/workbuddy-flow.d.ts +1 -1
- package/lib/auth/workbuddy-flow.js +14 -9
- package/lib/client/index.js +10 -1
- package/lib/client.js +6 -1
- package/lib/client.js.map +1 -1
- package/lib/index.js +21 -0
- package/lib/providers/antigravity.d.ts +34 -4
- package/lib/providers/antigravity.js +100 -28
- package/lib/providers/ark-provider.d.ts +10 -0
- package/lib/providers/ark-provider.js +6 -0
- package/lib/providers/ark.d.ts +96 -10
- package/lib/providers/ark.js +176 -48
- package/lib/providers/catalog.d.ts +91 -0
- package/lib/providers/catalog.js +190 -0
- package/lib/providers/common.d.ts +14 -0
- package/lib/providers/common.js +23 -0
- package/lib/providers/glm-provider.d.ts +10 -0
- package/lib/providers/glm-provider.js +6 -0
- package/lib/providers/glm.d.ts +92 -16
- package/lib/providers/glm.js +172 -85
- package/lib/providers/grok.d.ts +106 -27
- package/lib/providers/grok.js +312 -128
- package/lib/providers/kimi-provider.d.ts +10 -0
- package/lib/providers/kimi-provider.js +8 -3
- package/lib/providers/kimi.d.ts +104 -32
- package/lib/providers/kimi.js +198 -129
- package/lib/providers/mimo-provider.d.ts +14 -1
- package/lib/providers/mimo-provider.js +17 -6
- package/lib/providers/mimo.d.ts +164 -19
- package/lib/providers/mimo.js +318 -19
- package/lib/providers/model-cache.d.ts +57 -0
- package/lib/providers/model-cache.js +105 -0
- package/lib/providers/responses-stream.d.ts +8 -0
- package/lib/providers/responses-stream.js +21 -5
- package/lib/providers/workbuddy-provider.d.ts +17 -0
- package/lib/providers/workbuddy-provider.js +14 -6
- package/lib/providers/workbuddy.d.ts +140 -13
- package/lib/providers/workbuddy.js +520 -55
- package/package.json +1 -1
package/lib/providers/mimo.d.ts
CHANGED
|
@@ -2,14 +2,34 @@
|
|
|
2
2
|
* The Xiaomi MiMo wire adapter serving the `mimo` route.
|
|
3
3
|
*
|
|
4
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.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* session cookies the sign-on set — not a bearer token. The open platform's Token
|
|
6
|
+
* Plan spends its own `tp-` key at a separate endpoint; that fallback lives beside
|
|
7
|
+
* the account-level stream here.
|
|
8
|
+
*
|
|
9
|
+
* The model table this route publishes is MiMo's own listing, read once and kept in
|
|
10
|
+
* this plugin's cache (see `hydrate` and `fetchCatalog`), so a model released, renamed
|
|
11
|
+
* or retired server-side reaches the picker without a plugin update and without a
|
|
12
|
+
* request per refresh. `MIMO_MODELS` is the cold-start seed used while no listing has
|
|
13
|
+
* been read, the metadata overlay a live row is merged against, and the fallback a
|
|
14
|
+
* listing that does not answer leaves the picker with — never the authority on which
|
|
15
|
+
* models exist.
|
|
16
|
+
*
|
|
17
|
+
* Two bases are involved, and they are not the same surface:
|
|
18
|
+
* - the app surface, `<session.apiBase>/route/chat/completions`, which the stored
|
|
19
|
+
* session's cookies sign; and
|
|
20
|
+
* - the documented open-platform API at {@link MIMO_PUBLIC_API_BASE}, whose
|
|
21
|
+
* `/v1/models` route is documented to answer an OpenAI-style listing.
|
|
22
|
+
* The listing is therefore best-effort: it may name the public ids (`mimo-v2.5-pro`, …)
|
|
23
|
+
* rather than the app's (`mimo-pro`, …), and it may refuse the session credential
|
|
24
|
+
* outright. Either way the roster the seed describes stays until a listing answers.
|
|
25
|
+
* The endpoint is documented rather than probed from this machine — no MiMo account is
|
|
26
|
+
* stored here — so the first real read is the verification.
|
|
8
27
|
*
|
|
9
28
|
* @module dsh-plugin-subscription-oauth/providers/mimo
|
|
10
29
|
*/
|
|
11
30
|
import { LlmAdapter, type GenerateOptions, type LlmModelInfo, type LlmProviderInfo, type LlmResolvedModelInfo, type StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
12
31
|
import { type FetchFn } from '../http.js';
|
|
32
|
+
import type { CatalogEntry } from './catalog.js';
|
|
13
33
|
import type { TokenManager } from '../auth/tokens.js';
|
|
14
34
|
import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
|
|
15
35
|
/** The `mimo` provider route key. */
|
|
@@ -24,6 +44,17 @@ export declare const MIMO_CONTEXT_WINDOW = 1000000;
|
|
|
24
44
|
export declare const MIMO_DEFAULT_MAX_TOKENS = 128000;
|
|
25
45
|
/** The model the picker preselects. */
|
|
26
46
|
export declare const MIMO_DEFAULT_MODEL = "mimo-pro";
|
|
47
|
+
/**
|
|
48
|
+
* The documented open-platform API base.
|
|
49
|
+
*
|
|
50
|
+
* This is the host Xiaomi documents its List Models endpoint on, and it is *not* the
|
|
51
|
+
* app surface a stored session chats on (`{session.apiBase}/route/chat/completions`,
|
|
52
|
+
* per account and per region). The adapter's `apiBase` option overrides it, so a
|
|
53
|
+
* listing that turns out to live on another host needs no code change.
|
|
54
|
+
*/
|
|
55
|
+
export declare const MIMO_PUBLIC_API_BASE = "https://api.xiaomimimo.com";
|
|
56
|
+
/** Path appended to the base to list the models an account can reach. */
|
|
57
|
+
export declare const MIMO_MODELS_PATH = "/v1/models";
|
|
27
58
|
/** One signed-in MiMo account, as stored. */
|
|
28
59
|
export interface MimoSession {
|
|
29
60
|
accessToken: string;
|
|
@@ -42,45 +73,160 @@ export interface MimoSession {
|
|
|
42
73
|
}
|
|
43
74
|
/** Validate one stored value as a MiMo session, or undefined when it is not one. */
|
|
44
75
|
export declare function parseMimoSession(value: unknown): MimoSession | undefined;
|
|
45
|
-
/** One catalog row. */
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
76
|
+
/** One catalog row exposed as a selectable model. */
|
|
77
|
+
export type MimoModelEntry = CatalogEntry;
|
|
78
|
+
/**
|
|
79
|
+
* The MiMo *app* surface's models, as this build last knew them.
|
|
80
|
+
*
|
|
81
|
+
* A cold-start seed and a metadata overlay, not the catalog. It decides membership
|
|
82
|
+
* only while no listing has been read, it supplies the name, window and description a
|
|
83
|
+
* live row is silent about (`liveEntry` — a figure the listing states always wins),
|
|
84
|
+
* and it is what a listing that does not answer falls back to, so a refused probe can
|
|
85
|
+
* never empty the picker.
|
|
86
|
+
*
|
|
87
|
+
* Its ids (`mimo-pro`, …) belong to the app surface reached at `{session.apiBase}/route`.
|
|
88
|
+
* The documented public API is a different surface and may name different ids
|
|
89
|
+
* (`mimo-v2.5-pro`, …); a listing that returns those replaces this table rather than
|
|
90
|
+
* adding to it, which is the whole point of reading the listing at all.
|
|
91
|
+
*/
|
|
53
92
|
export declare const MIMO_MODELS: readonly MimoModelEntry[];
|
|
54
93
|
/** Constructor dependencies for {@link MimoAdapter}. */
|
|
55
94
|
export interface MimoAdapterOptions {
|
|
56
|
-
/**
|
|
95
|
+
/**
|
|
96
|
+
* Base the model listing is read from; defaults to {@link MIMO_PUBLIC_API_BASE}.
|
|
97
|
+
*
|
|
98
|
+
* The chat call does not use it: that one goes to the signed-in account's own
|
|
99
|
+
* `apiBase`, which is a per-region app host rather than the documented API.
|
|
100
|
+
*/
|
|
57
101
|
apiBase?: string;
|
|
102
|
+
/** Consult MiMo's own live model listing. Defaults to on. */
|
|
103
|
+
discovery?: boolean;
|
|
58
104
|
streamIdleTimeoutMs: number;
|
|
59
105
|
tokens: TokenManager<MimoSession>;
|
|
60
106
|
onWarn?: (message: string) => void;
|
|
61
107
|
resolveAttachments?: () => AttachmentStore | undefined;
|
|
62
108
|
hiddenModels?: () => Promise<ReadonlySet<string>>;
|
|
63
109
|
userModels?: () => Promise<readonly string[]>;
|
|
110
|
+
/**
|
|
111
|
+
* MiMo's last live listing, from this plugin's cache.
|
|
112
|
+
*
|
|
113
|
+
* Read once per session, before the first catalog query, so the picker offers real
|
|
114
|
+
* models without asking the endpoint anything. A route that fetched once and cached
|
|
115
|
+
* the answer has no reason to fetch again on every refresh; the Settings "refresh
|
|
116
|
+
* models" action is what re-reads it.
|
|
117
|
+
*/
|
|
118
|
+
cachedListing?: () => Promise<unknown>;
|
|
119
|
+
/** Record what a live listing just produced, replacing the cached table. */
|
|
120
|
+
saveListing?: (payload: unknown) => Promise<void>;
|
|
64
121
|
fetchFn?: FetchFn;
|
|
65
122
|
}
|
|
66
123
|
/** The MiMo wire adapter serving the `mimo` route. */
|
|
67
124
|
export declare class MimoAdapter extends LlmAdapter {
|
|
68
125
|
private readonly options;
|
|
69
126
|
private readonly fetchFn;
|
|
127
|
+
/**
|
|
128
|
+
* The catalog this route serves: entries derived from MiMo's own listing, either
|
|
129
|
+
* the one just read or the one the cache kept from the last read.
|
|
130
|
+
*
|
|
131
|
+
* Undefined means no listing has been read (yet), so {@link MIMO_MODELS} decides
|
|
132
|
+
* membership and the picker is never empty.
|
|
133
|
+
*/
|
|
134
|
+
private live;
|
|
135
|
+
/** Whether the cached listing has been read; it is read at most once per session. */
|
|
136
|
+
private hydrated;
|
|
137
|
+
/** Ids the user added by hand, cached so the sync catalog can name them. */
|
|
138
|
+
private userAddedIds;
|
|
139
|
+
/**
|
|
140
|
+
* Listing failures already reported, so one reason is stated once per session.
|
|
141
|
+
*
|
|
142
|
+
* A listing that does not answer is expected — the documented public API and the
|
|
143
|
+
* app surface are different hosts — and saying so on every refresh would drown out
|
|
144
|
+
* the warnings a user can act on.
|
|
145
|
+
*/
|
|
146
|
+
private readonly reportedListingFailures;
|
|
70
147
|
constructor(options: MimoAdapterOptions);
|
|
148
|
+
/**
|
|
149
|
+
* Adopt the rows of one live listing as this route's catalog.
|
|
150
|
+
*
|
|
151
|
+
* Everything the picker shows is derived here from MiMo's own answer: the ids it
|
|
152
|
+
* names, in its own order, with the name, window, modalities and ladder it states.
|
|
153
|
+
* {@link MIMO_MODELS} is consulted only as the metadata overlay for a figure the
|
|
154
|
+
* row is silent about, so a live figure is never overridden by a shipped guess.
|
|
155
|
+
*
|
|
156
|
+
* @param rows - decoded rows of the listing, in the listing's order.
|
|
157
|
+
* @returns the entries to publish, in picker order.
|
|
158
|
+
*/
|
|
159
|
+
private adoptListing;
|
|
160
|
+
/**
|
|
161
|
+
* Read the cached listing, once, so the picker works without asking the endpoint.
|
|
162
|
+
*
|
|
163
|
+
* Called before the first catalog query. A read failure is a cache miss, never a
|
|
164
|
+
* failure: the route simply has no listing until one is fetched, and the seed roster
|
|
165
|
+
* is what the picker shows in the meantime.
|
|
166
|
+
*/
|
|
167
|
+
hydrate(): Promise<void>;
|
|
71
168
|
providerInfo(provider: string): LlmProviderInfo;
|
|
72
169
|
listModels(_provider: string): Promise<readonly LlmModelInfo[]>;
|
|
73
170
|
private hidden;
|
|
74
171
|
resolveModel(_provider: string, model: string): Promise<LlmResolvedModelInfo>;
|
|
75
172
|
providerRetryPolicy(): undefined;
|
|
76
|
-
/** The catalog the Settings list reads:
|
|
77
|
-
catalogEntries(): readonly
|
|
78
|
-
id: string;
|
|
79
|
-
name: string;
|
|
80
|
-
contextWindow: number;
|
|
81
|
-
}[];
|
|
173
|
+
/** The catalog the Settings list reads: the route's own entries plus hand-added ids. */
|
|
174
|
+
catalogEntries(): readonly MimoModelEntry[];
|
|
82
175
|
/** The ids the route itself serves, without hand-added extras. */
|
|
83
176
|
ownCatalogEntries(): readonly MimoModelEntry[];
|
|
177
|
+
/**
|
|
178
|
+
* The catalog served: the membership the listing named — or the seed roster while no
|
|
179
|
+
* listing has been read — each merged with the metadata of the source that described
|
|
180
|
+
* it, then the ids the user added by hand.
|
|
181
|
+
*/
|
|
182
|
+
private catalog;
|
|
183
|
+
/**
|
|
184
|
+
* What the route itself serves, without the ids added by hand.
|
|
185
|
+
*
|
|
186
|
+
* No model is injected here: the configured default is an app-surface id, and the
|
|
187
|
+
* listing may come from the public API, where that id does not exist. What the
|
|
188
|
+
* listing names is exactly what the picker offers; a configured default that the
|
|
189
|
+
* listing does not name stays dispatchable through {@link resolveModel}.
|
|
190
|
+
*/
|
|
191
|
+
private ownCatalog;
|
|
192
|
+
/** Entries for ids the user added by hand that the route does not already serve. */
|
|
193
|
+
private addedEntries;
|
|
194
|
+
/** Re-read the ids the user added by hand. */
|
|
195
|
+
private syncUserModels;
|
|
196
|
+
/**
|
|
197
|
+
* Adopt the ids a caller reports as the served set, without a listing.
|
|
198
|
+
*
|
|
199
|
+
* {@link fetchCatalog} already derived the entries from the rows it read, and the
|
|
200
|
+
* controller reports those same ids back here; the entries it recorded are kept, so
|
|
201
|
+
* a reported id keeps the metadata the listing gave it. A caller that only has ids (a
|
|
202
|
+
* test, another discovery path) gets a conservative entry overlaid with the seed.
|
|
203
|
+
*
|
|
204
|
+
* @param ids - model ids the caller reports.
|
|
205
|
+
*/
|
|
206
|
+
discover(ids: readonly string[]): void;
|
|
207
|
+
/**
|
|
208
|
+
* Read MiMo's model listing and adopt it as this route's catalog.
|
|
209
|
+
*
|
|
210
|
+
* The documented endpoint is `GET <apiBase>/v1/models`, answering an OpenAI-style
|
|
211
|
+
* `{ object: 'list', data: [{ id, object, owned_by }] }`. Xiaomi documents either an
|
|
212
|
+
* `api-key` header or Bearer authentication; this sends the bearer for the account
|
|
213
|
+
* the caller resolved, plus the client identity the chat call uses.
|
|
214
|
+
*
|
|
215
|
+
* The read is best-effort by design. The documented public API is not the app surface
|
|
216
|
+
* the chat call goes to, so the answer may be a refusal, a body in another shape, or
|
|
217
|
+
* rows naming ids the app does not serve; in every one of those cases the roster
|
|
218
|
+
* already published stays exactly as it was, the failure is stated once through
|
|
219
|
+
* {@link MimoAdapterOptions.onWarn}, and the picker is never emptied by a probe.
|
|
220
|
+
*
|
|
221
|
+
* @param accessToken - bearer for the account, as the controller resolved it.
|
|
222
|
+
* @param signal - optional cancellation.
|
|
223
|
+
* @returns the model ids now served, or undefined when the listing is unavailable.
|
|
224
|
+
*/
|
|
225
|
+
fetchCatalog(accessToken: string, signal?: AbortSignal): Promise<readonly string[] | undefined>;
|
|
226
|
+
/** The base the listing is read from, without a trailing slash. */
|
|
227
|
+
private listingBase;
|
|
228
|
+
/** State one listing failure, once per distinct reason. */
|
|
229
|
+
private warnListingFailure;
|
|
84
230
|
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
85
231
|
/** The app's names for its models, as the Token Plan's endpoint names them. */
|
|
86
232
|
private static readonly TP_MODELS;
|
|
@@ -103,4 +249,3 @@ export declare class MimoAdapter extends LlmAdapter {
|
|
|
103
249
|
private toChatMessage;
|
|
104
250
|
private readStream;
|
|
105
251
|
}
|
|
106
|
-
export {};
|
package/lib/providers/mimo.js
CHANGED
|
@@ -2,15 +2,35 @@
|
|
|
2
2
|
* The Xiaomi MiMo wire adapter serving the `mimo` route.
|
|
3
3
|
*
|
|
4
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.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* session cookies the sign-on set — not a bearer token. The open platform's Token
|
|
6
|
+
* Plan spends its own `tp-` key at a separate endpoint; that fallback lives beside
|
|
7
|
+
* the account-level stream here.
|
|
8
|
+
*
|
|
9
|
+
* The model table this route publishes is MiMo's own listing, read once and kept in
|
|
10
|
+
* this plugin's cache (see `hydrate` and `fetchCatalog`), so a model released, renamed
|
|
11
|
+
* or retired server-side reaches the picker without a plugin update and without a
|
|
12
|
+
* request per refresh. `MIMO_MODELS` is the cold-start seed used while no listing has
|
|
13
|
+
* been read, the metadata overlay a live row is merged against, and the fallback a
|
|
14
|
+
* listing that does not answer leaves the picker with — never the authority on which
|
|
15
|
+
* models exist.
|
|
16
|
+
*
|
|
17
|
+
* Two bases are involved, and they are not the same surface:
|
|
18
|
+
* - the app surface, `<session.apiBase>/route/chat/completions`, which the stored
|
|
19
|
+
* session's cookies sign; and
|
|
20
|
+
* - the documented open-platform API at {@link MIMO_PUBLIC_API_BASE}, whose
|
|
21
|
+
* `/v1/models` route is documented to answer an OpenAI-style listing.
|
|
22
|
+
* The listing is therefore best-effort: it may name the public ids (`mimo-v2.5-pro`, …)
|
|
23
|
+
* rather than the app's (`mimo-pro`, …), and it may refuse the session credential
|
|
24
|
+
* outright. Either way the roster the seed describes stays until a listing answers.
|
|
25
|
+
* The endpoint is documented rather than probed from this machine — no MiMo account is
|
|
26
|
+
* stored here — so the first real read is the verification.
|
|
8
27
|
*
|
|
9
28
|
* @module dsh-plugin-subscription-oauth/providers/mimo
|
|
10
29
|
*/
|
|
11
30
|
import { LlmAdapter, errorChain, } from '@deepseek-ai/dsh-llm';
|
|
12
31
|
import { httpLlmError, isMissingOrInvalidCredential, mapFetchFailure, idleWatchdog } from './common.js';
|
|
13
32
|
import { proxiedFetch } from '../http.js';
|
|
33
|
+
import { composeCatalog, fallbackEntry, findEntry, liveEntry, liveModelFacts, resolveCatalog } from './catalog.js';
|
|
14
34
|
import { readMimoPlanKey, refreshMimoSession } from '../auth/mimo-flow.js';
|
|
15
35
|
/** The `mimo` provider route key. */
|
|
16
36
|
export const MIMO_PROVIDER_ID = 'mimo';
|
|
@@ -24,6 +44,30 @@ export const MIMO_CONTEXT_WINDOW = 1_000_000;
|
|
|
24
44
|
export const MIMO_DEFAULT_MAX_TOKENS = 128_000;
|
|
25
45
|
/** The model the picker preselects. */
|
|
26
46
|
export const MIMO_DEFAULT_MODEL = 'mimo-pro';
|
|
47
|
+
/**
|
|
48
|
+
* The documented open-platform API base.
|
|
49
|
+
*
|
|
50
|
+
* This is the host Xiaomi documents its List Models endpoint on, and it is *not* the
|
|
51
|
+
* app surface a stored session chats on (`{session.apiBase}/route/chat/completions`,
|
|
52
|
+
* per account and per region). The adapter's `apiBase` option overrides it, so a
|
|
53
|
+
* listing that turns out to live on another host needs no code change.
|
|
54
|
+
*/
|
|
55
|
+
export const MIMO_PUBLIC_API_BASE = 'https://api.xiaomimimo.com';
|
|
56
|
+
/** Path appended to the base to list the models an account can reach. */
|
|
57
|
+
export const MIMO_MODELS_PATH = '/v1/models';
|
|
58
|
+
/**
|
|
59
|
+
* Client identity the MiMo app sends, shared by the chat call and the listing read.
|
|
60
|
+
*
|
|
61
|
+
* The app gateway recognizes its own client; sending the same words on the listing
|
|
62
|
+
* costs nothing and keeps the two requests from drifting apart.
|
|
63
|
+
*/
|
|
64
|
+
const MIMO_CLIENT_HEADERS = {
|
|
65
|
+
'X-Mimo-Source': 'mimocode-cli-free',
|
|
66
|
+
'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',
|
|
67
|
+
'X-Client-Version': '26.929.292248',
|
|
68
|
+
};
|
|
69
|
+
/** Description shown for a model only the listing names. */
|
|
70
|
+
const MIMO_LISTING_DESCRIPTION = 'Reported by the MiMo model list';
|
|
27
71
|
/** Validate one stored value as a MiMo session, or undefined when it is not one. */
|
|
28
72
|
export function parseMimoSession(value) {
|
|
29
73
|
if (typeof value !== 'object' || value === null)
|
|
@@ -51,33 +95,120 @@ export function parseMimoSession(value) {
|
|
|
51
95
|
...typeof record.region === 'string' ? { region: record.region } : {},
|
|
52
96
|
};
|
|
53
97
|
}
|
|
54
|
-
/**
|
|
98
|
+
/**
|
|
99
|
+
* The MiMo *app* surface's models, as this build last knew them.
|
|
100
|
+
*
|
|
101
|
+
* A cold-start seed and a metadata overlay, not the catalog. It decides membership
|
|
102
|
+
* only while no listing has been read, it supplies the name, window and description a
|
|
103
|
+
* live row is silent about (`liveEntry` — a figure the listing states always wins),
|
|
104
|
+
* and it is what a listing that does not answer falls back to, so a refused probe can
|
|
105
|
+
* never empty the picker.
|
|
106
|
+
*
|
|
107
|
+
* Its ids (`mimo-pro`, …) belong to the app surface reached at `{session.apiBase}/route`.
|
|
108
|
+
* The documented public API is a different surface and may name different ids
|
|
109
|
+
* (`mimo-v2.5-pro`, …); a listing that returns those replaces this table rather than
|
|
110
|
+
* adding to it, which is the whole point of reading the listing at all.
|
|
111
|
+
*/
|
|
55
112
|
export const MIMO_MODELS = [
|
|
56
|
-
{
|
|
57
|
-
|
|
113
|
+
{
|
|
114
|
+
id: 'mimo-pro',
|
|
115
|
+
name: 'MiMo Pro',
|
|
116
|
+
description: 'Flagship MiMo model',
|
|
117
|
+
contextWindow: MIMO_CONTEXT_WINDOW,
|
|
118
|
+
inputModalities: ['text', 'image'],
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: 'mimo-flash',
|
|
122
|
+
name: 'MiMo Flash',
|
|
123
|
+
description: 'Fast MiMo model',
|
|
124
|
+
contextWindow: MIMO_CONTEXT_WINDOW,
|
|
125
|
+
inputModalities: ['text', 'image'],
|
|
126
|
+
},
|
|
58
127
|
];
|
|
59
128
|
/** The MiMo wire adapter serving the `mimo` route. */
|
|
60
129
|
export class MimoAdapter extends LlmAdapter {
|
|
61
130
|
options;
|
|
62
131
|
fetchFn;
|
|
132
|
+
/**
|
|
133
|
+
* The catalog this route serves: entries derived from MiMo's own listing, either
|
|
134
|
+
* the one just read or the one the cache kept from the last read.
|
|
135
|
+
*
|
|
136
|
+
* Undefined means no listing has been read (yet), so {@link MIMO_MODELS} decides
|
|
137
|
+
* membership and the picker is never empty.
|
|
138
|
+
*/
|
|
139
|
+
live;
|
|
140
|
+
/** Whether the cached listing has been read; it is read at most once per session. */
|
|
141
|
+
hydrated = false;
|
|
142
|
+
/** Ids the user added by hand, cached so the sync catalog can name them. */
|
|
143
|
+
userAddedIds = [];
|
|
144
|
+
/**
|
|
145
|
+
* Listing failures already reported, so one reason is stated once per session.
|
|
146
|
+
*
|
|
147
|
+
* A listing that does not answer is expected — the documented public API and the
|
|
148
|
+
* app surface are different hosts — and saying so on every refresh would drown out
|
|
149
|
+
* the warnings a user can act on.
|
|
150
|
+
*/
|
|
151
|
+
reportedListingFailures = new Set();
|
|
63
152
|
constructor(options) {
|
|
64
153
|
super();
|
|
65
154
|
this.options = options;
|
|
66
155
|
this.fetchFn = options.fetchFn ?? proxiedFetch;
|
|
67
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Adopt the rows of one live listing as this route's catalog.
|
|
159
|
+
*
|
|
160
|
+
* Everything the picker shows is derived here from MiMo's own answer: the ids it
|
|
161
|
+
* names, in its own order, with the name, window, modalities and ladder it states.
|
|
162
|
+
* {@link MIMO_MODELS} is consulted only as the metadata overlay for a figure the
|
|
163
|
+
* row is silent about, so a live figure is never overridden by a shipped guess.
|
|
164
|
+
*
|
|
165
|
+
* @param rows - decoded rows of the listing, in the listing's order.
|
|
166
|
+
* @returns the entries to publish, in picker order.
|
|
167
|
+
*/
|
|
168
|
+
adoptListing(rows) {
|
|
169
|
+
return rows
|
|
170
|
+
.map((row) => liveModelFacts(row))
|
|
171
|
+
.filter((facts) => facts !== undefined)
|
|
172
|
+
.map((facts) => liveEntry(facts, MIMO_MODELS, MIMO_CONTEXT_WINDOW, MIMO_LISTING_DESCRIPTION));
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Read the cached listing, once, so the picker works without asking the endpoint.
|
|
176
|
+
*
|
|
177
|
+
* Called before the first catalog query. A read failure is a cache miss, never a
|
|
178
|
+
* failure: the route simply has no listing until one is fetched, and the seed roster
|
|
179
|
+
* is what the picker shows in the meantime.
|
|
180
|
+
*/
|
|
181
|
+
async hydrate() {
|
|
182
|
+
if (this.hydrated)
|
|
183
|
+
return;
|
|
184
|
+
this.hydrated = true;
|
|
185
|
+
if (this.live !== undefined || this.options.cachedListing === undefined)
|
|
186
|
+
return;
|
|
187
|
+
try {
|
|
188
|
+
const rows = listingRows(await this.options.cachedListing());
|
|
189
|
+
// A live read that landed while the cache was being read is newer than the cache.
|
|
190
|
+
if (rows.length > 0 && this.live === undefined)
|
|
191
|
+
this.live = this.adoptListing(rows);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
this.options.onWarn?.(`cached MiMo model catalog unavailable (${errorChain(error)})`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
68
197
|
providerInfo(provider) {
|
|
69
198
|
return { id: provider, name: MIMO_PROVIDER_NAME };
|
|
70
199
|
}
|
|
71
200
|
async listModels(_provider) {
|
|
72
201
|
if (await this.options.tokens.session() === undefined)
|
|
73
202
|
return [];
|
|
203
|
+
await this.hydrate();
|
|
204
|
+
await this.syncUserModels();
|
|
74
205
|
const hidden = await this.hidden();
|
|
75
|
-
return
|
|
206
|
+
return resolveCatalog(this.catalog(), [], hidden).entries.map((entry) => ({
|
|
76
207
|
provider: MIMO_PROVIDER_ID,
|
|
77
208
|
id: entry.id,
|
|
78
209
|
name: entry.name,
|
|
79
|
-
description: entry.description,
|
|
80
|
-
inputModalities:
|
|
210
|
+
...entry.description === undefined ? {} : { description: entry.description },
|
|
211
|
+
...entry.inputModalities === undefined ? {} : { inputModalities: entry.inputModalities },
|
|
81
212
|
}));
|
|
82
213
|
}
|
|
83
214
|
async hidden() {
|
|
@@ -91,8 +222,13 @@ export class MimoAdapter extends LlmAdapter {
|
|
|
91
222
|
}
|
|
92
223
|
}
|
|
93
224
|
async resolveModel(_provider, model) {
|
|
94
|
-
|
|
225
|
+
await this.hydrate();
|
|
226
|
+
await this.syncUserModels();
|
|
227
|
+
const entry = findEntry(this.catalog(), model);
|
|
95
228
|
if (entry === undefined) {
|
|
229
|
+
// An id this build has never heard of is still dispatchable: the endpoint is the
|
|
230
|
+
// authority on what it serves, and a model released after this build should not
|
|
231
|
+
// be refused. It inherits the advertised window and the app's modalities.
|
|
96
232
|
return {
|
|
97
233
|
provider: MIMO_PROVIDER_ID,
|
|
98
234
|
id: model,
|
|
@@ -106,22 +242,162 @@ export class MimoAdapter extends LlmAdapter {
|
|
|
106
242
|
provider: MIMO_PROVIDER_ID,
|
|
107
243
|
id: entry.id,
|
|
108
244
|
name: entry.name,
|
|
109
|
-
description: entry.description,
|
|
245
|
+
...entry.description === undefined ? {} : { description: entry.description },
|
|
110
246
|
context: { contextWindow: entry.contextWindow },
|
|
111
247
|
defaultMaxTokens: MIMO_DEFAULT_MAX_TOKENS,
|
|
112
|
-
inputModalities:
|
|
248
|
+
...entry.inputModalities === undefined ? {} : { inputModalities: entry.inputModalities },
|
|
113
249
|
};
|
|
114
250
|
}
|
|
115
251
|
providerRetryPolicy() {
|
|
116
252
|
return undefined;
|
|
117
253
|
}
|
|
118
|
-
/** The catalog the Settings list reads:
|
|
254
|
+
/** The catalog the Settings list reads: the route's own entries plus hand-added ids. */
|
|
119
255
|
catalogEntries() {
|
|
120
|
-
return
|
|
256
|
+
return this.catalog();
|
|
121
257
|
}
|
|
122
258
|
/** The ids the route itself serves, without hand-added extras. */
|
|
123
259
|
ownCatalogEntries() {
|
|
124
|
-
return
|
|
260
|
+
return this.ownCatalog();
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* The catalog served: the membership the listing named — or the seed roster while no
|
|
264
|
+
* listing has been read — each merged with the metadata of the source that described
|
|
265
|
+
* it, then the ids the user added by hand.
|
|
266
|
+
*/
|
|
267
|
+
catalog() {
|
|
268
|
+
return [...this.ownCatalog(), ...this.addedEntries()];
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* What the route itself serves, without the ids added by hand.
|
|
272
|
+
*
|
|
273
|
+
* No model is injected here: the configured default is an app-surface id, and the
|
|
274
|
+
* listing may come from the public API, where that id does not exist. What the
|
|
275
|
+
* listing names is exactly what the picker offers; a configured default that the
|
|
276
|
+
* listing does not name stays dispatchable through {@link resolveModel}.
|
|
277
|
+
*/
|
|
278
|
+
ownCatalog() {
|
|
279
|
+
return composeCatalog({
|
|
280
|
+
curated: MIMO_MODELS,
|
|
281
|
+
...this.live === undefined ? {} : { live: this.live },
|
|
282
|
+
fallbackContextWindow: MIMO_CONTEXT_WINDOW,
|
|
283
|
+
liveDescription: MIMO_LISTING_DESCRIPTION,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
/** Entries for ids the user added by hand that the route does not already serve. */
|
|
287
|
+
addedEntries() {
|
|
288
|
+
if (this.userAddedIds.length === 0)
|
|
289
|
+
return [];
|
|
290
|
+
const known = new Set(this.ownCatalog().map((entry) => entry.id));
|
|
291
|
+
return this.userAddedIds
|
|
292
|
+
.filter((id) => !known.has(id))
|
|
293
|
+
.map((id) => fallbackEntry(id, 'Added by hand in Settings', MIMO_CONTEXT_WINDOW));
|
|
294
|
+
}
|
|
295
|
+
/** Re-read the ids the user added by hand. */
|
|
296
|
+
async syncUserModels() {
|
|
297
|
+
if (this.options.userModels === undefined)
|
|
298
|
+
return;
|
|
299
|
+
try {
|
|
300
|
+
this.userAddedIds = await this.options.userModels();
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
this.options.onWarn?.(`user-added models unavailable (${errorChain(error)})`);
|
|
304
|
+
this.userAddedIds = [];
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Adopt the ids a caller reports as the served set, without a listing.
|
|
309
|
+
*
|
|
310
|
+
* {@link fetchCatalog} already derived the entries from the rows it read, and the
|
|
311
|
+
* controller reports those same ids back here; the entries it recorded are kept, so
|
|
312
|
+
* a reported id keeps the metadata the listing gave it. A caller that only has ids (a
|
|
313
|
+
* test, another discovery path) gets a conservative entry overlaid with the seed.
|
|
314
|
+
*
|
|
315
|
+
* @param ids - model ids the caller reports.
|
|
316
|
+
*/
|
|
317
|
+
discover(ids) {
|
|
318
|
+
const known = new Map((this.live ?? []).map((entry) => [entry.id, entry]));
|
|
319
|
+
this.live = ids.map((id) => known.get(id)
|
|
320
|
+
?? liveEntry({ id }, MIMO_MODELS, MIMO_CONTEXT_WINDOW, MIMO_LISTING_DESCRIPTION));
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Read MiMo's model listing and adopt it as this route's catalog.
|
|
324
|
+
*
|
|
325
|
+
* The documented endpoint is `GET <apiBase>/v1/models`, answering an OpenAI-style
|
|
326
|
+
* `{ object: 'list', data: [{ id, object, owned_by }] }`. Xiaomi documents either an
|
|
327
|
+
* `api-key` header or Bearer authentication; this sends the bearer for the account
|
|
328
|
+
* the caller resolved, plus the client identity the chat call uses.
|
|
329
|
+
*
|
|
330
|
+
* The read is best-effort by design. The documented public API is not the app surface
|
|
331
|
+
* the chat call goes to, so the answer may be a refusal, a body in another shape, or
|
|
332
|
+
* rows naming ids the app does not serve; in every one of those cases the roster
|
|
333
|
+
* already published stays exactly as it was, the failure is stated once through
|
|
334
|
+
* {@link MimoAdapterOptions.onWarn}, and the picker is never emptied by a probe.
|
|
335
|
+
*
|
|
336
|
+
* @param accessToken - bearer for the account, as the controller resolved it.
|
|
337
|
+
* @param signal - optional cancellation.
|
|
338
|
+
* @returns the model ids now served, or undefined when the listing is unavailable.
|
|
339
|
+
*/
|
|
340
|
+
async fetchCatalog(accessToken, signal) {
|
|
341
|
+
if (this.options.discovery === false)
|
|
342
|
+
return undefined;
|
|
343
|
+
try {
|
|
344
|
+
const response = await this.fetchFn(`${this.listingBase()}${MIMO_MODELS_PATH}`, {
|
|
345
|
+
headers: {
|
|
346
|
+
'authorization': `Bearer ${accessToken}`,
|
|
347
|
+
'accept': 'application/json',
|
|
348
|
+
...MIMO_CLIENT_HEADERS,
|
|
349
|
+
},
|
|
350
|
+
...signal === undefined ? {} : { signal },
|
|
351
|
+
});
|
|
352
|
+
if (!response.ok) {
|
|
353
|
+
this.warnListingFailure(`Xiaomi MiMo model list unavailable (HTTP ${response.status}); keeping the built-in roster`);
|
|
354
|
+
return undefined;
|
|
355
|
+
}
|
|
356
|
+
let payload;
|
|
357
|
+
try {
|
|
358
|
+
payload = await response.json();
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
this.warnListingFailure(`Xiaomi MiMo model list could not be read (${errorChain(error)}); keeping the built-in roster`);
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
const rows = listingRows(payload);
|
|
365
|
+
const entries = this.adoptListing(rows);
|
|
366
|
+
if (entries.length === 0) {
|
|
367
|
+
this.warnListingFailure('Xiaomi MiMo model list named no usable models; keeping the built-in roster');
|
|
368
|
+
return undefined;
|
|
369
|
+
}
|
|
370
|
+
this.live = entries;
|
|
371
|
+
// The cached table is the endpoint's answer, kept so the next session does not ask
|
|
372
|
+
// again. A cache failure never fails the read that produced it.
|
|
373
|
+
if (this.options.saveListing !== undefined) {
|
|
374
|
+
try {
|
|
375
|
+
await this.options.saveListing(rows);
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
this.options.onWarn?.(`model catalog cache write failed (${errorChain(error)})`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return entries.map((entry) => entry.id);
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
if (signal?.aborted === true)
|
|
385
|
+
return undefined;
|
|
386
|
+
this.warnListingFailure(`Xiaomi MiMo model list unavailable; keeping the built-in roster (${errorChain(error)})`);
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/** The base the listing is read from, without a trailing slash. */
|
|
391
|
+
listingBase() {
|
|
392
|
+
const configured = this.options.apiBase?.trim();
|
|
393
|
+
return (configured === undefined || configured === '' ? MIMO_PUBLIC_API_BASE : configured).replace(/\/+$/, '');
|
|
394
|
+
}
|
|
395
|
+
/** State one listing failure, once per distinct reason. */
|
|
396
|
+
warnListingFailure(message) {
|
|
397
|
+
if (this.reportedListingFailures.has(message))
|
|
398
|
+
return;
|
|
399
|
+
this.reportedListingFailures.add(message);
|
|
400
|
+
this.options.onWarn?.(message);
|
|
125
401
|
}
|
|
126
402
|
async *stream(options) {
|
|
127
403
|
const held = await this.options.tokens.session();
|
|
@@ -138,9 +414,7 @@ export class MimoAdapter extends LlmAdapter {
|
|
|
138
414
|
'Content-Type': 'application/json',
|
|
139
415
|
'Accept': 'text/event-stream',
|
|
140
416
|
'Cookie': cookies,
|
|
141
|
-
|
|
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',
|
|
417
|
+
...MIMO_CLIENT_HEADERS,
|
|
144
418
|
},
|
|
145
419
|
body: JSON.stringify(body),
|
|
146
420
|
signal: watchdog.signal,
|
|
@@ -271,6 +545,10 @@ export class MimoAdapter extends LlmAdapter {
|
|
|
271
545
|
const decoder = new TextDecoder();
|
|
272
546
|
let buffer = '';
|
|
273
547
|
let started = false;
|
|
548
|
+
// `block-end` carries the *assembled* block, and the harness takes that one as the
|
|
549
|
+
// block's final content - it does not keep what the deltas accumulated. An empty text
|
|
550
|
+
// there renders the whole answer as a blank body, whatever streamed before it.
|
|
551
|
+
let text = '';
|
|
274
552
|
try {
|
|
275
553
|
while (true) {
|
|
276
554
|
const { done, value } = await reader.read();
|
|
@@ -301,6 +579,7 @@ export class MimoAdapter extends LlmAdapter {
|
|
|
301
579
|
yield { type: 'block-start', index: 0, blockType: 'text' };
|
|
302
580
|
started = true;
|
|
303
581
|
}
|
|
582
|
+
text += delta;
|
|
304
583
|
yield { type: 'text-delta', index: 0, text: delta };
|
|
305
584
|
}
|
|
306
585
|
const usage = chunk.usage;
|
|
@@ -314,8 +593,10 @@ export class MimoAdapter extends LlmAdapter {
|
|
|
314
593
|
}
|
|
315
594
|
}
|
|
316
595
|
}
|
|
596
|
+
// Close the text block with what streamed into it: `block-end` is the block the
|
|
597
|
+
// harness keeps, so an empty one would drop the whole answer.
|
|
317
598
|
if (started)
|
|
318
|
-
yield { type: 'block-end', index: 0, block: { type: 'text', text
|
|
599
|
+
yield { type: 'block-end', index: 0, block: { type: 'text', text } };
|
|
319
600
|
yield { type: 'finish', reason: { kind: 'stop' } };
|
|
320
601
|
}
|
|
321
602
|
finally {
|
|
@@ -323,3 +604,21 @@ export class MimoAdapter extends LlmAdapter {
|
|
|
323
604
|
}
|
|
324
605
|
}
|
|
325
606
|
}
|
|
607
|
+
/**
|
|
608
|
+
* The rows of one listing payload.
|
|
609
|
+
*
|
|
610
|
+
* MiMo's documented answer is an OpenAI-style envelope (`{ object: 'list', data: [...] }`);
|
|
611
|
+
* a bare array is read too, so a cache written in either shape — or a host that answers
|
|
612
|
+
* the rows directly — cannot silently empty the picker.
|
|
613
|
+
*
|
|
614
|
+
* @param payload - a listing response body or a cached listing.
|
|
615
|
+
* @returns the rows to adopt, or an empty list when the payload names none.
|
|
616
|
+
*/
|
|
617
|
+
function listingRows(payload) {
|
|
618
|
+
if (Array.isArray(payload))
|
|
619
|
+
return payload;
|
|
620
|
+
const data = typeof payload === 'object' && payload !== null
|
|
621
|
+
? payload.data
|
|
622
|
+
: undefined;
|
|
623
|
+
return Array.isArray(data) ? data : [];
|
|
624
|
+
}
|