@goodandready/dsh-subscriptions 0.6.25 → 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/gemini-schema.js +8 -1
- package/lib/rotate.js +8 -0
- package/lib/stream-rotate.js +43 -29
- package/lib/usage.js +57 -24
- package/lib/wire.js +40 -12
- package/package.json +1 -1
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/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",
|