neues modell
This commit is contained in:
@@ -36,7 +36,7 @@ from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ADAPTER_VERSION = "1.1.0"
|
||||
ADAPTER_VERSION = "2.2.0"
|
||||
DEFAULT_PROVIDER = "tensorx"
|
||||
PROVIDER_ID = DEFAULT_PROVIDER
|
||||
PROVIDERS: dict[str, dict] = {
|
||||
@@ -155,20 +155,78 @@ def output_permission_patterns(
|
||||
return patterns
|
||||
|
||||
|
||||
# Schreibende und bauende Kommandos, spiegelbildlich zur Denylist des
|
||||
# Claude-Code-Adapters. Reihenfolge ist bedeutsam: OpenCode wertet die Regeln
|
||||
# der Reihe nach aus, die zuletzt passende gewinnt. Das Catch-all steht deshalb
|
||||
# zuerst, die Sperren danach.
|
||||
SHELL_DENY_COMMANDS = (
|
||||
# Dateien loeschen, verschieben, ueberschreiben (POSIX)
|
||||
"rm *",
|
||||
"rmdir *",
|
||||
"mv *",
|
||||
"cp *",
|
||||
"dd *",
|
||||
"truncate *",
|
||||
"chmod *",
|
||||
"chown *",
|
||||
"ln *",
|
||||
"tee *",
|
||||
"sed -i*",
|
||||
# Git, schreibend
|
||||
"git checkout*",
|
||||
"git restore*",
|
||||
"git clean*",
|
||||
"git reset*",
|
||||
"git add*",
|
||||
"git commit*",
|
||||
"git push*",
|
||||
"git fetch*",
|
||||
"git pull*",
|
||||
"git remote*",
|
||||
# Bauen und Paketverwaltung
|
||||
"dotnet *",
|
||||
"msbuild *",
|
||||
"npm install*",
|
||||
"nuget *",
|
||||
# PowerShell, schreibend
|
||||
"Remove-Item *",
|
||||
"Move-Item *",
|
||||
"Copy-Item *",
|
||||
"New-Item *",
|
||||
"Set-Content *",
|
||||
"Add-Content *",
|
||||
"Clear-Content *",
|
||||
"Out-File *",
|
||||
"Set-ItemProperty *",
|
||||
"Rename-Item *",
|
||||
)
|
||||
|
||||
|
||||
def readonly_shell_permissions() -> dict[str, str]:
|
||||
return {
|
||||
"*": "deny",
|
||||
"rg *": "allow",
|
||||
"git status*": "allow",
|
||||
"git ls-files*": "allow",
|
||||
"git rev-parse*": "allow",
|
||||
"Get-ChildItem *": "allow",
|
||||
"Get-Content *": "allow",
|
||||
"Select-String *": "allow",
|
||||
"Test-Path *": "allow",
|
||||
"Resolve-Path *": "allow",
|
||||
"where.exe *": "allow",
|
||||
}
|
||||
"""Denylist schreibender und bauender Shell-Kommandos.
|
||||
|
||||
Ab Skill 12.0.0 eine **Denylist** statt der vorherigen Allowlist: Alles ist
|
||||
erlaubt, ausser den ausdruecklich gesperrten schreibenden und bauenden
|
||||
Kommandos. Damit entspricht die Werkzeugfreiheit der des Claude-Code-
|
||||
Adapters, der ebenfalls mit einer Denylist arbeitet - Voraussetzung dafuer,
|
||||
dass Laeufe beider Adapter als Werkzeugvergleich lesbar sind.
|
||||
|
||||
Die Allowlist konnte Kommandos nur als Praefix treffen und scheiterte
|
||||
deshalb an Pipelines: ``Get-ChildItem ... | Format-Table ...`` blieb
|
||||
gesperrt, obwohl ``Get-ChildItem`` erlaubt war.
|
||||
|
||||
OpenCode wertet die Regeln der Reihe nach aus; die **zuletzt passende Regel
|
||||
gewinnt**. Das Catch-all muss deshalb zuerst stehen. Python-Dicts erhalten
|
||||
ihre Einfuegereihenfolge, ``json.dump`` schreibt sie unveraendert.
|
||||
|
||||
Wie beim Claude-Adapter gilt: Mustervergleich auf Kommandozeilen ist nicht
|
||||
lueckenlos. Die belastbare Read-only-Garantie bleibt der Vorher/Nachher-
|
||||
Vergleich per ``git status``; die Denylist senkt das Risiko, sie ersetzt
|
||||
die Verifikation nicht.
|
||||
"""
|
||||
permissions = {"*": "allow"}
|
||||
permissions.update({muster: "deny" for muster in SHELL_DENY_COMMANDS})
|
||||
return permissions
|
||||
|
||||
|
||||
def task_permissions(mode: str, custom_names: list[str]) -> str | dict[str, str]:
|
||||
@@ -507,6 +565,27 @@ def normalize_result(
|
||||
"exit_code": exit_code,
|
||||
}
|
||||
|
||||
# Ein laufender Subagent taucht in der exportierten Session weder mit
|
||||
# eigenen Nachrichten noch mit Tokens auf. Wird der Lauf abgebrochen,
|
||||
# waehrend ein `task` noch laeuft, meldet OpenCode deshalb 0 Tokens - obwohl
|
||||
# gearbeitet wurde. Diese Null ist kein Messwert und darf nicht als solcher
|
||||
# ins Protokoll wandern.
|
||||
laufende_tasks = [
|
||||
tool
|
||||
for tool in tools
|
||||
if tool["name"] in ("task", "subagent") and tool["status"] == "running"
|
||||
]
|
||||
result["usage_captured"] = not (total_tokens == 0 and (tools or assistants))
|
||||
if not result["usage_captured"]:
|
||||
result["usage_note"] = (
|
||||
"nicht erfasst: OpenCode weist der Session keine Tokens zu"
|
||||
+ (
|
||||
f"; {len(laufende_tasks)} Subagent(en) liefen beim Abbruch noch"
|
||||
if laufende_tasks
|
||||
else ""
|
||||
)
|
||||
)
|
||||
|
||||
if local_runtime is not None:
|
||||
result["local_runtime"] = local_runtime
|
||||
result["context_window"] = local_runtime.get("loaded_context_length", 0)
|
||||
@@ -624,14 +703,39 @@ def run_lms(lms: Path, arguments: list[str], log, timeout: int = 1800) -> None:
|
||||
|
||||
|
||||
def lmstudio_reload_model(
|
||||
lms: Path, model: str, context_length: int, loaded_ids: list[str], log
|
||||
lms: Path,
|
||||
model: str,
|
||||
context_length: int,
|
||||
log,
|
||||
parallel: int = 4,
|
||||
gpu: str = "max",
|
||||
) -> None:
|
||||
"""Alle Instanzen des Modells entladen und genau eine neu laden."""
|
||||
for identifier in loaded_ids:
|
||||
run_lms(lms, ["unload", identifier], log, timeout=300)
|
||||
"""Saemtliche Modelle entladen und genau das angeforderte neu laden.
|
||||
|
||||
Entladen wird **alles**, nicht nur andere Instanzen desselben Modells: Ein
|
||||
nebenher geladenes zweites Modell belegt VRAM, das dem Lauf dann fehlt.
|
||||
Gemessen wurde der Extremfall - Gemma und Qwen 27B gleichzeitig geladen,
|
||||
15.836 von 16.303 MiB belegt, der Lauf fiel auf Bruchteile seines
|
||||
Durchsatzes zurueck.
|
||||
|
||||
``parallel=1`` gibt dem einen Agentenlauf den gesamten KV-Cache; jeder
|
||||
weitere Slot teilt ihn auf, ohne dass ein Einzellauf davon profitiert.
|
||||
``gpu=max`` erzwingt die vollstaendige Auslagerung auf die GPU.
|
||||
"""
|
||||
run_lms(lms, ["unload", "--all"], log, timeout=300)
|
||||
run_lms(
|
||||
lms,
|
||||
["load", model, "--context-length", str(context_length), "--yes"],
|
||||
[
|
||||
"load",
|
||||
model,
|
||||
"--context-length",
|
||||
str(context_length),
|
||||
"--parallel",
|
||||
str(parallel),
|
||||
"--gpu",
|
||||
gpu,
|
||||
"--yes",
|
||||
],
|
||||
log,
|
||||
)
|
||||
|
||||
@@ -644,6 +748,9 @@ def lmstudio_preflight(
|
||||
lms_path: str | None,
|
||||
catalog_dump: Path,
|
||||
log,
|
||||
parallel: int = 4,
|
||||
gpu: str = "max",
|
||||
context_target: str = "max",
|
||||
) -> dict:
|
||||
"""Prueft den lokalen Server und liefert die Runtime-Metadaten des Laufs.
|
||||
|
||||
@@ -673,6 +780,21 @@ def lmstudio_preflight(
|
||||
if item.get("state") == "loaded"
|
||||
]
|
||||
|
||||
def fremde_geladene(entries: list[dict]) -> list[str]:
|
||||
"""Geladene Modelle, die nicht das angeforderte sind.
|
||||
|
||||
Sie belegen VRAM, das dem Lauf fehlt, und veraendern damit dessen
|
||||
Durchsatz - eine Versuchsbedingung, die nicht unbemerkt bleiben darf.
|
||||
"""
|
||||
eigene = {str(item.get("id", "")) for item in lmstudio_instances(entries, model)}
|
||||
return [
|
||||
str(item.get("id", ""))
|
||||
for item in entries
|
||||
if item.get("state") == "loaded"
|
||||
and str(item.get("id", "")) not in eigene
|
||||
and item.get("type") != "embeddings"
|
||||
]
|
||||
|
||||
def select(entries: list[dict]) -> dict | None:
|
||||
instances = lmstudio_instances(entries, model)
|
||||
if not instances:
|
||||
@@ -720,6 +842,7 @@ def lmstudio_preflight(
|
||||
entry.get("state") != "loaded"
|
||||
or loaded_context < min_context
|
||||
or len(loaded_ids(catalog)) > 1
|
||||
or bool(fremde_geladene(catalog))
|
||||
)
|
||||
if needs_reload and autoload:
|
||||
if lms is None:
|
||||
@@ -727,8 +850,19 @@ def lmstudio_preflight(
|
||||
"--lmstudio-autoload benoetigt die 'lms'-CLI; sie wurde weder im "
|
||||
"PATH noch unter ~/.lmstudio/bin gefunden."
|
||||
)
|
||||
target_context = min(min_context, max_context) if max_context else min_context
|
||||
lmstudio_reload_model(lms, model, target_context, loaded_ids(catalog), log)
|
||||
# Freien VRAM in Kontext investieren statt verfallen lassen: Gemessen
|
||||
# kostet das Modellmaximum kaum Speicher und kein Tempo (gemma-4-e4b:
|
||||
# 131.072 statt 32.768 Kontext -> 6.854 statt 5.162 MiB, 46,8 statt
|
||||
# 48,3 tok/s). Ein groesseres Fenster ist fuer eine Codebasisanalyse
|
||||
# unmittelbar wirksam.
|
||||
if context_target == "max":
|
||||
target_context = max_context or min_context
|
||||
else:
|
||||
target_context = int(context_target)
|
||||
target_context = max(target_context, min_context)
|
||||
lmstudio_reload_model(
|
||||
lms, model, target_context, log, parallel=parallel, gpu=gpu
|
||||
)
|
||||
catalog = lmstudio_catalog(base_url)
|
||||
catalog_dump.write_text(
|
||||
json.dumps(catalog, indent=2, ensure_ascii=False), encoding="utf-8"
|
||||
@@ -749,6 +883,15 @@ def lmstudio_preflight(
|
||||
"Codebasis stillschweigend ab. Neu laden mit: "
|
||||
f"lms load {model} --context-length {min_context} --yes"
|
||||
)
|
||||
fremde = fremde_geladene(catalog)
|
||||
if fremde:
|
||||
raise RuntimeError(
|
||||
"Neben '" + model + "' sind weitere Modelle geladen ("
|
||||
+ ", ".join(fremde)
|
||||
+ "). Sie belegen VRAM, das dem Lauf fehlt, und veraendern dessen "
|
||||
"Durchsatz. Mit 'lms unload --all' entladen oder den Adapter mit "
|
||||
"--lmstudio-autoload aufrufen."
|
||||
)
|
||||
instances = loaded_ids(catalog)
|
||||
if len(instances) != 1:
|
||||
raise RuntimeError(
|
||||
@@ -772,6 +915,9 @@ def lmstudio_preflight(
|
||||
"capabilities": capabilities,
|
||||
"max_context_length": max_context,
|
||||
"loaded_context_length": loaded_context,
|
||||
"parallel_slots": parallel,
|
||||
"gpu_offload": gpu,
|
||||
"alleiniges_modell": True,
|
||||
}
|
||||
log(
|
||||
"LM-Studio-Preflight bestanden: "
|
||||
@@ -811,6 +957,29 @@ def main() -> int:
|
||||
default=LMSTUDIO_MIN_CONTEXT,
|
||||
help="Mindestgroesse des geladenen Kontextfensters (nur lmstudio)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--lmstudio-parallel",
|
||||
type=int,
|
||||
default=4,
|
||||
help=(
|
||||
"Gleichzeitige Vorhersage-Slots beim Laden (nur lmstudio). Jeder "
|
||||
"Slot teilt den KV-Cache; ein einzelner Agentenlauf profitiert von "
|
||||
"1 und verliert bei mehr."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--lmstudio-context",
|
||||
default="max",
|
||||
help=(
|
||||
"Kontextfenster beim Laden: 'max' fuer das Modellmaximum oder eine "
|
||||
"Tokenzahl (nur lmstudio). Nie kleiner als --min-context."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--lmstudio-gpu",
|
||||
default="max",
|
||||
help="GPU-Offload beim Laden: off, max oder 0..1 (nur lmstudio)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--lmstudio-autoload",
|
||||
action="store_true",
|
||||
@@ -820,7 +989,11 @@ def main() -> int:
|
||||
"--stall-timeout",
|
||||
type=int,
|
||||
default=600,
|
||||
help="Sekunden ohne stdout/stderr bis zum Abbruch; 0 deaktiviert",
|
||||
help=(
|
||||
"Sekunden ohne stdout/stderr bis zum Abbruch; 0 deaktiviert. "
|
||||
"In den Modi builtin und custom zwingend 0 - waehrend ein Subagent "
|
||||
"arbeitet, sendet OpenCode keine Ereignisse."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-runtime",
|
||||
@@ -855,6 +1028,35 @@ def main() -> int:
|
||||
if not template_path.is_file():
|
||||
parser.error(f"OpenCode-Konfiguration fehlt: {template_path}")
|
||||
|
||||
# OpenCode meldet keinen Fortschritt, solange ein Subagent arbeitet: Der
|
||||
# Ereignisstrom schweigt fuer die gesamte Dauer des Subagentenlaufs. Ein
|
||||
# positiver Stall-Timeout beendet einen Lauf mit Subagenten deshalb
|
||||
# zuverlaessig zu frueh und erzeugt eine Fehlmessung, die wie ein Haenger
|
||||
# aussieht. Der Fall wird nicht stillschweigend korrigiert, sondern
|
||||
# abgelehnt, damit die Entscheidung bewusst faellt.
|
||||
if args.mode != "solo" and args.stall_timeout > 0:
|
||||
parser.error(
|
||||
f"Modus '{args.mode}' laesst Subagenten zu; waehrend deren Laufzeit "
|
||||
"sendet OpenCode keine Ereignisse. Ein Stall-Timeout von "
|
||||
f"{args.stall_timeout}s wuerde den Lauf abbrechen, sobald ein "
|
||||
"Subagent arbeitet. '--stall-timeout 0' setzen und die Laufzeit "
|
||||
"ueber '--max-runtime' begrenzen."
|
||||
)
|
||||
# Lokale Inferenz ist um Groessenordnungen langsamer als ein Cloud-Gateway.
|
||||
# OpenCode sendet Ereignisse nur an Schrittgrenzen, sodass ein einzelner
|
||||
# Schritt die Stille beliebig lange ausdehnen kann - bei qwen/qwen3.8-27b
|
||||
# ueber 15 Minuten. Ein Stall-Timeout misst dann Modellgeschwindigkeit
|
||||
# statt Haenger.
|
||||
if provider_spec.get("local") and args.stall_timeout > 0:
|
||||
parser.error(
|
||||
f"Provider '{provider}' laeuft lokal; ein einzelner Schritt kann "
|
||||
"laenger dauern als jeder sinnvolle Stall-Timeout, ohne dass ein "
|
||||
f"Ereignis faellt. Ein Stall-Timeout von {args.stall_timeout}s "
|
||||
"wuerde die Modellgeschwindigkeit als Haenger werten. "
|
||||
"'--stall-timeout 0' setzen und die Laufzeit ueber "
|
||||
"'--max-runtime' begrenzen."
|
||||
)
|
||||
|
||||
opencode = resolve_opencode(args.opencode)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
result_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -895,6 +1097,9 @@ def main() -> int:
|
||||
lms_path=args.lms,
|
||||
catalog_dump=meta_dir / "lmstudio-modelle.json",
|
||||
log=log,
|
||||
parallel=args.lmstudio_parallel,
|
||||
gpu=args.lmstudio_gpu,
|
||||
context_target=args.lmstudio_context,
|
||||
)
|
||||
except RuntimeError as exc:
|
||||
log(f"Preflight fehlgeschlagen: {exc}")
|
||||
|
||||
Reference in New Issue
Block a user