@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 +47 -0
- package/README.ru.md +23 -0
- package/README.zh.md +14 -0
- package/lib/accounts.js +14 -2
- package/lib/adapter.js +25 -6
- package/lib/client.js +616 -13
- package/lib/forecast.js +71 -0
- package/lib/index.js +142 -3
- package/lib/messages.js +8 -1
- package/lib/ollama.js +70 -0
- package/lib/reset-credits.js +193 -0
- package/lib/rotate.js +28 -3
- package/lib/stream-rotate.js +4 -3
- package/lib/subscriptions.js +4 -4
- package/lib/vendors/codex.js +1 -1
- package/lib/vendors/grok.js +2 -0
- package/package.json +4 -1
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)
|
|
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
|
-
|
|
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
|
|
47
|
-
if (Array.isArray(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|