@goodandready/dsh-subscriptions 0.6.10 → 0.6.12

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
@@ -272,6 +272,23 @@ Supports Anthropic's adaptive thinking (`thinking: { type: "adaptive" }`) and re
272
272
 
273
273
  ---
274
274
 
275
+ ## 🛡️ Token Usage Normalization & Session Projection Fix (Added in v0.6.12)
276
+
277
+ - **Session Projection Fix (GitHub #4)**: Fixed an issue where ChatGPT Codex and other streaming provider responses caused DSH session recovery to fail on page reload with `received NaN, expected number on uncachedInputTokens and outputTokens`.
278
+ - **Canonical `TokenUsage` Normalization**: Introduced `toTokenUsage()` across all streaming pipelines (`codexResponsesStream`, `openaiChatStream`, `anthropicStream`, and `googleStream`) to map raw vendor usage payloads into canonical DSH `TokenUsage` (`inputTokens`, `outputTokens`, `cacheReadTokens`, `cacheWriteTokens`, `reasoningTokens`, `totalTokens`).
279
+ - **Strict Integer Guard**: All token counts are strictly guarded to non-negative finite integers (guaranteeing `0` fallback, never `NaN` or `undefined`). Disjoint uncached token calculation is performed when cached tokens are folded into prompt totals.
280
+ - **Defense in Depth**: Added adapter-level stream validation in `SubscriptionAdapter` to prevent malformed or invalid token usage chunks from ever reaching DSH session persistence.
281
+
282
+ ---
283
+
284
+ ## 🔒 OAuth Client Validation & Route Updates (Added in v0.6.11)
285
+
286
+ - **OAuth Client ID Guard (GitHub #2)**: uildAuthorizeUrl and ntigravity.authorizeUrl now strictly require a non-empty clientId. If unconfigured, the endpoint returns an explicit HTTP 400 (missing_client_id) and the UI prompts the user to configure ntigravityClientId in plugin settings or use "From CLI" instead of directing the browser to a failing Google OAuth page.
287
+ - **Config Schema Secret Key**: Added ntigravityClientSecret to the plugin configuration schema, allowing convenient entry of private client secrets alongside ntigravityClientId with automatic masking in public endpoints.
288
+ - **Zhipu GLM Console Route Migration (GitHub #3)**: Updated the Zhipu GLM authorization and API key center link to the active console route (https://bigmodel.cn/usercenter/proj-mgmt/apikeys), eliminating 404 navigation errors caused by vendor console restructuring.
289
+
290
+ ---
291
+
275
292
  ## 🛡️ Reliability & Quality Hardening (Added in v0.6.10)
276
293
 
277
294
  - **Structured Error Handling**: Eliminated all empty catch blocks across the plugin runtime via safe `bestEffort` helper logging debug diagnostics.
package/README.ru.md CHANGED
@@ -245,6 +245,23 @@ dsh-subscriptions:
245
245
 
246
246
  ---
247
247
 
248
+ ## 🛡️ Нормализация расхода токенов и защита проекций сессий (Добавлено в v0.6.12)
249
+
250
+ - **Защита от повреждения сессий (GitHub #4)**: Устранена критическая ошибка, из-за которой сессии с моделями ChatGPT Codex повреждались после первого ответа и не открывались после перезагрузки с ошибкой `received NaN, expected number on uncachedInputTokens and outputTokens`.
251
+ - **Каноническая нормализация `TokenUsage`**: Внедрена универсальная функция `toTokenUsage()` во всех потоковых адаптерах (`codexResponsesStream`, `openaiChatStream`, `anthropicStream`, `googleStream`) для приведения вендорских структур к стандартному интерфейсу DSH `TokenUsage` (`inputTokens`, `outputTokens`, `cacheReadTokens`, `cacheWriteTokens`, `reasoningTokens`, `totalTokens`).
252
+ - **Защита от `NaN` и disjoint-расчет**: Все значения токенов гарантированно приводятся к целым неотрицательным числам (с безопасным fallback в `0`, без `NaN` и `undefined`). Для провайдеров, объединяющих кэш с промптом, автоматически вычисляется раздельный объем некэшированного ввода.
253
+ - **Эшелонированная защита адаптера**: Добавлен перехват и валидация чанков расхода токенов на уровне `SubscriptionAdapter`, исключающий попадание некорректных данных в журнал и хранилище сессий DSH.
254
+
255
+ ---
256
+
257
+ ## 🔒 Защита OAuth Client ID и обновление ссылок (Добавлено в v0.6.11)
258
+
259
+ - **Защита OAuth Client ID (GitHub #2)**: uildAuthorizeUrl и ntigravity.authorizeUrl теперь строго требуют непустой clientId. Если параметр не задан, сервер возвращает явный код ошибки HTTP 400 (missing_client_id), а интерфейс выводит подсказку с предложением указать ntigravityClientId в настройках или нажать «📥 Из CLI», предотвращая отправку битых запросов в Google OAuth.
260
+ - **Секретный ключ в схеме настроек**: В схему Config добавлено поле ntigravityClientSecret для удобного ввода Client Secret рядом с ntigravityClientId с автоматическим маскированием в публичном API.
261
+ - **Актуализация маршрута Zhipu GLM (GitHub #3)**: Ссылка на консоль API-ключей Zhipu GLM обновлена на актуальный адрес (https://bigmodel.cn/usercenter/proj-mgmt/apikeys), устраняя ошибку 404 («页面跑丢啦!!»), вызванную миграцией маршрутов вендора.
262
+
263
+ ---
264
+
248
265
  ## 🛡️ Надежность и стандарты качества (Добавлено в v0.6.10)
249
266
 
250
267
  - **Структурированная обработка ошибок**: Устранены все пустые блоки catch в рантайме плагина с переходом на безопасный хелпер `bestEffort` с диагностическим логированием.
package/README.zh.md CHANGED
@@ -167,6 +167,23 @@ dsh plugin --profile web add @goodandready/dsh-subscriptions
167
167
 
168
168
  ---
169
169
 
170
+ ## 🛡️ Token 用量规范化与会话投影修复 (v0.6.12 新增)
171
+
172
+ - **会话投影崩溃修复 (GitHub #4)**:修复了使用 ChatGPT Codex 及其他流式模型时,首次回复后会话损坏、刷新页面报错 `received NaN, expected number on uncachedInputTokens and outputTokens` 且无法加载的问题。
173
+ - **全格式通用 TokenUsage 转换**:在所有流式传输通道(`codexResponsesStream`、`openaiChatStream`、`anthropicStream`、`googleStream`)中引入统一的 `toTokenUsage()` 转换逻辑,将各服务商的原始用量数据标准化为 DSH 规范的 `TokenUsage` 接口(`inputTokens`、`outputTokens`、`cacheReadTokens`、`cacheWriteTokens`、`reasoningTokens`、`totalTokens`)。
174
+ - **严格非负整数保底与独立缓存计算**:对所有 token 计数实施严格的非负整数约束(保底为 `0`,杜绝 `NaN` 与 `undefined`)。针对将缓存合并计入总 prompt 的供应商自动执行拆分,确保输入 token 数的准确性。
175
+ - **适配器层纵深防御**:在 `SubscriptionAdapter` 中新增用量数据过滤与净化机制,从根源上阻止异常用量流入 DSH 会话持久化与状态恢复流程。
176
+
177
+ ---
178
+
179
+ ## 🔒 OAuth Client ID 校验保护与控制台地址更新 (v0.6.11 新增)
180
+
181
+ - **Google OAuth Client ID 校验保护 (GitHub #2)**:uildAuthorizeUrl 和 ntigravity.authorizeUrl 严格要求非空 clientId。未配置时服务端返回明确的 HTTP 400 (missing_client_id),前端界面提示用户在插件设置中填写 ntigravityClientId 或使用“📥 从 CLI 导入”,避免浏览器跳转至 Google OAuth 400 报错页面。
182
+ - **配置架构新增密钥项**:在插件 Config 架构中新增 ntigravityClientSecret,方便用户在设置中配置客户端密钥,并在公共接口中自动脱敏脱密。
183
+ - **智谱 GLM 控制台地址迁移 (GitHub #3)**:将智谱 GLM 授权与 API Key 获取链接更新为最新控制台有效路由 (https://bigmodel.cn/usercenter/proj-mgmt/apikeys),彻底解决因服务商控制台路由调整导致的 404 错误。
184
+
185
+ ---
186
+
170
187
  ## 🛡️ 稳定性与代码质量强化 (v0.6.10 新增)
171
188
 
172
189
  - **结构化异常降级**: 全面移除运行时空 catch 块,统一采用带诊断日志的 `bestEffort` 安全兜底机制。
package/lib/adapter.js CHANGED
@@ -6,6 +6,7 @@ import { modelCatalog } from './messages.js'
6
6
  import { streamWithRotation } from './stream-rotate.js'
7
7
  import { pickFetch } from './proxy.js'
8
8
  import { quotaSnapshot, parseBody } from './ratelimit.js'
9
+ import { toTokenUsage } from './wire.js'
9
10
 
10
11
  // #94: heuristic for test/preview/beta/legacy model ids.
11
12
  function isDeprecatedId(id) {
@@ -176,7 +177,17 @@ export class SubscriptionAdapter extends LlmAdapter {
176
177
  throw err
177
178
  }
178
179
  },
179
- })) { yieldedAny = true; yield event }
180
+ })) {
181
+ if (event && event.type === 'usage') {
182
+ const safeUsage = toTokenUsage(event.usage)
183
+ if (!safeUsage) continue
184
+ yieldedAny = true
185
+ yield { ...event, usage: safeUsage }
186
+ continue
187
+ }
188
+ yieldedAny = true
189
+ yield event
190
+ }
180
191
  return
181
192
  } catch (err) {
182
193
  // #91: when the whole pool is exhausted and nothing has been streamed
@@ -16,9 +16,9 @@ export function analyzeSessionEvents(events = []) {
16
16
  const usage = ev.usage || (ev.type === 'usage' ? ev.usage : null) || (ev.data && ev.data.usage)
17
17
  if (!usage) continue
18
18
 
19
- const p = Number(usage.prompt_tokens || usage.input_tokens || usage.promptTokens || 0)
20
- const c = Number(usage.prompt_tokens_details?.cached_tokens || usage.cache_read_input_tokens || usage.cached_tokens || usage.cachedTokens || 0)
21
- const comp = Number(usage.completion_tokens || usage.output_tokens || usage.completionTokens || 0)
19
+ const p = Number(usage.inputTokens ?? usage.prompt_tokens ?? usage.input_tokens ?? usage.promptTokens ?? 0)
20
+ const c = Number(usage.cacheReadTokens ?? usage.prompt_tokens_details?.cached_tokens ?? usage.cache_read_input_tokens ?? usage.cached_tokens ?? usage.cachedTokens ?? 0)
21
+ const comp = Number(usage.outputTokens ?? usage.completion_tokens ?? usage.output_tokens ?? usage.completionTokens ?? 0)
22
22
 
23
23
  if (p <= 0 && comp <= 0) continue
24
24
 
package/lib/client.js CHANGED
@@ -292,7 +292,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
292
292
  "3. 粘贴到输入框中并点击 \"保存令牌\"。",
293
293
  "4. 若已安装 ZCode CLI (~/.zcode),点击 \"📥 从 CLI 导入\" 即可自动导入。",
294
294
  ],
295
- link: "https://open.bigmodel.cn/usercenter/apikeys",
295
+ link: "https://bigmodel.cn/usercenter/proj-mgmt/apikeys",
296
296
  },
297
297
  grok: {
298
298
  title: "xAI Grok",
@@ -434,6 +434,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
434
434
  "fcMinutes": "m",
435
435
  "reconnect": "Reconnect",
436
436
  "connect": "Connect",
437
+ "missingClientIdAntigravity": "Google OAuth requires a custom Client ID. Please configure antigravityClientId in plugin settings below, or click '📥 From CLI' to import active credentials.",
437
438
  "disconnect": "Disconnect",
438
439
  "removeSlot": "Remove slot",
439
440
  "pastePlaceholder": "Paste redirected URL or code",
@@ -603,6 +604,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
603
604
  "fcMinutes": "分",
604
605
  "reconnect": "重新连接",
605
606
  "connect": "连接",
607
+ "missingClientIdAntigravity": "Google OAuth 需要自定义 Client ID。请在下方设置中配置 antigravityClientId,或点击‘📥 从 CLI 导入’直接导入本地会话。",
606
608
  "disconnect": "断开连接",
607
609
  "removeSlot": "移除槽位",
608
610
  "pastePlaceholder": "粘贴重定向 URL 或授权代码",
@@ -985,6 +987,10 @@ const cssId = 'dsh-subscriptions/settings.module.css'
985
987
 
986
988
  const connect = async (provider, index) => {
987
989
  setErr('')
990
+ if (provider === 'antigravity' && (!draft || !draft.antigravityClientId || !String(draft.antigravityClientId).trim())) {
991
+ setErr(t('missingClientIdAntigravity'))
992
+ return
993
+ }
988
994
  const res = await fetch('/dsh-subscriptions/oauth/start?provider=' + encodeURIComponent(provider) + '&index=' + encodeURIComponent(index), { cache: 'no-store' })
989
995
  const data = await res.json().catch(() => ({}))
990
996
  if (!res.ok) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
@@ -64,6 +64,8 @@ export const Config = z.object({
64
64
  grokClientVersion: z.string().default('')
65
65
  .description('Grok CLI identity version header. Empty uses the built-in default.'),
66
66
  antigravityClientId: z.string().default(''),
67
+ antigravityClientSecret: z.string().default('')
68
+ .description('Google Cloud OAuth Client Secret for Antigravity.'),
67
69
  antigravityRedirectUri: z.string().default(''),
68
70
  customVendors: z.array(z.any()).default([])
69
71
  .description('Declarative OpenAI-Responses-compatible providers. See README.'),
package/lib/oauth.js CHANGED
@@ -7,6 +7,9 @@ export function buildAuthorizeUrl({
7
7
  extra,
8
8
  scope,
9
9
  }) {
10
+ if (!clientId || typeof clientId !== 'string' || !clientId.trim()) {
11
+ throw new Error('Missing required parameter: clientId')
12
+ }
10
13
  const url = new URL(authUrl)
11
14
  url.searchParams.set('response_type', 'code')
12
15
  url.searchParams.set('client_id', clientId)
@@ -1,5 +1,5 @@
1
1
  import { iterateSse, jsonSse } from './sse.js'
2
- import { httpError } from './wire.js'
2
+ import { httpError, toTokenUsage } from './wire.js'
3
3
 
4
4
  function responsesFailure(code, message) {
5
5
  const text = String(message || code || 'responses failed')
@@ -132,8 +132,8 @@ export class ResponsesStreamTranslator {
132
132
  }
133
133
  case 'response.completed': {
134
134
  this.terminated = true
135
- const usage = event.response?.usage
136
- if (usage !== undefined) chunks.push({ type: 'usage', usage })
135
+ const u = toTokenUsage(event.response?.usage)
136
+ if (u) chunks.push({ type: 'usage', usage: u })
137
137
  chunks.push({ type: 'finish', reason: { kind: this.sawToolCall ? 'tool-calls' : 'stop' } })
138
138
  return chunks
139
139
  }
@@ -51,7 +51,20 @@ export function registerOauthRoutes(ctx, state) {
51
51
  createdAt: Date.now(),
52
52
  })
53
53
  const cfg = { ...vendorConfig(provider, live()), redirectUri }
54
- const url = getVendor(provider).authorizeUrl(cfg, pkce)
54
+ let url
55
+ try {
56
+ url = getVendor(provider).authorizeUrl(cfg, pkce)
57
+ } catch (err) {
58
+ pending.delete(pkce.state)
59
+ writeJson(res, 400, {
60
+ ok: false,
61
+ error: {
62
+ code: 'missing_client_id',
63
+ message: err.message || 'OAuth authorization configuration error',
64
+ },
65
+ })
66
+ return
67
+ }
55
68
  // #89: if redirect_uri is loopback - start a temporary catch server.
56
69
  let autoCatch = false
57
70
  if (live().autoLoopback) {
@@ -52,6 +52,9 @@ export function defaults() {
52
52
  }
53
53
 
54
54
  export function authorizeUrl(cfg, pkce) {
55
+ if (!cfg || !cfg.clientId || !String(cfg.clientId).trim()) {
56
+ throw new Error('Google OAuth requires a custom Client ID. Please configure antigravityClientId in plugin settings or use "From CLI" to import active credentials.')
57
+ }
55
58
  return buildAuthorizeUrl({
56
59
  authUrl: AUTH,
57
60
  clientId: cfg.clientId,
@@ -47,7 +47,7 @@ export function defaults() {
47
47
  }
48
48
 
49
49
  export function authorizeUrl() {
50
- return 'https://open.bigmodel.cn/usercenter/apikeys'
50
+ return 'https://bigmodel.cn/usercenter/proj-mgmt/apikeys'
51
51
  }
52
52
 
53
53
  export async function listModels(_blob, cfg) {
package/lib/wire.js CHANGED
@@ -57,6 +57,102 @@ export function tokenBlobFromOAuth(json, extra) {
57
57
  }
58
58
  }
59
59
 
60
+ function toCount(v) {
61
+ if (typeof v === "number") {
62
+ return Number.isFinite(v) && v >= 0 ? Math.floor(v) : 0
63
+ }
64
+ if (typeof v === "string" && v.trim()) {
65
+ const n = Number(v)
66
+ return Number.isFinite(n) && n >= 0 ? Math.floor(n) : 0
67
+ }
68
+ return 0
69
+ }
70
+
71
+ /**
72
+ * Normalizes vendor usage metadata into canonical DSH TokenUsage format.
73
+ * Guarantees inputTokens and outputTokens are non-negative finite integer numbers (never NaN/undefined).
74
+ * Calculates disjoint uncached input tokens where providers fold cache hits into total prompt tokens.
75
+ * Returns null if raw is not an object or contains no token-related fields.
76
+ */
77
+ export function toTokenUsage(raw) {
78
+ if (!raw || typeof raw !== "object") return null
79
+
80
+ const hasKnownField = (
81
+ "inputTokens" in raw || "input_tokens" in raw || "promptTokens" in raw || "prompt_tokens" in raw || "input" in raw ||
82
+ "outputTokens" in raw || "output_tokens" in raw || "completionTokens" in raw || "completion_tokens" in raw || "output" in raw ||
83
+ "totalTokens" in raw || "total_tokens" in raw || "totalTokenCount" in raw ||
84
+ "promptTokenCount" in raw || "candidatesTokenCount" in raw
85
+ )
86
+ if (!hasKnownField) return null
87
+
88
+ const rawCached = toCount(
89
+ raw.input_token_details?.cached_tokens ??
90
+ raw.prompt_tokens_details?.cached_tokens ??
91
+ raw.cache_read_input_tokens ??
92
+ raw.cached_tokens ??
93
+ raw.cacheReadTokens ??
94
+ raw.cachedContentTokenCount
95
+ )
96
+
97
+ let inputTokens = 0
98
+ if (raw.inputTokens !== undefined) {
99
+ inputTokens = toCount(raw.inputTokens)
100
+ } else if (raw.prompt_tokens !== undefined || raw.promptTokens !== undefined) {
101
+ const prompt = toCount(raw.prompt_tokens ?? raw.promptTokens)
102
+ inputTokens = rawCached > 0 && prompt >= rawCached ? prompt - rawCached : prompt
103
+ } else if (raw.input_token_details?.cached_tokens !== undefined) {
104
+ const prompt = toCount(raw.input_tokens ?? raw.input)
105
+ inputTokens = rawCached > 0 && prompt >= rawCached ? prompt - rawCached : prompt
106
+ } else if (raw.promptTokenCount !== undefined) {
107
+ const prompt = toCount(raw.promptTokenCount)
108
+ inputTokens = rawCached > 0 && prompt >= rawCached ? prompt - rawCached : prompt
109
+ } else {
110
+ inputTokens = toCount(raw.input_tokens ?? raw.input)
111
+ }
112
+
113
+ let outputTokens = 0
114
+ if (raw.outputTokens !== undefined) {
115
+ outputTokens = toCount(raw.outputTokens)
116
+ } else {
117
+ outputTokens = toCount(
118
+ raw.completion_tokens ??
119
+ raw.output_tokens ??
120
+ raw.completionTokens ??
121
+ raw.output ??
122
+ raw.candidatesTokenCount
123
+ )
124
+ }
125
+
126
+ const cacheWriteTokens = toCount(raw.cacheWriteTokens ?? raw.cache_creation_input_tokens)
127
+ const reasoningTokens = toCount(
128
+ raw.reasoningTokens ??
129
+ raw.output_token_details?.reasoning_tokens ??
130
+ raw.completion_tokens_details?.reasoning_tokens ??
131
+ raw.reasoning_tokens
132
+ )
133
+ const rawTotal = toCount(raw.totalTokens ?? raw.total_tokens ?? raw.totalTokenCount)
134
+
135
+ const out = {
136
+ inputTokens,
137
+ outputTokens,
138
+ }
139
+
140
+ if (rawCached > 0 || raw.cacheReadTokens !== undefined || raw.cache_read_input_tokens !== undefined || raw.input_token_details?.cached_tokens !== undefined || raw.prompt_tokens_details?.cached_tokens !== undefined || raw.cachedContentTokenCount !== undefined) {
141
+ out.cacheReadTokens = rawCached
142
+ }
143
+ if (cacheWriteTokens > 0 || raw.cacheWriteTokens !== undefined || raw.cache_creation_input_tokens !== undefined) {
144
+ out.cacheWriteTokens = cacheWriteTokens
145
+ }
146
+ if (reasoningTokens > 0 || raw.reasoningTokens !== undefined || raw.output_token_details?.reasoning_tokens !== undefined || raw.completion_tokens_details?.reasoning_tokens !== undefined) {
147
+ out.reasoningTokens = reasoningTokens
148
+ }
149
+ if (rawTotal > 0 || raw.totalTokens !== undefined || raw.total_tokens !== undefined || raw.totalTokenCount !== undefined) {
150
+ out.totalTokens = rawTotal > 0 ? rawTotal : (inputTokens + (out.cacheReadTokens || 0) + (out.cacheWriteTokens || 0) + outputTokens)
151
+ }
152
+
153
+ return out
154
+ }
155
+
60
156
  function openBlock(kind, index) {
61
157
  return { kind, index, text: '', id: '', name: '' }
62
158
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.6.10",
3
+ "version": "0.6.12",
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",