# 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 kleine 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. 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. ### 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. `--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.