Skip to content

feat(providers): openclaude provider routing + resilience (resume, fallback, reconnect)#108

Open
sistemabritto wants to merge 13 commits into
evolution-foundation:mainfrom
sistemabritto:feat/provider-routing-resilience
Open

feat(providers): openclaude provider routing + resilience (resume, fallback, reconnect)#108
sistemabritto wants to merge 13 commits into
evolution-foundation:mainfrom
sistemabritto:feat/provider-routing-resilience

Conversation

@sistemabritto

Copy link
Copy Markdown

Summary

Hardens the OpenClaude/OpenAI-compatible provider routing and fixes several reliability issues surfaced while running EvoNexus against non-Anthropic providers (NVIDIA NIM, OpenRouter, Codex OAuth).

Supersedes #106 — same provider-routing foundation, but rebased clean on main and scoped to broadly-useful changes only (no workspace-specific skills).

What's included

Provider routing & resilience

  • feat(providers): chat tool-calling via openclaude + explicit provider mode
  • fix(providers): derive NVIDIA_API_KEY for NVIDIA NIM base URLs
  • feat(providers): NVIDIA NIM image generation + hardening
  • feat(terminal): conversation resume (--session-id/--resume pinned to the terminal-server session UUID) so a provider crash or server restart no longer loses the conversation; per-tier agent models (model_tiers maps agent opus|sonnet|haiku frontmatter to provider models); and --fallback-model from a configurable fallback_models chain
  • feat(chat): rate-limited fetch with retry/backoff (429/503, honors Retry-After) for OpenAI-compatible providers

Reliability fixes

  • fix(dashboard): auto-reconnect agent terminal/chat WebSocket with capped backoff + reconnect-on-visibility (a dropped socket left the terminal dead until remount)
  • fix(dashboard): /costs page crash on image cost entries missing token_usage.total_tokens

Backend foundation

  • feat(heartbeats): in-process Python handlers (handler field, max_turns=0) as an alternative to spawning a Claude agent + provider_fallback.py engine (429/quota detection, model/provider rotation with cooldown)

Other

  • feat(skills): OpenAI Images API provider (gpt-image-2) in the shared ai-image-creator skill, with split-token cost estimation
  • chore(deps): sync uv.lock; docs: codexplan → gpt-5.5 mapping note

Testing

  • terminal-server: provider-config tests pass (incl. new tier/fallback cases)
  • frontend: tsc -b + vite build green; no new lint errors
  • backend: tests/heartbeats 25/26 pass (the 1 failure is a data-driven assertion against a gitignored local heartbeats.yaml, not a code regression)
  • provider_fallback.py imports clean; the engine is foundation and not yet wired into the runner

🤖 Generated with Claude Code

sistemabritto and others added 12 commits June 13, 2026 20:57
… mode

Non-Anthropic providers in the agent chat previously bypassed openclaude
entirely — a raw /chat/completions fetch with no tool calling, no session
history, and a hard API-key requirement that broke Codex OAuth. The
terminal was separately blocked by the isCodeModel() name heuristic for
agentic models like openrouter/owl-alpha.

- chat-bridge: external providers in code mode now go through the Agent
  SDK with pathToClaudeCodeExecutable pointing at the openclaude binary
  and a clean whitelisted env (mirrors ClaudeBridge). Restores tool
  calling, structured streaming, UI tool approval, and session resume.
  Chat-only models keep the REST path; missing binary falls back with a
  clear log.
- chat-bridge: external providers get full system-prompt replacement for
  agent personas (append is too weak for GPT models), matching the
  terminal behavior.
- provider-config: new per-provider `mode: code|chat` field overrides the
  model-name heuristic; the terminal no longer refuses agentic models
  whose names don't match the code regex.
- providers.example.json: mode field on openrouter/omnirouter + new
  NVIDIA NIM provider entry (OpenAI-compatible endpoint).
- tests: provider-config mode override coverage (node --test).

Verified end-to-end against OpenRouter: text streaming and Read tool
call complete with all UI events (tool_use_start, tool_input_delta,
block_stop, result).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
OpenClaude >=0.18 detects integrate.api.nvidia.com and demands the key
in NVIDIA_API_KEY, exiting 1 when only OPENAI_API_KEY is set ("Claude
Code process exited with code 1" in the chat). Derive NVIDIA_API_KEY
from OPENAI_API_KEY in loadProviderConfig so the UI keeps a single key
field, and allowlist the var in both config layers.

Verified: chat session via Agent SDK + openclaude against NVIDIA NIM
(stepfun-ai/step-3.7-flash) completes with result: success.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extends the NVIDIA provider beyond chat/code into image generation, and
fixes three bugs found while dogfooding the multi-provider flow:

- ai-image-creator: NVIDIA NIM provider with FLUX models — flux.2-klein-4b
  (default, best composition, ~2.5s), flux.1-dev (30 steps), flux.1-schnell.
  Aspect-ratio mapping constrained to the dimension set and 1.06MP pixel
  budget the API validates; per-model cfg_scale/steps defaults (schnell
  requires cfg=0, klein requires cfg>=1); JPEG output auto-converted to
  PNG via ImageMagick with ffmpeg fallback (temp files beside the output —
  snap-confined ffmpeg cannot read /tmp). Uses NVIDIA_API_KEY from .env.

- backend/providers: saving provider config with a blank key field no
  longer wipes the stored secret — the UI submits empty password inputs
  when editing other fields (e.g. model), silently erasing the API key
  and breaking every session afterwards with auth errors.

- claude-bridge: terminal agent personas now resolve from WORKSPACE_ROOT
  with cwd fallback, matching chat-bridge (cab8966) — sessions started
  outside the workspace root were silently falling back to a generic
  "You are the X agent" persona.

- Makefile: `make telegram` fails loudly when screen/bun are missing
  instead of printing a false success message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…le providers

Enforce a minimum interval between chat completion requests and retry
429/503 responses with exponential backoff, honoring Retry-After.
Configurable via CHAT_MIN_INTERVAL_MS, CHAT_MAX_RETRIES, CHAT_BASE_DELAY_MS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
onclose only cleared the keepalive and never reconnected — a dropped
socket left the terminal/chat dead until the component remounted
(switching tabs). Now both components reconnect with capped exponential
backoff and rejoin immediately on visibilitychange, replaying the
session buffer on a cleared terminal to avoid duplicate output. If the
process died while disconnected, surface it instead of silently
restarting the agent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r fallback

- Pin the CLI conversation to the terminal-server session UUID:
  first start uses --session-id, restarts use --resume when a persisted
  conversation file exists — provider crashes and server restarts no
  longer lose the conversation.
- Per-agent model tiers: agents declare model: opus|sonnet|haiku in
  frontmatter; providers.json maps each tier to a provider model via
  the new model_tiers field.
- Pass --fallback-model from the new fallback_models chain (first entry
  distinct from the primary).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… totals

Older skill versions logged image cost entries without
token_usage.total_tokens — the unguarded access in the Image Generation
table threw a TypeError and unmounted the whole page. Normalize the
image-costs payload defensively, same pattern as normalizeCostData.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eator

New 'openai' provider hitting api.openai.com/v1/images/generations
directly, with the 'image2' model keyword (auto-detects provider).
Requires a platform API key (AI_IMG_CREATOR_OPENAI_KEY or
OPENAI_API_KEY) — ChatGPT Plus/Codex OAuth tokens lack the
api.model.images.request scope (verified: 401), so they cannot be used
for image generation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ude 0.18

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e cost estimate

Images API bills text input and image output at different per-token
rates — use the prompt/completion split when available instead of
total_tokens at the input rate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dation

- Heartbeats can now run an in-process Python handler instead of
  spawning a Claude agent: new 'handler' field (max_turns=0), DB
  migration, schema validation, dispatcher sync, runner execution path.
- provider_fallback.py: 429/quota detection + model/provider rotation
  with cooldown tracking (foundation; not yet wired into the runner).

Tests: tests/heartbeats 25/26 pass (the 1 failure is a data-driven seed
assertion against the gitignored local heartbeats.yaml, not a regression).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @sistemabritto, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

step7 now routes through provider_fallback.invoke_with_fallback so a
429/quota error rotates model→provider (NVIDIA chain → Codex → native
claude) instead of failing the run. Disable with
HEARTBEAT_PROVIDER_FALLBACK=0; unavailable engine falls back to the
native claude path (preserved as _step7_invoke_claude_native).

Also in provider_fallback.py:
- fix NameError on 429 (DEFAULT_COOLDOWN → DEFAULT_COOLDOWN_SECONDS)
- align default NVIDIA model chain with validated models; drop OpenRouter
  (stealth models 404 intermittently)
- derive NVIDIA_API_KEY for NVIDIA base URLs (openclaude ≥0.18 needs it)
- parse token usage + cost from the CLI JSON envelope so heartbeat runs
  land real numbers on /costs instead of nulls

Tested: invoke_with_fallback + step7_invoke_claude run end-to-end via
NVIDIA glm-5.1; tests/heartbeats 25/26 (the 1 failure is the pre-existing
data-driven seed assertion on the gitignored local heartbeats.yaml).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New security issues found

cmd = [magick_cmd, str(jpg_tmp), str(png_tmp)]
else:
cmd = ["ffmpeg", "-y", "-i", str(jpg_tmp), str(png_tmp)]
conv = subprocess.run(cmd, capture_output=True, text=True, timeout=60)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

Source: opengrep

Comment on lines +285 to +288
proc = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, cwd=str(WORKSPACE), start_new_session=True, env=run_env,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'Popen' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

Source: opengrep

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant