@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.
- package/lib/accounts-view.js +1 -0
- package/lib/accounts.js +7 -1
- package/lib/client-version-service.js +73 -0
- package/lib/client-version.js +3 -1
- package/lib/client.js +24 -0
- package/lib/compat.js +2 -1
- package/lib/config-schema.js +5 -1
- package/lib/index.js +68 -5
- package/lib/messages.js +10 -9
- package/lib/routes/status.js +8 -0
- package/lib/vendors/codex.js +1 -1
- package/lib/web-search-provider.js +64 -0
- package/package.json +1 -1
package/lib/accounts-view.js
CHANGED
|
@@ -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:
|
|
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
|
+
}
|
package/lib/client-version.js
CHANGED
|
@@ -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
|
package/lib/config-schema.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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:
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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)) : ""))
|
package/lib/routes/status.js
CHANGED
|
@@ -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
|
}
|
package/lib/vendors/codex.js
CHANGED
|
@@ -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.
|
|
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",
|