@goodandready/dsh-subscriptions 0.7.36 → 0.7.38

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 CHANGED
@@ -59,8 +59,15 @@ export class SubscriptionAdapter extends LlmAdapter {
59
59
  const builtin = modelCatalog(provider, cfg ? cfg.models : undefined)
60
60
  const targetAccount = accounts.find((a) => a.hasToken)
61
61
  let targetLive = null
62
+ const targetCached = targetAccount && typeof this.deps.getDynamicModels === 'function'
63
+ ? this.deps.getDynamicModels(targetAccount.ref)
64
+ : null
62
65
 
63
- if (targetAccount) {
66
+ // #27: resolution runs on every request. Do not hit live metadata when a
67
+ // last-known-good per-account snapshot already exists; background refresh
68
+ // updates it without turning a temporary metadata outage into a capability
69
+ // verdict.
70
+ if (targetAccount && !(Array.isArray(targetCached) && targetCached.length)) {
64
71
  const baseFetch = pickFetch(this.deps, targetAccount.ref)
65
72
  const listModelsFetch = (url, init, opts) =>
66
73
  fetchWithTimeout(baseFetch, url, init, { timeoutMs: 15000, ...(opts || {}) })
@@ -253,6 +253,8 @@ export const Config = z.object({
253
253
  .description('#694 Data-only vendor packs registered through the provider SDK.'),
254
254
  nativeTools: z.boolean().volatile().default(false)
255
255
  .description('#18 Opt-in subscription-native image/x/web/video tools. Off by default because these names shadow builtin tools; enable only when the host does not provide them.'),
256
+ toolPolicies: z.any().volatile().default({})
257
+ .description('#809 Per-provider tool policy map: { provider: { web, image, video, search, tools } }.'),
256
258
  })
257
259
 
258
260
  export function publicConfig(cfg) {
@@ -0,0 +1,48 @@
1
+ function statusOf(err) {
2
+ return Number(err && (err.status || err.statusCode)) || 0
3
+ }
4
+
5
+ /** #822: do not retry ambiguous image/video failures. */
6
+ export function classifyMediaFailure(err) {
7
+ const status = statusOf(err)
8
+ const code = String(err && err.code || '').toUpperCase()
9
+ if (code === 'ABORT_ERR' || code === 'CANCELED' || code === 'CANCELLED') return 'cancelled'
10
+ if (code === 'AMBIGUOUS' || code === 'TIMEOUT_AFTER_SUBMIT') return 'ambiguous'
11
+ if (status === 401 || status === 403 || status === 400 || status === 422) return 'fatal'
12
+ if (status === 429 || status === 408 || status >= 500) return 'retryable'
13
+ if (code === 'TIMEOUT' || code === 'EMPTY_RESPONSE' || code === 'NETWORK') return 'retryable'
14
+ return 'fatal'
15
+ }
16
+
17
+ export function shouldRetryMedia(err, attempt, maxRetries = 0) {
18
+ if (Number(attempt) >= Number(maxRetries)) return false
19
+ return classifyMediaFailure(err) === 'retryable'
20
+ }
21
+
22
+ /** #800: conservative media-pool selection. */
23
+ export function selectMediaAccount(accounts, { nowMs = Date.now(), defaultRef = null, exclude = [] } = {}) {
24
+ const skip = new Set(exclude)
25
+ const usable = (Array.isArray(accounts) ? accounts : []).filter((account) => {
26
+ if (!account || !account.hasToken || skip.has(account.ref)) return false
27
+ if (account.poolEnabled === false) return false
28
+ if (account.cooldownUntil && Number(account.cooldownUntil) > nowMs) return false
29
+ if (account.quarantineUntil && Number(account.quarantineUntil) > nowMs) return false
30
+ if (account.usagePercent != null && Number(account.usagePercent) >= 100) return false
31
+ return true
32
+ })
33
+ if (!usable.length) return null
34
+ return usable.find((account) => account.ref === defaultRef) || usable[0]
35
+ }
36
+
37
+ /** #809: per-provider tool policy. */
38
+ export function resolveToolPolicy(config, provider) {
39
+ const defaults = { web: true, image: true, video: true, search: true, tools: true }
40
+ const all = config && config.toolPolicies
41
+ const row = all && typeof all === 'object' ? all[provider] : null
42
+ if (!row || typeof row !== 'object') return defaults
43
+ return Object.fromEntries(Object.entries(defaults).map(([key, value]) => [key, row[key] === undefined ? value : Boolean(row[key])]))
44
+ }
45
+
46
+ export function toolAllowed(config, provider, kind) {
47
+ return resolveToolPolicy(config, provider)[kind] !== false
48
+ }
@@ -0,0 +1,22 @@
1
+ /** #835/#836: safe provider icon candidate with deterministic fallback. */
2
+ export function providerInitials(provider) {
3
+ const text = String(provider || '').trim()
4
+ if (!text) return '?'
5
+ return text.slice(0, 2).toUpperCase()
6
+ }
7
+
8
+ export function safeProviderIconUrl(raw) {
9
+ const value = String(raw || '').trim()
10
+ if (!value) return ''
11
+ try {
12
+ const url = new URL(value)
13
+ if (url.protocol === 'http:' || url.protocol === 'https:') return url.href
14
+ if (url.protocol === 'data:' && /^data:image\/(?:png|jpe?g|gif|webp|svg\+xml)[;,]/i.test(value)) return value
15
+ } catch { /* fall through */ }
16
+ return ''
17
+ }
18
+
19
+ export function resolveProviderIcon({ provider, iconUrl, hostIconUrl } = {}) {
20
+ const safe = safeProviderIconUrl(iconUrl) || safeProviderIconUrl(hostIconUrl)
21
+ return { url: safe, fallback: providerInitials(provider), source: safe ? 'url' : 'initials' }
22
+ }
@@ -97,6 +97,11 @@ export function getModelReasoningCapabilities(provider, modelId, catalog = []) {
97
97
  if (id.includes('reasoning') || id === 'grok-3-mini') {
98
98
  return { supported: true, levels: ['low', 'medium', 'high'], defaultEffort: 'medium' }
99
99
  }
100
+ // #27: grok-4.x supports reasoning effort even when the best-effort CLI
101
+ // metadata endpoint is temporarily unreachable.
102
+ if (/^grok-4(?:[.-]|$)/i.test(id)) {
103
+ return { supported: true, levels: ['low', 'medium', 'high'], defaultEffort: 'high' }
104
+ }
100
105
  return { supported: false, levels: [] }
101
106
  }
102
107
 
@@ -43,18 +43,37 @@ async function cliCatalogCached(blob, cfg, fetchImpl) {
43
43
  const now = Date.now()
44
44
  const hit = catalogCache.get(key)
45
45
  if (hit && now - hit.at < CATALOG_TTL_MS) return hit.map
46
- if (hit) catalogCache.delete(key)
47
- const map = await cliCatalog(blob, cfg, fetchImpl)
48
- if (map.size > 0) {
49
- catalogCache.set(key, { at: now, map })
50
- pruneCatalogCache(now)
46
+ try {
47
+ const map = await cliCatalog(blob, cfg, fetchImpl)
48
+ if (map.size > 0) {
49
+ catalogCache.set(key, { at: now, map })
50
+ pruneCatalogCache(now)
51
+ return map
52
+ }
53
+ // An empty response must not erase the last-known-good metadata.
54
+ if (hit) return hit.map
55
+ return map
56
+ } catch (err) {
57
+ if (hit) return hit.map
58
+ throw err
59
+ }
60
+ }
61
+
62
+ function grokFallbackReasoning(modelId) {
63
+ if (!/^grok-4(?:[.-]|$)/i.test(String(modelId || ''))) return undefined
64
+ return {
65
+ efforts: [
66
+ { id: 'high', name: 'High' },
67
+ { id: 'medium', name: 'Medium' },
68
+ { id: 'low', name: 'Low' },
69
+ ],
70
+ defaultEffort: 'high',
51
71
  }
52
- return map
53
72
  }
54
73
 
55
74
  function grokReasoningBody(modelId, effort, catalogMap) {
56
75
  const entry = catalogMap && catalogMap.get(modelId)
57
- const reasoning = entry && entry.reasoning
76
+ const reasoning = (entry && entry.reasoning) || grokFallbackReasoning(modelId)
58
77
  if (!reasoning || !reasoning.efforts || !reasoning.efforts.length) return undefined
59
78
  const allowed = new Set(reasoning.efforts.map((row) => row.id))
60
79
  const pick = effort != null && effort !== '' ? String(effort) : ''
@@ -221,7 +240,7 @@ export async function listModels(blob, cfg, fetchImpl) {
221
240
  try {
222
241
  const [res, extra] = await Promise.all([
223
242
  impl(MODELS, { headers: { Authorization: `Bearer ${blob.accessToken}`, Accept: 'application/json' } }),
224
- cliCatalog(blob, cfg, impl).catch(() => new Map()),
243
+ cliCatalogCached(blob, cfg, impl).catch(() => new Map()),
225
244
  ])
226
245
  const json = await readJson(res)
227
246
  const rows = []
@@ -229,6 +248,7 @@ export async function listModels(blob, cfg, fetchImpl) {
229
248
  const modelId = entry && entry.id
230
249
  if (!modelId || !isChatModel(modelId)) continue
231
250
  const enrich = extra.get(modelId) || {}
251
+ const reasoning = enrich.reasoning || grokFallbackReasoning(modelId)
232
252
  const note = /multi-agent/i.test(modelId)
233
253
  ? 'Chat only in Harness until xAI grants multi-agent tool beta.'
234
254
  : ''
@@ -237,6 +257,7 @@ export async function listModels(blob, cfg, fetchImpl) {
237
257
  name: enrich.name || modelId,
238
258
  inputModalities: grokModalities(modelId),
239
259
  ...enrich,
260
+ ...(reasoning ? { reasoning } : {}),
240
261
  ...(note ? { description: enrich.description ? `${enrich.description} ${note}` : note } : {}),
241
262
  })
242
263
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.7.36",
3
+ "version": "0.7.38",
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",