@ychris12138/dsh-usage-stats 0.2.9 → 0.2.10

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.md CHANGED
@@ -98,6 +98,7 @@ npx --yes github:Ychris12138/dsh-usage-stats --no-enable
98
98
  | Z.ai / 智谱 | 订阅 | `ZAI_API_KEY` | Coding Plan quota/subscription |
99
99
  | Kimi For Coding | 订阅 | `KIMI_API_KEY` | `/coding/v1/usages` |
100
100
  | MiniMax Coding Plan | 订阅 | `MINIMAX_API_KEY` | `/v1/token_plan/remains` |
101
+ | Ollama 云 | 订阅 | `OLLAMA_API_KEY` | `/api/usage`(5小时 + 周窗口) |
101
102
  | New API | 余额 | provider 推理 Token | `/api/usage/token/` |
102
103
  | Sub2API / Passion | 自动判别 | provider `apiKeyEnv` | `/v1/usage` |
103
104
  | Sub2API 面板(真实) | 余额 | provider 推理 Token | `/user/balance`(复用 apiKey) |
@@ -139,10 +140,28 @@ KIMI_API_KEY: your-kimi-key
139
140
  MINIMAX_API_KEY: your-minimax-key
140
141
  # 中国区 MiniMax 用户可选;默认 global
141
142
  MINIMAX_API_REGION: cn
143
+ OLLAMA_API_KEY: sk-ollama-your-key
142
144
  ```
143
145
 
144
146
  OpenCode Go 依次尝试 Harness credential、`~/.local/share/opencode/auth.json`,最后才使用显式 `OPENCODE_GO_AUTH_COOKIE + OPENCODE_GO_WORKSPACE_ID` 兼容回退。Bearer usage endpoint 目前不是公开 API,可能随上游变化;Cookie 等同登录凭据,不应进入日志或 issue。
145
147
 
148
+ Ollama 云读取 `OLLAMA_API_KEY`,调用 `/api/usage` 展示两个订阅窗口(5 小时会话 + 每周),无余额;`usage` 按 0..1 比例换算为进度条。
149
+
150
+ Ollama 适配器只对**已配置的 provider** 生效,不会自动添加账户:当 provider 的 id 为 `ollama`,或其 baseURL 主机为 `ollama.com`(含子域)时自动选用;本地 Ollama(`localhost:11434`)不会被当作云配额账户。特殊代理/自定义端点可用显式 monitor 绑定:
151
+
152
+ ```yaml
153
+ # ~/.dsh/profiles/web/cordis.patch.yml
154
+ - insert:
155
+ - id: usage-stats
156
+ name: dsh-usage-stats
157
+ config:
158
+ monitors:
159
+ relay-ollama: # 你配置的 provider id
160
+ adapter: ollama
161
+ usageBaseURL: https://ollama.example.com
162
+ credentialRef: OLLAMA_API_KEY # 非已配置 provider 时必填
163
+ ```
164
+
146
165
  Z.ai 全球区使用 `api.z.ai`,中国区使用 `open.bigmodel.cn`。MiniMax 优先使用官方 `www.minimax.io` / `www.minimaxi.com` Token Plan 地址,并解析 5 小时与周窗口的剩余比例和重置时间。
147
166
 
148
167
  ### New API、Sub2API 与自定义 monitor
package/lib/accounts.js CHANGED
@@ -52,6 +52,7 @@ const ADAPTERS = new Set([
52
52
  "zai-token-plan",
53
53
  "kimi-token-plan",
54
54
  "minimax-token-plan",
55
+ "ollama",
55
56
  "declarative"
56
57
  ]);
57
58
  const SENSITIVE_HEADERS = new Set([
@@ -189,6 +190,15 @@ function defaultAdapter(provider) {
189
190
  try {
190
191
  const hostname = new URL(provider.baseURL).hostname.toLowerCase();
191
192
  if (hostname === "passionapi.com" || hostname.endsWith(".passionapi.com")) return "sub2api";
193
+ // Ollama Cloud is detected by its canonical host so a user-configured
194
+ // provider with a custom id still gets the quota adapter; local Ollama
195
+ // (localhost:11434) never matches and stays without an account adapter.
196
+ // The id check is gated on the hostname so a local install that happens
197
+ // to use the canonical "ollama" id is not misread as a cloud account.
198
+ // Subdomains (e.g. api.ollama.com) are recognized for identification
199
+ // only: the usage query always targets the canonical ollama.com host,
200
+ // which is where the /api/usage endpoint is served.
201
+ if ((providerId === "ollama" || hostname === "ollama.com" || hostname.endsWith(".ollama.com")) && !privateHostname(hostname)) return "ollama";
192
202
  } catch {
193
203
  // A malformed provider URL is handled by the adapter when it is queried.
194
204
  }
@@ -198,7 +208,7 @@ function defaultAdapter(provider) {
198
208
 
199
209
  function adapterMode(adapter, monitor) {
200
210
  if (adapter === "declarative") return monitor.mode;
201
- if (["opencode-go", "zai-token-plan", "kimi-token-plan", "minimax-token-plan"].includes(adapter)) return "subscription";
211
+ if (["opencode-go", "zai-token-plan", "kimi-token-plan", "minimax-token-plan", "ollama"].includes(adapter)) return "subscription";
202
212
  return "balance";
203
213
  }
204
214
 
@@ -1118,7 +1128,8 @@ export async function queryAccount(spec, credentials, deps = {}) {
1118
1128
  const subscriptionId = spec.adapter === "zai-token-plan" ? "zai"
1119
1129
  : spec.adapter === "kimi-token-plan" ? "kimi"
1120
1130
  : spec.adapter === "minimax-token-plan" ? "minimax"
1121
- : "opencode-go";
1131
+ : spec.adapter === "ollama" ? "ollama"
1132
+ : "opencode-go";
1122
1133
  const provider = await collectSubscription(subscriptionId, credentials, {
1123
1134
  apiKeyRef: spec.apiKeyRef,
1124
1135
  region: spec.monitor.region
package/lib/client.js CHANGED
@@ -62,6 +62,7 @@ window.__ModuleLoader__.load({
62
62
  ".usg_accountCard{--usg-providerAccent:#1f6feb;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:linear-gradient(135deg,color-mix(in srgb,var(--usg-providerAccent) 8%,transparent),transparent 42%);border-radius:12px;padding:10px 11px;display:flex;flex-direction:column;gap:9px}",
63
63
  ".usg_accountCard[data-provider=deepseek],.usg_accountCard[data-provider=deepseek-official]{--usg-providerAccent:#1f6feb}",
64
64
  ".usg_accountCard[data-provider=opencode-go]{--usg-providerAccent:#00a67d}",
65
+ ".usg_accountCard[data-provider=ollama],.usg_accountCard[data-adapter=ollama]{--usg-providerAccent:#d97706}",
65
66
  ".usg_accountCard[data-provider=zai],.usg_accountCard[data-provider=zai-coding-cn]{--usg-providerAccent:#7656e8}",
66
67
  ".usg_accountCard[data-provider=openrouter]{--usg-providerAccent:#6366f1}",
67
68
  ".usg_accountCard[data-provider=moonshotai],.usg_accountCard[data-provider=moonshotai-cn],.usg_accountCard[data-provider=kimi],.usg_accountCard[data-provider=kimi-coding]{--usg-providerAccent:#e07a1f}",
@@ -960,6 +961,7 @@ window.__ModuleLoader__.load({
960
961
  "deepseek-official": "DS",
961
962
  deepseek: "DS",
962
963
  "opencode-go": "GO",
964
+ ollama: "OL",
963
965
  openrouter: "OR",
964
966
  moonshotai: "K",
965
967
  "moonshotai-cn": "K",
@@ -968,7 +970,7 @@ window.__ModuleLoader__.load({
968
970
  zai: "Z",
969
971
  "zai-coding-cn": "Z"
970
972
  };
971
- return known[provider.id] ?? String(provider.displayName ?? provider.id).replace(/[^A-Za-z0-9]/g, "").slice(0, 2).toUpperCase();
973
+ return known[provider.id] ?? known[provider.adapter] ?? String(provider.displayName ?? provider.id).replace(/[^A-Za-z0-9]/g, "").slice(0, 2).toUpperCase();
972
974
  }
973
975
 
974
976
  /** Balance-mode body rendered inside the shared provider account frame. */
@@ -1129,6 +1131,7 @@ window.__ModuleLoader__.load({
1129
1131
  return react_jsx_runtime.jsxs("article", {
1130
1132
  className: S.accountCard,
1131
1133
  "data-provider": provider.id,
1134
+ "data-adapter": provider.adapter ?? provider.accountMode ?? "",
1132
1135
  "data-account-mode": mode,
1133
1136
  children: [
1134
1137
  react_jsx_runtime.jsxs("div", {
@@ -38,6 +38,7 @@ const MINIMAX_LEGACY_HOSTS = {
38
38
  };
39
39
  const MINIMAX_USAGE_PATH = "/v1/api/openplatform/coding_plan/remains";
40
40
  const MINIMAX_TOKEN_PLAN_PATH = "/v1/token_plan/remains";
41
+ const OLLAMA_USAGE_URL = "https://ollama.com/api/usage";
41
42
  const DEFAULT_TIMEOUT_MS = 15000;
42
43
 
43
44
  const REFS = {
@@ -48,7 +49,8 @@ const REFS = {
48
49
  zaiRegion: "ZAI_API_REGION",
49
50
  kimiApiKey: "KIMI_API_KEY",
50
51
  minimaxApiKey: "MINIMAX_API_KEY",
51
- minimaxRegion: "MINIMAX_API_REGION"
52
+ minimaxRegion: "MINIMAX_API_REGION",
53
+ ollamaApiKey: "OLLAMA_API_KEY"
52
54
  };
53
55
 
54
56
  function numberOrNull(value) {
@@ -576,6 +578,61 @@ async function collectMiniMax(credentials, deps) {
576
578
  }
577
579
  }
578
580
 
581
+ /**
582
+ * Ollama Cloud usage monitor.
583
+ *
584
+ * Ollama's cloud /api/usage endpoint reports consumed usage ratios for two
585
+ * limit windows (a 5-hour session window and a weekly window), plus an
586
+ * activity cost. There is no monetary balance, so this adapter presents the
587
+ * two windows as subscription-style progress bars, mirroring OpenCode Go.
588
+ */
589
+ function ollamaWindowFromObject(limit, kind) {
590
+ if (limit === null || typeof limit !== "object") return null;
591
+ // limits.session.usage / limits.weekly.usage are 0..1 consumed ratios
592
+ // (observed 0.0x..0.3x on live data). clampPercent maps any numeric ratio
593
+ // onto 0..100, so an over-quota window renders as a fully-used bar rather
594
+ // than silently disappearing, mirroring the OpenCode Go adapter.
595
+ const ratio = numberOrNull(limit.usage);
596
+ if (ratio === null) return null;
597
+ const usedPercent = round1(clampPercent(ratio * 100));
598
+ return {
599
+ kind,
600
+ usedPercent,
601
+ remainingPercent: round1(100 - usedPercent)
602
+ };
603
+ }
604
+
605
+ function parseOllama(body) {
606
+ const limits = body?.limits;
607
+ if (limits === null || typeof limits !== "object") return [];
608
+ return [
609
+ ollamaWindowFromObject(limits.session, "session"),
610
+ ollamaWindowFromObject(limits.weekly, "weekly")
611
+ ].filter(Boolean);
612
+ }
613
+
614
+ async function collectOllama(credentials, deps) {
615
+ const apiKeyRef = deps.apiKeyRef ?? REFS.ollamaApiKey;
616
+ const apiKey = await resolveCredential(credentials, apiKeyRef);
617
+ if (apiKey === "") return { id: "ollama", displayName: "Ollama", mode: "subscription", status: "not-configured", plan: "Ollama", missingCredentials: [apiKeyRef], windows: [] };
618
+ try {
619
+ const body = await request(nonEmptyUrl(deps.baseURL, "/api/usage") ?? OLLAMA_USAGE_URL, {
620
+ headers: { authorization: `Bearer ${apiKey}`, accept: "application/json" }
621
+ }, deps, "json");
622
+ const windows = parseOllama(body);
623
+ return {
624
+ id: "ollama",
625
+ displayName: "Ollama",
626
+ mode: "subscription",
627
+ status: windows.length > 0 ? "ok" : "invalid-response",
628
+ plan: "Ollama",
629
+ windows
630
+ };
631
+ } catch (error) {
632
+ return { id: "ollama", displayName: "Ollama", mode: "subscription", status: normalizedStatus(error), plan: "Ollama", windows: [] };
633
+ }
634
+ }
635
+
579
636
  /** Query one subscription/token-plan adapter. */
580
637
  export async function collectSubscription(providerId, credentials, options = {}, deps = {}) {
581
638
  const shared = {
@@ -596,6 +653,7 @@ export async function collectSubscription(providerId, credentials, options = {},
596
653
  });
597
654
  if (providerId === "kimi") return collectKimi(credentials, shared);
598
655
  if (providerId === "minimax") return collectMiniMax(credentials, shared);
656
+ if (providerId === "ollama") return collectOllama(credentials, shared);
599
657
  return { id: providerId, displayName: providerId, mode: "subscription", status: "unavailable", windows: [] };
600
658
  }
601
659
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ychris12138/dsh-usage-stats",
3
3
  "description": "Token usage heatmap, provider balances, and subscription quotas for the dsh web GUI",
4
- "version": "0.2.9",
4
+ "version": "0.2.10",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/Ychris12138/dsh-usage-stats.git"