@ljwei-stak/model-router-galgame 0.4.32 → 0.8.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 +4998 -4
- package/.dsh-plugin/index.mjs +469 -730
- package/.dsh-plugin/official-tools-remote-service.mjs +152 -0
- package/.dsh-plugin/shared/harness-plan.mjs +119 -0
- package/.dsh-plugin/shared/official-team-runtime.mjs +327 -0
- package/.dsh-plugin/shared/official-tool-executor.mjs +734 -0
- package/.dsh-plugin/shared/official-tool-registry.mjs +123 -0
- package/.dsh-plugin/shared/official-tools-remote.mjs +136 -0
- package/.dsh-plugin/shared/official-tools-runtime.mjs +490 -0
- package/.dsh-plugin/shared/router.mjs +98 -11
- package/.dsh-plugin/shared/vendor-mimo-grok-adapter.mjs +298 -0
- package/.dsh-plugin/shared/vendor-minimax-adapter.mjs +247 -0
- package/.dsh-plugin/shared/zcode-bundle.mjs +193 -0
- package/.dsh-plugin/shared/zcode-installer.mjs +247 -0
- package/INSTALLATION_GUIDE.zh.md +26 -290
- package/MIGRATION.md +24 -0
- package/README.md +23 -561
- package/README.zh.md +20 -526
- package/cordis.patch.yml +3 -37
- package/package.json +131 -112
- package/.dsh-plugin/shared/approval-gate.mjs +0 -109
- package/.dsh-plugin/shared/error-diagnostics.mjs +0 -17
- package/.dsh-plugin/shared/gal-game-service.mjs +0 -318
- package/.dsh-plugin/shared/gal-game.mjs +0 -271
- package/.dsh-plugin/shared/gal-story-afterword.mjs +0 -112
- package/.dsh-plugin/shared/gal-story-catalog.mjs +0 -27
- package/.dsh-plugin/shared/gal-story-ensemble.mjs +0 -440
- package/.dsh-plugin/shared/gal-story-personal.mjs +0 -437
- package/.dsh-plugin/shared/gal-story-v2-phase2.mjs +0 -397
- package/.dsh-plugin/shared/gal-story-v2-phase3.mjs +0 -341
- package/.dsh-plugin/shared/gal-story-v2.mjs +0 -497
- package/.dsh-plugin/shared/gal-story.mjs +0 -442
- package/.dsh-plugin/shared/liangshen-compat.mjs +0 -64
- package/.dsh-plugin/shared/modlens-routing.mjs +0 -21
- package/.dsh-plugin/shared/npm-update.mjs +0 -317
- package/.dsh-plugin/shared/persona.mjs +0 -93
- package/.dsh-plugin/shared/session-compat.mjs +0 -223
- package/.dsh-plugin/shared/watcher.mjs +0 -29
- package/.dsh-plugin/shared/web-routing.mjs +0 -90
- package/GAL_GAME_CONCEPT.zh.md +0 -266
- package/GAL_GAME_PREVIEW.md +0 -111
- package/GAL_STORY_V2_OUTLINE.zh.md +0 -323
- package/MODLENS_DEPLOYMENT.md +0 -72
- package//345/244/247/346/250/241/345/236/213/345/250/230/344/272/272/347/211/251/350/256/276/345/256/232.md +0 -276
package/.dsh-plugin/index.mjs
CHANGED
|
@@ -1,795 +1,534 @@
|
|
|
1
|
+
import z from '@deepseek-ai/schemastery'
|
|
2
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
3
|
+
import { DEFAULT_ROUTER_SETTINGS } from './shared/router.mjs'
|
|
4
|
+
import { createPlanFromRoutes } from './shared/harness-plan.mjs'
|
|
5
|
+
import { registerOfficialToolsRemote } from './official-tools-remote-service.mjs'
|
|
1
6
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
nextCollaborationStage,
|
|
9
|
-
selectReasoningEffort,
|
|
10
|
-
textFromMessages,
|
|
11
|
-
} from './shared/router.mjs'
|
|
12
|
-
import { formatErrorChain } from './shared/error-diagnostics.mjs'
|
|
13
|
-
import { fetchLiveBenchSnapshot } from './shared/livebench.mjs'
|
|
14
|
-
import { buildPersonaPrompt, isPersonaPrompt } from './shared/persona.mjs'
|
|
7
|
+
getOfficialTool,
|
|
8
|
+
installCommandLine,
|
|
9
|
+
toolForProvider,
|
|
10
|
+
} from './shared/official-tool-registry.mjs'
|
|
11
|
+
import { officialToolExecutionCapabilities, officialToolReadiness } from './shared/official-tool-executor.mjs'
|
|
12
|
+
import { runOfficialTask, runOfficialTeam, sessionWorkspace } from './shared/official-team-runtime.mjs'
|
|
15
13
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
isApprovalGateReason,
|
|
25
|
-
} from './shared/approval-gate.mjs'
|
|
26
|
-
import {
|
|
27
|
-
webCapabilityForPlan,
|
|
28
|
-
webCapabilityStatus,
|
|
29
|
-
webInstruction,
|
|
30
|
-
} from './shared/web-routing.mjs'
|
|
31
|
-
import { registerNpmUpdateRoute } from './shared/npm-update.mjs'
|
|
32
|
-
import { registerGalGameRoutes } from './shared/gal-game-service.mjs'
|
|
33
|
-
import { watcherStatus } from './shared/watcher.mjs'
|
|
34
|
-
import { repairLiangshenPreset, reportLiangshenCompatibility } from './shared/liangshen-compat.mjs'
|
|
35
|
-
import { repairRouterSessions, reportRouterSessionCompatibility } from './shared/session-compat.mjs'
|
|
14
|
+
probeAllTools,
|
|
15
|
+
probeToolWith,
|
|
16
|
+
startInstall,
|
|
17
|
+
cancelInstall,
|
|
18
|
+
installStatus,
|
|
19
|
+
installedToolIds,
|
|
20
|
+
defaultRunner,
|
|
21
|
+
} from './shared/official-tools-runtime.mjs'
|
|
36
22
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
let routerSettingsReady = false
|
|
40
|
-
let routerSettingsPromise = Promise.resolve(false)
|
|
41
|
-
async function loadSettingsRuntime() {
|
|
42
|
-
if (settingsRuntimePromise !== undefined) return settingsRuntimePromise
|
|
43
|
-
settingsRuntimePromise = Promise.all([
|
|
44
|
-
import('@deepseek-ai/schemastery'),
|
|
45
|
-
import('@deepseek-ai/dsh-settings'),
|
|
46
|
-
]).then(([schemaModule, settingsModule]) => ({
|
|
47
|
-
z: schemaModule.default ?? schemaModule,
|
|
48
|
-
settingsNamespace: settingsModule.settingsNamespace,
|
|
49
|
-
}))
|
|
50
|
-
return settingsRuntimePromise
|
|
51
|
-
}
|
|
23
|
+
export const name = 'model-router-galgame'
|
|
24
|
+
export const inject = ['commands', 'llm', 'tools', 'typert', 'sandboxPolicy', 'sandbox']
|
|
52
25
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
cacheRead: z.number().min(0).default(0),
|
|
58
|
-
cacheWrite: z.number().min(0).default(0),
|
|
59
|
-
currency: z.string().default('USD'),
|
|
60
|
-
})
|
|
61
|
-
return z.object({
|
|
62
|
-
pricing: z.dict(price).default({}),
|
|
63
|
-
liveBenchEndpoint: z.string().default(DEFAULT_ROUTER_SETTINGS.liveBenchEndpoint),
|
|
64
|
-
liveBenchTtlMs: z.number().min(30000).default(DEFAULT_ROUTER_SETTINGS.liveBenchTtlMs),
|
|
65
|
-
budgetUsd: z.number().min(0).default(DEFAULT_ROUTER_SETTINGS.budgetUsd),
|
|
66
|
-
cacheReadRatio: z.number().min(0).max(1).default(DEFAULT_ROUTER_SETTINGS.cacheReadRatio),
|
|
67
|
-
cacheWriteRatio: z.number().min(0).max(1).default(DEFAULT_ROUTER_SETTINGS.cacheWriteRatio),
|
|
68
|
-
})
|
|
69
|
-
}
|
|
26
|
+
export const Config = z.object({
|
|
27
|
+
budgetUsd: z.number().min(0).max(1_000_000).default(DEFAULT_ROUTER_SETTINGS.budgetUsd).volatile(),
|
|
28
|
+
maxConsultOutputChars: z.number().step(1).min(500).max(50_000).default(12_000).volatile(),
|
|
29
|
+
})
|
|
70
30
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
state.liveBenchPromise = null
|
|
75
|
-
state.liveBenchError = null
|
|
76
|
-
}
|
|
31
|
+
const JSON_OUTPUT = {
|
|
32
|
+
schema: { type: 'json' },
|
|
33
|
+
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
77
34
|
}
|
|
78
35
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (settings === undefined || typeof settings.register !== 'function') return false
|
|
82
|
-
try {
|
|
83
|
-
const runtime = await loadSettingsRuntime()
|
|
84
|
-
const namespace = runtime.settingsNamespace(MODEL_ROUTER_SETTINGS_NAMESPACE)
|
|
85
|
-
const scope = settings.register(namespace, routerSettingsSchema(runtime.z), {
|
|
86
|
-
base: DEFAULT_ROUTER_SETTINGS,
|
|
87
|
-
})
|
|
88
|
-
routerSettings = scope.get()
|
|
89
|
-
routerSettingsReady = true
|
|
90
|
-
scope.watch(next => {
|
|
91
|
-
routerSettings = next
|
|
92
|
-
invalidateRouterPlans()
|
|
93
|
-
})
|
|
94
|
-
return true
|
|
95
|
-
} catch (error) {
|
|
96
|
-
ctx.logger?.warn?.(`model-router: settings registration unavailable: ${String(error)}`)
|
|
97
|
-
return false
|
|
98
|
-
}
|
|
36
|
+
function jsonValue(value) {
|
|
37
|
+
return JSON.parse(JSON.stringify(value))
|
|
99
38
|
}
|
|
100
39
|
|
|
101
|
-
|
|
102
|
-
|
|
40
|
+
function valueOf(config, key, fallback) {
|
|
41
|
+
const value = config?.[key]
|
|
42
|
+
return value !== undefined && typeof value?.get === 'function' ? value.get() : value ?? fallback
|
|
43
|
+
}
|
|
103
44
|
|
|
104
|
-
|
|
105
|
-
|
|
45
|
+
function finiteNumber(value, fallback) {
|
|
46
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback
|
|
47
|
+
}
|
|
106
48
|
|
|
107
|
-
|
|
108
|
-
|
|
49
|
+
function boundedInteger(value, fallback, minimum, maximum) {
|
|
50
|
+
return Math.min(maximum, Math.max(minimum, Math.floor(finiteNumber(value, fallback))))
|
|
51
|
+
}
|
|
109
52
|
|
|
110
|
-
function
|
|
111
|
-
|
|
112
|
-
if (state === undefined) {
|
|
113
|
-
state = {
|
|
114
|
-
mode: 'collective',
|
|
115
|
-
plan: null,
|
|
116
|
-
available: [],
|
|
117
|
-
directoryPromise: null,
|
|
118
|
-
turn: null,
|
|
119
|
-
failedModels: new Set(),
|
|
120
|
-
routeCooldowns: new Map(),
|
|
121
|
-
lastTarget: null,
|
|
122
|
-
lastStep: 0,
|
|
123
|
-
collaboration: null,
|
|
124
|
-
taskText: '',
|
|
125
|
-
personaInjected: false,
|
|
126
|
-
liveBench: null,
|
|
127
|
-
liveBenchFetchedAt: 0,
|
|
128
|
-
liveBenchPromise: null,
|
|
129
|
-
liveBenchError: null,
|
|
130
|
-
visionBridges: [],
|
|
131
|
-
hasImageBlocks: false,
|
|
132
|
-
}
|
|
133
|
-
states.set(agent, state)
|
|
134
|
-
allStates.add(state)
|
|
135
|
-
}
|
|
136
|
-
return state
|
|
53
|
+
function text(value) {
|
|
54
|
+
return typeof value === 'string' ? value.trim() : ''
|
|
137
55
|
}
|
|
138
56
|
|
|
139
|
-
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
const routes = []
|
|
143
|
-
const visionBridges = []
|
|
144
|
-
let providers = []
|
|
145
|
-
try { providers = ctx.llm.listProviders() } catch { providers = [] }
|
|
146
|
-
for (const provider of providers) {
|
|
147
|
-
// Synthetic ModLens routes are selectable in the native model picker,
|
|
148
|
-
// but collective routing evaluates the underlying provider only. This
|
|
149
|
-
// prevents a wrapper from competing with its own upstream route and
|
|
150
|
-
// avoids a second image conversion in collective mode.
|
|
151
|
-
const bridgeUpstream = modLensUpstream(provider?.id)
|
|
152
|
-
if (bridgeUpstream !== null) {
|
|
153
|
-
try {
|
|
154
|
-
const models = await ctx.llm.listModels(provider.id)
|
|
155
|
-
for (const model of models) visionBridges.push({ provider: provider.id, upstream: bridgeUpstream, model: model.id })
|
|
156
|
-
} catch {
|
|
157
|
-
// A failed synthetic catalog must not hide the usable upstream models.
|
|
158
|
-
}
|
|
159
|
-
continue
|
|
160
|
-
}
|
|
161
|
-
try {
|
|
162
|
-
const models = await ctx.llm.listModels(provider.id)
|
|
163
|
-
const resolvedRoutes = await Promise.all(models.map(async model => {
|
|
164
|
-
let resolved
|
|
165
|
-
try {
|
|
166
|
-
resolved = typeof ctx.llm.resolveModelInfo === 'function'
|
|
167
|
-
? await ctx.llm.resolveModelInfo(provider.id, model.id)
|
|
168
|
-
: undefined
|
|
169
|
-
} catch (error) {
|
|
170
|
-
ctx.logger?.debug?.(`model-router: reasoning metadata unavailable for ${provider.id}/${model.id}: ${String(error)}`)
|
|
171
|
-
}
|
|
172
|
-
const inputModalities = resolved?.inputModalities ?? model.inputModalities ?? model.input ?? []
|
|
173
|
-
const reasoning = resolved?.reasoning
|
|
174
|
-
return {
|
|
175
|
-
provider: provider.id,
|
|
176
|
-
model: model.id,
|
|
177
|
-
inputModalities: Array.isArray(inputModalities) ? [...inputModalities] : [],
|
|
178
|
-
...(resolved === undefined ? {} : {
|
|
179
|
-
reasoningKnown: true,
|
|
180
|
-
reasoningEfforts: Array.isArray(reasoning?.efforts) ? reasoning.efforts.map(effort => effort.id) : [],
|
|
181
|
-
...(reasoning?.defaultEffort === undefined ? {} : { defaultReasoningEffort: reasoning.defaultEffort }),
|
|
182
|
-
}),
|
|
183
|
-
}
|
|
184
|
-
}))
|
|
185
|
-
routes.push(...resolvedRoutes)
|
|
186
|
-
} catch (error) {
|
|
187
|
-
ctx.logger?.debug?.(`model-router: model discovery failed for ${provider.id}: ${String(error)}`)
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
state.available = routes
|
|
191
|
-
state.visionBridges = visionBridges
|
|
192
|
-
return routes
|
|
193
|
-
})().catch(error => {
|
|
194
|
-
state.directoryPromise = null
|
|
195
|
-
ctx.logger?.warn?.(`model-router: model discovery unavailable: ${String(error)}`)
|
|
196
|
-
return state.available
|
|
197
|
-
})
|
|
198
|
-
return state.directoryPromise
|
|
57
|
+
function errorText(error) {
|
|
58
|
+
if (error instanceof Error && error.message) return error.message
|
|
59
|
+
try { return String(error) } catch { return 'unknown error' }
|
|
199
60
|
}
|
|
200
61
|
|
|
201
|
-
function
|
|
202
|
-
|
|
62
|
+
function throwIfAborted(signal) {
|
|
63
|
+
if (signal?.aborted) throw signal.reason instanceof Error ? signal.reason : new Error('operation aborted')
|
|
203
64
|
}
|
|
204
65
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const ttl = Math.max(30000, Number(routerSettings.liveBenchTtlMs) || DEFAULT_ROUTER_SETTINGS.liveBenchTtlMs)
|
|
208
|
-
if (state.liveBench !== null && Date.now() - state.liveBenchFetchedAt < ttl) return state.liveBench
|
|
209
|
-
if (state.liveBenchPromise !== null) return state.liveBenchPromise
|
|
210
|
-
const configuredEndpoint = String(routerSettings.liveBenchEndpoint || DEFAULT_ROUTER_SETTINGS.liveBenchEndpoint)
|
|
211
|
-
// Migrate the endpoint used by the first prototype; it returned 404 after
|
|
212
|
-
// LiveBench moved to versioned CSV/JSON assets.
|
|
213
|
-
const endpoint = configuredEndpoint === 'https://livebench.ai/api/leaderboard'
|
|
214
|
-
? DEFAULT_ROUTER_SETTINGS.liveBenchEndpoint
|
|
215
|
-
: configuredEndpoint
|
|
216
|
-
state.liveBenchPromise = fetchLiveBenchSnapshot({
|
|
217
|
-
endpoint,
|
|
218
|
-
}).then(snapshot => {
|
|
219
|
-
state.liveBench = snapshot
|
|
220
|
-
state.liveBenchFetchedAt = snapshot.fetchedAt
|
|
221
|
-
state.liveBenchError = null
|
|
222
|
-
return snapshot
|
|
223
|
-
}).catch(error => {
|
|
224
|
-
state.liveBenchError = String(error)
|
|
225
|
-
ctx.logger?.warn?.(`model-router: LiveBench refresh failed; using ${state.liveBench === null ? 'experimental baseline' : 'last snapshot'}: ${String(error)}`)
|
|
226
|
-
return state.liveBench
|
|
227
|
-
}).finally(() => {
|
|
228
|
-
state.liveBenchPromise = null
|
|
229
|
-
})
|
|
230
|
-
return state.liveBenchPromise
|
|
66
|
+
function routeKey(route) {
|
|
67
|
+
return `${route.provider}\u0000${route.model}`
|
|
231
68
|
}
|
|
232
69
|
|
|
233
|
-
function
|
|
234
|
-
|
|
235
|
-
return `model-router-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
|
70
|
+
function uniqueStrings(values) {
|
|
71
|
+
return [...new Set(values.map(text).filter(Boolean))]
|
|
236
72
|
}
|
|
237
73
|
|
|
238
|
-
/**
|
|
239
|
-
function
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
return
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
74
|
+
/** Read only exact provider/model routes published by the official LLM service. */
|
|
75
|
+
export async function discoverConfiguredRoutes(ctx, signal) {
|
|
76
|
+
throwIfAborted(signal)
|
|
77
|
+
let providers
|
|
78
|
+
try { providers = ctx.llm.listProviders() } catch { return [] }
|
|
79
|
+
const routes = new Map()
|
|
80
|
+
for (const entry of Array.isArray(providers) ? providers : []) {
|
|
81
|
+
const provider = text(typeof entry === 'string' ? entry : entry?.id)
|
|
82
|
+
if (!provider) continue
|
|
83
|
+
throwIfAborted(signal)
|
|
84
|
+
let models
|
|
85
|
+
try { models = await ctx.llm.listModels(provider) } catch {
|
|
86
|
+
throwIfAborted(signal)
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
for (const listed of Array.isArray(models) ? models : []) {
|
|
90
|
+
const model = text(listed?.id ?? listed?.model)
|
|
91
|
+
if (!model) continue
|
|
92
|
+
throwIfAborted(signal)
|
|
93
|
+
let resolved = listed
|
|
94
|
+
try { resolved = await ctx.llm.resolveModelInfo(provider, model, signal) } catch {
|
|
95
|
+
throwIfAborted(signal)
|
|
96
|
+
}
|
|
97
|
+
const reasoning = resolved?.reasoning
|
|
98
|
+
const route = {
|
|
99
|
+
provider,
|
|
100
|
+
model,
|
|
101
|
+
name: text(resolved?.name ?? listed?.name) || model,
|
|
102
|
+
inputModalities: uniqueStrings(Array.isArray(resolved?.inputModalities)
|
|
103
|
+
? resolved.inputModalities : Array.isArray(listed?.inputModalities) ? listed.inputModalities : []),
|
|
104
|
+
reasoningKnown: reasoning !== undefined && reasoning !== null,
|
|
105
|
+
reasoningEfforts: uniqueStrings(Array.isArray(reasoning?.efforts) ? reasoning.efforts.map(item => item?.id ?? item) : []),
|
|
106
|
+
...text(reasoning?.defaultEffort) ? { defaultReasoningEffort: text(reasoning.defaultEffort) } : {},
|
|
107
|
+
}
|
|
108
|
+
routes.set(routeKey(route), route)
|
|
109
|
+
}
|
|
247
110
|
}
|
|
111
|
+
return [...routes.values()].sort((left, right) => routeKey(left).localeCompare(routeKey(right)))
|
|
248
112
|
}
|
|
249
113
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
114
|
+
/** Produce a route recommendation and work packages compatible with official Agent Teams. */
|
|
115
|
+
export async function createRoutePlan(ctx, task, config = {}, options = {}) {
|
|
116
|
+
const taskText = text(task)
|
|
117
|
+
if (!taskText) throw new Error('task must contain text')
|
|
118
|
+
const mode = options.mode === 'team' ? 'team' : 'single'
|
|
119
|
+
const configuredBudget = valueOf(config, 'budgetUsd', DEFAULT_ROUTER_SETTINGS.budgetUsd)
|
|
120
|
+
const budgetUsd = Math.max(0, finiteNumber(options.budgetUsd, finiteNumber(configuredBudget, 0)))
|
|
121
|
+
const [availableRoutes, installed] = await Promise.all([
|
|
122
|
+
discoverConfiguredRoutes(ctx, options.signal),
|
|
123
|
+
options.skipToolProbe === true
|
|
124
|
+
? Promise.resolve(Array.isArray(options.installedToolIds) ? options.installedToolIds : [])
|
|
125
|
+
: installedToolIds(),
|
|
126
|
+
])
|
|
127
|
+
const readiness = options.skipToolProbe === true ? []
|
|
128
|
+
: await Promise.all(installed.map(id => officialToolReadiness(id, options.workspace ?? process.cwd())))
|
|
129
|
+
const executable = new Set(Array.isArray(options.runnableToolIds)
|
|
130
|
+
? options.runnableToolIds : readiness.filter(item => item.ready).map(item => item.id))
|
|
131
|
+
return createPlanFromRoutes(taskText, availableRoutes, {
|
|
132
|
+
mode, budgetUsd, installedToolIds: installed,
|
|
133
|
+
runnableToolIds: installed.filter(id => executable.has(id)),
|
|
134
|
+
})
|
|
259
135
|
}
|
|
260
136
|
|
|
261
|
-
/**
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
let
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
137
|
+
/** One bounded, independent call through the same official LLM service. */
|
|
138
|
+
export async function consultConfiguredModel(ctx, route, task, outputLimit = 12_000, signal) {
|
|
139
|
+
const provider = text(route?.provider)
|
|
140
|
+
const model = text(route?.model)
|
|
141
|
+
const taskText = text(task)
|
|
142
|
+
if (!provider || !model) throw new Error('a configured provider and model are required')
|
|
143
|
+
if (!taskText) throw new Error('task must contain text')
|
|
144
|
+
throwIfAborted(signal)
|
|
145
|
+
const limit = boundedInteger(outputLimit, 12_000, 1, 50_000)
|
|
146
|
+
const controller = new AbortController()
|
|
147
|
+
const onAbort = () => controller.abort(signal.reason)
|
|
148
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
149
|
+
let answer = ''
|
|
150
|
+
let characterLimitReached = false
|
|
151
|
+
let finish = { kind: 'unknown' }
|
|
152
|
+
try {
|
|
153
|
+
const stream = ctx.llm.stream({
|
|
154
|
+
provider,
|
|
155
|
+
model,
|
|
156
|
+
messages: [{
|
|
157
|
+
role: 'user',
|
|
158
|
+
content: [{
|
|
159
|
+
type: 'text',
|
|
160
|
+
text: `You are an independent specialist consulted by another AI agent. Give a concise, evidence-oriented answer in the task's language.\n\nTask:\n${taskText}`,
|
|
161
|
+
}],
|
|
162
|
+
}],
|
|
163
|
+
maxTokens: Math.min(4_096, Math.max(256, Math.ceil(limit / 2))),
|
|
164
|
+
signal: controller.signal,
|
|
165
|
+
})
|
|
166
|
+
for await (const chunk of stream) {
|
|
167
|
+
if (chunk?.type === 'text-delta' && typeof chunk.text === 'string') {
|
|
168
|
+
const remaining = limit - answer.length
|
|
169
|
+
if (remaining > 0) answer += chunk.text.slice(0, remaining)
|
|
170
|
+
if (chunk.text.length > remaining) {
|
|
171
|
+
characterLimitReached = true
|
|
172
|
+
controller.abort(new Error('consultation output limit reached'))
|
|
173
|
+
break
|
|
174
|
+
}
|
|
175
|
+
} else if (chunk?.type === 'finish') {
|
|
176
|
+
const reason = chunk.reason
|
|
177
|
+
finish = { kind: text(reason?.kind) || 'unknown' }
|
|
178
|
+
if (finish.kind === 'error' || finish.kind === 'aborted') {
|
|
179
|
+
finish.error = text(reason?.failure?.message) || 'model request failed'
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} finally {
|
|
184
|
+
signal?.removeEventListener('abort', onAbort)
|
|
279
185
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
186
|
+
throwIfAborted(signal)
|
|
187
|
+
const tokenLimitReached = finish.kind === 'max-tokens'
|
|
188
|
+
const truncated = characterLimitReached || tokenLimitReached
|
|
189
|
+
if (!characterLimitReached && (finish.kind === 'error' || finish.kind === 'aborted')) {
|
|
190
|
+
return { ok: false, provider, model, answer, truncated, finish, error: finish.error }
|
|
283
191
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
model: route?.model ?? '',
|
|
287
|
-
mode: state.mode,
|
|
288
|
-
stage: stage?.purpose === 'synthesis' ? 'synthesis' : 'answer',
|
|
289
|
-
taskText: state.taskText,
|
|
290
|
-
})
|
|
291
|
-
return {
|
|
292
|
-
id: newMessageId(),
|
|
293
|
-
role: 'user',
|
|
294
|
-
content: [{ type: 'text', text }],
|
|
295
|
-
source: { kind: 'plugin', plugin: name, form: 'instructions' },
|
|
192
|
+
if (!answer.trim()) {
|
|
193
|
+
return { ok: false, provider, model, answer, truncated, finish, error: 'model returned no text' }
|
|
296
194
|
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/** A short, auditable routing explanation shown before the first work stage. */
|
|
300
|
-
function analysisMessage(plan) {
|
|
301
|
-
if (plan === null || plan === undefined) return null
|
|
302
|
-
const weights = plan.objectiveWeights ?? {}
|
|
303
|
-
const selected = plan.selected === null || plan.selected === undefined
|
|
304
|
-
? '待发现模型'
|
|
305
|
-
: `${plan.selected.provider}/${plan.selected.model}`
|
|
306
|
-
const text = [
|
|
307
|
-
'[Model Router 路由分析]',
|
|
308
|
-
`任务类型:${plan.taskType};复杂度:${plan.complexity?.band ?? 'unknown'}(${Math.round((plan.complexity?.value ?? 0) * 100)}%)`,
|
|
309
|
-
Array.isArray(plan.taskTypes) && plan.taskTypes.length > 1 ? `业务方向:${plan.taskTypes.join('、')}(分别建立执行工作包)` : '',
|
|
310
|
-
`本轮权重:质量 ${Math.round((weights.quality ?? 0) * 100)}%,成本 ${Math.round((weights.cost ?? 0) * 100)}%,推理等级 ${Math.round((weights.reasoning ?? 0) * 100)}%,延迟 ${Math.round((weights.latency ?? 0) * 100)}%,专长 ${Math.round((weights.specialty ?? 0) * 100)}%,风险 ${Math.round((weights.risk ?? 0) * 100)}%`,
|
|
311
|
-
`质量下限:${Math.round(Number(plan.optimization?.qualityFloor ?? 0) * 100)}%;首选路由:${selected}${plan.selected?.reasoningEffort ? `;推理等级:${plan.selected.reasoningEffort}` : ';推理等级:提供方默认'}`,
|
|
312
|
-
`预计总费用:$${Number(plan.estimatedCost ?? 0).toFixed(6)};相对全高质量基线节省:$${Number((plan.optimization?.baselineAllStrongCost ?? 0) - (plan.estimatedCost ?? 0)).toFixed(6)}`,
|
|
313
|
-
`缓存计费比例:读取 ${Math.round(Number(plan.optimization?.cacheReadRatio ?? 0) * 100)}%,写入 ${Math.round(Number(plan.optimization?.cacheWriteRatio ?? 0) * 100)}%(未填写时按普通输入计费)`,
|
|
314
|
-
Number(plan.optimization?.budgetUsd ?? 0) > 0 ? `预算上限:$${Number(plan.optimization.budgetUsd).toFixed(6)};${plan.optimization.budgetExceeded ? '仍超预算,已在质量下限内尽量压缩' : '满足预算约束'}` : '',
|
|
315
|
-
`LiveBench:${plan.optimization?.liveBench?.fetchedAt ? `快照于 ${new Date(Number(plan.optimization.liveBench.fetchedAt)).toISOString()}${plan.optimization.liveBench.stale ? '(本次刷新失败,沿用上次快照)' : ''}` : '未完成联网核验,使用实验基线'}`,
|
|
316
|
-
plan.web?.needsWeb ? `联网策略:${plan.web.directBrowser ? 'Ego Browser 可见窗口优先' : 'ModSearch 搜索/抓取,失败时 Ego Browser 窗口兜底'};反爬处理:人工接管后继续` : '',
|
|
317
|
-
String(plan.reason ?? ''),
|
|
318
|
-
].filter(Boolean).join('\n')
|
|
319
195
|
return {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
196
|
+
ok: true, provider, model, answer, truncated, finish,
|
|
197
|
+
...(tokenLimitReached
|
|
198
|
+
? { truncationReason: 'model-token-limit', notice: '模型达到本次调用的输出 token 上限,回答可能不完整。' }
|
|
199
|
+
: characterLimitReached
|
|
200
|
+
? { truncationReason: 'output-character-limit', notice: '回答达到字符上限,后续内容已截断。' }
|
|
201
|
+
: {}),
|
|
324
202
|
}
|
|
325
203
|
}
|
|
326
204
|
|
|
327
|
-
function
|
|
328
|
-
const
|
|
329
|
-
|
|
205
|
+
function explicitRoute(args, routes) {
|
|
206
|
+
const provider = text(args.provider)
|
|
207
|
+
const model = text(args.model)
|
|
208
|
+
if (Boolean(provider) !== Boolean(model)) throw new Error('provider and model must be supplied together')
|
|
209
|
+
if (!provider) return null
|
|
210
|
+
const route = routes.find(item => item.provider === provider && item.model === model)
|
|
211
|
+
if (!route) throw new Error(`route ${provider}/${model} is not configured in DeepSeek Harness`)
|
|
212
|
+
return route
|
|
330
213
|
}
|
|
331
214
|
|
|
332
|
-
function
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
&&
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
&& Array.isArray(available)
|
|
339
|
-
&& available.length > 0
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
function routeKey(provider, model) {
|
|
343
|
-
return `${String(provider ?? '')}/${String(model ?? '')}`
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function modelFallbackError(failure) {
|
|
347
|
-
const text = `${String(failure?.code ?? '')} ${String(failure?.message ?? '')} ${formatErrorChain(failure)}`.toLowerCase()
|
|
348
|
-
return /unsupported[_ -]reasoning[_ -]effort|no[_ -]adapter|invalid[_ -]model|invalid[_ -]credential|missing[_ -]credential|auth(?:entication|orization)?|api key|quota|region|not available|not supported|unsupported provider stream event|provider protocol|invalid (?:stream|event)|codex\.rate_limits|freeusagelimit|rate limit|too many requests|\b(?:401|403|404|429)\b/.test(text)
|
|
215
|
+
function currentAgentRoute(agent) {
|
|
216
|
+
try {
|
|
217
|
+
const config = agent?.session?.requestHeader?.()?.config
|
|
218
|
+
if (text(config?.provider) && text(config?.model)) return { provider: config.provider, model: config.model }
|
|
219
|
+
} catch { /* background tools need not have a session-backed agent */ }
|
|
220
|
+
return null
|
|
349
221
|
}
|
|
350
222
|
|
|
351
|
-
function
|
|
352
|
-
const
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
if (
|
|
356
|
-
|
|
357
|
-
return 2 * 60 * 1000
|
|
223
|
+
function chooseConsultRoute(plan, routes, agent) {
|
|
224
|
+
const current = currentAgentRoute(agent)
|
|
225
|
+
const differs = route => current === null || routeKey(route) !== routeKey(current)
|
|
226
|
+
const preferred = routes.find(route => route.provider === plan.selected?.provider && route.model === plan.selected?.model)
|
|
227
|
+
if (preferred && differs(preferred)) return preferred
|
|
228
|
+
return routes.find(differs) ?? preferred ?? routes[0]
|
|
358
229
|
}
|
|
359
230
|
|
|
360
|
-
function
|
|
361
|
-
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
231
|
+
function commandText(plan) {
|
|
232
|
+
const selected = plan.selected ? `${plan.selected.provider}/${plan.selected.model}` : '没有可用路线'
|
|
233
|
+
const channel = plan.executionChannel === 'official-cli'
|
|
234
|
+
? `官方 CLI(${plan.channelLabel ?? plan.channelTool})`
|
|
235
|
+
: '官方模型目录 API'
|
|
236
|
+
return [
|
|
237
|
+
`推荐路线:${selected}`,
|
|
238
|
+
`复杂度:${plan.complexity.band};任务类型:${plan.taskType}`,
|
|
239
|
+
`执行渠道:${channel};估算成本:$${plan.estimatedCost.toFixed(6)}(仅估算)`,
|
|
240
|
+
`工作包:${plan.subtasks.map(item => `${item.name} → ${item.recommendedProvider}/${item.recommended}`).join(';')}`,
|
|
241
|
+
plan.team.handoff,
|
|
242
|
+
].join('\n')
|
|
368
243
|
}
|
|
369
244
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
if (
|
|
245
|
+
/** Register model-facing tools and the human /router command. */
|
|
246
|
+
export function apply(ctx, config = {}) {
|
|
247
|
+
// The official Host injects typert; direct lightweight uses of apply may
|
|
248
|
+
// supply only the model/command services and do not expose the Desktop RPC.
|
|
249
|
+
if (ctx.typert) registerOfficialToolsRemote(ctx)
|
|
250
|
+
ctx.on('tools/pre-execute', async (exec, next) => {
|
|
251
|
+
const decision = await next()
|
|
252
|
+
if (decision.kind !== 'allow') return decision
|
|
253
|
+
if (exec.name === 'model_router_tool_install') {
|
|
254
|
+
const requested = getOfficialTool(text(exec.arguments?.tool))
|
|
255
|
+
const label = requested?.label ?? '官方 CLI'
|
|
256
|
+
const desktopInstaller = requested?.manager === 'signed-windows-installer'
|
|
379
257
|
return {
|
|
380
|
-
|
|
381
|
-
|
|
258
|
+
kind: 'ask',
|
|
259
|
+
reason: desktopInstaller
|
|
260
|
+
? `Download and open the verified official ${label} desktop installer`
|
|
261
|
+
: `Install ${label} globally with the plugin's fixed official command`,
|
|
262
|
+
displayReason: {
|
|
263
|
+
en: desktopInstaller
|
|
264
|
+
? `Download and open the verified ${label} installer? You can select the installation directory in its window.`
|
|
265
|
+
: `Install ${label} globally using the fixed official package?`,
|
|
266
|
+
zh: desktopInstaller
|
|
267
|
+
? `下载并打开已验签的 ${label} 安装器?安装窗口中可选择非 C 盘目录。`
|
|
268
|
+
: `使用插件固定的官方软件包,在本机全局安装 ${label}?`,
|
|
269
|
+
},
|
|
382
270
|
}
|
|
383
271
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
/**
|
|
397
|
-
* Remove an official OpenCode website URL only when it is a user override and
|
|
398
|
-
* the built-in model catalog is still in use. This lets the catalog restore
|
|
399
|
-
* its per-model /zen and /zen/v1 endpoints without touching custom gateways.
|
|
400
|
-
*/
|
|
401
|
-
async function repairOpenCodeEndpoint(ctx) {
|
|
402
|
-
const settings = settingsService(ctx)
|
|
403
|
-
if (settings === undefined || typeof settings.describe !== 'function' || typeof settings.mutate !== 'function') {
|
|
404
|
-
return 'unavailable'
|
|
405
|
-
}
|
|
406
|
-
let descriptor
|
|
407
|
-
try {
|
|
408
|
-
const descriptors = settings.describe()
|
|
409
|
-
descriptor = (Array.isArray(descriptors) ? descriptors : [])
|
|
410
|
-
.find(entry => String(entry?.ns) === LLM_SETTINGS_NAMESPACE)
|
|
411
|
-
} catch (error) {
|
|
412
|
-
ctx.logger?.debug?.(`model-router: settings inspection unavailable: ${String(error)}`)
|
|
413
|
-
return 'unavailable'
|
|
414
|
-
}
|
|
415
|
-
// The settings service can be mounted after this plugin. Report this state
|
|
416
|
-
// separately so the bounded startup retry can observe the namespace later.
|
|
417
|
-
if (descriptor === undefined) return 'pending'
|
|
418
|
-
const ops = collectOpenCodeEndpointRepairs(descriptor?.user)
|
|
419
|
-
if (ops.length === 0) return 'clean'
|
|
420
|
-
try {
|
|
421
|
-
await settings.mutate(LLM_SETTINGS_NAMESPACE, ops, descriptor.revision)
|
|
422
|
-
ctx.logger?.info?.(`model-router: restored OpenCode catalog endpoints for ${ops.length} route(s)`)
|
|
423
|
-
return 'repaired'
|
|
424
|
-
} catch (error) {
|
|
425
|
-
// A concurrent settings write can make the revision stale. The next
|
|
426
|
-
// settings/updated event retries the same repair against the new revision.
|
|
427
|
-
ctx.logger?.warn?.(`model-router: could not repair OpenCode endpoint: ${String(error)}`)
|
|
428
|
-
return 'retry'
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
/**
|
|
433
|
-
* Serialize endpoint repairs and retry only while the settings namespace is
|
|
434
|
-
* coming online or a concurrent write makes the revision stale. A bounded
|
|
435
|
-
* timer avoids leaving a desktop process alive indefinitely during shutdown.
|
|
436
|
-
*/
|
|
437
|
-
function createOpenCodeRepairScheduler(ctx) {
|
|
438
|
-
const control = { inFlight: null, retryIndex: 0 }
|
|
439
|
-
|
|
440
|
-
const wait = delay => new Promise(resolve => setTimeout(resolve, delay))
|
|
441
|
-
|
|
442
|
-
/**
|
|
443
|
-
* Keep one repair promise for all callers. Request hooks await the same
|
|
444
|
-
* bounded retry sequence, so a startup registration race cannot leak a
|
|
445
|
-
* stale OpenCode website URL into the next model request.
|
|
446
|
-
*/
|
|
447
|
-
const run = async () => {
|
|
448
|
-
let result = 'retry'
|
|
449
|
-
for (let attempt = 0; attempt <= OPEN_CODE_REPAIR_DELAYS_MS.length; attempt += 1) {
|
|
450
|
-
result = await repairOpenCodeEndpoint(ctx)
|
|
451
|
-
if (result === 'clean' || result === 'repaired') {
|
|
452
|
-
control.retryIndex = 0
|
|
453
|
-
return result
|
|
272
|
+
if ((exec.name === 'model_router_tool_run' || exec.name === 'model_router_team_execute')
|
|
273
|
+
&& exec.arguments?.mode === 'workspace-write') {
|
|
274
|
+
return {
|
|
275
|
+
kind: 'ask',
|
|
276
|
+
reason: 'Official CLI models will use their normal tools in an isolated Git worktree and integrate their patch into the current workspace',
|
|
277
|
+
displayReason: {
|
|
278
|
+
en: 'Allow the official CLI model to use shell, skills, configured MCP and other normal tools in an isolated Git worktree, then apply its patch to this workspace?',
|
|
279
|
+
zh: '允许官方 CLI 模型在独立 Git 工作区使用终端、技能、已配置 MCP 等工具,并将改动补丁应用回当前工作区?',
|
|
280
|
+
},
|
|
454
281
|
}
|
|
455
|
-
if (attempt === OPEN_CODE_REPAIR_DELAYS_MS.length) return result
|
|
456
|
-
control.retryIndex = attempt + 1
|
|
457
|
-
await wait(OPEN_CODE_REPAIR_DELAYS_MS[attempt])
|
|
458
282
|
}
|
|
459
|
-
return
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
const schedule = () => {
|
|
463
|
-
if (control.inFlight !== null) return control.inFlight
|
|
464
|
-
const promise = run().catch(error => {
|
|
465
|
-
ctx.logger?.debug?.(`model-router: OpenCode repair scheduler failed: ${String(error)}`)
|
|
466
|
-
return 'retry'
|
|
467
|
-
})
|
|
468
|
-
control.inFlight = promise
|
|
469
|
-
void promise.finally(() => {
|
|
470
|
-
if (control.inFlight === promise) control.inFlight = null
|
|
471
|
-
})
|
|
472
|
-
return promise
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
return schedule
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
export function apply(ctx) {
|
|
479
|
-
const repairLiangshen = () => reportLiangshenCompatibility(ctx, repairLiangshenPreset())
|
|
480
|
-
repairLiangshen()
|
|
481
|
-
reportRouterSessionCompatibility(ctx, repairRouterSessions())
|
|
482
|
-
|
|
483
|
-
ctx.inject?.(['connection', 'llm', 'webServer'], galCtx => {
|
|
484
|
-
registerGalGameRoutes(galCtx)
|
|
485
|
-
})
|
|
486
|
-
|
|
487
|
-
// Desktop-only package mutation is exposed through an authenticated,
|
|
488
|
-
// fixed-purpose route when the optional native capabilities are present.
|
|
489
|
-
ctx.inject?.(['connection', 'desktopProfiles', 'desktopPnpm', 'webServer'], desktopCtx => {
|
|
490
|
-
registerNpmUpdateRoute(desktopCtx, import.meta.url)
|
|
283
|
+
return decision
|
|
491
284
|
})
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
return next()
|
|
540
|
-
}, { prepend: true })
|
|
541
|
-
|
|
285
|
+
registerOfficialToolModels(ctx, config)
|
|
286
|
+
ctx.tools.register(defineTool({
|
|
287
|
+
name: 'model_router_routes',
|
|
288
|
+
description: 'List provider/model routes registered in the official DeepSeek Harness model directory. Credential and network availability are not verified. No API keys or endpoints are returned.',
|
|
289
|
+
parameters: {},
|
|
290
|
+
output: JSON_OUTPUT,
|
|
291
|
+
async execute(_args, exec) {
|
|
292
|
+
const routes = await discoverConfiguredRoutes(ctx, exec.signal)
|
|
293
|
+
return jsonValue({ routes, count: routes.length, availabilityNotice: '目录记录不证明账号凭据和网络当前可用。' })
|
|
294
|
+
},
|
|
295
|
+
}))
|
|
296
|
+
ctx.tools.register(defineTool({
|
|
297
|
+
name: 'model_router_plan',
|
|
298
|
+
description: 'Recommend configured Harness model routes for a task and optionally produce work packages for official Agent Teams. Costs are local estimates.',
|
|
299
|
+
parameters: {
|
|
300
|
+
task: { type: 'string', required: true, description: 'Task to analyze.' },
|
|
301
|
+
mode: { type: 'string', enum: ['single', 'team'], description: 'Use team to produce Agent Teams work packages.' },
|
|
302
|
+
budgetUsd: { type: 'number', description: 'Optional local estimated cost ceiling in USD.' },
|
|
303
|
+
},
|
|
304
|
+
output: JSON_OUTPUT,
|
|
305
|
+
async execute(args, exec) {
|
|
306
|
+
return jsonValue(await createRoutePlan(ctx, args.task, config, { mode: args.mode, budgetUsd: args.budgetUsd, signal: exec.signal }))
|
|
307
|
+
},
|
|
308
|
+
}))
|
|
309
|
+
ctx.tools.register(defineTool({
|
|
310
|
+
name: 'model_router_consult',
|
|
311
|
+
description: 'Ask one already configured Harness model for an independent opinion. Supply both provider and model for an explicit route, or omit both for a recommended route different from the current model when available.',
|
|
312
|
+
parameters: {
|
|
313
|
+
task: { type: 'string', required: true, description: 'Task or question for the consulted model.' },
|
|
314
|
+
provider: { type: 'string', description: 'Configured provider id; pair with model.' },
|
|
315
|
+
model: { type: 'string', description: 'Configured model id; pair with provider.' },
|
|
316
|
+
outputLimit: { type: 'number', description: 'Maximum returned characters, from 500 to 50000.' },
|
|
317
|
+
},
|
|
318
|
+
output: JSON_OUTPUT,
|
|
319
|
+
async execute(args, exec) {
|
|
320
|
+
const routes = await discoverConfiguredRoutes(ctx, exec.signal)
|
|
321
|
+
if (routes.length === 0) throw new Error('no configured model routes are available')
|
|
322
|
+
const requested = explicitRoute(args, routes)
|
|
323
|
+
const plan = requested ? null : await createRoutePlan(ctx, args.task, config, { signal: exec.signal })
|
|
324
|
+
const route = requested ?? chooseConsultRoute(plan, routes, exec.agent)
|
|
325
|
+
const fallback = valueOf(config, 'maxConsultOutputChars', 12_000)
|
|
326
|
+
const limit = boundedInteger(args.outputLimit, finiteNumber(fallback, 12_000), 500, 50_000)
|
|
327
|
+
return jsonValue(await consultConfiguredModel(ctx, route, args.task, limit, exec.signal))
|
|
328
|
+
},
|
|
329
|
+
}))
|
|
542
330
|
ctx.commands.register({
|
|
543
331
|
name: 'router',
|
|
544
|
-
description: '
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
const state = stateFor(agent)
|
|
554
|
-
const value = String(rawInput ?? '').trim().toLowerCase()
|
|
555
|
-
if (value === 'mode single' || value === 'single') {
|
|
556
|
-
state.mode = 'single'
|
|
557
|
-
if (state.plan !== null) state.plan = { ...state.plan, mode: 'single' }
|
|
558
|
-
return { kind: 'success', text: 'Model Router 已切换到单独会话:保留你在原生模型选择器中的选择。' }
|
|
559
|
-
}
|
|
560
|
-
if (value === 'mode collective' || value === 'collective') {
|
|
561
|
-
state.mode = 'collective'
|
|
562
|
-
if (state.plan !== null) state.plan = { ...state.plan, mode: 'collective' }
|
|
563
|
-
return { kind: 'success', text: 'Model Router 已切换到集体合作:下一条问题将按复杂度、专长、成本和延迟自动分配。' }
|
|
564
|
-
}
|
|
565
|
-
if (value === 'plan' || value === '') {
|
|
566
|
-
return { kind: 'success', text: state.plan === null ? '还没有可展示的路由方案。' : JSON.stringify(state.plan) }
|
|
332
|
+
description: 'Show an official-model route recommendation for a task.',
|
|
333
|
+
input: { hint: 'Describe the task to plan' },
|
|
334
|
+
async handler({ rawInput, signal }) {
|
|
335
|
+
if (!text(rawInput)) return { kind: 'error', text: '用法:/router <需要规划的任务>' }
|
|
336
|
+
try {
|
|
337
|
+
const plan = await createRoutePlan(ctx, rawInput, config, { signal })
|
|
338
|
+
return { kind: 'success', text: commandText(plan) }
|
|
339
|
+
} catch (error) {
|
|
340
|
+
return { kind: 'error', text: `无法生成路由计划:${errorText(error)}` }
|
|
567
341
|
}
|
|
568
|
-
|
|
569
|
-
|
|
342
|
+
},
|
|
343
|
+
})
|
|
344
|
+
ctx.commands.register({
|
|
345
|
+
name: 'tools',
|
|
346
|
+
description: 'Show official model CLI install status, or install one registry tool.',
|
|
347
|
+
input: { hint: '留空查看状态;或输入 install/cancel <工具id>' },
|
|
348
|
+
async handler({ rawInput, signal }) {
|
|
349
|
+
const input = text(rawInput)
|
|
350
|
+
const installMatch = input.match(/^install\s+([A-Za-z0-9_-]+)$/i)
|
|
351
|
+
const cancelMatch = input.match(/^cancel\s+([A-Za-z0-9_-]+)$/i)
|
|
352
|
+
if (cancelMatch) {
|
|
353
|
+
try { return { kind: 'success', text: `已请求取消 ${cancelInstall(cancelMatch[1]).tool} 的安装;请使用 /tools 查看最新状态。` } }
|
|
354
|
+
catch (error) { return { kind: 'error', text: `无法取消安装:${errorText(error)}` } }
|
|
570
355
|
}
|
|
571
|
-
if (
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
356
|
+
if (!installMatch) {
|
|
357
|
+
if (input) return { kind: 'error', text: '用法:/tools 查看状态,或 /tools install/cancel <工具id>' }
|
|
358
|
+
try {
|
|
359
|
+
const probes = await probeAllTools({ fresh: true })
|
|
360
|
+
const lines = probes.map(probe => {
|
|
361
|
+
const tool = getOfficialTool(probe.id)
|
|
362
|
+
const command = installCommandLine(tool)
|
|
363
|
+
const status = probe.status === 'installed'
|
|
364
|
+
? `已安装 ${probe.version ?? ''}`
|
|
365
|
+
: probe.status === 'unsupported'
|
|
366
|
+
? '不支持一键安装'
|
|
367
|
+
: '未安装'
|
|
368
|
+
return `• ${tool.label}(${probe.id}):${status}${command && probe.status === 'not-installed' ? `\n 安装:/tools install ${probe.id}(即 ${command})` : ''}`
|
|
369
|
+
})
|
|
370
|
+
return { kind: 'success', text: `官方工具状态:\n${lines.join('\n')}` }
|
|
371
|
+
} catch (error) {
|
|
372
|
+
return { kind: 'error', text: `探测失败:${errorText(error)}` }
|
|
373
|
+
}
|
|
577
374
|
}
|
|
578
|
-
|
|
579
|
-
|
|
375
|
+
try {
|
|
376
|
+
const job = startInstall(installMatch[1])
|
|
377
|
+
const tool = getOfficialTool(installMatch[1])
|
|
378
|
+
const settled = await waitForInstall(job.tool, signal)
|
|
379
|
+
if (settled.status === 'succeeded') {
|
|
380
|
+
const probe = await probeToolWith(tool, defaultRunner)
|
|
381
|
+
return { kind: 'success', text: `${tool.label} 安装完成${probe.version ? `,探测版本 ${probe.version}` : ''}。` }
|
|
382
|
+
}
|
|
383
|
+
if (settled.status === 'installer-opened') {
|
|
384
|
+
return { kind: 'success', text: `${tool.label} 官方安装器已验证并打开。请在安装窗口选择非 C 盘目录并完成安装,然后运行 /tools 重新检测;当前尚未确认安装完成。` }
|
|
385
|
+
}
|
|
386
|
+
return { kind: 'error', text: `${tool.label} 安装失败:${settled.error ?? '未知原因'}\n${settled.outputTail.slice(-6).join('\n')}` }
|
|
387
|
+
} catch (error) {
|
|
388
|
+
return { kind: 'error', text: `无法开始安装:${errorText(error)}` }
|
|
580
389
|
}
|
|
581
|
-
return { kind: 'error', text: '用法:/router mode collective、/router mode single、/router plan、/router safety、/router web 或 /router watcher' }
|
|
582
390
|
},
|
|
583
391
|
})
|
|
392
|
+
}
|
|
584
393
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
394
|
+
/** Poll an install job until it settles or the signal aborts. */
|
|
395
|
+
async function waitForInstall(toolId, signal) {
|
|
396
|
+
for (;;) {
|
|
397
|
+
if (signal?.aborted) {
|
|
398
|
+
try { cancelInstall(toolId) } catch { /* install may already have finished */ }
|
|
399
|
+
throw new Error('已请求取消安装;请重新检测实际安装状态。')
|
|
589
400
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
void scheduleOpenCodeRepair()
|
|
597
|
-
ctx.on('settings/updated', (namespace) => {
|
|
598
|
-
if (String(namespace) === 'dsh-liangshen') queueMicrotask(repairLiangshen)
|
|
599
|
-
if (String(namespace) === LLM_SETTINGS_NAMESPACE) void scheduleOpenCodeRepair()
|
|
600
|
-
})
|
|
601
|
-
ctx.on('settings/document-updated', (namespace) => {
|
|
602
|
-
if (String(namespace) === LLM_SETTINGS_NAMESPACE) void scheduleOpenCodeRepair()
|
|
603
|
-
})
|
|
401
|
+
const job = installStatus(toolId)
|
|
402
|
+
if (!job) throw new Error('安装任务丢失。')
|
|
403
|
+
if (job.status !== 'running') return job
|
|
404
|
+
await new Promise(resolve => setTimeout(resolve, 1_500))
|
|
405
|
+
}
|
|
406
|
+
}
|
|
604
407
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
state.hasImageBlocks ||= messages.some(message => contentHasImage(message?.content))
|
|
623
|
-
if (state.mode !== 'collective' || signal?.aborted) {
|
|
624
|
-
if (state.taskText === '') state.taskText = inputText(messages)
|
|
625
|
-
const proposed = await next()
|
|
626
|
-
if (signal?.aborted || proposed === undefined || proposed === null || proposed.kind !== 'enter') return proposed
|
|
627
|
-
const hasPersona = proposed.messages.some(message => message?.content?.some(block => isPersonaPrompt(block?.text)))
|
|
628
|
-
if (hasPersona) state.personaInjected = true
|
|
629
|
-
const personaContext = state.personaInjected ? null : personaMessage(state, agent, Number.isFinite(Number(step)) ? Number(step) : 1)
|
|
630
|
-
if (personaContext === null) return proposed
|
|
631
|
-
if (personaContext !== null) state.personaInjected = true
|
|
632
|
-
return { ...proposed, messages: [...proposed.messages, personaContext] }
|
|
633
|
-
}
|
|
634
|
-
const available = await discover(ctx, state)
|
|
635
|
-
if (signal?.aborted) return next()
|
|
636
|
-
if (state.plan === null || state.plan.mode !== state.mode) {
|
|
637
|
-
await routerSettingsPromise
|
|
638
|
-
state.taskText = inputText(messages)
|
|
639
|
-
const liveBench = await liveBenchFor(ctx, state)
|
|
640
|
-
const readyRoutes = available.filter(route => !routeTemporarilyUnavailable(state, routeKey(route.provider, route.model)))
|
|
641
|
-
const routable = readyRoutes.length > 0 ? readyRoutes : available
|
|
642
|
-
const plan = buildPlan({
|
|
643
|
-
text: state.taskText,
|
|
644
|
-
available: routable,
|
|
645
|
-
mode: state.mode,
|
|
646
|
-
pricing: routerSettings.pricing,
|
|
647
|
-
liveBench,
|
|
648
|
-
liveBenchError: state.liveBenchError,
|
|
649
|
-
budgetUsd: routerSettings.budgetUsd,
|
|
650
|
-
cacheReadRatio: routerSettings.cacheReadRatio,
|
|
651
|
-
cacheWriteRatio: routerSettings.cacheWriteRatio,
|
|
408
|
+
/** Register the model-facing official-tool probes and installer. */
|
|
409
|
+
function registerOfficialToolModels(ctx, config) {
|
|
410
|
+
ctx.tools.register(defineTool({
|
|
411
|
+
name: 'model_router_tools',
|
|
412
|
+
description: 'Probe the fixed registry of official model tools (Kimi Code, Claude Code, Codex, MiniMax Code, MiMo Code, Grok Build, ZCode) and report which are installed with their versions. Never reads credentials.',
|
|
413
|
+
parameters: {},
|
|
414
|
+
output: JSON_OUTPUT,
|
|
415
|
+
async execute(_args, exec) {
|
|
416
|
+
throwIfAborted(exec.signal)
|
|
417
|
+
const probes = await probeAllTools()
|
|
418
|
+
return jsonValue({
|
|
419
|
+
tools: probes,
|
|
420
|
+
executionCapabilities: officialToolExecutionCapabilities(),
|
|
421
|
+
executionReadiness: await Promise.all(probes.map(probe => probe.installed
|
|
422
|
+
? officialToolReadiness(probe.id)
|
|
423
|
+
: Promise.resolve({ id: probe.id, ready: false, reason: 'CLI 尚未安装或版本检测失败。' }))),
|
|
424
|
+
installHint: '未安装的工具可由 model_router_tool_install 按注册表固定命令安装;版本与包名不接受自定义。',
|
|
652
425
|
})
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
426
|
+
},
|
|
427
|
+
}))
|
|
428
|
+
ctx.tools.register(defineTool({
|
|
429
|
+
name: 'model_router_tool_install',
|
|
430
|
+
description: 'Install one official model tool by registry id using its pinned official source. ZCode opens a verified interactive desktop installer with a directory picker. Only registry ids are accepted; arbitrary packages or executables are refused.',
|
|
431
|
+
parameters: {
|
|
432
|
+
tool: { type: 'string', required: true, description: 'Registry tool id, e.g. kimi-code.' },
|
|
433
|
+
},
|
|
434
|
+
output: JSON_OUTPUT,
|
|
435
|
+
async execute(args, exec) {
|
|
436
|
+
const job = startInstall(args.tool)
|
|
437
|
+
const settled = await waitForInstall(job.tool, exec.signal)
|
|
438
|
+
const tool = getOfficialTool(args.tool)
|
|
439
|
+
const probe = settled.status === 'succeeded'
|
|
440
|
+
? await probeToolWith(tool, defaultRunner)
|
|
665
441
|
: null
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
let target = state.plan.selected
|
|
700
|
-
if (state.plan.complexity?.band === 'complex' && state.plan.subtasks?.length > 1) {
|
|
701
|
-
const assignment = state.plan.subtasks[(Math.max(1, step) - 1) % state.plan.subtasks.length]
|
|
702
|
-
const assignedRoute = state.available.find(route => route.provider === assignment?.recommendedProvider && route.model === assignment?.recommended)
|
|
703
|
-
?? state.available.find(route => route.model === assignment?.recommended)
|
|
704
|
-
if (assignedRoute !== undefined) {
|
|
705
|
-
target = { provider: assignedRoute.provider, model: assignedRoute.model, reasoningEffort: assignment?.recommendedReasoningEffort, estimatedCost: target.estimatedCost }
|
|
706
|
-
}
|
|
707
|
-
// The final subtask is the public answer synthesis. Prefer the user's
|
|
708
|
-
// requested DeepSeek V4 Pro when it is actually available; otherwise the
|
|
709
|
-
// deterministic plan's fallback remains in force and is shown in the UI.
|
|
710
|
-
const synthesis = state.plan.synthesizer
|
|
711
|
-
if (assignment?.purpose === 'synthesis' && synthesis?.provider && synthesis?.model) {
|
|
712
|
-
const synthesisRoute = state.available.find(route => route.provider === synthesis.provider && route.model === synthesis.model)
|
|
713
|
-
if (synthesisRoute !== undefined) {
|
|
714
|
-
target = { provider: synthesisRoute.provider, model: synthesisRoute.model, reasoningEffort: synthesis.reasoningEffort, estimatedCost: target.estimatedCost }
|
|
715
|
-
}
|
|
442
|
+
return jsonValue({
|
|
443
|
+
...settled,
|
|
444
|
+
postInstallProbe: probe,
|
|
445
|
+
notice: settled.status === 'installer-opened'
|
|
446
|
+
? '已打开 ZCode 官方安装窗口,请选择安装目录并完成安装,之后重新检测;此状态不代表安装完成。'
|
|
447
|
+
: '安装命令完全来自服务端注册表;实际版本以探测横幅为准。',
|
|
448
|
+
})
|
|
449
|
+
},
|
|
450
|
+
}))
|
|
451
|
+
ctx.tools.register(defineTool({
|
|
452
|
+
name: 'model_router_tool_run',
|
|
453
|
+
description: 'Run one supported official model CLI in the current Harness session workspace. Claude, Codex, MiMo and Grok support read-only; Kimi, MiniMax and ZCode require workspace-write. Write mode needs approval and a clean Git repository. Optional provider/model must match a configured Harness route and selected vendor; cliModel can specify that vendor CLI’s own configured model name. Without cliModel, Kimi, MiniMax, MiMo, Grok and ZCode use the CLI default.',
|
|
454
|
+
parameters: {
|
|
455
|
+
tool: { type: 'string', required: true, description: 'Fixed registry tool id, e.g. claude-code or codex.' },
|
|
456
|
+
task: { type: 'string', required: true, description: 'Concrete task for the official CLI model.' },
|
|
457
|
+
provider: { type: 'string', description: 'Optional configured provider, paired with model.' },
|
|
458
|
+
model: { type: 'string', description: 'Optional model ID from the Harness directory, paired with provider. This ID is advisory for CLIs except Claude/Codex.' },
|
|
459
|
+
cliModel: { type: 'string', description: 'Optional model name already configured in this vendor CLI; requires provider and model. MiniMax/MiMo require provider/model format. ZCode 3.14.3 cannot switch models per call.' },
|
|
460
|
+
mode: { type: 'string', enum: ['read-only', 'workspace-write'], description: 'Default is read-only. Kimi, MiniMax and ZCode require workspace-write. Write mode requires official approval and a clean Git repository.' },
|
|
461
|
+
},
|
|
462
|
+
output: JSON_OUTPUT,
|
|
463
|
+
async execute(args, exec) {
|
|
464
|
+
const { cwd, root, sandboxMode } = await sessionWorkspace(ctx, exec)
|
|
465
|
+
const mode = args.mode === 'workspace-write' ? 'workspace-write' : 'read-only'
|
|
466
|
+
if (mode === 'workspace-write' && sandboxMode === 'read-only') throw new Error('当前 Harness 会话为只读模式,不能请求可编辑 CLI 执行')
|
|
467
|
+
let modelId = null
|
|
468
|
+
if (text(args.provider) || text(args.model)) {
|
|
469
|
+
if (!text(args.provider) || !text(args.model)) throw new Error('provider 和 model 必须同时提供')
|
|
470
|
+
const tool = toolForProvider(args.provider)
|
|
471
|
+
if (tool?.id !== args.tool) throw new Error('所选模型供应商与官方 CLI 工具不匹配')
|
|
472
|
+
const routes = await discoverConfiguredRoutes(ctx, exec.signal)
|
|
473
|
+
if (!routes.some(route => route.provider === args.provider && route.model === args.model)) throw new Error('所选 provider/model 不在官方模型目录中')
|
|
474
|
+
modelId = args.tool === 'claude-code' || args.tool === 'codex' ? args.model : null
|
|
716
475
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
target = { provider: fallback.provider, model: fallback.model, reasoningEffort: fallback.reasoningEffort, estimatedCost: target.estimatedCost }
|
|
476
|
+
if (text(args.cliModel)) {
|
|
477
|
+
if (!text(args.provider) || !text(args.model)) throw new Error('cliModel 需要同时提供已配置的 provider 和 model 路线')
|
|
478
|
+
if (args.tool === 'zcode') throw new Error('ZCode 3.14.3 不支持在单次调用中指定 CLI 模型')
|
|
479
|
+
modelId = args.cliModel
|
|
722
480
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
if (fallback === null) return next()
|
|
769
|
-
if (state.plan !== null) {
|
|
770
|
-
const plan = state.plan
|
|
771
|
-
const failedStage = plan.subtasks?.[Math.max(0, state.lastStep - 1)]
|
|
772
|
-
const failedStageIndex = Math.max(0, state.lastStep - 1)
|
|
773
|
-
const isSynthesisFailure = failedStage?.purpose === 'synthesis'
|
|
774
|
-
const subtasks = Array.isArray(plan.subtasks)
|
|
775
|
-
? plan.subtasks.map((task, index) => index === failedStageIndex
|
|
776
|
-
? { ...task, recommendedProvider: fallback.provider, recommended: fallback.model, recommendedReasoningEffort: fallback.reasoningEffort }
|
|
777
|
-
: task)
|
|
778
|
-
: plan.subtasks
|
|
779
|
-
state.plan = {
|
|
780
|
-
...plan,
|
|
781
|
-
...(plan.selected === null ? {} : { selected: { ...plan.selected, provider: fallback.provider, model: fallback.model, reasoningEffort: fallback.reasoningEffort } }),
|
|
782
|
-
subtasks,
|
|
783
|
-
...(isSynthesisFailure ? {
|
|
784
|
-
synthesizer: { provider: fallback.provider, model: fallback.model, reasoningEffort: fallback.reasoningEffort },
|
|
785
|
-
} : {}),
|
|
481
|
+
const result = await runOfficialTask({ toolId: args.tool, task: args.task, modelId, workspace: cwd, allowedRoot: root, mode, signal: exec.signal, sandbox: ctx.sandbox })
|
|
482
|
+
return jsonValue({ ...result,
|
|
483
|
+
...(!modelId && text(args.model) ? { modelNotice: result.modelNotice
|
|
484
|
+
?? 'Harness 模型 ID 未经此厂商 CLI 验证;本次使用厂商 CLI 已配置的默认模型。' } : {}),
|
|
485
|
+
})
|
|
486
|
+
},
|
|
487
|
+
}))
|
|
488
|
+
ctx.tools.register(defineTool({
|
|
489
|
+
name: 'model_router_team_execute',
|
|
490
|
+
description: 'Plan a complex task into dependent work packages, route among configured providers with ready official CLIs, and run each package sequentially. Claude/Codex request the planned model ID; other CLIs use their configured default unless cliModelsJson supplies exact CLI names. Editable runs use one isolated Git worktree and integrate source changes after CLI success. Confirm the actual model from vendor records.',
|
|
491
|
+
parameters: {
|
|
492
|
+
task: { type: 'string', required: true, description: 'Full task to plan, distribute and execute.' },
|
|
493
|
+
mode: { type: 'string', enum: ['read-only', 'workspace-write'], description: 'Default read-only; workspace-write needs a clean Git repository and approval.' },
|
|
494
|
+
budgetUsd: { type: 'number', description: 'Estimated planning ceiling only, not a vendor billing limit.' },
|
|
495
|
+
cliModelsJson: { type: 'string', description: 'Optional JSON object mapping official tool IDs or work package IDs to exact model names configured in those CLIs. MiniMax/MiMo require provider/model; ZCode 3.14.3 cannot switch per call.' },
|
|
496
|
+
},
|
|
497
|
+
output: JSON_OUTPUT,
|
|
498
|
+
async execute(args, exec) {
|
|
499
|
+
const { cwd, root, sandboxMode } = await sessionWorkspace(ctx, exec)
|
|
500
|
+
const mode = args.mode === 'workspace-write' ? 'workspace-write' : 'read-only'
|
|
501
|
+
if (mode === 'workspace-write' && sandboxMode === 'read-only') throw new Error('当前 Harness 会话为只读模式,不能请求可编辑团队执行')
|
|
502
|
+
const [routes, installed] = await Promise.all([discoverConfiguredRoutes(ctx, exec.signal), installedToolIds()])
|
|
503
|
+
const readiness = await Promise.all(installed.map(id => officialToolReadiness(id, cwd)))
|
|
504
|
+
const supported = new Set(readiness.filter(item => item.ready).map(item => item.id))
|
|
505
|
+
const capabilities = new Map(officialToolExecutionCapabilities().map(item => [item.id, item]))
|
|
506
|
+
const executableRoutes = routes.filter(route => {
|
|
507
|
+
const tool = toolForProvider(route.provider)
|
|
508
|
+
return tool && installed.includes(tool.id) && supported.has(tool.id)
|
|
509
|
+
&& capabilities.get(tool.id)?.modes?.includes(mode)
|
|
510
|
+
})
|
|
511
|
+
if (executableRoutes.length === 0) return jsonValue({ status: 'blocked',
|
|
512
|
+
reason: `官方模型目录中没有同时满足已配置路线、已安装 CLI、托管执行适配器与 ${mode} 模式的供应商;Kimi、MiniMax、ZCode 仅支持经审批的 workspace-write。`,
|
|
513
|
+
installed, executionCapabilities: officialToolExecutionCapabilities(), executionReadiness: readiness })
|
|
514
|
+
const configuredBudget = valueOf(config, 'budgetUsd', DEFAULT_ROUTER_SETTINGS.budgetUsd)
|
|
515
|
+
const budgetUsd = Math.max(0, finiteNumber(args.budgetUsd, finiteNumber(configuredBudget, 0)))
|
|
516
|
+
const plan = createPlanFromRoutes(args.task, executableRoutes, {
|
|
517
|
+
mode: 'team', budgetUsd, installedToolIds: installed,
|
|
518
|
+
runnableToolIds: installed.filter(id => supported.has(id)),
|
|
519
|
+
})
|
|
520
|
+
const bindingsText = text(args.cliModelsJson)
|
|
521
|
+
if (bindingsText.length > 4_000) throw new Error('cliModelsJson 超过 4000 字符上限')
|
|
522
|
+
let cliModels = null
|
|
523
|
+
if (bindingsText) {
|
|
524
|
+
try { cliModels = JSON.parse(bindingsText) }
|
|
525
|
+
catch { throw new Error('cliModelsJson 不是有效的 JSON 对象') }
|
|
786
526
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
})
|
|
527
|
+
const execution = await runOfficialTeam({ plan, task: args.task, workspace: cwd,
|
|
528
|
+
allowedRoot: root, mode, installedIds: installed, cliModels, signal: exec.signal, sandbox: ctx.sandbox })
|
|
529
|
+
return jsonValue({ plan, execution,
|
|
530
|
+
modelNotice: '团队向 Claude/Codex 请求 Harness 推荐模型 ID;其他厂商默认使用 CLI 已配置模型。cliModelsJson 可按工具或工作包指定准确 CLI 模型名。实际模型须以各厂商记录核对。',
|
|
531
|
+
billingNotice: 'budgetUsd 仅影响估算与路由,无法限制官方 CLI 账号实际费用。' })
|
|
532
|
+
},
|
|
533
|
+
}))
|
|
795
534
|
}
|