Skip to content

Commit f7b942f

Browse files
committed
Fix provider routing and Telegram memory
1 parent e355f13 commit f7b942f

11 files changed

Lines changed: 829 additions & 65 deletions

File tree

ADWs/runner.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,74 @@ def run_claude(prompt: str, log_name: str = "unnamed", timeout: int = 600, agent
214214
agent_label = f"@{agent}"
215215
else:
216216
agent_label = ""
217+
218+
use_fallback = os.environ.get("ADW_PROVIDER_FALLBACK", "1").lower() not in (
219+
"0", "false", "no",
220+
)
221+
if use_fallback:
222+
try:
223+
sys.path.insert(0, str(WORKSPACE / "dashboard" / "backend"))
224+
from provider_fallback import invoke_with_fallback
225+
226+
provider_label = "[fallback]"
227+
console.print(f" [step]▶[/step] {log_name} [dim]{agent_label} {provider_label}[/dim]", end="")
228+
start_time = datetime.now()
229+
result = invoke_with_fallback(
230+
prompt=prompt,
231+
max_turns=10,
232+
timeout_seconds=timeout,
233+
agent=agent or "",
234+
)
235+
duration = (datetime.now() - start_time).total_seconds()
236+
stdout = result.get("output", "") or ""
237+
stderr = result.get("error", "") or ""
238+
success = result.get("status") == "success"
239+
240+
usage = None
241+
if result.get("tokens_in") is not None or result.get("tokens_out") is not None or result.get("cost_usd") is not None:
242+
usage = {
243+
"input_tokens": result.get("tokens_in") or 0,
244+
"output_tokens": result.get("tokens_out") or 0,
245+
"cost_usd": result.get("cost_usd") or 0,
246+
}
247+
248+
result_text = stdout
249+
try:
250+
json_result = json.loads(stdout)
251+
usage = usage or _parse_usage(json_result)
252+
result_text = json_result.get("result", stdout)
253+
except (json.JSONDecodeError, TypeError, AttributeError):
254+
pass
255+
256+
full_prompt = f"[agent:{agent}] {prompt}" if agent else prompt
257+
returncode = 0 if success else 1
258+
_log_to_file(log_name, full_prompt, result_text, stderr, returncode, duration, usage)
259+
_save_metrics(log_name, duration, returncode, agent, result_text, usage)
260+
261+
model_info = f" | {result.get('provider_id')}:{result.get('model')}" if result.get("provider_id") else ""
262+
if success:
263+
cost_str = ""
264+
if usage:
265+
tokens_total = usage["input_tokens"] + usage["output_tokens"]
266+
cost_str = f" | {tokens_total:,}tok | ${usage['cost_usd']:.2f}"
267+
console.print(f"\r [success]✓[/success] {log_name} [dim]({duration:.0f}s{cost_str}{model_info})[/dim]")
268+
else:
269+
console.print(f"\r [error]✗[/error] {log_name} [dim](fallback exhausted, {duration:.0f}s{model_info})[/dim]")
270+
if stderr:
271+
for err_line in stderr.strip().splitlines()[:3]:
272+
console.print(f" [error]{err_line}[/error]")
273+
274+
return {
275+
"success": success,
276+
"stdout": result_text,
277+
"stderr": stderr,
278+
"returncode": returncode,
279+
"duration": duration,
280+
"usage": usage,
281+
}
282+
except Exception as e:
283+
console.print(f"\r [warning]⚠[/warning] {log_name} [dim](fallback unavailable: {e}; using active provider)[/dim]")
284+
217285
provider_label = f"[{cli_command}]" if cli_command != "claude" else ""
218286
console.print(f" [step]▶[/step] {log_name} [dim]{agent_label} {provider_label}[/dim]", end="")
219287

Makefile

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -150,20 +150,50 @@ uninstall: ## 🗑️ Full cleanup — stop services, remove nginx, da
150150
bling-auth: ## 🔐 Bling OAuth2 login (one-time: capture access + refresh tokens into .env)
151151
@python3 .claude/skills/int-bling/scripts/bling_auth.py
152152

153-
telegram: ## 📨 Start Telegram bot in background (screen)
153+
telegram: ## 📨 Start Telegram bot using active provider in background (screen)
154154
@command -v screen >/dev/null 2>&1 || { echo "❌ 'screen' is not installed — run: sudo apt install screen"; exit 1; }
155-
@command -v bun >/dev/null 2>&1 || [ -x "$$HOME/.bun/bin/bun" ] || { echo "❌ 'bun' is not installed (required by the telegram plugin MCP) — run: curl -fsSL https://bun.sh/install | bash"; exit 1; }
156-
@if screen -list | grep -q '\.telegram'; then \
155+
@if screen -list | grep -qE '\.telegram[[:space:]]'; then \
157156
echo "⚠ Telegram bot is already running. Use 'make telegram-stop' first or 'make telegram-attach' to connect."; \
158157
else \
159-
screen -dmS telegram env CLAUDE_CODE_USE_OPENAI=1 OPENAI_MODEL=codexplan claude --channels plugin:telegram@claude-plugins-official --dangerously-skip-permissions; \
160-
echo "✅ Telegram bot running on Codex (GPT-5.5) in background (screen: telegram)"; \
158+
screen -dmS telegram python3 scripts/telegram_provider_bot.py; \
159+
printf "⏳ Waiting for Telegram provider bot"; \
160+
for i in $$(seq 1 10); do \
161+
pgrep -af '[p]ython3 scripts/telegram_provider_bot.py' >/dev/null && break; \
162+
printf "."; sleep 1; \
163+
done; \
164+
pgrep -af '[p]ython3 scripts/telegram_provider_bot.py' >/dev/null && echo " ✅ ready" || { echo " ❌ Telegram provider bot did not start — check: screen -r telegram"; exit 1; }; \
165+
echo "✅ Telegram bot running with active EvoNexus provider (screen: telegram)"; \
161166
echo "📺 Ver: screen -r telegram"; \
162167
echo "🛑 Parar: make telegram-stop"; \
163168
fi
164169

165-
telegram-stop: ## 🛑 Stop the Telegram bot
166-
@screen -S telegram -X quit 2>/dev/null && echo "✅ Telegram bot stopped" || echo "⚠ Was not running"
170+
telegram-channel: ## 🧪 Start legacy Claude Code Channels Telegram bot via LiteLLM proxy
171+
@command -v screen >/dev/null 2>&1 || { echo "❌ 'screen' is not installed — run: sudo apt install screen"; exit 1; }
172+
@command -v bun >/dev/null 2>&1 || [ -x "$$HOME/.bun/bin/bun" ] || { echo "❌ 'bun' is not installed (required by the telegram plugin MCP) — run: curl -fsSL https://bun.sh/install | bash"; exit 1; }
173+
@command -v curl >/dev/null 2>&1 || { echo "❌ 'curl' is not installed"; exit 1; }
174+
@[ -x .venv/bin/litellm ] || { echo "❌ litellm not installed in .venv — run: uv pip install 'litellm[proxy]'"; exit 1; }
175+
@if screen -list | grep -q '\.telegram-proxy'; then \
176+
echo "ℹ LiteLLM proxy already running (screen: telegram-proxy)"; \
177+
else \
178+
screen -dmS telegram-proxy bash scripts/telegram_litellm_proxy.sh; \
179+
printf "⏳ Starting LiteLLM proxy (Anthropic→NVIDIA) on :4000"; \
180+
for i in $$(seq 1 30); do \
181+
curl -s -m2 http://127.0.0.1:4000/health/readiness >/dev/null 2>&1 && break; \
182+
printf "."; sleep 1; \
183+
done; \
184+
curl -s -m2 http://127.0.0.1:4000/health/readiness >/dev/null 2>&1 && echo " ✅ ready" || { echo " ❌ proxy failed to start — check: screen -r telegram-proxy"; exit 1; }; \
185+
fi
186+
@if screen -list | grep -qE '\.telegram[[:space:]]'; then \
187+
echo "⚠ Telegram bot is already running. Use 'make telegram-stop' first or 'make telegram-attach' to connect."; \
188+
else \
189+
screen -dmS telegram env ANTHROPIC_BASE_URL=http://127.0.0.1:4000 ANTHROPIC_AUTH_TOKEN=sk-evonexus-telegram-local ANTHROPIC_MODEL=telegram-nvidia claude --channels plugin:telegram@claude-plugins-official --dangerously-skip-permissions; \
190+
echo "✅ Legacy Telegram channel running (screen: telegram)"; \
191+
fi
192+
193+
telegram-stop: ## 🛑 Stop the Telegram bot (and its LiteLLM proxy)
194+
@screen -S telegram -X quit 2>/dev/null && echo "✅ Telegram bot stopped" || echo "⚠ Bot was not running"
195+
@screen -S telegram-debug -X quit 2>/dev/null && echo "✅ Telegram debug bot stopped" || true
196+
@screen -S telegram-proxy -X quit 2>/dev/null && echo "✅ LiteLLM proxy stopped" || echo "⚠ Proxy was not running"
167197

168198
telegram-attach: ## 📺 Connect to Telegram terminal (Ctrl+A D to detach)
169199
@screen -r telegram

config/litellm-telegram.yaml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# LiteLLM proxy — Anthropic /v1/messages → NVIDIA NIM (OpenAI-compatible)
2+
#
3+
# Why: the Telegram bot runs via `claude --channels` (real Claude binary), which
4+
# only speaks the Anthropic Messages API. NVIDIA NIM is OpenAI-compatible. This
5+
# proxy translates between them so the bot can run on an NVIDIA model instead of
6+
# Anthropic. Point the bot at it with ANTHROPIC_BASE_URL + ANTHROPIC_AUTH_TOKEN.
7+
#
8+
# Start: make telegram (launches this proxy + the channel)
9+
# Model: z-ai/glm-5.1 on NVIDIA NIM.
10+
# MiniMax M3 works for direct calls, but can leave Claude Code channels stuck
11+
# in "Clauding..." through the Anthropic translation path.
12+
13+
model_list:
14+
# Named entry used by ANTHROPIC_MODEL.
15+
# Use the native nvidia_nim provider (NOT openai/): the Anthropic /v1/messages
16+
# endpoint routes openai/ models through the OpenAI Responses API, which NVIDIA
17+
# NIM does not implement (→ 404). nvidia_nim/ forces /chat/completions.
18+
- model_name: telegram-nvidia
19+
litellm_params:
20+
model: nvidia_nim/z-ai/glm-5.1
21+
api_key: os.environ/NVIDIA_API_KEY
22+
23+
# Wildcard so Claude Code's background/small-model calls (title generation,
24+
# quota probes) also route to NVIDIA instead of failing with an unknown model.
25+
- model_name: "*"
26+
litellm_params:
27+
model: nvidia_nim/z-ai/glm-5.1
28+
api_key: os.environ/NVIDIA_API_KEY
29+
30+
litellm_settings:
31+
# NVIDIA NIM rejects some OpenAI params Claude Code sends — drop them instead
32+
# of returning 400 and breaking the conversation.
33+
drop_params: true
34+
35+
general_settings:
36+
master_key: sk-evonexus-telegram-local

config/providers.example.json

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"active_provider": "anthropic",
3+
"telegram_provider": "",
34
"providers": {
45
"anthropic": {
56
"name": "Anthropic (Claude nativo)",
@@ -35,7 +36,20 @@
3536
"OPENAI_MODEL": ""
3637
},
3738
"default_base_url": "https://integrate.api.nvidia.com/v1",
38-
"default_model": "deepseek-ai/deepseek-v3.1",
39+
"default_model": "z-ai/glm-5.1",
40+
"fallback_models": [
41+
"minimaxai/minimax-m3"
42+
],
43+
"model_tiers": {
44+
"opus": "z-ai/glm-5.1",
45+
"sonnet": "z-ai/glm-5.1",
46+
"haiku": "z-ai/glm-5.1"
47+
},
48+
"fallback_providers": [
49+
"openrouter",
50+
"codex_auth",
51+
"anthropic"
52+
],
3953
"requires_logout": true
4054
},
4155
"omnirouter": {

dashboard/backend/heartbeat_runner.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,11 @@ def step7_invoke_claude(
241241
prompt=prompt,
242242
max_turns=max_turns,
243243
timeout_seconds=timeout_seconds,
244-
agent=agent,
244+
# The heartbeat prompt already embeds the full agent identity
245+
# from .claude/agents/{agent}.md. Passing --agent here breaks
246+
# OpenClaude-backed providers, which can misparse the expanded
247+
# agent markdown as CLI options.
248+
agent="",
245249
)
246250
# Preserve the step7 contract; provider_fallback already returns
247251
# status/output/error/duration_ms/tokens_*/cost_usd and adds

dashboard/backend/provider_fallback.py

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -116,17 +116,14 @@ def _read_providers_config() -> dict:
116116
# ── Default fallback chains ───────────────────────────────────────────────────
117117

118118
# NVIDIA: models are independent — quota on one doesn't block others.
119-
# Order matches the validated fast, tool-calling-capable models (and mirrors
120-
# providers.json model_tiers / fallback_models). OpenRouter is intentionally
121-
# NOT in the chain — its stealth models 404 intermittently ("buga mto").
119+
# Primary is GLM 5.1 on NVIDIA; if quota/rate-limit hits, rotate inside
120+
# NVIDIA first, then leave the provider only after these core models fail.
122121
NVIDIA_MODEL_CHAIN = [
123-
"z-ai/glm-5.1", # Primary (sonnet tier)
124-
"moonshotai/kimi-k2.6", # 2nd (opus tier, deep reasoning)
125-
"qwen/qwen3.5-397b-a17b", # 3rd
126-
"stepfun-ai/step-3.7-flash", # 4th (haiku tier, fastest)
122+
"z-ai/glm-5.1", # Primary — Telegram + agents default
123+
"minimaxai/minimax-m3", # 2nd
127124
]
128125

129-
# Provider chain: NVIDIA → Codex (GPT-5.5 OAuth) → Claude nativo
126+
# Provider chain: NVIDIA → OpenRouter (owl-alpha) → Codex (GPT-5.x OAuth) → Claude nativo
130127
DEFAULT_PROVIDER_CHAIN = [
131128
{
132129
"provider_id": "nvidia",
@@ -138,6 +135,16 @@ def _read_providers_config() -> dict:
138135
},
139136
"model_chain": NVIDIA_MODEL_CHAIN,
140137
},
138+
{
139+
"provider_id": "openrouter",
140+
"cli_command": "openclaude",
141+
"base_url": "https://openrouter.ai/api/v1",
142+
"env_vars": {
143+
"CLAUDE_CODE_USE_OPENAI": "1",
144+
"OPENAI_BASE_URL": "https://openrouter.ai/api/v1",
145+
},
146+
"model_chain": ["openrouter/owl-alpha"],
147+
},
141148
{
142149
"provider_id": "codex_auth",
143150
"cli_command": "openclaude",
@@ -178,26 +185,42 @@ def _build_provider_entry(provider_id: str, providers: dict) -> dict:
178185
prov = providers.get(provider_id, {})
179186
env_vars = {k: v for k, v in prov.get("env_vars", {}).items()
180187
if v and k not in ("OPENAI_API_KEY", "OPENAI_MODEL")}
181-
model_chain = prov.get("fallback_models",
182-
[prov.get("default_model") or prov.get("env_vars", {}).get("OPENAI_MODEL")])
188+
189+
primary_model = prov.get("default_model") or prov.get("env_vars", {}).get("OPENAI_MODEL")
190+
model_chain = []
191+
if primary_model:
192+
model_chain.append(primary_model)
193+
for fallback_model in prov.get("fallback_models", []):
194+
if fallback_model and fallback_model not in model_chain:
195+
model_chain.append(fallback_model)
196+
197+
if not model_chain and prov.get("cli_command") == "claude":
198+
model_chain = [None]
183199

184200
return {
185201
"provider_id": provider_id,
186202
"cli_command": prov.get("cli_command", "openclaude"),
187203
"base_url": prov.get("default_base_url") or prov.get("env_vars", {}).get("OPENAI_BASE_URL"),
188204
"env_vars": env_vars,
189-
"model_chain": [m for m in model_chain if m],
205+
"model_chain": model_chain,
190206
}
191207

192208

193209
def _get_api_key(provider_id: str, config: dict) -> str:
210+
if provider_id in {"codex_auth", "anthropic"}:
211+
return ""
212+
194213
prov = config.get("providers", {}).get(provider_id, {})
195214
env_vars = prov.get("env_vars", {})
196215
for key_name in ("OPENAI_API_KEY", "NVIDIA_API_KEY", "GEMINI_API_KEY"):
197216
val = env_vars.get(key_name, "")
198217
if val and "****" not in val:
199218
return val
200-
return os.environ.get("OPENAI_API_KEY", "")
219+
220+
if provider_id in {"nvidia", "openrouter", "openai", "gemini"}:
221+
return os.environ.get("OPENAI_API_KEY", "")
222+
223+
return ""
201224

202225

203226
# ── Attempt record ─────────────────────────────────────────────────────────────
@@ -211,6 +234,7 @@ class FallbackAttempt:
211234
prompt: str
212235
max_turns: int
213236
timeout_seconds: int
237+
agent: str = ""
214238
env_overrides: dict = field(default_factory=dict)
215239
_result: dict | None = field(default=None, repr=False)
216240

@@ -220,6 +244,7 @@ def run(self) -> dict:
220244
prompt=self.prompt,
221245
max_turns=self.max_turns,
222246
timeout_seconds=self.timeout_seconds,
247+
agent=self.agent,
223248
env_overrides=self.env_overrides,
224249
)
225250
return self._result
@@ -254,6 +279,7 @@ def _invoke_cli(
254279
prompt: str,
255280
max_turns: int,
256281
timeout_seconds: int,
282+
agent: str = "",
257283
env_overrides: dict | None = None,
258284
) -> dict:
259285
cli_bin = shutil.which(cli_command)
@@ -267,7 +293,10 @@ def _invoke_cli(
267293
}
268294

269295
cmd = [cli_bin, "--print", "--max-turns", str(max_turns),
270-
"--dangerously-skip-permissions", "--output-format", "json", "--", prompt]
296+
"--dangerously-skip-permissions", "--output-format", "json"]
297+
if agent:
298+
cmd.extend(["--agent", agent])
299+
cmd.extend(["--", prompt])
271300

272301
run_env = dict(os.environ)
273302
if env_overrides:
@@ -404,6 +433,7 @@ def attempts(
404433
prompt=prompt,
405434
max_turns=max_turns,
406435
timeout_seconds=timeout_seconds,
436+
agent=agent,
407437
env_overrides=env_overrides,
408438
)
409439
self._attempts_log.append(attempt)

0 commit comments

Comments
 (0)