- Deterministic scoring.py (quant 60 / qual 40 / flags -15, profitability heaviest) - Per-company JSON ledger with forecast-target chaining deck N-1 -> N - Single-shot sandbox agent with guided-JSON fallback ladder (no tool loop) - Portfolio dashboard with sparklines, KPI hit rates, BDEF category bars - 48 unit tests green; endpoints smoke-tested; npm check+build green Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
160 lines
4.9 KiB
TypeScript
160 lines
4.9 KiB
TypeScript
import { sdk } from '../sdk'
|
||
import { configFile } from '../file-models/config'
|
||
|
||
const { InputSpec, Value, List } = sdk
|
||
|
||
const inputSpec = InputSpec.of({
|
||
models: Value.list(
|
||
List.obj(
|
||
{
|
||
name: 'Model Catalog',
|
||
description:
|
||
'The local models this service can serve on your Sparks. Each grader ' +
|
||
'references one of these by its alias. The job runner loads models in ' +
|
||
'waves so you can run a panel across more models than fit in GPU memory ' +
|
||
'at once.',
|
||
default: [],
|
||
minLength: 1,
|
||
maxLength: 16,
|
||
},
|
||
{
|
||
uniqueBy: 'alias',
|
||
displayAs: '{{alias}} → {{hfModel}}',
|
||
spec: InputSpec.of({
|
||
alias: Value.text({
|
||
name: 'Alias',
|
||
description: 'Short name graders use to pick this model (e.g. "qwen-32b").',
|
||
required: true,
|
||
default: null,
|
||
placeholder: 'grader-a',
|
||
patterns: [
|
||
{ regex: '^[a-z0-9][a-z0-9-]{0,30}$',
|
||
description: 'Lowercase letters, numbers, dashes (max 31 chars).' },
|
||
],
|
||
}),
|
||
hfModel: Value.text({
|
||
name: 'Hugging Face Model ID',
|
||
description: 'The model vLLM serves. Must be present in the Spark HF cache for air-gapped mode.',
|
||
required: true,
|
||
default: null,
|
||
placeholder: 'Qwen/Qwen3-32B-FP8',
|
||
}),
|
||
spark: Value.select({
|
||
name: 'Served On',
|
||
description:
|
||
'Which Spark serves this model. Air-gapped grading mode requires the ' +
|
||
'head (primary) Spark; the secondary is used only in local-services mode.',
|
||
default: 'primary',
|
||
values: { primary: 'Primary (head) Spark', secondary: 'Secondary Spark' },
|
||
}),
|
||
port: Value.number({
|
||
name: 'vLLM Port',
|
||
description: 'Host port the vLLM container for this model listens on. Unique per Spark.',
|
||
required: true,
|
||
default: 8001,
|
||
integer: true,
|
||
min: 1,
|
||
max: 65535,
|
||
}),
|
||
}),
|
||
},
|
||
),
|
||
),
|
||
gpuMemoryUtilization: Value.text({
|
||
name: 'GPU Memory Utilization',
|
||
description: 'vLLM --gpu-memory-utilization (0–1). Lower it if you co-resident multiple models per Spark.',
|
||
required: true,
|
||
default: '0.85',
|
||
}),
|
||
maxModelLen: Value.number({
|
||
name: 'Max Model Length',
|
||
description: 'vLLM --max-model-len (context window). Deck text is chunked to fit.',
|
||
required: true,
|
||
default: 32768,
|
||
integer: true,
|
||
min: 2048,
|
||
}),
|
||
toolCallParser: Value.text({
|
||
name: 'Tool-Call Parser',
|
||
description:
|
||
'vLLM tool-call parser for the grader\'s read-file tool loop. Match the ' +
|
||
'served model family (Qwen3 → "hermes"). Empty disables native tool-calling.',
|
||
required: false,
|
||
default: 'hermes',
|
||
}),
|
||
maxConcurrentModels: Value.number({
|
||
name: 'Max Co-resident Models (head Spark)',
|
||
description:
|
||
'How many distinct models may load on the head Spark at once. The job runner ' +
|
||
'loads models in waves so it never exceeds this. 1 is safest.',
|
||
required: true,
|
||
default: 1,
|
||
integer: true,
|
||
min: 1,
|
||
max: 8,
|
||
}),
|
||
proxyPort: Value.number({
|
||
name: 'Model Proxy Port',
|
||
description: 'Port for the on-Spark LiteLLM router that exposes every model alias on one endpoint.',
|
||
required: true,
|
||
default: 4000,
|
||
integer: true,
|
||
min: 1,
|
||
max: 65535,
|
||
}),
|
||
})
|
||
|
||
export const configureModels = sdk.Action.withInput(
|
||
'configure-models',
|
||
|
||
async ({ effects }) => ({
|
||
name: 'Configure Models',
|
||
description: 'Define the local model catalog served on your Sparks and the serving knobs.',
|
||
warning: null,
|
||
allowedStatuses: 'any',
|
||
group: 'Setup',
|
||
visibility: 'enabled',
|
||
}),
|
||
|
||
inputSpec,
|
||
|
||
async ({ effects }) => {
|
||
const cfg = await configFile.read().const(effects)
|
||
if (!cfg) return {}
|
||
return {
|
||
models: cfg.models,
|
||
gpuMemoryUtilization: cfg.gpuMemoryUtilization,
|
||
maxModelLen: cfg.maxModelLen,
|
||
toolCallParser: cfg.toolCallParser,
|
||
maxConcurrentModels: cfg.maxConcurrentModels,
|
||
proxyPort: cfg.proxyPort,
|
||
}
|
||
},
|
||
|
||
async ({ effects, input }) => {
|
||
await configFile.merge(effects, {
|
||
models: input.models,
|
||
gpuMemoryUtilization: input.gpuMemoryUtilization,
|
||
maxModelLen: input.maxModelLen,
|
||
toolCallParser: input.toolCallParser ?? '',
|
||
maxConcurrentModels: input.maxConcurrentModels,
|
||
proxyPort: input.proxyPort,
|
||
})
|
||
|
||
return {
|
||
version: '1',
|
||
title: 'Models Configured',
|
||
message:
|
||
'Saved ' + input.models.length + ' model(s). Make sure each is present in ' +
|
||
'the Spark HF cache for air-gapped runs, then set "Configure Graders".',
|
||
result: {
|
||
type: 'single',
|
||
value: input.models.map((m) => m.alias).join(', '),
|
||
copyable: false,
|
||
qr: false,
|
||
masked: false,
|
||
},
|
||
}
|
||
},
|
||
)
|