@ljwei-stak/dsh-model-router 0.19.0 → 0.20.1
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 +220 -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 +26 -0
- package/README.i18n.yaml +3 -2
- package/README.md +247 -50
- package/README.zh.md +357 -160
- package/docs/WORKBENCH_USER_GUIDE.zh.md +32 -8
- package/docs/releases/0.20.0-validation.md +26 -0
- package/package.json +5 -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,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.20.1 — 2026-10-16
|
|
4
|
+
|
|
5
|
+
- Expand the English and Simplified Chinese READMEs together: architecture and execution contracts, workbench/session request examples, platform capabilities and executable trust, sequential team lifecycle, model identity, worktree integration/retry, accounting formulas, routing weights/floors/search limits, persistence and verification layers. Add matching navigation anchors and record the reviewed translation pair in `README.i18n.yaml`.
|
|
6
|
+
- Correct stale claims about Windows Codex editable support and the Harness/native permission boundary. Distinguish registry-file consistency from npm provenance and clarify installer-marker and ZCode first-seen script trust.
|
|
7
|
+
- Forward explicit unsandboxed confirmation through the workbench retry dispatcher, allowing an approved native CLI team retry to reach the existing execution gate. Missing confirmation remains blocked. Add focused regression coverage; no new model calls or billing claims. The dated 0.20.0 real-execution report remains unchanged.
|
|
8
|
+
|
|
9
|
+
Validation: strict frozen pnpm 10.34.6 install, real client build and generated-client consistency passed; 834 tests, 831 passed, 3 conditional skips, no failures. The new gateway/Host retry regression fails before the forwarding fix and passes afterward. Bilingual checks cover section anchors, table shape, local links and protocol/formula/command examples.
|
|
10
|
+
|
|
11
|
+
## 0.20.0 — 2026-10-15
|
|
12
|
+
|
|
13
|
+
Fully quit Harness, including its tray or WebUI Host process, and restart after upgrading.
|
|
14
|
+
|
|
15
|
+
- 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.
|
|
16
|
+
- 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.
|
|
17
|
+
- 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.
|
|
18
|
+
- 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.
|
|
19
|
+
- 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.
|
|
20
|
+
- 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.
|
|
21
|
+
- 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.
|
|
22
|
+
- 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.
|
|
23
|
+
- 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.
|
|
24
|
+
|
|
25
|
+
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.
|
|
26
|
+
|
|
27
|
+
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.
|
|
28
|
+
|
|
3
29
|
## 0.19.0 — 2026-10-12
|
|
4
30
|
|
|
5
31
|
Stable minor under the npm latest tag. Fully quit Harness (including its tray or WebUI Host process) and restart after upgrading.
|
package/README.i18n.yaml
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
# README translation pairing for this repository.
|
|
2
|
-
|
|
3
|
-
README.
|
|
2
|
+
# Reviewed together; Git blob hashes identify the exact synchronized texts.
|
|
3
|
+
README.md: 4f6b76b9602764b9d3c8e360f17c873a5180e16b
|
|
4
|
+
README.zh.md: 4e281f5632268114a86549380ba730df9ab69d07
|