tensorx adapter
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: run-experiment
|
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 <Pfad-zur-Prompt-Datei>" 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 <Pfad-zur-Prompt-Datei>" oder wenn der User einen Versuch/ein Experiment ausführen und tracken will.
|
||||||
argument-hint: <Pfad zur Prompt-Datei> <Root-Verzeichnis>
|
argument-hint: <Pfad zur Prompt-Datei> <Root-Verzeichnis>
|
||||||
version: 7.0.0
|
version: 8.0.0
|
||||||
---
|
---
|
||||||
|
|
||||||
# RunExperiment – Versuchslauf mit Messprotokoll
|
# 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
|
- **`## 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.
|
Teil gilt unabhängig davon, mit welchem LLM oder welcher CLI gearbeitet wird.
|
||||||
- **`## Werkzeugadapter`** beschreibt **wie** das mit einem konkreten Werkzeug umgesetzt 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.
|
ausschließlich dem gewählten Adapterabschnitt zu entnehmen.
|
||||||
|
|
||||||
**Arbeitsteilung mit der Prompt-Datei:** Der Prompt enthält ausschließlich die *Analyseanweisung*
|
**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.
|
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.
|
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:
|
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.
|
Keine Modell-ID an eine CLI übergeben, die sie nicht unterstützt.
|
||||||
|
|
||||||
Unter Windows liegt `claude` in der Regel **nicht im PATH**. Erst
|
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-terra` | ausgewogenes Verhältnis aus Leistung und Verbrauch |
|
||||||
| `gpt-5.6-luna` | schnelle und günstige GPT-5.6-Variante |
|
| `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
|
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").
|
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','') }
|
Sort-Object { [int]($_.Name -replace '\D','') }
|
||||||
$iteration = if ($iterationen) { $iterationen[-1].Name } else { 'Iteration 1' }
|
$iteration = if ($iterationen) { $iterationen[-1].Name } else { 'Iteration 1' }
|
||||||
$zelle = Join-Path (Join-Path (Join-Path $iteration $modell) $modus) $effort
|
$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 {
|
do {
|
||||||
$id4 = '{0:x4}' -f (Get-Random -Maximum 65536)
|
$id4 = '{0:x4}' -f (Get-Random -Maximum 65536)
|
||||||
$lauf = Join-Path "<promptverzeichnis>\$zelle" "<NN>_Lauf_$(Get-Date -Format 'yyyy-MM-dd_HHmmss')_${skillVer}-$id4"
|
$lauf = Join-Path "<promptverzeichnis>\$zelle" "<NN>_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
|
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.
|
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 = "<Verzeichnis des Skills>"
|
||||||
|
$lauf = "<absoluter Pfad zum Laufverzeichnis>"
|
||||||
|
$root = "<Root-Verzeichnis der Codebasis>"
|
||||||
|
$modell = "<vom User gewählte Modell-ID, z.B. z-ai/glm-5.2>"
|
||||||
|
$effort = "<vom User gewählte Stufe>"
|
||||||
|
|
||||||
|
# Prompt zusammenstellen (wie bei den anderen Adaptern)
|
||||||
|
$prompt = (Get-Content "<prompt-datei>" -Raw) + "`n`n<Werkzeugkontext-Block>`n`n<Ausgabe-Block>"
|
||||||
|
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)
|
### 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
|
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 |
|
| 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 |
|
| **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.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 |
|
| **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 |
|
||||||
|
|||||||
Binary file not shown.
@@ -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 <Pfad zur combined_prompt.md> \
|
||||||
|
--root <Root-Verzeichnis der Codebasis> \
|
||||||
|
--output <Laufverzeichnis/Ergebnisse> \
|
||||||
|
--model <Modell-ID, z.B. z-ai/glm-5.2 oder moonshotai/kimi-k3> \
|
||||||
|
--effort <low|medium|high|xhigh> \
|
||||||
|
[--max-turns 50] \
|
||||||
|
[--temperature 1.0] \
|
||||||
|
[--timeout 0]
|
||||||
|
|
||||||
|
Ausgaben:
|
||||||
|
<Laufverzeichnis>/RawResult.json – normalisierte Messdaten
|
||||||
|
<Laufverzeichnis>/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()
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user