@goodandready/dsh-subscriptions 0.7.40 → 0.7.42

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.
@@ -168,6 +168,7 @@ export async function createAccountsView({ live, store, fetch, pmL, pmE, concurr
168
168
  availableModels: info.availableModels || null,
169
169
  poolEnabled: slot.poolEnabled !== false,
170
170
  isDefault: !!slot.isDefault,
171
+ proxyBypass: Array.isArray(slot.proxyBypass) ? slot.proxyBypass : [],
171
172
  ...(Array.isArray(slot.poolModels) ? { poolModels: slot.poolModels } : {}),
172
173
  credentialError: info.credentialError || null,
173
174
  duplicateOf: info.duplicateOf || null,
package/lib/accounts.js CHANGED
@@ -50,6 +50,7 @@ import { accountIdentityKey, credentialState } from './account-reliability.js'
50
50
  import { recordSwitch, recordExhaust, recordBroken, computeHealthScore, healthBadge } from './health.js'
51
51
  import { executeWithRetry } from './backoff.js'
52
52
  import { getVendor } from './vendors/index.js'
53
+ import { versionForProvider } from './client-version-service.js'
53
54
 
54
55
  const SKEW_MS = 60 * 1000
55
56
  const USAGE_TTL_MS = 2 * 60 * 1000
@@ -74,6 +75,7 @@ export function normalizeSlots(slots) {
74
75
  ...(slot.proxyUrl ? { proxyUrl: String(slot.proxyUrl) } : {}),
75
76
  ...(slot.expiresAt ? { expiresAt: Number(slot.expiresAt) } : {}),
76
77
  ...(slot.maxConcurrency !== undefined ? { maxConcurrency: Number(slot.maxConcurrency) } : {}),
78
+ ...(Array.isArray(slot.proxyBypass) ? { proxyBypass: slot.proxyBypass.map((host) => String(host).trim()).filter(Boolean) } : {}),
77
79
  ...(slot.poolEnabled === false ? { poolEnabled: false } : {}),
78
80
  ...(slot.isDefault ? { isDefault: true } : {}),
79
81
  ...(Array.isArray(slot.poolModels)
@@ -98,6 +100,8 @@ export function vendorConfig(provider, cfg) {
98
100
  return String(value).trim()
99
101
  }
100
102
  const modelsKey = `${provider}Models`
103
+ const manualVersion = provider === 'claude' ? pick('CcVersion', '') : pick('ClientVersion', '')
104
+ const resolvedVersion = versionForProvider(provider, manualVersion, d.clientVersion || d.claudeCcVersion || '')
101
105
  return {
102
106
  clientId: pick('ClientId', d.clientId),
103
107
  clientSecret: pick('ClientSecret', d.clientSecret || ''),
@@ -105,7 +109,8 @@ export function vendorConfig(provider, cfg) {
105
109
  baseUrl: pick('BaseUrl', d.baseUrl || ''),
106
110
  originator: pick('Originator', d.originator || ''),
107
111
  systemPrefix: pick('SystemPrefix', d.systemPrefix || ''),
108
- clientVersion: pick('ClientVersion', d.clientVersion || ''),
112
+ clientVersion: resolvedVersion.version,
113
+ ...(provider === 'claude' ? { claudeCcVersion: resolvedVersion.version } : {}),
109
114
  // #93/#92: per-vendor request shaping (codex only consumes these today).
110
115
  verbosity: pick('Verbosity', ''),
111
116
  fastMode: !!(cfg && cfg[`${provider}FastMode`]),
@@ -427,6 +432,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
427
432
  isPro: !!slot.isPro,
428
433
  poolEnabled: slot.poolEnabled !== false,
429
434
  isDefault: !!slot.isDefault,
435
+ proxyBypass: Array.isArray(slot.proxyBypass) ? slot.proxyBypass : [],
430
436
  ...(Array.isArray(slot.poolModels) ? { poolModels: slot.poolModels } : {}),
431
437
  credentialError: info.credentialError || null,
432
438
  lastUsedAt: lastUsedAt.get(slot.ref) || 0,
@@ -0,0 +1,73 @@
1
+ import { execFileSync } from 'node:child_process'
2
+ import { resolveClientVersion } from './client-version.js'
3
+
4
+ const PACKAGES = { codex: '@openai/codex', claude: '@anthropic-ai/claude-code' }
5
+ const BINARIES = { codex: 'codex', claude: 'claude' }
6
+ const TTL_MS = 6 * 60 * 60 * 1000
7
+ const state = {
8
+ codex: { npm: '', local: '', at: 0 },
9
+ claude: { npm: '', local: '', at: 0 },
10
+ }
11
+ let inflight = null
12
+
13
+ function npmMetaUrl(name) {
14
+ return 'https://registry.npmjs.org/' + encodeURIComponent(name) + '/latest'
15
+ }
16
+
17
+ function localVersion(binary) {
18
+ try {
19
+ const out = execFileSync(binary, ['--version'], { encoding: 'utf8', timeout: 1500, stdio: ['ignore', 'pipe', 'ignore'] })
20
+ const m = /\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/.exec(String(out))
21
+ return m ? m[0] : ''
22
+ } catch { return '' }
23
+ }
24
+
25
+ async function npmVersion(name, fetchImpl, signal) {
26
+ try {
27
+ const res = await fetchImpl(npmMetaUrl(name), { headers: { Accept: 'application/json' }, signal })
28
+ if (!res.ok) return ''
29
+ const json = await res.json()
30
+ return typeof json?.version === 'string' ? json.version : ''
31
+ } catch { return '' }
32
+ }
33
+
34
+ export function clientVersionInfo(fallbacks = {}, configs = {}) {
35
+ const out = {}
36
+ for (const provider of Object.keys(PACKAGES)) {
37
+ const npm = state[provider].npm
38
+ const local = state[provider].local
39
+ out[provider] = resolveClientVersion({
40
+ configValue: configs[provider] || '',
41
+ npmVersion: npm,
42
+ localVersion: local,
43
+ fallback: fallbacks[provider] || '',
44
+ })
45
+ }
46
+ return out
47
+ }
48
+
49
+ /** @param {{ fetchImpl?: typeof fetch, force?: boolean, signal?: AbortSignal, localVersionFn?: (binary: string) => string }} [options] */
50
+ export async function refreshClientVersions({ fetchImpl = fetch, force = false, signal, localVersionFn = localVersion } = {}) {
51
+ const now = Date.now()
52
+ if (!force && Object.values(state).every((row) => row.at && now - row.at < TTL_MS)) return clientVersionInfo()
53
+ if (inflight) return inflight
54
+ inflight = (async () => {
55
+ for (const [provider, name] of Object.entries(PACKAGES)) {
56
+ const npm = await npmVersion(name, fetchImpl, signal)
57
+ const local = localVersionFn(BINARIES[provider])
58
+ state[provider] = { npm, local, at: now }
59
+ }
60
+ return clientVersionInfo()
61
+ })().finally(() => { inflight = null })
62
+ return inflight
63
+ }
64
+
65
+ export function resetClientVersionCache() {
66
+ for (const key of Object.keys(state)) state[key] = { npm: '', local: '', at: 0 }
67
+ inflight = null
68
+ }
69
+
70
+ export function versionForProvider(provider, configValue, fallback) {
71
+ const cached = state[provider] || {}
72
+ return resolveClientVersion({ configValue, npmVersion: cached.npm, localVersion: cached.local, fallback })
73
+ }
@@ -1,4 +1,6 @@
1
- /** #798: choose a client version with deterministic source precedence. */
1
+ /** #798: choose a client version with deterministic source precedence.
2
+ * @param {{ configValue?: string, npmVersion?: string, localVersion?: string, fallback?: string }} [options]
3
+ */
2
4
  export function resolveClientVersion({ configValue, npmVersion, localVersion, fallback } = {}) {
3
5
  const values = [
4
6
  ['config', configValue],
package/lib/client.js CHANGED
@@ -249,6 +249,7 @@ window.__ModuleLoader__.load({
249
249
  "subsLogged": "connected",
250
250
  "subsNotLogged": "not connected",
251
251
  "subsCooldown": "cooldown",
252
+ "clientVersions": "Client versions",
252
253
  "settingsLoading": "Loading settings…",
253
254
  "settingsUnavailable": "Settings are unavailable",
254
255
  "settingsRetry": "Retry",
@@ -264,6 +265,7 @@ window.__ModuleLoader__.load({
264
265
  "pastePlaceholder": "Paste redirected URL or code",
265
266
  "submitCode": "Submit code",
266
267
  "proxyPlaceholder": "Per-account proxy (http://, https://, socks5://) - optional",
268
+ "proxyBypassPlaceholder": "Proxy bypass hosts (comma-separated)",
267
269
  "proxyCheck": "Check proxy",
268
270
  "proxyOk": "proxy ok",
269
271
  "proxyFail": "proxy fail",
@@ -655,6 +657,7 @@ window.__ModuleLoader__.load({
655
657
  "subsLogged": "已连接",
656
658
  "subsNotLogged": "未连接",
657
659
  "subsCooldown": "冷却中",
660
+ "clientVersions": "客户端版本",
658
661
  "settingsLoading": "正在加载设置…",
659
662
  "settingsUnavailable": "设置加载失败",
660
663
  "settingsRetry": "重试",
@@ -670,6 +673,7 @@ window.__ModuleLoader__.load({
670
673
  "pastePlaceholder": "粘贴重定向 URL 或授权代码",
671
674
  "submitCode": "提交授权代码",
672
675
  "proxyPlaceholder": "单账户独立代理 (http://, https://, socks5://) - 可选",
676
+ "proxyBypassPlaceholder": "代理绕过主机(逗号分隔)",
673
677
  "proxyCheck": "测试代理",
674
678
  "proxyOk": "代理连接成功",
675
679
  "proxyFail": "代理连接失败",
@@ -1101,6 +1105,7 @@ window.__ModuleLoader__.load({
1101
1105
  const [accounts, setAccounts] = React.useState([])
1102
1106
  const [providers, setProviders] = React.useState([])
1103
1107
  const [modelsCatalog, setModelsCatalog] = React.useState([])
1108
+ const [clientVersions, setClientVersions] = React.useState(null)
1104
1109
  const [modelQuery, setModelQuery] = React.useState('')
1105
1110
  const [modelSort, setModelSort] = React.useState('name')
1106
1111
  const [modelLimit, setModelLimit] = React.useState(40)
@@ -1488,6 +1493,7 @@ window.__ModuleLoader__.load({
1488
1493
  setAccounts((data && data.accounts) || [])
1489
1494
  setProviders((data && data.providers) || [])
1490
1495
  setModelsCatalog((data && data.modelsCatalog) || [])
1496
+ setClientVersions((data && data.clientVersions) || null)
1491
1497
  }
1492
1498
 
1493
1499
  let reloadSeq = 0
@@ -1967,6 +1973,14 @@ window.__ModuleLoader__.load({
1967
1973
  className: updater.message.includes(t('updateSuccessRestart')) ? 'dsub-alert-ok' : 'dsub-alert-bad',
1968
1974
  style: { marginBottom: '12px' }
1969
1975
  }, updater.message) : null,
1976
+ clientVersions ? h('div', {
1977
+ 'data-pane': 'overview',
1978
+ className: 'dsub-sub',
1979
+ style: { marginBottom: '10px' },
1980
+ }, t('clientVersions') + ': ' + ['codex', 'claude'].map((provider) => {
1981
+ const row = clientVersions[provider]
1982
+ return provider + ' ' + (row && row.version ? row.version : '?') + ' (' + (row && row.source || 'none') + ')'
1983
+ }).join(' · ')) : null,
1970
1984
  smoke.result ? h('div', {
1971
1985
  'data-pane': 'overview',
1972
1986
  className: smoke.result.ok ? 'dsub-alert-ok' : 'dsub-alert-bad',
@@ -3222,6 +3236,16 @@ h('div', { className: 'dsub-row' },
3222
3236
  updateSlot(row.slot.provider, row.slot.index, { proxyUrl: e.target.value })
3223
3237
  },
3224
3238
  }),
3239
+ h('input', {
3240
+ className: 'dsub-grow',
3241
+ value: (Array.isArray(row.slot.proxyBypass) ? row.slot.proxyBypass : []).join(','),
3242
+ placeholder: t('proxyBypassPlaceholder'),
3243
+ onChange: (e) => {
3244
+ updateSlot(row.slot.provider, row.slot.index, {
3245
+ proxyBypass: e.target.value.split(',').map((host) => host.trim()).filter(Boolean),
3246
+ })
3247
+ },
3248
+ }),
3225
3249
  h('button', {
3226
3250
  type: 'button', className: 'dsub-mini',
3227
3251
  disabled: checking['proxy:' + key],
package/lib/compat.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /** #816: normalize host API shape differences in one place. */
2
2
  export function toolCallId(value) {
3
3
  if (!value || typeof value !== 'object') return ''
4
- return String(value.toolCallId || value.tool_call_id || value.callId || value.call_id || value.id || '')
4
+ return String(value.toolCallId || value.tool_call_id || value.toolUseId || value.tool_use_id || value.callId || value.call_id || value.id || '')
5
5
  }
6
6
 
7
7
  export function normalizeRpcResult(value) {
@@ -14,6 +14,7 @@ export function normalizeRpcResult(value) {
14
14
 
15
15
  export function hostCompatibilityError(detail) {
16
16
  const suffix = detail ? `: ${String(detail)}` : ''
17
+ /** @type {Error & { code?: string }} */
17
18
  const err = new Error(`unsupported DSH host API${suffix}`)
18
19
  err.code = 'DSH_HOST_INCOMPATIBLE'
19
20
  return err
@@ -117,7 +117,11 @@ export const Config = z.object({
117
117
  codexRedirectUri: z.string().volatile().default(''),
118
118
  codexBaseUrl: z.string().volatile().default(''),
119
119
  codexClientVersion: z.string().volatile().default('')
120
- .description('Codex CLI identity version for /models catalog gating (e.g. 0.160.0). Empty uses the built-in default.'),
120
+ .description('Codex CLI identity version for /models catalog gating (e.g. 0.160.0). Empty uses npm/local/built-in resolution.'),
121
+ codexWebSearch: z.boolean().volatile().default(true)
122
+ .description('#815 Expose Codex subscription search as the host web_search provider. Off releases the seam.'),
123
+ codexWebSearchModel: z.string().volatile().default('gpt-5.6-luna')
124
+ .description('#815 Model used for Codex web-search turns.'),
121
125
  claudeClientId: z.string().volatile().default(''),
122
126
  claudeRedirectUri: z.string().volatile().default(''),
123
127
  grokClientId: z.string().volatile().default(''),
package/lib/index.js CHANGED
@@ -23,6 +23,12 @@ import { OllamaAdapter, ollamaAlive, ollamaModels, ollamaBase } from './ollama.j
23
23
  import { createResetCreditService } from './reset-credits.js'
24
24
  import { maskEmail, maskLabel } from './mask.js'
25
25
  import { proxyFetch } from './proxy.js'
26
+ import { shouldBypassProxy } from './feature-helpers.js'
27
+ import { refreshClientVersions } from './client-version-service.js'
28
+ import { normalizeRpcResult, hostCompatibilityError } from './compat.js'
29
+ import { CodexWebSearchProvider } from './web-search-provider.js'
30
+ import { identityHeaders } from './vendors/codex.js'
31
+ import { webSearchRetryDelay } from './feature-helpers.js'
26
32
  import { HistoryStore, resolveHistoryDir } from './history.js'
27
33
  import { createDiagnosticsReport } from './diagnostics.js'
28
34
  import { createAccountsView } from './accounts-view.js'
@@ -48,7 +54,7 @@ export { pushToWebdav, pullFromWebdav, pushToGist, pullFromGist, buildS3SigV4Hea
48
54
  import { syncVaultPush, syncVaultPull, getVaultSyncStatus } from './vault-sync.js'
49
55
 
50
56
  export const name = '@goodandready/dsh-subscriptions'
51
- export const inject = ['llm', 'credentials', 'webServer', 'settings', 'tools']
57
+ export const inject = ['llm', 'credentials', 'webServer', 'settings', 'tools', 'web']
52
58
 
53
59
  const NS = 'dsh-subscriptions'
54
60
 
@@ -167,11 +173,16 @@ export function apply(ctx, rawConfig) {
167
173
  const parsed = plainConfig(next)
168
174
  const prevConfig = cachedConfig
169
175
  try {
176
+ let rpc
170
177
  if (typeof settingsSvc?.update === 'function') {
171
- await settingsSvc.update(NS, structuredClone(parsed), expectedRevision !== undefined ? expectedRevision : describeRow()?.revision)
178
+ rpc = await settingsSvc.update(NS, structuredClone(parsed), expectedRevision !== undefined ? expectedRevision : describeRow()?.revision)
172
179
  } else if (typeof settingsSvc?.replace === 'function') {
173
- await settingsSvc.replace(NS, structuredClone(parsed), expectedRevision !== undefined ? expectedRevision : describeRow()?.revision)
180
+ rpc = await settingsSvc.replace(NS, structuredClone(parsed), expectedRevision !== undefined ? expectedRevision : describeRow()?.revision)
181
+ } else {
182
+ throw hostCompatibilityError('settings.update/replace is missing')
174
183
  }
184
+ const result = rpc === undefined ? { ok: true } : normalizeRpcResult(rpc)
185
+ if (!result.ok) throw new Error('settings update was not acknowledged by the host')
175
186
  cachedConfig = parsed
176
187
  clearKeyRotationCache()
177
188
  clearOllamaHealthCache()
@@ -213,6 +224,9 @@ export function apply(ctx, rawConfig) {
213
224
  const row = describeRow()
214
225
  if (row && row.value) syncSnapshot(plainConfig(Config(plainConfig(row.value))))
215
226
  else syncSnapshot()
227
+ if ((live().slots || []).some((slot) => slot && (slot.provider === 'codex' || slot.provider === 'claude'))) {
228
+ refreshClientVersions({ fetchImpl: fetch }).catch(() => {})
229
+ }
216
230
  reconcileSlots().catch(() => {})
217
231
  } catch { /* keep existing snapshot on error */ }
218
232
  }
@@ -227,10 +241,14 @@ export function apply(ctx, rawConfig) {
227
241
  }, 'dsh-subscriptions: settings change listener')
228
242
  }
229
243
 
230
- Promise.resolve().then(() => {
244
+ Promise.resolve().then(async () => {
231
245
  reconcileSlots().catch((err) => {
232
246
  if (logger && logger.warn) logger.warn('[dsh-subscriptions] reconcileSlots initial error: ' + (err && err.message || err))
233
247
  })
248
+ const logged = await store.loggedInProviders().catch(() => [])
249
+ if (logged.some((provider) => provider === 'codex' || provider === 'claude')) {
250
+ refreshClientVersions({ fetchImpl: fetch }).catch(() => {})
251
+ }
234
252
  })
235
253
 
236
254
  const fetchForRef = (ref) => {
@@ -238,7 +256,11 @@ export function apply(ctx, rawConfig) {
238
256
  if (!parsed) return null
239
257
  const slot = normalizeSlots(live().slots).find((s) => s.provider === parsed.provider && s.index === parsed.index)
240
258
  if (!slot || !slot.proxyUrl) return null
241
- return proxyFetch(slot.proxyUrl)
259
+ const proxied = proxyFetch(slot.proxyUrl)
260
+ if (!proxied) return null
261
+ const bypass = Array.isArray(slot.proxyBypass) ? slot.proxyBypass : []
262
+ if (!bypass.length) return proxied
263
+ return (url, init) => shouldBypassProxy(bypass, String(url)) ? fetch(url, init) : proxied(url, init)
242
264
  }
243
265
 
244
266
  function redirectFor(provider, ref, reqOrigin) {
@@ -275,6 +297,47 @@ export function apply(ctx, rawConfig) {
275
297
  }
276
298
  const recordHistory = (entry) => history.add(entry)
277
299
 
300
+ // #815: register Codex subscription search behind the host web seam.
301
+ if (ctx.web && typeof ctx.web.registerSearchProvider === 'function') {
302
+ const provider = new CodexWebSearchProvider({
303
+ available: () => !!live().codexWebSearch,
304
+ search: async (request, signal) => {
305
+ const rows = await store.listAccounts('codex')
306
+ const row = (rows || []).find((account) => account && account.hasToken && account.ref)
307
+ if (!row) throw new Error('Codex web search requires a connected Codex account')
308
+ const raw = await store.resolveRaw(row.ref)
309
+ if (!raw) throw new Error('Codex web search requires a connected Codex account')
310
+ const blob = await store.ensureFresh('codex', parseBlob(raw), row.ref)
311
+ const cfg = vendorConfig('codex', live())
312
+ const base = (cfg.baseUrl || 'https://chatgpt.com/backend-api/codex').replace(/\/$/, '')
313
+ const fetchFn = fetchForRef(row.ref) || fetch
314
+ let attempt = 0
315
+ for (;;) {
316
+ const res = await fetchFn(base + '/responses', {
317
+ method: 'POST',
318
+ headers: identityHeaders(blob, cfg, { 'Content-Type': 'application/json', Accept: 'application/json' }),
319
+ body: JSON.stringify({
320
+ model: live().codexWebSearchModel || 'gpt-5.6-luna',
321
+ input: [{ role: 'user', content: [{ type: 'input_text', text: String(request.query || '') }] }],
322
+ tools: [{ type: 'web_search' }],
323
+ stream: false,
324
+ }),
325
+ signal,
326
+ })
327
+ if (res.ok) return res.json()
328
+ const delay = webSearchRetryDelay(attempt, res.status)
329
+ if (delay == null) throw new Error('Codex web search failed: HTTP ' + res.status)
330
+ await new Promise((resolve) => setTimeout(resolve, delay))
331
+ attempt += 1
332
+ }
333
+ },
334
+ })
335
+ ctx.effect(() => {
336
+ const dispose = ctx.web.registerSearchProvider(provider)
337
+ return () => { if (typeof dispose === 'function') dispose() }
338
+ }, 'dsh-subscriptions: codex web search provider')
339
+ }
340
+
278
341
  // #85: host-only reset credit service for Codex accounts.
279
342
  const resetCredits = createResetCreditService({ loadBlob: (ref) => store.loadBlob(ref) })
280
343
  function refForSlot(provider, indexOrAlias) {
package/lib/messages.js CHANGED
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto'
2
2
  import { extractImages, toOpenAiImage, toAnthropicImage, toGeminiImage, toCodexImage } from './images.js'
3
3
  import { normalizeTools } from './tools-normalizer.js'
4
4
  import { geminiFunctionDeclarations } from './gemini-schema.js'
5
+ import { toolCallId as callIdOf } from './compat.js'
5
6
 
6
7
  function flattenText(content) {
7
8
  if (typeof content === 'string') return content
@@ -45,7 +46,7 @@ export function openaiMessages(options) {
45
46
  function: { name: block.name, arguments: block.arguments || '{}' },
46
47
  })),
47
48
  ...((message.tool_calls || message.toolCalls || []).map((c) => ({
48
- id: c.id || c.call_id,
49
+ id: callIdOf(c),
49
50
  type: 'function',
50
51
  function: {
51
52
  name: c.name || (c.function && c.function.name),
@@ -61,7 +62,7 @@ export function openaiMessages(options) {
61
62
  continue
62
63
  }
63
64
  if (message.role === 'tool') {
64
- const toolCallId = String(message.toolCallId || message.tool_call_id || message.call_id || message.id || '')
65
+ const toolCallId = callIdOf(message)
65
66
  const content = typeof message.content === 'string'
66
67
  ? message.content
67
68
  : (flattenText(message.content) || JSON.stringify(message.content || ''))
@@ -89,7 +90,7 @@ export function openaiMessages(options) {
89
90
  for (const result of results) {
90
91
  out.push({
91
92
  role: 'tool',
92
- tool_call_id: String(result.toolCallId || result.tool_call_id || result.call_id || result.id || ''),
93
+ tool_call_id: callIdOf(result),
93
94
  content: typeof result.content === 'string' ? result.content : (flattenText(result.content) || '(no output)'),
94
95
  })
95
96
  }
@@ -213,7 +214,7 @@ export function codexResponsesBody(options, fallbackInstructions, vendorCfg) {
213
214
  const calls = [
214
215
  ...toolCalls(message.content),
215
216
  ...((message.tool_calls || message.toolCalls || []).map((c) => ({
216
- id: c.id || c.call_id,
217
+ id: callIdOf(c),
217
218
  name: c.name || (c.function && c.function.name),
218
219
  arguments: typeof c.arguments === 'string' ? c.arguments : (c.function && c.function.arguments) || c.arguments || '{}',
219
220
  }))),
@@ -232,7 +233,7 @@ export function codexResponsesBody(options, fallbackInstructions, vendorCfg) {
232
233
  }
233
234
  if (message.role === 'tool') {
234
235
  flush()
235
- const callId = String(message.toolCallId || message.tool_call_id || message.call_id || message.id || '')
236
+ const callId = callIdOf(message)
236
237
  const outText = typeof message.content === 'string'
237
238
  ? message.content
238
239
  : (flattenText(message.content) || JSON.stringify(message.content || ''))
@@ -259,7 +260,7 @@ export function codexResponsesBody(options, fallbackInstructions, vendorCfg) {
259
260
  }
260
261
  for (const result of results) {
261
262
  flush()
262
- const callId = String(result.toolCallId || result.tool_call_id || result.call_id || result.id || '')
263
+ const callId = callIdOf(result)
263
264
  const outText = typeof result.content === 'string'
264
265
  ? result.content
265
266
  : (flattenText(result.content) || (result.output != null ? (typeof result.output === 'string' ? result.output : JSON.stringify(result.output)) : ''))
@@ -324,7 +325,7 @@ export function anthropicPayload(options, extraSystem) {
324
325
  const calls = [
325
326
  ...toolCalls(message.content),
326
327
  ...((message.tool_calls || message.toolCalls || []).map((c) => ({
327
- id: c.id || c.call_id,
328
+ id: callIdOf(c),
328
329
  name: c.name || (c.function && c.function.name),
329
330
  arguments: c.arguments || (c.function && c.function.arguments) || "{}",
330
331
  }))),
@@ -342,7 +343,7 @@ export function anthropicPayload(options, extraSystem) {
342
343
  continue
343
344
  }
344
345
  if (message.role === "tool") {
345
- const toolUseId = String(message.toolCallId || message.tool_call_id || message.tool_use_id || message.id || "")
346
+ const toolUseId = callIdOf(message)
346
347
  const images = extractImages(message.content)
347
348
  let content = typeof message.content === "string"
348
349
  ? message.content
@@ -375,7 +376,7 @@ export function anthropicPayload(options, extraSystem) {
375
376
  if (blk) blocks.push(blk)
376
377
  }
377
378
  for (const result of toolResults(message.content)) {
378
- const toolUseId = String(result.toolCallId || result.tool_call_id || result.tool_use_id || result.id || "")
379
+ const toolUseId = callIdOf(result)
379
380
  const content = typeof result.content === "string"
380
381
  ? result.content
381
382
  : (flattenText(result.content) || (result.output != null ? (typeof result.output === "string" ? result.output : JSON.stringify(result.output)) : ""))
@@ -30,6 +30,7 @@ import { assessHealth } from '../health-report.js'
30
30
  import { probeOllama } from '../ollama-health.js'
31
31
  import { defaultSnapshotStore } from '../vault-snapshots.js'
32
32
  import { collectSourceChain } from './sources.js'
33
+ import { clientVersionInfo } from '../client-version-service.js'
33
34
 
34
35
  export function registerStatusRoutes(ctx, state) {
35
36
  const {
@@ -71,6 +72,13 @@ export function registerStatusRoutes(ctx, state) {
71
72
  liveConfig: live(),
72
73
  hideDeprecated: !!live().hideDeprecatedModels,
73
74
  }),
75
+ clientVersions: clientVersionInfo({
76
+ codex: getVendor('codex').defaults().clientVersion,
77
+ claude: getVendor('claude').defaults().clientVersion,
78
+ }, {
79
+ codex: live().codexClientVersion,
80
+ claude: live().claudeCcVersion,
81
+ }),
74
82
  queue: defaultConcurrencyQueue.stats(),
75
83
  }
76
84
  }
@@ -60,7 +60,7 @@ export function authorizeUrl(cfg, pkce) {
60
60
  })
61
61
  }
62
62
 
63
- function identityHeaders(blob, config, extra) {
63
+ export function identityHeaders(blob, config, extra) {
64
64
  const originator = headerSafeValue(config.originator || 'codex_cli_rs')
65
65
  const activeOrgId = headerSafeValue(blob.selectedOrgId || blob.accountId || '')
66
66
  return {
@@ -0,0 +1,64 @@
1
+ const MAX_BYTES = 64 * 1024
2
+
3
+ function outputText(payload) {
4
+ if (typeof payload?.output_text === 'string') return payload.output_text
5
+ if (!Array.isArray(payload?.output)) return ''
6
+ return payload.output
7
+ .flatMap((item) => Array.isArray(item?.content) ? item.content : [])
8
+ .filter((block) => block && (block.type === 'output_text' || block.type === 'text'))
9
+ .map((block) => String(block.text || ''))
10
+ .join('')
11
+ }
12
+
13
+ function citationSources(payload) {
14
+ const sources = []
15
+ const seen = new Set()
16
+ const visit = (value) => {
17
+ if (!value || typeof value !== 'object') return
18
+ if (Array.isArray(value)) { value.forEach(visit); return }
19
+ for (const key of Object.keys(value)) {
20
+ if (key !== 'content' && key !== 'annotations' && key !== 'citations') continue
21
+ const nested = value[key]
22
+ if (!Array.isArray(nested)) continue
23
+ for (const item of nested) {
24
+ if (item && typeof item === 'object') visit(item)
25
+ }
26
+ }
27
+ const annotations = Array.isArray(value.annotations) ? value.annotations : []
28
+ for (const annotation of annotations) {
29
+ const url = String(annotation?.url || annotation?.link || '')
30
+ if (!url || seen.has(url)) continue
31
+ seen.add(url)
32
+ sources.push({ url, ...(annotation.title ? { title: String(annotation.title) } : {}) })
33
+ }
34
+ }
35
+ visit(payload?.output)
36
+ visit(payload?.citations)
37
+ return sources
38
+ }
39
+
40
+ /** #815: map a Codex responses payload into the host WebSearchResult contract. */
41
+ export function normalizeCodexSearch(payload, maxBytes = MAX_BYTES) {
42
+ return {
43
+ content: String(outputText(payload) || '').slice(0, maxBytes),
44
+ sources: citationSources(payload).slice(0, 50),
45
+ truncated: false,
46
+ }
47
+ }
48
+
49
+ export class CodexWebSearchProvider {
50
+ constructor({ available, search }) {
51
+ this.id = 'subscriptions-codex'
52
+ this._available = available
53
+ this._search = search
54
+ }
55
+
56
+ available() {
57
+ try { return !!this._available() } catch { return false }
58
+ }
59
+
60
+ async search(request, signal) {
61
+ if (!this.available()) throw new Error('Codex web search is not available')
62
+ return normalizeCodexSearch(await this._search(request, signal))
63
+ }
64
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.7.40",
3
+ "version": "0.7.42",
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",