dsh-codearts 1.1.1 → 1.1.4

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/lib/index.mjs CHANGED
@@ -1,9 +1,8 @@
1
- import { A as filterEnabledModels, C as rateLimitFrame, D as FALLBACK_CODEARTS_MODELS, E as CodeArtsCatalog, F as codeartsAccountsPath, I as codeartsAuthPath, L as startLogin, M as isBenefitModel, N as CodeArtsAccountStore, O as clearBenefitBlocked, P as CodeArtsCredentialStore, S as prepareChatBody, T as upstreamErrorMessage, _ as isQueueableRate, a as SNAP_HOST, b as maxTokensCeilingFor, c as claimDailyWelfare, d as fetchCallerIdentity, f as fetchModels, g as isParamInvalidFrame, h as isConcurrencyCap, i as refreshCatalog, j as isBenefitBlocked, k as creditRatioLabel, l as clearQueueStatus, m as fetchWelfareCampaigns, n as codeartsUsagePath, o as canonicalModel, p as fetchQueueStatus, r as discoverModels, s as chatStream, t as CodeArtsUsageTracker, u as degradeTokenCeiling, v as isTpmLimit, w as signedGet, x as pickDailyWelfareCampaign, y as isWelfareClaimedStatus } from "./usage-zgF0QuQ4.mjs";
1
+ import { A as filterEnabledModels, C as rateLimitFrame, D as FALLBACK_CODEARTS_MODELS, E as CodeArtsCatalog, F as MANUAL_ACCOUNT_ID, I as codeartsAccountsPath, L as codeartsAuthPath, M as isBenefitModel, N as CodeArtsAccountStore, O as clearBenefitBlocked, P as CodeArtsCredentialStore, R as startLogin, S as prepareChatBody, T as upstreamErrorMessage, _ as isQueueableRate, a as SNAP_HOST, b as maxTokensCeilingFor, c as claimDailyWelfare, d as fetchCallerIdentity, f as fetchModels, g as isParamInvalidFrame, h as isConcurrencyCap, i as refreshCatalog, j as isBenefitBlocked, k as creditRatioLabel, l as clearQueueStatus, m as fetchWelfareCampaigns, n as codeartsUsagePath, o as canonicalModel, p as fetchQueueStatus, r as discoverModels, s as chatStream, t as CodeArtsUsageTracker, u as degradeTokenCeiling, v as isTpmLimit, w as signedGet, x as pickDailyWelfareCampaign, y as isWelfareClaimedStatus } from "./usage-B0NfG8vZ.mjs";
2
+ import { i as lazyApi, r as createProvider } from "./models-CgKCibpk.mjs";
2
3
  import z from "@deepseek-ai/schemastery";
3
4
  import { randomBytes, timingSafeEqual } from "node:crypto";
4
5
  import { createServer } from "node:http";
5
- import { createProvider } from "@earendil-works/pi-ai";
6
- import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
7
6
  import { resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
8
7
  import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
9
8
  import * as settingsModule from "@deepseek-ai/dsh-settings";
@@ -106,6 +105,22 @@ var CodeArtsAccountPool = class {
106
105
  account.disabled = true;
107
106
  account.lastError = reason;
108
107
  }
108
+ /**
109
+ * 用户重新登录(凭据已换新)后调用:把账号从禁用/冷却里捞出来。
110
+ *
111
+ * `sync()` 只对齐成员、特意保留运行时状态(account-pool 不读凭据文档),所以
112
+ * 「同一账号重新登录」不会自动清掉 disabled —— 以前只能靠重启(见 issue #1)。
113
+ * 登录成功的语义就是「这个号现在该能用了」,由登录路径显式调这里复位。
114
+ */
115
+ revive(id) {
116
+ const account = this.accounts.get(id);
117
+ if (account === void 0) return;
118
+ account.disabled = false;
119
+ account.coolUntil = 0;
120
+ account.cappedUntil = 0;
121
+ account.errorCount = 0;
122
+ account.lastError = void 0;
123
+ }
109
124
  /** 上游 5xx / 软限流:短时冷却。 */
110
125
  cooldown(id, ms, reason) {
111
126
  const account = this.accounts.get(id);
@@ -529,6 +544,9 @@ var CodeArtsCreditCache = class {
529
544
  }
530
545
  };
531
546
  //#endregion
547
+ //#region node_modules/.pnpm/@earendil-works+pi-ai@1.3.0_@aws-sdk+credential-provider-node@3.972.84_@smithy+signature-v4@5.7.4_ws@8.22.0_zod@4.6.5/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.lazy.js
548
+ const openAICompletionsApi = () => lazyApi(() => import("./openai-completions-Cn5ZQvOO.mjs"));
549
+ //#endregion
532
550
  //#region src/adapter.ts
533
551
  /**
534
552
  * The `codearts` pi-ai provider.
@@ -706,6 +724,56 @@ var CodeArtsPiAiAdapter = class extends PiAiAdapter {
706
724
  }
707
725
  };
708
726
  //#endregion
727
+ //#region src/pi-ai-compat.ts
728
+ /**
729
+ * `@earendil-works/pi-ai` 运行时自检。
730
+ *
731
+ * 本插件编译时对齐的是 1.3.0,而宿主(`dsh-llm-pi-ai` 0.2.1-rc.2 起)会先把系统提示
732
+ * 折叠成一条 `role:"system"`、`content` 为**字符串**的消息,再交给 provider。1.3.0 之前
733
+ * 的 `estimateMessageTokens` 没有 system 分支:字符串内容被按字符遍历,每个"块"是单个
734
+ * 字符,`block.name` 为 undefined,于是每条消息都死在
735
+ * `Cannot read properties of undefined (reading 'length')` 上 —— 而且是在出网之前,
736
+ * 用户只看得到一句「本轮运行失败」。
737
+ *
738
+ * 什么时候会拿到旧的那份:profile 顶层被别的插件 hoist 了一个低版本,而本插件的依赖
739
+ * 没落地到自己的 `node_modules`。1.1.4 起 pi-ai 直接打包进 `lib/`(不再经 npm 解析),
740
+ * 版本漂移在结构上不可能了;这里仍探测一次,把「产物损坏/不兼容」明说,而不是让
741
+ * 每条消息各自崩在一个看不懂的 TypeError 上。
742
+ *
743
+ * 探测用能力而不是版本号:不依赖 package.json 能不能读到,也对未来的版本漂移免疫。
744
+ *
745
+ * @module dsh-codearts/pi-ai-compat
746
+ */
747
+ /** 折叠后的系统消息要能被估token;从这一版起才支持。 */
748
+ const MIN_PI_AI_VERSION = "1.3.0";
749
+ let probe;
750
+ /** 不兼容时的说明;兼容时 undefined。结果只算一次(模块加载后版本不会再变)。 */
751
+ function probePiAiCompat() {
752
+ probe ??= runProbe();
753
+ return probe;
754
+ }
755
+ async function runProbe() {
756
+ const hint = `需要 @earendil-works/pi-ai >= ${MIN_PI_AI_VERSION} 的行为;1.1.4 起它已打进插件产物,探测失败说明产物损坏或被改动,请重装 dsh-codearts`;
757
+ let estimate;
758
+ try {
759
+ ({estimateMessageTokens: estimate} = await import("./estimate-BePoD-Np.mjs").then((n) => n.n));
760
+ } catch (error) {
761
+ return `pi-ai 不兼容:读不到 @earendil-works/pi-ai/utils/estimate(${messageOf(error)})。${hint}`;
762
+ }
763
+ if (typeof estimate !== "function") return `pi-ai 不兼容:这份 @earendil-works/pi-ai 没有 estimateMessageTokens。${hint}`;
764
+ try {
765
+ estimate({
766
+ role: "system",
767
+ content: "probe"
768
+ });
769
+ } catch (error) {
770
+ return `pi-ai 不兼容:这份 @earendil-works/pi-ai 处理不了宿主折叠出的 system 消息(${messageOf(error)}),每条请求都会在出网前失败。${hint}`;
771
+ }
772
+ }
773
+ function messageOf(error) {
774
+ return error instanceof Error ? error.message : String(error);
775
+ }
776
+ //#endregion
709
777
  //#region src/loopback.ts
710
778
  /**
711
779
  * Shared loopback gates for the plugin's local HTTP surfaces: the loopback
@@ -816,6 +884,40 @@ function queuedMessage(model, gate) {
816
884
  const upstream = gate.message.trim();
817
885
  return `${upstream === "" ? `上游排队中:${model} 第 ${gate.position} 位(共 ${gate.length} 条)` : upstream} 插件没有一直等下去,请稍后重试。`;
818
886
  }
887
+ /**
888
+ * 「一个账号都选不出来」时的报错:把真实原因说清 —— 是禁用(附最后错误)、冷却
889
+ * (附剩余秒数),还是本地槽位全满。以前三种都回同一句「没有可用账号」,禁用那
890
+ * 种还把真因(比如 billion-context 本地拒签的 403)整个吞掉(issue #1)。
891
+ *
892
+ * 全部被禁用时回 401(宿主只自动重试 429,401 一次就把「去重新登录」说清);
893
+ * 只要还有会自己恢复的(冷却/槽位满)就回 429,宿主重试有概率直接救回来。
894
+ */
895
+ function noAccountError(accounts) {
896
+ const now = Date.now();
897
+ const disabled = accounts.filter((a) => a.disabled);
898
+ const cooling = accounts.filter((a) => !a.disabled && now < a.coolUntil);
899
+ const busy = accounts.filter((a) => !a.disabled && now >= a.coolUntil);
900
+ const parts = [];
901
+ if (disabled.length > 0) {
902
+ const reasons = disabled.map((a) => a.lastError === void 0 ? a.id : `${a.id}(${a.lastError.slice(0, 120)})`).join("、");
903
+ parts.push(`${disabled.length} 个被禁用:${reasons}——请到设置卡片重新登录(若在用 billion-context,先检查它的 resign 配置)`);
904
+ }
905
+ if (cooling.length > 0) {
906
+ const secs = Math.max(1, Math.ceil((Math.min(...cooling.map((a) => a.coolUntil)) - now) / 1e3));
907
+ parts.push(`${cooling.length} 个在短时冷却中(约 ${secs} 秒后恢复)`);
908
+ }
909
+ if (busy.length > 0) parts.push(`${busy.length} 个的本地并发槽位已满(每账号最多 ${busy[0]?.maxConcurrent ?? 2} 个在飞请求),等几秒再发`);
910
+ const message = `codearts 当前没有可用账号:${parts.join(";")}`;
911
+ return disabled.length === accounts.length ? {
912
+ status: 401,
913
+ kind: "session_dead",
914
+ message
915
+ } : {
916
+ status: 429,
917
+ kind: "soft_rate",
918
+ message
919
+ };
920
+ }
819
921
  function readBody$1(req) {
820
922
  return new Promise((resolve, reject) => {
821
923
  const chunks = [];
@@ -1199,7 +1301,8 @@ function createCodeArtsShim(options) {
1199
1301
  }
1200
1302
  }
1201
1303
  if (lastKind === void 0) {
1202
- writeOpenAIError(res, 429, "soft_rate", "codearts 当前没有可用账号(全部被禁用或冷却中),请稍后重试");
1304
+ const none = noAccountError(pool.list());
1305
+ writeOpenAIError(res, none.status, none.kind, none.message);
1203
1306
  return;
1204
1307
  }
1205
1308
  const explanation = upstreamErrorMessage(lastMessage, lastStatus);
@@ -1483,11 +1586,14 @@ async function buildStatus(deps) {
1483
1586
  const accounts = await deps.store.list();
1484
1587
  const cred = await deps.store.current();
1485
1588
  const usage = usageOf(deps);
1589
+ const piAi = await probePiAiCompat();
1590
+ const compat = piAi === void 0 ? {} : { piAiError: piAi };
1486
1591
  if (accounts.length === 0 || cred === void 0) return {
1487
1592
  status: "signed-out",
1488
1593
  models,
1489
1594
  accounts,
1490
- usage
1595
+ usage,
1596
+ ...compat
1491
1597
  };
1492
1598
  const usable = accounts.some(accountUsable);
1493
1599
  const identity = {
@@ -1499,11 +1605,13 @@ async function buildStatus(deps) {
1499
1605
  };
1500
1606
  if (!usable) return {
1501
1607
  status: "expired",
1502
- ...identity
1608
+ ...identity,
1609
+ ...compat
1503
1610
  };
1504
1611
  return {
1505
1612
  status: "signed-in",
1506
1613
  ...identity,
1614
+ ...compat,
1507
1615
  ...await creditsOf(deps)
1508
1616
  };
1509
1617
  }
@@ -1613,6 +1721,7 @@ function tokenPostHandler(deps) {
1613
1721
  }
1614
1722
  const label = typeof body?.label === "string" && body.label.trim() !== "" ? body.label.trim() : void 0;
1615
1723
  await deps.store.set(token, label);
1724
+ deps.reviveAccount?.(MANUAL_ACCOUNT_ID);
1616
1725
  json(res, 200, await buildStatus(deps));
1617
1726
  } catch (error) {
1618
1727
  json(res, 500, { error: safeMessage(error) });
@@ -1882,6 +1991,8 @@ function loginStartHandler(deps, active, recent) {
1882
1991
  const identity = await probeIdentity(token);
1883
1992
  const id = await deps.store.upsertFromOAuth(token, void 0, identity ?? void 0);
1884
1993
  if (identity !== null && id !== identity.userId) await deps.store.adoptIdentity(id, identity);
1994
+ deps.reviveAccount?.(id);
1995
+ if (identity !== null && identity.userId !== id) deps.reviveAccount?.(identity.userId);
1885
1996
  recordLoginOutcome(recent, session.url, "done");
1886
1997
  } catch {
1887
1998
  recordLoginOutcome(recent, session.url, "error");
@@ -2084,6 +2195,7 @@ function apply(ctx, rawConfig) {
2084
2195
  enabledModels,
2085
2196
  setEnabledModels,
2086
2197
  pool: () => pool.list(),
2198
+ reviveAccount: (id) => pool.revive(id),
2087
2199
  usage: () => usage.snapshot(),
2088
2200
  queue: () => queue.snapshot(),
2089
2201
  recentRequests: () => recentRequestShapes(),
@@ -2200,9 +2312,11 @@ function apply(ctx, rawConfig) {
2200
2312
  logger: ctx.logger
2201
2313
  });
2202
2314
  usage.load().catch(() => {});
2203
- shim.ready.then(() => {
2315
+ shim.ready.then(async () => {
2204
2316
  if (stopped) return;
2205
2317
  discover();
2318
+ const incompatible = await probePiAiCompat();
2319
+ if (incompatible !== void 0) ctx.logger.error(`dsh-codearts: ${incompatible}`);
2206
2320
  const codearts = createCodeArtsAdapter({
2207
2321
  shim,
2208
2322
  catalog,
@@ -0,0 +1,377 @@
1
+ //#region node_modules/.pnpm/@earendil-works+pi-ai@1.3.0_@aws-sdk+credential-provider-node@3.972.84_@smithy+signature-v4@5.7.4_ws@8.22.0_zod@4.6.5/node_modules/@earendil-works/pi-ai/dist/utils/event-stream.js
2
+ var FifoQueue = class {
3
+ incoming = [];
4
+ outgoing = [];
5
+ get length() {
6
+ return this.incoming.length + this.outgoing.length;
7
+ }
8
+ enqueue(value) {
9
+ this.incoming.push(value);
10
+ }
11
+ dequeue() {
12
+ if (this.outgoing.length === 0) while (this.incoming.length > 0) this.outgoing.push(this.incoming.pop());
13
+ return this.outgoing.pop();
14
+ }
15
+ };
16
+ var EventStream = class {
17
+ queue = new FifoQueue();
18
+ waiting = new FifoQueue();
19
+ done = false;
20
+ finalResultPromise;
21
+ resolveFinalResult;
22
+ isComplete;
23
+ extractResult;
24
+ constructor(isComplete, extractResult) {
25
+ this.isComplete = isComplete;
26
+ this.extractResult = extractResult;
27
+ this.finalResultPromise = new Promise((resolve) => {
28
+ this.resolveFinalResult = resolve;
29
+ });
30
+ }
31
+ push(event) {
32
+ if (this.done) return;
33
+ if (this.isComplete(event)) {
34
+ this.done = true;
35
+ this.resolveFinalResult(this.extractResult(event));
36
+ }
37
+ const waiter = this.waiting.dequeue();
38
+ if (waiter) waiter({
39
+ value: event,
40
+ done: false
41
+ });
42
+ else this.queue.enqueue(event);
43
+ }
44
+ end(result) {
45
+ this.done = true;
46
+ if (result !== void 0) this.resolveFinalResult(result);
47
+ while (this.waiting.length > 0) this.waiting.dequeue()({
48
+ value: void 0,
49
+ done: true
50
+ });
51
+ }
52
+ async *[Symbol.asyncIterator]() {
53
+ while (true) if (this.queue.length > 0) yield this.queue.dequeue();
54
+ else if (this.done) return;
55
+ else {
56
+ const result = await new Promise((resolve) => this.waiting.enqueue(resolve));
57
+ if (result.done) return;
58
+ yield result.value;
59
+ }
60
+ }
61
+ result() {
62
+ return this.finalResultPromise;
63
+ }
64
+ };
65
+ /**
66
+ * Event stream of one assistant response. It also times the response: the final message (`done` or `error` event, or
67
+ * the result passed to `end()`) gets `durationMs`, measured with a monotonic clock from the stream's creation, unless
68
+ * the message already has one or its `timestamp` predates the stream. A stream that forwards a response which started
69
+ * elsewhere, such as a deferred result fetched later, therefore leaves it untimed.
70
+ */
71
+ var AssistantMessageEventStream = class extends EventStream {
72
+ #startedAt = Date.now();
73
+ #startedAtMonotonic = performance.now();
74
+ constructor() {
75
+ super((event) => event.type === "done" || event.type === "error", (event) => {
76
+ if (event.type === "done") return event.message;
77
+ else if (event.type === "error") return event.error;
78
+ throw new Error("Unexpected event type for final result");
79
+ });
80
+ }
81
+ push(event) {
82
+ if (event.type === "done") this.#time(event.message);
83
+ else if (event.type === "error") this.#time(event.error);
84
+ super.push(event);
85
+ }
86
+ end(result) {
87
+ if (result !== void 0) this.#time(result);
88
+ super.end(result);
89
+ }
90
+ #time(message) {
91
+ if (this.done || message.durationMs !== void 0 || message.timestamp < this.#startedAt) return;
92
+ message.durationMs = Math.max(0, Math.round(performance.now() - this.#startedAtMonotonic));
93
+ }
94
+ };
95
+ //#endregion
96
+ //#region node_modules/.pnpm/@earendil-works+pi-ai@1.3.0_@aws-sdk+credential-provider-node@3.972.84_@smithy+signature-v4@5.7.4_ws@8.22.0_zod@4.6.5/node_modules/@earendil-works/pi-ai/dist/api/lazy.js
97
+ function createSetupErrorMessage(model, error, timestamp) {
98
+ return {
99
+ role: "assistant",
100
+ content: [],
101
+ api: model.api,
102
+ provider: model.provider,
103
+ model: model.id,
104
+ usage: {
105
+ input: 0,
106
+ output: 0,
107
+ cacheRead: 0,
108
+ cacheWrite: 0,
109
+ totalTokens: 0,
110
+ cost: {
111
+ input: 0,
112
+ output: 0,
113
+ cacheRead: 0,
114
+ cacheWrite: 0,
115
+ total: 0
116
+ }
117
+ },
118
+ stopReason: "error",
119
+ errorMessage: error instanceof Error ? error.message : String(error),
120
+ timestamp
121
+ };
122
+ }
123
+ function hasResult(source) {
124
+ return typeof source.result === "function";
125
+ }
126
+ async function forwardStream(target, source) {
127
+ for await (const event of source) target.push(event);
128
+ target.end(hasResult(source) ? await source.result() : void 0);
129
+ }
130
+ /**
131
+ * Returns a stream synchronously while running async setup (auth resolution,
132
+ * lazy module loading) behind it. Setup failures terminate the stream with an
133
+ * error event.
134
+ */
135
+ function lazyStream(model, setup) {
136
+ const startedAt = Date.now();
137
+ const outer = new AssistantMessageEventStream();
138
+ setup().then((inner) => forwardStream(outer, inner)).catch((error) => {
139
+ const message = createSetupErrorMessage(model, error, startedAt);
140
+ outer.push({
141
+ type: "error",
142
+ reason: "error",
143
+ error: message
144
+ });
145
+ outer.end(message);
146
+ });
147
+ return outer;
148
+ }
149
+ function lazyApi(load, capabilities) {
150
+ const api = {
151
+ stream: (model, context, options) => lazyStream(model, async () => (await load()).stream(model, context, options)),
152
+ streamSimple: (model, context, options) => lazyStream(model, async () => (await load()).streamSimple(model, context, options))
153
+ };
154
+ if (capabilities?.fetchDeferred) api.fetchDeferred = (model, handle, options) => lazyStream(model, async () => {
155
+ const implementation = await load();
156
+ if (!implementation.fetchDeferred) throw new Error("API does not support deferred responses");
157
+ return implementation.fetchDeferred(model, handle, options);
158
+ });
159
+ if (capabilities?.cancelDeferred) api.cancelDeferred = async (model, handle, options) => {
160
+ const implementation = await load();
161
+ if (!implementation.cancelDeferred) throw new Error("API cannot cancel deferred responses");
162
+ await implementation.cancelDeferred(model, handle, options);
163
+ };
164
+ return api;
165
+ }
166
+ //#endregion
167
+ //#region node_modules/.pnpm/@earendil-works+pi-ai@1.3.0_@aws-sdk+credential-provider-node@3.972.84_@smithy+signature-v4@5.7.4_ws@8.22.0_zod@4.6.5/node_modules/@earendil-works/pi-ai/dist/utils/diagnostics.js
168
+ function formatThrownValue(value) {
169
+ if (value instanceof Error) return value.message || value.name;
170
+ if (typeof value === "string") return value;
171
+ return String(value);
172
+ }
173
+ //#endregion
174
+ //#region node_modules/.pnpm/@earendil-works+pi-ai@1.3.0_@aws-sdk+credential-provider-node@3.972.84_@smithy+signature-v4@5.7.4_ws@8.22.0_zod@4.6.5/node_modules/@earendil-works/pi-ai/dist/utils/models-error.js
175
+ var ModelsError = class extends Error {
176
+ code;
177
+ constructor(code, message, options) {
178
+ super(withCauseDetail(message, options?.cause), options);
179
+ this.name = "ModelsError";
180
+ this.code = code;
181
+ }
182
+ };
183
+ /** Callers surface `error.message` only, so keep the underlying reason in it. */
184
+ function withCauseDetail(message, cause) {
185
+ if (cause === void 0 || cause === null) return message;
186
+ const detail = formatThrownValue(cause).trim();
187
+ if (!detail || message.includes(detail)) return message;
188
+ return `${message}: ${detail}`;
189
+ }
190
+ //#endregion
191
+ //#region node_modules/.pnpm/@earendil-works+pi-ai@1.3.0_@aws-sdk+credential-provider-node@3.972.84_@smithy+signature-v4@5.7.4_ws@8.22.0_zod@4.6.5/node_modules/@earendil-works/pi-ai/dist/utils/model-operations.js
192
+ /** The type of a model. Models without `type` are chat models. */
193
+ function getModelType(model) {
194
+ return model.type ?? "chat";
195
+ }
196
+ /** Runtime-checked model type narrowing, including legacy chat models without `type`. */
197
+ function isModelType(model, type) {
198
+ return getModelType(model) === type;
199
+ }
200
+ function imageErrorResult(model, error, aborted = false) {
201
+ return {
202
+ api: model.api,
203
+ provider: model.provider,
204
+ model: model.id,
205
+ output: [],
206
+ stopReason: aborted ? "aborted" : "error",
207
+ errorMessage: error instanceof Error ? error.message : String(error),
208
+ timestamp: Date.now()
209
+ };
210
+ }
211
+ function classifierErrorResult(model, error, aborted = false) {
212
+ return {
213
+ api: model.api,
214
+ provider: model.provider,
215
+ model: model.id,
216
+ answers: {},
217
+ stopReason: aborted ? "aborted" : "error",
218
+ errorMessage: error instanceof Error ? error.message : String(error),
219
+ timestamp: Date.now()
220
+ };
221
+ }
222
+ //#endregion
223
+ //#region node_modules/.pnpm/@earendil-works+pi-ai@1.3.0_@aws-sdk+credential-provider-node@3.972.84_@smithy+signature-v4@5.7.4_ws@8.22.0_zod@4.6.5/node_modules/@earendil-works/pi-ai/dist/models.js
224
+ const KNOWN_MODEL_TYPES = {
225
+ chat: true,
226
+ image: true,
227
+ classifier: true
228
+ };
229
+ /** Models from stores and remote sources may have types that only newer versions know. */
230
+ function hasKnownModelType(model) {
231
+ return Object.hasOwn(KNOWN_MODEL_TYPES, getModelType(model));
232
+ }
233
+ /**
234
+ * Builds a provider from parts. Built-in provider factories and models.json
235
+ * custom providers both go through this. A single `api` streams all chat
236
+ * models; an `api` map dispatches on `model.api`, and a model whose api has
237
+ * no entry produces a stream error. One-shot operation maps dispatch on
238
+ * `model.api` the same way. At least one concrete implementation across
239
+ * `api`/`images`/`classifiers` is required; empty maps are rejected.
240
+ */
241
+ function createProvider(input) {
242
+ const single = input.api && typeof input.api.stream === "function" ? input.api : void 0;
243
+ const byApi = single || !input.api ? void 0 : input.api;
244
+ const images = input.images;
245
+ const classifiers = input.classifiers;
246
+ const streams = single ? [single] : Object.values(byApi ?? {}).filter((entry) => entry !== void 0);
247
+ const imageImplementations = Object.values(images ?? {}).filter((entry) => entry !== void 0);
248
+ const classifierImplementations = Object.values(classifiers ?? {}).filter((entry) => entry !== void 0);
249
+ if (streams.length === 0 && imageImplementations.length === 0 && classifierImplementations.length === 0) throw new Error(`Provider ${input.id}: at least one of "api", "images", or "classifiers" is required.`);
250
+ const baselineModels = input.models;
251
+ let dynamicModels = [];
252
+ const fetchModels = input.fetchModels;
253
+ const currentModels = () => {
254
+ const merged = [...baselineModels];
255
+ for (const model of dynamicModels) {
256
+ const index = merged.findIndex((entry) => getModelType(entry) === getModelType(model) && entry.id === model.id);
257
+ if (index >= 0) merged[index] = model;
258
+ else merged.push(model);
259
+ }
260
+ return merged;
261
+ };
262
+ const apiFor = (model) => single ?? byApi?.[model.api];
263
+ const dispatch = (model, run) => {
264
+ const streams = apiFor(model);
265
+ if (!streams) return lazyStream(model, async () => {
266
+ throw new ModelsError("stream", `Provider ${input.id} has no API implementation for "${model.api}"`);
267
+ });
268
+ return run(streams);
269
+ };
270
+ const provider = {
271
+ id: input.id,
272
+ name: input.name ?? input.id,
273
+ baseUrl: input.baseUrl,
274
+ headers: input.headers,
275
+ auth: input.auth,
276
+ getModels: () => currentModels().filter((model) => isModelType(model, "chat")),
277
+ getAllModels: currentModels,
278
+ refreshModels: fetchModels ? async (context) => {
279
+ if (context.stored) {
280
+ const restored = context.stored.models.filter((model) => model.provider === input.id).map((model) => model);
281
+ if (!await context.publish({ update: () => {
282
+ dynamicModels = restored;
283
+ } })) return;
284
+ }
285
+ if (!context.allowNetwork || context.signal.aborted) return;
286
+ const fetched = await fetchModels(context);
287
+ if (context.signal.aborted) return;
288
+ const refreshed = fetched.filter(hasKnownModelType);
289
+ await context.publish({
290
+ persist: {
291
+ models: refreshed,
292
+ checkedAt: Date.now()
293
+ },
294
+ update: () => {
295
+ dynamicModels = refreshed;
296
+ }
297
+ });
298
+ } : void 0,
299
+ filterModels: input.filterModels,
300
+ filterAllModels: input.filterAllModels,
301
+ stream: (model, context, options) => dispatch(model, (streams) => streams.stream(model, context, options)),
302
+ streamSimple: (model, context, options) => dispatch(model, (streams) => streams.streamSimple(model, context, options))
303
+ };
304
+ if (streams.some((entry) => entry.fetchDeferred !== void 0)) provider.fetchDeferred = (model, handle, options) => lazyStream(model, async () => {
305
+ const implementation = apiFor(model);
306
+ if (!implementation?.fetchDeferred) throw new ModelsError("provider", `Provider ${input.id} does not support deferred responses for "${model.api}"`);
307
+ return implementation.fetchDeferred(model, handle, options);
308
+ });
309
+ if (streams.some((entry) => entry.cancelDeferred !== void 0)) provider.cancelDeferred = async (model, handle, options) => {
310
+ const implementation = apiFor(model);
311
+ if (!implementation?.cancelDeferred) throw new ModelsError("provider", `Provider ${input.id} cannot cancel deferred responses for "${model.api}"`);
312
+ await implementation.cancelDeferred(model, handle, options);
313
+ };
314
+ if (images && imageImplementations.length > 0) provider.generateImages = async (model, context, options) => {
315
+ const implementation = images[model.api];
316
+ if (!implementation) return imageErrorResult(model, new ModelsError("provider", `Provider ${input.id} has no image generation implementation for "${model.api}"`));
317
+ return implementation.generateImages(model, context, options);
318
+ };
319
+ if (classifiers && classifierImplementations.length > 0) provider.classify = async (model, context, options) => {
320
+ const implementation = classifiers[model.api];
321
+ if (!implementation) return classifierErrorResult(model, new ModelsError("provider", `Provider ${input.id} has no classifier implementation for "${model.api}"`));
322
+ return implementation.classify(model, context, options);
323
+ };
324
+ return provider;
325
+ }
326
+ function calculateCost(model, usage) {
327
+ const inputTokens = usage.input + usage.cacheRead + usage.cacheWrite;
328
+ let rates = model.cost;
329
+ let matchedThreshold = -1;
330
+ for (const tier of model.cost.tiers ?? []) if (inputTokens > tier.inputTokensAbove && tier.inputTokensAbove > matchedThreshold) {
331
+ rates = tier;
332
+ matchedThreshold = tier.inputTokensAbove;
333
+ }
334
+ const longWrite = usage.cacheWrite1h ?? 0;
335
+ const shortWrite = usage.cacheWrite - longWrite;
336
+ usage.cost.input = rates.input / 1e6 * usage.input;
337
+ usage.cost.output = rates.output / 1e6 * usage.output;
338
+ usage.cost.cacheRead = rates.cacheRead / 1e6 * usage.cacheRead;
339
+ usage.cost.cacheWrite = (rates.cacheWrite * shortWrite + rates.input * 2 * longWrite) / 1e6;
340
+ usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
341
+ return usage.cost;
342
+ }
343
+ const EXTENDED_THINKING_LEVELS = [
344
+ "off",
345
+ "minimal",
346
+ "low",
347
+ "medium",
348
+ "high",
349
+ "xhigh",
350
+ "max"
351
+ ];
352
+ function getSupportedThinkingLevels(model) {
353
+ if (!model.reasoning) return ["off"];
354
+ return EXTENDED_THINKING_LEVELS.filter((level) => {
355
+ const mapped = model.thinkingLevelMap?.[level];
356
+ if (mapped === null) return false;
357
+ if (level === "xhigh" || level === "max") return mapped !== void 0;
358
+ return true;
359
+ });
360
+ }
361
+ function clampThinkingLevel(model, level) {
362
+ const availableLevels = getSupportedThinkingLevels(model);
363
+ if (availableLevels.includes(level)) return level;
364
+ const requestedIndex = EXTENDED_THINKING_LEVELS.indexOf(level);
365
+ if (requestedIndex === -1) return availableLevels[0] ?? "off";
366
+ for (let i = requestedIndex; i < EXTENDED_THINKING_LEVELS.length; i++) {
367
+ const candidate = EXTENDED_THINKING_LEVELS[i];
368
+ if (availableLevels.includes(candidate)) return candidate;
369
+ }
370
+ for (let i = requestedIndex - 1; i >= 0; i--) {
371
+ const candidate = EXTENDED_THINKING_LEVELS[i];
372
+ if (availableLevels.includes(candidate)) return candidate;
373
+ }
374
+ return availableLevels[0] ?? "off";
375
+ }
376
+ //#endregion
377
+ export { AssistantMessageEventStream as a, lazyApi as i, clampThinkingLevel as n, createProvider as r, calculateCost as t };