@goodandready/dsh-subscriptions 0.4.16 → 0.4.18

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
@@ -136,6 +136,46 @@ const res = await ctx.subscriptions.request('codex', '/backend-api/codex/images/
136
136
 
137
137
  ---
138
138
 
139
+ ### 9. 🦛 Local Ollama Gateway & Seamless Fallback (`v0.4.17`)
140
+ * **Native Provider (`ollama`)**: When a local Ollama is reachable at `ollamaBaseUrl` (default `http://127.0.0.1:11434`), it appears in the native DSH model picker with the models discovered from `/api/tags`. No API key needed.
141
+ * **Seamless Quota Fallback (`ollamaFallback`, on by default)**: When every account of a provider is exhausted (or unreachable) and nothing has been streamed yet, the chat continues on a local model (`ollamaFallbackModel`, or the first model from `/api/tags`). The fallback is logged and recorded in request history as `kind: fallback`.
142
+ * **Free ($0) Emergency Path**: Works with no internet and no quota — ideal for offline demos.
143
+
144
+ ### 10. ⚡ Reasoning Effort, Verbosity & Fast Mode (`v0.4.17`)
145
+ * **Reasoning Effort**: Codex models advertise their supported effort levels from the live catalog; the native picker validates and the chosen effort is transmitted as `reasoning.effort` in the Codex `/responses` protocol. Grok forwards effort with its own catalog-aware filtering.
146
+ * **Verbosity (`codexVerbosity`)**: `low` / `medium` / `high` is sent as `text.verbosity` for Codex reasoning models. Empty = protocol default.
147
+ * **Fast Mode (`codexFastMode`)**: Sends `service_tier: priority` (1.5x speed billing tier) with every Codex request. The active-subscription chip shows a `⚡` prefix while enabled.
148
+
149
+ ### 11. 🚦 Family-Scoped Cooldowns & Model Filtering (`v0.4.17`)
150
+ * **Reasoning vs Standard**: A 429 on a reasoning model (claude `*thinking*`, grok `*reasoning*`, all codex models) cools down only the reasoning family of that account — standard models on the same account keep working immediately. Legacy cooldowns (from older versions) still block the whole account until expiry.
151
+ * **Hide Deprecated Models (`hideDeprecatedModels`)**: Filters `test`/`preview`/`dev`/`alpha`/`beta`/`legacy` model ids out of the native picker (applies to live catalogs and the static fallback).
152
+
153
+ ---
154
+
155
+ ### 12. 🧯 Safe Reset Credits (`v0.4.18`)
156
+ * **Reset Card Visibility**: The Codex account card shows how many ChatGPT quota reset cards are available and when the earliest one expires.
157
+ * **Deliberate Confirmation Flow**: Consuming a card requires an explicit checkbox ("I understand one attempt will be consumed") plus a mandatory 5-second cooldown before the Reset button activates.
158
+ * **Double-Click Proof**: A host-side single-flight gate (synchronous pending lock before the first network await) makes it impossible for a double click or a concurrent call to consume two cards. Uncertain network results return the challenge to "prepared" for a safe retry of the same request.
159
+ * **Honest Results**: Server verdicts are shown verbatim: `reset` / `nothing to reset` (nothing consumed) / `no usable credit` / `already redeemed`.
160
+
161
+ ### 13. 📊 Composer Quota Indicator & Runway Forecast (`v0.4.18`)
162
+ * **Placement**: Renders in the input area next to the model switcher (`conversation.input.right` slot).
163
+ * **Four display modes (`composerQuota` setting)**: `off` / `percent` (`85%`) / `bar` (40px mini bar, green>30 / amber 10-30 / red <10) / `forecast`.
164
+ * **Runway Forecast**: A sliding 24h window of remaining-percent samples (up to 192 points) feeds a recency-weighted least-squares burn rate; below 30 min of observation or <1% consumed it stays `calibrating…`, with no consumption it says `no usage`. Ready state shows `~4.5h` / `~12m`.
165
+ * **Auto-Hide**: No indicator for local Ollama or when nothing is active.
166
+
167
+ ### 14. 🧑‍💻 SUBS Pill & Session Console (`v0.4.18`)
168
+ * **Pill in the Session Header**: `SUBS (N)` in the session header actions area with a pool-health LED: green <50% max usage, amber 50-90%, red ≥90%, gray when nothing is connected.
169
+ * **Modal Console**: Clicking the pill opens an in-session modal listing every account (provider #index, connected/not, cooldown, quota %) and a shortcut to the settings card. Closes on outside click, ✕ or Escape.
170
+
171
+ ### 15. 🫧 Draggable HUD Widget (`v0.4.18`)
172
+ * **Floating Bubble** on `shell.overlay`: a 64px circle with an SVG ring gauge of the active subscription balance, mounted via `createPortal`.
173
+ * **Drag & Dock**: Drag anywhere; it snaps to any screen edge within 24px (peek-style half-hidden until hovered) and remembers its position in `localStorage`.
174
+ * **Frosted Panel**: Hovering reveals a `backdrop-filter: blur(28px)` panel listing every account with usage bars. Clicking the bubble refreshes quota data; data also refreshes every 60 s.
175
+ * **Settings Card Polish (`v0.4.18`)**: Chevron switched to the core `IconChevronDownOutline14` primitive; explicit settings snapshot states (loading / unavailable + Retry) guard against phantom input.
176
+
177
+ ---
178
+
139
179
  ## 📦 Quick Installation
140
180
 
141
181
  ```bash
@@ -155,6 +195,13 @@ dsh-subscriptions:
155
195
  cooldownMs: 60000
156
196
  autoLoopback: true # v0.4.9: catch loopback OAuth callbacks automatically
157
197
  privacyMask: false # v0.4.9: mask emails and account identifiers in the UI
198
+ ollamaBaseUrl: http://127.0.0.1:11434 # v0.4.17: local Ollama gateway
199
+ ollamaFallback: true # v0.4.17: seamless fallback when all accounts are exhausted
200
+ ollamaFallbackModel: '' # v0.4.17: e.g. qwen2.5-coder; empty = first model from /api/tags
201
+ hideDeprecatedModels: false # v0.4.17: filter test/preview/beta/legacy model ids
202
+ codexVerbosity: '' # v0.4.17: low | medium | high (text.verbosity)
203
+ codexFastMode: false # v0.4.17: service_tier priority (1.5x speed tier)
204
+ composerQuota: 'off' # v0.4.18: composer indicator: off | percent | bar | forecast
158
205
  # Per-slot fields (v0.4.9): expiresAt (ms epoch), proxyUrl (http/https/socks5://)
159
206
  accounts:
160
207
  codex:
package/README.ru.md CHANGED
@@ -136,6 +136,22 @@ const res = await ctx.subscriptions.request('codex', '/backend-api/codex/images/
136
136
 
137
137
  ---
138
138
 
139
+ ### 9. 🦛 Локальный шлюз Ollama и бесшовный фолбэк (`v0.4.17`)
140
+ * **Нативный провайдер (`ollama`)**: если локальный Ollama доступен по `ollamaBaseUrl` (по умолчанию `http://127.0.0.1:11434`), он появляется в нативном селекторе моделей DSH с моделями из `/api/tags`. Ключ не нужен.
141
+ * **Бесшовный фолбэк (`ollamaFallback`, включён по умолчанию)**: когда все аккаунты провайдера исчерпаны (или недоступны) и ещё ничего не выведено, чат продолжается на локальной модели (`ollamaFallbackModel`, либо первая из `/api/tags`). Фолбэк пишется в журнал и в историю запросов как `kind: fallback`.
142
+ * **Бесплатный ($0) аварийный путь**: работает без интернета и без квот — удобно для офлайн-демо.
143
+
144
+ ### 10. ⚡ Effort, Verbosity и Fast Mode (`v0.4.17`)
145
+ * **Уровень рассуждений (Effort)**: модели Codex объявляют поддерживаемые уровни из живого каталога; нативный селектор валидирует выбор, и выбранный уровень передаётся как `reasoning.effort` в протокол `/responses`. Grok передаёт effort со своей каталог-осознанной фильтрацией.
146
+ * **Детализация (`codexVerbosity`)**: `low` / `medium` / `high` передаётся как `text.verbosity` для reasoning-моделей Codex. Пусто = дефолт протокола.
147
+ * **Fast Mode (`codexFastMode`)**: c каждым Codex-запросом передаётся `service_tier: priority` (платный скоростной тир 1.5x). В чипе активной подписки при включённом режиме отображается `⚡`.
148
+
149
+ ### 11. 🚦 Кулдауны по семействам и фильтрация моделей (`v0.4.17`)
150
+ * **Reasoning vs Standard**: 429 на reasoning-модели (claude `*thinking*`, grok `*reasoning*`, все codex-модели) охлаждает только семейство reasoning этого аккаунта — standard-модели того же аккаунта продолжают работать. Legacy-кулдауны (от старых версий) по-прежнему блокируют весь аккаунт до истечения.
151
+ * **Скрыть устаревшие модели (`hideDeprecatedModels`)**: убирает из нативного селектора id с `test`/`preview`/`dev`/`alpha`/`beta`/`legacy` (действует и на живые каталоги, и на статичный фолбэк).
152
+
153
+ ---
154
+
139
155
  ## 📦 Быстрая установка
140
156
 
141
157
  ```bash
@@ -152,6 +168,13 @@ dsh-subscriptions:
152
168
  cooldownMs: 60000
153
169
  autoLoopback: true # v0.4.9: автоматически ловить loopback-callback
154
170
  privacyMask: false # v0.4.9: маскировать email и учётные записи в UI
171
+ ollamaBaseUrl: http://127.0.0.1:11434 # v0.4.17: локальный шлюз Ollama
172
+ ollamaFallback: true # v0.4.17: бесшовный фолбэк при исчерпании всех аккаунтов
173
+ ollamaFallbackModel: '' # v0.4.17: например qwen2.5-coder; пусто = первая модель из /api/tags
174
+ hideDeprecatedModels: false # v0.4.17: фильтровать test/preview/beta/legacy id моделей
175
+ codexVerbosity: '' # v0.4.17: low | medium | high (text.verbosity)
176
+ codexFastMode: false # v0.4.17: service_tier priority (скоростной тир 1.5x)
177
+ composerQuota: 'off' # v0.4.18: индикатор в строке ввода: off | percent | bar | forecast
155
178
  # Поля слота (v0.4.9): expiresAt (ms), proxyUrl (http/https/socks5://)
156
179
  accounts:
157
180
  codex:
package/README.zh.md CHANGED
@@ -89,6 +89,20 @@ graph LR
89
89
  * 每个账号可配置独立代理(`http://`、`https://`、`socks5://`);令牌刷新、供应商检查、模型请求均走该代理。
90
90
  * 账号卡片内的 **Check proxy** 按钮一键检测代理延迟。
91
91
 
92
+ ### 本地 Ollama 网关与无缝回退(`v0.4.17`)
93
+ * **原生 provider (`ollama`)**:本地 Ollama 可达时(`ollamaBaseUrl`,默认 `http://127.0.0.1:11434`),自动出现在 DSH 原生模型选择器中(模型来自 `/api/tags`),无需密钥。
94
+ * **无缝配额回退 (`ollamaFallback`,默认开启)**:某供应商的所有账号耗尽或不可达且尚未输出任何内容时,对话自动切换到本地模型(`ollamaFallbackModel` 或 `/api/tags` 第一个模型),并记录到请求历史(`kind: fallback`)。
95
+ * 免费($0)应急通道:无网络、无配额也能用。
96
+
97
+ ### Effort、Verbosity 与 Fast Mode(`v0.4.17`)
98
+ * **推理力度 (Effort)**:Codex 模型从实时目录上报支持的力度等级,原生选择器校验后以 `reasoning.effort` 传入 `/responses` 协议;Grok 按自身目录过滤转发。
99
+ * **详略程度 (`codexVerbosity`)**:`low`/`medium`/`high` 以 `text.verbosity` 传给 Codex 推理模型;留空为协议默认。
100
+ * **Fast Mode (`codexFastMode`)**:每次 Codex 请求携带 `service_tier: priority`(1.5x 速度计费档);启用时活动订阅芯片显示 `⚡`。
101
+
102
+ ### 按模型家族的冷却与模型过滤(`v0.4.17`)
103
+ * **Reasoning 与 Standard 分离**:推理模型(claude `*thinking*`、grok `*reasoning*`、全部 codex 模型)触发 429 时,只冷却该账号的 reasoning 家族——同账号的 standard 模型立即可用;旧版冷却仍按整账号生效。
104
+ * **隐藏过时模型 (`hideDeprecatedModels`)**:从原生选择器中过滤 `test`/`preview`/`dev`/`alpha`/`beta`/`legacy` 模型 id(同时作用于实时目录与静态回退)。
105
+
92
106
  ### 隐私模式与诊断报告(`v0.4.9`)
93
107
  * **`privacyMask`**:一个开关即可在整个界面隐藏个人数据(邮箱显示为 `j***n@example.com`),服务端遮蔽,适合屏幕共享。
94
108
  * **匿名诊断报告**:设置卡片内一键生成(插件/运行时版本、系统、各供应商健康状态、HTTP 状态聚合、最近错误与耗时、非敏感配置)并自动复制到剪贴板;令牌、邮箱、凭据名与代理地址严格排除。
package/lib/accounts.js CHANGED
@@ -42,6 +42,9 @@ export function vendorConfig(provider, cfg) {
42
42
  originator: pick('Originator', d.originator || ''),
43
43
  systemPrefix: pick('SystemPrefix', d.systemPrefix || ''),
44
44
  clientVersion: pick('ClientVersion', d.clientVersion || ''),
45
+ // #93/#92: per-vendor request shaping (codex only consumes these today).
46
+ verbosity: pick('Verbosity', ''),
47
+ fastMode: !!(cfg && cfg[`${provider}FastMode`]),
45
48
  models: Array.isArray(cfg && cfg[modelsKey]) && cfg[modelsKey].length
46
49
  ? cfg[modelsKey]
47
50
  : (d.models || []),
@@ -124,7 +127,8 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
124
127
  }
125
128
  return {
126
129
  ...base,
127
- cooldownUntil: cooldowns.get(ref) || 0,
130
+ cooldownUntil: cooldowns.get(ref) ? cooldowns.get(ref).until : 0,
131
+ cooldownFamilies: cooldowns.get(ref) && cooldowns.get(ref).families ? cooldowns.get(ref).families : null,
128
132
  quota: quotas.get(ref) || null,
129
133
  usage: windows.get(ref) || base.usage || null,
130
134
  health: health.get(ref) || null,
@@ -272,7 +276,15 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
272
276
  resolveRaw,
273
277
  ensureFresh,
274
278
  refreshUsage,
275
- rememberCooldown(ref, until) { cooldowns.set(ref, until) },
279
+ // #87: family-scoped cooldowns. families=null blocks the whole account
280
+ // (legacy behavior); a list blocks only those model families.
281
+ rememberCooldown(ref, until, families) {
282
+ if (!families || !families.length) { cooldowns.set(ref, { until: Number(until) || 0, families: null }); return }
283
+ const prev = cooldowns.get(ref)
284
+ const merged = new Set(families)
285
+ if (prev && Array.isArray(prev.families)) for (const f of prev.families) merged.add(f)
286
+ cooldowns.set(ref, { until: Math.max(Number(until) || 0, prev ? prev.until : 0), families: Array.from(merged) })
287
+ },
276
288
  rememberQuota(ref, snap) { if (snap) quotas.set(ref, snap); else quotas.delete(ref) },
277
289
  shouldSkipRefresh(ref, now, retryMs) { const f = refreshFailures.get(ref); if (!f) return false; return (Number(f.at) + Number(retryMs)) > Number(now) },
278
290
  getQuota(ref) { return quotas.get(ref) || null },
package/lib/adapter.js CHANGED
@@ -5,6 +5,11 @@ import { modelCatalog } from './messages.js'
5
5
  import { streamWithRotation } from './stream-rotate.js'
6
6
  import { pickFetch } from './proxy.js'
7
7
 
8
+ // #94: heuristic for test/preview/beta/legacy model ids.
9
+ function isDeprecatedId(id) {
10
+ return /(^|[-_:])(test|preview|dev|alpha|beta|snapshot|experimental|legacy|deprecated)([-_:]|$)/i.test(id)
11
+ }
12
+
8
13
  function asLlmError(err) {
9
14
  if (err instanceof LlmError) return err
10
15
  const code = (err && err.code) || 'VENDOR'
@@ -37,16 +42,22 @@ export class SubscriptionAdapter extends LlmAdapter {
37
42
  if (!accounts.some((a) => a.hasToken)) return []
38
43
  const cfg = this.deps.vendorConfig(provider)
39
44
  const fetchImpl = this.deps.fetchImpl || fetch
45
+ let models = null
40
46
  try {
41
47
  const blob = await this.deps.ensureFresh(
42
48
  provider,
43
49
  await this.deps.loadBlob(accounts.find((a) => a.hasToken).ref),
44
50
  accounts.find((a) => a.hasToken).ref,
45
51
  )
46
- const models = await getVendor(provider).listModels(blob, cfg, fetchImpl)
47
- if (Array.isArray(models) && models.length) return models
52
+ const live = await getVendor(provider).listModels(blob, cfg, fetchImpl)
53
+ if (Array.isArray(live) && live.length) models = live
48
54
  } catch { /* use built-in catalog */ }
49
- return modelCatalog(provider, cfg.models)
55
+ if (!models) models = modelCatalog(provider, cfg.models)
56
+ // #94: optionally hide test/preview/beta/legacy ids from the picker.
57
+ if (typeof this.deps.hideDeprecatedModels === 'function' && this.deps.hideDeprecatedModels()) {
58
+ models = models.filter((m) => !isDeprecatedId(String((m && m.id) || '')))
59
+ }
60
+ return models
50
61
  }
51
62
 
52
63
  async resolveModel(provider, model, _signal) {
@@ -69,18 +80,19 @@ export class SubscriptionAdapter extends LlmAdapter {
69
80
  async *stream(options) {
70
81
  const provider = options.provider
71
82
  const deps = this.deps
83
+ let yieldedAny = false
72
84
  try {
73
85
  if (typeof deps.refreshUsage === 'function') {
74
86
  await deps.refreshUsage(provider)
75
87
  }
76
- yield* streamWithRotation({
88
+ for await (const event of streamWithRotation({
77
89
  accounts: await deps.listAccounts(provider),
78
90
  nowMs: () => Date.now(),
79
91
  cooldownMs: deps.cooldownMs(),
80
92
  switchAtRemaining: typeof deps.switchAtRemaining === 'function' ? deps.switchAtRemaining() : (deps.switchAtRemaining ?? 0),
81
93
  options,
82
94
  onCooldown: (account) => {
83
- deps.rememberCooldown(account.ref, account.cooldownUntil)
95
+ deps.rememberCooldown(account.ref, account.cooldownUntil, account.cooldownFamilies || null)
84
96
  if (typeof deps.recordSwitch === 'function') deps.recordSwitch(account.ref)
85
97
  },
86
98
  streamOnce: async function* (account, opts) {
@@ -148,8 +160,15 @@ export class SubscriptionAdapter extends LlmAdapter {
148
160
  throw err
149
161
  }
150
162
  },
151
- })
163
+ })) { yieldedAny = true; yield event }
164
+ return
152
165
  } catch (err) {
166
+ // #91: when the whole pool is exhausted and nothing has been streamed
167
+ // yet, continue seamlessly on local Ollama instead of failing the chat.
168
+ if (!yieldedAny && typeof deps.ollamaFallback === 'function') {
169
+ yield* deps.ollamaFallback({ options, provider, err })
170
+ return
171
+ }
153
172
  throw asLlmError(err)
154
173
  }
155
174
  }