@goodandready/dsh-subscriptions 0.6.64 → 0.6.65
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/README.md +6 -0
- package/README.ru.md +6 -0
- package/lib/adapter.js +12 -2
- package/lib/client.js +30 -2
- package/lib/diagnostics.js +2 -0
- package/lib/messages.js +20 -2
- package/lib/reasoning-effort.js +323 -0
- package/lib/routes/status.js +76 -0
- package/lib/vendors/claude.js +25 -6
- package/lib/wire.js +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -207,6 +207,12 @@ const res = await ctx.subscriptions.request('codex', '/backend-api/codex/images/
|
|
|
207
207
|
* **In-App Toast Alerts (`enableToasts`)**: Lightweight floating toast alerts in the DSH chat interface notifying you when accounts enter quarantine, recover from cooldowns, or exceed quota safety thresholds.
|
|
208
208
|
* **Automated Webhook Integration**: Instant event notifications dispatched via `notifyQuarantineEntered` and `notifyQuarantineReleased`.
|
|
209
209
|
|
|
210
|
+
### 19. 🧠 Session Reasoning Effort Management & Contract Matrix (v0.6.65)
|
|
211
|
+
* **Session-Scoped Reasoning Effort Control (#505)**: Dynamically adjust reasoning effort (`low`, `medium`, `high`, `max`) or disable reasoning (`off`) per conversation session via REST endpoints (`GET`, `POST`, `DELETE /dsh-subscriptions/session-reasoning`).
|
|
212
|
+
* **Model-Switch Invariant & Safety Guard**: Automatic suppression of reasoning parameters when switching from a reasoning model (e.g. `o1`) to a standard model (e.g. `gpt-4o`), avoiding upstream HTTP 400 parameter errors.
|
|
213
|
+
* **Extended Multi-Vendor Capability**: Native reasoning support across Codex (`o1`, `o3`), Claude (`claude-3-7-sonnet*`, `claude-4.6+` thinking tokens & budgets), Antigravity/Gemini (`thinkingConfig`), Grok (`grok-3-mini`), and Kimi (`k1.5`).
|
|
214
|
+
* **Comprehensive Adapter Contract Matrix (#521)**: Automated offline wire contract verification (`test/contract-matrix.test.mjs`) testing 8 providers across 7 wire protocol capabilities (`stream`, `tools`, `reasoning`, `usage`, `caching`, `auth_errors`, `rate_limit`).
|
|
215
|
+
|
|
210
216
|
### 18. 🔑 Antigravity Client Secret & Resilient CLI Refresh (v0.6.60)
|
|
211
217
|
* **Client Secret in Schema (`antigravityClientSecret`)**: Configure custom Google Cloud OAuth client secrets with automatic mask redaction (`••••••`), resolving token refresh rejections for confidential OAuth clients.
|
|
212
218
|
* **Zero-Config CLI Token Refresh**: Bundled public application credentials for the standard Gemini / Antigravity CLI client (`Google Gemini CLI Client ID` / `Google Gemini CLI Client Secret`), ensuring imported CLI sessions keep refreshing indefinitely without dying after 1 hour.
|
package/README.ru.md
CHANGED
|
@@ -183,6 +183,12 @@ const res = await ctx.subscriptions.request('codex', '/backend-api/codex/images/
|
|
|
183
183
|
* **Всплывающие уведомления (Toast Alerts)**: Легковесные всплывающие тосты в веб-интерфейсе DSH при событиях карантина (с отображением причины), восстановлении доступа и критическом расходе квоты. Включение управляется параметром `enableToasts` (по умолчанию `true`).
|
|
184
184
|
* **Интеграция с вебхуками**: Автоматическая отправка уведомлений через `notifyQuarantineEntered` и `notifyQuarantineReleased`.
|
|
185
185
|
|
|
186
|
+
### 15. 🧠 Управление Reasoning Effort в сессии и матрица контрактов (v0.6.65)
|
|
187
|
+
* **Управление глубиной рассуждений в сессии (#505)**: Динамическая настройка усилия рассуждений (`low`, `medium`, `high`, `max`) или отключение (`off`) в рамках диалога через REST API (`GET`, `POST`, `DELETE /dsh-subscriptions/session-reasoning`).
|
|
188
|
+
* **Безопасность при смене модели**: Автоматическое подавление параметров рассуждений при переключении с reasoning-модели (напр. `o1`) на обычную (напр. `gpt-4o`), предотвращающее ошибки HTTP 400 от upstream API.
|
|
189
|
+
* **Поддержка ведущих вендоров**: Codex (`o1`, `o3`), Claude (`claude-3-7-sonnet*`, `claude-4.6+` thinking budget), Antigravity/Gemini (`thinkingConfig`), Grok (`grok-3-mini`) и Kimi (`k1.5`).
|
|
190
|
+
* **Матрица контрактных проверок адаптеров (#521)**: Тестовый набор (`test/contract-matrix.test.mjs`) для 8 провайдеров по 7 ключевым возможностям протокола (`stream`, `tools`, `reasoning`, `usage`, `caching`, `auth_errors`, `rate_limit`).
|
|
191
|
+
|
|
186
192
|
### 14. 🔑 Antigravity Client Secret и надежный refresh CLI-токенов (v0.6.60)
|
|
187
193
|
* **Client Secret в схеме настроек (`antigravityClientSecret`)**: Поддержка ввода пользовательского Google Cloud OAuth Client Secret с автоматическим маскированием (`••••••`), устраняющая ошибки обновления токенов для конфиденциальных OAuth-клиентов.
|
|
188
194
|
* **Бесшовный refresh сессий из CLI**: Встроенные официальные публичные учетные данные Gemini / Antigravity CLI (`Google Gemini CLI Client ID` / `Google Gemini CLI Client Secret`), предотвращающие отказ Google OAuth через 1 час после импорта через `From CLI`.
|
package/lib/adapter.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveEffectiveReasoning, getModelReasoningCapabilities } from './reasoning-effort.js'
|
|
1
2
|
import { inferModelCapabilities, isDeprecatedId } from './models-catalog.js'
|
|
2
3
|
import { fetchWithTimeout } from "./http.js"
|
|
3
4
|
import { LlmAdapter, LlmError, attributionHeaders } from '@deepseek-ai/dsh-llm'
|
|
@@ -95,6 +96,11 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
95
96
|
async resolveModel(provider, model) {
|
|
96
97
|
const rows = await this.listModels(provider)
|
|
97
98
|
const found = rows.find((row) => row && row.id === model)
|
|
99
|
+
const caps = getModelReasoningCapabilities(provider, model, rows)
|
|
100
|
+
const reasoningMeta = found && found.reasoning ? found.reasoning : (caps.supported ? {
|
|
101
|
+
efforts: caps.levels.map((lvl) => ({ id: lvl, name: lvl.charAt(0).toUpperCase() + lvl.slice(1) })),
|
|
102
|
+
...(caps.defaultEffort ? { defaultEffort: caps.defaultEffort } : {}),
|
|
103
|
+
} : undefined)
|
|
98
104
|
if (found) {
|
|
99
105
|
return {
|
|
100
106
|
provider,
|
|
@@ -103,10 +109,10 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
103
109
|
...(found.description ? { description: found.description } : {}),
|
|
104
110
|
...(found.inputModalities ? { inputModalities: found.inputModalities } : {}),
|
|
105
111
|
...(found.contextWindow ? { context: { contextWindow: found.contextWindow } } : {}),
|
|
106
|
-
...(
|
|
112
|
+
...(reasoningMeta ? { reasoning: reasoningMeta } : {}),
|
|
107
113
|
}
|
|
108
114
|
}
|
|
109
|
-
return { provider, id: model, name: model }
|
|
115
|
+
return { provider, id: model, name: model, ...(reasoningMeta ? { reasoning: reasoningMeta } : {}) }
|
|
110
116
|
}
|
|
111
117
|
|
|
112
118
|
async prepareCall(provider, model, signal) {
|
|
@@ -136,9 +142,13 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
136
142
|
const effectiveDrainSafetyPercent = options.drainSafetyPercent !== undefined
|
|
137
143
|
? options.drainSafetyPercent
|
|
138
144
|
: (typeof deps.drainSafetyPercent === 'function' ? deps.drainSafetyPercent() : (deps.drainSafetyPercent ?? 5))
|
|
145
|
+
const sessionId = options.sessionId || options.session_id || options.conversationId
|
|
146
|
+
const effectiveReasoning = resolveEffectiveReasoning(sessionId, options)
|
|
139
147
|
const decisionTrace = {}
|
|
140
148
|
const streamOptions = {
|
|
141
149
|
...options,
|
|
150
|
+
...(effectiveReasoning.effort ? { reasoningEffort: effectiveReasoning.effort } : {}),
|
|
151
|
+
...(effectiveReasoning.budgetTokens ? { thinkingBudget: effectiveReasoning.budgetTokens } : {}),
|
|
142
152
|
_decisionTrace: decisionTrace,
|
|
143
153
|
autoPacing: effectiveAutoPacing,
|
|
144
154
|
drainBeforeReset: effectiveDrainBeforeReset,
|
package/lib/client.js
CHANGED
|
@@ -609,7 +609,14 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
609
609
|
"wizardRecommendations": "Recommendations & Next Steps",
|
|
610
610
|
"wizardCopy": "Copy Masked Diagnostic Report",
|
|
611
611
|
"cachePinnedBadge": "Cache Pinned",
|
|
612
|
-
"field.enableToasts": "Show popup toast notifications on quarantine or quota events"
|
|
612
|
+
"field.enableToasts": "Show popup toast notifications on quarantine or quota events",
|
|
613
|
+
"reasoningEffortTitle": "Reasoning Effort Control",
|
|
614
|
+
"reasoningEffortDesc": "Configure reasoning depth for models with extended thinking capabilities.",
|
|
615
|
+
"reasoningLevelLow": "Low",
|
|
616
|
+
"reasoningLevelMed": "Medium",
|
|
617
|
+
"reasoningLevelHigh": "High",
|
|
618
|
+
"reasoningLevelMax": "Max",
|
|
619
|
+
"reasoningLevelOff": "Off"
|
|
613
620
|
};
|
|
614
621
|
|
|
615
622
|
const zh = {
|
|
@@ -867,7 +874,14 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
867
874
|
"wizardRecommendations": "排查建议与修复步骤",
|
|
868
875
|
"wizardCopy": "复制脱敏诊断报告",
|
|
869
876
|
"cachePinnedBadge": "缓存粘性",
|
|
870
|
-
"field.enableToasts": "当账户发生隔离或额度达到警戒线时显示即时弹窗提醒"
|
|
877
|
+
"field.enableToasts": "当账户发生隔离或额度达到警戒线时显示即时弹窗提醒",
|
|
878
|
+
"reasoningEffortTitle": "推理强度控制",
|
|
879
|
+
"reasoningEffortDesc": "为具备扩展思考能力的模型配置推理深度与思考预算。",
|
|
880
|
+
"reasoningLevelLow": "低",
|
|
881
|
+
"reasoningLevelMed": "中",
|
|
882
|
+
"reasoningLevelHigh": "高",
|
|
883
|
+
"reasoningLevelMax": "最高",
|
|
884
|
+
"reasoningLevelOff": "关闭"
|
|
871
885
|
};
|
|
872
886
|
// #51: live countdown to the quota window reset (account.quota.resetAt).
|
|
873
887
|
function cleanErrorMessage(raw) {
|
|
@@ -1971,6 +1985,20 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1971
1985
|
})
|
|
1972
1986
|
),
|
|
1973
1987
|
),
|
|
1988
|
+
React.createElement('details', { className: 'dsub-group' },
|
|
1989
|
+
React.createElement('summary', { className: 'dsub-groupHead' }, '🧠 ' + t('reasoningEffortTitle')),
|
|
1990
|
+
React.createElement('div', { className: 'dsub-sub', style: { marginTop: '4px' } }, t('reasoningEffortDesc')),
|
|
1991
|
+
React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '8px', marginTop: '10px' } },
|
|
1992
|
+
['off', 'low', 'medium', 'high', 'max'].map((lvl) => {
|
|
1993
|
+
const label = lvl === 'off' ? t('reasoningLevelOff') : (lvl.charAt(0).toUpperCase() + lvl.slice(1))
|
|
1994
|
+
return React.createElement('span', {
|
|
1995
|
+
key: lvl,
|
|
1996
|
+
className: 'dsub-model-tag',
|
|
1997
|
+
style: { padding: '4px 10px', fontSize: '12px', cursor: 'default' }
|
|
1998
|
+
}, '⚙️ ' + label)
|
|
1999
|
+
})
|
|
2000
|
+
)
|
|
2001
|
+
),
|
|
1974
2002
|
React.createElement('details', { className: 'dsub-details' },
|
|
1975
2003
|
React.createElement('summary', { className: 'dsub-summary' }, t('advancedSettings')),
|
|
1976
2004
|
React.createElement('div', { className: 'dsub-block' },
|
package/lib/diagnostics.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getSessionReasoningStats } from './reasoning-effort.js'
|
|
1
2
|
import { defaultConcurrencyQueue } from './concurrency-queue.js'
|
|
2
3
|
import { calculateCacheMetrics } from './analytics.js'
|
|
3
4
|
import { getSessionPinStats } from './session-pin.js'
|
|
@@ -52,6 +53,7 @@ export async function createDiagnosticsReport({ live, store, history, NS, pkgVer
|
|
|
52
53
|
cache: calculateCacheMetrics(rows),
|
|
53
54
|
queue: defaultConcurrencyQueue.stats(),
|
|
54
55
|
pins: getSessionPinStats(),
|
|
56
|
+
reasoning: getSessionReasoningStats(),
|
|
55
57
|
decisions: {
|
|
56
58
|
recent: rows.filter((r) => r && r.decisionTrace).slice(0, 10).map((r) => ({
|
|
57
59
|
ts: r.ts,
|
package/lib/messages.js
CHANGED
|
@@ -289,8 +289,14 @@ export function codexResponsesBody(options, fallbackInstructions, vendorCfg) {
|
|
|
289
289
|
instructions,
|
|
290
290
|
input,
|
|
291
291
|
...(responsesTools && responsesTools.length ? { tools: responsesTools } : {}),
|
|
292
|
-
// #93: reasoning effort chosen
|
|
293
|
-
...(
|
|
292
|
+
// #93, #505: reasoning effort chosen flows to the protocol only when supported and not 'off'
|
|
293
|
+
...((() => {
|
|
294
|
+
const eff = options && String(options.reasoningEffort || '').toLowerCase().trim()
|
|
295
|
+
if (eff && eff !== 'off' && /^(low|medium|high)$/.test(eff)) {
|
|
296
|
+
return { reasoning: { effort: eff } }
|
|
297
|
+
}
|
|
298
|
+
return {}
|
|
299
|
+
})()),
|
|
294
300
|
// #93: verbosity comes from the codexVerbosity setting (low/medium/high).
|
|
295
301
|
...(vendorCfg && /^(low|medium|high)$/.test(String(vendorCfg.verbosity || ''))
|
|
296
302
|
? { text: { verbosity: String(vendorCfg.verbosity) } } : {}),
|
|
@@ -501,6 +507,18 @@ export function googleContents(options) {
|
|
|
501
507
|
generationConfig: {
|
|
502
508
|
...(options.temperature != null ? { temperature: options.temperature } : {}),
|
|
503
509
|
...(options.maxTokens != null ? { maxOutputTokens: options.maxTokens } : {}),
|
|
510
|
+
...((() => {
|
|
511
|
+
const budget = options && (options.thinkingBudget || options.budgetTokens || (options.thinking && options.thinking.budget_tokens))
|
|
512
|
+
if (budget != null && Number(budget) >= 0) {
|
|
513
|
+
return { thinkingConfig: { thinkingBudget: Number(budget) } }
|
|
514
|
+
}
|
|
515
|
+
const eff = options && String(options.reasoningEffort || '').toLowerCase().trim()
|
|
516
|
+
if (eff === 'off') return { thinkingConfig: { thinkingBudget: 0 } }
|
|
517
|
+
if (eff === 'low') return { thinkingConfig: { thinkingBudget: 1024 } }
|
|
518
|
+
if (eff === 'medium') return { thinkingConfig: { thinkingBudget: 8192 } }
|
|
519
|
+
if (eff === 'high') return { thinkingConfig: { thinkingBudget: 24576 } }
|
|
520
|
+
return {}
|
|
521
|
+
})()),
|
|
504
522
|
},
|
|
505
523
|
}
|
|
506
524
|
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
// Reasoning effort management and model capability mapping (#505).
|
|
2
|
+
// Enforces 0 Cyrillic characters.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Known default effort levels per vendor.
|
|
6
|
+
* @type {Record<string, string[]>}
|
|
7
|
+
*/
|
|
8
|
+
export const DEFAULT_VENDOR_EFFORTS = {
|
|
9
|
+
codex: ['low', 'medium', 'high'],
|
|
10
|
+
claude: ['low', 'medium', 'high'],
|
|
11
|
+
antigravity: ['low', 'medium', 'high'],
|
|
12
|
+
grok: ['low', 'medium', 'high'],
|
|
13
|
+
kimi: ['low', 'medium', 'high', 'max'],
|
|
14
|
+
cursor: ['low', 'medium', 'high', 'max'],
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Inspect model and catalog to determine supported reasoning levels.
|
|
19
|
+
* @param {string} provider
|
|
20
|
+
* @param {string} modelId
|
|
21
|
+
* @param {Array<any>} [catalog]
|
|
22
|
+
* @returns {{ supported: boolean, levels: string[], defaultEffort?: string }}
|
|
23
|
+
*/
|
|
24
|
+
export function getModelReasoningCapabilities(provider, modelId, catalog = []) {
|
|
25
|
+
const p = String(provider || '').toLowerCase().trim()
|
|
26
|
+
const id = String(modelId || '').toLowerCase().trim()
|
|
27
|
+
|
|
28
|
+
// 1. Check catalog entry if provided
|
|
29
|
+
if (Array.isArray(catalog) && catalog.length) {
|
|
30
|
+
const entry = catalog.find((m) => m && (m.id === id || m.slug === id || m.name === id))
|
|
31
|
+
if (entry && entry.reasoning && Array.isArray(entry.reasoning.efforts) && entry.reasoning.efforts.length) {
|
|
32
|
+
const levels = entry.reasoning.efforts.map((row) => (typeof row === 'string' ? row : row.id)).filter(Boolean)
|
|
33
|
+
if (levels.length) {
|
|
34
|
+
return {
|
|
35
|
+
supported: true,
|
|
36
|
+
levels,
|
|
37
|
+
defaultEffort: entry.reasoning.defaultEffort || 'medium',
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 2. Vendor-specific heuristics
|
|
44
|
+
if (p === 'claude') {
|
|
45
|
+
// Claude 3.7+ Sonnet (both hybrid thinking and adaptive)
|
|
46
|
+
if (/claude-(?:3[.-]7|37)-sonnet/.test(id)) {
|
|
47
|
+
return { supported: true, levels: ['low', 'medium', 'high'], defaultEffort: 'medium' }
|
|
48
|
+
}
|
|
49
|
+
// Claude 4.6+ Sonnet / Opus
|
|
50
|
+
const m = /^claude-(opus|sonnet|haiku|fable)(?:-(\d+))?(?:-(\d+))?/.exec(id)
|
|
51
|
+
if (m) {
|
|
52
|
+
const [, family, maj, min] = m
|
|
53
|
+
const major = maj ? Number(maj) : null
|
|
54
|
+
const minor = min ? Number(min) : null
|
|
55
|
+
const atLeast = (wantMaj, wantMin) => (
|
|
56
|
+
major === null || major > wantMaj || (major === wantMaj && (minor === null || minor >= wantMin))
|
|
57
|
+
)
|
|
58
|
+
if ((family === 'sonnet' || family === 'opus') && atLeast(4, 6)) {
|
|
59
|
+
const levels = ['low', 'medium', 'high']
|
|
60
|
+
if (family === 'opus') {
|
|
61
|
+
if (atLeast(4, 7)) levels.push('xhigh')
|
|
62
|
+
levels.push('max')
|
|
63
|
+
}
|
|
64
|
+
return { supported: true, levels, defaultEffort: 'medium' }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { supported: false, levels: [] }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (p === 'codex') {
|
|
71
|
+
// OpenAI reasoning models: o1, o3, gpt-5.1-codex, or models containing 'reasoning'
|
|
72
|
+
if (
|
|
73
|
+
id.startsWith('o1') ||
|
|
74
|
+
id.startsWith('o3') ||
|
|
75
|
+
id.includes('reasoning') ||
|
|
76
|
+
id.includes('codex')
|
|
77
|
+
) {
|
|
78
|
+
return { supported: true, levels: ['low', 'medium', 'high'], defaultEffort: 'medium' }
|
|
79
|
+
}
|
|
80
|
+
return { supported: false, levels: [] }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (p === 'antigravity') {
|
|
84
|
+
if (
|
|
85
|
+
id.includes('thinking') ||
|
|
86
|
+
id.includes('gemini-3.1-pro') ||
|
|
87
|
+
id.includes('gemini-3.6-flash') ||
|
|
88
|
+
id.includes('gemini-3.7-flash') ||
|
|
89
|
+
id.includes('gemini-3.8-flash')
|
|
90
|
+
) {
|
|
91
|
+
return { supported: true, levels: ['low', 'medium', 'high'], defaultEffort: 'medium' }
|
|
92
|
+
}
|
|
93
|
+
return { supported: false, levels: [] }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (p === 'grok') {
|
|
97
|
+
if (id.includes('reasoning') || id === 'grok-3-mini') {
|
|
98
|
+
return { supported: true, levels: ['low', 'medium', 'high'], defaultEffort: 'medium' }
|
|
99
|
+
}
|
|
100
|
+
return { supported: false, levels: [] }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (p === 'kimi') {
|
|
104
|
+
if (id.includes('k1.5') || id.includes('reasoning')) {
|
|
105
|
+
return { supported: true, levels: ['low', 'medium', 'high', 'max'], defaultEffort: 'medium' }
|
|
106
|
+
}
|
|
107
|
+
return { supported: false, levels: [] }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { supported: false, levels: [] }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Extract raw effort and budget from incoming request options.
|
|
115
|
+
* @param {any} options
|
|
116
|
+
* @returns {{ effort?: string, budgetTokens?: number }}
|
|
117
|
+
*/
|
|
118
|
+
export function extractReasoningOptions(options) {
|
|
119
|
+
if (!options || typeof options !== 'object') return {}
|
|
120
|
+
let effort = options.reasoningEffort
|
|
121
|
+
if (!effort && options.reasoning && typeof options.reasoning.effort === 'string') {
|
|
122
|
+
effort = options.reasoning.effort
|
|
123
|
+
}
|
|
124
|
+
if (!effort && options.thinking && typeof options.thinking.effort === 'string') {
|
|
125
|
+
effort = options.thinking.effort
|
|
126
|
+
}
|
|
127
|
+
if (!effort && typeof options.effort === 'string') {
|
|
128
|
+
effort = options.effort
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let budgetTokens = options.thinkingBudget ?? options.budgetTokens
|
|
132
|
+
if (budgetTokens == null && options.thinking && options.thinking.budget_tokens != null) {
|
|
133
|
+
budgetTokens = options.thinking.budget_tokens
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const normalizedEffort = typeof effort === 'string' ? effort.toLowerCase().trim() : undefined
|
|
137
|
+
const normalizedBudget = typeof budgetTokens === 'number' && !Number.isNaN(budgetTokens) ? budgetTokens : undefined
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
...(normalizedEffort ? { effort: normalizedEffort } : {}),
|
|
141
|
+
...(normalizedBudget != null ? { budgetTokens: normalizedBudget } : {}),
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Validate effort level against model capabilities.
|
|
147
|
+
* @param {string} provider
|
|
148
|
+
* @param {string} modelId
|
|
149
|
+
* @param {string|undefined} effort
|
|
150
|
+
* @param {Array<any>} [catalog]
|
|
151
|
+
* @returns {{ valid: boolean, effort?: string, reason?: string }}
|
|
152
|
+
*/
|
|
153
|
+
export function validateReasoningEffort(provider, modelId, effort, catalog = []) {
|
|
154
|
+
if (!effort) return { valid: true, effort: undefined }
|
|
155
|
+
const eff = String(effort).toLowerCase().trim()
|
|
156
|
+
if (eff === 'off' || eff === 'none') {
|
|
157
|
+
return { valid: true, effort: 'off' }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const caps = getModelReasoningCapabilities(provider, modelId, catalog)
|
|
161
|
+
if (!caps.supported) {
|
|
162
|
+
return { valid: false, reason: 'model_not_reasoning' }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!caps.levels.includes(eff)) {
|
|
166
|
+
return { valid: false, reason: 'unsupported_effort_level' }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return { valid: true, effort: eff }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const DEFAULT_TTL_MS = 60 * 60 * 1000 // 1 hour
|
|
173
|
+
const MAX_STORED_SESSIONS = 500
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @typedef {Object} SessionReasoningRecord
|
|
177
|
+
* @property {string} provider
|
|
178
|
+
* @property {string} model
|
|
179
|
+
* @property {string} effort
|
|
180
|
+
* @property {number} [budgetTokens]
|
|
181
|
+
* @property {number} updatedAt
|
|
182
|
+
* @property {number} expiresAt
|
|
183
|
+
*/
|
|
184
|
+
|
|
185
|
+
/** @type {Map<string, SessionReasoningRecord>} */
|
|
186
|
+
const sessionReasoningStore = new Map()
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Store session-scoped reasoning configuration.
|
|
190
|
+
* @param {string} sessionId
|
|
191
|
+
* @param {object} params
|
|
192
|
+
* @param {string} params.provider
|
|
193
|
+
* @param {string} params.model
|
|
194
|
+
* @param {string} params.effort
|
|
195
|
+
* @param {number} [params.budgetTokens]
|
|
196
|
+
* @param {number} [params.ttlMs]
|
|
197
|
+
* @param {Array<any>} [catalog]
|
|
198
|
+
* @returns {{ ok: boolean, error?: string }}
|
|
199
|
+
*/
|
|
200
|
+
export function setSessionReasoning(sessionId, { provider, model, effort, budgetTokens, ttlMs = DEFAULT_TTL_MS }, catalog = []) {
|
|
201
|
+
if (!sessionId || typeof sessionId !== 'string') {
|
|
202
|
+
return { ok: false, error: 'invalid_session_id' }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const validation = validateReasoningEffort(provider, model, effort, catalog)
|
|
206
|
+
if (!validation.valid) {
|
|
207
|
+
return { ok: false, error: validation.reason }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (sessionReasoningStore.size >= MAX_STORED_SESSIONS) {
|
|
211
|
+
pruneSessionReasoning()
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const now = Date.now()
|
|
215
|
+
sessionReasoningStore.set(sessionId, {
|
|
216
|
+
provider: String(provider || ''),
|
|
217
|
+
model: String(model || ''),
|
|
218
|
+
effort: validation.effort || 'off',
|
|
219
|
+
budgetTokens: typeof budgetTokens === 'number' ? budgetTokens : undefined,
|
|
220
|
+
updatedAt: now,
|
|
221
|
+
expiresAt: now + ttlMs,
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
return { ok: true }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Retrieve session-scoped reasoning configuration.
|
|
229
|
+
* @param {string} sessionId
|
|
230
|
+
* @param {number} [now]
|
|
231
|
+
* @returns {SessionReasoningRecord|null}
|
|
232
|
+
*/
|
|
233
|
+
export function getSessionReasoning(sessionId, now = Date.now()) {
|
|
234
|
+
if (!sessionId) return null
|
|
235
|
+
const record = sessionReasoningStore.get(sessionId)
|
|
236
|
+
if (!record) return null
|
|
237
|
+
if (record.expiresAt <= now) {
|
|
238
|
+
sessionReasoningStore.delete(sessionId)
|
|
239
|
+
return null
|
|
240
|
+
}
|
|
241
|
+
// Sliding TTL extension
|
|
242
|
+
record.expiresAt = now + DEFAULT_TTL_MS
|
|
243
|
+
return record
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Clear session-scoped reasoning configuration.
|
|
248
|
+
* @param {string} sessionId
|
|
249
|
+
* @returns {boolean}
|
|
250
|
+
*/
|
|
251
|
+
export function clearSessionReasoning(sessionId) {
|
|
252
|
+
if (!sessionId) return false
|
|
253
|
+
return sessionReasoningStore.delete(sessionId)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Prune expired session reasoning entries.
|
|
258
|
+
* @param {number} [now]
|
|
259
|
+
* @returns {number} count of pruned sessions
|
|
260
|
+
*/
|
|
261
|
+
export function pruneSessionReasoning(now = Date.now()) {
|
|
262
|
+
let pruned = 0
|
|
263
|
+
for (const [id, record] of sessionReasoningStore.entries()) {
|
|
264
|
+
if (record.expiresAt <= now) {
|
|
265
|
+
sessionReasoningStore.delete(id)
|
|
266
|
+
pruned++
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return pruned
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Get internal store statistics.
|
|
274
|
+
* @returns {{ activeSessions: number }}
|
|
275
|
+
*/
|
|
276
|
+
export function getSessionReasoningStats() {
|
|
277
|
+
return { activeSessions: sessionReasoningStore.size }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Resolve effective reasoning effort for a request turn.
|
|
282
|
+
* Automatically guards against model switching: if the session has a stored
|
|
283
|
+
* reasoning effort but the target model is non-reasoning, the parameter is suppressed.
|
|
284
|
+
* @param {string|undefined} sessionId
|
|
285
|
+
* @param {any} options
|
|
286
|
+
* @param {Array<any>} [catalog]
|
|
287
|
+
* @returns {{ effort?: string, budgetTokens?: number }}
|
|
288
|
+
*/
|
|
289
|
+
export function resolveEffectiveReasoning(sessionId, options, catalog = []) {
|
|
290
|
+
const extracted = extractReasoningOptions(options)
|
|
291
|
+
const provider = options && options.provider
|
|
292
|
+
const targetModel = options && options.model
|
|
293
|
+
|
|
294
|
+
// 1. Explicit request-level option takes precedence
|
|
295
|
+
if (extracted.effort || extracted.budgetTokens) {
|
|
296
|
+
const val = validateReasoningEffort(provider, targetModel, extracted.effort, catalog)
|
|
297
|
+
if (val.valid) {
|
|
298
|
+
return {
|
|
299
|
+
effort: val.effort === 'off' ? 'off' : val.effort,
|
|
300
|
+
budgetTokens: extracted.budgetTokens,
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// If invalid (e.g. non-reasoning model), suppress so request does not fail
|
|
304
|
+
return { effort: undefined, budgetTokens: undefined }
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 2. Fall back to session-scoped configuration
|
|
308
|
+
if (sessionId) {
|
|
309
|
+
const record = getSessionReasoning(sessionId)
|
|
310
|
+
if (record && record.effort) {
|
|
311
|
+
// Validate that the target model currently being invoked supports reasoning
|
|
312
|
+
const val = validateReasoningEffort(provider, targetModel, record.effort, catalog)
|
|
313
|
+
if (val.valid && val.effort !== 'off') {
|
|
314
|
+
return {
|
|
315
|
+
effort: val.effort,
|
|
316
|
+
budgetTokens: record.budgetTokens,
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return { effort: undefined, budgetTokens: undefined }
|
|
323
|
+
}
|
package/lib/routes/status.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getModelReasoningCapabilities,
|
|
3
|
+
setSessionReasoning,
|
|
4
|
+
getSessionReasoning,
|
|
5
|
+
clearSessionReasoning,
|
|
6
|
+
DEFAULT_VENDOR_EFFORTS,
|
|
7
|
+
validateReasoningEffort,
|
|
8
|
+
} from '../reasoning-effort.js'
|
|
1
9
|
import { defaultConcurrencyQueue } from '../concurrency-queue.js'
|
|
2
10
|
import { calculateAnalytics } from '../analytics.js'
|
|
3
11
|
import { runDiagnosticWizard } from '../diagnostic-wizard.js'
|
|
@@ -614,4 +622,72 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
614
622
|
writeJson(res, 200, { ok: true, report })
|
|
615
623
|
}),
|
|
616
624
|
}), 'dsh-subscriptions: /diagnostic-wizard')
|
|
625
|
+
|
|
626
|
+
// #505: Session-scoped reasoning effort management
|
|
627
|
+
ctx.effect(() => ctx.webServer.register({
|
|
628
|
+
kind: 'exact',
|
|
629
|
+
path: '/dsh-subscriptions/session-reasoning',
|
|
630
|
+
handler: safeJsonHandler(async (req, res) => {
|
|
631
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
632
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
633
|
+
return
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const q = queryOf(req)
|
|
637
|
+
if (req.method === 'GET') {
|
|
638
|
+
const sessionId = q.get('sessionId') || ''
|
|
639
|
+
const provider = q.get('provider') || ''
|
|
640
|
+
const model = q.get('model') || ''
|
|
641
|
+
const current = sessionId ? getSessionReasoning(sessionId) : null
|
|
642
|
+
const caps = model ? getModelReasoningCapabilities(provider, model) : { supported: false, levels: [] }
|
|
643
|
+
writeJson(res, 200, {
|
|
644
|
+
ok: true,
|
|
645
|
+
sessionId: sessionId || null,
|
|
646
|
+
effort: (current && current.effort) || (caps.supported ? caps.defaultEffort : null),
|
|
647
|
+
budgetTokens: (current && current.budgetTokens) || null,
|
|
648
|
+
capabilities: caps,
|
|
649
|
+
defaults: DEFAULT_VENDOR_EFFORTS,
|
|
650
|
+
validateReasoningEffort,
|
|
651
|
+
})
|
|
652
|
+
return
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (req.method === 'POST') {
|
|
656
|
+
let body = {}
|
|
657
|
+
try { body = JSON.parse((await readBody(req, 64 * 1024)).toString('utf8') || '{}') } catch { /* ignore */ }
|
|
658
|
+
const { sessionId, provider, model, effort, budgetTokens } = body
|
|
659
|
+
if (!sessionId) {
|
|
660
|
+
writeJson(res, 400, { ok: false, error: { code: 'bad_request', message: 'sessionId is required' } })
|
|
661
|
+
return
|
|
662
|
+
}
|
|
663
|
+
const val = validateReasoningEffort(provider, model, effort)
|
|
664
|
+
if (!val.valid) {
|
|
665
|
+
writeJson(res, 400, { ok: false, error: { code: val.reason || 'invalid_effort', message: 'invalid reasoning effort for model' } })
|
|
666
|
+
return
|
|
667
|
+
}
|
|
668
|
+
const result = setSessionReasoning(sessionId, { provider, model, effort, budgetTokens })
|
|
669
|
+
if (!result.ok) {
|
|
670
|
+
writeJson(res, 400, { ok: false, error: { code: result.error || 'validation_failed', message: 'invalid reasoning effort for model' } })
|
|
671
|
+
return
|
|
672
|
+
}
|
|
673
|
+
writeJson(res, 200, { ok: true, sessionId, effort: effort || 'off', model: model || null })
|
|
674
|
+
return
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (req.method === 'DELETE') {
|
|
678
|
+
let body = {}
|
|
679
|
+
try { body = JSON.parse((await readBody(req, 64 * 1024)).toString('utf8') || '{}') } catch { /* ignore */ }
|
|
680
|
+
const sessionId = q.get('sessionId') || body.sessionId
|
|
681
|
+
if (!sessionId) {
|
|
682
|
+
writeJson(res, 400, { ok: false, error: { code: 'bad_request', message: 'sessionId is required' } })
|
|
683
|
+
return
|
|
684
|
+
}
|
|
685
|
+
const cleared = clearSessionReasoning(sessionId)
|
|
686
|
+
writeJson(res, 200, { ok: true, sessionId, cleared })
|
|
687
|
+
return
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET, POST, DELETE only' } })
|
|
691
|
+
}),
|
|
692
|
+
}), 'dsh-subscriptions: /session-reasoning')
|
|
617
693
|
}
|
package/lib/vendors/claude.js
CHANGED
|
@@ -33,15 +33,29 @@ export function providerInfo() {
|
|
|
33
33
|
const BASE_EFFORT_LEVELS = ['low', 'medium', 'high']
|
|
34
34
|
|
|
35
35
|
export function effortLevelsForModel(modelId) {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
const s = String(modelId || '').toLowerCase()
|
|
37
|
+
let m = /^claude-(opus|sonnet|haiku|fable)(?:-(\d+))?(?:-(\d+))?/.exec(s)
|
|
38
|
+
let family, major, minor
|
|
39
|
+
if (m) {
|
|
40
|
+
family = m[1]
|
|
41
|
+
major = m[2] ? Number(m[2]) : null
|
|
42
|
+
minor = m[3] ? Number(m[3]) : null
|
|
43
|
+
} else {
|
|
44
|
+
m = /^claude-(\d+)[-.](\d+)-(opus|sonnet|haiku|fable)/.exec(s)
|
|
45
|
+
if (m) {
|
|
46
|
+
major = Number(m[1])
|
|
47
|
+
minor = Number(m[2])
|
|
48
|
+
family = m[3]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (!family || family === 'haiku' || family === 'fable') return null
|
|
42
52
|
const atLeast = (wantMajor, wantMinor) => (
|
|
43
53
|
major === null || major > wantMajor || (major === wantMajor && (minor === null || minor >= wantMinor))
|
|
44
54
|
)
|
|
55
|
+
// Claude 3.7+ Sonnet supports extended thinking
|
|
56
|
+
if (family === 'sonnet' && major === 3 && minor !== null && minor >= 7) {
|
|
57
|
+
return [...BASE_EFFORT_LEVELS]
|
|
58
|
+
}
|
|
45
59
|
// Adaptive thinking / effort exist from Opus 4.6 and Sonnet 4.6 onward.
|
|
46
60
|
if (!atLeast(4, 6)) return null
|
|
47
61
|
const levels = [...BASE_EFFORT_LEVELS]
|
|
@@ -174,6 +188,11 @@ export async function listModels(blob, cfg, fetchImpl) {
|
|
|
174
188
|
// selection) leaves the request untouched so behavior stays opt-in.
|
|
175
189
|
function applyClaudeThinking(payload, options) {
|
|
176
190
|
const effort = options && String(options.reasoningEffort || '')
|
|
191
|
+
const budget = options && (options.thinkingBudget || options.budgetTokens || (options.thinking && options.thinking.budget_tokens))
|
|
192
|
+
if (budget && Number(budget) > 0) {
|
|
193
|
+
payload.thinking = { type: 'enabled', budget_tokens: Math.max(1024, Number(budget)) }
|
|
194
|
+
return payload
|
|
195
|
+
}
|
|
177
196
|
if (!effort || effort === 'off') return payload
|
|
178
197
|
const levels = effortLevelsForModel(payload && payload.model)
|
|
179
198
|
if (!levels || !levels.includes(effort)) return payload
|
package/lib/wire.js
CHANGED
|
@@ -275,6 +275,8 @@ export async function* anthropicStream(body) {
|
|
|
275
275
|
id: toolId,
|
|
276
276
|
argumentsDelta: delta.partial_json,
|
|
277
277
|
}
|
|
278
|
+
} else if (delta.type === 'thinking_delta' && delta.thinking) {
|
|
279
|
+
yield { type: 'reasoning-delta', index: json.index ?? 0, text: delta.thinking }
|
|
278
280
|
}
|
|
279
281
|
} else if (json.type === 'message_delta') {
|
|
280
282
|
sawTerminal = true
|
|
@@ -354,7 +356,9 @@ export async function* googleStream(body) {
|
|
|
354
356
|
}
|
|
355
357
|
const parts = cand && cand.content && Array.isArray(cand.content.parts) ? cand.content.parts : []
|
|
356
358
|
for (const part of parts) {
|
|
357
|
-
if (part.text) {
|
|
359
|
+
if (part.thought && part.text) {
|
|
360
|
+
yield { type: 'reasoning-delta', index, text: part.text }
|
|
361
|
+
} else if (part.text) {
|
|
358
362
|
if (!started) {
|
|
359
363
|
yield { type: 'block-start', index, blockType: 'text' }
|
|
360
364
|
started = true
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.65",
|
|
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",
|