Skip to content

feat(llm): dynamic model discovery with per-provider API listing and 1-day cache (#991)#1000

Merged
bug-ops merged 7 commits intomainfrom
worktree-991
Feb 26, 2026
Merged

feat(llm): dynamic model discovery with per-provider API listing and 1-day cache (#991)#1000
bug-ops merged 7 commits intomainfrom
worktree-991

Conversation

@bug-ops
Copy link
Owner

@bug-ops bug-ops commented Feb 26, 2026

Parent Epic

#991

Summary

Implements full dynamic model discovery for all providers with disk-backed 24h cache.

Closes #992, #993, #994, #995, #996, #997.

Changes

Infrastructure (#992)

  • RemoteModelInfo struct: id, display_name, context_window, created_at
  • ModelCache in zeph-llm/src/model_cache.rs: per-provider JSON cache at ~/.cache/zeph/models/{slug}.json, 24h TTL, atomic writes via .tmp + rename
  • LlmProvider::list_models_remote() async trait method with default fallback to list_models()

Provider Implementations

Dispatch Layer (#996)

  • AnyProvider::list_models_remote() — dispatches to active variant
  • RouterProvider::list_models_remote() — aggregates from all fallback providers, deduplicates by id
  • ModelOrchestrator::list_models_remote() — aggregates across all sub-providers

User-Facing (#997)

  • Agent::set_model(model_id) — validates input, hot-swaps provider model
  • /model — lists discovered models with display names and cache age
  • /model <id> — switches active model
  • /model refresh — clears all ~/.cache/zeph/models/*.json and re-fetches
  • ACP AvailableCommandsUpdate populated with model list on session start

Security

  • cache_slug() enforces [a-zA-Z0-9_] allowlist (path traversal prevention)
  • API error bodies logged at debug only; status code exposed to user
  • set_model() validates: non-empty, max 256 chars, ASCII printable
  • UTF-8 safe title truncation in ACP via chars().take()

Tests

  • 20 new unit tests covering: cache stale/fresh logic, JSON round-trip, slug derivation, response deserialization, pagination logic, /model command handlers, set_model validation
  • 2942 tests pass total

Each Status loopback event now emits a leading newline chunk before the
status text so that consecutive calls (matching skills / building context /
thinking) accumulate as separate lines rather than a single concatenated
string in the ACP thought bubble.

Closes #988
ToolCall.title was always set to the tool name ("bash"), making all
shell invocations indistinguishable in the Zed ACP chat UI.

Changes:
- Add `params: Option<serde_json::Value>` to `LoopbackEvent::ToolStart`
  so the raw input is forwarded to the ACP layer without extra queries.
- Pass `tc.input` as params in `send_tool_start` at the call site.
- In `loopback_event_to_updates`: derive title from `command`, `path`,
  or `url` key in params (truncated to 120 chars); set `raw_input` on
  the ToolCall so the IDE can display it in "View Raw Input".
- Fix `permission_denied_returns_blocked_error` test to use an isolated
  temp permission file, preventing pollution from a pre-existing
  `bash = "allow"` entry in the default config.
Implements issue #992 and #993 from epic #991.

- Add zeph-llm/src/model_cache.rs: disk-backed cache for remote model
  listings with 24-hour TTL, atomic write via tmp-rename, invalidate().
- Add OllamaProvider::list_models_remote via ollama_rs list_local_models.
- Add CompatibleProvider::list_models_remote delegating to inner OpenAi.
- Add AnyProvider::list_models_remote dispatch; Router and Orchestrator
  fall back to list_models() until their own implementations land.
- Add orchestrator SubProvider::list_models_remote for Claude/Ollama/OpenAI.
- Add Agent::set_model stub and handle_model_command with cache-first
  listing, /model refresh to invalidate caches, and /model <id> switch.
- Fix post-merge clippy: redundant closures, let-else, doc sections,
  map_unwrap_or, duplicate set_status_tx in CompatibleProvider.
@github-actions github-actions bot added enhancement New feature or request llm LLM provider related rust core dependencies size/XL and removed enhancement New feature or request labels Feb 26, 2026
@github-actions github-actions bot added the enhancement New feature or request label Feb 26, 2026
@bug-ops bug-ops enabled auto-merge (squash) February 26, 2026 21:57
…e paths

Use an atomic counter to generate unique temp directories for each
model_cache test, eliminating race conditions from shared path under
parallel test execution.

Also apply nightly fmt to affected files (any.rs, claude.rs, openai.rs,
agent/mod.rs).
- cache_slug: enforce [a-zA-Z0-9_] allowlist, fallback to 'unknown'
- claude/openai: log API error body at debug level, expose status code only
- agent set_model: validate non-empty, max 256 chars, ASCII printable
- any: add debug log for Router/Orchestrator list_models() fallback
- agent /model refresh: clear all ~/.cache/zeph/models/*.json files
- acp: fix UTF-8 panic in title truncation via chars().take()
- model_cache for_slug: sanitize input slug as defense-in-depth
- claude list_models_remote: add # Panics doc for static URL expect
- CHANGELOG: document all epic #991 additions
@github-actions github-actions bot added the documentation Improvements or additions to documentation label Feb 26, 2026
@bug-ops bug-ops changed the title feat(llm): model_cache infrastructure and Ollama/Claude/OpenAI remote model listing feat(llm): dynamic model discovery with per-provider API listing and 1-day cache (#991) Feb 26, 2026
@bug-ops bug-ops merged commit efc9da1 into main Feb 26, 2026
28 checks passed
@bug-ops bug-ops deleted the worktree-991 branch February 26, 2026 22:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment