dsh-harbor-evolution 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/index.js +20 -2
- package/lib/candidate.js +7 -1
- package/lib/evolution.js +56 -14
- package/lib/model-runtime.js +216 -0
- package/lib/service.js +12 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,8 +14,8 @@ npx --yes dsh-harbor-evolution@latest setup --project-root "$PWD"
|
|
|
14
14
|
|
|
15
15
|
The setup command installs both required runtimes:
|
|
16
16
|
|
|
17
|
-
- `harbor-dsh-evolution==0.7.
|
|
18
|
-
- `dsh-harbor-evolution@0.7.
|
|
17
|
+
- `harbor-dsh-evolution==0.7.1` in a managed Python environment.
|
|
18
|
+
- `dsh-harbor-evolution@0.7.1` in the selected DSH profile.
|
|
19
19
|
|
|
20
20
|
It then stores the absolute Harbor executable paths and `projectRoot` in the profile's `harbor-evolution` block and verifies the integration. Existing unrelated profile entries are preserved, and rerunning setup updates the same block.
|
|
21
21
|
|
|
@@ -57,6 +57,12 @@ The Web UI is intentionally read-only. Starting an evaluation or deciding promot
|
|
|
57
57
|
|
|
58
58
|
A direct evaluation requires `candidatePath`, `datasetPath`, `stackPath`, and explicit `mode`; `promotion-eligible` additionally requires `policyPath`. Prefer the Skill because it will not run or compare Jobs until the material identities and evaluation contract are resolved.
|
|
59
59
|
|
|
60
|
+
## Candidate model binding
|
|
61
|
+
|
|
62
|
+
Before each Job, the Plugin snapshots the current DSH Agent selection—provider, model, and reasoning effort—then starts a per-Job local Model Broker. The Candidate uses the temporary `dsh-host` adapter through `dsh-host-broker` / `dsh-host-model-gateway/v1`; it receives only a short-lived Job capability file, never GPT Auth, Codex OAuth, or an upstream API key.
|
|
63
|
+
|
|
64
|
+
`harbor_eval_run`, `harbor_context_preview`, and `harbor_evolution_doctor` inherit that selection by default. Advanced callers can override `candidateProvider` and `candidateModel` only as a pair, plus an optional `candidateReasoningEffort`. `openai-codex` performs a GPT Auth sign-in check before Harbor starts. The resulting model binding is part of Context v2 comparison identity, so any provider/model/reasoning change requires a new baseline.
|
|
65
|
+
|
|
60
66
|
`harbor_eval_result` defaults to the stable Summary. Use `view=job`, `view=dataset`, `view=progress`, `view=trial` plus a returned `trialId`, or `view=governance` to inspect sanitized instructions, generated output, evidence, and evaluator source without coupling the Agent to artifact file paths.
|
|
61
67
|
|
|
62
68
|
## What setup writes
|
package/index.js
CHANGED
|
@@ -5,11 +5,12 @@ import path from 'node:path'
|
|
|
5
5
|
import { fileURLToPath } from 'node:url'
|
|
6
6
|
|
|
7
7
|
import { loadBundledSkill } from './lib/official-skill.js'
|
|
8
|
+
import { CandidateModelRuntime } from './lib/model-runtime.js'
|
|
8
9
|
import { EvolutionService } from './lib/service.js'
|
|
9
10
|
import { installDashboardWeb } from './lib/web.js'
|
|
10
11
|
|
|
11
12
|
export const name = 'harbor-evolution'
|
|
12
|
-
export const inject = ['tools', 'skills']
|
|
13
|
+
export const inject = ['tools', 'skills', 'llm', 'agentDefaultModel']
|
|
13
14
|
|
|
14
15
|
const packageDir = path.dirname(fileURLToPath(import.meta.url))
|
|
15
16
|
const checkoutPythonPackage = path.resolve(packageDir, '../harbor-plugin')
|
|
@@ -30,6 +31,13 @@ export const Config = Schema.object({
|
|
|
30
31
|
pluginImportPath: Schema.string().default('dsh-evolution'),
|
|
31
32
|
pythonPath: Schema.string().default(''),
|
|
32
33
|
timeoutMs: Schema.number().default(1800000),
|
|
34
|
+
candidateProvider: Schema.string().default(''),
|
|
35
|
+
candidateModel: Schema.string().default(''),
|
|
36
|
+
candidateReasoningEffort: Schema.string().default(''),
|
|
37
|
+
modelBrokerBindHost: Schema.string().default('127.0.0.1'),
|
|
38
|
+
modelBrokerAdvertisedHost: Schema.string().default('host.docker.internal'),
|
|
39
|
+
modelBrokerMaxRequests: Schema.number().min(1).default(1000),
|
|
40
|
+
modelBrokerMaxRequestBytes: Schema.number().min(1024).default(33554432),
|
|
33
41
|
})
|
|
34
42
|
|
|
35
43
|
function jsonTool(definition, execute) {
|
|
@@ -57,7 +65,8 @@ export function apply(ctx, config) {
|
|
|
57
65
|
: ''
|
|
58
66
|
),
|
|
59
67
|
}
|
|
60
|
-
const
|
|
68
|
+
const modelRuntime = new CandidateModelRuntime(ctx, resolved)
|
|
69
|
+
const service = new EvolutionService(resolved, { pluginVersion: packageJson.version }, modelRuntime)
|
|
61
70
|
|
|
62
71
|
ctx.skills.register(loadBundledSkill())
|
|
63
72
|
installDashboardWeb(ctx, service)
|
|
@@ -103,6 +112,9 @@ export function apply(ctx, config) {
|
|
|
103
112
|
stackPath: { type: 'string', required: true },
|
|
104
113
|
policyPath: { type: 'string' },
|
|
105
114
|
mode: { type: 'string', required: true },
|
|
115
|
+
candidateProvider: { type: 'string', description: 'Optional Candidate provider. Supply it together with candidateModel; defaults to the current DSH Agent model.' },
|
|
116
|
+
candidateModel: { type: 'string' },
|
|
117
|
+
candidateReasoningEffort: { type: 'string' },
|
|
106
118
|
},
|
|
107
119
|
}, args => service.doctor(args)))
|
|
108
120
|
|
|
@@ -124,6 +136,9 @@ export function apply(ctx, config) {
|
|
|
124
136
|
datasetPath: { type: 'string', required: true },
|
|
125
137
|
stackPath: { type: 'string', required: true },
|
|
126
138
|
mode: { type: 'string', required: true },
|
|
139
|
+
candidateProvider: { type: 'string', description: 'Optional Candidate provider. Supply it together with candidateModel; defaults to the current DSH Agent model.' },
|
|
140
|
+
candidateModel: { type: 'string' },
|
|
141
|
+
candidateReasoningEffort: { type: 'string' },
|
|
127
142
|
},
|
|
128
143
|
}, args => service.previewContext(args)))
|
|
129
144
|
|
|
@@ -139,6 +154,9 @@ export function apply(ctx, config) {
|
|
|
139
154
|
mode: { type: 'string', required: true },
|
|
140
155
|
policyPath: { type: 'string' },
|
|
141
156
|
jobName: { type: 'string' },
|
|
157
|
+
candidateProvider: { type: 'string', description: 'Optional Candidate provider. Supply it together with candidateModel; defaults to the current DSH Agent model and is frozen before the Job starts.' },
|
|
158
|
+
candidateModel: { type: 'string' },
|
|
159
|
+
candidateReasoningEffort: { type: 'string' },
|
|
142
160
|
},
|
|
143
161
|
}, args => service.run(args)))
|
|
144
162
|
|
package/lib/candidate.js
CHANGED
|
@@ -4,7 +4,7 @@ import path from 'node:path'
|
|
|
4
4
|
|
|
5
5
|
export const MANIFEST_NAME = 'candidate-manifest.json'
|
|
6
6
|
const DIGEST_PREFIX = Buffer.from('harbor-dsh-candidate-v1\0')
|
|
7
|
-
const EXCLUDED_DIRS = new Set(['.git', 'node_modules', '__pycache__'])
|
|
7
|
+
const EXCLUDED_DIRS = new Set(['.git', 'node_modules', '__pycache__', '.harbor-runtime'])
|
|
8
8
|
const EXCLUDED_FILES = new Set([MANIFEST_NAME, '.DS_Store'])
|
|
9
9
|
const LOCKFILES = ['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lock', 'bun.lockb']
|
|
10
10
|
const CREDENTIAL_FILES = new Set([
|
|
@@ -57,6 +57,12 @@ export async function computeCandidate(candidateDir) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
async function validateCandidateContract(root) {
|
|
60
|
+
try {
|
|
61
|
+
await stat(path.join(root, '.harbor-runtime'))
|
|
62
|
+
throw new Error('Candidate must not contain the reserved .harbor-runtime path')
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (error.code !== 'ENOENT') throw error
|
|
65
|
+
}
|
|
60
66
|
for (const required of ['cordis.yml', 'package.json']) {
|
|
61
67
|
try {
|
|
62
68
|
if (!(await stat(path.join(root, required))).isFile()) throw new Error()
|
package/lib/evolution.js
CHANGED
|
@@ -119,6 +119,19 @@ function strictInputs(config, args) {
|
|
|
119
119
|
return { projectRoot, candidate, dataset, stack, jobs, mode, policy }
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
function candidateModelCliArgs(binding) {
|
|
123
|
+
if (!binding?.provider || !binding?.model) throw new Error('Candidate model binding is required')
|
|
124
|
+
return [
|
|
125
|
+
'--candidate-model-provider', binding.provider,
|
|
126
|
+
'--candidate-model', binding.model,
|
|
127
|
+
...(binding.reasoning_effort === undefined
|
|
128
|
+
? []
|
|
129
|
+
: ['--candidate-reasoning-effort', binding.reasoning_effort]),
|
|
130
|
+
'--candidate-model-transport', binding.transport,
|
|
131
|
+
'--candidate-model-protocol', binding.protocol,
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
|
|
122
135
|
export async function validateDataset(config, args) {
|
|
123
136
|
const dataset = resolveWithin(config.projectRoot, args.datasetPath, 'datasetPath')
|
|
124
137
|
return cliJson(config, ['dataset', 'validate', dataset, '--project-root', config.projectRoot], { allowedExitCodes: [0, 2] })
|
|
@@ -157,11 +170,12 @@ export async function previewContext(config, args) {
|
|
|
157
170
|
'--stack', inputs.stack,
|
|
158
171
|
'--jobs-dir', inputs.jobs,
|
|
159
172
|
'--mode', inputs.mode,
|
|
173
|
+
...candidateModelCliArgs(args.candidateModelBinding),
|
|
160
174
|
])
|
|
161
175
|
return { manifest, ...preview }
|
|
162
176
|
}
|
|
163
177
|
|
|
164
|
-
export async function runEvaluation(config, args) {
|
|
178
|
+
export async function runEvaluation(config, args, modelRuntime) {
|
|
165
179
|
const manifest = await snapshot(config, args)
|
|
166
180
|
const inputs = strictInputs(config, args)
|
|
167
181
|
const datasetValidation = await validateDataset(config, args)
|
|
@@ -176,6 +190,7 @@ export async function runEvaluation(config, args) {
|
|
|
176
190
|
'context', 'preview', '--project-root', inputs.projectRoot,
|
|
177
191
|
'--candidate', inputs.candidate, '--dataset', inputs.dataset,
|
|
178
192
|
'--stack', inputs.stack, '--jobs-dir', inputs.jobs, '--mode', inputs.mode,
|
|
193
|
+
...candidateModelCliArgs(args.candidateModelBinding),
|
|
179
194
|
])
|
|
180
195
|
const jobName = args.jobName ?? makeJobName(manifest)
|
|
181
196
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(jobName)) throw new Error('jobName contains unsupported characters')
|
|
@@ -186,6 +201,8 @@ export async function runEvaluation(config, args) {
|
|
|
186
201
|
'--ak', `candidate_path=${inputs.candidate}`,
|
|
187
202
|
'--ak', `candidate_version=${manifest.version}`,
|
|
188
203
|
'--ak', `candidate_digest=${manifest.digest}`,
|
|
204
|
+
'--ak', `candidate_model_provider=${args.candidateModelBinding.provider}`,
|
|
205
|
+
'--ak', `candidate_model=${args.candidateModelBinding.model}`,
|
|
189
206
|
'--job-name', jobName,
|
|
190
207
|
'--jobs-dir', inputs.jobs,
|
|
191
208
|
'--plugin', config.pluginImportPath,
|
|
@@ -194,22 +211,47 @@ export async function runEvaluation(config, args) {
|
|
|
194
211
|
'--plugin-kwarg', `stack_path=${inputs.stack}`,
|
|
195
212
|
'--plugin-kwarg', `project_root=${inputs.projectRoot}`,
|
|
196
213
|
'--plugin-kwarg', `mode=${inputs.mode}`,
|
|
214
|
+
'--plugin-kwarg', `candidate_model_provider=${args.candidateModelBinding.provider}`,
|
|
215
|
+
'--plugin-kwarg', `candidate_model=${args.candidateModelBinding.model}`,
|
|
216
|
+
'--plugin-kwarg', `candidate_model_transport=${args.candidateModelBinding.transport}`,
|
|
217
|
+
'--plugin-kwarg', `candidate_model_protocol=${args.candidateModelBinding.protocol}`,
|
|
197
218
|
]
|
|
219
|
+
if (args.candidateModelBinding.reasoning_effort !== undefined) {
|
|
220
|
+
harborArgs.push('--ak', `candidate_reasoning_effort=${args.candidateModelBinding.reasoning_effort}`)
|
|
221
|
+
harborArgs.push('--plugin-kwarg', `candidate_reasoning_effort=${args.candidateModelBinding.reasoning_effort}`)
|
|
222
|
+
}
|
|
198
223
|
if (inputs.policy) harborArgs.push('--plugin-kwarg', `policy_path=${inputs.policy}`)
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
env: { ...process.env, ...(config.pythonPath ? { PYTHONPATH: config.pythonPath } : {}) },
|
|
224
|
+
const lease = await modelRuntime.openLease(args.candidateModelBinding, {
|
|
225
|
+
candidateDigest: manifest.digest,
|
|
226
|
+
jobName,
|
|
203
227
|
})
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
228
|
+
try {
|
|
229
|
+
const processResult = await runProcess(config.harborBin, harborArgs, {
|
|
230
|
+
cwd: config.projectRoot,
|
|
231
|
+
timeoutMs: config.timeoutMs,
|
|
232
|
+
env: {
|
|
233
|
+
...process.env,
|
|
234
|
+
...(config.pythonPath ? { PYTHONPATH: config.pythonPath } : {}),
|
|
235
|
+
HSE_MODEL_GATEWAY_URL: lease.endpoint,
|
|
236
|
+
HSE_MODEL_GATEWAY_TOKEN: lease.token,
|
|
237
|
+
HSE_MODEL_GATEWAY_PROVIDER: lease.candidateProvider,
|
|
238
|
+
HSE_MODEL_GATEWAY_INFO: JSON.stringify(lease.modelInfo),
|
|
239
|
+
HSE_MODEL_GATEWAY_PROTOCOL: lease.protocol,
|
|
240
|
+
},
|
|
241
|
+
})
|
|
242
|
+
const jobDir = path.join(inputs.jobs, jobName)
|
|
243
|
+
const summary = JSON.parse(await readFile(path.join(jobDir, 'evaluation-summary.json'), 'utf8'))
|
|
244
|
+
return {
|
|
245
|
+
manifest,
|
|
246
|
+
candidateModelBinding: args.candidateModelBinding,
|
|
247
|
+
job: path.relative(inputs.projectRoot, jobDir),
|
|
248
|
+
summary,
|
|
249
|
+
doctor,
|
|
250
|
+
contextPreview: preview,
|
|
251
|
+
process: { code: processResult.code },
|
|
252
|
+
}
|
|
253
|
+
} finally {
|
|
254
|
+
await lease.close()
|
|
213
255
|
}
|
|
214
256
|
}
|
|
215
257
|
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { randomBytes, timingSafeEqual } from 'node:crypto'
|
|
2
|
+
import { once } from 'node:events'
|
|
3
|
+
import { createServer } from 'node:http'
|
|
4
|
+
|
|
5
|
+
export const MODEL_GATEWAY_PROTOCOL = 'dsh-host-model-gateway/v1'
|
|
6
|
+
export const CANDIDATE_GATEWAY_PROVIDER = 'dsh-host'
|
|
7
|
+
|
|
8
|
+
function nonBlank(value) {
|
|
9
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function sameSecret(expected, actual) {
|
|
13
|
+
const left = Buffer.from(expected)
|
|
14
|
+
const right = Buffer.from(actual)
|
|
15
|
+
return left.length === right.length && timingSafeEqual(left, right)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function readJsonBody(request, maxBytes) {
|
|
19
|
+
const chunks = []
|
|
20
|
+
let size = 0
|
|
21
|
+
for await (const chunk of request) {
|
|
22
|
+
size += chunk.length
|
|
23
|
+
if (size > maxBytes) {
|
|
24
|
+
throw Object.assign(new Error('model gateway request is too large'), { statusCode: 413 })
|
|
25
|
+
}
|
|
26
|
+
chunks.push(chunk)
|
|
27
|
+
}
|
|
28
|
+
let value
|
|
29
|
+
try {
|
|
30
|
+
value = JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
31
|
+
} catch {
|
|
32
|
+
throw Object.assign(new Error('model gateway request must be valid JSON'), { statusCode: 400 })
|
|
33
|
+
}
|
|
34
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value) || !Array.isArray(value.messages)) {
|
|
35
|
+
throw Object.assign(new Error('model gateway request requires a messages array'), { statusCode: 400 })
|
|
36
|
+
}
|
|
37
|
+
return value
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function sendJson(response, statusCode, value) {
|
|
41
|
+
const body = `${JSON.stringify(value)}\n`
|
|
42
|
+
response.writeHead(statusCode, {
|
|
43
|
+
'content-type': 'application/json; charset=utf-8',
|
|
44
|
+
'content-length': Buffer.byteLength(body),
|
|
45
|
+
'cache-control': 'no-store',
|
|
46
|
+
})
|
|
47
|
+
response.end(body)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function authorized(request, token) {
|
|
51
|
+
const value = request.headers.authorization
|
|
52
|
+
return typeof value === 'string' && value.startsWith('Bearer ')
|
|
53
|
+
&& sameSecret(token, value.slice('Bearer '.length))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function listen(server, host) {
|
|
57
|
+
server.listen(0, host)
|
|
58
|
+
await once(server, 'listening')
|
|
59
|
+
const address = server.address()
|
|
60
|
+
if (address === null || typeof address === 'string') {
|
|
61
|
+
throw new Error('model gateway did not bind a TCP port')
|
|
62
|
+
}
|
|
63
|
+
return address.port
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Freeze the current DSH Agent model per Job, then proxy only that model to an
|
|
68
|
+
* immutable Candidate. The Candidate receives a short-lived capability, never
|
|
69
|
+
* the Host provider's credential or route selection.
|
|
70
|
+
*/
|
|
71
|
+
export class CandidateModelRuntime {
|
|
72
|
+
constructor(ctx, config) {
|
|
73
|
+
this.ctx = ctx
|
|
74
|
+
this.config = config
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async resolve(args = {}) {
|
|
78
|
+
const explicitProvider = nonBlank(args.candidateProvider)
|
|
79
|
+
const explicitModel = nonBlank(args.candidateModel)
|
|
80
|
+
if (Boolean(explicitProvider) !== Boolean(explicitModel)) {
|
|
81
|
+
throw new Error('candidateProvider and candidateModel must be supplied together')
|
|
82
|
+
}
|
|
83
|
+
const configuredProvider = nonBlank(this.config.candidateProvider)
|
|
84
|
+
const configuredModel = nonBlank(this.config.candidateModel)
|
|
85
|
+
if (Boolean(configuredProvider) !== Boolean(configuredModel)) {
|
|
86
|
+
throw new Error('Harbor candidateProvider and candidateModel configuration must be supplied together')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const inherited = this.ctx.agentDefaultModel.currentSelection()
|
|
90
|
+
const provider = explicitProvider ?? configuredProvider ?? inherited.provider
|
|
91
|
+
const model = explicitModel ?? configuredModel ?? inherited.model
|
|
92
|
+
const explicitReasoning = nonBlank(args.candidateReasoningEffort)
|
|
93
|
+
const configuredReasoning = nonBlank(this.config.candidateReasoningEffort)
|
|
94
|
+
const canInheritReasoning = provider === inherited.provider && model === inherited.model
|
|
95
|
+
const reasoningEffort = explicitReasoning ?? configuredReasoning
|
|
96
|
+
?? (canInheritReasoning ? inherited.reasoningEffort : undefined)
|
|
97
|
+
|
|
98
|
+
if (!this.ctx.llm.listProviders().some(item => item.id === provider)) {
|
|
99
|
+
throw new Error(`Candidate model provider "${provider}" is not registered in DeepSeek Harness`)
|
|
100
|
+
}
|
|
101
|
+
const modelInfo = await this.ctx.llm.resolveModelInfo(provider, model)
|
|
102
|
+
if (provider === 'openai-codex') {
|
|
103
|
+
const auth = this.ctx.get('codexAuth')
|
|
104
|
+
if (auth === undefined || typeof auth.status !== 'function') {
|
|
105
|
+
throw new Error('Candidate model openai-codex requires the dsh-codex-auth service')
|
|
106
|
+
}
|
|
107
|
+
const status = await auth.status()
|
|
108
|
+
if (!status.configured) {
|
|
109
|
+
throw new Error('Candidate model openai-codex is not signed in; complete GPT Auth in Settings before starting Harbor')
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
provider,
|
|
115
|
+
model,
|
|
116
|
+
...(reasoningEffort === undefined ? {} : { reasoning_effort: String(reasoningEffort) }),
|
|
117
|
+
transport: 'dsh-host-broker',
|
|
118
|
+
protocol: MODEL_GATEWAY_PROTOCOL,
|
|
119
|
+
model_info: modelInfo,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async openLease(binding, scope) {
|
|
124
|
+
const token = randomBytes(32).toString('base64url')
|
|
125
|
+
const route = `/harbor-model-gateway/v1/${randomBytes(18).toString('base64url')}`
|
|
126
|
+
const controllers = new Set()
|
|
127
|
+
let requestCount = 0
|
|
128
|
+
const server = createServer(async (request, response) => {
|
|
129
|
+
if (request.url !== route || !authorized(request, token)) {
|
|
130
|
+
sendJson(response, 404, { error: 'not found' })
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
if (request.method === 'GET') {
|
|
134
|
+
sendJson(response, 200, {
|
|
135
|
+
protocol: MODEL_GATEWAY_PROTOCOL,
|
|
136
|
+
candidate_digest: scope.candidateDigest,
|
|
137
|
+
job: scope.jobName,
|
|
138
|
+
binding: {
|
|
139
|
+
provider: binding.provider,
|
|
140
|
+
model: binding.model,
|
|
141
|
+
...(binding.reasoning_effort === undefined ? {} : { reasoning_effort: binding.reasoning_effort }),
|
|
142
|
+
},
|
|
143
|
+
})
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
if (request.method !== 'POST') {
|
|
147
|
+
sendJson(response, 405, { error: 'method not allowed' })
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
if (requestCount >= this.config.modelBrokerMaxRequests) {
|
|
151
|
+
sendJson(response, 429, { error: 'model gateway request budget exhausted' })
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
requestCount += 1
|
|
155
|
+
const controller = new AbortController()
|
|
156
|
+
controllers.add(controller)
|
|
157
|
+
response.on('close', () => {
|
|
158
|
+
if (!response.writableEnded) controller.abort(new Error('Candidate disconnected'))
|
|
159
|
+
})
|
|
160
|
+
try {
|
|
161
|
+
const body = await readJsonBody(request, this.config.modelBrokerMaxRequestBytes)
|
|
162
|
+
const {
|
|
163
|
+
provider: _provider,
|
|
164
|
+
model: _model,
|
|
165
|
+
reasoningEffort: _reasoningEffort,
|
|
166
|
+
signal: _signal,
|
|
167
|
+
...requestOptions
|
|
168
|
+
} = body
|
|
169
|
+
response.writeHead(200, {
|
|
170
|
+
'content-type': 'application/x-ndjson; charset=utf-8',
|
|
171
|
+
'cache-control': 'no-store',
|
|
172
|
+
})
|
|
173
|
+
const stream = this.ctx.llm.stream({
|
|
174
|
+
...requestOptions,
|
|
175
|
+
provider: binding.provider,
|
|
176
|
+
model: binding.model,
|
|
177
|
+
...(binding.reasoning_effort === undefined ? {} : { reasoningEffort: binding.reasoning_effort }),
|
|
178
|
+
signal: controller.signal,
|
|
179
|
+
})
|
|
180
|
+
for await (const chunk of stream) {
|
|
181
|
+
if (!response.write(`${JSON.stringify(chunk)}\n`)) await once(response, 'drain')
|
|
182
|
+
}
|
|
183
|
+
response.end()
|
|
184
|
+
} catch (error) {
|
|
185
|
+
if (!response.headersSent) {
|
|
186
|
+
const statusCode = Number.isInteger(error?.statusCode) ? error.statusCode : 502
|
|
187
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
188
|
+
sendJson(response, statusCode, { error: message })
|
|
189
|
+
} else if (!response.writableEnded) {
|
|
190
|
+
response.destroy(error instanceof Error ? error : new Error(String(error)))
|
|
191
|
+
}
|
|
192
|
+
} finally {
|
|
193
|
+
controllers.delete(controller)
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
const port = await listen(server, this.config.modelBrokerBindHost)
|
|
198
|
+
const endpoint = `http://${this.config.modelBrokerAdvertisedHost}:${port}${route}`
|
|
199
|
+
let closed = false
|
|
200
|
+
return {
|
|
201
|
+
protocol: MODEL_GATEWAY_PROTOCOL,
|
|
202
|
+
endpoint,
|
|
203
|
+
token,
|
|
204
|
+
candidateProvider: CANDIDATE_GATEWAY_PROVIDER,
|
|
205
|
+
modelInfo: binding.model_info,
|
|
206
|
+
async close() {
|
|
207
|
+
if (closed) return
|
|
208
|
+
closed = true
|
|
209
|
+
for (const controller of controllers) controller.abort(new Error('Harbor Job ended'))
|
|
210
|
+
const close = new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve()))
|
|
211
|
+
server.closeAllConnections()
|
|
212
|
+
await close
|
|
213
|
+
},
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
package/lib/service.js
CHANGED
|
@@ -48,9 +48,10 @@ export async function resolveEvaluatorStackPath(config, governance, explicitPath
|
|
|
48
48
|
|
|
49
49
|
/** One Host-side boundary shared by Agent tools and the Web dashboard. */
|
|
50
50
|
export class EvolutionService {
|
|
51
|
-
constructor(config, metadata = {}) {
|
|
51
|
+
constructor(config, metadata = {}, modelRuntime) {
|
|
52
52
|
this.config = config
|
|
53
53
|
this.metadata = metadata
|
|
54
|
+
this.modelRuntime = modelRuntime
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
snapshot(args) {
|
|
@@ -61,8 +62,9 @@ export class EvolutionService {
|
|
|
61
62
|
return initializeProject(this.config, args)
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
run(args) {
|
|
65
|
-
|
|
65
|
+
async run(args) {
|
|
66
|
+
const candidateModelBinding = await this.modelRuntime.resolve(args)
|
|
67
|
+
return runEvaluation(this.config, { ...args, candidateModelBinding }, this.modelRuntime)
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
result(args) {
|
|
@@ -82,16 +84,19 @@ export class EvolutionService {
|
|
|
82
84
|
return compareCandidates(this.config, args)
|
|
83
85
|
}
|
|
84
86
|
|
|
85
|
-
doctor(args) {
|
|
86
|
-
|
|
87
|
+
async doctor(args) {
|
|
88
|
+
const candidateModelBinding = await this.modelRuntime.resolve(args)
|
|
89
|
+
const result = await runDoctor(this.config, args)
|
|
90
|
+
return { ...result, candidate_model_binding: candidateModelBinding }
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
validateDataset(args) {
|
|
90
94
|
return validateDataset(this.config, args)
|
|
91
95
|
}
|
|
92
96
|
|
|
93
|
-
previewContext(args) {
|
|
94
|
-
|
|
97
|
+
async previewContext(args) {
|
|
98
|
+
const candidateModelBinding = await this.modelRuntime.resolve(args)
|
|
99
|
+
return previewContext(this.config, { ...args, candidateModelBinding })
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
dashboard() {
|