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, }, } }, )