@ljwei-stak/dsh-model-router 0.24.0 → 0.25.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 +2489 -4459
- package/.dsh-plugin/index.mjs +128 -50
- package/.dsh-plugin/shared/budget-reservations.mjs +26 -7
- package/.dsh-plugin/shared/execution-accounting.mjs +61 -0
- package/.dsh-plugin/shared/execution-lifecycle.mjs +5 -3
- package/.dsh-plugin/shared/official-team-runtime.mjs +5 -0
- package/.dsh-plugin/shared/router-state.mjs +17 -4
- package/.dsh-plugin/shared/run-ledger.mjs +15 -1
- package/.dsh-plugin/shared/subscription-usage.mjs +210 -0
- package/.dsh-plugin/shared/task-executors.mjs +94 -21
- package/CHANGELOG.md +10 -0
- package/README.i18n.yaml +2 -2
- package/README.md +25 -20
- package/README.zh.md +22 -18
- package/docs/WORKBENCH_USER_GUIDE.zh.md +8 -6
- package/docs/assets/workbench-subscription-usage.png +0 -0
- package/docs/releases/0.25.0-validation.md +41 -0
- package/package.json +7 -3
|
@@ -43,6 +43,24 @@ const BASE_ENV_KEYS = Object.freeze([
|
|
|
43
43
|
])
|
|
44
44
|
|
|
45
45
|
const READ_ONLY_INSTRUCTION = 'Read the task supplied on standard input. Answer in the task language. Do not modify files.'
|
|
46
|
+
const executionBoundaryFailure = error => ['ROUTER_CHECKPOINT_FAILED', 'ROUTER_BUDGET_CHANGED'].includes(error?.code)
|
|
47
|
+
const usageFields = ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens']
|
|
48
|
+
|
|
49
|
+
/** Subscription evidence contains reported usage only, never prices or secrets. */
|
|
50
|
+
function subscriptionReceipt(result, { provider, model, toolId = null, channel = 'harness-llm', now = Date.now } = {}) {
|
|
51
|
+
const usage = result?.usage && typeof result.usage === 'object' && !Array.isArray(result.usage)
|
|
52
|
+
? Object.fromEntries(usageFields.filter(key => typeof result.usage[key] === 'number'
|
|
53
|
+
&& Number.isFinite(result.usage[key]) && result.usage[key] >= 0).map(key => [key, result.usage[key]])) : null
|
|
54
|
+
const notStarted = result?.notStarted === true || result?.blocked === true
|
|
55
|
+
const incomplete = result?.usageIncomplete === true || !usage
|
|
56
|
+
|| !['inputTokens', 'outputTokens'].every(key => Object.hasOwn(usage, key))
|
|
57
|
+
|| usageFields.some(key => result?.usage?.[key] !== undefined && !Object.hasOwn(usage ?? {}, key))
|
|
58
|
+
return { provider: String(provider ?? ''), model: String(reportedCliModel(result?.actualModel ?? result?.reportedModel) ?? model ?? ''),
|
|
59
|
+
toolId, channel, usage: usage && Object.keys(usage).length ? usage : null,
|
|
60
|
+
...(incomplete ? { usageIncomplete: true } : {}), ...(notStarted ? { notStarted: true } : {}),
|
|
61
|
+
finishedAt: typeof result?.finishedAt === 'number' && Number.isFinite(result.finishedAt)
|
|
62
|
+
&& result.finishedAt >= 0 ? result.finishedAt : now() }
|
|
63
|
+
}
|
|
46
64
|
|
|
47
65
|
function additionalApiFields(value) {
|
|
48
66
|
if (!value || typeof value !== 'object') return {}
|
|
@@ -409,6 +427,7 @@ export function looksLikeLoginFailure(detail) {
|
|
|
409
427
|
}
|
|
410
428
|
|
|
411
429
|
const count = value => Number.isFinite(value) && value >= 0 ? Math.round(value) : 0
|
|
430
|
+
const knownCount = value => typeof value === 'number' && Number.isFinite(value) && value >= 0
|
|
412
431
|
|
|
413
432
|
/** Token usage reported by the CLI, in the Harness TokenUsage shape (disjoint cache counts). */
|
|
414
433
|
export function usageFromOutput(format, stdout = '') {
|
|
@@ -422,11 +441,14 @@ export function usageFromOutput(format, stdout = '') {
|
|
|
422
441
|
? Object.keys(result.modelUsage) : []
|
|
423
442
|
return {
|
|
424
443
|
usage: {
|
|
425
|
-
inputTokens: count(usage.input_tokens),
|
|
426
|
-
outputTokens: count(usage.output_tokens),
|
|
444
|
+
...(knownCount(usage.input_tokens) ? { inputTokens: count(usage.input_tokens) } : {}),
|
|
445
|
+
...(knownCount(usage.output_tokens) ? { outputTokens: count(usage.output_tokens) } : {}),
|
|
427
446
|
cacheReadTokens: count(usage.cache_read_input_tokens),
|
|
428
447
|
cacheWriteTokens: count(usage.cache_creation_input_tokens),
|
|
429
448
|
},
|
|
449
|
+
...(!knownCount(usage.input_tokens) || !knownCount(usage.output_tokens)
|
|
450
|
+
|| ['cache_read_input_tokens', 'cache_creation_input_tokens'].some(key => usage[key] !== undefined && !knownCount(usage[key]))
|
|
451
|
+
? { usageIncomplete: true } : {}),
|
|
430
452
|
...(Number.isFinite(reported) && reported >= 0 ? { reportedCostUsd: reported } : {}),
|
|
431
453
|
...(models.length === 1 && /^[A-Za-z0-9][A-Za-z0-9._:/\[\]-]{0,199}$/.test(models[0]) ? { reportedModel: models[0] } : {}),
|
|
432
454
|
}
|
|
@@ -436,24 +458,33 @@ export function usageFromOutput(format, stdout = '') {
|
|
|
436
458
|
if (!completed) return null
|
|
437
459
|
const cached = count(completed.usage.cached_input_tokens)
|
|
438
460
|
return { usage: {
|
|
439
|
-
inputTokens: Math.max(0, count(completed.usage.input_tokens) - cached),
|
|
440
|
-
outputTokens: count(completed.usage.output_tokens),
|
|
461
|
+
...(knownCount(completed.usage.input_tokens) ? { inputTokens: Math.max(0, count(completed.usage.input_tokens) - cached) } : {}),
|
|
462
|
+
...(knownCount(completed.usage.output_tokens) ? { outputTokens: count(completed.usage.output_tokens) } : {}),
|
|
441
463
|
cacheReadTokens: cached,
|
|
442
464
|
cacheWriteTokens: 0,
|
|
443
|
-
}
|
|
465
|
+
}, ...(!knownCount(completed.usage.input_tokens) || !knownCount(completed.usage.output_tokens)
|
|
466
|
+
|| completed.usage.cached_input_tokens !== undefined && (!knownCount(completed.usage.cached_input_tokens)
|
|
467
|
+
|| completed.usage.cached_input_tokens > completed.usage.input_tokens) ? { usageIncomplete: true } : {}) }
|
|
444
468
|
}
|
|
445
469
|
if (format === 'gemini-json') {
|
|
446
470
|
const models = JSON.parse(stdout)?.stats?.models
|
|
447
|
-
if (!models || typeof models !== 'object') return null
|
|
471
|
+
if (!models || typeof models !== 'object' || Array.isArray(models) || !Object.keys(models).length) return null
|
|
448
472
|
let input = 0
|
|
449
473
|
let output = 0
|
|
450
474
|
let cached = 0
|
|
475
|
+
let inputKnown = true, outputKnown = true, incomplete = false
|
|
451
476
|
for (const entry of Object.values(models)) {
|
|
477
|
+
inputKnown &&= knownCount(entry?.tokens?.prompt)
|
|
478
|
+
outputKnown &&= knownCount(entry?.tokens?.candidates)
|
|
479
|
+
incomplete ||= entry?.tokens?.cached !== undefined && (!knownCount(entry.tokens.cached)
|
|
480
|
+
|| entry.tokens.cached > entry.tokens.prompt)
|
|
452
481
|
input += count(entry?.tokens?.prompt)
|
|
453
482
|
output += count(entry?.tokens?.candidates)
|
|
454
483
|
cached += count(entry?.tokens?.cached)
|
|
455
484
|
}
|
|
456
|
-
return { usage: { inputTokens: Math.max(0, input - cached)
|
|
485
|
+
return { usage: { ...(inputKnown ? { inputTokens: Math.max(0, input - cached) } : {}),
|
|
486
|
+
...(outputKnown ? { outputTokens: output } : {}), cacheReadTokens: cached, cacheWriteTokens: 0 },
|
|
487
|
+
...(!inputKnown || !outputKnown || incomplete ? { usageIncomplete: true } : {}) }
|
|
457
488
|
}
|
|
458
489
|
} catch { /* usage is optional */ }
|
|
459
490
|
return null
|
|
@@ -621,7 +652,7 @@ function captureProcess(spawnImpl, file, args, { cwd, env, stdin, timeoutMs, sig
|
|
|
621
652
|
})
|
|
622
653
|
}
|
|
623
654
|
|
|
624
|
-
async function useApi(apiFallback, { route, task, signal, reason, adapter, preference, exitCode = null, timedOut = false, detail = '', raw = '', skipped = false, attempted = false }) {
|
|
655
|
+
async function useApi(apiFallback, { route, task, signal, reason, adapter, preference, exitCode = null, timedOut = false, detail = '', raw = '', skipped = false, attempted = false, subscriptionResult = null }) {
|
|
625
656
|
const provider = String(route?.provider ?? '')
|
|
626
657
|
const model = String(route?.model ?? '')
|
|
627
658
|
const fallback = {
|
|
@@ -637,7 +668,7 @@ async function useApi(apiFallback, { route, task, signal, reason, adapter, prefe
|
|
|
637
668
|
}
|
|
638
669
|
}
|
|
639
670
|
try {
|
|
640
|
-
const api = await apiFallback({ route, task, signal, reason, detail, raw: String(raw ?? '').slice(-40_000), toolId: adapter?.id ?? null, attempted, timedOut, exitCode })
|
|
671
|
+
const api = await apiFallback({ route, task, signal, reason, detail, raw: String(raw ?? '').slice(-40_000), toolId: adapter?.id ?? null, attempted, timedOut, exitCode, subscriptionResult })
|
|
641
672
|
// A billing-aware fallback can pause the step for the user's decision.
|
|
642
673
|
if (api?.paused) {
|
|
643
674
|
return {
|
|
@@ -646,6 +677,7 @@ async function useApi(apiFallback, { route, task, signal, reason, adapter, prefe
|
|
|
646
677
|
pause: { ...api.pause, ...(detail ? { detail } : {}), ...(fallback.loginRequired ? { loginRequired: true } : {}) },
|
|
647
678
|
...reasoningExecutionFields(api),
|
|
648
679
|
...additionalApiFields(api),
|
|
680
|
+
...(api?.billing ? { billing: api.billing } : {}),
|
|
649
681
|
error: api.error,
|
|
650
682
|
}
|
|
651
683
|
}
|
|
@@ -674,6 +706,7 @@ async function useApi(apiFallback, { route, task, signal, reason, adapter, prefe
|
|
|
674
706
|
error: api?.ok === true && answer.trim() ? undefined : (api?.error || reason),
|
|
675
707
|
}
|
|
676
708
|
} catch (error) {
|
|
709
|
+
if (executionBoundaryFailure(error)) throw error
|
|
677
710
|
if (signal?.aborted) {
|
|
678
711
|
return {
|
|
679
712
|
ok: false, provider, model, preference, channel: 'harness-llm', toolId: adapter?.id ?? null,
|
|
@@ -833,6 +866,8 @@ export async function executeAssignedTask({
|
|
|
833
866
|
return useApi(apiFallback, {
|
|
834
867
|
route, task: prompt, signal, adapter, preference, exitCode: verified.exitCode ?? null,
|
|
835
868
|
timedOut: verified.status === 'timed-out', attempted: true,
|
|
869
|
+
subscriptionResult: { usage: verified.usage, usageIncomplete: verified.usageIncomplete,
|
|
870
|
+
notStarted: verified.notStarted === true, actualModel: verified.actualModel, reportedModel: verified.reportedModel },
|
|
836
871
|
reason: verified.error || verified.reason || '官方 CLI 执行失败,已回退模型目录 API。',
|
|
837
872
|
detail: redactDiagnostic(verifiedDiagnostic(verified), childEnvironment(adapter, credentials, credentialMode).secret),
|
|
838
873
|
raw: [verified.error, verified.reason, verified.detail, verified.stderrTail, verified.stdoutTail].filter(item => typeof item === 'string').join('\n'),
|
|
@@ -861,7 +896,7 @@ export async function executeAssignedTask({
|
|
|
861
896
|
: probe.timedOut ? `${adapter.label} 检测超时,已回退模型目录 API。`
|
|
862
897
|
: `${adapter.label} 无法运行,已回退模型目录 API。`
|
|
863
898
|
if (probe.cancelled) {
|
|
864
|
-
return { ok: false, provider, model, preference, channel: 'official-cli', toolId: adapter.id, answer: '', fallback: null, timedOut: false, exitCode: null, error: reason, cancelled: true }
|
|
899
|
+
return { ok: false, provider, model, preference, channel: 'official-cli', toolId: adapter.id, answer: '', fallback: null, timedOut: false, exitCode: null, error: reason, cancelled: true, notStarted: true }
|
|
865
900
|
}
|
|
866
901
|
return useApi(apiFallback, { route, task: prompt, signal, adapter, preference, reason })
|
|
867
902
|
}
|
|
@@ -871,11 +906,14 @@ export async function executeAssignedTask({
|
|
|
871
906
|
...(Number.isSafeInteger(stopGraceMs) && stopGraceMs >= 10 && stopGraceMs <= STOP_GRACE_MS ? { stopGraceMs } : {}),
|
|
872
907
|
})
|
|
873
908
|
if (run.cancelled) {
|
|
874
|
-
|
|
909
|
+
const usage = usageFromOutput(adapter.format, run.stdout)
|
|
910
|
+
return { ok: false, provider, model, preference, channel: 'official-cli', toolId: adapter.id, answer: '', fallback: null, timedOut: false, exitCode: run.exitCode, error: '执行已取消。', cancelled: true,
|
|
911
|
+
credentialSource, ...(usage?.usage ? { usage: usage.usage } : {}), ...(usage?.usageIncomplete ? { usageIncomplete: true } : {}) }
|
|
875
912
|
}
|
|
876
913
|
if (!run.ok) {
|
|
877
914
|
return useApi(apiFallback, {
|
|
878
915
|
route, task: prompt, signal, adapter, preference, exitCode: run.exitCode, timedOut: run.timedOut, attempted: true,
|
|
916
|
+
subscriptionResult: usageFromOutput(adapter.format, run.stdout),
|
|
879
917
|
reason: run.timedOut ? `${adapter.label} 执行超时,已回退模型目录 API。`
|
|
880
918
|
: run.outputLimit ? `${adapter.label} 输出超过上限,已回退模型目录 API。`
|
|
881
919
|
: `${adapter.label} 执行失败,已回退模型目录 API。`,
|
|
@@ -889,6 +927,7 @@ export async function executeAssignedTask({
|
|
|
889
927
|
if (!parsed.ok) {
|
|
890
928
|
return useApi(apiFallback, {
|
|
891
929
|
route, task: prompt, signal, adapter, preference, exitCode: run.exitCode, attempted: true,
|
|
930
|
+
subscriptionResult: usageFromOutput(adapter.format, run.stdout),
|
|
892
931
|
reason: `${parsed.error} 已回退模型目录 API。`,
|
|
893
932
|
detail: failureDetail(adapter.format, run.stdout, run.stderr, secret),
|
|
894
933
|
raw: `${run.stdout.slice(-20_000)}\n${run.stderr.slice(-20_000)}`,
|
|
@@ -896,7 +935,8 @@ export async function executeAssignedTask({
|
|
|
896
935
|
}
|
|
897
936
|
const answer = redact(parsed.answer, secret)
|
|
898
937
|
if (!answer.trim()) {
|
|
899
|
-
return useApi(apiFallback, { route, task: prompt, signal, adapter, preference, exitCode: run.exitCode, attempted: true,
|
|
938
|
+
return useApi(apiFallback, { route, task: prompt, signal, adapter, preference, exitCode: run.exitCode, attempted: true,
|
|
939
|
+
subscriptionResult: usageFromOutput(adapter.format, run.stdout), reason: '官方 CLI 没有返回文本,已回退模型目录 API。' })
|
|
900
940
|
}
|
|
901
941
|
return officialSuccess({
|
|
902
942
|
route, adapter, preference, answer, exitCode: run.exitCode,
|
|
@@ -954,14 +994,26 @@ const failureSummary = text => `${String(text ?? '').replace(/,?已(?:回退
|
|
|
954
994
|
* - subscription-only: never fall back to an API key.
|
|
955
995
|
* - api-only: never use the subscription.
|
|
956
996
|
*/
|
|
957
|
-
export async function executeRouteWithBilling({ route, billing = null, apiFallback, ...options } = {}) {
|
|
997
|
+
export async function executeRouteWithBilling({ route, billing = null, apiFallback, onSubscriptionResult, ...options } = {}) {
|
|
958
998
|
const plan = billingPlan(route, billing?.routes ?? [])
|
|
999
|
+
const subscriptionAttempts = []
|
|
1000
|
+
const recordSubscription = async receipt => {
|
|
1001
|
+
// This executor attempts the subscription once, then may use its API
|
|
1002
|
+
// fallback. Keep a bounded vector so unrelated result fields cannot grow it.
|
|
1003
|
+
if (subscriptionAttempts.length >= 2) throw new Error('订阅尝试证据超过本步上限。')
|
|
1004
|
+
subscriptionAttempts.push(receipt)
|
|
1005
|
+
const event = { ...structuredClone(receipt), billing: 'subscription', subscriptionAttempts: structuredClone(subscriptionAttempts) }
|
|
1006
|
+
await awaitExecutionCheckpoint(onSubscriptionResult, event,
|
|
1007
|
+
() => ({ status: 'failed', packages: [{ ...event, ok: false, ran: !receipt.notStarted }] }), 'subscription-result')
|
|
1008
|
+
}
|
|
959
1009
|
// Every result names the billing mode it ran under, whichever branch produced it.
|
|
960
|
-
const result = await routeWithBilling(plan, { route, billing, apiFallback, ...options })
|
|
961
|
-
return { ...result, billingMode: result?.billingMode ?? plan.mode
|
|
1010
|
+
const result = await routeWithBilling(plan, { route, billing, apiFallback, ...options, onSubscriptionResult: recordSubscription })
|
|
1011
|
+
return { ...result, billingMode: result?.billingMode ?? plan.mode,
|
|
1012
|
+
...(subscriptionAttempts.length ? { subscriptionAttempts,
|
|
1013
|
+
...(result?.billing ? {} : { billing: 'subscription' }) } : {}) }
|
|
962
1014
|
}
|
|
963
1015
|
|
|
964
|
-
async function routeWithBilling(plan, { route, billing = null, apiFallback, ...options } = {}) {
|
|
1016
|
+
async function routeWithBilling(plan, { route, billing = null, apiFallback, onSubscriptionResult, ...options } = {}) {
|
|
965
1017
|
if (!billing) return executeAssignedTask({ ...options, route, apiFallback })
|
|
966
1018
|
const now = typeof billing.now === 'function' ? billing.now : Date.now
|
|
967
1019
|
const sub = plan.subscription
|
|
@@ -972,7 +1024,7 @@ async function routeWithBilling(plan, { route, billing = null, apiFallback, ...o
|
|
|
972
1024
|
const base = { provider, model, billingMode: plan.mode }
|
|
973
1025
|
const refuse = (error, extra = {}) => ({
|
|
974
1026
|
ok: false, ...base, preference: executionPreference(route), channel: sub?.kind === 'plan-key' ? 'harness-llm' : 'official-cli',
|
|
975
|
-
toolId: sub?.toolId ?? null, answer: '', fallback: null, error, ...extra,
|
|
1027
|
+
toolId: sub?.toolId ?? null, answer: '', fallback: null, notStarted: true, error, ...extra,
|
|
976
1028
|
})
|
|
977
1029
|
const viaApi = async (reason, note, skipped = false) => {
|
|
978
1030
|
if (!apiRoute || (sub?.kind === 'plan-key' && apiRoute.provider === sub.route.provider && apiRoute.model === sub.route.model)) {
|
|
@@ -1016,11 +1068,16 @@ async function routeWithBilling(plan, { route, billing = null, apiFallback, ...o
|
|
|
1016
1068
|
}
|
|
1017
1069
|
|
|
1018
1070
|
if (sub.kind === 'plan-key') {
|
|
1071
|
+
if (typeof apiFallback !== 'function') return refuse('编程套餐执行器不可用,此步骤未启动。', { notStarted: true, billing: 'subscription' })
|
|
1019
1072
|
const reasoningRoute = routeForReasoningSwitch(route, sub.route)
|
|
1020
1073
|
const originalEffort = typeof route?.reasoningEffort === 'string' && route.reasoningEffort ? route.reasoningEffort : null
|
|
1021
1074
|
let attempt
|
|
1022
1075
|
try { attempt = await apiFallback({ route: reasoningRoute, task: options.task, signal: options.signal, reason: '编程套餐(订阅)', subscription: true }) }
|
|
1023
|
-
catch (error) {
|
|
1076
|
+
catch (error) {
|
|
1077
|
+
if (executionBoundaryFailure(error)) throw error
|
|
1078
|
+
attempt = { ok: false, error: String(error?.message ?? error) }
|
|
1079
|
+
}
|
|
1080
|
+
await onSubscriptionResult(subscriptionReceipt(attempt, { ...sub.route, now }))
|
|
1024
1081
|
const droppedEffort = originalEffort && reasoningRoute.reasoningEffort !== originalEffort ? originalEffort : null
|
|
1025
1082
|
if (attempt?.ok === true && String(attempt.answer ?? '').trim()) {
|
|
1026
1083
|
return {
|
|
@@ -1083,8 +1140,15 @@ async function routeWithBilling(plan, { route, billing = null, apiFallback, ...o
|
|
|
1083
1140
|
// No subscription login: the existing CLI-with-key / catalog API behaviour.
|
|
1084
1141
|
return executeAssignedTask({ ...options, route, apiFallback })
|
|
1085
1142
|
}
|
|
1143
|
+
const confirmedSubscription = retrySubscription || login === 'subscription' || plan.mode === 'subscription-only'
|
|
1086
1144
|
if (retrySubscription) options = { ...options, skipOfficial: null }
|
|
1087
1145
|
const guarded = async args => {
|
|
1146
|
+
if (args?.attempted === true && !confirmedSubscription) {
|
|
1147
|
+
const pause = pauseFor('CLI 计费方式尚未核对,已暂停,未启动额外 API 调用。', '请先体检确认账号订阅或 API Key,再选择重试方式。')
|
|
1148
|
+
return { ok: false, paused: true, billing: 'unknown', pause, error: pause.reason }
|
|
1149
|
+
}
|
|
1150
|
+
if (args?.attempted === true && confirmedSubscription) await onSubscriptionResult(subscriptionReceipt(args.subscriptionResult,
|
|
1151
|
+
{ provider, model, toolId: args.toolId ?? sub.toolId, channel: 'official-cli', now }))
|
|
1088
1152
|
const vendor = vendorKey({ toolId: args?.toolId ?? sub.toolId })
|
|
1089
1153
|
const info = classify(`${args?.detail ?? ''}
|
|
1090
1154
|
${args?.raw ?? ''}`, vendor)
|
|
@@ -1112,13 +1176,16 @@ ${args?.raw ?? ''}`, vendor)
|
|
|
1112
1176
|
const pause = pauseFor(summary, args?.detail ?? '', { timedOut: args?.timedOut === true, exitCode: args?.exitCode ?? null })
|
|
1113
1177
|
return { ok: false, paused: true, pause, error: pause.reason }
|
|
1114
1178
|
}
|
|
1115
|
-
return apiFallback(args)
|
|
1179
|
+
return { ...await apiFallback(args), billing: 'api' }
|
|
1116
1180
|
}
|
|
1117
1181
|
const result = await executeAssignedTask({ ...options, route, apiFallback: guarded, credentialMode: 'session-only' })
|
|
1182
|
+
if (confirmedSubscription && result.channel === 'official-cli' && (result.ok === true || result.cancelled && !result.notStarted)) {
|
|
1183
|
+
await onSubscriptionResult(subscriptionReceipt(result, { provider, model, toolId: sub.toolId, channel: 'official-cli', now }))
|
|
1184
|
+
}
|
|
1118
1185
|
return {
|
|
1119
1186
|
...result,
|
|
1120
1187
|
billingMode: plan.mode,
|
|
1121
|
-
...(result.
|
|
1188
|
+
...(result.channel === 'official-cli' && !result.billing ? { billing: confirmedSubscription ? 'subscription' : 'unknown' } : {}),
|
|
1122
1189
|
}
|
|
1123
1190
|
}
|
|
1124
1191
|
|
|
@@ -1233,6 +1300,7 @@ export async function observeExecutionProgress(hook, value) {
|
|
|
1233
1300
|
export async function awaitExecutionCheckpoint(hook, value, partial, stage) {
|
|
1234
1301
|
if (typeof hook !== 'function') return
|
|
1235
1302
|
try { await hook(value) } catch (cause) {
|
|
1303
|
+
if (executionBoundaryFailure(cause) && cause.execution) throw cause
|
|
1236
1304
|
const budgetChanged = cause?.code === 'ROUTER_BUDGET_CHANGED'
|
|
1237
1305
|
const error = new Error(budgetChanged ? String(cause.message) : `执行检查点保存失败:${String(cause?.message ?? cause)}`, { cause })
|
|
1238
1306
|
error.code = budgetChanged ? 'ROUTER_BUDGET_CHANGED' : 'ROUTER_CHECKPOINT_FAILED'
|
|
@@ -1324,7 +1392,7 @@ async function runManualTool(target, prompt, options) {
|
|
|
1324
1392
|
*/
|
|
1325
1393
|
async function runPackages({ packages, task, routingBypassed, routes, previous = [], targets = null, overrides = {}, choices = {}, options }) {
|
|
1326
1394
|
const routeList = Array.isArray(routes) ? routes : []
|
|
1327
|
-
const { credentialsFor, onPackageStart, onPackage, onStage, onCheckpointStart, onCheckpointResult, billing, ...runOptions } = options
|
|
1395
|
+
const { credentialsFor, onPackageStart, onPackage, onStage, onCheckpointStart, onCheckpointResult, onSubscriptionResult, billing, ...runOptions } = options
|
|
1328
1396
|
await observeExecutionProgress(onStage, 'preparing')
|
|
1329
1397
|
const effectivePackages = checkedPackageSelections(packages, routeList, overrides, runOptions.runVerified, targets)
|
|
1330
1398
|
const results = []
|
|
@@ -1406,6 +1474,11 @@ async function runPackages({ packages, task, routingBypassed, routes, previous =
|
|
|
1406
1474
|
else {
|
|
1407
1475
|
result = await executeRouteWithBilling({
|
|
1408
1476
|
...runOptions,
|
|
1477
|
+
onSubscriptionResult: async event => {
|
|
1478
|
+
event.id = item.id
|
|
1479
|
+
await awaitExecutionCheckpoint(onSubscriptionResult, event,
|
|
1480
|
+
() => ({ ...partial(), packages: [...results, { ...event, ok: false, ran: !event.notStarted }] }), 'subscription-result')
|
|
1481
|
+
},
|
|
1409
1482
|
billing: billing ? { ...billing, routes: billing.routes ?? routeList, ...(choices[item.id] ? { choice: choices[item.id] } : {}) } : null,
|
|
1410
1483
|
credentials, route, task: promptText,
|
|
1411
1484
|
})
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.25.0 — 2026-10-24
|
|
4
|
+
|
|
5
|
+
- Separate API financial accounting from subscription usage. API costs and API reservations govern monetary budgets; subscriptions track calls and reported input/output/cache tokens, with incomplete or missing usage retained as unknown.
|
|
6
|
+
- Keep confirmed subscription execution independent of exhausted API budgets or missing API prices. Before an actual API call or subscription-to-API fallback, estimate the actual API route and acquire the applicable confirmation and atomic reservation. A CLI using an API key remains API-billed.
|
|
7
|
+
- Preserve subscription-attempt receipts alongside API fallback results and retain both ledgers across retry replacement and history trimming. Old reference-dollar fields remain readable without converting them into current subscription money or fabricated token counts.
|
|
8
|
+
- Show API money and subscription quantities separately in the workbench, execution preview and history. Subscription tokens do not imply vendor allowance, remaining quota or percentage. Additional API quality/review calls still incur API charges.
|
|
9
|
+
- Upgrade persistent state to schema v3 while migrating readable v1/v2 histories and protected execution state. Fully stop all Hosts before updating and keep state backups before downgrading.
|
|
10
|
+
|
|
11
|
+
Final validation and release evidence are tracked in the [0.25.0 acceptance report](docs/releases/0.25.0-validation.md). No new real vendor inference or installed-DSH end-to-end claim is made by these changes.
|
|
12
|
+
|
|
3
13
|
## 0.24.0 — 2026-10-24
|
|
4
14
|
|
|
5
15
|
- Reserve remaining execution estimates in shared persistent state before admission. Daily/monthly checks account for recorded API spending plus other active or interrupted reservations, preventing concurrent tasks from independently admitting the same estimated capacity. Unknown estimates or unresolved costs under a finite budget require explicit confirmation.
|
package/README.i18n.yaml
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
# README translation pairing for this repository.
|
|
2
2
|
# Reviewed together; Git blob hashes identify the exact synchronized texts.
|
|
3
|
-
README.md:
|
|
4
|
-
README.zh.md:
|
|
3
|
+
README.md: 58ad39f472cdee7cbf9d4a2e07aa5b669e06683c
|
|
4
|
+
README.zh.md: 3737b74fdfb6f9ddf20e575217a5e07a5faa2a91
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Want release notes only? Click **Watch → Custom → Releases** (you do not nee
|
|
|
10
10
|
|
|
11
11
|
A cost-aware model-routing workbench for **DeepSeek Harness Desktop**. Plan locally, compare configured routes, inspect execution permissions, and run dependent work packages through model APIs or supported official vendor CLIs.
|
|
12
12
|
|
|
13
|
-
Current version: **0.
|
|
13
|
+
Current version: **0.25.0** (`@ljwei-stak/dsh-model-router`). API calls use monetary accounting and daily/monthly budget reservations; subscriptions use a separate ledger of calls and reported tokens. Confirmed subscription execution is not blocked only because the API budget is exhausted or API prices are missing. A switch to an API must be priced and admitted before that API call; mixed results retain both channel receipts. The workbench separates API spending/reservations from subscription usage, with unknown counters kept unknown. Workbench execution shows live elapsed time and per-package progress, supports cancellation with completed results and reported costs retained, and reconnects to the active job when the panel is reopened. Choose algorithm recommendations or manually assign each decomposed package to an exact model API or official tool, preserving dependencies, refreshed cost previews and retry targets. Routing now supports task-specific quality evidence, joint model/effort selection, historical P90 resource estimates and dependency-aware search. Manual mixed execution is read-only; tools use their configured default model, while the existing editable official-team workflow remains available. Compatibility remains **DeepSeek Harness Desktop 0.2.0-rc.1 / 0.2.0-rc.2 / 0.2.1-rc.2**, including Hermes/Pi installation and terminal support; neither has a verified managed runner yet. Real vendor execution evidence remains tied to **0.20.0**, with no new model experiments in this release. The router and [GAL](https://github.com/Alice-Marx/deepseek-harness-galgame) have been independent plugins since 0.12.0.
|
|
14
14
|
|
|
15
15
|
Official CLI teamwork runs packages sequentially with dependency context and no automatic model-directory API fallback. Editable teams explicitly choose the Harness sandbox or the vendor CLI's normal environment. In the latter, Git worktrees collect edits but do not restrict OS access. CLIs retain their configured model unless explicitly mapped. See [real Windows validation and its limits](docs/releases/0.20.0-validation.md).
|
|
16
16
|
|
|
@@ -27,22 +27,22 @@ Both README versions describe the same interfaces, formulas, defaults and limits
|
|
|
27
27
|
**Install (DSH Plugin Hub / web profile):** With the DSH CLI available, install the versioned npm package with:
|
|
28
28
|
|
|
29
29
|
```sh
|
|
30
|
-
dsh plugin --profile web add @ljwei-stak/dsh-model-router@0.
|
|
30
|
+
dsh plugin --profile web add @ljwei-stak/dsh-model-router@0.25.0
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
**Install in Harness Desktop:** Launch Desktop once to initialize its `desktop` profile, quit Desktop fully, then run:
|
|
34
34
|
|
|
35
35
|
```sh
|
|
36
|
-
dsh plugin --profile desktop add @ljwei-stak/dsh-model-router@0.
|
|
36
|
+
dsh plugin --profile desktop add @ljwei-stak/dsh-model-router@0.25.0
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
You can also paste the package spec into **Harness Desktop → Plugins → Add plugin**. Restart Desktop after installation. A global `npm install -g` by itself does not register a Desktop plugin.
|
|
40
40
|
|
|
41
|
-
**Package and entry point:** [`@ljwei-stak/dsh-model-router@0.
|
|
41
|
+
**Package and entry point:** [`@ljwei-stak/dsh-model-router@0.25.0` on npm](https://www.npmjs.com/package/@ljwei-stak/dsh-model-router). The root [`package.json`](package.json) declares the package and bundle patch; [`.dsh-plugin/index.mjs`](.dsh-plugin/index.mjs) exports the required `apply(ctx)` entry point. License: [MIT](LICENSE).
|
|
42
42
|
|
|
43
43
|
**Configuration:** There is no supported import of a user-authored JSON/YAML configuration file. Configure providers, models, and credentials on Harness's **Models** page; configure routing profiles and preferences in the plugin settings. Persistent router state is stored in `$DSH_HOME/model-router/state.json` (default: `~/.dsh/model-router/state.json`). Provider CLIs retain their own login and configuration. The router does not copy API keys into model profiles or terminal logs.
|
|
44
44
|
|
|
45
|
-
**Screenshots and evidence:** The [workbench screenshot](docs/assets/workbench-task-preview.jpg) uses fixture models, prices, and health data; it is a simulated UI preview, not a live Harness or model run. See the [routing workflow diagram](docs/assets/routing-workflow.svg) for the execution flow. The [real Windows validation report](docs/releases/0.20.0-validation.md) documents specific 0.20.0 executions and their limits; 0.
|
|
45
|
+
**Screenshots and evidence:** The [workbench screenshot](docs/assets/workbench-task-preview.jpg) uses fixture models, prices, and health data; it is a simulated UI preview, not a live Harness or model run. See the [routing workflow diagram](docs/assets/routing-workflow.svg) for the execution flow. The [real Windows validation report](docs/releases/0.20.0-validation.md) documents specific 0.20.0 executions and their limits; 0.25.0 has no verified end-to-end vendor run. The [live progress screenshot](docs/assets/workbench-run-progress.png) is an offline simulation; see the [0.25.0 acceptance report](docs/releases/0.25.0-validation.md) for API/subscription separation, checkpoints and validation boundaries.
|
|
46
46
|
|
|
47
47
|
**Permissions and risks:** Planning is local and makes no model call. Execution sends the selected task/context to the chosen model API or vendor CLI and may incur charges. The default Harness-restricted environment separates read-only runs from approved `workspace-write` runs. An explicitly selected `cli-native` editable team runs with the current OS user's permissions, outside the Harness process sandbox; a Git worktree collects changes but does not restrict OS access. External providers and CLI services apply their own data handling and billing terms.
|
|
48
48
|
|
|
@@ -63,7 +63,7 @@ You can also paste the package spec into **Harness Desktop → Plugins → Add p
|
|
|
63
63
|
| 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. |
|
|
64
64
|
| Official tools | Detect, install/update, check launch readiness and inspect login status for twelve registry tools; installation jobs have logs and cancellation. |
|
|
65
65
|
| Interactive terminal | PowerShell/system shell and all twelve registry CLI targets (ZCode TUI on verified Windows desktop installs), including fixed login commands, live output and multiple sessions. |
|
|
66
|
-
| Costs and subscriptions |
|
|
66
|
+
| Costs and subscriptions | API money and concurrent budget reservations are separate from subscription calls/tokens. Unknown subscription usage stays unknown; API fallback is re-estimated and admitted before the paid call. |
|
|
67
67
|
| History and quality | Per-package results, dependency views, failure details, rerun/reassignment, ratings and optional stronger-model review. |
|
|
68
68
|
| Execution boundaries | Shows what each route can read/write and whether its process uses the Harness sandbox. |
|
|
69
69
|
|
|
@@ -78,16 +78,16 @@ Generating a route recommendation is local: it does not start a vendor CLI or ma
|
|
|
78
78
|
Open **Plugins → Add plugin** and enter:
|
|
79
79
|
|
|
80
80
|
```text
|
|
81
|
-
@ljwei-stak/dsh-model-router@0.
|
|
81
|
+
@ljwei-stak/dsh-model-router@0.25.0
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
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.
|
|
84
|
+
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.25.0** and the sidebar should contain **模型路由** (Model Router). A global `npm install -g` alone does not register a Desktop plugin.
|
|
85
85
|
|
|
86
86
|
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.
|
|
87
87
|
|
|
88
88
|
### Upgrade from the old package name
|
|
89
89
|
|
|
90
|
-
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.
|
|
90
|
+
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.25.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.
|
|
91
91
|
|
|
92
92
|
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).
|
|
93
93
|
|
|
@@ -102,7 +102,7 @@ The workbench groups the workflow into four navigation tabs. Its header summariz
|
|
|
102
102
|
| **任务与执行** — Task and execution | Describe the task, generate advice, choose routed read-only execution or official CLI teamwork, preview and confirm, then inspect history. |
|
|
103
103
|
| **模型配置** — Model configuration | Refresh/search the model directory and edit exact-route profiles. |
|
|
104
104
|
| **官方工具** — Official tools | Complete onboarding checks, inspect/install tools and use the interactive terminal. |
|
|
105
|
-
| **预算与安全** — Budget and security |
|
|
105
|
+
| **预算与安全** — Budget and security | Manage API money/budgets and separate subscription usage; inspect subscription state and execution boundaries. |
|
|
106
106
|
|
|
107
107
|
Switching tabs keeps the panels mounted, preserving in-progress installation and terminal state.
|
|
108
108
|
|
|
@@ -351,18 +351,21 @@ Implementation: [official-team-runtime.mjs](.dsh-plugin/shared/official-team-run
|
|
|
351
351
|
|
|
352
352
|
## Costs, subscriptions and quality
|
|
353
353
|
|
|
354
|
-
- **
|
|
354
|
+
- **API budgets:** per-run planning targets and daily/monthly monetary checks apply to API-billed execution. Admission atomically checks recorded API spending plus other API reservations, then reserves the new API estimate. Unknown API prices, costs or reservations under a finite limit require explicit confirmation. Later API calls recheck fresh spending and holds. Confirmed subscriptions do not consume API money or fail only because its budget is full or its price is missing. Switching a subscription to API is a new monetary admission: estimate the actual API route, obtain any required confirmation, and reserve atomically before the call. Local estimates are not a hard provider billing cap.
|
|
355
355
|
- **Checkpoints and interruption:** routed executions, retries/reassignment, official teams, single-tool runs and session consultations use the durable lifecycle. Optional reviews and quality upgrades reserve extra capacity before another call. Before a quality upgrade starts, the first receipt and the pending additional attempt are saved, so interruption cannot erase the first charge or imply the second attempt was free. The UI displays actual spending and reservations separately; a reservation is not an invoice. Confirmed dead owners retain interrupted holds and unknown usage evidence until reconciled; elapsed time alone does not free them, and no call is automatically replayed.
|
|
356
356
|
- **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`.
|
|
357
|
-
- **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. A subscription-to-API route change must pass a fresh estimate and reservation check for
|
|
358
|
-
- **
|
|
357
|
+
- **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. A subscription-to-API route change must pass a fresh estimate and reservation check for the actual API route. A missing price or exceeded finite budget requires the applicable confirmation; without it, the API call stops. Managed editable/single-tool/team runs do not automatically retry on the API.
|
|
358
|
+
- **Subscription usage:** record daily/monthly calls, reported input/output/cache tokens and unknown/incomplete-usage counts, with provider/tool/model groups. Missing counters are not zero. A subscription-to-API attempt keeps the subscription usage receipt as well as the API financial result. Historical reference-dollar fields may remain in old records, but current subscription statistics do not convert them to money or fabricate missing token data. Token counts do not establish a vendor subscription allowance, remaining quota or percentage.
|
|
359
|
+
- **API accounting:** valid CLI-reported money, including zero, takes precedence for API-billed results. Otherwise token-based CLI cost requires complete usage and an actual reported model matching configured pricing or an explicit CLI mapping. Unknown or mismatched identity keeps API cost unknown; the recommended price does not prove actual cost. A CLI using an API key still belongs to API accounting. Provider bills remain authoritative. Replaced retry receipts and evicted history retain prior accounting.
|
|
359
360
|
- **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.
|
|
360
361
|
|
|
361
362
|
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.
|
|
362
363
|
|
|
363
364
|
### Actual cost versus planning estimate
|
|
364
365
|
|
|
365
|
-
|
|
366
|
+
The formula below applies to API-billed results. Algorithmic route-price comparisons are planning inputs; they do not turn a subscription into an API bill. The execution preview separates API money from estimated subscription tokens.
|
|
367
|
+
|
|
368
|
+
For API-billed results, a valid CLI-reported USD amount, including explicit zero, wins over token-price calculation. Otherwise, token-based CLI accounting requires complete normalized usage, known nonnegative prices, and a reported actual model matching the priced route or its explicit CLI mapping:
|
|
366
369
|
|
|
367
370
|
<!-- formula:actual-cost -->
|
|
368
371
|
```math
|
|
@@ -373,7 +376,7 @@ $`C_a`$ is `actualUsd`; $`I_u`$, $`O`$, $`R`$, and $`W`$ correspond to `inputTok
|
|
|
373
376
|
|
|
374
377
|
These ledger counters are disjoint after adapter normalization; do not add cached tokens to an inclusive input count. Missing optional cache prices use the input price. Incomplete usage, absent prices or unknown/unmatched identity keep the amount `null`, not zero. Complete terminal evidence can survive a failed/cancelled/timed-out run; partial events are marked `usageIncomplete`.
|
|
375
378
|
|
|
376
|
-
For example, a complete API result with 1,000 uncached input tokens at \$1/million and 500 output tokens at \$5/million costs \$0.0035. A subscription result
|
|
379
|
+
For example, a complete API result with 1,000 uncached input tokens at \$1/million and 500 output tokens at \$5/million costs \$0.0035. A subscription result instead records the reported tokens and call count; it is not converted to API money. A provider-reported \$0 for an API attempt is explicit evidence for that attempt, not proof that earlier unknown attempts were free. Actual provider bills are authoritative. Official-team mode does not automatically fall back to API, but a vendor CLI using its own API credentials still incurs API-billed usage.
|
|
377
380
|
|
|
378
381
|
<a id="routing"></a>
|
|
379
382
|
|
|
@@ -452,13 +455,13 @@ Implementation: [router.mjs](.dsh-plugin/shared/router.mjs), [assignment-solver.
|
|
|
452
455
|
|
|
453
456
|
## Data and privacy
|
|
454
457
|
|
|
455
|
-
Router profiles/preferences are held in the Harness plugin settings. Runtime state is stored in `<DSH_HOME>/model-router/state.json` (default `~/.dsh/model-router/state.json`): health/onboarding, subscription cooldown, ratings, up to 200 recent runs, up to 100 execution reservations, 50 terminal metadata records and opt-in public data snapshots. Runs referenced by a reservation are protected from history trimming. Applied rates and data/feedback policy versions are frozen in run records. Preferences are local to that shared DSH_HOME, not authenticated per-user accounts; public data requests do not upload tasks or feedback.
|
|
458
|
+
Router profiles/preferences are held in the Harness plugin settings. Runtime state is stored in `<DSH_HOME>/model-router/state.json` (default `~/.dsh/model-router/state.json`): health/onboarding, subscription cooldown and separate usage aggregates, ratings, up to 200 recent runs, up to 100 execution reservations, 50 terminal metadata records and opt-in public data snapshots. Runs referenced by a reservation are protected from history trimming. Applied rates and data/feedback policy versions are frozen in run records. Preferences are local to that shared DSH_HOME, not authenticated per-user accounts; public data requests do not upload tasks or feedback.
|
|
456
459
|
|
|
457
460
|
Version 0.15.0 also retains bounded spending aggregates for up to 90 populated calendar dates and 24 months. These contain amounts/counts, not task or response content.
|
|
458
461
|
|
|
459
462
|
Run history includes task text, output excerpts, route/workspace, failures and costs. Stored task text is capped at 20,000 characters and each answer at 4,000; reruns can therefore depend on truncated stored context. The plugin does not store API keys in model profiles or terminal transcripts. Task execution still sends the selected task/context to the chosen provider or CLI.
|
|
460
463
|
|
|
461
|
-
State writes use a lock and atomic replacement.
|
|
464
|
+
State writes use a lock and atomic replacement. Schema v3 preserves separate subscription usage and channel-aware reservations while migrating readable v1/v2 state without discarding API history, holds or old reference fields. Historical missing tokens stay unknown. Older plugin versions reject v3 state; fully stop every Host process before upgrading and back up state before downgrading. Unparseable state is preserved as a `state.json.corrupt-<timestamp>` backup and surfaced as a notice. Avoid sharing unredacted histories or logs.
|
|
462
465
|
|
|
463
466
|
<a id="development"></a>
|
|
464
467
|
|
|
@@ -496,7 +499,8 @@ Open [the local preview](http://127.0.0.1:4173). It uses mock models, tools and
|
|
|
496
499
|
| `shared/official-tool-executor.mjs`, `official-team-runtime.mjs` | Trusted entry, permissions/process capture, dependency execution and patch integration. |
|
|
497
500
|
| `shared/task-executors.mjs`, `subscription-billing.mjs` | Routed CLI/API execution, subscription policy and fallback. |
|
|
498
501
|
| `shared/run-ledger.mjs`, `router-state.mjs` | Identity/usage/cost records, retained spending and locked atomic persistence. |
|
|
499
|
-
| `shared/budget-reservations.mjs`, `execution-lifecycle.mjs` | Cross-process budget admission,
|
|
502
|
+
| `shared/budget-reservations.mjs`, `execution-lifecycle.mjs` | Cross-process API budget admission, channel-aware reservations, mandatory checkpoints and interrupted-owner evidence. |
|
|
503
|
+
| `shared/subscription-usage.mjs` | Independent observed subscription usage, calendar aggregation and retry/history retention. |
|
|
500
504
|
| Other `.dsh-plugin/shared/` modules | Terminal contracts, tool health, public snapshots and bounded feedback. |
|
|
501
505
|
| `.dsh-plugin/client.js` | Generated Desktop client bundle; rebuild after client changes. |
|
|
502
506
|
| `scripts/` and `tests/` | Build/package/verification helpers and regression tests. |
|
|
@@ -532,13 +536,14 @@ When maintaining these docs, update both languages in the same change: matching
|
|
|
532
536
|
| Symptom | Check |
|
|
533
537
|
| --- | --- |
|
|
534
538
|
| No routes or planning disabled | Configure Harness Models, refresh the directory and wait for tool detection. |
|
|
535
|
-
|
|
|
539
|
+
| API cost unknown | Supply both input/output USD prices for each compared API route and inspect usage/model evidence. |
|
|
540
|
+
| Subscription usage unknown | The vendor did not report complete counters, or the historical record lacks them. Do not infer tokens from old dollar references or invent remaining quota. |
|
|
536
541
|
| CLI installed but not executable | Review launch-readiness reason, platform/mode support and installation source. |
|
|
537
542
|
| Login/entitlement still fails | Check the vendor account, CLI login, network and actual model access; installation alone is insufficient. |
|
|
538
543
|
| Frontend says Host is older | Fully quit Harness, including the tray process, then restart. |
|
|
539
544
|
| Preview differs from old advice | Directory, login, quota or budget state changed; review the new preview. |
|
|
540
545
|
| Budget remains reserved after interruption | Check the original task, retained worktree and provider bill. The hold is not automatically cleared or replayed, and this release has no manual reconciliation/release control. |
|
|
541
|
-
| Older plugin cannot read state | Schema
|
|
546
|
+
| Older plugin cannot read state | Schema v3 preserves protected execution and separate subscription usage. Restore a compatible version; do not erase accounting to bypass checks. |
|
|
542
547
|
| Terminal TUI does not render | Check whether the card reports PTY or pipe fallback. |
|
|
543
548
|
| Logged in, but no executable team route | Check `readyModes`, platform, selected install and permission mode; terminal login alone does not establish managed capability. |
|
|
544
549
|
| Skills/MCP missing in Claude read-only | Restricted mode disables them; editable/native choices have different permissions and require the corresponding confirmation. |
|