@goodandready/dsh-subscriptions 0.6.63 → 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 +7 -0
- package/README.ru.md +7 -0
- package/lib/adapter.js +12 -2
- package/lib/client.js +30 -2
- package/lib/diagnostics.js +2 -0
- package/lib/http.js +22 -6
- 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
|
@@ -102,6 +102,7 @@ graph LR
|
|
|
102
102
|
* **Dynamic Model Catalog & Live Quota Visualizer (#507)**: Dynamic model catalog with pool-level health calculation (individual quarantine degrades rather than disables the pool), context window badges, capability tags (`vision`, `tools`, `reasoning`, `coding`, `fast`), and live reset countdowns.
|
|
103
103
|
* **Cache-Aware Session Pinning (#520)**: Retains session affinity for prompt-caching providers (Claude, Codex, Antigravity, Grok) to maximize token cache reuse while strictly releasing pins when an account enters cooldown, quarantine, or quota exhaustion.
|
|
104
104
|
* **Guided Diagnostic Wizard (#519)**: Interactive step-by-step checklist verifying credentials, network/proxy connectivity, token freshness, and model catalog reachability without paid inference and with full secret redaction.
|
|
105
|
+
* **Desktop Electron Compatibility (#524)**: Non-HTTP app schemes (`dsh-app://`) bypass web-only host equality checks while strictly preserving loopback socket verification and fail-closed cross-site protection.
|
|
105
106
|
|
|
106
107
|
---
|
|
107
108
|
|
|
@@ -206,6 +207,12 @@ const res = await ctx.subscriptions.request('codex', '/backend-api/codex/images/
|
|
|
206
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.
|
|
207
208
|
* **Automated Webhook Integration**: Instant event notifications dispatched via `notifyQuarantineEntered` and `notifyQuarantineReleased`.
|
|
208
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
|
+
|
|
209
216
|
### 18. 🔑 Antigravity Client Secret & Resilient CLI Refresh (v0.6.60)
|
|
210
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.
|
|
211
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
|
@@ -102,6 +102,7 @@ graph LR
|
|
|
102
102
|
* **Динамический каталог моделей с пуловой доступностью и квотами (#507)**: Обогащенная витрина моделей с расчетом здоровья пула (карантин одного аккаунта не блокирует модель при наличии живых), тегами возможностей (`vision`, `tools`, `reasoning`, `coding`, `fast`), размером контекста и таймером сброса квот.
|
|
103
103
|
* **Cache-Aware Session Pinning (#520)**: Сохранение привязки сессии к подходящему аккаунту для провайдеров с поддержкой кэширования промптов (Claude, Codex, Antigravity, Grok) с гарантированным немедленным освобождением при кулдауне, карантине или исчерпании квоты.
|
|
104
104
|
* **Мастер интерактивной диагностики (#519)**: Пошаговый чек-лист состояния аккаунтов (валидность ключей, прокси, токен, доступность каталога) без платного инференса и с автоматическим маскированием секретов.
|
|
105
|
+
* **Совместимость с DSH Desktop (#524)**: Специфичные схемы Electron (`dsh-app://`) пропускаются к проверке loopback-сокета, устраняя ложные блокировки 403 на десктопных клиентах при сохранении полной защиты от cross-site атак.
|
|
105
106
|
|
|
106
107
|
---
|
|
107
108
|
|
|
@@ -182,6 +183,12 @@ const res = await ctx.subscriptions.request('codex', '/backend-api/codex/images/
|
|
|
182
183
|
* **Всплывающие уведомления (Toast Alerts)**: Легковесные всплывающие тосты в веб-интерфейсе DSH при событиях карантина (с отображением причины), восстановлении доступа и критическом расходе квоты. Включение управляется параметром `enableToasts` (по умолчанию `true`).
|
|
183
184
|
* **Интеграция с вебхуками**: Автоматическая отправка уведомлений через `notifyQuarantineEntered` и `notifyQuarantineReleased`.
|
|
184
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
|
+
|
|
185
192
|
### 14. 🔑 Antigravity Client Secret и надежный refresh CLI-токенов (v0.6.60)
|
|
186
193
|
* **Client Secret в схеме настроек (`antigravityClientSecret`)**: Поддержка ввода пользовательского Google Cloud OAuth Client Secret с автоматическим маскированием (`••••••`), устраняющая ошибки обновления токенов для конфиденциальных OAuth-клиентов.
|
|
187
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/http.js
CHANGED
|
@@ -93,13 +93,17 @@ export function isTrustedSettingsRequest(request, targetCtx) {
|
|
|
93
93
|
const secFetchSite = String(headers['sec-fetch-site'] || '').toLowerCase()
|
|
94
94
|
if (secFetchSite === 'cross-site') return false
|
|
95
95
|
|
|
96
|
-
// 2. If origin header is present,
|
|
96
|
+
// 2. If origin header is present, verify origin matches host (#524)
|
|
97
97
|
const host = String(Array.isArray(headers.host) ? headers.host[0] : (headers.host || headers['x-forwarded-host'] || ''))
|
|
98
98
|
const origin = headers.origin
|
|
99
99
|
if (origin) {
|
|
100
100
|
try {
|
|
101
101
|
const u = new URL(origin)
|
|
102
|
-
if (
|
|
102
|
+
if (u.protocol === 'http:' || u.protocol === 'https:') {
|
|
103
|
+
if (!host || u.host.toLowerCase() !== host.toLowerCase()) {
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
} else if (u.protocol !== 'dsh-app:') {
|
|
103
107
|
return false
|
|
104
108
|
}
|
|
105
109
|
} catch {
|
|
@@ -107,12 +111,16 @@ export function isTrustedSettingsRequest(request, targetCtx) {
|
|
|
107
111
|
}
|
|
108
112
|
}
|
|
109
113
|
|
|
110
|
-
// 3. If referer header is present and no origin, referer
|
|
114
|
+
// 3. If referer header is present and no origin, verify http/https referer matches host (#524)
|
|
111
115
|
const referer = headers.referer
|
|
112
116
|
if (!origin && referer) {
|
|
113
117
|
try {
|
|
114
118
|
const u = new URL(referer)
|
|
115
|
-
if (
|
|
119
|
+
if (u.protocol === 'http:' || u.protocol === 'https:') {
|
|
120
|
+
if (!host || u.host.toLowerCase() !== host.toLowerCase()) {
|
|
121
|
+
return false
|
|
122
|
+
}
|
|
123
|
+
} else if (u.protocol !== 'dsh-app:') {
|
|
116
124
|
return false
|
|
117
125
|
}
|
|
118
126
|
} catch {
|
|
@@ -166,13 +174,21 @@ export function isTrustedSettingsRequest(request, targetCtx) {
|
|
|
166
174
|
if (origin && host) {
|
|
167
175
|
try {
|
|
168
176
|
const u = new URL(origin)
|
|
169
|
-
if (u.
|
|
177
|
+
if (u.protocol === 'http:' || u.protocol === 'https:') {
|
|
178
|
+
if (u.host.toLowerCase() === host.toLowerCase()) return true
|
|
179
|
+
} else if (u.protocol === 'dsh-app:') {
|
|
180
|
+
return true
|
|
181
|
+
}
|
|
170
182
|
} catch { /* bestEffort */ }
|
|
171
183
|
}
|
|
172
184
|
if (referer && host) {
|
|
173
185
|
try {
|
|
174
186
|
const u = new URL(referer)
|
|
175
|
-
if (u.
|
|
187
|
+
if (u.protocol === 'http:' || u.protocol === 'https:') {
|
|
188
|
+
if (u.host.toLowerCase() === host.toLowerCase()) return true
|
|
189
|
+
} else if (u.protocol === 'dsh-app:') {
|
|
190
|
+
return true
|
|
191
|
+
}
|
|
176
192
|
} catch { /* bestEffort */ }
|
|
177
193
|
}
|
|
178
194
|
}
|
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",
|