@goodandready/dsh-subscriptions 0.6.11 → 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,15 @@ 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
+
275
284
  ## 🔒 OAuth Client Validation & Route Updates (Added in v0.6.11)
276
285
 
277
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.
package/README.ru.md CHANGED
@@ -245,6 +245,15 @@ 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
+
248
257
  ## 🔒 Защита OAuth Client ID и обновление ссылок (Добавлено в v0.6.11)
249
258
 
250
259
  - **Защита OAuth Client ID (GitHub #2)**: uildAuthorizeUrl и ntigravity.authorizeUrl теперь строго требуют непустой clientId. Если параметр не задан, сервер возвращает явный код ошибки HTTP 400 (missing_client_id), а интерфейс выводит подсказку с предложением указать ntigravityClientId в настройках или нажать «📥 Из CLI», предотвращая отправку битых запросов в Google OAuth.
package/README.zh.md CHANGED
@@ -167,6 +167,15 @@ 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
+
170
179
  ## 🔒 OAuth Client ID 校验保护与控制台地址更新 (v0.6.11 新增)
171
180
 
172
181
  - **Google OAuth Client ID 校验保护 (GitHub #2)**:uildAuthorizeUrl 和 ntigravity.authorizeUrl 严格要求非空 clientId。未配置时服务端返回明确的 HTTP 400 (missing_client_id),前端界面提示用户在插件设置中填写 ntigravityClientId 或使用“📥 从 CLI 导入”,避免浏览器跳转至 Google OAuth 400 报错页面。
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
 
@@ -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
  }
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.11",
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",