mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
a88045aacfa71ae78891e29061c5d66f53fa3e36
23
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a88045aacf |
feat(grok): convert interactive intake/secretary to the grok CLI; delete opencode
Move the last Grok runtime off opencode onto xAI's official `grok` CLI, for full parity with the Claude path. The intake/secretary chat now runs per-turn headless `grok -p` invocations that resume one session id (proven live: context carries across runs), with streaming-json deltas mapped to the existing panel StreamChunk kinds — the IntakeDriver loop, message source, relay, and idle reaper are reused unchanged; only the SessionFactory differs (GrokCliSession replaces the opencode-serve session). - GrokCliSession + a pure, unit-tested streaming-json -> StreamChunk assembler (thought coalesced to one block, text streamed live, end captures the session id for -r, fenced-draft fallback, clear errors incl. rate-limit). - intake propose_draft and secretary read_company_state/read_task/submit_directive are now FastMCP servers (roboco-intake / roboco-secretary) wired into ~/.grok/config.toml, launched via `uv run --directory /app` to resolve the installed package. The secretary tools reuse the shared backend helpers. - Orchestrator: interactive spawn mounts the subscription auth + per-agent usage dir (no metered xAI key, no permission env — grok flags carry per-role perms); usage/cost now read a captured usage.json (drop the opencode.db reader, the _opencode_db_path/_grok_usage_from_opencode methods, and the cost-cap's opencode read). hosts["opencode"] -> hosts["grok_usage"]; OPENCODE_DATA_DIR -> GROK_USAGE_DATA_DIR. - Fix one-shot usage capture: `-s` does not pin the session id (grok generates its own), so the entrypoint now reads the real id back from the JSON run log and the reader uses it; usage is captured per-turn on the interactive path. - Delete the opencode layer: opencode_config/opencode_usage/opencode_session, the docker/grok/*.js plugins, the old one-shot entrypoint, and their tests. - Compose (all three files), .env.example, and stale comments updated to the grok-CLI runtime; add the SuperGrok auth mount + grok-usage dir. Gate green: ruff, mypy (296 files), xenon, tests. NAS build/verify pending. |
||
|
|
499f6fc509 |
feat(grok-cli): read captured usage at finalize; keep interactive serve working
The provider mounts the per-agent data dir and points the entrypoint's usage file at it; the orchestrator's grok finalize reads that usage.json first (the grok-CLI total, priced at the output rate) and falls back to opencode.db for the still-opencode interactive intake/secretary path. Re-add _reasoning_effort_for to grok.py as a clearly-temporary shim for that interactive path (it needs opencode's "minimal" variant, distinct from the CLI's --effort) until it is converted too. |
||
|
|
c139e2d017 |
feat(grok-cli): wire usage capture into the run (session id + post-run extract)
The provider pins a fixed session id (ROBOCO_AGENT_SESSION_ID, reused from the agent session id as on the Claude path); the entrypoint passes it to 'grok -p -s <id>' so the run's session store is locatable, then runs the usage reader post-run (best-effort) to write the captured tokens + cost. The orchestrator-side finalize that reads that file follows. |
||
|
|
579dfb997b |
feat(grok-cli): capture per-session token usage + notional cost
Grok runs on the SuperGrok subscription, but — exactly like Claude on Max — we still record per-agent tokens and a notional cost for the dashboard. The grok CLI writes a cumulative totalTokens per turn into ~/.grok/sessions/<cwd>/<session-id>/updates.jsonl (the grok analogue of the Claude transcript / old opencode.db); the max is the session total. This reader locates that file (url-encoded cwd), extracts the total, and prices it at the output rate (no input/output split from the CLI; conservative + matches the reasoning-at-output convention). Validated against a real grok-build session (18253 tokens -> $0.0365). Entrypoint + finalize wiring follows. |
||
|
|
aa4dc08d58 |
feat(grok-cli): GrokCliProvider — subscription auth mount, mirrors ClaudeCodeProvider
Replace the opencode GrokProvider with GrokCliProvider: reuses the orchestrator's shared mount/auth/git assembly (gateway + identity) exactly like the Claude path, mounts the host ~/.grok/auth.json read-only (SuperGrok subscription) instead of injecting an xAI key, and sets the slim env the grok-cli entrypoint + renderer read (ROBOCO_AGENT_ID for per-role flags, model, mcp-config, prompt). Provider routing fields are blanked before the shared step so the grok endpoint is never mislabelled ANTHROPIC_*. Per-role permission logic now lives in grok_cli_config, so the provider is slim. Registry/orchestrator/exports updated; provider tests rewritten for the CLI behavior (no XAI key, auth mount present/absent). |
||
|
|
db135ccb40 |
feat(grok-cli): render config.toml + map per-role grok CLI flags
First piece of the Grok CLI provider that replaces the opencode runtime: a pure, unit-tested module the agent entrypoint runs to translate the mounted mcp-config.json into ~/.grok/config.toml ([mcp_servers]) and compute the per-role 'grok -p' flags — subagent/shell/edit tool removal, raw-git-mutation and rm-rf denies, reasoning effort — mirroring ClaudeCodeProvider's per-role permissions with native grok flags instead of an opencode permission block + JS guard plugins. Uses tomli_w. The rendered config + env injection are validated live against grok-build (the model called the server through it). |
||
|
|
e29168653a |
fix(grok): make the opencode runtime actually load — proven live on grok-build-0.1
Live verification (opencode 1.17.8 + grok-build-0.1, funded key) showed the Grok runtime was loading INERT, three ways: 1. The provider override `provider.xai.npm=@ai-sdk/openai` failed model resolution (ProviderModelNotFoundError) — opencode can't resolve that package from its module path. Worse, ANY custom `provider.xai` block (even just options) breaks plugin-tool registration. opencode's BUILT-IN xai provider drives grok-build-0.1 with working tool-calls, so emit NO provider block; the key + base reach it via XAI_API_KEY / XAI_BASE_URL env (provider.options.apiKey alone does NOT authenticate). 2. Plugins referenced by absolute path in the config `plugin:` array never registered their hooks/tools. opencode 1.17.8 only registers from the plugin AUTO-DISCOVERY dir (~/.config/opencode/plugin/). Bake all plugins there. 3. Plugins must use a NAMED export, not `export default`. Changes: - opencode_config: no `provider` block, no `plugin` array; drop the dead XaiTarget + timeout machinery; build_opencode_config now takes a model string. - GrokProvider / orchestrator interactive env: inject XAI_API_KEY + XAI_BASE_URL (drop the now-unused OPENAI_*). - secret-scrub / budget-feed / secretary-tools / intake-tools: named exports; baked into /home/agent/.config/opencode/plugin/ (drop the EXTRA_PLUGINS env). - agent-grok* Dockerfiles: plugin dir + agent ownership; drop the unneeded @ai-sdk/openai global install. Verified live end-to-end: grok-build-0.1 calls read_company_state AND submit_directive through secretary-tools.js and the backend receives both with the agent token; a tool.execute.before guard fires; built-in tool-calls work. Targeted gate green (ruff/mypy/xenon + opencode_config/providers/interactive tests; node --check the plugins). |
||
|
|
8e728bdf9d |
feat(grok): scope opencode edit/bash/external-dir permissions per role
Grok wrote ONE global permission block, so a Grok pr_reviewer (or qa / PM / auditor) ran with edit=allow + bash=allow on untrusted PR content. Now the permissions are derived per role, mirroring orchestrator._get_role_permissions on the Claude path: - edit — allow only roles that write code (role_config.allows_write: developer / documenter); everyone else edit=deny. - bash — allow only roles that legitimately run a shell (developer / documenter / cell_pm / main_pm); the read-only reviewers (qa / pr_reviewer / auditor) and the board get bash=deny. secret-scrub still guards the rest. - external_directory — only the pr_reviewer reads scratch outside its cwd (the /tmp diff); delivery roles get deny. One-shot roles resolve these in GrokProvider._append_grok_env; the interactive intake/secretary set edit=deny + bash=deny in the orchestrator (intake keeps external-dir reads for sibling product repos, the secretary does not). The Claude path is untouched — the permission env is a GROK-only contract. Targeted gate green (ruff/mypy/xenon + provider + interactive-spawn tests). |
||
|
|
82945ae023 |
feat(grok): start the in-container SDK server + budget feed (Claude parity)
The keystone of the Grok parity work (CEO's "take Claude as baseline, create what's missing" call): the one-shot Grok container now starts the same SDK server the Claude path runs, so the per-verb circuit breaker (the flow/do MCP servers already POST /verb/attempted to it), the per-session budget/loop counters, the terminal-verb tracking, and the SessionEnd post-mortem all work on Grok instead of being silently absent. - entrypoint: launch roboco.agent_sdk.server (bare venv python, not `uv run` which would re-sync the drifted clone lock and stall), wait for /health, reset counters; run opencode WITHOUT exec so the script regains control to run the post-mortem and the silent-exit substitute after the run returns. - budget-feed.js: opencode plugin that gates on /budget/status in tool.execute.before (halt/loop deny — the only place to stop a runaway one-shot run; opencode has no PostToolUse-deny) and records the executed tool + args-hash in tool.execute.after. Fail-open; bare-verb normalization for MCP-namespaced terminal verbs. - silent-exit substitute: on a graceful exit with no terminal verb the entrypoint posts /terminal/force_substitute so the task isn't left stuck claimed/in_progress (Stop-hook parity at the boundary). - opencode_config: wire budget-feed into the plugin array; add ROBOCO_OPENCODE_EXTRA_PLUGINS so per-image role tool plugins load scoped to one role; read the per-role ROBOCO_GROK_EDIT_PERMISSION. Targeted gate green (ruff/mypy/xenon + opencode_config tests; node --check on the plugins; bash -n on the entrypoint). |
||
|
|
9ed84f051c |
fix(grok): allow external-directory reads so the pr-reviewer can work
Live NAS run showed the Grok pr-reviewer claim the review and fetch the diff, then write it to /tmp and FAIL to read it back: opencode auto-denied "external_directory (/tmp/*)" — its file tools refuse paths outside the project cwd, and in headless serve/run mode an "ask" permission auto-rejects (no human). Add permission.external_directory (default "allow", env ROBOCO_GROK_EXTERNAL_DIR_PERMISSION) to the generated opencode.json. The container is the sandbox and secret-scrub still blocks credential-file reads, so allowing in-container external-dir reads is safe and unblocks legitimate scratch use (e.g. the pr-reviewer grepping a large diff in /tmp). Verified live against grok-build-0.1: with external_directory:"allow" the Read tool reads a file outside cwd and returns its contents (no auto-reject); the plain-string form is accepted by opencode 1.17.8. Needs a rebuild of roboco-agent-grok + a pr-reviewer re-run on the NAS to confirm. |
||
|
|
997a19e074 |
fix(grok): make the live interactive path work — store perms, error surfacing, variant
Found by actually running opencode serve locally (the path was doc-verified but never executed). Three fixes: 1. EACCES on the opencode store mount (the live intake crash): on Linux docker auto-creates a missing bind source as root:root, so the non-root agent user could not mkdir/write in /home/agent/.local/share/opencode and opencode died at boot. _ensure_opencode_data_dir pre-creates the per-agent dir 0777 before the mount (one-shot via the _GrokHost seam, interactive in both spawns). 2. Silent blank reply on a model error: opencode reports a turn failure in info.error with parts=[], NOT as a part — verified live (a bad xAI key returns info.error APIError). send() / normalize_opencode_message now surface it as an "error" StreamChunk so a failed turn is never blank (the original intake bug class). Confirmed live: the error now renders. 3. Reasoning variant on the serve path: the live OpenAPI shows the message body accepts a "variant" field (it is NOT CLI-only, as the docs implied), so the pin is unblocked. send() passes ROBOCO_GROK_VARIANT as the per-turn variant; the orchestrator sets it per-role (_reasoning_effort_for) for interactive Grok, the same lever as the one-shot --variant. opencode serve startup, POST /session, session-id extraction, the part-type mapping (text/reasoning/tool), and the error path are all validated against a live opencode 1.17.8. A real successful grok reply still needs a funded key. |
||
|
|
0d76ebf586 |
feat(grok): route interactive intake/secretary to opencode-serve images
Wire the GROK interactive path the in-place way (matching how the interactive roles already choose ANTHROPIC_* per route), so a GROK route launches the Grok-native opencode-serve image instead of the Claude SDK-driver image: - _spawn_intake_container / _spawn_secretary_container pick the grok-prompter / grok-secretary image (ensuring the base→grok→interactive build chain) when the route is GROK, and stamp provider_type on the spec + AgentConfig so finalize routes usage to the opencode store. - _build_intake_run_cmd / _build_secretary_run_cmd inject OPENAI_* + the opencode store mount + system-prompt env for GROK via a shared _append_interactive_provider_env, keeping ANTHROPIC_* for every other provider. The intake's minimal mounts (no gateway MCP) are preserved, so Grok intake matches the Claude intake's tool surface (the spec). - Add a per-agent opencode store mount to the interactive host paths so interactive Grok usage/cost is captured like the one-shot path. Removes the interim Phase-0 routing guard (the real path supersedes it) and retires the unused AgentProvider.spawn_interactive/InteractiveSpawnSpec seam — the interactive roles have a bespoke assembly that the one-shot provider surface doesn't fit, so the fork lives in their own builders. UNVERIFIED-LIVE: end-to-end intake/secretary chat on Grok needs the stack up + opencode serve confirmed against grok-build-0.1. |
||
|
|
6f60d180e9 |
feat(grok): make interactive spawns first-class on AgentProvider (additive)
The AgentProvider ABC modelled only the one-shot lifecycle (spawn/stop/ health_check/remove), so the interactive intake/secretary roles could never route through a provider. Add an opt-in interactive surface: - supports_interactive class flag (default False). - InteractiveSpawnSpec: the resolved AgentConfig + session id + role-specific image + optional HMAC token — everything a provider needs without importing orchestrator internals. - spawn_interactive(spec): a non-abstract default that declines via ProviderError, so every existing one-shot provider is unchanged. Pure scaffolding — no provider opts in yet (GrokProvider flips the flag when its interactive driver lands). Zero behavioural change. |
||
|
|
e36549f01f |
feat(grok): capture one-shot Grok usage/cost from the opencode store
A GROK agent runs opencode, not Claude Code: it has no SDK /usage/status server and writes no Claude transcript, so _resolve_final_token_usage found nothing and every Grok agent finalized at 0 tokens / $0 — the opencode_usage reader existed but had no caller. - Mount a per-agent opencode data dir ($DATA/opencode/<agent_id> → /home/agent/.local/share/opencode) so opencode.db is captured, and mount the same host dir into the orchestrator (/data/opencode) in all three compose files so the finalizer can read it back — the opencode analogue of the mounted Claude transcript. - _resolve_final_token_usage branches on provider_type: GROK reads opencode.db via opencode_usage (reasoning folded into output, billed at the output rate) and skips the SDK/transcript path. A 0-token read logs a WARNING so a silent mount failure isn't mistaken for a real zero-cost run. - ROBOCO_OPENCODE_DATA_DIR overrides the in-orchestrator path for local runs. |
||
|
|
ff8867dd1f |
fix(grok): stop opencode subagent-stream hang at the config layer
The Grok pr_reviewer wedged in_progress forever: opencode's default agent ran with the subagent `task` tool enabled, spawned an Explore subagent on grok-build-0.1 whose model call opened an SSE stream that went idle, and the run hung with no timeout. - Hard-disable opencode's subagent `task` tool in the generated opencode.json. No RoboCo role uses opencode-internal subagents — work flows through the gateway verbs — so removing the tool kills the hang trigger outright. - Set provider.xai.options.timeout + chunkTimeout (operator-tunable via ROBOCO_GROK_REQUEST_TIMEOUT_MS / ROBOCO_GROK_CHUNK_TIMEOUT_MS) as the defence-in-depth backstop; chunkTimeout aborts an idle stream. - Bundle the permission + timeout + subagent knobs into an OpencodeGuards dataclass (keeps the builder under the arg-count gate). - Drop the dead ROBOCO_AGENT_TOOLS spawn env (it had no consumer); opencode tool restriction lives in the rendered config now. |
||
|
|
5725ec998b |
feat(grok): reasoning-effort by role (cut grok-build cost on cheap roles)
grok-build-0.1 reasons heavily by default and reasoning bills at the output rate (a live "say ok" call emitted ~300 reasoning tokens, ~85% of its cost). Confirmed live that opencode's `--variant minimal` cuts reasoning ~54% (298 -> 136 tokens, same prompt). GrokProvider now picks reasoning effort by role: code-quality roles (developer, qa, pr_reviewer) keep full reasoning; coordination / docs / board roles (cell_pm, main_pm, documenter, product_owner, head_marketing, auditor, prompter, secretary) run "minimal". It's passed to opencode via the entrypoint's `--variant`. Operators can force one effort for ALL grok agents with the ROBOCO_GROK_REASONING_EFFORT env (minimal | high | max, or default/full). Tests cover the role map, the env override, and the spawn env wiring. |
||
|
|
b0857915a1 |
fix(grok): correct opencode provider (Responses API), stdin, reasoning cost
A live opencode run against api.x.ai/v1 surfaced three real bugs: 1. Provider package — grok-build-0.1 is driven via the OpenAI Responses API (opencode calls model.responses()). @ai-sdk/openai-compatible is chat/completions only and errors "responses is not a function". Switch the generated opencode.json provider + the grok image to @ai-sdk/openai. 2. Headless hang — `opencode run` blocks after init without a TTY; close stdin (`< /dev/null`) in the entrypoint so it proceeds to the model call. 3. Reasoning-token cost — grok-build-0.1 is a reasoning model; reasoning tokens bill as output but opencode stores them in a separate column. cost_for_session folds tokens_reasoning into output (else ~22x undercount). Verified end-to-end against a real session row (input=6120, output=1, reasoning=226, cache_read=1856): our pricing reproduces opencode's stored USD cost ($0.0069452) exactly. Tests anchored to that real row. |
||
|
|
af6cad98d8 |
feat(grok): read opencode session usage for cost capture
Confirmed by inspecting a local opencode run: opencode persists per-session usage in SQLite at ~/.local/share/opencode/opencode.db — the `session` table carries cost + tokens_input/output/reasoning/cache_read/cache_write. xAI's response usage object (prompt_tokens, completion_tokens, prompt_tokens_details.cached_tokens, completion_tokens_details.reasoning_tokens) maps directly onto those columns. Add opencode_usage.read_session_usage / cost_for_session: read the opencode DB and price the tokens via roboco.billing.pricing (our cost stays authoritative; opencode's own `cost` column is kept for reference). Tested against a fixture DB mirroring the real schema (single session, summed sessions, missing/empty DB). Remaining wiring (for the live spawn): mount the opencode data dir on grok spawn + call cost_for_session at reap to record the usage rollup. |
||
|
|
07f55117de |
feat(grok): price grok-build-0.1 + secret-scrub opencode plugin
- pricing.py: add grok-build-0.1 rates ($1/1M input, $0.20 cached, $2/1M output), verified against xAI's published pricing. Grok is a priced non-Anthropic model, so cost computes the moment usage is captured. - secret-scrub.js: an opencode tool.execute.before plugin porting the security-critical bash-guard deny rules (git network ops, credential-file reads, /proc env, internal-host HTTP, roboco.* imports, ROBOCO_AGENT_ID forgery, env dumps, destructive rm) to the opencode runtime — restoring the guard the Claude Code hook can't provide there. Throwing denies the call (confirmed by opencode's env-protection example). Wired into the generated opencode.json plugin array + baked into the grok image. Deny logic verified via node (9 deny + 5 allow cases). UNVALIDATED against a live opencode runtime: confirm it fires in the live E2E spawn before a Grok dev-agent touches a real repo; the bash permission is operator-tunable as a second gate. Cost CAPTURE (distinct from pricing) is intentionally NOT built yet: opencode's plugin hooks expose model info but no token/usage object, so the capture path is unconfirmed and needs the live spawn to settle. |
||
|
|
085414dcf5 |
feat(providers): native Grok runtime — opencode image, config gen, panel key
Complete the native Grok (xAI) path so grok-build-0.1 runs as a real RoboCo agent, not just the provider seam. - roboco-agent-grok image (docker/agent-grok.Dockerfile): FROM agent-base + opencode (the OpenAI-protocol runtime). One image serves every role; role behaviour comes from the mounted manifest / mcp-config / system prompt, exactly as on the Claude path. - Entrypoint renders opencode.json at spawn from the GrokProvider env contract + the mounted Claude Code mcp-config.json (roboco.llm.providers.opencode_config): translates RoboCo's gateway servers (roboco-flow / roboco-do / ...) into opencode's mcp block, declares the xAI OpenAI-compatible provider + model, and wires permissions + instructions. Pure, unit-tested translation. - Orchestrator registers GrokProvider with the registry-qualified image (_qualify_agent_image) so it resolves in local and registry deploys. - Compose (both files + the registry compose) gain an agent-grok-image builder service. - Panel: a Grok (xAI) API key card on the AI Providers page, plus the grok ModelProvider value. KNOWN PARITY GAP (opencode runtime): the bash-guard PAT-scrub and the transcript-based usage/cost capture are Claude Code hooks and do not transfer to opencode. bash permission is operator-tunable (ROBOCO_GROK_BASH_PERMISSION) so a deployment can fail closed until a security/usage-parity opencode plugin lands. That plugin and live E2E validation are the remaining work to finalize with xAI. |
||
|
|
a956083f9f |
feat(providers): pluggable agent providers + Grok (xAI) backend
Add a roboco/llm/providers/ seam — an AgentProvider lifecycle ABC and a ProviderRegistry keyed by ModelProvider — so the orchestrator can drive agent backends other than Claude Code. The first non-Claude backend is GrokProvider for xAI's grok-build-0.1. xAI is OpenAI-compatible only (no Anthropic-Messages endpoint), so a Grok agent runs an OpenAI-protocol runtime pointed at https://api.x.ai/v1 rather than the ANTHROPIC_BASE_URL injection the other providers use. It reuses the orchestrator's existing mount/auth assembly, so it inherits the same MCP gateway + tool-manifest wiring as every other agent by construction, and passes its prompt via env (never an argv positional). The change is purely additive: only GROK routes through the registry; Anthropic / Ollama Cloud / self-hosted spawns run the existing _spawn_container path unchanged. Includes: - ModelProvider.GROK (migration 038) + a seeded Grok provider row (migration 039) + a grok-build-0.1 catalog entry - GET/PUT /api/providers/grok-key to store the xAI key (Fernet-encrypted, reusing the existing provider-key machinery) - ClaudeCodeProvider reference adapter over the current spawn - unit tests for the registry, GrokProvider (gateway wiring, no ANTHROPIC_* leak, prompt-injection safety, failure paths) and routing The dedicated roboco-agent-grok image and the exact OpenAI-protocol CLI invocation are the remaining piece to finalise with xAI. |
||
|
|
73b7c16211 |
[3cc1729c] Add self-hosted LLM provider with dynamic model discovery (#128)
* [684dace4] Self-hosted LLM provider: API layer, hooks, UI section, routing mode button, and Mix mode grouping (#124) (#126) * [684dace4] feat(providers): add self-hosted LLM API types, endpoints, and React Query hooks - Add ModelProvider.SELF_HOSTED enum value to types/index.ts - Extend RoutingMode to include 'self_hosted' in lib/api/providers.ts - Add SelfHostedConfig, SelfHostedTestResult, SelfHostedModel interfaces - Add SelfHostedConfigPayload for PUT requests - Add 5 providersApi methods: getSelfHostedConfig, saveSelfHostedConfig, testSelfHosted, getSelfHostedModels, refreshSelfHostedModels - Add 5 React Query hooks: useSelfHostedConfig, useSetSelfHostedConfig, useTestSelfHosted, useSelfHostedModels, useRefreshSelfHostedModels - Cache keys follow existing providerKeys pattern with proper invalidation * [684dace4] feat(settings): create SelfHostedSection component with full self-hosted LLM UI - Base URL text input with placeholder showing saved URL when set - Optional auth token field (type='password') with Eye/EyeOff toggle button - Save button that calls useSetSelfHostedConfig mutation - Test Connection button disabled until a URL is saved; shows inline green 'Connected — N models' badge on success or red error badge on fail - Three empty states: no URL configured (CTA), error state (last-checked + Retry), connected with 0 models (pull-guidance) - Model list with auto-discovered chip, Refresh Models button, and Last refreshed relative timestamp when test_status === 'connected' - Token field shows masked placeholder when has_auth_token is true (consistent with Ollama Cloud key field pattern) * [684dace4] feat(settings): add Self-Hosted mode button, model picker, and Mix mode provider grouping - Wire SelfHostedSection into AIRoutingCard with testResult state tracking - Expand routing mode grid from 3 to 4 buttons (2×2 on mobile, 4-col on md+) - 4th 'Self-Hosted' mode button disabled until test_status === 'connected' - Self-hosted model picker appears below mode grid when mode === 'self_hosted' - flipToSelfHosted handler sends mode='self_hosted' with optional default_model - Mix mode per-agent dropdown now groups entries under SelectGroup/SelectLabel headings: Anthropic, Ollama Cloud, Self-Hosted with colored ProviderBadge pill - saveMix validates self-hosted model selection requires a successful test - ProviderBadge helper renders blue/violet/purple pills for each provider type - pnpm typecheck and pnpm lint pass with zero errors --------- Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev> * [2897ce90] Implement self-hosted LLM provider API, routing, and discovery (#125) (#127) * [2897ce90] feat(provider): add self-hosted LLM provider API, routing, and discovery - Add migration 027 to seed Self-Hosted (Ollama) LOCAL provider row - Add probe_ollama_tags() helper for Ollama /api/tags connectivity checks - Extend ModelRoutingService: derive_mode returns 'self_hosted' for LOCAL GLOBAL assignments; apply_mode handles 'self_hosted' mode; upsert_assignment routes non-catalog model names to LOCAL provider; resolve_for_agent falls back to Anthropic when self-hosted server is unreachable - Add PUT /api/providers/self-hosted, POST /api/providers/self-hosted/test, GET /api/providers/self-hosted/models endpoints - Extend ApplyModeRequest and ModeResponse literals with 'self_hosted' - Add SelfHostedConfigRequest, SelfHostedConfigResponse, SelfHostedTestResponse schemas * [2897ce90] test(provider): add integration tests for self-hosted routing and route endpoints - Add llm_setup_with_local fixture that seeds LOCAL provider row - Test derive_mode returns 'self_hosted' for single GLOBAL LOCAL assignment - Test apply_mode('self_hosted') clears prior assignments, enables LOCAL, inserts GLOBAL - Test apply_mode('self_hosted') requires default_model argument - Test upsert_assignment routes non-catalog model names to LOCAL provider - Test mix mode accepts self-hosted model names without ValueError - Test resolve_for_agent returns base_url when LOCAL server is reachable - Test resolve_for_agent falls back to Anthropic when LOCAL server is unreachable - Test upsert_assignment raises ValueError when model unknown and no LOCAL provider - Add app_client_with_local fixture for route tests - Test PUT /self-hosted saves base_url and enables provider - Test PUT /self-hosted stores encrypted token when auth_token provided - Test PUT /self-hosted returns 404 when LOCAL provider not seeded - Test POST /self-hosted/test returns {ok:true,model_count:N} when reachable - Test POST /self-hosted/test returns {ok:false,error} (never 500) when unreachable - Test GET /self-hosted/models returns model name list - Test GET /self-hosted/models returns 404 when not configured - Test GET /self-hosted/models returns 503 when server unreachable - Rename migration from 027 to 028 to rebase on 027_system_settings * [2897ce90] chore(migration): remove superseded 027 migration, fix formatter changes to provider schemas --------- Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev> * [042462df] feat(providers): align self-hosted types, hooks, and UI to backend contract (#129) (#131) - SelfHostedConfig now has {base_url: string, has_token: boolean, enabled: boolean} - SelfHostedTestResult now has {ok: boolean, model_count: number | null, error: string | null} - Remove SelfHostedTestStatus type and refreshSelfHostedModels POST API function - Remove SELF_HOSTED from ModelProvider enum (LOCAL covers self-hosted semantics) - useRefreshSelfHostedModels now invalidates GET cache instead of calling POST - isSelfHostedConnected derived from testResult?.ok === true - Self-hosted model picker uses value='__clear__' sentinel (no empty-string SelectItem) - self-hosted-section.tsx reads result.ok/result.error and config?.has_token - pnpm typecheck passes with zero errors Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev> * [f66d6d4d] Fix self-hosted API S1-S4/L1-L5: routes, schemas, services, migration 028, and tests (#130) (#132) * [f66d6d4d] fix(provider): self-hosted API S1-S4/L1-L5 - routes, schemas, services, migration 028, and tests AC1: Add GET /providers/self-hosted returning {base_url, has_token, enabled} AC2: GET /self-hosted/models now returns list[SelfHostedModelEntry] with model_name and display_name AC3: probe_ollama_tags generic except logs exception server-side and returns hardcoded generic string AC4: upsert_assignment calls ProviderService.update_provider(enabled=True) when routing to LOCAL AC5: derive_mode return annotation is Literal[...] — type:ignore comments removed AC6: All migration refs in routes/services say 028 (not 027) AC7: Migration 028 downgrade() deletes model_assignments before provider_configs AC8: PUT /self-hosted only passes enabled=True when data.base_url is non-empty AC9: ModelProvider.LOCAL docstring updated to describe self-hosted Ollama provider AC10: Direct unit tests for probe_ollama_tags (5 cases) in tests/unit/llm/ AC11: Contract tests added/updated for GET /providers/self-hosted, models, and test endpoints AC12: test_migration_028_seed_self_hosted.py with upgrade and FK-safe downgrade tests AC13: test_apply_mode_ollama_without_provider_returns_404 asserts exactly HTTPStatus.NOT_FOUND AC14: ruff and mypy pass with zero errors * [f66d6d4d] fix(tests): add AC4 test proving LOCAL.enabled transitions False->True in upsert_assignment The existing tests (test_upsert_assignment_routes_unknown_model_to_local and test_mix_mode_with_self_hosted_models) both use llm_setup_with_local which seeds LOCAL with enabled=True, making the AC4 assertion vacuous. New test test_upsert_assignment_enables_local_when_disabled: - Creates LOCAL ProviderConfigTable row with enabled=False - Asserts pre-condition: local.enabled is False - Calls upsert_assignment with a non-catalog model name ('non-catalog-model:7b') - Refreshes LOCAL row via db_session.refresh(local) - Asserts row.provider.type == ModelProvider.LOCAL and local.enabled is True This proves the state transition from False->True, not merely that the already-enabled state is preserved. ruff and mypy still pass with zero errors. --------- Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev> * [7cd6ae6e] fix(providers): type SelfHostedConfig.base_url as string | null to match backend contract (#133) (#136) Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev> * [46ee9104] test(migration_028): replace upgrade test with self-seeding contract test (#134) (#135) Remove test_migration_028_upgrade_local_row_inserted which relied on alembic upgrade head having run (and thus the Self-Hosted Ollama row being present). Replace it with test_migration_028_upgrade_insert_contract that: - Executes the exact INSERT SQL from migration 028 upgrade() directly - Asserts name='Self-Hosted (Ollama)', type='local', enabled=False - Runs the INSERT a second time and asserts exactly one row (ON CONFLICT DO NOTHING idempotency) The downgrade test is left byte-for-byte unchanged. Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev> * [f0d19f30] test(provider): add DELETE-before-seed isolation and app_client_with_ollama fixture (#137) (#138) - Add ModelAssignmentTable import to test_provider_routes.py - Fix app_client_with_local: execute DELETE on ModelAssignmentTable then DELETE on ProviderConfigTable (FK-safe order) and flush before seeding - Add new app_client_with_ollama fixture with same isolation pattern, seeding only ANTHROPIC + OLLAMA_CLOUD rows - Update 7 tests to use app_client_with_ollama instead of app_client: test_get_catalog, test_get_ollama_key_status, test_set_ollama_key, test_get_current_mode, test_apply_mode_anthropic_clears_assignments, test_apply_mode_unknown_returns_4xx, test_apply_mode_mix_without_per_agent_returns_400 Fixes order-dependent failures in test_get_self_hosted_models_not_configured_returns_404: routes call db.commit() which persists rows across test sessions; without DELETE-before-seed, stale LOCAL provider rows with base_url set from prior runs cause the test to see 503 instead of 404. Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev> * refactor(llm): split resolve_for_agent and apply_mode to clear xenon rank C resolve_for_agent and apply_mode were cyclomatic rank C, failing the xenon gate (--max-absolute B). Extract behavior-preserving helpers: - resolve_for_agent -> _resolve_assignment (precedence ladder), _route_from_resolved / _local_route_or_none / _decrypt_route_or_none (None signals fall-through to legacy), _legacy_route. - apply_mode -> _apply_anthropic / _apply_ollama / _apply_self_hosted / _apply_mix dispatched from a thin if/elif. No behavior change. Also correct the stale 'default: Kimi K2.6' docstring (OLLAMA_DEFAULT_MODEL is minimax-m3:cloud). --------- Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev> Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev> Co-authored-by: Renn F <rennf93@users.noreply.github.com> |
||
|
|
9aa30fb945 | 100% Coverage |