From a33790cfc73dbc39a28ad7c8537159ecf239ca4b Mon Sep 17 00:00:00 2001 From: Evan Reichard Date: Sat, 20 Dec 2025 20:19:19 -0500 Subject: [PATCH] feat: add opencode package and update models --- .../programs/terminal/opencode/default.nix | 9 +- packages/llama-cpp/default.nix | 4 +- packages/opencode/bundle.ts | 33 +++ packages/opencode/default.nix | 227 ++++++++++++++++++ .../opencode/relax-bun-version-check.patch | 28 +++ .../x86_64-linux/lin-va-desktop/default.nix | 56 ++--- 6 files changed, 326 insertions(+), 31 deletions(-) create mode 100644 packages/opencode/bundle.ts create mode 100644 packages/opencode/default.nix create mode 100644 packages/opencode/relax-bun-version-check.patch diff --git a/modules/home/programs/terminal/opencode/default.nix b/modules/home/programs/terminal/opencode/default.nix index c58ac64..ad57052 100755 --- a/modules/home/programs/terminal/opencode/default.nix +++ b/modules/home/programs/terminal/opencode/default.nix @@ -16,6 +16,7 @@ in config = mkIf cfg.enable { programs.opencode = { enable = true; + package = pkgs.reichard.opencode; enableMcpIntegration = true; settings = { theme = "catppuccin"; @@ -42,12 +43,18 @@ in baseURL = "https://llm-api.va.reichard.io/v1"; }; models = { - "gpt-oss-20b-thinking" = { + gpt-oss-20b-thinking = { name = "GPT OSS (20B)"; }; + devstral-small-2-instruct = { + name = "Devstral Small 2 (24B)"; + }; qwen3-coder-30b-instruct = { name = "Qwen3 Coder (30B)"; }; + qwen3-next-80b-instruct = { + name = "Qwen3 Next (80B) - Instruct"; + }; qwen3-30b-2507-thinking = { name = "Qwen3 2507 (30B) Thinking"; }; diff --git a/packages/llama-cpp/default.nix b/packages/llama-cpp/default.nix index 8f3aa7f..444ae02 100644 --- a/packages/llama-cpp/default.nix +++ b/packages/llama-cpp/default.nix @@ -7,12 +7,12 @@ vulkanSupport = true; }).overrideAttrs (oldAttrs: rec { - version = "7426"; + version = "7486"; src = pkgs.fetchFromGitHub { owner = "ggml-org"; repo = "llama.cpp"; tag = "b${version}"; - hash = "sha256-la+hA+Fw3xFjAyR4XgNmehghGS6zAKh9gHqJnlw2tMQ="; + hash = "sha256-I9wPNI0yn4I0zHge1Y7q+RYqYvHSyJWKAxY3pHbCTuY="; leaveDotGit = true; postFetch = '' git -C "$out" rev-parse --short HEAD > $out/COMMIT diff --git a/packages/opencode/bundle.ts b/packages/opencode/bundle.ts new file mode 100644 index 0000000..3dce4b0 --- /dev/null +++ b/packages/opencode/bundle.ts @@ -0,0 +1,33 @@ +#!/usr/bin/env bun + +import solidPlugin from "./node_modules/@opentui/solid/scripts/solid-plugin"; +import fs from "fs"; + +const version = process.env.OPENCODE_VERSION!; +const channel = process.env.OPENCODE_CHANNEL!; + +const result = await Bun.build({ + target: "bun", + outdir: "./dist", + entrypoints: ["./src/index.ts", "./src/cli/cmd/tui/worker.ts"], + plugins: [solidPlugin], + naming: { + entry: "[dir]/[name].js", + }, + define: { + OPENCODE_VERSION: JSON.stringify(version), + OPENCODE_CHANNEL: JSON.stringify(channel), + }, + external: ["@opentui/core-*"], +}); + +if (!result.success) { + console.error("Bundle failed:", result.logs); + process.exit(1); +} + +// Move worker file to worker.ts at the dist root so the code can find it +if (fs.existsSync("./dist/cli/cmd/tui/worker.js")) { + fs.renameSync("./dist/cli/cmd/tui/worker.js", "./dist/worker.ts"); + fs.rmdirSync("./dist/cli/cmd/tui", { recursive: true }); +} diff --git a/packages/opencode/default.nix b/packages/opencode/default.nix new file mode 100644 index 0000000..8a823f5 --- /dev/null +++ b/packages/opencode/default.nix @@ -0,0 +1,227 @@ +{ lib +, stdenvNoCC +, bun +, fetchFromGitHub +, fzf +, makeBinaryWrapper +, models-dev +, nix-update-script +, ripgrep +, testers +, writableTmpDirAsHomeHook +, +}: +let + pname = "opencode"; + version = "1.0.170"; + src = fetchFromGitHub { + owner = "sst"; + repo = "opencode"; + tag = "v${version}"; + hash = "sha256-Y0thIZ20p0FSBAH0mJfFn8e+OEUvlZyTuk+/yEt8Sy8="; + }; + + node_modules = stdenvNoCC.mkDerivation { + pname = "${pname}-node_modules"; + inherit version src; + + impureEnvVars = lib.fetchers.proxyImpureEnvVars ++ [ + "GIT_PROXY_COMMAND" + "SOCKS_SERVER" + ]; + + nativeBuildInputs = [ + bun + writableTmpDirAsHomeHook + ]; + + dontConfigure = true; + + buildPhase = '' + runHook preBuild + + export BUN_INSTALL_CACHE_DIR=$(mktemp -d) + + bun install \ + --cpu="*" \ + --filter=./packages/opencode \ + --force \ + --frozen-lockfile \ + --ignore-scripts \ + --no-progress \ + --os="*" \ + --production + + bun run ./nix/scripts/canonicalize-node-modules.ts + bun run ./nix/scripts/normalize-bun-binaries.ts + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + mkdir -p $out + while IFS= read -r dir; do + rel="''${dir#./}" + dest="$out/$rel" + mkdir -p "$(dirname "$dest")" + cp -R "$dir" "$dest" + done < <(find . -type d -name node_modules -prune | sort) + + runHook postInstall + ''; + + # NOTE: Required else we get errors that our fixed-output derivation references store paths + dontFixup = true; + + outputHash = "sha256-Aq774bgU12HkrF2oAtfu9kyQFlxUeDbmwlS9lz4Z4ZI="; + outputHashAlgo = "sha256"; + outputHashMode = "recursive"; + }; +in +stdenvNoCC.mkDerivation (finalAttrs: { + inherit + pname + version + src + node_modules + ; + + nativeBuildInputs = [ + bun + makeBinaryWrapper + models-dev + ]; + + patches = [ + # NOTE: Relax Bun version check to be a warning instead of an error + ./relax-bun-version-check.patch + ]; + + configurePhase = '' + runHook preConfigure + + cp -R ${node_modules}/. . + + runHook postConfigure + ''; + + env.MODELS_DEV_API_JSON = "${models-dev}/dist/_api.json"; + env.OPENCODE_VERSION = finalAttrs.version; + env.OPENCODE_CHANNEL = "stable"; + + preBuild = '' + chmod -R u+w ./packages/opencode/node_modules + pushd ./packages/opencode/node_modules/@parcel/ + for pkg in ../../../../node_modules/.bun/@parcel+watcher-*; do + linkName=$(basename "$pkg" | sed 's/@.*+\(.*\)@.*/\1/') + ln -sf "$pkg/node_modules/@parcel/$linkName" "$linkName" + done + popd + + pushd ./packages/opencode/node_modules/@opentui/ + for pkg in ../../../../node_modules/.bun/@opentui+core-*; do + linkName=$(basename "$pkg" | sed 's/@.*+\(.*\)@.*/\1/') + ln -sf "$pkg/node_modules/@opentui/$linkName" "$linkName" + done + popd + ''; + + buildPhase = '' + runHook preBuild + + + cd ./packages/opencode + cp ${./bundle.ts} ./bundle.ts + bun run ./bundle.ts + + runHook postBuild + ''; + + dontStrip = true; + + installPhase = '' + runHook preInstall + + mkdir -p $out/lib/opencode + # Copy the bundled dist directory + cp -r dist $out/lib/opencode/ + + # Fix WASM paths in worker.ts - use absolute paths to the installed location + # Main wasm is tree-sitter-.wasm, language wasms are tree-sitter--.wasm + main_wasm=$(find "$out/lib/opencode/dist" -maxdepth 1 -name 'tree-sitter-[a-z0-9]*.wasm' -print -quit) + + substituteInPlace $out/lib/opencode/dist/worker.ts \ + --replace-fail 'module2.exports = "../../../tree-sitter-' 'module2.exports = "'"$out"'/lib/opencode/dist/tree-sitter-' \ + --replace-fail 'new URL("tree-sitter.wasm", import.meta.url).href' "\"$main_wasm\"" + + # Copy only the native modules we need (marked as external in bundle.ts) + mkdir -p $out/lib/opencode/node_modules/.bun + mkdir -p $out/lib/opencode/node_modules/@opentui + + # Copy @opentui/core platform-specific packages + for pkg in ../../node_modules/.bun/@opentui+core-*; do + if [ -d "$pkg" ]; then + cp -r "$pkg" $out/lib/opencode/node_modules/.bun/$(basename "$pkg") + fi + done + + mkdir -p $out/bin + makeWrapper ${lib.getExe bun} $out/bin/opencode \ + --add-flags "run" \ + --add-flags "$out/lib/opencode/dist/index.js" \ + --prefix PATH : ${ + lib.makeBinPath [ + fzf + ripgrep + ] + } \ + --argv0 opencode + + runHook postInstall + ''; + + postInstall = '' + # Add symlinks for platform-specific native modules + for pkg in $out/lib/opencode/node_modules/.bun/@opentui+core-*; do + if [ -d "$pkg" ]; then + pkgName=$(basename "$pkg" | sed 's/@opentui+\(core-[^@]*\)@.*/\1/') + ln -sf ../.bun/$(basename "$pkg")/node_modules/@opentui/$pkgName \ + $out/lib/opencode/node_modules/@opentui/$pkgName + fi + done + ''; + + passthru = { + tests.version = testers.testVersion { + package = finalAttrs.finalPackage; + command = "HOME=$(mktemp -d) opencode --version"; + inherit (finalAttrs) version; + }; + updateScript = nix-update-script { + extraArgs = [ + "--subpackage" + "node_modules" + ]; + }; + }; + + meta = { + description = "AI coding agent built for the terminal"; + longDescription = '' + OpenCode is a terminal-based agent that can build anything. + It combines a TypeScript/JavaScript core with a Go-based TUI + to provide an interactive AI coding experience. + ''; + homepage = "https://github.com/sst/opencode"; + license = lib.licenses.mit; + platforms = [ + "aarch64-linux" + "x86_64-linux" + "aarch64-darwin" + "x86_64-darwin" + ]; + mainProgram = "opencode"; + }; +}) diff --git a/packages/opencode/relax-bun-version-check.patch b/packages/opencode/relax-bun-version-check.patch new file mode 100644 index 0000000..5d14f16 --- /dev/null +++ b/packages/opencode/relax-bun-version-check.patch @@ -0,0 +1,28 @@ +From 0e07ea8225f5667e39c6aa59eea726266f0afab0 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= +Date: Thu, 13 Nov 2025 10:16:31 +0100 +Subject: [PATCH] Change Bun version check from error to warning +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Signed-off-by: Jörg Thalheim +--- + packages/script/src/index.ts | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/packages/script/src/index.ts b/packages/script/src/index.ts +index 141d2b75..de06d0dc 100644 +--- a/packages/script/src/index.ts ++++ b/packages/script/src/index.ts +@@ -10,7 +10,7 @@ if (!expectedBunVersion) { + } + + if (process.versions.bun !== expectedBunVersion) { +- throw new Error(`This script requires bun@${expectedBunVersion}, but you are using bun@${process.versions.bun}`) ++ console.warn(`Warning: This script expects bun@${expectedBunVersion}, but you are using bun@${process.versions.bun}`) + } + + const CHANNEL = process.env["OPENCODE_CHANNEL"] ?? (await $`git branch --show-current`.text().then((x) => x.trim())) +-- +2.51.0 diff --git a/systems/x86_64-linux/lin-va-desktop/default.nix b/systems/x86_64-linux/lin-va-desktop/default.nix index 511b68b..6bb71d1 100755 --- a/systems/x86_64-linux/lin-va-desktop/default.nix +++ b/systems/x86_64-linux/lin-va-desktop/default.nix @@ -91,41 +91,41 @@ in package = pkgs.reichard.llama-swap; settings = { models = { - # https://huggingface.co/mradermacher/gpt-oss-20b-heretic-v2-i1-GGUF/tree/main - # --chat-template-kwargs '{\"reasoning_effort\":\"low\"}' - "gpt-oss-20b-thinking" = { - name = "GPT OSS (20B) - Thinking"; - cmd = "${pkgs.reichard.llama-cpp}/bin/llama-server --port \${PORT} -m /mnt/ssd/Models/gpt-oss-20b-heretic-v2.i1-MXFP4_MOE.gguf -c 131072 --temp 1.0 --top-p 1.0 --top-k 40 -dev CUDA0"; - }; - - # https://huggingface.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF/tree/main - "qwen3-coder-30b-instruct" = { - name = "Qwen3 Coder (30B) - Instruct"; - cmd = "${pkgs.reichard.llama-cpp}/bin/llama-server --port \${PORT} -m /mnt/ssd/Models/Qwen3-Coder-30B-A3B-Instruct-Q4_K_M.gguf -c 262144 --temp 0.7 --min-p 0.0 --top-p 0.8 --top-k 20 --repeat-penalty 1.05 --cache-type-k q8_0 --cache-type-v q8_0 -ts 70,30"; - }; - - # https://huggingface.co/unsloth/Qwen3-30B-A3B-Instruct-2507-GGUF/tree/main - "qwen3-30b-2507-instruct" = { - name = "Qwen3 2507 (30B) - Instruct"; - cmd = "${pkgs.reichard.llama-cpp}/bin/llama-server --port \${PORT} -m /mnt/ssd/Models/Qwen3-30B-A3B-Instruct-2507-Q4_K_M.gguf -c 262144 --temp 0.7 --min-p 0.0 --top-p 0.8 --top-k 20 --repeat-penalty 1.05 --cache-type-k q8_0 --cache-type-v q8_0 -ts 70,30"; - }; - - # https://huggingface.co/unsloth/Qwen3-30B-A3B-Thinking-2507-GGUF/tree/main - "qwen3-30b-2507-thinking" = { - name = "Qwen3 2507 (30B) - Thinking"; - cmd = "${pkgs.reichard.llama-cpp}/bin/llama-server --port \${PORT} -m /mnt/ssd/Models/Qwen3-30B-A3B-Thinking-2507-UD-Q4_K_XL.gguf -c 262144 --temp 0.7 --min-p 0.0 --top-p 0.8 --top-k 20 --repeat-penalty 1.05 --cache-type-k q8_0 --cache-type-v q8_0 -ts 70,30"; + # https://huggingface.co/unsloth/Devstral-Small-2-24B-Instruct-2512-GGUF/tree/main + "devstral-small-2-instruct" = { + name = "Devstral Small 2 (24B) - Instruct"; + cmd = "${pkgs.reichard.llama-cpp}/bin/llama-server --port \${PORT} -m /mnt/ssd/Models/Devstral-Small-2-24B-Instruct-2512-UD-Q4_K_XL.gguf -c 98304 -ctk q8_0 -ctv q8_0 -fit off -dev CUDA0"; }; # https://huggingface.co/unsloth/Qwen3-Next-80B-A3B-Instruct-GGUF/tree/main "qwen3-next-80b-instruct" = { name = "Qwen3 Next (80B) - Instruct"; - cmd = "${pkgs.reichard.llama-cpp}/bin/llama-server --port \${PORT} -m /mnt/ssd/Models/Qwen3-Next-80B-A3B-Instruct-UD-Q4_K_XL.gguf --ctx-size 262144 --temp 0.7 --min-p 0.0 --top-p 0.8 --top-k 20 --repeat-penalty 1.05 --cache-type-k q8_0 --cache-type-v q8_0"; + cmd = "${pkgs.reichard.llama-cpp}/bin/llama-server --port \${PORT} -m /mnt/ssd/Models/Qwen3-Next-80B-A3B-Instruct-UD-Q4_K_XL.gguf -c 131072 --temp 0.7 --min-p 0.0 --top-p 0.8 --top-k 20 --repeat-penalty 1.05 -ctk q8_0 -ctv q8_0 -fit off -ncmoe 15 -ts 77,23"; }; - # https://huggingface.co/unsloth/Devstral-Small-2-24B-Instruct-2512-GGUF/tree/main - "devstral-small-2-instruct" = { - name = "Devstral Small 2 (24B) - Instruct"; - cmd = "${pkgs.reichard.llama-cpp}/bin/llama-server --port \${PORT} -m /mnt/ssd/Models/Devstral-Small-2-24B-Instruct-2512-UD-Q4_K_XL.gguf -c 98304 -ctk q8_0 -ctv q8_0 -dev CUDA0"; + # https://huggingface.co/unsloth/Qwen3-30B-A3B-Instruct-2507-GGUF/tree/main + "qwen3-30b-2507-instruct" = { + name = "Qwen3 2507 (30B) - Instruct"; + cmd = "${pkgs.reichard.llama-cpp}/bin/llama-server --port \${PORT} -m /mnt/ssd/Models/Qwen3-30B-A3B-Instruct-2507-Q4_K_M.gguf -c 262144 --temp 0.7 --min-p 0.0 --top-p 0.8 --top-k 20 --repeat-penalty 1.05 -ctk q8_0 -ctv q8_0 -ts 70,30"; + }; + + # https://huggingface.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF/tree/main + "qwen3-coder-30b-instruct" = { + name = "Qwen3 Coder (30B) - Instruct"; + cmd = "${pkgs.reichard.llama-cpp}/bin/llama-server --port \${PORT} -m /mnt/ssd/Models/Qwen3-Coder-30B-A3B-Instruct-Q4_K_M.gguf -c 262144 --temp 0.7 --min-p 0.0 --top-p 0.8 --top-k 20 --repeat-penalty 1.05 -ctk q8_0 -ctv q8_0 -ts 70,30"; + }; + + # https://huggingface.co/unsloth/Qwen3-30B-A3B-Thinking-2507-GGUF/tree/main + "qwen3-30b-2507-thinking" = { + name = "Qwen3 2507 (30B) - Thinking"; + cmd = "${pkgs.reichard.llama-cpp}/bin/llama-server --port \${PORT} -m /mnt/ssd/Models/Qwen3-30B-A3B-Thinking-2507-UD-Q4_K_XL.gguf -c 262144 --temp 0.7 --min-p 0.0 --top-p 0.8 --top-k 20 --repeat-penalty 1.05 -ctk q8_0 -ctv q8_0 -ts 70,30"; + }; + + # https://huggingface.co/mradermacher/gpt-oss-20b-heretic-v2-i1-GGUF/tree/main + # --chat-template-kwargs '{\"reasoning_effort\":\"low\"}' + "gpt-oss-20b-thinking" = { + name = "GPT OSS (20B) - Thinking"; + cmd = "${pkgs.reichard.llama-cpp}/bin/llama-server --port \${PORT} -m /mnt/ssd/Models/gpt-oss-20b-heretic-v2.i1-MXFP4_MOE.gguf -c 131072 --temp 1.0 --top-p 1.0 --top-k 40 -dev CUDA0"; }; # https://huggingface.co/unsloth/Qwen3-VL-8B-Instruct-GGUF/tree/main