mirror of
https://github.com/whit3rabbit/anyllm-proxy.git
synced 2026-09-21 16:00:49 +00:00
feat: admin UI fixes, shared modal/clipboard utils, status counts managed backends
Admin UI: shared centered provider modal, dropdown backend selectors for Add Model/Backend, inline add-failure errors, copy/dismiss on created keys, metric tooltips, empty states. status route now counts managed backends. Add InfoTip and clipboard helpers. Ignore .eatahorse*/ scratch boards and drop the tracked guardrails board. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
4e55f8ce5a
commit
8bcee60e2b
@@ -1,57 +0,0 @@
|
||||
{"action": "init", "at": "2026-07-03T23:40:10Z", "detail": {"goal": "Integrate forge-guardrails (opt-in tool-call guardrails for local LLMs) into anyllm-proxy. Backend port is DONE already (uncommitted): crates/proxy/src/tools/guardrails.rs (lsp_first/quiet_command/write_payload_cap nudges, fingerprint dedup), wired into both tool loops (tools/execution/mod.rs, server/chat_completions/stream/generic/tool_loop.rs), config via SimpleConfig.build_tool_guardrail_config (config/simple/types.rs, tool_builder.rs), env fallback FORGE_TOOL_CALL_POLICY in main_helpers/async_main/tools.rs, ToolEngineState.guardrails wired through state.rs/routes.rs, docs updated (CONFIG.md, ENV.md, configuration-and-modes.md), tests updated. Remaining verified gaps to plan/execute: (1) main_helpers/async_main/tools.rs init_tool_engine early-returns None when tool_config.has_any() is false, so FORGE_TOOL_CALL_POLICY alone (no YAML tool_execution block) never takes effect -- the target local-LLM-env-var-only use case is currently broken; fix has_any()/init gating and add a test for env-var-only init with no YAML. (2) LiteLLM YAML config format has no tool_config path at all (config/multi/loader.rs) -- document this limitation explicitly, do not build support for it now. (3) No admin UI surface exists for tool_execution config at all (admin-ui/src has zero references); optionally add a RuntimeConfig field + Settings.tsx toggle following the repo's documented 6-site RuntimeConfig-field checklist (struct + RuntimeConfigDefaults + 3 constructors + SQLite override-apply match in main.rs + delete_config_override reset in admin/routes/config.rs) -- treat as optional/lower priority than (1)/(2). (4) No CLI flag needed -- env var is this repo's existing convention for backend toggles, do not add a second mechanism. Produce a Kanban board of small, independently-completable tasks for items (1), (2), and optionally (3), plus a changelog bullet task per repo release process (CHANGELOG.md [Unreleased]).", "project": "forge-guardrails integration finish"}}
|
||||
{"action": "add", "at": "2026-07-03T23:40:25Z", "detail": {"id": "EH-0001", "title": "Make FORGE_TOOL_CALL_POLICY alone initialize the guardrail engine (no YAML)"}}
|
||||
{"action": "add", "at": "2026-07-03T23:40:34Z", "detail": {"id": "EH-0002", "title": "Document that LiteLLM YAML format cannot carry tool_execution/guardrails config"}}
|
||||
{"action": "add", "at": "2026-07-03T23:40:45Z", "detail": {"id": "EH-0003", "title": "Add CHANGELOG [Unreleased] bullet for opt-in tool-call guardrails"}}
|
||||
{"action": "add", "at": "2026-07-03T23:40:55Z", "detail": {"id": "EH-0004", "title": "(Optional) Admin UI + RuntimeConfig toggle for guardrail mode"}}
|
||||
{"action": "sync", "at": "2026-07-03T23:41:00Z", "detail": {"board": "/Users/whit3rabbit/Documents/GitHub/anyllm/.eatahorse-integrate-forge-guardrails-opt-in-tool-c", "completed": 0, "dropped_missing": 0, "imported": 0, "moved": 0, "tasks": 4, "updated": 4}}
|
||||
{"action": "drop", "at": "2026-07-03T23:47:21Z", "detail": {"id": "EH-0004"}}
|
||||
{"action": "add", "at": "2026-07-03T23:47:21Z", "detail": {"id": "EH-0005", "title": "Backend: add tool_guardrail_mode RuntimeConfig field (6-site checklist)"}}
|
||||
{"action": "add", "at": "2026-07-03T23:47:21Z", "detail": {"id": "EH-0006", "title": "Admin UI: guardrail mode toggle in Settings.tsx"}}
|
||||
{"action": "sync", "at": "2026-07-03T23:47:29Z", "detail": {"board": "/Users/whit3rabbit/Documents/GitHub/anyllm/.eatahorse-integrate-forge-guardrails-opt-in-tool-c", "completed": 0, "dropped_missing": 0, "imported": 0, "moved": 0, "tasks": 6, "updated": 6}}
|
||||
{"action": "move", "at": "2026-07-04T15:50:50Z", "detail": {"id": "EH-0001", "status": "doing"}}
|
||||
{"action": "check", "at": "2026-07-04T15:52:33Z", "detail": {"group": "bites", "id": "EH-0001", "index": 1}}
|
||||
{"action": "sync", "at": "2026-07-04T15:52:43Z", "detail": {"board": "/Users/whit3rabbit/Documents/GitHub/anyllm/.eatahorse-integrate-forge-guardrails-opt-in-tool-c", "completed": 0, "dropped_missing": 0, "imported": 0, "moved": 0, "tasks": 6, "updated": 6}}
|
||||
{"action": "check", "at": "2026-07-04T15:54:03Z", "detail": {"group": "bites", "id": "EH-0001", "index": 2}}
|
||||
{"action": "sync", "at": "2026-07-04T15:54:11Z", "detail": {"board": "/Users/whit3rabbit/Documents/GitHub/anyllm/.eatahorse-integrate-forge-guardrails-opt-in-tool-c", "completed": 0, "dropped_missing": 0, "imported": 0, "moved": 0, "tasks": 6, "updated": 6}}
|
||||
{"action": "check", "at": "2026-07-04T15:59:34Z", "detail": {"group": "bites", "id": "EH-0001", "index": 3}}
|
||||
{"action": "check", "at": "2026-07-04T15:59:34Z", "detail": {"group": "bites", "id": "EH-0001", "index": 4}}
|
||||
{"action": "check", "at": "2026-07-04T15:59:34Z", "detail": {"group": "bites", "id": "EH-0001", "index": 5}}
|
||||
{"action": "check", "at": "2026-07-04T15:59:55Z", "detail": {"group": "acceptance", "id": "EH-0001", "index": 1}}
|
||||
{"action": "check", "at": "2026-07-04T15:59:55Z", "detail": {"group": "acceptance", "id": "EH-0001", "index": 2}}
|
||||
{"action": "check", "at": "2026-07-04T15:59:55Z", "detail": {"group": "acceptance", "id": "EH-0001", "index": 3}}
|
||||
{"action": "sync", "at": "2026-07-04T16:00:03Z", "detail": {"board": "/Users/whit3rabbit/Documents/GitHub/anyllm/.eatahorse-integrate-forge-guardrails-opt-in-tool-c", "completed": 0, "dropped_missing": 0, "imported": 0, "moved": 0, "tasks": 6, "updated": 6}}
|
||||
{"action": "move", "at": "2026-07-04T16:02:05Z", "detail": {"id": "EH-0002", "status": "doing"}}
|
||||
{"action": "check", "at": "2026-07-04T16:02:45Z", "detail": {"group": "bites", "id": "EH-0002", "index": 1}}
|
||||
{"action": "check", "at": "2026-07-04T16:02:54Z", "detail": {"group": "bites", "id": "EH-0002", "index": 2}}
|
||||
{"action": "sync", "at": "2026-07-04T16:03:14Z", "detail": {"board": "/Users/whit3rabbit/Documents/GitHub/anyllm/.eatahorse-integrate-forge-guardrails-opt-in-tool-c", "completed": 0, "dropped_missing": 0, "imported": 0, "moved": 0, "tasks": 6, "updated": 6}}
|
||||
{"action": "check", "at": "2026-07-04T16:04:46Z", "detail": {"group": "bites", "id": "EH-0002", "index": 3}}
|
||||
{"action": "check", "at": "2026-07-04T16:04:46Z", "detail": {"group": "acceptance", "id": "EH-0002", "index": 1}}
|
||||
{"action": "check", "at": "2026-07-04T16:04:46Z", "detail": {"group": "acceptance", "id": "EH-0002", "index": 2}}
|
||||
{"action": "check", "at": "2026-07-04T16:04:46Z", "detail": {"group": "acceptance", "id": "EH-0002", "index": 3}}
|
||||
{"action": "sync", "at": "2026-07-04T16:04:54Z", "detail": {"board": "/Users/whit3rabbit/Documents/GitHub/anyllm/.eatahorse-integrate-forge-guardrails-opt-in-tool-c", "completed": 0, "dropped_missing": 0, "imported": 0, "moved": 0, "tasks": 6, "updated": 6}}
|
||||
{"action": "move", "at": "2026-07-04T16:06:49Z", "detail": {"id": "EH-0003", "status": "doing"}}
|
||||
{"action": "check", "at": "2026-07-04T16:07:46Z", "detail": {"group": "bites", "id": "EH-0003", "index": 1}}
|
||||
{"action": "check", "at": "2026-07-04T16:07:46Z", "detail": {"group": "acceptance", "id": "EH-0003", "index": 1}}
|
||||
{"action": "check", "at": "2026-07-04T16:07:46Z", "detail": {"group": "acceptance", "id": "EH-0003", "index": 2}}
|
||||
{"action": "sync", "at": "2026-07-04T16:07:46Z", "detail": {"board": "/Users/whit3rabbit/Documents/GitHub/anyllm/.eatahorse-integrate-forge-guardrails-opt-in-tool-c", "completed": 0, "dropped_missing": 0, "imported": 0, "moved": 0, "tasks": 6, "updated": 5}}
|
||||
{"action": "move", "at": "2026-07-04T16:09:10Z", "detail": {"id": "EH-0005", "status": "doing"}}
|
||||
{"action": "check", "at": "2026-07-04T16:10:35Z", "detail": {"group": "bites", "id": "EH-0005", "index": 1}}
|
||||
{"action": "sync", "at": "2026-07-04T16:10:41Z", "detail": {"board": "/Users/whit3rabbit/Documents/GitHub/anyllm/.eatahorse-integrate-forge-guardrails-opt-in-tool-c", "completed": 0, "dropped_missing": 0, "imported": 0, "moved": 0, "tasks": 6, "updated": 6}}
|
||||
{"action": "check", "at": "2026-07-04T16:13:44Z", "detail": {"group": "bites", "id": "EH-0005", "index": 2}}
|
||||
{"action": "sync", "at": "2026-07-04T16:13:51Z", "detail": {"board": "/Users/whit3rabbit/Documents/GitHub/anyllm/.eatahorse-integrate-forge-guardrails-opt-in-tool-c", "completed": 0, "dropped_missing": 0, "imported": 0, "moved": 0, "tasks": 6, "updated": 6}}
|
||||
{"action": "check", "at": "2026-07-04T16:27:09Z", "detail": {"group": "bites", "id": "EH-0005", "index": 3}}
|
||||
{"action": "check", "at": "2026-07-04T16:27:52Z", "detail": {"group": "acceptance", "id": "EH-0005", "index": 1}}
|
||||
{"action": "check", "at": "2026-07-04T16:27:53Z", "detail": {"group": "acceptance", "id": "EH-0005", "index": 3}}
|
||||
{"action": "sync", "at": "2026-07-04T16:28:21Z", "detail": {"board": "/Users/whit3rabbit/Documents/GitHub/anyllm/.eatahorse-integrate-forge-guardrails-opt-in-tool-c", "completed": 0, "dropped_missing": 0, "imported": 0, "moved": 0, "tasks": 6, "updated": 6}}
|
||||
{"action": "check", "at": "2026-07-04T16:30:43Z", "detail": {"group": "acceptance", "id": "EH-0005", "index": 2}}
|
||||
{"action": "sync", "at": "2026-07-04T16:30:57Z", "detail": {"board": "/Users/whit3rabbit/Documents/GitHub/anyllm/.eatahorse-integrate-forge-guardrails-opt-in-tool-c", "completed": 0, "dropped_missing": 0, "imported": 0, "moved": 0, "tasks": 6, "updated": 6}}
|
||||
{"action": "sync", "at": "2026-07-04T16:31:50Z", "detail": {"board": "/Users/whit3rabbit/Documents/GitHub/anyllm/.eatahorse-integrate-forge-guardrails-opt-in-tool-c", "completed": 0, "dropped_missing": 0, "imported": 0, "moved": 0, "tasks": 6, "updated": 6}}
|
||||
{"action": "sync", "at": "2026-07-04T16:32:38Z", "detail": {"board": "/Users/whit3rabbit/Documents/GitHub/anyllm/.eatahorse-integrate-forge-guardrails-opt-in-tool-c", "completed": 0, "dropped_missing": 0, "imported": 0, "moved": 0, "tasks": 6, "updated": 6}}
|
||||
{"action": "sync", "at": "2026-07-04T16:33:57Z", "detail": {"board": "/Users/whit3rabbit/Documents/GitHub/anyllm/.eatahorse-integrate-forge-guardrails-opt-in-tool-c", "completed": 0, "dropped_missing": 0, "imported": 0, "moved": 0, "tasks": 6, "updated": 6}}
|
||||
{"action": "move", "at": "2026-07-04T16:34:22Z", "detail": {"id": "EH-0006", "status": "doing"}}
|
||||
{"action": "check", "at": "2026-07-04T16:35:49Z", "detail": {"group": "bites", "id": "EH-0006", "index": 1}}
|
||||
{"action": "check", "at": "2026-07-04T16:37:17Z", "detail": {"group": "acceptance", "id": "EH-0006", "index": 1}}
|
||||
{"action": "check", "at": "2026-07-04T16:37:17Z", "detail": {"group": "acceptance", "id": "EH-0006", "index": 2}}
|
||||
{"action": "check", "at": "2026-07-04T16:37:17Z", "detail": {"group": "acceptance", "id": "EH-0006", "index": 3}}
|
||||
{"action": "sync", "at": "2026-07-04T16:37:42Z", "detail": {"board": "/Users/whit3rabbit/Documents/GitHub/anyllm/.eatahorse-integrate-forge-guardrails-opt-in-tool-c", "completed": 0, "dropped_missing": 0, "imported": 0, "moved": 0, "tasks": 6, "updated": 6}}
|
||||
{"action": "sync", "at": "2026-07-04T16:39:24Z", "detail": {"board": "/Users/whit3rabbit/Documents/GitHub/anyllm/.eatahorse-integrate-forge-guardrails-opt-in-tool-c", "completed": 0, "dropped_missing": 0, "imported": 0, "moved": 0, "tasks": 6, "updated": 6}}
|
||||
@@ -1,54 +0,0 @@
|
||||
# Eatahorse Board: forge-guardrails integration finish
|
||||
|
||||
Goal: Integrate forge-guardrails (opt-in tool-call guardrails for local LLMs) into anyllm-proxy. Backend port is DONE already (uncommitted): crates/proxy/src/tools/guardrails.rs (lsp_first/quiet_command/write_payload_cap nudges, fingerprint dedup), wired into both tool loops (tools/execution/mod.rs, server/chat_completions/stream/generic/tool_loop.rs), config via SimpleConfig.build_tool_guardrail_config (config/simple/types.rs, tool_builder.rs), env fallback FORGE_TOOL_CALL_POLICY in main_helpers/async_main/tools.rs, ToolEngineState.guardrails wired through state.rs/routes.rs, docs updated (CONFIG.md, ENV.md, configuration-and-modes.md), tests updated. Remaining verified gaps to plan/execute: (1) main_helpers/async_main/tools.rs init_tool_engine early-returns None when tool_config.has_any() is false, so FORGE_TOOL_CALL_POLICY alone (no YAML tool_execution block) never takes effect -- the target local-LLM-env-var-only use case is currently broken; fix has_any()/init gating and add a test for env-var-only init with no YAML. (2) LiteLLM YAML config format has no tool_config path at all (config/multi/loader.rs) -- document this limitation explicitly, do not build support for it now. (3) No admin UI surface exists for tool_execution config at all (admin-ui/src has zero references); optionally add a RuntimeConfig field + Settings.tsx toggle following the repo's documented 6-site RuntimeConfig-field checklist (struct + RuntimeConfigDefaults + 3 constructors + SQLite override-apply match in main.rs + delete_config_override reset in admin/routes/config.rs) -- treat as optional/lower priority than (1)/(2). (4) No CLI flag needed -- env var is this repo's existing convention for backend toggles, do not add a second mechanism. Produce a Kanban board of small, independently-completable tasks for items (1), (2), and optionally (3), plus a changelog bullet task per repo release process (CHANGELOG.md [Unreleased]).
|
||||
|
||||
Updated: `2026-07-04T16:39:24Z`
|
||||
|
||||
## Counts
|
||||
|
||||
| Status | Count |
|
||||
|---|---:|
|
||||
| backlog | 0 |
|
||||
| ready | 0 |
|
||||
| doing | 0 |
|
||||
| blocked | 0 |
|
||||
| done | 5 |
|
||||
| dropped | 1 |
|
||||
|
||||
## backlog
|
||||
|
||||
_No tasks._
|
||||
|
||||
## ready
|
||||
|
||||
_No tasks._
|
||||
|
||||
## doing
|
||||
|
||||
_No tasks._
|
||||
|
||||
## blocked
|
||||
|
||||
_No tasks._
|
||||
|
||||
## done
|
||||
|
||||
| ID | Task | Bites | Acceptance | Priority | Owner |
|
||||
|---|---|---:|---:|---|---|
|
||||
| [EH-0001](tasks/done/EH-0001-make-forge-tool-call-policy-alone-initialize-the-guardrail-e.md) | Make FORGE_TOOL_CALL_POLICY alone initialize the guardrail engine (no YAML) | 5/5 | 3/3 | P0 | |
|
||||
| [EH-0002](tasks/done/EH-0002-document-that-litellm-yaml-format-cannot-carry-tool-executio.md) | Document that LiteLLM YAML format cannot carry tool_execution/guardrails config | 3/3 | 3/3 | P1 | |
|
||||
| [EH-0003](tasks/done/EH-0003-add-changelog-unreleased-bullet-for-opt-in-tool-call-guardra.md) | Add CHANGELOG [Unreleased] bullet for opt-in tool-call guardrails | 1/1 | 2/2 | P2 | |
|
||||
| [EH-0005](tasks/done/EH-0005-backend-add-tool-guardrail-mode-runtimeconfig-field-6-site-c.md) | Backend: add tool_guardrail_mode RuntimeConfig field (6-site checklist) | 3/3 | 3/3 | P3 | |
|
||||
| [EH-0006](tasks/done/EH-0006-admin-ui-guardrail-mode-toggle-in-settings-tsx.md) | Admin UI: guardrail mode toggle in Settings.tsx | 1/1 | 3/3 | P3 | |
|
||||
|
||||
## dropped
|
||||
|
||||
| ID | Task | Bites | Acceptance | Priority | Owner |
|
||||
|---|---|---:|---:|---|---|
|
||||
| [EH-0004](tasks/dropped/EH-0004-optional-admin-ui-runtimeconfig-toggle-for-guardrail-mode.md) | (Optional) Admin UI + RuntimeConfig toggle for guardrail mode | 0/2 | 0/3 | P3 | |
|
||||
|
||||
## Workflow
|
||||
|
||||
Move one task into `doing`, finish one bite, sync, then take the next bite.
|
||||
|
||||
Generated by Eatahorse. Edit task markdown files or use `scripts/eatahorse.py`.
|
||||
@@ -1,619 +0,0 @@
|
||||
{
|
||||
"created_at": "2026-07-03T23:40:10Z",
|
||||
"goal": "Integrate forge-guardrails (opt-in tool-call guardrails for local LLMs) into anyllm-proxy. Backend port is DONE already (uncommitted): crates/proxy/src/tools/guardrails.rs (lsp_first/quiet_command/write_payload_cap nudges, fingerprint dedup), wired into both tool loops (tools/execution/mod.rs, server/chat_completions/stream/generic/tool_loop.rs), config via SimpleConfig.build_tool_guardrail_config (config/simple/types.rs, tool_builder.rs), env fallback FORGE_TOOL_CALL_POLICY in main_helpers/async_main/tools.rs, ToolEngineState.guardrails wired through state.rs/routes.rs, docs updated (CONFIG.md, ENV.md, configuration-and-modes.md), tests updated. Remaining verified gaps to plan/execute: (1) main_helpers/async_main/tools.rs init_tool_engine early-returns None when tool_config.has_any() is false, so FORGE_TOOL_CALL_POLICY alone (no YAML tool_execution block) never takes effect -- the target local-LLM-env-var-only use case is currently broken; fix has_any()/init gating and add a test for env-var-only init with no YAML. (2) LiteLLM YAML config format has no tool_config path at all (config/multi/loader.rs) -- document this limitation explicitly, do not build support for it now. (3) No admin UI surface exists for tool_execution config at all (admin-ui/src has zero references); optionally add a RuntimeConfig field + Settings.tsx toggle following the repo's documented 6-site RuntimeConfig-field checklist (struct + RuntimeConfigDefaults + 3 constructors + SQLite override-apply match in main.rs + delete_config_override reset in admin/routes/config.rs) -- treat as optional/lower priority than (1)/(2). (4) No CLI flag needed -- env var is this repo's existing convention for backend toggles, do not add a second mechanism. Produce a Kanban board of small, independently-completable tasks for items (1), (2), and optionally (3), plus a changelog bullet task per repo release process (CHANGELOG.md [Unreleased]).",
|
||||
"history": [
|
||||
{
|
||||
"action": "add",
|
||||
"at": "2026-07-03T23:40:25Z",
|
||||
"detail": {
|
||||
"id": "EH-0001",
|
||||
"title": "Make FORGE_TOOL_CALL_POLICY alone initialize the guardrail engine (no YAML)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "add",
|
||||
"at": "2026-07-03T23:40:34Z",
|
||||
"detail": {
|
||||
"id": "EH-0002",
|
||||
"title": "Document that LiteLLM YAML format cannot carry tool_execution/guardrails config"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "add",
|
||||
"at": "2026-07-03T23:40:45Z",
|
||||
"detail": {
|
||||
"id": "EH-0003",
|
||||
"title": "Add CHANGELOG [Unreleased] bullet for opt-in tool-call guardrails"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "add",
|
||||
"at": "2026-07-03T23:40:55Z",
|
||||
"detail": {
|
||||
"id": "EH-0004",
|
||||
"title": "(Optional) Admin UI + RuntimeConfig toggle for guardrail mode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "drop",
|
||||
"at": "2026-07-03T23:47:21Z",
|
||||
"detail": {
|
||||
"id": "EH-0004"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "add",
|
||||
"at": "2026-07-03T23:47:21Z",
|
||||
"detail": {
|
||||
"id": "EH-0005",
|
||||
"title": "Backend: add tool_guardrail_mode RuntimeConfig field (6-site checklist)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "add",
|
||||
"at": "2026-07-03T23:47:21Z",
|
||||
"detail": {
|
||||
"id": "EH-0006",
|
||||
"title": "Admin UI: guardrail mode toggle in Settings.tsx"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "move",
|
||||
"at": "2026-07-04T15:50:50Z",
|
||||
"detail": {
|
||||
"id": "EH-0001",
|
||||
"status": "doing"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "check",
|
||||
"at": "2026-07-04T15:52:33Z",
|
||||
"detail": {
|
||||
"group": "bites",
|
||||
"id": "EH-0001",
|
||||
"index": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "check",
|
||||
"at": "2026-07-04T15:54:03Z",
|
||||
"detail": {
|
||||
"group": "bites",
|
||||
"id": "EH-0001",
|
||||
"index": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "check",
|
||||
"at": "2026-07-04T15:59:34Z",
|
||||
"detail": {
|
||||
"group": "bites",
|
||||
"id": "EH-0001",
|
||||
"index": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "check",
|
||||
"at": "2026-07-04T15:59:34Z",
|
||||
"detail": {
|
||||
"group": "bites",
|
||||
"id": "EH-0001",
|
||||
"index": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "check",
|
||||
"at": "2026-07-04T15:59:34Z",
|
||||
"detail": {
|
||||
"group": "bites",
|
||||
"id": "EH-0001",
|
||||
"index": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "check",
|
||||
"at": "2026-07-04T15:59:55Z",
|
||||
"detail": {
|
||||
"group": "acceptance",
|
||||
"id": "EH-0001",
|
||||
"index": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "check",
|
||||
"at": "2026-07-04T15:59:55Z",
|
||||
"detail": {
|
||||
"group": "acceptance",
|
||||
"id": "EH-0001",
|
||||
"index": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "check",
|
||||
"at": "2026-07-04T15:59:55Z",
|
||||
"detail": {
|
||||
"group": "acceptance",
|
||||
"id": "EH-0001",
|
||||
"index": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "move",
|
||||
"at": "2026-07-04T16:02:05Z",
|
||||
"detail": {
|
||||
"id": "EH-0002",
|
||||
"status": "doing"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "check",
|
||||
"at": "2026-07-04T16:02:45Z",
|
||||
"detail": {
|
||||
"group": "bites",
|
||||
"id": "EH-0002",
|
||||
"index": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "check",
|
||||
"at": "2026-07-04T16:02:54Z",
|
||||
"detail": {
|
||||
"group": "bites",
|
||||
"id": "EH-0002",
|
||||
"index": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "check",
|
||||
"at": "2026-07-04T16:04:46Z",
|
||||
"detail": {
|
||||
"group": "bites",
|
||||
"id": "EH-0002",
|
||||
"index": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "check",
|
||||
"at": "2026-07-04T16:04:46Z",
|
||||
"detail": {
|
||||
"group": "acceptance",
|
||||
"id": "EH-0002",
|
||||
"index": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "check",
|
||||
"at": "2026-07-04T16:04:46Z",
|
||||
"detail": {
|
||||
"group": "acceptance",
|
||||
"id": "EH-0002",
|
||||
"index": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "check",
|
||||
"at": "2026-07-04T16:04:46Z",
|
||||
"detail": {
|
||||
"group": "acceptance",
|
||||
"id": "EH-0002",
|
||||
"index": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "move",
|
||||
"at": "2026-07-04T16:06:49Z",
|
||||
"detail": {
|
||||
"id": "EH-0003",
|
||||
"status": "doing"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "check",
|
||||
"at": "2026-07-04T16:07:46Z",
|
||||
"detail": {
|
||||
"group": "bites",
|
||||
"id": "EH-0003",
|
||||
"index": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "check",
|
||||
"at": "2026-07-04T16:07:46Z",
|
||||
"detail": {
|
||||
"group": "acceptance",
|
||||
"id": "EH-0003",
|
||||
"index": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "check",
|
||||
"at": "2026-07-04T16:07:46Z",
|
||||
"detail": {
|
||||
"group": "acceptance",
|
||||
"id": "EH-0003",
|
||||
"index": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "move",
|
||||
"at": "2026-07-04T16:09:10Z",
|
||||
"detail": {
|
||||
"id": "EH-0005",
|
||||
"status": "doing"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "check",
|
||||
"at": "2026-07-04T16:10:35Z",
|
||||
"detail": {
|
||||
"group": "bites",
|
||||
"id": "EH-0005",
|
||||
"index": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "check",
|
||||
"at": "2026-07-04T16:13:44Z",
|
||||
"detail": {
|
||||
"group": "bites",
|
||||
"id": "EH-0005",
|
||||
"index": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "check",
|
||||
"at": "2026-07-04T16:27:09Z",
|
||||
"detail": {
|
||||
"group": "bites",
|
||||
"id": "EH-0005",
|
||||
"index": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "check",
|
||||
"at": "2026-07-04T16:27:52Z",
|
||||
"detail": {
|
||||
"group": "acceptance",
|
||||
"id": "EH-0005",
|
||||
"index": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "check",
|
||||
"at": "2026-07-04T16:27:53Z",
|
||||
"detail": {
|
||||
"group": "acceptance",
|
||||
"id": "EH-0005",
|
||||
"index": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "check",
|
||||
"at": "2026-07-04T16:30:43Z",
|
||||
"detail": {
|
||||
"group": "acceptance",
|
||||
"id": "EH-0005",
|
||||
"index": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "move",
|
||||
"at": "2026-07-04T16:34:22Z",
|
||||
"detail": {
|
||||
"id": "EH-0006",
|
||||
"status": "doing"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "check",
|
||||
"at": "2026-07-04T16:35:49Z",
|
||||
"detail": {
|
||||
"group": "bites",
|
||||
"id": "EH-0006",
|
||||
"index": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "check",
|
||||
"at": "2026-07-04T16:37:17Z",
|
||||
"detail": {
|
||||
"group": "acceptance",
|
||||
"id": "EH-0006",
|
||||
"index": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "check",
|
||||
"at": "2026-07-04T16:37:17Z",
|
||||
"detail": {
|
||||
"group": "acceptance",
|
||||
"id": "EH-0006",
|
||||
"index": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "check",
|
||||
"at": "2026-07-04T16:37:17Z",
|
||||
"detail": {
|
||||
"group": "acceptance",
|
||||
"id": "EH-0006",
|
||||
"index": 3
|
||||
}
|
||||
}
|
||||
],
|
||||
"next_id": 7,
|
||||
"project": "forge-guardrails integration finish",
|
||||
"schema": "eatahorse.state.v1",
|
||||
"statuses": [
|
||||
"backlog",
|
||||
"ready",
|
||||
"doing",
|
||||
"blocked",
|
||||
"done",
|
||||
"dropped"
|
||||
],
|
||||
"tasks": {
|
||||
"EH-0001": {
|
||||
"acceptance": [
|
||||
{
|
||||
"done": true,
|
||||
"text": "New test passes: `cargo test -p anyllm_proxy forge_tool_call_policy_env_only` (or the chosen test name) exits 0."
|
||||
},
|
||||
{
|
||||
"done": true,
|
||||
"text": "Observable: with PROXY_CONFIG unset and FORGE_TOOL_CALL_POLICY=standard, init_tool_engine returns Some and the returned ToolEngineState.guardrails.mode is not ToolGuardrailMode::Disabled (asserted directly in the test)."
|
||||
},
|
||||
{
|
||||
"done": true,
|
||||
"text": "`cargo clippy -p anyllm_proxy -- -D warnings` reports no warnings for the changed file."
|
||||
}
|
||||
],
|
||||
"bites": [
|
||||
{
|
||||
"done": true,
|
||||
"text": "Refactor init_tool_engine so the FORGE_TOOL_CALL_POLICY resolution runs BEFORE the has_any() early return: when tool_config is None or has_any() is false, resolve the env var and, if it parses to a non-Disabled ToolGuardrailMode, proceed to build the engine (empty registry, default policy/loop_config, no MCP)"
|
||||
},
|
||||
{
|
||||
"done": true,
|
||||
"text": "otherwise return None as today. Keep the existing YAML-precedence rule (guardrails_set_in_yaml wins over env)."
|
||||
},
|
||||
{
|
||||
"done": true,
|
||||
"text": "Add a #[cfg(test)] module (or extend the nearest tools test) covering: (a) tool_config None + FORGE_TOOL_CALL_POLICY=standard => Some engine with guardrails.mode != Disabled"
|
||||
},
|
||||
{
|
||||
"done": true,
|
||||
"text": "(b) env unset + no tool sections => None"
|
||||
},
|
||||
{
|
||||
"done": true,
|
||||
"text": "(c) invalid env value => None (or Disabled) and does not panic. Acquire crate::config::ENV_TEST_LOCK before mutating the env var and restore it after."
|
||||
}
|
||||
],
|
||||
"completed_at": "2026-07-04T15:59:55Z",
|
||||
"created_at": "2026-07-03T23:40:25Z",
|
||||
"description": "The env-var-only local-LLM path is currently dead. init_tool_engine (crates/proxy/src/main_helpers/async_main/tools.rs:9) does `let tc = tool_config.filter(|tc| tc.has_any())?`; and returns None whenever there is no tool_execution/builtin/mcp section. But loader.rs sets tool_config: None when PROXY_CONFIG is unset (loader.rs:100-107) and has_any() is false for a simple YAML without a tool_execution block. So FORGE_TOOL_CALL_POLICY is never consulted in the exact scenario it targets. Fix the gating so a valid non-Disabled FORGE_TOOL_CALL_POLICY builds a ToolEngineState with an empty registry + default policy/loop_config, whether tool_config is None or present-but-empty. Preserve current behavior: unset/empty/invalid/disabled env with no YAML tool sections still yields None (or a Disabled engine that is inert).",
|
||||
"dropped_at": null,
|
||||
"estimate": "",
|
||||
"external_ids": {},
|
||||
"id": "EH-0001",
|
||||
"notes": "",
|
||||
"owner": "",
|
||||
"path": "tasks/done/EH-0001-make-forge-tool-call-policy-alone-initialize-the-guardrail-e.md",
|
||||
"priority": "P0",
|
||||
"source": "manual",
|
||||
"status": "done",
|
||||
"tags": [],
|
||||
"title": "Make FORGE_TOOL_CALL_POLICY alone initialize the guardrail engine (no YAML)",
|
||||
"updated_at": "2026-07-04T16:39:24Z"
|
||||
},
|
||||
"EH-0002": {
|
||||
"acceptance": [
|
||||
{
|
||||
"done": true,
|
||||
"text": "`grep -rin 'litellm' docs/CONFIG.md docs/codedocs/configuration-and-modes.md` shows a line tying LiteLLM/model_list format to tool_execution/guardrails NOT being supported."
|
||||
},
|
||||
{
|
||||
"done": true,
|
||||
"text": "loader.rs:91 comment references the doc-noted limitation (inspect crates/proxy/src/config/multi/loader.rs)."
|
||||
},
|
||||
{
|
||||
"done": true,
|
||||
"text": "No new parsing code added: `git diff --stat` shows changes limited to docs plus the loader.rs comment line."
|
||||
}
|
||||
],
|
||||
"bites": [
|
||||
{
|
||||
"done": true,
|
||||
"text": "Add an explicit note to docs/CONFIG.md (and docs/codedocs/configuration-and-modes.md where tool_execution/guardrails are described) stating: tool_execution and guardrails are only read from the simple native YAML format (top-level `models:`) or the FORGE_TOOL_CALL_POLICY env var"
|
||||
},
|
||||
{
|
||||
"done": true,
|
||||
"text": "the LiteLLM `model_list:` format ignores them."
|
||||
},
|
||||
{
|
||||
"done": true,
|
||||
"text": "Expand the inline comment at loader.rs:91 from 'LiteLLM format has no tool sections' to reference the documented limitation so future readers know it is intentional, not a TODO."
|
||||
}
|
||||
],
|
||||
"completed_at": "2026-07-04T16:04:46Z",
|
||||
"created_at": "2026-07-03T23:40:34Z",
|
||||
"description": "The LiteLLM model_list format has no tool_config path: loader.rs:91 hard-codes `tool_config: None` for that branch. Users on LiteLLM YAML who set a guardrails block will see it silently ignored, and only FORGE_TOOL_CALL_POLICY env works for them. Document this limitation explicitly across the guardrails/tool_execution docs. Do NOT add LiteLLM parsing support.",
|
||||
"dropped_at": null,
|
||||
"estimate": "",
|
||||
"external_ids": {},
|
||||
"id": "EH-0002",
|
||||
"notes": "",
|
||||
"owner": "",
|
||||
"path": "tasks/done/EH-0002-document-that-litellm-yaml-format-cannot-carry-tool-executio.md",
|
||||
"priority": "P1",
|
||||
"source": "manual",
|
||||
"status": "done",
|
||||
"tags": [],
|
||||
"title": "Document that LiteLLM YAML format cannot carry tool_execution/guardrails config",
|
||||
"updated_at": "2026-07-04T16:39:24Z"
|
||||
},
|
||||
"EH-0003": {
|
||||
"acceptance": [
|
||||
{
|
||||
"done": true,
|
||||
"text": "`grep -in 'guardrail' CHANGELOG.md` returns a line located between the `## [Unreleased]` and the next `## [` version header."
|
||||
},
|
||||
{
|
||||
"done": true,
|
||||
"text": "The bullet mentions both FORGE_TOOL_CALL_POLICY and the simple-YAML tool_execution.guardrails path."
|
||||
}
|
||||
],
|
||||
"bites": [
|
||||
{
|
||||
"done": true,
|
||||
"text": "Under the `## [Unreleased]` header in CHANGELOG.md, add an `### Added` bullet describing opt-in Forge-style tool-call guardrails (lsp_first/quiet_command/write_payload_cap nudges, fingerprint dedup) configurable via simple YAML tool_execution.guardrails or the FORGE_TOOL_CALL_POLICY env var, noting the LiteLLM-format limitation."
|
||||
}
|
||||
],
|
||||
"completed_at": "2026-07-04T16:07:46Z",
|
||||
"created_at": "2026-07-03T23:40:45Z",
|
||||
"description": "Per the repo release process, every user-visible change needs an [Unreleased] bullet before release. The guardrails feature (config + FORGE_TOOL_CALL_POLICY env + nudges) is user-visible and the section is currently empty (CHANGELOG.md line ~11).",
|
||||
"dropped_at": null,
|
||||
"estimate": "",
|
||||
"external_ids": {},
|
||||
"id": "EH-0003",
|
||||
"notes": "",
|
||||
"owner": "",
|
||||
"path": "tasks/done/EH-0003-add-changelog-unreleased-bullet-for-opt-in-tool-call-guardra.md",
|
||||
"priority": "P2",
|
||||
"source": "manual",
|
||||
"status": "done",
|
||||
"tags": [],
|
||||
"title": "Add CHANGELOG [Unreleased] bullet for opt-in tool-call guardrails",
|
||||
"updated_at": "2026-07-04T16:39:24Z"
|
||||
},
|
||||
"EH-0004": {
|
||||
"acceptance": [
|
||||
{
|
||||
"done": false,
|
||||
"text": "`cargo build -p anyllm_proxy` compiles after the field is added at all constructor sites."
|
||||
},
|
||||
{
|
||||
"done": false,
|
||||
"text": "`grep -rin 'tool_guardrail' crates/proxy/src/admin/state.rs crates/proxy/src/main.rs crates/proxy/src/admin/routes/config.rs crates/proxy/admin-ui/src/tabs/settings/Settings.tsx` returns matches in all four (confirming all non-compiler-caught sites were touched)."
|
||||
},
|
||||
{
|
||||
"done": false,
|
||||
"text": "Headless: an admin config PUT setting the field then GET returns the new value (assert via a config-route test or curl against a running proxy). (manual residual: toggle the control in `cargo run -p anyllm_proxy -- --webui` and confirm it survives a restart.)"
|
||||
}
|
||||
],
|
||||
"bites": [
|
||||
{
|
||||
"done": false,
|
||||
"text": "Add a RuntimeConfig field (e.g. tool_guardrail_mode: String) at all 6 sites: struct + RuntimeConfigDefaults in crates/proxy/src/admin/state.rs, the 3 constructors (SharedState::new_for_test, cost/mod.rs test, main.rs), the SQLite override-apply match in main.rs, and delete_config_override reset in crates/proxy/src/admin/routes/config.rs. Make the runtime value flow into the guardrail mode actually used at request time."
|
||||
},
|
||||
{
|
||||
"done": false,
|
||||
"text": "Add a toggle/select control to crates/proxy/admin-ui/src/tabs/settings/Settings.tsx wired to the existing admin config GET/PUT route for the new field."
|
||||
}
|
||||
],
|
||||
"completed_at": null,
|
||||
"created_at": "2026-07-03T23:40:55Z",
|
||||
"description": "No admin surface exists for tool_execution guardrails (admin-ui/src has zero references). Optionally expose a runtime-tunable guardrail mode following the documented 6-site RuntimeConfig-field checklist so it persists and resets like other runtime config. Lower priority than the P0/P1 cards; skip if scope is tight. Note the two non-compiler-caught sites (SQLite override-apply match in main.rs, delete_config_override reset in admin/routes/config.rs) that silently break persistence/reset if missed.",
|
||||
"dropped_at": "2026-07-03T23:47:21Z",
|
||||
"estimate": "",
|
||||
"external_ids": {},
|
||||
"id": "EH-0004",
|
||||
"notes": "",
|
||||
"owner": "",
|
||||
"path": "tasks/dropped/EH-0004-optional-admin-ui-runtimeconfig-toggle-for-guardrail-mode.md",
|
||||
"priority": "P3",
|
||||
"source": "manual",
|
||||
"status": "dropped",
|
||||
"tags": [],
|
||||
"title": "(Optional) Admin UI + RuntimeConfig toggle for guardrail mode",
|
||||
"updated_at": "2026-07-04T16:39:24Z"
|
||||
},
|
||||
"EH-0005": {
|
||||
"acceptance": [
|
||||
{
|
||||
"done": true,
|
||||
"text": "cargo build -p anyllm_proxy compiles after the field is added at all constructor sites"
|
||||
},
|
||||
{
|
||||
"done": true,
|
||||
"text": "grep -rin 'tool_guardrail' crates/proxy/src/admin/state.rs crates/proxy/src/main_helpers/async_main/admin.rs crates/proxy/src/admin/routes/config.rs returns matches in all three backend files (confirms the non-compiler-caught sites were touched; path corrected from main.rs to main_helpers/async_main/admin.rs, see Notes)"
|
||||
},
|
||||
{
|
||||
"done": true,
|
||||
"text": "Headless: a config-route test (or curl against a running proxy) does a config PUT setting tool_guardrail_mode then a GET returns the new value"
|
||||
}
|
||||
],
|
||||
"bites": [
|
||||
{
|
||||
"done": true,
|
||||
"text": "Add tool_guardrail_mode: String to the RuntimeConfig struct + RuntimeConfigDefaults in crates/proxy/src/admin/state.rs"
|
||||
},
|
||||
{
|
||||
"done": true,
|
||||
"text": "Update the 3 constructors (SharedState::new_for_test, cost/mod.rs test, main.rs) to set the new field"
|
||||
},
|
||||
{
|
||||
"done": true,
|
||||
"text": "Add the field to the SQLite override-apply match in main.rs and the delete_config_override reset in crates/proxy/src/admin/routes/config.rs, and make the runtime value flow into the guardrail mode used at request time"
|
||||
}
|
||||
],
|
||||
"completed_at": "2026-07-04T16:30:43Z",
|
||||
"created_at": "2026-07-03T23:47:21Z",
|
||||
"description": "Expose a runtime-tunable guardrail mode as a RuntimeConfig field so it persists/resets like other runtime config. Backend only; the admin UI control is a separate card (split from former EH-0004). Watch the two non-compiler-caught sites (SQLite override-apply match in main.rs, delete_config_override reset in admin/routes/config.rs) that silently break persistence/reset if missed.",
|
||||
"dropped_at": null,
|
||||
"estimate": "",
|
||||
"external_ids": {},
|
||||
"id": "EH-0005",
|
||||
"notes": "Field, 3 constructors, SQLite override-apply, delete-reset, GET/PUT config-route\nsupport, and runtime-mode wiring into `evaluate_tool_guardrails`\n(`tools::resolve_runtime_guardrails` + `AppState::effective_tool_guardrails`,\nconsumed by `handler.rs`, `routes/messages.rs`, and the streaming\n`tool_loop.rs`) are all done. `cargo build -p anyllm_proxy` and\n`cargo test -p anyllm_proxy` (599 passed) are clean, and\n`admin::routes::tests::put_config_tool_guardrail_mode_then_get_returns_new_value`\ncovers the PUT-then-GET round trip headlessly.\n\nAcceptance check 2 as literally worded (\"... crates/proxy/src/main.rs ...\")\ndoes not hold: `main.rs` is now a thin 167-line entry point (env/arg\nbootstrapping only) — the actual SQLite override-apply match lives in\n`crates/proxy/src/main_helpers/async_main/admin.rs::init_admin` (that's where\n`anthropic_thinking_repair`'s override-apply arm already lived pre-refactor,\nper `git grep`). I added the `tool_guardrail_mode` arm there, matching the\nother fields, not in `main.rs` itself. Leaving acceptance item 2 unchecked\nsince a literal `grep ... crates/proxy/src/main.rs` returns no match; the\nunderlying intent (both non-compiler-caught sites touched) is satisfied. If a\nhuman confirms the check should target `main_helpers/async_main/admin.rs`\ninstead, this can be marked done.\n\nResolved: confirmed `main_helpers/async_main/admin.rs` is the correct, established\nlocation for this class of override-apply arm — `anthropic_thinking_repair`'s\nown arm already lived there before this card touched the file (`git grep\nanthropic_thinking_repair crates/proxy/src/main_helpers/async_main/admin.rs`\nshows matching struct-init + match-arm lines at 108/114/149-150, same shape as\nthe new `tool_guardrail_mode` arm at 109/115/152-161). The card's description\nand acceptance-check path were written against the pre-refactor layout where\nthis logic lived in `main.rs`; `main.rs` is now a thin 167-line bootstrap\nentry point post-refactor (unrelated to this card). Updated acceptance check 2\ntext above to reference the current path and re-ran the grep against all\nthree correct files — all match. No source change was needed; this was a\nstale path reference in the acceptance-check text, not a missed\nimplementation site.",
|
||||
"owner": "",
|
||||
"path": "tasks/done/EH-0005-backend-add-tool-guardrail-mode-runtimeconfig-field-6-site-c.md",
|
||||
"priority": "P3",
|
||||
"source": "manual",
|
||||
"status": "done",
|
||||
"tags": [],
|
||||
"title": "Backend: add tool_guardrail_mode RuntimeConfig field (6-site checklist)",
|
||||
"updated_at": "2026-07-04T16:39:24Z"
|
||||
},
|
||||
"EH-0006": {
|
||||
"acceptance": [
|
||||
{
|
||||
"done": true,
|
||||
"text": "grep -rin 'tool_guardrail' crates/proxy/admin-ui/src/tabs/settings/Settings.tsx returns the new control"
|
||||
},
|
||||
{
|
||||
"done": true,
|
||||
"text": "npm run build in crates/proxy/admin-ui compiles with the new control present"
|
||||
},
|
||||
{
|
||||
"done": true,
|
||||
"text": "Manual: in cargo run -p anyllm_proxy -- --webui, toggling the control then restarting shows the value persisted"
|
||||
}
|
||||
],
|
||||
"bites": [
|
||||
{
|
||||
"done": true,
|
||||
"text": "Add a select/toggle control for tool_guardrail_mode to crates/proxy/admin-ui/src/tabs/settings/Settings.tsx, wired to the existing admin config GET/PUT route"
|
||||
}
|
||||
],
|
||||
"completed_at": "2026-07-04T16:37:17Z",
|
||||
"created_at": "2026-07-03T23:47:21Z",
|
||||
"description": "Add a toggle/select control for tool_guardrail_mode to the admin UI, wired to the existing admin config GET/PUT route. Depends on the backend RuntimeConfig-field card being done first (split from former EH-0004).",
|
||||
"dropped_at": null,
|
||||
"estimate": "",
|
||||
"external_ids": {},
|
||||
"id": "EH-0006",
|
||||
"notes": "Verified the \"manual\" acceptance item headlessly by driving the same admin API the UI control\ncalls (no browser available in this sandbox): started `cargo run -p anyllm_proxy -- --webui`\nagainst a scratch `ANYLLM_HOME`, fetched the admin token + CSRF token, `PUT\n/admin/api/config {\"tool_guardrail_mode\":\"standard\"}` (confirmed `disabled` -> `standard` and\n`overridden_keys` includes it), killed and restarted the process, and confirmed\n`GET /admin/api/config` still returned `tool_guardrail_mode: \"standard\"` with the override\nintact. This exercises the exact backend path the new `<select>` in Settings.tsx invokes\n(`save.mutate({ tool_guardrail_mode })` -> `PUT /admin/api/config`), so it stands in for a\nliteral browser click.",
|
||||
"owner": "",
|
||||
"path": "tasks/done/EH-0006-admin-ui-guardrail-mode-toggle-in-settings-tsx.md",
|
||||
"priority": "P3",
|
||||
"source": "manual",
|
||||
"status": "done",
|
||||
"tags": [],
|
||||
"title": "Admin UI: guardrail mode toggle in Settings.tsx",
|
||||
"updated_at": "2026-07-04T16:39:24Z"
|
||||
}
|
||||
},
|
||||
"updated_at": "2026-07-04T16:39:24Z",
|
||||
"version": 1
|
||||
}
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
---
|
||||
id: "EH-0001"
|
||||
title: "Make FORGE_TOOL_CALL_POLICY alone initialize the guardrail engine (no YAML)"
|
||||
status: "done"
|
||||
priority: "P0"
|
||||
owner: ""
|
||||
tags: []
|
||||
estimate: ""
|
||||
source: "manual"
|
||||
path: "tasks/done/EH-0001-make-forge-tool-call-policy-alone-initialize-the-guardrail-e.md"
|
||||
created_at: "2026-07-03T23:40:25Z"
|
||||
updated_at: "2026-07-04T16:39:24Z"
|
||||
completed_at: "2026-07-04T15:59:55Z"
|
||||
dropped_at: null
|
||||
---
|
||||
|
||||
# EH-0001: Make FORGE_TOOL_CALL_POLICY alone initialize the guardrail engine (no YAML)
|
||||
|
||||
Status: `done` · Priority: `P0` · Owner: `unassigned` · Estimate: `unsized`
|
||||
Tags: none
|
||||
Completed: `2026-07-04T15:59:55Z`
|
||||
|
||||
## Description
|
||||
The env-var-only local-LLM path is currently dead. init_tool_engine (crates/proxy/src/main_helpers/async_main/tools.rs:9) does `let tc = tool_config.filter(|tc| tc.has_any())?`; and returns None whenever there is no tool_execution/builtin/mcp section. But loader.rs sets tool_config: None when PROXY_CONFIG is unset (loader.rs:100-107) and has_any() is false for a simple YAML without a tool_execution block. So FORGE_TOOL_CALL_POLICY is never consulted in the exact scenario it targets. Fix the gating so a valid non-Disabled FORGE_TOOL_CALL_POLICY builds a ToolEngineState with an empty registry + default policy/loop_config, whether tool_config is None or present-but-empty. Preserve current behavior: unset/empty/invalid/disabled env with no YAML tool sections still yields None (or a Disabled engine that is inert).
|
||||
|
||||
## One-bite-at-a-time plan
|
||||
- [x] Refactor init_tool_engine so the FORGE_TOOL_CALL_POLICY resolution runs BEFORE the has_any() early return: when tool_config is None or has_any() is false, resolve the env var and, if it parses to a non-Disabled ToolGuardrailMode, proceed to build the engine (empty registry, default policy/loop_config, no MCP)
|
||||
- [x] otherwise return None as today. Keep the existing YAML-precedence rule (guardrails_set_in_yaml wins over env).
|
||||
- [x] Add a #[cfg(test)] module (or extend the nearest tools test) covering: (a) tool_config None + FORGE_TOOL_CALL_POLICY=standard => Some engine with guardrails.mode != Disabled
|
||||
- [x] (b) env unset + no tool sections => None
|
||||
- [x] (c) invalid env value => None (or Disabled) and does not panic. Acquire crate::config::ENV_TEST_LOCK before mutating the env var and restore it after.
|
||||
|
||||
## Acceptance checks
|
||||
- [x] New test passes: `cargo test -p anyllm_proxy forge_tool_call_policy_env_only` (or the chosen test name) exits 0.
|
||||
- [x] Observable: with PROXY_CONFIG unset and FORGE_TOOL_CALL_POLICY=standard, init_tool_engine returns Some and the returned ToolEngineState.guardrails.mode is not ToolGuardrailMode::Disabled (asserted directly in the test).
|
||||
- [x] `cargo clippy -p anyllm_proxy -- -D warnings` reports no warnings for the changed file.
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
<!-- Eatahorse: edit checkboxes/status/title, then run `python scripts/eatahorse.py sync` or let the Claude Code hook sync it. -->
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
---
|
||||
id: "EH-0002"
|
||||
title: "Document that LiteLLM YAML format cannot carry tool_execution/guardrails config"
|
||||
status: "done"
|
||||
priority: "P1"
|
||||
owner: ""
|
||||
tags: []
|
||||
estimate: ""
|
||||
source: "manual"
|
||||
path: "tasks/done/EH-0002-document-that-litellm-yaml-format-cannot-carry-tool-executio.md"
|
||||
created_at: "2026-07-03T23:40:34Z"
|
||||
updated_at: "2026-07-04T16:39:24Z"
|
||||
completed_at: "2026-07-04T16:04:46Z"
|
||||
dropped_at: null
|
||||
---
|
||||
|
||||
# EH-0002: Document that LiteLLM YAML format cannot carry tool_execution/guardrails config
|
||||
|
||||
Status: `done` · Priority: `P1` · Owner: `unassigned` · Estimate: `unsized`
|
||||
Tags: none
|
||||
Completed: `2026-07-04T16:04:46Z`
|
||||
|
||||
## Description
|
||||
The LiteLLM model_list format has no tool_config path: loader.rs:91 hard-codes `tool_config: None` for that branch. Users on LiteLLM YAML who set a guardrails block will see it silently ignored, and only FORGE_TOOL_CALL_POLICY env works for them. Document this limitation explicitly across the guardrails/tool_execution docs. Do NOT add LiteLLM parsing support.
|
||||
|
||||
## One-bite-at-a-time plan
|
||||
- [x] Add an explicit note to docs/CONFIG.md (and docs/codedocs/configuration-and-modes.md where tool_execution/guardrails are described) stating: tool_execution and guardrails are only read from the simple native YAML format (top-level `models:`) or the FORGE_TOOL_CALL_POLICY env var
|
||||
- [x] the LiteLLM `model_list:` format ignores them.
|
||||
- [x] Expand the inline comment at loader.rs:91 from 'LiteLLM format has no tool sections' to reference the documented limitation so future readers know it is intentional, not a TODO.
|
||||
|
||||
## Acceptance checks
|
||||
- [x] `grep -rin 'litellm' docs/CONFIG.md docs/codedocs/configuration-and-modes.md` shows a line tying LiteLLM/model_list format to tool_execution/guardrails NOT being supported.
|
||||
- [x] loader.rs:91 comment references the doc-noted limitation (inspect crates/proxy/src/config/multi/loader.rs).
|
||||
- [x] No new parsing code added: `git diff --stat` shows changes limited to docs plus the loader.rs comment line.
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
<!-- Eatahorse: edit checkboxes/status/title, then run `python scripts/eatahorse.py sync` or let the Claude Code hook sync it. -->
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
---
|
||||
id: "EH-0003"
|
||||
title: "Add CHANGELOG [Unreleased] bullet for opt-in tool-call guardrails"
|
||||
status: "done"
|
||||
priority: "P2"
|
||||
owner: ""
|
||||
tags: []
|
||||
estimate: ""
|
||||
source: "manual"
|
||||
path: "tasks/done/EH-0003-add-changelog-unreleased-bullet-for-opt-in-tool-call-guardra.md"
|
||||
created_at: "2026-07-03T23:40:45Z"
|
||||
updated_at: "2026-07-04T16:39:24Z"
|
||||
completed_at: "2026-07-04T16:07:46Z"
|
||||
dropped_at: null
|
||||
---
|
||||
|
||||
# EH-0003: Add CHANGELOG [Unreleased] bullet for opt-in tool-call guardrails
|
||||
|
||||
Status: `done` · Priority: `P2` · Owner: `unassigned` · Estimate: `unsized`
|
||||
Tags: none
|
||||
Completed: `2026-07-04T16:07:46Z`
|
||||
|
||||
## Description
|
||||
Per the repo release process, every user-visible change needs an [Unreleased] bullet before release. The guardrails feature (config + FORGE_TOOL_CALL_POLICY env + nudges) is user-visible and the section is currently empty (CHANGELOG.md line ~11).
|
||||
|
||||
## One-bite-at-a-time plan
|
||||
- [x] Under the `## [Unreleased]` header in CHANGELOG.md, add an `### Added` bullet describing opt-in Forge-style tool-call guardrails (lsp_first/quiet_command/write_payload_cap nudges, fingerprint dedup) configurable via simple YAML tool_execution.guardrails or the FORGE_TOOL_CALL_POLICY env var, noting the LiteLLM-format limitation.
|
||||
|
||||
## Acceptance checks
|
||||
- [x] `grep -in 'guardrail' CHANGELOG.md` returns a line located between the `## [Unreleased]` and the next `## [` version header.
|
||||
- [x] The bullet mentions both FORGE_TOOL_CALL_POLICY and the simple-YAML tool_execution.guardrails path.
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
<!-- Eatahorse: edit checkboxes/status/title, then run `python scripts/eatahorse.py sync` or let the Claude Code hook sync it. -->
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
---
|
||||
id: "EH-0005"
|
||||
title: "Backend: add tool_guardrail_mode RuntimeConfig field (6-site checklist)"
|
||||
status: "done"
|
||||
priority: "P3"
|
||||
owner: ""
|
||||
tags: []
|
||||
estimate: ""
|
||||
source: "manual"
|
||||
path: "tasks/done/EH-0005-backend-add-tool-guardrail-mode-runtimeconfig-field-6-site-c.md"
|
||||
created_at: "2026-07-03T23:47:21Z"
|
||||
updated_at: "2026-07-04T16:39:24Z"
|
||||
completed_at: "2026-07-04T16:30:43Z"
|
||||
dropped_at: null
|
||||
---
|
||||
|
||||
# EH-0005: Backend: add tool_guardrail_mode RuntimeConfig field (6-site checklist)
|
||||
|
||||
Status: `done` · Priority: `P3` · Owner: `unassigned` · Estimate: `unsized`
|
||||
Tags: none
|
||||
Completed: `2026-07-04T16:30:43Z`
|
||||
|
||||
## Description
|
||||
Expose a runtime-tunable guardrail mode as a RuntimeConfig field so it persists/resets like other runtime config. Backend only; the admin UI control is a separate card (split from former EH-0004). Watch the two non-compiler-caught sites (SQLite override-apply match in main.rs, delete_config_override reset in admin/routes/config.rs) that silently break persistence/reset if missed.
|
||||
|
||||
## One-bite-at-a-time plan
|
||||
- [x] Add tool_guardrail_mode: String to the RuntimeConfig struct + RuntimeConfigDefaults in crates/proxy/src/admin/state.rs
|
||||
- [x] Update the 3 constructors (SharedState::new_for_test, cost/mod.rs test, main.rs) to set the new field
|
||||
- [x] Add the field to the SQLite override-apply match in main.rs and the delete_config_override reset in crates/proxy/src/admin/routes/config.rs, and make the runtime value flow into the guardrail mode used at request time
|
||||
|
||||
## Acceptance checks
|
||||
- [x] cargo build -p anyllm_proxy compiles after the field is added at all constructor sites
|
||||
- [x] grep -rin 'tool_guardrail' crates/proxy/src/admin/state.rs crates/proxy/src/main_helpers/async_main/admin.rs crates/proxy/src/admin/routes/config.rs returns matches in all three backend files (confirms the non-compiler-caught sites were touched; path corrected from main.rs to main_helpers/async_main/admin.rs, see Notes)
|
||||
- [x] Headless: a config-route test (or curl against a running proxy) does a config PUT setting tool_guardrail_mode then a GET returns the new value
|
||||
|
||||
## Notes
|
||||
Field, 3 constructors, SQLite override-apply, delete-reset, GET/PUT config-route
|
||||
support, and runtime-mode wiring into `evaluate_tool_guardrails`
|
||||
(`tools::resolve_runtime_guardrails` + `AppState::effective_tool_guardrails`,
|
||||
consumed by `handler.rs`, `routes/messages.rs`, and the streaming
|
||||
`tool_loop.rs`) are all done. `cargo build -p anyllm_proxy` and
|
||||
`cargo test -p anyllm_proxy` (599 passed) are clean, and
|
||||
`admin::routes::tests::put_config_tool_guardrail_mode_then_get_returns_new_value`
|
||||
covers the PUT-then-GET round trip headlessly.
|
||||
|
||||
Acceptance check 2 as literally worded ("... crates/proxy/src/main.rs ...")
|
||||
does not hold: `main.rs` is now a thin 167-line entry point (env/arg
|
||||
bootstrapping only) — the actual SQLite override-apply match lives in
|
||||
`crates/proxy/src/main_helpers/async_main/admin.rs::init_admin` (that's where
|
||||
`anthropic_thinking_repair`'s override-apply arm already lived pre-refactor,
|
||||
per `git grep`). I added the `tool_guardrail_mode` arm there, matching the
|
||||
other fields, not in `main.rs` itself. Leaving acceptance item 2 unchecked
|
||||
since a literal `grep ... crates/proxy/src/main.rs` returns no match; the
|
||||
underlying intent (both non-compiler-caught sites touched) is satisfied. If a
|
||||
human confirms the check should target `main_helpers/async_main/admin.rs`
|
||||
instead, this can be marked done.
|
||||
|
||||
Resolved: confirmed `main_helpers/async_main/admin.rs` is the correct, established
|
||||
location for this class of override-apply arm — `anthropic_thinking_repair`'s
|
||||
own arm already lived there before this card touched the file (`git grep
|
||||
anthropic_thinking_repair crates/proxy/src/main_helpers/async_main/admin.rs`
|
||||
shows matching struct-init + match-arm lines at 108/114/149-150, same shape as
|
||||
the new `tool_guardrail_mode` arm at 109/115/152-161). The card's description
|
||||
and acceptance-check path were written against the pre-refactor layout where
|
||||
this logic lived in `main.rs`; `main.rs` is now a thin 167-line bootstrap
|
||||
entry point post-refactor (unrelated to this card). Updated acceptance check 2
|
||||
text above to reference the current path and re-ran the grep against all
|
||||
three correct files — all match. No source change was needed; this was a
|
||||
stale path reference in the acceptance-check text, not a missed
|
||||
implementation site.
|
||||
|
||||
<!-- Eatahorse: edit checkboxes/status/title, then run `python scripts/eatahorse.py sync` or let the Claude Code hook sync it. -->
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
---
|
||||
id: "EH-0006"
|
||||
title: "Admin UI: guardrail mode toggle in Settings.tsx"
|
||||
status: "done"
|
||||
priority: "P3"
|
||||
owner: ""
|
||||
tags: []
|
||||
estimate: ""
|
||||
source: "manual"
|
||||
path: "tasks/done/EH-0006-admin-ui-guardrail-mode-toggle-in-settings-tsx.md"
|
||||
created_at: "2026-07-03T23:47:21Z"
|
||||
updated_at: "2026-07-04T16:39:24Z"
|
||||
completed_at: "2026-07-04T16:37:17Z"
|
||||
dropped_at: null
|
||||
---
|
||||
|
||||
# EH-0006: Admin UI: guardrail mode toggle in Settings.tsx
|
||||
|
||||
Status: `done` · Priority: `P3` · Owner: `unassigned` · Estimate: `unsized`
|
||||
Tags: none
|
||||
Completed: `2026-07-04T16:37:17Z`
|
||||
|
||||
## Description
|
||||
Add a toggle/select control for tool_guardrail_mode to the admin UI, wired to the existing admin config GET/PUT route. Depends on the backend RuntimeConfig-field card being done first (split from former EH-0004).
|
||||
|
||||
## One-bite-at-a-time plan
|
||||
- [x] Add a select/toggle control for tool_guardrail_mode to crates/proxy/admin-ui/src/tabs/settings/Settings.tsx, wired to the existing admin config GET/PUT route
|
||||
|
||||
## Acceptance checks
|
||||
- [x] grep -rin 'tool_guardrail' crates/proxy/admin-ui/src/tabs/settings/Settings.tsx returns the new control
|
||||
- [x] npm run build in crates/proxy/admin-ui compiles with the new control present
|
||||
- [x] Manual: in cargo run -p anyllm_proxy -- --webui, toggling the control then restarting shows the value persisted
|
||||
|
||||
## Notes
|
||||
Verified the "manual" acceptance item headlessly by driving the same admin API the UI control
|
||||
calls (no browser available in this sandbox): started `cargo run -p anyllm_proxy -- --webui`
|
||||
against a scratch `ANYLLM_HOME`, fetched the admin token + CSRF token, `PUT
|
||||
/admin/api/config {"tool_guardrail_mode":"standard"}` (confirmed `disabled` -> `standard` and
|
||||
`overridden_keys` includes it), killed and restarted the process, and confirmed
|
||||
`GET /admin/api/config` still returned `tool_guardrail_mode: "standard"` with the override
|
||||
intact. This exercises the exact backend path the new `<select>` in Settings.tsx invokes
|
||||
(`save.mutate({ tool_guardrail_mode })` -> `PUT /admin/api/config`), so it stands in for a
|
||||
literal browser click.
|
||||
|
||||
<!-- Eatahorse: edit checkboxes/status/title, then run `python scripts/eatahorse.py sync` or let the Claude Code hook sync it. -->
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
---
|
||||
id: "EH-0004"
|
||||
title: "(Optional) Admin UI + RuntimeConfig toggle for guardrail mode"
|
||||
status: "dropped"
|
||||
priority: "P3"
|
||||
owner: ""
|
||||
tags: []
|
||||
estimate: ""
|
||||
source: "manual"
|
||||
path: "tasks/dropped/EH-0004-optional-admin-ui-runtimeconfig-toggle-for-guardrail-mode.md"
|
||||
created_at: "2026-07-03T23:40:55Z"
|
||||
updated_at: "2026-07-04T16:39:24Z"
|
||||
completed_at: null
|
||||
dropped_at: "2026-07-03T23:47:21Z"
|
||||
---
|
||||
|
||||
# EH-0004: (Optional) Admin UI + RuntimeConfig toggle for guardrail mode
|
||||
|
||||
Status: `dropped` · Priority: `P3` · Owner: `unassigned` · Estimate: `unsized`
|
||||
Tags: none
|
||||
|
||||
## Description
|
||||
No admin surface exists for tool_execution guardrails (admin-ui/src has zero references). Optionally expose a runtime-tunable guardrail mode following the documented 6-site RuntimeConfig-field checklist so it persists and resets like other runtime config. Lower priority than the P0/P1 cards; skip if scope is tight. Note the two non-compiler-caught sites (SQLite override-apply match in main.rs, delete_config_override reset in admin/routes/config.rs) that silently break persistence/reset if missed.
|
||||
|
||||
## One-bite-at-a-time plan
|
||||
- [ ] Add a RuntimeConfig field (e.g. tool_guardrail_mode: String) at all 6 sites: struct + RuntimeConfigDefaults in crates/proxy/src/admin/state.rs, the 3 constructors (SharedState::new_for_test, cost/mod.rs test, main.rs), the SQLite override-apply match in main.rs, and delete_config_override reset in crates/proxy/src/admin/routes/config.rs. Make the runtime value flow into the guardrail mode actually used at request time.
|
||||
- [ ] Add a toggle/select control to crates/proxy/admin-ui/src/tabs/settings/Settings.tsx wired to the existing admin config GET/PUT route for the new field.
|
||||
|
||||
## Acceptance checks
|
||||
- [ ] `cargo build -p anyllm_proxy` compiles after the field is added at all constructor sites.
|
||||
- [ ] `grep -rin 'tool_guardrail' crates/proxy/src/admin/state.rs crates/proxy/src/main.rs crates/proxy/src/admin/routes/config.rs crates/proxy/admin-ui/src/tabs/settings/Settings.tsx` returns matches in all four (confirming all non-compiler-caught sites were touched).
|
||||
- [ ] Headless: an admin config PUT setting the field then GET returns the new value (assert via a config-route test or curl against a running proxy). (manual residual: toggle the control in `cargo run -p anyllm_proxy -- --webui` and confirm it survives a restart.)
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
<!-- Eatahorse: edit checkboxes/status/title, then run `python scripts/eatahorse.py sync` or let the Claude Code hook sync it. -->
|
||||
@@ -56,3 +56,4 @@ crates/proxy/admin-ui/node_modules/
|
||||
# dist/ is committed so `include_str!` works at compile time without a build step
|
||||
# crates/proxy/admin-ui/dist/
|
||||
.synrepo/
|
||||
.eatahorse*/
|
||||
+8
-8
File diff suppressed because one or more lines are too long
@@ -298,7 +298,11 @@ export function useCreateManagedBackend() {
|
||||
return useMutation<ManagedBackend, Error, CreateManagedBackendRequest>({
|
||||
mutationFn: (data) =>
|
||||
mutatingFetch<ManagedBackend>('POST', '/admin/api/backends/managed', data),
|
||||
onSuccess: () => { qc.invalidateQueries({ queryKey: ['managed-backends'] }) },
|
||||
// Adding the first managed backend flips /admin/api/status configured -> true.
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['managed-backends'] })
|
||||
qc.invalidateQueries({ queryKey: ['status'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -316,7 +320,11 @@ export function useDeleteManagedBackend() {
|
||||
return useMutation<void, Error, string>({
|
||||
mutationFn: (name) =>
|
||||
mutatingFetch<void>('DELETE', `/admin/api/backends/managed/${name}`),
|
||||
onSuccess: () => { qc.invalidateQueries({ queryKey: ['managed-backends'] }) },
|
||||
// Deleting the last managed backend can flip configured -> false.
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['managed-backends'] })
|
||||
qc.invalidateQueries({ queryKey: ['status'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/** Tiny "(?)" marker with a native tooltip. Dependency-free — uses the browser's
|
||||
* title attribute (same approach as the uptime day cells). For richer hover cards,
|
||||
* swap the implementation here; every call site stays the same. */
|
||||
export default function InfoTip({ text }: { text: string }) {
|
||||
return (
|
||||
<span className="info-tip" title={text} aria-label={text} role="img">
|
||||
?
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -65,7 +65,7 @@ body { background: var(--bg-base); color: var(--text-1); font-family: var(--font
|
||||
a { color: var(--accent); text-decoration: none; }
|
||||
|
||||
/* App layout */
|
||||
.app-layout { display: flex; min-height: 100vh; }
|
||||
.app-layout { display: flex; min-height: 100vh; max-width: 100vw; overflow-x: hidden; }
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar { width: 220px; min-width: 220px; background: var(--bg-raised); border-right: 1px solid var(--border); display: flex; flex-direction: column; position: sticky; top: 0; height: 100vh; }
|
||||
@@ -88,7 +88,12 @@ a { color: var(--accent); text-decoration: none; }
|
||||
@keyframes pulse { 0%, 100% { opacity: 0.9; } 50% { opacity: 0.4; } }
|
||||
|
||||
/* Layout */
|
||||
.tab-content { flex: 1; min-width: 0; padding: 14px 20px; max-width: 1400px; margin: 0 auto; overflow-x: auto; }
|
||||
/* tab-content is a plain fluid flex child so it always tracks the viewport and its
|
||||
overflow-x scrolls (rather than clipping) when a page is genuinely too wide. The
|
||||
1400px reading-width cap + centering lives on the page root, not this flex box —
|
||||
putting max-width/margin:auto on the flex item itself breaks its shrink behaviour. */
|
||||
.tab-content { flex: 1; min-width: 0; padding: 14px 20px; overflow-x: auto; }
|
||||
.tab-content > * { max-width: 1400px; margin-inline: auto; }
|
||||
|
||||
/* Stats */
|
||||
.stats-row { display: flex; border: 1px solid var(--border); border-radius: var(--r); overflow: hidden; margin-bottom: 14px; background: var(--bg-raised); }
|
||||
@@ -230,6 +235,11 @@ button[disabled] { opacity: 0.55; cursor: not-allowed; }
|
||||
/* Key result */
|
||||
.key-result { background: var(--bg-sunken); border: 1px solid var(--border); border-left: 3px solid var(--ok); border-radius: var(--r); padding: 12px; margin-bottom: 12px; font-family: var(--font-mono); font-size: 12px; word-break: break-all; }
|
||||
.key-result-label { color: var(--ok); font-size: 11px; margin-bottom: 4px; font-weight: 600; }
|
||||
.key-result-value { display: flex; align-items: center; gap: 10px; }
|
||||
.key-result-value > .mono { flex: 1; word-break: break-all; }
|
||||
.key-result-actions { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
|
||||
.key-result-dismiss { background: none; border: none; color: var(--text-3); cursor: pointer; font-size: 18px; line-height: 1; padding: 0 4px; font-family: var(--font-ui); }
|
||||
.key-result-dismiss:hover { color: var(--text-1); }
|
||||
|
||||
/* Modal */
|
||||
.modal-backdrop { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.7); display: flex; align-items: center; justify-content: center; z-index: 100; }
|
||||
@@ -346,6 +356,10 @@ button[disabled] { opacity: 0.55; cursor: not-allowed; }
|
||||
.empty-cta-body { color: var(--text-2); font-size: 12px; max-width: 520px; line-height: 1.5; }
|
||||
.empty-cta .btn { margin-top: 6px; }
|
||||
|
||||
/* Info tooltip marker (native title=) */
|
||||
.info-tip { display: inline-flex; align-items: center; justify-content: center; width: 13px; height: 13px; margin-left: 5px; border-radius: 50%; border: 1px solid var(--border); color: var(--text-3); font-size: 9px; line-height: 1; cursor: help; vertical-align: middle; font-family: var(--font-ui); }
|
||||
.info-tip:hover { color: var(--text-1); border-color: var(--text-3); }
|
||||
|
||||
/* Inline error (non-modal) */
|
||||
.inline-error { margin-top: 8px; padding: 6px 10px; background: var(--err-dim); border-left: 3px solid var(--err); border-radius: var(--r); font-size: 12px; }
|
||||
|
||||
@@ -402,23 +416,18 @@ button[disabled] { opacity: 0.55; cursor: not-allowed; }
|
||||
.provider-tile:hover { border-color: var(--accent); background: var(--bg-hover); }
|
||||
.provider-tile.has-backends { border-left: 2px solid var(--accent); }
|
||||
.provider-tile-name { font-size: 12px; font-weight: 500; color: var(--text-1); line-height: 1.3; }
|
||||
.provider-tile-id { font-size: 10px; color: var(--text-3); font-family: var(--font-mono); line-height: 1.2; word-break: break-all; }
|
||||
.provider-tile-count { font-size: 10px; color: var(--text-3); font-family: var(--font-mono); }
|
||||
|
||||
/* Provider detail panel (lightbox overlay) */
|
||||
.provider-scrim { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.65); z-index: 50; }
|
||||
.provider-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: min(640px, 90vw); max-height: 80vh; overflow-y: auto; z-index: 51; background: var(--bg-raised); border: 1px solid var(--border); border-top: 2px solid var(--accent); border-radius: var(--rm); }
|
||||
.provider-panel-header { display: flex; align-items: center; gap: 12px; padding: 16px 16px 12px; border-bottom: 1px solid var(--border-sub); }
|
||||
.provider-panel-header h3 { flex: 1; font-size: 16px; font-weight: 600; margin: 0; }
|
||||
.provider-panel-close { background: none; border: none; color: var(--text-3); cursor: pointer; font-size: 20px; line-height: 1; padding: 0 4px; font-family: var(--font-ui); }
|
||||
.provider-panel-close:hover { color: var(--text-1); }
|
||||
.provider-panel-caps { display: flex; flex-wrap: wrap; gap: 5px; padding: 10px 16px; border-bottom: 1px solid var(--border-sub); align-items: center; }
|
||||
/* Provider detail panel (rendered inside shared Modal .modal-body) */
|
||||
.provider-panel-caps { display: flex; flex-wrap: wrap; gap: 5px; padding: 0 0 10px; border-bottom: 1px solid var(--border-sub); align-items: center; }
|
||||
.badge-cap { font-size: 10px; padding: 2px 6px; border-radius: 1px; background: var(--bg-hover); color: var(--text-2); border: 1px solid var(--border); font-family: var(--font-mono); }
|
||||
.badge-cap.active { background: var(--accent-dim); color: var(--accent); border-color: var(--accent-bdr); }
|
||||
.provider-panel-meta { display: flex; gap: 16px; padding: 8px 16px; font-size: 12px; color: var(--text-2); border-bottom: 1px solid var(--border-sub); flex-wrap: wrap; }
|
||||
.provider-panel-meta { display: flex; gap: 16px; padding: 8px 0; font-size: 12px; color: var(--text-2); border-bottom: 1px solid var(--border-sub); flex-wrap: wrap; }
|
||||
.provider-panel-meta .mono { font-family: var(--font-mono); }
|
||||
|
||||
/* Backend rows within panel */
|
||||
.provider-panel-section { padding: 12px 16px; }
|
||||
.provider-panel-section { padding: 12px 0 0; }
|
||||
.provider-panel-section-label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--text-2); font-weight: 500; margin-bottom: 8px; }
|
||||
.provider-backend-row { display: flex; align-items: center; gap: 8px; padding: 7px 10px; background: var(--bg-sunken); border: 1px solid var(--border-sub); border-radius: var(--r); margin-bottom: 4px; font-size: 13px; }
|
||||
.provider-backend-row .backend-name { font-family: var(--font-mono); font-weight: 500; flex: 1; }
|
||||
@@ -458,6 +467,7 @@ button[disabled] { opacity: 0.55; cursor: not-allowed; }
|
||||
.sidebar-item { white-space: nowrap; padding: 8px 10px; border-left: 0; border-bottom: 2px solid transparent; border-radius: var(--r); }
|
||||
.sidebar-item.active { border-left-color: transparent; border-bottom-color: var(--accent); }
|
||||
.tab-content { width: 100%; max-width: none; margin: 0; padding: 12px; overflow-x: hidden; }
|
||||
.tab-content > * { max-width: none; margin-inline: 0; }
|
||||
|
||||
.stats-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.stat, .stats-row .admin-surface.stat { border-right: 1px solid var(--border); border-bottom: 1px solid var(--border); }
|
||||
@@ -508,7 +518,6 @@ button[disabled] { opacity: 0.55; cursor: not-allowed; }
|
||||
.route-provider-models, .route-provider-reorder, .route-provider-toggle, .route-provider-remove { grid-column: 2; justify-self: start; }
|
||||
.route-provider-id { display: block; margin-left: 0; margin-top: 2px; }
|
||||
|
||||
.provider-panel { width: calc(100vw - 24px); max-height: calc(100svh - 24px); }
|
||||
.login-title { margin-bottom: 18px; }
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { BackendUptimeInfo } from '../../api/types'
|
||||
import EmptyState from '../../components/shared/EmptyState'
|
||||
import StatusDot from '../../components/shared/StatusDot'
|
||||
import { AdminSurface } from '../../components/shared/Performative'
|
||||
import { ManagedBackendsSection } from '../settings/ManagedBackendsSection'
|
||||
|
||||
export default function Backends() {
|
||||
const { data, isLoading, error } = useBackends()
|
||||
@@ -19,13 +20,17 @@ export default function Backends() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Add/Edit/Delete lives here so backends are configured and monitored in one place. */}
|
||||
<ManagedBackendsSection />
|
||||
|
||||
<div className="section-label" style={{ marginTop: 8 }}>Backend Status</div>
|
||||
<EmptyState loading={isLoading} error={error?.message} empty={data?.length === 0} />
|
||||
<div className="backend-cards">
|
||||
{data?.map((b) => {
|
||||
const health = healthMap.get(b.name)
|
||||
const dot = health?.status === 'up' ? 'ok' : health?.status === 'down' ? 'err' : 'dim'
|
||||
return (
|
||||
<AdminSurface className="card" key={b.name} glowOnHover>
|
||||
<AdminSurface className="card" key={b.name}>
|
||||
<div className="card-header">
|
||||
<span className="card-name">{b.name}</span>
|
||||
<StatusDot status={dot} pulse={dot === 'ok'} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMetrics } from '../../api/queries'
|
||||
import LiveFeed from '../../components/feed/LiveFeed'
|
||||
import { AdminSurface, AnimatedMetric } from '../../components/shared/Performative'
|
||||
import InfoTip from '../../components/shared/InfoTip'
|
||||
import ObservabilityPanel from './ObservabilityPanel'
|
||||
|
||||
export default function Dashboard() {
|
||||
@@ -22,13 +23,13 @@ export default function Dashboard() {
|
||||
</div>
|
||||
</AdminSurface>
|
||||
<AdminSurface className="stat">
|
||||
<div className="stat-label">P50 Latency</div>
|
||||
<div className="stat-label">P50 Latency<InfoTip text="Median response latency — half of requests were faster than this." /></div>
|
||||
<div className="stat-value">
|
||||
{m ? <AnimatedMetric value={m.p50_latency_ms ?? 0} format={(n) => `${Math.round(n)}ms`} /> : '—'}
|
||||
</div>
|
||||
</AdminSurface>
|
||||
<AdminSurface className="stat">
|
||||
<div className="stat-label">P95 Latency</div>
|
||||
<div className="stat-label">P95 Latency<InfoTip text="95th-percentile latency — 95% of requests were faster than this. Captures tail slowness." /></div>
|
||||
<div className="stat-value">
|
||||
{m ? <AnimatedMetric value={m.p95_latency_ms ?? 0} format={(n) => `${Math.round(n)}ms`} /> : '—'}
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useObservability, useBackends } from '../../api/queries'
|
||||
import LineChart from '../../components/shared/LineChart'
|
||||
import EmptyState from '../../components/shared/EmptyState'
|
||||
import { AdminSurface, AnimatedMetric } from '../../components/shared/Performative'
|
||||
import InfoTip from '../../components/shared/InfoTip'
|
||||
|
||||
export default function ObservabilityPanel() {
|
||||
const [windowHours, setWindowHours] = useState(6)
|
||||
@@ -58,11 +59,11 @@ export default function ObservabilityPanel() {
|
||||
<div className="stat-value"><AnimatedMetric value={data.total_output_tokens} /></div>
|
||||
</AdminSurface>
|
||||
<AdminSurface className="stat">
|
||||
<div className="stat-label">Window Failures</div>
|
||||
<div className="stat-label">Window Failures<InfoTip text="Failed (error) requests within the selected time window and backend filter." /></div>
|
||||
<div className="stat-value"><AnimatedMetric value={data.total_errors} /></div>
|
||||
</AdminSurface>
|
||||
<AdminSurface className="stat">
|
||||
<div className="stat-label">Window Cost</div>
|
||||
<div className="stat-label">Window Cost<InfoTip text="Estimated USD spend within the selected time window and backend filter, from model pricing." /></div>
|
||||
<div className="stat-value"><AnimatedMetric value={data.total_cost_usd} precision={2} format={(n) => `$${n.toFixed(2)}`} /></div>
|
||||
</AdminSurface>
|
||||
</div>
|
||||
|
||||
@@ -28,12 +28,17 @@ export default function KeyCreateForm({ onCreated }: { onCreated: (key: string)
|
||||
<form onSubmit={(e) => { e.preventDefault(); handleSubmit() }}>
|
||||
<div className="form-row" style={{ flexWrap: 'wrap' }}>
|
||||
<input name="description" placeholder="Description" value={desc} onChange={(e) => setDesc(e.target.value)} />
|
||||
<input name="max_budget_usd" placeholder="Spend limit USD" type="number" value={spendLimit} onChange={(e) => setSpendLimit(e.target.value)} style={{ width: 120 }} />
|
||||
<input name="max_budget_usd" placeholder="Spend limit USD" type="number" value={spendLimit} onChange={(e) => setSpendLimit(e.target.value)} style={{ width: 160 }} />
|
||||
<input name="rpm_limit" placeholder="RPM limit" type="number" value={rpmLimit} onChange={(e) => setRpmLimit(e.target.value)} style={{ width: 100 }} />
|
||||
<AdminButton type="submit" tone="primary" loading={create.isPending}>
|
||||
Create
|
||||
</AdminButton>
|
||||
</div>
|
||||
{!spendLimit && !rpmLimit && (
|
||||
<div className="form-hint" style={{ color: 'var(--warn)', marginTop: 6 }}>
|
||||
No limits set — this key will be unrestricted (unlimited spend and requests).
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -6,6 +6,9 @@ import Badge from '../../components/shared/Badge'
|
||||
import BudgetBar from '../../components/shared/BudgetBar'
|
||||
import KeyCreateForm from './KeyCreateForm'
|
||||
import KeyEditModal from './KeyEditModal'
|
||||
import { AdminButton } from '../../components/shared/Performative'
|
||||
import { pushToast } from '../../store/toast'
|
||||
import { copyToClipboard } from '../../utils/clipboard'
|
||||
import type { VirtualKey } from '../../api/types'
|
||||
|
||||
export default function Keys() {
|
||||
@@ -60,7 +63,30 @@ export default function Keys() {
|
||||
{newKey && (
|
||||
<div className="key-result">
|
||||
<div className="key-result-label">New key (copy now — not shown again)</div>
|
||||
{newKey}
|
||||
<div className="key-result-value">
|
||||
<span className="mono">{newKey}</span>
|
||||
<div className="key-result-actions">
|
||||
<AdminButton
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
const ok = await copyToClipboard(newKey)
|
||||
pushToast(ok
|
||||
? { variant: 'success', message: 'Key copied to clipboard' }
|
||||
: { variant: 'error', message: 'Copy failed — select and copy manually' })
|
||||
}}
|
||||
>
|
||||
Copy
|
||||
</AdminButton>
|
||||
<button
|
||||
type="button"
|
||||
className="key-result-dismiss"
|
||||
aria-label="Dismiss"
|
||||
onClick={() => setNewKey(null)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="toolbar">
|
||||
|
||||
@@ -31,6 +31,10 @@ export default function Models() {
|
||||
const { data: backends } = useBackends()
|
||||
const { data: managedBackends } = useManagedBackends()
|
||||
const [name, setName] = useState('')
|
||||
// Last value we auto-filled into Virtual Name from a discovered model. Lets us
|
||||
// re-prefill when the user picks a different model, without clobbering a name
|
||||
// they typed by hand (name !== autoName => they edited it, leave it alone).
|
||||
const [autoName, setAutoName] = useState('')
|
||||
const [model, setModel] = useState('')
|
||||
const [backendName, setBackendName] = useState('')
|
||||
const [discoverSource, setDiscoverSource] = useState('openrouter')
|
||||
@@ -47,6 +51,13 @@ export default function Models() {
|
||||
})
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
add.mutate(
|
||||
{ model_name: name, actual_model: model, backend_name: backendName },
|
||||
{ onSuccess: () => { setName(''); setAutoName(''); setModel(''); setBackendName('') } },
|
||||
)
|
||||
}
|
||||
|
||||
function doRemove() {
|
||||
if (!pendingRemove) return Promise.resolve()
|
||||
return remove.mutateAsync(pendingRemove).then(() => undefined)
|
||||
@@ -109,7 +120,17 @@ export default function Models() {
|
||||
{discover.data.models.map((m) => (
|
||||
<div
|
||||
key={m.id}
|
||||
onClick={() => setModel(m.id)}
|
||||
onClick={() => {
|
||||
setModel(m.id)
|
||||
// Prefill Virtual Name from the catalog display name (still editable).
|
||||
// Overwrite only if empty or still holding a prior auto-fill; keep a
|
||||
// hand-typed name.
|
||||
if (!name || name === autoName) {
|
||||
const next = m.name && m.name !== m.id ? m.name : m.id
|
||||
setName(next)
|
||||
setAutoName(next)
|
||||
}
|
||||
}}
|
||||
className={`models-discover-item${model === m.id ? ' is-selected' : ''}`}
|
||||
>
|
||||
<span className="mono">{m.id}</span>
|
||||
@@ -125,38 +146,35 @@ export default function Models() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Datalist for backend name suggestions */}
|
||||
<datalist id="backends-list">
|
||||
{backends?.map(b => (
|
||||
<option key={b.name} value={b.name}>{b.name}</option>
|
||||
))}
|
||||
{managedBackends?.backends.map(b => (
|
||||
<option key={`managed-${b.name}`} value={b.name}>{b.name} (managed)</option>
|
||||
))}
|
||||
</datalist>
|
||||
|
||||
{/* Manual add model form */}
|
||||
<div className="form-group">
|
||||
<div className="form-label">Add Model</div>
|
||||
<div className="form-row" style={{ flexWrap: 'wrap' }}>
|
||||
<input name="model-name" placeholder="Virtual name" value={name} onChange={(e) => setName(e.target.value)} />
|
||||
<input name="model-id" placeholder="Model ID" value={model} onChange={(e) => setModel(e.target.value)} />
|
||||
<input
|
||||
<select
|
||||
name="backend"
|
||||
placeholder="Backend"
|
||||
value={backendName}
|
||||
onChange={(e) => setBackendName(e.target.value)}
|
||||
list="backends-list"
|
||||
/>
|
||||
>
|
||||
<option value="">Backend…</option>
|
||||
{backends?.map(b => (
|
||||
<option key={b.name} value={b.name}>{b.name}</option>
|
||||
))}
|
||||
{managedBackends?.backends.map(b => (
|
||||
<option key={`managed-${b.name}`} value={b.name}>{b.name} (managed)</option>
|
||||
))}
|
||||
</select>
|
||||
<AdminButton
|
||||
tone="primary"
|
||||
onClick={() => add.mutate({ model_name: name, actual_model: model, backend_name: backendName })}
|
||||
onClick={handleAdd}
|
||||
disabled={!name || !model || !backendName || add.isPending}
|
||||
loading={add.isPending}
|
||||
>
|
||||
Add
|
||||
</AdminButton>
|
||||
</div>
|
||||
{add.isError && <div className="inline-error">{add.error.message}</div>}
|
||||
</div>
|
||||
|
||||
<div className="toolbar">
|
||||
|
||||
@@ -11,6 +11,7 @@ import { getProviderFields } from '../../utils/providerFields'
|
||||
import { groupByTier } from '../../utils/providerTiers'
|
||||
import AsyncBoundary from '../../components/shared/AsyncBoundary'
|
||||
import ConfirmDialog from '../../components/shared/ConfirmDialog'
|
||||
import Modal from '../../components/shared/Modal'
|
||||
import StatusDot from '../../components/shared/StatusDot'
|
||||
import ProviderIcon from '../../components/shared/ProviderIcon'
|
||||
import { AdminButton, AdminSurface } from '../../components/shared/Performative'
|
||||
@@ -34,6 +35,7 @@ function ProviderTile({
|
||||
>
|
||||
<ProviderIcon id={provider.id} size={28} />
|
||||
<span className="provider-tile-name">{provider.display_name}</span>
|
||||
<span className="provider-tile-id">{provider.id}</span>
|
||||
{backendCount > 0 && (
|
||||
<span className="provider-tile-count">
|
||||
{backendCount} key{backendCount !== 1 ? 's' : ''}
|
||||
@@ -196,22 +198,6 @@ function ProviderDetailPanel({
|
||||
onClose: () => void
|
||||
onDeleteBackend: (b: ManagedBackend) => void
|
||||
}) {
|
||||
// ESC to close
|
||||
useEffect(() => {
|
||||
function onKey(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') onClose()
|
||||
}
|
||||
document.addEventListener('keydown', onKey)
|
||||
return () => document.removeEventListener('keydown', onKey)
|
||||
}, [onClose])
|
||||
|
||||
// Lock body scroll
|
||||
useEffect(() => {
|
||||
const prev = document.body.style.overflow
|
||||
document.body.style.overflow = 'hidden'
|
||||
return () => { document.body.style.overflow = prev }
|
||||
}, [])
|
||||
|
||||
const caps = provider.capabilities
|
||||
const capList: [string, boolean][] = [
|
||||
['chat', caps.chat_completions],
|
||||
@@ -223,73 +209,61 @@ function ProviderDetailPanel({
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="provider-scrim" onClick={onClose} />
|
||||
<AdminSurface className="provider-panel" role="dialog" aria-modal="true" aria-label={provider.display_name}>
|
||||
{/* Header */}
|
||||
<div className="provider-panel-header">
|
||||
<ProviderIcon id={provider.id} size={36} />
|
||||
<h3>{provider.display_name}</h3>
|
||||
<button type="button" className="provider-panel-close" onClick={onClose} aria-label="Close">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<Modal open onClose={onClose} title={`${provider.display_name} (${provider.id})`} size="md">
|
||||
{/* Capabilities */}
|
||||
<div className="provider-panel-caps">
|
||||
{capList.map(([label, active]) => (
|
||||
<span key={label} className={`badge-cap${active ? ' active' : ''}`}>
|
||||
{label}
|
||||
</span>
|
||||
))}
|
||||
<span style={{ marginLeft: 'auto' }} className="badge-cap active">
|
||||
{provider.model_count} models
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Capabilities */}
|
||||
<div className="provider-panel-caps">
|
||||
{capList.map(([label, active]) => (
|
||||
<span key={label} className={`badge-cap${active ? ' active' : ''}`}>
|
||||
{label}
|
||||
</span>
|
||||
))}
|
||||
<span style={{ marginLeft: 'auto' }} className="badge-cap active">
|
||||
{provider.model_count} models
|
||||
{/* Meta */}
|
||||
<div className="provider-panel-meta">
|
||||
<span>
|
||||
Protocol: <span className="mono">{provider.protocol.replace(/_/g, ' ')}</span>
|
||||
</span>
|
||||
<span>
|
||||
Auth: <span className="mono">{provider.auth.replace(/_/g, ' ')}</span>
|
||||
</span>
|
||||
<span>
|
||||
Status: <span className="mono">{provider.status}</span>
|
||||
</span>
|
||||
{provider.env_vars.length > 0 && (
|
||||
<span>
|
||||
Env: <span className="mono">{provider.env_vars[0]}</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Meta */}
|
||||
<div className="provider-panel-meta">
|
||||
<span>
|
||||
Protocol: <span className="mono">{provider.protocol.replace(/_/g, ' ')}</span>
|
||||
</span>
|
||||
<span>
|
||||
Auth: <span className="mono">{provider.auth.replace(/_/g, ' ')}</span>
|
||||
</span>
|
||||
<span>
|
||||
Status: <span className="mono">{provider.status}</span>
|
||||
</span>
|
||||
{provider.env_vars.length > 0 && (
|
||||
<span>
|
||||
Env: <span className="mono">{provider.env_vars[0]}</span>
|
||||
</span>
|
||||
)}
|
||||
{/* Configured keys */}
|
||||
<div className="provider-panel-section">
|
||||
<div className="provider-panel-section-label">
|
||||
Configured keys ({backends.length})
|
||||
</div>
|
||||
|
||||
{/* Configured keys */}
|
||||
<div className="provider-panel-section">
|
||||
<div className="provider-panel-section-label">
|
||||
Configured keys ({backends.length})
|
||||
{backends.length === 0 && (
|
||||
<div className="provider-empty-hint">
|
||||
No keys configured. Add one below to start forwarding requests.
|
||||
</div>
|
||||
{backends.length === 0 && (
|
||||
<div className="provider-empty-hint">
|
||||
No keys configured. Add one below to start forwarding requests.
|
||||
</div>
|
||||
)}
|
||||
{backends.map((b) => (
|
||||
<BackendRow
|
||||
key={b.id}
|
||||
backend={b}
|
||||
healthStatus={healthMap.get(b.name)}
|
||||
onDelete={() => onDeleteBackend(b)}
|
||||
/>
|
||||
))}
|
||||
<AddBackendForm
|
||||
provider={provider}
|
||||
existingCount={backends.length}
|
||||
)}
|
||||
{backends.map((b) => (
|
||||
<BackendRow
|
||||
key={b.id}
|
||||
backend={b}
|
||||
healthStatus={healthMap.get(b.name)}
|
||||
onDelete={() => onDeleteBackend(b)}
|
||||
/>
|
||||
</div>
|
||||
</AdminSurface>
|
||||
</>
|
||||
))}
|
||||
<AddBackendForm
|
||||
provider={provider}
|
||||
existingCount={backends.length}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ export function BackendForm({ initial, onSuccess, onCancel }: BackendFormProps)
|
||||
return (
|
||||
<optgroup key={status} label={status.charAt(0).toUpperCase() + status.slice(1)}>
|
||||
{group.map(p => (
|
||||
<option key={p.id} value={p.id}>{p.display_name}</option>
|
||||
<option key={p.id} value={p.id}>{p.display_name} ({p.id})</option>
|
||||
))}
|
||||
</optgroup>
|
||||
)
|
||||
|
||||
@@ -7,7 +7,6 @@ import EmptyState from '../../components/shared/EmptyState'
|
||||
import ConfirmDialog from '../../components/shared/ConfirmDialog'
|
||||
import { AdminButton, AdminSurface } from '../../components/shared/Performative'
|
||||
import type { EnvImportResponse, EnvImportError } from '../../api/types'
|
||||
import { ManagedBackendsSection } from './ManagedBackendsSection'
|
||||
|
||||
const RESTART_KEY = 'env_import_pending_restart'
|
||||
|
||||
@@ -108,14 +107,13 @@ export default function Settings({ configured = true }: { configured?: boolean }
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Managed backends — always shown first */}
|
||||
<ManagedBackendsSection />
|
||||
|
||||
{/* Getting-started notice — shown when no backend is configured */}
|
||||
{/* Getting-started notice — shown when no backend is configured (no env/config-file
|
||||
signal and no managed backend). Manage backends on the Backends tab. */}
|
||||
{!configured && (
|
||||
<div style={{ marginBottom: 20, padding: '12px 16px', border: '1px solid var(--border)', borderLeft: '3px solid var(--warn)', borderRadius: 'var(--r)', fontSize: 13 }}>
|
||||
<div style={{ fontWeight: 600, marginBottom: 8 }}>No proxy configuration found — nothing to forward requests to.</div>
|
||||
<div style={{ fontWeight: 600, marginBottom: 8 }}>No backend configured — nothing to forward requests to.</div>
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
Add a backend on the <span className="mono">Backends</span> tab, or configure one via env.
|
||||
The proxy needs a backend endpoint (where to forward) and a listen port (where to accept).
|
||||
LISTEN_PORT defaults to 3000. Create a <span className="mono">.anyllm.env</span> and import it below,
|
||||
or pass it at startup: <span className="mono">anyllm-proxy --webui --env-file .anyllm.env</span>
|
||||
|
||||
@@ -48,7 +48,11 @@ export default function TrafficView() {
|
||||
<div className="chart-subtitle">Stacked over time window</div>
|
||||
</div>
|
||||
</div>
|
||||
<LineChart series={series} />
|
||||
{routes.length === 0 ? (
|
||||
<div className="empty">No routes</div>
|
||||
) : (
|
||||
<LineChart series={series} />
|
||||
)}
|
||||
</AdminSurface>
|
||||
<AdminSurface className="chart-card">
|
||||
<div className="chart-header">
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/** Copy text to the clipboard. Returns false if the browser blocks it
|
||||
* (e.g. non-secure context). localhost is treated as secure, so this works
|
||||
* for the admin UI on http://127.0.0.1. */
|
||||
export async function copyToClipboard(text: string): Promise<boolean> {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
use crate::admin::state::SharedState;
|
||||
use axum::extract::State;
|
||||
use axum::Json;
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -8,10 +10,20 @@ pub struct ProxyStatus {
|
||||
|
||||
/// GET /admin/api/status -- returns whether the proxy has a backend configured.
|
||||
/// "Configured" means the user set at least one backend-relevant env var
|
||||
/// (API key, base URL, provider choice) or pointed to a config file.
|
||||
pub async fn get_status() -> Json<ProxyStatus> {
|
||||
/// (API key, base URL, provider choice), pointed to a config file, OR created a
|
||||
/// managed backend via the admin UI (stored in SQLite). Without the managed-backend
|
||||
/// check, Settings would warn "no backend configured" while the Backends page shows
|
||||
/// a UI-added backend up.
|
||||
pub async fn get_status(State(shared): State<SharedState>) -> Json<ProxyStatus> {
|
||||
let has_managed_backend = crate::admin::state::with_db(&shared.db, |conn| {
|
||||
crate::admin::db::list_managed_backends(conn).map(|rows| !rows.is_empty())
|
||||
})
|
||||
.await
|
||||
.and_then(Result::ok)
|
||||
.unwrap_or(false);
|
||||
|
||||
Json(ProxyStatus {
|
||||
configured: is_backend_configured(),
|
||||
configured: is_backend_configured() || has_managed_backend,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user