Der TensorX-Wrapper wird providerneutral: opencode-tensorx-adapter.py heisst
jetzt opencode-adapter.py und waehlt ueber --provider {tensorx,lmstudio}
Gateway und Modellvorlage. Der TensorX-Pfad bleibt unveraendert; die vier
bestehenden Regressionstests laufen durch.
Neu fuer den lokalen Betrieb:
- opencode-lmstudio.json fuer google/gemma-4-e4b und qwen/qwen3.8-27b
- Preflight ueber /api/v0/models: Servererreichbarkeit, Modellverfuegbarkeit,
tool_use-Faehigkeit, geladenes Kontextfenster (--min-context, Standard 32768)
und genau eine geladene Instanz; --lmstudio-autoload stellt das selbst her
- local_runtime in RawResult.json (Quantisierung, Architektur, Runtime,
lms-Version, Instanzbezeichner, Kontextfenster) fuer Kap. 4.3
- effort_applied, da der lokale Endpunkt keinen Thinking-Level annimmt
Drei Befunde aus der Inbetriebnahme, alle im Adapter abgefangen: LM Studio
laedt standardmaessig nur 8192 Kontexttokens; ein erneutes lms load erzeugt
eine zweite Instanz und macht das Routing mehrdeutig; Effort ist lokal
wirkungslos. Dazu zwei Korrekturen am gemeinsamen Pfad (Abbruchgrund nur
einmal in errors, saubere lms-Versionskennung).
Enthaelt ausserdem die bislang nicht committeten Laeufe der Iterationen 8
und 9 sowie Versuch 2 (Iterationen 1 bis 3). Der laufende Lauf unter
Iteration 10 ist bewusst nicht enthalten.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
288 lines
10 KiB
Python
288 lines
10 KiB
Python
import importlib.util
|
|
import json
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
|
|
ADAPTER_PATH = Path(__file__).with_name("opencode-adapter.py")
|
|
SPEC = importlib.util.spec_from_file_location("opencode_adapter", ADAPTER_PATH)
|
|
ADAPTER = importlib.util.module_from_spec(SPEC)
|
|
SPEC.loader.exec_module(ADAPTER)
|
|
|
|
|
|
class OpenCodeAdapterTests(unittest.TestCase):
|
|
def test_model_reference_keeps_upstream_slashes(self):
|
|
self.assertEqual(
|
|
(
|
|
"tensorx/qwen/qwen3.8-flash-next",
|
|
"qwen/qwen3.8-flash-next",
|
|
),
|
|
ADAPTER.normalize_model("qwen/qwen3.8-flash-next"),
|
|
)
|
|
self.assertEqual(
|
|
(
|
|
"tensorx/qwen/qwen3.8-flash-next",
|
|
"qwen/qwen3.8-flash-next",
|
|
),
|
|
ADAPTER.normalize_model("tensorx/qwen/qwen3.8-flash-next"),
|
|
)
|
|
|
|
def test_custom_mode_translates_agents_and_restricts_delegation(self):
|
|
base = {
|
|
"provider": {
|
|
"tensorx": {
|
|
"models": {"qwen/qwen3.8-flash-next": {"name": "Qwen"}}
|
|
}
|
|
}
|
|
}
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
temp = Path(temp_dir)
|
|
root = temp / "root"
|
|
output = root / "run" / "Ergebnisse"
|
|
root.mkdir()
|
|
output.mkdir(parents=True)
|
|
agents = temp / "agents.json"
|
|
agents.write_text(
|
|
json.dumps(
|
|
{
|
|
"reviewer": {
|
|
"description": "Prueft Fakten",
|
|
"prompt": "Pruefe nur die zugewiesenen Fakten.",
|
|
}
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
config = ADAPTER.build_run_config(
|
|
base,
|
|
"tensorx/qwen/qwen3.8-flash-next",
|
|
"qwen/qwen3.8-flash-next",
|
|
"custom",
|
|
root,
|
|
output,
|
|
agents,
|
|
)
|
|
|
|
self.assertEqual("allow", config["permission"]["task"]["reviewer"])
|
|
self.assertEqual("deny", config["permission"]["task"]["*"])
|
|
self.assertEqual("subagent", config["agent"]["reviewer"]["mode"])
|
|
self.assertEqual(
|
|
"tensorx/qwen/qwen3.8-flash-next",
|
|
config["agent"]["reviewer"]["model"],
|
|
)
|
|
self.assertEqual("deny", config["agent"]["reviewer"]["permission"]["task"])
|
|
self.assertIn(
|
|
ADAPTER.normalized_path(output) + "/**",
|
|
config["permission"]["edit"],
|
|
)
|
|
self.assertIn(
|
|
"run/Ergebnisse/**",
|
|
config["permission"]["edit"],
|
|
)
|
|
|
|
def test_output_permissions_include_parent_relative_worktree_path(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
temp = Path(temp_dir)
|
|
root = temp / "QuellCode" / "Product"
|
|
output = temp / "Versuche" / "Lauf" / "Ergebnisse"
|
|
root.mkdir(parents=True)
|
|
output.mkdir(parents=True)
|
|
|
|
patterns = ADAPTER.output_permission_patterns(root, output, temp)
|
|
|
|
self.assertIn("Versuche/Lauf/Ergebnisse/**", patterns)
|
|
self.assertIn("../../Versuche/Lauf/Ergebnisse", patterns)
|
|
self.assertIn("../../Versuche/Lauf/Ergebnisse/**", patterns)
|
|
self.assertIn(ADAPTER.normalized_path(output) + "/**", patterns)
|
|
|
|
def test_normalize_result_uses_exported_metrics_and_tool_calls(self):
|
|
session = {
|
|
"info": {
|
|
"id": "ses_test",
|
|
"version": "1.18.25",
|
|
"model": {
|
|
"id": "qwen/qwen3.8-flash-next",
|
|
"providerID": "tensorx",
|
|
},
|
|
"tokens": {
|
|
"input": 100,
|
|
"output": 10,
|
|
"reasoning": 5,
|
|
"cache": {"read": 20, "write": 0},
|
|
},
|
|
"cost": 0.1,
|
|
},
|
|
"messages": [
|
|
{
|
|
"info": {"role": "assistant", "finish": "stop"},
|
|
"parts": [
|
|
{
|
|
"type": "tool",
|
|
"tool": "task",
|
|
"state": {
|
|
"status": "completed",
|
|
"input": {
|
|
"subagent_type": "reviewer",
|
|
"description": "Pruefen",
|
|
},
|
|
},
|
|
},
|
|
{"type": "text", "text": "Fertig"},
|
|
],
|
|
}
|
|
],
|
|
}
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
output = Path(temp_dir)
|
|
(output / "StRS.md").write_text("Inhalt", encoding="utf-8")
|
|
result = ADAPTER.normalize_result(
|
|
session=session,
|
|
events=[],
|
|
model_ref="tensorx/qwen/qwen3.8-flash-next",
|
|
mode="custom",
|
|
effort="low",
|
|
exit_code=0,
|
|
timed_out=False,
|
|
interrupted=False,
|
|
duration_s=1.5,
|
|
output_dir=output,
|
|
errors=[],
|
|
)
|
|
|
|
self.assertFalse(result["is_error"])
|
|
self.assertEqual(135, result["usage"]["total_tokens"])
|
|
self.assertEqual(1, result["tool_call_count"])
|
|
self.assertEqual(1, result["subagent_stats"]["spawned"])
|
|
self.assertEqual({"reviewer": 1}, result["subagent_stats"]["by_type"])
|
|
self.assertEqual("Fertig", result["result"])
|
|
self.assertEqual(1, len(result["written_files"]))
|
|
|
|
|
|
class OpenCodeLmStudioTests(unittest.TestCase):
|
|
def test_model_reference_uses_lmstudio_provider(self):
|
|
self.assertEqual(
|
|
("lmstudio/google/gemma-4-e4b", "google/gemma-4-e4b"),
|
|
ADAPTER.normalize_model("google/gemma-4-e4b", "lmstudio"),
|
|
)
|
|
self.assertEqual(
|
|
("lmstudio/qwen/qwen3.8-27b", "qwen/qwen3.8-27b"),
|
|
ADAPTER.normalize_model("lmstudio/qwen/qwen3.8-27b", "lmstudio"),
|
|
)
|
|
|
|
def test_template_declares_both_local_models_without_key(self):
|
|
template = json.loads(
|
|
(ADAPTER_PATH.parent / "opencode-lmstudio.json").read_text(
|
|
encoding="utf-8-sig"
|
|
)
|
|
)
|
|
provider = template["provider"]["lmstudio"]
|
|
self.assertEqual(
|
|
"http://localhost:1234/v1", provider["options"]["baseURL"]
|
|
)
|
|
self.assertEqual(
|
|
{"google/gemma-4-e4b", "qwen/qwen3.8-27b"},
|
|
set(provider["models"]),
|
|
)
|
|
# Lokale Server pruefen den Key nicht; er darf nur ein Platzhalter sein.
|
|
self.assertEqual("lm-studio", provider["options"]["apiKey"])
|
|
|
|
def test_build_run_config_pins_loaded_context_window(self):
|
|
base = json.loads(
|
|
(ADAPTER_PATH.parent / "opencode-lmstudio.json").read_text(
|
|
encoding="utf-8-sig"
|
|
)
|
|
)
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
root = Path(temp_dir) / "root"
|
|
output = root / "run" / "Ergebnisse"
|
|
output.mkdir(parents=True)
|
|
config = ADAPTER.build_run_config(
|
|
base,
|
|
"lmstudio/google/gemma-4-e4b",
|
|
"google/gemma-4-e4b",
|
|
"solo",
|
|
root,
|
|
output,
|
|
None,
|
|
provider="lmstudio",
|
|
context_limit=32768,
|
|
)
|
|
|
|
model = config["provider"]["lmstudio"]["models"]["google/gemma-4-e4b"]
|
|
self.assertEqual(32768, model["limit"]["context"])
|
|
self.assertEqual("deny", config["permission"]["task"])
|
|
self.assertEqual("deny", config["permission"]["webfetch"])
|
|
|
|
def test_normalize_result_reports_local_runtime_and_zero_cost(self):
|
|
runtime = {
|
|
"provider": "lmstudio",
|
|
"quantization": "Q4_K_M",
|
|
"arch": "gemma4",
|
|
"compatibility_type": "gguf",
|
|
"loaded_context_length": 32768,
|
|
"max_context_length": 131072,
|
|
}
|
|
session = {
|
|
"info": {
|
|
"id": "ses_local",
|
|
"model": {"id": "google/gemma-4-e4b"},
|
|
"tokens": {"input": 10, "output": 4, "reasoning": 2,
|
|
"cache": {"read": 0, "write": 0}},
|
|
"cost": 0,
|
|
},
|
|
"messages": [
|
|
{
|
|
"info": {"role": "assistant", "finish": "stop"},
|
|
"parts": [{"type": "text", "text": "Fertig"}],
|
|
}
|
|
],
|
|
}
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
output = Path(temp_dir)
|
|
(output / "StRS.md").write_text("Inhalt", encoding="utf-8")
|
|
result = ADAPTER.normalize_result(
|
|
session=session,
|
|
events=[],
|
|
model_ref="lmstudio/google/gemma-4-e4b",
|
|
mode="solo",
|
|
effort="high",
|
|
exit_code=0,
|
|
timed_out=False,
|
|
interrupted=False,
|
|
duration_s=2.0,
|
|
output_dir=output,
|
|
errors=[],
|
|
provider="lmstudio",
|
|
effort_applied=False,
|
|
local_runtime=runtime,
|
|
)
|
|
|
|
self.assertEqual("lmstudio", result["provider"])
|
|
self.assertEqual("opencode-lmstudio", result["adapter"])
|
|
self.assertFalse(result["effort_applied"])
|
|
self.assertEqual(32768, result["context_window"])
|
|
self.assertEqual("Q4_K_M", result["local_runtime"]["quantization"])
|
|
self.assertEqual(0, result["cost"])
|
|
self.assertIn("lokaler Betrieb", result["cost_source"])
|
|
self.assertEqual(16, result["usage"]["total_tokens"])
|
|
|
|
def test_tensorx_result_keeps_remote_shape(self):
|
|
session = {"info": {"model": {"id": "qwen/qwen3.8-flash-next"},
|
|
"tokens": {"input": 1, "output": 1}}, "messages": []}
|
|
result = ADAPTER.normalize_result(
|
|
session=session, events=[], model_ref="tensorx/qwen/qwen3.8-flash-next",
|
|
mode="solo", effort="low", exit_code=0, timed_out=False,
|
|
interrupted=False, duration_s=1.0, output_dir=Path("."), errors=[],
|
|
)
|
|
self.assertEqual("tensorx", result["provider"])
|
|
self.assertEqual("opencode-tensorx", result["adapter"])
|
|
self.assertTrue(result["effort_applied"])
|
|
self.assertNotIn("local_runtime", result)
|
|
self.assertNotIn("context_window", result)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|