@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.
@@ -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 { /* a blocked saved method still allows normal detection */ }
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?.actualModel ? { actualModel: String(result.actualModel) } : {}),
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
- actualModel: result?.reportedModel && typeof result.reportedModel === 'string' ? result.reportedModel : null,
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
- const explicit = checkedModel(route?.cliModel)
197
- if (explicit) return explicit
198
- if (adapter?.id === 'claude-code' || adapter?.id === 'codex' || adapter?.id === 'gemini') {
199
- return checkedModel(route?.model)
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
- return null
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
- return scrubbed.length > DIAGNOSTIC_CHARS ? `${scrubbed.slice(0, DIAGNOSTIC_CHARS)}…` : scrubbed
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 = Number(result.total_cost_usd)
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 > 0 ? { reportedCostUsd: 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 = Number(value.total_cost_usd)
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 > 0 ? { reportedCostUsd: 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 cost = parts.reduce((total, part) => total + (Number.isFinite(part.cost) && part.cost > 0 ? part.cost : 0), 0)
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
- ...(cost > 0 ? { reportedCostUsd: Number(cost.toFixed(8)) } : {}),
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 officialSuccess({ route, adapter, preference, answer, exitCode, version, credentialSource, usage = null }) {
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, { runner, env = process.env, exists = async () => false, home = '', readAuthState = async () => null } = {}) {
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 runner('claude', ['auth', 'status', '--json'], { timeoutMs: LOGIN_CHECK_TIMEOUT_MS, env: sessionEnvironment(toolId, env) })
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 runner('codex', ['login', 'status'], { timeoutMs: LOGIN_CHECK_TIMEOUT_MS, env: sessionEnvironment(toolId, env) })
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
- README.md: 4d820ba4402b344268124315ba74228dc6c2e79f
3
- README.zh.md: f00ed637e2f3a22b28cf7f5e4a4015f9c09eba0c
2
+ # Reviewed together; Git blob hashes identify the exact synchronized texts.
3
+ README.md: 4f6b76b9602764b9d3c8e360f17c873a5180e16b
4
+ README.zh.md: 4e281f5632268114a86549380ba730df9ab69d07