From 37275c96d610a8b7958d126bebe4ef8d580c0bde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Schw=C3=B6rer?= Date: Fri, 28 Aug 2026 07:38:12 +0200 Subject: [PATCH] tensorx adapter --- .claude/skills/run-experiment/SKILL.md | 126 +++- .../glm-kimi-adapter.cpython-313.pyc | Bin 0 -> 29258 bytes .../skills/run-experiment/glm-kimi-adapter.py | 628 ++++++++++++++++++ 3 files changed, 749 insertions(+), 5 deletions(-) create mode 100644 .claude/skills/run-experiment/__pycache__/glm-kimi-adapter.cpython-313.pyc create mode 100644 .claude/skills/run-experiment/glm-kimi-adapter.py diff --git a/.claude/skills/run-experiment/SKILL.md b/.claude/skills/run-experiment/SKILL.md index aa54ef97..50905094 100644 --- a/.claude/skills/run-experiment/SKILL.md +++ b/.claude/skills/run-experiment/SKILL.md @@ -1,8 +1,8 @@ --- name: run-experiment -description: Führt einen Versuchs-Prompt aus einer Prompt-Datei als messbaren Headless-Lauf mit Claude Code oder Codex CLI aus und schreibt ein Messprotokoll mit Start-/Endzeit, Modell, Tokenverbrauch und weiteren Metriken. Verwenden bei "/run-experiment " oder wenn der User einen Versuch/ein Experiment ausführen und tracken will. +description: Führt einen Versuchs-Prompt aus einer Prompt-Datei als messbaren Headless-Lauf mit Claude Code, Codex CLI oder dem Python-API-Adapter (GLM/Kimi) aus und schreibt ein Messprotokoll mit Start-/Endzeit, Modell, Tokenverbrauch und weiteren Metriken. Verwenden bei "/run-experiment " oder wenn der User einen Versuch/ein Experiment ausführen und tracken will. argument-hint: -version: 7.0.0 +version: 8.0.0 --- # RunExperiment – Versuchslauf mit Messprotokoll @@ -101,7 +101,7 @@ Der Skill ist zweigeteilt: - **`## Prozess`** beschreibt werkzeugneutral, **was** je Lauf zu tun und zu erheben ist. Dieser Teil gilt unabhängig davon, mit welchem LLM oder welcher CLI gearbeitet wird. - **`## Werkzeugadapter`** beschreibt **wie** das mit einem konkreten Werkzeug umgesetzt wird. - Ausgearbeitet sind Adapter für Claude Code und Codex CLI. Konkrete Flags und Rohfelder sind + Ausgearbeitet sind Adapter für Claude Code, Codex CLI und den Python-API-Adapter (GLM/Kimi). Konkrete Flags und Rohfelder sind ausschließlich dem gewählten Adapterabschnitt zu entnehmen. **Arbeitsteilung mit der Prompt-Datei:** Der Prompt enthält ausschließlich die *Analyseanweisung* @@ -124,7 +124,8 @@ per stdin übergebenen Text angehängt (siehe Schritt „Prompt zusammenstellen" frühere Läufe entstanden sind, und dürfen nicht nachträglich angepasst werden. 2. Aus dem Metadaten-Block der Prompt-Datei (falls vorhanden) Versuch/Prompt-Version übernehmen. 3. **Werkzeugadapter bestimmen und CLI-Pfad auflösen.** Die Modell-ID entscheidet eindeutig: - `claude-*` verwendet Claude Code, OpenAI-IDs wie `gpt-*` oder `o*` verwenden Codex CLI. + `claude-*` verwendet Claude Code, OpenAI-IDs wie `gpt-*` oder `o*` verwenden Codex CLI, + `z-ai/*` und `moonshotai/*` verwenden den Python-API-Adapter über den TensorX-Gateway. Keine Modell-ID an eine CLI übergeben, die sie nicht unterstützt. Unter Windows liegt `claude` in der Regel **nicht im PATH**. Erst @@ -162,6 +163,11 @@ per stdin übergebenen Text angehängt (siehe Schritt „Prompt zusammenstellen" | `gpt-5.6-terra` | ausgewogenes Verhältnis aus Leistung und Verbrauch | | `gpt-5.6-luna` | schnelle und günstige GPT-5.6-Variante | + | TensorX-Modell-ID | Einordnung | + |---|---| + | `z-ai/glm-5.2` | Z.AI GLM 5.2 über TensorX-Gateway | + | `moonshotai/kimi-k3` | Moonshot Kimi K3 – 1-Mio.-Kontext, 2,8T Parameter | + In der Frage den letzten verwendeten Stand nennen, damit der User bewusst wechseln oder bewusst wiederholen kann (z. B. „Lauf 3 lief mit `claude-sonnet-5`, 11.516.200 Tokens, 23:40"). @@ -256,7 +262,7 @@ per stdin übergebenen Text angehängt (siehe Schritt „Prompt zusammenstellen" Sort-Object { [int]($_.Name -replace '\D','') } $iteration = if ($iterationen) { $iterationen[-1].Name } else { 'Iteration 1' } $zelle = Join-Path (Join-Path (Join-Path $iteration $modell) $modus) $effort - $skillVer = 'v7.0.0' # entspricht version: im Frontmatter dieses Skills + $skillVer = 'v8.0.0' # entspricht version: im Frontmatter dieses Skills do { $id4 = '{0:x4}' -f (Get-Random -Maximum 65536) $lauf = Join-Path "\$zelle" "_Lauf_$(Get-Date -Format 'yyyy-MM-dd_HHmmss')_${skillVer}-$id4" @@ -997,6 +1003,114 @@ die angeforderte ID; da `codex exec --json` sie im Ereignisstrom nicht wiederhol Modellkontrolle im Protokoll `nicht prüfbar`. Temperatur und weitere Sampling-Parameter sind in diesem CLI-Ablauf nicht steuerbar; Effort und Service-Tier werden dagegen explizit festgelegt. +### Adapter: Python API / GLM & Kimi-Modelle über TensorX + +Dieser Adapter wurde für Modelle entwickelt, die über den **TensorX API-Gateway** +(`https://api.tensorx.ai/v1`) erreichbar sind. Er nutzt das Skript `glm-kimi-adapter.py`, +das einen minimalen Agent-Loop mit Tool-Calling direkt gegen die OpenAI-kompatible +REST-API implementiert. Referenzstand bei Einführung: **Python 3.13, requests 2.34**. + +**Verfügbare Modell-IDs über TensorX:** + +| TensorX-Modell-ID | Hersteller | Effort-Parameter | +|---|---|---| +| `z-ai/glm-5.2` | Z.AI (Zhipu AI) | `thinking` (`{"type":"enabled","level":"…"}`) | +| `moonshotai/kimi-k3` | Moonshot AI | `reasoning_effort` (top-level) | + +Das Modell-Präfix (`z-ai/` bzw. `moonshotai/`) bestimmt, welcher Effort-Parameter an die +API gesendet wird. Der Adapter wählt ihn automatisch anhand des Präfixes. + +**Authentifizierung.** Der API-Key wird **automatisch aus der Cline providers.json** +gelesen (`~/.cline/data/settings/providers.json`, Provider `tensorx`). Alternativ kann +er per `--api-key` oder Umgebungsvariable `TENSORX_API_KEY` übergeben werden. Der Key +wird **nicht** in Laufartefakten gespeichert. + +**Unterstützter Agentenmodus:** Nur `solo`. Der Adapter implementiert keine Subagenten; +`builtin` und `custom` sind nicht freigegeben und führen zum Abbruch. + +**Isolation.** Der Adapter ist ein eigenständiges Python-Skript, das nur die +Python-Standardbibliothek und `requests` benötigt. Es liest die Codebasis über die +implementierten Tools (`read_file`, `list_directory`, `search_files`, +`execute_command`) und schreibt Ergebnisdateien ausschließlich über das `write_file`-Tool +ins Ausgabeverzeichnis. Schreibende und bauende Shell-Kommandos sind durch eine +Denylist gesperrt (analog zum Claude-Code-Adapter). Der bereinigte Snapshot bleibt +zusätzliche Pflicht. + +**Effort-Steuerung.** Der Adapter mappt die Skill-Effort-Stufen auf beide APIs: + +| Skill-Effort | GLM `thinking.level` | Kimi `reasoning_effort` | +|---|---|---| +| `low` | `low` | `low` | +| `medium` | `medium` | `medium` | +| `high` | `high` | `high` | +| `xhigh` | `xhigh` | `high` (höchste verfügbare Stufe) | +| `max` | `xhigh` | `high` | + +**Der Aufruf** (als Background-Task starten): + +```powershell +$skillDir = "" +$lauf = "" +$root = "" +$modell = "" +$effort = "" + +# Prompt zusammenstellen (wie bei den anderen Adaptern) +$prompt = (Get-Content "" -Raw) + "`n`n`n`n" +Set-Content -Path "$lauf\_meta\combined_prompt.md" -Value $prompt -Encoding utf8 +Set-Content -Path "$lauf\_meta\startzeit.txt" -Value (Get-Date -Format o) + +# API-Key wird automatisch aus Cline providers.json gelesen +python "$skillDir\glm-kimi-adapter.py" ` + --prompt "$lauf\_meta\combined_prompt.md" ` + --root $root ` + --output "$lauf\Ergebnisse" ` + --model $modell ` + --effort $effort ` + --max-turns 50 ` + --result-dir $lauf ` + 2> "$lauf\Stderr.log" + +Set-Content -Path "$lauf\_meta\endzeit.txt" -Value (Get-Date -Format o) +``` + +**Gelieferte Messgrößen** – vollständig aus `RawResult.json` (vom Adapter geschrieben): + +| Messgröße | Feld | +|---|---| +| Abbruchstatus | `is_error`, `subtype`, `finish_reason` | +| Dauer | `duration_ms` (Wanduhr, selbst gemessen; keine separate API-Zeit) | +| Tokens gesamt | `usage.total_tokens` über alle Turns akkumuliert | +| Input-Tokens | `usage.prompt_tokens` | +| Output-Tokens | `usage.completion_tokens` | +| Reasoning-Tokens | `usage.reasoning_tokens` (aus `completion_tokens_details.reasoning_tokens`) | +| Cache-Read-Tokens | `usage.cached_tokens` (soweit von TensorX geliefert) | +| Cache-Write-Tokens | **nicht erfasst** – TensorX liefert keine | +| Agent-Turns | `num_turns` | +| Tatsächlich eingesetztes Modell | `model` (aus API-Antwort; mit `model_requested` abzugleichen) | +| Tool-Aufrufe | `tool_call_count`, `tool_call_types` (nach Werkzeugname) | +| Erzeugte Artefakte | `written_files` (Pfad und Größe je Datei) | +| Abschlusstext | `result` | + +**Semantik der Token-Zählung.** `usage.total_tokens` wird über alle Turns aufsummiert. +Jeder Turn umfasst dabei den vollen Kontext (Eingabe + Ausgabe); die Token-Zahl ist +daher die Summe aller API-Aufrufe, nicht die eines einzelnen Austauschs. +Reasoning-Tokens sind eine Teilmenge von `completion_tokens` und werden nicht erneut +addiert. + +**Nicht erfasst und niemals schätzen:** Cache-Write-Tokens, Permission-Denials (das +Deny-Modell ist ein hartes Blockieren, keine zählbaren Denials), Subagenten-Stats (keine +Subagenten), Session-ID (keine persistierte Session), API-Dauer (nicht von der API +geliefert). Sampling-Parameter: Temperatur ist steuerbar (`--temperature`), +Reasoning-Effort ist steuerbar (`--effort`). + +**Pflichtprüfung nach dem Lauf:** +1. `Ergebnisse\` ist **nicht leer** – sonst Fehlmessung (analog zu MAJOR 6.1.0). +2. `Stderr.log` enthält keine Abbruchmeldung. +3. `model` in `RawResult.json` stimmt mit `model_requested` überein (Modellkontrolle). +4. `tool_call_count` > 0 (ein Lauf ohne Tool-Aufrufe hat die Codebasis nicht analysiert). + + ### Weitere Adapter (für Versuch 4 nachzurüsten) Kapitel 4 der Arbeit sieht zusätzlich Qwen Code CLI über LM Studio und DeepSeek über die @@ -1057,6 +1171,8 @@ der Historie unten – im selben Arbeitsschritt. | Version | Änderung | Grund | Verwendet in | |---|---|---|---| +| **8.0.0** | **Neuer Python-API-Adapter für GLM- und Kimi-Modelle über den TensorX-Gateway.** Das Skript `glm-kimi-adapter.py` implementiert einen minimalen Agent-Loop mit Tool-Calling (read_file, list_directory, search_files, execute_command, write_file) direkt gegen die OpenAI-kompatible TensorX-API (`https://api.tensorx.ai/v1`). Modelle: `z-ai/glm-5.2` und `moonshotai/kimi-k3`. Der API-Key wird automatisch aus der Cline providers.json gelesen. Token-Verbrauch wird pro Turn aus dem API-Response `usage`-Objekt akkumuliert, inkl. Reasoning-Tokens (`completion_tokens_details.reasoning_tokens`). Effort-Mapping anhand des Modell-Präfixes: `z-ai/*` nutzt `thinking.level`, `moonshotai/*` nutzt `reasoning_effort`. Nur Modus `solo` freigegeben. Neue Modell-Tabelle für TensorX-Modell-IDs im Vorbereitungsabschnitt. | Die Cline CLI (`npm i -g cline`) versprach einen Headless-Modus mit GLM-Support, ihr natives Binary (143 MB, Bun-kompiliert) wurde jedoch durch die Application-Control-Richtlinie der Maschine blockiert (EPERM/Zugriff verweigert). Der Python-Adapter umgeht dieses Problem und bietet zusätzliche Vorteile: exakte Token-Metriken direkt aus der API (inkl. Reasoning-Tokens, die Claude Code nur als `thinking_tokens` liefert), keine externen Binary-Abhängigkeiten, API-Key aus bestehender Cline-Konfiguration. MAJOR, da neues Werkzeug mit anderer Isolationsarchitektur und anderen Metrik-Quellen eine neue Versuchsbedingung bildet; der nächste Lauf eröffnet eine neue Iteration. | ab dem ersten GLM/Kimi-Lauf | + | **7.0.0** | **Isolationsmechanismus wird modusabhängig.** In den Modi `solo` und `builtin` unverändert `--safe-mode`; im Modus `custom` und bei jedem Lauf mit `--mcp-config` **kein** `--safe-mode`, stattdessen `--strict-mcp-config` plus `--disallowedTools Skill WebSearch WebFetch SlashCommand`. Neue Pflicht-Umgebungsprüfung auf Hooks, Plugins und Output-Styles im User-Profil; neue Protokollfelder für die MCP-Konfiguration und die Umgebungsprüfung. | `--safe-mode` schaltet ausweislich der CLI-Hilfe „MCP servers, custom commands and agents" ab – also genau das, was V2 und V3 untersuchen. Smoke-Test am 2026-08-26 (CLI 2.1.246, identischer Aufruf, nur `--safe-mode` variiert): mit Flag `spawned` = 0 und die Meldung, die Rollen seien „nicht in der Agent-Registry registriert"; ohne Flag `spawned` = 2 mit `{"modulinventar": 1, "konsistenzpruefer": 1}`. Ohne Ersatz hätte der erste V2-Lauf stillschweigend als V1-Lauf gemessen. Der Ersatz wurde gegengeprüft: nichts vorgeladen, keine Skills, kein Webzugriff, alle Rollen verfügbar. **MAJOR: Läufe im Modus `custom` sind hinsichtlich der Isolation nicht unmittelbar mit `solo`- und `builtin`-Läufen vergleichbar** – Plugins, Hooks und Output-Styles sind dort nicht durch das Flag, sondern nur durch die Umgebungsprüfung ausgeschlossen. | ab dem ersten V2-Lauf | | **6.1.0** | Zwei adapterunabhängige Pflichtprüfungen nach jedem Lauf: **(a)** `Ergebnisse\` darf nicht leer sein – sonst Fehlmessung, unabhängig von `is_error`; **(b)** `Stderr.log` auf Abbruchmeldungen prüfen. Der Ausführungsabschnitt setzt `CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0`. Neues Protokollfeld „Gültigkeit". Dokumentiert, dass die Rückgabe eines Hintergrund-Subagenten eine Start-Quittung ist und ihre Länge kein Ertragsmaß. | Lauf `…160037_v4.5.0-116d` meldete `is_error: false`, `subtype: success` und lieferte **null Ergebnisdateien** bei 193,4 Mio. Tokens – der Headless-Modus hatte nach 600 s abgebrochen, während zehn Hintergrund-Subagenten noch liefen. Ohne die neue Prüfung wäre der Lauf als gültiger Messpunkt mit „0 Anforderungen" in den Modellvergleich eingegangen. MINOR: neue Prüfschritte; die Umgebungsvariable ändert die Versuchsbedingung für künftige `builtin`-Läufe und ist dort zu vermerken. | ab sofort | | **6.0.0** | **Windows-taugliche Codex-Isolation.** Pro Lauf wird im System-Temp-Verzeichnis außerhalb des IDE-Projektbaums eine Kopie der versionierten und nicht ignorierten Dateien des Codebasis-Roots erstellt. Codex läuft ausschließlich dort mit `--sandbox workspace-write`; SHA-256-Manifeste vor und nach dem Lauf erkennen jede Änderung. Ergebnisse und Nachweise bleiben im Laufverzeichnis, die temporäre Kopie wird danach entfernt. | Der erste Codex-Lauf mit 5.0.0 konnte fachlich nicht starten, weil die Windows-Read-only-Sandbox sämtliche lesenden Kindprozesse vor dem Start blockierte. Eine Kopie im Laufverzeichnis wurde zudem vom C#-Projektservice automatisch mit ignorierten `obj`-Dateien verändert. MAJOR, weil Sandboxmodus und Arbeitsverzeichnis eine neue Versuchsbedingung bilden; der erste Lauf eröffnet deshalb eine neue Iteration. | ab dem ersten wiederholten Codex-Lauf | diff --git a/.claude/skills/run-experiment/__pycache__/glm-kimi-adapter.cpython-313.pyc b/.claude/skills/run-experiment/__pycache__/glm-kimi-adapter.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..330415434c67a8fdfe760e38734133de63d65685 GIT binary patch literal 29258 zcmcJ&33OZ8nI`tIgIEdfJ0BO3gh+sksD%`@aMMmw!Xs2tQ7RfFArd7LWW5JUCDdZ} zB$=k{Ig#WgV!3@%th6)5O8ZRHr#)l(xKCPTSC3VmNI`N@Q27S-&n5jE_s6}9ZG6LsvZ7xnCI5Dn~Y6pieiBj(_(Ic+*+ z5>2PfqM4_BwWlqotfH0Ys^n5^qD?ReMj=Nq31+vZO(o_F7Qrgm+-k8vu;VY+tri^k zqjw(u=$*ggUEtPmLZwjUR=25y@>Z2l?bZl9 zel^IaSf~~1k}&npgxMuD0H#7{BnZ5ll5Zz_??ztDPvzSpxRAyT=+4-X?A?hn%7wkH>dcz9>ckRZ-=$JvKYj-;Sv&PYR~h%RRxD$o-IvM{T8hwvEVN=r zXb%gm+!1<+g;wndJev!Yj z2xrP41&Z zi<2|btZ(rOKNpbW=lGEYf3WwY>+1a6f-e-9nDqxuL-SMq*;&5nMR)H>{`i?89^WEA zG(R7dX68fu>A+mT$uIgRl9kG+vFo@m=jUf#{l3}RKyaF03{LTW@rqB9LZ;FAtNx&C$RF}ep@bmc z6bN3Ob@Lv-Pnr(`*u?-OC+}O7_?P`uUv!|WHyFA;FNUa#k|C%4H~8xTaSDK;`8l*$ znw(*YC`muc_45nj{Ivi+CHKq7oS*ii`_L;*Z??K837{2K@U=?*PzVJ}t=qv{__N96 zd@U-9p?tO0iSp6=*Zsk%#o%T$LH^qk+MSgOAZUR-98keKob5n;EaNgw-=P^8ZdOYyZfQS8o$(dk4!sJ2gFpLvE zDIjGgnO_VoEK(6?FfFd7Q?w3>)BXu0mHe6Db2K%m1bL3QP7bv2VRxUK54(JUR%*>& zcZWP*=92RT;jD#M_he?|zj6fwgN$b9ufIO$p9(C_y*?9|o_YOMa?_AwF4vszRaa6U^-30 zlX~>@rQs zs40{<8yvb zye1tV)Oa;n^Drs3ct*TgiSJa`b2XXmwe6@bKF4Up*wd-TE{w6^xoY%NYNxb>@0!kF z%L}nRF=Jl32)ok`@GNBRHW4Q6=D!pwH+dHQlUE6Ibn{J^;51cRcxCoFWh~b;jOEqB zlZ!PN*2`N&LQ8u@!l-N#VcYgy!l@n}exh3pU2z?FPn9tGgOl@9H2rf@V4?v%kkF%8 zv?*brQoZ7=Ge@Lpg?T=Ym1GD=<5vQ+{)Cn`s)TNK-Zv#Ba|`6O&o zz2byF5W0k>`WC1Bmn5P>q)XdrlS^54wnQG)aGY(TMh-4reCjvN|_77i^9$IYdi=9;LvX4703HP^-MrCVy9 zuI!;b|5o^G;T890Sx2<2W8-+FtRqr35VH?1YvWeOtqV6V+`hWn5wkXIT6agSyVttb zCt}vFP3xhk_0Y!4_iJO;p=EX4?znaB=C#{1F?;oD-n&Kb6s_gNYC0nJj)<}2^M{t) zO$#5j@ZW2Sn43T2RJyW{P`n23ClX)?LcQPywQ{LH3;>nscFFHMMZmnOnF z^qvZc@FHmLn*5$zkl#~Ncv~(_gwU6hK;rm9g2@@|x{7!HdQgU)rMCgFegZ=x$v<^T zYIX(im3L`k8UrLXPtITlV;e-6LkSbHaOOoyWLJVFXZ=3PE)@V6KP9p%5I$D5T;&sF z2q;JZ*p!fkMQJ88hb#WjBn6l*O-#*)f-E;PS}aX024<->s84WV4s}d!ItW5rgNvB5 za_mB2VOy;B@Fi(?Sa+CS3F8&?P$+;|FHd(`2H^=p3JfQl49@swWeQj1N3j%WOANUA z0gyqAmZCjR^v{C+y9T;yj+dvjltBRTO+-Y=m_#9*obH5ve&S{SWGJBpwLFv1OCb^K zHr(cs^HQ=WQf+{Wt~dCD(Mih%`ci&G=E zVge;hzycs_Kru@RBkj`x(LaUA97Kw3GK3E~`LPxy?1Pj@+)d#&5G6ZR06K8^<(>f1o<4MV{L}tdT|UkJ=%Qh;&~zMEg7#66-T$h8axvr| zr=KeNRFtl0;nDS=xVUA*Pj{d9HMV*@jvq4A?N{XRrAa8X&}k2}2}?r0W6nxaqua zc2OogKyLH^SUf~2tW=}%WU{G*9w~tNQxu0tjZNf8zAOImm*)dP@fg12_(>PxEOEan zC|N1^=BdqshG;=Ute|OWC~hl{+OV&;My;(eYx~l%hi2RIwXYwP>4fjqt($jK-&a&) z3OAOZLFEA2M70ylER8l>nl!yWEU!&2T%{a8*~(3xI;Z{E%z>Y}d9ei5p~znky zrxwfTTaT0VyeI{ZS~XmFkI6LVb_21n-IXvJFlI0S2|dwYl5c+GtC_))V9b zd-;h`DK=ar`v`Q7KSO*S{u@+L8Fw_UX;-hUdDc~HQpCRJSH?YG5NX@_MCwFUzg6E` z%l)_s&il2dzCz9WE!y6Fn)lmOvkmn9m11n{8E%lw{z|G}g|tW>Q$2)?o^VqDIL_iC0RT+O`f`v3kZ*C-LlOT_p!W zTgC~xE_FV4Vv*L1SNCPg^Pyh^{fJ611oeVCXh15g&7F@@hEhu(&yn212xww-?06Jt z51meSJVK706HI$FC`s!n-VupUuU=l`9$J|@9vkO10AHjq4$lo;=QuM?>t1cDW~h%~ z@s#gKiqABr0+z8dj8vdNrBD6CXA~>~u0cgcJYg)OVDlO?FqW~`$S@Y(z*uy&Wls`g z83jATSh*~Xg0Z%xQOa^UWQ--ALIzLj73^_7%^rOUbxme&2sFj>@%!vN987aA+jgN0 z3t}_L5u~8!d7A_=QQlJx<3zyF6is6~{DaB^pHDR@QB{(d$JjILo12*O9Zu|GIB5I% zzzy!}q;FO_437-<1(HgoaF)1@_P^@-ao0}{-dFvN{zLuWSU$A;J6p8-1@SmyQH}=O zkg$Gi;H2j=kN2^YX9h1nQS(g+RZG~wPuvLkC1*m#Csd|z0Xrz*oRQIiPH`Cdgv&DV zaqr?4s$UW(iz8G}ZU$1OeN#LNC!q)D1E(CEzraNHPk|Ex;u`_1LlRohAEAT+Cm|^i z2AgC=kQpD~ra;&+#m-USnGjh)h+RAf2b>i~oSdKlLg}Ih4zVb4!oxXCH#viIYeIuV z8}diCqJVgb`ko>BG_udn($QiNaE~cL2L>k_#?R9o_@t@jU))$Jcwn#pVf}jOFLrNs z3`9EyVjaiiE#S4Auibj%<{RfpPl-Z{0}v#wpc7AfENYs>ye z4zB2;>L2pB2G?@v){UDtR*GZxnuzg4q~iqK&mSAOl4|TKj?#|`O0bDIO22q)eCHq-PLm)=aR>D&~bg294%ul~zyQ*1NUdo?mqi_O%4ZO$kd zgE>f_W#bZZ6k;}?JFYr`wHh>eO&-FmJ07JbO53K;8$;N1GB%<{Y(yr3J8cS@y=G70 zjtuaLoQd(dY-=Q2N40 zB+law1+Kd$#5DZe~syc%wssPQQ_k2(4V9*qfU{Xyx|iGC~V)5L&zfpMoxV4s>Rn z(fs<&{Dx?L!$-L#pV-Sk&aH|USKa;Uov*Im zc=yeB-duluvv?p{Jn&IL#g2s{54?{)pMa42%l^U&F` z-oEbpSJiJ>R}ArT{_UQ(de*d?<*rz{D_&9e_V8Q7Ym;l<^_unL>s=dt8==2??Z>bE zM6I)!l5r7+qi+t zullcxEGNslu8Liu|mGAGQ>nI;8mpr#)rT{6ecDzsX8&vG!Dr<`?BGq=tpKwWkhfe$l}~4(I^0 zjm##bq`~jA-B^TQlFUx=gBW?NNpE2wWw6}`HDf6v*^`ABuH`OtWIz~E`i^HW)d){& zlSSQzkTbLcwY2$z$VzcfJt@wpoQ7N?$ORqrLv+NUg_ee%xUQZox(NRS2Q?cC*TL^TKn^f1%vu#BZROG)xUFIb7|l;T6{d-aUi!260SMG)AA z#V*vG5d`5%WshZKKvH6sA_2lL_4XYfJTrJ=c(gky+v0JopYdTgCi%w1JXj9clx1<` zmyxGQkefg+P=b&ano^^`CC-6Um(WQhu}f$suTLdxlhEgbV%oT@Uu?5RJ*|wv!iOd(%}$S|mZ7;bx`T$rcBFkfH8t;|2d! z#!zROOo;Qu2oG1wz>JQO;U$Run9z@u5_zXprZb8M28T}$4vhDY3=Q=T4~(Df9UUF? z3=3k2y8CrPg6kuqQ7DNfthZrm;8rg^>v85+WB8wo$p=gRd@-EL-Nd z7&52@8q0(RGCC5`5gh*H+BP}ll!^6ofoj}e1}X`{1brn(2o8e;CXsSXFIn0 zZ|x1snulg9DVf|BzSr^Yp?3~#n%iUM_K)(VM>;OIaZ7L2?S1GdzgzWAv?Nt=+>dQC2z&Ddj8$Z?_6FTi4=84 zEuG7X7;2>mto=ARHOE)Z-yV-No{1KoSw0r8X?(Zhor-s>-l8CnN87q%T@$f){>s?Nc9gwOBsyiiU)X2qujhVNt0K3) zU?5NPv+f+YA851#R?PdCAC~$d7{J?;3V{NH4?hHtHdWxsxlA`mX9vy00e>GcPYUMa@a}b zASKn3uAMkYfsDob6z;Jz+>=(05dV9?3WRq44xXo(yE_jSpP21&d;69;M|S`UYqw89 z&1}=&7_~RX>`t(H^NQ{?Z#o*Hj)s_{X<7f!o_BlS*I(PT?~2-YeQ$ED{!as&_O__K zEnZl+Ii5qRVtT(Nj#`37Ke05;GGeW)t2;MZ7MvbK_jcdmvwEaT%P^-CHc7Y`X?g#{vZD`6dLEYdJuEu>$gI!RFP+%3arWX{vo~i~ zI%76|>G&rG+eh}I+o7+&5iwT8txUudF?U3?9dh@kHf&^zbt)4de~*HI8-P}I2*L)b z-UDekG-8Y(vL9xWmH5%f6IVAFN$gE`P-Q4ucpL81(S3z0KBp0sE|Fi{DR zxSNBfj8kD|t&(ZMPPZuQJ7^xGy0RymZjM)*)|$^6Zx*bnyewX@>6tYePQ#FCTLimX z?adJ^jmUq*gxFjb3*2+zLm`(!f8Y7@!mC~gRRohFZt#*$fkVja(gv+wYqpc9klzjI zy{`#*r$`Z@AUiC^qYyDEl0@zYa)lzHm@)KIL>08rtj|9&I!nR>p2aKBFbnwk{uy7$ z)eko3EKFEnaONUAII>tH<+W*23MD8~X;+N9$S#U0e2PSzg&nPv$fA`vrO7P9hCXmG zp)MHCg#Z|ereJu^2U|&J9%{JO-bGkqiGeT+4O{wU{XrM>VWC)0R$|ATde9MM_>?ft z;ke@i?UK+Dx(4Tx(E^h*^MOg|@@5z$(J=@LKXVCHD`PmzoNGel%pipX%RU&}O(oTL zA;0%u^UsQeasx(8KGG?jfx-}$rN~pN+i-q+A5PdzmTj_a5zh#r;Xs5^m=n6uk&!b3 z4qa$vDB&0y85le>K6vcdh-Y+s^y1k;`GjXmh79$dmDP)(5H3NXd0q;EKMT?Mq696~ zDSyJ^S%lf%oUFYk-lAIYaGbV;QL$4H|22jF8#oC=QllUGTFkaf{4qsQwG$dx`-uM| z`Lq-BQ#TS8)|;?l^Ir*oCoIE{Gks`N^5dFs7TTLQVoSs|NuK?ZFZl>)gyzC5nGuLl zKwKxxlm};t`_x)^`{k&$CSs|L)b5FxI+yz6wa&H1wbBT`Co=d#)OKO%_@|DdA1=m= zE7z{x^FGpPEPAM2nYd>6df}h7+}?Zl;GKgj5*Vhdy}!(F*yvk2^DwV&Gp{k4*SKa` ze>s-dyL6fz5bsp2>etjU2Pn4IsH1h=ym2AsIJR^uZpgd+a?HTT>s=eBXnFs#cKO9$ z8cX6i1(A~7v7FuUI#{o|vKPp{;CXY^`9^*%ftmMVh)dYBxsz>f( z2PC#OK_9exRgef7f)0FVAYhM@d&WD%Tq^%Uyc-DPxY~z3$ZLf%BIbI~nt2Yqpt|tG z3m3hny_n7P1PiQ>r#GUdC+RU<{- zQbEf~D)W|QZB3CgwXanzs@x?eCx$tGrcN%Ph;Nd9}AXD@=Jtz4&Lq;3|St z-D{pvR#iqmwO@h-UZeC_u+D4C+NV}8@2#Wj1QbXUEW>Zd>06cU<0sRW19Qo0ZF;@$*MS+ z(p^wS)c4O(KCwaAGghwn(%J|fXjJTcgj%IX*Z<8mB=7_>AQ%Yqz6J<NYQ zEtIoWyxZ%{HjBNx(>O_1WcNC~y9tXA)hhW3^<#8aVvjdZsjr~z{x6{ukrK4{u)48^ z?Me?dbK@#uSNfZ&;5L9Eocd2Q;5*2_;6)No7;$7+P%41@?q&S(sj6XZB%}#*9Lb_ zMlabiTGQ!uB6hEr+P%+fhg;-zz}@f7gR2(~2wg(ATQjI){K(|GWaa~yH^IW^Vr`SR zkhqjeX(ajq1yR^r2#uBh2`yv(w0H;n&J?FL+hGMfP_UAC4s)mu8Z_$Rc119Rl*+)8 zsRFt!sbd3-puji}hTEC-6`A*KyMuyVTfvSRk`LBpkPqR?2{C}XCt&#}-GFZD99iy( z^EfWk0W+ap1h0@0_>litK^5b^2B=aXfs}Bh&mc;WB{UBMd*#bI34smna!A5w*uKjl z?94DMiYO?o8-?8xAGUR~do5(@znf3!7U?)5{u&_SCOL7cRpGXZjKcuP7nqgAk0`G6 zsamDt6O;iF^iVWmCmJbS)y>QLEXe?}^~$(dBVoOQt30F`vTYy}WJB_NBzKNUJ{*Be zmza5?h~q8HC}3R!-4gJK!|K=GNT>raZG?CX&13P59gA-quV66rfKs5>U_Qaf{eY~! z%kmc&5Sg_566Xle2(@6Qi2~){#R+zvfM=3qRN|WiV8#HmbKdxz6!?2=3kiEFgpMzB zFrS9v%Q)3WO4#LNO7f-zh^_8Z9@_Eoz*NGnTv?DDJ*RQUAjrM#U_&gMek6yvP!5_z zm(e*inO>yMPZTDFGBT(#Fm(3;PM=;HYD_arAc!kFlv!ilmLyS}QQ$VX@*TxyM#_cD zd~Mdvlk+(Fp^d42r@oXDR$_WXSSs6iif^IH;t-rCcKJFJmtyq&M2Gu0+-(?G6YNVR zY9{*3;FB-BNZ6B}aY$b&(piiF$3c^wP)1Z*nF7C_Xu(l{;&Ve1AkNn^!?O_^y6 zOrKr+TPlWnHfc*YPQ62ib!h=S5kC`5h#ynDo(=0%!a~bhnK0rfa+J)L(9B8G@`CyI z6s!(SCSa}Y5c+6-K`c{o+&A!(hA^~C+?J8cD_fmo0*vC- z(@|sFmQKSsjz(@*!$xnse%FTT{wp{;=X+F3r$4n=mj>g7#keh?pa}MXg=OR`fzN7( zb)cj0Zs|8lzgfO?>Qj^bfvG%hwBB~yG%f3JlR!@1!{X{y<9bQFde{2#&oru%L(3P^jBQazBn(ohY-pg5Uz1JRV8jKnT5rn8nL1WC=v^MzX zC*M06*?01Od#q(BY8*lsr5lbJ>sA+;UQ^XQN35|wYV1c)+);G5^iFA{ykosD=GedK zI2d&t+!(okF6J14oLMPQA2qsB%BRH@cW>Oe5vl3fn1~hkE?Zc!4KZWmTF++l!D#bA z;t8BR7i;!JjUEKY!ENZd*|TEUH1ko|h*j=dosL$zmxtqdWp`Wdw5;4%tBvKkmJOfS ziy+OuHFR@mWpH)ZYH_6LSfuRu1IvkzERMLNWW{l(4Am*AdfW7tY4u2~WDiu;Yz4Po zzWMS>>1toh*0`*R8*^{jZ`xNh55V@#E4*QJvWM}?i_=#J}*{KzdQ&F2W$010G;~Isns`P6?<>zkV!-Nrllro zsae~*;kGTz=(Nzlt;G{@PUj_$3vWTX5^f*KU0MwWSkePj?DJJMeyJx;-@H{PFcay8g#+ z{Lve?U)(hCQ3L$IFw7{Z z*(_*^7Bu~^Zr%5zxgX5kI~ChE66-h{@7VLBh95NiMblh6k;^Ij?w*zK+Ca3rEmq#Xl|!K>E~ntOcgrmME9+NZQvCaltM2RM4-1!*fBVGB z`Dj7?mX#oFTuyGhvT3b4TGt?!2*qtdpeF&3M)8Y%4oq`e*7>LP!)+|D;+?dvYte^ez$^r|QZUx;ehN`0MFomR#3>oLC^_h$-%~Ux2gt`>#+UL+LIRU7+NlJ$uI^Xh^&R!)6UmY1ttR zfPfhIIS@I3Fg+K7IGmQ}Lg2DTy5{jYr8uBOhl?Md1CcL)&m|O|!&&M%P{rvBpy+bQ z=RlUF#ZE%`b0I4-^29ZhLUot+nf=M59W|+TWO9m=W22nzgBgb4S@l{}*;E(P#^VAz z;VPV_=UMXLI;*UCfm2;er+&m{;lu>&0K(F% z!{t^`(@d))X0zYP%Au)CpK3?i(q^X~UMV;I&e~_{Y@tHEuuJKuv=4Pmo7Un4exXlj zV7-yD>Y6%<7N=AyzjPZKad}u4J*CWAfcM&z$$QNx1ss^253ag&ru!ws@<^49L!>~$ z3^}Vskuq}1(SEA`pVZ3Tm^E4WQ)pT9Kec73Z;I!X?gIhWtOCOs1$QuKTiaXGFp5uU zdp1iYrQOW#tzAixeglVTg;u6#rt);8!xf*>U%o>0*DfU#y^sY~smF*Jc5R&sZ2HaY z=AK)JF|zq!kB5{Lc09G*Sho`7p>x!ZM=9TPWJd%(Q~i*kF8rnEb{n%td)5e-JzZMX zRo)KF(?Q$Sb7sqz&1(-v;ONuk{sD6CM9zJ`KWA|72NkFbHQDOo&AISjWX}ts$CR|` z_ZaYw{6Kz(GaEgCJSF=wU4)*={FeO)9ez4g*xO}>-QxfM^D6hk|16wgm^2r%$sy)5 z3&ZR(M~BzpdEx20ja^W}JTEG~^c%`%R>3KZ2xq%A3>&EVe+phcN49a##7v&EsS@%KI zVn5X)g^La7yNgPlU{b4)z9k>$b6!xXLrC#HHUEQg(aBuKZBg$%NgW5OCF<-KID>;tyD{$c^v^DY_3|yw;i{ciZEe4akTRDUzv1eq6|wmy<$^ZJ?+hDo zg);Gv!|g-#n#HBf$-CAvFKuq&(--V27dAV?Ce{Xa9kz&h%b_$VADvaMxNe&AU-7|O zqnig;nC`l!iwzCQK(PX|I-KKjfwS(S+d`TtEiNDB)5&0hlJ(YYCp9psN8~`cBI(?s zrz(*p<8Fq{Xq9p?^lzyHlbTcvBy8PwJM=W7@oXKT+7SN|UB`qr;gV7Li{u(Go8g<< z4v~?KO{YV!_BT(-apt&f)6w8JEGpibmwO-y$vk2NW1SG^TLGl?%QvGw}&^ z!n2yLTsZDFiF6WULLgbSZ9-AWXG;4@XDZicxuN;~L2kLI3ajf*k}CHzR4S|LC;y2z{GD7uKrqt znSyWj23^ff_tKHpASO1{_Hog9_InaZd60lH`{j!H`Q9KdAV+^u^I^yp4qORbWda$V znNgh-Mp{n}_Tx7=$ai$LcOK~OXlp-!o7@!zTignqy3wnJ-+r-93!iCe+`=o0v|!TH z<&Q0V+IqlExA$i>AbB~x1miDU#4SneVoWk$69$Lfsk2cAkpF9ejJ|-RQ~Ymf{L8XTU4icP>*m9iS>hC&fL&W?#=Gb-_jHsuGA5+BbXBDZ;V)1_?u)imVB!S}pPR{>9 z&c7z-|0L%=IV4qqWyp4kDih|3TNM9@oCG90`YV zHTn2FaI1*_!Xl9)-?fd+SMk!l`)0YZ>`GL5aj|9gUCPcyJ(R;P@Ek{L?dQLz|3WzK{-OuoqeAi|NpuwSma~ms#l5 zbf{%55IH=~Lcfv@HB)JR7J4NeYFaz?%buq~t>5q8$YY>AX;Az3{Tl)c>`e!nzhAfh zO615>HM6a2BL^=r=*uu0-85H3%@r|o)zZLk>?L$N*UISIm*2Yl{gdl~DDGdd_ioyc zMeWC8_7e~>6_?+AURQl8T775JI(w)$OtBJyB!Da?U5_ z0^I6=TOWC6#8UatV!w6j=BZ6fMbuIesl5Cv%S*p0D!KdeotN(h?*vybynE@LOY6=J zPi)uGd$qBm!DX0jK>FEsy9}98#zylk(@oQ+u_S6N*;1>l4e}( zE$2x2(R&3yFMq%Mp7p*bQhN4*@f^h`^NHtG$~xHV#gV+mNM6^{=`Fo3$D_hU7P`F0 zc^t}_x_kA`)tIAp)3H11*d24UKpv;HZE6e9V=JSW7UXM>?{@!r>z}uNs{ruxcjg~B z8aBe4N6to%oXraUo7|GSmTy?Td*SU%Z(WMz?%vGpjOKR6a`!Ere5f`2@u@#LB^MAW zZeMqeX z_GO7b_=AJ%t_?9#*!Mu&zojkEnI1Y|SX}w;i|@R+@kYF)4m#muKOT#deEUx6_oTJ% zKmF>x`k%YscVm`pmNZ36n%3)o)bfLtM>#sOWq!Pynd^LQ_PYn@3XhnrY17sbwROa7 zotRkV^~*g`ZArY8Upltj9n}_nTEs`{_C$;JERDpq`J39RsJ806p|``|39mJ5IPMol zt4AU)`XUrQ@tf^^)3#o`o`dC6*z-VpXh+K+58Eh@b__<^pVc>A8x8l0f82WC9qqmt zd2uY#bt!V`rAXoU1MOFKHqKhQQoCiB{ngEDeTu)m zPj#Y_L#4QH_<#P3V*83EHSFh?`t^AS4SqmW|ZvQ6!6NRmO0?p%Uc1N0fl#e54 zRgrTS9#PbPt^Z=nfR1}25x?SZ@_UQU^W3j_)5TiNuXp#kE>>zjs?^~lVH_W)ePn#x z*^n@dk5A1{j*p8qv|Yh~O#-nz!S+hF$IF)$e1kT$CP0WBIql@owk_}2uv8AQRK%PS zKY)`kpPVBFoI$#8n9&WmP!n;@rZY&DYhEj>83FV6Z8i^l+jf1E@@Rv=lG z>XEiwrTwg%Q{{fl**@mXA9FeEpW|cB#6ENXmTUZ9xRNcc79iUEtpZMGi|6i&7qmjq z{K%lq)o*F^R`pWORtaY*06_eVcnACAX=UOux&stIB>?Z!AaZ;w_$Ylz&=Ky`@z-4nilOwC2+S$lROa zjTccsWzm*KSDm+I;i~FaPCRntl-nLza|=z6bon~{)^3)xK3;zD(-K}uMz%TGk{PU& zEEmb@S+ZIx(W{b+Y-)@23eoD(PpRM+w5zSw{7^!G|WHnG0HY(eJvVH0aRgQW|x8>33Re87jw>Z33_psMm=VSWX zDzU0MSMs+wyjBO;YfX4eUt4^E>cGn67Khhb0eh|2J*KZMw_DY+g52<04YAj{<}rP3 zO{fY~dsifa-n&-MUh8`((cUe$O?6;Zv&G@H*2!M${g3HutJb30a~o;!T16VX)=(^7 z8*<=1=`nr%uAx9x{JVXxs8ssjEoi1xD%aMqhSOS>!!b?iM_S{5bqw3Kw(Rq*lX;vm xABqmD-1xQ)gY9q>1eCjc?vGDH(?ON{L?U+3ztIlt=RVlKXV9(rYnKY{{|9&SC0YOg literal 0 HcmV?d00001 diff --git a/.claude/skills/run-experiment/glm-kimi-adapter.py b/.claude/skills/run-experiment/glm-kimi-adapter.py new file mode 100644 index 00000000..316faa65 --- /dev/null +++ b/.claude/skills/run-experiment/glm-kimi-adapter.py @@ -0,0 +1,628 @@ +#!/usr/bin/env python3 +""" +TensorX API Adapter fuer den run-experiment Skill. + +Dieser Adapter fuehrt einen Headless-Versuchslauf mit einem OpenAI-kompatiblen +Modell (Z.AI GLM oder Moonshot Kimi) ueber den TensorX API-Gateway durch. +Er implementiert einen minimalen Agent-Loop mit Tool-Calling und erfasst +Token-Metadaten (inkl. Reasoning-Tokens) aus jeder API-Antwort. + +Der API-Key wird automatisch aus der Cline providers.json gelesen +(~/.cline/data/settings/providers.json, Provider "tensorx"). + +Verwendung: + python glm-kimi-adapter.py \ + --prompt \ + --root \ + --output \ + --model \ + --effort \ + [--max-turns 50] \ + [--temperature 1.0] \ + [--timeout 0] + +Ausgaben: + /RawResult.json – normalisierte Messdaten + /Stderr.log – Fehler- und Debug-Ausgaben + +Der Adapter ist bewusst eigenstaendig (nur Python-Standardbibliothek + requests). +""" + +import argparse +import hashlib +import json +import os +import re +import subprocess +import sys +import time +import traceback +from datetime import datetime, timezone +from pathlib import Path + +try: + import requests +except ImportError: + sys.stderr.write("FEHLER: 'requests' ist nicht installiert.\n") + sys.exit(2) + +# --------------------------------------------------------------------------- +# Provider-Konfiguration +# --------------------------------------------------------------------------- + +PROVIDERS = { + "tensorx": { + "name": "TensorX API Gateway", + "base_url": "https://api.tensorx.ai/v1", + "env_key": "TENSORX_API_KEY", + }, +} + +# Modell-Praefix -> Effort-Parameter-Typ +# z-ai/* Modelle nutzen den 'thinking'-Parameter (level: none|low|medium|high|xhigh) +# moonshotai/* Modelle nutzen 'reasoning_effort' (low|medium|high) +MODEL_EFFORT_TYPE = { + "z-ai": "thinking", + "moonshotai": "reasoning_effort", +} + +# Effort-Mapping: Skill-Effort -> API-Wert je Effort-Typ +EFFORT_MAP = { + "low": {"thinking": "low", "reasoning_effort": "low"}, + "medium": {"thinking": "medium", "reasoning_effort": "medium"}, + "high": {"thinking": "high", "reasoning_effort": "high"}, + "xhigh": {"thinking": "xhigh", "reasoning_effort": "high"}, + "max": {"thinking": "xhigh", "reasoning_effort": "high"}, +} + + +def load_cline_api_key(): + """ + Liest den TensorX API-Key aus der Cline providers.json. + Pfad: ~/.cline/data/settings/providers.json + Rueckgabe: (api_key, base_url) oder (None, None). + """ + home = Path.home() + providers_file = home / ".cline" / "data" / "settings" / "providers.json" + if not providers_file.is_file(): + return None, None + try: + data = json.loads(providers_file.read_text(encoding="utf-8")) + tx = data.get("providers", {}).get("tensorx", {}).get("settings", {}) + return tx.get("apiKey"), tx.get("baseUrl") + except (json.JSONDecodeError, KeyError): + return None, None + +# Denylist fuer schreibende/bauende Kommandos +DENIED_COMMAND_PATTERNS = [ + r"\brm\b", r"\brmdir\b", r"\bmv\b", r"\bcp\b", r"\bdd\b", + r"\btruncate\b", r"\bchmod\b", r"\bchown\b", r"\bln\b", r"\btee\b", + r"\bsed\s+-i\b", r"\bgit\s+checkout\b", r"\bgit\s+restore\b", + r"\bgit\s+clean\b", r"\bgit\s+reset\b", r"\bgit\s+add\b", + r"\bgit\s+commit\b", r"\bgit\s+push\b", r"\bgit\s+fetch\b", + r"\bdotnet\b", r"\bmsbuild\b", r"\bnpm\s+install\b", r"\bnuget\b", + r"\bpip\s+install\b", r">\s*", r">>\s*", +] + +# --------------------------------------------------------------------------- +# Tool-Definitionen (OpenAI Function Calling Format) +# --------------------------------------------------------------------------- + +TOOLS = [ + { + "type": "function", + "function": { + "name": "read_file", + "description": ( + "Lies den Inhalt einer Textdatei. Der Pfad ist relativ zum " + "Arbeitsverzeichnis (Root der Codebasis)." + ), + "parameters": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Relativer Pfad zur Datei (z.B. 'src/Program.cs')", + }, + }, + "required": ["path"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "list_directory", + "description": "Liste den Inhalt eines Verzeichnisses mit Typ-Kennzeichnung.", + "parameters": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Relativer Pfad zum Verzeichnis (leer = Root)", + }, + }, + "required": ["path"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "search_files", + "description": ( + "Durchsuche Dateien mit einem Regex-Muster (aehnlich grep -rn). " + "Gibt Treffer mit Dateiname, Zeilennummer und Zeileninhalt zurueck." + ), + "parameters": { + "type": "object", + "properties": { + "pattern": {"type": "string", "description": "Regex-Suchmuster"}, + "path": { + "type": "string", + "description": "Relativer Pfad zum Startverzeichnis (leer = Root)", + }, + "file_pattern": { + "type": "string", + "description": "Dateifilter (z.B. '*.cs'), optional", + }, + }, + "required": ["pattern"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "execute_command", + "description": ( + "Fuehre einen schreibgeschuetzten Shell-Befehl im Arbeitsverzeichnis " + "aus. Schreibende und bauende Kommandos werden abgelehnt." + ), + "parameters": { + "type": "object", + "properties": { + "command": {"type": "string", "description": "Der auszufuehrende Befehl"}, + }, + "required": ["command"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "write_file", + "description": ( + "Schreibe eine Ergebnisdatei in das Ausgabeverzeichnis. Der Pfad " + "ist relativ zum Ausgabeverzeichnis (z.B. 'StRS.md')." + ), + "parameters": { + "type": "object", + "properties": { + "path": {"type": "string", "description": "Relativer Pfad"}, + "content": {"type": "string", "description": "Vollstaendiger Dateiinhalt"}, + }, + "required": ["path", "content"], + }, + }, + }, +] + + +# --------------------------------------------------------------------------- +# Pfad-Sicherheit +# --------------------------------------------------------------------------- + +def safe_join(root: str, rel_path: str) -> Path: + """Verbindet root und rel_path, verhindert Path-Traversal.""" + root_resolved = Path(root).resolve() + target = (root_resolved / rel_path).resolve() + if not str(target).startswith(str(root_resolved)): + raise ValueError(f"Pfad '{rel_path}' verlaesst das Root-Verzeichnis") + return target + +# --------------------------------------------------------------------------- +# Tool-Implementierungen +# --------------------------------------------------------------------------- + +def tool_read_file(root: str, args: dict) -> str: + path = args.get("path", "") + try: + full = safe_join(root, path) + if not full.is_file(): + return f"FEHLER: Datei nicht gefunden: {path}" + content = full.read_text(encoding="utf-8", errors="replace") + if len(content) > 200000: + content = content[:200000] + "\n\n[... Datei abgeschnitten bei 200.000 Zeichen ...]" + return content + except ValueError as e: + return f"FEHLER: {e}" + except Exception as e: + return f"FEHLER beim Lesen von {path}: {e}" + + +def tool_list_directory(root: str, args: dict) -> str: + path = args.get("path", "") + try: + full = safe_join(root, path) if path else Path(root).resolve() + if not full.is_dir(): + return f"FEHLER: Verzeichnis nicht gefunden: {path}" + entries = [] + for child in sorted(full.iterdir(), key=lambda c: (c.is_file(), c.name.lower())): + typ = "[DIR] " if child.is_dir() else "[FILE]" + size = "" + if child.is_file(): + try: + size = f" ({child.stat().st_size:,} bytes)" + except OSError: + pass + entries.append(f"{typ} {child.name}{size}") + return "\n".join(entries) if entries else "(leeres Verzeichnis)" + except ValueError as e: + return f"FEHLER: {e}" + except Exception as e: + return f"FEHLER beim Auflisten von {path}: {e}" + + +def tool_search_files(root: str, args: dict) -> str: + pattern = args.get("pattern", "") + path = args.get("path", "") + file_pattern = args.get("file_pattern", "") + if not pattern: + return "FEHLER: Kein Suchmuster angegeben" + try: + regex = re.compile(pattern, re.IGNORECASE) + search_root = safe_join(root, path) if path else Path(root).resolve() + if not search_root.is_dir(): + return f"FEHLER: Verzeichnis nicht gefunden: {path}" + results = [] + max_results = 500 + max_file_size = 5 * 1024 * 1024 + for file_path in search_root.rglob("*"): + if not file_path.is_file(): + continue + if file_pattern: + import fnmatch + if not fnmatch.fnmatch(file_path.name, file_pattern): + continue + try: + if file_path.stat().st_size > max_file_size: + continue + except OSError: + continue + try: + rel = file_path.relative_to(Path(root).resolve()) + except ValueError: + continue + try: + with open(file_path, "r", encoding="utf-8", errors="replace") as f: + for line_no, line in enumerate(f, 1): + if regex.search(line): + results.append(f"{rel}:{line_no}: {line.rstrip()[:300]}") + if len(results) >= max_results: + results.append(f"\n[... Suche bei {max_results} Treffern abgeschnitten ...]") + return "\n".join(results) + except Exception: + continue + return "\n".join(results) if results else "Keine Treffer." + except re.error as e: + return f"FEHLER: Ungueltiges Regex-Muster: {e}" + except ValueError as e: + return f"FEHLER: {e}" + except Exception as e: + return f"FEHLER bei der Suche: {e}" + + +def tool_execute_command(root: str, args: dict) -> str: + command = args.get("command", "") + if not command: + return "FEHLER: Kein Befehl angegeben" + for pat in DENIED_COMMAND_PATTERNS: + if re.search(pat, command, re.IGNORECASE): + return "ABGELEHNT: Befehl enthaelt verbotenes Muster. Schreibende und bauende Kommandos sind gesperrt." + try: + result = subprocess.run( + command, shell=True, cwd=root, capture_output=True, text=True, timeout=60, + ) + output = result.stdout or "" + if result.stderr: + output += f"\n[STDERR]\n{result.stderr}" + if len(output) > 100000: + output = output[:100000] + "\n\n[... Ausgabe abgeschnitten ...]" + return output.strip() if output.strip() else "(keine Ausgabe)" + except subprocess.TimeoutExpired: + return "FEHLER: Befehl nach 60 Sekunden abgebrochen" + except Exception as e: + return f"FEHLER bei Befehlsausfuehrung: {e}" + + +def tool_write_file(output_dir: str, args: dict) -> str: + path = args.get("path", "") + content = args.get("content", "") + if not path: + return "FEHLER: Kein Dateipfad angegeben" + try: + base = Path(output_dir).resolve() + target = (base / path).resolve() + if not str(target).startswith(str(base)): + return f"FEHLER: Pfad '{path}' verlaesst das Ausgabeverzeichnis" + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(content, encoding="utf-8") + return f"OK: Datei geschrieben: {path} ({len(content):,} Zeichen)" + except Exception as e: + return f"FEHLER beim Schreiben von {path}: {e}" + + +def execute_tool(name: str, args: dict, root: str, output_dir: str) -> str: + """Dispatch eines Tool-Aufrufs.""" + dispatch = { + "read_file": lambda a: tool_read_file(root, a), + "list_directory": lambda a: tool_list_directory(root, a), + "search_files": lambda a: tool_search_files(root, a), + "execute_command": lambda a: tool_execute_command(root, a), + "write_file": lambda a: tool_write_file(output_dir, a), + } + handler = dispatch.get(name) + if handler: + return handler(args) + return f"FEHLER: Unbekanntes Werkzeug: {name}" + + +# --------------------------------------------------------------------------- +# API-Aufruf +# --------------------------------------------------------------------------- + +def call_api(provider, model, messages, api_key, effort, temperature, timeout): + """Ruft die Chat-Completions-API auf und gibt die JSON-Antwort zurueck.""" + url = f"{provider['base_url']}/chat/completions" + headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} + body = { + "model": model, "messages": messages, "tools": TOOLS, + "tool_choice": "auto", "temperature": temperature, "stream": False, + } + # Effort-Parameter anhand des Modell-Praefixes waehlen + model_prefix = model.split("/")[0] if "/" in model else "" + effort_type = MODEL_EFFORT_TYPE.get(model_prefix, "thinking") + effort_val = EFFORT_MAP.get(effort, {}).get(effort_type, "medium") + if effort_type == "thinking": + body["thinking"] = {"type": "enabled", "level": effort_val} + elif effort_type == "reasoning_effort": + body["reasoning_effort"] = effort_val + resp = requests.post(url, headers=headers, json=body, + timeout=timeout if timeout > 0 else 1800) + if resp.status_code != 200: + raise RuntimeError(f"API-Fehler {resp.status_code}: {resp.text[:2000]}") + return resp.json() + + +# --------------------------------------------------------------------------- +# Agent-Loop +# --------------------------------------------------------------------------- + +def run_agent_loop(provider, model, system_prompt, user_prompt, api_key, effort, + root, output_dir, max_turns, temperature, timeout): + """Fuehrt den Agent-Loop durch und sammelt Metriken.""" + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt}, + ] + total_usage = {"prompt_tokens": 0, "completion_tokens": 0, + "total_tokens": 0, "cached_tokens": 0, "reasoning_tokens": 0} + turns = 0 + tool_calls_log = [] + final_content = "" + model_reported = model + finish_reason = None + errors = [] + start_time = time.time() + + while turns < max_turns: + turns += 1 + try: + response = call_api(provider, model, messages, api_key, + effort, temperature, timeout) + except Exception as e: + errors.append(f"Turn {turns}: API-Fehler: {e}") + break + usage = response.get("usage", {}) + total_usage["prompt_tokens"] += usage.get("prompt_tokens", 0) + total_usage["completion_tokens"] += usage.get("completion_tokens", 0) + total_usage["total_tokens"] += usage.get("total_tokens", 0) + cached = usage.get("prompt_tokens_details", {}).get("cached_tokens", 0) + total_usage["cached_tokens"] += cached + # Reasoning/Thinking-Tokens aus completion_tokens_details + comp_details = usage.get("completion_tokens_details", {}) + total_usage["reasoning_tokens"] += comp_details.get("reasoning_tokens", 0) + if response.get("model"): + model_reported = response["model"] + choices = response.get("choices", []) + if not choices: + errors.append(f"Turn {turns}: Keine choices in API-Antwort") + break + choice = choices[0] + finish_reason = choice.get("finish_reason") + msg = choice.get("message", {}) + messages.append(msg) + content = msg.get("content", "") + if content: + final_content = content + tool_calls = msg.get("tool_calls", []) + if not tool_calls: + break + for tc in tool_calls: + func = tc.get("function", {}) + tool_name = func.get("name", "") + tool_args_str = func.get("arguments", "{}") + tc_id = tc.get("id", "") + try: + tool_args = json.loads(tool_args_str) + except json.JSONDecodeError: + tool_args = {} + tool_calls_log.append({"turn": turns, "name": tool_name, "args": tool_args}) + result = execute_tool(tool_name, tool_args, root, output_dir) + messages.append({"role": "tool", "tool_call_id": tc_id, + "name": tool_name, "content": result}) + + end_time = time.time() + duration_s = end_time - start_time + written_files = [] + if os.path.isdir(output_dir): + for f in sorted(Path(output_dir).rglob("*")): + if f.is_file(): + try: + written_files.append({"path": str(f.relative_to(output_dir)), + "size": f.stat().st_size}) + except OSError: + pass + tool_call_types = {} + for tc in tool_calls_log: + name = tc["name"] + tool_call_types[name] = tool_call_types.get(name, 0) + 1 + return { + "is_error": len(errors) > 0 and not final_content, + "subtype": "success" if final_content else "error", + "duration_ms": int(duration_s * 1000), + "duration_api_ms": int(duration_s * 1000), + "num_turns": turns, "model": model_reported, "model_requested": model, + "provider": provider["__id"], + "usage": { + "prompt_tokens": total_usage["prompt_tokens"], + "completion_tokens": total_usage["completion_tokens"], + "total_tokens": total_usage["total_tokens"], + "cached_tokens": total_usage["cached_tokens"], + "cache_read_tokens": total_usage["cached_tokens"], + "cache_creation_tokens": 0, + "reasoning_tokens": total_usage["reasoning_tokens"], + "output_tokens_details": { + "thinking_tokens": total_usage["reasoning_tokens"], + }, + }, + "modelUsage": { + model_reported: { + "input_tokens": total_usage["prompt_tokens"], + "output_tokens": total_usage["completion_tokens"], + "cache_read_input_tokens": total_usage["cached_tokens"], + "cache_creation_input_tokens": 0, + "reasoning_tokens": total_usage["reasoning_tokens"], + } + }, + "tool_calls": tool_calls_log, + "tool_call_count": len(tool_calls_log), + "tool_call_types": tool_call_types, + "written_files": written_files, "result": final_content, + "finish_reason": finish_reason, "errors": errors, "session_id": "", + "adapter": "python-glm-kimi", "adapter_version": "1.0.0", + } + + +# --------------------------------------------------------------------------- +# Hauptprogramm +# --------------------------------------------------------------------------- + +def main(): + parser = argparse.ArgumentParser(description="TensorX API Adapter fuer run-experiment (GLM/Kimi)") + parser.add_argument("--prompt", required=True, help="Pfad zur combined_prompt.md") + parser.add_argument("--root", required=True, help="Root-Verzeichnis der Codebasis") + parser.add_argument("--output", required=True, help="Ausgabeverzeichnis (Ergebnisse)") + parser.add_argument("--model", required=True, help="Modell-ID (z.B. z-ai/glm-5.2, moonshotai/kimi-k3)") + parser.add_argument("--provider", default="tensorx", help="API-Provider (default: tensorx)") + parser.add_argument("--api-key", default=None, help="API-Key (default: aus Cline providers.json)") + parser.add_argument("--effort", default="high", choices=["low", "medium", "high", "xhigh", "max"]) + parser.add_argument("--max-turns", type=int, default=50) + parser.add_argument("--temperature", type=float, default=1.0) + parser.add_argument("--timeout", type=int, default=0, help="Timeout in Sek (0=keins)") + parser.add_argument("--result-dir", default=None, help="Verzeichnis fuer RawResult.json") + args = parser.parse_args() + + provider = PROVIDERS.get(args.provider, PROVIDERS["tensorx"]).copy() + provider["__id"] = args.provider + + # API-Key: erst --api-key, dann Cline providers.json, dann Env-Var + api_key = args.api_key + base_url_override = None + if not api_key: + cline_key, cline_url = load_cline_api_key() + if cline_key: + api_key = cline_key + base_url_override = cline_url + sys.stderr.write("[glm-kimi-adapter] API-Key aus Cline providers.json gelesen.\n") + else: + api_key = os.environ.get(provider.get("env_key", ""), "") + if base_url_override: + provider["base_url"] = base_url_override + if not api_key: + sys.stderr.write( + "FEHLER: Kein API-Key gefunden. Weder --api-key, noch Cline providers.json, " + f"noch Umgebungsvariable {provider.get('env_key', '')}.\n" + ) + sys.exit(2) + + prompt_path = Path(args.prompt) + if not prompt_path.is_file(): + sys.stderr.write(f"FEHLER: Prompt-Datei nicht gefunden: {args.prompt}\n") + sys.exit(2) + user_prompt = prompt_path.read_text(encoding="utf-8") + + system_prompt = ( + "Du bist ein Requirements Engineer im Reverse Requirements Engineering " + "eines Legacy-ERP-Systems. Du analysierst die Codebasis im Arbeitsverzeichnis " + "und erstellst eine Anforderungsspezifikation nach ISO/IEC/IEEE 29148:2018.\n\n" + "Werkzeuge: read_file, list_directory, search_files, execute_command, write_file.\n" + "Die Codebasis wird ausschliesslich GELESEN. Schreibe Ergebnisdateien mit " + "write_file ins Ausgabeverzeichnis. Sprache: Deutsch fuer Anforderungen." + ) + + output_dir = Path(args.output).resolve() + output_dir.mkdir(parents=True, exist_ok=True) + result_dir = Path(args.result_dir) if args.result_dir else output_dir.parent + result_dir.mkdir(parents=True, exist_ok=True) + + start_iso = datetime.now(timezone.utc).isoformat() + sys.stderr.write(f"[glm-kimi-adapter] Start: {start_iso}\n") + sys.stderr.write(f"[glm-kimi-adapter] Provider: {provider['name']}\n") + sys.stderr.write(f"[glm-kimi-adapter] Modell: {args.model}\n") + sys.stderr.write(f"[glm-kimi-adapter] Effort: {args.effort}\n") + + try: + result = run_agent_loop( + provider=provider, model=args.model, system_prompt=system_prompt, + user_prompt=user_prompt, api_key=api_key, effort=args.effort, + root=args.root, output_dir=str(output_dir), max_turns=args.max_turns, + temperature=args.temperature, timeout=args.timeout, + ) + except Exception as e: + tb = traceback.format_exc() + sys.stderr.write(f"[glm-kimi-adapter] FEHLER: {e}\n{tb}\n") + result = { + "is_error": True, "subtype": "error", "error": str(e), + "duration_ms": 0, "num_turns": 0, "model": args.model, + "model_requested": args.model, "provider": args.provider, + "usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0, + "cached_tokens": 0, "cache_read_tokens": 0, "cache_creation_tokens": 0, + "reasoning_tokens": 0}, + "modelUsage": {}, "tool_calls": [], "tool_call_count": 0, + "tool_call_types": {}, "written_files": [], "result": "", + "errors": [str(e)], "adapter": "python-glm-kimi", "adapter_version": "1.0.0", + } + + end_iso = datetime.now(timezone.utc).isoformat() + result["start_time"] = start_iso + result["end_time"] = end_iso + + raw_result_path = result_dir / "RawResult.json" + raw_result_path.write_text(json.dumps(result, indent=2, ensure_ascii=False), encoding="utf-8") + + sys.stderr.write(f"[glm-kimi-adapter] Ende: {end_iso}\n") + sys.stderr.write(f"[glm-kimi-adapter] Turns: {result['num_turns']}\n") + sys.stderr.write(f"[glm-kimi-adapter] Tokens gesamt: {result['usage']['total_tokens']:,}\n") + sys.stderr.write(f"[glm-kimi-adapter] Tool-Calls: {result['tool_call_count']}\n") + sys.stderr.write(f"[glm-kimi-adapter] Ergebnisdateien: {len(result['written_files'])}\n") + sys.stderr.write(f"[glm-kimi-adapter] RawResult: {raw_result_path}\n") + + sys.exit(1 if result["is_error"] else 0) + + +if __name__ == "__main__": + main() + +