ci / build (push) Canceled after 0s
In managed mode the extension switches models by rewriting the daemon config's defaultModel and restarting it, but loadState() preferred the persisted model, so the daemon came back on the old one every time. State now records the default it was saved under; a different default wins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J39imRDT8L7xYbWzsVWd8A
118 lines
5.4 KiB
TypeScript
118 lines
5.4 KiB
TypeScript
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import type { FimOrder } from '../../shared/prompt.ts'
|
|
|
|
export type ContextMode = 'symbols' | 'simple' | 'raw'
|
|
|
|
/** What a model was trained on; the toggle only offers these modes. */
|
|
export interface ModelProfile {
|
|
path: string
|
|
order: FimOrder
|
|
modes: ContextMode[]
|
|
defaultMode: ContextMode
|
|
/** Symbol-context format the model was trained on (shared/symbols.ts): 1 = Stage C, 2 = + namespace imports. */
|
|
contextFormat?: 1 | 2
|
|
/** Trained with the TS-errors header section (Stage D+). */
|
|
errors?: boolean
|
|
/** Override symbols.members for this model (member lists after `expr.`). */
|
|
members?: boolean
|
|
/** Trained for next-edit prediction (shared/nextedit.ts), enables /v1/next-edit. */
|
|
nextEdit?: boolean
|
|
/** Another profile (trained with nextEdit) that serves /v1/next-edit for this model, in a second llama-server. */
|
|
nextEditModel?: string
|
|
}
|
|
|
|
export interface DaemonConfig {
|
|
host: string
|
|
port: number
|
|
tokenizer: { json: string; config: string }
|
|
llamaServer: {
|
|
binary: string
|
|
port: number
|
|
ctx: number
|
|
/** Extra flags, e.g. ["-ngl", "99"] on Apple Silicon, ["-t", "16"] on the laptop CPU. */
|
|
args: string[]
|
|
/** Extra flags for the next-edit companion server only. Default: n-gram speculative decoding (the output is
|
|
* mostly a copy of the region, so drafts from the prompt are accepted; exact with greedy sampling). */
|
|
nextEditArgs?: string[]
|
|
}
|
|
models: Record<string, ModelProfile>
|
|
defaultModel: string
|
|
prompt: {
|
|
prefixTokens: number
|
|
suffixTokens: number
|
|
/** Prefix window start moves in multiples of this many lines (KV-cache stability). */
|
|
prefixChunkLines: number
|
|
maxLines: number
|
|
nPredictSingleLine: number
|
|
nPredictMultiLine: number
|
|
}
|
|
symbols: {
|
|
/** Token budget for the __symbols__.d.ts context (Stage C was trained with up to 2048). */
|
|
budget: number
|
|
/** How long a request waits for fresh context when the anchor changes before using stale/none. */
|
|
waitMs: number
|
|
/** Member completions after `expr.`: in training, but every `.` changes the header (cold prefill). */
|
|
members: boolean
|
|
}
|
|
thisRepair: { enabled: boolean; waitMs: number }
|
|
/** Cut a half-typed word off the prompt and force the output to start with it (default on). */
|
|
tokenHealing?: boolean
|
|
nextEditOptions?: { regionBefore: number; regionAfter: number; maxTokens: number; beforeTokens: number; afterTokens: number }
|
|
telemetry: { enabled: boolean; dir: string }
|
|
/** Where opted-in edit recordings from editors are appended (the opt-in is the editor setting). */
|
|
recording: { dir: string }
|
|
stateFile: string
|
|
/** Workspace roots for clients that can't send one (Zed via /v1/completions): files are looked up under these. */
|
|
workspaceRoots?: string[]
|
|
/** Exit after this many minutes without any request (0 = never). Editors poll every 15 s while open. */
|
|
idleShutdownMinutes?: number
|
|
/** Opaque tag reported by /health (the VS Code extension uses it to replace daemons left over from older versions). */
|
|
instanceTag?: string
|
|
}
|
|
|
|
/** Toggles that survive daemon restarts and are shared by every editor talking to this daemon. */
|
|
export interface RuntimeState {
|
|
enabled: boolean
|
|
model: string
|
|
mode: ContextMode
|
|
/** config.defaultModel when this state was saved: a changed default (editor model setting, new release) wins. */
|
|
defaultModel?: string
|
|
}
|
|
|
|
export function loadConfig(configPath: string): DaemonConfig {
|
|
const dir = path.dirname(path.resolve(configPath))
|
|
const raw = JSON.parse(fs.readFileSync(configPath, 'utf8')) as DaemonConfig
|
|
const abs = (p: string) => (path.isAbsolute(p) || !p.includes('/') ? p : path.resolve(dir, p)) // bare names (e.g. "llama-server") stay on PATH
|
|
raw.tokenizer = { json: abs(raw.tokenizer.json), config: abs(raw.tokenizer.config) }
|
|
raw.llamaServer.binary = abs(raw.llamaServer.binary)
|
|
for (const m of Object.values(raw.models)) m.path = abs(m.path)
|
|
raw.telemetry.dir = abs(raw.telemetry.dir)
|
|
raw.recording.dir = abs(raw.recording.dir)
|
|
raw.stateFile = abs(raw.stateFile)
|
|
if (!raw.models[raw.defaultModel]) throw new Error(`defaultModel ${raw.defaultModel} is not in models`)
|
|
for (const [name, m] of Object.entries(raw.models)) {
|
|
if (!m.modes.includes(m.defaultMode)) throw new Error(`model ${name}: defaultMode must be one of its modes`)
|
|
if (m.nextEditModel && !raw.models[m.nextEditModel]?.nextEdit) throw new Error(`model ${name}: nextEditModel ${m.nextEditModel} must be a profile with nextEdit`)
|
|
}
|
|
return raw
|
|
}
|
|
|
|
export function loadState(config: DaemonConfig): RuntimeState {
|
|
const fallback: RuntimeState = { enabled: true, model: config.defaultModel, mode: config.models[config.defaultModel].defaultMode, defaultModel: config.defaultModel }
|
|
try {
|
|
const s = JSON.parse(fs.readFileSync(config.stateFile, 'utf8')) as Partial<RuntimeState>
|
|
const keepModel = s.defaultModel === config.defaultModel && s.model && config.models[s.model]
|
|
const model = keepModel ? s.model! : fallback.model
|
|
const mode = s.mode && config.models[model].modes.includes(s.mode) ? s.mode : config.models[model].defaultMode
|
|
return { enabled: s.enabled ?? true, model, mode, defaultModel: config.defaultModel }
|
|
} catch {
|
|
return fallback
|
|
}
|
|
}
|
|
|
|
export function saveState(config: DaemonConfig, state: RuntimeState): void {
|
|
fs.mkdirSync(path.dirname(config.stateFile), { recursive: true })
|
|
fs.writeFileSync(config.stateFile, JSON.stringify(state, null, 2) + '\n')
|
|
}
|