dsh-kenari-plugin 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +281 -0
  3. package/README.md +283 -0
  4. package/assets/kenari-favicon-128.png +0 -0
  5. package/assets/kenari-favicon-256.png +0 -0
  6. package/client/index.js +1998 -0
  7. package/cordis.patch.yml +137 -0
  8. package/lib/billing.d.ts +120 -0
  9. package/lib/billing.d.ts.map +1 -0
  10. package/lib/billing.js +209 -0
  11. package/lib/billing.js.map +1 -0
  12. package/lib/catalog-view.d.ts +78 -0
  13. package/lib/catalog-view.d.ts.map +1 -0
  14. package/lib/catalog-view.js +113 -0
  15. package/lib/catalog-view.js.map +1 -0
  16. package/lib/catalog.d.ts +239 -0
  17. package/lib/catalog.d.ts.map +1 -0
  18. package/lib/catalog.js +445 -0
  19. package/lib/catalog.js.map +1 -0
  20. package/lib/default-route.d.ts +35 -0
  21. package/lib/default-route.d.ts.map +1 -0
  22. package/lib/default-route.js +111 -0
  23. package/lib/default-route.js.map +1 -0
  24. package/lib/errors.d.ts +29 -0
  25. package/lib/errors.d.ts.map +1 -0
  26. package/lib/errors.js +74 -0
  27. package/lib/errors.js.map +1 -0
  28. package/lib/favicon.d.ts +24 -0
  29. package/lib/favicon.d.ts.map +1 -0
  30. package/lib/favicon.js +68 -0
  31. package/lib/favicon.js.map +1 -0
  32. package/lib/http.d.ts +104 -0
  33. package/lib/http.d.ts.map +1 -0
  34. package/lib/http.js +247 -0
  35. package/lib/http.js.map +1 -0
  36. package/lib/index.d.ts +83 -0
  37. package/lib/index.d.ts.map +1 -0
  38. package/lib/index.js +285 -0
  39. package/lib/index.js.map +1 -0
  40. package/lib/llm/adapter.d.ts +78 -0
  41. package/lib/llm/adapter.d.ts.map +1 -0
  42. package/lib/llm/adapter.js +429 -0
  43. package/lib/llm/adapter.js.map +1 -0
  44. package/lib/llm/candidates.d.ts +57 -0
  45. package/lib/llm/candidates.d.ts.map +1 -0
  46. package/lib/llm/candidates.js +86 -0
  47. package/lib/llm/candidates.js.map +1 -0
  48. package/lib/llm/recovery.d.ts +48 -0
  49. package/lib/llm/recovery.d.ts.map +1 -0
  50. package/lib/llm/recovery.js +118 -0
  51. package/lib/llm/recovery.js.map +1 -0
  52. package/lib/llm/retry.d.ts +35 -0
  53. package/lib/llm/retry.d.ts.map +1 -0
  54. package/lib/llm/retry.js +53 -0
  55. package/lib/llm/retry.js.map +1 -0
  56. package/lib/plans.d.ts +59 -0
  57. package/lib/plans.d.ts.map +1 -0
  58. package/lib/plans.js +118 -0
  59. package/lib/plans.js.map +1 -0
  60. package/lib/session-title.d.ts +63 -0
  61. package/lib/session-title.d.ts.map +1 -0
  62. package/lib/session-title.js +280 -0
  63. package/lib/session-title.js.map +1 -0
  64. package/lib/settings.d.ts +35 -0
  65. package/lib/settings.d.ts.map +1 -0
  66. package/lib/settings.js +78 -0
  67. package/lib/settings.js.map +1 -0
  68. package/lib/tools/account.d.ts +11 -0
  69. package/lib/tools/account.d.ts.map +1 -0
  70. package/lib/tools/account.js +132 -0
  71. package/lib/tools/account.js.map +1 -0
  72. package/lib/tools/billing.d.ts +10 -0
  73. package/lib/tools/billing.d.ts.map +1 -0
  74. package/lib/tools/billing.js +100 -0
  75. package/lib/tools/billing.js.map +1 -0
  76. package/lib/tools/count-tokens.d.ts +11 -0
  77. package/lib/tools/count-tokens.d.ts.map +1 -0
  78. package/lib/tools/count-tokens.js +84 -0
  79. package/lib/tools/count-tokens.js.map +1 -0
  80. package/lib/tools/data.d.ts +12 -0
  81. package/lib/tools/data.d.ts.map +1 -0
  82. package/lib/tools/data.js +205 -0
  83. package/lib/tools/data.js.map +1 -0
  84. package/lib/tools/docs.d.ts +13 -0
  85. package/lib/tools/docs.d.ts.map +1 -0
  86. package/lib/tools/docs.js +209 -0
  87. package/lib/tools/docs.js.map +1 -0
  88. package/lib/tools/documents.d.ts +10 -0
  89. package/lib/tools/documents.d.ts.map +1 -0
  90. package/lib/tools/documents.js +118 -0
  91. package/lib/tools/documents.js.map +1 -0
  92. package/lib/tools/media.d.ts +28 -0
  93. package/lib/tools/media.d.ts.map +1 -0
  94. package/lib/tools/media.js +599 -0
  95. package/lib/tools/media.js.map +1 -0
  96. package/lib/tools/shared.d.ts +89 -0
  97. package/lib/tools/shared.d.ts.map +1 -0
  98. package/lib/tools/shared.js +128 -0
  99. package/lib/tools/shared.js.map +1 -0
  100. package/lib/tools/x-search.d.ts +11 -0
  101. package/lib/tools/x-search.d.ts.map +1 -0
  102. package/lib/tools/x-search.js +129 -0
  103. package/lib/tools/x-search.js.map +1 -0
  104. package/lib/web/fallback.d.ts +39 -0
  105. package/lib/web/fallback.d.ts.map +1 -0
  106. package/lib/web/fallback.js +79 -0
  107. package/lib/web/fallback.js.map +1 -0
  108. package/lib/web/fetch.d.ts +19 -0
  109. package/lib/web/fetch.d.ts.map +1 -0
  110. package/lib/web/fetch.js +41 -0
  111. package/lib/web/fetch.js.map +1 -0
  112. package/lib/web/search.d.ts +21 -0
  113. package/lib/web/search.d.ts.map +1 -0
  114. package/lib/web/search.js +47 -0
  115. package/lib/web/search.js.map +1 -0
  116. package/package.json +84 -0
@@ -0,0 +1,137 @@
1
+ # The dsh-kenari-plugin bundle patch. Applied after the dsh-base and
2
+ # dsh-web-app layers, addressing their rows by id; the last write wins per row.
3
+ #
4
+ # A patch replaces the targeted row's whole `config` rather than merging into
5
+ # it, so a row overridden here must restate every key it owns. `disabled` is a
6
+ # key of the row itself, independent of `config`.
7
+ #
8
+ # dsh-web-app disables the `tool-web` row on the web profile by patching
9
+ # `disabled: true` (its own patch writes no config), so re-enabling needs only
10
+ # `disabled: false` — the row's base config (fetch: true, searchTimeoutMs:
11
+ # 60000) stays intact.
12
+
13
+ - insert:
14
+ # The main plugin: registers web providers, tools, and settings sections.
15
+ - id: kenari
16
+ name: dsh-kenari-plugin
17
+
18
+ # Pin the composed Kenari-first provider into the web seam. Both keys must be
19
+ # restated: replacing `config` drops anything omitted.
20
+ - id: web
21
+ config:
22
+ searchProvider: kenari-fallback
23
+ fetchProvider: kenari-fallback
24
+
25
+ # Re-enable the web tools that dsh-web-app disabled for the web profile.
26
+ - id: tool-web
27
+ disabled: false
28
+
29
+ # Session-title byte budget: the plugin writes a `yyyyMMddHHmmss-` prefix (15
30
+ # bytes) into every new session title, while dsh-base caps any accepted title at
31
+ # 80. Bump the cap to 96 so a prefixed title stays within policy. This number
32
+ # MUST stay in sync with the plugin's `sessionTitleMaxBytes` setting.
33
+ - id: session-title
34
+ config:
35
+ fallbackMaxWords: 5
36
+ fallbackMaxBytes: 40
37
+ maxTitleBytes: 96
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # Kenari as a session model — zero-code preset on the dormant `llm-pi-ai` row.
41
+ #
42
+ # dsh-base mounts `llm-pi-ai` with no config, so no models exist until a
43
+ # `llm-pi-ai:` settings section supplies provider profiles. Supplying a
44
+ # `providers` dict here is the same thing the web Models page writes; the
45
+ # composition layer and a user-settings layer merge per provider route, so a
46
+ # user adding their own route (or overriding this one) still works.
47
+ #
48
+ # Everything below is verified against https://kenari.id/v1 on 2026-09-10 with
49
+ # `step-3-7-flash:free` (free, so the probe cost nothing):
50
+ # - `max_tokens` is the output-cap field the gateway reads; sending
51
+ # `max_completion_tokens` instead is silently ignored (HTTP 200, cap lost).
52
+ # - `stream_options: {include_usage: true}` is accepted and the last SSE chunk
53
+ # carries `usage`, so `supportsUsageInStreaming` is on for cached_tokens
54
+ # accounting.
55
+ # - streams carry `finish_reason`; tool definitions produce `tool_calls`.
56
+ # - the [OI] line lives at `https://kenari.id/v1`; the Anthropic line is the
57
+ # bare host `https://kenari.id` (no /v1) and Responses is /v1 again. A wrong
58
+ # shape answers 405, not 404.
59
+ #
60
+ # The models below are a FALLBACK preset, not the whole story. `:free` models are
61
+ # deliberately absent: the intended default is whatever the account's plan gives
62
+ # free cache reads on, which only the plan table knows and only the Host can read
63
+ # at runtime (the browser cannot — /api/plans sends no CORS header). So the plugin
64
+ # computes that default on load (src/default-route.ts): plan -> its
65
+ # `free_cache_models` -> written into this route's user layer once. If the key or
66
+ # the plan is unavailable, this preset stands, and it is exactly the free-cache
67
+ # list of the Kreator and Studio plans.
68
+ #
69
+ # Consequences worth knowing: none of these four is free, so an account with no
70
+ # balance reaches HTTP 402 on its first session until it picks something else —
71
+ # the trade for not defaulting to free models. Field shape mirrors what
72
+ # src/catalog.ts's `toModelProfile()` writes, so the preset and the computed
73
+ # default produce identical entries for the same ids.
74
+ #
75
+ # Failure recovery: `retryPolicy` below must stay in sync with the plugin's
76
+ # `modelRetryMaxRetries` / `modelRetryDelayMs` (which drive the kenari-direct
77
+ # route). dsh captures a route's retry policy once, at adapter registration, and
78
+ # freezes it — so the plugin cannot change this route's retry timing at runtime.
79
+ # ---------------------------------------------------------------------------
80
+ - id: llm-pi-ai
81
+ config:
82
+ providers:
83
+ kenari:
84
+ displayName: Kenari
85
+ apiKeyEnv: KENARI_API_KEY
86
+ api: openai-completions
87
+ baseURL: https://kenari.id/v1
88
+ # A flat 5s between retries: dsh's backoff is `initialDelay * 2^n` capped
89
+ # at `maxDelay`, so making the two equal (and disabling jitter) is the
90
+ # only way to express "wait 5s every time".
91
+ # CONTEXT_WINDOW_EXCEEDED is deliberately left out of the retryable set:
92
+ # retrying the same context on the same model can only fail again, and
93
+ # the plugin's escalation is the thing that can actually fix it.
94
+ retryPolicy:
95
+ mode: normal
96
+ maxRetries: 5
97
+ backoff:
98
+ initialDelayMs: 5000
99
+ maxDelayMs: 5000
100
+ jitterRatio: 0
101
+ compat:
102
+ supportsDeveloperRole: false
103
+ maxTokensField: max_tokens
104
+ supportsUsageInStreaming: true
105
+ supportsFinishReason: true
106
+ supportsReasoningEffort: true
107
+ supportsStore: false
108
+ defaultInput: [text]
109
+ models:
110
+ # `name` 填 dsh 模型目录里的「显示名称」(不填就是空框),`maxTokens` 填每请求
111
+ # 输出上限(不填则整条路由共用 pi-ai 的 defaultMaxTokens 32768)。两者都由
112
+ # catalog.ts 的 displayNameOf / recommendedMaxTokens 推导——写模型时用的是同一套
113
+ # 规则,所以手动加的和这里预置的取值口径一致。窗口的 1/4 夹在 [8192, 65536],
114
+ # 这四个的窗口都是 ~1M,所以都取到上限。
115
+ - id: deepseek-v4-flash
116
+ name: DeepSeek v4 Flash
117
+ contextWindow: 1048576
118
+ maxTokens: 65536
119
+ input: [text]
120
+ reasoningEfforts: { low: low, high: high, max: max }
121
+ - id: glm-5-3-flash
122
+ name: GLM 5.3 Flash
123
+ contextWindow: 1000000
124
+ maxTokens: 65536
125
+ input: [text, image]
126
+ reasoningEfforts: { low: low, high: high, max: max }
127
+ - id: gpt-5-6-luna
128
+ name: GPT 5.6 Luna
129
+ contextWindow: 872000
130
+ maxTokens: 65536
131
+ input: [text, image]
132
+ reasoningEfforts: { off: none, low: low, medium: medium, high: high, xhigh: xhigh, max: max }
133
+ - id: mimo-v2-5
134
+ name: MiMo v2.5
135
+ contextWindow: 1050000
136
+ maxTokens: 65536
137
+ input: [text, image]
@@ -0,0 +1,120 @@
1
+ /**
2
+ * 计费账本与余额监控:把「本次调用花了多少」升级成「这个会话一共花了多少、还剩多少额度」。
3
+ *
4
+ * 两种口径分开记,绝不混为一谈:
5
+ * - **实际**:响应体带 `cost_micro_idr` 时才写实际值;
6
+ * - **预估**:目录 `pricing_lines` 单价 × 数量,标注「预估」并计入封顶判断。
7
+ *
8
+ * 会话作用域取自 `exec.agent.id`(dsh 的 SessionId);无 agent 的调用(web provider、
9
+ * 目录查询)落在 `global` 作用域,仍计入总额,只是不参与单会话封顶。
10
+ * @module dsh-kenari-plugin/billing
11
+ */
12
+ import type { KenariHttpDeps } from './http.js';
13
+ /** micro-IDR → 可读 Rupiah 字符串(整除则不带小数)。 */
14
+ export declare function formatRupiah(microIdr: number): string;
15
+ /** 无会话归属的调用落在该作用域。 */
16
+ export declare const GLOBAL_SCOPE = "global";
17
+ /** 一次计费调用:estimated 为真表示按目录单价推算,而非响应回显。 */
18
+ export interface SpendRecord {
19
+ tool: string;
20
+ model?: string;
21
+ microIdr: number;
22
+ estimated: boolean;
23
+ at: number;
24
+ }
25
+ /** 一次模型调用的 token 计量(cached_tokens 命中率的原始数据)。 */
26
+ export interface UsageRecord {
27
+ model?: string;
28
+ inputTokens: number;
29
+ outputTokens: number;
30
+ cacheReadTokens?: number;
31
+ cacheWriteTokens?: number;
32
+ reasoningTokens?: number;
33
+ at: number;
34
+ }
35
+ /** 作用域花费汇总。 */
36
+ export interface SpendTotals {
37
+ calls: number;
38
+ microIdr: number;
39
+ estimatedMicroIdr: number;
40
+ actualMicroIdr: number;
41
+ }
42
+ /** token 计量汇总;hitRate 无缓存数据时为 undefined(不冒充 0%)。 */
43
+ export interface UsageTotals {
44
+ calls: number;
45
+ inputTokens: number;
46
+ outputTokens: number;
47
+ cacheReadTokens: number;
48
+ cacheWriteTokens: number;
49
+ /** 缓存命中率 = cacheRead / (input + cacheRead)。 */
50
+ hitRate?: number;
51
+ }
52
+ /** 预算封顶拒绝:带可执行建议,错误文本由工具原样透出给模型。 */
53
+ export declare class KenariBudgetError extends Error {
54
+ constructor(message: string);
55
+ }
56
+ /** 计费账本:按作用域(会话)累计花费与 token 用量。 */
57
+ export declare class BillingLedger {
58
+ private readonly capMicroIdr?;
59
+ private readonly spends;
60
+ private readonly usages;
61
+ constructor(capMicroIdr?: number | undefined);
62
+ /** 记录一次花费(实际或预估)。 */
63
+ recordSpend(scope: string, record: Omit<SpendRecord, 'at'> & {
64
+ at?: number;
65
+ }): void;
66
+ /** 记录一次模型 token 用量。 */
67
+ recordUsage(scope: string, record: Omit<UsageRecord, 'at'> & {
68
+ at?: number;
69
+ }): void;
70
+ /** 作用域花费汇总。 */
71
+ spendTotals(scope: string): SpendTotals;
72
+ /** 全进程花费合计(含所有会话,web provider 的按次计费也在内)。 */
73
+ globalSpendTotals(): SpendTotals;
74
+ /** 按工具聚合(金额降序)。 */
75
+ byTool(scope: string): {
76
+ key: string;
77
+ totals: SpendTotals;
78
+ }[];
79
+ /** 按模型聚合(金额降序);无模型的调用归入 `(无模型)`。 */
80
+ byModel(scope: string): {
81
+ key: string;
82
+ totals: SpendTotals;
83
+ }[];
84
+ /** 作用域 token 汇总。 */
85
+ usageTotals(scope: string): UsageTotals;
86
+ /** 预算状态;未配置封顶时 capMicroIdr 为 undefined。 */
87
+ capStatus(scope: string): {
88
+ capMicroIdr?: number;
89
+ spentMicroIdr: number;
90
+ remainingMicroIdr?: number;
91
+ exceeded: boolean;
92
+ };
93
+ /**
94
+ * 花费前预检:已达封顶则拒绝并给出可执行建议。
95
+ * 预检只能看已知花费,所以是「不新增花钱调用」而非「保证总额不超」。
96
+ */
97
+ assertWithinBudget(scope: string, tool: string): void;
98
+ /** 有记录的会话作用域(诊断用)。 */
99
+ scopes(): string[];
100
+ private aggregate;
101
+ }
102
+ /**
103
+ * 余额监控:`kenari_balance` 只有 MCP 入口,故按 TTL 缓存,避免每次花费都打一次 MCP。
104
+ * TTL 取自实时配置,设置页改完即生效。分享页 key 会 403 —— 取不到余额时静默降级
105
+ * (告警是便利功能,不阻断调用)。
106
+ */
107
+ export declare class BalanceMonitor {
108
+ private readonly deps;
109
+ private cached;
110
+ constructor(deps: KenariHttpDeps);
111
+ /** 当前余额(Rupiah);取不到返回 undefined(不抛错)。 */
112
+ balanceRp(signal?: AbortSignal): Promise<number | undefined>;
113
+ /** 余额低于阈值时返回告警文本,否则 undefined。 */
114
+ alertIfLow(thresholdRp: number, signal?: AbortSignal): Promise<string | undefined>;
115
+ /** 丢弃缓存(测试与充值后刷新用)。 */
116
+ invalidate(): void;
117
+ }
118
+ /** 从 `kenari_balance` 文本里解析 Rupiah 数额;识别印尼式千分点。 */
119
+ export declare function parseRupiah(text: string): number | undefined;
120
+ //# sourceMappingURL=billing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"billing.d.ts","sourceRoot":"","sources":["../src/billing.ts"],"names":[],"mappings":"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"}
package/lib/billing.js ADDED
@@ -0,0 +1,209 @@
1
+ /**
2
+ * 计费账本与余额监控:把「本次调用花了多少」升级成「这个会话一共花了多少、还剩多少额度」。
3
+ *
4
+ * 两种口径分开记,绝不混为一谈:
5
+ * - **实际**:响应体带 `cost_micro_idr` 时才写实际值;
6
+ * - **预估**:目录 `pricing_lines` 单价 × 数量,标注「预估」并计入封顶判断。
7
+ *
8
+ * 会话作用域取自 `exec.agent.id`(dsh 的 SessionId);无 agent 的调用(web provider、
9
+ * 目录查询)落在 `global` 作用域,仍计入总额,只是不参与单会话封顶。
10
+ * @module dsh-kenari-plugin/billing
11
+ */
12
+ import { kenariMcpCall } from './http.js';
13
+ /** micro-IDR → 可读 Rupiah 字符串(整除则不带小数)。 */
14
+ export function formatRupiah(microIdr) {
15
+ const rp = microIdr / 1_000_000;
16
+ return `Rp ${Number.isInteger(rp) ? rp.toLocaleString('id-ID') : rp.toFixed(6).replace(/0+$/, '').replace(/\.$/, '')}`;
17
+ }
18
+ /** 无会话归属的调用落在该作用域。 */
19
+ export const GLOBAL_SCOPE = 'global';
20
+ /** 预算封顶拒绝:带可执行建议,错误文本由工具原样透出给模型。 */
21
+ export class KenariBudgetError extends Error {
22
+ constructor(message) {
23
+ super(message);
24
+ this.name = 'KenariBudgetError';
25
+ }
26
+ }
27
+ /** 计费账本:按作用域(会话)累计花费与 token 用量。 */
28
+ export class BillingLedger {
29
+ capMicroIdr;
30
+ spends = new Map();
31
+ usages = new Map();
32
+ constructor(capMicroIdr) {
33
+ this.capMicroIdr = capMicroIdr;
34
+ }
35
+ /** 记录一次花费(实际或预估)。 */
36
+ recordSpend(scope, record) {
37
+ const list = this.spends.get(scope) ?? [];
38
+ list.push({ ...record, at: record.at ?? Date.now() });
39
+ this.spends.set(scope, list);
40
+ }
41
+ /** 记录一次模型 token 用量。 */
42
+ recordUsage(scope, record) {
43
+ const list = this.usages.get(scope) ?? [];
44
+ list.push({ ...record, at: record.at ?? Date.now() });
45
+ this.usages.set(scope, list);
46
+ }
47
+ /** 作用域花费汇总。 */
48
+ spendTotals(scope) {
49
+ const list = this.spends.get(scope) ?? [];
50
+ let microIdr = 0;
51
+ let estimatedMicroIdr = 0;
52
+ for (const item of list) {
53
+ microIdr += item.microIdr;
54
+ if (item.estimated)
55
+ estimatedMicroIdr += item.microIdr;
56
+ }
57
+ return { calls: list.length, microIdr, estimatedMicroIdr, actualMicroIdr: microIdr - estimatedMicroIdr };
58
+ }
59
+ /** 全进程花费合计(含所有会话,web provider 的按次计费也在内)。 */
60
+ globalSpendTotals() {
61
+ let calls = 0;
62
+ let microIdr = 0;
63
+ let estimatedMicroIdr = 0;
64
+ for (const scope of this.spends.keys()) {
65
+ const totals = this.spendTotals(scope);
66
+ calls += totals.calls;
67
+ microIdr += totals.microIdr;
68
+ estimatedMicroIdr += totals.estimatedMicroIdr;
69
+ }
70
+ return { calls, microIdr, estimatedMicroIdr, actualMicroIdr: microIdr - estimatedMicroIdr };
71
+ }
72
+ /** 按工具聚合(金额降序)。 */
73
+ byTool(scope) {
74
+ return this.aggregate(scope, (item) => item.tool);
75
+ }
76
+ /** 按模型聚合(金额降序);无模型的调用归入 `(无模型)`。 */
77
+ byModel(scope) {
78
+ return this.aggregate(scope, (item) => item.model ?? '(无模型)');
79
+ }
80
+ /** 作用域 token 汇总。 */
81
+ usageTotals(scope) {
82
+ const list = this.usages.get(scope) ?? [];
83
+ let inputTokens = 0;
84
+ let outputTokens = 0;
85
+ let cacheReadTokens = 0;
86
+ let cacheWriteTokens = 0;
87
+ for (const item of list) {
88
+ inputTokens += item.inputTokens;
89
+ outputTokens += item.outputTokens;
90
+ cacheReadTokens += item.cacheReadTokens ?? 0;
91
+ cacheWriteTokens += item.cacheWriteTokens ?? 0;
92
+ }
93
+ const billableInput = inputTokens + cacheReadTokens;
94
+ return {
95
+ calls: list.length,
96
+ inputTokens,
97
+ outputTokens,
98
+ cacheReadTokens,
99
+ cacheWriteTokens,
100
+ ...(billableInput > 0 && cacheReadTokens > 0
101
+ ? { hitRate: cacheReadTokens / billableInput }
102
+ : {}),
103
+ };
104
+ }
105
+ /** 预算状态;未配置封顶时 capMicroIdr 为 undefined。 */
106
+ capStatus(scope) {
107
+ const spentMicroIdr = this.spendTotals(scope).microIdr;
108
+ if (this.capMicroIdr === undefined)
109
+ return { spentMicroIdr, exceeded: false };
110
+ return {
111
+ capMicroIdr: this.capMicroIdr,
112
+ spentMicroIdr,
113
+ remainingMicroIdr: this.capMicroIdr - spentMicroIdr,
114
+ exceeded: spentMicroIdr >= this.capMicroIdr,
115
+ };
116
+ }
117
+ /**
118
+ * 花费前预检:已达封顶则拒绝并给出可执行建议。
119
+ * 预检只能看已知花费,所以是「不新增花钱调用」而非「保证总额不超」。
120
+ */
121
+ assertWithinBudget(scope, tool) {
122
+ const status = this.capStatus(scope);
123
+ if (!status.exceeded)
124
+ return;
125
+ throw new KenariBudgetError(`本会话 Kenari 预算已用尽:上限 ${formatRupiah(status.capMicroIdr ?? 0)},已记录 ${formatRupiah(status.spentMicroIdr)},`
126
+ + `因此拒绝 ${tool} 以不再产生费用。`
127
+ + '可选:改用免费模型(目录中 id 带 :free,如 step-3-7-flash:free)、提高 cordis 配置里的 budgetCapRp、或等下次会话重新计算。');
128
+ }
129
+ /** 有记录的会话作用域(诊断用)。 */
130
+ scopes() {
131
+ return [...new Set([...this.spends.keys(), ...this.usages.keys()])];
132
+ }
133
+ aggregate(scope, keyOf) {
134
+ const groups = new Map();
135
+ for (const item of this.spends.get(scope) ?? []) {
136
+ const key = keyOf(item);
137
+ const list = groups.get(key) ?? [];
138
+ list.push(item);
139
+ groups.set(key, list);
140
+ }
141
+ return [...groups.entries()]
142
+ .map(([key, list]) => {
143
+ let microIdr = 0;
144
+ let estimatedMicroIdr = 0;
145
+ for (const item of list) {
146
+ microIdr += item.microIdr;
147
+ if (item.estimated)
148
+ estimatedMicroIdr += item.microIdr;
149
+ }
150
+ return { key, totals: { calls: list.length, microIdr, estimatedMicroIdr, actualMicroIdr: microIdr - estimatedMicroIdr } };
151
+ })
152
+ .sort((a, b) => b.totals.microIdr - a.totals.microIdr);
153
+ }
154
+ }
155
+ /**
156
+ * 余额监控:`kenari_balance` 只有 MCP 入口,故按 TTL 缓存,避免每次花费都打一次 MCP。
157
+ * TTL 取自实时配置,设置页改完即生效。分享页 key 会 403 —— 取不到余额时静默降级
158
+ * (告警是便利功能,不阻断调用)。
159
+ */
160
+ export class BalanceMonitor {
161
+ deps;
162
+ cached;
163
+ constructor(deps) {
164
+ this.deps = deps;
165
+ }
166
+ /** 当前余额(Rupiah);取不到返回 undefined(不抛错)。 */
167
+ async balanceRp(signal) {
168
+ const ttlMs = this.deps.config.balanceCacheTtlMs ?? 300_000;
169
+ if (ttlMs > 0 && this.cached !== undefined && Date.now() - this.cached.fetchedAt < ttlMs) {
170
+ return this.cached.rp;
171
+ }
172
+ try {
173
+ const text = await kenariMcpCall(this.deps, 'kenari_balance', {}, signal);
174
+ const rp = parseRupiah(text);
175
+ if (rp === undefined)
176
+ return undefined;
177
+ this.cached = { rp, fetchedAt: Date.now() };
178
+ return rp;
179
+ }
180
+ catch (err) {
181
+ this.deps.logger?.warn(`kenari: 余额查询失败(告警降级):${String(err)}`);
182
+ return undefined;
183
+ }
184
+ }
185
+ /** 余额低于阈值时返回告警文本,否则 undefined。 */
186
+ async alertIfLow(thresholdRp, signal) {
187
+ if (thresholdRp <= 0)
188
+ return undefined;
189
+ const rp = await this.balanceRp(signal);
190
+ if (rp === undefined || rp >= thresholdRp)
191
+ return undefined;
192
+ return `⚠️ Kenari 余额偏低:Rp ${rp.toLocaleString('id-ID')}(阈值 Rp ${thresholdRp.toLocaleString('id-ID')})。改用免费模型(id 带 :free)或充值(QRIS 最低 Rp 1.000)。`;
193
+ }
194
+ /** 丢弃缓存(测试与充值后刷新用)。 */
195
+ invalidate() {
196
+ this.cached = undefined;
197
+ }
198
+ }
199
+ /** 从 `kenari_balance` 文本里解析 Rupiah 数额;识别印尼式千分点。 */
200
+ export function parseRupiah(text) {
201
+ const match = /(?:Rp|IDR)\s*([0-9][0-9.,]*)/i.exec(text);
202
+ if (match === null)
203
+ return undefined;
204
+ // 印尼写法:`.` 千分位、`,` 小数位
205
+ const normalized = match[1].replace(/\./g, '').replace(/,/g, '.');
206
+ const value = Number(normalized);
207
+ return Number.isFinite(value) ? value : undefined;
208
+ }
209
+ //# sourceMappingURL=billing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"billing.js","sourceRoot":"","sources":["../src/billing.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * 浏览器侧模型浏览器的数据端点:`GET /api/kenari.models`。
3
+ *
4
+ * 为什么需要它:浏览器只装得下已经存在的 Remote 命名空间,而 dsh 没有任何一个
5
+ * 能承载"每模型能力标签 + 套餐归属"的命名空间(新增一个要动 dsh 的 api-remotes 装配)。
6
+ * 但 dsh 的 Connection 服务给了插件一个**同源 Fetch 路由**注册点,
7
+ * 于是浏览器可以直接读本端点的 JSON——数据仍全部在 Host 侧算,浏览器不解析 Kenari。
8
+ *
9
+ * 鉴权由 dsh 的 Connection 层负责(Host/Origin 围栏 + 浏览器会话),
10
+ * 所以这里假定"能到达本处理器就是可信的同源页面"。
11
+ * @module dsh-kenari-plugin/catalog-view
12
+ */
13
+ import type { Context } from '@deepseek-ai/cordis';
14
+ import { toModelProfile } from './catalog.js';
15
+ import type { CapabilityTag, KenariCatalog } from './catalog.js';
16
+ import type { KenariPlans } from './plans.js';
17
+ import type { KenariHttpDeps } from './http.js';
18
+ /** 浏览器读取模型视图的路径(Connection 的 Fetch 路由要求落在 `/api/` 下)。 */
19
+ export declare const KENARI_MODEL_VIEW_PATH = "/api/kenari.models";
20
+ /** 本端点的依赖。 */
21
+ export interface CatalogViewDeps {
22
+ readonly http: KenariHttpDeps;
23
+ readonly catalog: KenariCatalog;
24
+ readonly plans: KenariPlans;
25
+ }
26
+ /** 一个模型的浏览器视图:目录事实 + 派生标签 + 可直接写入路由的 profile。 */
27
+ interface ModelView {
28
+ id: string;
29
+ /**
30
+ * 展示名,**不是**目录原样给的 `name`。
31
+ *
32
+ * 目录里只有 8 个模型带 `name`,其余 70 多个要按 id 还原(见 `displayNameOf`)。
33
+ * 若这里原样透传,无名模型的 `name` 就等于 id,界面把 id 印两遍;而同一个模型的
34
+ * `profile.name` 又已经是派生名——同一份数据里两个口径,改这里就是为了消掉它。
35
+ */
36
+ name: string;
37
+ ownedBy?: string;
38
+ contextWindow?: number;
39
+ free: boolean;
40
+ beta: boolean;
41
+ sunsetAt?: number;
42
+ chatCapable: boolean;
43
+ endpoints: string[];
44
+ input: string[];
45
+ tags: CapabilityTag[];
46
+ plans: string[];
47
+ freeCachePlans: string[];
48
+ profile: ReturnType<typeof toModelProfile>;
49
+ }
50
+ /** 一次视图响应。`plansError` 非空表示套餐维度缺失,界面据此只降级标签。 */
51
+ export interface CatalogView {
52
+ generatedAt: number;
53
+ plans: {
54
+ id: string;
55
+ name: string;
56
+ priceIdr?: number;
57
+ modelCount: number;
58
+ }[];
59
+ plansError?: string;
60
+ tags: readonly CapabilityTag[];
61
+ models: ModelView[];
62
+ }
63
+ /**
64
+ * 组装一次完整视图。
65
+ *
66
+ * 目录是必需事实(失败即端点失败);套餐表是可选事实(失败只标记 `plansError`),
67
+ * 因为模型目录公开且稳定,而套餐表是站点路由、形状随时可能变。
68
+ */
69
+ export declare function buildCatalogView(deps: CatalogViewDeps, signal?: AbortSignal): Promise<CatalogView>;
70
+ /**
71
+ * 注册模型视图路由。
72
+ *
73
+ * `connection` 是 web profile 才有的服务,所以在缺它的部署里静默跳过——
74
+ * 插件其余能力(工具、web provider、设置节)不依赖浏览器。
75
+ */
76
+ export declare function registerCatalogView(ctx: Context, deps: CatalogViewDeps): void;
77
+ export {};
78
+ //# sourceMappingURL=catalog-view.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-view.d.ts","sourceRoot":"","sources":["../src/catalog-view.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAGlD,OAAO,EACyE,cAAc,EAC7F,MAAM,cAAc,CAAA;AACrB,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAe,MAAM,cAAc,CAAA;AAE7E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAE/C,yDAAyD;AACzD,eAAO,MAAM,sBAAsB,uBAAuB,CAAA;AAE1D,cAAc;AACd,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAA;IAC7B,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAA;IAC/B,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAA;CAC5B;AAED,iDAAiD;AACjD,UAAU,SAAS;IACjB,EAAE,EAAE,MAAM,CAAA;IACV;;;;;;OAMG;IACH,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,IAAI,EAAE,OAAO,CAAA;IACb,IAAI,EAAE,OAAO,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,OAAO,CAAA;IACpB,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,IAAI,EAAE,aAAa,EAAE,CAAA;IACrB,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,OAAO,EAAE,UAAU,CAAC,OAAO,cAAc,CAAC,CAAA;CAC3C;AAED,gDAAgD;AAChD,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IAC5E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,SAAS,aAAa,EAAE,CAAA;IAC9B,MAAM,EAAE,SAAS,EAAE,CAAA;CACpB;AAuBD;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,eAAe,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAqCxG;AAiBD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,eAAe,GAAG,IAAI,CAS7E"}