@goodandready/dsh-subscriptions 0.6.11 → 0.6.13
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 +9 -0
- package/README.ru.md +9 -0
- package/README.zh.md +9 -0
- package/lib/adapter.js +12 -1
- package/lib/analyze-session.js +3 -3
- package/lib/index.js +169 -15
- package/lib/responses-stream.js +3 -3
- package/lib/wire.js +96 -0
- package/package.json +1 -1
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
|
-
})) {
|
|
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
|
package/lib/analyze-session.js
CHANGED
|
@@ -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
|
|
20
|
-
const c = Number(usage.prompt_tokens_details?.cached_tokens
|
|
21
|
-
const comp = Number(usage.completion_tokens
|
|
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/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import { createSubscriptionsService } from './subscriptions.js'
|
|
|
20
20
|
import { encryptWithPassphrase, decryptWithPassphrase } from './crypto.js'
|
|
21
21
|
import { quotaSnapshot } from './ratelimit.js'
|
|
22
22
|
import { createAccountStore, normalizeSlots, vendorConfig } from './accounts.js'
|
|
23
|
+
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
23
24
|
import { startLoopback } from './loopback.js'
|
|
24
25
|
import { OllamaAdapter, ollamaAlive, ollamaModels, ollamaBase } from './ollama.js'
|
|
25
26
|
import { createResetCreditService } from './reset-credits.js'
|
|
@@ -57,6 +58,11 @@ export function apply(ctx, config) {
|
|
|
57
58
|
const scope = sctx.settings.register(NS, Config, { base: config })
|
|
58
59
|
settingsApi = scope
|
|
59
60
|
getConfig = () => scope.get() ?? config
|
|
61
|
+
Promise.resolve().then(() => {
|
|
62
|
+
reconcileSlots().catch((err) => {
|
|
63
|
+
if (logger && logger.warn) logger.warn('[dsh-subscriptions] reconcileSlots initial error: ' + (err && err.message || err))
|
|
64
|
+
})
|
|
65
|
+
})
|
|
60
66
|
sctx.effect(() => () => {
|
|
61
67
|
settingsApi = undefined
|
|
62
68
|
getConfig = () => config
|
|
@@ -243,23 +249,126 @@ export function apply(ctx, config) {
|
|
|
243
249
|
})
|
|
244
250
|
|
|
245
251
|
let handle
|
|
252
|
+
let registeredByUs = new Set()
|
|
253
|
+
function disposeAdapterHandle(h) {
|
|
254
|
+
if (!h) return
|
|
255
|
+
try {
|
|
256
|
+
if (typeof h === 'function') h()
|
|
257
|
+
else if (typeof h.dispose === 'function') h.dispose()
|
|
258
|
+
} catch (err) {
|
|
259
|
+
if (logger && logger.debug) logger.debug('[dsh-subscriptions] dispose handle error: ' + (err && err.message || err))
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
246
263
|
async function syncAdapter() {
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
264
|
+
const desired = await store.loggedInProviders()
|
|
265
|
+
const globalProviders = ctx.llm && typeof ctx.llm.listProviders === 'function'
|
|
266
|
+
? ctx.llm.listProviders().map((p) => p.id)
|
|
267
|
+
: []
|
|
268
|
+
const globalSet = new Set(globalProviders)
|
|
269
|
+
|
|
270
|
+
const claimable = desired.filter((p) => !globalSet.has(p) || registeredByUs.has(p))
|
|
271
|
+
const skipped = desired.filter((p) => globalSet.has(p) && !registeredByUs.has(p))
|
|
272
|
+
|
|
273
|
+
if (skipped.length && logger && logger.info) {
|
|
274
|
+
logger.info(`[dsh-subscriptions] syncAdapter skipping already-bound providers: ${skipped.join(', ')}`)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!handle && claimable.length) {
|
|
278
|
+
handle = ctx.llm.registerAdapter(claimable, adapter)
|
|
279
|
+
registeredByUs = new Set(claimable)
|
|
250
280
|
return
|
|
251
281
|
}
|
|
252
|
-
if (!handle)
|
|
253
|
-
|
|
254
|
-
|
|
282
|
+
if (!handle) {
|
|
283
|
+
registeredByUs.clear()
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
if (!claimable.length) {
|
|
287
|
+
disposeAdapterHandle(handle)
|
|
255
288
|
handle = undefined
|
|
289
|
+
registeredByUs.clear()
|
|
256
290
|
return
|
|
257
291
|
}
|
|
292
|
+
if (typeof handle.replace === 'function') {
|
|
293
|
+
try {
|
|
294
|
+
handle.replace(claimable)
|
|
295
|
+
registeredByUs = new Set(claimable)
|
|
296
|
+
return
|
|
297
|
+
} catch (err) {
|
|
298
|
+
if (logger && logger.warn) logger.warn('[dsh-subscriptions] handle.replace failed: ' + (err && err.message || err))
|
|
299
|
+
disposeAdapterHandle(handle)
|
|
300
|
+
handle = undefined
|
|
301
|
+
registeredByUs.clear()
|
|
302
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
disposeAdapterHandle(handle)
|
|
305
|
+
handle = undefined
|
|
306
|
+
registeredByUs.clear()
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const currentGlobal = new Set((ctx.llm && typeof ctx.llm.listProviders === 'function' ? ctx.llm.listProviders() : []).map((p) => p.id))
|
|
310
|
+
const reClaimable = desired.filter((p) => !currentGlobal.has(p))
|
|
311
|
+
if (reClaimable.length) {
|
|
312
|
+
handle = ctx.llm.registerAdapter(reClaimable, adapter)
|
|
313
|
+
registeredByUs = new Set(reClaimable)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function reconcileSlots() {
|
|
318
|
+
const api = settingsApi
|
|
319
|
+
if (!api || typeof api.replace !== 'function') return
|
|
320
|
+
const creds = ctx.credentials
|
|
321
|
+
if (!creds || typeof creds.describe !== 'function') return
|
|
322
|
+
|
|
258
323
|
try {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
324
|
+
const existing = normalizeSlots(live().slots)
|
|
325
|
+
const existingRefs = new Set(existing.map((s) => s.ref))
|
|
326
|
+
const candidateProviders = PROVIDERS.filter((p) => isProvider(p))
|
|
327
|
+
const additions = []
|
|
328
|
+
|
|
329
|
+
for (const provider of candidateProviders) {
|
|
330
|
+
for (let index = 1; index <= 10; index++) {
|
|
331
|
+
const ref = oauthRef(provider, index)
|
|
332
|
+
if (existingRefs.has(ref)) continue
|
|
333
|
+
let d
|
|
334
|
+
try {
|
|
335
|
+
d = await creds.describe(credentialRef(ref))
|
|
336
|
+
} catch (err) {
|
|
337
|
+
if (logger && logger.debug) logger.debug(`[dsh-subscriptions reconcile] describe ${ref} error: ` + (err && err.message || err))
|
|
338
|
+
continue
|
|
339
|
+
}
|
|
340
|
+
if (d && d.configured) {
|
|
341
|
+
let label = d.label || ''
|
|
342
|
+
try {
|
|
343
|
+
const blob = await store.loadBlob(ref).catch(() => null)
|
|
344
|
+
if (blob && (blob.email || blob.label)) {
|
|
345
|
+
label = blob.email || blob.label || label
|
|
346
|
+
}
|
|
347
|
+
} catch (loadErr) {
|
|
348
|
+
if (logger && logger.debug) logger.debug(`[dsh-subscriptions reconcile] loadBlob ${ref} error: ` + (loadErr && loadErr.message || loadErr))
|
|
349
|
+
}
|
|
350
|
+
additions.push({ provider, index, label })
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (!additions.length) return
|
|
356
|
+
if (logger && logger.info) {
|
|
357
|
+
logger.info(`[dsh-subscriptions reconcile] restoring orphaned vault slots: ${additions.map((a) => `${a.provider}#${a.index}`).join(', ')}`)
|
|
358
|
+
}
|
|
359
|
+
const currentSlots = Array.isArray(live().slots) ? live().slots : []
|
|
360
|
+
const newSlots = [...currentSlots, ...additions]
|
|
361
|
+
const parsed = Config({ ...live(), slots: stripLegacySlots(newSlots) })
|
|
362
|
+
await api.replace(parsed)
|
|
363
|
+
syncCustomVendors()
|
|
364
|
+
await syncAdapter()
|
|
365
|
+
refreshModels().catch((err) => {
|
|
366
|
+
if (logger && logger.debug) logger.debug('[dsh-subscriptions reconcile] refreshModels error: ' + (err && err.message || err))
|
|
367
|
+
})
|
|
368
|
+
} catch (e) {
|
|
369
|
+
if (logger && logger.warn) {
|
|
370
|
+
logger.warn('[dsh-subscriptions reconcile] failed: ' + (e && e.message || e))
|
|
371
|
+
}
|
|
263
372
|
}
|
|
264
373
|
}
|
|
265
374
|
|
|
@@ -471,22 +580,67 @@ export function apply(ctx, config) {
|
|
|
471
580
|
}
|
|
472
581
|
|
|
473
582
|
ctx.effect(() => {
|
|
474
|
-
syncAdapter().catch(() => {
|
|
475
|
-
|
|
583
|
+
syncAdapter().catch((err) => {
|
|
584
|
+
if (logger && logger.debug) logger.debug('[dsh-subscriptions] syncAdapter initial error: ' + (err && err.message || err))
|
|
585
|
+
})
|
|
586
|
+
syncOllama().catch((err) => {
|
|
587
|
+
if (logger && logger.debug) logger.debug('[dsh-subscriptions] syncOllama initial error: ' + (err && err.message || err))
|
|
588
|
+
})
|
|
589
|
+
reconcileSlots().catch((err) => {
|
|
590
|
+
if (logger && logger.debug) logger.debug('[dsh-subscriptions] reconcileSlots effect error: ' + (err && err.message || err))
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
let retries = 0
|
|
594
|
+
let retryTimer
|
|
595
|
+
const scheduleRetry = () => {
|
|
596
|
+
if (retries >= 30) return
|
|
597
|
+
retryTimer = setTimeout(retrySync, 1000)
|
|
598
|
+
if (retryTimer && typeof retryTimer.unref === 'function') retryTimer.unref()
|
|
599
|
+
}
|
|
600
|
+
const retrySync = async () => {
|
|
601
|
+
try {
|
|
602
|
+
const loggedIn = await store.loggedInProviders()
|
|
603
|
+
if (!loggedIn.length) return
|
|
604
|
+
const registered = ctx.llm && typeof ctx.llm.listProviders === 'function'
|
|
605
|
+
? ctx.llm.listProviders().map((p) => p.id)
|
|
606
|
+
: []
|
|
607
|
+
const regSet = new Set(registered)
|
|
608
|
+
const pending = loggedIn.filter((p) => !regSet.has(p))
|
|
609
|
+
if (pending.length === 0) return
|
|
610
|
+
retries += 1
|
|
611
|
+
await syncAdapter()
|
|
612
|
+
if (retries < 30) {
|
|
613
|
+
scheduleRetry()
|
|
614
|
+
}
|
|
615
|
+
} catch (err) {
|
|
616
|
+
if (logger && logger.debug) logger.debug('[dsh-subscriptions] retrySync error: ' + (err && err.message || err))
|
|
617
|
+
if (retries < 30) {
|
|
618
|
+
scheduleRetry()
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
scheduleRetry()
|
|
623
|
+
|
|
476
624
|
// #75: eager usage refresh at startup so the windows (5h/7d) land in the blob immediately
|
|
477
625
|
// and the active subscription chip shows 5h/7d/... right away instead of waiting for probeInterval.
|
|
478
626
|
const eager = async () => {
|
|
479
627
|
try {
|
|
480
628
|
for (const slot of normalizeSlots(live().slots)) {
|
|
481
|
-
store.refreshUsage(slot.provider).catch(() => {
|
|
629
|
+
store.refreshUsage(slot.provider).catch((err) => {
|
|
630
|
+
if (logger && logger.debug) logger.debug('[dsh-subscriptions] eager usage error: ' + (err && err.message || err))
|
|
631
|
+
})
|
|
482
632
|
}
|
|
483
|
-
} catch (err) {
|
|
633
|
+
} catch (err) {
|
|
634
|
+
if (logger && logger.debug) logger.debug('[dsh-subscriptions] eager refresh error: ' + (err && err.message || err))
|
|
635
|
+
}
|
|
484
636
|
}
|
|
485
637
|
eager()
|
|
486
638
|
return () => {
|
|
639
|
+
if (retryTimer) clearTimeout(retryTimer)
|
|
487
640
|
if (handle) {
|
|
488
|
-
|
|
641
|
+
disposeAdapterHandle(handle)
|
|
489
642
|
handle = undefined
|
|
643
|
+
registeredByUs.clear()
|
|
490
644
|
}
|
|
491
645
|
}
|
|
492
646
|
}, 'dsh-subscriptions: llm adapter')
|
package/lib/responses-stream.js
CHANGED
|
@@ -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
|
|
136
|
-
if (
|
|
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.
|
|
3
|
+
"version": "0.6.13",
|
|
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",
|