ci / build (push) Canceled after 0s
- shared/nextedit.ts: next-edit prompt format (region rewrite or NO_EDIT, Qwen spare special tokens) - shared/diagnostics.ts: TS errors near the cursor as context; symbols format 2 adds namespace imports - shared/prompt.ts: token healing (cut the half-typed word, force it via grammar), errors header section - daemon: model profiles carry contextFormat/errors/members/nextEdit; /v1/next-edit with edit-burst tracking; /v1/prewarm; /v1/debug/last; token healing on by default - extension: next-edit decorations (Tab apply / Esc dismiss), suggest on cursor move, prewarm on focus, Show Last Prompt, update check against releases, diagnostics in the edit recorder - models/README: training-data attribution; release script profiles for stage-d/stage-e - .gitea/workflows/ci.yml: build + test, publish release on vscode-v* tags (needs a runner + RELEASE_TOKEN) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J39imRDT8L7xYbWzsVWd8A
134 lines
7.7 KiB
JavaScript
134 lines
7.7 KiB
JavaScript
// 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') },
|
||
// Lock file of the public fine-tuning corpus (training workspace), for the attribution table.
|
||
sources: { type: 'string', default: path.join(repo, '..', 'pipeline', 'config', 'public-sources.lock.json') },
|
||
}, 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 E: Stage D + next-edit prediction (shared/nextedit.ts)
|
||
'stage-e': { order: 'spm', modes: ['symbols', 'simple'], defaultMode: 'symbols', contextFormat: 2, errors: true, members: true, nextEdit: true, description: 'Suffix-first FIM with symbols + TS errors, plus next-edit prediction (rewrites a region around the cursor)' },
|
||
// Stage D: next-line / block-close / multi-line fixes, namespace-import symbols (format 2), TS errors in the header
|
||
'stage-d': { order: 'spm', modes: ['symbols', 'simple'], defaultMode: 'symbols', contextFormat: 2, errors: true, members: true, description: 'Suffix-first FIM with symbols (incl. namespace imports) + TS errors; better next-line, block closing and multi-line completions' },
|
||
'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 sources = fs.existsSync(values.sources) ? JSON.parse(fs.readFileSync(values.sources, 'utf8')) : []
|
||
const sourcesTable = sources.length
|
||
? ['| repository | commit | license |', '|---|---|---|', ...sources.map(src => `| [${src.url.replace(/^https:\/\/|\.git$/g, '')}](${src.url.replace(/\.git$/, '')}) | \`${src.commit.slice(0, 10)}\` | ${src.license} |`)].join('\n')
|
||
: '(source list not available when this file was generated)'
|
||
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.
|
||
|
||
## Training data
|
||
|
||
Fine-tuned on permissively licensed open-source repositories (below, pinned commits) and on the publisher's own
|
||
code (not published). Secrets were scrubbed from all training data; held-out evaluation repos were never trained on.
|
||
|
||
${sourcesTable}
|
||
|
||
## 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`)
|