feat(opencode): remplace le provider Ollama par llama.cpp

Le tool-calling local ne fonctionnait jamais via Ollama. Refonte du
support local d'OpenCode autour de llama.cpp: profil, catalogue,
matérialisation de la config OpenCode et surface first-run alignés sur
llama-server (backend + frontend).

QA vert (commandes réelles): domain 244, application 81+64, infra 263
(10 échecs = bind-port sandbox identiques sur develop, non-régression),
frontend 574, tsc propre. Réserve E2E live non bloquante faute de
llama-server joignable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 00:41:19 +02:00
parent eaba05d27d
commit 4e70631c40
12 changed files with 478 additions and 140 deletions

View File

@ -9,6 +9,7 @@ import type { AgentProfile } from "@/domain";
import {
defaultHttpChatConfig,
defaultInjection,
defaultOpenCodeConfig,
emptyCustomProfile,
isProfileValid,
isRelativeSafe,
@ -16,6 +17,7 @@ import {
isValidHttpUrl,
parseArgs,
validateHttpChatConfig,
validateOpenCodeConfig,
validateProfile,
} from "./profile";
@ -189,6 +191,58 @@ describe("validateProfile for openAiCompatible profiles", () => {
});
});
describe("validateOpenCodeConfig", () => {
it("a well-formed llama.cpp config is valid", () => {
expect(validateOpenCodeConfig(defaultOpenCodeConfig())).toEqual({});
});
it("reports a bad base URL scheme and an empty model", () => {
const errors = validateOpenCodeConfig({ baseURL: "ftp://x", model: " " });
expect(errors.baseURL).toBeDefined();
expect(errors.model).toBeDefined();
});
it("an optional API key is free-form (no env-var-name constraint)", () => {
// Unlike the openAiCompatible adapter, OpenCode carries the key itself.
expect(
validateOpenCodeConfig({
baseURL: "http://localhost:8080/v1",
apiKey: "sk-secret-123",
model: "qwen3-coder-30b",
}),
).toEqual({});
});
});
describe("validateProfile for openCode profiles", () => {
function opencode(overrides: Partial<AgentProfile> = {}): AgentProfile {
return base({
name: "OpenCode + llama.cpp",
command: "opencode",
structuredAdapter: "openCode",
opencode: defaultOpenCodeConfig(),
...overrides,
});
}
it("the reference llama.cpp profile is valid", () => {
expect(validateProfile(opencode())).toEqual({});
expect(isProfileValid(opencode())).toBe(true);
});
it("surfaces opencode field errors through the profile validator", () => {
const errors = validateProfile(
opencode({ opencode: { baseURL: "nope", model: "" } }),
);
expect(errors.baseURL).toBeDefined();
expect(errors.model).toBeDefined();
expect(
isProfileValid(opencode({ opencode: { baseURL: "nope", model: "m" } })),
).toBe(false);
});
it("a missing opencode config on an openCode profile is invalid", () => {
const errors = validateProfile(opencode({ opencode: undefined }));
expect(errors.baseURL).toBeDefined();
expect(errors.model).toBeDefined();
});
});
describe("defaultInjection", () => {
it("produces a sensible default per strategy", () => {
expect(defaultInjection("conventionFile")).toEqual({