@goodandready/dsh-subscriptions 0.6.68 → 0.6.69

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
@@ -207,6 +207,11 @@ 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
+ ### 23. 🧠 Antigravity Tiered Models, Tool Sentinel & Stream Resilience (v0.6.69)
211
+ * **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.
212
+ * **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.
213
+ * **Stream Resilience & Empty Stream Retry (#534)**: Idle SSE timeout (30s), empty response detection, and transient stream retry with automatic account rotation.
214
+
210
215
  ### 22. ⚡ xAI Grok RFC 8628 Device Authorization Flow (v0.6.68)
211
216
  * **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
217
 
package/README.ru.md CHANGED
@@ -183,6 +183,11 @@ 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
+ ### 19. 🧠 Antigravity Tiered-модели, сентинел тулов и устойчивость стримов (v0.6.69)
187
+ * **Сентинел реплея тулов Gemini 3 (#534)**: Автоматическая вставка `thoughtSignature: 'skip_thought_signature_validator'` для реплея истории вызовов тулов без сбоев валидации Google API.
188
+ * **Tiered-модели и уровни мышления (#534)**: Выбор уровней High/Medium/Low для Gemini 3.8 и 3.7 Flash с маппингом в upstream `*-flash-tiered` и `thinkingLevel`.
189
+ * **Устойчивость стримов и авто-ретрай (#534)**: Защита от зависания SSE (таймаут 30с), детекция пустых ответов и авто-ротация аккаунтов по `EMPTY_RESPONSE`/`TIMEOUT`.
190
+
186
191
  ### 18. ⚡ RFC 8628 Device Code авторизация для xAI Grok (v0.6.68)
187
192
  * **Авторизация по коду устройства (GH #17, #532)**: Нативная поддержка RFC 8628 device code flow для xAI Grok. Устраняет проблему, когда xAI для CLI client ID открывает страницу ввода кода для Grok Build без обратного редиректа на loopback-порт. Отображает одноразовый код на карточке и автоматически поллит токены в фоне.
188
193
 
package/README.zh.md CHANGED
@@ -132,6 +132,11 @@ graph LR
132
132
  queueTimeoutMs: 30000 # v0.6.62: 队列最大等待超时时间(毫秒)
133
133
  * **自动化 Webhook 告警集成**:通过 `notifyQuarantineEntered` 与 `notifyQuarantineReleased` 自动向外部通知系统派发隔离生命周期事件。
134
134
 
135
+ ### Antigravity 分档思考模型、工具回放哨兵与流式自愈 (v0.6.69)
136
+ * **Gemini 3 工具回放哨兵 (#534)**:在重建的工具调用中注入 `thoughtSignature: 'skip_thought_signature_validator'`,彻底解决多轮工具调用历史重放时 Gemini 3 的校验失败问题。
137
+ * **分档 Flash 模型与思考档位 (#534)**:将 `gemini-3.8-flash-tiered` 与 `3.7` 自动解析展开为 High/Medium/Low 档位选择,并在调用时自动组装 `thinkingConfig.thinkingLevel`。
138
+ * **流式空响应自愈与账号轮换 (#534)**:引入 30 秒 SSE 空闲超时保护与空响应检测,在未输出前自动重试一次,失败时通过 `EMPTY_RESPONSE` / `TIMEOUT` 自动轮换至下一个可用账号。
139
+
135
140
  ### xAI Grok RFC 8628 设备码授权流支持 (v0.6.68)
136
141
  * **RFC 8628 设备码授权流 (GH #17, #532)**:为 xAI Grok 新增原生设备码登录授权支持。彻底解决 xAI 针对 CLI 客户端标识返回 Grok Build 验证码页面且不触发回环重定向回调导致的死锁问题。直接在槽位卡片展示用户授权码,并自动在后台轮询兑换会话令牌。
137
142
 
@@ -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
- for (const row of catalogRows(json)) {
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/messages.js CHANGED
@@ -478,7 +478,13 @@ export function googleContents(options) {
478
478
  }))),
479
479
  ]
480
480
  for (const call of calls) {
481
- parts.push({ functionCall: { name: call.name, args: safeJson(call.arguments) } })
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
@@ -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
- 'claude-sonnet-4.6-thinking',
46
- 'claude-opus-4.6-thinking',
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) => fetchImpl(u, { ...(i || {}), signal })
189
- const ready = await withProject(blob, preflight, saveBlob)
190
- const { envelope, sessionId } = streamEnvelope({
191
- projectId: ready.projectId,
192
- model: options.model,
193
- request: googleContents(options),
194
- userAgent: 'antigravity',
195
- sessionId: ready.sessionId,
196
- paidTierId: ready.paidTierId,
197
- })
198
- const res = await fetchImpl(`${CODE_ASSIST_STREAM}:streamGenerateContent?alt=sse`, {
199
- method: 'POST',
200
- headers: {
201
- ...headers,
202
- ...assistHeaders(ready.accessToken, { ...headersFor(ready.projectId || ''), Accept: 'text/event-stream' }),
203
- },
204
- body: JSON.stringify(envelope),
205
- signal,
206
- })
207
- if (!res.ok) throwHttpError(res.status, await res.text())
208
- if (saveBlob && sessionId && sessionId !== ready.sessionId) {
209
- await saveBlob({ ...ready, sessionId })
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
- export async function* googleStream(body) {
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.68",
3
+ "version": "0.6.69",
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",