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 {
ContextInjection,
HttpChatConfig,
InjectionStrategy,
OpenCodeConfig,
} from "@/domain";
/** A field-keyed validation error map (empty ⇒ valid). */
@ -20,6 +21,7 @@ export type ProfileErrors = Partial<
| "flag"
| "var"
| "endpoint"
| "baseURL"
| "model"
| "apiKeyEnv"
| "requestTimeoutMs"
@ -95,6 +97,23 @@ export function validateHttpChatConfig(c: HttpChatConfig): ProfileErrors {
return errors;
}
/**
* Validates an {@link OpenCodeConfig} draft the way the backend
* `OpenCodeConfig::new` would (base URL http/https + non-empty, model non-empty;
* the API key is a free-form, optional secret). Returns an empty object when the
* draft is valid.
*/
export function validateOpenCodeConfig(c: OpenCodeConfig): ProfileErrors {
const errors: ProfileErrors = {};
if (!isValidHttpUrl(c.baseURL)) {
errors.baseURL = "Base URL must start with http:// or https://.";
}
if (c.model.trim().length === 0) {
errors.model = "Model is required.";
}
return errors;
}
/**
* Validates a profile draft the way the backend would, so the wizard can surface
* errors before any `invoke`. Returns an empty object when the draft is valid.
@ -125,8 +144,15 @@ export function validateProfile(p: AgentProfile): ProfileErrors {
Object.assign(errors, validateHttpChatConfig(p.chatHttp));
}
}
if (p.structuredAdapter === "openCode" && p.opencode?.model !== undefined) {
if (p.opencode.model.trim().length === 0) errors.model = "Model is required.";
// An OpenCode profile carries its own llama.cpp endpoint config; the backend
// refuses to persist it unless base URL + model are well-formed, so mirror that.
if (p.structuredAdapter === "openCode") {
if (!p.opencode) {
errors.baseURL = "Base URL must start with http:// or https://.";
errors.model = "Model is required.";
} else {
Object.assign(errors, validateOpenCodeConfig(p.opencode));
}
}
return errors;
}
@ -168,6 +194,19 @@ export function defaultHttpChatConfig(): HttpChatConfig {
};
}
/**
* A sensible default {@link OpenCodeConfig} for a fresh OpenCode profile draft —
* pre-fills a local `llama-server` endpoint so the form starts valid. Mirrors the
* backend reference profile (`http://localhost:8080/v1`, `qwen3-coder-30b`).
*/
export function defaultOpenCodeConfig(): OpenCodeConfig {
return {
baseURL: "http://localhost:8080/v1",
apiKey: "sk-no-key",
model: "qwen3-coder-30b",
};
}
/** A fresh, empty custom-profile draft (id minted client-side). */
export function emptyCustomProfile(): AgentProfile {
return {