@goodandready/dsh-subscriptions 0.7.45 → 0.7.47

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,80 @@
1
+ const FAMILIES = [
2
+ { re: /claude/i, budget: 1024, effort: 'high', levels: ['low', 'medium', 'high'] },
3
+ { re: /gpt-oss/i, budget: 8192, effort: 'medium', levels: ['low', 'medium', 'high'] },
4
+ { re: /gemini-3\./i, budgets: { low: 1024, medium: 8192, high: 24576 }, effort: 'medium', levels: ['low', 'medium', 'high'] },
5
+ ]
6
+
7
+ function familyFor(model) {
8
+ return FAMILIES.find((family) => family.re.test(String(model || ''))) || null
9
+ }
10
+
11
+ function codedError(message, code) {
12
+ /** @type {Error & { code?: string }} */
13
+ const err = new Error(message)
14
+ err.code = code
15
+ return err
16
+ }
17
+
18
+ /** #826: family-specific Antigravity reasoning budget with explicit errors. */
19
+ export function antigravityReasoning(model, effort, maxTokens) {
20
+ const family = familyFor(model)
21
+ if (!family) return { supported: false, thinkingBudget: null, effort: null, maxTokens }
22
+ const requested = String(effort || family.effort).toLowerCase()
23
+ if (!family.levels.includes(requested)) {
24
+ throw codedError('unsupported reasoning effort for ' + model + ': ' + requested, 'UNSUPPORTED_REASONING_EFFORT')
25
+ }
26
+ const thinkingBudget = family.budgets ? family.budgets[requested] : family.budget
27
+ if (maxTokens != null && Number(maxTokens) <= thinkingBudget) {
28
+ throw codedError('maxTokens must exceed thinking budget', 'UNSUPPORTED_REASONING_EFFORT')
29
+ }
30
+ return { supported: true, thinkingBudget, effort: requested, maxTokens }
31
+ }
32
+
33
+ function deref(schema, root, seen) {
34
+ if (!schema || typeof schema !== 'object') return schema
35
+ if (typeof schema.$ref === 'string') {
36
+ const ref = schema.$ref
37
+ if (!ref.startsWith('#/')) {
38
+ throw codedError('remote $ref is not supported', 'UNSUPPORTED_TOOL_SCHEMA')
39
+ }
40
+ if (seen.has(ref)) {
41
+ throw codedError('cyclic $ref is not supported', 'UNSUPPORTED_TOOL_SCHEMA')
42
+ }
43
+ const path = ref.slice(2).split('/').map((part) => part.replace(/~1/g, '/').replace(/~0/g, '~'))
44
+ let target = root
45
+ for (const key of path) target = target && target[key]
46
+ if (!target) {
47
+ throw codedError('missing local $ref: ' + ref, 'UNSUPPORTED_TOOL_SCHEMA')
48
+ }
49
+ return deref(target, root, new Set([...seen, ref]))
50
+ }
51
+ return schema
52
+ }
53
+
54
+ function strip(schema, root, seen) {
55
+ if (Array.isArray(schema)) return schema.map((item) => strip(item, root, seen))
56
+ if (!schema || typeof schema !== 'object') return schema
57
+ const resolved = deref(schema, root, seen)
58
+ if (resolved !== schema) return strip(resolved, root, seen)
59
+ const out = {}
60
+ for (const [key, value] of Object.entries(schema)) {
61
+ if (key === '$schema' || key === '$id' || key === '$defs' || key === 'definitions') continue
62
+ out[key] = strip(value, root, seen)
63
+ }
64
+ if (Array.isArray(out.anyOf)) {
65
+ const variants = out.anyOf.filter((v) => v && typeof v === 'object')
66
+ if (variants.length === 1) return variants[0]
67
+ if (variants.every((v) => v.type === variants[0].type)) out.type = variants[0].type
68
+ }
69
+ if (Array.isArray(out.oneOf)) {
70
+ const variants = out.oneOf.filter((v) => v && typeof v === 'object')
71
+ if (variants.length === 1) return variants[0]
72
+ if (variants.every((v) => v.type === variants[0].type)) out.type = variants[0].type
73
+ }
74
+ return out
75
+ }
76
+
77
+ /** #826: expand local refs, reject cycles/remote refs, strip metadata, widen unions. */
78
+ export function normalizeAntigravityToolSchema(schema) {
79
+ return strip(schema, schema, new Set())
80
+ }
@@ -0,0 +1,49 @@
1
+ import { execFile } from 'node:child_process'
2
+ import { promisify } from 'node:util'
3
+
4
+ const execFileAsync = promisify(execFile)
5
+ const SERVICE = 'Claude Code-credentials'
6
+
7
+ export function validClaudeBlob(blob) {
8
+ if (!blob || typeof blob !== 'object') return false
9
+ if (typeof blob.accessToken !== 'string' || !blob.accessToken) return false
10
+ if (typeof blob.refreshToken !== 'string' || !blob.refreshToken) return false
11
+ return Number.isFinite(Number(blob.expiresAt))
12
+ }
13
+
14
+ export function mergeClaudeCredentials(current, cli) {
15
+ if (!validClaudeBlob(current)) return validClaudeBlob(cli) ? { ...cli } : null
16
+ if (!validClaudeBlob(cli)) return { ...current }
17
+ const currentExpiry = Number(current.expiresAt) || 0
18
+ const cliExpiry = Number(cli.expiresAt) || 0
19
+ if (cli.accessToken && cli.accessToken !== current.accessToken && cliExpiry > currentExpiry) {
20
+ return { ...current, ...cli }
21
+ }
22
+ return { ...current }
23
+ }
24
+
25
+ export function canWriteClaudeKeychain(currentBlob, expectedAccessToken) {
26
+ return Boolean(currentBlob && expectedAccessToken && currentBlob.accessToken === expectedAccessToken)
27
+ }
28
+
29
+ /** macOS-only read; returns null on any failure so accounts stay usable. */
30
+ export async function readClaudeKeychain({ exec = execFileAsync, service = SERVICE } = {}) {
31
+ if (process.platform !== 'darwin') return null
32
+ try {
33
+ const { stdout } = await exec('/usr/bin/security', ['find-generic-password', '-s', service, '-w'])
34
+ const blob = JSON.parse(String(stdout || '').trim())
35
+ return validClaudeBlob(blob) ? blob : null
36
+ } catch { return null }
37
+ }
38
+
39
+ /** Write back only when the stored blob still owns the expected old access token. */
40
+ export async function writeClaudeKeychain(blob, expectedAccessToken, { exec = execFileAsync, service = SERVICE, account = 'claude' } = {}) {
41
+ if (process.platform !== 'darwin') return false
42
+ if (!validClaudeBlob(blob) || !expectedAccessToken) return false
43
+ const current = await readClaudeKeychain({ exec, service })
44
+ if (!canWriteClaudeKeychain(current, expectedAccessToken)) return false
45
+ try {
46
+ await exec('/usr/bin/security', ['add-generic-password', '-U', '-s', service, '-a', account, '-w', JSON.stringify(blob)])
47
+ return true
48
+ } catch { return false }
49
+ }
package/lib/client.js CHANGED
@@ -300,6 +300,10 @@ window.__ModuleLoader__.load({
300
300
  "discovered": "discovered",
301
301
  "shown": "shown",
302
302
  "searchModels": "Search models by name or id",
303
+ "compact": "Compact",
304
+ "heatmap": "Heatmap · last 24h",
305
+ "editModels": "Edit models",
306
+ "hidden": "hidden",
303
307
  "tabRouting": "Routing",
304
308
  "tabModels": "Models",
305
309
  "tabVault": "Vault",
@@ -725,6 +729,10 @@ window.__ModuleLoader__.load({
725
729
  "discovered": "已发现",
726
730
  "shown": "已显示",
727
731
  "searchModels": "按名称或 ID 搜索模型",
732
+ "compact": "紧凑模式",
733
+ "heatmap": "热力图 · 最近 24 小时",
734
+ "editModels": "编辑模型",
735
+ "hidden": "隐藏",
728
736
  "tabRouting": "路由",
729
737
  "tabModels": "模型",
730
738
  "tabVault": "Vault",
@@ -191,16 +191,39 @@ window.__ModuleLoader__.load({
191
191
  const [sort, setSort] = React.useState('risk')
192
192
  const [usage, setUsage] = React.useState({})
193
193
  const [speed, setSpeed] = React.useState('standard')
194
+ const [analytics, setAnalytics] = React.useState(null)
195
+ const [compact, setCompact] = React.useState(false)
196
+ const [editing, setEditing] = React.useState(false)
197
+ const [draft, setDraft] = React.useState({})
194
198
  const usageRef = React.useRef({ inflight: {}, gen: {} })
195
199
  const pull = () => fetch('/dsh-subscriptions/config', { cache: 'no-store' })
196
200
  .then((res) => res.json())
197
201
  .then((cfg) => {
198
202
  setData(cfg)
199
203
  setSpeed((cfg.config && cfg.config.codexSubagentSpeed) || 'standard')
204
+ setDraft(JSON.parse(JSON.stringify((cfg.config && cfg.config.modelOverrides) || {})))
200
205
  if (!provider && cfg.accounts && cfg.accounts.length) setProvider(cfg.accounts[0].provider)
201
206
  })
202
207
  .catch(() => {})
203
- React.useEffect(() => { pull() }, [])
208
+ React.useEffect(() => {
209
+ pull()
210
+ fetch('/dsh-subscriptions/analytics', { cache: 'no-store' })
211
+ .then((res) => res.json())
212
+ .then((out) => setAnalytics((out && out.analytics) || null))
213
+ .catch(() => {})
214
+ }, [])
215
+ const saveOverrides = () => {
216
+ if (!data || !data.config) return
217
+ fetch('/dsh-subscriptions/config', {
218
+ method: 'PUT',
219
+ headers: { 'content-type': 'application/json' },
220
+ body: JSON.stringify({ config: Object.assign({}, data.config, { modelOverrides: draft }), revision: data.revision }),
221
+ }).then(() => { setEditing(false); pull() }).catch(() => {})
222
+ }
223
+ const cancelOverrides = () => {
224
+ setDraft(JSON.parse(JSON.stringify((data && data.config && data.config.modelOverrides) || {})))
225
+ setEditing(false)
226
+ }
204
227
  const loadUsage = (row) => {
205
228
  const key = row.provider + ':' + row.index
206
229
  if (usageRef.current.inflight[key]) return
@@ -264,10 +287,53 @@ window.__ModuleLoader__.load({
264
287
  u && u.value ? h('span', { className: 'dsub-dim' }, JSON.stringify(u.value).slice(0, 180)) : null),
265
288
  )
266
289
  }),
267
- h('div', { className: 'dsub-h' }, t('modelEditor') + ' · ' + t('discovered') + ' ' + models.length + ' · ' + t('shown') + ' ' + shown.length),
290
+ h('div', { className: 'dsub-row' },
291
+ h('label', { className: 'dsub-sub' }, h('input', { type: 'checkbox', checked: compact, onChange: (e) => setCompact(e.target.checked) }), ' ' + t('compact')),
292
+ ),
293
+ h('div', { className: 'dsub-h' }, t('heatmap')),
294
+ h('div', { className: 'dsub-row', style: { gap: 2 } },
295
+ ((analytics && analytics.hourly) || []).slice(-24).map((hour, i) => h('span', {
296
+ key: i,
297
+ title: (hour && hour.requests || 0) + ' req',
298
+ style: { display: 'inline-block', width: 8, height: 18, background: 'rgba(88,166,255,' + Math.min(1, ((hour && hour.requests) || 0) / 10 + 0.1) + ')' },
299
+ }))),
300
+ h('div', { className: 'dsub-row' },
301
+ h('span', { className: 'dsub-h' }, t('modelEditor') + ' · ' + t('discovered') + ' ' + models.length + ' · ' + t('shown') + ' ' + shown.length),
302
+ h('button', { type: 'button', className: 'dsub-mini', onClick: () => setEditing(!editing) }, t('editModels')),
303
+ editing ? h('button', { type: 'button', className: 'dsub-mini', onClick: saveOverrides }, t('save')) : null,
304
+ editing ? h('button', { type: 'button', className: 'dsub-mini', onClick: cancelOverrides }, t('cancel')) : null,
305
+ ),
268
306
  h('input', { className: 'dsub-grow', value: query, placeholder: t('searchModels'), onChange: (e) => setQuery(e.target.value) }),
269
307
  shown.slice(0, 40).map((m) => h('div', { key: m.id, className: 'dsub-row' },
270
- h('span', { className: 'dsub-h' }, m.independent ? '★ ' : ''), h('span', { className: 'dsub-sub' }, m.name || m.id), h('span', { className: 'dsub-dim' }, m.id))),
308
+ h('span', { className: 'dsub-h' }, m.independent ? '★ ' : ''),
309
+ editing
310
+ ? h('input', {
311
+ className: 'dsub-grow',
312
+ value: (draft[m.id] && draft[m.id].name) || '',
313
+ placeholder: m.name || m.id,
314
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { [m.id]: Object.assign({}, d[m.id], { name: e.target.value }) })),
315
+ })
316
+ : h('span', { className: 'dsub-sub' }, (draft[m.id] && draft[m.id].name) || m.name || m.id),
317
+ editing ? h('input', {
318
+ type: 'number',
319
+ className: 'dsub-inp',
320
+ style: { width: '90px', minWidth: '90px' },
321
+ value: (draft[m.id] && draft[m.id].contextWindow) || '',
322
+ placeholder: 'ctx',
323
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { [m.id]: Object.assign({}, d[m.id], { contextWindow: Number(e.target.value) || undefined }) })),
324
+ }) : null,
325
+ editing ? h('select', {
326
+ className: 'dsub-select',
327
+ value: (draft[m.id] && draft[m.id].reasoningEffort) || '',
328
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { [m.id]: Object.assign({}, d[m.id], { reasoningEffort: e.target.value || undefined }) })),
329
+ }, ['', 'low', 'medium', 'high'].map((level) => h('option', { key: level || 'default', value: level }, level || 'effort'))) : null,
330
+ editing ? h('label', { className: 'dsub-dim' },
331
+ h('input', {
332
+ type: 'checkbox',
333
+ checked: !!(draft[m.id] && draft[m.id].hidden),
334
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { [m.id]: Object.assign({}, d[m.id], { hidden: e.target.checked }) })),
335
+ }), ' ' + t('hidden')) : null,
336
+ h('span', { className: 'dsub-dim' }, m.id))),
271
337
  )
272
338
  }
273
339
 
@@ -171,6 +171,8 @@ export const Config = z.object({
171
171
  .description('#788 Per-provider model glob rules. First match wins; account may be a ref, label or index.'),
172
172
  tiers: z.array(z.any()).volatile().default([])
173
173
  .description('#817 Named picker tiers: { id, name, members: [accountRef] }. No cross-tier fallback.'),
174
+ modelOverrides: z.any().volatile().default({})
175
+ .description('#807 Per-model editor overrides: display name, hidden, context window, default reasoning effort.'),
174
176
  rotationStrategy: z.string().volatile().default('round-robin')
175
177
  .description('Load balancing strategy among available accounts: round-robin (balanced), least-loaded (fair-share quota), quota-aware (urgent quota first), or priority (sequential).'),
176
178
  poolEnabled: z.boolean().volatile().default(true)
@@ -3,6 +3,7 @@ import { readFile } from 'node:fs/promises'
3
3
  import { homedir } from 'node:os'
4
4
  import { join } from 'node:path'
5
5
  import { readClaudeCredentials } from './vendors/claude-cli.js'
6
+ import { readClaudeKeychain } from './claude-keychain.js'
6
7
  import { readCopilotCredentials } from './vendors/copilot-cli.js'
7
8
  import { normalizeExpiresAt } from './blob.js'
8
9
  import { isGoogleClientId, extractGoogleClientIdFromJwt, emailFromToken } from './jwt.js'
@@ -250,11 +251,17 @@ async function discoverLocalCliSessionsInner() {
250
251
  }
251
252
 
252
253
  // 8. Claude CLI Direct Import
253
- const claudeCreds = await readClaudeCredentials(activeHome)
254
+ const fileCreds = await readClaudeCredentials(activeHome)
255
+ const keychainCreds = fileCreds ? null : await readClaudeKeychain()
256
+ const claudeCreds = fileCreds || (keychainCreds ? {
257
+ ...keychainCreds,
258
+ sourcePath: 'macOS Keychain (Claude Code-credentials)',
259
+ email: keychainCreds.email || '',
260
+ } : null)
254
261
  if (claudeCreds) {
255
262
  const claudeEntry = {
256
263
  provider: 'claude',
257
- source: 'file',
264
+ source: fileCreds ? 'file' : 'keychain',
258
265
  path: claudeCreds.sourcePath || homeFile('.claude', '.credentials.json'),
259
266
  email: claudeCreds.email || 'Claude Code CLI User',
260
267
  hasRefreshToken: !!claudeCreds.refreshToken,
@@ -0,0 +1,38 @@
1
+ export const MAX_REFERENCE_IMAGES = 5
2
+
3
+ /** #801: validate attachment refs before any generation call. */
4
+ export function validateReferenceImages(refs) {
5
+ if (refs == null) return { ok: true, refs: [] }
6
+ if (!Array.isArray(refs)) {
7
+ return { ok: false, error: 'referenceImages must be an array' }
8
+ }
9
+ const clean = refs.map((ref) => String(ref || '').trim()).filter(Boolean)
10
+ if (!clean.length) return { ok: false, error: 'referenceImages must not be empty' }
11
+ if (clean.length > MAX_REFERENCE_IMAGES) return { ok: false, error: 'at most 5 reference images are allowed' }
12
+ if (new Set(clean).size !== clean.length) return { ok: false, error: 'duplicate reference images are not allowed' }
13
+ return { ok: true, refs: clean }
14
+ }
15
+
16
+ /** #801: provider-specific image-edit request bodies. */
17
+ export function referenceImageRequest(provider, { prompt, references = [], model, size } = {}) {
18
+ const checked = validateReferenceImages(references)
19
+ if (!checked.ok) {
20
+ const err = new Error(checked.error)
21
+ err.code = 'INVALID_MEDIA_REFERENCE'
22
+ throw err
23
+ }
24
+ if (provider === 'grok') {
25
+ return {
26
+ model: model || 'grok-2-image',
27
+ prompt,
28
+ image_urls: checked.refs,
29
+ ...(size ? { size } : {}),
30
+ }
31
+ }
32
+ return {
33
+ model: model || 'gpt-image-1',
34
+ prompt,
35
+ images: checked.refs.map((ref) => ({ image_url: ref })),
36
+ ...(size ? { size } : {}),
37
+ }
38
+ }
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto'
2
2
  import { buildAuthorizeUrl } from '../oauth.js'
3
3
  import { googleContents } from '../messages.js'
4
+ import { antigravityReasoning, normalizeAntigravityToolSchema } from '../antigravity-reasoning.js'
4
5
  import { formTokenRequest, googleStream, throwHttpError, tokenBlobFromOAuth , readErrorBody } from '../wire.js'
5
6
  import { fetchWithTimeout } from '../http.js'
6
7
  import { emailFromToken, avatarFromToken, scopesFromToken } from '../jwt.js'
@@ -221,6 +222,17 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, signal, s
221
222
  if (tiered) {
222
223
  if (!request.generationConfig) request.generationConfig = {}
223
224
  request.generationConfig.thinkingConfig = /** @type {any} */ ({ thinkingLevel: tiered.thinkingLevel })
225
+ } else {
226
+ const reasoning = antigravityReasoning(options.model, options.reasoningEffort, options.maxTokens)
227
+ if (reasoning.supported) {
228
+ if (!request.generationConfig) request.generationConfig = {}
229
+ request.generationConfig.thinkingConfig = /** @type {any} */ ({ thinkingBudget: reasoning.thinkingBudget })
230
+ }
231
+ }
232
+ for (const declaration of (request.tools && request.tools[0] && request.tools[0].functionDeclarations) || []) {
233
+ if (declaration && declaration.parameters) {
234
+ declaration.parameters = normalizeAntigravityToolSchema(declaration.parameters)
235
+ }
224
236
  }
225
237
  const { envelope, sessionId } = streamEnvelope({
226
238
  projectId: ready.projectId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.7.45",
3
+ "version": "0.7.47",
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",