agentram 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agentram/cli.py +11 -5
- package/agentram/orchestration.py +47 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
package/agentram/cli.py
CHANGED
|
@@ -660,7 +660,7 @@ def supervisor_workflow_text(context: McpContext, prompt: str) -> str:
|
|
|
660
660
|
try:
|
|
661
661
|
outputs[slot] = future.result()
|
|
662
662
|
except Exception as error: # noqa: BLE001 - agent boundary
|
|
663
|
-
outputs[slot] = f"error: {error}"
|
|
663
|
+
outputs[slot] = f"error: {model_error_hint(str(error))}"
|
|
664
664
|
if not main_agent:
|
|
665
665
|
outputs["main"] = "No main agent bound. Use /bind-agent main <provider> <model>."
|
|
666
666
|
if small_agent and outputs.get("small") and not outputs["small"].startswith("error:"):
|
|
@@ -821,10 +821,16 @@ def model_error_hint(message: str) -> str:
|
|
|
821
821
|
lower = message.lower()
|
|
822
822
|
if "base_url is required" in lower:
|
|
823
823
|
return message + " | Fix: /remove-model <id> or re-bind with --base-url http://localhost:8000/v1"
|
|
824
|
-
if "actively refused" in lower or "connection refused" in lower or "winerror 10061" in lower:
|
|
825
|
-
return message + " | Fix: start your OpenAI-compatible server at base_url, or change --base-url."
|
|
826
|
-
if "
|
|
827
|
-
return message + " | Fix:
|
|
824
|
+
if "actively refused" in lower or "connection refused" in lower or "winerror 10061" in lower:
|
|
825
|
+
return message + " | Fix: start your OpenAI-compatible server at base_url, or change --base-url."
|
|
826
|
+
if "http 404" in lower or "404: not found" in lower:
|
|
827
|
+
return message + " | Fix: verify base_url includes /v1, provider supports /chat/completions, and model name exists."
|
|
828
|
+
if "http 400" in lower and "not supported" in lower:
|
|
829
|
+
return message + " | Fix: this account/provider rejects that model. Bind a supported model name from your local endpoint."
|
|
830
|
+
if "returned non-json" in lower or "expecting value" in lower:
|
|
831
|
+
return message + " | Fix: endpoint returned empty/HTML/non-JSON. Check base_url, auth key, proxy, and server logs."
|
|
832
|
+
if "missing api key env" in lower:
|
|
833
|
+
return message + " | Fix: set environment variable, then bind using --api-key-env ENV_NAME, not raw key."
|
|
828
834
|
return message
|
|
829
835
|
|
|
830
836
|
def claude_template_root() -> Path:
|
|
@@ -6,6 +6,7 @@ import json
|
|
|
6
6
|
import os
|
|
7
7
|
import shlex
|
|
8
8
|
import subprocess
|
|
9
|
+
import urllib.error
|
|
9
10
|
import urllib.request
|
|
10
11
|
from typing import Any, Callable
|
|
11
12
|
from uuid import uuid4
|
|
@@ -125,8 +126,23 @@ class MultiModelCodingOrchestrator:
|
|
|
125
126
|
headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"} if api_key else {"Content-Type": "application/json"},
|
|
126
127
|
method="POST",
|
|
127
128
|
)
|
|
128
|
-
|
|
129
|
-
|
|
129
|
+
url = endpoint.base_url.rstrip("/") + "/chat/completions"
|
|
130
|
+
try:
|
|
131
|
+
with urllib.request.urlopen(request, timeout=120) as response:
|
|
132
|
+
raw = response.read().decode("utf-8")
|
|
133
|
+
except urllib.error.HTTPError as error:
|
|
134
|
+
body = error.read().decode("utf-8", errors="replace") if error.fp else ""
|
|
135
|
+
hint = f"OpenAI-compatible HTTP {error.code} at {url}"
|
|
136
|
+
if error.code == 404:
|
|
137
|
+
hint += " | base_url should include /v1 but not /chat/completions; verify provider URL and model name"
|
|
138
|
+
raise RuntimeError(f"{hint} | response={body[:500]}") from error
|
|
139
|
+
try:
|
|
140
|
+
data = json.loads(raw)
|
|
141
|
+
except json.JSONDecodeError as error:
|
|
142
|
+
streamed = parse_openai_sse(raw)
|
|
143
|
+
if streamed:
|
|
144
|
+
return streamed
|
|
145
|
+
raise RuntimeError(f"OpenAI-compatible returned non-JSON at {url} | response={raw[:500]}") from error
|
|
130
146
|
return str(data.get("choices", [{}])[0].get("message", {}).get("content", ""))
|
|
131
147
|
|
|
132
148
|
def _call_anthropic(self, endpoint: AgentModelEndpoint, prompt: str) -> str:
|
|
@@ -175,3 +191,32 @@ class MultiModelCodingOrchestrator:
|
|
|
175
191
|
if process.returncode != 0:
|
|
176
192
|
raise RuntimeError(error or f"claude command failed with exit code {process.returncode}")
|
|
177
193
|
return output or error
|
|
194
|
+
|
|
195
|
+
def parse_openai_sse(raw: str) -> str:
|
|
196
|
+
parts: list[str] = []
|
|
197
|
+
for line in raw.splitlines():
|
|
198
|
+
stripped = line.strip()
|
|
199
|
+
if not stripped.startswith("data:"):
|
|
200
|
+
continue
|
|
201
|
+
payload = stripped[5:].strip()
|
|
202
|
+
if not payload or payload == "[DONE]":
|
|
203
|
+
continue
|
|
204
|
+
try:
|
|
205
|
+
item = json.loads(payload)
|
|
206
|
+
except json.JSONDecodeError:
|
|
207
|
+
continue
|
|
208
|
+
choices = item.get("choices", [])
|
|
209
|
+
if not choices or not isinstance(choices, list):
|
|
210
|
+
continue
|
|
211
|
+
choice = choices[0]
|
|
212
|
+
if not isinstance(choice, dict):
|
|
213
|
+
continue
|
|
214
|
+
delta = choice.get("delta", {})
|
|
215
|
+
if isinstance(delta, dict):
|
|
216
|
+
content = delta.get("content")
|
|
217
|
+
if content:
|
|
218
|
+
parts.append(str(content))
|
|
219
|
+
message = choice.get("message", {})
|
|
220
|
+
if isinstance(message, dict) and message.get("content"):
|
|
221
|
+
parts.append(str(message.get("content")))
|
|
222
|
+
return "".join(parts).strip()
|
package/package.json
CHANGED