# OpenCode-Adapter Ein Wrapper, zwei Provider. `opencode-adapter.py` startet OpenCode headless und normalisiert den Lauf nach `RawResult.json`. Welcher Provider gilt, entscheidet `--provider`: | `--provider` | Modell-IDs | Betrieb | Vorlage | |---|---|---|---| | `tensorx` (Standard) | `z-ai/*`, `qwen/qwen3.8-flash-next`, `moonshotai/*` | Remote-Gateway `https://api.tensorx.ai/v1` | `opencode-tensorx.json` | | `lmstudio` | `google/gemma-4-e4b`, `qwen/qwen3.8-27b` | lokaler LM-Studio-Server `http://localhost:1234/v1` | `opencode-lmstudio.json` | `qwen/qwen3.8-flash-next` (TensorX) und `qwen/qwen3.8-27b` (lokal) teilen sich das Präfix `qwen/`. Das Präfix allein bestimmt den Adapter deshalb **nicht** – maßgeblich ist die vollständige Modell-ID gemäß dieser Tabelle. Der Abschnitt **LM Studio** unten beschreibt alles, was nur für den lokalen Betrieb gilt. Alle übrigen Abschnitte gelten für beide Provider. ## Voraussetzungen und Authentifizierung 1. OpenCode installieren und die Version protokollieren: ```powershell npm install -g opencode-ai opencode --version ``` 2. Nur für `--provider tensorx`: TensorX einmal im OpenCode-Credential-Store anmelden (`--provider lmstudio` braucht keine Anmeldung – der lokale Server prüft keinen Key): ```powershell opencode auth login opencode auth list ``` Die statische Datei `opencode-tensorx.json` enthält Provider, Basis-URL, Modelle und Effort-Varianten, aber keinen API-Key. Der Adapter liest weder Cline-Dateien noch einen Cline-Credential-Store. OpenCode löst das Credential selbst auf. Keys niemals in Laufartefakte, Prompts oder die Konfigurationsvorlage schreiben. ## Modell- und Effort-Mapping Der Adapter ergänzt intern das Providerpräfix (`tensorx/` bzw. `lmstudio/`). Aus `qwen/qwen3.8-flash-next` wird daher für OpenCode `tensorx/qwen/qwen3.8-flash-next`; im Messprotokoll bleibt die ursprüngliche TensorX-ID. Die Stufen `low`, `medium`, `high`, `xhigh` und `max` werden als OpenCode-Varianten aus der Providervorlage übergeben. Für Qwen und GLM über TensorX enthält die Variante `thinking: {type: enabled, level: ...}`; `max` wird auf `xhigh` abgebildet, falls der Provider keine eigene `max`-Stufe kennt. Nur in der Vorlage vorhandene Varianten werden per `--variant` gesetzt. **Bei `lmstudio` ist Effort nicht steuerbar.** Der OpenAI-kompatible Endpunkt von LM Studio nimmt keinen Thinking-Level entgegen; `opencode-lmstudio.json` deklariert deshalb bewusst keine Varianten. Der Adapter protokolliert das im `Adapter.log` und setzt `effort_applied: false` in `RawResult.json`. Der übergebene `--effort`-Wert bleibt als angeforderte Bedingung erhalten, ist im Protokoll aber unter „Sampling-Parameter" als **nicht steuerbar** auszuweisen – niemals so darzustellen, als hätte er gewirkt. Reasoning-Tokens liefern die Modelle trotzdem, sofern sie von sich aus mit `reasoning_content` antworten. ## Isolierte Laufkonfiguration Für jeden Lauf schreibt der Adapter `_meta/opencode-config.json` und setzt nur für den Kindprozess `OPENCODE_CONFIG` auf diese Datei. Der Aufruf verwendet `opencode run --pure`, damit keine interaktive Oberfläche benötigt wird. Der Prompt wird über stdin übergeben. Die Berechtigungen beginnen mit `deny` und erlauben gezielt: - Lesen, Suchen, Auflisten sowie eine Read-only-Shell-Allowlist; - Schreiben und externe Verzeichnisse ausschließlich für das angegebene Ergebnisverzeichnis; - im Modus `solo` keine Tasks; - im Modus `builtin` nur OpenCodes `general`- und `explore`-Subagenten; - im Modus `custom` nur Rollen aus der mit `--agents` übergebenen JSON-Datei. Für das Ergebnisziel erzeugt der Wrapper kanonische sowie zum aktiven OpenCode-Root und zur Git-Worktree-Wurzel relative Allow-Patterns. Das ist unter Windows notwendig, wenn Root und Laufverzeichnis im selben Git-Worktree liegen, das Laufverzeichnis aber außerhalb des Root-Unterordners liegt. Die Shell-Rechte sind seit Skill 12.0.0 eine **Denylist**, spiegelbildlich zum Claude-Code-Adapter: Erlaubt ist alles, gesperrt sind ausdrücklich die schreibenden und bauenden Kommandos – `rm`, `rmdir`, `mv`, `cp`, `dd`, `truncate`, `chmod`, `chown`, `ln`, `tee`, `sed -i`, die schreibenden `git`-Kommandos einschließlich `fetch`, `pull` und `remote`, `dotnet`, `msbuild`, `npm install`, `nuget` sowie `Remove-Item`, `Move-Item`, `Copy-Item`, `New-Item`, `Set-Content`, `Add-Content`, `Clear-Content`, `Out-File`, `Set-ItemProperty` und `Rename-Item`. **Die Reihenfolge ist bedeutsam.** OpenCode wertet die Regeln der Reihe nach aus; die *zuletzt passende* Regel gewinnt – nicht die spezifischste, und `deny` gewinnt nicht automatisch. Das Catch-all `"*": "allow"` muss deshalb **zuerst** stehen, die Sperren danach. Python-Dicts erhalten ihre Einfügereihenfolge, `json.dump` schreibt sie unverändert. Die frühere Allowlist konnte Kommandos nur als Präfix treffen und scheiterte deshalb an Pipelines: `Get-ChildItem ... | Format-Table ...` blieb gesperrt, obwohl `Get-ChildItem` erlaubt war. Zugleich war die Werkzeugfreiheit nicht mit der des Claude-Adapters vergleichbar. Wie beim Claude-Adapter gilt: Mustervergleich auf Kommandozeilen ist **nicht lückenlos**. Die belastbare Read-only-Garantie bleibt der Vorher/Nachher-Vergleich per `git status`; die Denylist senkt das Risiko, sie ersetzt die Verifikation nicht. Webzugriff, Skills, Rückfragen und Weiterdelegation durch Subagenten bleiben gesperrt. Bei `custom` übersetzt der Adapter `description` und `prompt` jeder Rolle in eine explizite OpenCode-Subagentenkonfiguration mit demselben Modell wie der Hauptagent. ## LM Studio (nur `--provider lmstudio`) Lokale Läufe sind eine eigene Versuchsbedingung: kein Netzzugriff, keine Providerkosten, gewichtsbezogene Reproduzierbarkeitsangaben (Quantisierung, Runtime, Kontextfenster) und ein Kontextfenster, das der Server beim Laden festlegt. ### Vorbereitung ```powershell lms server start # OpenAI-kompatibler Endpunkt auf Port 1234 lms ls # heruntergeladene Modelle lms get qwen/qwen3.8-27b # fehlendes Modell holen (mehrere GB) lms ps # geladene Instanzen samt Kontextfenster ``` ### Preflight des Adapters Vor dem Start von OpenCode prüft der Adapter über `GET /api/v0/models` und bricht mit Exitcode `2` und einer Handlungsanweisung ab, wenn eine Bedingung verletzt ist: | Prüfung | Abbruchgrund | |---|---| | Server erreichbar | `lms server start` fehlt | | Modell vorhanden | nicht heruntergeladen → `lms get ` | | `capabilities` enthält `tool_use` | ohne Tool-Calling ist kein Analyselauf möglich | | `max_context_length` ≥ `--min-context` | Modell kann die Bedingung nicht erfüllen | | Zustand `loaded` und `loaded_context_length` ≥ `--min-context` | zu kleines Fenster schneidet die Codebasis **stillschweigend** ab | | genau **eine** geladene Instanz | mehrere Instanzen (`modell`, `modell:2`) beantworten dieselbe `model`-Angabe; das Routing wäre nicht reproduzierbar | `--min-context` ist standardmäßig `32768`. Ein bewusst kleinerer Wert ist zulässig, gehört aber als abweichende Versuchsbedingung ins Protokoll. `--lmstudio-autoload` stellt den Sollzustand selbst her: Es entlädt **alle** Instanzen des Modells und lädt genau eine mit `--min-context` neu. Ohne das Flag meldet der Preflight nur den exakten `lms`-Befehl. Die Standardgröße von LM Studio (häufig 8192) reicht für eine Codebasisanalyse nicht. Das geladene Fenster wird zusätzlich als `limit.context` in die Laufkonfiguration geschrieben, damit OpenCode nicht mehr Kontext sendet, als der Server vorhält. ### Speicherhygiene und Ladeparameter Der lokale Durchsatz haengt fast vollstaendig davon ab, ob Gewichte und KV-Cache in den VRAM passen. Gemessen am 01.09.2026 auf einer RTX 5080 Laptop GPU (16.303 MiB): | Konfiguration | VRAM belegt | Generierung | |---|---:|---:| | `gemma-4-e4b`, 32k, parallel 1 | 5.162 MiB | 48,3 tok/s | | `gemma-4-e4b`, 32k, parallel 4 | 5.222 MiB | 47,7 tok/s | | `gemma-4-e4b`, **131k**, parallel 4 | 6.854 MiB | 46,8 tok/s | | `qwen3.8-27b`, 32k, `--gpu max` | 15.696 MiB (308 frei) | Abbruch nach 10 min | | beide Modelle gleichzeitig geladen | 15.836 MiB (168 frei) | 0,028 Mio. Tokens/h | Daraus die drei Regeln, die der Preflight seit Adapter 2.2.0 durchsetzt: 1. **Genau ein Modell ist geladen.** `--lmstudio-autoload` entlaedt `--all` und laedt nur das angeforderte Modell; ein fremdes geladenes Modell fuehrt sonst zum Abbruch. Ein nebenher geladenes Modell belegt VRAM, das dem Lauf fehlt, und veraendert dessen Durchsatz um Groessenordnungen. 2. **Der Kontext wird auf das Modellmaximum geladen** (`--lmstudio-context max`, Standard). Freier VRAM ist in Kontext besser investiert als ungenutzt: vierfaches Fenster fuer 1,7 GB und 1,5 tok/s. `--min-context` bleibt die Gueltigkeitsschwelle, nicht die Ladegroesse. 3. **`--lmstudio-gpu max`** erzwingt die vollstaendige Auslagerung, **`--lmstudio-parallel 4`** ist messbar kostenneutral und hilft, wenn Subagenten nebenlaeufig anfragen. **Modelle jenseits der VRAM-Grenze sind nicht messbar.** `qwen3.8-27b` belegt mit 17,74 GB Gewichten mehr, als die Karte hat; der Rest laeuft auf der CPU. Auch eine kleinere Quantisierung loest das nicht, weil der KV-Cache eines 27B-Modells bei brauchbarem Kontext mehrere GB zusaetzlich fordert. Fuer eine 16-GB-Karte ist die 9B-Klasse die groesste, die mit vollem Fenster hineinpasst - dann aber in hoher Quantisierung, um den Speicher zu nutzen. ### Zusätzliche Laufartefakte und Messfelder | Datei | Inhalt | |---|---| | `_meta/lmstudio-modelle.json` | Rohantwort von `/api/v0/models` zum Zeitpunkt des Preflights | `RawResult.json` enthält bei lokalen Läufen zusätzlich: | Messgröße | Feld | |---|---| | Kontextfenster des Laufs | `context_window` (= `local_runtime.loaded_context_length`) | | Quantisierungsstufe | `local_runtime.quantization` | | Runtime und Architektur | `local_runtime.compatibility_type`, `local_runtime.arch` | | Runtime-Version | `local_runtime.lms_version` | | Instanzbezeichner | `local_runtime.instance_id` | | Endpunkt | `local_runtime.base_url` | | Kosten | `cost` ist `0`; `cost_source` weist „nicht erfasst (lokaler Betrieb)" aus | | Effortwirkung | `effort_applied` ist `false` | Damit sind die von Kapitel 4.3 geforderten Angaben für lokalen Betrieb – Runtime samt Version und Quantisierungsstufe – vollständig erfasst. ### Laufzeitverhalten Kleine lokale Modelle beenden eine Aufgabe nicht zuverlässig von selbst; im Smoke-Test lief `google/gemma-4-e4b` über 50 Schritte weiter, ohne die geforderte Datei zu schreiben. Der Stall-Timeout greift dabei **nicht**, weil laufend Text erzeugt wird. Für lokale Läufe deshalb immer ein absolutes `--max-runtime` setzen und einen Abbruch als solchen protokollieren, statt ihn als Ergebnis zu werten. ## Aufruf ```powershell $skillDir = "" $lauf = "" $root = "" $provider = "" $modell = "" $effort = "" $modus = "" $agents = "" python "$skillDir\opencode-adapter.py" ` --prompt "$lauf\_meta\combined_prompt.md" ` --root $root ` --output "$lauf\Ergebnisse" ` --provider $provider ` --model $modell ` --effort $effort ` --mode $modus ` --agents $agents ` --stall-timeout 600 ` --max-runtime 0 ` --result-dir $lauf ` --title "run-experiment $modell $modus $effort" ``` Für `--provider lmstudio` kommen hinzu: ```powershell --min-context 32768 ` # Mindestgröße des geladenen Kontextfensters --lmstudio-autoload ` # Modell notfalls selbst neu laden --max-runtime 3600 # absolutes Limit; lokale Modelle terminieren nicht zuverlässig ``` `--base-url` (Standard `http://localhost:1234`) und `--lms` (Pfad zur CLI) sind nur nötig, wenn Port oder Installationsort abweichen. `--agents` bei `solo` und `builtin` weglassen. `--stall-timeout 600` beendet den gesamten OpenCode-Prozessbaum, wenn zehn Minuten lang weder stdout noch stderr Aktivität zeigen. `--stall-timeout 0` deaktiviert diese Sicherung. **In den Modi `builtin` und `custom` ist `--stall-timeout 0` zwingend.** OpenCode sendet **keine Ereignisse, solange ein Subagent arbeitet** – der Ereignisstrom schweigt für dessen gesamte Laufzeit. Jeder positive Stall-Timeout bricht den Lauf deshalb ab, sobald der erste Subagent startet, und erzeugt eine Fehlmessung, die wie ein Hänger aussieht. Der Adapter lehnt diese Kombination seit Version 1.3.0 mit einer Fehlermeldung ab, statt sie stillschweigend zu korrigieren. Die Laufzeit wird in diesen Modi ausschließlich über `--max-runtime` begrenzt. `--max-runtime 0` setzt kein absolutes Laufzeitlimit. `Ctrl+C` beendet ebenfalls den Prozessbaum und persistiert soweit möglich das Teilergebnis. Ein leeres `Ergebnisse`-Verzeichnis macht den Lauf standardmäßig zu einem Fehler. Nur ein bewusst textueller Smoke-Test darf diese Prüfung mit `--allow-empty-output` abschalten. ## Laufartefakte und Messfelder | Datei | Inhalt | |---|---| | `OpenCodeEvents.jsonl` | unveränderter, inkrementell geschriebener JSON-Ereignisstrom | | `OpenCode.log` | OpenCode-stderr, inkrementell geschrieben | | `Adapter.log` | Start, Lebenszyklus, Abbruchgrund und Abschluss des Wrappers | | `_meta/opencode-config.json` | tatsächlich verwendete, keyfreie Laufkonfiguration | | `_meta/opencode-session.json` | exportierte Session, soweit eine Session-ID vorliegt | | `RawResult.json` | normalisierte Metriken für das gemeinsame Messprotokoll | Aus `RawResult.json` verwenden: | Messgröße | Feld | |---|---| | Erfolg/Abbruch | `is_error`, `subtype`, `timed_out`, `interrupted`, `exit_code`, `errors` | | Modellkontrolle | `provider`, `model`, `model_requested` | | Zeit | `duration_ms`, `start_time`, `end_time` | | Tokens | `usage.prompt_tokens`, `completion_tokens`, `reasoning_tokens`, `cache_read_tokens`, `cache_creation_tokens`, `total_tokens` | | Turns und Ende | `num_turns`, `finish_reason` | | Tools | `tool_call_count`, `tool_call_types`, `tool_calls` | | Subagenten | `subagent_stats`, `subagent_details` | | Ergebnisdateien | `written_files` | | Abschlusstext | `result` | | Reproduzierbarkeit | `adapter_version`, `opencode_version`, `config_path`, `session_id`, `adapter` | | Effortwirkung | `effort`, `effort_applied` | | Lokaler Betrieb | `local_runtime`, `context_window`, `cost_source` (nur `lmstudio`) | `usage.total_tokens` übernimmt nach Möglichkeit OpenCodes Gesamtwert. Fehlt dieser, addiert der Adapter Input, Output, Reasoning, Cache-Read und Cache-Write aus den gelieferten OpenCode-Feldern. Kosten werden nur protokolliert, wenn OpenCode sie liefert; nicht schätzen. Bei `lmstudio` entstehen keine Providerkosten – `cost` ist definitionsgemäß `0`, nicht „unbekannt". Cache-Read und Cache-Write liefert der lokale Server nicht; sie sind im Protokoll als `nicht erfasst` auszuweisen. ## Pflichtprüfung 1. `RawResult.json` existiert und `is_error` ist `false`. 2. `timed_out` und `interrupted` sind `false`; `exit_code` ist `0`. 3. `OpenCode.log` und `errors` enthalten keinen Provider- oder Permissionfehler. 4. Für Analyseversuche ist `Ergebnisse/` nicht leer und `tool_call_count` größer null. 5. `model_requested` entspricht der angeforderten Modell-ID; `provider` entspricht `--provider`. 6. Bei `custom` sind die erwarteten Rollen in `subagent_stats.by_type` nachweisbar. 7. Bei `lmstudio` zusätzlich: `local_runtime.loaded_context_length` ≥ `--min-context`, `local_runtime.quantization` und `local_runtime.lms_version` sind gefüllt, und `effort_applied` ist `false` (im Protokoll als nicht steuerbar vermerken). Ein absichtlich textueller Smoke-Test darf von den Prüfungen 4 und 6 abweichen, muss aber Antwort, Exitcode, Modell, Sessionexport und Tokenmetriken bestätigen.