Files
2026-09-01 08:12:07 +02:00

16 KiB
Raw Permalink Blame History

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:

    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):

    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

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 <ID>
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

$skillDir = "<Verzeichnis des Skills>"
$lauf     = "<absoluter Pfad zum Laufverzeichnis>"
$root     = "<Root-Verzeichnis der Codebasis>"
$provider = "<tensorx|lmstudio>"
$modell   = "<Modell-ID>"
$effort   = "<low|medium|high|xhigh|max>"
$modus    = "<solo|builtin|custom>"
$agents   = "<Agenten-JSON; nur bei custom>"

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:

  --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.