@ljwei-stak/dsh-model-router 0.19.0 → 0.20.0
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/.dsh-plugin/client.js +82 -20
- package/.dsh-plugin/index.mjs +219 -74
- package/.dsh-plugin/shared/official-team-runtime.mjs +189 -49
- package/.dsh-plugin/shared/official-tool-executor.mjs +342 -69
- package/.dsh-plugin/shared/official-tools-remote.mjs +12 -0
- package/.dsh-plugin/shared/official-tools-runtime.mjs +22 -1
- package/.dsh-plugin/shared/run-ledger.mjs +51 -2
- package/.dsh-plugin/shared/task-executors.mjs +62 -18
- package/.dsh-plugin/shared/tool-health.mjs +24 -6
- package/CHANGELOG.md +18 -0
- package/README.md +19 -13
- package/README.zh.md +18 -12
- package/docs/WORKBENCH_USER_GUIDE.zh.md +32 -8
- package/docs/releases/0.20.0-validation.md +26 -0
- package/package.json +4 -3
|
@@ -127,6 +127,7 @@ const rerunRequestCodec = strictCodec(`${OFFICIAL_TOOLS_REMOTE_PACKAGE}#RerunReq
|
|
|
127
127
|
...(provider ? { provider, model } : {}),
|
|
128
128
|
confirmOverBudget: request.confirmOverBudget === true,
|
|
129
129
|
confirmWrite: request.confirmWrite === true,
|
|
130
|
+
...(request.confirmUnsandboxed === undefined ? {} : { confirmUnsandboxed: request.confirmUnsandboxed === true }),
|
|
130
131
|
...(['api', 'subscription', 'cancel'].includes(request.subscriptionChoice) ? { subscriptionChoice: request.subscriptionChoice } : {}),
|
|
131
132
|
}
|
|
132
133
|
})
|
|
@@ -152,11 +153,22 @@ export function parseRunRequest(value) {
|
|
|
152
153
|
throw new TypeError('budgetUsd must be a non-negative number')
|
|
153
154
|
}
|
|
154
155
|
const confirmed = Array.isArray(request.confirmedReasons) ? request.confirmedReasons : []
|
|
156
|
+
if (request.executor !== undefined && !['routed', 'official-team'].includes(request.executor)) throw new TypeError('executor is invalid')
|
|
157
|
+
if (request.executionMode !== undefined && !['read-only', 'workspace-write'].includes(request.executionMode)) throw new TypeError('executionMode is invalid')
|
|
158
|
+
if (request.executionEnvironment !== undefined && !['harness', 'cli-native'].includes(request.executionEnvironment)) throw new TypeError('executionEnvironment is invalid')
|
|
159
|
+
const officialTeam = request.executor === 'official-team'
|
|
160
|
+
if (officialTeam && (request.planMode !== 'team' || provider || model)) throw new TypeError('official-team requires a team plan without an explicit route')
|
|
161
|
+
if (!officialTeam && request.executionMode === 'workspace-write') throw new TypeError('workspace-write requires official-team')
|
|
162
|
+
if (request.executionEnvironment === 'cli-native' && (!officialTeam || request.executionMode !== 'workspace-write')) {
|
|
163
|
+
throw new TypeError('cli-native requires official-team with workspace-write')
|
|
164
|
+
}
|
|
155
165
|
if (confirmed.length > RUN_CONFIRMATION_CODES.length || confirmed.some(code => !RUN_CONFIRMATION_CODES.includes(code))) {
|
|
156
166
|
throw new TypeError('confirmedReasons contains an unknown reason')
|
|
157
167
|
}
|
|
158
168
|
return {
|
|
159
169
|
task: request.task,
|
|
170
|
+
...(officialTeam ? { executor: 'official-team', executionMode: request.executionMode ?? 'read-only',
|
|
171
|
+
...(request.executionEnvironment === undefined ? {} : { executionEnvironment: request.executionEnvironment }) } : {}),
|
|
160
172
|
...(provider ? { provider, model } : {}),
|
|
161
173
|
...(['single', 'team'].includes(request.planMode) ? { planMode: request.planMode } : {}),
|
|
162
174
|
...(['economy', 'balanced', 'quality'].includes(request.preset) ? { preset: request.preset } : {}),
|
|
@@ -850,6 +850,22 @@ export async function probeToolWith(tool, runner, {
|
|
|
850
850
|
return probe
|
|
851
851
|
}
|
|
852
852
|
|
|
853
|
+
/** Reuse the concrete entry discovered by the installation probe for login checks. */
|
|
854
|
+
export async function loginLaunchForProbe(probe, {
|
|
855
|
+
workspace = process.cwd(),
|
|
856
|
+
fileExists = async path => (await stat(path).catch(() => null))?.isFile() === true,
|
|
857
|
+
findNode = async cwd => (await import('./official-tool-executor.mjs')).findNodeExecutable(cwd),
|
|
858
|
+
} = {}) {
|
|
859
|
+
if (probe?.installed !== true || !getOfficialTool(probe.id)) return null
|
|
860
|
+
const entry = probe.packageEntry || probe.executablePath
|
|
861
|
+
if (typeof entry !== 'string' || !isAbsolute(entry) || !(await fileExists(entry))) return null
|
|
862
|
+
if (/\.(?:mjs|cjs|js)$/i.test(entry)) {
|
|
863
|
+
const node = await findNode(workspace)
|
|
864
|
+
return node ? { file: node, args: [entry], useShell: false } : null
|
|
865
|
+
}
|
|
866
|
+
return { file: entry, args: [], useShell: IS_WINDOWS && /\.(?:cmd|bat)$/i.test(entry) }
|
|
867
|
+
}
|
|
868
|
+
|
|
853
869
|
async function selectedPackageEntries(tool, { plan, preferences: selectedPreferences }) {
|
|
854
870
|
const { findGlobalPackageEntries, officialToolEntrySelection } = await import('./official-tool-executor.mjs')
|
|
855
871
|
const selection = officialToolEntrySelection(tool.id, { preferences: selectedPreferences, plan })
|
|
@@ -888,7 +904,12 @@ async function probeUncached(tool, runner, { selectedPreferences, plan, fileExis
|
|
|
888
904
|
...selectedPreferences,
|
|
889
905
|
allowScriptInstall: installMethodFor(tool, selectedPreferences.methods?.[tool.id])?.kind === 'script' ? true : selectedPreferences.allowScriptInstall,
|
|
890
906
|
}, currentInstallOptions())
|
|
891
|
-
} catch {
|
|
907
|
+
} catch (error) {
|
|
908
|
+
if (usesConfiguredInstallPlan(tool.id, selectedPreferences)) {
|
|
909
|
+
return { id: tool.id, installed: false, version: null, status: 'probe-failed',
|
|
910
|
+
detail: `无法检查所选安装方式或目录:${String(error?.message ?? error).slice(0, 200)}` }
|
|
911
|
+
}
|
|
912
|
+
}
|
|
892
913
|
}
|
|
893
914
|
// npm and pnpm can write the same shim into a unified bin directory. Its
|
|
894
915
|
// banner belongs to whichever manager wrote it last, not the selected copy.
|
|
@@ -12,12 +12,30 @@ export const MAX_QUALITY_BIAS = 0.04
|
|
|
12
12
|
const finite = value => typeof value === 'number' && Number.isFinite(value)
|
|
13
13
|
const routeKey = (provider, model) => `${String(provider ?? '')}\u0000${String(model ?? '')}`
|
|
14
14
|
|
|
15
|
+
function modelIdentity(value) {
|
|
16
|
+
if (typeof value === 'string' && value.trim()) return value.trim()
|
|
17
|
+
if (value && typeof value === 'object' && !Array.isArray(value)
|
|
18
|
+
&& typeof value.providerId === 'string' && value.providerId
|
|
19
|
+
&& typeof value.modelId === 'string' && value.modelId) return `${value.providerId}/${value.modelId}`
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
|
|
15
23
|
/** USD cost of one package. Prefers a CLI-reported cost, else usage × configured price. */
|
|
16
24
|
export function actualCost(result, pricing) {
|
|
17
25
|
if (result && Object.hasOwn(result, 'reportedCostUsd') && finite(result.reportedCostUsd) && result.reportedCostUsd >= 0) {
|
|
18
26
|
return { costUsd: result.reportedCostUsd, costSource: 'cli-reported' }
|
|
19
27
|
}
|
|
20
28
|
const usage = result && Object.hasOwn(result, 'usage') ? result.usage : null
|
|
29
|
+
if (usage && result.usageIncomplete === true) return { costUsd: null, costSource: 'usage-incomplete' }
|
|
30
|
+
if (usage && result.channel === 'official-cli') {
|
|
31
|
+
const actualModel = modelIdentity(result.actualModel) ?? modelIdentity(result.reportedModel)
|
|
32
|
+
if (!actualModel) return { costUsd: null, costSource: 'model-unknown' }
|
|
33
|
+
const pricedModel = pricing?.model ?? result.model
|
|
34
|
+
const provider = pricing?.provider ?? result.provider
|
|
35
|
+
const matches = [pricedModel, pricing?.cliModel, provider && pricedModel ? `${provider}/${pricedModel}` : null]
|
|
36
|
+
.some(value => typeof value === 'string' && value === actualModel)
|
|
37
|
+
if (!matches) return { costUsd: null, costSource: 'model-price-unverified' }
|
|
38
|
+
}
|
|
21
39
|
if (!usage || !pricing || !Object.hasOwn(pricing, 'input') || !Object.hasOwn(pricing, 'output')
|
|
22
40
|
|| pricing.input === undefined || pricing.input === null || pricing.output === undefined || pricing.output === null) {
|
|
23
41
|
return { costUsd: null, costSource: usage ? 'price-missing' : 'usage-missing' }
|
|
@@ -268,12 +286,25 @@ function storedPackage(planned, result, pricing, finishedAt, billingFor = billin
|
|
|
268
286
|
channel: result?.channel ?? null,
|
|
269
287
|
toolId: result?.toolId ?? null,
|
|
270
288
|
credentialSource: result?.credentialSource ?? null,
|
|
271
|
-
...(result?.
|
|
289
|
+
...(result?.channel === 'official-cli' ? {
|
|
290
|
+
requestedCliModel: result.requestedCliModel ?? null,
|
|
291
|
+
actualModel: modelIdentity(result.actualModel) ?? modelIdentity(result.reportedModel),
|
|
292
|
+
modelNotice: result.modelNotice ?? null,
|
|
293
|
+
} : {}),
|
|
294
|
+
...(result?.channel !== 'official-cli' && result?.actualModel ? { actualModel: String(result.actualModel) } : {}),
|
|
295
|
+
...(result?.reportedModel !== undefined ? { reportedModel: result.reportedModel } : {}),
|
|
296
|
+
...(typeof result?.source === 'string' ? { source: result.source } : {}),
|
|
297
|
+
...(typeof result?.sandboxEnforcement === 'string' ? { sandboxEnforcement: result.sandboxEnforcement } : {}),
|
|
298
|
+
...(result?.usageIncomplete === true ? { usageIncomplete: true } : {}),
|
|
299
|
+
...(typeof result?.executionEnvironment === 'string' ? { executionEnvironment: result.executionEnvironment } : {}),
|
|
300
|
+
...(finite(result?.reportedCostUsd) && result.reportedCostUsd >= 0 ? { reportedCostUsd: result.reportedCostUsd } : {}),
|
|
272
301
|
fallback: result?.fallback ?? null,
|
|
273
302
|
...(result?.billingMode ? { billingMode: String(result.billingMode) } : {}),
|
|
274
303
|
...(result?.billingSwitch ? { billingSwitch: { ...result.billingSwitch } } : {}),
|
|
275
304
|
...(result?.subscriptionRoute ? { subscriptionRoute: { provider: String(result.subscriptionRoute.provider ?? ''), model: String(result.subscriptionRoute.model ?? '') } } : {}),
|
|
276
305
|
error: result?.ok ? null : (result?.error ?? null),
|
|
306
|
+
...(!result?.ok && result?.diagnostic ? { diagnostic: String(result.diagnostic).slice(0, 2_000) } : {}),
|
|
307
|
+
...(Number.isInteger(result?.exitCode) ? { exitCode: result.exitCode } : {}),
|
|
277
308
|
answer: String(result?.answer ?? '').slice(0, MAX_STORED_ANSWER),
|
|
278
309
|
answerTruncated: String(result?.answer ?? '').length > MAX_STORED_ANSWER,
|
|
279
310
|
usage: result?.usage ?? null,
|
|
@@ -375,12 +406,21 @@ export function teamExecutionResults(plan, execution) {
|
|
|
375
406
|
cancelled: result.status === 'cancelled',
|
|
376
407
|
provider: result.provider,
|
|
377
408
|
model: result.recommendedModel,
|
|
409
|
+
requestedCliModel: result.requestedCliModel ?? null,
|
|
378
410
|
actualModel: result.actualModel ?? null,
|
|
411
|
+
...(result.reportedModel !== undefined ? { reportedModel: result.reportedModel } : {}),
|
|
412
|
+
modelNotice: result.modelNotice ?? null,
|
|
413
|
+
...(typeof result.source === 'string' ? { source: result.source } : {}),
|
|
414
|
+
...(typeof result.sandboxEnforcement === 'string' ? { sandboxEnforcement: result.sandboxEnforcement } : {}),
|
|
415
|
+
...(result.usageIncomplete === true ? { usageIncomplete: true } : {}),
|
|
416
|
+
...(typeof result.executionEnvironment === 'string' ? { executionEnvironment: result.executionEnvironment } : {}),
|
|
379
417
|
channel: 'official-cli',
|
|
380
418
|
toolId: result.toolId ?? null,
|
|
381
419
|
answer: String(result.finalText ?? ''),
|
|
382
420
|
error: result.status === 'succeeded' ? null : String(result.error ?? (TEAM_FAILURE.has(result.status) ? result.status : '官方 CLI 未成功完成。')),
|
|
383
421
|
stderrTail: result.outputTail ?? null,
|
|
422
|
+
diagnostic: result.outputTail ?? null,
|
|
423
|
+
exitCode: result.exitCode ?? null,
|
|
384
424
|
...(result.usage ? { usage: result.usage } : {}),
|
|
385
425
|
...(finite(result.reportedCostUsd) ? { reportedCostUsd: result.reportedCostUsd } : {}),
|
|
386
426
|
}))
|
|
@@ -421,9 +461,18 @@ export function buildToolRunRecord({ toolId, toolLabel, provider, model, cliMode
|
|
|
421
461
|
id: packageId, ok: succeeded, cancelled: result?.status === 'cancelled',
|
|
422
462
|
provider: plan.directRoute.provider, model: plan.directRoute.model,
|
|
423
463
|
channel: 'official-cli', toolId,
|
|
424
|
-
|
|
464
|
+
requestedCliModel: Object.hasOwn(result ?? {}, 'requestedModel') ? result.requestedModel : cliModel,
|
|
465
|
+
actualModel: modelIdentity(result?.actualModel) ?? modelIdentity(result?.reportedModel),
|
|
466
|
+
...(result?.reportedModel !== undefined ? { reportedModel: result.reportedModel } : {}),
|
|
467
|
+
modelNotice: result?.modelNotice ?? null,
|
|
468
|
+
...(typeof result?.source === 'string' ? { source: result.source } : {}),
|
|
469
|
+
...(typeof result?.sandboxEnforcement === 'string' ? { sandboxEnforcement: result.sandboxEnforcement } : {}),
|
|
470
|
+
...(result?.usageIncomplete === true ? { usageIncomplete: true } : {}),
|
|
425
471
|
answer: String(result?.finalText ?? ''),
|
|
426
472
|
error: succeeded ? null : String(result?.error ?? result?.reason ?? result?.status ?? '官方 CLI 未成功完成。'),
|
|
473
|
+
...(!succeeded && typeof result?.diagnostic === 'string' && result.diagnostic.trim()
|
|
474
|
+
? { diagnostic: result.diagnostic } : {}),
|
|
475
|
+
...(Number.isInteger(result?.exitCode) ? { exitCode: result.exitCode } : {}),
|
|
427
476
|
...(result?.usage ? { usage: result.usage } : {}),
|
|
428
477
|
...(finite(result?.reportedCostUsd) ? { reportedCostUsd: result.reportedCostUsd } : {}),
|
|
429
478
|
}],
|
|
@@ -36,7 +36,7 @@ const BASE_ENV_KEYS = Object.freeze([
|
|
|
36
36
|
'PATH', 'Path', 'PATHEXT', 'SystemRoot', 'windir', 'ComSpec',
|
|
37
37
|
'HOME', 'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH', 'APPDATA', 'LOCALAPPDATA',
|
|
38
38
|
'PROGRAMDATA', 'TEMP', 'TMP', 'TMPDIR', 'LANG', 'LC_ALL', 'TERM',
|
|
39
|
-
'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy',
|
|
39
|
+
'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'all_proxy', 'no_proxy',
|
|
40
40
|
'SSL_CERT_FILE', 'NODE_EXTRA_CA_CERTS',
|
|
41
41
|
])
|
|
42
42
|
|
|
@@ -50,7 +50,9 @@ export const TASK_ADAPTERS = Object.freeze([
|
|
|
50
50
|
executable: 'claude',
|
|
51
51
|
portable: true,
|
|
52
52
|
apiKeyEnv: 'ANTHROPIC_API_KEY',
|
|
53
|
-
sessionEnv: Object.freeze(['CLAUDE_CODE_OAUTH_TOKEN', 'CLAUDE_CONFIG_DIR'
|
|
53
|
+
sessionEnv: Object.freeze(['CLAUDE_CODE_OAUTH_TOKEN', 'CLAUDE_CONFIG_DIR', 'ANTHROPIC_AUTH_TOKEN',
|
|
54
|
+
'ANTHROPIC_BASE_URL', 'ANTHROPIC_MODEL', 'ANTHROPIC_DEFAULT_HAIKU_MODEL', 'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
55
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL', 'ANTHROPIC_SMALL_FAST_MODEL']),
|
|
54
56
|
format: 'claude-json',
|
|
55
57
|
buildArgs(modelId) {
|
|
56
58
|
return [
|
|
@@ -193,17 +195,22 @@ async function checkedWorkspace(workspace) {
|
|
|
193
195
|
}
|
|
194
196
|
|
|
195
197
|
function cliModelFor(route, adapter) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
198
|
+
// Catalog IDs are routing recommendations, not vendor CLI aliases. Omit
|
|
199
|
+
// the model argument unless the route explicitly binds a CLI model.
|
|
200
|
+
const value = route?.cliModel
|
|
201
|
+
if (value === undefined || value === null || value === '') return null
|
|
202
|
+
const explicit = checkedModel(value)
|
|
203
|
+
if (!explicit) throw new TypeError('cliModel must be a bounded model identifier')
|
|
204
|
+
if ((adapter?.id === 'minimax-code' || adapter?.id === 'mimo-code') && !explicit.includes('/')) {
|
|
205
|
+
throw new TypeError(`${adapter.id} cliModel requires provider/model`)
|
|
200
206
|
}
|
|
201
|
-
|
|
207
|
+
if (adapter?.id === 'zcode') throw new TypeError('ZCode CLI does not support a per-call cliModel')
|
|
208
|
+
return explicit
|
|
202
209
|
}
|
|
203
210
|
|
|
204
211
|
/** Variables that switch a CLI from its account login to API-key billing. */
|
|
205
212
|
const API_KEY_VARIABLES = Object.freeze({
|
|
206
|
-
'claude-code': ['ANTHROPIC_API_KEY'],
|
|
213
|
+
'claude-code': ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN'],
|
|
207
214
|
codex: ['OPENAI_API_KEY', 'CODEX_API_KEY'],
|
|
208
215
|
gemini: ['GEMINI_API_KEY', 'GOOGLE_API_KEY'],
|
|
209
216
|
})
|
|
@@ -324,7 +331,7 @@ function parseAdapterOutput(format, stdout) {
|
|
|
324
331
|
const DIAGNOSTIC_CHARS = 400
|
|
325
332
|
|
|
326
333
|
/** Shorten and scrub a CLI diagnostic before it reaches the user or the run ledger. */
|
|
327
|
-
export function redactDiagnostic(value, secret = '') {
|
|
334
|
+
export function redactDiagnostic(value, secret = '', maxChars = DIAGNOSTIC_CHARS) {
|
|
328
335
|
const scrubbed = redact(String(value ?? ''), secret)
|
|
329
336
|
.replace(/sk-[A-Za-z0-9_-]{8,}/g, 'sk-[redacted]')
|
|
330
337
|
.replace(/(Bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi, '$1[redacted]')
|
|
@@ -332,7 +339,10 @@ export function redactDiagnostic(value, secret = '') {
|
|
|
332
339
|
.replace(/\b(api[_-]?key|token|secret|password)(["'\s:=]+)[^\s"',}]{6,}/gi, '$1$2[redacted]')
|
|
333
340
|
.replace(/\s+/g, ' ')
|
|
334
341
|
.trim()
|
|
335
|
-
|
|
342
|
+
const limit = Number.isInteger(maxChars) ? Math.max(100, Math.min(4_000, maxChars)) : DIAGNOSTIC_CHARS
|
|
343
|
+
if (scrubbed.length <= limit) return scrubbed
|
|
344
|
+
const head = Math.floor(limit / 3)
|
|
345
|
+
return `${scrubbed.slice(0, head)}…${scrubbed.slice(-(limit - head - 1))}`
|
|
336
346
|
}
|
|
337
347
|
|
|
338
348
|
function jsonLines(stdout) {
|
|
@@ -389,7 +399,9 @@ export function usageFromOutput(format, stdout = '') {
|
|
|
389
399
|
const result = JSON.parse(stdout)
|
|
390
400
|
const usage = result?.usage
|
|
391
401
|
if (!usage || typeof usage !== 'object') return null
|
|
392
|
-
const reported =
|
|
402
|
+
const reported = result.total_cost_usd
|
|
403
|
+
const models = result.modelUsage && typeof result.modelUsage === 'object' && !Array.isArray(result.modelUsage)
|
|
404
|
+
? Object.keys(result.modelUsage) : []
|
|
393
405
|
return {
|
|
394
406
|
usage: {
|
|
395
407
|
inputTokens: count(usage.input_tokens),
|
|
@@ -397,7 +409,8 @@ export function usageFromOutput(format, stdout = '') {
|
|
|
397
409
|
cacheReadTokens: count(usage.cache_read_input_tokens),
|
|
398
410
|
cacheWriteTokens: count(usage.cache_creation_input_tokens),
|
|
399
411
|
},
|
|
400
|
-
...(Number.isFinite(reported) && reported
|
|
412
|
+
...(Number.isFinite(reported) && reported >= 0 ? { reportedCostUsd: reported } : {}),
|
|
413
|
+
...(models.length === 1 && /^[A-Za-z0-9][A-Za-z0-9._:/\[\]-]{0,199}$/.test(models[0]) ? { reportedModel: models[0] } : {}),
|
|
401
414
|
}
|
|
402
415
|
}
|
|
403
416
|
if (format === 'codex-jsonl') {
|
|
@@ -442,26 +455,27 @@ export function usageFromEvents(format, value) {
|
|
|
442
455
|
if (format === 'grok-end') {
|
|
443
456
|
const usage = value?.usage
|
|
444
457
|
if (!usage || typeof usage !== 'object') return null
|
|
445
|
-
const reported =
|
|
458
|
+
const reported = value.total_cost_usd
|
|
446
459
|
return {
|
|
447
460
|
usage: {
|
|
448
461
|
inputTokens: count(usage.input_tokens), outputTokens: count(usage.output_tokens),
|
|
449
462
|
cacheReadTokens: count(usage.cache_read_input_tokens), cacheWriteTokens: count(usage.cache_creation_input_tokens),
|
|
450
463
|
},
|
|
451
|
-
...(value.cost_is_partial !== true && Number.isFinite(reported) && reported
|
|
464
|
+
...(value.cost_is_partial !== true && Number.isFinite(reported) && reported >= 0 ? { reportedCostUsd: reported } : {}),
|
|
452
465
|
}
|
|
453
466
|
}
|
|
454
467
|
if (format === 'mimo-steps') {
|
|
455
468
|
const parts = (Array.isArray(value) ? value : []).filter(part => part?.tokens && typeof part.tokens === 'object')
|
|
456
469
|
if (!parts.length) return null
|
|
457
470
|
const sum = pick => parts.reduce((total, part) => total + count(pick(part)), 0)
|
|
458
|
-
const
|
|
471
|
+
const costKnown = parts.every(part => Number.isFinite(part.cost) && part.cost >= 0)
|
|
472
|
+
const cost = costKnown ? parts.reduce((total, part) => total + part.cost, 0) : null
|
|
459
473
|
return {
|
|
460
474
|
usage: {
|
|
461
475
|
inputTokens: sum(part => part.tokens.input), outputTokens: sum(part => part.tokens.output) + sum(part => part.tokens.reasoning),
|
|
462
476
|
cacheReadTokens: sum(part => part.tokens.cache?.read), cacheWriteTokens: sum(part => part.tokens.cache?.write),
|
|
463
477
|
},
|
|
464
|
-
...(
|
|
478
|
+
...(costKnown ? { reportedCostUsd: Number(cost.toFixed(8)) } : {}),
|
|
465
479
|
}
|
|
466
480
|
}
|
|
467
481
|
if (format === 'minimax-result') {
|
|
@@ -648,11 +662,30 @@ async function useApi(apiFallback, { route, task, signal, reason, adapter, prefe
|
|
|
648
662
|
}
|
|
649
663
|
}
|
|
650
664
|
|
|
651
|
-
function
|
|
665
|
+
function reportedCliModel(value) {
|
|
666
|
+
if (typeof value === 'string' && value.trim()) return value.trim()
|
|
667
|
+
if (value && typeof value === 'object' && !Array.isArray(value)
|
|
668
|
+
&& typeof value.providerId === 'string' && value.providerId
|
|
669
|
+
&& typeof value.modelId === 'string' && value.modelId) {
|
|
670
|
+
return `${value.providerId}/${value.modelId}`
|
|
671
|
+
}
|
|
672
|
+
return null
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function officialSuccess({ route, adapter, preference, answer, exitCode, version, credentialSource, usage = null,
|
|
676
|
+
requestedCliModel = null, reportedModel = null, modelNotice = null, source, sandboxEnforcement }) {
|
|
677
|
+
const actualModel = reportedCliModel(reportedModel)
|
|
652
678
|
return {
|
|
653
679
|
ok: true,
|
|
654
680
|
provider: route.provider,
|
|
655
681
|
model: route.model,
|
|
682
|
+
recommendedModel: route.model,
|
|
683
|
+
requestedCliModel,
|
|
684
|
+
actualModel,
|
|
685
|
+
...(reportedModel !== null && reportedModel !== undefined ? { reportedModel } : {}),
|
|
686
|
+
modelNotice: modelNotice ?? (!requestedCliModel
|
|
687
|
+
? '本次使用厂商 CLI 已配置的默认模型;Harness 推荐模型 ID 未作为 CLI 模型参数。'
|
|
688
|
+
: actualModel ? null : '该 CLI 未返回可核验的实际模型 ID;请以厂商运行记录核对。'),
|
|
656
689
|
preference,
|
|
657
690
|
channel: 'official-cli',
|
|
658
691
|
toolId: adapter.id,
|
|
@@ -664,8 +697,11 @@ function officialSuccess({ route, adapter, preference, answer, exitCode, version
|
|
|
664
697
|
exitCode,
|
|
665
698
|
version: version ?? null,
|
|
666
699
|
credentialSource,
|
|
700
|
+
...(typeof source === 'string' ? { source } : {}),
|
|
701
|
+
...(typeof sandboxEnforcement === 'string' ? { sandboxEnforcement } : {}),
|
|
667
702
|
...(usage?.usage ? { usage: usage.usage } : {}),
|
|
668
|
-
...(usage?.reportedCostUsd ? { reportedCostUsd: usage.reportedCostUsd } : {}),
|
|
703
|
+
...(Number.isFinite(usage?.reportedCostUsd) && usage.reportedCostUsd >= 0 ? { reportedCostUsd: usage.reportedCostUsd } : {}),
|
|
704
|
+
...(usage?.usageIncomplete === true ? { usageIncomplete: true } : {}),
|
|
669
705
|
}
|
|
670
706
|
}
|
|
671
707
|
|
|
@@ -758,6 +794,13 @@ export async function executeAssignedTask({
|
|
|
758
794
|
return officialSuccess({
|
|
759
795
|
route, adapter, preference, answer: redact(verified.finalText, secret),
|
|
760
796
|
exitCode: verified.exitCode ?? 0, version: null, credentialSource,
|
|
797
|
+
requestedCliModel: Object.hasOwn(verified, 'requestedModel') ? verified.requestedModel : modelId,
|
|
798
|
+
reportedModel: verified.reportedModel,
|
|
799
|
+
modelNotice: verified.modelNotice,
|
|
800
|
+
source: verified.source,
|
|
801
|
+
sandboxEnforcement: verified.sandboxEnforcement,
|
|
802
|
+
usage: { usage: verified.usage, reportedCostUsd: verified.reportedCostUsd,
|
|
803
|
+
usageIncomplete: verified.usageIncomplete },
|
|
761
804
|
})
|
|
762
805
|
}
|
|
763
806
|
if (verified && verified.status !== 'unsupported') {
|
|
@@ -833,6 +876,7 @@ export async function executeAssignedTask({
|
|
|
833
876
|
route, adapter, preference, answer, exitCode: run.exitCode,
|
|
834
877
|
version: versionFromBanner(`${probe.stdout}\n${probe.stderr}`),
|
|
835
878
|
credentialSource,
|
|
879
|
+
requestedCliModel: modelId,
|
|
836
880
|
usage: usageFromOutput(adapter.format, run.stdout),
|
|
837
881
|
})
|
|
838
882
|
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { OFFICIAL_TOOLS, getOfficialTool } from './official-tool-registry.mjs'
|
|
16
16
|
import { compareReleaseVersions } from './latest-versions.mjs'
|
|
17
|
+
import { loginLaunchForProbe } from './official-tools-runtime.mjs'
|
|
17
18
|
|
|
18
19
|
export const LOGIN_CHECK_TIMEOUT_MS = 5_000
|
|
19
20
|
export const HEALTH_CACHE_MS = 10 * 60_000
|
|
@@ -34,7 +35,7 @@ export const LOGIN_GUIDES = Object.freeze({
|
|
|
34
35
|
|
|
35
36
|
/** Environment variable names whose presence means a CLI can authenticate without a session. */
|
|
36
37
|
const KEY_ENV = Object.freeze({
|
|
37
|
-
'claude-code': ['ANTHROPIC_API_KEY', 'CLAUDE_CODE_OAUTH_TOKEN'],
|
|
38
|
+
'claude-code': ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'CLAUDE_CODE_OAUTH_TOKEN'],
|
|
38
39
|
codex: ['OPENAI_API_KEY', 'CODEX_API_KEY'],
|
|
39
40
|
gemini: ['GEMINI_API_KEY', 'GOOGLE_API_KEY'],
|
|
40
41
|
'kimi-code': ['KIMI_API_KEY', 'MOONSHOT_API_KEY'],
|
|
@@ -92,7 +93,7 @@ function grokHomeHint(env) {
|
|
|
92
93
|
|
|
93
94
|
/** Variables that make the CLI bill an API account rather than a subscription login. */
|
|
94
95
|
const API_KEY_ENV = Object.freeze({
|
|
95
|
-
'claude-code': ['ANTHROPIC_API_KEY'],
|
|
96
|
+
'claude-code': ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN'],
|
|
96
97
|
codex: ['OPENAI_API_KEY', 'CODEX_API_KEY'],
|
|
97
98
|
gemini: ['GEMINI_API_KEY', 'GOOGLE_API_KEY'],
|
|
98
99
|
'kimi-code': ['KIMI_API_KEY', 'MOONSHOT_API_KEY'],
|
|
@@ -151,14 +152,30 @@ function guide(toolId) {
|
|
|
151
152
|
* Login state for one installed tool: 'logged-in', 'logged-out', or 'unknown'.
|
|
152
153
|
* `runner(executable, args, { timeoutMs })` resolves { ok, code, stdout, stderr, timedOut }.
|
|
153
154
|
*/
|
|
154
|
-
export async function checkLogin(toolId, {
|
|
155
|
+
export async function checkLogin(toolId, {
|
|
156
|
+
runner, env = process.env, exists = async () => false, home = '', readAuthState = async () => null,
|
|
157
|
+
probe, resolveLoginLaunch = loginLaunchForProbe,
|
|
158
|
+
} = {}) {
|
|
155
159
|
const keyNames = (KEY_ENV[toolId] ?? []).filter(name => typeof env?.[name] === 'string' && env[name].trim())
|
|
156
160
|
const viaKey = keyNames.length > 0 ? { state: 'logged-in', detail: `检测到环境变量 ${keyNames.join('、')}(未验证有效性)。`, source: 'environment',
|
|
157
161
|
billing: apiKeyEnvPresent(toolId, env) ? 'api-key' : 'subscription',
|
|
158
162
|
accountLogin: keyNames.some(name => !(API_KEY_ENV[toolId] ?? []).includes(name)) } : null
|
|
159
163
|
const keyBilling = apiKeyEnvPresent(toolId, env) ? 'api-key' : null
|
|
164
|
+
// A health report must inspect the same npm/pnpm/native copy whose version
|
|
165
|
+
// was shown. Never replace a missing selected entry with a bare PATH command.
|
|
166
|
+
const statusCommand = async (command, args) => {
|
|
167
|
+
const launch = probe === undefined
|
|
168
|
+
? { file: command, args: [] }
|
|
169
|
+
: await resolveLoginLaunch(probe)
|
|
170
|
+
if (!launch) return null
|
|
171
|
+
return runner(launch.file, [...launch.args, ...args], {
|
|
172
|
+
timeoutMs: LOGIN_CHECK_TIMEOUT_MS, env: sessionEnvironment(toolId, env),
|
|
173
|
+
...(launch.useShell === undefined ? {} : { useShell: launch.useShell }),
|
|
174
|
+
})
|
|
175
|
+
}
|
|
160
176
|
if (toolId === 'claude-code' && typeof runner === 'function') {
|
|
161
|
-
const outcome = await
|
|
177
|
+
const outcome = await statusCommand('claude', ['auth', 'status', '--json'])
|
|
178
|
+
if (!outcome) return viaKey ?? { state: 'unknown', detail: '所选安装副本的登录检测入口不可用;未使用 PATH 中的其他 Claude Code。' }
|
|
162
179
|
if (outcome.timedOut) return viaKey ?? { state: 'unknown', detail: '登录状态检测超时。' }
|
|
163
180
|
try {
|
|
164
181
|
const status = JSON.parse(outcome.stdout)
|
|
@@ -170,7 +187,8 @@ export async function checkLogin(toolId, { runner, env = process.env, exists = a
|
|
|
170
187
|
return viaKey ?? { state: 'unknown', detail: '无法解析 claude auth status 输出。' }
|
|
171
188
|
}
|
|
172
189
|
if (toolId === 'codex' && typeof runner === 'function') {
|
|
173
|
-
const outcome = await
|
|
190
|
+
const outcome = await statusCommand('codex', ['login', 'status'])
|
|
191
|
+
if (!outcome) return viaKey ?? { state: 'unknown', detail: '所选安装副本的登录检测入口不可用;未使用 PATH 中的其他 Codex。' }
|
|
174
192
|
if (outcome.timedOut) return viaKey ?? { state: 'unknown', detail: '登录状态检测超时。' }
|
|
175
193
|
const output = `${outcome.stdout}\n${outcome.stderr}`
|
|
176
194
|
if (/not logged in/i.test(output)) return viaKey ?? { state: 'logged-out', detail: 'codex login status 报告未登录。', source: 'cli' }
|
|
@@ -234,7 +252,7 @@ export async function checkToolHealth(probe, options = {}) {
|
|
|
234
252
|
const tool = getOfficialTool(probe?.id)
|
|
235
253
|
if (!tool) throw new TypeError('unknown official tool')
|
|
236
254
|
const installed = probe.installed === true
|
|
237
|
-
const login = installed ? await checkLogin(tool.id, options) : { state: 'unknown', detail: probe.status === 'not-installed' && /检测到已安装/.test(String(probe.detail ?? '')) ? String(probe.detail) : '尚未安装。' }
|
|
255
|
+
const login = installed ? await checkLogin(tool.id, { ...options, probe }) : { state: 'unknown', detail: probe.status === 'not-installed' && /检测到已安装/.test(String(probe.detail ?? '')) ? String(probe.detail) : '尚未安装。' }
|
|
238
256
|
const { command, steps } = guide(tool.id)
|
|
239
257
|
return {
|
|
240
258
|
id: tool.id,
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.20.0 — 2026-10-15
|
|
4
|
+
|
|
5
|
+
Fully quit Harness, including its tray or WebUI Host process, and restart after upgrading.
|
|
6
|
+
|
|
7
|
+
- Add **官方工具协作** to the workbench launcher for **团队分工**. Choose read-only checks or approved editing in an isolated Git worktree. Preview and the session tool share `planOfficialTeam` / `executeOfficialTeamPlan`, filter CLI readiness for the selected mode, execute dependency packages sequentially and record results. This executor has no automatic model-directory API fallback; editable integration retains the existing clean-source and matching-baseline checks.
|
|
8
|
+
- Preserve every vendor CLI's configured model by default. Catalog recommendation IDs are no longer passed as implicit CLI model arguments. Only explicit `cliModel`, saved route mappings or team `cliModelsJson` select a CLI model; package bindings take precedence over tool bindings and saved mappings. ZCode keeps its per-call model limitation.
|
|
9
|
+
- Retain verified CLI model, entry, sandbox, usage and reported-cost evidence through routed, single-tool and team records. Separate recommended, requested and actual reported model identities. Preserve a valid reported zero cost; incomplete usage or unknown/unmatched actual CLI model identity cannot be costed using a recommended catalog price.
|
|
10
|
+
- Bind installation/version and CLI login-status checks to the actual selected npm, pnpm or native copy. Refuse silent PATH substitution when the saved installation cannot be resolved or its concrete entry is unavailable.
|
|
11
|
+
- Keep Windows Codex's original `CODEX_HOME` under the reviewed Harness ACL state-directory contract for read-only tasks. Explicitly selected native editable teams can use Codex's own workspace-write policy. Real Codex network checks failed in this batch; they are not reported as accepted execution.
|
|
12
|
+
- Store Windows isolated Git worktrees in Host-derived per-source private temporary storage. Real use exposed source drives with Modify permission but no WRITE_OWNER, where the SDK could not initialize its ACL boundary. Keep source authorization and patch checks, reject redirected storage, and preserve failed worktrees for retry. A no-model SDK confined-write and cross-drive integration probe passed.
|
|
13
|
+
- Add an explicit **厂商 CLI 正常环境** option for editable official teams. It retains normal vendor state, shell, skills and MCP under current user permissions, outside Harness's process sandbox. Git worktrees collect changes rather than restrict OS access. Startup and retry require separate confirmation even when legacy direct-CLI confirmation is disabled; restricted runs never switch automatically. Native Kimi and ordinary npm MiniMax entries additionally require registry package/manifest/entry attestation.
|
|
14
|
+
- Preserve bounded, scrubbed failure diagnostics and exit codes, including Claude result errors. Keep verified usage and reported costs after failures, cancellation and timeouts, mark partial usage incomplete, and retain earlier team results if a later adapter throws. Reassignment applies the new route's saved CLI model mapping.
|
|
15
|
+
- Clear obsolete failure details after a successful rerun and display the stored execution mode accurately. Update the English/Chinese README and workbench guide, including the existing Windows ZCode bundled TUI terminal support.
|
|
16
|
+
|
|
17
|
+
Real Windows validation: authenticated workbench RPC executed one Claude package; the installed production runtime then executed Claude analysis → MiniMax delivery with dependency context and source patch integration. Independent JSON readback passed. Successful calls report $0.253848 known API cost in total; earlier unknown amounts and unpriced subscription reference usage remain separate. File/shell tools passed; every MCP service and vendor are not claimed tested. See [validation evidence and limits](docs/releases/0.20.0-validation.md). Automated checks and exact totals are included in the GitHub release notes.
|
|
18
|
+
|
|
19
|
+
Engineering checks: frozen-lockfile strict-peer install passed; 833 tests, 830 passed, 3 conditional skips, no failures; client build and generated-client consistency passed. Skipped process/PTY checks are not counted as real execution acceptance.
|
|
20
|
+
|
|
3
21
|
## 0.19.0 — 2026-10-12
|
|
4
22
|
|
|
5
23
|
Stable minor under the npm latest tag. Fully quit Harness (including its tray or WebUI Host process) and restart after upgrading.
|
package/README.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# Model Router · DeepSeek Harness Desktop
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Current release: **0.20.0**. See [routing research](docs/ROUTING_RESEARCH.zh.md), [mathematical derivation and experiments](docs/ROUTING_DERIVATION_EXPERIMENTS.zh.md), and the [LiveBench evidence audit and validation protocol](docs/ROUTING_LIVEBENCH_VALIDATION.zh.md). The latest public 2026-06-25 score/cost tables are aggregates; its paired question-quality/cost matrix has not been obtained, and no real routing gain is claimed.
|
|
4
4
|
|
|
5
5
|
A cost-aware model-routing workbench for **DeepSeek Harness Desktop**. Describe a task, compare your configured models, inspect the plan, and explicitly choose when to execute it.
|
|
6
6
|
|
|
7
|
+
**0.20.0:** **官方工具协作** runs dependent team packages with official CLIs and no automatic API fallback. For editable tasks, explicitly choose **Harness 受限环境** or **厂商 CLI 正常环境**. The latter retains normal vendor login, configuration, skills and MCP under current user permissions, requires a separate confirmation, and uses a Git worktree to collect changes rather than restrict OS access. CLIs keep their configured model unless explicitly mapped. Real Windows Claude execution and a Claude → MiniMax dependency handoff with file integration passed; see [validation and limits](docs/releases/0.20.0-validation.md).
|
|
8
|
+
|
|
7
9
|
**0.19.0:** interactive terminals follow the full registry and saved installation, including OpenCode, Step and the ZCode desktop-bundled TUI. ZCode has independent desktop-directory configuration and desktop update/reinstall controls; its bundled CLI follows desktop updates. Fresh terminal discovery discards stale responses.
|
|
8
10
|
|
|
9
11
|
**0.18.2:** isolates configurable vendor scripts beneath the shared install root, preventing Step from replacing other tools’ npm packages. Gemini/OpenCode installation success is separate from restricted execution support; Windows Gemini uses its verified package entry through Node. Upgrade and restart Harness before updating Step.
|
|
@@ -22,7 +24,7 @@ A cost-aware model-routing workbench for **DeepSeek Harness Desktop**. Describe
|
|
|
22
24
|
|
|
23
25
|
**0.16.0:** [dynamic data and adaptive feedback](docs/ROUTING_ADAPTIVE_LEARNING.zh.md) adds opt-in public benchmark/curated fixed-USD pricing refresh and task-specific, time-decayed, shrinkage-regularized subjective utility. Objective proxy quality floors remain separate. Public sources default off; preferences are shared within one local DSH_HOME and limited to the latest 200 runs. This is not a universal vendor-price scraper or evidence of real quality–cost gains.
|
|
24
26
|
|
|
25
|
-
Current version: **0.
|
|
27
|
+
Current version: **0.20.0** (`@ljwei-stak/dsh-model-router`). Its declared host compatibility is **DeepSeek Harness Desktop 0.2.0-rc.1 / 0.2.0-rc.2**. The router and [GAL](https://github.com/Alice-Marx/deepseek-harness-galgame) have been independent plugins since 0.12.0.
|
|
26
28
|
|
|
27
29
|
**0.15.0 changes:** a four-tab workbench with routing advice directly below task planning, protection against stale execution previews, preserved spending totals across history pruning and retries, and a standalone mock UI preview for source development.
|
|
28
30
|
|
|
@@ -38,7 +40,7 @@ For self-testing, follow the [desktop acceptance and quality–cost experiment s
|
|
|
38
40
|
| Model configuration | Reads exact `provider/model` routes from Harness; optional user-supplied quality, USD prices, specialties, CLI model names, execution and billing preferences. |
|
|
39
41
|
| Routing presets | Economy, balanced and quality-first presets; quality floors, estimated token costs and bounded assignment search. Missing prices stay unknown. |
|
|
40
42
|
| Read-only execution | Preview and confirm a routed run from the workbench, or invoke it in an official Harness session. Supported headless CLIs and model-directory API calls return recorded results. |
|
|
41
|
-
| Editable CLI execution |
|
|
43
|
+
| Editable CLI execution | A sequential CLI team from the workbench, or one supported CLI from a session, using approved isolated Git worktrees and conditional patch integration. |
|
|
42
44
|
| Official tools | Detect, install/update, check launch readiness and inspect login status for ten registry tools; installation jobs have logs and cancellation. |
|
|
43
45
|
| Interactive terminal | PowerShell/system shell and all ten registry CLI targets (ZCode TUI on verified Windows desktop installs), including fixed login commands, live output and multiple sessions. |
|
|
44
46
|
| Costs and subscriptions | Daily/monthly budget checks, economy downgrade or pause, subscription-first routing, quota cooldown and explicit handling of other subscription failures. |
|
|
@@ -54,16 +56,16 @@ Generating a route recommendation is local: it does not start a vendor CLI or ma
|
|
|
54
56
|
Open **Plugins → Add plugin** and enter:
|
|
55
57
|
|
|
56
58
|
```text
|
|
57
|
-
@ljwei-stak/dsh-model-router@0.
|
|
59
|
+
@ljwei-stak/dsh-model-router@0.20.0
|
|
58
60
|
```
|
|
59
61
|
|
|
60
|
-
Use the official HTTPS npm source `https://registry.npmjs.org/` when a mirror lacks that exact version. Enable the plugin, then fully quit Harness (including its tray process) and restart it. Plugin details should show **0.
|
|
62
|
+
Use the official HTTPS npm source `https://registry.npmjs.org/` when a mirror lacks that exact version. Enable the plugin, then fully quit Harness (including its tray process) and restart it. Plugin details should show **0.20.0** and the sidebar should contain **模型路由** (Model Router). A global `npm install -g` alone does not register a Desktop plugin.
|
|
61
63
|
|
|
62
64
|
You can instead install a local `.tgz` archive or its extracted inner `package` directory through the plugin manager. Both must contain `package.json` and `.dsh-plugin`. See [releases](https://github.com/Alice-Marx/dsh-model-router/releases) for archived builds, or build your own checkout below.
|
|
63
65
|
|
|
64
66
|
### Upgrade from the old package name
|
|
65
67
|
|
|
66
|
-
The old name `@ljwei-stak/model-router-galgame` ended at 0.13.0. Remove that plugin entry while retaining your profile/application data, then add `@ljwei-stak/dsh-model-router@0.
|
|
68
|
+
The old name `@ljwei-stak/model-router-galgame` ended at 0.13.0. Remove that plugin entry while retaining your profile/application data, then add `@ljwei-stak/dsh-model-router@0.20.0`. Do not keep both entries installed: they share the internal plugin id `model-router-galgame` and tool names. The retained id lets existing router settings and history carry over.
|
|
67
69
|
|
|
68
70
|
For the combined 0.11.x package, back up the profile and export the GAL saves you want to preserve before upgrading. Story playback, artwork, music and saves now belong to the independent `@ljwei-stak/dsh-galgame` package. The detailed save limitations are in [MIGRATION.md](docs/archive/MIGRATION.md).
|
|
69
71
|
|
|
@@ -73,7 +75,7 @@ The 0.15.0 workbench groups the workflow into four navigation tabs. Its header s
|
|
|
73
75
|
|
|
74
76
|
| Tab | What you do there |
|
|
75
77
|
| --- | --- |
|
|
76
|
-
| **任务与执行** — Task and execution | Describe the task,
|
|
78
|
+
| **任务与执行** — Task and execution | Describe the task, generate advice, choose routed read-only execution or official CLI teamwork, preview and confirm, then inspect history. |
|
|
77
79
|
| **模型配置** — Model configuration | Refresh/search the model directory and edit exact-route profiles. |
|
|
78
80
|
| **官方工具** — Official tools | Complete onboarding checks, inspect/install tools and use the interactive terminal. |
|
|
79
81
|
| **预算与安全** — Budget and security | Set routing/cost/review policies and inspect subscription status and execution boundaries. |
|
|
@@ -88,8 +90,8 @@ Switching tabs keeps the panels mounted, preserving in-progress installation and
|
|
|
88
90
|
2. Optionally enter quality and prices for the routes you want compared. Prices are **USD per million tokens**; quality is a user-supplied **0–100** estimate. Leave unknown prices blank rather than entering zero.
|
|
89
91
|
3. In **任务与执行**, describe concrete deliverables, constraints and acceptance criteria. Select **单任务** (single recommendation), **团队分工** (dependent packages for sufficiently complex requests), or **指定模型** (one exact route, without comparing others). Team mode alone does not start any team.
|
|
90
92
|
4. Set the per-run estimate target; `0` removes that planning constraint. Click **生成路由建议**. The result appears directly below planning. Check the route, complexity, cost, execution channel and each package's dependencies/checklist.
|
|
91
|
-
5.
|
|
92
|
-
6. Inspect **执行记录与子任务** for the actual channel, response, failures, cost and ratings.
|
|
93
|
+
5. In **在工作台执行**, choose **模型路由(只读)** for the existing routed executor, or first select **团队分工** and choose **官方工具协作**. For official teamwork, select **只读检查** or **编辑文件**. Enter an existing absolute workspace path, or reuse the last run's path. Click **预览执行计划**, review each package, its permissions and required confirmations, then explicitly confirm execution. The Host rechecks the plan before starting; changed conditions can require confirmation again.
|
|
94
|
+
6. Inspect **执行记录与子任务** for the actual channel, response, failures, cost and ratings. Official teamwork replans using only installed CLIs ready for the selected permission mode. Edits require approval and a clean Git repository; successful worktree changes are conditionally integrated. An unavailable or failed CLI never silently becomes an API call in this mode.
|
|
93
95
|
|
|
94
96
|
Changing task text, planning mode, profiles, directory or relevant settings invalidates the prior recommendation. Changing execution inputs also discards the pending or completed preview, so execution requires a new preview of the current request. Previewing execution uses current Host state and can differ from a previous local plan.
|
|
95
97
|
|
|
@@ -136,7 +138,7 @@ A read-only single-tool request:
|
|
|
136
138
|
{"tool":"codex","task":"Inspect the repository and list UI layout issues; do not edit files","mode":"read-only"}
|
|
137
139
|
```
|
|
138
140
|
|
|
139
|
-
Supply `provider` and `model` together when selecting an exact catalog route.
|
|
141
|
+
Supply `provider` and `model` together when selecting an exact catalog route. All CLIs retain their own configured model by default; a catalog recommendation is never automatically passed as a CLI model argument. To switch explicitly, save a verified **官方 CLI 模型名** in the route profile, pass `cliModel` for one tool call, or use team `cliModelsJson`. Team bindings prefer a package id, then a tool id, then the saved route mapping. ZCode uses its own default and cannot switch model per call. History records the recommendation, requested CLI name and reported actual model separately; a request is not proof of the model used. Check vendor records when no actual model is reported.
|
|
140
142
|
|
|
141
143
|
The CLI team runner is the plugin's own sequential dependency executor. It does not control the model selection or lifecycle of Harness's built-in Agent Teams.
|
|
142
144
|
|
|
@@ -146,7 +148,7 @@ The CLI team runner is the plugin's own sequential dependency executor. It does
|
|
|
146
148
|
| --- | --- | --- |
|
|
147
149
|
| `kimi-code` | Kimi Code | Approved `workspace-write` |
|
|
148
150
|
| `claude-code` | Claude Code | `read-only`, approved `workspace-write` |
|
|
149
|
-
| `codex` | Codex CLI | `read-only
|
|
151
|
+
| `codex` | Codex CLI | Windows managed `read-only`; editable execution is currently unsupported |
|
|
150
152
|
| `minimax-code` | MiniMax Code | Approved `workspace-write` |
|
|
151
153
|
| `mimo-code` | MiMo Code | `read-only`, approved `workspace-write` |
|
|
152
154
|
| `grok-build` | Grok Build | `read-only`, approved `workspace-write` |
|
|
@@ -172,15 +174,19 @@ With no shared directory, ordinary checks discover the selected manager's packag
|
|
|
172
174
|
|
|
173
175
|
Installation, trusted launch readiness, account login and model entitlement are separate checks. An unknown login status is not proof of logout. On Windows, supported managed launch entries are checked against publisher signatures or official npm-tarball attestations; ZCode also records trust for its bundled script per signed build. See [the registry](.dsh-plugin/shared/official-tool-registry.mjs) and [executor](.dsh-plugin/shared/official-tool-executor.mjs) for the exact platform rules.
|
|
174
176
|
|
|
177
|
+
Version and CLI login-status checks use the selected installation's concrete entry. A missing selected copy cannot be replaced by a different command on PATH; its login status remains unknown when that entry cannot be checked.
|
|
178
|
+
|
|
175
179
|
In a session, `/tools install|uninstall|repair|method <tool-id> [method]` does the same; agents can call `model_router_tool_install`, `model_router_tool_repair` and `model_router_tool_uninstall`. The install directory and download sources always come from the user's own plugin settings and never travel over the RPC, so no call can redirect an install at an arbitrary package or URL.
|
|
176
180
|
|
|
177
181
|
`model_router_execute` has fixed headless adapters for Claude, Codex and Gemini. Other suppliers use the model-directory API unless a supported subscription route applies. Missing/unusable CLIs can fall back to that API; subscription failures follow the policy below.
|
|
178
182
|
|
|
179
183
|
Editable execution needs a clean Git repository, Host approval and the Harness process sandbox. Changes are made in an isolated worktree and applied back only after successful execution and a matching clean source workspace. Failed or conflicting integration is reported. Git-ignored outputs need separate review. Read-only CLI flags do not necessarily limit file reads to the workspace; check **安全边界**, especially Codex's broader readable scope.
|
|
180
184
|
|
|
185
|
+
For Windows Codex tasks in the Harness environment, only read-only is available: the outer ACL sandbox grants the original `CODEX_HOME` state and a private temporary directory, while Codex retains `--sandbox read-only`. Explicitly confirmed native editable teams can use Codex with its own workspace-write policy. Codex network readiness remains account/environment dependent and did not pass the real connection checks in this batch.
|
|
186
|
+
|
|
181
187
|
### Interactive terminal
|
|
182
188
|
|
|
183
|
-
In **官方工具 → 官方工具终端**, select the shell or an installed CLI, choose interactive/login mode where available, enter an existing absolute directory, and review the startup confirmation.
|
|
189
|
+
In **官方工具 → 官方工具终端**, select the shell or an installed CLI, choose interactive/login mode where available, enter an existing absolute directory, and review the startup confirmation. Verified Windows ZCode desktop installations provide their bundled TUI as a terminal target.
|
|
184
190
|
|
|
185
191
|
These sessions run **outside the Harness sandbox**, with the Host user's login environment, proxies and API-key variables. Commands typed there can read/write files with that user's permissions. Terminal launches are separate from routed execution and its approval/billing policy.
|
|
186
192
|
|
|
@@ -193,7 +199,7 @@ The plugin does not persist terminal input/output. It stores session metadata on
|
|
|
193
199
|
- **Budgets:** per-run planning target plus daily/monthly checks. Over-budget routing can try the economy preset or pause for confirmation. Unknown prices remain unknown. These are local accounting/estimation controls, not a hard provider billing cap.
|
|
194
200
|
- **Subscription-first:** the default is to prefer a detected CLI account subscription or a configured coding-plan route. API-key environment variables are removed for CLI subscription attempts. `billing` can be `subscription-first`, `api-only` or `subscription-only`; `subscription` can be `cli-login`, `plan-key` or `none`.
|
|
195
201
|
- **Fallback:** detected quota exhaustion/rate limits mark a subscription in cooldown and can retry the routed step on its configured API route, subject to policy and availability. Other attempted subscription failures pause by default (`onSubscriptionFailure: "ask"`): choose API retry, subscription retry or cancel in history. Managed editable/single-tool/team runs do not automatically retry on the API.
|
|
196
|
-
- **Accounting:** API costs count toward budgets. Subscription runs display
|
|
202
|
+
- **Accounting:** API costs count toward budgets. Subscription runs display a reference cost separately. A valid CLI-reported amount, including zero, takes precedence. Token-based CLI cost requires complete usage and an actual reported model matching a configured model or its explicit CLI mapping; unknown or mismatched identity keeps cost unknown. The recommendation's price alone cannot establish actual CLI cost. Provider bills remain authoritative. Version 0.15.0 retains spending from evicted detailed runs and replaced retry results, so trimming history or retrying a step does not erase earlier charges. Costs already discarded by older versions cannot be reconstructed.
|
|
197
203
|
- **Quality and preference:** optional `off`/`sample`/`always` stronger-model review can add paid calls; it is not treated as human feedback. Explicit ratings adjust separate subjective utility for the exact route and task type, using time decay and low-sample shrinkage (default limit ±0.04, configurable up to ±0.1). They do not change objective quality scores or their hard floors; learning can be disabled, ratings withdrawn, or its starting point reset.
|
|
198
204
|
|
|
199
205
|
Coding-plan keys and endpoints stay in Harness provider settings. A `plan-key` profile may name an exact configured `apiRoute` as its fallback; the profile editor does not accept credentials or arbitrary commands. Vendor plan eligibility and terms must be checked with the vendor.
|