@ljwei-stak/model-router-galgame 0.4.32 → 0.9.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 +5438 -4
- package/.dsh-plugin/index.mjs +498 -727
- package/.dsh-plugin/official-tools-remote-service.mjs +152 -0
- package/.dsh-plugin/shared/harness-plan.mjs +134 -0
- package/.dsh-plugin/shared/model-profiles.mjs +101 -0
- package/.dsh-plugin/shared/official-team-runtime.mjs +349 -0
- package/.dsh-plugin/shared/official-tool-executor.mjs +780 -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 +640 -0
- package/.dsh-plugin/shared/router.mjs +284 -73
- 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 +132 -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,566 @@
|
|
|
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 { applyModelProfiles, parseModelProfilesJson } from './shared/model-profiles.mjs'
|
|
6
|
+
import { registerOfficialToolsRemote } from './official-tools-remote-service.mjs'
|
|
1
7
|
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'
|
|
8
|
+
getOfficialTool,
|
|
9
|
+
installCommandLine,
|
|
10
|
+
toolForProvider,
|
|
11
|
+
} from './shared/official-tool-registry.mjs'
|
|
12
|
+
import { officialToolExecutionCapabilities, officialToolReadiness } from './shared/official-tool-executor.mjs'
|
|
13
|
+
import { runOfficialTask, runOfficialTeam, sessionWorkspace } from './shared/official-team-runtime.mjs'
|
|
15
14
|
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'
|
|
36
|
-
|
|
37
|
-
let settingsRuntimePromise
|
|
38
|
-
let routerSettings = { ...DEFAULT_ROUTER_SETTINGS }
|
|
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
|
-
}
|
|
52
|
-
|
|
53
|
-
function routerSettingsSchema(z) {
|
|
54
|
-
const price = z.object({
|
|
55
|
-
input: z.number().min(0).default(0),
|
|
56
|
-
output: z.number().min(0).default(0),
|
|
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
|
-
}
|
|
70
|
-
|
|
71
|
-
function invalidateRouterPlans() {
|
|
72
|
-
for (const state of allStates) {
|
|
73
|
-
state.plan = null
|
|
74
|
-
state.liveBenchPromise = null
|
|
75
|
-
state.liveBenchError = null
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async function registerRouterSettings(ctx) {
|
|
80
|
-
const settings = settingsService(ctx)
|
|
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
|
-
}
|
|
99
|
-
}
|
|
15
|
+
probeAllTools,
|
|
16
|
+
probeToolWith,
|
|
17
|
+
startInstall,
|
|
18
|
+
cancelInstall,
|
|
19
|
+
installStatus,
|
|
20
|
+
installedToolIds,
|
|
21
|
+
defaultRunner,
|
|
22
|
+
} from './shared/official-tools-runtime.mjs'
|
|
100
23
|
|
|
101
24
|
export const name = 'model-router-galgame'
|
|
102
|
-
export const inject = ['commands', 'llm', '
|
|
103
|
-
|
|
104
|
-
const LLM_SETTINGS_NAMESPACE = 'llm-pi-ai'
|
|
105
|
-
const OPEN_CODE_REPAIR_DELAYS_MS = Object.freeze([25, 100, 250, 500, 1000, 2000])
|
|
25
|
+
export const inject = ['commands', 'llm', 'tools', 'typert', 'sandboxPolicy', 'sandbox']
|
|
106
26
|
|
|
107
|
-
const
|
|
108
|
-
|
|
27
|
+
export const Config = z.object({
|
|
28
|
+
budgetUsd: z.number().min(0).max(1_000_000).default(DEFAULT_ROUTER_SETTINGS.budgetUsd).volatile(),
|
|
29
|
+
maxConsultOutputChars: z.number().step(1).min(500).max(50_000).default(12_000).volatile(),
|
|
30
|
+
modelProfilesJson: z.string().max(32_000).default('[]').volatile(),
|
|
31
|
+
})
|
|
109
32
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
33
|
+
const JSON_OUTPUT = {
|
|
34
|
+
schema: { type: 'json' },
|
|
35
|
+
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
137
36
|
}
|
|
138
37
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
state.directoryPromise = (async () => {
|
|
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
|
|
38
|
+
function jsonValue(value) {
|
|
39
|
+
return JSON.parse(JSON.stringify(value))
|
|
199
40
|
}
|
|
200
41
|
|
|
201
|
-
function
|
|
202
|
-
|
|
42
|
+
function valueOf(config, key, fallback) {
|
|
43
|
+
const value = config?.[key]
|
|
44
|
+
return value !== undefined && typeof value?.get === 'function' ? value.get() : value ?? fallback
|
|
203
45
|
}
|
|
204
46
|
|
|
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
|
|
47
|
+
function finiteNumber(value, fallback) {
|
|
48
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback
|
|
231
49
|
}
|
|
232
50
|
|
|
233
|
-
function
|
|
234
|
-
|
|
235
|
-
return `model-router-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
|
51
|
+
function boundedInteger(value, fallback, minimum, maximum) {
|
|
52
|
+
return Math.min(maximum, Math.max(minimum, Math.floor(finiteNumber(value, fallback))))
|
|
236
53
|
}
|
|
237
54
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const text = collaborationInstruction(plan, step)
|
|
241
|
-
if (text === '') return null
|
|
242
|
-
return {
|
|
243
|
-
id: newMessageId(),
|
|
244
|
-
role: 'user',
|
|
245
|
-
content: [{ type: 'text', text }],
|
|
246
|
-
source: { kind: 'plugin', plugin: name, form: 'relay' },
|
|
247
|
-
}
|
|
55
|
+
function text(value) {
|
|
56
|
+
return typeof value === 'string' ? value.trim() : ''
|
|
248
57
|
}
|
|
249
58
|
|
|
250
|
-
function
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
return {
|
|
254
|
-
id: newMessageId(),
|
|
255
|
-
role: 'user',
|
|
256
|
-
content: [{ type: 'text', text }],
|
|
257
|
-
source: { kind: 'plugin', plugin: name, form: 'instructions' },
|
|
258
|
-
}
|
|
59
|
+
function errorText(error) {
|
|
60
|
+
if (error instanceof Error && error.message) return error.message
|
|
61
|
+
try { return String(error) } catch { return 'unknown error' }
|
|
259
62
|
}
|
|
260
63
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
* message so the collaboration stages and their audit records remain free of
|
|
264
|
-
* stylistic instructions.
|
|
265
|
-
*/
|
|
266
|
-
function personaMessage(state, agent, step) {
|
|
267
|
-
const plan = state.plan
|
|
268
|
-
const isCollective = state.mode === 'collective'
|
|
269
|
-
const stage = isCollective ? collaborationStage(plan, step) : null
|
|
270
|
-
const finalStage = !isCollective || stage === null || stage.purpose === 'synthesis'
|
|
271
|
-
|| plan?.complexity?.band !== 'complex'
|
|
272
|
-
if (!finalStage) return null
|
|
273
|
-
let route = state.lastTarget
|
|
274
|
-
if (isCollective && plan !== null && plan !== undefined) {
|
|
275
|
-
const task = plan.complexity?.band === 'complex' ? plan.subtasks?.[Math.max(1, Number(step) || 1) - 1] : plan.subtasks?.[0]
|
|
276
|
-
if (task?.recommended) route = { provider: task.recommendedProvider, model: task.recommended }
|
|
277
|
-
if (task?.purpose === 'synthesis' && plan.synthesizer?.model) route = plan.synthesizer
|
|
278
|
-
if (route?.model === undefined || route?.model === '') route = plan.selected
|
|
279
|
-
}
|
|
280
|
-
if (!isCollective) {
|
|
281
|
-
const header = typeof agent.session?.requestHeader === 'function' ? agent.session.requestHeader() : null
|
|
282
|
-
route = route ?? header?.config ?? agent.options
|
|
283
|
-
}
|
|
284
|
-
const text = buildPersonaPrompt({
|
|
285
|
-
provider: route?.provider ?? '',
|
|
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' },
|
|
296
|
-
}
|
|
64
|
+
function throwIfAborted(signal) {
|
|
65
|
+
if (signal?.aborted) throw signal.reason instanceof Error ? signal.reason : new Error('operation aborted')
|
|
297
66
|
}
|
|
298
67
|
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
return {
|
|
320
|
-
id: newMessageId(),
|
|
321
|
-
role: 'user',
|
|
322
|
-
content: [{ type: 'text', text }],
|
|
323
|
-
source: { kind: 'plugin', plugin: name, form: 'notice', summary: '路由分析与任务分配' },
|
|
324
|
-
}
|
|
68
|
+
function routeKey(route) {
|
|
69
|
+
return `${route.provider}\u0000${route.model}`
|
|
325
70
|
}
|
|
326
71
|
|
|
327
|
-
function
|
|
328
|
-
const
|
|
329
|
-
return
|
|
72
|
+
function configuredRoutesWithProfiles(routes, config) {
|
|
73
|
+
const profiles = parseModelProfilesJson(valueOf(config, 'modelProfilesJson', '[]'))
|
|
74
|
+
return applyModelProfiles(routes, profiles)
|
|
330
75
|
}
|
|
331
76
|
|
|
332
|
-
function
|
|
333
|
-
return
|
|
334
|
-
&& Array.isArray(plan.subtasks)
|
|
335
|
-
&& plan.subtasks.length >= 3
|
|
336
|
-
&& plan.selected !== null
|
|
337
|
-
&& plan.selected !== undefined
|
|
338
|
-
&& Array.isArray(available)
|
|
339
|
-
&& available.length > 0
|
|
77
|
+
function uniqueStrings(values) {
|
|
78
|
+
return [...new Set(values.map(text).filter(Boolean))]
|
|
340
79
|
}
|
|
341
80
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
81
|
+
/** Read only exact provider/model routes published by the official LLM service. */
|
|
82
|
+
export async function discoverConfiguredRoutes(ctx, signal) {
|
|
83
|
+
throwIfAborted(signal)
|
|
84
|
+
let providers
|
|
85
|
+
try { providers = ctx.llm.listProviders() } catch { return [] }
|
|
86
|
+
const routes = new Map()
|
|
87
|
+
for (const entry of Array.isArray(providers) ? providers : []) {
|
|
88
|
+
const provider = text(typeof entry === 'string' ? entry : entry?.id)
|
|
89
|
+
if (!provider) continue
|
|
90
|
+
throwIfAborted(signal)
|
|
91
|
+
let models
|
|
92
|
+
try { models = await ctx.llm.listModels(provider) } catch {
|
|
93
|
+
throwIfAborted(signal)
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
for (const listed of Array.isArray(models) ? models : []) {
|
|
97
|
+
const model = text(listed?.id ?? listed?.model)
|
|
98
|
+
if (!model) continue
|
|
99
|
+
throwIfAborted(signal)
|
|
100
|
+
let resolved = listed
|
|
101
|
+
try { resolved = await ctx.llm.resolveModelInfo(provider, model, signal) } catch {
|
|
102
|
+
throwIfAborted(signal)
|
|
103
|
+
}
|
|
104
|
+
const reasoning = resolved?.reasoning
|
|
105
|
+
const route = {
|
|
106
|
+
provider,
|
|
107
|
+
model,
|
|
108
|
+
name: text(resolved?.name ?? listed?.name) || model,
|
|
109
|
+
inputModalities: uniqueStrings(Array.isArray(resolved?.inputModalities)
|
|
110
|
+
? resolved.inputModalities : Array.isArray(listed?.inputModalities) ? listed.inputModalities : []),
|
|
111
|
+
reasoningKnown: reasoning !== undefined && reasoning !== null,
|
|
112
|
+
reasoningEfforts: uniqueStrings(Array.isArray(reasoning?.efforts) ? reasoning.efforts.map(item => item?.id ?? item) : []),
|
|
113
|
+
...text(reasoning?.defaultEffort) ? { defaultReasoningEffort: text(reasoning.defaultEffort) } : {},
|
|
114
|
+
}
|
|
115
|
+
routes.set(routeKey(route), route)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return [...routes.values()].sort((left, right) => routeKey(left).localeCompare(routeKey(right)))
|
|
358
119
|
}
|
|
359
120
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
const
|
|
363
|
-
if (
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
121
|
+
/** Produce a route recommendation and work packages compatible with official Agent Teams. */
|
|
122
|
+
export async function createRoutePlan(ctx, task, config = {}, options = {}) {
|
|
123
|
+
const taskText = text(task)
|
|
124
|
+
if (!taskText) throw new Error('task must contain text')
|
|
125
|
+
const mode = options.mode === 'team' ? 'team' : 'single'
|
|
126
|
+
const configuredBudget = valueOf(config, 'budgetUsd', DEFAULT_ROUTER_SETTINGS.budgetUsd)
|
|
127
|
+
const budgetUsd = Math.max(0, finiteNumber(options.budgetUsd, finiteNumber(configuredBudget, 0)))
|
|
128
|
+
const [discoveredRoutes, installed] = await Promise.all([
|
|
129
|
+
discoverConfiguredRoutes(ctx, options.signal),
|
|
130
|
+
options.skipToolProbe === true
|
|
131
|
+
? Promise.resolve(Array.isArray(options.installedToolIds) ? options.installedToolIds : [])
|
|
132
|
+
: installedToolIds(),
|
|
133
|
+
])
|
|
134
|
+
const availableRoutes = configuredRoutesWithProfiles(discoveredRoutes, config)
|
|
135
|
+
const readiness = options.skipToolProbe === true ? []
|
|
136
|
+
: await Promise.all(installed.map(id => officialToolReadiness(id, options.workspace ?? process.cwd())))
|
|
137
|
+
const executable = new Set(Array.isArray(options.runnableToolIds)
|
|
138
|
+
? options.runnableToolIds : readiness.filter(item => item.ready).map(item => item.id))
|
|
139
|
+
return createPlanFromRoutes(taskText, availableRoutes, {
|
|
140
|
+
mode, budgetUsd, installedToolIds: installed,
|
|
141
|
+
runnableToolIds: installed.filter(id => executable.has(id)),
|
|
142
|
+
})
|
|
368
143
|
}
|
|
369
144
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
145
|
+
/** One bounded, independent call through the same official LLM service. */
|
|
146
|
+
export async function consultConfiguredModel(ctx, route, task, outputLimit = 12_000, signal) {
|
|
147
|
+
const provider = text(route?.provider)
|
|
148
|
+
const model = text(route?.model)
|
|
149
|
+
const taskText = text(task)
|
|
150
|
+
if (!provider || !model) throw new Error('a configured provider and model are required')
|
|
151
|
+
if (!taskText) throw new Error('task must contain text')
|
|
152
|
+
throwIfAborted(signal)
|
|
153
|
+
const limit = boundedInteger(outputLimit, 12_000, 1, 50_000)
|
|
154
|
+
const controller = new AbortController()
|
|
155
|
+
const onAbort = () => controller.abort(signal.reason)
|
|
156
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
157
|
+
let answer = ''
|
|
158
|
+
let characterLimitReached = false
|
|
159
|
+
let finish = { kind: 'unknown' }
|
|
160
|
+
try {
|
|
161
|
+
const stream = ctx.llm.stream({
|
|
162
|
+
provider,
|
|
163
|
+
model,
|
|
164
|
+
messages: [{
|
|
165
|
+
role: 'user',
|
|
166
|
+
content: [{
|
|
167
|
+
type: 'text',
|
|
168
|
+
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}`,
|
|
169
|
+
}],
|
|
170
|
+
}],
|
|
171
|
+
maxTokens: Math.min(4_096, Math.max(256, Math.ceil(limit / 2))),
|
|
172
|
+
signal: controller.signal,
|
|
173
|
+
})
|
|
174
|
+
for await (const chunk of stream) {
|
|
175
|
+
if (chunk?.type === 'text-delta' && typeof chunk.text === 'string') {
|
|
176
|
+
const remaining = limit - answer.length
|
|
177
|
+
if (remaining > 0) answer += chunk.text.slice(0, remaining)
|
|
178
|
+
if (chunk.text.length > remaining) {
|
|
179
|
+
characterLimitReached = true
|
|
180
|
+
controller.abort(new Error('consultation output limit reached'))
|
|
181
|
+
break
|
|
182
|
+
}
|
|
183
|
+
} else if (chunk?.type === 'finish') {
|
|
184
|
+
const reason = chunk.reason
|
|
185
|
+
finish = { kind: text(reason?.kind) || 'unknown' }
|
|
186
|
+
if (finish.kind === 'error' || finish.kind === 'aborted') {
|
|
187
|
+
finish.error = text(reason?.failure?.message) || 'model request failed'
|
|
188
|
+
}
|
|
382
189
|
}
|
|
383
190
|
}
|
|
191
|
+
} finally {
|
|
192
|
+
signal?.removeEventListener('abort', onAbort)
|
|
193
|
+
}
|
|
194
|
+
throwIfAborted(signal)
|
|
195
|
+
const tokenLimitReached = finish.kind === 'max-tokens'
|
|
196
|
+
const truncated = characterLimitReached || tokenLimitReached
|
|
197
|
+
if (!characterLimitReached && (finish.kind === 'error' || finish.kind === 'aborted')) {
|
|
198
|
+
return { ok: false, provider, model, answer, truncated, finish, error: finish.error }
|
|
199
|
+
}
|
|
200
|
+
if (!answer.trim()) {
|
|
201
|
+
return { ok: false, provider, model, answer, truncated, finish, error: 'model returned no text' }
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
ok: true, provider, model, answer, truncated, finish,
|
|
205
|
+
...(tokenLimitReached
|
|
206
|
+
? { truncationReason: 'model-token-limit', notice: '模型达到本次调用的输出 token 上限,回答可能不完整。' }
|
|
207
|
+
: characterLimitReached
|
|
208
|
+
? { truncationReason: 'output-character-limit', notice: '回答达到字符上限,后续内容已截断。' }
|
|
209
|
+
: {}),
|
|
384
210
|
}
|
|
385
|
-
return null
|
|
386
211
|
}
|
|
387
212
|
|
|
388
|
-
function
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
213
|
+
function explicitRoute(args, routes) {
|
|
214
|
+
const provider = text(args.provider)
|
|
215
|
+
const model = text(args.model)
|
|
216
|
+
if (Boolean(provider) !== Boolean(model)) throw new Error('provider and model must be supplied together')
|
|
217
|
+
if (!provider) return null
|
|
218
|
+
const route = routes.find(item => item.provider === provider && item.model === model)
|
|
219
|
+
if (!route) throw new Error(`route ${provider}/${model} is not configured in DeepSeek Harness`)
|
|
220
|
+
return route
|
|
394
221
|
}
|
|
395
222
|
|
|
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'
|
|
223
|
+
function currentAgentRoute(agent) {
|
|
420
224
|
try {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
}
|
|
225
|
+
const config = agent?.session?.requestHeader?.()?.config
|
|
226
|
+
if (text(config?.provider) && text(config?.model)) return { provider: config.provider, model: config.model }
|
|
227
|
+
} catch { /* background tools need not have a session-backed agent */ }
|
|
228
|
+
return null
|
|
430
229
|
}
|
|
431
230
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
const wait = delay => new Promise(resolve => setTimeout(resolve, delay))
|
|
231
|
+
function chooseConsultRoute(plan, routes, agent) {
|
|
232
|
+
const current = currentAgentRoute(agent)
|
|
233
|
+
const differs = route => current === null || routeKey(route) !== routeKey(current)
|
|
234
|
+
const preferred = routes.find(route => route.provider === plan.selected?.provider && route.model === plan.selected?.model)
|
|
235
|
+
if (preferred && differs(preferred)) return preferred
|
|
236
|
+
return routes.find(differs) ?? preferred ?? routes[0]
|
|
237
|
+
}
|
|
441
238
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
|
454
|
-
}
|
|
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
|
-
}
|
|
459
|
-
return result
|
|
239
|
+
/** Manual package > manual tool > saved route mapping > CLI default. */
|
|
240
|
+
export function resolveTeamCliModelBindings(plan, routes, requested = {}) {
|
|
241
|
+
if (!requested || typeof requested !== 'object' || Array.isArray(requested)) {
|
|
242
|
+
throw new Error('cliModelsJson 必须是以工作包或官方工具 ID 为键的 JSON 对象')
|
|
460
243
|
}
|
|
461
|
-
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
void promise.finally(() => {
|
|
470
|
-
if (control.inFlight === promise) control.inFlight = null
|
|
471
|
-
})
|
|
472
|
-
return promise
|
|
244
|
+
const byRoute = new Map(routes.map(route => [routeKey(route), route]))
|
|
245
|
+
const bindings = Object.create(null)
|
|
246
|
+
for (const item of plan.team.workPackages) {
|
|
247
|
+
const toolId = toolForProvider(item.recommendedProvider)?.id
|
|
248
|
+
const profile = byRoute.get(`${item.recommendedProvider}\u0000${item.recommendedModel}`)
|
|
249
|
+
if (profile?.cliModel && toolId !== 'zcode') bindings[item.id] = profile.cliModel
|
|
250
|
+
if (Object.hasOwn(requested, toolId)) bindings[item.id] = requested[toolId]
|
|
251
|
+
if (Object.hasOwn(requested, item.id)) bindings[item.id] = requested[item.id]
|
|
473
252
|
}
|
|
474
|
-
|
|
475
|
-
return
|
|
253
|
+
// Keep the supplied keys so the runtime can reject unknown tool/package IDs.
|
|
254
|
+
return { ...bindings, ...Object.fromEntries(Object.entries(requested)
|
|
255
|
+
.filter(([key]) => !Object.hasOwn(bindings, key))) }
|
|
476
256
|
}
|
|
477
257
|
|
|
478
|
-
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
})
|
|
258
|
+
function commandText(plan) {
|
|
259
|
+
const selected = plan.selected ? `${plan.selected.provider}/${plan.selected.model}` : '没有可用路线'
|
|
260
|
+
const channel = plan.executionChannel === 'official-cli'
|
|
261
|
+
? `官方 CLI(${plan.channelLabel ?? plan.channelTool})`
|
|
262
|
+
: '官方模型目录 API'
|
|
263
|
+
return [
|
|
264
|
+
`推荐路线:${selected}`,
|
|
265
|
+
`复杂度:${plan.complexity.band};任务类型:${plan.taskType}`,
|
|
266
|
+
`执行渠道:${channel};估算成本:${plan.estimatedCost === null ? '价格资料不足' : `$${plan.estimatedCost.toFixed(6)}(仅估算)`}`,
|
|
267
|
+
`工作包:${plan.subtasks.map(item => `${item.name} → ${item.recommendedProvider}/${item.recommended}`).join(';')}`,
|
|
268
|
+
plan.team.handoff,
|
|
269
|
+
].join('\n')
|
|
270
|
+
}
|
|
492
271
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
//
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
272
|
+
/** Register model-facing tools and the human /router command. */
|
|
273
|
+
export function apply(ctx, config = {}) {
|
|
274
|
+
// The official Host injects typert; direct lightweight uses of apply may
|
|
275
|
+
// supply only the model/command services and do not expose the Desktop RPC.
|
|
276
|
+
if (ctx.typert) registerOfficialToolsRemote(ctx)
|
|
277
|
+
ctx.on('tools/pre-execute', async (exec, next) => {
|
|
278
|
+
const decision = await next()
|
|
279
|
+
if (decision.kind !== 'allow') return decision
|
|
280
|
+
if (exec.name === 'model_router_tool_install') {
|
|
281
|
+
const requested = getOfficialTool(text(exec.arguments?.tool))
|
|
282
|
+
const label = requested?.label ?? '官方 CLI'
|
|
283
|
+
const desktopInstaller = requested?.manager === 'signed-windows-installer'
|
|
284
|
+
return {
|
|
285
|
+
kind: 'ask',
|
|
286
|
+
reason: desktopInstaller
|
|
287
|
+
? `Download and open the verified official ${label} desktop installer`
|
|
288
|
+
: `Install ${label} globally with the plugin's fixed official command`,
|
|
289
|
+
displayReason: {
|
|
290
|
+
en: desktopInstaller
|
|
291
|
+
? `Download and open the verified ${label} installer? You can select the installation directory in its window.`
|
|
292
|
+
: `Install ${label} globally using the fixed official package?`,
|
|
293
|
+
zh: desktopInstaller
|
|
294
|
+
? `下载并打开已验签的 ${label} 安装器?安装窗口中可选择非 C 盘目录。`
|
|
295
|
+
: `使用插件固定的官方软件包,在本机全局安装 ${label}?`,
|
|
296
|
+
},
|
|
515
297
|
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
ctx.on('approval/request', (request, next) => {
|
|
527
|
-
if (!isApprovalGateReason(request?.reason)) return next()
|
|
528
|
-
const state = request?.agent === undefined ? null : stateFor(request.agent)
|
|
529
|
-
const context = approvalSafetyContext(state, state?.lastStep)
|
|
530
|
-
const decorated = decorateApprovalReason(request.reason, context)
|
|
531
|
-
if (decorated !== request.reason) {
|
|
532
|
-
try {
|
|
533
|
-
request.reason = decorated
|
|
534
|
-
} catch {
|
|
535
|
-
// Some hosts freeze event payloads. In that case the gate still
|
|
536
|
-
// receives the original reason and remains fully fail-safe.
|
|
298
|
+
}
|
|
299
|
+
if ((exec.name === 'model_router_tool_run' || exec.name === 'model_router_team_execute')
|
|
300
|
+
&& exec.arguments?.mode === 'workspace-write') {
|
|
301
|
+
return {
|
|
302
|
+
kind: 'ask',
|
|
303
|
+
reason: 'Official CLI models will use their normal tools in an isolated Git worktree and integrate their patch into the current workspace',
|
|
304
|
+
displayReason: {
|
|
305
|
+
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?',
|
|
306
|
+
zh: '允许官方 CLI 模型在独立 Git 工作区使用终端、技能、已配置 MCP 等工具,并将改动补丁应用回当前工作区?',
|
|
307
|
+
},
|
|
537
308
|
}
|
|
538
309
|
}
|
|
539
|
-
return
|
|
540
|
-
}
|
|
541
|
-
|
|
310
|
+
return decision
|
|
311
|
+
})
|
|
312
|
+
registerOfficialToolModels(ctx, config)
|
|
313
|
+
ctx.tools.register(defineTool({
|
|
314
|
+
name: 'model_router_routes',
|
|
315
|
+
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.',
|
|
316
|
+
parameters: {},
|
|
317
|
+
output: JSON_OUTPUT,
|
|
318
|
+
async execute(_args, exec) {
|
|
319
|
+
const routes = await discoverConfiguredRoutes(ctx, exec.signal)
|
|
320
|
+
return jsonValue({ routes, count: routes.length, availabilityNotice: '目录记录不证明账号凭据和网络当前可用。' })
|
|
321
|
+
},
|
|
322
|
+
}))
|
|
323
|
+
ctx.tools.register(defineTool({
|
|
324
|
+
name: 'model_router_plan',
|
|
325
|
+
description: 'Analyze task difficulty, recommend only configured Harness model routes, and optionally split compound work into dependent packages. Cost estimates require user-supplied USD prices for the exact routes.',
|
|
326
|
+
parameters: {
|
|
327
|
+
task: { type: 'string', required: true, description: 'Task to analyze.' },
|
|
328
|
+
mode: { type: 'string', enum: ['single', 'team'], description: 'Use team to produce Agent Teams work packages.' },
|
|
329
|
+
budgetUsd: { type: 'number', description: 'Optional local estimated cost ceiling in USD.' },
|
|
330
|
+
},
|
|
331
|
+
output: JSON_OUTPUT,
|
|
332
|
+
async execute(args, exec) {
|
|
333
|
+
return jsonValue(await createRoutePlan(ctx, args.task, config, { mode: args.mode, budgetUsd: args.budgetUsd, signal: exec.signal }))
|
|
334
|
+
},
|
|
335
|
+
}))
|
|
336
|
+
ctx.tools.register(defineTool({
|
|
337
|
+
name: 'model_router_consult',
|
|
338
|
+
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.',
|
|
339
|
+
parameters: {
|
|
340
|
+
task: { type: 'string', required: true, description: 'Task or question for the consulted model.' },
|
|
341
|
+
provider: { type: 'string', description: 'Configured provider id; pair with model.' },
|
|
342
|
+
model: { type: 'string', description: 'Configured model id; pair with provider.' },
|
|
343
|
+
outputLimit: { type: 'number', description: 'Maximum returned characters, from 500 to 50000.' },
|
|
344
|
+
},
|
|
345
|
+
output: JSON_OUTPUT,
|
|
346
|
+
async execute(args, exec) {
|
|
347
|
+
const routes = await discoverConfiguredRoutes(ctx, exec.signal)
|
|
348
|
+
if (routes.length === 0) throw new Error('no configured model routes are available')
|
|
349
|
+
const requested = explicitRoute(args, routes)
|
|
350
|
+
const plan = requested ? null : await createRoutePlan(ctx, args.task, config, { signal: exec.signal })
|
|
351
|
+
const route = requested ?? chooseConsultRoute(plan, routes, exec.agent)
|
|
352
|
+
const fallback = valueOf(config, 'maxConsultOutputChars', 12_000)
|
|
353
|
+
const limit = boundedInteger(args.outputLimit, finiteNumber(fallback, 12_000), 500, 50_000)
|
|
354
|
+
return jsonValue(await consultConfiguredModel(ctx, route, args.task, limit, exec.signal))
|
|
355
|
+
},
|
|
356
|
+
}))
|
|
542
357
|
ctx.commands.register({
|
|
543
358
|
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) }
|
|
359
|
+
description: 'Show an official-model route recommendation for a task.',
|
|
360
|
+
input: { hint: 'Describe the task to plan' },
|
|
361
|
+
async handler({ rawInput, signal }) {
|
|
362
|
+
if (!text(rawInput)) return { kind: 'error', text: '用法:/router <需要规划的任务>' }
|
|
363
|
+
try {
|
|
364
|
+
const plan = await createRoutePlan(ctx, rawInput, config, { signal })
|
|
365
|
+
return { kind: 'success', text: commandText(plan) }
|
|
366
|
+
} catch (error) {
|
|
367
|
+
return { kind: 'error', text: `无法生成路由计划:${errorText(error)}` }
|
|
567
368
|
}
|
|
568
|
-
|
|
569
|
-
|
|
369
|
+
},
|
|
370
|
+
})
|
|
371
|
+
ctx.commands.register({
|
|
372
|
+
name: 'tools',
|
|
373
|
+
description: 'Show official model CLI install status, or install one registry tool.',
|
|
374
|
+
input: { hint: '留空查看状态;或输入 install/cancel <工具id>' },
|
|
375
|
+
async handler({ rawInput, signal }) {
|
|
376
|
+
const input = text(rawInput)
|
|
377
|
+
const installMatch = input.match(/^install\s+([A-Za-z0-9_-]+)$/i)
|
|
378
|
+
const cancelMatch = input.match(/^cancel\s+([A-Za-z0-9_-]+)$/i)
|
|
379
|
+
if (cancelMatch) {
|
|
380
|
+
try { return { kind: 'success', text: `已请求取消 ${cancelInstall(cancelMatch[1]).tool} 的安装;请使用 /tools 查看最新状态。` } }
|
|
381
|
+
catch (error) { return { kind: 'error', text: `无法取消安装:${errorText(error)}` } }
|
|
570
382
|
}
|
|
571
|
-
if (
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
383
|
+
if (!installMatch) {
|
|
384
|
+
if (input) return { kind: 'error', text: '用法:/tools 查看状态,或 /tools install/cancel <工具id>' }
|
|
385
|
+
try {
|
|
386
|
+
const probes = await probeAllTools({ fresh: true })
|
|
387
|
+
const lines = probes.map(probe => {
|
|
388
|
+
const tool = getOfficialTool(probe.id)
|
|
389
|
+
const command = installCommandLine(tool)
|
|
390
|
+
const status = probe.status === 'installed'
|
|
391
|
+
? `已安装 ${probe.version ?? ''}`
|
|
392
|
+
: probe.status === 'unsupported'
|
|
393
|
+
? '不支持一键安装'
|
|
394
|
+
: '未安装'
|
|
395
|
+
return `• ${tool.label}(${probe.id}):${status}${command && probe.status === 'not-installed' ? `\n 安装:/tools install ${probe.id}(即 ${command})` : ''}`
|
|
396
|
+
})
|
|
397
|
+
return { kind: 'success', text: `官方工具状态:\n${lines.join('\n')}` }
|
|
398
|
+
} catch (error) {
|
|
399
|
+
return { kind: 'error', text: `探测失败:${errorText(error)}` }
|
|
400
|
+
}
|
|
577
401
|
}
|
|
578
|
-
|
|
579
|
-
|
|
402
|
+
try {
|
|
403
|
+
const job = startInstall(installMatch[1])
|
|
404
|
+
const tool = getOfficialTool(installMatch[1])
|
|
405
|
+
const settled = await waitForInstall(job.tool, signal)
|
|
406
|
+
if (settled.status === 'succeeded') {
|
|
407
|
+
const probe = await probeToolWith(tool, defaultRunner)
|
|
408
|
+
return { kind: 'success', text: `${tool.label} 安装完成${probe.version ? `,探测版本 ${probe.version}` : ''}。` }
|
|
409
|
+
}
|
|
410
|
+
if (settled.status === 'installer-opened') {
|
|
411
|
+
return { kind: 'success', text: `${tool.label} 官方安装器已验证并打开。请在安装窗口选择非 C 盘目录并完成安装,然后运行 /tools 重新检测;当前尚未确认安装完成。` }
|
|
412
|
+
}
|
|
413
|
+
return { kind: 'error', text: `${tool.label} 安装失败:${settled.error ?? '未知原因'}\n${settled.outputTail.slice(-6).join('\n')}` }
|
|
414
|
+
} catch (error) {
|
|
415
|
+
return { kind: 'error', text: `无法开始安装:${errorText(error)}` }
|
|
580
416
|
}
|
|
581
|
-
return { kind: 'error', text: '用法:/router mode collective、/router mode single、/router plan、/router safety、/router web 或 /router watcher' }
|
|
582
417
|
},
|
|
583
418
|
})
|
|
419
|
+
}
|
|
584
420
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
421
|
+
/** Poll an install job until it settles or the signal aborts. */
|
|
422
|
+
async function waitForInstall(toolId, signal) {
|
|
423
|
+
for (;;) {
|
|
424
|
+
if (signal?.aborted) {
|
|
425
|
+
try { cancelInstall(toolId) } catch { /* install may already have finished */ }
|
|
426
|
+
throw new Error('已请求取消安装;请重新检测实际安装状态。')
|
|
589
427
|
}
|
|
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
|
-
})
|
|
428
|
+
const job = installStatus(toolId)
|
|
429
|
+
if (!job) throw new Error('安装任务丢失。')
|
|
430
|
+
if (job.status !== 'running') return job
|
|
431
|
+
await new Promise(resolve => setTimeout(resolve, 1_500))
|
|
432
|
+
}
|
|
433
|
+
}
|
|
604
434
|
|
|
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,
|
|
435
|
+
/** Register the model-facing official-tool probes and installer. */
|
|
436
|
+
function registerOfficialToolModels(ctx, config) {
|
|
437
|
+
ctx.tools.register(defineTool({
|
|
438
|
+
name: 'model_router_tools',
|
|
439
|
+
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.',
|
|
440
|
+
parameters: {},
|
|
441
|
+
output: JSON_OUTPUT,
|
|
442
|
+
async execute(_args, exec) {
|
|
443
|
+
throwIfAborted(exec.signal)
|
|
444
|
+
const probes = await probeAllTools()
|
|
445
|
+
return jsonValue({
|
|
446
|
+
tools: probes,
|
|
447
|
+
executionCapabilities: officialToolExecutionCapabilities(),
|
|
448
|
+
executionReadiness: await Promise.all(probes.map(probe => probe.installed
|
|
449
|
+
? officialToolReadiness(probe.id)
|
|
450
|
+
: Promise.resolve({ id: probe.id, ready: false, reason: 'CLI 尚未安装或版本检测失败。' }))),
|
|
451
|
+
installHint: '未安装的工具可由 model_router_tool_install 按注册表固定命令安装;版本与包名不接受自定义。',
|
|
652
452
|
})
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
453
|
+
},
|
|
454
|
+
}))
|
|
455
|
+
ctx.tools.register(defineTool({
|
|
456
|
+
name: 'model_router_tool_install',
|
|
457
|
+
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.',
|
|
458
|
+
parameters: {
|
|
459
|
+
tool: { type: 'string', required: true, description: 'Registry tool id, e.g. kimi-code.' },
|
|
460
|
+
},
|
|
461
|
+
output: JSON_OUTPUT,
|
|
462
|
+
async execute(args, exec) {
|
|
463
|
+
const job = startInstall(args.tool)
|
|
464
|
+
const settled = await waitForInstall(job.tool, exec.signal)
|
|
465
|
+
const tool = getOfficialTool(args.tool)
|
|
466
|
+
const probe = settled.status === 'succeeded'
|
|
467
|
+
? await probeToolWith(tool, defaultRunner)
|
|
665
468
|
: 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
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
-
}
|
|
469
|
+
return jsonValue({
|
|
470
|
+
...settled,
|
|
471
|
+
postInstallProbe: probe,
|
|
472
|
+
notice: settled.status === 'installer-opened'
|
|
473
|
+
? '已打开 ZCode 官方安装窗口,请选择安装目录并完成安装,之后重新检测;此状态不代表安装完成。'
|
|
474
|
+
: '安装命令完全来自服务端注册表;实际版本以探测横幅为准。',
|
|
475
|
+
})
|
|
476
|
+
},
|
|
477
|
+
}))
|
|
478
|
+
ctx.tools.register(defineTool({
|
|
479
|
+
name: 'model_router_tool_run',
|
|
480
|
+
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.',
|
|
481
|
+
parameters: {
|
|
482
|
+
tool: { type: 'string', required: true, description: 'Fixed registry tool id, e.g. claude-code or codex.' },
|
|
483
|
+
task: { type: 'string', required: true, description: 'Concrete task for the official CLI model.' },
|
|
484
|
+
provider: { type: 'string', description: 'Optional configured provider, paired with model.' },
|
|
485
|
+
model: { type: 'string', description: 'Optional model ID from the Harness directory, paired with provider. This ID is advisory for CLIs except Claude/Codex.' },
|
|
486
|
+
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.' },
|
|
487
|
+
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.' },
|
|
488
|
+
},
|
|
489
|
+
output: JSON_OUTPUT,
|
|
490
|
+
async execute(args, exec) {
|
|
491
|
+
const { cwd, root, sandboxMode } = await sessionWorkspace(ctx, exec)
|
|
492
|
+
const mode = args.mode === 'workspace-write' ? 'workspace-write' : 'read-only'
|
|
493
|
+
if (mode === 'workspace-write' && sandboxMode === 'read-only') throw new Error('当前 Harness 会话为只读模式,不能请求可编辑 CLI 执行')
|
|
494
|
+
let modelId = null
|
|
495
|
+
if (text(args.provider) || text(args.model)) {
|
|
496
|
+
if (!text(args.provider) || !text(args.model)) throw new Error('provider 和 model 必须同时提供')
|
|
497
|
+
const tool = toolForProvider(args.provider)
|
|
498
|
+
if (tool?.id !== args.tool) throw new Error('所选模型供应商与官方 CLI 工具不匹配')
|
|
499
|
+
const routes = configuredRoutesWithProfiles(await discoverConfiguredRoutes(ctx, exec.signal), config)
|
|
500
|
+
const route = routes.find(item => item.provider === args.provider && item.model === args.model)
|
|
501
|
+
if (!route) throw new Error('所选 provider/model 不在官方模型目录中')
|
|
502
|
+
modelId = route.cliModel && args.tool !== 'zcode'
|
|
503
|
+
? route.cliModel
|
|
504
|
+
: args.tool === 'claude-code' || args.tool === 'codex' ? args.model : null
|
|
716
505
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
target = { provider: fallback.provider, model: fallback.model, reasoningEffort: fallback.reasoningEffort, estimatedCost: target.estimatedCost }
|
|
506
|
+
if (text(args.cliModel)) {
|
|
507
|
+
if (!text(args.provider) || !text(args.model)) throw new Error('cliModel 需要同时提供已配置的 provider 和 model 路线')
|
|
508
|
+
if (args.tool === 'zcode') throw new Error('ZCode 3.14.3 不支持在单次调用中指定 CLI 模型')
|
|
509
|
+
modelId = args.cliModel
|
|
722
510
|
}
|
|
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
|
-
|
|
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
|
-
} : {}),
|
|
511
|
+
const result = await runOfficialTask({ toolId: args.tool, task: args.task, modelId, workspace: cwd, allowedRoot: root, mode, signal: exec.signal, sandbox: ctx.sandbox })
|
|
512
|
+
return jsonValue({ ...result,
|
|
513
|
+
...(!modelId && text(args.model) ? { modelNotice: result.modelNotice
|
|
514
|
+
?? 'Harness 模型 ID 未经此厂商 CLI 验证;本次使用厂商 CLI 已配置的默认模型。' } : {}),
|
|
515
|
+
})
|
|
516
|
+
},
|
|
517
|
+
}))
|
|
518
|
+
ctx.tools.register(defineTool({
|
|
519
|
+
name: 'model_router_team_execute',
|
|
520
|
+
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.',
|
|
521
|
+
parameters: {
|
|
522
|
+
task: { type: 'string', required: true, description: 'Full task to plan, distribute and execute.' },
|
|
523
|
+
mode: { type: 'string', enum: ['read-only', 'workspace-write'], description: 'Default read-only; workspace-write needs a clean Git repository and approval.' },
|
|
524
|
+
budgetUsd: { type: 'number', description: 'Estimated planning ceiling only, not a vendor billing limit.' },
|
|
525
|
+
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.' },
|
|
526
|
+
},
|
|
527
|
+
output: JSON_OUTPUT,
|
|
528
|
+
async execute(args, exec) {
|
|
529
|
+
const { cwd, root, sandboxMode } = await sessionWorkspace(ctx, exec)
|
|
530
|
+
const mode = args.mode === 'workspace-write' ? 'workspace-write' : 'read-only'
|
|
531
|
+
if (mode === 'workspace-write' && sandboxMode === 'read-only') throw new Error('当前 Harness 会话为只读模式,不能请求可编辑团队执行')
|
|
532
|
+
const [discoveredRoutes, installed] = await Promise.all([discoverConfiguredRoutes(ctx, exec.signal), installedToolIds()])
|
|
533
|
+
const routes = configuredRoutesWithProfiles(discoveredRoutes, config)
|
|
534
|
+
const readiness = await Promise.all(installed.map(id => officialToolReadiness(id, cwd)))
|
|
535
|
+
const supported = new Set(readiness.filter(item => item.ready).map(item => item.id))
|
|
536
|
+
const capabilities = new Map(officialToolExecutionCapabilities().map(item => [item.id, item]))
|
|
537
|
+
const executableRoutes = routes.filter(route => {
|
|
538
|
+
const tool = toolForProvider(route.provider)
|
|
539
|
+
return tool && installed.includes(tool.id) && supported.has(tool.id)
|
|
540
|
+
&& capabilities.get(tool.id)?.modes?.includes(mode)
|
|
541
|
+
})
|
|
542
|
+
if (executableRoutes.length === 0) return jsonValue({ status: 'blocked',
|
|
543
|
+
reason: `官方模型目录中没有同时满足已配置路线、已安装 CLI、托管执行适配器与 ${mode} 模式的供应商;Kimi、MiniMax、ZCode 仅支持经审批的 workspace-write。`,
|
|
544
|
+
installed, executionCapabilities: officialToolExecutionCapabilities(), executionReadiness: readiness })
|
|
545
|
+
const configuredBudget = valueOf(config, 'budgetUsd', DEFAULT_ROUTER_SETTINGS.budgetUsd)
|
|
546
|
+
const budgetUsd = Math.max(0, finiteNumber(args.budgetUsd, finiteNumber(configuredBudget, 0)))
|
|
547
|
+
const plan = createPlanFromRoutes(args.task, executableRoutes, {
|
|
548
|
+
mode: 'team', budgetUsd, installedToolIds: installed,
|
|
549
|
+
runnableToolIds: installed.filter(id => supported.has(id)),
|
|
550
|
+
})
|
|
551
|
+
const bindingsText = text(args.cliModelsJson)
|
|
552
|
+
if (bindingsText.length > 4_000) throw new Error('cliModelsJson 超过 4000 字符上限')
|
|
553
|
+
let cliModels = null
|
|
554
|
+
if (bindingsText) {
|
|
555
|
+
try { cliModels = JSON.parse(bindingsText) }
|
|
556
|
+
catch { throw new Error('cliModelsJson 不是有效的 JSON 对象') }
|
|
786
557
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
})
|
|
558
|
+
cliModels = resolveTeamCliModelBindings(plan, executableRoutes, cliModels ?? {})
|
|
559
|
+
const execution = await runOfficialTeam({ plan, task: args.task, workspace: cwd,
|
|
560
|
+
allowedRoot: root, mode, installedIds: installed, cliModels, signal: exec.signal, sandbox: ctx.sandbox })
|
|
561
|
+
return jsonValue({ plan, execution,
|
|
562
|
+
modelNotice: '已配置 cliModel 的路线按工作包传给官方 CLI;Claude/Codex 在未配置映射时请求 Harness 模型 ID。其他厂商无映射时使用 CLI 默认模型。多数 CLI 尚不返回可核验的实际模型 ID,须以厂商运行记录核对。',
|
|
563
|
+
billingNotice: 'budgetUsd 仅影响估算与路由,无法限制官方 CLI 账号实际费用。' })
|
|
564
|
+
},
|
|
565
|
+
}))
|
|
795
566
|
}
|