@goodandready/dsh-subscriptions 0.6.68 → 0.6.70
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 +12 -0
- package/README.ru.md +12 -0
- package/README.zh.md +12 -0
- package/lib/adapter-manager.js +1 -1
- package/lib/cascade.js +27 -2
- package/lib/client.js +143 -4
- package/lib/code-assist.js +24 -4
- package/lib/config-schema.js +2 -0
- package/lib/diagnostics.js +6 -1
- package/lib/error-buffer.js +49 -0
- package/lib/index.js +36 -27
- package/lib/messages.js +7 -1
- package/lib/rotate.js +1 -0
- package/lib/routes/status.js +43 -6
- package/lib/stream-rotate.js +9 -0
- package/lib/vendors/antigravity.js +78 -26
- package/lib/wire.js +6 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -207,6 +207,18 @@ const res = await ctx.subscriptions.request('codex', '/backend-api/codex/images/
|
|
|
207
207
|
* **In-App Toast Alerts (`enableToasts`)**: Lightweight floating toast alerts in the DSH chat interface notifying you when accounts enter quarantine, recover from cooldowns, or exceed quota safety thresholds.
|
|
208
208
|
* **Automated Webhook Integration**: Instant event notifications dispatched via `notifyQuarantineEntered` and `notifyQuarantineReleased`.
|
|
209
209
|
|
|
210
|
+
### 24. 🛡️ Resilience, Diagnostics & Fallback Enhancements (v0.6.70)
|
|
211
|
+
* **Cross-Vendor Cascading Fallback & Provider Priority Ordering (#536)**: Configure fallback ordering across subscription providers with interactive reordering in WebUI.
|
|
212
|
+
* **1-Click Connection Smoke Test & Latency Ping (#536)**: Test individual account connections on the spot with real-time latency badges.
|
|
213
|
+
* **Live Cooldown Countdown & Availability Toasts (#536)**: Exact countdown timer for cooled-down accounts and automated toast alerts upon recovery.
|
|
214
|
+
* **Proactive OAuth Token Heartbeat (#536)**: Pre-emptive background token refresh before expiry to prevent first-query latency spikes.
|
|
215
|
+
* **Recent Errors Ring Buffer Inspector (#536)**: In-memory sanitized log of the last 20 runtime failures in WebUI Diagnostics.
|
|
216
|
+
|
|
217
|
+
### 23. 🧠 Antigravity Tiered Models, Tool Sentinel & Stream Resilience (v0.6.69)
|
|
218
|
+
* **Gemini 3 Tool Replay Sentinel (#534)**: Injects `thoughtSignature: 'skip_thought_signature_validator'` on reconstructed tool calls, enabling reliable multi-turn tool calling without Gemini 3 validation failures.
|
|
219
|
+
* **Tiered Flash Models & Thinking Levels (#534)**: Exposes Gemini 3.8 and 3.7 Flash High, Medium, and Low thinking levels with upstream `*-flash-tiered` mapping.
|
|
220
|
+
* **Stream Resilience & Empty Stream Retry (#534)**: Idle SSE timeout (30s), empty response detection, and transient stream retry with automatic account rotation.
|
|
221
|
+
|
|
210
222
|
### 22. ⚡ xAI Grok RFC 8628 Device Authorization Flow (v0.6.68)
|
|
211
223
|
* **RFC 8628 Device Code Authorization (GH #17, #532)**: Native device authorization grant support for xAI Grok. Solves the issue where xAI presents a Grok Build user-code verification page for CLI client IDs without redirecting back to loopback callbacks. Provides direct user code display and automatic token polling.
|
|
212
224
|
|
package/README.ru.md
CHANGED
|
@@ -183,6 +183,18 @@ const res = await ctx.subscriptions.request('codex', '/backend-api/codex/images/
|
|
|
183
183
|
* **Всплывающие уведомления (Toast Alerts)**: Легковесные всплывающие тосты в веб-интерфейсе DSH при событиях карантина (с отображением причины), восстановлении доступа и критическом расходе квоты. Включение управляется параметром `enableToasts` (по умолчанию `true`).
|
|
184
184
|
* **Интеграция с вебхуками**: Автоматическая отправка уведомлений через `notifyQuarantineEntered` и `notifyQuarantineReleased`.
|
|
185
185
|
|
|
186
|
+
### 20. 🛡️ Отказоустойчивость, диагностика и порядок фолбэка (v0.6.70)
|
|
187
|
+
* **Межвендорный каскадный фолбэк с настройкой очередности (#536)**: Настройка порядка переключения провайдеров при исчерпании квот с интерактивным изменением приоритета (▲/▼) в WebUI.
|
|
188
|
+
* **1-Click проверка подключения и пинг задержки (#536)**: Проверка конкретного аккаунта в один клик с отображением задержки (`✓ 240ms`).
|
|
189
|
+
* **Живой таймер кулдауна и уведомления о восстановлении (#536)**: Точный обратный отсчет кулдауна и всплывающие in-app тосты при возвращении аккаунта в строй.
|
|
190
|
+
* **Превентивный OAuth Heartbeat (#536)**: Упреждающее фоновое обновление токенов до истечения срока действия.
|
|
191
|
+
* **Журнал последних ошибок (Ring Buffer) (#536)**: Встроенный инспектор последних 20 ошибок в WebUI Diagnostics с кнопкой очистки.
|
|
192
|
+
|
|
193
|
+
### 19. 🧠 Antigravity Tiered-модели, сентинел тулов и устойчивость стримов (v0.6.69)
|
|
194
|
+
* **Сентинел реплея тулов Gemini 3 (#534)**: Автоматическая вставка `thoughtSignature: 'skip_thought_signature_validator'` для реплея истории вызовов тулов без сбоев валидации Google API.
|
|
195
|
+
* **Tiered-модели и уровни мышления (#534)**: Выбор уровней High/Medium/Low для Gemini 3.8 и 3.7 Flash с маппингом в upstream `*-flash-tiered` и `thinkingLevel`.
|
|
196
|
+
* **Устойчивость стримов и авто-ретрай (#534)**: Защита от зависания SSE (таймаут 30с), детекция пустых ответов и авто-ротация аккаунтов по `EMPTY_RESPONSE`/`TIMEOUT`.
|
|
197
|
+
|
|
186
198
|
### 18. ⚡ RFC 8628 Device Code авторизация для xAI Grok (v0.6.68)
|
|
187
199
|
* **Авторизация по коду устройства (GH #17, #532)**: Нативная поддержка RFC 8628 device code flow для xAI Grok. Устраняет проблему, когда xAI для CLI client ID открывает страницу ввода кода для Grok Build без обратного редиректа на loopback-порт. Отображает одноразовый код на карточке и автоматически поллит токены в фоне.
|
|
188
200
|
|
package/README.zh.md
CHANGED
|
@@ -132,6 +132,18 @@ graph LR
|
|
|
132
132
|
queueTimeoutMs: 30000 # v0.6.62: 队列最大等待超时时间(毫秒)
|
|
133
133
|
* **自动化 Webhook 告警集成**:通过 `notifyQuarantineEntered` 与 `notifyQuarantineReleased` 自动向外部通知系统派发隔离生命周期事件。
|
|
134
134
|
|
|
135
|
+
### 🛡️ 弹性容灾、故障诊断与跨服务商回退 (v0.6.70)
|
|
136
|
+
* **跨服务商级联故障转移与优先级排序 (#536)**: 支持自定义跨服务商回退顺序,可在 WebUI 中通过 ▲/▼ 交互调整优先级。
|
|
137
|
+
* **一键链路测试与延迟检测 (#536)**: 在账户列表或状态弹窗中一键发起探活并显示往返延迟。
|
|
138
|
+
* **冷却倒计时与就绪通知 (#536)**: 实时展示受限账户恢复倒计时,解除冷却时自动弹出就绪通知。
|
|
139
|
+
* **主动 OAuth 令牌心跳 (#536)**: 提前预刷即将过期的访问令牌,避免用户请求时产生额外等待。
|
|
140
|
+
* **最近错误环形日志审查 (#536)**: 内存保存最近 20 条已脱敏的运行错误,支持在 Diagnostics 中查看与清空。
|
|
141
|
+
|
|
142
|
+
### Antigravity 分档思考模型、工具回放哨兵与流式自愈 (v0.6.69)
|
|
143
|
+
* **Gemini 3 工具回放哨兵 (#534)**:在重建的工具调用中注入 `thoughtSignature: 'skip_thought_signature_validator'`,彻底解决多轮工具调用历史重放时 Gemini 3 的校验失败问题。
|
|
144
|
+
* **分档 Flash 模型与思考档位 (#534)**:将 `gemini-3.8-flash-tiered` 与 `3.7` 自动解析展开为 High/Medium/Low 档位选择,并在调用时自动组装 `thinkingConfig.thinkingLevel`。
|
|
145
|
+
* **流式空响应自愈与账号轮换 (#534)**:引入 30 秒 SSE 空闲超时保护与空响应检测,在未输出前自动重试一次,失败时通过 `EMPTY_RESPONSE` / `TIMEOUT` 自动轮换至下一个可用账号。
|
|
146
|
+
|
|
135
147
|
### xAI Grok RFC 8628 设备码授权流支持 (v0.6.68)
|
|
136
148
|
* **RFC 8628 设备码授权流 (GH #17, #532)**:为 xAI Grok 新增原生设备码登录授权支持。彻底解决 xAI 针对 CLI 客户端标识返回 Grok Build 验证码页面且不触发回环重定向回调导致的死锁问题。直接在槽位卡片展示用户授权码,并自动在后台轮询兑换会话令牌。
|
|
137
149
|
|
package/lib/adapter-manager.js
CHANGED
|
@@ -75,7 +75,7 @@ export function createAdapterManager({
|
|
|
75
75
|
const loggedList = Array.isArray(logged) ? logged : Object.keys(logged || {}).filter((p) => logged[p])
|
|
76
76
|
const available = loggedList.filter((p) => p !== provider)
|
|
77
77
|
visited.add(provider)
|
|
78
|
-
const nextVendor = resolveFallbackVendor(provider, available, cfg.cascadingChain, visited)
|
|
78
|
+
const nextVendor = resolveFallbackVendor(provider, available, cfg.cascadingChain, visited, cfg.cascadingFallbackOrder)
|
|
79
79
|
if (!nextVendor) throw err
|
|
80
80
|
visited.add(nextVendor)
|
|
81
81
|
const targetModel = mapFallbackModel(provider, options && options.model, nextVendor)
|
package/lib/cascade.js
CHANGED
|
@@ -29,10 +29,35 @@ export const DEFAULT_CASCADE_CHAINS = {
|
|
|
29
29
|
* @param {Set<string>} [visited] - set of already attempted providers in this request cascade
|
|
30
30
|
* @returns {string|null} - the next candidate provider or null if none available
|
|
31
31
|
*/
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Resolves the next candidate fallback provider from the cascade chain.
|
|
34
|
+
* @param {string} currentProvider - currently failing provider
|
|
35
|
+
* @param {Set<string>|Array<string>} availableProviders - providers that currently have logged-in accounts
|
|
36
|
+
* @param {Record<string, string[]>} [customChains] - user-configured custom chains
|
|
37
|
+
* @param {Set<string>} [visited] - set of already attempted providers in this request cascade
|
|
38
|
+
* @param {string[]} [globalOrder] - configured global fallback priority sequence
|
|
39
|
+
* @returns {string|null} - the next candidate provider or null if none available
|
|
40
|
+
*/
|
|
41
|
+
export function resolveFallbackVendor(currentProvider, availableProviders, customChains = {}, visited = new Set(), globalOrder = []) {
|
|
33
42
|
const availSet = new Set(availableProviders || [])
|
|
34
|
-
const
|
|
43
|
+
const custom = customChains && customChains[currentProvider]
|
|
44
|
+
if (Array.isArray(custom) && custom.length > 0) {
|
|
45
|
+
for (const candidate of custom) {
|
|
46
|
+
if (candidate !== currentProvider && !visited.has(candidate) && availSet.has(candidate)) {
|
|
47
|
+
return candidate
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (Array.isArray(globalOrder) && globalOrder.length > 0) {
|
|
53
|
+
for (const candidate of globalOrder) {
|
|
54
|
+
if (candidate !== currentProvider && !visited.has(candidate) && availSet.has(candidate)) {
|
|
55
|
+
return candidate
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
35
59
|
|
|
60
|
+
const chain = DEFAULT_CASCADE_CHAINS[currentProvider] || []
|
|
36
61
|
for (const candidate of chain) {
|
|
37
62
|
if (candidate !== currentProvider && !visited.has(candidate) && availSet.has(candidate)) {
|
|
38
63
|
return candidate
|
package/lib/client.js
CHANGED
|
@@ -430,6 +430,17 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
430
430
|
"smokeTesting": "Pinging…",
|
|
431
431
|
"smokeOk": "Smoke test passed",
|
|
432
432
|
"smokeFail": "Smoke test failed",
|
|
433
|
+
"testConnection": "Test",
|
|
434
|
+
"testConnectionSuccess": "Connection verified",
|
|
435
|
+
"cascadingFallback": "Cross-vendor fallback",
|
|
436
|
+
"cascadingFallbackHint": "Automatically route queries to alternative providers when all accounts of a primary provider are exhausted.",
|
|
437
|
+
"cascadingFallbackOrder": "Fallback Provider Order",
|
|
438
|
+
"cascadingFallbackOrderHint": "Sequence of alternative providers to try when primary accounts are exhausted.",
|
|
439
|
+
"recentErrors": "Recent Runtime Errors",
|
|
440
|
+
"recentErrorsDesc": "Last 20 runtime failures across model streams and providers (sanitized).",
|
|
441
|
+
"recentErrorsClear": "Clear Log",
|
|
442
|
+
"recentErrorsEmpty": "No recent errors recorded",
|
|
443
|
+
"toastCooldownRecovered": "is now available (cooldown expired)",
|
|
433
444
|
"modelsCatalogTitle": "Curated Subscription Models",
|
|
434
445
|
"modelsCatalogDesc": "Available model catalog across active subscriptions in DeepSeek Harness.",
|
|
435
446
|
"rotationDecisionsTitle": "Rotation & Routing Decisions",
|
|
@@ -702,6 +713,17 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
702
713
|
"smokeTesting": "正在探活…",
|
|
703
714
|
"smokeOk": "冒烟测试成功",
|
|
704
715
|
"smokeFail": "冒烟测试失败",
|
|
716
|
+
"testConnection": "测试",
|
|
717
|
+
"testConnectionSuccess": "连接已验证",
|
|
718
|
+
"cascadingFallback": "跨服务商自动故障转移",
|
|
719
|
+
"cascadingFallbackHint": "当首选服务商所有账户配额耗尽或限流时,自动无缝转接至备选服务商。",
|
|
720
|
+
"cascadingFallbackOrder": "故障转移供应商顺序",
|
|
721
|
+
"cascadingFallbackOrderHint": "首选服务商耗尽时按此顺序依次尝试备选服务商。",
|
|
722
|
+
"recentErrors": "最近运行错误记录",
|
|
723
|
+
"recentErrorsDesc": "最近 20 条流式传输与 API 调用失败记录(已脱敏)。",
|
|
724
|
+
"recentErrorsClear": "清空记录",
|
|
725
|
+
"recentErrorsEmpty": "暂无错误记录",
|
|
726
|
+
"toastCooldownRecovered": "已解除冷却恢复可用",
|
|
705
727
|
"modelsCatalogTitle": "精选订阅模型",
|
|
706
728
|
"modelsCatalogDesc": "当前 DeepSeek Harness 活跃订阅下可用模型一览。",
|
|
707
729
|
"rotationDecisionsTitle": "轮询调度与路由决策",
|
|
@@ -987,7 +1009,13 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
987
1009
|
if (account.quarantineReason && account.quarantineUntil && account.quarantineUntil > Date.now()) {
|
|
988
1010
|
return account.quarantineReason === 'VERIFY' ? t('verifyAccount') : String(account.quarantineReason)
|
|
989
1011
|
}
|
|
990
|
-
if (account.cooldownUntil && account.cooldownUntil > Date.now())
|
|
1012
|
+
if (account.cooldownUntil && account.cooldownUntil > Date.now()) {
|
|
1013
|
+
const leftSec = Math.max(0, Math.ceil((account.cooldownUntil - Date.now()) / 1000))
|
|
1014
|
+
const m = Math.floor(leftSec / 60)
|
|
1015
|
+
const s = leftSec % 60
|
|
1016
|
+
const pad = (n) => String(n).padStart(2, "0")
|
|
1017
|
+
return t("coolingDown") + " (" + pad(m) + ":" + pad(s) + ")"
|
|
1018
|
+
}
|
|
991
1019
|
if (account.usagePercent != null && account.usagePercent >= 100) return t('usageFull')
|
|
992
1020
|
return t('connected')
|
|
993
1021
|
}
|
|
@@ -1114,12 +1142,14 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1114
1142
|
fetch('/dsh-subscriptions/diagnostics', { cache: 'no-store' })
|
|
1115
1143
|
.then((r) => r.json())
|
|
1116
1144
|
.then((d) => {
|
|
1145
|
+
if (d && d.ok && d.report) setDiagReport(d.report)
|
|
1117
1146
|
const txt = d && d.ok ? JSON.stringify(d.report, null, 2) : ('error: ' + ((d && d.error && d.error.message) || 'unknown'))
|
|
1118
1147
|
setDiag(txt)
|
|
1119
1148
|
try { navigator.clipboard.writeText(txt) } catch { /* clipboard write disallowed */ }
|
|
1120
1149
|
})
|
|
1121
1150
|
.catch((e) => setDiag('error: ' + String(e && e.message || e)))
|
|
1122
1151
|
}
|
|
1152
|
+
const [diagReport, setDiagReport] = React.useState(null)
|
|
1123
1153
|
const [checkRes, setCheckRes] = React.useState({})
|
|
1124
1154
|
const [hostPing, setHostPing] = React.useState({ ok: true, latencyMs: null })
|
|
1125
1155
|
const [telemetry, setTelemetry] = React.useState(null)
|
|
@@ -1259,6 +1289,10 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1259
1289
|
.then((r) => r.json())
|
|
1260
1290
|
.then((d) => { if (d && d.ok && d.queue) setQueueStats(d.queue) })
|
|
1261
1291
|
.catch(() => {})
|
|
1292
|
+
fetch('/dsh-subscriptions/diagnostics', { cache: 'no-store' })
|
|
1293
|
+
.then((r) => r.json())
|
|
1294
|
+
.then((d) => { if (d && d.ok && d.report) setDiagReport(d.report) })
|
|
1295
|
+
.catch(() => {})
|
|
1262
1296
|
}
|
|
1263
1297
|
|
|
1264
1298
|
const runSmokeTest = async (provider, index) => {
|
|
@@ -2081,6 +2115,48 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
2081
2115
|
React.createElement('span', null, t('notifyLimits')),
|
|
2082
2116
|
),
|
|
2083
2117
|
React.createElement('div', { className: 'dsub-sub' }, t('notifyLimitsHint')),
|
|
2118
|
+
React.createElement('label', { className: 'dsub-row', style: { marginTop: 10 } },
|
|
2119
|
+
React.createElement('input', {
|
|
2120
|
+
type: 'checkbox',
|
|
2121
|
+
checked: Boolean(draft.cascadingFallback),
|
|
2122
|
+
onChange: (e) => setDraft((d) => Object.assign({}, d, { cascadingFallback: e.target.checked })),
|
|
2123
|
+
}),
|
|
2124
|
+
React.createElement('span', null, t('cascadingFallback')),
|
|
2125
|
+
),
|
|
2126
|
+
React.createElement('div', { className: 'dsub-sub' }, t('cascadingFallbackHint')),
|
|
2127
|
+
Boolean(draft.cascadingFallback) ? React.createElement('div', { style: { marginTop: 8, padding: '8px 12px', background: 'var(--dsw-alias-bg-layer-2)', borderRadius: 8 } },
|
|
2128
|
+
React.createElement('div', { style: { fontSize: 12, fontWeight: 600, marginBottom: 4 } }, t('cascadingFallbackOrder')),
|
|
2129
|
+
React.createElement('div', { className: 'dsub-sub', style: { marginBottom: 8 } }, t('cascadingFallbackOrderHint')),
|
|
2130
|
+
React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 4 } },
|
|
2131
|
+
(Array.isArray(draft.cascadingFallbackOrder) && draft.cascadingFallbackOrder.length ? draft.cascadingFallbackOrder : (providers.length ? providers : ['antigravity', 'claude', 'codex', 'grok', 'copilot', 'cursor'])).map((p, idx, arr) => {
|
|
2132
|
+
return React.createElement('div', { key: p, style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 8px', background: 'var(--dsw-alias-bg-layer-1)', borderRadius: 6, fontSize: 12 } },
|
|
2133
|
+
React.createElement('span', null, (idx + 1) + '. ' + p.toUpperCase()),
|
|
2134
|
+
React.createElement('div', { style: { display: 'flex', gap: 4 } },
|
|
2135
|
+
idx > 0 ? React.createElement('button', {
|
|
2136
|
+
type: 'button', className: 'dsub-mini', style: { padding: '1px 6px', fontSize: 10 },
|
|
2137
|
+
onClick: () => {
|
|
2138
|
+
const next = arr.slice()
|
|
2139
|
+
const tmp = next[idx - 1]
|
|
2140
|
+
next[idx - 1] = next[idx]
|
|
2141
|
+
next[idx] = tmp
|
|
2142
|
+
setDraft((d) => Object.assign({}, d, { cascadingFallbackOrder: next }))
|
|
2143
|
+
}
|
|
2144
|
+
}, '▲') : null,
|
|
2145
|
+
idx < arr.length - 1 ? React.createElement('button', {
|
|
2146
|
+
type: 'button', className: 'dsub-mini', style: { padding: '1px 6px', fontSize: 10 },
|
|
2147
|
+
onClick: () => {
|
|
2148
|
+
const next = arr.slice()
|
|
2149
|
+
const tmp = next[idx + 1]
|
|
2150
|
+
next[idx + 1] = next[idx]
|
|
2151
|
+
next[idx] = tmp
|
|
2152
|
+
setDraft((d) => Object.assign({}, d, { cascadingFallbackOrder: next }))
|
|
2153
|
+
}
|
|
2154
|
+
}, '▼') : null,
|
|
2155
|
+
)
|
|
2156
|
+
)
|
|
2157
|
+
})
|
|
2158
|
+
)
|
|
2159
|
+
) : null,
|
|
2084
2160
|
),
|
|
2085
2161
|
),
|
|
2086
2162
|
),
|
|
@@ -2105,6 +2181,39 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
2105
2181
|
}, t('diagCopy')),
|
|
2106
2182
|
React.createElement('pre', { className: 'dsub-diag' }, diag),
|
|
2107
2183
|
) : null,
|
|
2184
|
+
React.createElement('details', { className: 'dsub-details', style: { marginTop: 12 } },
|
|
2185
|
+
React.createElement('summary', { className: 'dsub-summary' }, '⚠️ ' + t('recentErrors')),
|
|
2186
|
+
React.createElement('div', { className: 'dsub-block' },
|
|
2187
|
+
React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 } },
|
|
2188
|
+
React.createElement('span', { className: 'dsub-sub' }, t('recentErrorsDesc')),
|
|
2189
|
+
React.createElement('button', {
|
|
2190
|
+
type: 'button', className: 'dsub-mini',
|
|
2191
|
+
onClick: async () => {
|
|
2192
|
+
await fetch('/dsh-subscriptions/diagnostics/errors/clear', { method: 'POST' }).catch(() => {})
|
|
2193
|
+
genDiag()
|
|
2194
|
+
}
|
|
2195
|
+
}, t('recentErrorsClear'))
|
|
2196
|
+
),
|
|
2197
|
+
(diagReport && Array.isArray(diagReport.recentErrors) && diagReport.recentErrors.length ? diagReport.recentErrors : []).length === 0
|
|
2198
|
+
? React.createElement('div', { className: 'dsub-dim', style: { padding: 8 } }, t('recentErrorsEmpty'))
|
|
2199
|
+
: React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } },
|
|
2200
|
+
(diagReport.recentErrors || []).map((err, i) => React.createElement('div', {
|
|
2201
|
+
key: i,
|
|
2202
|
+
style: { padding: '6px 10px', background: 'var(--dsw-alias-bg-layer-2)', borderRadius: 6, fontSize: 11.5, borderLeft: '3px solid var(--dsw-alias-state-error-primary)' }
|
|
2203
|
+
},
|
|
2204
|
+
React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', marginBottom: 2 } },
|
|
2205
|
+
React.createElement('span', { style: { fontWeight: 600 } }, (err.provider || 'unknown').toUpperCase() + (err.model ? ' · ' + err.model : '')),
|
|
2206
|
+
React.createElement('span', { className: 'dsub-dim' }, (err.ts || '').slice(11, 19))
|
|
2207
|
+
),
|
|
2208
|
+
React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center' } },
|
|
2209
|
+
err.code ? React.createElement('span', { className: 'dsub-pillTag', style: { background: 'color-mix(in srgb,var(--dsw-alias-state-error-primary) 15%,transparent)', color: 'var(--dsw-alias-state-error-primary)' } }, err.code) : null,
|
|
2210
|
+
err.status ? React.createElement('span', { className: 'dsub-dim' }, 'HTTP ' + err.status) : null,
|
|
2211
|
+
React.createElement('span', { style: { opacity: 0.9, wordBreak: 'break-word' } }, err.message)
|
|
2212
|
+
)
|
|
2213
|
+
))
|
|
2214
|
+
)
|
|
2215
|
+
)
|
|
2216
|
+
),
|
|
2108
2217
|
wizardModal ? React.createElement('div', { className: 'dsub-modalWrap', onClick: (e) => { if (e.target === e.currentTarget) setWizardModal(null) } },
|
|
2109
2218
|
React.createElement('div', { className: 'dsub-modal' },
|
|
2110
2219
|
React.createElement('div', { className: 'dsub-modalHead' },
|
|
@@ -2269,8 +2378,16 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
2269
2378
|
React.createElement('button', {
|
|
2270
2379
|
type: 'button', className: 'dsub-mini',
|
|
2271
2380
|
disabled: checking[key],
|
|
2381
|
+
title: t('smokeDesc'),
|
|
2272
2382
|
onClick: () => doCheck(row.slot.provider, row.slot.index).catch((e) => setErr(cleanErrorMessage(e.message || e))),
|
|
2273
|
-
}, checking[key] ? t('checking') : t('
|
|
2383
|
+
}, checking[key] ? t('checking') : ('⚡ ' + t('testConnection'))),
|
|
2384
|
+
(function(){
|
|
2385
|
+
var r = checkRes[key];
|
|
2386
|
+
if (!r) return null;
|
|
2387
|
+
var cls = r.ok ? 'dsub-badge-ok' : 'dsub-badge-bad';
|
|
2388
|
+
var txt = r.ok ? ('✓ ' + (r.latencyMs != null ? r.latencyMs + 'ms' : 'OK')) : ('✗ ' + ((r.error && (r.error.code || r.error.message)) || 'FAIL'));
|
|
2389
|
+
return React.createElement('span', { className: 'dsub-badge ' + cls, style: { marginLeft: 4 }, title: r.ok ? t('testConnectionSuccess') : ((r.error && r.error.message) || 'fail') }, txt);
|
|
2390
|
+
})(),
|
|
2274
2391
|
React.createElement('button', {
|
|
2275
2392
|
type: 'button', className: 'dsub-mini',
|
|
2276
2393
|
title: t('wizardTitle'),
|
|
@@ -3141,6 +3258,24 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
3141
3258
|
)
|
|
3142
3259
|
}
|
|
3143
3260
|
|
|
3261
|
+
const prevCooldownsRef = React.useRef(new Map())
|
|
3262
|
+
React.useEffect(() => {
|
|
3263
|
+
for (const a of (state.accounts || [])) {
|
|
3264
|
+
const isCooled = Boolean(a.cooldownUntil && a.cooldownUntil > now)
|
|
3265
|
+
const wasCooled = prevCooldownsRef.current.get(a.ref)
|
|
3266
|
+
if (wasCooled && !isCooled) {
|
|
3267
|
+
const toastId = 't_cool_' + a.ref + '_' + Date.now()
|
|
3268
|
+
setToasts((prev) => [{
|
|
3269
|
+
id: toastId,
|
|
3270
|
+
message: (a.label || a.ref) + ' ' + t('toastCooldownRecovered'),
|
|
3271
|
+
cls: 'dsub-toast-ok',
|
|
3272
|
+
}, ...prev].slice(0, 3))
|
|
3273
|
+
setTimeout(() => dismissToast(toastId), 5000)
|
|
3274
|
+
}
|
|
3275
|
+
prevCooldownsRef.current.set(a.ref, isCooled)
|
|
3276
|
+
}
|
|
3277
|
+
}, [state.accounts, now])
|
|
3278
|
+
|
|
3144
3279
|
// Accounts list
|
|
3145
3280
|
const rows = state.accounts.map((a, i) => {
|
|
3146
3281
|
const isLogged = state.logged.includes(a.provider)
|
|
@@ -3157,8 +3292,12 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
3157
3292
|
statusText = a.quarantineReason === 'VERIFY' ? t('verifyAccount') : String(a.quarantineReason)
|
|
3158
3293
|
} else if (cooled) {
|
|
3159
3294
|
statusClass = 'dsub-statusWarn'
|
|
3160
|
-
|
|
3161
|
-
|
|
3295
|
+
const leftSec = Math.max(0, Math.ceil((a.cooldownUntil - now) / 1000))
|
|
3296
|
+
const m = Math.floor(leftSec / 60)
|
|
3297
|
+
const s = leftSec % 60
|
|
3298
|
+
const pad = (n) => String(n).padStart(2, '0')
|
|
3299
|
+
const fams = Array.isArray(a.cooldownFamilies) && a.cooldownFamilies.length ? ' · ' + a.cooldownFamilies.join('/') : ''
|
|
3300
|
+
statusText = t('subsCooldown') + ' (' + pad(m) + ':' + pad(s) + ')' + fams
|
|
3162
3301
|
} else if (isLogged) {
|
|
3163
3302
|
if (pct >= 90) { statusClass = 'dsub-statusBad'; statusText = '90%+ ' + t('subsUsed') }
|
|
3164
3303
|
else if (pct >= 50) { statusClass = 'dsub-statusWarn'; statusText = pct + '% ' + t('subsUsed') }
|
package/lib/code-assist.js
CHANGED
|
@@ -156,21 +156,28 @@ function catalogRows(json) {
|
|
|
156
156
|
return []
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
export function antigravityTieredAlias(model) {
|
|
160
|
+
const match = /^gemini-(3\.[78])-flash-(low|medium|high)$/.exec(model || '')
|
|
161
|
+
return match ? { model: `gemini-${match[1]}-flash-tiered`, thinkingLevel: match[2] } : null
|
|
162
|
+
}
|
|
163
|
+
|
|
159
164
|
export async function fetchAvailableModels(fetchImpl, token, extraHeaders, fallbackIds, provider) {
|
|
160
165
|
try {
|
|
161
166
|
const json = await postAssist(fetchImpl, 'fetchAvailableModels', token, {}, extraHeaders)
|
|
162
167
|
const out = []
|
|
163
|
-
|
|
168
|
+
const catalog = catalogRows(json)
|
|
169
|
+
const publishedIds = new Set(catalog.map((row) => String(row?.id || '').replace(/^models\//, '')))
|
|
170
|
+
for (const row of catalog) {
|
|
164
171
|
if (!row) continue
|
|
165
172
|
if (typeof row === 'string') {
|
|
166
|
-
if (row && !row.startsWith('chat_')) out.push({ provider, id: row, name: row })
|
|
173
|
+
if (row && !row.startsWith('chat_') && !row.startsWith('tab_')) out.push({ provider, id: row, name: row })
|
|
167
174
|
continue
|
|
168
175
|
}
|
|
169
176
|
if (row.isInternal) continue
|
|
170
177
|
const raw = String(row.id || row.name || row.model || '').replace(/^models\//, '')
|
|
171
|
-
if (!raw || raw.startsWith('chat_')) continue
|
|
178
|
+
if (!raw || raw.startsWith('chat_') || raw.startsWith('tab_')) continue
|
|
172
179
|
const display = row.displayName ?? row.display_name
|
|
173
|
-
if (display === '') continue
|
|
180
|
+
if (display === '' && !raw.endsWith('-tiered')) continue
|
|
174
181
|
out.push({
|
|
175
182
|
provider,
|
|
176
183
|
id: raw,
|
|
@@ -178,6 +185,19 @@ export async function fetchAvailableModels(fetchImpl, token, extraHeaders, fallb
|
|
|
178
185
|
...(row.recommended ? { description: 'Recommended' } : {}),
|
|
179
186
|
...(row.maxTokens ? { contextWindow: row.maxTokens } : {}),
|
|
180
187
|
})
|
|
188
|
+
const tiered = /^gemini-(3\.[78])-flash-tiered$/.exec(raw)
|
|
189
|
+
if (tiered) {
|
|
190
|
+
for (const level of ['high', 'medium', 'low']) {
|
|
191
|
+
const id = `gemini-${tiered[1]}-flash-${level}`
|
|
192
|
+
if (publishedIds.has(id)) continue
|
|
193
|
+
out.push({
|
|
194
|
+
provider,
|
|
195
|
+
id,
|
|
196
|
+
name: `Gemini ${tiered[1]} Flash (${level[0].toUpperCase()}${level.slice(1)})`,
|
|
197
|
+
...(row.maxTokens ? { contextWindow: row.maxTokens } : {}),
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
}
|
|
181
201
|
}
|
|
182
202
|
if (out.length) {
|
|
183
203
|
out.sort((a, b) => Number(Boolean(b.description)) - Number(Boolean(a.description)) || a.name.localeCompare(b.name))
|
package/lib/config-schema.js
CHANGED
|
@@ -108,6 +108,8 @@ export const Config = z.object({
|
|
|
108
108
|
.description('Custom model catalog for Grok.'),
|
|
109
109
|
cascadingFallback: z.boolean().volatile().default(false)
|
|
110
110
|
.description('#349 Opt-in cross-vendor fallback when all accounts of a provider are exhausted or rate limited.'),
|
|
111
|
+
cascadingFallbackOrder: z.array(z.string()).volatile().default(PROVIDERS)
|
|
112
|
+
.description('Configurable global priority sequence for cross-vendor fallback.'),
|
|
111
113
|
cascadingChain: z.any().volatile().default({})
|
|
112
114
|
.description('#349 Custom cross-vendor fallback chains (e.g. { claude: ["copilot", "cursor"] }).'),
|
|
113
115
|
webhookUrl: z.string().volatile().default('')
|
package/lib/diagnostics.js
CHANGED
|
@@ -6,6 +6,7 @@ import { getSessionPinStats } from './session-pin.js'
|
|
|
6
6
|
import { maskText } from './mask.js'
|
|
7
7
|
import { normalizeSlots } from './accounts.js'
|
|
8
8
|
import { PROVIDERS } from './refs.js'
|
|
9
|
+
import { defaultErrorBuffer } from './error-buffer.js'
|
|
9
10
|
|
|
10
11
|
function scrubReport(v) {
|
|
11
12
|
if (typeof v === 'string') return maskText(v)
|
|
@@ -18,7 +19,7 @@ function scrubReport(v) {
|
|
|
18
19
|
return v
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
export async function createDiagnosticsReport({ live, store, history, NS, pkgVersion }) {
|
|
22
|
+
export async function createDiagnosticsReport({ live, store, history, NS, pkgVersion, heartbeatStats }) {
|
|
22
23
|
const cfg = live()
|
|
23
24
|
const slots = normalizeSlots(cfg.slots)
|
|
24
25
|
const mk = () => ({ loggedIn: false, slots: 0, configured: 0, cooldown: 0, proxy: 0, maxUsagePercent: null })
|
|
@@ -51,6 +52,8 @@ export async function createDiagnosticsReport({ live, store, history, NS, pkgVer
|
|
|
51
52
|
runtime: { node: process.version, platform: process.platform, arch: process.arch, uptimeSec: Math.round(process.uptime()) },
|
|
52
53
|
providers,
|
|
53
54
|
requests: { total: rows.length, byStatus, lastErrors },
|
|
55
|
+
recentErrors: defaultErrorBuffer.getEntries(),
|
|
56
|
+
heartbeat: heartbeatStats || { lastRunAt: null, proactiveRefreshCount: 0 },
|
|
54
57
|
cache: calculateCacheMetrics(rows),
|
|
55
58
|
queue: defaultConcurrencyQueue.stats(),
|
|
56
59
|
pins: getSessionPinStats(),
|
|
@@ -78,6 +81,8 @@ export async function createDiagnosticsReport({ live, store, history, NS, pkgVer
|
|
|
78
81
|
privacyMask: !!cfg.privacyMask,
|
|
79
82
|
customVendors: Array.isArray(cfg.customVendors) ? cfg.customVendors.length : 0,
|
|
80
83
|
proxySlots: slots.filter((s) => s.proxyUrl).length,
|
|
84
|
+
cascadingFallback: !!cfg.cascadingFallback,
|
|
85
|
+
cascadingFallbackOrder: cfg.cascadingFallbackOrder || PROVIDERS,
|
|
81
86
|
},
|
|
82
87
|
})
|
|
83
88
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { cleanErrorMessage, maskText } from './mask.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ring buffer holding the most recent runtime errors across vendors and streams.
|
|
5
|
+
* Sanitizes all text to prevent credential or PII leaks in diagnostics.
|
|
6
|
+
*/
|
|
7
|
+
export class ErrorRingBuffer {
|
|
8
|
+
/**
|
|
9
|
+
* @param {number} [limit=20]
|
|
10
|
+
*/
|
|
11
|
+
constructor(limit = 20) {
|
|
12
|
+
this.limit = limit
|
|
13
|
+
/** @type {Array<{ id: string, ts: string, provider: string, ref: string, model: string, status: number|null, code: string, message: string }>} */
|
|
14
|
+
this.entries = []
|
|
15
|
+
this._nextId = 1
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {{ provider?: string, ref?: string, model?: string, status?: number|null, code?: string, message?: string }} [param0]
|
|
20
|
+
*/
|
|
21
|
+
record({ provider = 'unknown', ref = '', model = '', status = null, code = '', message = '' } = {}) {
|
|
22
|
+
const rawMsg = cleanErrorMessage(maskText(String(message || '')))
|
|
23
|
+
const entry = {
|
|
24
|
+
id: String(this._nextId++),
|
|
25
|
+
ts: new Date().toISOString(),
|
|
26
|
+
provider: String(provider || ''),
|
|
27
|
+
ref: String(ref || ''),
|
|
28
|
+
model: String(model || ''),
|
|
29
|
+
status: typeof status === 'number' ? status : null,
|
|
30
|
+
code: String(code || ''),
|
|
31
|
+
message: rawMsg.slice(0, 250),
|
|
32
|
+
}
|
|
33
|
+
this.entries.unshift(entry)
|
|
34
|
+
if (this.entries.length > this.limit) {
|
|
35
|
+
this.entries.length = this.limit
|
|
36
|
+
}
|
|
37
|
+
return entry
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getEntries() {
|
|
41
|
+
return this.entries.slice()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
clear() {
|
|
45
|
+
this.entries = []
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const defaultErrorBuffer = new ErrorRingBuffer(20)
|
package/lib/index.js
CHANGED
|
@@ -416,37 +416,45 @@ export function apply(ctx, rawConfig) {
|
|
|
416
416
|
const pmE = (s) => privacyOn() ? maskEmail(s) : String(s || '')
|
|
417
417
|
const pmL = (s) => privacyOn() ? maskLabel(s) : String(s || '')
|
|
418
418
|
|
|
419
|
-
const
|
|
419
|
+
const heartbeatStats = { lastRunAt: 0, proactiveRefreshCount: 0 }
|
|
420
|
+
const runProactiveHeartbeat = async () => {
|
|
421
|
+
heartbeatStats.lastRunAt = Date.now()
|
|
422
|
+
const cfg = live()
|
|
423
|
+
const ahead = Number(cfg.refreshAheadMs) || 5 * 60 * 1000
|
|
424
|
+
const retryMs = Number(cfg.refreshRetryMs) || 10 * 60 * 1000
|
|
425
|
+
const now = Date.now()
|
|
426
|
+
let refreshed = 0
|
|
427
|
+
for (const slot of normalizeSlots(cfg.slots)) {
|
|
428
|
+
const ref = slot.ref
|
|
429
|
+
try {
|
|
430
|
+
const info = await store.describeRef(ref)
|
|
431
|
+
if (!info.configured) continue
|
|
432
|
+
if (info.cooldownUntil && info.cooldownUntil > now) continue
|
|
433
|
+
const raw = await store.resolveRaw(ref)
|
|
434
|
+
if (!raw) continue
|
|
435
|
+
const blob = await store.loadBlob(ref).catch(() => null)
|
|
436
|
+
if (!blob || !blob.refreshToken) continue
|
|
437
|
+
if (!blob.expiresAt) continue
|
|
438
|
+
if (blob.expiresAt - now > ahead) continue
|
|
439
|
+
if (typeof store.shouldSkipRefresh === "function" && store.shouldSkipRefresh(ref, now, retryMs)) continue
|
|
440
|
+
await store.ensureFresh(slot.provider, blob, ref).then(() => {
|
|
441
|
+
refreshed++
|
|
442
|
+
heartbeatStats.proactiveRefreshCount++
|
|
443
|
+
}).catch((e) => {
|
|
444
|
+
if (logger && logger.warn) logger.warn("[dsh-subscriptions] background refresh failed for " + ref + ": " + String(e && e.message || e))
|
|
445
|
+
})
|
|
446
|
+
} catch { /* best-effort */ }
|
|
447
|
+
}
|
|
448
|
+
return refreshed
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const diagnosticsReport = () => createDiagnosticsReport({ live, store, history, NS, pkgVersion, heartbeatStats })
|
|
420
452
|
const accountsView = () => createAccountsView({ live, store, fetch, pmL })
|
|
421
453
|
|
|
422
454
|
// Ponytail: background refresh ahead of expiry, single timer
|
|
423
455
|
ctx.effect(() => {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const ahead = Number(cfg.refreshAheadMs) || 5 * 60 * 1000
|
|
427
|
-
const retryMs = Number(cfg.refreshRetryMs) || 10 * 60 * 1000
|
|
428
|
-
const now = Date.now()
|
|
429
|
-
for (const slot of normalizeSlots(cfg.slots)) {
|
|
430
|
-
const ref = slot.ref
|
|
431
|
-
try {
|
|
432
|
-
const info = await store.describeRef(ref)
|
|
433
|
-
if (!info.configured) continue
|
|
434
|
-
if (info.cooldownUntil && info.cooldownUntil > now) continue
|
|
435
|
-
const raw = await store.resolveRaw(ref)
|
|
436
|
-
if (!raw) continue
|
|
437
|
-
const blob = await store.loadBlob(ref).catch(() => null)
|
|
438
|
-
if (!blob || !blob.refreshToken) continue
|
|
439
|
-
if (!blob.expiresAt) continue
|
|
440
|
-
if (blob.expiresAt - now > ahead) continue
|
|
441
|
-
if (typeof store.shouldSkipRefresh === 'function' && store.shouldSkipRefresh(ref, now, retryMs)) continue
|
|
442
|
-
await store.ensureFresh(slot.provider, blob, ref).catch((e) => {
|
|
443
|
-
if (logger && logger.warn) logger.warn('[dsh-subscriptions] background refresh failed for ' + ref + ': ' + String(e && e.message || e))
|
|
444
|
-
})
|
|
445
|
-
} catch { /* best-effort */ }
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
tick().catch(() => {})
|
|
449
|
-
const id = setInterval(() => { tick().catch(() => {}) }, 60 * 1000)
|
|
456
|
+
runProactiveHeartbeat().catch(() => {})
|
|
457
|
+
const id = setInterval(() => { runProactiveHeartbeat().catch(() => {}) }, 60 * 1000)
|
|
450
458
|
return () => clearInterval(id)
|
|
451
459
|
}, 'dsh-subscriptions: refresh ahead')
|
|
452
460
|
|
|
@@ -509,6 +517,7 @@ export function apply(ctx, rawConfig) {
|
|
|
509
517
|
refForSlot,
|
|
510
518
|
resetCredits,
|
|
511
519
|
diagnosticsReport,
|
|
520
|
+
triggerHeartbeat: runProactiveHeartbeat,
|
|
512
521
|
pending,
|
|
513
522
|
completeOAuth,
|
|
514
523
|
fetchForRef,
|
package/lib/messages.js
CHANGED
|
@@ -478,7 +478,13 @@ export function googleContents(options) {
|
|
|
478
478
|
}))),
|
|
479
479
|
]
|
|
480
480
|
for (const call of calls) {
|
|
481
|
-
|
|
481
|
+
// DSH provider-neutral tool history cannot retain Gemini's opaque thoughtSignature.
|
|
482
|
+
// Gemini 3 requires one on replay after a tool result; Google's documented sentinel
|
|
483
|
+
// permits reconstructed/foreign history.
|
|
484
|
+
parts.push({
|
|
485
|
+
functionCall: { name: call.name, args: safeJson(call.arguments) },
|
|
486
|
+
thoughtSignature: 'skip_thought_signature_validator',
|
|
487
|
+
})
|
|
482
488
|
}
|
|
483
489
|
for (const result of toolResults(message.content)) {
|
|
484
490
|
const name = result.toolName || result.name || 'tool'
|
package/lib/rotate.js
CHANGED
|
@@ -18,6 +18,7 @@ export function isRegionError(err) {
|
|
|
18
18
|
|
|
19
19
|
export function isSwitchableError(err) {
|
|
20
20
|
if (!err || typeof err !== 'object') return false
|
|
21
|
+
if (err.name === 'TimeoutError' || err.code === 'TIMEOUT' || err.code === 'EMPTY_RESPONSE') return true
|
|
21
22
|
if (err.name === 'AbortError' || err.code === 'ABORT_ERR' || err.code === 'ERR_ABORTED' || (err.message && /aborted/i.test(err.message))) return false
|
|
22
23
|
const code = err.code || (err.failure && err.failure.code)
|
|
23
24
|
if (SWITCH_CODES.has(code)) return true
|
package/lib/routes/status.js
CHANGED
|
@@ -19,6 +19,7 @@ import { isTrustedSettingsRequest, queryOf, readBody, safeJsonHandler, writeJson
|
|
|
19
19
|
import { quotaSnapshot } from '../ratelimit.js'
|
|
20
20
|
import { PROVIDERS, displayName, droppedCredentialRefs, isProvider, oauthRef } from '../refs.js'
|
|
21
21
|
import { getVendor } from '../vendors/index.js'
|
|
22
|
+
import { defaultErrorBuffer } from '../error-buffer.js'
|
|
22
23
|
|
|
23
24
|
export function registerStatusRoutes(ctx, state) {
|
|
24
25
|
const {
|
|
@@ -298,6 +299,40 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
298
299
|
}),
|
|
299
300
|
}), 'dsh-subscriptions: /diagnostics')
|
|
300
301
|
|
|
302
|
+
ctx.effect(() => ctx.webServer.register({
|
|
303
|
+
kind: 'exact',
|
|
304
|
+
path: '/dsh-subscriptions/diagnostics/errors/clear',
|
|
305
|
+
handler: safeJsonHandler(async (req, res) => {
|
|
306
|
+
if (req.method !== 'POST') {
|
|
307
|
+
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
308
|
+
return
|
|
309
|
+
}
|
|
310
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
311
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
defaultErrorBuffer.clear()
|
|
315
|
+
writeJson(res, 200, { ok: true, cleared: true })
|
|
316
|
+
}),
|
|
317
|
+
}), 'dsh-subscriptions: /diagnostics/errors/clear')
|
|
318
|
+
|
|
319
|
+
ctx.effect(() => ctx.webServer.register({
|
|
320
|
+
kind: 'exact',
|
|
321
|
+
path: '/dsh-subscriptions/heartbeat/run',
|
|
322
|
+
handler: safeJsonHandler(async (req, res) => {
|
|
323
|
+
if (req.method !== 'POST') {
|
|
324
|
+
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
325
|
+
return
|
|
326
|
+
}
|
|
327
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
328
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
const refreshedCount = typeof state.triggerHeartbeat === 'function' ? await state.triggerHeartbeat() : 0
|
|
332
|
+
writeJson(res, 200, { ok: true, refreshed: refreshedCount })
|
|
333
|
+
}),
|
|
334
|
+
}), 'dsh-subscriptions: /heartbeat/run')
|
|
335
|
+
|
|
301
336
|
|
|
302
337
|
// ponytail: cheap per-vendor probe; never sets cooldown, never returns tokens
|
|
303
338
|
ctx.effect(() => ctx.webServer.register({
|
|
@@ -345,10 +380,6 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
345
380
|
return
|
|
346
381
|
}
|
|
347
382
|
const vendor = getVendor(provider)
|
|
348
|
-
if (typeof vendor.check !== 'function') {
|
|
349
|
-
writeJson(res, 200, { ok: true, provider, index: index, ref, email: pmE(blob.email), label: pmL(blob.label), expiresAt: blob.expiresAt || null, quota: info.quota || null })
|
|
350
|
-
return
|
|
351
|
-
}
|
|
352
383
|
let capturedQuota = null
|
|
353
384
|
const probeFetch = async (url, init = {}) => {
|
|
354
385
|
const doFetch = (typeof fetchForRef === "function" && fetchForRef(ref)) || fetch
|
|
@@ -360,8 +391,14 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
360
391
|
return res2
|
|
361
392
|
}
|
|
362
393
|
try {
|
|
363
|
-
|
|
364
|
-
|
|
394
|
+
if (typeof vendor.check === 'function') {
|
|
395
|
+
await vendor.check(blob, vendorConfig(provider, live()), probeFetch)
|
|
396
|
+
} else if (typeof vendor.listModels === 'function') {
|
|
397
|
+
await vendor.listModels(blob, vendorConfig(provider, live()), probeFetch)
|
|
398
|
+
} else if (typeof vendor.usage === 'function') {
|
|
399
|
+
await vendor.usage(blob, vendorConfig(provider, live()), probeFetch)
|
|
400
|
+
}
|
|
401
|
+
const latencyMs = Date.now() - t0
|
|
365
402
|
writeJson(res, 200, { ok: true, provider, index: index, ref, latencyMs, email: pmE(blob.email), label: pmL(blob.label), expiresAt: blob.expiresAt || null, quota: capturedQuota || info.quota || null, usagePercent: info.usagePercent ?? null })
|
|
366
403
|
} catch (e) {
|
|
367
404
|
const isTimeout = isTimeoutError(e)
|
package/lib/stream-rotate.js
CHANGED
|
@@ -4,6 +4,7 @@ import { pickAccountWithTrace, markCooldown, isSwitchableError, isRegionError, m
|
|
|
4
4
|
import { putInQuarantine, REASON_RATE_LIMIT, REASON_HARD_LIMIT, REASON_REVOKED } from './quarantine.js'
|
|
5
5
|
import { notifySessionExpired, notifyQuarantineEntered } from './alerts.js'
|
|
6
6
|
import { withRunawayGuard } from './runaway-guard.js'
|
|
7
|
+
import { defaultErrorBuffer } from './error-buffer.js'
|
|
7
8
|
|
|
8
9
|
export async function* streamWithRotation({
|
|
9
10
|
accounts,
|
|
@@ -163,6 +164,14 @@ export async function* streamWithRotation({
|
|
|
163
164
|
return
|
|
164
165
|
} catch (err) {
|
|
165
166
|
lastError = err
|
|
167
|
+
defaultErrorBuffer.record({
|
|
168
|
+
provider: options && options.provider,
|
|
169
|
+
ref: account.ref || account.id,
|
|
170
|
+
model: options && options.model,
|
|
171
|
+
status: err && (err.status || err.statusCode),
|
|
172
|
+
code: err && (err.code || err.name),
|
|
173
|
+
message: err && (err.message || String(err)),
|
|
174
|
+
})
|
|
166
175
|
// If chunks were already yielded to the caller, never rotate mid-stream
|
|
167
176
|
// as that would repeat or scramble generated tokens.
|
|
168
177
|
if (firstChunkDelivered) throw err
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto'
|
|
|
2
2
|
import { buildAuthorizeUrl } from '../oauth.js'
|
|
3
3
|
import { googleContents } from '../messages.js'
|
|
4
4
|
import { formTokenRequest, googleStream, throwHttpError, tokenBlobFromOAuth } from '../wire.js'
|
|
5
|
+
import { fetchWithTimeout } from '../http.js'
|
|
5
6
|
import { emailFromToken } from '../jwt.js'
|
|
6
7
|
import {
|
|
7
8
|
CODE_ASSIST_STREAM,
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
antigravityIdentityHeaders,
|
|
10
11
|
discoverProject,
|
|
11
12
|
fetchAvailableModels,
|
|
13
|
+
antigravityTieredAlias,
|
|
12
14
|
retrieveQuotaPercent,
|
|
13
15
|
streamEnvelope,
|
|
14
16
|
assistHeaders,
|
|
@@ -38,14 +40,22 @@ export function defaults() {
|
|
|
38
40
|
clientSecret: '',
|
|
39
41
|
redirectUri: 'http://localhost:8085/oauth/callback',
|
|
40
42
|
models: [
|
|
43
|
+
'gemini-3.8-flash-high',
|
|
41
44
|
'gemini-3.8-flash-medium',
|
|
45
|
+
'gemini-3.8-flash-low',
|
|
46
|
+
'gemini-3.8-flash-tiered',
|
|
47
|
+
'gemini-3.7-flash-high',
|
|
42
48
|
'gemini-3.7-flash-medium',
|
|
49
|
+
'gemini-3.7-flash-low',
|
|
50
|
+
'gemini-3.7-flash-tiered',
|
|
51
|
+
'gemini-3.6-flash-high',
|
|
43
52
|
'gemini-3.6-flash-medium',
|
|
53
|
+
'gemini-3.6-flash-low',
|
|
44
54
|
'gemini-3.1-pro-low',
|
|
45
|
-
'
|
|
46
|
-
'claude-
|
|
55
|
+
'gemini-3.1-pro-high',
|
|
56
|
+
'claude-sonnet-4-6',
|
|
57
|
+
'claude-opus-4-6-thinking',
|
|
47
58
|
'gpt-oss-120b-medium',
|
|
48
|
-
'gemini-3.1-pro-high-vertex',
|
|
49
59
|
'gemini-3-flash',
|
|
50
60
|
'gemini-2.5-pro',
|
|
51
61
|
'gemini-2.5-flash',
|
|
@@ -183,30 +193,72 @@ export async function usage(blob, _cfg, fetchImpl) {
|
|
|
183
193
|
return retrieveQuotaPercent(fetchImpl || fetch, blob.accessToken, headersFor(blob.projectId || ''))
|
|
184
194
|
}
|
|
185
195
|
|
|
196
|
+
const RESPONSE_TIMEOUT_MS = 30000
|
|
197
|
+
|
|
198
|
+
function isTransientFailure(err) {
|
|
199
|
+
return err?.name === 'TimeoutError' || err?.code === 'TIMEOUT' || err?.code === 'EMPTY_RESPONSE' || err instanceof TypeError
|
|
200
|
+
}
|
|
201
|
+
|
|
186
202
|
export async function* streamOnce({ blob, options, fetchImpl, headers, signal, saveBlob }) {
|
|
203
|
+
const impl = fetchImpl || fetch
|
|
187
204
|
// Abort must cover the project pre-flight as well as the stream.
|
|
188
|
-
const preflight = (u, i) =>
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
205
|
+
const preflight = (u, i) => fetchWithTimeout(impl, u, { ...(i || {}), signal }, { timeoutMs: RESPONSE_TIMEOUT_MS })
|
|
206
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
207
|
+
let emitted = false
|
|
208
|
+
try {
|
|
209
|
+
const ready = await withProject(blob, preflight, saveBlob)
|
|
210
|
+
const tiered = antigravityTieredAlias(options.model)
|
|
211
|
+
const request = googleContents(options)
|
|
212
|
+
if (tiered) {
|
|
213
|
+
if (!request.generationConfig) request.generationConfig = {}
|
|
214
|
+
request.generationConfig.thinkingConfig = /** @type {any} */ ({ thinkingLevel: tiered.thinkingLevel })
|
|
215
|
+
}
|
|
216
|
+
const { envelope, sessionId } = streamEnvelope({
|
|
217
|
+
projectId: ready.projectId,
|
|
218
|
+
model: tiered ? tiered.model : options.model,
|
|
219
|
+
request,
|
|
220
|
+
userAgent: 'antigravity',
|
|
221
|
+
sessionId: ready.sessionId,
|
|
222
|
+
paidTierId: ready.paidTierId,
|
|
223
|
+
})
|
|
224
|
+
const res = await fetchWithTimeout(impl, `${CODE_ASSIST_STREAM}:streamGenerateContent?alt=sse`, {
|
|
225
|
+
method: 'POST',
|
|
226
|
+
headers: {
|
|
227
|
+
...headers,
|
|
228
|
+
...assistHeaders(ready.accessToken, { ...headersFor(ready.projectId || ''), Accept: 'text/event-stream' }),
|
|
229
|
+
},
|
|
230
|
+
body: JSON.stringify(envelope),
|
|
231
|
+
signal,
|
|
232
|
+
}, { timeoutMs: RESPONSE_TIMEOUT_MS })
|
|
233
|
+
if (!res.ok) throwHttpError(res.status, await res.text())
|
|
234
|
+
if (saveBlob && sessionId && sessionId !== ready.sessionId) {
|
|
235
|
+
await saveBlob({ ...ready, sessionId })
|
|
236
|
+
}
|
|
237
|
+
const pending = []
|
|
238
|
+
let hasContent = false
|
|
239
|
+
for await (const event of googleStream(res.body, { signal, idleTimeoutMs: RESPONSE_TIMEOUT_MS })) {
|
|
240
|
+
if (!hasContent) {
|
|
241
|
+
if (event.type === 'text-delta' || event.type === 'tool-call-delta' || event.type === 'reasoning-delta') {
|
|
242
|
+
hasContent = true
|
|
243
|
+
for (const item of pending) { emitted = true; yield item }
|
|
244
|
+
} else if (event.type !== 'finish') {
|
|
245
|
+
pending.push(event)
|
|
246
|
+
continue
|
|
247
|
+
} else {
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
emitted = true
|
|
252
|
+
yield event
|
|
253
|
+
}
|
|
254
|
+
if (!hasContent) {
|
|
255
|
+
const err = /** @type {any} */ (new Error('Antigravity returned no content'))
|
|
256
|
+
err.code = 'EMPTY_RESPONSE'
|
|
257
|
+
throw err
|
|
258
|
+
}
|
|
259
|
+
return
|
|
260
|
+
} catch (err) {
|
|
261
|
+
if (emitted || signal?.aborted || attempt === 1 || !isTransientFailure(err)) throw err
|
|
262
|
+
}
|
|
210
263
|
}
|
|
211
|
-
yield* googleStream(res.body)
|
|
212
264
|
}
|
package/lib/wire.js
CHANGED
|
@@ -328,14 +328,18 @@ export async function* anthropicStream(body) {
|
|
|
328
328
|
yield { type: 'finish', reason: finish }
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
-
|
|
331
|
+
/**
|
|
332
|
+
* @param {any} body
|
|
333
|
+
* @param {{ signal?: AbortSignal, idleTimeoutMs?: number }} [options]
|
|
334
|
+
*/
|
|
335
|
+
export async function* googleStream(body, { signal, idleTimeoutMs } = {}) {
|
|
332
336
|
let index = 0
|
|
333
337
|
let started = false
|
|
334
338
|
let callCount = 0
|
|
335
339
|
const functionCallIndexes = new Map()
|
|
336
340
|
let finish = { kind: 'stop' }
|
|
337
341
|
let sawTerminal = false
|
|
338
|
-
for await (const data of iterateSse(body)) {
|
|
342
|
+
for await (const data of iterateSse(body, { signal, idleTimeoutMs })) {
|
|
339
343
|
if (data === '[DONE]') {
|
|
340
344
|
sawTerminal = true
|
|
341
345
|
break
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.70",
|
|
4
4
|
"description": "Use ChatGPT Codex, Claude, Grok, Antigravity, Kimi, GLM, Cursor, Kiro, Copilot, Qwen, ERNIE, Spark, JetBrains, Perplexity, Replit, and Cody subscriptions as DeepSeek Harness LLM providers via OAuth.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|