@goodandready/dsh-subscriptions 0.6.24 → 0.6.26
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/lib/client.js +44 -0
- package/lib/gemini-schema.js +8 -1
- package/lib/rotate.js +8 -0
- package/lib/routes/proxy.js +4 -0
- package/lib/stream-rotate.js +43 -29
- package/lib/usage.js +57 -24
- package/lib/wire.js +40 -12
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -410,6 +410,9 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
410
410
|
"diagCopy": "Copy report",
|
|
411
411
|
"diagIssues": "Open issue tracker",
|
|
412
412
|
"diagHint": "The report has no tokens, emails, proxy addresses or personal data.",
|
|
413
|
+
"cacheAnalyze": "Analyze Session Cache",
|
|
414
|
+
"cacheHitRate": "Cache Hit Rate",
|
|
415
|
+
"tokensSaved": "Saved Tokens",
|
|
413
416
|
"subsPill": "SUBS",
|
|
414
417
|
"subsModalTitle": "Subscription Hub",
|
|
415
418
|
"subsActiveHero": "Active Subscription",
|
|
@@ -590,6 +593,9 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
590
593
|
"diagCopy": "复制报告",
|
|
591
594
|
"diagIssues": "打开 Issue 反馈",
|
|
592
595
|
"diagHint": "诊断报告不包含令牌、邮箱、代理地址或任何个人隐私数据。",
|
|
596
|
+
"cacheAnalyze": "分析会话缓存",
|
|
597
|
+
"cacheHitRate": "缓存命中率",
|
|
598
|
+
"tokensSaved": "已节省 Token",
|
|
593
599
|
"subsPill": "订阅",
|
|
594
600
|
"subsModalTitle": "订阅管理中心",
|
|
595
601
|
"subsActiveHero": "当前主用订阅",
|
|
@@ -802,6 +808,8 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
802
808
|
const [device, setDevice] = React.useState({}) // #90 key -> {state,userCode,authUrl,intervalMs,status}
|
|
803
809
|
const [proxyRes, setProxyRes] = React.useState({}) // #88 key -> {ok,latencyMs,error}
|
|
804
810
|
const [diag, setDiag] = React.useState('')
|
|
811
|
+
const [sessionAnalysis, setSessionAnalysis] = React.useState(null)
|
|
812
|
+
const [analyzingCache, setAnalyzingCache] = React.useState(false)
|
|
805
813
|
const [reset, setReset] = React.useState({}) // #85 key -> challenge state
|
|
806
814
|
const [, setResetTick] = React.useState(0)
|
|
807
815
|
React.useEffect(() => {
|
|
@@ -854,6 +862,37 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
854
862
|
}
|
|
855
863
|
}
|
|
856
864
|
// #99: one click - fetch the anonymized report and copy it to the clipboard.
|
|
865
|
+
const runAnalyzeSession = () => {
|
|
866
|
+
setAnalyzingCache(true)
|
|
867
|
+
fetch('/dsh-subscriptions/history?limit=50', { cache: 'no-store' })
|
|
868
|
+
.then((r) => r.json())
|
|
869
|
+
.then((h) => {
|
|
870
|
+
const items = (h && h.items) || []
|
|
871
|
+
const events = items.map((it) => ({
|
|
872
|
+
usage: {
|
|
873
|
+
promptTokens: it.promptTokens || (it.tokens && it.tokens.prompt) || 0,
|
|
874
|
+
cachedTokens: it.cachedTokens || (it.tokens && it.tokens.cached) || 0,
|
|
875
|
+
completionTokens: it.completionTokens || (it.tokens && it.tokens.completion) || 0,
|
|
876
|
+
},
|
|
877
|
+
}))
|
|
878
|
+
return fetch('/dsh-subscriptions/analyze-session', {
|
|
879
|
+
method: 'POST',
|
|
880
|
+
headers: { 'Content-Type': 'application/json' },
|
|
881
|
+
body: JSON.stringify({ events }),
|
|
882
|
+
})
|
|
883
|
+
})
|
|
884
|
+
.then((r) => r.json())
|
|
885
|
+
.then((d) => {
|
|
886
|
+
setAnalyzingCache(false)
|
|
887
|
+
if (d && d.ok && d.analysis) {
|
|
888
|
+
setSessionAnalysis(d.analysis)
|
|
889
|
+
}
|
|
890
|
+
})
|
|
891
|
+
.catch((e) => {
|
|
892
|
+
setAnalyzingCache(false)
|
|
893
|
+
setErr(cleanErrorMessage(e && e.message || e))
|
|
894
|
+
})
|
|
895
|
+
}
|
|
857
896
|
const genDiag = () => {
|
|
858
897
|
fetch('/dsh-subscriptions/diagnostics', { cache: 'no-store' })
|
|
859
898
|
.then((r) => r.json())
|
|
@@ -1481,9 +1520,14 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1481
1520
|
React.createElement('div', { className: 'dsub-block' },
|
|
1482
1521
|
React.createElement('div', { className: 'dsub-row' },
|
|
1483
1522
|
React.createElement('button', { type: 'button', className: 'dsub-mini', onClick: genDiag }, t('diagGenerate')),
|
|
1523
|
+
React.createElement('button', { type: 'button', className: 'dsub-mini', onClick: runAnalyzeSession }, analyzingCache ? '…' : t('cacheAnalyze')),
|
|
1484
1524
|
React.createElement('a', { href: 'https://github.com/GooDAnDReaDY/dsh-subscriptions/issues', target: '_blank', rel: 'noopener noreferrer', className: 'dsub-mini' }, t('diagIssues')),
|
|
1485
1525
|
),
|
|
1486
1526
|
React.createElement('div', { className: 'dsub-sub' }, t('diagHint')),
|
|
1527
|
+
sessionAnalysis ? React.createElement('div', { className: 'dsub-sub', style: { marginTop: '6px', fontWeight: 500 } },
|
|
1528
|
+
t('cacheHitRate') + ': ' + sessionAnalysis.weightedCacheHitPercent + '% · ' +
|
|
1529
|
+
t('tokensSaved') + ': ' + sessionAnalysis.savedTokens + ' / ' + sessionAnalysis.totalTokens
|
|
1530
|
+
) : null,
|
|
1487
1531
|
diag ? React.createElement('div', { className: 'dsub-row' },
|
|
1488
1532
|
React.createElement('button', {
|
|
1489
1533
|
type: 'button', className: 'dsub-mini',
|
package/lib/gemini-schema.js
CHANGED
|
@@ -52,7 +52,14 @@ export function toGeminiSchema(schema, depth) {
|
|
|
52
52
|
out.items = toGeminiSchema(Array.isArray(items) ? items[0] : (items || { type: 'string' }), level + 1)
|
|
53
53
|
}
|
|
54
54
|
if (Array.isArray(schema.enum) && schema.enum.length) {
|
|
55
|
-
|
|
55
|
+
const cleaned = [...new Set(
|
|
56
|
+
schema.enum
|
|
57
|
+
.map((item) => String(item ?? '').trim())
|
|
58
|
+
.filter((item) => item.length > 0),
|
|
59
|
+
)]
|
|
60
|
+
if (cleaned.length > 0) {
|
|
61
|
+
out.enum = cleaned
|
|
62
|
+
}
|
|
56
63
|
}
|
|
57
64
|
return out
|
|
58
65
|
}
|
package/lib/rotate.js
CHANGED
|
@@ -8,11 +8,19 @@ const SWITCH_CODES = new Set([
|
|
|
8
8
|
'LICENSE_REQUIRED',
|
|
9
9
|
])
|
|
10
10
|
|
|
11
|
+
export function isRegionError(err) {
|
|
12
|
+
if (!err || typeof err !== 'object') return false
|
|
13
|
+
const status = Number(err.status || err.statusCode) || 0
|
|
14
|
+
const msg = String(err.message || '')
|
|
15
|
+
return status === 400 && (/FAILED_PRECONDITION/i.test(msg) || /User location is not supported/i.test(msg))
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
export function isSwitchableError(err) {
|
|
12
19
|
if (!err || typeof err !== 'object') return false
|
|
13
20
|
if (err.name === 'AbortError' || err.code === 'ABORT_ERR' || err.code === 'ERR_ABORTED' || (err.message && /aborted/i.test(err.message))) return false
|
|
14
21
|
const code = err.code || (err.failure && err.failure.code)
|
|
15
22
|
if (SWITCH_CODES.has(code)) return true
|
|
23
|
+
if (isRegionError(err)) return true
|
|
16
24
|
const status = err.status || err.statusCode
|
|
17
25
|
return status === 401 || status === 403 || status === 429 || status === 500 || status === 502 || status === 503
|
|
18
26
|
}
|
package/lib/routes/proxy.js
CHANGED
|
@@ -19,6 +19,10 @@ export function registerProxyRoutes(ctx, state) {
|
|
|
19
19
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
20
20
|
return
|
|
21
21
|
}
|
|
22
|
+
if (!isTrustedSettingsRequest(req)) {
|
|
23
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
24
|
+
return
|
|
25
|
+
}
|
|
22
26
|
const raw = await readBody(req, 128 * 1024).catch(() => Buffer.alloc(0))
|
|
23
27
|
let body = {}
|
|
24
28
|
try { body = JSON.parse(raw.toString('utf8') || '{}') } catch { body = {} }
|
package/lib/stream-rotate.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { pickAccount, markCooldown, isSwitchableError, modelFamily } from './rotate.js'
|
|
1
|
+
import { pickAccount, markCooldown, isSwitchableError, isRegionError, modelFamily } from './rotate.js'
|
|
2
2
|
import { putInQuarantine, REASON_RATE_LIMIT, REASON_HARD_LIMIT, REASON_REVOKED } from './quarantine.js'
|
|
3
3
|
|
|
4
4
|
export async function* streamWithRotation({
|
|
@@ -45,38 +45,52 @@ export async function* streamWithRotation({
|
|
|
45
45
|
tried.add(account.ref || account.id)
|
|
46
46
|
|
|
47
47
|
let firstChunkDelivered = false
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
48
|
+
let regionRetries = 0
|
|
49
|
+
|
|
50
|
+
while (true) {
|
|
51
|
+
try {
|
|
52
|
+
for await (const chunk of streamOnce(account, options)) {
|
|
53
|
+
firstChunkDelivered = true
|
|
54
|
+
yield chunk
|
|
55
|
+
}
|
|
56
|
+
return
|
|
57
|
+
} catch (err) {
|
|
58
|
+
lastError = err
|
|
59
|
+
// If chunks were already yielded to the caller, never rotate mid-stream
|
|
60
|
+
// as that would repeat or scramble generated tokens.
|
|
61
|
+
if (firstChunkDelivered) throw err
|
|
60
62
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
// Transient region 400 error: retry same account up to 2 times before rotating (#391 / GH #9)
|
|
64
|
+
if (isRegionError(err) && regionRetries < 2 && (!options || !options.signal || !options.signal.aborted)) {
|
|
65
|
+
regionRetries++
|
|
66
|
+
await new Promise((r) => setTimeout(r, 400 * regionRetries))
|
|
67
|
+
if (options && options.signal && options.signal.aborted) throw err
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
65
70
|
|
|
66
|
-
|
|
67
|
-
const code = String(err && err.code || '')
|
|
68
|
-
const reason = (status === 401 || code === 'AUTH' || code === 'TOKEN_REVOKED')
|
|
69
|
-
? REASON_REVOKED
|
|
70
|
-
: (status === 403 || code === 'HARD_LIMIT' || code === 'LICENSE_REQUIRED')
|
|
71
|
-
? REASON_HARD_LIMIT
|
|
72
|
-
: REASON_RATE_LIMIT
|
|
71
|
+
if (!isSwitchableError(err)) throw err
|
|
73
72
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
// Move slot to cooldown and quarantine (#172)
|
|
74
|
+
const cooled = markCooldown(account, nowMs(), cooldownMs, modelFamily(options && options.provider, options && options.model))
|
|
75
|
+
account.cooldownUntil = cooled.cooldownUntil
|
|
76
|
+
if (cooled.cooldownFamilies) account.cooldownFamilies = cooled.cooldownFamilies
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
const status = Number(err && (err.status || err.statusCode) || 0)
|
|
79
|
+
const code = String(err && err.code || '')
|
|
80
|
+
const reason = (status === 401 || code === 'AUTH' || code === 'TOKEN_REVOKED')
|
|
81
|
+
? REASON_REVOKED
|
|
82
|
+
: (status === 403 || code === 'HARD_LIMIT' || code === 'LICENSE_REQUIRED')
|
|
83
|
+
? REASON_HARD_LIMIT
|
|
84
|
+
: REASON_RATE_LIMIT
|
|
85
|
+
|
|
86
|
+
const quarantined = putInQuarantine(account, reason, nowMs())
|
|
87
|
+
account.quarantineUntil = quarantined.quarantineUntil
|
|
88
|
+
account.quarantineReason = quarantined.quarantineReason
|
|
89
|
+
|
|
90
|
+
if (onCooldown) onCooldown(account)
|
|
91
|
+
// Break inner retry loop to continue to next account in outer while (true) loop
|
|
92
|
+
break
|
|
93
|
+
}
|
|
80
94
|
}
|
|
81
95
|
}
|
|
82
96
|
}
|
package/lib/usage.js
CHANGED
|
@@ -70,37 +70,70 @@ export function windowLabel(id) {
|
|
|
70
70
|
return known || { zh: id, en: id }
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
function resolveWindowId(value, key) {
|
|
74
|
+
if (value && typeof value === 'object') {
|
|
75
|
+
const semantic = value.modelId ?? value.model ?? value.window ?? value.name ?? value.id ?? value.scope
|
|
76
|
+
if (typeof semantic === 'string' && semantic.trim() && !/^\d+$/.test(semantic.trim())) {
|
|
77
|
+
return { id: semantic.trim(), isModelBucket: Boolean(value.modelId || value.model) }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (typeof key === 'string' && key.trim() && !/^\d+$/.test(key.trim())) {
|
|
81
|
+
return { id: key.trim(), isModelBucket: false }
|
|
82
|
+
}
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
|
|
73
86
|
// Walk vendor usage JSON; every node carrying a utilization-style number
|
|
74
|
-
// becomes a named window keyed by its JSON key.
|
|
87
|
+
// becomes a named window keyed by its semantic identifier or JSON key.
|
|
75
88
|
export function usageWindows(obj) {
|
|
76
89
|
const out = []
|
|
77
90
|
const seen = new Set()
|
|
78
91
|
function walk(value, key, depth) {
|
|
79
|
-
if (!value || typeof value !==
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
92
|
+
if (!value || typeof value !== 'object' || depth > 6) return
|
|
93
|
+
const resolved = resolveWindowId(value, key)
|
|
94
|
+
if (resolved && !seen.has(resolved.id)) {
|
|
95
|
+
const pct = numberOrNull(
|
|
96
|
+
value.utilization ?? value.used_percent ?? value.usedPercent ?? value.creditUsagePercent ?? value.used_percentage,
|
|
97
|
+
)
|
|
98
|
+
const frac = numberOrNull(value.remainingFraction)
|
|
99
|
+
const ufrac = numberOrNull(value.usedFraction)
|
|
100
|
+
let usedPercent = null
|
|
101
|
+
if (pct != null) usedPercent = pct
|
|
102
|
+
else if (frac != null) usedPercent = (1 - frac) * 100
|
|
103
|
+
else if (ufrac != null) usedPercent = ufrac * 100
|
|
104
|
+
|
|
105
|
+
if (usedPercent != null) {
|
|
106
|
+
seen.add(resolved.id)
|
|
107
|
+
const label = windowLabel(resolved.id)
|
|
108
|
+
out.push({
|
|
109
|
+
id: resolved.id,
|
|
110
|
+
zh: label.zh,
|
|
111
|
+
en: label.en,
|
|
112
|
+
usedPercent,
|
|
113
|
+
isModelBucket: resolved.isModelBucket,
|
|
114
|
+
})
|
|
115
|
+
}
|
|
99
116
|
}
|
|
100
117
|
for (const [k, child] of Object.entries(value)) {
|
|
101
|
-
if (child && typeof child ===
|
|
118
|
+
if (child && typeof child === 'object') walk(child, k, depth + 1)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
walk(obj, '', 0)
|
|
122
|
+
|
|
123
|
+
// Separate model buckets from standard named windows
|
|
124
|
+
const standardWindows = out.filter((w) => !w.isModelBucket)
|
|
125
|
+
const modelBuckets = out.filter((w) => w.isModelBucket)
|
|
126
|
+
|
|
127
|
+
let keptModelBuckets = []
|
|
128
|
+
if (modelBuckets.length) {
|
|
129
|
+
// Only surface model buckets when usage > 0, keeping the tightest 1-2
|
|
130
|
+
const active = modelBuckets.filter((w) => w.usedPercent > 0)
|
|
131
|
+
if (active.length) {
|
|
132
|
+
active.sort((a, b) => b.usedPercent - a.usedPercent)
|
|
133
|
+
keptModelBuckets = active.slice(0, 2)
|
|
102
134
|
}
|
|
103
135
|
}
|
|
104
|
-
|
|
105
|
-
|
|
136
|
+
|
|
137
|
+
const result = [...standardWindows, ...keptModelBuckets].map(({ isModelBucket: _, ...rest }) => rest)
|
|
138
|
+
return result.length ? result : null
|
|
106
139
|
}
|
package/lib/wire.js
CHANGED
|
@@ -190,7 +190,7 @@ export async function* openaiChatStream(body) {
|
|
|
190
190
|
yield {
|
|
191
191
|
type: 'tool-call-delta',
|
|
192
192
|
index: block.index,
|
|
193
|
-
id: block.id,
|
|
193
|
+
id: String(block.id || `call_${block.index}`),
|
|
194
194
|
...(block.name ? { name: block.name } : {}),
|
|
195
195
|
argumentsDelta: fragment,
|
|
196
196
|
}
|
|
@@ -207,20 +207,24 @@ export { streamResponses as codexResponsesStream } from './responses-stream.js'
|
|
|
207
207
|
export async function* anthropicStream(body) {
|
|
208
208
|
let index = 0
|
|
209
209
|
let textIndex = 0
|
|
210
|
+
const toolCallIds = new Map()
|
|
210
211
|
for await (const data of iterateSse(body)) {
|
|
211
212
|
const json = jsonSse(data)
|
|
212
213
|
if (!json) continue
|
|
213
214
|
if (json.type === 'content_block_start') {
|
|
214
215
|
const block = json.content_block || {}
|
|
215
216
|
if (block.type === 'text') {
|
|
216
|
-
textIndex = json.index
|
|
217
|
+
textIndex = json.index ?? index++
|
|
217
218
|
yield { type: 'block-start', index: textIndex, blockType: 'text' }
|
|
218
219
|
} else if (block.type === 'tool_use') {
|
|
219
|
-
|
|
220
|
+
const bIndex = json.index ?? index++
|
|
221
|
+
const toolId = String(block.id || `toolu_${bIndex}`)
|
|
222
|
+
toolCallIds.set(bIndex, toolId)
|
|
223
|
+
yield { type: 'block-start', index: bIndex, blockType: 'tool-call' }
|
|
220
224
|
yield {
|
|
221
225
|
type: 'tool-call-delta',
|
|
222
|
-
index:
|
|
223
|
-
id:
|
|
226
|
+
index: bIndex,
|
|
227
|
+
id: toolId,
|
|
224
228
|
name: block.name,
|
|
225
229
|
argumentsDelta: '',
|
|
226
230
|
}
|
|
@@ -228,16 +232,25 @@ export async function* anthropicStream(body) {
|
|
|
228
232
|
} else if (json.type === 'content_block_delta') {
|
|
229
233
|
const delta = json.delta || {}
|
|
230
234
|
if (delta.type === 'text_delta' && delta.text) {
|
|
231
|
-
yield { type: 'text-delta', index: json.index
|
|
235
|
+
yield { type: 'text-delta', index: json.index ?? textIndex, text: delta.text }
|
|
232
236
|
} else if (delta.type === 'input_json_delta' && delta.partial_json) {
|
|
237
|
+
const bIndex = json.index ?? 0
|
|
238
|
+
const toolId = toolCallIds.get(bIndex) || `toolu_${bIndex}`
|
|
233
239
|
yield {
|
|
234
240
|
type: 'tool-call-delta',
|
|
235
|
-
index:
|
|
241
|
+
index: bIndex,
|
|
242
|
+
id: toolId,
|
|
236
243
|
argumentsDelta: delta.partial_json,
|
|
237
244
|
}
|
|
238
245
|
}
|
|
239
246
|
} else if (json.type === 'message_delta' && json.usage) {
|
|
240
|
-
yield {
|
|
247
|
+
yield {
|
|
248
|
+
type: 'usage',
|
|
249
|
+
usage: {
|
|
250
|
+
input: Number(json.usage.input_tokens ?? json.usage.input ?? 0) || 0,
|
|
251
|
+
output: Number(json.usage.output_tokens ?? json.usage.output ?? 0) || 0,
|
|
252
|
+
},
|
|
253
|
+
}
|
|
241
254
|
}
|
|
242
255
|
}
|
|
243
256
|
yield { type: 'finish', reason: { kind: 'stop' } }
|
|
@@ -246,6 +259,8 @@ export async function* anthropicStream(body) {
|
|
|
246
259
|
export async function* googleStream(body) {
|
|
247
260
|
let index = 0
|
|
248
261
|
let started = false
|
|
262
|
+
let callCount = 0
|
|
263
|
+
const functionCallIndexes = new Map()
|
|
249
264
|
for await (const data of iterateSse(body)) {
|
|
250
265
|
const json = jsonSse(data)
|
|
251
266
|
const wrapped = json && json.response ? json.response : json
|
|
@@ -261,10 +276,23 @@ export async function* googleStream(body) {
|
|
|
261
276
|
yield { type: 'text-delta', index, text: part.text }
|
|
262
277
|
}
|
|
263
278
|
if (part.functionCall) {
|
|
264
|
-
|
|
279
|
+
const rawId = part.functionCall.id
|
|
280
|
+
let callIndex
|
|
281
|
+
let callId
|
|
282
|
+
if (rawId && functionCallIndexes.has(rawId)) {
|
|
283
|
+
callIndex = functionCallIndexes.get(rawId)
|
|
284
|
+
callId = String(rawId)
|
|
285
|
+
} else {
|
|
286
|
+
callCount++
|
|
287
|
+
callIndex = index + callCount
|
|
288
|
+
callId = String(rawId || `gemini_call_${callCount}`)
|
|
289
|
+
if (rawId) functionCallIndexes.set(rawId, callIndex)
|
|
290
|
+
yield { type: 'block-start', index: callIndex, blockType: 'tool-call' }
|
|
291
|
+
}
|
|
265
292
|
yield {
|
|
266
293
|
type: 'tool-call-delta',
|
|
267
|
-
index:
|
|
294
|
+
index: callIndex,
|
|
295
|
+
id: callId,
|
|
268
296
|
name: part.functionCall.name,
|
|
269
297
|
argumentsDelta: JSON.stringify(part.functionCall.args || {}),
|
|
270
298
|
}
|
|
@@ -274,8 +302,8 @@ export async function* googleStream(body) {
|
|
|
274
302
|
yield {
|
|
275
303
|
type: 'usage',
|
|
276
304
|
usage: {
|
|
277
|
-
input: wrapped.usageMetadata.promptTokenCount,
|
|
278
|
-
output: wrapped.usageMetadata.candidatesTokenCount,
|
|
305
|
+
input: Number(wrapped.usageMetadata.promptTokenCount ?? 0) || 0,
|
|
306
|
+
output: Number(wrapped.usageMetadata.candidatesTokenCount ?? 0) || 0,
|
|
279
307
|
},
|
|
280
308
|
}
|
|
281
309
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.26",
|
|
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",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"url": "https://github.com/GooDAnDReaDY/dsh-subscriptions/issues"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
|
-
"test": "eslint lib/ && node --test test/*.test.mjs",
|
|
40
|
+
"test": "eslint lib/ test/ && node --test test/*.test.mjs",
|
|
41
41
|
"typecheck": "tsc -p tsconfig.checkjs.json"
|
|
42
42
|
},
|
|
43
43
|
"dsh": {
|