@goodandready/dsh-subscriptions 0.6.64 → 0.6.66
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 +12 -0
- package/README.ru.md +12 -0
- package/lib/adapter.js +12 -2
- package/lib/client.js +70 -2
- package/lib/config-schema.js +4 -0
- package/lib/diagnostics.js +4 -0
- package/lib/import-auth.js +25 -0
- package/lib/messages.js +20 -2
- package/lib/reasoning-effort.js +323 -0
- package/lib/routes/accounts.js +1 -1
- package/lib/routes/status.js +76 -0
- package/lib/vendors/claude.js +25 -6
- package/lib/vendors/copilot-cli.js +400 -0
- package/lib/vendors/copilot.js +31 -1
- package/lib/wire.js +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -207,6 +207,18 @@ 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
|
+
### 20. 🤖 Native Copilot CLI SDK Mode & Local Tool Calling (v0.6.66)
|
|
211
|
+
* **Direct Copilot CLI Integration (#508)**: Native driver `copilot-cli.js` supporting execution via local `copilot` / `github-copilot-cli` binaries with zero network token emulation risk.
|
|
212
|
+
* **Smart Mode Switching (`copilotMode`)**: Choose between `auto` (prefers local CLI with graceful web API fallback), `cli` (strict CLI SDK execution), and `web` (direct OAuth/API).
|
|
213
|
+
* **Multi-Source Credential Detection**: Automatically discovers tokens from GitHub CLI (`gh hosts.yml`), Copilot config files (`hosts.json`, `apps.json`), and environment variables (`GITHUB_COPILOT_TOKEN`, `GH_TOKEN`).
|
|
214
|
+
* **Full Tool-Calling & Streaming**: Seamless translation of CLI process output into real-time streaming chunks and multi-turn tool-call deltas.
|
|
215
|
+
|
|
216
|
+
### 19. 🧠 Session Reasoning Effort Management & Contract Matrix (v0.6.65)
|
|
217
|
+
* **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`).
|
|
218
|
+
* **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.
|
|
219
|
+
* **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`).
|
|
220
|
+
* **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`).
|
|
221
|
+
|
|
210
222
|
### 18. 🔑 Antigravity Client Secret & Resilient CLI Refresh (v0.6.60)
|
|
211
223
|
* **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
224
|
* **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,18 @@ 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
|
+
### 16. 🤖 Нативный режим Copilot CLI SDK и локальный Tool Calling (v0.6.66)
|
|
187
|
+
* **Прямая интеграция с Copilot CLI (#508)**: Драйвер `copilot-cli.js` для работы через локальный бинарник `copilot` / `github-copilot-cli` без риска сетевых блокировок при эмуляции токенов.
|
|
188
|
+
* **Гибкие режимы работы (`copilotMode`)**: Поддержка режимов `auto` (приоритет CLI с автоматическим fallback на web-API), `cli` (строгий режим CLI SDK) и `web` (прямое подключение по OAuth/API).
|
|
189
|
+
* **Многоканальное обнаружение учетных данных**: Чтение токенов из `gh hosts.yml`, `hosts.json`, `apps.json` и переменных окружения (`GITHUB_COPILOT_TOKEN`, `GH_TOKEN`).
|
|
190
|
+
* **Потоковая передача и вызовы инструментов**: Полная поддержка real-time стриминга и событий `tool-call-delta` при исполнении через CLI.
|
|
191
|
+
|
|
192
|
+
### 15. 🧠 Управление Reasoning Effort в сессии и матрица контрактов (v0.6.65)
|
|
193
|
+
* **Управление глубиной рассуждений в сессии (#505)**: Динамическая настройка усилия рассуждений (`low`, `medium`, `high`, `max`) или отключение (`off`) в рамках диалога через REST API (`GET`, `POST`, `DELETE /dsh-subscriptions/session-reasoning`).
|
|
194
|
+
* **Безопасность при смене модели**: Автоматическое подавление параметров рассуждений при переключении с reasoning-модели (напр. `o1`) на обычную (напр. `gpt-4o`), предотвращающее ошибки HTTP 400 от upstream API.
|
|
195
|
+
* **Поддержка ведущих вендоров**: Codex (`o1`, `o3`), Claude (`claude-3-7-sonnet*`, `claude-4.6+` thinking budget), Antigravity/Gemini (`thinkingConfig`), Grok (`grok-3-mini`) и Kimi (`k1.5`).
|
|
196
|
+
* **Матрица контрактных проверок адаптеров (#521)**: Тестовый набор (`test/contract-matrix.test.mjs`) для 8 провайдеров по 7 ключевым возможностям протокола (`stream`, `tools`, `reasoning`, `usage`, `caching`, `auth_errors`, `rate_limit`).
|
|
197
|
+
|
|
186
198
|
### 14. 🔑 Antigravity Client Secret и надежный refresh CLI-токенов (v0.6.60)
|
|
187
199
|
* **Client Secret в схеме настроек (`antigravityClientSecret`)**: Поддержка ввода пользовательского Google Cloud OAuth Client Secret с автоматическим маскированием (`••••••`), устраняющая ошибки обновления токенов для конфиденциальных OAuth-клиентов.
|
|
188
200
|
* **Бесшовный 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
|
@@ -395,6 +395,13 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
395
395
|
"codexVerbosityLow": "Low",
|
|
396
396
|
"codexVerbosityMedium": "Medium",
|
|
397
397
|
"codexVerbosityHigh": "High",
|
|
398
|
+
"copilotMode": "Copilot execution mode",
|
|
399
|
+
"copilotModeHint": "Execution mode: auto (prefer local CLI with web fallback), cli (strict CLI SDK), or web (direct API).",
|
|
400
|
+
"copilotModeAuto": "Auto (CLI with Web fallback)",
|
|
401
|
+
"copilotModeCli": "CLI Only (Local copilot-cli)",
|
|
402
|
+
"copilotModeWeb": "Web Only (Direct API)",
|
|
403
|
+
"copilotCliPath": "Custom Copilot CLI path",
|
|
404
|
+
"copilotCliPathHint": "Optional path to copilot or github-copilot-cli binary.",
|
|
398
405
|
"ollamaFallback": "Ollama local fallback",
|
|
399
406
|
"ollamaFallbackHint": "Automatically route queries to a local Ollama instance when cloud quota is exhausted.",
|
|
400
407
|
"ollamaBaseUrl": "Ollama Base URL",
|
|
@@ -609,7 +616,14 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
609
616
|
"wizardRecommendations": "Recommendations & Next Steps",
|
|
610
617
|
"wizardCopy": "Copy Masked Diagnostic Report",
|
|
611
618
|
"cachePinnedBadge": "Cache Pinned",
|
|
612
|
-
"field.enableToasts": "Show popup toast notifications on quarantine or quota events"
|
|
619
|
+
"field.enableToasts": "Show popup toast notifications on quarantine or quota events",
|
|
620
|
+
"reasoningEffortTitle": "Reasoning Effort Control",
|
|
621
|
+
"reasoningEffortDesc": "Configure reasoning depth for models with extended thinking capabilities.",
|
|
622
|
+
"reasoningLevelLow": "Low",
|
|
623
|
+
"reasoningLevelMed": "Medium",
|
|
624
|
+
"reasoningLevelHigh": "High",
|
|
625
|
+
"reasoningLevelMax": "Max",
|
|
626
|
+
"reasoningLevelOff": "Off"
|
|
613
627
|
};
|
|
614
628
|
|
|
615
629
|
const zh = {
|
|
@@ -653,6 +667,13 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
653
667
|
"codexVerbosityLow": "低",
|
|
654
668
|
"codexVerbosityMedium": "中",
|
|
655
669
|
"codexVerbosityHigh": "高",
|
|
670
|
+
"copilotMode": "Copilot 执行模式",
|
|
671
|
+
"copilotModeHint": "执行模式:auto(优先本地 CLI,带 Web 回退)、cli(仅本地 CLI)、web(仅直连 API)。",
|
|
672
|
+
"copilotModeAuto": "自动(优先本地 CLI)",
|
|
673
|
+
"copilotModeCli": "仅本地 CLI",
|
|
674
|
+
"copilotModeWeb": "仅直连 API",
|
|
675
|
+
"copilotCliPath": "自定义 Copilot CLI 路径",
|
|
676
|
+
"copilotCliPathHint": "可选的 copilot 或 github-copilot-cli 二进制文件路径。",
|
|
656
677
|
"ollamaFallback": "Ollama 本地模型自动兜底",
|
|
657
678
|
"ollamaFallbackHint": "当云端配额耗尽或服务商故障时,自动将请求转发给本地 Ollama 实例。",
|
|
658
679
|
"ollamaBaseUrl": "Ollama 服务基础 URL",
|
|
@@ -867,7 +888,14 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
867
888
|
"wizardRecommendations": "排查建议与修复步骤",
|
|
868
889
|
"wizardCopy": "复制脱敏诊断报告",
|
|
869
890
|
"cachePinnedBadge": "缓存粘性",
|
|
870
|
-
"field.enableToasts": "当账户发生隔离或额度达到警戒线时显示即时弹窗提醒"
|
|
891
|
+
"field.enableToasts": "当账户发生隔离或额度达到警戒线时显示即时弹窗提醒",
|
|
892
|
+
"reasoningEffortTitle": "推理强度控制",
|
|
893
|
+
"reasoningEffortDesc": "为具备扩展思考能力的模型配置推理深度与思考预算。",
|
|
894
|
+
"reasoningLevelLow": "低",
|
|
895
|
+
"reasoningLevelMed": "中",
|
|
896
|
+
"reasoningLevelHigh": "高",
|
|
897
|
+
"reasoningLevelMax": "最高",
|
|
898
|
+
"reasoningLevelOff": "关闭"
|
|
871
899
|
};
|
|
872
900
|
// #51: live countdown to the quota window reset (account.quota.resetAt).
|
|
873
901
|
function cleanErrorMessage(raw) {
|
|
@@ -1890,6 +1918,32 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1890
1918
|
),
|
|
1891
1919
|
React.createElement('div', { className: 'dsub-sub' },
|
|
1892
1920
|
t('codexVerbosityHint')),
|
|
1921
|
+
React.createElement('div', { className: 'dsub-row' },
|
|
1922
|
+
React.createElement('span', { className: 'dsub-h' }, t('copilotMode')),
|
|
1923
|
+
React.createElement('select', {
|
|
1924
|
+
className: 'dsub-select',
|
|
1925
|
+
value: draft.copilotMode || 'auto',
|
|
1926
|
+
onChange: (e) => setDraft((d) => Object.assign({}, d, { copilotMode: e.target.value })),
|
|
1927
|
+
},
|
|
1928
|
+
React.createElement('option', { value: 'auto' }, t('copilotModeAuto')),
|
|
1929
|
+
React.createElement('option', { value: 'cli' }, t('copilotModeCli')),
|
|
1930
|
+
React.createElement('option', { value: 'web' }, t('copilotModeWeb')),
|
|
1931
|
+
),
|
|
1932
|
+
),
|
|
1933
|
+
React.createElement('div', { className: 'dsub-sub' },
|
|
1934
|
+
t('copilotModeHint')),
|
|
1935
|
+
React.createElement('div', { className: 'dsub-row' },
|
|
1936
|
+
React.createElement('span', { className: 'dsub-h' }, t('copilotCliPath')),
|
|
1937
|
+
React.createElement('input', {
|
|
1938
|
+
className: 'dsub-input',
|
|
1939
|
+
type: 'text',
|
|
1940
|
+
value: draft.copilotCliPath || '',
|
|
1941
|
+
placeholder: 'copilot',
|
|
1942
|
+
onChange: (e) => setDraft((d) => Object.assign({}, d, { copilotCliPath: e.target.value })),
|
|
1943
|
+
}),
|
|
1944
|
+
),
|
|
1945
|
+
React.createElement('div', { className: 'dsub-sub' },
|
|
1946
|
+
t('copilotCliPathHint')),
|
|
1893
1947
|
),
|
|
1894
1948
|
React.createElement('details', { className: 'dsub-group', open: true },
|
|
1895
1949
|
React.createElement('summary', { className: 'dsub-groupHead' }, t('groupFallback')),
|
|
@@ -1971,6 +2025,20 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1971
2025
|
})
|
|
1972
2026
|
),
|
|
1973
2027
|
),
|
|
2028
|
+
React.createElement('details', { className: 'dsub-group' },
|
|
2029
|
+
React.createElement('summary', { className: 'dsub-groupHead' }, '🧠 ' + t('reasoningEffortTitle')),
|
|
2030
|
+
React.createElement('div', { className: 'dsub-sub', style: { marginTop: '4px' } }, t('reasoningEffortDesc')),
|
|
2031
|
+
React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '8px', marginTop: '10px' } },
|
|
2032
|
+
['off', 'low', 'medium', 'high', 'max'].map((lvl) => {
|
|
2033
|
+
const label = lvl === 'off' ? t('reasoningLevelOff') : (lvl.charAt(0).toUpperCase() + lvl.slice(1))
|
|
2034
|
+
return React.createElement('span', {
|
|
2035
|
+
key: lvl,
|
|
2036
|
+
className: 'dsub-model-tag',
|
|
2037
|
+
style: { padding: '4px 10px', fontSize: '12px', cursor: 'default' }
|
|
2038
|
+
}, '⚙️ ' + label)
|
|
2039
|
+
})
|
|
2040
|
+
)
|
|
2041
|
+
),
|
|
1974
2042
|
React.createElement('details', { className: 'dsub-details' },
|
|
1975
2043
|
React.createElement('summary', { className: 'dsub-summary' }, t('advancedSettings')),
|
|
1976
2044
|
React.createElement('div', { className: 'dsub-block' },
|
package/lib/config-schema.js
CHANGED
|
@@ -96,6 +96,10 @@ export const Config = z.object({
|
|
|
96
96
|
.description('Custom model catalog for Codex.'),
|
|
97
97
|
claudeSystemPrefix: z.string().volatile().default('')
|
|
98
98
|
.description('System prompt prefix for Claude.'),
|
|
99
|
+
copilotMode: z.string().volatile().default('auto')
|
|
100
|
+
.description('#508 Execution mode for GitHub Copilot: auto (prefer CLI if available), cli (require copilot CLI), or web (direct web/OAuth API).'),
|
|
101
|
+
copilotCliPath: z.string().volatile().default('')
|
|
102
|
+
.description('#508 Custom binary path for copilot-cli (empty detects in PATH and ~/.npm-global).'),
|
|
99
103
|
claudeCcVersion: z.string().volatile().default('')
|
|
100
104
|
.description('Claude Code version header for billing attribution (default: 2.1.289.7e2).'),
|
|
101
105
|
claudeModels: z.array(z.any()).volatile().default([])
|
package/lib/diagnostics.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { copilotCliInfo } from './vendors/copilot.js'
|
|
2
|
+
import { getSessionReasoningStats } from './reasoning-effort.js'
|
|
1
3
|
import { defaultConcurrencyQueue } from './concurrency-queue.js'
|
|
2
4
|
import { calculateCacheMetrics } from './analytics.js'
|
|
3
5
|
import { getSessionPinStats } from './session-pin.js'
|
|
@@ -52,6 +54,8 @@ export async function createDiagnosticsReport({ live, store, history, NS, pkgVer
|
|
|
52
54
|
cache: calculateCacheMetrics(rows),
|
|
53
55
|
queue: defaultConcurrencyQueue.stats(),
|
|
54
56
|
pins: getSessionPinStats(),
|
|
57
|
+
reasoning: getSessionReasoningStats(),
|
|
58
|
+
copilotCli: copilotCliInfo(cfg),
|
|
55
59
|
decisions: {
|
|
56
60
|
recent: rows.filter((r) => r && r.decisionTrace).slice(0, 10).map((r) => ({
|
|
57
61
|
ts: r.ts,
|
package/lib/import-auth.js
CHANGED
|
@@ -3,6 +3,7 @@ import { readFile } from 'node:fs/promises'
|
|
|
3
3
|
import { homedir } from 'node:os'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
import { readClaudeCredentials } from './vendors/claude-cli.js'
|
|
6
|
+
import { readCopilotCredentials } from './vendors/copilot-cli.js'
|
|
6
7
|
import { normalizeExpiresAt } from './blob.js'
|
|
7
8
|
import { isGoogleClientId, extractGoogleClientIdFromJwt, emailFromToken } from './jwt.js'
|
|
8
9
|
import { DEFAULT_CLI_CLIENT_ID, DEFAULT_CLI_CLIENT_SECRET } from './vendors/antigravity.js'
|
|
@@ -264,6 +265,19 @@ async function discoverLocalCliSessionsInner() {
|
|
|
264
265
|
detected['claude-cli'] = claudeEntry
|
|
265
266
|
}
|
|
266
267
|
|
|
268
|
+
// 9. GitHub Copilot token and CLI credentials
|
|
269
|
+
const copilotCreds = await readCopilotCredentials(activeHome)
|
|
270
|
+
if (copilotCreds) {
|
|
271
|
+
const copilotEntry = {
|
|
272
|
+
provider: 'copilot',
|
|
273
|
+
source: copilotCreds.source || 'file',
|
|
274
|
+
path: copilotCreds.path || 'copilot-cli',
|
|
275
|
+
email: copilotCreds.email || 'GitHub Copilot User',
|
|
276
|
+
hasRefreshToken: !!copilotCreds.refreshToken,
|
|
277
|
+
}
|
|
278
|
+
detected.copilot = copilotEntry
|
|
279
|
+
detected['copilot-cli'] = copilotEntry
|
|
280
|
+
}
|
|
267
281
|
// 9. GitHub Copilot token
|
|
268
282
|
const copilotPaths = [
|
|
269
283
|
homeFile('.config', 'github-copilot', 'hosts.json'),
|
|
@@ -317,6 +331,17 @@ export async function loadLocalCliBlob(provider, options = {}) {
|
|
|
317
331
|
}
|
|
318
332
|
|
|
319
333
|
if (canonical === 'copilot') {
|
|
334
|
+
const creds = await readCopilotCredentials(options && options.home)
|
|
335
|
+
if (creds && creds.accessToken) {
|
|
336
|
+
return {
|
|
337
|
+
accessToken: creds.accessToken,
|
|
338
|
+
refreshToken: creds.refreshToken || creds.accessToken,
|
|
339
|
+
githubToken: creds.githubToken || creds.accessToken,
|
|
340
|
+
expiresAt: creds.expiresAt || 0,
|
|
341
|
+
email: creds.email || 'GitHub Copilot User',
|
|
342
|
+
label: creds.label || creds.email || 'GitHub Copilot',
|
|
343
|
+
}
|
|
344
|
+
}
|
|
320
345
|
let tok = process.env.GITHUB_COPILOT_TOKEN || process.env.GH_TOKEN || ''
|
|
321
346
|
let email = 'GitHub Copilot User'
|
|
322
347
|
if (info.source === 'file' && info.path) {
|
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/accounts.js
CHANGED
|
@@ -258,7 +258,7 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
258
258
|
body = null
|
|
259
259
|
}
|
|
260
260
|
const rawProv = body && body.provider
|
|
261
|
-
const prov = (rawProv === 'claude-cli') ? 'claude' : rawProv
|
|
261
|
+
const prov = (rawProv === 'claude-cli') ? 'claude' : (rawProv === 'copilot-cli') ? 'copilot' : rawProv
|
|
262
262
|
if (!prov || !isProvider(prov)) {
|
|
263
263
|
writeJson(res, 400, { ok: false, error: { code: 'bad_request', message: 'unknown or missing provider' } })
|
|
264
264
|
return
|
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
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
// Native copilot-cli SDK mode with local tool calling (#508).
|
|
2
|
+
// Enforces 0 Cyrillic characters.
|
|
3
|
+
|
|
4
|
+
import { readFile } from 'node:fs/promises'
|
|
5
|
+
import { existsSync, accessSync, constants } from 'node:fs'
|
|
6
|
+
import { homedir } from 'node:os'
|
|
7
|
+
import { join, delimiter } from 'node:path'
|
|
8
|
+
import { spawn } from 'node:child_process'
|
|
9
|
+
import { LlmError } from '@deepseek-ai/dsh-llm'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Standard candidate binary names for GitHub Copilot CLI.
|
|
13
|
+
*/
|
|
14
|
+
export const COPILOT_CLI_BINARIES = [
|
|
15
|
+
'copilot',
|
|
16
|
+
'copilot.cmd',
|
|
17
|
+
'copilot.exe',
|
|
18
|
+
'github-copilot-cli',
|
|
19
|
+
'github-copilot-cli.cmd',
|
|
20
|
+
'copilot-cli',
|
|
21
|
+
'gh-copilot',
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Read GitHub Copilot credentials from local environments or config files.
|
|
26
|
+
* @param {string} [home] Test seam for home directory
|
|
27
|
+
* @param {NodeJS.ProcessEnv} [env] Test seam for process environment
|
|
28
|
+
* @returns {Promise<{ accessToken: string, githubToken: string, refreshToken?: string, expiresAt?: number, email?: string, label?: string, source: string, path?: string } | null>}
|
|
29
|
+
*/
|
|
30
|
+
export async function readCopilotCredentials(home, env = process.env) {
|
|
31
|
+
const activeHome = home || homedir()
|
|
32
|
+
|
|
33
|
+
// 1. Environment variables
|
|
34
|
+
const envToken = env.GITHUB_COPILOT_TOKEN || env.GH_TOKEN || env.GITHUB_TOKEN || env.COPILOT_API_KEY
|
|
35
|
+
if (envToken && typeof envToken === 'string' && envToken.trim()) {
|
|
36
|
+
const clean = envToken.trim()
|
|
37
|
+
return {
|
|
38
|
+
accessToken: clean,
|
|
39
|
+
githubToken: clean,
|
|
40
|
+
refreshToken: clean,
|
|
41
|
+
expiresAt: 0,
|
|
42
|
+
email: 'GitHub Copilot User',
|
|
43
|
+
label: 'copilot-env',
|
|
44
|
+
source: 'env',
|
|
45
|
+
path: env.GITHUB_COPILOT_TOKEN ? 'GITHUB_COPILOT_TOKEN' : (env.GH_TOKEN ? 'GH_TOKEN' : 'GITHUB_TOKEN'),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 2. Known configuration files
|
|
50
|
+
const fileCandidates = [
|
|
51
|
+
join(activeHome, '.config', 'github-copilot', 'hosts.json'),
|
|
52
|
+
join(activeHome, '.config', 'github-copilot', 'apps.json'),
|
|
53
|
+
join(activeHome, '.copilot-cli', 'config.json'),
|
|
54
|
+
join(activeHome, '.config', 'gh', 'hosts.yml'),
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
for (const filePath of fileCandidates) {
|
|
58
|
+
try {
|
|
59
|
+
if (!existsSync(filePath)) continue
|
|
60
|
+
const content = await readFile(filePath, 'utf8')
|
|
61
|
+
if (filePath.endsWith('.json')) {
|
|
62
|
+
const json = JSON.parse(content)
|
|
63
|
+
if (!json) continue
|
|
64
|
+
const gh = json['github.com'] || json
|
|
65
|
+
const tok = gh.oauth_token || gh.token || gh.access_token || gh.accessToken
|
|
66
|
+
if (tok) {
|
|
67
|
+
return {
|
|
68
|
+
accessToken: String(tok),
|
|
69
|
+
githubToken: String(tok),
|
|
70
|
+
refreshToken: String(tok),
|
|
71
|
+
expiresAt: 0,
|
|
72
|
+
email: gh.user || 'GitHub Copilot User',
|
|
73
|
+
label: gh.user || 'copilot-cli',
|
|
74
|
+
source: 'file',
|
|
75
|
+
path: filePath,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} else if (filePath.endsWith('.yml') || filePath.endsWith('.yaml')) {
|
|
79
|
+
const lines = content.split('\n')
|
|
80
|
+
let inGithub = false
|
|
81
|
+
let user = ''
|
|
82
|
+
let oauthToken = ''
|
|
83
|
+
for (const line of lines) {
|
|
84
|
+
const trimmed = line.trim()
|
|
85
|
+
if (/^github\.com:/.test(trimmed)) {
|
|
86
|
+
inGithub = true
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
if (inGithub) {
|
|
90
|
+
if (/^[a-zA-Z0-9.-]+:/.test(trimmed) && !trimmed.startsWith('oauth_token') && !trimmed.startsWith('user')) {
|
|
91
|
+
break
|
|
92
|
+
}
|
|
93
|
+
const userMatch = /user:\s*([^\s#]+)/.exec(trimmed)
|
|
94
|
+
if (userMatch) user = userMatch[1]
|
|
95
|
+
const tokenMatch = /oauth_token:\s*([^\s#]+)/.exec(trimmed)
|
|
96
|
+
if (tokenMatch) oauthToken = tokenMatch[1]
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (oauthToken) {
|
|
100
|
+
return {
|
|
101
|
+
accessToken: oauthToken,
|
|
102
|
+
githubToken: oauthToken,
|
|
103
|
+
refreshToken: oauthToken,
|
|
104
|
+
expiresAt: 0,
|
|
105
|
+
email: user || 'GitHub Copilot User',
|
|
106
|
+
label: user || 'gh-hosts',
|
|
107
|
+
source: 'file',
|
|
108
|
+
path: filePath,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
// Continue to next candidate
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Locate Copilot CLI binary on system.
|
|
122
|
+
* @param {string} [customPath] Explicit user-configured path
|
|
123
|
+
* @param {NodeJS.ProcessEnv} [env] Process environment
|
|
124
|
+
* @param {string} [platform] Process platform
|
|
125
|
+
* @returns {string|null} Resolved executable path or null
|
|
126
|
+
*/
|
|
127
|
+
export function findCopilotCli(customPath, env = process.env, platform = process.platform) {
|
|
128
|
+
if (customPath && typeof customPath === 'string' && customPath.trim()) {
|
|
129
|
+
const p = customPath.trim()
|
|
130
|
+
try {
|
|
131
|
+
if (existsSync(p)) return p
|
|
132
|
+
} catch { /* best-effort */ }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const pathVar = env.PATH || ''
|
|
136
|
+
const pathDirs = pathVar.split(delimiter).filter(Boolean)
|
|
137
|
+
|
|
138
|
+
const home = env.HOME || env.USERPROFILE || homedir()
|
|
139
|
+
if (home) {
|
|
140
|
+
pathDirs.push(
|
|
141
|
+
join(home, '.npm-global', 'bin'),
|
|
142
|
+
join(home, '.local', 'bin'),
|
|
143
|
+
join(home, 'AppData', 'Roaming', 'npm'),
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
for (const dir of pathDirs) {
|
|
148
|
+
for (const bin of COPILOT_CLI_BINARIES) {
|
|
149
|
+
const fullPath = join(dir, bin)
|
|
150
|
+
try {
|
|
151
|
+
if (existsSync(fullPath)) {
|
|
152
|
+
if (platform !== 'win32') {
|
|
153
|
+
try {
|
|
154
|
+
accessSync(fullPath, constants.X_OK)
|
|
155
|
+
} catch {
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return fullPath
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
// Continue searching
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return null
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Verify Copilot CLI availability and return status.
|
|
172
|
+
* @param {{ customPath?: string, env?: NodeJS.ProcessEnv }} [options]
|
|
173
|
+
* @returns {{ available: boolean, path?: string, error?: string }}
|
|
174
|
+
*/
|
|
175
|
+
export function isCopilotCliAvailable(options = {}) {
|
|
176
|
+
const binary = findCopilotCli(options.customPath, options.env)
|
|
177
|
+
if (binary) {
|
|
178
|
+
return { available: true, path: binary }
|
|
179
|
+
}
|
|
180
|
+
return { available: false, error: 'copilot_binary_not_found' }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Translate DSH turn options to Copilot CLI arguments and environment.
|
|
185
|
+
* @param {any} options
|
|
186
|
+
* @param {any} [blob]
|
|
187
|
+
* @param {string} [binaryPath]
|
|
188
|
+
* @returns {{ args: string[], env: NodeJS.ProcessEnv, inputPayload: string }}
|
|
189
|
+
*/
|
|
190
|
+
export function prepareCopilotCliInvocation(options, blob = {}, _binaryPath = 'copilot') {
|
|
191
|
+
const model = options.model || 'claude-3.7-sonnet'
|
|
192
|
+
const token = blob.accessToken || blob.githubToken || blob.refreshToken || ''
|
|
193
|
+
|
|
194
|
+
const args = [
|
|
195
|
+
'--model', model,
|
|
196
|
+
'--stream',
|
|
197
|
+
'--json',
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
if (options.temperature != null) {
|
|
201
|
+
args.push('--temperature', String(options.temperature))
|
|
202
|
+
}
|
|
203
|
+
if (options.maxTokens != null) {
|
|
204
|
+
args.push('--max-tokens', String(options.maxTokens))
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (Array.isArray(options.tools) && options.tools.length) {
|
|
208
|
+
args.push('--tools', JSON.stringify(options.tools))
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const env = {
|
|
212
|
+
...process.env,
|
|
213
|
+
...(token ? {
|
|
214
|
+
GITHUB_TOKEN: token,
|
|
215
|
+
GH_TOKEN: token,
|
|
216
|
+
GITHUB_COPILOT_TOKEN: token,
|
|
217
|
+
COPILOT_API_KEY: token,
|
|
218
|
+
} : {}),
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const inputPayload = JSON.stringify({
|
|
222
|
+
model,
|
|
223
|
+
messages: options.messages || (options.prompt ? [{ role: 'user', content: options.prompt }] : []),
|
|
224
|
+
tools: options.tools || [],
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
return { args, env, inputPayload }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Stream execution from local Copilot CLI process.
|
|
232
|
+
* Yields canonical DSH events: block-start, text-delta, tool-call-delta, finish, usage.
|
|
233
|
+
* @param {object} params
|
|
234
|
+
* @param {string} params.binaryPath
|
|
235
|
+
* @param {any} params.options
|
|
236
|
+
* @param {any} [params.blob]
|
|
237
|
+
* @param {any} [params.config]
|
|
238
|
+
* @param {AbortSignal} [params.signal]
|
|
239
|
+
* @param {Function} [params.spawnImpl] Test seam for child_process.spawn
|
|
240
|
+
* @returns {AsyncGenerator<any, void, unknown>}
|
|
241
|
+
*/
|
|
242
|
+
export async function* streamCopilotCli({ binaryPath, options, blob, _config, signal, spawnImpl }) {
|
|
243
|
+
if (signal && signal.aborted) {
|
|
244
|
+
throw new LlmError('Request aborted by client', 'ABORTED')
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const { args, env, inputPayload } = prepareCopilotCliInvocation(options, blob, binaryPath)
|
|
248
|
+
const spawner = spawnImpl || spawn
|
|
249
|
+
|
|
250
|
+
let child
|
|
251
|
+
try {
|
|
252
|
+
child = spawner(binaryPath, args, {
|
|
253
|
+
env,
|
|
254
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
255
|
+
})
|
|
256
|
+
} catch (err) {
|
|
257
|
+
throw new LlmError(`Failed to spawn Copilot CLI: ${err && err.message ? err.message : err}`, 'INTERNAL_ERROR')
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (signal) {
|
|
261
|
+
const abortHandler = () => {
|
|
262
|
+
try { child.kill('SIGTERM') } catch { /* best-effort */ }
|
|
263
|
+
}
|
|
264
|
+
signal.addEventListener('abort', abortHandler, { once: true })
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (child.stdin) {
|
|
268
|
+
child.stdin.write(inputPayload)
|
|
269
|
+
child.stdin.end()
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
let textStarted = false
|
|
273
|
+
let toolIndex = 0
|
|
274
|
+
let sawTools = false
|
|
275
|
+
const toolCallMap = new Map()
|
|
276
|
+
|
|
277
|
+
let stdoutBuffer = ''
|
|
278
|
+
let stderrBuffer = ''
|
|
279
|
+
|
|
280
|
+
if (child.stderr) {
|
|
281
|
+
child.stderr.on('data', (d) => { stderrBuffer += d.toString() })
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const chunkQueue = []
|
|
285
|
+
let resolveNext = null
|
|
286
|
+
let processEnded = false
|
|
287
|
+
let processError = null
|
|
288
|
+
|
|
289
|
+
child.stdout.on('data', (chunk) => {
|
|
290
|
+
stdoutBuffer += chunk.toString()
|
|
291
|
+
let newlineIndex
|
|
292
|
+
while ((newlineIndex = stdoutBuffer.indexOf('\n')) >= 0) {
|
|
293
|
+
const line = stdoutBuffer.slice(0, newlineIndex).trim()
|
|
294
|
+
stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1)
|
|
295
|
+
if (line) {
|
|
296
|
+
chunkQueue.push(line)
|
|
297
|
+
if (resolveNext) {
|
|
298
|
+
const r = resolveNext
|
|
299
|
+
resolveNext = null
|
|
300
|
+
r()
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
child.on('error', (err) => {
|
|
307
|
+
processError = err
|
|
308
|
+
if (resolveNext) {
|
|
309
|
+
const r = resolveNext
|
|
310
|
+
resolveNext = null
|
|
311
|
+
r()
|
|
312
|
+
}
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
child.on('close', (code) => {
|
|
316
|
+
processEnded = true
|
|
317
|
+
if (stdoutBuffer.trim()) {
|
|
318
|
+
chunkQueue.push(stdoutBuffer.trim())
|
|
319
|
+
stdoutBuffer = ''
|
|
320
|
+
}
|
|
321
|
+
if (code !== 0 && !chunkQueue.length) {
|
|
322
|
+
processError = new LlmError(`Copilot CLI exited with code ${code}: ${stderrBuffer.trim() || 'unknown error'}`, 'VENDOR')
|
|
323
|
+
}
|
|
324
|
+
if (resolveNext) {
|
|
325
|
+
const r = resolveNext
|
|
326
|
+
resolveNext = null
|
|
327
|
+
r()
|
|
328
|
+
}
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
while (true) {
|
|
332
|
+
if (chunkQueue.length) {
|
|
333
|
+
const line = chunkQueue.shift()
|
|
334
|
+
const raw = line.startsWith('data:') ? line.slice(5).trim() : line
|
|
335
|
+
if (raw === '[DONE]') break
|
|
336
|
+
|
|
337
|
+
let parsed = null
|
|
338
|
+
try { parsed = JSON.parse(raw) } catch { /* text fragment */ }
|
|
339
|
+
|
|
340
|
+
if (parsed) {
|
|
341
|
+
// 1. Text delta
|
|
342
|
+
const text = parsed.text || parsed.delta?.content || parsed.content || (parsed.choices && parsed.choices[0]?.delta?.content)
|
|
343
|
+
if (typeof text === 'string' && text) {
|
|
344
|
+
if (!textStarted) {
|
|
345
|
+
yield { type: 'block-start', index: 0, blockType: 'text' }
|
|
346
|
+
textStarted = true
|
|
347
|
+
}
|
|
348
|
+
yield { type: 'text-delta', index: 0, text }
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 2. Tool calls
|
|
352
|
+
const toolCalls = parsed.tool_calls || parsed.delta?.tool_calls || (parsed.choices && parsed.choices[0]?.delta?.tool_calls) || (parsed.tool_call ? [parsed.tool_call] : null)
|
|
353
|
+
if (Array.isArray(toolCalls)) {
|
|
354
|
+
sawTools = true
|
|
355
|
+
for (const tc of toolCalls) {
|
|
356
|
+
const id = tc.id || `call_${toolIndex}`
|
|
357
|
+
const name = tc.name || (tc.function && tc.function.name)
|
|
358
|
+
const argsDelta = tc.argumentsDelta || tc.arguments || (tc.function && tc.function.arguments) || ''
|
|
359
|
+
let idx = toolCallMap.get(id)
|
|
360
|
+
if (idx === undefined) {
|
|
361
|
+
idx = ++toolIndex
|
|
362
|
+
toolCallMap.set(id, idx)
|
|
363
|
+
yield { type: 'block-start', index: idx, blockType: 'tool-call' }
|
|
364
|
+
}
|
|
365
|
+
yield {
|
|
366
|
+
type: 'tool-call-delta',
|
|
367
|
+
index: idx,
|
|
368
|
+
id,
|
|
369
|
+
...(name ? { name } : {}),
|
|
370
|
+
argumentsDelta: typeof argsDelta === 'string' ? argsDelta : JSON.stringify(argsDelta),
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// 3. Usage
|
|
376
|
+
if (parsed.usage) {
|
|
377
|
+
yield { type: 'usage', usage: parsed.usage }
|
|
378
|
+
}
|
|
379
|
+
} else if (raw) {
|
|
380
|
+
// Plain text stdout line
|
|
381
|
+
if (!textStarted) {
|
|
382
|
+
yield { type: 'block-start', index: 0, blockType: 'text' }
|
|
383
|
+
textStarted = true
|
|
384
|
+
}
|
|
385
|
+
yield { type: 'text-delta', index: 0, text: raw }
|
|
386
|
+
}
|
|
387
|
+
} else if (processEnded) {
|
|
388
|
+
if (processError) throw processError
|
|
389
|
+
break
|
|
390
|
+
} else {
|
|
391
|
+
await new Promise((res) => { resolveNext = res })
|
|
392
|
+
if (processError && !chunkQueue.length) throw processError
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
yield {
|
|
397
|
+
type: 'finish',
|
|
398
|
+
reason: { kind: sawTools ? 'tool-calls' : 'stop' },
|
|
399
|
+
}
|
|
400
|
+
}
|
package/lib/vendors/copilot.js
CHANGED
|
@@ -2,6 +2,7 @@ import { LlmError } from "@deepseek-ai/dsh-llm"
|
|
|
2
2
|
import { openaiMessages, openaiTools, modelCatalog } from "../messages.js"
|
|
3
3
|
import { fetchWithTimeout } from "../http.js"
|
|
4
4
|
import { openaiChatStream, readJson, httpError } from "../wire.js"
|
|
5
|
+
import { findCopilotCli, streamCopilotCli, isCopilotCliAvailable, COPILOT_CLI_BINARIES } from "./copilot-cli.js"
|
|
5
6
|
|
|
6
7
|
export const id = "copilot"
|
|
7
8
|
|
|
@@ -53,6 +54,14 @@ const COPILOT_MODELS = [
|
|
|
53
54
|
}
|
|
54
55
|
]
|
|
55
56
|
|
|
57
|
+
export function copilotCliInfo(config = {}) {
|
|
58
|
+
const status = isCopilotCliAvailable({ customPath: config && config.copilotCliPath })
|
|
59
|
+
return {
|
|
60
|
+
...status,
|
|
61
|
+
supportedBinaries: COPILOT_CLI_BINARIES,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
56
65
|
export function providerInfo() {
|
|
57
66
|
return { id, name: "GitHub Copilot" }
|
|
58
67
|
}
|
|
@@ -280,7 +289,28 @@ export async function usage(blob, config, fetchImpl) {
|
|
|
280
289
|
}
|
|
281
290
|
}
|
|
282
291
|
|
|
283
|
-
export async function* streamOnce({ blob, options, fetchImpl, headers, config, signal }) {
|
|
292
|
+
export async function* streamOnce({ blob, options, fetchImpl, headers, config, signal, spawnImpl }) {
|
|
293
|
+
const mode = (config && config.copilotMode) || (blob && blob.copilotMode) || 'auto'
|
|
294
|
+
const customPath = (config && config.copilotCliPath) || (blob && blob.copilotCliPath) || ''
|
|
295
|
+
|
|
296
|
+
if (mode !== 'web') {
|
|
297
|
+
const cliPath = findCopilotCli(customPath)
|
|
298
|
+
if (cliPath) {
|
|
299
|
+
try {
|
|
300
|
+
let yieldedAny = false
|
|
301
|
+
for await (const chunk of streamCopilotCli({ binaryPath: cliPath, options, blob, config, signal, spawnImpl })) {
|
|
302
|
+
yieldedAny = true
|
|
303
|
+
yield chunk
|
|
304
|
+
}
|
|
305
|
+
if (yieldedAny) return
|
|
306
|
+
} catch (err) {
|
|
307
|
+
if (mode === 'cli') throw err
|
|
308
|
+
// In auto mode, fallback seamlessly to web / OAuth execution
|
|
309
|
+
}
|
|
310
|
+
} else if (mode === 'cli') {
|
|
311
|
+
throw new LlmError('GitHub Copilot CLI binary not found on system', 'NOT_CONFIGURED')
|
|
312
|
+
}
|
|
313
|
+
}
|
|
284
314
|
const impl = fetchImpl || fetch
|
|
285
315
|
let copilotToken = blob && blob.accessToken
|
|
286
316
|
const githubToken = blob && (blob.refreshToken || blob.githubToken)
|
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.66",
|
|
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",
|