// Updates models/ with GGUF files + manifest.json for the extension's managed mode. // // node scripts/make-model-release.mjs --from [stage-c stage-b ...] // // --from defaults to ../../models (the training workspace next to this repo). Models are // hard-linked (no second copy) and tracked with Git LFS. Then publish with: // git add models && git commit -m "Models: …" && git push // The extension reads /media/branch/main/models/manifest.json (Gitea's /media/ route // serves LFS content); model URLs in the manifest are relative to it. import fs from 'node:fs' import path from 'node:path' import crypto from 'node:crypto' import { parseArgs } from 'node:util' const repo = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..') const { values, positionals } = parseArgs({ options: { from: { type: 'string', default: path.join(repo, '..', 'models') } }, allowPositionals: true }) const out = path.join(repo, 'models') // Prompt layout each model was trained with (the extension only offers matching context modes). const profiles = { 'stage-c': { order: 'spm', modes: ['symbols', 'simple'], defaultMode: 'simple', description: 'Suffix-first FIM with repo/symbol-context headers (default)' }, 'stage-b': { order: 'spm', modes: ['raw'], defaultMode: 'raw', description: 'Suffix-first FIM without context headers (previous stage)' }, } const wanted = positionals.length ? positionals : ['stage-c', 'stage-b'] const sha256 = file => new Promise((resolve, reject) => { const h = crypto.createHash('sha256') fs.createReadStream(file).on('data', d => h.update(d)).on('end', () => resolve(h.digest('hex'))).on('error', reject) }) const models = [] for (const name of wanted) { if (!profiles[name]) throw new Error(`no profile for ${name}: add one to scripts/make-model-release.mjs`) const file = `${name}-q8_0.gguf` const src = path.join(values.from, file) if (!fs.existsSync(src)) throw new Error(`${src} not found (use --from )`) const dest = path.join(out, file) if (path.resolve(src) !== path.resolve(dest)) { fs.rmSync(dest, { force: true }) fs.linkSync(src, dest) // hard link: no second 500 MB copy } models.push({ name, file, url: file, sha256: await sha256(src), size: fs.statSync(src).size, ...profiles[name] }) console.log(`${file} ${(fs.statSync(src).size / 1e6).toFixed(0)} MB`) } const manifest = { schema: 1, default: wanted[0], models, llamaCpp: { build: 'b11368', assets: { 'linux-x64': { url: 'https://github.com/ggml-org/llama.cpp/releases/download/b11368/llama-b11368-bin-ubuntu-x64.tar.gz', sha256: '1c267259a8db2a28f2b212aef4df67b197aa184377a222369135f6fafdbfe235' }, 'linux-arm64': { url: 'https://github.com/ggml-org/llama.cpp/releases/download/b11368/llama-b11368-bin-ubuntu-arm64.tar.gz', sha256: '98f78c5d1f6ec04f0d64bc028bd9345d5665c29fd18c1a7c8c3ed0ba54ceb94e' }, 'darwin-arm64': { url: 'https://github.com/ggml-org/llama.cpp/releases/download/b11368/llama-b11368-bin-macos-arm64.tar.gz', sha256: '2e98cd5d0afb2cc00e62db33f1bd9048d4942ceef9bab9affb01832d6ad3735c' }, 'darwin-x64': { url: 'https://github.com/ggml-org/llama.cpp/releases/download/b11368/llama-b11368-bin-macos-x64.tar.gz', sha256: 'b46659767fbf39f8a8ff3cb78f7e8722e6b5bc92f3eb448bbe322463d83675b7' }, }, }, } fs.writeFileSync(path.join(out, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n') fs.writeFileSync(path.join(out, 'NOTICE'), `These model files are modified versions of Qwen2.5-Coder-0.5B (https://huggingface.co/Qwen/Qwen2.5-Coder-0.5B), Copyright (c) Alibaba Cloud, licensed under the Apache License, Version 2.0 (see LICENSE). Modifications: full-parameter fine-tuning for suffix-first fill-in-the-middle completion (and, for stage-c, a repository/symbol-context header), followed by Q8_0 GGUF conversion. Fine-tuning data: permissively licensed (MIT) open-source TypeScript/Vue repositories and the publisher's own code. Secrets were scrubbed from the training data. `) const row = m => `| \`${m.name}\` | ${m.description} | ${m.order} | ${m.modes.join(', ')} | ${(m.size / 1e6).toFixed(0)} MB |` fs.writeFileSync(path.join(out, 'README.md'), `# Models Q8_0 GGUF fill-in-the-middle models for the [Local Autocomplete](../extensions/vscode) extension, fine-tuned from [Qwen2.5-Coder-0.5B](https://huggingface.co/Qwen/Qwen2.5-Coder-0.5B) for **TypeScript, Vue and Node.js**. | model | description | prompt order | context modes | size | |---|---|---|---|---| ${models.map(row).join('\n')} The extension reads [\`manifest.json\`](manifest.json), downloads the selected model once, verifies its SHA-256 and runs it locally with llama.cpp. The files are stored with Git LFS; download links use Gitea's \`/media/\` route: https://git.harvmaster.com/Harvmaster/autocomplete-model/media/branch/main/models/ ## Evaluation (612 held-out samples from private repos the model never saw, suffix-first prompts) | | exact match | edit similarity | still parses | |---|---:|---:|---:| | Qwen2.5-Coder-0.5B (base) | 0.270 | 0.506 | 0.783 | | Qwen2.5-Coder-1.5B (base) | 0.366 | 0.623 | 0.882 | | \`stage-b\` | 0.391 | 0.689 | 0.952 | | \`stage-c\` (with symbol context) | 0.397 | 0.681 | 0.954 | The base models were evaluated in their native prefix-first order (they can't do suffix-first). Suffix-first prompts let the editor reuse the KV cache while typing: about 2 tokens re-processed per keystroke, ~90–100 ms end-to-end on a laptop CPU. ## Using the files directly Any llama.cpp-based runtime works, but the prompt must match the training layout. The [daemon](../daemon) and [\`shared/prompt.ts\`](../shared/prompt.ts) are the reference. Suffix-first (\`spm\`) order: <|fim_prefix|><|fim_suffix|>{suffix}<|fim_middle|>{prefix} → model generates the middle, then <|endoftext|> \`stage-c\` additionally expects a repo-level header: \`<|repo_name|>{repo}\\n<|file_sep|>{path}\\n\`, optionally with a \`__symbols__.d.ts\` pseudo-file of available symbols before it. ## License Apache-2.0 ([LICENSE](LICENSE), [NOTICE](NOTICE)), as a derivative of Qwen2.5-Coder. `) console.log(`updated ${path.relative(process.cwd(), out)}/: manifest.json, NOTICE, README.md`)