@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.
@@ -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
- if (!Array.isArray(tools) || !tools.length) return undefined
69
- return tools.map((tool) => ({
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 || { type: 'object' }),
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
- if (text || !results.length) out.push({ role: 'user', content: text })
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 (!Array.isArray(tools) || !tools.length) return undefined
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 || { type: 'object' },
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.filter(Boolean).join('\n\n') || fallbackInstructions
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 === 'system') {
302
+ if (message.role === "system") {
269
303
  systemParts.push(flattenText(message.content))
270
304
  continue
271
305
  }
272
- if (message.role === 'assistant') {
306
+ if (message.role === "assistant") {
273
307
  const blocks = []
274
308
  const text = flattenText(message.content)
275
- if (text) blocks.push({ type: 'text', text })
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: 'tool_use',
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: 'assistant', content: blocks.length ? blocks : [{ type: 'text', text: '' }] })
327
+ messages.push({ role: "assistant", content: blocks.length ? blocks : [{ type: "text", text: "" }] })
294
328
  continue
295
329
  }
296
- if (message.role === 'tool') {
297
- const toolUseId = String(message.toolCallId || message.tool_call_id || message.tool_use_id || message.id || '')
298
- const content = typeof message.content === 'string'
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: 'user',
336
+ role: "user",
303
337
  content: [{
304
- type: 'tool_result',
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: 'text', text })
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 === 'string'
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 === 'string' ? result.output : JSON.stringify(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: 'tool_result',
359
+ type: "tool_result",
321
360
  tool_use_id: toolUseId,
322
- content: content || '',
361
+ content: content || "",
323
362
  })
324
363
  }
325
- messages.push({ role: 'user', content: blocks.length ? blocks : [{ type: 'text', text: '' }] })
364
+ messages.push({ role: "user", content: blocks.length ? blocks : [{ type: "text", text: "" }] })
326
365
  }
327
- const tools = Array.isArray(options.tools)
328
- ? options.tools.map((tool) => ({
329
- name: tool.name,
330
- description: tool.description || '',
331
- input_schema: tool.parameters || { type: 'object' },
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
- ...(systemParts.length ? { system: systemParts.join('\n\n') } : {}),
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
- ...(systemParts.length ? { systemInstruction: { parts: systemParts } } : {}),
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
+ }
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.6.27",
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",