@goodandready/dsh-subscriptions 0.6.91 → 0.7.2
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-manager.js +3 -0
- package/lib/adapter.js +10 -2
- package/lib/client.js +170 -8
- package/lib/coexistence.js +45 -0
- package/lib/config-schema.js +16 -0
- package/lib/index.js +12 -1
- package/lib/messages.js +1 -1
- package/lib/model-settings.js +49 -0
- package/lib/native-tools.js +220 -0
- package/lib/reset-clock.js +16 -6
- package/lib/route-report.js +28 -0
- package/lib/routes/status.js +87 -2
- package/lib/speed-mode.js +17 -0
- package/lib/stream-rotate.js +10 -2
- package/lib/subscriptions.js +1 -1
- package/lib/usage-export.js +52 -0
- package/package.json +1 -1
package/lib/adapter-manager.js
CHANGED
|
@@ -36,6 +36,7 @@ export function createAdapterManager({
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const adapter = new SubscriptionAdapter({
|
|
39
|
+
live: () => live(),
|
|
39
40
|
listAccounts: (provider) => store.listAccounts(provider),
|
|
40
41
|
loadBlob: (ref) => store.loadBlob(ref),
|
|
41
42
|
ensureFresh: (provider, blob, ref) => store.ensureFresh(provider, blob, ref),
|
|
@@ -46,6 +47,8 @@ export function createAdapterManager({
|
|
|
46
47
|
drainSafetyPercent: () => live().drainSafetyPercent,
|
|
47
48
|
switchAtRemaining: () => live().switchAtRemaining,
|
|
48
49
|
rotationStrategy: () => live().rotationStrategy,
|
|
50
|
+
rateLimitWait: () => live().rateLimitWait,
|
|
51
|
+
rateLimitMaxWaitMs: () => live().rateLimitMaxWaitMs,
|
|
49
52
|
enableSoftFailureSentinel: () => live().enableSoftFailureSentinel,
|
|
50
53
|
rememberCooldown: (ref, until, families) => store.rememberCooldown(ref, until, families),
|
|
51
54
|
rememberQuarantine: (ref, reason, until, attempts) => store.rememberQuarantine(ref, reason, until, attempts),
|
package/lib/adapter.js
CHANGED
|
@@ -3,6 +3,8 @@ import { inferModelCapabilities, isDeprecatedId } from './models-catalog.js'
|
|
|
3
3
|
import { fetchWithTimeout } from "./http.js"
|
|
4
4
|
import { LlmAdapter, LlmError, attributionHeaders } from '@deepseek-ai/dsh-llm'
|
|
5
5
|
import { displayName } from './refs.js'
|
|
6
|
+
import { applyModelSettings, bestAccountPlan } from './model-settings.js'
|
|
7
|
+
import { getSpeedMode } from './speed-mode.js'
|
|
6
8
|
import { getVendor } from './vendors/index.js'
|
|
7
9
|
import { modelCatalog } from './messages.js'
|
|
8
10
|
import { streamWithRotation } from './stream-rotate.js'
|
|
@@ -21,6 +23,9 @@ function asLlmError(err) {
|
|
|
21
23
|
try {
|
|
22
24
|
const out = new LlmError(message, code)
|
|
23
25
|
if (err && err.validationUrl) out.validationUrl = err.validationUrl
|
|
26
|
+
if (err && err.retryAfterMs != null) out.retryAfterMs = err.retryAfterMs
|
|
27
|
+
if (err && err.resetAt != null) out.resetAt = err.resetAt
|
|
28
|
+
if (err && err.retryAfter != null) out.retryAfter = err.retryAfter
|
|
24
29
|
return out
|
|
25
30
|
} catch {
|
|
26
31
|
return err
|
|
@@ -86,7 +91,7 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
86
91
|
capabilities: Array.isArray(m.capabilities) && m.capabilities.length ? m.capabilities : caps.capabilities,
|
|
87
92
|
}
|
|
88
93
|
})
|
|
89
|
-
return models
|
|
94
|
+
return applyModelSettings(provider, models, this.deps.live ? this.deps.live() : {}, bestAccountPlan(accounts))
|
|
90
95
|
}
|
|
91
96
|
|
|
92
97
|
imageRequestPricing() {
|
|
@@ -112,7 +117,7 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
112
117
|
...(reasoningMeta ? { reasoning: reasoningMeta } : {}),
|
|
113
118
|
}
|
|
114
119
|
}
|
|
115
|
-
return { provider, id: model, name: model, ...(reasoningMeta ? { reasoning: reasoningMeta } : {}) }
|
|
120
|
+
return { provider, id: model, name: (found && found.name) || model, ...(reasoningMeta ? { reasoning: reasoningMeta } : {}), ...(found && found.contextWindow ? { context: { contextWindow: found.contextWindow } } : {}) }
|
|
116
121
|
}
|
|
117
122
|
|
|
118
123
|
async prepareCall(provider, model, signal) {
|
|
@@ -153,6 +158,7 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
153
158
|
const decisionTrace = {}
|
|
154
159
|
const streamOptions = {
|
|
155
160
|
...options,
|
|
161
|
+
...(provider === 'codex' && getSpeedMode(sessionId) === 'fast' ? { service_tier: 'priority' } : {}),
|
|
156
162
|
...(effectiveReasoning.effort ? { reasoningEffort: effectiveReasoning.effort } : {}),
|
|
157
163
|
...(effectiveReasoning.budgetTokens ? { thinkingBudget: effectiveReasoning.budgetTokens } : {}),
|
|
158
164
|
_decisionTrace: decisionTrace,
|
|
@@ -161,6 +167,8 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
161
167
|
drainSafetyPercent: effectiveDrainSafetyPercent,
|
|
162
168
|
rotationStrategy: effectiveRotationStrategy,
|
|
163
169
|
enableSoftFailureSentinel: effectiveSoftFailureSentinel,
|
|
170
|
+
rateLimitWait: typeof deps.rateLimitWait === 'function' ? deps.rateLimitWait() : deps.rateLimitWait,
|
|
171
|
+
rateLimitMaxWaitMs: typeof deps.rateLimitMaxWaitMs === 'function' ? deps.rateLimitMaxWaitMs() : deps.rateLimitMaxWaitMs,
|
|
164
172
|
}
|
|
165
173
|
for await (const event of streamWithRotation({
|
|
166
174
|
accounts: await deps.listAccounts(provider),
|
package/lib/client.js
CHANGED
|
@@ -106,6 +106,10 @@ window.__ModuleLoader__.load({
|
|
|
106
106
|
"hideDeprecated": "Hide deprecated models",
|
|
107
107
|
"hideDeprecatedHint": "Hide older or superseded model variants from model dropdowns.",
|
|
108
108
|
"composerQuota": "Composer quota indicator",
|
|
109
|
+
"composerQuotaDisplay": "Status-bar quota scope",
|
|
110
|
+
"composerQuotaDisplayHint": "Show quota for the provider of the current model, or hide the status-bar pill.",
|
|
111
|
+
"composerQuotaDisplayCurrent": "Current model",
|
|
112
|
+
"composerQuotaDisplayHidden": "Hidden",
|
|
109
113
|
"composerQuotaHint": "Display subscription usage indicator directly in the composer toolbar.",
|
|
110
114
|
"composerQuotaOff": "Disabled",
|
|
111
115
|
"composerQuotaPercent": "Percentage",
|
|
@@ -255,11 +259,23 @@ window.__ModuleLoader__.load({
|
|
|
255
259
|
"verifyLink": "Open verification link",
|
|
256
260
|
"verifySuffix": " then reconnect.",
|
|
257
261
|
"addAccount": "+ Add account",
|
|
262
|
+
"slotAdvanced": "Advanced slot settings",
|
|
263
|
+
"providerGallery": "Add provider",
|
|
264
|
+
"providerGalleryHint": "Unconnected providers are hidden from the main list to keep the page focused.",
|
|
265
|
+
"vipOnly": "VIP only",
|
|
266
|
+
"vipOnlyHint": "Reserve this account for VIP-tagged requests.",
|
|
267
|
+
"isPro": "Pro",
|
|
268
|
+
"isProHint": "Treat this account as Pro for weighted priority.",
|
|
269
|
+
"tags": "Tags",
|
|
270
|
+
"tagsPlaceholder": "team, prod",
|
|
271
|
+
"weight": "Weight",
|
|
272
|
+
"maxQueueSizeSlot": "Queue",
|
|
258
273
|
"save": "Save",
|
|
259
274
|
"saved": "Saved",
|
|
260
275
|
"accountLabel": "Account",
|
|
261
276
|
"plan": "Plan",
|
|
262
277
|
"storedAs": "Stored as",
|
|
278
|
+
"slashFast": "Toggle Codex Speed (fast/standard)",
|
|
263
279
|
"slashLogin": "Login to a subscription provider",
|
|
264
280
|
"slashLogout": "Log out a subscription provider",
|
|
265
281
|
"forecast": "≈",
|
|
@@ -404,6 +420,10 @@ window.__ModuleLoader__.load({
|
|
|
404
420
|
"hideDeprecated": "隐藏已废弃的旧模型",
|
|
405
421
|
"hideDeprecatedHint": "在模型下拉列表中隐藏早期或已被替代的旧版模型。",
|
|
406
422
|
"composerQuota": "输入框额度指示器",
|
|
423
|
+
"composerQuotaDisplay": "状态栏额度范围",
|
|
424
|
+
"composerQuotaDisplayHint": "显示当前模型所属服务商的额度,或隐藏状态栏指示器。",
|
|
425
|
+
"composerQuotaDisplayCurrent": "当前模型",
|
|
426
|
+
"composerQuotaDisplayHidden": "隐藏",
|
|
407
427
|
"composerQuotaHint": "在聊天输入框工具栏直接显示当前订阅额度指示器。",
|
|
408
428
|
"composerQuotaOff": "已禁用",
|
|
409
429
|
"composerQuotaPercent": "百分比显示",
|
|
@@ -553,11 +573,23 @@ window.__ModuleLoader__.load({
|
|
|
553
573
|
"verifyLink": "打开验证链接",
|
|
554
574
|
"verifySuffix": ",完成后点击重新连接。",
|
|
555
575
|
"addAccount": "+ 添加账户",
|
|
576
|
+
"slotAdvanced": "账户高级设置",
|
|
577
|
+
"providerGallery": "添加服务商",
|
|
578
|
+
"providerGalleryHint": "未连接的服务商默认隐藏在列表中,保持页面聚焦。",
|
|
579
|
+
"vipOnly": "仅 VIP",
|
|
580
|
+
"vipOnlyHint": "将此账户保留给 VIP 请求。",
|
|
581
|
+
"isPro": "Pro",
|
|
582
|
+
"isProHint": "按 Pro 账户参与加权优先级。",
|
|
583
|
+
"tags": "标签",
|
|
584
|
+
"tagsPlaceholder": "team, prod",
|
|
585
|
+
"weight": "权重",
|
|
586
|
+
"maxQueueSizeSlot": "队列",
|
|
556
587
|
"save": "保存",
|
|
557
588
|
"saved": "已保存",
|
|
558
589
|
"accountLabel": "账户",
|
|
559
590
|
"plan": "套餐方案",
|
|
560
591
|
"storedAs": "存储标识",
|
|
592
|
+
"slashFast": "切换 Codex 加速模式(快速/标准)",
|
|
561
593
|
"slashLogin": "登录订阅服务商",
|
|
562
594
|
"slashLogout": "登出订阅服务商",
|
|
563
595
|
"forecast": "≈",
|
|
@@ -1785,6 +1817,18 @@ window.__ModuleLoader__.load({
|
|
|
1785
1817
|
),
|
|
1786
1818
|
h('details', { className: 'dsub-group', open: true },
|
|
1787
1819
|
h('summary', { className: 'dsub-groupHead' }, t('groupRotation')),
|
|
1820
|
+
h('div', { className: 'dsub-row' },
|
|
1821
|
+
h('span', { className: 'dsub-h' }, t('composerQuotaDisplay')),
|
|
1822
|
+
h('select', {
|
|
1823
|
+
className: 'dsub-select',
|
|
1824
|
+
value: draft.composerQuotaDisplay || 'current',
|
|
1825
|
+
onChange: (e) => setDraft((d) => Object.assign({}, d, { composerQuotaDisplay: e.target.value })),
|
|
1826
|
+
},
|
|
1827
|
+
h('option', { value: 'current' }, t('composerQuotaDisplayCurrent')),
|
|
1828
|
+
h('option', { value: 'hidden' }, t('composerQuotaDisplayHidden')),
|
|
1829
|
+
),
|
|
1830
|
+
),
|
|
1831
|
+
h('div', { className: 'dsub-sub' }, t('composerQuotaDisplayHint')),
|
|
1788
1832
|
h('div', { className: 'dsub-row' },
|
|
1789
1833
|
h('span', { className: 'dsub-h' }, t('composerQuota')),
|
|
1790
1834
|
h('select', {
|
|
@@ -2347,7 +2391,17 @@ window.__ModuleLoader__.load({
|
|
|
2347
2391
|
)
|
|
2348
2392
|
) : null,
|
|
2349
2393
|
),
|
|
2350
|
-
|
|
2394
|
+
(function(){
|
|
2395
|
+
const configuredSlot = (slot) => {
|
|
2396
|
+
const account = accountOf(slot.provider, slot.index)
|
|
2397
|
+
return !!((account && (account.configured || account.hasToken))
|
|
2398
|
+
|| slot.label || slot.proxyUrl || slot.expiresAt
|
|
2399
|
+
|| (Array.isArray(slot.tags) && slot.tags.length) || slot.vipOnly || slot.isPro || slot.weight)
|
|
2400
|
+
}
|
|
2401
|
+
const visibleNames = names.filter((prov) => slots.some((slot) => slot.provider === prov.id && configuredSlot(slot)))
|
|
2402
|
+
const hiddenNames = names.filter((prov) => visibleNames.indexOf(prov) < 0)
|
|
2403
|
+
return h(React.Fragment, null,
|
|
2404
|
+
visibleNames.map((prov) => {
|
|
2351
2405
|
const rows = slots
|
|
2352
2406
|
.map((slot, i) => ({ slot: slot, i: i }))
|
|
2353
2407
|
.filter((row) => row.slot.provider === prov.id)
|
|
@@ -2637,6 +2691,59 @@ window.__ModuleLoader__.load({
|
|
|
2637
2691
|
},
|
|
2638
2692
|
})
|
|
2639
2693
|
),
|
|
2694
|
+
h('details', { className: 'dsub-details' },
|
|
2695
|
+
h('summary', { className: 'dsub-summary' }, t('slotAdvanced')),
|
|
2696
|
+
h('div', { className: 'dsub-row' },
|
|
2697
|
+
h('label', { className: 'dsub-ref-tag', title: t('vipOnlyHint') },
|
|
2698
|
+
h('input', {
|
|
2699
|
+
type: 'checkbox',
|
|
2700
|
+
checked: !!row.slot.vipOnly,
|
|
2701
|
+
onChange: (e) => updateSlot(row.slot.provider, row.slot.index, { vipOnly: e.target.checked }),
|
|
2702
|
+
}),
|
|
2703
|
+
' ' + t('vipOnly')
|
|
2704
|
+
),
|
|
2705
|
+
h('label', { className: 'dsub-ref-tag', title: t('isProHint') },
|
|
2706
|
+
h('input', {
|
|
2707
|
+
type: 'checkbox',
|
|
2708
|
+
checked: !!row.slot.isPro,
|
|
2709
|
+
onChange: (e) => updateSlot(row.slot.provider, row.slot.index, { isPro: e.target.checked }),
|
|
2710
|
+
}),
|
|
2711
|
+
' ' + t('isPro')
|
|
2712
|
+
),
|
|
2713
|
+
),
|
|
2714
|
+
h('div', { className: 'dsub-row' },
|
|
2715
|
+
h('span', { className: 'dsub-ref-tag' }, t('tags')),
|
|
2716
|
+
h('input', {
|
|
2717
|
+
className: 'dsub-inp',
|
|
2718
|
+
style: { width: '160px', minWidth: '160px' },
|
|
2719
|
+
value: (Array.isArray(row.slot.tags) ? row.slot.tags : []).join(','),
|
|
2720
|
+
placeholder: t('tagsPlaceholder'),
|
|
2721
|
+
onChange: (e) => updateSlot(row.slot.provider, row.slot.index, {
|
|
2722
|
+
tags: e.target.value.split(',').map((value) => value.trim()).filter(Boolean),
|
|
2723
|
+
}),
|
|
2724
|
+
}),
|
|
2725
|
+
h('span', { className: 'dsub-ref-tag' }, t('weight')),
|
|
2726
|
+
h('input', {
|
|
2727
|
+
type: 'number', min: 0, max: 100,
|
|
2728
|
+
style: { width: '52px', padding: '1px 4px', fontSize: '11px', textAlign: 'center' },
|
|
2729
|
+
value: row.slot.weight !== undefined ? row.slot.weight : 0,
|
|
2730
|
+
onChange: (e) => {
|
|
2731
|
+
const value = parseInt(e.target.value, 10)
|
|
2732
|
+
updateSlot(row.slot.provider, row.slot.index, { weight: isNaN(value) ? 0 : Math.max(0, value) })
|
|
2733
|
+
},
|
|
2734
|
+
}),
|
|
2735
|
+
h('span', { className: 'dsub-ref-tag' }, t('maxQueueSizeSlot')),
|
|
2736
|
+
h('input', {
|
|
2737
|
+
type: 'number', min: 0, max: 1000,
|
|
2738
|
+
style: { width: '58px', padding: '1px 4px', fontSize: '11px', textAlign: 'center' },
|
|
2739
|
+
value: row.slot.maxQueueSize !== undefined ? row.slot.maxQueueSize : 0,
|
|
2740
|
+
onChange: (e) => {
|
|
2741
|
+
const value = parseInt(e.target.value, 10)
|
|
2742
|
+
updateSlot(row.slot.provider, row.slot.index, { maxQueueSize: isNaN(value) ? 0 : Math.max(0, value) })
|
|
2743
|
+
},
|
|
2744
|
+
}),
|
|
2745
|
+
),
|
|
2746
|
+
),
|
|
2640
2747
|
),
|
|
2641
2748
|
(function(){
|
|
2642
2749
|
var r = proxyRes[key]
|
|
@@ -2917,6 +3024,20 @@ window.__ModuleLoader__.load({
|
|
|
2917
3024
|
}, t('addAccount')),
|
|
2918
3025
|
)
|
|
2919
3026
|
}),
|
|
3027
|
+
hiddenNames.length ? h('details', { className: 'dsub-group', open: visibleNames.length === 0 },
|
|
3028
|
+
h('summary', { className: 'dsub-groupHead' }, '➕ ' + t('providerGallery')),
|
|
3029
|
+
h('div', { className: 'dsub-sub' }, t('providerGalleryHint')),
|
|
3030
|
+
h('div', { className: 'dsub-model-grid' },
|
|
3031
|
+
hiddenNames.map((prov) => h('button', {
|
|
3032
|
+
key: prov.id,
|
|
3033
|
+
type: 'button',
|
|
3034
|
+
className: 'dsub-mini',
|
|
3035
|
+
onClick: () => addSlot(prov.id),
|
|
3036
|
+
}, prov.name))
|
|
3037
|
+
)
|
|
3038
|
+
) : null
|
|
3039
|
+
)
|
|
3040
|
+
})(),
|
|
2920
3041
|
h('div', { className: 'dsub-foot' },
|
|
2921
3042
|
h('button', {
|
|
2922
3043
|
type: 'button', className: 'dsub-save',
|
|
@@ -3103,10 +3224,28 @@ window.__ModuleLoader__.load({
|
|
|
3103
3224
|
labels: (data && data.labels) || {},
|
|
3104
3225
|
expiryNotifyDays: (data && data.expiryNotifyDays) || 7,
|
|
3105
3226
|
composerQuota: (data && data.composerQuota) || 'off',
|
|
3227
|
+
composerQuotaDisplay: (data && data.composerQuotaDisplay) || 'current',
|
|
3106
3228
|
active: (data && data.active) || null,
|
|
3107
3229
|
error: null,
|
|
3108
3230
|
}
|
|
3109
|
-
} catch { return { logged: [], usage: 0, expiresAt: {}, labels: {}, expiryNotifyDays: 7, fastMode: false, composerQuota: 'off', active: null, error: 'status unavailable' } }
|
|
3231
|
+
} catch { return { logged: [], usage: 0, expiresAt: {}, labels: {}, expiryNotifyDays: 7, fastMode: false, composerQuota: 'off', composerQuotaDisplay: 'current', active: null, error: 'status unavailable' } }
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
function currentModelProvider(props) {
|
|
3235
|
+
let session = null
|
|
3236
|
+
try {
|
|
3237
|
+
const ctx = typeof window !== 'undefined' ? window.__dshCtx : null
|
|
3238
|
+
session = (props && (props.session || props.conversation))
|
|
3239
|
+
|| (ctx && ctx.sessions && (ctx.sessions.current || ctx.sessions.active
|
|
3240
|
+
|| (typeof ctx.sessions.getCurrent === 'function' && ctx.sessions.getCurrent())))
|
|
3241
|
+
|| null
|
|
3242
|
+
} catch { /* sessions service is optional */ }
|
|
3243
|
+
const model = session && (session.model || session.modelId
|
|
3244
|
+
|| (session.header && session.header.model) || (session.state && session.state.model))
|
|
3245
|
+
if (!model) return null
|
|
3246
|
+
if (typeof model === 'object') return model.provider || null
|
|
3247
|
+
const text = String(model)
|
|
3248
|
+
return text.includes('/') ? text.split('/')[0] : null
|
|
3110
3249
|
}
|
|
3111
3250
|
|
|
3112
3251
|
function ComposerQuota(props) {
|
|
@@ -3120,11 +3259,22 @@ window.__ModuleLoader__.load({
|
|
|
3120
3259
|
fetch('/dsh-subscriptions/analytics', { cache: 'no-store' })
|
|
3121
3260
|
.then((res) => res.json())
|
|
3122
3261
|
.catch(() => ({})),
|
|
3123
|
-
]).then(([r, analyticsPayload]) => {
|
|
3262
|
+
]).then(async ([r, analyticsPayload]) => {
|
|
3124
3263
|
if (!alive) return
|
|
3125
3264
|
const mode = r.composerQuota || 'off'
|
|
3126
|
-
const
|
|
3127
|
-
|
|
3265
|
+
const current = currentModelProvider(props)
|
|
3266
|
+
let a = r.active
|
|
3267
|
+
if (current && (!a || a.provider !== current)) {
|
|
3268
|
+
const cfg = await fetch('/dsh-subscriptions/config', { cache: 'no-store' })
|
|
3269
|
+
.then((res) => res.json())
|
|
3270
|
+
.catch(() => null)
|
|
3271
|
+
const rows = (cfg && cfg.accounts) || []
|
|
3272
|
+
a = rows.find((row) => row && row.provider === current && row.usagePercent != null)
|
|
3273
|
+
|| rows.find((row) => row && row.provider === current)
|
|
3274
|
+
|| null
|
|
3275
|
+
}
|
|
3276
|
+
const hidden = r.composerQuotaDisplay === 'hidden'
|
|
3277
|
+
if (mode !== 'off' && !hidden && a && a.provider && a.provider !== 'ollama' && a.usagePercent != null) {
|
|
3128
3278
|
const remaining = Math.max(0, 100 - a.usagePercent)
|
|
3129
3279
|
const forecast = (analyticsPayload && analyticsPayload.analytics && analyticsPayload.analytics.forecast) || []
|
|
3130
3280
|
const row = forecast.find((f) => f.provider === a.provider && (!f.ref || !a.ref || f.ref === a.ref))
|
|
@@ -3134,9 +3284,9 @@ window.__ModuleLoader__.load({
|
|
|
3134
3284
|
? { status: 'idle' }
|
|
3135
3285
|
: { status: 'forecast', runwaySeconds: Math.max(0, Number(row.hoursToExhaustion) * 3600) }
|
|
3136
3286
|
}
|
|
3137
|
-
setState({ mode, active: a, remaining, est })
|
|
3287
|
+
setState({ mode, active: a, remaining, est, hidden })
|
|
3138
3288
|
} else {
|
|
3139
|
-
setState({ mode, active: a, remaining: null, est: null })
|
|
3289
|
+
setState({ mode, active: a, remaining: null, est: null, hidden })
|
|
3140
3290
|
}
|
|
3141
3291
|
}).catch(() => {})
|
|
3142
3292
|
}
|
|
@@ -3144,7 +3294,7 @@ window.__ModuleLoader__.load({
|
|
|
3144
3294
|
const id = setInterval(pull, 60 * 1000)
|
|
3145
3295
|
return () => { alive = false; clearInterval(id) }
|
|
3146
3296
|
}, [])
|
|
3147
|
-
if (state.mode === 'off' || !state.active || state.remaining == null) return null
|
|
3297
|
+
if (state.mode === 'off' || state.hidden || !state.active || state.remaining == null) return null
|
|
3148
3298
|
const cls = state.remaining <= 10 ? ' dsub-cqBarFull' : (state.remaining <= 30 ? ' dsub-cqBarWarn' : '')
|
|
3149
3299
|
var value = null
|
|
3150
3300
|
if (state.mode === 'percent') {
|
|
@@ -3555,6 +3705,7 @@ window.__ModuleLoader__.load({
|
|
|
3555
3705
|
const line = (line) => line.trim()
|
|
3556
3706
|
const sources = [
|
|
3557
3707
|
{ name: 'login', description: t('slashLogin') },
|
|
3708
|
+
{ name: 'fast', description: t('slashFast') },
|
|
3558
3709
|
{ name: 'logout', description: t('slashLogout') },
|
|
3559
3710
|
]
|
|
3560
3711
|
const disposers = sources.map((src) =>
|
|
@@ -3579,6 +3730,17 @@ window.__ModuleLoader__.load({
|
|
|
3579
3730
|
return refreshLoggedIn().then((logged) =>
|
|
3580
3731
|
({ text: 'Connected: ' + (logged.length ? logged.join(', ') : 'none') }))
|
|
3581
3732
|
}
|
|
3733
|
+
if (src.name === 'fast') {
|
|
3734
|
+
const mode = /\bstandard\b|\boff\b/.test(t2) ? 'standard' : 'fast'
|
|
3735
|
+
const sid = (_session && (_session.id || _session.sessionId || (_session.header && _session.header.id))) || 'default'
|
|
3736
|
+
return fetch('/dsh-subscriptions/speed', {
|
|
3737
|
+
method: 'POST',
|
|
3738
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3739
|
+
body: JSON.stringify({ sessionId: sid, mode }),
|
|
3740
|
+
}).then((r) => r.json()).then((d) => ({
|
|
3741
|
+
text: (d && d.mode === 'fast' ? 'Codex Speed: fast' : 'Codex Speed: standard'),
|
|
3742
|
+
})).catch((e) => ({ text: 'Codex Speed: ' + String(e && e.message || e) }))
|
|
3743
|
+
}
|
|
3582
3744
|
run(src.name, t2)
|
|
3583
3745
|
return Promise.resolve('handled')
|
|
3584
3746
|
},
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
|
|
5
|
+
export const CONFLICTING_PLUGINS = Object.freeze([
|
|
6
|
+
'dsh-plugin-subscriptions',
|
|
7
|
+
'dsh-super-subscriptions',
|
|
8
|
+
'dsh-agy-link',
|
|
9
|
+
'dsh-codex-connect',
|
|
10
|
+
'dsh-key-rotation',
|
|
11
|
+
'@goodandready/dsh-key-rotation',
|
|
12
|
+
])
|
|
13
|
+
|
|
14
|
+
let activeConflicts = []
|
|
15
|
+
|
|
16
|
+
export function detectConflicts({ dependencies = [], loaded = [] } = {}) {
|
|
17
|
+
const names = new Set([...(dependencies || []), ...(loaded || [])].filter(Boolean).map(String))
|
|
18
|
+
return CONFLICTING_PLUGINS.filter((name) => names.has(name))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function setActiveConflicts(conflicts) {
|
|
22
|
+
activeConflicts = Array.isArray(conflicts) ? conflicts.slice() : []
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getActiveConflicts() {
|
|
26
|
+
return activeConflicts.slice()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function readProfilePluginNames(profileDir = process.env.DSH_PROFILE_DIR) {
|
|
30
|
+
const candidates = []
|
|
31
|
+
if (profileDir) candidates.push(profileDir)
|
|
32
|
+
try { candidates.push(process.cwd()) } catch { /* ignore cwd errors */ }
|
|
33
|
+
const home = process.env.DSH_HOME || path.join(os.homedir(), '.dsh')
|
|
34
|
+
const profile = process.env.DSH_PROFILE
|
|
35
|
+
if (profile) candidates.push(path.join(home, 'profiles', profile))
|
|
36
|
+
for (const dir of candidates) {
|
|
37
|
+
try {
|
|
38
|
+
const pkg = JSON.parse(readFileSync(path.join(dir, 'package.json'), 'utf8'))
|
|
39
|
+
const deps = Object.keys(pkg.dependencies || {})
|
|
40
|
+
const bundles = (pkg.dsh && pkg.dsh.profile && pkg.dsh.profile.bundles) || []
|
|
41
|
+
return [...deps, ...bundles].filter(Boolean)
|
|
42
|
+
} catch { /* try next candidate */ }
|
|
43
|
+
}
|
|
44
|
+
return []
|
|
45
|
+
}
|
package/lib/config-schema.js
CHANGED
|
@@ -78,6 +78,16 @@ export const Config = z.object({
|
|
|
78
78
|
.description('#91 Ollama model used for the fallback (for example qwen2.5-coder). Empty = first model from /api/tags.'),
|
|
79
79
|
hideDeprecatedModels: z.boolean().volatile().default(false)
|
|
80
80
|
.description('#94 Hide test/preview/beta/legacy model ids from the native model picker.'),
|
|
81
|
+
modelDisplayNames: z.array(z.object({ provider: z.string(), id: z.string(), name: z.string() })).volatile().default([])
|
|
82
|
+
.description('#681 Per-model display names shown in the native model picker.'),
|
|
83
|
+
hiddenModels: z.array(z.object({ provider: z.string(), id: z.string() })).volatile().default([])
|
|
84
|
+
.description('#681 Per-model visibility switches for the native model picker.'),
|
|
85
|
+
modelContextWindows: z.array(z.object({ provider: z.string(), id: z.string(), contextWindow: z.number() })).volatile().default([])
|
|
86
|
+
.description('#681 Per-model context-window override.'),
|
|
87
|
+
modelReasoningDefaults: z.array(z.object({ provider: z.string(), id: z.string(), effort: z.string() })).volatile().default([])
|
|
88
|
+
.description('#681 Per-model default reasoning effort.'),
|
|
89
|
+
modelPlans: z.array(z.object({ provider: z.string(), id: z.string(), minPlan: z.string(), plan: z.string() })).volatile().default([])
|
|
90
|
+
.description('#690 Minimum subscription plan per model, shown in the picker metadata.'),
|
|
81
91
|
codexClientId: z.string().volatile().default(''),
|
|
82
92
|
codexVerbosity: z.string().volatile().default('')
|
|
83
93
|
.description('#93 Response verbosity for Codex reasoning models: low, medium or high. Empty = protocol default.'),
|
|
@@ -85,6 +95,12 @@ export const Config = z.object({
|
|
|
85
95
|
.description('#92 Fast Mode for Codex: sends service_tier priority (1.5x speed billing tier) with every request.'),
|
|
86
96
|
composerQuota: z.string().volatile().default('off')
|
|
87
97
|
.description('#84 Composer quota indicator mode: off, percent, bar or forecast (predictive runway from a sliding window).'),
|
|
98
|
+
composerQuotaDisplay: z.string().volatile().default('current')
|
|
99
|
+
.description('#682 Status-bar quota scope: current (model provider) or hidden.'),
|
|
100
|
+
rateLimitWait: z.boolean().volatile().default(true)
|
|
101
|
+
.description('#684 Report the disclosed reset to dsh-llm-retry instead of parking the account on a guess.'),
|
|
102
|
+
rateLimitMaxWaitMs: z.number().volatile().default(21600000)
|
|
103
|
+
.description('#684 Maximum reset wait reported to dsh-llm-retry (default 6h).'),
|
|
88
104
|
codexRedirectUri: z.string().volatile().default(''),
|
|
89
105
|
codexBaseUrl: z.string().volatile().default(''),
|
|
90
106
|
codexClientVersion: z.string().volatile().default('')
|
package/lib/index.js
CHANGED
|
@@ -14,6 +14,8 @@ import { parseBlob } from './blob.js'
|
|
|
14
14
|
import { createVendorFromProfile, validateProfiles } from './vendor-factory.js'
|
|
15
15
|
import { registerCustomProviderIds, registerDisplayName } from './refs.js'
|
|
16
16
|
import { createSubscriptionsService } from './subscriptions.js'
|
|
17
|
+
import { registerSubscriptionTools } from './native-tools.js'
|
|
18
|
+
import { detectConflicts, readProfilePluginNames, setActiveConflicts } from './coexistence.js'
|
|
17
19
|
import { createAccountStore, normalizeSlots, vendorConfig } from './accounts.js'
|
|
18
20
|
import { OllamaAdapter, ollamaAlive, ollamaModels, ollamaBase } from './ollama.js'
|
|
19
21
|
import { createResetCreditService } from './reset-credits.js'
|
|
@@ -39,7 +41,7 @@ export { pushToWebdav, pullFromWebdav, pushToGist, pullFromGist, buildS3SigV4Hea
|
|
|
39
41
|
import { syncVaultPush, syncVaultPull, getVaultSyncStatus } from './vault-sync.js'
|
|
40
42
|
|
|
41
43
|
export const name = '@goodandready/dsh-subscriptions'
|
|
42
|
-
export const inject = ['llm', 'credentials', 'webServer', 'settings']
|
|
44
|
+
export const inject = ['llm', 'credentials', 'webServer', 'settings', 'tools']
|
|
43
45
|
|
|
44
46
|
const NS = 'dsh-subscriptions'
|
|
45
47
|
|
|
@@ -75,6 +77,11 @@ const OK_HTML = `<!doctype html>
|
|
|
75
77
|
|
|
76
78
|
export function apply(ctx, rawConfig) {
|
|
77
79
|
const logger = typeof ctx.logger === 'function' ? ctx.logger('subscriptions') : (ctx.logger || undefined)
|
|
80
|
+
const conflicts = detectConflicts({ dependencies: readProfilePluginNames() })
|
|
81
|
+
setActiveConflicts(conflicts)
|
|
82
|
+
if (conflicts.length && logger && logger.warn) {
|
|
83
|
+
logger.warn('[dsh-subscriptions] conflicting subscription plugins detected: ' + conflicts.join(', ') + '; keep one provider owner')
|
|
84
|
+
}
|
|
78
85
|
|
|
79
86
|
// rawConfig still holds Volatile boxes, so unwrap before validating.
|
|
80
87
|
const resolveEntry = () => {
|
|
@@ -377,6 +384,10 @@ export function apply(ctx, rawConfig) {
|
|
|
377
384
|
})
|
|
378
385
|
},
|
|
379
386
|
}), 'dsh-subscriptions: image generation service')
|
|
387
|
+
ctx.effect(() => registerSubscriptionTools(ctx, {
|
|
388
|
+
subscriptions,
|
|
389
|
+
images: () => ctx.subscriptionImages,
|
|
390
|
+
}), 'dsh-subscriptions: native tools')
|
|
380
391
|
|
|
381
392
|
const pending = new Map()
|
|
382
393
|
|
package/lib/messages.js
CHANGED
|
@@ -301,7 +301,7 @@ export function codexResponsesBody(options, fallbackInstructions, vendorCfg) {
|
|
|
301
301
|
...(vendorCfg && /^(low|medium|high)$/.test(String(vendorCfg.verbosity || ''))
|
|
302
302
|
? { text: { verbosity: String(vendorCfg.verbosity) } } : {}),
|
|
303
303
|
// #92: Fast Mode = 1.5x speed billing tier on the Codex backend.
|
|
304
|
-
...(vendorCfg && vendorCfg.fastMode ? { service_tier: 'priority' } : {}),
|
|
304
|
+
...((vendorCfg && vendorCfg.fastMode) || options.service_tier === 'priority' ? { service_tier: 'priority' } : {}),
|
|
305
305
|
...(options.maxTokens != null ? { max_output_tokens: options.maxTokens } : {}),
|
|
306
306
|
...(options.temperature != null ? { temperature: options.temperature } : {}),
|
|
307
307
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
function rowsFor(rows, provider) {
|
|
2
|
+
return (Array.isArray(rows) ? rows : []).filter((row) => row && row.provider === provider)
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
const PLAN_RANK = { free: 0, plus: 1, pro: 2, max: 3, team: 3, enterprise: 4 }
|
|
6
|
+
|
|
7
|
+
export function planRank(name) {
|
|
8
|
+
return PLAN_RANK[String(name || '').toLowerCase()] ?? 0
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function bestAccountPlan(accounts) {
|
|
12
|
+
let best = null
|
|
13
|
+
for (const account of accounts || []) {
|
|
14
|
+
const plan = account && (account.plan || account.paidTierName)
|
|
15
|
+
if (!plan) continue
|
|
16
|
+
if (!best || planRank(plan) > planRank(best)) best = String(plan)
|
|
17
|
+
}
|
|
18
|
+
return best
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function applyModelSettings(provider, models, cfg = {}, accountPlan = null) {
|
|
22
|
+
const list = Array.isArray(models) ? models : []
|
|
23
|
+
const hidden = new Set(rowsFor(cfg.hiddenModels, provider).map((row) => String(row.id)))
|
|
24
|
+
const plans = new Map(rowsFor(cfg.modelPlans, provider).map((row) => [String(row.id), String(row.minPlan || '')]))
|
|
25
|
+
const accountRank = accountPlan ? planRank(accountPlan) : null
|
|
26
|
+
const names = new Map(rowsFor(cfg.modelDisplayNames, provider).map((row) => [String(row.id), String(row.name)]))
|
|
27
|
+
const contexts = new Map(rowsFor(cfg.modelContextWindows, provider).map((row) => [String(row.id), Number(row.contextWindow)]))
|
|
28
|
+
const efforts = new Map(rowsFor(cfg.modelReasoningDefaults, provider).map((row) => [String(row.id), String(row.effort).toLowerCase()]))
|
|
29
|
+
return list
|
|
30
|
+
.filter((model) => {
|
|
31
|
+
if (!model || hidden.has(String(model.id))) return false
|
|
32
|
+
const minPlan = plans.get(String(model.id))
|
|
33
|
+
if (minPlan && accountRank != null && planRank(minPlan) > accountRank) return false
|
|
34
|
+
return true
|
|
35
|
+
})
|
|
36
|
+
.map((model) => {
|
|
37
|
+
const id = String(model.id)
|
|
38
|
+
const minPlan = plans.get(id)
|
|
39
|
+
const contextWindow = contexts.get(id)
|
|
40
|
+
const effort = efforts.get(id)
|
|
41
|
+
return {
|
|
42
|
+
...model,
|
|
43
|
+
name: names.get(id) || model.name || model.id,
|
|
44
|
+
...(Number.isFinite(contextWindow) && contextWindow > 0 ? { contextWindow } : {}),
|
|
45
|
+
...(effort ? { reasoning: { ...(model.reasoning || {}), defaultEffort: effort } } : {}),
|
|
46
|
+
...(minPlan ? { plan: minPlan, minPlan } : {}),
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
const TOOL_OUTPUT = {
|
|
5
|
+
schema: {
|
|
6
|
+
type: 'object',
|
|
7
|
+
additionalProperties: true,
|
|
8
|
+
properties: { text: { type: 'string' } },
|
|
9
|
+
required: ['text'],
|
|
10
|
+
},
|
|
11
|
+
render(_args, value) {
|
|
12
|
+
return [{ type: 'text', text: String((value && value.text) || '') }]
|
|
13
|
+
},
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function textResult(text) {
|
|
17
|
+
return { text: String(text) }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function extractResponseText(payload) {
|
|
21
|
+
const body = payload && typeof payload === 'object' ? payload : {}
|
|
22
|
+
if (typeof body.output_text === 'string' && body.output_text) return body.output_text
|
|
23
|
+
if (typeof body.answer === 'string' && body.answer) return body.answer
|
|
24
|
+
if (Array.isArray(body.output)) {
|
|
25
|
+
const parts = []
|
|
26
|
+
for (const item of body.output) {
|
|
27
|
+
for (const content of (item && item.content) || []) {
|
|
28
|
+
if (content && typeof content.text === 'string') parts.push(content.text)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (parts.length) return parts.join('\n')
|
|
32
|
+
}
|
|
33
|
+
if (Array.isArray(body.choices)) {
|
|
34
|
+
return body.choices.map((choice) => choice && choice.message && choice.message.content)
|
|
35
|
+
.filter((value) => typeof value === 'string').join('\n')
|
|
36
|
+
}
|
|
37
|
+
return JSON.stringify(body).slice(0, 4000)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function outputDir(exec) {
|
|
41
|
+
const cwd = (exec && exec.agent && exec.agent.session && exec.agent.session.header
|
|
42
|
+
&& exec.agent.session.header.cwd) || process.cwd()
|
|
43
|
+
return path.join(cwd, 'generated', 'subscriptions')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function saveImages(images, exec) {
|
|
47
|
+
const dir = outputDir(exec)
|
|
48
|
+
await mkdir(dir, { recursive: true })
|
|
49
|
+
const out = []
|
|
50
|
+
for (let i = 0; i < images.length; i += 1) {
|
|
51
|
+
const image = images[i]
|
|
52
|
+
if (!image || typeof image.b64_json !== 'string') continue
|
|
53
|
+
const file = path.join(dir, `image-${Date.now()}-${i}.png`)
|
|
54
|
+
await writeFile(file, Buffer.from(image.b64_json, 'base64'))
|
|
55
|
+
out.push(file)
|
|
56
|
+
}
|
|
57
|
+
return out
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function registerSubscriptionTools(ctx, deps) {
|
|
61
|
+
const tools = ctx && ctx.tools
|
|
62
|
+
if (!tools || typeof tools.register !== 'function') return () => {}
|
|
63
|
+
const disposers = []
|
|
64
|
+
const register = (definition) => {
|
|
65
|
+
try {
|
|
66
|
+
const off = tools.register(definition)
|
|
67
|
+
if (typeof off === 'function') disposers.push(off)
|
|
68
|
+
} catch (err) {
|
|
69
|
+
// Another plugin may already own this tool name (for example x_search
|
|
70
|
+
// from a Grok search plugin or image_generate from an image plugin).
|
|
71
|
+
// Keep our remaining tools registered instead of failing the whole plugin.
|
|
72
|
+
if (!/already registered/i.test(String(err && err.message || err))) throw err
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
register({
|
|
77
|
+
name: 'image_generate',
|
|
78
|
+
description: 'Generate an image with the active ChatGPT/Codex or Grok subscription. Returns saved file paths.',
|
|
79
|
+
output: TOOL_OUTPUT,
|
|
80
|
+
parameters: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: {
|
|
83
|
+
prompt: { type: 'string', description: 'Image prompt' },
|
|
84
|
+
provider: { type: 'string', enum: ['gpt', 'codex', 'grok'], description: 'Preferred provider' },
|
|
85
|
+
size: { type: 'string', description: 'Image size, e.g. 1024x1024' },
|
|
86
|
+
quality: { type: 'string', description: 'low, medium or high' },
|
|
87
|
+
},
|
|
88
|
+
required: ['prompt'],
|
|
89
|
+
},
|
|
90
|
+
async execute(args, exec) {
|
|
91
|
+
const images = deps.images && deps.images()
|
|
92
|
+
if (!images || typeof images.generate !== 'function') throw new Error('subscription image service is unavailable')
|
|
93
|
+
const preferred = String((args && args.provider) || 'gpt').toLowerCase()
|
|
94
|
+
const order = preferred === 'grok' ? ['grok', 'codex'] : ['codex', 'grok']
|
|
95
|
+
let lastError = null
|
|
96
|
+
for (const provider of order) {
|
|
97
|
+
try {
|
|
98
|
+
const rows = await images.generate({
|
|
99
|
+
provider,
|
|
100
|
+
prompt: args && args.prompt,
|
|
101
|
+
size: args && args.size,
|
|
102
|
+
quality: args && args.quality,
|
|
103
|
+
signal: exec && exec.signal,
|
|
104
|
+
})
|
|
105
|
+
const files = await saveImages(rows, exec)
|
|
106
|
+
if (!files.length) throw new Error('image response contained no images')
|
|
107
|
+
return textResult(files.map((file) => `Generated image: ${file}`).join('\n'))
|
|
108
|
+
} catch (err) {
|
|
109
|
+
lastError = err
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
throw lastError || new Error('no subscription image provider is available')
|
|
113
|
+
},
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
register({
|
|
117
|
+
name: 'x_search',
|
|
118
|
+
description: 'Search X posts through the active Grok subscription and return an answer with citations.',
|
|
119
|
+
output: TOOL_OUTPUT,
|
|
120
|
+
parameters: {
|
|
121
|
+
type: 'object',
|
|
122
|
+
properties: {
|
|
123
|
+
query: { type: 'string', description: 'Search query' },
|
|
124
|
+
model: { type: 'string', description: 'Optional Grok model id' },
|
|
125
|
+
},
|
|
126
|
+
required: ['query'],
|
|
127
|
+
},
|
|
128
|
+
async execute(args, exec) {
|
|
129
|
+
const res = await deps.subscriptions.request({
|
|
130
|
+
provider: 'grok',
|
|
131
|
+
path: '/v1/responses',
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
134
|
+
body: {
|
|
135
|
+
model: (args && args.model) || 'grok-4.6',
|
|
136
|
+
input: args && args.query,
|
|
137
|
+
tools: [{ type: 'x_search' }],
|
|
138
|
+
stream: false,
|
|
139
|
+
},
|
|
140
|
+
signal: exec && exec.signal,
|
|
141
|
+
})
|
|
142
|
+
const data = await res.json().catch(() => ({}))
|
|
143
|
+
if (!res.ok) throw new Error(`x_search HTTP ${res.status}: ${extractResponseText(data)}`)
|
|
144
|
+
return textResult(extractResponseText(data))
|
|
145
|
+
},
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
register({
|
|
149
|
+
name: 'web_search',
|
|
150
|
+
description: 'Search the web through the active ChatGPT/Codex subscription and return an answer with citations.',
|
|
151
|
+
output: TOOL_OUTPUT,
|
|
152
|
+
parameters: {
|
|
153
|
+
type: 'object',
|
|
154
|
+
properties: {
|
|
155
|
+
query: { type: 'string', description: 'Search query' },
|
|
156
|
+
model: { type: 'string', description: 'Optional Codex model id' },
|
|
157
|
+
},
|
|
158
|
+
required: ['query'],
|
|
159
|
+
},
|
|
160
|
+
async execute(args, exec) {
|
|
161
|
+
const res = await deps.subscriptions.request({
|
|
162
|
+
provider: 'codex',
|
|
163
|
+
path: '/responses',
|
|
164
|
+
method: 'POST',
|
|
165
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
166
|
+
body: {
|
|
167
|
+
model: (args && args.model) || 'gpt-5.6',
|
|
168
|
+
input: args && args.query,
|
|
169
|
+
tools: [{ type: 'web_search_preview' }],
|
|
170
|
+
stream: false,
|
|
171
|
+
},
|
|
172
|
+
signal: exec && exec.signal,
|
|
173
|
+
})
|
|
174
|
+
const data = await res.json().catch(() => ({}))
|
|
175
|
+
if (!res.ok) throw new Error(`web_search HTTP ${res.status}: ${extractResponseText(data)}`)
|
|
176
|
+
return textResult(extractResponseText(data))
|
|
177
|
+
},
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
register({
|
|
181
|
+
name: 'video_generate',
|
|
182
|
+
description: 'Submit a short video generation request through the active Grok subscription.',
|
|
183
|
+
output: TOOL_OUTPUT,
|
|
184
|
+
parameters: {
|
|
185
|
+
type: 'object',
|
|
186
|
+
properties: {
|
|
187
|
+
prompt: { type: 'string', description: 'Video prompt' },
|
|
188
|
+
duration: { type: 'number', description: 'Duration in seconds (1-15)' },
|
|
189
|
+
aspect_ratio: { type: 'string', description: 'e.g. 16:9, 1:1, 9:16' },
|
|
190
|
+
image_url: { type: 'string', description: 'Optional source image URL' },
|
|
191
|
+
},
|
|
192
|
+
required: ['prompt'],
|
|
193
|
+
},
|
|
194
|
+
async execute(args, exec) {
|
|
195
|
+
const res = await deps.subscriptions.request({
|
|
196
|
+
provider: 'grok',
|
|
197
|
+
path: '/v1/videos',
|
|
198
|
+
method: 'POST',
|
|
199
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
200
|
+
body: {
|
|
201
|
+
model: 'grok-imagine-video-1.5',
|
|
202
|
+
prompt: args && args.prompt,
|
|
203
|
+
duration: args && args.duration,
|
|
204
|
+
aspect_ratio: args && args.aspect_ratio,
|
|
205
|
+
image_url: args && args.image_url,
|
|
206
|
+
},
|
|
207
|
+
signal: exec && exec.signal,
|
|
208
|
+
})
|
|
209
|
+
const data = await res.json().catch(() => ({}))
|
|
210
|
+
if (!res.ok) throw new Error(`video_generate HTTP ${res.status}: ${extractResponseText(data)}`)
|
|
211
|
+
return textResult(`Video request submitted: ${JSON.stringify(data).slice(0, 1000)}`)
|
|
212
|
+
},
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
return () => {
|
|
216
|
+
for (const off of disposers) {
|
|
217
|
+
try { off() } catch { /* best-effort cleanup */ }
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
package/lib/reset-clock.js
CHANGED
|
@@ -80,15 +80,25 @@ export function resolveAccurateReset(err, account, provider, nowMs = Date.now(),
|
|
|
80
80
|
return targetReset + (Number(safetyBufferMs) || 0)
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (windowDefault && err && (err.status === 429 || err.code === 'RATE_LIMIT' || err.code === 'QUOTA')) {
|
|
86
|
-
return now + windowDefault + (Number(safetyBufferMs) || 0)
|
|
87
|
-
}
|
|
88
|
-
|
|
83
|
+
// #684: a 429 without a disclosed reset is a local backoff, not a whole
|
|
84
|
+
// vendor window. Parking on a guessed 2-24h window burns a healthy account.
|
|
89
85
|
return null
|
|
90
86
|
}
|
|
91
87
|
|
|
88
|
+
export function nextCooldownMs(accounts, now = Date.now()) {
|
|
89
|
+
const current = Number(now) || Date.now()
|
|
90
|
+
let next = null
|
|
91
|
+
for (const account of accounts || []) {
|
|
92
|
+
if (!account) continue
|
|
93
|
+
const candidates = [account.cooldownUntil, account.quarantineUntil, account.quota && account.quota.resetAt]
|
|
94
|
+
for (const value of candidates) {
|
|
95
|
+
const ts = Number(value)
|
|
96
|
+
if (Number.isFinite(ts) && ts > current) next = next === null ? ts : Math.min(next, ts)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return next === null ? null : next - current
|
|
100
|
+
}
|
|
101
|
+
|
|
92
102
|
export function formatRollingCountdown(resetAt, lang = 'en', now = Date.now()) {
|
|
93
103
|
const ts = Number(resetAt)
|
|
94
104
|
if (!Number.isFinite(ts) || ts <= 0) return ''
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function buildRouteReport(last, accounts, now = Date.now()) {
|
|
2
|
+
if (!last || !last.provider) return null
|
|
3
|
+
const current = Number(now) || Date.now()
|
|
4
|
+
const account = (Array.isArray(accounts) ? accounts : []).find((row) => row && row.ref === last.ref)
|
|
5
|
+
const source = (account && account.quota && account.quota.source) || account && account.quotaSource || 'unknown'
|
|
6
|
+
return {
|
|
7
|
+
at: last.at || last.timestamp || null,
|
|
8
|
+
route: {
|
|
9
|
+
provider: last.provider,
|
|
10
|
+
model: last.model || null,
|
|
11
|
+
path: last.path || null,
|
|
12
|
+
account: last.ref || null,
|
|
13
|
+
},
|
|
14
|
+
billing: {
|
|
15
|
+
provider: last.billingProvider || last.provider,
|
|
16
|
+
route: last.billingRoute || last.path || null,
|
|
17
|
+
},
|
|
18
|
+
quota: {
|
|
19
|
+
usedPercent: account && account.usagePercent != null ? account.usagePercent : null,
|
|
20
|
+
source,
|
|
21
|
+
ageMs: account && account.usageAt ? Math.max(0, current - Number(account.usageAt)) : null,
|
|
22
|
+
resetAt: (account && account.quota && account.quota.resetAt) || null,
|
|
23
|
+
confidence: source === 'unknown' ? 'unknown' : 'measured',
|
|
24
|
+
},
|
|
25
|
+
healthScore: account && account.healthScore != null ? account.healthScore : null,
|
|
26
|
+
stale: Boolean(account && account.usageAt && current - Number(account.usageAt) > 5 * 60 * 1000),
|
|
27
|
+
}
|
|
28
|
+
}
|
package/lib/routes/status.js
CHANGED
|
@@ -10,6 +10,10 @@ import {
|
|
|
10
10
|
} from '../reasoning-effort.js'
|
|
11
11
|
import { defaultConcurrencyQueue } from '../concurrency-queue.js'
|
|
12
12
|
import { calculateAnalytics } from '../analytics.js'
|
|
13
|
+
import { setSpeedMode, getSpeedMode, clearSpeedMode } from '../speed-mode.js'
|
|
14
|
+
import { getActiveConflicts } from '../coexistence.js'
|
|
15
|
+
import { buildUsageRows, buildCallRows, toCsv } from '../usage-export.js'
|
|
16
|
+
import { buildRouteReport } from '../route-report.js'
|
|
13
17
|
import { runDiagnosticWizard } from '../diagnostic-wizard.js'
|
|
14
18
|
import { buildEnrichedCatalog } from '../models-catalog.js'
|
|
15
19
|
// #374: Sensitive GET routes protected by same-origin check
|
|
@@ -254,7 +258,7 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
254
258
|
labels,
|
|
255
259
|
expiryNotifyDays: live().expiryNotifyDays,
|
|
256
260
|
fastMode: !!live().codexFastMode,
|
|
257
|
-
composerQuota: String(live().composerQuota || 'off'),
|
|
261
|
+
composerQuota: String(live().composerQuota || 'off'), composerQuotaDisplay: String(live().composerQuotaDisplay || 'current'),
|
|
258
262
|
active,
|
|
259
263
|
})
|
|
260
264
|
}),
|
|
@@ -297,7 +301,14 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
297
301
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
298
302
|
return
|
|
299
303
|
}
|
|
300
|
-
|
|
304
|
+
const accounts = await accountsView()
|
|
305
|
+
const last = history.recent(1)[0]
|
|
306
|
+
writeJson(res, 200, {
|
|
307
|
+
ok: true,
|
|
308
|
+
report: await diagnosticsReport(),
|
|
309
|
+
conflicts: getActiveConflicts(),
|
|
310
|
+
route: buildRouteReport(last, accounts),
|
|
311
|
+
})
|
|
301
312
|
}),
|
|
302
313
|
}), 'dsh-subscriptions: /diagnostics')
|
|
303
314
|
|
|
@@ -436,6 +447,45 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
436
447
|
}),
|
|
437
448
|
}), 'dsh-subscriptions: /history')
|
|
438
449
|
|
|
450
|
+
// #687: quota snapshot and per-call ledger export for DSH quota plugins.
|
|
451
|
+
ctx.effect(() => ctx.webServer.register({
|
|
452
|
+
kind: 'exact',
|
|
453
|
+
path: '/dsh-subscriptions/usage',
|
|
454
|
+
handler: safeJsonHandler(async (req, res) => {
|
|
455
|
+
if (req.method !== 'GET') {
|
|
456
|
+
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
|
|
457
|
+
return
|
|
458
|
+
}
|
|
459
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
460
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
461
|
+
return
|
|
462
|
+
}
|
|
463
|
+
const q = queryOf(req)
|
|
464
|
+
const type = q.get('type') === 'calls' ? 'calls' : 'accounts'
|
|
465
|
+
const format = String(q.get('format') || 'json').toLowerCase()
|
|
466
|
+
const now = Date.now()
|
|
467
|
+
const accounts = await accountsView()
|
|
468
|
+
const calls = history.recent(500)
|
|
469
|
+
const rows = type === 'calls' ? buildCallRows(calls) : buildUsageRows(accounts, now)
|
|
470
|
+
if (format === 'csv') {
|
|
471
|
+
res.writeHead(200, {
|
|
472
|
+
'Content-Type': 'text/csv; charset=utf-8',
|
|
473
|
+
'Content-Disposition': 'attachment; filename="dsh-subscriptions-' + type + '.csv"',
|
|
474
|
+
'Cache-Control': 'no-store',
|
|
475
|
+
})
|
|
476
|
+
res.end(toCsv(rows))
|
|
477
|
+
return
|
|
478
|
+
}
|
|
479
|
+
writeJson(res, 200, {
|
|
480
|
+
ok: true,
|
|
481
|
+
generatedAt: now,
|
|
482
|
+
type,
|
|
483
|
+
rows,
|
|
484
|
+
summary: { accounts: Array.isArray(accounts) ? accounts.length : 0, calls: Array.isArray(calls) ? calls.length : 0 },
|
|
485
|
+
})
|
|
486
|
+
}),
|
|
487
|
+
}), 'dsh-subscriptions: /usage')
|
|
488
|
+
|
|
439
489
|
// Telemetry summary route for UI stat cards (requests, latency, success rate)
|
|
440
490
|
ctx.effect(() => ctx.webServer.register({
|
|
441
491
|
kind: 'exact',
|
|
@@ -716,6 +766,41 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
716
766
|
}),
|
|
717
767
|
}), 'dsh-subscriptions: /queue')
|
|
718
768
|
|
|
769
|
+
// #681: per-session Codex Speed/Fast toggle.
|
|
770
|
+
ctx.effect(() => ctx.webServer.register({
|
|
771
|
+
kind: 'exact',
|
|
772
|
+
path: '/dsh-subscriptions/speed',
|
|
773
|
+
handler: safeJsonHandler(async (req, res) => {
|
|
774
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
775
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
776
|
+
return
|
|
777
|
+
}
|
|
778
|
+
if (req.method === 'GET') {
|
|
779
|
+
const sid = queryOf(req).get('sessionId') || 'default'
|
|
780
|
+
writeJson(res, 200, { ok: true, sessionId: sid, mode: getSpeedMode(sid) || 'standard' })
|
|
781
|
+
return
|
|
782
|
+
}
|
|
783
|
+
if (req.method !== 'POST') {
|
|
784
|
+
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET or POST only' } })
|
|
785
|
+
return
|
|
786
|
+
}
|
|
787
|
+
let body = {}
|
|
788
|
+
try { body = JSON.parse((await readBody(req, 16 * 1024)).toString('utf8') || '{}') } catch {
|
|
789
|
+
writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
|
|
790
|
+
return
|
|
791
|
+
}
|
|
792
|
+
const sid = String(body.sessionId || 'default').slice(0, 200)
|
|
793
|
+
const mode = String(body.mode || '').toLowerCase()
|
|
794
|
+
if (mode !== 'fast' && mode !== 'standard' && mode !== 'off') {
|
|
795
|
+
writeJson(res, 400, { ok: false, error: { code: 'mode', message: 'mode must be fast or standard' } })
|
|
796
|
+
return
|
|
797
|
+
}
|
|
798
|
+
if (mode === 'fast') setSpeedMode(sid, 'fast')
|
|
799
|
+
else clearSpeedMode(sid)
|
|
800
|
+
writeJson(res, 200, { ok: true, sessionId: sid, mode: mode === 'fast' ? 'fast' : 'standard' })
|
|
801
|
+
}),
|
|
802
|
+
}), 'dsh-subscriptions: /speed')
|
|
803
|
+
|
|
719
804
|
// #519: Interactive Diagnostic Wizard for accounts and connectivity
|
|
720
805
|
ctx.effect(() => ctx.webServer.register({
|
|
721
806
|
kind: 'exact',
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const modes = new Map()
|
|
2
|
+
|
|
3
|
+
export function setSpeedMode(sessionId, mode) {
|
|
4
|
+
const id = String(sessionId || '').trim()
|
|
5
|
+
if (!id) return
|
|
6
|
+
const value = String(mode || '').toLowerCase()
|
|
7
|
+
if (value === 'fast') modes.set(id, 'fast')
|
|
8
|
+
else modes.delete(id)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getSpeedMode(sessionId) {
|
|
12
|
+
return modes.get(String(sessionId || '').trim()) || null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function clearSpeedMode(sessionId) {
|
|
16
|
+
modes.delete(String(sessionId || '').trim())
|
|
17
|
+
}
|
package/lib/stream-rotate.js
CHANGED
|
@@ -7,7 +7,7 @@ import { notifySessionExpired, notifyQuarantineEntered } from './alerts.js'
|
|
|
7
7
|
import { withRunawayGuard } from './runaway-guard.js'
|
|
8
8
|
import { defaultErrorBuffer } from './error-buffer.js'
|
|
9
9
|
import { withSoftFailureSentinel } from './sentinel.js'
|
|
10
|
-
import { resolveAccurateReset } from './reset-clock.js'
|
|
10
|
+
import { resolveAccurateReset, nextCooldownMs } from './reset-clock.js'
|
|
11
11
|
|
|
12
12
|
export async function* streamWithRotation({
|
|
13
13
|
accounts,
|
|
@@ -88,7 +88,15 @@ export async function* streamWithRotation({
|
|
|
88
88
|
return
|
|
89
89
|
}
|
|
90
90
|
syncTrace('exhausted')
|
|
91
|
-
if (lastError)
|
|
91
|
+
if (lastError) {
|
|
92
|
+
const wait = nextCooldownMs(pool, nowMs())
|
|
93
|
+
if (wait != null && options && options.rateLimitWait !== false) {
|
|
94
|
+
const maxWait = Number(options.rateLimitMaxWaitMs) || 6 * 3600 * 1000
|
|
95
|
+
lastError.retryAfterMs = Math.min(wait, maxWait)
|
|
96
|
+
lastError.resetAt = nowMs() + lastError.retryAfterMs
|
|
97
|
+
}
|
|
98
|
+
throw lastError
|
|
99
|
+
}
|
|
92
100
|
const err = /** @type {Error & { code?: string }} */ (new Error('no usable subscription account for this provider'))
|
|
93
101
|
err.code = 'RATE_LIMIT'
|
|
94
102
|
throw err
|
package/lib/subscriptions.js
CHANGED
|
@@ -20,7 +20,7 @@ const OPENAI_COMPAT = [
|
|
|
20
20
|
export const ALLOWLIST = {
|
|
21
21
|
codex: ["/responses", "/models", "/images/generations", "/backend-api/codex/responses", "/backend-api/codex/models", "/backend-api/codex/images/generations"],
|
|
22
22
|
claude: ["/v1/messages", "/api/oauth/profile", "/api/oauth/usage", "/v1/models"],
|
|
23
|
-
grok: ["/v1/models", "/responses", "/v1/responses", "/v1/billing", "/v1/chat/completions", "/images/generations", "/v1/images/generations"],
|
|
23
|
+
grok: ["/v1/models", "/responses", "/v1/responses", "/v1/billing", "/v1/chat/completions", "/images/generations", "/v1/images/generations", "/v1/videos"],
|
|
24
24
|
antigravity: ["/v1/models", "/v1/generateContent", "/v1/streamGenerateContent", "/v1/loadCodeAssist", "/v1/fetchAvailableModels", "/v1/countTokens", "/v1/internal:loadCodeAssist"],
|
|
25
25
|
kimi: OPENAI_COMPAT,
|
|
26
26
|
glm: [...OPENAI_COMPAT, "/api/paas/v4/chat/completions", "/api/paas/v4/models"],
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
function csvCell(value) {
|
|
2
|
+
const text = value == null ? '' : String(value)
|
|
3
|
+
return /[",\n\r]/.test(text) ? '"' + text.replace(/"/g, '""') + '"' : text
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function toCsv(rows) {
|
|
7
|
+
const list = Array.isArray(rows) ? rows : []
|
|
8
|
+
if (!list.length) return ''
|
|
9
|
+
const headers = Object.keys(list[0])
|
|
10
|
+
const lines = [headers.map(csvCell).join(',')]
|
|
11
|
+
for (const row of list) lines.push(headers.map((key) => csvCell(row[key])).join(','))
|
|
12
|
+
return lines.join('\n') + '\n'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function buildUsageRows(accounts, now = Date.now()) {
|
|
16
|
+
const current = Number(now) || Date.now()
|
|
17
|
+
return (Array.isArray(accounts) ? accounts : []).map((account) => ({
|
|
18
|
+
ref: account.ref || '',
|
|
19
|
+
provider: account.provider || '',
|
|
20
|
+
index: account.index ?? '',
|
|
21
|
+
label: account.label || '',
|
|
22
|
+
plan: account.plan || account.paidTierName || '',
|
|
23
|
+
configured: Boolean(account.hasToken || account.configured),
|
|
24
|
+
usagePercent: account.usagePercent ?? '',
|
|
25
|
+
remainingPercent: account.usagePercent != null ? Math.max(0, 100 - Number(account.usagePercent)) : '',
|
|
26
|
+
quotaSource: (account.quota && account.quota.source) || 'unknown',
|
|
27
|
+
usageAgeMs: account.usageAt ? Math.max(0, current - Number(account.usageAt)) : '',
|
|
28
|
+
resetAt: (account.quota && account.quota.resetAt) || '',
|
|
29
|
+
cooldownUntil: account.cooldownUntil || '',
|
|
30
|
+
quarantineUntil: account.quarantineUntil || '',
|
|
31
|
+
quarantineReason: account.quarantineReason || '',
|
|
32
|
+
healthScore: account.healthScore ?? '',
|
|
33
|
+
monthlyCost: account.monthlyCost ?? '',
|
|
34
|
+
}))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function buildCallRows(calls) {
|
|
38
|
+
return (Array.isArray(calls) ? calls : []).map((call) => ({
|
|
39
|
+
at: call.at || call.timestamp || '',
|
|
40
|
+
provider: call.provider || '',
|
|
41
|
+
ref: call.ref || '',
|
|
42
|
+
model: call.model || '',
|
|
43
|
+
path: call.path || '',
|
|
44
|
+
ok: call.ok !== false,
|
|
45
|
+
status: call.status ?? '',
|
|
46
|
+
inputTokens: call.inputTokens ?? '',
|
|
47
|
+
outputTokens: call.outputTokens ?? '',
|
|
48
|
+
cacheReadTokens: call.cacheReadTokens ?? '',
|
|
49
|
+
durationMs: call.durationMs ?? '',
|
|
50
|
+
error: call.error || '',
|
|
51
|
+
}))
|
|
52
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.2",
|
|
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",
|