Files
autocomplete-model/scripts/make-model-release.mjs
T
HarvmasterandClaude Opus 5.5 fa8f775779 Restructure into a monorepo: models, VS Code extension, daemon, shared
- models/: GGUFs (Git LFS), manifest.json, model card, Apache-2.0 LICENSE + NOTICE
- extensions/vscode/: the Local Autocomplete extension (v0.5.0, publisher harvmaster)
- daemon/: acd completion daemon (bundled into the extension; also runs standalone)
- shared/: prompt layout + symbol extraction shared with the training pipeline
- scripts/: make-model-release.mjs, publish-extension-release.mjs (Gitea releases)
- README with quick start, settings, architecture, build and release instructions;
  MIT license for code; CHANGELOG

The default model manifest URL moves to models/manifest.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:37:19 +00:00

115 lines
6.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Updates models/ with GGUF files + manifest.json for the extension's managed mode.
//
// node scripts/make-model-release.mjs --from <dir with *-q8_0.gguf> [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 <repo>/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 <dir>)`)
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/<file>
## 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`)