feat: Modell der Coder/Judge-Schleife per PI_CODER_MODEL wählbar
PI_CODER_MODEL=<provider>/<id> setzt das Modell für beide Rollen; ohne Variable bleibt es bei llama-cpp-single/coding_model. Die Bereitschafts- prüfung nutzt baseUrl des Modells statt fest Port 8001. Neuer Provider strata (lokaler Strata-Server, 127.0.0.1:9090) in models.json. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
0cb637783c
commit
a608f1dda8
6 changed files with 120 additions and 42 deletions
|
|
@ -144,6 +144,21 @@ Falls die GGUF-Datei an einem anderen Ort liegt:
|
|||
HF_HOME=/mnt/daten/huggingface ./start-coding-server.sh
|
||||
```
|
||||
|
||||
### Anderes Modell für die Schleife
|
||||
|
||||
Ohne weitere Angabe arbeiten Coder und Judge mit `llama-cpp-single/coding_model` (Port 8001). Ein anderes Modell aus
|
||||
`models.json` setzt die Umgebungsvariable `PI_CODER_MODEL` beim Start von pi, als `<provider>/<id>`:
|
||||
|
||||
```bash
|
||||
PI_CODER_MODEL=strata/qwen3.8-flash-next-iq3_s \
|
||||
pi --provider strata --model qwen3.8-flash-next-iq3_s
|
||||
```
|
||||
|
||||
`--provider`/`--model` machen das Modell auch für freie Eingaben zum Standard dieses Laufs; die globalen
|
||||
Einstellungen bleiben unverändert. Die Bereitschaftsprüfung fragt den Endpunkt aus `baseUrl` des Modells ab. Der
|
||||
Provider `strata` (lokaler Strata-Server auf `127.0.0.1:9090`) steht in `models.json`; den Server startet man außerhalb
|
||||
dieses Repos.
|
||||
|
||||
---
|
||||
|
||||
## 4. Neues Projekt anlegen
|
||||
|
|
|
|||
|
|
@ -48,6 +48,9 @@ nur die Persona im System-Prompt ändert sich.
|
|||
Coder- und Judge-Rollen werden durch verschiedene System-Prompts realisiert —
|
||||
dasselbe Modell, derselbe Container, unterschiedliche Persona.
|
||||
|
||||
Ein anderes Modell für beide Rollen (z. B. den lokalen Strata-Server) setzt `PI_CODER_MODEL=<provider>/<id>`,
|
||||
siehe [BEDIENUNGSANLEITUNG.md](BEDIENUNGSANLEITUNG.md#anderes-modell-für-die-schleife).
|
||||
|
||||
---
|
||||
|
||||
## Voraussetzungen
|
||||
|
|
|
|||
22
models.json
22
models.json
|
|
@ -39,6 +39,28 @@
|
|||
]
|
||||
},
|
||||
|
||||
"strata": {
|
||||
"baseUrl": "http://127.0.0.1:9090/v1",
|
||||
"api": "openai-completions",
|
||||
"apiKey": "none",
|
||||
"compat": {
|
||||
"supportsDeveloperRole": false,
|
||||
"supportsReasoningEffort": true,
|
||||
"maxTokensField": "max_tokens"
|
||||
},
|
||||
"models": [
|
||||
{
|
||||
"id": "qwen3.8-flash-next-iq3_s",
|
||||
"name": "Strata Qwen3.8-Flash-Next IQ3_S (lokal :9090)",
|
||||
"reasoning": true,
|
||||
"input": ["text"],
|
||||
"contextWindow": 262144,
|
||||
"maxTokens": 16384,
|
||||
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"openrouter": {
|
||||
"models": [
|
||||
{ "id": "qwen/qwen3-235b-a22b:free", "name": "Qwen3 235B (Free)" },
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// pi-coding-extension.ts
|
||||
// Automatisierter Coder-Judge-Fix-Workflow für AI-Coding-Assistenten
|
||||
// Ein Modell (coding_model) auf Single-Server (Port 8001); Rollen Coder/Judge via System-Prompt
|
||||
// Ein Modell (Standard: coding_model auf Single-Server :8001, sonst PI_CODER_MODEL); Rollen Coder/Judge via System-Prompt
|
||||
|
||||
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
||||
import { Type } from "typebox";
|
||||
|
|
@ -432,13 +432,30 @@ async function tickTaskMdStatus(
|
|||
);
|
||||
}
|
||||
|
||||
// Single-Server: genau ein Modell (coding_model auf :8001) für beide Rollen.
|
||||
// Single-Modell: genau ein Modell (Standard coding_model auf :8001, oder PI_CODER_MODEL) für beide Rollen.
|
||||
// Der Rollenwechsel Coder/Judge erfolgt NICHT mehr über den Modellwechsel, sondern
|
||||
// über den System-Prompt (siehe before_agent_start-Hook + currentRole). Diese Funktion
|
||||
// stellt nur sicher, dass das eine Single-Modell aktiv ist; nach dem ersten Aufruf
|
||||
// ist der Wechsel via currentModelKey gecacht und damit ein No-op.
|
||||
const SINGLE_PROVIDER = "llama-cpp-single";
|
||||
const SINGLE_MODEL_ID = "coding_model";
|
||||
// PI_CODER_MODEL="<provider>/<id>" setzt ein anderes Modell für beide Rollen (z. B. strata/qwen3.8-flash-next-iq3_s);
|
||||
// ohne die Variable bleibt es beim Single-Server auf :8001.
|
||||
const DEFAULT_SINGLE_MODEL = "llama-cpp-single/coding_model";
|
||||
const SINGLE_MODEL_SPEC = (process.env.PI_CODER_MODEL || DEFAULT_SINGLE_MODEL).trim();
|
||||
const SINGLE_PROVIDER = SINGLE_MODEL_SPEC.slice(0, SINGLE_MODEL_SPEC.indexOf("/"));
|
||||
const SINGLE_MODEL_ID = SINGLE_MODEL_SPEC.slice(SINGLE_MODEL_SPEC.indexOf("/") + 1);
|
||||
const SINGLE_LABEL = `${SINGLE_PROVIDER}/${SINGLE_MODEL_ID}`;
|
||||
|
||||
// Endpunkt des Single-Modells laut models.json, für Bereitschaftsprüfung und Meldungen.
|
||||
function singleEndpoint(ctx: ExtensionCommandContext): string {
|
||||
const model = ctx.modelRegistry.find(SINGLE_PROVIDER, SINGLE_MODEL_ID);
|
||||
return model?.baseUrl ? model.baseUrl.replace(/\/+$/, "") : "";
|
||||
}
|
||||
|
||||
function notReadyMessage(ctx: ExtensionCommandContext): string {
|
||||
const where = singleEndpoint(ctx) || SINGLE_LABEL;
|
||||
const hint = SINGLE_MODEL_SPEC === DEFAULT_SINGLE_MODEL ? "start-coding-server.sh ausführen" : "Server starten";
|
||||
return `Server nicht bereit (${where}) — ${hint}`;
|
||||
}
|
||||
|
||||
async function ensureSingleModel(
|
||||
pi: ExtensionAPI,
|
||||
|
|
@ -518,17 +535,20 @@ async function sendAndWait(
|
|||
async function waitUntilModelReady(
|
||||
pi: ExtensionAPI,
|
||||
ctx: ExtensionCommandContext,
|
||||
port: number,
|
||||
modelAlias: string,
|
||||
maxWaitMs = 180_000
|
||||
): Promise<boolean> {
|
||||
const endpoint = singleEndpoint(ctx);
|
||||
if (!endpoint) {
|
||||
ctx.ui.notify(`Modell ${SINGLE_LABEL} nicht in models.json gefunden`, "error");
|
||||
return false;
|
||||
}
|
||||
const deadline = Date.now() + maxWaitMs;
|
||||
const body = JSON.stringify({
|
||||
model: modelAlias,
|
||||
model: SINGLE_MODEL_ID,
|
||||
messages: [{ role: "user", content: "ping" }],
|
||||
max_tokens: 1, temperature: 0.0, stream: false,
|
||||
});
|
||||
// Body als Datei — verhindert Shell-Injection wenn modelAlias Sonderzeichen enthält
|
||||
// Body als Datei — verhindert Shell-Injection wenn die Modell-ID Sonderzeichen enthält
|
||||
const tmpBody = `/tmp/pi_ready_${Date.now()}_${Math.random().toString(36).slice(2)}.json`;
|
||||
await pi.exec("bash", ["-c", `printf "%s" "$1" > "${tmpBody}"`, "_", body], { cwd: ctx.cwd });
|
||||
let notified = false;
|
||||
|
|
@ -536,13 +556,13 @@ async function waitUntilModelReady(
|
|||
while (Date.now() < deadline) {
|
||||
const r = await pi.exec("bash", ["-c",
|
||||
`curl -s -o /dev/null -w "%{http_code}" --max-time 5 ` +
|
||||
`-X POST http://localhost:${port}/v1/chat/completions ` +
|
||||
`-X POST "$1/chat/completions" ` +
|
||||
`-H "Content-Type: application/json" ` +
|
||||
`-d "@${tmpBody}"`
|
||||
`-d "@${tmpBody}"`, "_", endpoint
|
||||
], { cwd: ctx.cwd });
|
||||
if (r.stdout?.trim() === "200") return true;
|
||||
if (!notified) {
|
||||
ctx.ui.notify(`Modell-Server (Port ${port}) lädt noch — warte bis zu 3 min…`, "info");
|
||||
ctx.ui.notify(`Modell-Server (${endpoint}) lädt noch — warte bis zu 3 min…`, "info");
|
||||
notified = true;
|
||||
}
|
||||
await new Promise(res => setTimeout(res, 3000));
|
||||
|
|
@ -1128,15 +1148,15 @@ export default function (pi: ExtensionAPI) {
|
|||
// ── Manuelle Kommandos ───────────────────────────────────────────────────
|
||||
|
||||
pi.registerCommand("coder", {
|
||||
description: "Implementiert <auftrag> ohne Review-Loop → Coder-Rolle (:8001).",
|
||||
description: "Implementiert <auftrag> ohne Review-Loop → Coder-Rolle (Single-Modell).",
|
||||
handler: async function (args: string, ctx: ExtensionCommandContext) {
|
||||
const task = (args || "").trim();
|
||||
if (!task) {
|
||||
ctx.ui.notify("Benutzung: /coder <auftrag>", "error");
|
||||
return;
|
||||
}
|
||||
if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
|
||||
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
|
||||
if (!await waitUntilModelReady(pi, ctx)) {
|
||||
ctx.ui.notify(notReadyMessage(ctx), "error");
|
||||
return;
|
||||
}
|
||||
await ensureLocalGitRepo(pi, ctx);
|
||||
|
|
@ -1149,10 +1169,10 @@ export default function (pi: ExtensionAPI) {
|
|||
});
|
||||
|
||||
pi.registerCommand("judge", {
|
||||
description: "Review gegen TASK.md + git show HEAD → Judge-Rolle (:8001).",
|
||||
description: "Review gegen TASK.md + git show HEAD → Judge-Rolle (Single-Modell).",
|
||||
handler: async function (args: string, ctx: ExtensionCommandContext) {
|
||||
if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
|
||||
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
|
||||
if (!await waitUntilModelReady(pi, ctx)) {
|
||||
ctx.ui.notify(notReadyMessage(ctx), "error");
|
||||
return;
|
||||
}
|
||||
await selectRole(pi, ctx, "judge");
|
||||
|
|
@ -1163,10 +1183,10 @@ export default function (pi: ExtensionAPI) {
|
|||
});
|
||||
|
||||
pi.registerCommand("fix", {
|
||||
description: "Fixt Judge-Kritik, committet Ergebnis → Coder-Rolle (:8001).",
|
||||
description: "Fixt Judge-Kritik, committet Ergebnis → Coder-Rolle (Single-Modell).",
|
||||
handler: async function (args: string, ctx: ExtensionCommandContext) {
|
||||
if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
|
||||
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
|
||||
if (!await waitUntilModelReady(pi, ctx)) {
|
||||
ctx.ui.notify(notReadyMessage(ctx), "error");
|
||||
return;
|
||||
}
|
||||
await ensureLocalGitRepo(pi, ctx);
|
||||
|
|
@ -1178,10 +1198,10 @@ export default function (pi: ExtensionAPI) {
|
|||
});
|
||||
|
||||
pi.registerCommand("shipit", {
|
||||
description: "Finale Freigabe gegen TASK.md + git log → Judge-Rolle (:8001).",
|
||||
description: "Finale Freigabe gegen TASK.md + git log → Judge-Rolle (Single-Modell).",
|
||||
handler: async function (args: string, ctx: ExtensionCommandContext) {
|
||||
if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
|
||||
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
|
||||
if (!await waitUntilModelReady(pi, ctx)) {
|
||||
ctx.ui.notify(notReadyMessage(ctx), "error");
|
||||
return;
|
||||
}
|
||||
await selectRole(pi, ctx, "judge");
|
||||
|
|
@ -1246,11 +1266,11 @@ export default function (pi: ExtensionAPI) {
|
|||
: `--continue: Überspringe Implementierung, starte direkt mit Judge-Prüfung.`;
|
||||
ctx.ui.notify(continueMsg, "info");
|
||||
|
||||
// Im --continue-Modus: Single-Server prüfen (Port 8001 — Coder + Judge in einem).
|
||||
ctx.ui.setStatus("optimize", "Single-Server wird geprüft (Port 8001)…");
|
||||
const serverReady = await waitUntilModelReady(pi, ctx, 8001, "coding_model");
|
||||
// Im --continue-Modus: Single-Modell prüfen (Coder + Judge in einem).
|
||||
ctx.ui.setStatus("optimize", `Single-Modell wird geprüft (${SINGLE_LABEL})…`);
|
||||
const serverReady = await waitUntilModelReady(pi, ctx);
|
||||
if (!serverReady) {
|
||||
finalNotify(ctx, "⛔ Server nicht erreichbar", "Port 8001 — kein HTTP 200 nach 3 min. start-coding-server.sh ausführen");
|
||||
finalNotify(ctx, "⛔ Server nicht erreichbar", `${notReadyMessage(ctx)} (kein HTTP 200 nach 3 min)`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
|
|
@ -1260,7 +1280,7 @@ export default function (pi: ExtensionAPI) {
|
|||
const taskPreview = task.length > 55 ? task.slice(0, 52) + "…" : task;
|
||||
ctx.ui.setStatus("optimize", `◉ Coder liest Anforderungen + implementiert: ${taskPreview}`);
|
||||
if (!await selectRole(pi, ctx, "coder")) {
|
||||
finalNotify(ctx, "⛔ Modell-Fehler", "Modell (llama-cpp-single) nicht verfügbar");
|
||||
finalNotify(ctx, "⛔ Modell-Fehler", `Modell (${SINGLE_LABEL}) nicht verfügbar`);
|
||||
return;
|
||||
}
|
||||
currentActivity = "Coder implementiert…";
|
||||
|
|
@ -1274,8 +1294,8 @@ export default function (pi: ExtensionAPI) {
|
|||
// GPU-Ladevorgangs und ist kein verlässliches Signal. Nur HTTP 200 auf einen
|
||||
// echten Completion-Request bedeutet: Modell ist im VRAM und bereit.
|
||||
ctx.ui.setStatus("optimize", "Single-Server wird geprüft…");
|
||||
if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
|
||||
finalNotify(ctx, "⛔ Server nicht erreichbar", "Port 8001 — kein HTTP 200 nach 3 min. start-coding-server.sh ausführen");
|
||||
if (!await waitUntilModelReady(pi, ctx)) {
|
||||
finalNotify(ctx, "⛔ Server nicht erreichbar", `${notReadyMessage(ctx)} (kein HTTP 200 nach 3 min)`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -1313,7 +1333,7 @@ export default function (pi: ExtensionAPI) {
|
|||
for (let round = 1; round <= maxRounds; round++) {
|
||||
const prog = "●".repeat(round - 1) + "◉" + "○".repeat(maxRounds - round);
|
||||
if (!await selectRole(pi, ctx, "judge")) {
|
||||
finalNotify(ctx, "⛔ Modell-Fehler", "Modell (llama-cpp-single) nicht verfügbar");
|
||||
finalNotify(ctx, "⛔ Modell-Fehler", `Modell (${SINGLE_LABEL}) nicht verfügbar`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1378,7 +1398,7 @@ export default function (pi: ExtensionAPI) {
|
|||
? (currentBlockers.length > 50 ? currentBlockers.slice(0, 47) + "…" : currentBlockers)
|
||||
: "Kritikpunkte aus Judge-Bericht";
|
||||
if (!await selectRole(pi, ctx, "coder")) {
|
||||
finalNotify(ctx, "⛔ Modell-Fehler", "Modell (llama-cpp-single) nicht verfügbar");
|
||||
finalNotify(ctx, "⛔ Modell-Fehler", `Modell (${SINGLE_LABEL}) nicht verfügbar`);
|
||||
return;
|
||||
}
|
||||
currentActivity = `Coder fixt: ${blockerHint}`;
|
||||
|
|
@ -1471,15 +1491,15 @@ export default function (pi: ExtensionAPI) {
|
|||
// ── Schlanke Kommandos für kleine Änderungen ─────────────────────────────
|
||||
|
||||
pi.registerCommand("patch", {
|
||||
description: "Gezielte Minimaländerung ohne Refactoring, committet → Coder-Rolle (:8001).",
|
||||
description: "Gezielte Minimaländerung ohne Refactoring, committet → Coder-Rolle (Single-Modell).",
|
||||
handler: async function (args: string, ctx: ExtensionCommandContext) {
|
||||
const change = (args || "").trim();
|
||||
if (!change) {
|
||||
ctx.ui.notify("Benutzung: /patch <beschreibung der änderung>", "error");
|
||||
return;
|
||||
}
|
||||
if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
|
||||
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
|
||||
if (!await waitUntilModelReady(pi, ctx)) {
|
||||
ctx.ui.notify(notReadyMessage(ctx), "error");
|
||||
return;
|
||||
}
|
||||
await ensureLocalGitRepo(pi, ctx);
|
||||
|
|
@ -1491,10 +1511,10 @@ export default function (pi: ExtensionAPI) {
|
|||
});
|
||||
|
||||
pi.registerCommand("quick_check", {
|
||||
description: "Schnelle OK/PROBLEM-Prüfung einer kleinen Codeänderung → Judge-Rolle (:8001).",
|
||||
description: "Schnelle OK/PROBLEM-Prüfung einer kleinen Codeänderung → Judge-Rolle (Single-Modell).",
|
||||
handler: async function (args: string, ctx: ExtensionCommandContext) {
|
||||
if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
|
||||
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
|
||||
if (!await waitUntilModelReady(pi, ctx)) {
|
||||
ctx.ui.notify(notReadyMessage(ctx), "error");
|
||||
return;
|
||||
}
|
||||
await selectRole(pi, ctx, "judge");
|
||||
|
|
@ -1562,8 +1582,8 @@ export default function (pi: ExtensionAPI) {
|
|||
ctx.ui.notify("Benutzung: /plan <auftrag>", "error");
|
||||
return;
|
||||
}
|
||||
if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
|
||||
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
|
||||
if (!await waitUntilModelReady(pi, ctx)) {
|
||||
ctx.ui.notify(notReadyMessage(ctx), "error");
|
||||
return;
|
||||
}
|
||||
await writeTaskMd(pi, ctx, task);
|
||||
|
|
@ -1645,8 +1665,8 @@ export default function (pi: ExtensionAPI) {
|
|||
}
|
||||
|
||||
// Standard-Verhalten: unterbrochenen Prozess wieder aufnehmen
|
||||
if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
|
||||
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
|
||||
if (!await waitUntilModelReady(pi, ctx)) {
|
||||
ctx.ui.notify(notReadyMessage(ctx), "error");
|
||||
return;
|
||||
}
|
||||
await selectRole(pi, ctx, "coder");
|
||||
|
|
|
|||
|
|
@ -15,6 +15,13 @@ test("models.json: Provider llama-cpp-single hat genau ein Modell coding_model a
|
|||
assert.deepEqual(p.models.map((m) => m.id), ["coding_model"]);
|
||||
});
|
||||
|
||||
test("models.json: Provider strata zeigt auf den lokalen Strata-Server (:9090)", () => {
|
||||
const p = models.providers["strata"];
|
||||
assert.ok(p, "Provider strata fehlt");
|
||||
assert.match(p.baseUrl, /^http:\/\/127\.0\.0\.1:9090\/v1$/);
|
||||
assert.deepEqual(p.models.map((m) => m.id), ["qwen3.8-flash-next-iq3_s"]);
|
||||
});
|
||||
|
||||
test("models.json: tote Dual-/Alt-Provider sind entfernt", () => {
|
||||
for (const dead of ["llama-cpp", "llama-cpp-coder", "llama-cpp-judge"]) {
|
||||
assert.ok(!(dead in models.providers), `Toter Provider '${dead}' noch vorhanden`);
|
||||
|
|
|
|||
|
|
@ -61,3 +61,14 @@ test("LICENCE.md: deterministisch in der Doku-Phase, nur wenn nicht vorhanden, p
|
|||
assert.match(extSource, /git config user\.name/);
|
||||
assert.match(extSource, /new Date\(\)\.getFullYear\(\)/);
|
||||
});
|
||||
|
||||
test("Single-Modell: Standard bleibt llama-cpp-single/coding_model, PI_CODER_MODEL überschreibt", () => {
|
||||
assert.match(extSource, /DEFAULT_SINGLE_MODEL = "llama-cpp-single\/coding_model"/);
|
||||
assert.match(extSource, /process\.env\.PI_CODER_MODEL \|\| DEFAULT_SINGLE_MODEL/);
|
||||
});
|
||||
|
||||
test("Bereitschaftsprüfung: Endpunkt aus models.json statt fest Port 8001", () => {
|
||||
assert.doesNotMatch(extSource, /waitUntilModelReady\(pi, ctx, 8001/);
|
||||
assert.doesNotMatch(extSource, /localhost:\$\{port\}/);
|
||||
assert.match(extSource, /function singleEndpoint\b/);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue