@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.
@@ -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
+ }
@@ -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
- for await (const chunk of streamOnce(account, options)) {
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
 
@@ -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.26",
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",