@goodandready/dsh-subscriptions 0.6.27 → 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/gemini-schema.js +7 -5
- package/lib/images.js +125 -0
- package/lib/messages.js +122 -34
- package/lib/runaway-guard.js +105 -0
- package/lib/stream-rotate.js +3 -1
- package/lib/tools-normalizer.js +118 -0
- package/package.json +1 -1
package/lib/gemini-schema.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeTools } from './tools-normalizer.js'
|
|
1
2
|
const TYPE_MAP = {
|
|
2
3
|
string: 'string',
|
|
3
4
|
number: 'number',
|
|
@@ -33,7 +34,7 @@ export function toGeminiSchema(schema, depth) {
|
|
|
33
34
|
else type = 'string'
|
|
34
35
|
}
|
|
35
36
|
const out = { type }
|
|
36
|
-
if (typed && typed.nullable) out.nullable = true
|
|
37
|
+
if ((typed && typed.nullable) || schema.nullable === true) out.nullable = true
|
|
37
38
|
if (schema.description) out.description = String(schema.description)
|
|
38
39
|
if (type === 'object') {
|
|
39
40
|
const props = schema.properties
|
|
@@ -65,10 +66,11 @@ export function toGeminiSchema(schema, depth) {
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
export function geminiFunctionDeclarations(tools) {
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
const norm = normalizeTools(tools)
|
|
70
|
+
if (!norm) return undefined
|
|
71
|
+
return norm.map((tool) => ({
|
|
70
72
|
name: tool.name,
|
|
71
|
-
description: tool.description
|
|
72
|
-
parameters: toGeminiSchema(tool.parameters
|
|
73
|
+
description: tool.description,
|
|
74
|
+
parameters: toGeminiSchema(tool.parameters),
|
|
73
75
|
}))
|
|
74
76
|
}
|
package/lib/images.js
CHANGED
|
@@ -109,3 +109,128 @@ export async function generateOnce(options) {
|
|
|
109
109
|
}
|
|
110
110
|
return parseImages(payload)
|
|
111
111
|
}
|
|
112
|
+
|
|
113
|
+
// Multimodal Payload Normalization for Vision Models
|
|
114
|
+
export function parseDataUri(uri) {
|
|
115
|
+
if (typeof uri !== 'string') return null
|
|
116
|
+
const m = /^data:([a-zA-Z0-9.+/-]+);base64,(.+)$/.exec(uri.trim())
|
|
117
|
+
if (!m) return null
|
|
118
|
+
return { mimeType: m[1], base64: m[2] }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function normalizeImage(item) {
|
|
122
|
+
if (!item) return null
|
|
123
|
+
|
|
124
|
+
// 1. Direct string (data URI or URL)
|
|
125
|
+
if (typeof item === 'string') {
|
|
126
|
+
const dataUri = parseDataUri(item)
|
|
127
|
+
if (dataUri) {
|
|
128
|
+
return { mimeType: dataUri.mimeType, base64: dataUri.base64, url: item.trim() }
|
|
129
|
+
}
|
|
130
|
+
if (/^https?:\/\//i.test(item.trim())) {
|
|
131
|
+
return { mimeType: 'image/jpeg', base64: null, url: item.trim() }
|
|
132
|
+
}
|
|
133
|
+
return null
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (typeof item !== 'object') return null
|
|
137
|
+
|
|
138
|
+
// 2. Anthropic style: { type: 'image', source: { type: 'base64', media_type, data } }
|
|
139
|
+
if (item.source && item.source.type === 'base64' && item.source.data) {
|
|
140
|
+
const mimeType = item.source.media_type || 'image/jpeg'
|
|
141
|
+
const base64 = item.source.data
|
|
142
|
+
return {
|
|
143
|
+
mimeType,
|
|
144
|
+
base64,
|
|
145
|
+
url: `data:${mimeType};base64,${base64}`,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 3. Gemini style: { inlineData: { mimeType, data } }
|
|
150
|
+
if (item.inlineData && item.inlineData.data) {
|
|
151
|
+
const mimeType = item.inlineData.mimeType || 'image/jpeg'
|
|
152
|
+
const base64 = item.inlineData.data
|
|
153
|
+
return {
|
|
154
|
+
mimeType,
|
|
155
|
+
base64,
|
|
156
|
+
url: `data:${mimeType};base64,${base64}`,
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 4. OpenAI style: { type: 'image_url', image_url: { url } } or { type: 'image', image: '...' }
|
|
161
|
+
const rawUrl = (item.image_url && (item.image_url.url || item.image_url)) ||
|
|
162
|
+
item.image || item.url
|
|
163
|
+
|
|
164
|
+
if (typeof rawUrl === 'string') {
|
|
165
|
+
const dataUri = parseDataUri(rawUrl)
|
|
166
|
+
if (dataUri) {
|
|
167
|
+
return { mimeType: dataUri.mimeType, base64: dataUri.base64, url: rawUrl.trim() }
|
|
168
|
+
}
|
|
169
|
+
if (/^https?:\/\//i.test(rawUrl.trim())) {
|
|
170
|
+
return { mimeType: 'image/jpeg', base64: null, url: rawUrl.trim() }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return null
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function extractImages(content) {
|
|
178
|
+
if (!Array.isArray(content)) {
|
|
179
|
+
const norm = normalizeImage(content)
|
|
180
|
+
return norm ? [norm] : []
|
|
181
|
+
}
|
|
182
|
+
const images = []
|
|
183
|
+
for (const block of content) {
|
|
184
|
+
if (!block) continue
|
|
185
|
+
if (typeof block === 'string') {
|
|
186
|
+
const norm = normalizeImage(block)
|
|
187
|
+
if (norm) images.push(norm)
|
|
188
|
+
continue
|
|
189
|
+
}
|
|
190
|
+
if (block.type === 'image' || block.type === 'image_url' || block.inlineData || block.image_url || block.image) {
|
|
191
|
+
const norm = normalizeImage(block)
|
|
192
|
+
if (norm) images.push(norm)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return images
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function toOpenAiImage(norm) {
|
|
199
|
+
const url = norm.url || (norm.base64 ? `data:${norm.mimeType};base64,${norm.base64}` : null)
|
|
200
|
+
if (!url) return null
|
|
201
|
+
return {
|
|
202
|
+
type: 'image_url',
|
|
203
|
+
image_url: { url },
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function toAnthropicImage(norm) {
|
|
208
|
+
if (!norm.base64) return null
|
|
209
|
+
return {
|
|
210
|
+
type: 'image',
|
|
211
|
+
source: {
|
|
212
|
+
type: 'base64',
|
|
213
|
+
media_type: norm.mimeType || 'image/jpeg',
|
|
214
|
+
data: norm.base64,
|
|
215
|
+
},
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function toGeminiImage(norm) {
|
|
220
|
+
if (!norm.base64) return null
|
|
221
|
+
return {
|
|
222
|
+
inlineData: {
|
|
223
|
+
mimeType: norm.mimeType || 'image/jpeg',
|
|
224
|
+
data: norm.base64,
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function toCodexImage(norm) {
|
|
230
|
+
const url = norm.url || (norm.base64 ? `data:${norm.mimeType};base64,${norm.base64}` : null)
|
|
231
|
+
if (!url) return null
|
|
232
|
+
return {
|
|
233
|
+
type: 'input_image',
|
|
234
|
+
image_url: url,
|
|
235
|
+
}
|
|
236
|
+
}
|
package/lib/messages.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { extractImages, toOpenAiImage, toAnthropicImage, toGeminiImage, toCodexImage } from './images.js'
|
|
2
|
+
import { normalizeTools } from './tools-normalizer.js'
|
|
1
3
|
import { geminiFunctionDeclarations } from './gemini-schema.js'
|
|
2
4
|
|
|
3
5
|
function flattenText(content) {
|
|
@@ -64,7 +66,18 @@ export function openaiMessages(options) {
|
|
|
64
66
|
}
|
|
65
67
|
const results = toolResults(message.content)
|
|
66
68
|
const text = flattenText(message.content)
|
|
67
|
-
|
|
69
|
+
const images = extractImages(message.content)
|
|
70
|
+
if (images.length) {
|
|
71
|
+
const parts = []
|
|
72
|
+
if (text) parts.push({ type: 'text', text })
|
|
73
|
+
for (const img of images) {
|
|
74
|
+
const blk = toOpenAiImage(img)
|
|
75
|
+
if (blk) parts.push(blk)
|
|
76
|
+
}
|
|
77
|
+
out.push({ role: 'user', content: parts })
|
|
78
|
+
} else if (text || !results.length) {
|
|
79
|
+
out.push({ role: 'user', content: text })
|
|
80
|
+
}
|
|
68
81
|
for (const result of results) {
|
|
69
82
|
out.push({
|
|
70
83
|
role: 'tool',
|
|
@@ -77,14 +90,14 @@ export function openaiMessages(options) {
|
|
|
77
90
|
}
|
|
78
91
|
|
|
79
92
|
export function openaiTools(options) {
|
|
80
|
-
const tools = options.tools
|
|
81
|
-
if (!
|
|
93
|
+
const tools = normalizeTools(options && options.tools)
|
|
94
|
+
if (!tools) return undefined
|
|
82
95
|
return tools.map((tool) => ({
|
|
83
96
|
type: 'function',
|
|
84
97
|
function: {
|
|
85
98
|
name: tool.name,
|
|
86
|
-
description: tool.description
|
|
87
|
-
parameters: tool.parameters
|
|
99
|
+
description: tool.description,
|
|
100
|
+
parameters: tool.parameters,
|
|
88
101
|
},
|
|
89
102
|
}))
|
|
90
103
|
}
|
|
@@ -147,6 +160,18 @@ export function reconcileResponsesInput(items) {
|
|
|
147
160
|
return result
|
|
148
161
|
}
|
|
149
162
|
|
|
163
|
+
export function normalizeSystemPrefix(parts) {
|
|
164
|
+
if (!Array.isArray(parts)) return ""
|
|
165
|
+
return parts
|
|
166
|
+
.map((p) => {
|
|
167
|
+
if (!p) return ""
|
|
168
|
+
const s = typeof p === "string" ? p : (p.text || String(p))
|
|
169
|
+
return s.replace(/\r\n/g, "\n").trim()
|
|
170
|
+
})
|
|
171
|
+
.filter(Boolean)
|
|
172
|
+
.join("\n\n")
|
|
173
|
+
}
|
|
174
|
+
|
|
150
175
|
export function codexResponsesBody(options, fallbackInstructions, vendorCfg) {
|
|
151
176
|
const systemParts = []
|
|
152
177
|
if (options.system) systemParts.push(options.system)
|
|
@@ -215,6 +240,15 @@ export function codexResponsesBody(options, fallbackInstructions, vendorCfg) {
|
|
|
215
240
|
const results = toolResults(message.content)
|
|
216
241
|
const text = flattenText(message.content)
|
|
217
242
|
if (text) addText('user', text)
|
|
243
|
+
const images = extractImages(message.content)
|
|
244
|
+
for (const img of images) {
|
|
245
|
+
const blk = toCodexImage(img)
|
|
246
|
+
if (blk) {
|
|
247
|
+
if (pendingRole && pendingRole !== 'user') flush()
|
|
248
|
+
pendingRole = 'user'
|
|
249
|
+
pendingContent.push(blk)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
218
252
|
for (const result of results) {
|
|
219
253
|
flush()
|
|
220
254
|
const callId = String(result.toolCallId || result.tool_call_id || result.call_id || result.id || '')
|
|
@@ -239,7 +273,7 @@ export function codexResponsesBody(options, fallbackInstructions, vendorCfg) {
|
|
|
239
273
|
description: tool.function.description,
|
|
240
274
|
parameters: tool.function.parameters,
|
|
241
275
|
}))
|
|
242
|
-
const instructions = systemParts
|
|
276
|
+
const instructions = normalizeSystemPrefix(systemParts) || (fallbackInstructions ? fallbackInstructions.replace(/\r\n/g, "\n").trim() : undefined)
|
|
243
277
|
return {
|
|
244
278
|
model: options.model,
|
|
245
279
|
stream: true,
|
|
@@ -265,77 +299,123 @@ export function anthropicPayload(options, extraSystem) {
|
|
|
265
299
|
if (options.system) systemParts.push(options.system)
|
|
266
300
|
const messages = []
|
|
267
301
|
for (const message of options.messages || []) {
|
|
268
|
-
if (message.role ===
|
|
302
|
+
if (message.role === "system") {
|
|
269
303
|
systemParts.push(flattenText(message.content))
|
|
270
304
|
continue
|
|
271
305
|
}
|
|
272
|
-
if (message.role ===
|
|
306
|
+
if (message.role === "assistant") {
|
|
273
307
|
const blocks = []
|
|
274
308
|
const text = flattenText(message.content)
|
|
275
|
-
if (text) blocks.push({ type:
|
|
309
|
+
if (text) blocks.push({ type: "text", text })
|
|
276
310
|
const calls = [
|
|
277
311
|
...toolCalls(message.content),
|
|
278
312
|
...((message.tool_calls || message.toolCalls || []).map((c) => ({
|
|
279
313
|
id: c.id || c.call_id,
|
|
280
314
|
name: c.name || (c.function && c.function.name),
|
|
281
|
-
arguments: c.arguments || (c.function && c.function.arguments) ||
|
|
315
|
+
arguments: c.arguments || (c.function && c.function.arguments) || "{}",
|
|
282
316
|
}))),
|
|
283
317
|
]
|
|
284
318
|
for (const call of calls) {
|
|
285
319
|
if (!call.id) continue
|
|
286
320
|
blocks.push({
|
|
287
|
-
type:
|
|
321
|
+
type: "tool_use",
|
|
288
322
|
id: call.id,
|
|
289
323
|
name: call.name,
|
|
290
324
|
input: safeJson(call.arguments),
|
|
291
325
|
})
|
|
292
326
|
}
|
|
293
|
-
messages.push({ role:
|
|
327
|
+
messages.push({ role: "assistant", content: blocks.length ? blocks : [{ type: "text", text: "" }] })
|
|
294
328
|
continue
|
|
295
329
|
}
|
|
296
|
-
if (message.role ===
|
|
297
|
-
const toolUseId = String(message.toolCallId || message.tool_call_id || message.tool_use_id || message.id ||
|
|
298
|
-
const content = typeof message.content ===
|
|
330
|
+
if (message.role === "tool") {
|
|
331
|
+
const toolUseId = String(message.toolCallId || message.tool_call_id || message.tool_use_id || message.id || "")
|
|
332
|
+
const content = typeof message.content === "string"
|
|
299
333
|
? message.content
|
|
300
|
-
: (flattenText(message.content) || JSON.stringify(message.content ||
|
|
334
|
+
: (flattenText(message.content) || JSON.stringify(message.content || ""))
|
|
301
335
|
messages.push({
|
|
302
|
-
role:
|
|
336
|
+
role: "user",
|
|
303
337
|
content: [{
|
|
304
|
-
type:
|
|
338
|
+
type: "tool_result",
|
|
305
339
|
tool_use_id: toolUseId,
|
|
306
|
-
content: content ||
|
|
340
|
+
content: content || "",
|
|
307
341
|
}],
|
|
308
342
|
})
|
|
309
343
|
continue
|
|
310
344
|
}
|
|
311
345
|
const blocks = []
|
|
312
346
|
const text = flattenText(message.content)
|
|
313
|
-
if (text) blocks.push({ type:
|
|
347
|
+
if (text) blocks.push({ type: "text", text })
|
|
348
|
+
const images = extractImages(message.content)
|
|
349
|
+
for (const img of images) {
|
|
350
|
+
const blk = toAnthropicImage(img)
|
|
351
|
+
if (blk) blocks.push(blk)
|
|
352
|
+
}
|
|
314
353
|
for (const result of toolResults(message.content)) {
|
|
315
|
-
const toolUseId = String(result.toolCallId || result.tool_call_id || result.tool_use_id || result.id ||
|
|
316
|
-
const content = typeof result.content ===
|
|
354
|
+
const toolUseId = String(result.toolCallId || result.tool_call_id || result.tool_use_id || result.id || "")
|
|
355
|
+
const content = typeof result.content === "string"
|
|
317
356
|
? result.content
|
|
318
|
-
: (flattenText(result.content) || (result.output != null ? (typeof result.output ===
|
|
357
|
+
: (flattenText(result.content) || (result.output != null ? (typeof result.output === "string" ? result.output : JSON.stringify(result.output)) : ""))
|
|
319
358
|
blocks.push({
|
|
320
|
-
type:
|
|
359
|
+
type: "tool_result",
|
|
321
360
|
tool_use_id: toolUseId,
|
|
322
|
-
content: content ||
|
|
361
|
+
content: content || "",
|
|
323
362
|
})
|
|
324
363
|
}
|
|
325
|
-
messages.push({ role:
|
|
364
|
+
messages.push({ role: "user", content: blocks.length ? blocks : [{ type: "text", text: "" }] })
|
|
326
365
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
366
|
+
|
|
367
|
+
const useCacheControl = options.cacheControl !== false
|
|
368
|
+
const systemText = normalizeSystemPrefix(systemParts)
|
|
369
|
+
let system
|
|
370
|
+
if (systemText) {
|
|
371
|
+
if (useCacheControl) {
|
|
372
|
+
system = [{ type: "text", text: systemText, cache_control: { type: "ephemeral" } }]
|
|
373
|
+
} else {
|
|
374
|
+
system = systemText
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const normTools = normalizeTools(options.tools)
|
|
379
|
+
const tools = normTools
|
|
380
|
+
? normTools.map((tool, idx) => {
|
|
381
|
+
const item = {
|
|
382
|
+
name: tool.name,
|
|
383
|
+
description: tool.description,
|
|
384
|
+
input_schema: tool.parameters,
|
|
385
|
+
}
|
|
386
|
+
if (useCacheControl && idx === normTools.length - 1) {
|
|
387
|
+
item.cache_control = { type: "ephemeral" }
|
|
388
|
+
}
|
|
389
|
+
return item
|
|
390
|
+
})
|
|
333
391
|
: undefined
|
|
392
|
+
|
|
393
|
+
if (useCacheControl) {
|
|
394
|
+
const userTurnIndices = []
|
|
395
|
+
for (let i = 0; i < messages.length; i++) {
|
|
396
|
+
if (messages[i].role === "user") {
|
|
397
|
+
userTurnIndices.push(i)
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (userTurnIndices.length >= 2) {
|
|
401
|
+
const targetIdx = userTurnIndices[userTurnIndices.length - 2]
|
|
402
|
+
const targetMsg = messages[targetIdx]
|
|
403
|
+
if (Array.isArray(targetMsg.content) && targetMsg.content.length > 0) {
|
|
404
|
+
const lastBlockIdx = targetMsg.content.length - 1
|
|
405
|
+
const lastBlock = targetMsg.content[lastBlockIdx]
|
|
406
|
+
targetMsg.content[lastBlockIdx] = {
|
|
407
|
+
...lastBlock,
|
|
408
|
+
cache_control: { type: "ephemeral" },
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
334
414
|
return {
|
|
335
415
|
model: options.model,
|
|
336
416
|
max_tokens: options.maxTokens || 8192,
|
|
337
417
|
stream: true,
|
|
338
|
-
...(
|
|
418
|
+
...(system ? { system } : {}),
|
|
339
419
|
messages,
|
|
340
420
|
...(tools && tools.length ? { tools } : {}),
|
|
341
421
|
...(options.temperature != null ? { temperature: options.temperature } : {}),
|
|
@@ -371,6 +451,11 @@ export function googleContents(options) {
|
|
|
371
451
|
const parts = []
|
|
372
452
|
const text = flattenText(message.content)
|
|
373
453
|
if (text) parts.push({ text })
|
|
454
|
+
const images = extractImages(message.content)
|
|
455
|
+
for (const img of images) {
|
|
456
|
+
const blk = toGeminiImage(img)
|
|
457
|
+
if (blk) parts.push(blk)
|
|
458
|
+
}
|
|
374
459
|
const calls = [
|
|
375
460
|
...toolCalls(message.content),
|
|
376
461
|
...((message.tool_calls || message.toolCalls || []).map((c) => ({
|
|
@@ -400,7 +485,10 @@ export function googleContents(options) {
|
|
|
400
485
|
const tools = declarations ? [{ functionDeclarations: declarations }] : undefined
|
|
401
486
|
return {
|
|
402
487
|
contents,
|
|
403
|
-
...(
|
|
488
|
+
...(() => {
|
|
489
|
+
const systemText = normalizeSystemPrefix(systemParts)
|
|
490
|
+
return systemText ? { systemInstruction: { parts: [{ text: systemText }] } } : {}
|
|
491
|
+
})(),
|
|
404
492
|
...(tools ? { tools } : {}),
|
|
405
493
|
generationConfig: {
|
|
406
494
|
...(options.temperature != null ? { temperature: options.temperature } : {}),
|
|
@@ -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/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",
|