@goodandready/dsh-subscriptions 0.6.26 → 0.6.28
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/adapter.js +34 -3
- package/lib/client.js +195 -7
- package/lib/crypto.js +32 -13
- package/lib/gemini-schema.js +7 -5
- package/lib/history.js +104 -0
- package/lib/images.js +125 -0
- package/lib/import-auth.js +34 -3
- package/lib/messages.js +122 -34
- package/lib/refs.js +1 -0
- package/lib/routes/accounts.js +101 -84
- package/lib/runaway-guard.js +105 -0
- package/lib/stream-rotate.js +3 -1
- package/lib/tools-normalizer.js +118 -0
- package/lib/usage.js +21 -0
- package/lib/vault.js +3 -3
- package/lib/vendors/copilot.js +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
export class RunawayDetector {
|
|
2
|
+
constructor(options = {}) {
|
|
3
|
+
this.maxIdenticalConsecutive = options.maxIdenticalConsecutive || 30
|
|
4
|
+
this.maxPatternRepetitions = options.maxPatternRepetitions || 6
|
|
5
|
+
this.minPatternLength = options.minPatternLength || 4
|
|
6
|
+
this.maxPatternLength = options.maxPatternLength || 80
|
|
7
|
+
this.history = ''
|
|
8
|
+
this.lastChunk = null
|
|
9
|
+
this.identicalCount = 0
|
|
10
|
+
this.tripped = false
|
|
11
|
+
this.reason = null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
feed(text) {
|
|
15
|
+
if (this.tripped || !text || typeof text !== 'string') return false
|
|
16
|
+
|
|
17
|
+
// 1. Check identical consecutive text chunks
|
|
18
|
+
if (this.lastChunk === text) {
|
|
19
|
+
this.identicalCount++
|
|
20
|
+
if (this.identicalCount >= this.maxIdenticalConsecutive) {
|
|
21
|
+
this.tripped = true
|
|
22
|
+
this.reason = `identical_chunk_loop (${this.identicalCount} repetitions of "${text.slice(0, 16)}")`
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
this.lastChunk = text
|
|
27
|
+
this.identicalCount = 1
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 2. Sliding window n-gram repetition detection
|
|
31
|
+
const maxHistLen = this.maxPatternLength * this.maxPatternRepetitions + 128
|
|
32
|
+
this.history += text
|
|
33
|
+
if (this.history.length > maxHistLen * 2) {
|
|
34
|
+
this.history = this.history.slice(-maxHistLen)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const hLen = this.history.length
|
|
38
|
+
for (let pLen = this.minPatternLength; pLen <= this.maxPatternLength; pLen++) {
|
|
39
|
+
const requiredLen = pLen * this.maxPatternRepetitions
|
|
40
|
+
if (hLen < requiredLen) break
|
|
41
|
+
|
|
42
|
+
const pattern = this.history.slice(hLen - pLen)
|
|
43
|
+
let matches = 0
|
|
44
|
+
for (let i = 1; i < this.maxPatternRepetitions; i++) {
|
|
45
|
+
const seg = this.history.slice(hLen - (i + 1) * pLen, hLen - i * pLen)
|
|
46
|
+
if (seg === pattern) {
|
|
47
|
+
matches++
|
|
48
|
+
} else {
|
|
49
|
+
break
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (matches >= this.maxPatternRepetitions - 1) {
|
|
53
|
+
this.tripped = true
|
|
54
|
+
this.reason = `pattern_repetition_loop (${pLen} chars repeated ${this.maxPatternRepetitions} times: "${pattern.slice(0, 20)}...")`
|
|
55
|
+
return true
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
isTripped() {
|
|
63
|
+
return this.tripped
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getReason() {
|
|
67
|
+
return this.reason
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function* withRunawayGuard(stream, options = {}) {
|
|
72
|
+
if (options && options.enabled === false) {
|
|
73
|
+
yield* stream
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const detector = new RunawayDetector(options)
|
|
78
|
+
|
|
79
|
+
for await (const chunk of stream) {
|
|
80
|
+
if (chunk && chunk.type === 'finish') {
|
|
81
|
+
yield chunk
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const text = (chunk && typeof chunk === 'object')
|
|
86
|
+
? (typeof chunk.text === 'string' ? chunk.text : null)
|
|
87
|
+
: null
|
|
88
|
+
|
|
89
|
+
if (text && (chunk.type === 'text-delta' || chunk.type === 'reasoning-delta' || !chunk.type)) {
|
|
90
|
+
if (detector.feed(text)) {
|
|
91
|
+
yield {
|
|
92
|
+
type: 'finish',
|
|
93
|
+
reason: {
|
|
94
|
+
kind: 'stop',
|
|
95
|
+
message: 'runaway_loop_detected',
|
|
96
|
+
detail: detector.getReason(),
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
yield chunk
|
|
104
|
+
}
|
|
105
|
+
}
|
package/lib/stream-rotate.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
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
|
+
import { withRunawayGuard } from './runaway-guard.js'
|
|
3
4
|
|
|
4
5
|
export async function* streamWithRotation({
|
|
5
6
|
accounts,
|
|
@@ -49,7 +50,8 @@ export async function* streamWithRotation({
|
|
|
49
50
|
|
|
50
51
|
while (true) {
|
|
51
52
|
try {
|
|
52
|
-
|
|
53
|
+
const guardedStream = withRunawayGuard(streamOnce(account, options), options && options.runawayGuard)
|
|
54
|
+
for await (const chunk of guardedStream) {
|
|
53
55
|
firstChunkDelivered = true
|
|
54
56
|
yield chunk
|
|
55
57
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export function sanitizeToolName(name) {
|
|
2
|
+
if (typeof name !== 'string' || !name.trim()) return 'tool_fn'
|
|
3
|
+
const sanitized = name.trim().replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64)
|
|
4
|
+
return sanitized || 'tool_fn'
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function sanitizeDescription(desc) {
|
|
8
|
+
if (typeof desc === 'string') return desc.trim()
|
|
9
|
+
if (desc == null) return ''
|
|
10
|
+
return String(desc).trim()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function normalizeJsonSchema(schema, depth = 0) {
|
|
14
|
+
if (depth > 12) return { type: 'string' }
|
|
15
|
+
if (!schema || typeof schema !== 'object') {
|
|
16
|
+
return { type: 'object' }
|
|
17
|
+
}
|
|
18
|
+
if (Array.isArray(schema)) {
|
|
19
|
+
return normalizeJsonSchema(schema[0] || { type: 'object' }, depth + 1)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const out = {}
|
|
23
|
+
|
|
24
|
+
let type = schema.type
|
|
25
|
+
let nullable = false
|
|
26
|
+
if (Array.isArray(type)) {
|
|
27
|
+
nullable = type.includes('null')
|
|
28
|
+
type = type.find((t) => t && t !== 'null') || 'string'
|
|
29
|
+
}
|
|
30
|
+
if (typeof type === 'string') {
|
|
31
|
+
out.type = type.toLowerCase()
|
|
32
|
+
} else if (schema.properties) {
|
|
33
|
+
out.type = 'object'
|
|
34
|
+
} else if (schema.items) {
|
|
35
|
+
out.type = 'array'
|
|
36
|
+
} else {
|
|
37
|
+
out.type = depth === 0 ? 'object' : 'string'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (nullable) {
|
|
41
|
+
out.nullable = true
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (schema.description) {
|
|
45
|
+
out.description = String(schema.description)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (out.type === 'object') {
|
|
49
|
+
if (schema.properties && typeof schema.properties === 'object' && !Array.isArray(schema.properties)) {
|
|
50
|
+
out.properties = {}
|
|
51
|
+
for (const [key, val] of Object.entries(schema.properties)) {
|
|
52
|
+
if (typeof key === 'string' && key) {
|
|
53
|
+
out.properties[key] = normalizeJsonSchema(val, depth + 1)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (Array.isArray(schema.required)) {
|
|
59
|
+
const propKeys = new Set(Object.keys(out.properties))
|
|
60
|
+
const reqs = schema.required
|
|
61
|
+
.filter((k) => typeof k === 'string' && k.length > 0)
|
|
62
|
+
.filter((k) => propKeys.size === 0 || propKeys.has(k))
|
|
63
|
+
if (reqs.length > 0) {
|
|
64
|
+
out.required = [...new Set(reqs)]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (typeof schema.additionalProperties === 'boolean') {
|
|
69
|
+
out.additionalProperties = schema.additionalProperties
|
|
70
|
+
}
|
|
71
|
+
} else if (out.type === 'array') {
|
|
72
|
+
if (schema.items) {
|
|
73
|
+
out.items = normalizeJsonSchema(
|
|
74
|
+
Array.isArray(schema.items) ? schema.items[0] : schema.items,
|
|
75
|
+
depth + 1,
|
|
76
|
+
)
|
|
77
|
+
} else {
|
|
78
|
+
out.items = { type: 'string' }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0) {
|
|
83
|
+
const cleaned = [...new Set(
|
|
84
|
+
schema.enum
|
|
85
|
+
.map((item) => (item != null ? String(item).trim() : ''))
|
|
86
|
+
.filter((item) => item.length > 0),
|
|
87
|
+
)]
|
|
88
|
+
if (cleaned.length > 0) {
|
|
89
|
+
out.enum = cleaned
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return out
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function normalizeTools(tools) {
|
|
97
|
+
if (!Array.isArray(tools) || !tools.length) return undefined
|
|
98
|
+
const validTools = []
|
|
99
|
+
|
|
100
|
+
for (const tool of tools) {
|
|
101
|
+
if (!tool || typeof tool !== 'object') continue
|
|
102
|
+
const rawName = tool.name || (tool.function && tool.function.name)
|
|
103
|
+
if (!rawName) continue
|
|
104
|
+
|
|
105
|
+
const name = sanitizeToolName(rawName)
|
|
106
|
+
const description = sanitizeDescription(tool.description || (tool.function && tool.function.description))
|
|
107
|
+
const rawParams = tool.parameters || tool.input_schema || (tool.function && tool.function.parameters)
|
|
108
|
+
const parameters = normalizeJsonSchema(rawParams)
|
|
109
|
+
|
|
110
|
+
validTools.push({
|
|
111
|
+
name,
|
|
112
|
+
description,
|
|
113
|
+
parameters,
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return validTools.length ? validTools : undefined
|
|
118
|
+
}
|
package/lib/usage.js
CHANGED
|
@@ -49,6 +49,24 @@ export function asUsageSnapshot(percent) {
|
|
|
49
49
|
if (usedPercent == null) return null
|
|
50
50
|
return { usedPercent }
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
export function parseResetAt(val) {
|
|
54
|
+
if (val == null) return null
|
|
55
|
+
if (typeof val === 'number' && Number.isFinite(val) && val > 0) {
|
|
56
|
+
return val < 1e11 ? Math.round(val * 1000) : Math.round(val)
|
|
57
|
+
}
|
|
58
|
+
if (typeof val === 'string' && val.trim()) {
|
|
59
|
+
const s = val.trim()
|
|
60
|
+
const n = Number(s)
|
|
61
|
+
if (Number.isFinite(n) && n > 0) {
|
|
62
|
+
return n < 1e11 ? Math.round(n * 1000) : Math.round(n)
|
|
63
|
+
}
|
|
64
|
+
const parsed = Date.parse(s)
|
|
65
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed
|
|
66
|
+
}
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
|
|
52
70
|
// Named limit windows reported by vendors (five_hour, seven_day, primary...).
|
|
53
71
|
// Label map is a data table: extend it when a vendor adds a window name.
|
|
54
72
|
const WINDOW_LABELS = {
|
|
@@ -105,11 +123,14 @@ export function usageWindows(obj) {
|
|
|
105
123
|
if (usedPercent != null) {
|
|
106
124
|
seen.add(resolved.id)
|
|
107
125
|
const label = windowLabel(resolved.id)
|
|
126
|
+
const rawReset = value.resetAt ?? value.reset_at ?? value.resetTime ?? value.resets_at ?? value.resetsAt ?? value.reset_time
|
|
127
|
+
const resetAt = parseResetAt(rawReset)
|
|
108
128
|
out.push({
|
|
109
129
|
id: resolved.id,
|
|
110
130
|
zh: label.zh,
|
|
111
131
|
en: label.en,
|
|
112
132
|
usedPercent,
|
|
133
|
+
...(resetAt ? { resetAt } : {}),
|
|
113
134
|
isModelBucket: resolved.isModelBucket,
|
|
114
135
|
})
|
|
115
136
|
}
|
package/lib/vault.js
CHANGED
|
@@ -9,7 +9,7 @@ export const VAULT_FORMAT = 'dsh-subscriptions-vault-v1'
|
|
|
9
9
|
/**
|
|
10
10
|
* Encrypts slots configuration and credential blobs into a portable vault string.
|
|
11
11
|
*/
|
|
12
|
-
export function exportVault({ slots = [], blobs = {}, exportedBy = 'dsh-subscriptions' }, passphrase) {
|
|
12
|
+
export function exportVault({ slots = [], blobs = {}, exportedBy = 'dsh-subscriptions', algorithm = 'scrypt' }, passphrase) {
|
|
13
13
|
const pass = String(passphrase || '').trim()
|
|
14
14
|
if (!pass || pass.length < 4) {
|
|
15
15
|
throw new Error('passphrase must be at least 4 characters')
|
|
@@ -24,7 +24,7 @@ export function exportVault({ slots = [], blobs = {}, exportedBy = 'dsh-subscrip
|
|
|
24
24
|
blobs: blobs && typeof blobs === 'object' ? blobs : {},
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
const encrypted = encryptWithPassphrase(JSON.stringify(payload), pass)
|
|
27
|
+
const encrypted = encryptWithPassphrase(JSON.stringify(payload), pass, { algorithm })
|
|
28
28
|
return {
|
|
29
29
|
ok: true,
|
|
30
30
|
vault: encrypted,
|
|
@@ -43,7 +43,7 @@ export function importVault(encryptedPayload, passphrase) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
const raw = String(encryptedPayload || '').trim()
|
|
46
|
-
if (!raw.startsWith('DSHE1:')) {
|
|
46
|
+
if (!raw.startsWith('DSHE1:') && !raw.startsWith('DSHE2:')) {
|
|
47
47
|
throw new Error('invalid vault format: missing DSHE1 header')
|
|
48
48
|
}
|
|
49
49
|
|
package/lib/vendors/copilot.js
CHANGED
|
@@ -42,6 +42,14 @@ export const COPILOT_MODELS = [
|
|
|
42
42
|
maxTokens: 65536,
|
|
43
43
|
inputModalities: ["text"],
|
|
44
44
|
reasoning: { efforts: [{ id: "low", name: "Low" }, { id: "medium", name: "Medium" }, { id: "high", name: "High" }] }
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: "o1",
|
|
48
|
+
name: "o1 (Copilot)",
|
|
49
|
+
contextWindow: 200000,
|
|
50
|
+
maxTokens: 65536,
|
|
51
|
+
inputModalities: ["text", "image"],
|
|
52
|
+
reasoning: { efforts: [{ id: "low", name: "Low" }, { id: "medium", name: "Medium" }, { id: "high", name: "High" }] }
|
|
45
53
|
}
|
|
46
54
|
]
|
|
47
55
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.28",
|
|
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",
|