dsh-connect-agnes-token-plan 0.5.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,143 @@
1
+ import { a as name } from "./host-config-BNGgm_j7.js";
2
+ import { i as raccoonRoster, n as RACCOON_PROVIDER_ID, r as buildRaccoonDescriptors, t as RACCOON_DISPLAY_NAME } from "./raccoon-models-DV1sQN4n.js";
3
+ import { n as buildRetryPolicyConfig, t as reclassifyStream } from "./llm-error-fix-DQUng8AL.js";
4
+ import { createProvider } from "@earendil-works/pi-ai";
5
+ import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
6
+ import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
7
+ import { resolveImageAttachmentAccess, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
8
+
9
+ //#region src/host/raccoon-llm-adapter.ts
10
+ /**
11
+ * The peer-dependent half of the directly-registered Raccoon provider —
12
+ * "second upstream provider" (ROADMAP §6.1).
13
+ *
14
+ * Mirrors `llm-adapter.ts`'s shape: ONE `PiAiAdapter` carrying ONE profile
15
+ * (`sensenova-raccoon` → `https://xiaohuanxiong.com/api/web/llm/v2`), an
16
+ * INERT pi-ai auth plane (the Raccoon JWT is resolved per request from the
17
+ * plugin's own credential store, pi-ai never manufactures it), both IMAGE
18
+ * hooks wired (a vision model that receives an image answers
19
+ * `UNSUPPORTED_CONTENT` otherwise), and no token baked into the profile —
20
+ * the picker advertises models without one and a request fails at resolve
21
+ * time, where the panel status is visible.
22
+ *
23
+ * The descriptor mapping lives in the peer-free `raccoon-models.ts`, so this
24
+ * module holds only assembly against the runtime and is exercised by the
25
+ * wiring/e2e checks, exactly as `llm-adapter.ts` is.
26
+ *
27
+ * @module dsh-connect-agnes-token-plan/raccoon-llm-adapter
28
+ */
29
+ /** Idle ceiling while one stream read is outstanding (dsh-llm-pi-ai default). */
30
+ const STREAM_IDLE_TIMEOUT_MS = 3e5;
31
+ /**
32
+ * Image budgets at the dsh-llm-pi-ai defaults. The Raccoon gateway caps one
33
+ * request body at ~10 MB (`HTTP_413`), so the per-image byte ceiling is the
34
+ * binding constraint; the pixel budget keeps an image inside it.
35
+ */
36
+ const REQUEST_IMAGE_BUDGETS = {
37
+ maxRequestImageBytes: 20971520,
38
+ requestImagePixelBudget: 64e4,
39
+ requestImageMaxBytes: 1048576
40
+ };
41
+ /**
42
+ * Inert pi-ai auth plane.
43
+ *
44
+ * Authentication goes through `resolveToken` (the stored Raccoon JWT, read
45
+ * per request from `raccoon-store.ts`) — pi-ai's own credential lifecycle
46
+ * must never manufacture a credential for this route, so every ambient
47
+ * question answers "nothing stored, nothing set".
48
+ */
49
+ const INERT_AUTH = {
50
+ credentials: {
51
+ async read() {},
52
+ async list() {
53
+ return [];
54
+ },
55
+ async modify() {},
56
+ async delete() {}
57
+ },
58
+ authContext: {
59
+ async env() {},
60
+ async fileExists() {
61
+ return false;
62
+ }
63
+ }
64
+ };
65
+ /**
66
+ * Assemble the Raccoon adapter for one credential/catalogue snapshot.
67
+ *
68
+ * A fresh instance per rebuild is deliberate: `PiAiAdapter` memoizes the
69
+ * profiles snapshot internally, so the caller REPLACES the registered
70
+ * adapter when the credential or catalogue changes and emits
71
+ * `llm/adapters-updated`.
72
+ * @param {object} options - wiring.
73
+ * @param {object[]} [options.rows] - the Raccoon roster rows (`raccoonRoster`).
74
+ * @param {object} [options.officeIdentity] - the credential's `office_identity`
75
+ * (`""` for a personal account) — feeds the per-request headers.
76
+ * @param {() => Promise<string>} options.resolveToken - resolves the live Raccoon
77
+ * JWT per request (refreshes inside the expiry window first).
78
+ * @param {(service: string) => unknown} [options.get] - service resolver for
79
+ * the image hooks (`attachments`, `fs`).
80
+ * @returns {{adapter: object, providerIds: string[]}} the adapter and the ids
81
+ * it owns.
82
+ */
83
+ function createRaccoonAdapter({ rows, officeIdentity = "", resolveToken, get } = {}) {
84
+ const models = buildRaccoonDescriptors(rows ?? raccoonRoster(null), { officeIdentity });
85
+ const provider = {
86
+ ...createProvider({
87
+ id: RACCOON_PROVIDER_ID,
88
+ name: RACCOON_DISPLAY_NAME,
89
+ auth: { apiKey: {
90
+ name: "Raccoon access token",
91
+ async resolve({ credential } = {}) {
92
+ const token = credential?.key;
93
+ return token === void 0 || token === "" ? void 0 : {
94
+ auth: { apiKey: token },
95
+ source: RACCOON_DISPLAY_NAME
96
+ };
97
+ }
98
+ } },
99
+ models,
100
+ api: openAICompletionsApi()
101
+ }),
102
+ getModels: () => models
103
+ };
104
+ const profiles = /* @__PURE__ */ new Map([[RACCOON_PROVIDER_ID, {
105
+ provider: RACCOON_PROVIDER_ID,
106
+ displayName: RACCOON_DISPLAY_NAME,
107
+ streamIdleTimeoutMs: STREAM_IDLE_TIMEOUT_MS,
108
+ retryPolicy: resolveRetryPolicy(buildRetryPolicyConfig(), `${name}.${RACCOON_PROVIDER_ID}.retryPolicy`),
109
+ configuredMaxTokens: /* @__PURE__ */ new Map(),
110
+ modelErrors: /* @__PURE__ */ new Map(),
111
+ ...REQUEST_IMAGE_BUDGETS,
112
+ piProvider: provider
113
+ }]]);
114
+ const inner = new PiAiAdapter({
115
+ profiles: () => profiles,
116
+ auth: INERT_AUTH,
117
+ resolveApiKey: async () => resolveToken(),
118
+ resolveAttachments: () => get?.("attachments"),
119
+ resolveImageAccess: (attachments, ref) => resolveImageAttachmentAccess(attachments, (hostPath) => (get?.("fs"))?.processPathFromHostPath?.(hostPath), ref)
120
+ });
121
+ return {
122
+ adapter: new Proxy(inner, { get(target, prop, receiver) {
123
+ const value = Reflect.get(target, prop, receiver);
124
+ if (prop === "stream") return (options) => reclassifyStream(target.stream(options));
125
+ if (typeof value === "function" && prop === "prepareCall") return (...args) => {
126
+ const prepared = value.apply(target, args);
127
+ if (prepared && typeof prepared.then === "function") return prepared.then((p) => p && typeof p.stream === "function" ? {
128
+ ...p,
129
+ stream: (o) => reclassifyStream(p.stream(o))
130
+ } : p);
131
+ return prepared && typeof prepared.stream === "function" ? {
132
+ ...prepared,
133
+ stream: (o) => reclassifyStream(prepared.stream(o))
134
+ } : prepared;
135
+ };
136
+ return value;
137
+ } }),
138
+ providerIds: [RACCOON_PROVIDER_ID]
139
+ };
140
+ }
141
+
142
+ //#endregion
143
+ export { createRaccoonAdapter };
@@ -0,0 +1,491 @@
1
+ import { c as num, p as str, u as obj } from "./host-config-BNGgm_j7.js";
2
+ import { randomBytes } from "node:crypto";
3
+
4
+ //#region src/host/raccoon.ts
5
+ /**
6
+ * The Raccoon Work(商汤小浣熊)protocol layer — the pure, peer-free API half
7
+ * of the second upstream provider (ROADMAP §6.1 "second upstream provider").
8
+ *
9
+ * Mechanism reference only (ROADMAP §6.1: "机制参考,不抄代码"): the endpoints,
10
+ * the QR/SMS login walk, and the credit semantics were probed against the live
11
+ * gateway and re-stated here, not ported. The desktop `~/.box-agent` token
12
+ * route is REJECTED (§6.1.1: the refresh token is single-use; a 401 ×2 write-
13
+ * back probe), so this half never touches a desktop credential file — login is
14
+ * a self-built WeChat-QR walk whose tokens land in the DSH credentials service
15
+ * (`raccoon-store.ts`), never in this plugin's directory, git, or logs.
16
+ *
17
+ * Wire facts this module encodes (all probed 2026-09 against the gateway):
18
+ * - envelope `{ code, message, details, data }`, success is `code === 0`;
19
+ * - the QR page is public: any self-made 32-hex code enters `pending` and is
20
+ * verified server-side, so the code is generated LOCALLY (`generateQrCode`);
21
+ * - the refresh token is SINGLE-USE: the server rotates BOTH tokens, so a
22
+ * refresh always re-stores the pair (the store owns that write-back);
23
+ * - `thinking` control is a PROVIDER-LEVEL dialect (`extra_body.thinking`,
24
+ * two states only); `reasoning_effort` is accepted by the schema but has
25
+ * no observable effect, so this module never emits it.
26
+ *
27
+ * Everything here takes an injected fetcher and pure data, so the offline
28
+ * suites exercise it without a network; no Host peer is imported.
29
+ *
30
+ * @module dsh-connect-agnes-token-plan/raccoon
31
+ */
32
+ /** The gateway this provider talks to. */
33
+ const RACCOON_API_BASE = "https://xiaohuanxiong.com";
34
+ /** Auth endpoints. */
35
+ const RACCOON_AUTH_PREFIX = "/api/web/auth/v1";
36
+ /** Inference + model catalogue. */
37
+ const RACCOON_LLM_PREFIX = "/api/web/llm/v2";
38
+ /** Credits (balance). */
39
+ const RACCOON_POINTS_PREFIX = "/api/web/points/v1";
40
+ /** The OpenAI-compatible chat endpoint the adapter targets. */
41
+ const RACCOON_CHAT_URL = `${RACCOON_API_BASE}${RACCOON_LLM_PREFIX}/chat/completions`;
42
+ /** The WeChat-QR login page the phone opens after a scan. */
43
+ function raccoonQrLoginUrl(code) {
44
+ const params = new URLSearchParams({
45
+ code: str(code, ""),
46
+ appname: "商汤小浣熊官网"
47
+ });
48
+ return `${RACCOON_API_BASE}/login/mp?${params.toString()}`;
49
+ }
50
+ /**
51
+ * One 32-hex scan code (16 random bytes), the exact shape the gateway's own
52
+ * client generates. Any self-made code is accepted and held `pending` until a
53
+ * phone confirms it, so generation is local and needs no server round-trip.
54
+ * @returns {string} the code.
55
+ */
56
+ function generateRaccoonQrCode() {
57
+ return randomBytes(16).toString("hex");
58
+ }
59
+ /** The QR poll's status values, exactly as the gateway spells them. */
60
+ const RACCOON_QR_STATUS = Object.freeze({
61
+ PENDING: "pending",
62
+ LOGGING: "logging",
63
+ CANCELED: "canceled",
64
+ SUCCESS: "success"
65
+ });
66
+ /** Decode a JWT's `exp` claim to MILLISECONDS; `undefined` on any failure. */
67
+ function decodeRaccoonJwtExpMs(token) {
68
+ if (typeof token !== "string" || token.length === 0) return void 0;
69
+ const parts = token.split(".");
70
+ if (parts.length < 2) return void 0;
71
+ try {
72
+ const payload = JSON.parse(Buffer.from(parts[1] ?? "", "base64url").toString("utf8"));
73
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) return void 0;
74
+ const seconds = num(payload.exp);
75
+ return typeof seconds === "number" ? seconds * 1e3 : void 0;
76
+ } catch {
77
+ return;
78
+ }
79
+ }
80
+ /**
81
+ * The header set one Raccoon request carries.
82
+ *
83
+ * `X-Org-Code` is EMPTY for a personal account (the client always sends the
84
+ * header, with an empty value for personal). The optional client-identity
85
+ * headers let a Host pose as the desktop client's family; they are advisory —
86
+ * the gateway does not gate on them.
87
+ * @param {object} credential - `{ access_token, office_identity?, device_id? }`.
88
+ * @param {object} [options]
89
+ * @param {string} [options.platform] - `X-Client-Platform` (e.g. `desktop-windows`).
90
+ * @param {string} [options.version] - `X-Client-Version` (e.g. `v1.0.35`).
91
+ * @returns {object} the header map.
92
+ */
93
+ function raccoonHeaders(credential, options = {}) {
94
+ const source = obj(credential);
95
+ const headers = {
96
+ Accept: "application/json",
97
+ "Content-Type": "application/json",
98
+ Authorization: `Bearer ${str(source.access_token, "")}`,
99
+ "X-Org-Code": str(source.office_identity, ""),
100
+ "X-Raccoon-Language": "zh"
101
+ };
102
+ if (typeof options.platform === "string" && options.platform !== "") headers["X-Client-Platform"] = options.platform;
103
+ if (typeof options.version === "string" && options.version !== "") headers["X-Client-Version"] = options.version;
104
+ if (typeof source.device_id === "string" && source.device_id !== "") headers["X-Client-Device-ID"] = source.device_id;
105
+ return headers;
106
+ }
107
+ /**
108
+ * Parse one gateway envelope. Success is `code === 0`; anything else is a
109
+ * refusal whose `code`/`message` are surfaced (redacted at the call site when
110
+ * a credential may be echoed back).
111
+ *
112
+ * `code` is read with a plain `Number` check, NOT the shared `num` helper:
113
+ * `num` rejects `0` (it means "positive or fall back"), but `0` is exactly
114
+ * this gateway's SUCCESS value — routing it through `num` would read every
115
+ * successful envelope as a `-1` refusal.
116
+ * @param {unknown} raw - the parsed JSON body.
117
+ * @param {number} [status] - the HTTP status, for a machine-readable detail.
118
+ * @returns {{code: number, message: string, data: object|null, status?: number}}
119
+ */
120
+ function parseRaccoonEnvelope(raw, status) {
121
+ const body = obj(raw);
122
+ const parsedCode = Number(body.code);
123
+ return {
124
+ code: Number.isFinite(parsedCode) ? parsedCode : -1,
125
+ message: str(body.message, ""),
126
+ data: body.data === null || typeof body.data !== "object" || Array.isArray(body.data) ? null : obj(body.data),
127
+ ...typeof status === "number" ? { status } : {}
128
+ };
129
+ }
130
+ /**
131
+ * One poll of the QR login status. Any anomaly degrades to `pending`: the
132
+ * caller polls on a 2 s cadence, and a transient network hiccup or an unknown
133
+ * status must never read as `success` (an empty token would stall the flow)
134
+ * or `canceled` (a live scan would be voided).
135
+ * @param {string} code - the scan code being polled.
136
+ * @param {typeof fetch} [fetcher] - injected fetch.
137
+ * @returns {Promise<object>} `{ status }` plus, on success, the token pair.
138
+ */
139
+ async function pollRaccoonQrLogin(code, fetcher) {
140
+ const effective = fetcher ?? globalThis.fetch;
141
+ let envelope;
142
+ try {
143
+ const response = await effective(`${RACCOON_API_BASE}${RACCOON_AUTH_PREFIX}/login_with_qrcode_code`, {
144
+ method: "POST",
145
+ headers: {
146
+ "Content-Type": "application/json",
147
+ Accept: "application/json"
148
+ },
149
+ body: JSON.stringify({ qrcode_code: str(code, "") })
150
+ });
151
+ envelope = parseRaccoonEnvelope(await response.json().catch(() => ({})), response.status);
152
+ } catch {
153
+ return { status: RACCOON_QR_STATUS.PENDING };
154
+ }
155
+ if (envelope.code !== 0 || envelope.data === null) return { status: RACCOON_QR_STATUS.PENDING };
156
+ const rawStatus = str(envelope.data.status, "");
157
+ if (rawStatus === RACCOON_QR_STATUS.CANCELED) return { status: RACCOON_QR_STATUS.CANCELED };
158
+ if (rawStatus === RACCOON_QR_STATUS.LOGGING) return {
159
+ status: RACCOON_QR_STATUS.LOGGING,
160
+ ...str(envelope.data.expired_at, "") !== "" ? { expiredAt: str(envelope.data.expired_at, "") } : {}
161
+ };
162
+ if (rawStatus === RACCOON_QR_STATUS.SUCCESS) {
163
+ const accessToken = str(envelope.data.access_token, "");
164
+ if (accessToken === "") return { status: RACCOON_QR_STATUS.PENDING };
165
+ const refreshToken = str(envelope.data.refresh_token, "");
166
+ const expMs = decodeRaccoonJwtExpMs(accessToken);
167
+ return {
168
+ status: RACCOON_QR_STATUS.SUCCESS,
169
+ accessToken,
170
+ refreshToken,
171
+ ...expMs !== void 0 ? { expiresAtMs: expMs } : {}
172
+ };
173
+ }
174
+ return { status: RACCOON_QR_STATUS.PENDING };
175
+ }
176
+ /**
177
+ * Rotate a credential pair through the refresh endpoint. The response carries
178
+ * the NEW pair (the refresh token is single-use — the server rotates both), so
179
+ * the caller must re-store what this returns; keeping the old refresh token is
180
+ * what strands a desktop-file route in a 401 loop.
181
+ * @param {object} credential - `{ refresh_token }`.
182
+ * @param {typeof fetch} [fetcher] - injected fetch.
183
+ * @returns {Promise<object>} `{ ok, accessToken, refreshToken, expiresAtMs?, code?, message? }`.
184
+ */
185
+ async function refreshRaccoonCredential(credential, fetcher) {
186
+ const effective = fetcher ?? globalThis.fetch;
187
+ const refreshToken = str(obj(credential).refresh_token, "");
188
+ if (refreshToken === "") return {
189
+ ok: false,
190
+ code: "no_refresh_token",
191
+ message: "the stored credential has no refresh token"
192
+ };
193
+ let envelope;
194
+ try {
195
+ const response = await effective(`${RACCOON_API_BASE}${RACCOON_AUTH_PREFIX}/refresh_token`, {
196
+ method: "POST",
197
+ headers: {
198
+ "Content-Type": "application/json",
199
+ Accept: "application/json"
200
+ },
201
+ body: JSON.stringify({ refresh_token: refreshToken })
202
+ });
203
+ envelope = parseRaccoonEnvelope(await response.json().catch(() => ({})), response.status);
204
+ } catch (error) {
205
+ return {
206
+ ok: false,
207
+ code: "refresh_failed",
208
+ message: error instanceof Error ? error.message : String(error)
209
+ };
210
+ }
211
+ if (envelope.code !== 0 || envelope.data === null) return {
212
+ ok: false,
213
+ code: envelope.code === 401 || envelope.code === 200003 ? "session_dead" : "refresh_rejected",
214
+ message: envelope.message || `refresh refused (code ${envelope.code})`
215
+ };
216
+ const accessToken = str(envelope.data.access_token, "");
217
+ if (accessToken === "") return {
218
+ ok: false,
219
+ code: "refresh_rejected",
220
+ message: "the refresh response carried no access token"
221
+ };
222
+ const refreshTokenOut = str(envelope.data.refresh_token, "");
223
+ const expMs = decodeRaccoonJwtExpMs(accessToken);
224
+ return {
225
+ ok: true,
226
+ accessToken,
227
+ refreshToken: refreshTokenOut,
228
+ ...expMs !== void 0 ? { expiresAtMs: expMs } : {}
229
+ };
230
+ }
231
+ /**
232
+ * Fetch the live model catalogue, or `null` when it cannot be read.
233
+ *
234
+ * The adapter falls back to a static roster when this returns `null`, so a
235
+ * transient catalogue outage degrades to the known-good models instead of
236
+ * breaking the provider — the silent-fallback discipline of `console-client`.
237
+ * @param {object} credential - `{ access_token }`.
238
+ * @param {typeof fetch} [fetcher] - injected fetch.
239
+ * @returns {Promise<object[]|null>} the normalized `[{id, name, multiplier, vision, contextWindow, maxOutputLength}]`, or `null`.
240
+ */
241
+ async function fetchRaccoonCatalog(credential, fetcher) {
242
+ const effective = fetcher ?? globalThis.fetch;
243
+ try {
244
+ const response = await effective(`${RACCOON_API_BASE}${RACCOON_LLM_PREFIX}/model_catalog`, {
245
+ headers: raccoonHeaders(credential),
246
+ signal: AbortSignal.timeout(3e4)
247
+ });
248
+ if (!response.ok) return null;
249
+ const envelope = parseRaccoonEnvelope(await response.json().catch(() => ({})), response.status);
250
+ if (envelope.code !== 0 || envelope.data === null) return null;
251
+ const categories = Array.isArray(envelope.data.categories) ? envelope.data.categories : [];
252
+ for (const category of categories) {
253
+ if (obj(category).type !== "chat") continue;
254
+ const models = Array.isArray(obj(category).models) ? obj(category).models : [];
255
+ const seen = /* @__PURE__ */ new Set();
256
+ const out = [];
257
+ for (const raw of models) {
258
+ const model = obj(raw);
259
+ if (model.visible === false) continue;
260
+ const id = str(model.id, "");
261
+ if (id === "" || seen.has(id)) continue;
262
+ seen.add(id);
263
+ out.push({
264
+ id,
265
+ name: str(model.name, id),
266
+ multiplier: typeof model.multiplier === "number" ? model.multiplier : void 0,
267
+ vision: model.vision === true || (Array.isArray(model.input_modalities) ? model.input_modalities.includes("image") : false),
268
+ contextWindow: num(model.context_window ?? model.context_length),
269
+ maxOutputLength: num(model.max_output_tokens ?? model.max_output_length)
270
+ });
271
+ }
272
+ if (out.length > 0) return out;
273
+ }
274
+ return null;
275
+ } catch {
276
+ return null;
277
+ }
278
+ }
279
+ /**
280
+ * Read the account's credit balance. Read-only: the gateway has NO endpoint
281
+ * for the daily 300-point grant (the server awards it automatically as
282
+ * `daily_grant`), so the panel must not offer a check-in button.
283
+ * @param {object} credential - `{ access_token }`.
284
+ * @param {typeof fetch} [fetcher] - injected fetch.
285
+ * @returns {Promise<number|null>} the balance, or `null` when unreadable.
286
+ */
287
+ async function fetchRaccoonBalance(credential, fetcher) {
288
+ const effective = fetcher ?? globalThis.fetch;
289
+ try {
290
+ const response = await effective(`${RACCOON_API_BASE}${RACCOON_POINTS_PREFIX}/balance`, {
291
+ headers: raccoonHeaders(credential),
292
+ signal: AbortSignal.timeout(3e4)
293
+ });
294
+ if (!response.ok) return null;
295
+ const envelope = parseRaccoonEnvelope(await response.json().catch(() => ({})), response.status);
296
+ if (envelope.code !== 0 || envelope.data === null) return null;
297
+ return numOrNullSafe(envelope.data.balance ?? envelope.data.available ?? envelope.data.amount);
298
+ } catch {
299
+ return null;
300
+ }
301
+ }
302
+ /** Read a finite number (0 counts), else `null`. */
303
+ function numOrNullSafe(value) {
304
+ const parsed = Number(value);
305
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
306
+ }
307
+ /**
308
+ * The static fallback roster: the six `visible: true` chat models the gateway
309
+ * listed at probe time (2026-09). Used ONLY when `fetchRaccoonCatalog` comes
310
+ * back empty, so the provider still offers models; a fresh catalogue always
311
+ * wins over this table.
312
+ */
313
+ const RACCOON_FALLBACK_MODELS = Object.freeze([
314
+ {
315
+ id: "sn-sensenova-6-8-flash",
316
+ name: "SenseNova 6.8 Flash",
317
+ multiplier: 0,
318
+ vision: true,
319
+ contextWindow: 256e3,
320
+ maxOutputLength: 63999
321
+ },
322
+ {
323
+ id: "sn-sensenova-6-8-flash-lite",
324
+ name: "SenseNova 6.8 Flash Lite",
325
+ multiplier: 0,
326
+ vision: true,
327
+ contextWindow: 256e3,
328
+ maxOutputLength: 63999
329
+ },
330
+ {
331
+ id: "sn-glm-5-3",
332
+ name: "GLM-5.3",
333
+ multiplier: .75,
334
+ vision: true,
335
+ contextWindow: 1e6,
336
+ maxOutputLength: 65536
337
+ },
338
+ {
339
+ id: "sn-kimi-k3",
340
+ name: "Kimi K3",
341
+ multiplier: 1,
342
+ vision: true,
343
+ contextWindow: 1e6,
344
+ maxOutputLength: 65536
345
+ },
346
+ {
347
+ id: "sn-glm-5-3-flash",
348
+ name: "GLM-5.3 Flash",
349
+ multiplier: .2,
350
+ vision: false,
351
+ contextWindow: 1e6,
352
+ maxOutputLength: 65536
353
+ },
354
+ {
355
+ id: "sn-deepseek-v4-1-flash",
356
+ name: "DeepSeek V4.1 Flash",
357
+ multiplier: .25,
358
+ vision: false,
359
+ contextWindow: 1e6,
360
+ maxOutputLength: 65536
361
+ }
362
+ ]);
363
+
364
+ //#endregion
365
+ //#region src/host/raccoon-models.ts
366
+ /**
367
+ * Raccoon catalogue entry → pi-ai model descriptor mapping — the peer-free half
368
+ * of the second upstream provider.
369
+ *
370
+ * Mirrors `llm-models.ts`'s discipline: build plain objects only (no Host peer
371
+ * import) so the mapping decisions are testable on a clean checkout.
372
+ * `raccoon-llm-adapter.ts` is the peer-dependent half that hands these
373
+ * descriptors to `createProvider`.
374
+ *
375
+ * Two decisions carried here:
376
+ * 1. `reasoning: false` for now — the Raccoon gateway's ONLY effective
377
+ * thinking channel is `extra_body.thinking` (a provider-level dialect),
378
+ * which pi-ai's openai-completions does NOT emit. Offering a thinking
379
+ * selector would 400 or be silently ignored, so the honest v1 shape is
380
+ * "no toggle; the gateway defaults to thinking ON". This is a stated
381
+ * limitation, not an oversight.
382
+ * 2. The Raccoon auth/identity headers ride in each descriptor's `headers`
383
+ * map (pi-ai merges `model.headers` into the OpenAI client), because the
384
+ * gateway reads `X-Org-Code` / `X-Raccoon-Language` per request.
385
+ *
386
+ * @module dsh-connect-agnes-token-plan/raccoon-models
387
+ */
388
+ /** The provider id this plugin registers under for Raccoon. */
389
+ const RACCOON_PROVIDER_ID = "sensenova-raccoon";
390
+ /** What the DSH model picker shows as the Raccoon provider's name. */
391
+ const RACCOON_DISPLAY_NAME = "SenseNova Raccoon";
392
+ /**
393
+ * The Raccoon LLM base URL the OpenAI-compatible chat endpoint addresses.
394
+ * `openAICompletionsApi` appends `chat/completions` to this.
395
+ */
396
+ const RACCOON_BASE_URL = `${RACCOON_API_BASE}${RACCOON_LLM_PREFIX}`;
397
+ /** The zero-cost sentinel: per-token prices are unknowable (credit-gated). */
398
+ const NO_COST = Object.freeze({
399
+ input: 0,
400
+ output: 0,
401
+ cacheRead: 0,
402
+ cacheWrite: 0
403
+ });
404
+ /** The Raccoon auth/identity headers every request carries. */
405
+ function raccoonRequestHeaders(officeIdentity = "") {
406
+ return {
407
+ Accept: "application/json",
408
+ "Content-Type": "application/json",
409
+ "X-Org-Code": str(officeIdentity, ""),
410
+ "X-Raccoon-Language": "zh"
411
+ };
412
+ }
413
+ /**
414
+ * The Raccoon model rows the panel shows and the adapter offers.
415
+ *
416
+ * A live catalogue row always beats the static fallback roster: the caller
417
+ * passes the fetched catalogue, and only when it is `null`/empty does this
418
+ * fall back to `RACCOON_FALLBACK_MODELS`. The row keeps only what the picker
419
+ * and panel need (id, a display name, vision, window, multiplier) — the raw
420
+ * catalogue field the gateway adds later does not leak into the panel.
421
+ * @param {object[]|null} [catalog] - the `fetchRaccoonCatalog` result.
422
+ * @returns {object[]} the Raccoon model rows.
423
+ */
424
+ function raccoonRoster(catalog) {
425
+ const rows = Array.isArray(catalog) && catalog.length > 0 ? catalog : RACCOON_FALLBACK_MODELS;
426
+ const out = [];
427
+ for (const row of rows) {
428
+ const id = str(row?.id, "");
429
+ if (id === "") continue;
430
+ out.push({
431
+ id,
432
+ name: str(row?.name, id),
433
+ vision: row?.vision === true,
434
+ multiplier: typeof row?.multiplier === "number" ? row.multiplier : void 0,
435
+ contextWindow: num(row?.contextWindow),
436
+ maxOutputLength: num(row?.maxOutputLength)
437
+ });
438
+ }
439
+ return out;
440
+ }
441
+ /**
442
+ * Map one Raccoon row onto the pi-ai model descriptor the adapter offers.
443
+ * @param {object} row - a {@link raccoonRoster} row (must carry `id`).
444
+ * @param {object} [options] - `{ officeIdentity }` for the request headers.
445
+ * @returns {object} the pi-ai descriptor.
446
+ */
447
+ function raccoonToDescriptor(row, options = {}) {
448
+ const id = str(row?.id, "");
449
+ if (id === "") throw new Error("racconToDescriptor: row has no id");
450
+ const vision = row?.vision === true;
451
+ const multiplier = typeof row?.multiplier === "number" ? row.multiplier : void 0;
452
+ const suffix = multiplier === void 0 ? "" : multiplier === 0 ? "(free)" : multiplier === 1 ? "" : `(×${multiplier})`;
453
+ return {
454
+ id,
455
+ name: `${str(row?.name, id)}${suffix}`,
456
+ api: "openai-completions",
457
+ provider: RACCOON_PROVIDER_ID,
458
+ baseUrl: RACCOON_BASE_URL,
459
+ input: vision ? ["text", "image"] : ["text"],
460
+ reasoning: false,
461
+ cost: { ...NO_COST },
462
+ contextWindow: num(row?.contextWindow) ?? 256e3,
463
+ maxTokens: num(row?.maxOutputLength) ?? 32e3,
464
+ headers: raccoonRequestHeaders(options.officeIdentity),
465
+ compat: {
466
+ maxTokensField: "max_tokens",
467
+ supportsDeveloperRole: false
468
+ }
469
+ };
470
+ }
471
+ /**
472
+ * Build the whole descriptor list for one Raccoon roster.
473
+ * @param {object[]} [roster] - the {@link raccoonRoster} result.
474
+ * @param {object} [options] - `{ officeIdentity }`.
475
+ * @returns {object[]} the pi-ai descriptors.
476
+ */
477
+ function buildRaccoonDescriptors(roster, options = {}) {
478
+ const list = Array.isArray(roster) ? roster : raccoonRoster(null);
479
+ const out = [];
480
+ const seen = /* @__PURE__ */ new Set();
481
+ for (const row of list) {
482
+ const id = str(row?.id, "");
483
+ if (id === "" || seen.has(id)) continue;
484
+ seen.add(id);
485
+ out.push(raccoonToDescriptor(row, options));
486
+ }
487
+ return out;
488
+ }
489
+
490
+ //#endregion
491
+ export { RACCOON_FALLBACK_MODELS as a, fetchRaccoonBalance as c, pollRaccoonQrLogin as d, raccoonQrLoginUrl as f, raccoonRoster as i, fetchRaccoonCatalog as l, RACCOON_PROVIDER_ID as n, RACCOON_QR_STATUS as o, refreshRaccoonCredential as p, buildRaccoonDescriptors as r, decodeRaccoonJwtExpMs as s, RACCOON_DISPLAY_NAME as t, generateRaccoonQrCode as u };