Skip to content

test: add semantic cache e2e test suite skeleton - #3425

Merged
akshaydeo merged 6 commits into
devfrom
test-semanticcache-scaffolding
May 12, 2026
Merged

test: add semantic cache e2e test suite skeleton#3425
akshaydeo merged 6 commits into
devfrom
test-semanticcache-scaffolding

Conversation

@Pratham-Mishra04

Copy link
Copy Markdown
Collaborator

Summary

Adds a standalone end-to-end test suite for the semantic_cache plugin under tests/semanticcache. The suite validates the full caching lifecycle against a live Bifrost instance — plugin creation/teardown, cache miss/hit assertions, cross-provider behavior, streaming, and log cross-checking — without provisioning any infrastructure itself.

Changes

  • e2e_test.goTestMain entry point: loads config, initializes the report directory, checks Bifrost reachability, enforces plugin-absent precondition (with RUN_FORCE=1 auto-delete), runs all phases, and performs best-effort teardown on exit.
  • preconditions_test.go — Phase 0 checks: Bifrost reachable, OpenAI configured, optional providers (Gemini, Anthropic) present with warnings if absent.
  • http_test.go — HTTP helpers for all request types: chat completions (streaming and non-streaming), text completions, embeddings, image generation, and the Responses API. Each helper dumps full request/response bodies to the report directory for forensics.
  • plugin_test.go — Plugin lifecycle helpers (pluginCreate, pluginUpdate, pluginDelete, pluginGet) mirroring the exact wire format the UI sends to /api/plugins.
  • assert_test.go — Assertion helpers (assertMiss, assertHit, assertNoCacheDebug, assertSameCacheID, assertDifferentCacheID) plus a configurable async write-settle wait (SC_WRITE_SETTLE_MS) to account for the plugin's async PostLLMHook store write.
  • cache_test.go — Cache management helpers (clearByCacheID, clearByCacheKey) wrapping the /api/cache/clear/* endpoints.
  • logs_crosscheck_test.go — Cross-checks the persisted log row's cache_debug against the in-flight response stamp, with polling to handle Bifrost's async logging pipeline and float epsilon tolerance for JSON encoder differences.
  • fixtures_test.go — Hand-curated paraphrase pairs for Phase 2 semantic cases, designed to land well above (canonical→paraphrase) or well below (canonical→unrelated) the default 0.8 similarity threshold.
  • log_test.go — Structured per-run logging to reports/<UTC-timestamp>/run.log with optional TRAIL_SESSION_ID stamping for trail integration.
  • go.mod — Standalone module (github.com/maximhq/bifrost/tests/semanticcache), consistent with the tests/governance pattern, excluded from the repo's go.work.
  • README.md — Documents prerequisites, env vars, run commands, trail integration, and report output format.
  • .gitignore — Excludes reports/ and *.log from version control.

Notable design decisions: the suite is intentionally verify-only (no infrastructure provisioning), uses a dedicated vector store namespace (BifrostSemanticCachePluginE2E) to isolate test data, and writes full wire-level request/response artifacts per step to support post-mortem debugging without re-running.

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Documentation
  • Chore/CI

Affected areas

  • Core (Go)
  • Transports (HTTP)
  • Providers/Integrations
  • Plugins
  • UI (React)
  • Docs

How to test

Requires a running Bifrost instance with Weaviate configured, OpenAI (required), and optionally Gemini and Anthropic providers.

cd tests/semanticcache

# All phases
GOWORK=off go test -v ./...

# Single phase
GOWORK=off go test -v -run TestPhase1_DirectOnly ./...

# Auto-delete any pre-existing plugin row before run
RUN_FORCE=1 GOWORK=off go test -v ./...

# Keep plugin after run for post-mortem inspection
RUN_KEEP_PLUGIN=1 GOWORK=off go test -v ./...

Environment variables:

Variable Default Purpose
BIFROST_URL http://localhost:8080 Bifrost base URL
SC_CHAT_MODEL_OPENAI openai/gpt-4o-mini OpenAI chat model
SC_CHAT_MODEL_OPENAI_ALT openai/gpt-4o Alternate OpenAI model for cache-by-model cases
SC_EMBED_MODEL_OPENAI text-embedding-3-small Embedding model for Phase 2
SC_CHAT_MODEL_GEMINI gemini/gemini-2.5-flash Gemini chat model
SC_CHAT_MODEL_ANTHROPIC anthropic/claude-haiku-4-5 Anthropic chat model
SC_NAMESPACE BifrostSemanticCachePluginE2E Vector store namespace
SC_WRITE_SETTLE_MS 500 Async write settle wait in ms
RUN_FORCE unset 1 to delete pre-existing plugin before run
RUN_KEEP_PLUGIN unset 1 to skip teardown on exit
TRAIL_SESSION_ID unset Stamped onto every log line for trail integration

Screenshots/Recordings

N/A

Breaking changes

  • No

Related issues

N/A

Security considerations

No secrets are stored in the test suite. API keys are consumed from the existing Bifrost provider configuration and never passed directly through the test harness.

Checklist

  • I read docs/contributing/README.md and followed the guidelines
  • I added/updated tests where appropriate
  • I updated documentation where needed
  • I verified builds succeed (Go and UI)
  • I verified the CI pipeline passes locally if applicable

@coderabbitai

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: abc17d8e-a99e-4c01-ac7d-1587cca94b3a

📥 Commits

Reviewing files that changed from the base of the PR and between 197b4b6 and 3579222.

📒 Files selected for processing (12)
  • tests/semanticcache/.gitignore
  • tests/semanticcache/README.md
  • tests/semanticcache/assert_test.go
  • tests/semanticcache/cache_test.go
  • tests/semanticcache/e2e_test.go
  • tests/semanticcache/fixtures_test.go
  • tests/semanticcache/go.mod
  • tests/semanticcache/http_test.go
  • tests/semanticcache/log_test.go
  • tests/semanticcache/logs_crosscheck_test.go
  • tests/semanticcache/plugin_test.go
  • tests/semanticcache/preconditions_test.go
✅ Files skipped from review due to trivial changes (2)
  • tests/semanticcache/.gitignore
  • tests/semanticcache/README.md
🚧 Files skipped from review as they are similar to previous changes (10)
  • tests/semanticcache/preconditions_test.go
  • tests/semanticcache/go.mod
  • tests/semanticcache/logs_crosscheck_test.go
  • tests/semanticcache/e2e_test.go
  • tests/semanticcache/plugin_test.go
  • tests/semanticcache/fixtures_test.go
  • tests/semanticcache/log_test.go
  • tests/semanticcache/cache_test.go
  • tests/semanticcache/assert_test.go
  • tests/semanticcache/http_test.go

📝 Walkthrough

Summary by CodeRabbit

  • Tests

    • Added a comprehensive end-to-end semantic cache test suite with HTTP harness, logging, fixtures, cross-checks, plugin lifecycle helpers, and utilities for verifying cache hits/misses and cache-debug metadata.
    • Included helpers for clearing cache by ID/key and test precondition checks.
  • Documentation

    • New README describing test setup, required services/endpoints, environment variables, run instructions, and generated report artifacts.
  • Chores

    • Test artifacts and log files now ignored via .gitignore.

Walkthrough

This PR adds a complete end-to-end test suite for Bifrost's semantic cache functionality. It introduces a modular test infrastructure with HTTP utilities for OpenAI-compatible APIs (chat, embeddings, text completion, image generation), cache validation assertions, plugin management helpers, test fixtures, and precondition checks.

Changes

Semantic Cache End-to-End Test Suite

Layer / File(s) Summary
Module setup and test harness entry point
tests/semanticcache/go.mod, tests/semanticcache/e2e_test.go
Go module declaration and TestMain entry point that loads config, initializes logging, verifies Bifrost reachability, checks/deletes plugin preconditions (RUN_FORCE optional), runs tests, and performs teardown cleanup. Low-level HTTP helpers (doRaw, readAllSafe) and providersList decode provider responses.
Logging and reporting infrastructure
tests/semanticcache/log_test.go
Per-run timestamped reports/<timestamp>/ directory, shared run.log with mutex, structured logf formatting (timestamp, level, phase/case/step), optional TRAIL_SESSION_ID correlation, reportPath, and dumpJSON for forensic artifacts.
HTTP configuration and request execution foundation
tests/semanticcache/http_test.go (lines 1–45, 183–217)
Environment-driven config (Bifrost URL, model names, namespace, 120s HTTP client). Wire-schema types for cache debug and model payloads. doJSON marshals request bodies, POSTs to configured Bifrost endpoints, and returns status/body/headers with wrapped errors.
Chat completions API types and handlers
tests/semanticcache/http_test.go (lines 47–112, 113–182, 219–283, 482–652)
Chat request/response/message/tool types with pointer semantics, cache header builder, postChat helper that dumps request/response JSON and extracts cache_debug, and SSE streaming support parsing data: events, aggregating chunks, detecting [DONE], and deriving cache-debug metadata.
Text completion, embeddings, image generation, and /v1/responses endpoints
tests/semanticcache/http_test.go (lines 284–333, 334–381, 382–431, 432–480, 654–674)
Request/response types and postTextCompletion, postEmbedding, postImageGen, and postResponses helpers following shared execution, JSON dump, HTTP 200 enforcement, unmarshalling, and centralized logCacheDebugFields logging.
Plugin management helpers
tests/semanticcache/plugin_test.go
Plugin request/response models and envelope types, config builders for direct-only and semantic modes, and HTTP helpers: pluginGet (404-aware), pluginCreate (expects 201), pluginUpdate (PUT with enabled+config), and pluginDelete (accepts 200/404).
Cache operations and cache debug assertions
tests/semanticcache/cache_test.go, tests/semanticcache/assert_test.go
HTTP DELETE helpers clearByCacheID and clearByCacheKey. Configurable cacheWriteSettle (500ms default, overridable). Assertion helpers assertMiss, assertHit, assertNoCacheDebug, assertSameCacheID, assertDifferentCacheID, and nil-safe dereference helpers.
Test fixtures and log cross-validation
tests/semanticcache/fixtures_test.go, tests/semanticcache/logs_crosscheck_test.go
Curated paraphrase and image prompt fixtures with lookup helpers. findLogByCacheDebug polls /api/logs for matching cache_debug. assertLogMatchesResponseCacheDebug compares response vs persisted log cache_debug fields with float epsilon tolerance and pointer equality checks, failing with aggregated diffs on mismatch.
Environment preconditions (Phase 0)
tests/semanticcache/preconditions_test.go
TestPreconditions verifies Bifrost reachability (GET /api/plugins), requires openai provider, and emits PASS/WARN for optional gemini/anthropic providers via case-insensitive lookup helper.
Project documentation and gitignore
tests/semanticcache/README.md, tests/semanticcache/.gitignore
README documents prerequisites, execution commands, required GOWORK=off, environment variables, trail log/session correlation, report layout (reports/<UTC-timestamp>/), and current implementation scope. .gitignore ignores reports/ and *.log.

🎯 3 (Moderate) | ⏱️ ~25 minutes

🐰 I hopped through tests with careful art,

fixtures tucked and helpers smart,
chat streams parsed and logs aligned,
cache hits traced and crosschecks signed,
semantic cache, well-tested start.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 49.02% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'test: add semantic cache e2e test suite skeleton' clearly and concisely describes the main change: adding a new end-to-end test suite for semantic cache, matching the substantial test file additions.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering all template sections: clear summary of purpose, detailed changes for each file, correct type (Chore/CI), affected areas (Plugins), thorough testing instructions with environment variables, and complete checklist.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test-semanticcache-scaffolding

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Comment @coderabbitai help to get the list of available commands and usage tips.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@Pratham-Mishra04 Pratham-Mishra04 changed the title feat: add semantic cache e2e test suite skeleton test: add semantic cache e2e test suite skeleton May 12, 2026
@greptile-apps

greptile-apps Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 5/5

This is a new, additive test module with no changes to production code paths; safe to merge.

All changes are confined to a new standalone test module that has no effect on the main Bifrost binary. The suite is verify-only and the only finding is a defensive gap in the pre-check that would produce confusing downstream failures rather than data loss or security risk.

tests/semanticcache/e2e_test.go — the plugin pre-check fallthrough behaviour is worth a second look before Phase 2+ cases are wired in.

Important Files Changed

Filename Overview
tests/semanticcache/e2e_test.go TestMain entry point with plugin pre-check, doRaw helper, and providersList; the pre-check silently treats all non-200 responses as plugin-absent, including 5xx errors
tests/semanticcache/http_test.go HTTP helpers for all Bifrost request types; logCacheDebugFields hardcodes status=200 (previously flagged); cacheHeaders.apply idiom using a dummy http.Request is functional but unusual
tests/semanticcache/assert_test.go Assertion helpers for cache hit/miss/no-debug and cache ID equality, with configurable async write-settle wait; clean and well-documented
tests/semanticcache/plugin_test.go Plugin lifecycle helpers (create/update/delete/get) mirroring UI wire format; pluginGet decodes directly into pluginResponse while create/update use pluginEnvelope
tests/semanticcache/logs_crosscheck_test.go Cross-checks persisted log cache_debug against in-flight response stamp with polling and float epsilon tolerance; well-reasoned design
tests/semanticcache/fixtures_test.go Hand-curated paraphrase pairs with wide margin from 0.8 threshold on both sides; good design to avoid flaky boundary cases

Reviews (3): Last reviewed commit: "test(semanticcache): e2e suite scaffoldi..." | Re-trigger Greptile

Comment thread tests/semanticcache/README.md
  registered + GET /api/plugins/{name} shape parity

  - handlers/cache.go: resolver pattern; plugin looked up
  per request.
    Returns HTTP 400 'plugin is not loaded' instead of 405
  when missing.
  - server/server.go: register cache routes unconditionally
   at startup.
  - handlers/plugins.go: getPlugin returns
  buildPluginResponse so the
    shape matches POST/PUT (status field populated, not
  empty).
  - handlers/cache_test.go: updated unit tests + 2 new
  plugin-not-loaded cases.
  behavioral fixes

  - PreLLMHook: bail when no search path can serve the
  request
    (x-bf-cache-type=semantic against a direct-only plugin
  was writing
    orphan entries under a random UUID with no lookup
  path).
  - PostLLMHook: stamp cache_debug telemetry BEFORE the
  write-skip
    check, so callers retain observability on misses even
  when
    no_store=true or large-payload modes apply.
  - resolveTTL: non-positive override (0s, negative
  durations) falls
    back to plugin default, matching how Init treats
  Config.TTL=0.
  - shouldSkipCaching renamed to shouldSkipCacheWrite; unit
   tests updated.
@Pratham-Mishra04
Pratham-Mishra04 force-pushed the fix-semanticcache-plugin branch from 7951b9a to 6c1cd4b Compare May 12, 2026 16:36
@Pratham-Mishra04
Pratham-Mishra04 force-pushed the test-semanticcache-scaffolding branch from 424d4c9 to 197b4b6 Compare May 12, 2026 16:36

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/semanticcache/cache_test.go`:
- Around line 10-40: The tests currently append raw cache identifiers into
endpoint paths in clearByCacheID and clearByCacheKey which can break routes for
reserved characters; fix by URL-escaping the dynamic segment (use
net/url.PathEscape or url.PathEscape) before concatenating it into the path
passed to doJSON (e.g., replace direct use of cacheID/key with an escaped
variable), and add the necessary import for net/url if missing.

In `@tests/semanticcache/e2e_test.go`:
- Around line 41-42: The test currently ignores the error return from doRaw when
making the pre-check GET for "/api/plugins/"+pluginName; update calls to doRaw
(used in the plugin pre-checks around the GET for pluginName and similar calls
later) to capture the returned error, assert it is nil, and fail the test
immediately with a clear message (e.g., using t.Fatalf or t.Fatal) if an error
occurred, rather than allowing status to be 0 and producing misleading
downstream failures; modify both the initial plugin GET (where doRaw returns
status, body, _, _) and the later doRaw usages (the block around lines 52–56) to
handle and check the error return.
- Around line 125-133: The test incorrectly rejects a wrapped-but-empty response
because the condition in the providers-list parsing only returns when
len(wrap.Providers) > 0; change the logic in the decoding block that uses
json.Unmarshal into the variable wrap (and the field Providers) to accept the
wrapped shape even when empty (i.e., return wrap.Providers whenever
json.Unmarshal(body, &wrap) succeeds), so the fallback bare-list decoding is
only used when the wrapped decode fails.

In `@tests/semanticcache/http_test.go`:
- Around line 560-562: The test is mutating the caller's chatRequest by setting
req.Stream directly; instead create a local copy of the request before changing
the Stream field (e.g., copy := *req; copy.Stream = &streamFlag; and pass &copy
to postChatStream) so the original req remains unchanged; apply the same pattern
for the other occurrences noted (lines 567-575) to avoid leaking stream=true
into later calls.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: df3b7305-29b0-46ba-bb81-bea190c64650

📥 Commits

Reviewing files that changed from the base of the PR and between 6c1cd4b and 197b4b6.

📒 Files selected for processing (12)
  • tests/semanticcache/.gitignore
  • tests/semanticcache/README.md
  • tests/semanticcache/assert_test.go
  • tests/semanticcache/cache_test.go
  • tests/semanticcache/e2e_test.go
  • tests/semanticcache/fixtures_test.go
  • tests/semanticcache/go.mod
  • tests/semanticcache/http_test.go
  • tests/semanticcache/log_test.go
  • tests/semanticcache/logs_crosscheck_test.go
  • tests/semanticcache/plugin_test.go
  • tests/semanticcache/preconditions_test.go

Comment thread tests/semanticcache/cache_test.go
Comment thread tests/semanticcache/e2e_test.go Outdated
Comment thread tests/semanticcache/e2e_test.go Outdated
Comment thread tests/semanticcache/http_test.go
   + preconditions

  New Go test module under tests/semanticcache/:
  - HTTP helpers: chat/text
  completion/responses/embedding/image-gen +
    SSE streaming, with cache_debug parsing baked in.
  - Plugin reconfig helpers that mirror the UI wire shape
  (POST always
    sends path:"", PUT always re-sends config, rehydrate
  from response).
  - Structured logger that dumps to t.Logf +
  reports/<run>/run.log.
  - Paraphrase pair fixtures hand-curated for cosine ≥0.85
  hits and
    ≤0.6 misses (verified via TestParaphraseFixtures
  pre-flight in PR 5).
  - /api/logs cross-check: findLogByCacheDebug +
  assertLogMatchesResponseCacheDebug.
  - TestPreconditions validates Bifrost reachability +
  providers.
@Pratham-Mishra04
Pratham-Mishra04 force-pushed the test-semanticcache-scaffolding branch from 197b4b6 to 3579222 Compare May 12, 2026 19:00
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 12, 2026

akshaydeo commented May 12, 2026

Copy link
Copy Markdown
Contributor

Merge activity

  • May 12, 7:30 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • May 12, 7:43 PM UTC: @akshaydeo merged this pull request with Graphite.

@akshaydeo
akshaydeo changed the base branch from fix-semanticcache-plugin to graphite-base/3425 May 12, 2026 19:40
@akshaydeo
akshaydeo changed the base branch from graphite-base/3425 to dev May 12, 2026 19:42
@akshaydeo
akshaydeo dismissed coderabbitai[bot]’s stale review May 12, 2026 19:42

The base branch was changed.

@akshaydeo
akshaydeo requested a review from a team as a code owner May 12, 2026 19:42
@akshaydeo
akshaydeo merged commit 1967a2c into dev May 12, 2026
9 of 10 checks passed
@akshaydeo
akshaydeo deleted the test-semanticcache-scaffolding branch May 12, 2026 19:43
akshaydeo pushed a commit that referenced this pull request May 13, 2026
## Summary

Adds a standalone end-to-end test suite for the `semantic_cache` plugin under `tests/semanticcache`. The suite validates the full caching lifecycle against a live Bifrost instance — plugin creation/teardown, cache miss/hit assertions, cross-provider behavior, streaming, and log cross-checking — without provisioning any infrastructure itself.

## Changes

- **`e2e_test.go`** — `TestMain` entry point: loads config, initializes the report directory, checks Bifrost reachability, enforces plugin-absent precondition (with `RUN_FORCE=1` auto-delete), runs all phases, and performs best-effort teardown on exit.
- **`preconditions_test.go`** — Phase 0 checks: Bifrost reachable, OpenAI configured, optional providers (Gemini, Anthropic) present with warnings if absent.
- **`http_test.go`** — HTTP helpers for all request types: chat completions (streaming and non-streaming), text completions, embeddings, image generation, and the Responses API. Each helper dumps full request/response bodies to the report directory for forensics.
- **`plugin_test.go`** — Plugin lifecycle helpers (`pluginCreate`, `pluginUpdate`, `pluginDelete`, `pluginGet`) mirroring the exact wire format the UI sends to `/api/plugins`.
- **`assert_test.go`** — Assertion helpers (`assertMiss`, `assertHit`, `assertNoCacheDebug`, `assertSameCacheID`, `assertDifferentCacheID`) plus a configurable async write-settle wait (`SC_WRITE_SETTLE_MS`) to account for the plugin's async PostLLMHook store write.
- **`cache_test.go`** — Cache management helpers (`clearByCacheID`, `clearByCacheKey`) wrapping the `/api/cache/clear/*` endpoints.
- **`logs_crosscheck_test.go`** — Cross-checks the persisted log row's `cache_debug` against the in-flight response stamp, with polling to handle Bifrost's async logging pipeline and float epsilon tolerance for JSON encoder differences.
- **`fixtures_test.go`** — Hand-curated paraphrase pairs for Phase 2 semantic cases, designed to land well above (canonical→paraphrase) or well below (canonical→unrelated) the default 0.8 similarity threshold.
- **`log_test.go`** — Structured per-run logging to `reports/<UTC-timestamp>/run.log` with optional `TRAIL_SESSION_ID` stamping for trail integration.
- **`go.mod`** — Standalone module (`github.com/maximhq/bifrost/tests/semanticcache`), consistent with the `tests/governance` pattern, excluded from the repo's `go.work`.
- **`README.md`** — Documents prerequisites, env vars, run commands, trail integration, and report output format.
- **`.gitignore`** — Excludes `reports/` and `*.log` from version control.

Notable design decisions: the suite is intentionally verify-only (no infrastructure provisioning), uses a dedicated vector store namespace (`BifrostSemanticCachePluginE2E`) to isolate test data, and writes full wire-level request/response artifacts per step to support post-mortem debugging without re-running.

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [x] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

Requires a running Bifrost instance with Weaviate configured, OpenAI (required), and optionally Gemini and Anthropic providers.

```sh
cd tests/semanticcache

# All phases
GOWORK=off go test -v ./...

# Single phase
GOWORK=off go test -v -run TestPhase1_DirectOnly ./...

# Auto-delete any pre-existing plugin row before run
RUN_FORCE=1 GOWORK=off go test -v ./...

# Keep plugin after run for post-mortem inspection
RUN_KEEP_PLUGIN=1 GOWORK=off go test -v ./...
```

Environment variables:

| Variable | Default | Purpose |
|---|---|---|
| `BIFROST_URL` | `http://localhost:8080` | Bifrost base URL |
| `SC_CHAT_MODEL_OPENAI` | `openai/gpt-4o-mini` | OpenAI chat model |
| `SC_CHAT_MODEL_OPENAI_ALT` | `openai/gpt-4o` | Alternate OpenAI model for cache-by-model cases |
| `SC_EMBED_MODEL_OPENAI` | `text-embedding-3-small` | Embedding model for Phase 2 |
| `SC_CHAT_MODEL_GEMINI` | `gemini/gemini-2.5-flash` | Gemini chat model |
| `SC_CHAT_MODEL_ANTHROPIC` | `anthropic/claude-haiku-4-5` | Anthropic chat model |
| `SC_NAMESPACE` | `BifrostSemanticCachePluginE2E` | Vector store namespace |
| `SC_WRITE_SETTLE_MS` | `500` | Async write settle wait in ms |
| `RUN_FORCE` | unset | `1` to delete pre-existing plugin before run |
| `RUN_KEEP_PLUGIN` | unset | `1` to skip teardown on exit |
| `TRAIL_SESSION_ID` | unset | Stamped onto every log line for trail integration |

## Screenshots/Recordings

N/A

## Breaking changes

- [x] No

## Related issues

N/A

## Security considerations

No secrets are stored in the test suite. API keys are consumed from the existing Bifrost provider configuration and never passed directly through the test harness.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [x] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable
akshaydeo pushed a commit that referenced this pull request May 13, 2026
## Summary

Adds a standalone end-to-end test suite for the `semantic_cache` plugin under `tests/semanticcache`. The suite validates the full caching lifecycle against a live Bifrost instance — plugin creation/teardown, cache miss/hit assertions, cross-provider behavior, streaming, and log cross-checking — without provisioning any infrastructure itself.

## Changes

- **`e2e_test.go`** — `TestMain` entry point: loads config, initializes the report directory, checks Bifrost reachability, enforces plugin-absent precondition (with `RUN_FORCE=1` auto-delete), runs all phases, and performs best-effort teardown on exit.
- **`preconditions_test.go`** — Phase 0 checks: Bifrost reachable, OpenAI configured, optional providers (Gemini, Anthropic) present with warnings if absent.
- **`http_test.go`** — HTTP helpers for all request types: chat completions (streaming and non-streaming), text completions, embeddings, image generation, and the Responses API. Each helper dumps full request/response bodies to the report directory for forensics.
- **`plugin_test.go`** — Plugin lifecycle helpers (`pluginCreate`, `pluginUpdate`, `pluginDelete`, `pluginGet`) mirroring the exact wire format the UI sends to `/api/plugins`.
- **`assert_test.go`** — Assertion helpers (`assertMiss`, `assertHit`, `assertNoCacheDebug`, `assertSameCacheID`, `assertDifferentCacheID`) plus a configurable async write-settle wait (`SC_WRITE_SETTLE_MS`) to account for the plugin's async PostLLMHook store write.
- **`cache_test.go`** — Cache management helpers (`clearByCacheID`, `clearByCacheKey`) wrapping the `/api/cache/clear/*` endpoints.
- **`logs_crosscheck_test.go`** — Cross-checks the persisted log row's `cache_debug` against the in-flight response stamp, with polling to handle Bifrost's async logging pipeline and float epsilon tolerance for JSON encoder differences.
- **`fixtures_test.go`** — Hand-curated paraphrase pairs for Phase 2 semantic cases, designed to land well above (canonical→paraphrase) or well below (canonical→unrelated) the default 0.8 similarity threshold.
- **`log_test.go`** — Structured per-run logging to `reports/<UTC-timestamp>/run.log` with optional `TRAIL_SESSION_ID` stamping for trail integration.
- **`go.mod`** — Standalone module (`github.com/maximhq/bifrost/tests/semanticcache`), consistent with the `tests/governance` pattern, excluded from the repo's `go.work`.
- **`README.md`** — Documents prerequisites, env vars, run commands, trail integration, and report output format.
- **`.gitignore`** — Excludes `reports/` and `*.log` from version control.

Notable design decisions: the suite is intentionally verify-only (no infrastructure provisioning), uses a dedicated vector store namespace (`BifrostSemanticCachePluginE2E`) to isolate test data, and writes full wire-level request/response artifacts per step to support post-mortem debugging without re-running.

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [x] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

Requires a running Bifrost instance with Weaviate configured, OpenAI (required), and optionally Gemini and Anthropic providers.

```sh
cd tests/semanticcache

# All phases
GOWORK=off go test -v ./...

# Single phase
GOWORK=off go test -v -run TestPhase1_DirectOnly ./...

# Auto-delete any pre-existing plugin row before run
RUN_FORCE=1 GOWORK=off go test -v ./...

# Keep plugin after run for post-mortem inspection
RUN_KEEP_PLUGIN=1 GOWORK=off go test -v ./...
```

Environment variables:

| Variable | Default | Purpose |
|---|---|---|
| `BIFROST_URL` | `http://localhost:8080` | Bifrost base URL |
| `SC_CHAT_MODEL_OPENAI` | `openai/gpt-4o-mini` | OpenAI chat model |
| `SC_CHAT_MODEL_OPENAI_ALT` | `openai/gpt-4o` | Alternate OpenAI model for cache-by-model cases |
| `SC_EMBED_MODEL_OPENAI` | `text-embedding-3-small` | Embedding model for Phase 2 |
| `SC_CHAT_MODEL_GEMINI` | `gemini/gemini-2.5-flash` | Gemini chat model |
| `SC_CHAT_MODEL_ANTHROPIC` | `anthropic/claude-haiku-4-5` | Anthropic chat model |
| `SC_NAMESPACE` | `BifrostSemanticCachePluginE2E` | Vector store namespace |
| `SC_WRITE_SETTLE_MS` | `500` | Async write settle wait in ms |
| `RUN_FORCE` | unset | `1` to delete pre-existing plugin before run |
| `RUN_KEEP_PLUGIN` | unset | `1` to skip teardown on exit |
| `TRAIL_SESSION_ID` | unset | Stamped onto every log line for trail integration |

## Screenshots/Recordings

N/A

## Breaking changes

- [x] No

## Related issues

N/A

## Security considerations

No secrets are stored in the test suite. API keys are consumed from the existing Bifrost provider configuration and never passed directly through the test harness.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [x] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable
akshaydeo pushed a commit that referenced this pull request May 14, 2026
## Summary

Adds a standalone end-to-end test suite for the `semantic_cache` plugin under `tests/semanticcache`. The suite validates the full caching lifecycle against a live Bifrost instance — plugin creation/teardown, cache miss/hit assertions, cross-provider behavior, streaming, and log cross-checking — without provisioning any infrastructure itself.

## Changes

- **`e2e_test.go`** — `TestMain` entry point: loads config, initializes the report directory, checks Bifrost reachability, enforces plugin-absent precondition (with `RUN_FORCE=1` auto-delete), runs all phases, and performs best-effort teardown on exit.
- **`preconditions_test.go`** — Phase 0 checks: Bifrost reachable, OpenAI configured, optional providers (Gemini, Anthropic) present with warnings if absent.
- **`http_test.go`** — HTTP helpers for all request types: chat completions (streaming and non-streaming), text completions, embeddings, image generation, and the Responses API. Each helper dumps full request/response bodies to the report directory for forensics.
- **`plugin_test.go`** — Plugin lifecycle helpers (`pluginCreate`, `pluginUpdate`, `pluginDelete`, `pluginGet`) mirroring the exact wire format the UI sends to `/api/plugins`.
- **`assert_test.go`** — Assertion helpers (`assertMiss`, `assertHit`, `assertNoCacheDebug`, `assertSameCacheID`, `assertDifferentCacheID`) plus a configurable async write-settle wait (`SC_WRITE_SETTLE_MS`) to account for the plugin's async PostLLMHook store write.
- **`cache_test.go`** — Cache management helpers (`clearByCacheID`, `clearByCacheKey`) wrapping the `/api/cache/clear/*` endpoints.
- **`logs_crosscheck_test.go`** — Cross-checks the persisted log row's `cache_debug` against the in-flight response stamp, with polling to handle Bifrost's async logging pipeline and float epsilon tolerance for JSON encoder differences.
- **`fixtures_test.go`** — Hand-curated paraphrase pairs for Phase 2 semantic cases, designed to land well above (canonical→paraphrase) or well below (canonical→unrelated) the default 0.8 similarity threshold.
- **`log_test.go`** — Structured per-run logging to `reports/<UTC-timestamp>/run.log` with optional `TRAIL_SESSION_ID` stamping for trail integration.
- **`go.mod`** — Standalone module (`github.com/maximhq/bifrost/tests/semanticcache`), consistent with the `tests/governance` pattern, excluded from the repo's `go.work`.
- **`README.md`** — Documents prerequisites, env vars, run commands, trail integration, and report output format.
- **`.gitignore`** — Excludes `reports/` and `*.log` from version control.

Notable design decisions: the suite is intentionally verify-only (no infrastructure provisioning), uses a dedicated vector store namespace (`BifrostSemanticCachePluginE2E`) to isolate test data, and writes full wire-level request/response artifacts per step to support post-mortem debugging without re-running.

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [x] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

Requires a running Bifrost instance with Weaviate configured, OpenAI (required), and optionally Gemini and Anthropic providers.

```sh
cd tests/semanticcache

# All phases
GOWORK=off go test -v ./...

# Single phase
GOWORK=off go test -v -run TestPhase1_DirectOnly ./...

# Auto-delete any pre-existing plugin row before run
RUN_FORCE=1 GOWORK=off go test -v ./...

# Keep plugin after run for post-mortem inspection
RUN_KEEP_PLUGIN=1 GOWORK=off go test -v ./...
```

Environment variables:

| Variable | Default | Purpose |
|---|---|---|
| `BIFROST_URL` | `http://localhost:8080` | Bifrost base URL |
| `SC_CHAT_MODEL_OPENAI` | `openai/gpt-4o-mini` | OpenAI chat model |
| `SC_CHAT_MODEL_OPENAI_ALT` | `openai/gpt-4o` | Alternate OpenAI model for cache-by-model cases |
| `SC_EMBED_MODEL_OPENAI` | `text-embedding-3-small` | Embedding model for Phase 2 |
| `SC_CHAT_MODEL_GEMINI` | `gemini/gemini-2.5-flash` | Gemini chat model |
| `SC_CHAT_MODEL_ANTHROPIC` | `anthropic/claude-haiku-4-5` | Anthropic chat model |
| `SC_NAMESPACE` | `BifrostSemanticCachePluginE2E` | Vector store namespace |
| `SC_WRITE_SETTLE_MS` | `500` | Async write settle wait in ms |
| `RUN_FORCE` | unset | `1` to delete pre-existing plugin before run |
| `RUN_KEEP_PLUGIN` | unset | `1` to skip teardown on exit |
| `TRAIL_SESSION_ID` | unset | Stamped onto every log line for trail integration |

## Screenshots/Recordings

N/A

## Breaking changes

- [x] No

## Related issues

N/A

## Security considerations

No secrets are stored in the test suite. API keys are consumed from the existing Bifrost provider configuration and never passed directly through the test harness.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [x] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable
akshaydeo pushed a commit that referenced this pull request May 15, 2026
## Summary

Adds a standalone end-to-end test suite for the `semantic_cache` plugin under `tests/semanticcache`. The suite validates the full caching lifecycle against a live Bifrost instance — plugin creation/teardown, cache miss/hit assertions, cross-provider behavior, streaming, and log cross-checking — without provisioning any infrastructure itself.

## Changes

- **`e2e_test.go`** — `TestMain` entry point: loads config, initializes the report directory, checks Bifrost reachability, enforces plugin-absent precondition (with `RUN_FORCE=1` auto-delete), runs all phases, and performs best-effort teardown on exit.
- **`preconditions_test.go`** — Phase 0 checks: Bifrost reachable, OpenAI configured, optional providers (Gemini, Anthropic) present with warnings if absent.
- **`http_test.go`** — HTTP helpers for all request types: chat completions (streaming and non-streaming), text completions, embeddings, image generation, and the Responses API. Each helper dumps full request/response bodies to the report directory for forensics.
- **`plugin_test.go`** — Plugin lifecycle helpers (`pluginCreate`, `pluginUpdate`, `pluginDelete`, `pluginGet`) mirroring the exact wire format the UI sends to `/api/plugins`.
- **`assert_test.go`** — Assertion helpers (`assertMiss`, `assertHit`, `assertNoCacheDebug`, `assertSameCacheID`, `assertDifferentCacheID`) plus a configurable async write-settle wait (`SC_WRITE_SETTLE_MS`) to account for the plugin's async PostLLMHook store write.
- **`cache_test.go`** — Cache management helpers (`clearByCacheID`, `clearByCacheKey`) wrapping the `/api/cache/clear/*` endpoints.
- **`logs_crosscheck_test.go`** — Cross-checks the persisted log row's `cache_debug` against the in-flight response stamp, with polling to handle Bifrost's async logging pipeline and float epsilon tolerance for JSON encoder differences.
- **`fixtures_test.go`** — Hand-curated paraphrase pairs for Phase 2 semantic cases, designed to land well above (canonical→paraphrase) or well below (canonical→unrelated) the default 0.8 similarity threshold.
- **`log_test.go`** — Structured per-run logging to `reports/<UTC-timestamp>/run.log` with optional `TRAIL_SESSION_ID` stamping for trail integration.
- **`go.mod`** — Standalone module (`github.com/maximhq/bifrost/tests/semanticcache`), consistent with the `tests/governance` pattern, excluded from the repo's `go.work`.
- **`README.md`** — Documents prerequisites, env vars, run commands, trail integration, and report output format.
- **`.gitignore`** — Excludes `reports/` and `*.log` from version control.

Notable design decisions: the suite is intentionally verify-only (no infrastructure provisioning), uses a dedicated vector store namespace (`BifrostSemanticCachePluginE2E`) to isolate test data, and writes full wire-level request/response artifacts per step to support post-mortem debugging without re-running.

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [x] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

Requires a running Bifrost instance with Weaviate configured, OpenAI (required), and optionally Gemini and Anthropic providers.

```sh
cd tests/semanticcache

# All phases
GOWORK=off go test -v ./...

# Single phase
GOWORK=off go test -v -run TestPhase1_DirectOnly ./...

# Auto-delete any pre-existing plugin row before run
RUN_FORCE=1 GOWORK=off go test -v ./...

# Keep plugin after run for post-mortem inspection
RUN_KEEP_PLUGIN=1 GOWORK=off go test -v ./...
```

Environment variables:

| Variable | Default | Purpose |
|---|---|---|
| `BIFROST_URL` | `http://localhost:8080` | Bifrost base URL |
| `SC_CHAT_MODEL_OPENAI` | `openai/gpt-4o-mini` | OpenAI chat model |
| `SC_CHAT_MODEL_OPENAI_ALT` | `openai/gpt-4o` | Alternate OpenAI model for cache-by-model cases |
| `SC_EMBED_MODEL_OPENAI` | `text-embedding-3-small` | Embedding model for Phase 2 |
| `SC_CHAT_MODEL_GEMINI` | `gemini/gemini-2.5-flash` | Gemini chat model |
| `SC_CHAT_MODEL_ANTHROPIC` | `anthropic/claude-haiku-4-5` | Anthropic chat model |
| `SC_NAMESPACE` | `BifrostSemanticCachePluginE2E` | Vector store namespace |
| `SC_WRITE_SETTLE_MS` | `500` | Async write settle wait in ms |
| `RUN_FORCE` | unset | `1` to delete pre-existing plugin before run |
| `RUN_KEEP_PLUGIN` | unset | `1` to skip teardown on exit |
| `TRAIL_SESSION_ID` | unset | Stamped onto every log line for trail integration |

## Screenshots/Recordings

N/A

## Breaking changes

- [x] No

## Related issues

N/A

## Security considerations

No secrets are stored in the test suite. API keys are consumed from the existing Bifrost provider configuration and never passed directly through the test harness.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [x] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable
akshaydeo pushed a commit that referenced this pull request May 15, 2026
## Summary

Adds a standalone end-to-end test suite for the `semantic_cache` plugin under `tests/semanticcache`. The suite validates the full caching lifecycle against a live Bifrost instance — plugin creation/teardown, cache miss/hit assertions, cross-provider behavior, streaming, and log cross-checking — without provisioning any infrastructure itself.

## Changes

- **`e2e_test.go`** — `TestMain` entry point: loads config, initializes the report directory, checks Bifrost reachability, enforces plugin-absent precondition (with `RUN_FORCE=1` auto-delete), runs all phases, and performs best-effort teardown on exit.
- **`preconditions_test.go`** — Phase 0 checks: Bifrost reachable, OpenAI configured, optional providers (Gemini, Anthropic) present with warnings if absent.
- **`http_test.go`** — HTTP helpers for all request types: chat completions (streaming and non-streaming), text completions, embeddings, image generation, and the Responses API. Each helper dumps full request/response bodies to the report directory for forensics.
- **`plugin_test.go`** — Plugin lifecycle helpers (`pluginCreate`, `pluginUpdate`, `pluginDelete`, `pluginGet`) mirroring the exact wire format the UI sends to `/api/plugins`.
- **`assert_test.go`** — Assertion helpers (`assertMiss`, `assertHit`, `assertNoCacheDebug`, `assertSameCacheID`, `assertDifferentCacheID`) plus a configurable async write-settle wait (`SC_WRITE_SETTLE_MS`) to account for the plugin's async PostLLMHook store write.
- **`cache_test.go`** — Cache management helpers (`clearByCacheID`, `clearByCacheKey`) wrapping the `/api/cache/clear/*` endpoints.
- **`logs_crosscheck_test.go`** — Cross-checks the persisted log row's `cache_debug` against the in-flight response stamp, with polling to handle Bifrost's async logging pipeline and float epsilon tolerance for JSON encoder differences.
- **`fixtures_test.go`** — Hand-curated paraphrase pairs for Phase 2 semantic cases, designed to land well above (canonical→paraphrase) or well below (canonical→unrelated) the default 0.8 similarity threshold.
- **`log_test.go`** — Structured per-run logging to `reports/<UTC-timestamp>/run.log` with optional `TRAIL_SESSION_ID` stamping for trail integration.
- **`go.mod`** — Standalone module (`github.com/maximhq/bifrost/tests/semanticcache`), consistent with the `tests/governance` pattern, excluded from the repo's `go.work`.
- **`README.md`** — Documents prerequisites, env vars, run commands, trail integration, and report output format.
- **`.gitignore`** — Excludes `reports/` and `*.log` from version control.

Notable design decisions: the suite is intentionally verify-only (no infrastructure provisioning), uses a dedicated vector store namespace (`BifrostSemanticCachePluginE2E`) to isolate test data, and writes full wire-level request/response artifacts per step to support post-mortem debugging without re-running.

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [x] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

Requires a running Bifrost instance with Weaviate configured, OpenAI (required), and optionally Gemini and Anthropic providers.

```sh
cd tests/semanticcache

# All phases
GOWORK=off go test -v ./...

# Single phase
GOWORK=off go test -v -run TestPhase1_DirectOnly ./...

# Auto-delete any pre-existing plugin row before run
RUN_FORCE=1 GOWORK=off go test -v ./...

# Keep plugin after run for post-mortem inspection
RUN_KEEP_PLUGIN=1 GOWORK=off go test -v ./...
```

Environment variables:

| Variable | Default | Purpose |
|---|---|---|
| `BIFROST_URL` | `http://localhost:8080` | Bifrost base URL |
| `SC_CHAT_MODEL_OPENAI` | `openai/gpt-4o-mini` | OpenAI chat model |
| `SC_CHAT_MODEL_OPENAI_ALT` | `openai/gpt-4o` | Alternate OpenAI model for cache-by-model cases |
| `SC_EMBED_MODEL_OPENAI` | `text-embedding-3-small` | Embedding model for Phase 2 |
| `SC_CHAT_MODEL_GEMINI` | `gemini/gemini-2.5-flash` | Gemini chat model |
| `SC_CHAT_MODEL_ANTHROPIC` | `anthropic/claude-haiku-4-5` | Anthropic chat model |
| `SC_NAMESPACE` | `BifrostSemanticCachePluginE2E` | Vector store namespace |
| `SC_WRITE_SETTLE_MS` | `500` | Async write settle wait in ms |
| `RUN_FORCE` | unset | `1` to delete pre-existing plugin before run |
| `RUN_KEEP_PLUGIN` | unset | `1` to skip teardown on exit |
| `TRAIL_SESSION_ID` | unset | Stamped onto every log line for trail integration |

## Screenshots/Recordings

N/A

## Breaking changes

- [x] No

## Related issues

N/A

## Security considerations

No secrets are stored in the test suite. API keys are consumed from the existing Bifrost provider configuration and never passed directly through the test harness.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [x] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable
akshaydeo pushed a commit that referenced this pull request May 20, 2026
## Summary

Adds a standalone end-to-end test suite for the `semantic_cache` plugin under `tests/semanticcache`. The suite validates the full caching lifecycle against a live Bifrost instance — plugin creation/teardown, cache miss/hit assertions, cross-provider behavior, streaming, and log cross-checking — without provisioning any infrastructure itself.

## Changes

- **`e2e_test.go`** — `TestMain` entry point: loads config, initializes the report directory, checks Bifrost reachability, enforces plugin-absent precondition (with `RUN_FORCE=1` auto-delete), runs all phases, and performs best-effort teardown on exit.
- **`preconditions_test.go`** — Phase 0 checks: Bifrost reachable, OpenAI configured, optional providers (Gemini, Anthropic) present with warnings if absent.
- **`http_test.go`** — HTTP helpers for all request types: chat completions (streaming and non-streaming), text completions, embeddings, image generation, and the Responses API. Each helper dumps full request/response bodies to the report directory for forensics.
- **`plugin_test.go`** — Plugin lifecycle helpers (`pluginCreate`, `pluginUpdate`, `pluginDelete`, `pluginGet`) mirroring the exact wire format the UI sends to `/api/plugins`.
- **`assert_test.go`** — Assertion helpers (`assertMiss`, `assertHit`, `assertNoCacheDebug`, `assertSameCacheID`, `assertDifferentCacheID`) plus a configurable async write-settle wait (`SC_WRITE_SETTLE_MS`) to account for the plugin's async PostLLMHook store write.
- **`cache_test.go`** — Cache management helpers (`clearByCacheID`, `clearByCacheKey`) wrapping the `/api/cache/clear/*` endpoints.
- **`logs_crosscheck_test.go`** — Cross-checks the persisted log row's `cache_debug` against the in-flight response stamp, with polling to handle Bifrost's async logging pipeline and float epsilon tolerance for JSON encoder differences.
- **`fixtures_test.go`** — Hand-curated paraphrase pairs for Phase 2 semantic cases, designed to land well above (canonical→paraphrase) or well below (canonical→unrelated) the default 0.8 similarity threshold.
- **`log_test.go`** — Structured per-run logging to `reports/<UTC-timestamp>/run.log` with optional `TRAIL_SESSION_ID` stamping for trail integration.
- **`go.mod`** — Standalone module (`github.com/maximhq/bifrost/tests/semanticcache`), consistent with the `tests/governance` pattern, excluded from the repo's `go.work`.
- **`README.md`** — Documents prerequisites, env vars, run commands, trail integration, and report output format.
- **`.gitignore`** — Excludes `reports/` and `*.log` from version control.

Notable design decisions: the suite is intentionally verify-only (no infrastructure provisioning), uses a dedicated vector store namespace (`BifrostSemanticCachePluginE2E`) to isolate test data, and writes full wire-level request/response artifacts per step to support post-mortem debugging without re-running.

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [x] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

Requires a running Bifrost instance with Weaviate configured, OpenAI (required), and optionally Gemini and Anthropic providers.

```sh
cd tests/semanticcache

# All phases
GOWORK=off go test -v ./...

# Single phase
GOWORK=off go test -v -run TestPhase1_DirectOnly ./...

# Auto-delete any pre-existing plugin row before run
RUN_FORCE=1 GOWORK=off go test -v ./...

# Keep plugin after run for post-mortem inspection
RUN_KEEP_PLUGIN=1 GOWORK=off go test -v ./...
```

Environment variables:

| Variable | Default | Purpose |
|---|---|---|
| `BIFROST_URL` | `http://localhost:8080` | Bifrost base URL |
| `SC_CHAT_MODEL_OPENAI` | `openai/gpt-4o-mini` | OpenAI chat model |
| `SC_CHAT_MODEL_OPENAI_ALT` | `openai/gpt-4o` | Alternate OpenAI model for cache-by-model cases |
| `SC_EMBED_MODEL_OPENAI` | `text-embedding-3-small` | Embedding model for Phase 2 |
| `SC_CHAT_MODEL_GEMINI` | `gemini/gemini-2.5-flash` | Gemini chat model |
| `SC_CHAT_MODEL_ANTHROPIC` | `anthropic/claude-haiku-4-5` | Anthropic chat model |
| `SC_NAMESPACE` | `BifrostSemanticCachePluginE2E` | Vector store namespace |
| `SC_WRITE_SETTLE_MS` | `500` | Async write settle wait in ms |
| `RUN_FORCE` | unset | `1` to delete pre-existing plugin before run |
| `RUN_KEEP_PLUGIN` | unset | `1` to skip teardown on exit |
| `TRAIL_SESSION_ID` | unset | Stamped onto every log line for trail integration |

## Screenshots/Recordings

N/A

## Breaking changes

- [x] No

## Related issues

N/A

## Security considerations

No secrets are stored in the test suite. API keys are consumed from the existing Bifrost provider configuration and never passed directly through the test harness.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [x] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable
akshaydeo added a commit that referenced this pull request May 20, 2026
* feat: add granular RBAC checks for API keys, inference, metrics, and filter inaccessible sidebar items (#3295)

This PR improves RBAC granularity in the sidebar by introducing dedicated resource types for `APIKeys`, `Inference`, and `Metrics`, and fixes sidebar visibility logic so that items and groups are hidden when the user lacks access rather than relying on broader, less specific permissions.

- Added three new `RbacResource` enum values: `APIKeys`, `Inference`, and `Metrics` to the fallback RBAC context.
- The API Keys sidebar item now gates access via the new `hasAPIKeyAccess` (`RbacResource.APIKeys`) check instead of the generic `hasSettingsAccess`.
- The MCP Logs sidebar item now correctly gates access via `hasMCPGatewayAccess` instead of the unrelated `hasLogsAccess`.
- Introduced an `accessibleItems` memoized computation that filters out sidebar items and entire groups whose sub-items are all inaccessible, ensuring users never see empty navigation sections. Previously, access filtering only happened during search.
- Removed unused imports (`PanelLeft`, `PanelRight`, `cn`).

- [ ] Bug fix
- [x] Feature
- [x] Refactor
- [ ] Documentation
- [ ] Chore/CI

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [x] UI (React)
- [ ] Docs

1. Log in as a user with restricted RBAC permissions that exclude `APIKeys` and/or `Settings`.
2. Verify the API Keys entry under the Config section is hidden for users without `APIKeys` view permission.
3. Verify the MCP Logs entry is hidden for users without `MCPGateway` view permission.
4. Verify that sidebar groups with no accessible sub-items are hidden entirely rather than showing an empty group.
5. Verify that users with full access see no change in sidebar behavior.

```sh
cd ui
pnpm i || npm i
pnpm build || npm run build
```

_Add before/after screenshots showing sidebar items hidden for restricted users._

- [ ] Yes
- [x] No

_Link related issues here._

Access control checks for API Keys management are now scoped to a dedicated `APIKeys` RBAC resource rather than the broader `Settings` resource, reducing the risk of unintended access to key management for users who have settings visibility but should not manage API keys.

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* fix: hide delete log button instead of disabling it when user lacks delete access (#3314)

The delete button in log tables was always rendered (just disabled) for users without delete access. This PR hides the actions column entirely when the user lacks delete permissions, and fixes the RBAC resource check for MCP logs to use the correct `MCPGateway` resource instead of `Logs`.

- The actions column in both the workspace logs and MCP logs tables is now conditionally included in the column definitions only when `hasDeleteAccess` is `true`, rather than always rendering a disabled button.
- The delete button styling was updated to use more visible destructive colors (`text-destructive/60 border-destructive/60`) instead of the previous muted secondary foreground styles.
- The RBAC resource used to gate delete access on the MCP logs page was corrected from `RbacResource.Logs` to `RbacResource.MCPGateway`.

- [x] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [x] UI (React)
- [ ] Docs

1. Log in as a user **without** delete access on Logs or MCPGateway resources.
2. Navigate to the workspace logs page and the MCP logs page.
3. Verify the delete button/column is not visible.
4. Log in as a user **with** delete access.
5. Verify the delete button appears and is functional.

```sh
cd ui
pnpm i
pnpm test
pnpm build
```

Before: Delete button rendered but disabled for users without access.
After: Delete column is hidden entirely for users without delete access.

- [ ] Yes
- [x] No

The RBAC fix ensures MCP log deletion is gated on the correct `MCPGateway` resource permission, preventing users with only `Logs` delete access from incorrectly being granted delete access to MCP logs.

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* feat: add `MCPLogs` RBAC resource and enforce access control on MCP logs route and sidebar (#3316)

Introduces a dedicated `MCPLogs` RBAC resource, decoupling MCP log access control from the `MCPGateway` resource. This allows permissions for viewing and deleting MCP logs to be managed independently from gateway-level permissions.

- Added `MCPLogs` as a new `RbacResource` enum value in the fallback RBAC context.
- The MCP Logs route now checks `MCPLogs` view permission and renders a `NoPermissionView` when access is denied, rather than rendering the page unconditionally.
- Delete access on the MCP Logs page now checks `RbacResource.MCPLogs` instead of `RbacResource.MCPGateway`.
- The sidebar MCP Logs entry now uses `hasMCPLogsAccess` (derived from `RbacResource.MCPLogs`) to control visibility, rather than reusing `hasMCPGatewayAccess`.

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [x] UI (React)
- [ ] Docs

1. Configure a role that has `MCPGateway` access but **no** `MCPLogs` access.
2. Log in as a user with that role and navigate to the MCP Logs page — the `NoPermissionView` should be displayed and the sidebar entry should be hidden.
3. Grant the role `MCPLogs` view access and confirm the page and sidebar entry become accessible.
4. Verify that delete functionality on the MCP Logs page is gated by `MCPLogs` delete permission independently of `MCPGateway` delete permission.

```sh
cd ui
pnpm i || npm i
pnpm test || npm test
pnpm build || npm run build
```

N/A

- [x] Yes
- [ ] No

Any role configuration that previously relied on `MCPGateway` permissions to grant access to MCP Logs will need to be updated to explicitly grant `MCPLogs` permissions.

N/A

Access to MCP log data (which may contain sensitive tool execution details) is now enforced by a dedicated RBAC resource, reducing the risk of unintended access through overly broad `MCPGateway` permissions.

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* fix: replace unsafe inline jsonb cast with `bifrost_safe_jsonb` PL/pgSQL helper to prevent malformed JSON from aborting list queries (#3407)

## Summary

The `/api/logs` list query was aborting entirely when a single row contained malformed JSON in `input_history` or `responses_input_history`. The previous inline guard only checked the first character before casting to `jsonb`, so rows that appeared array-shaped but contained malformed JSON (unterminated structures, trailing commas, unpaired UTF-16 surrogates, `\u0000` escapes, etc.) would trigger a `22P02`/`22P05` error and kill the entire response. This PR fixes that by introducing a PL/pgSQL helper function (`bifrost_safe_jsonb`) that wraps the cast in an `EXCEPTION` block and falls back to returning the raw text on any parse failure.

## Changes

- Added a new migration `migrationAddSafeJsonbFunction` that installs the `bifrost_safe_jsonb(text)` PL/pgSQL function on Postgres. The function validates the input, attempts the `jsonb` cast inside an `EXCEPTION` block, and returns the last array element on success or the raw text on any failure.
- Replaced the multi-condition inline `CASE` guards in `listSelectColumns` for Postgres with calls to `bifrost_safe_jsonb`, simplifying the SQL and correctly handling all malformed-JSON edge cases that the previous character-check approach missed.
- For SQLite, added `json_valid()`, `json_type()`, and `json_array_length()` guards to the `CASE` expressions to prevent extraction attempts on invalid or empty arrays.
- Added `safe_jsonb_test.go` covering both the SQLite and Postgres dialect branches of `listSelectColumns`, as well as direct invocation of `bifrost_safe_jsonb` across all relevant edge cases (malformed structures, surrogate pairs, `\u0000` escapes, non-array values, SQL `NULL`).

## Type of change

- [x] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
cd framework && docker compose up -d postgres

go test ./framework/logstore/ -run 'MalformedInputHistory|BifrostSafeJsonb' -count=1 -v
```

Insert a row into the logs table with a malformed JSON value in `input_history` (e.g., `[{"key": "val"` — unterminated) and verify that a call to the list endpoint returns successfully without a 500 error, with the malformed row's `input_history` returned as raw text rather than aborting the query.

## Test Coverage

### `TestSearchLogs_MalformedInputHistory_{SQLite,Postgres}` — end-to-end list query

| # | Case | Column | Payload shape | Pre-fix behavior | Path exercised |
| --- | --- | --- | --- | --- | --- |
| 1 | `unterminated_object_in_array` | `input_history` | `[{"role":"user","content":"hi"` | 22P02 aborts query | EXCEPTION fallback |
| 2 | `garbage_after_bracket` | `input_history` | `[abc, not json]` | 22P02 aborts query | EXCEPTION fallback |
| 3 | `trailing_comma` | `input_history` | `[{"role":"user","content":"hi"},]` | 22P02 aborts query | EXCEPTION fallback |
| 4 | `unclosed_array_only` | `input_history` | `[` | 22P02 aborts query | EXCEPTION fallback |
| 5 | `open_bracket_then_brace_unclosed` | `input_history` | `[{` | 22P02 aborts query | EXCEPTION fallback |
| 6 | `nan_value_not_valid_json` | `input_history` | `[NaN]` | 22P02 aborts query | EXCEPTION fallback |
| 7 | `infinity_value_not_valid_json` | `input_history` | `[Infinity]` | 22P02 aborts query | EXCEPTION fallback |
| 8 | `unpaired_high_surrogate` | `input_history` | `[{"...":"bad \uD800 surrogate"}]` | 22P05 aborts query | EXCEPTION fallback |
| 9 | `unpaired_low_surrogate` | `input_history` | `[{"...":"bad \uDC00 low"}]` | 22P05 aborts query | EXCEPTION fallback |
| 10 | `bad_surrogate_pair_high_then_ascii` | `input_history` | `[{"c":"\uD800A"}]` | 22P05 aborts query | EXCEPTION fallback |
| 11 | `u0000_escape_inside_string` | `input_history` | `[{"...":"null byte � here"}]` | 22P05 aborts query | EXCEPTION fallback |
| 12 | `literal_backslash_u0000_valid_jsonb` | `input_history` | `[{"...":"... \\u0000 literal"}]` | OK (degraded to raw by old guard) | Fast path, last-element extraction |
| 13 | `single_element_array` | `input_history` | `[{"role":"user","content":"only one"}]` | OK | Fast path |
| 14 | `array_of_primitives` | `input_history` | `[1,2,3]` | OK | Fast path |
| 15 | `array_with_null_last_element` | `input_history` | `[{...}, null]` | OK | Fast path |
| 16 | `deeply_nested_valid` | `input_history` | `[{"role":"user","content":{"nested":{"deep":{"value":42}}}}]` | OK | Fast path |
| 17 | `unicode_emoji_content` | `input_history` | `[{"...":"hello 🎉 world ✨"}]` | OK | Fast path |
| 18 | `large_valid_array` | `input_history` | 1001-element array | OK | Fast path at scale |
| 19 | `leading_whitespace_then_array` | `input_history` | `   [\t{...}]` | OK | `btrim` + fast path |
| 20 | `top_level_object_not_array` | `input_history` | `{"not":"an array"}` | OK | Non-array fall-through |
| 21 | `null_literal` | `input_history` | `null` | OK | Non-array fall-through |
| 22 | `whitespace_only` | `input_history` | `"   \t  "` | OK | Empty-after-btrim fall-through |
| 23 | `realtime_turn_malformed_passthrough` | `input_history` (object_type=`realtime.turn`) | `[{"role":"user"` | OK (outer CASE bypassed safe fn) | Realtime-turn bypass branch |
| 24 | `malformed_responses_input_history` | `responses_input_history` | `[{"role":"user"` | 22P02 aborts query | Mirror column, EXCEPTION fallback |
| 25 | `valid_responses_input_history` | `responses_input_history` | `[{...},{...}]` | OK | Mirror column, fast path |

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

[https://github.com/maximhq/bifrost/issues/3255](https://github.com/maximhq/bifrost/issues/3255#issuecomment-4427506449)

## Security considerations

None. The function is `IMMUTABLE` and operates only on text values already stored in the database. No new inputs are exposed.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* feat: add required headers input to prompt playground settings panel (#3412)

## Summary

Adds support for server-configured required request headers in the prompt playground. When the server specifies `required_headers` in its client config, users can now provide values for those headers directly in the settings panel, and they are forwarded with every chat completion request.

## Changes

- Added `customHeaders` state and `requiredHeaders` derived from the core config's `client_config.required_headers` to the `PromptContext`, keeping header keys in sync with the server config while preserving user-entered values.
- Exposed a "Required Headers" section in the settings panel that renders an input field for each required header name when any are configured.
- Extended `ExecutionConfig` in the executor to accept `customHeaders`, which are merged into the fetch request headers (skipping any entries with empty names or values).
- Passed `customHeaders` through both `handleSubmit` and `handleSubmitToolResult` execution paths and included it in their respective `useCallback` dependency arrays.

## Type of change

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [x] UI (React)
- [ ] Docs

## How to test

1. Configure `required_headers` in the server's client config (e.g., `["X-My-Custom-Header"]`).
2. Open the prompt playground and navigate to the settings panel.
3. Verify a "Required Headers" section appears with an input for each configured header name.
4. Enter a value for each header and send a chat completion request.
5. Confirm the header is present in the outgoing request.
6. Remove a header from the server config and verify it disappears from the UI without affecting other header values.

```sh
cd ui
pnpm i || npm i
pnpm test || npm test
pnpm build || npm run build
```

## Screenshots/Recordings

_Add before/after screenshots of the settings panel showing the new Required Headers section._

## Breaking changes

- [x] No

## Related issues

## Security considerations

Header values are entered by the user and sent only to the configured backend endpoint. Empty header names or values are explicitly skipped before being added to the request, preventing accidental forwarding of blank headers. Users should be cautious not to enter sensitive credentials unless the connection to the server is secured.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* fix: skip pagination clamp for virtual keys export requests (#3416)

## Summary

When exporting virtual keys, pagination clamping was being applied unnecessarily, which could interfere with retrieving the full dataset. This PR skips the pagination limit/offset clamping when the export flag is set, while still ensuring the offset is non-negative.

## Changes

- Pagination clamping via `ClampPaginationParams` is now bypassed when `params.Export` is `true`, allowing exports to retrieve data without artificially constrained limits
- A minimal guard ensures `params.Offset` is still set to `0` if negative during an export request

## Type of change

- [x] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [x] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

Trigger a virtual keys export request and verify that all keys are returned without being truncated by pagination limits. Compare the export result count against the total number of virtual keys in the system.

```sh
go test ./...
```

## Screenshots/Recordings

N/A

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

https://github.com/maximhq/bifrost/issues/3414

## Security considerations

No additional security implications. Export access is still gated by existing authentication and authorization checks.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* chore: bump `@maximhq/bifrost` to v1.6.3 (#3417)

## Summary

Bumps the `@maximhq/bifrost` NPX package version to `1.6.3` to align the `package.json` and `package-lock.json` version fields, which were previously out of sync.

## Changes

- Updated `package.json` version from `1.6.2` to `1.6.3`
- Corrected `package-lock.json` to reflect `1.6.3` consistently across both the lockfile root and the package entry (previously mismatched at `1.0.6` and `1.0.4`)

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [x] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
cd npx/bifrost
npm install
npm pack --dry-run
# Verify the reported version is 1.6.3
```

## Screenshots/Recordings

N/A

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

N/A

## Security considerations

No security implications.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* feat: add volume histogram chart to MCP logs page and fix drag-select bar click suppression (#3431)

Adds a log volume histogram chart to the MCP Logs page, matching the existing chart behavior on the main Logs page. Also fixes a bug where clicking a bar immediately after a drag-select would overwrite the dragged time range with a single-bucket zoom.

- Added `useGetMCPHistogramQuery` to the MCP Logs page to fetch histogram data with optional polling, and rendered the `LogsVolumeChart` component in the MCP Logs view.
- Added `handleTimeRangeChange`, `handleResetZoom`, and `isZoomed` logic to the MCP Logs page, mirroring the behavior already present on the main Logs page.
- Fixed `isZoomed` on the main Logs page to return `false` when a named `period` (e.g. `"1h"`) is active, so resetting zoom correctly clears the zoomed state.
- When resetting zoom, `period: "1h"` and `polling: true` are now explicitly set in URL state to ensure the page returns to a live-polling relative range.
- Fixed a race condition in `LogsVolumeChart` where Recharts fires a Bar `onClick` event immediately after a drag-select `mouseUp`, which was overwriting the dragged range with a single-bucket zoom. A `suppressNextBarClickRef` ref is set during drag completion and cleared on the next bar click to suppress the spurious event.

- [x] Bug fix
- [x] Feature

- [x] UI (React)

1. Navigate to the MCP Logs page and confirm the log volume histogram chart renders and updates with polling.
2. Click a bar in the histogram and confirm the time range zooms into that bucket.
3. Drag-select a range on the histogram and confirm the time range updates to the dragged selection without immediately snapping to a single bucket.
4. Click "Reset Zoom" and confirm the chart returns to the default 1-hour live-polling view.

```sh
cd ui
pnpm i
pnpm build
```

Before: MCP Logs page had no histogram chart.
After: MCP Logs page displays the log volume histogram with zoom, drag-select, and reset zoom functionality identical to the main Logs page.

- [x] No

None.

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* refactor: semantic cache plugin (#3210)

## Summary

This PR refactors the semantic cache plugin to simplify its internal state management, improves cache lookup correctness, and adds a new `cache_hit_types` filter to the logs API and UI. The direct cache lookup path is now a single deterministic point-fetch by a UUIDv5 `directCacheID` (replacing the previous dual-path of chunk lookup + legacy metadata scan), and several context keys are consolidated. The UI gains a "Local Caching" filter sidebar section and cache hit type badges in the log detail view.

## Changes

- **Semantic cache plugin refactor:**
  - Replaced the dual direct-search path (`performDirectChunkLookup` + `performLegacyDirectSearch`) with a single `performDirectSearch` that does an O(1) `GetChunk` by deterministic `directCacheID` (UUIDv5 derived from provider, model, cacheKey, requestHash, paramsHash).
  - `generateDirectCacheID` now returns an error instead of silently falling back to a string concatenation, making failures explicit.
  - `request_hash` is no longer stored as a top-level metadata field; it is encoded into the `directCacheID` instead.
  - Reduced context keys from ~10 to 4 (`directCacheIDKey`, `paramsHashKey`, `embeddingsKey`, `embeddingsInputTokensKey`), removing stale keys like `requestIDKey`, `requestHashKey`, `isCacheHitKey`, and `cacheHitTypeKey`.
  - `shouldSkipCaching` is extracted into its own method; cache-hit detection now reads `CacheDebug.CacheHit` from the response rather than a context flag.
  - `buildUnifiedMetadata` no longer accepts `requestHash` as a parameter.
  - `addSingleResponse` renamed to `addNonStreamingResponse`.
  - `StreamAccumulator` fields `HasError`, `FinalTimestamp`, and `FinishReason` on `StreamChunk` are removed; error streams are handled by early return in `PostLLMHook`.
  - Streaming replay goroutine now guards every send with `ctx.Done()` to prevent goroutine leaks on dropped consumers.
  - A background `runStreamCleanupLoop` goroutine (started by `Init`, stopped by `Cleanup` via `stopCh`) replaces the one-shot cleanup call, periodically reaping stale stream accumulators.
  - `buildResponseFromResult` now accepts `threshold`, `similarity`, and `inputTokens` as pointers, and `attachCacheDebug` is extracted as a shared helper for both streaming and non-streaming paths.
  - `isExpiredEntry` is extracted as a standalone function.
  - `chunkSortKey` replaces the large inline sort comparator in `processAccumulatedStream`.
  - Tools, stop sequences, modalities, include lists, and other order-insensitive set fields are now hashed with `hashSortedSet` / `sortedStringSet` to prevent MCP's randomized map iteration from perturbing the request hash.
  - `extractAttachmentsForCaching` is extracted so attachment URLs are included in the cache key metadata rather than the embedding text.
  - `extractTextForEmbedding` no longer returns a `paramsHash`; callers compute it once via `buildRequestMetadataForCaching` + `hashMap`.
  - `generateEmbedding` moved from `utils.go` to `search.go`.
  - `generateRequestHash` now accepts prebuilt metadata to avoid recomputing it.
  - `removeField` no longer mutates the input slice's backing array.
  - Added `PronunciationDictionaryLocators`, `TimestampGranularities`, `Include`, `AdditionalFormats`, and `InputImages` to their respective parameter metadata extractors.
  - Public context key names changed from `semantic_cache_*` to `semantic_cache-*` (underscore → hyphen separator after the plugin prefix).
  - `SelectFields` no longer includes `request_hash`.
  - `VectorStoreProperties` no longer includes a `request_hash` entry.
  - `CacheByModel` and `CacheByProvider` default-value log messages added.

- **Log filtering — `cache_hit_types`:**
  - Added `CacheHitTypes []string` to `SearchFilters` in `framework/logstore/tables.go`.
  - `applyFilters` in `rdb.go` applies a JSON path filter on `cache_debug` for both SQLite (`json_extract`) and PostgreSQL (`substring` regex) dialects, restricted to the allowlist `["direct", "semantic"]`.
  - `canUseMatViewFilters` excludes queries with `CacheHitTypes` set from the materialized-view fast path.
  - HTTP handlers (`getLogs`, `getLogsStats`, `parseHistogramFilters`) parse a `cache_hit_types` comma-separated query parameter.

- **UI:**
  - Added a "Local Caching" filter section to `LogsFilterSidebar` with checkboxes for "Direct cache" and "Semantic cache".
  - `cache_hit_types` is added to URL state, filter state, and the `buildFilterParams` API helper.
  - Log detail view shows "Direct Cache" (indigo) and "Semantic Cache" (rose) badges based on `cache_debug.hit_type`.
  - Plugins form now filters the provider dropdown to embedding-capable providers only (`EmbeddingSupportedProviders` for built-ins; `custom_provider_config.allowed_requests.embedding` for custom providers), shows an error message when no embedding provider is configured, and disables the toggle accordingly.
  - Embedding model input replaced with `ModelMultiselect` (single-select mode) scoped to the selected provider.
  - Provider dropdown clears the embedding model when the provider changes.
  - Provider icons rendered in the provider dropdown.
  - `EmbeddingSupportedProviders` constant added to `ui/lib/constants/logs.ts`.

- **Misc:**
  - HTTP request logging in `CorsMiddleware` and an auth debug log are commented out.
  - `transports/bifrost-http/v1.5.x` added to `.gitignore`.
  - Minor formatting fixes in `core/schemas/bifrost.go` and `framework/modelcatalog/sync.go`.
  - Missing newline at end of `sync.go` added.

## Type of change

- [ ] Bug fix
- [x] Feature
- [x] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [x] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [x] UI (React)
- [ ] Docs

## How to test

```sh
# Core/Transports
go test ./plugins/semanticcache/...
go test ./framework/logstore/...
go test ./transports/bifrost-http/...

# UI
cd ui
pnpm i
pnpm build
```

- Configure the semantic cache plugin with a direct and/or semantic cache type and verify that cache hits are recorded with the correct `hit_type` in `cache_debug`.
- Query `/logs?cache_hit_types=direct` and `/logs?cache_hit_types=semantic` and confirm only matching entries are returned.
- In the UI, open the logs filter sidebar and verify the "Local Caching" section appears with "Direct cache" and "Semantic cache" checkboxes that correctly filter the log list.
- Open a log detail for a cache hit and confirm the appropriate badge ("Direct Cache" or "Semantic Cache") is displayed.
- In the plugins form, verify that only embedding-capable providers appear in the provider dropdown and that the embedding model field uses the model multiselect.

## Breaking changes

- [x] Yes

The public semantic cache context key names have changed from `semantic_cache_*` to `semantic_cache-*`. Any caller setting `CacheKey`, `CacheTTLKey`, `CacheThresholdKey`, `CacheTypeKey`, or `CacheNoStoreKey` via the old string values will no longer be recognized by the plugin. Update all call sites to use the exported constants from the plugin package rather than raw string literals.

`request_hash` is no longer stored as a top-level metadata field in the vector store. Existing cache entries written by prior versions will not be found by the new direct-search path (they will be treated as misses and re-populated).

`ClearCacheForRequestID` is documented as currently broken for entries written by the new direct-search path; callers should not rely on it until the TODO is resolved.

## Related issues

N/A

## Security considerations

The `CacheHitTypes` filter allowlists values to `"direct"` and `"semantic"` before interpolating them into SQL, preventing arbitrary input from reaching the JSON path expression.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* feat: remove `cleanup_on_shutdown` from semantic cache plugin config (#3330)

## Summary

Removes the `cleanup_on_shutdown` option from the semantic cache plugin. Cache data now always persists between Bifrost restarts. The previous behavior of deleting all cache entries and the vector store namespace on shutdown is no longer supported.

## Changes

- Removed `CleanUpOnShutdown` field from `Config` struct in `plugins/semanticcache/main.go` and stripped the corresponding shutdown deletion logic from `Cleanup()`
- Removed `cleanup_on_shutdown` from the JSON config schema (`transports/config.schema.json`), Helm values schema (`helm-charts/bifrost/values.schema.json`), Helm template helper (`_helpers.tpl`), and default `values.yaml`
- Removed `cleanup_on_shutdown` from all example Kubernetes values files and documentation code samples
- Added migration guide entry (Breaking Change 16) in `docs/migration-guides/v1.5.0.mdx` describing the removal, how to clear cache data using the existing invalidation endpoints, and how to handle dimension/provider/model rotation without the old escape hatch
- Updated the semantic caching feature docs to remove references to `cleanup_on_shutdown` and the associated warning block
- Removed `TestCleanup_DeletesEntriesAndNamespaceWhenEnabled` test and simplified `newTestPlugin` helper to drop the `cleanupOnShutdown` parameter across all test files

## Type of change

- [ ] Bug fix
- [ ] Feature
- [x] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [x] Docs

## How to test

```sh
go test ./plugins/semanticcache/...
```

Verify that passing `cleanup_on_shutdown` in a semantic cache plugin config is rejected by schema validation. Confirm that restarting Bifrost with a semantic cache configured leaves existing vector store entries intact.

## Breaking changes

- [x] Yes
- [ ] No

The `cleanup_on_shutdown` field is removed from the semantic cache plugin config schema and will be rejected by validation. Remove it from `config.json`, Helm values, and any `PUT /api/config` payloads. To clear cache data, use `DELETE /api/cache/clear/{cacheId}`, `DELETE /api/cache/clear-by-key/{cacheKey}`, or rotate `vector_store_namespace` to a fresh name.

## Related issues

See Breaking Change 16 in the v1.5.0 migration guide.

## Security considerations

None.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [x] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* refactor: semantic cache ui revamp (#3331)

## Summary

Replaces the separate `PluginsForm` component with a fully self-contained `CachingView` that introduces a first-class **Direct / Direct + Semantic** mode toggle for the local cache plugin. Previously, the UI only exposed provider-backed semantic cache settings and had no concept of direct-only (hash-based) caching as a distinct, supported mode. This rewrite makes direct-only mode the default and gates semantic configuration behind an explicit mode selection.

## Changes

- Deleted `pluginsForm.tsx` and consolidated all local cache configuration logic directly into `cachingView.tsx`.
- Introduced a `CacheMode` type (`"direct"` | `"semantic"`) with a tab-based picker. Direct-only mode requires no embedding provider; semantic mode adds vector similarity on top and requires a provider, model, and dimension.
- The enable/disable toggle now immediately calls `updatePlugin` or `createPlugin` (for first-time setup) rather than deferring the enabled-state change to the Save button, decoupling the plugin lifecycle from config edits.
- Added `inferMode` to derive the active mode from a saved config, `isEmptyConfig` to detect zero-value configs from the API, `buildPayload` to strip semantic-only fields when persisting a direct-only config, and `validateForSave` for inline validation surfaced before the user clicks Save.
- Structural change warnings (provider/model/dimension drift vs. server state) are now shown only when the user has actually modified those fields, rather than permanently in semantic mode.
- Removed the Zod `cacheConfigSchema` validation path in favor of the new `validateForSave` function.
- Removed the effect that auto-seeded a default provider/model/dimension on first load, since direct-only mode no longer requires those fields.
- Per-request override documentation expanded to include `x-bf-cache-key` and `x-bf-cache-no-store` with clearer descriptions.

## Type of change

- [ ] Bug fix
- [ ] Feature
- [x] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [x] UI (React)
- [ ] Docs

## How to test

```sh
cd ui
pnpm i || npm i
pnpm build || npm run build
```

1. Navigate to the Workspace → Config → Caching view.
2. Verify the page loads with **Direct only** selected by default and no provider/model/dimension fields visible.
3. Switch to **Direct + Semantic** and confirm provider, model, and dimension fields appear with inline validation.
4. Toggle caching on without a vector store configured and confirm the toggle is disabled.
5. Save a direct-only config and confirm the plugin is created/updated with `dimension: 1` and no provider fields.
6. Save a semantic config with a valid provider, model, and dimension and confirm the full payload is persisted.
7. Reload the page and confirm the saved mode and config are correctly hydrated.

## Screenshots/Recordings

Before/after screenshots recommended showing the mode tab picker, the conditional semantic fields, and the structural change warning banner.

## Breaking changes

- [x] Yes
- [ ] No

The `PluginsForm` component is removed. Any code importing it directly will need to be updated. The enable/disable toggle now persists immediately rather than requiring a Save click, which changes the interaction model for existing users.

## Related issues

N/A

## Security considerations

No new auth, secrets, or PII handling introduced. API keys for embedding providers continue to be inherited from the provider's existing configuration and are not re-entered or stored in the cache config.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* fix: resolve cache plugin at request time to support post-boot loads and plugin reloads (#3423)

## Summary

The `CacheHandler` previously captured a reference to the `semantic_cache` plugin at boot time. This caused two bugs: (1) if the plugin was not present in `config.json` at startup, cache-clear routes were never registered, resulting in HTTP 405 for the entire process lifetime; (2) if the plugin was loaded or reloaded via `/api/plugins` after boot, the handler held a stale (or nil) pointer and would silently misbehave. Additionally, `GET /api/plugins/:name` was returning the raw plugin config without runtime status, causing the UI to see an empty status when refetching a single plugin.

## Changes

- `CacheHandler` now accepts a `CacheClearerResolver` function instead of a concrete plugin pointer. The resolver is called on every cache-clear request, so plugin lifecycle changes via `/api/plugins` are always honored.
- `CacheClearer` and `CacheClearerResolver` are exported so server wiring can supply the resolver without importing the plugin's concrete type.
- Cache routes are registered unconditionally at startup. When no plugin is loaded, requests return HTTP 400 with a descriptive message instead of HTTP 405.
- The server wiring in `RegisterAPIRoutes` uses a closure over `lib.FindPluginAs` to resolve the plugin per request, replacing the boot-time capture.
- `getPlugin` now returns the same response shape as list/create/update (with runtime status merged in), fixing the empty status seen by `useGetPluginQuery` in the UI.
- Tests cover the new "plugin not loaded" path for both `clearCache` and `clearCacheByKey`, and existing tests are updated to use the resolver-based constructor.

## Type of change

- [x] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [x] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./transports/bifrost-http/handlers/...
go test ./transports/bifrost-http/...
```

1. Start the server **without** `semantic_cache` in `config.json`. Issue `DELETE /api/cache/clear/{cacheId}` — expect HTTP 400 with `"semantic_cache plugin is not loaded"` (previously HTTP 405).
2. Load the `semantic_cache` plugin via `POST /api/plugins`. Repeat the request — expect the cache-clear to succeed.
3. Reload or remove the plugin via `PUT`/`DELETE /api/plugins`. Verify the handler reflects the new state on the next request without a server restart.
4. Issue `GET /api/plugins/{name}` for a loaded plugin and confirm the response includes runtime status fields, matching the shape returned by the list endpoint.

## Breaking changes

- [x] Yes
- [ ] No

`NewCacheHandler` now accepts a `CacheClearerResolver` function instead of a `schemas.LLMPlugin`. Any caller constructing a `CacheHandler` directly must be updated to pass a resolver.

## Related issues

## Security considerations

None. The change does not affect authentication, secrets, or PII handling.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* fix: decouple cache telemetry from write decision and guard no-op search paths in semantic cache (#3424)

## Summary

Fixes several correctness issues in the semantic cache plugin's `PostLLMHook` and related helpers: cache telemetry (`cache_debug`) was previously invisible to callers using `no-store`, cache-hit replay detection was fragile, non-positive per-request TTL overrides could silently kill cache writes, and requests with a `cache_type` header narrowed to a path the plugin cannot serve would still produce orphan cache entries.

## Changes

- **Early exit for unsupported search paths in `PreLLMHook`**: When `resolveCacheTypes` resolves to a path the plugin cannot actually serve (e.g. `x-bf-cache-type=semantic` against a direct-only plugin, or an unknown header value), the hook now clears cache state and returns early instead of proceeding to generate an embedding or write an orphan entry under a random request UUID that no future read can match.

- **Separated cache-hit replay handling from write-skip logic**: The `shouldSkipCaching` method (which conflated cache-hit detection with write-skip conditions) is replaced by `shouldSkipCacheWrite`. Cache-hit replay is now handled as a dedicated early return in `PostLLMHook` before any telemetry stamping, while `shouldSkipCacheWrite` gates only the write decision after telemetry is already stamped. This ensures `cache_debug` is always populated for callers using `no-store` or large-payload modes.

- **Telemetry stamped before write decision**: `stampCacheDebugForMiss` is now called before `shouldSkipCacheWrite` is consulted, so observability is not conditional on whether the entry is ultimately written.

- **Non-positive TTL overrides fall back to plugin default**: `resolveTTL` now treats a zero or negative per-request TTL override as "use default" rather than applying it, which would have set `expires_at=now` and silently discarded the cache write.

- **Cleaned up stale comments**: Removed an outdated ordering constraint comment in `PostLLMHook` that no longer applies after the restructuring.

- **Tests updated**: Test cases for `shouldSkipCaching` are renamed and updated to reflect the new `shouldSkipCacheWrite` contract. The cache-hit replay test case is removed from this suite (it is now an early return in `PostLLMHook`, not a condition inside the helper). A new default-is-false test is added.

## Type of change

- [x] Bug fix
- [ ] Feature
- [x] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./plugins/semanticcache/...
```

Validate the following scenarios:
- A request with `x-bf-cache-type=semantic` against a plugin configured with `Provider=""` or `Dimension=1` should log a warning and skip caching entirely — no orphan entry should appear in the store.
- A request with `Cache-Control: no-store` should still produce a populated `cache_debug` field in the response with `cache_hit=false`.
- A per-request TTL override of `0s` should fall back to the plugin's configured default TTL and not silently discard the cache write.

## Breaking changes

- [x] No

## Security considerations

None.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* test: add semantic cache e2e test suite skeleton (#3425)

## Summary

Adds a standalone end-to-end test suite for the `semantic_cache` plugin under `tests/semanticcache`. The suite validates the full caching lifecycle against a live Bifrost instance — plugin creation/teardown, cache miss/hit assertions, cross-provider behavior, streaming, and log cross-checking — without provisioning any infrastructure itself.

## Changes

- **`e2e_test.go`** — `TestMain` entry point: loads config, initializes the report directory, checks Bifrost reachability, enforces plugin-absent precondition (with `RUN_FORCE=1` auto-delete), runs all phases, and performs best-effort teardown on exit.
- **`preconditions_test.go`** — Phase 0 checks: Bifrost reachable, OpenAI configured, optional providers (Gemini, Anthropic) present with warnings if absent.
- **`http_test.go`** — HTTP helpers for all request types: chat completions (streaming and non-streaming), text completions, embeddings, image generation, and the Responses API. Each helper dumps full request/response bodies to the report directory for forensics.
- **`plugin_test.go`** — Plugin lifecycle helpers (`pluginCreate`, `pluginUpdate`, `pluginDelete`, `pluginGet`) mirroring the exact wire format the UI sends to `/api/plugins`.
- **`assert_test.go`** — Assertion helpers (`assertMiss`, `assertHit`, `assertNoCacheDebug`, `assertSameCacheID`, `assertDifferentCacheID`) plus a configurable async write-settle wait (`SC_WRITE_SETTLE_MS`) to account for the plugin's async PostLLMHook store write.
- **`cache_test.go`** — Cache management helpers (`clearByCacheID`, `clearByCacheKey`) wrapping the `/api/cache/clear/*` endpoints.
- **`logs_crosscheck_test.go`** — Cross-checks the persisted log row's `cache_debug` against the in-flight response stamp, with polling to handle Bifrost's async logging pipeline and float epsilon tolerance for JSON encoder differences.
- **`fixtures_test.go`** — Hand-curated paraphrase pairs for Phase 2 semantic cases, designed to land well above (canonical→paraphrase) or well below (canonical→unrelated) the default 0.8 similarity threshold.
- **`log_test.go`** — Structured per-run logging to `reports/<UTC-timestamp>/run.log` with optional `TRAIL_SESSION_ID` stamping for trail integration.
- **`go.mod`** — Standalone module (`github.com/maximhq/bifrost/tests/semanticcache`), consistent with the `tests/governance` pattern, excluded from the repo's `go.work`.
- **`README.md`** — Documents prerequisites, env vars, run commands, trail integration, and report output format.
- **`.gitignore`** — Excludes `reports/` and `*.log` from version control.

Notable design decisions: the suite is intentionally verify-only (no infrastructure provisioning), uses a dedicated vector store namespace (`BifrostSemanticCachePluginE2E`) to isolate test data, and writes full wire-level request/response artifacts per step to support post-mortem debugging without re-running.

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [x] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

Requires a running Bifrost instance with Weaviate configured, OpenAI (required), and optionally Gemini and Anthropic providers.

```sh
cd tests/semanticcache

# All phases
GOWORK=off go test -v ./...

# Single phase
GOWORK=off go test -v -run TestPhase1_DirectOnly ./...

# Auto-delete any pre-existing plugin row before run
RUN_FORCE=1 GOWORK=off go test -v ./...

# Keep plugin after run for post-mortem inspection
RUN_KEEP_PLUGIN=1 GOWORK=off go test -v ./...
```

Environment variables:

| Variable | Default | Purpose |
|---|---|---|
| `BIFROST_URL` | `http://localhost:8080` | Bifrost base URL |
| `SC_CHAT_MODEL_OPENAI` | `openai/gpt-4o-mini` | OpenAI chat model |
| `SC_CHAT_MODEL_OPENAI_ALT` | `openai/gpt-4o` | Alternate OpenAI model for cache-by-model cases |
| `SC_EMBED_MODEL_OPENAI` | `text-embedding-3-small` | Embedding model for Phase 2 |
| `SC_CHAT_MODEL_GEMINI` | `gemini/gemini-2.5-flash` | Gemini chat model |
| `SC_CHAT_MODEL_ANTHROPIC` | `anthropic/claude-haiku-4-5` | Anthropic chat model |
| `SC_NAMESPACE` | `BifrostSemanticCachePluginE2E` | Vector store namespace |
| `SC_WRITE_SETTLE_MS` | `500` | Async write settle wait in ms |
| `RUN_FORCE` | unset | `1` to delete pre-existing plugin before run |
| `RUN_KEEP_PLUGIN` | unset | `1` to skip teardown on exit |
| `TRAIL_SESSION_ID` | unset | Stamped onto every log line for trail integration |

## Screenshots/Recordings

N/A

## Breaking changes

- [x] No

## Related issues

N/A

## Security considerations

No secrets are stored in the test suite. API keys are consumed from the existing Bifrost provider configuration and never passed directly through the test harness.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [x] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* test: add direct cache e2e test suite (#3426)

## Summary

Adds a comprehensive end-to-end test suite (`TestDirect`) for the semantic cache plugin operating in direct-only mode. The suite covers 55 test cases (plan §1.1–1.55) validating cache hit/miss behavior, key isolation, TTL handling, config flag mutations, normalization, streaming, multi-endpoint support, parameter hashing, tool definitions, and cache management operations.

## Changes

- Introduces `tests/semanticcache/direct_test.go` with `TestDirect`, covering:
  - **Basic hit/miss and key isolation** (1.1, 1.2, 1.3, 1.4)
  - **`cache_by_model` and `cache_by_provider` flag behavior** (1.5–1.8), including serial config-mutation cases that restore baseline via `t.Cleanup`
  - **`exclude_system_prompt` flag** (1.9, 1.10)
  - **Conversation threshold boundary conditions** (1.11, 1.12)
  - **TTL expiry, per-request TTL override, invalid TTL fallback, and zero/negative TTL fallback** (1.13, 1.14, 1.15, 1.54)
  - **`no-store` header semantics**, including case-sensitivity and explicit `false` value (1.16, 1.17, 1.45, 1.46)
  - **`cache-type` header behavior** in direct-only mode, including the `semantic` header bug case (1.18, 1.19)
  - **Streaming SSE**: hit/miss, chunk replay order, and non-final chunk cache_debug absence (1.24, 1.25, 1.47)
  - **Multi-endpoint coverage**: text completions, responses API, embeddings, and image generation (1.20–1.23)
  - **Input normalization**: case folding, whitespace trimming, Unicode, and large prompts (1.26–1.29)
  - **Image attachment hashing**: same URL hits, different URL misses (1.30, 1.31)
  - **Edge cases**: nil content messages, empty messages array, unknown cache ID deletion (1.42, 1.43, 1.40)
  - **Parameter hash isolation**: temperature, top_p, seed, max_tokens, top_logprobs, tools (order-independent and name-change), prompt_cache_key, service_tier, store flag (1.32–1.37, 1.48–1.52)
  - **Cache management**: clear by cache ID, clear by key (1.38, 1.39)
  - **Plugin status round-trip** via GET (1.44)
  - **`/api/logs` cross-check**: verifies persisted `cache_debug` matches in-flight response stamp (1.55)
  - **`responses` API `previous_response_id` isolation** (1.53)
  - **Threshold header no-op in direct-only mode** (1.41)
- Adds helper functions: `simpleChat`, `chatWithSystem`, `chatWithImage`, `restoreDirectBaseline`, `assertHitAndReturnCacheDebug`
- Establishes a parallelism contract: cases that mutate plugin config run serially (no `t.Parallel()`); all others run concurrently with unique cache keys to prevent collisions

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [x] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
# Run the full direct-mode suite
go test ./tests/semanticcache/... -run TestDirect -v -timeout 300s

# Skip the expensive image generation case
SC_SKIP_IMAGE_GEN=1 go test ./tests/semanticcache/... -run TestDirect -v -timeout 300s
```

Required environment variables (same as the broader semantic cache e2e suite):
- `OPENAI_MODEL` — primary OpenAI-compatible model (e.g. `openai/gpt-4o-mini`)
- `OPENAI_MODEL_ALT` — secondary model for cross-model isolation cases
- `OPENAI_EMBED` — embedding model name (e.g. `text-embedding-3-small`)
- `ANTHRO_MODEL` — (optional) Anthropic model; cases 1.7 and 1.8 skip if unset
- `SC_SKIP_IMAGE_GEN=1` — (optional) skip case 1.23 to avoid DALL-E costs

## Screenshots/Recordings

N/A — test-only change.

## Breaking changes

- [x] No

## Related issues

## Security considerations

No new auth, secrets, or PII surface. Test prompts are benign and do not contain sensitive data.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* test: add semantic cache e2e test suite (#3427)

## Summary

Adds a comprehensive integration test suite for the semantic cache mode (Phase 2), covering the full lifecycle of semantic similarity-based cache hits and misses using Weaviate as the vector store and OpenAI's `text-embedding-3-small` as the embedding model. This suite validates that the semantic cache behaves correctly across a wide range of real-world scenarios, complementing the existing direct-mode (Phase 1) tests.

## Changes

- Added `TestParaphraseFixtures` to pre-flight all paraphrase pairs against the live embedding model, asserting cosine similarity thresholds before any semantic cache cases run. This prevents flaky downstream failures caused by borderline fixture pairs.
- Added `TestSemantic` containing 44 sub-cases (2.1–2.44) covering:
  - Semantic hit on paraphrase, miss on unrelated content
  - Per-request threshold overrides (relax, tighten, clamp above/below valid range)
  - `x-bf-cache-type` header forcing direct-only or semantic-only lookup paths
  - Cache key and model/provider isolation in semantic mode
  - `cache_by_model=false` and `cache_by_provider=false` cross-model/cross-provider hits
  - Streaming replay of semantic hits, including tool call preservation
  - TTL expiry, per-request TTL, TTL=0 fallback, and `no-store` header semantics
  - Namespace isolation and dimension-change silent miss behavior
  - Embedding endpoint bypass (semantic search skipped for `/v1/embeddings`)
  - Image generation and Responses API semantic hits
  - Text completion semantic hits
  - Gemini provider with OpenAI embedding provider
  - `params_hash` isolation (temperature, service tier, store flag, prompt cache key, previous response ID)
  - `exclude_system_prompt` flag effect on semantic matching
  - Conversation message threshold skipping semantic search
  - Attachment URL changes causing misses
  - `cache_debug` field presence and correctness on hits and misses, including log endpoint cross-check
  - Streaming chunk-level `cache_debug` placement (final chunk only)
- Serial (non-parallel) cases that mutate plugin config restore baseline via `t.Cleanup` to avoid test pollution.
- A dedicated Weaviate namespace (`cfg.Namespace + "Semantic"`) is used to avoid dimension conflicts with the Phase 1 direct-mode namespace.

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [x] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
# Run fixture pre-flight (requires OpenAI embedding access)
go test ./tests/semanticcache/... -run TestParaphraseFixtures -v

# Run full semantic suite
go test ./tests/semanticcache/... -run TestSemantic -v -timeout 10m

# Skip fixture verification if embedding access is unavailable
SC_SKIP_FIXTURE_VERIFY=1 go test ./tests/semanticcache/... -run TestSemantic -v -timeout 10m

# Skip image generation cases if DALL-E is unavailable
SC_SKIP_IMAGE_GEN=1 go test ./tests/semanticcache/... -run TestSemantic -v -timeout 10m
```

Required environment/config:
- `cfg.OpenAIEmbed` — embedding model name (e.g. `text-embedding-3-small`)
- `cfg.OpenAIModel` / `cfg.OpenAIModelAlt` — chat models for isolation tests
- `cfg.AnthroModel` — optional; skipped if empty (case 2.13)
- `cfg.GeminiModel` — optional; skipped if empty (case 2.28)
- `cfg.Namespace` — base Weaviate namespace; suite appends `Semantic` suffix
- `SC_SKIP_FIXTURE_VERIFY=1` — skip embedding pre-flight
- `SC_SKIP_IMAGE_GEN=1` — skip DALL-E case

## Screenshots/Recordings

N/A

## Breaking changes

- [x] No

## Related issues

N/A

## Security considerations

No new auth, secrets, or PII handling introduced. Tests call live external APIs (OpenAI, optionally Anthropic/Gemini) and require valid credentials in the test environment; no credentials are hardcoded.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* test: add semantic cache plugin lifecycle tests (#3428)

## Summary

Adds an end-to-end lifecycle test for the semantic cache plugin, covering the full disable → re-enable → delete → recreate flow and asserting that namespace data persists across each state transition.

## Changes

- Introduces `TestLifecycle` in `tests/semanticcache/lifecycle_test.go`, which runs 10 serial subtests (3.1–3.10) exercising the plugin's lifecycle state machine:
  - **3.1** – Disabling the plugin via PUT sets `enabled=false` and `status=disabled`
  - **3.2** – Requests while disabled bypass the cache pipeline entirely (no `cache_debug` header)
  - **3.3 / 3.4** – Cache-clear endpoints (`/api/cache/clear/{id}` and `/api/cache/clear-by-key/{k}`) return HTTP 400 when the plugin is not loaded
  - **3.5** – Re-enabling via PUT restores `enabled=true` and `status=active`
  - **3.6** – Entries written before disable are still queryable after re-enable
  - **3.7** – DELETE removes both the DB row and the in-memory plugin instance
  - **3.8** – Requests after delete bypass the cache pipeline (no `cache_debug` header)
  - **3.9** – Recreating the plugin with the same config succeeds and surfaces `status=active`
  - **3.10** – Entries written before delete are still queryable after recreate, validating the namespace-persistence contract introduced by the removal of `CleanUpOnShutdown`
- Tests are intentionally serial (no `t.Parallel()`) because each subtest mutates globally shared plugin lifecycle state
- A `t.Cleanup` handler performs best-effort key clearing regardless of which lifecycle state the plugin is left in at teardown

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [x] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./tests/semanticcache/... -run TestLifecycle -v
```

Expected outcome: all 10 subtests (3.1–3.10) pass, with structured log output at each step confirming correct status transitions and cache hit/miss behaviour.

## Breaking changes

- [x] No

## Related issues

## Security considerations

None. Tests run against a local Bifrost instance and do not introduce new auth paths, secrets handling, or PII exposure.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* feat: add `test-semantic-cache` and `test-semantic-cache-complete` Makefile targets (#3429)

## Summary

Adds Makefile targets for running `semantic_cache` plugin unit tests and end-to-end tests, with optional integration of the `trail` CLI for capture-based debugging sessions.

## Changes

- Added `test-semantic-cache` target that runs e2e tests from `tests/semanticcache`, supporting a `CACHE_TYPE` variable (`direct` or `semantic`) to filter which test phases are executed. Automatically wraps the run in `trail run` if the `trail` binary is available on `PATH`.
- Added `test-semantic-cache-complete` target that runs both the plugin unit tests (`plugins/semanticcache`) and the e2e tests in sequence, optionally wrapping the entire session in a single `trail run` invocation.
- Added `_test-semantic-cache-complete-inner` as an internal helper target that performs the actual sequential execution of unit and e2e tests with formatted output banners.
- Registered all three new targets in the `.PHONY` declaration.

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [x] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
# Run all semantic_cache e2e tests
make test-semantic-cache

# Run only direct cache tests
CACHE_TYPE=direct make test-semantic-cache

# Run only semantic cache tests
CACHE_TYPE=semantic make test-semantic-cache

# Run both unit and e2e tests together
make test-semantic-cache-complete

# Force e2e run regardless of preconditions
RUN_FORCE=1 make test-semantic-cache-complete
```

If `trail` is installed and on `PATH`, all commands will automatically wrap execution in a `trail run` session for capture-based debugging.

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

## Security considerations

None.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* harness improvements (#3457)

* makefile diff fixes (#3462)

## Summary

Briefly explain the purpose of this PR and the problem it solves.

## Changes

- What was changed and why
- Any notable design decisions or trade-offs

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

Describe the steps to validate this change. Include commands and expected outcomes.

```sh
# Core/Transports
go version
go test ./...

# UI
cd ui
pnpm i || npm i
pnpm test || npm test
pnpm build || npm run build
```

If adding new configs or environment variables, document them here.

## Screenshots/Recordings

If UI changes, add before/after screenshots or short clips.

## Breaking changes

- [ ] Yes
- [ ] No

If yes, describe impact and migration instructions.

## Related issues

Link related issues and discussions. Example: Closes #123

## Security considerations

Note any security implications (auth, secrets, PII, sandboxing, etc.).

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* Preserve Anthropic output schema refs (#3449)

* feat: use the new parameter json schema compliant to json schema spec (#3444)

* feat: replace log delete button with actions dropdown menu and pin actions column (#3480)

## Summary

Replaces the direct delete button in the logs and MCP logs action columns with a dropdown menu triggered by a `MoreHorizontal` icon. This improves the UI by providing a more scalable actions pattern while keeping the delete functionality accessible. The actions column is also now properly pinned to the right side of the table when the user has delete access.

## Changes

- Replaced the inline destructive `Trash2` button with a `DropdownMenu` containing a "Delete" item for both logs and MCP logs tables
- The actions column trigger is now a ghost `MoreHorizontal` icon button, reducing visual noise in the table
- The actions column is pinned to the right only when `hasDeleteAccess` is true; otherwise no fixed columns are configured
- Fixed `fixedColumnIds` to include `"actions"` so the column receives correct sticky positioning behavior
- Removed `overflow-hidden` from pinned cells in the MCP logs table to prevent the dropdown from being clipped
- Reduced the actions column size from 72 to 56px

## Type of change

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [x] UI (React)
- [ ] Docs

## How to test

1. Navigate to the Logs page as a user with delete access.
2. Confirm the actions column is pinned to the right of the table.
3. Click the `⋯` icon on any row and verify the dropdown appears with a "Delete" option.
4. Click "Delete" and confirm the log is deleted without the row click handler firing.
5. Repeat on the MCP Logs page.
6. Log in as a user without delete access and confirm the actions column is not present.

```sh
cd ui
pnpm i
pnpm build
```

## Screenshots/Recordings

_Add before/after screenshots showing the old delete button vs. the new dropdown._

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

## Security considerations

No new security implications. Delete access gating remains unchanged.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* fix: constrain model catalog table column widths and truncate overflowing text (#3481)

## Summary

Fixes layout overflow issues in the Model Catalog table where long provider names and model badge text would break out of their columns or cause uneven column sizing.

## Changes

- Added `table-fixed` layout with explicit `<colgroup>` column widths (26% / 44% / 16% / 14%) to enforce stable column proportions
- Added `overflow-hidden` and `truncate` to the Provider name cell so long names are clipped cleanly instead of overflowing
- Added `shrink-0` to the "CUSTOM" badge so it doesn't compress when the provider name is long
- Added `max-w-[220px] truncate` to model name badges in `ModelsUsedCell` to prevent …
@coderabbitai coderabbitai Bot mentioned this pull request Jul 5, 2026
18 tasks
akhsaul pushed a commit to akhsaul/bifrost that referenced this pull request Aug 27, 2026
## Summary

Adds a standalone end-to-end test suite for the `semantic_cache` plugin under `tests/semanticcache`. The suite validates the full caching lifecycle against a live Bifrost instance — plugin creation/teardown, cache miss/hit assertions, cross-provider behavior, streaming, and log cross-checking — without provisioning any infrastructure itself.

## Changes

- **`e2e_test.go`** — `TestMain` entry point: loads config, initializes the report directory, checks Bifrost reachability, enforces plugin-absent precondition (with `RUN_FORCE=1` auto-delete), runs all phases, and performs best-effort teardown on exit.
- **`preconditions_test.go`** — Phase 0 checks: Bifrost reachable, OpenAI configured, optional providers (Gemini, Anthropic) present with warnings if absent.
- **`http_test.go`** — HTTP helpers for all request types: chat completions (streaming and non-streaming), text completions, embeddings, image generation, and the Responses API. Each helper dumps full request/response bodies to the report directory for forensics.
- **`plugin_test.go`** — Plugin lifecycle helpers (`pluginCreate`, `pluginUpdate`, `pluginDelete`, `pluginGet`) mirroring the exact wire format the UI sends to `/api/plugins`.
- **`assert_test.go`** — Assertion helpers (`assertMiss`, `assertHit`, `assertNoCacheDebug`, `assertSameCacheID`, `assertDifferentCacheID`) plus a configurable async write-settle wait (`SC_WRITE_SETTLE_MS`) to account for the plugin's async PostLLMHook store write.
- **`cache_test.go`** — Cache management helpers (`clearByCacheID`, `clearByCacheKey`) wrapping the `/api/cache/clear/*` endpoints.
- **`logs_crosscheck_test.go`** — Cross-checks the persisted log row's `cache_debug` against the in-flight response stamp, with polling to handle Bifrost's async logging pipeline and float epsilon tolerance for JSON encoder differences.
- **`fixtures_test.go`** — Hand-curated paraphrase pairs for Phase 2 semantic cases, designed to land well above (canonical→paraphrase) or well below (canonical→unrelated) the default 0.8 similarity threshold.
- **`log_test.go`** — Structured per-run logging to `reports/<UTC-timestamp>/run.log` with optional `TRAIL_SESSION_ID` stamping for trail integration.
- **`go.mod`** — Standalone module (`github.com/maximhq/bifrost/tests/semanticcache`), consistent with the `tests/governance` pattern, excluded from the repo's `go.work`.
- **`README.md`** — Documents prerequisites, env vars, run commands, trail integration, and report output format.
- **`.gitignore`** — Excludes `reports/` and `*.log` from version control.

Notable design decisions: the suite is intentionally verify-only (no infrastructure provisioning), uses a dedicated vector store namespace (`BifrostSemanticCachePluginE2E`) to isolate test data, and writes full wire-level request/response artifacts per step to support post-mortem debugging without re-running.

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [x] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

Requires a running Bifrost instance with Weaviate configured, OpenAI (required), and optionally Gemini and Anthropic providers.

```sh
cd tests/semanticcache

# All phases
GOWORK=off go test -v ./...

# Single phase
GOWORK=off go test -v -run TestPhase1_DirectOnly ./...

# Auto-delete any pre-existing plugin row before run
RUN_FORCE=1 GOWORK=off go test -v ./...

# Keep plugin after run for post-mortem inspection
RUN_KEEP_PLUGIN=1 GOWORK=off go test -v ./...
```

Environment variables:

| Variable | Default | Purpose |
|---|---|---|
| `BIFROST_URL` | `http://localhost:8080` | Bifrost base URL |
| `SC_CHAT_MODEL_OPENAI` | `openai/gpt-4o-mini` | OpenAI chat model |
| `SC_CHAT_MODEL_OPENAI_ALT` | `openai/gpt-4o` | Alternate OpenAI model for cache-by-model cases |
| `SC_EMBED_MODEL_OPENAI` | `text-embedding-3-small` | Embedding model for Phase 2 |
| `SC_CHAT_MODEL_GEMINI` | `gemini/gemini-2.5-flash` | Gemini chat model |
| `SC_CHAT_MODEL_ANTHROPIC` | `anthropic/claude-haiku-4-5` | Anthropic chat model |
| `SC_NAMESPACE` | `BifrostSemanticCachePluginE2E` | Vector store namespace |
| `SC_WRITE_SETTLE_MS` | `500` | Async write settle wait in ms |
| `RUN_FORCE` | unset | `1` to delete pre-existing plugin before run |
| `RUN_KEEP_PLUGIN` | unset | `1` to skip teardown on exit |
| `TRAIL_SESSION_ID` | unset | Stamped onto every log line for trail integration |

## Screenshots/Recordings

N/A

## Breaking changes

- [x] No

## Related issues

N/A

## Security considerations

No secrets are stored in the test suite. API keys are consumed from the existing Bifrost provider configuration and never passed directly through the test harness.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [x] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable
akhsaul pushed a commit to akhsaul/bifrost that referenced this pull request Aug 27, 2026
* feat: add granular RBAC checks for API keys, inference, metrics, and filter inaccessible sidebar items (#3295)

This PR improves RBAC granularity in the sidebar by introducing dedicated resource types for `APIKeys`, `Inference`, and `Metrics`, and fixes sidebar visibility logic so that items and groups are hidden when the user lacks access rather than relying on broader, less specific permissions.

- Added three new `RbacResource` enum values: `APIKeys`, `Inference`, and `Metrics` to the fallback RBAC context.
- The API Keys sidebar item now gates access via the new `hasAPIKeyAccess` (`RbacResource.APIKeys`) check instead of the generic `hasSettingsAccess`.
- The MCP Logs sidebar item now correctly gates access via `hasMCPGatewayAccess` instead of the unrelated `hasLogsAccess`.
- Introduced an `accessibleItems` memoized computation that filters out sidebar items and entire groups whose sub-items are all inaccessible, ensuring users never see empty navigation sections. Previously, access filtering only happened during search.
- Removed unused imports (`PanelLeft`, `PanelRight`, `cn`).

- [ ] Bug fix
- [x] Feature
- [x] Refactor
- [ ] Documentation
- [ ] Chore/CI

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [x] UI (React)
- [ ] Docs

1. Log in as a user with restricted RBAC permissions that exclude `APIKeys` and/or `Settings`.
2. Verify the API Keys entry under the Config section is hidden for users without `APIKeys` view permission.
3. Verify the MCP Logs entry is hidden for users without `MCPGateway` view permission.
4. Verify that sidebar groups with no accessible sub-items are hidden entirely rather than showing an empty group.
5. Verify that users with full access see no change in sidebar behavior.

```sh
cd ui
pnpm i || npm i
pnpm build || npm run build
```

_Add before/after screenshots showing sidebar items hidden for restricted users._

- [ ] Yes
- [x] No

_Link related issues here._

Access control checks for API Keys management are now scoped to a dedicated `APIKeys` RBAC resource rather than the broader `Settings` resource, reducing the risk of unintended access to key management for users who have settings visibility but should not manage API keys.

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* fix: hide delete log button instead of disabling it when user lacks delete access (#3314)

The delete button in log tables was always rendered (just disabled) for users without delete access. This PR hides the actions column entirely when the user lacks delete permissions, and fixes the RBAC resource check for MCP logs to use the correct `MCPGateway` resource instead of `Logs`.

- The actions column in both the workspace logs and MCP logs tables is now conditionally included in the column definitions only when `hasDeleteAccess` is `true`, rather than always rendering a disabled button.
- The delete button styling was updated to use more visible destructive colors (`text-destructive/60 border-destructive/60`) instead of the previous muted secondary foreground styles.
- The RBAC resource used to gate delete access on the MCP logs page was corrected from `RbacResource.Logs` to `RbacResource.MCPGateway`.

- [x] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [x] UI (React)
- [ ] Docs

1. Log in as a user **without** delete access on Logs or MCPGateway resources.
2. Navigate to the workspace logs page and the MCP logs page.
3. Verify the delete button/column is not visible.
4. Log in as a user **with** delete access.
5. Verify the delete button appears and is functional.

```sh
cd ui
pnpm i
pnpm test
pnpm build
```

Before: Delete button rendered but disabled for users without access.
After: Delete column is hidden entirely for users without delete access.

- [ ] Yes
- [x] No

The RBAC fix ensures MCP log deletion is gated on the correct `MCPGateway` resource permission, preventing users with only `Logs` delete access from incorrectly being granted delete access to MCP logs.

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* feat: add `MCPLogs` RBAC resource and enforce access control on MCP logs route and sidebar (#3316)

Introduces a dedicated `MCPLogs` RBAC resource, decoupling MCP log access control from the `MCPGateway` resource. This allows permissions for viewing and deleting MCP logs to be managed independently from gateway-level permissions.

- Added `MCPLogs` as a new `RbacResource` enum value in the fallback RBAC context.
- The MCP Logs route now checks `MCPLogs` view permission and renders a `NoPermissionView` when access is denied, rather than rendering the page unconditionally.
- Delete access on the MCP Logs page now checks `RbacResource.MCPLogs` instead of `RbacResource.MCPGateway`.
- The sidebar MCP Logs entry now uses `hasMCPLogsAccess` (derived from `RbacResource.MCPLogs`) to control visibility, rather than reusing `hasMCPGatewayAccess`.

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [x] UI (React)
- [ ] Docs

1. Configure a role that has `MCPGateway` access but **no** `MCPLogs` access.
2. Log in as a user with that role and navigate to the MCP Logs page — the `NoPermissionView` should be displayed and the sidebar entry should be hidden.
3. Grant the role `MCPLogs` view access and confirm the page and sidebar entry become accessible.
4. Verify that delete functionality on the MCP Logs page is gated by `MCPLogs` delete permission independently of `MCPGateway` delete permission.

```sh
cd ui
pnpm i || npm i
pnpm test || npm test
pnpm build || npm run build
```

N/A

- [x] Yes
- [ ] No

Any role configuration that previously relied on `MCPGateway` permissions to grant access to MCP Logs will need to be updated to explicitly grant `MCPLogs` permissions.

N/A

Access to MCP log data (which may contain sensitive tool execution details) is now enforced by a dedicated RBAC resource, reducing the risk of unintended access through overly broad `MCPGateway` permissions.

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* fix: replace unsafe inline jsonb cast with `bifrost_safe_jsonb` PL/pgSQL helper to prevent malformed JSON from aborting list queries (#3407)

## Summary

The `/api/logs` list query was aborting entirely when a single row contained malformed JSON in `input_history` or `responses_input_history`. The previous inline guard only checked the first character before casting to `jsonb`, so rows that appeared array-shaped but contained malformed JSON (unterminated structures, trailing commas, unpaired UTF-16 surrogates, `\u0000` escapes, etc.) would trigger a `22P02`/`22P05` error and kill the entire response. This PR fixes that by introducing a PL/pgSQL helper function (`bifrost_safe_jsonb`) that wraps the cast in an `EXCEPTION` block and falls back to returning the raw text on any parse failure.

## Changes

- Added a new migration `migrationAddSafeJsonbFunction` that installs the `bifrost_safe_jsonb(text)` PL/pgSQL function on Postgres. The function validates the input, attempts the `jsonb` cast inside an `EXCEPTION` block, and returns the last array element on success or the raw text on any failure.
- Replaced the multi-condition inline `CASE` guards in `listSelectColumns` for Postgres with calls to `bifrost_safe_jsonb`, simplifying the SQL and correctly handling all malformed-JSON edge cases that the previous character-check approach missed.
- For SQLite, added `json_valid()`, `json_type()`, and `json_array_length()` guards to the `CASE` expressions to prevent extraction attempts on invalid or empty arrays.
- Added `safe_jsonb_test.go` covering both the SQLite and Postgres dialect branches of `listSelectColumns`, as well as direct invocation of `bifrost_safe_jsonb` across all relevant edge cases (malformed structures, surrogate pairs, `\u0000` escapes, non-array values, SQL `NULL`).

## Type of change

- [x] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
cd framework && docker compose up -d postgres

go test ./framework/logstore/ -run 'MalformedInputHistory|BifrostSafeJsonb' -count=1 -v
```

Insert a row into the logs table with a malformed JSON value in `input_history` (e.g., `[{"key": "val"` — unterminated) and verify that a call to the list endpoint returns successfully without a 500 error, with the malformed row's `input_history` returned as raw text rather than aborting the query.

## Test Coverage

### `TestSearchLogs_MalformedInputHistory_{SQLite,Postgres}` — end-to-end list query

| # | Case | Column | Payload shape | Pre-fix behavior | Path exercised |
| --- | --- | --- | --- | --- | --- |
| 1 | `unterminated_object_in_array` | `input_history` | `[{"role":"user","content":"hi"` | 22P02 aborts query | EXCEPTION fallback |
| 2 | `garbage_after_bracket` | `input_history` | `[abc, not json]` | 22P02 aborts query | EXCEPTION fallback |
| 3 | `trailing_comma` | `input_history` | `[{"role":"user","content":"hi"},]` | 22P02 aborts query | EXCEPTION fallback |
| 4 | `unclosed_array_only` | `input_history` | `[` | 22P02 aborts query | EXCEPTION fallback |
| 5 | `open_bracket_then_brace_unclosed` | `input_history` | `[{` | 22P02 aborts query | EXCEPTION fallback |
| 6 | `nan_value_not_valid_json` | `input_history` | `[NaN]` | 22P02 aborts query | EXCEPTION fallback |
| 7 | `infinity_value_not_valid_json` | `input_history` | `[Infinity]` | 22P02 aborts query | EXCEPTION fallback |
| 8 | `unpaired_high_surrogate` | `input_history` | `[{"...":"bad \uD800 surrogate"}]` | 22P05 aborts query | EXCEPTION fallback |
| 9 | `unpaired_low_surrogate` | `input_history` | `[{"...":"bad \uDC00 low"}]` | 22P05 aborts query | EXCEPTION fallback |
| 10 | `bad_surrogate_pair_high_then_ascii` | `input_history` | `[{"c":"\uD800A"}]` | 22P05 aborts query | EXCEPTION fallback |
| 11 | `u0000_escape_inside_string` | `input_history` | `[{"...":"null byte � here"}]` | 22P05 aborts query | EXCEPTION fallback |
| 12 | `literal_backslash_u0000_valid_jsonb` | `input_history` | `[{"...":"... \\u0000 literal"}]` | OK (degraded to raw by old guard) | Fast path, last-element extraction |
| 13 | `single_element_array` | `input_history` | `[{"role":"user","content":"only one"}]` | OK | Fast path |
| 14 | `array_of_primitives` | `input_history` | `[1,2,3]` | OK | Fast path |
| 15 | `array_with_null_last_element` | `input_history` | `[{...}, null]` | OK | Fast path |
| 16 | `deeply_nested_valid` | `input_history` | `[{"role":"user","content":{"nested":{"deep":{"value":42}}}}]` | OK | Fast path |
| 17 | `unicode_emoji_content` | `input_history` | `[{"...":"hello 🎉 world ✨"}]` | OK | Fast path |
| 18 | `large_valid_array` | `input_history` | 1001-element array | OK | Fast path at scale |
| 19 | `leading_whitespace_then_array` | `input_history` | `   [\t{...}]` | OK | `btrim` + fast path |
| 20 | `top_level_object_not_array` | `input_history` | `{"not":"an array"}` | OK | Non-array fall-through |
| 21 | `null_literal` | `input_history` | `null` | OK | Non-array fall-through |
| 22 | `whitespace_only` | `input_history` | `"   \t  "` | OK | Empty-after-btrim fall-through |
| 23 | `realtime_turn_malformed_passthrough` | `input_history` (object_type=`realtime.turn`) | `[{"role":"user"` | OK (outer CASE bypassed safe fn) | Realtime-turn bypass branch |
| 24 | `malformed_responses_input_history` | `responses_input_history` | `[{"role":"user"` | 22P02 aborts query | Mirror column, EXCEPTION fallback |
| 25 | `valid_responses_input_history` | `responses_input_history` | `[{...},{...}]` | OK | Mirror column, fast path |

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

[https://github.com/maximhq/bifrost/issues/3255](https://github.com/maximhq/bifrost/issues/3255#issuecomment-4427506449)

## Security considerations

None. The function is `IMMUTABLE` and operates only on text values already stored in the database. No new inputs are exposed.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* feat: add required headers input to prompt playground settings panel (#3412)

## Summary

Adds support for server-configured required request headers in the prompt playground. When the server specifies `required_headers` in its client config, users can now provide values for those headers directly in the settings panel, and they are forwarded with every chat completion request.

## Changes

- Added `customHeaders` state and `requiredHeaders` derived from the core config's `client_config.required_headers` to the `PromptContext`, keeping header keys in sync with the server config while preserving user-entered values.
- Exposed a "Required Headers" section in the settings panel that renders an input field for each required header name when any are configured.
- Extended `ExecutionConfig` in the executor to accept `customHeaders`, which are merged into the fetch request headers (skipping any entries with empty names or values).
- Passed `customHeaders` through both `handleSubmit` and `handleSubmitToolResult` execution paths and included it in their respective `useCallback` dependency arrays.

## Type of change

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [x] UI (React)
- [ ] Docs

## How to test

1. Configure `required_headers` in the server's client config (e.g., `["X-My-Custom-Header"]`).
2. Open the prompt playground and navigate to the settings panel.
3. Verify a "Required Headers" section appears with an input for each configured header name.
4. Enter a value for each header and send a chat completion request.
5. Confirm the header is present in the outgoing request.
6. Remove a header from the server config and verify it disappears from the UI without affecting other header values.

```sh
cd ui
pnpm i || npm i
pnpm test || npm test
pnpm build || npm run build
```

## Screenshots/Recordings

_Add before/after screenshots of the settings panel showing the new Required Headers section._

## Breaking changes

- [x] No

## Related issues

## Security considerations

Header values are entered by the user and sent only to the configured backend endpoint. Empty header names or values are explicitly skipped before being added to the request, preventing accidental forwarding of blank headers. Users should be cautious not to enter sensitive credentials unless the connection to the server is secured.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* fix: skip pagination clamp for virtual keys export requests (#3416)

## Summary

When exporting virtual keys, pagination clamping was being applied unnecessarily, which could interfere with retrieving the full dataset. This PR skips the pagination limit/offset clamping when the export flag is set, while still ensuring the offset is non-negative.

## Changes

- Pagination clamping via `ClampPaginationParams` is now bypassed when `params.Export` is `true`, allowing exports to retrieve data without artificially constrained limits
- A minimal guard ensures `params.Offset` is still set to `0` if negative during an export request

## Type of change

- [x] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [x] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

Trigger a virtual keys export request and verify that all keys are returned without being truncated by pagination limits. Compare the export result count against the total number of virtual keys in the system.

```sh
go test ./...
```

## Screenshots/Recordings

N/A

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

https://github.com/maximhq/bifrost/issues/3414

## Security considerations

No additional security implications. Export access is still gated by existing authentication and authorization checks.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* chore: bump `@maximhq/bifrost` to v1.6.3 (#3417)

## Summary

Bumps the `@maximhq/bifrost` NPX package version to `1.6.3` to align the `package.json` and `package-lock.json` version fields, which were previously out of sync.

## Changes

- Updated `package.json` version from `1.6.2` to `1.6.3`
- Corrected `package-lock.json` to reflect `1.6.3` consistently across both the lockfile root and the package entry (previously mismatched at `1.0.6` and `1.0.4`)

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [x] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
cd npx/bifrost
npm install
npm pack --dry-run
# Verify the reported version is 1.6.3
```

## Screenshots/Recordings

N/A

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

N/A

## Security considerations

No security implications.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* feat: add volume histogram chart to MCP logs page and fix drag-select bar click suppression (#3431)

Adds a log volume histogram chart to the MCP Logs page, matching the existing chart behavior on the main Logs page. Also fixes a bug where clicking a bar immediately after a drag-select would overwrite the dragged time range with a single-bucket zoom.

- Added `useGetMCPHistogramQuery` to the MCP Logs page to fetch histogram data with optional polling, and rendered the `LogsVolumeChart` component in the MCP Logs view.
- Added `handleTimeRangeChange`, `handleResetZoom`, and `isZoomed` logic to the MCP Logs page, mirroring the behavior already present on the main Logs page.
- Fixed `isZoomed` on the main Logs page to return `false` when a named `period` (e.g. `"1h"`) is active, so resetting zoom correctly clears the zoomed state.
- When resetting zoom, `period: "1h"` and `polling: true` are now explicitly set in URL state to ensure the page returns to a live-polling relative range.
- Fixed a race condition in `LogsVolumeChart` where Recharts fires a Bar `onClick` event immediately after a drag-select `mouseUp`, which was overwriting the dragged range with a single-bucket zoom. A `suppressNextBarClickRef` ref is set during drag completion and cleared on the next bar click to suppress the spurious event.

- [x] Bug fix
- [x] Feature

- [x] UI (React)

1. Navigate to the MCP Logs page and confirm the log volume histogram chart renders and updates with polling.
2. Click a bar in the histogram and confirm the time range zooms into that bucket.
3. Drag-select a range on the histogram and confirm the time range updates to the dragged selection without immediately snapping to a single bucket.
4. Click "Reset Zoom" and confirm the chart returns to the default 1-hour live-polling view.

```sh
cd ui
pnpm i
pnpm build
```

Before: MCP Logs page had no histogram chart.
After: MCP Logs page displays the log volume histogram with zoom, drag-select, and reset zoom functionality identical to the main Logs page.

- [x] No

None.

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* refactor: semantic cache plugin (#3210)

## Summary

This PR refactors the semantic cache plugin to simplify its internal state management, improves cache lookup correctness, and adds a new `cache_hit_types` filter to the logs API and UI. The direct cache lookup path is now a single deterministic point-fetch by a UUIDv5 `directCacheID` (replacing the previous dual-path of chunk lookup + legacy metadata scan), and several context keys are consolidated. The UI gains a "Local Caching" filter sidebar section and cache hit type badges in the log detail view.

## Changes

- **Semantic cache plugin refactor:**
  - Replaced the dual direct-search path (`performDirectChunkLookup` + `performLegacyDirectSearch`) with a single `performDirectSearch` that does an O(1) `GetChunk` by deterministic `directCacheID` (UUIDv5 derived from provider, model, cacheKey, requestHash, paramsHash).
  - `generateDirectCacheID` now returns an error instead of silently falling back to a string concatenation, making failures explicit.
  - `request_hash` is no longer stored as a top-level metadata field; it is encoded into the `directCacheID` instead.
  - Reduced context keys from ~10 to 4 (`directCacheIDKey`, `paramsHashKey`, `embeddingsKey`, `embeddingsInputTokensKey`), removing stale keys like `requestIDKey`, `requestHashKey`, `isCacheHitKey`, and `cacheHitTypeKey`.
  - `shouldSkipCaching` is extracted into its own method; cache-hit detection now reads `CacheDebug.CacheHit` from the response rather than a context flag.
  - `buildUnifiedMetadata` no longer accepts `requestHash` as a parameter.
  - `addSingleResponse` renamed to `addNonStreamingResponse`.
  - `StreamAccumulator` fields `HasError`, `FinalTimestamp`, and `FinishReason` on `StreamChunk` are removed; error streams are handled by early return in `PostLLMHook`.
  - Streaming replay goroutine now guards every send with `ctx.Done()` to prevent goroutine leaks on dropped consumers.
  - A background `runStreamCleanupLoop` goroutine (started by `Init`, stopped by `Cleanup` via `stopCh`) replaces the one-shot cleanup call, periodically reaping stale stream accumulators.
  - `buildResponseFromResult` now accepts `threshold`, `similarity`, and `inputTokens` as pointers, and `attachCacheDebug` is extracted as a shared helper for both streaming and non-streaming paths.
  - `isExpiredEntry` is extracted as a standalone function.
  - `chunkSortKey` replaces the large inline sort comparator in `processAccumulatedStream`.
  - Tools, stop sequences, modalities, include lists, and other order-insensitive set fields are now hashed with `hashSortedSet` / `sortedStringSet` to prevent MCP's randomized map iteration from perturbing the request hash.
  - `extractAttachmentsForCaching` is extracted so attachment URLs are included in the cache key metadata rather than the embedding text.
  - `extractTextForEmbedding` no longer returns a `paramsHash`; callers compute it once via `buildRequestMetadataForCaching` + `hashMap`.
  - `generateEmbedding` moved from `utils.go` to `search.go`.
  - `generateRequestHash` now accepts prebuilt metadata to avoid recomputing it.
  - `removeField` no longer mutates the input slice's backing array.
  - Added `PronunciationDictionaryLocators`, `TimestampGranularities`, `Include`, `AdditionalFormats`, and `InputImages` to their respective parameter metadata extractors.
  - Public context key names changed from `semantic_cache_*` to `semantic_cache-*` (underscore → hyphen separator after the plugin prefix).
  - `SelectFields` no longer includes `request_hash`.
  - `VectorStoreProperties` no longer includes a `request_hash` entry.
  - `CacheByModel` and `CacheByProvider` default-value log messages added.

- **Log filtering — `cache_hit_types`:**
  - Added `CacheHitTypes []string` to `SearchFilters` in `framework/logstore/tables.go`.
  - `applyFilters` in `rdb.go` applies a JSON path filter on `cache_debug` for both SQLite (`json_extract`) and PostgreSQL (`substring` regex) dialects, restricted to the allowlist `["direct", "semantic"]`.
  - `canUseMatViewFilters` excludes queries with `CacheHitTypes` set from the materialized-view fast path.
  - HTTP handlers (`getLogs`, `getLogsStats`, `parseHistogramFilters`) parse a `cache_hit_types` comma-separated query parameter.

- **UI:**
  - Added a "Local Caching" filter section to `LogsFilterSidebar` with checkboxes for "Direct cache" and "Semantic cache".
  - `cache_hit_types` is added to URL state, filter state, and the `buildFilterParams` API helper.
  - Log detail view shows "Direct Cache" (indigo) and "Semantic Cache" (rose) badges based on `cache_debug.hit_type`.
  - Plugins form now filters the provider dropdown to embedding-capable providers only (`EmbeddingSupportedProviders` for built-ins; `custom_provider_config.allowed_requests.embedding` for custom providers), shows an error message when no embedding provider is configured, and disables the toggle accordingly.
  - Embedding model input replaced with `ModelMultiselect` (single-select mode) scoped to the selected provider.
  - Provider dropdown clears the embedding model when the provider changes.
  - Provider icons rendered in the provider dropdown.
  - `EmbeddingSupportedProviders` constant added to `ui/lib/constants/logs.ts`.

- **Misc:**
  - HTTP request logging in `CorsMiddleware` and an auth debug log are commented out.
  - `transports/bifrost-http/v1.5.x` added to `.gitignore`.
  - Minor formatting fixes in `core/schemas/bifrost.go` and `framework/modelcatalog/sync.go`.
  - Missing newline at end of `sync.go` added.

## Type of change

- [ ] Bug fix
- [x] Feature
- [x] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [x] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [x] UI (React)
- [ ] Docs

## How to test

```sh
# Core/Transports
go test ./plugins/semanticcache/...
go test ./framework/logstore/...
go test ./transports/bifrost-http/...

# UI
cd ui
pnpm i
pnpm build
```

- Configure the semantic cache plugin with a direct and/or semantic cache type and verify that cache hits are recorded with the correct `hit_type` in `cache_debug`.
- Query `/logs?cache_hit_types=direct` and `/logs?cache_hit_types=semantic` and confirm only matching entries are returned.
- In the UI, open the logs filter sidebar and verify the "Local Caching" section appears with "Direct cache" and "Semantic cache" checkboxes that correctly filter the log list.
- Open a log detail for a cache hit and confirm the appropriate badge ("Direct Cache" or "Semantic Cache") is displayed.
- In the plugins form, verify that only embedding-capable providers appear in the provider dropdown and that the embedding model field uses the model multiselect.

## Breaking changes

- [x] Yes

The public semantic cache context key names have changed from `semantic_cache_*` to `semantic_cache-*`. Any caller setting `CacheKey`, `CacheTTLKey`, `CacheThresholdKey`, `CacheTypeKey`, or `CacheNoStoreKey` via the old string values will no longer be recognized by the plugin. Update all call sites to use the exported constants from the plugin package rather than raw string literals.

`request_hash` is no longer stored as a top-level metadata field in the vector store. Existing cache entries written by prior versions will not be found by the new direct-search path (they will be treated as misses and re-populated).

`ClearCacheForRequestID` is documented as currently broken for entries written by the new direct-search path; callers should not rely on it until the TODO is resolved.

## Related issues

N/A

## Security considerations

The `CacheHitTypes` filter allowlists values to `"direct"` and `"semantic"` before interpolating them into SQL, preventing arbitrary input from reaching the JSON path expression.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* feat: remove `cleanup_on_shutdown` from semantic cache plugin config (#3330)

## Summary

Removes the `cleanup_on_shutdown` option from the semantic cache plugin. Cache data now always persists between Bifrost restarts. The previous behavior of deleting all cache entries and the vector store namespace on shutdown is no longer supported.

## Changes

- Removed `CleanUpOnShutdown` field from `Config` struct in `plugins/semanticcache/main.go` and stripped the corresponding shutdown deletion logic from `Cleanup()`
- Removed `cleanup_on_shutdown` from the JSON config schema (`transports/config.schema.json`), Helm values schema (`helm-charts/bifrost/values.schema.json`), Helm template helper (`_helpers.tpl`), and default `values.yaml`
- Removed `cleanup_on_shutdown` from all example Kubernetes values files and documentation code samples
- Added migration guide entry (Breaking Change 16) in `docs/migration-guides/v1.5.0.mdx` describing the removal, how to clear cache data using the existing invalidation endpoints, and how to handle dimension/provider/model rotation without the old escape hatch
- Updated the semantic caching feature docs to remove references to `cleanup_on_shutdown` and the associated warning block
- Removed `TestCleanup_DeletesEntriesAndNamespaceWhenEnabled` test and simplified `newTestPlugin` helper to drop the `cleanupOnShutdown` parameter across all test files

## Type of change

- [ ] Bug fix
- [ ] Feature
- [x] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [x] Docs

## How to test

```sh
go test ./plugins/semanticcache/...
```

Verify that passing `cleanup_on_shutdown` in a semantic cache plugin config is rejected by schema validation. Confirm that restarting Bifrost with a semantic cache configured leaves existing vector store entries intact.

## Breaking changes

- [x] Yes
- [ ] No

The `cleanup_on_shutdown` field is removed from the semantic cache plugin config schema and will be rejected by validation. Remove it from `config.json`, Helm values, and any `PUT /api/config` payloads. To clear cache data, use `DELETE /api/cache/clear/{cacheId}`, `DELETE /api/cache/clear-by-key/{cacheKey}`, or rotate `vector_store_namespace` to a fresh name.

## Related issues

See Breaking Change 16 in the v1.5.0 migration guide.

## Security considerations

None.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [x] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* refactor: semantic cache ui revamp (#3331)

## Summary

Replaces the separate `PluginsForm` component with a fully self-contained `CachingView` that introduces a first-class **Direct / Direct + Semantic** mode toggle for the local cache plugin. Previously, the UI only exposed provider-backed semantic cache settings and had no concept of direct-only (hash-based) caching as a distinct, supported mode. This rewrite makes direct-only mode the default and gates semantic configuration behind an explicit mode selection.

## Changes

- Deleted `pluginsForm.tsx` and consolidated all local cache configuration logic directly into `cachingView.tsx`.
- Introduced a `CacheMode` type (`"direct"` | `"semantic"`) with a tab-based picker. Direct-only mode requires no embedding provider; semantic mode adds vector similarity on top and requires a provider, model, and dimension.
- The enable/disable toggle now immediately calls `updatePlugin` or `createPlugin` (for first-time setup) rather than deferring the enabled-state change to the Save button, decoupling the plugin lifecycle from config edits.
- Added `inferMode` to derive the active mode from a saved config, `isEmptyConfig` to detect zero-value configs from the API, `buildPayload` to strip semantic-only fields when persisting a direct-only config, and `validateForSave` for inline validation surfaced before the user clicks Save.
- Structural change warnings (provider/model/dimension drift vs. server state) are now shown only when the user has actually modified those fields, rather than permanently in semantic mode.
- Removed the Zod `cacheConfigSchema` validation path in favor of the new `validateForSave` function.
- Removed the effect that auto-seeded a default provider/model/dimension on first load, since direct-only mode no longer requires those fields.
- Per-request override documentation expanded to include `x-bf-cache-key` and `x-bf-cache-no-store` with clearer descriptions.

## Type of change

- [ ] Bug fix
- [ ] Feature
- [x] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [x] UI (React)
- [ ] Docs

## How to test

```sh
cd ui
pnpm i || npm i
pnpm build || npm run build
```

1. Navigate to the Workspace → Config → Caching view.
2. Verify the page loads with **Direct only** selected by default and no provider/model/dimension fields visible.
3. Switch to **Direct + Semantic** and confirm provider, model, and dimension fields appear with inline validation.
4. Toggle caching on without a vector store configured and confirm the toggle is disabled.
5. Save a direct-only config and confirm the plugin is created/updated with `dimension: 1` and no provider fields.
6. Save a semantic config with a valid provider, model, and dimension and confirm the full payload is persisted.
7. Reload the page and confirm the saved mode and config are correctly hydrated.

## Screenshots/Recordings

Before/after screenshots recommended showing the mode tab picker, the conditional semantic fields, and the structural change warning banner.

## Breaking changes

- [x] Yes
- [ ] No

The `PluginsForm` component is removed. Any code importing it directly will need to be updated. The enable/disable toggle now persists immediately rather than requiring a Save click, which changes the interaction model for existing users.

## Related issues

N/A

## Security considerations

No new auth, secrets, or PII handling introduced. API keys for embedding providers continue to be inherited from the provider's existing configuration and are not re-entered or stored in the cache config.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* fix: resolve cache plugin at request time to support post-boot loads and plugin reloads (#3423)

## Summary

The `CacheHandler` previously captured a reference to the `semantic_cache` plugin at boot time. This caused two bugs: (1) if the plugin was not present in `config.json` at startup, cache-clear routes were never registered, resulting in HTTP 405 for the entire process lifetime; (2) if the plugin was loaded or reloaded via `/api/plugins` after boot, the handler held a stale (or nil) pointer and would silently misbehave. Additionally, `GET /api/plugins/:name` was returning the raw plugin config without runtime status, causing the UI to see an empty status when refetching a single plugin.

## Changes

- `CacheHandler` now accepts a `CacheClearerResolver` function instead of a concrete plugin pointer. The resolver is called on every cache-clear request, so plugin lifecycle changes via `/api/plugins` are always honored.
- `CacheClearer` and `CacheClearerResolver` are exported so server wiring can supply the resolver without importing the plugin's concrete type.
- Cache routes are registered unconditionally at startup. When no plugin is loaded, requests return HTTP 400 with a descriptive message instead of HTTP 405.
- The server wiring in `RegisterAPIRoutes` uses a closure over `lib.FindPluginAs` to resolve the plugin per request, replacing the boot-time capture.
- `getPlugin` now returns the same response shape as list/create/update (with runtime status merged in), fixing the empty status seen by `useGetPluginQuery` in the UI.
- Tests cover the new "plugin not loaded" path for both `clearCache` and `clearCacheByKey`, and existing tests are updated to use the resolver-based constructor.

## Type of change

- [x] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [x] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./transports/bifrost-http/handlers/...
go test ./transports/bifrost-http/...
```

1. Start the server **without** `semantic_cache` in `config.json`. Issue `DELETE /api/cache/clear/{cacheId}` — expect HTTP 400 with `"semantic_cache plugin is not loaded"` (previously HTTP 405).
2. Load the `semantic_cache` plugin via `POST /api/plugins`. Repeat the request — expect the cache-clear to succeed.
3. Reload or remove the plugin via `PUT`/`DELETE /api/plugins`. Verify the handler reflects the new state on the next request without a server restart.
4. Issue `GET /api/plugins/{name}` for a loaded plugin and confirm the response includes runtime status fields, matching the shape returned by the list endpoint.

## Breaking changes

- [x] Yes
- [ ] No

`NewCacheHandler` now accepts a `CacheClearerResolver` function instead of a `schemas.LLMPlugin`. Any caller constructing a `CacheHandler` directly must be updated to pass a resolver.

## Related issues

## Security considerations

None. The change does not affect authentication, secrets, or PII handling.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* fix: decouple cache telemetry from write decision and guard no-op search paths in semantic cache (#3424)

## Summary

Fixes several correctness issues in the semantic cache plugin's `PostLLMHook` and related helpers: cache telemetry (`cache_debug`) was previously invisible to callers using `no-store`, cache-hit replay detection was fragile, non-positive per-request TTL overrides could silently kill cache writes, and requests with a `cache_type` header narrowed to a path the plugin cannot serve would still produce orphan cache entries.

## Changes

- **Early exit for unsupported search paths in `PreLLMHook`**: When `resolveCacheTypes` resolves to a path the plugin cannot actually serve (e.g. `x-bf-cache-type=semantic` against a direct-only plugin, or an unknown header value), the hook now clears cache state and returns early instead of proceeding to generate an embedding or write an orphan entry under a random request UUID that no future read can match.

- **Separated cache-hit replay handling from write-skip logic**: The `shouldSkipCaching` method (which conflated cache-hit detection with write-skip conditions) is replaced by `shouldSkipCacheWrite`. Cache-hit replay is now handled as a dedicated early return in `PostLLMHook` before any telemetry stamping, while `shouldSkipCacheWrite` gates only the write decision after telemetry is already stamped. This ensures `cache_debug` is always populated for callers using `no-store` or large-payload modes.

- **Telemetry stamped before write decision**: `stampCacheDebugForMiss` is now called before `shouldSkipCacheWrite` is consulted, so observability is not conditional on whether the entry is ultimately written.

- **Non-positive TTL overrides fall back to plugin default**: `resolveTTL` now treats a zero or negative per-request TTL override as "use default" rather than applying it, which would have set `expires_at=now` and silently discarded the cache write.

- **Cleaned up stale comments**: Removed an outdated ordering constraint comment in `PostLLMHook` that no longer applies after the restructuring.

- **Tests updated**: Test cases for `shouldSkipCaching` are renamed and updated to reflect the new `shouldSkipCacheWrite` contract. The cache-hit replay test case is removed from this suite (it is now an early return in `PostLLMHook`, not a condition inside the helper). A new default-is-false test is added.

## Type of change

- [x] Bug fix
- [ ] Feature
- [x] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./plugins/semanticcache/...
```

Validate the following scenarios:
- A request with `x-bf-cache-type=semantic` against a plugin configured with `Provider=""` or `Dimension=1` should log a warning and skip caching entirely — no orphan entry should appear in the store.
- A request with `Cache-Control: no-store` should still produce a populated `cache_debug` field in the response with `cache_hit=false`.
- A per-request TTL override of `0s` should fall back to the plugin's configured default TTL and not silently discard the cache write.

## Breaking changes

- [x] No

## Security considerations

None.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* test: add semantic cache e2e test suite skeleton (#3425)

## Summary

Adds a standalone end-to-end test suite for the `semantic_cache` plugin under `tests/semanticcache`. The suite validates the full caching lifecycle against a live Bifrost instance — plugin creation/teardown, cache miss/hit assertions, cross-provider behavior, streaming, and log cross-checking — without provisioning any infrastructure itself.

## Changes

- **`e2e_test.go`** — `TestMain` entry point: loads config, initializes the report directory, checks Bifrost reachability, enforces plugin-absent precondition (with `RUN_FORCE=1` auto-delete), runs all phases, and performs best-effort teardown on exit.
- **`preconditions_test.go`** — Phase 0 checks: Bifrost reachable, OpenAI configured, optional providers (Gemini, Anthropic) present with warnings if absent.
- **`http_test.go`** — HTTP helpers for all request types: chat completions (streaming and non-streaming), text completions, embeddings, image generation, and the Responses API. Each helper dumps full request/response bodies to the report directory for forensics.
- **`plugin_test.go`** — Plugin lifecycle helpers (`pluginCreate`, `pluginUpdate`, `pluginDelete`, `pluginGet`) mirroring the exact wire format the UI sends to `/api/plugins`.
- **`assert_test.go`** — Assertion helpers (`assertMiss`, `assertHit`, `assertNoCacheDebug`, `assertSameCacheID`, `assertDifferentCacheID`) plus a configurable async write-settle wait (`SC_WRITE_SETTLE_MS`) to account for the plugin's async PostLLMHook store write.
- **`cache_test.go`** — Cache management helpers (`clearByCacheID`, `clearByCacheKey`) wrapping the `/api/cache/clear/*` endpoints.
- **`logs_crosscheck_test.go`** — Cross-checks the persisted log row's `cache_debug` against the in-flight response stamp, with polling to handle Bifrost's async logging pipeline and float epsilon tolerance for JSON encoder differences.
- **`fixtures_test.go`** — Hand-curated paraphrase pairs for Phase 2 semantic cases, designed to land well above (canonical→paraphrase) or well below (canonical→unrelated) the default 0.8 similarity threshold.
- **`log_test.go`** — Structured per-run logging to `reports/<UTC-timestamp>/run.log` with optional `TRAIL_SESSION_ID` stamping for trail integration.
- **`go.mod`** — Standalone module (`github.com/maximhq/bifrost/tests/semanticcache`), consistent with the `tests/governance` pattern, excluded from the repo's `go.work`.
- **`README.md`** — Documents prerequisites, env vars, run commands, trail integration, and report output format.
- **`.gitignore`** — Excludes `reports/` and `*.log` from version control.

Notable design decisions: the suite is intentionally verify-only (no infrastructure provisioning), uses a dedicated vector store namespace (`BifrostSemanticCachePluginE2E`) to isolate test data, and writes full wire-level request/response artifacts per step to support post-mortem debugging without re-running.

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [x] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

Requires a running Bifrost instance with Weaviate configured, OpenAI (required), and optionally Gemini and Anthropic providers.

```sh
cd tests/semanticcache

# All phases
GOWORK=off go test -v ./...

# Single phase
GOWORK=off go test -v -run TestPhase1_DirectOnly ./...

# Auto-delete any pre-existing plugin row before run
RUN_FORCE=1 GOWORK=off go test -v ./...

# Keep plugin after run for post-mortem inspection
RUN_KEEP_PLUGIN=1 GOWORK=off go test -v ./...
```

Environment variables:

| Variable | Default | Purpose |
|---|---|---|
| `BIFROST_URL` | `http://localhost:8080` | Bifrost base URL |
| `SC_CHAT_MODEL_OPENAI` | `openai/gpt-4o-mini` | OpenAI chat model |
| `SC_CHAT_MODEL_OPENAI_ALT` | `openai/gpt-4o` | Alternate OpenAI model for cache-by-model cases |
| `SC_EMBED_MODEL_OPENAI` | `text-embedding-3-small` | Embedding model for Phase 2 |
| `SC_CHAT_MODEL_GEMINI` | `gemini/gemini-2.5-flash` | Gemini chat model |
| `SC_CHAT_MODEL_ANTHROPIC` | `anthropic/claude-haiku-4-5` | Anthropic chat model |
| `SC_NAMESPACE` | `BifrostSemanticCachePluginE2E` | Vector store namespace |
| `SC_WRITE_SETTLE_MS` | `500` | Async write settle wait in ms |
| `RUN_FORCE` | unset | `1` to delete pre-existing plugin before run |
| `RUN_KEEP_PLUGIN` | unset | `1` to skip teardown on exit |
| `TRAIL_SESSION_ID` | unset | Stamped onto every log line for trail integration |

## Screenshots/Recordings

N/A

## Breaking changes

- [x] No

## Related issues

N/A

## Security considerations

No secrets are stored in the test suite. API keys are consumed from the existing Bifrost provider configuration and never passed directly through the test harness.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [x] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* test: add direct cache e2e test suite (#3426)

## Summary

Adds a comprehensive end-to-end test suite (`TestDirect`) for the semantic cache plugin operating in direct-only mode. The suite covers 55 test cases (plan §1.1–1.55) validating cache hit/miss behavior, key isolation, TTL handling, config flag mutations, normalization, streaming, multi-endpoint support, parameter hashing, tool definitions, and cache management operations.

## Changes

- Introduces `tests/semanticcache/direct_test.go` with `TestDirect`, covering:
  - **Basic hit/miss and key isolation** (1.1, 1.2, 1.3, 1.4)
  - **`cache_by_model` and `cache_by_provider` flag behavior** (1.5–1.8), including serial config-mutation cases that restore baseline via `t.Cleanup`
  - **`exclude_system_prompt` flag** (1.9, 1.10)
  - **Conversation threshold boundary conditions** (1.11, 1.12)
  - **TTL expiry, per-request TTL override, invalid TTL fallback, and zero/negative TTL fallback** (1.13, 1.14, 1.15, 1.54)
  - **`no-store` header semantics**, including case-sensitivity and explicit `false` value (1.16, 1.17, 1.45, 1.46)
  - **`cache-type` header behavior** in direct-only mode, including the `semantic` header bug case (1.18, 1.19)
  - **Streaming SSE**: hit/miss, chunk replay order, and non-final chunk cache_debug absence (1.24, 1.25, 1.47)
  - **Multi-endpoint coverage**: text completions, responses API, embeddings, and image generation (1.20–1.23)
  - **Input normalization**: case folding, whitespace trimming, Unicode, and large prompts (1.26–1.29)
  - **Image attachment hashing**: same URL hits, different URL misses (1.30, 1.31)
  - **Edge cases**: nil content messages, empty messages array, unknown cache ID deletion (1.42, 1.43, 1.40)
  - **Parameter hash isolation**: temperature, top_p, seed, max_tokens, top_logprobs, tools (order-independent and name-change), prompt_cache_key, service_tier, store flag (1.32–1.37, 1.48–1.52)
  - **Cache management**: clear by cache ID, clear by key (1.38, 1.39)
  - **Plugin status round-trip** via GET (1.44)
  - **`/api/logs` cross-check**: verifies persisted `cache_debug` matches in-flight response stamp (1.55)
  - **`responses` API `previous_response_id` isolation** (1.53)
  - **Threshold header no-op in direct-only mode** (1.41)
- Adds helper functions: `simpleChat`, `chatWithSystem`, `chatWithImage`, `restoreDirectBaseline`, `assertHitAndReturnCacheDebug`
- Establishes a parallelism contract: cases that mutate plugin config run serially (no `t.Parallel()`); all others run concurrently with unique cache keys to prevent collisions

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [x] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
# Run the full direct-mode suite
go test ./tests/semanticcache/... -run TestDirect -v -timeout 300s

# Skip the expensive image generation case
SC_SKIP_IMAGE_GEN=1 go test ./tests/semanticcache/... -run TestDirect -v -timeout 300s
```

Required environment variables (same as the broader semantic cache e2e suite):
- `OPENAI_MODEL` — primary OpenAI-compatible model (e.g. `openai/gpt-4o-mini`)
- `OPENAI_MODEL_ALT` — secondary model for cross-model isolation cases
- `OPENAI_EMBED` — embedding model name (e.g. `text-embedding-3-small`)
- `ANTHRO_MODEL` — (optional) Anthropic model; cases 1.7 and 1.8 skip if unset
- `SC_SKIP_IMAGE_GEN=1` — (optional) skip case 1.23 to avoid DALL-E costs

## Screenshots/Recordings

N/A — test-only change.

## Breaking changes

- [x] No

## Related issues

## Security considerations

No new auth, secrets, or PII surface. Test prompts are benign and do not contain sensitive data.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* test: add semantic cache e2e test suite (#3427)

## Summary

Adds a comprehensive integration test suite for the semantic cache mode (Phase 2), covering the full lifecycle of semantic similarity-based cache hits and misses using Weaviate as the vector store and OpenAI's `text-embedding-3-small` as the embedding model. This suite validates that the semantic cache behaves correctly across a wide range of real-world scenarios, complementing the existing direct-mode (Phase 1) tests.

## Changes

- Added `TestParaphraseFixtures` to pre-flight all paraphrase pairs against the live embedding model, asserting cosine similarity thresholds before any semantic cache cases run. This prevents flaky downstream failures caused by borderline fixture pairs.
- Added `TestSemantic` containing 44 sub-cases (2.1–2.44) covering:
  - Semantic hit on paraphrase, miss on unrelated content
  - Per-request threshold overrides (relax, tighten, clamp above/below valid range)
  - `x-bf-cache-type` header forcing direct-only or semantic-only lookup paths
  - Cache key and model/provider isolation in semantic mode
  - `cache_by_model=false` and `cache_by_provider=false` cross-model/cross-provider hits
  - Streaming replay of semantic hits, including tool call preservation
  - TTL expiry, per-request TTL, TTL=0 fallback, and `no-store` header semantics
  - Namespace isolation and dimension-change silent miss behavior
  - Embedding endpoint bypass (semantic search skipped for `/v1/embeddings`)
  - Image generation and Responses API semantic hits
  - Text completion semantic hits
  - Gemini provider with OpenAI embedding provider
  - `params_hash` isolation (temperature, service tier, store flag, prompt cache key, previous response ID)
  - `exclude_system_prompt` flag effect on semantic matching
  - Conversation message threshold skipping semantic search
  - Attachment URL changes causing misses
  - `cache_debug` field presence and correctness on hits and misses, including log endpoint cross-check
  - Streaming chunk-level `cache_debug` placement (final chunk only)
- Serial (non-parallel) cases that mutate plugin config restore baseline via `t.Cleanup` to avoid test pollution.
- A dedicated Weaviate namespace (`cfg.Namespace + "Semantic"`) is used to avoid dimension conflicts with the Phase 1 direct-mode namespace.

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [x] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
# Run fixture pre-flight (requires OpenAI embedding access)
go test ./tests/semanticcache/... -run TestParaphraseFixtures -v

# Run full semantic suite
go test ./tests/semanticcache/... -run TestSemantic -v -timeout 10m

# Skip fixture verification if embedding access is unavailable
SC_SKIP_FIXTURE_VERIFY=1 go test ./tests/semanticcache/... -run TestSemantic -v -timeout 10m

# Skip image generation cases if DALL-E is unavailable
SC_SKIP_IMAGE_GEN=1 go test ./tests/semanticcache/... -run TestSemantic -v -timeout 10m
```

Required environment/config:
- `cfg.OpenAIEmbed` — embedding model name (e.g. `text-embedding-3-small`)
- `cfg.OpenAIModel` / `cfg.OpenAIModelAlt` — chat models for isolation tests
- `cfg.AnthroModel` — optional; skipped if empty (case 2.13)
- `cfg.GeminiModel` — optional; skipped if empty (case 2.28)
- `cfg.Namespace` — base Weaviate namespace; suite appends `Semantic` suffix
- `SC_SKIP_FIXTURE_VERIFY=1` — skip embedding pre-flight
- `SC_SKIP_IMAGE_GEN=1` — skip DALL-E case

## Screenshots/Recordings

N/A

## Breaking changes

- [x] No

## Related issues

N/A

## Security considerations

No new auth, secrets, or PII handling introduced. Tests call live external APIs (OpenAI, optionally Anthropic/Gemini) and require valid credentials in the test environment; no credentials are hardcoded.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* test: add semantic cache plugin lifecycle tests (#3428)

## Summary

Adds an end-to-end lifecycle test for the semantic cache plugin, covering the full disable → re-enable → delete → recreate flow and asserting that namespace data persists across each state transition.

## Changes

- Introduces `TestLifecycle` in `tests/semanticcache/lifecycle_test.go`, which runs 10 serial subtests (3.1–3.10) exercising the plugin's lifecycle state machine:
  - **3.1** – Disabling the plugin via PUT sets `enabled=false` and `status=disabled`
  - **3.2** – Requests while disabled bypass the cache pipeline entirely (no `cache_debug` header)
  - **3.3 / 3.4** – Cache-clear endpoints (`/api/cache/clear/{id}` and `/api/cache/clear-by-key/{k}`) return HTTP 400 when the plugin is not loaded
  - **3.5** – Re-enabling via PUT restores `enabled=true` and `status=active`
  - **3.6** – Entries written before disable are still queryable after re-enable
  - **3.7** – DELETE removes both the DB row and the in-memory plugin instance
  - **3.8** – Requests after delete bypass the cache pipeline (no `cache_debug` header)
  - **3.9** – Recreating the plugin with the same config succeeds and surfaces `status=active`
  - **3.10** – Entries written before delete are still queryable after recreate, validating the namespace-persistence contract introduced by the removal of `CleanUpOnShutdown`
- Tests are intentionally serial (no `t.Parallel()`) because each subtest mutates globally shared plugin lifecycle state
- A `t.Cleanup` handler performs best-effort key clearing regardless of which lifecycle state the plugin is left in at teardown

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [x] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./tests/semanticcache/... -run TestLifecycle -v
```

Expected outcome: all 10 subtests (3.1–3.10) pass, with structured log output at each step confirming correct status transitions and cache hit/miss behaviour.

## Breaking changes

- [x] No

## Related issues

## Security considerations

None. Tests run against a local Bifrost instance and do not introduce new auth paths, secrets handling, or PII exposure.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* feat: add `test-semantic-cache` and `test-semantic-cache-complete` Makefile targets (#3429)

## Summary

Adds Makefile targets for running `semantic_cache` plugin unit tests and end-to-end tests, with optional integration of the `trail` CLI for capture-based debugging sessions.

## Changes

- Added `test-semantic-cache` target that runs e2e tests from `tests/semanticcache`, supporting a `CACHE_TYPE` variable (`direct` or `semantic`) to filter which test phases are executed. Automatically wraps the run in `trail run` if the `trail` binary is available on `PATH`.
- Added `test-semantic-cache-complete` target that runs both the plugin unit tests (`plugins/semanticcache`) and the e2e tests in sequence, optionally wrapping the entire session in a single `trail run` invocation.
- Added `_test-semantic-cache-complete-inner` as an internal helper target that performs the actual sequential execution of unit and e2e tests with formatted output banners.
- Registered all three new targets in the `.PHONY` declaration.

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [x] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
# Run all semantic_cache e2e tests
make test-semantic-cache

# Run only direct cache tests
CACHE_TYPE=direct make test-semantic-cache

# Run only semantic cache tests
CACHE_TYPE=semantic make test-semantic-cache

# Run both unit and e2e tests together
make test-semantic-cache-complete

# Force e2e run regardless of preconditions
RUN_FORCE=1 make test-semantic-cache-complete
```

If `trail` is installed and on `PATH`, all commands will automatically wrap execution in a `trail run` session for capture-based debugging.

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

## Security considerations

None.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* harness improvements (#3457)

* makefile diff fixes (#3462)

## Summary

Briefly explain the purpose of this PR and the problem it solves.

## Changes

- What was changed and why
- Any notable design decisions or trade-offs

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

Describe the steps to validate this change. Include commands and expected outcomes.

```sh
# Core/Transports
go version
go test ./...

# UI
cd ui
pnpm i || npm i
pnpm test || npm test
pnpm build || npm run build
```

If adding new configs or environment variables, document them here.

## Screenshots/Recordings

If UI changes, add before/after screenshots or short clips.

## Breaking changes

- [ ] Yes
- [ ] No

If yes, describe impact and migration instructions.

## Related issues

Link related issues and discussions. Example: Closes #123

## Security considerations

Note any security implications (auth, secrets, PII, sandboxing, etc.).

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* Preserve Anthropic output schema refs (#3449)

* feat: use the new parameter json schema compliant to json schema spec (#3444)

* feat: replace log delete button with actions dropdown menu and pin actions column (#3480)

## Summary

Replaces the direct delete button in the logs and MCP logs action columns with a dropdown menu triggered by a `MoreHorizontal` icon. This improves the UI by providing a more scalable actions pattern while keeping the delete functionality accessible. The actions column is also now properly pinned to the right side of the table when the user has delete access.

## Changes

- Replaced the inline destructive `Trash2` button with a `DropdownMenu` containing a "Delete" item for both logs and MCP logs tables
- The actions column trigger is now a ghost `MoreHorizontal` icon button, reducing visual noise in the table
- The actions column is pinned to the right only when `hasDeleteAccess` is true; otherwise no fixed columns are configured
- Fixed `fixedColumnIds` to include `"actions"` so the column receives correct sticky positioning behavior
- Removed `overflow-hidden` from pinned cells in the MCP logs table to prevent the dropdown from being clipped
- Reduced the actions column size from 72 to 56px

## Type of change

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [x] UI (React)
- [ ] Docs

## How to test

1. Navigate to the Logs page as a user with delete access.
2. Confirm the actions column is pinned to the right of the table.
3. Click the `⋯` icon on any row and verify the dropdown appears with a "Delete" option.
4. Click "Delete" and confirm the log is deleted without the row click handler firing.
5. Repeat on the MCP Logs page.
6. Log in as a user without delete access and confirm the actions column is not present.

```sh
cd ui
pnpm i
pnpm build
```

## Screenshots/Recordings

_Add before/after screenshots showing the old delete button vs. the new dropdown._

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

## Security considerations

No new security implications. Delete access gating remains unchanged.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* fix: constrain model catalog table column widths and truncate overflowing text (#3481)

## Summary

Fixes layout overflow issues in the Model Catalog table where long provider names and model badge text would break out of their columns or cause uneven column sizing.

## Changes

- Added `table-fixed` layout with explicit `<colgroup>` column widths (26% / 44% / 16% / 14%) to enforce stable column proportions
- Added `overflow-hidden` and `truncate` to the Provider name cell so long names are clipped cleanly instead of overflowing
- Added `shrink-0` to the "CUSTOM" badge so it doesn't compress when the provider name is long
- Added `max-w-[220px] truncate` to model name badges in `ModelsUsedCell` to prevent …
occcat pushed a commit to occcat/bifrost that referenced this pull request Sep 2, 2026
## Summary

Adds a standalone end-to-end test suite for the `semantic_cache` plugin under `tests/semanticcache`. The suite validates the full caching lifecycle against a live Bifrost instance — plugin creation/teardown, cache miss/hit assertions, cross-provider behavior, streaming, and log cross-checking — without provisioning any infrastructure itself.

## Changes

- **`e2e_test.go`** — `TestMain` entry point: loads config, initializes the report directory, checks Bifrost reachability, enforces plugin-absent precondition (with `RUN_FORCE=1` auto-delete), runs all phases, and performs best-effort teardown on exit.
- **`preconditions_test.go`** — Phase 0 checks: Bifrost reachable, OpenAI configured, optional providers (Gemini, Anthropic) present with warnings if absent.
- **`http_test.go`** — HTTP helpers for all request types: chat completions (streaming and non-streaming), text completions, embeddings, image generation, and the Responses API. Each helper dumps full request/response bodies to the report directory for forensics.
- **`plugin_test.go`** — Plugin lifecycle helpers (`pluginCreate`, `pluginUpdate`, `pluginDelete`, `pluginGet`) mirroring the exact wire format the UI sends to `/api/plugins`.
- **`assert_test.go`** — Assertion helpers (`assertMiss`, `assertHit`, `assertNoCacheDebug`, `assertSameCacheID`, `assertDifferentCacheID`) plus a configurable async write-settle wait (`SC_WRITE_SETTLE_MS`) to account for the plugin's async PostLLMHook store write.
- **`cache_test.go`** — Cache management helpers (`clearByCacheID`, `clearByCacheKey`) wrapping the `/api/cache/clear/*` endpoints.
- **`logs_crosscheck_test.go`** — Cross-checks the persisted log row's `cache_debug` against the in-flight response stamp, with polling to handle Bifrost's async logging pipeline and float epsilon tolerance for JSON encoder differences.
- **`fixtures_test.go`** — Hand-curated paraphrase pairs for Phase 2 semantic cases, designed to land well above (canonical→paraphrase) or well below (canonical→unrelated) the default 0.8 similarity threshold.
- **`log_test.go`** — Structured per-run logging to `reports/<UTC-timestamp>/run.log` with optional `TRAIL_SESSION_ID` stamping for trail integration.
- **`go.mod`** — Standalone module (`github.com/maximhq/bifrost/tests/semanticcache`), consistent with the `tests/governance` pattern, excluded from the repo's `go.work`.
- **`README.md`** — Documents prerequisites, env vars, run commands, trail integration, and report output format.
- **`.gitignore`** — Excludes `reports/` and `*.log` from version control.

Notable design decisions: the suite is intentionally verify-only (no infrastructure provisioning), uses a dedicated vector store namespace (`BifrostSemanticCachePluginE2E`) to isolate test data, and writes full wire-level request/response artifacts per step to support post-mortem debugging without re-running.

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [x] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

Requires a running Bifrost instance with Weaviate configured, OpenAI (required), and optionally Gemini and Anthropic providers.

```sh
cd tests/semanticcache

# All phases
GOWORK=off go test -v ./...

# Single phase
GOWORK=off go test -v -run TestPhase1_DirectOnly ./...

# Auto-delete any pre-existing plugin row before run
RUN_FORCE=1 GOWORK=off go test -v ./...

# Keep plugin after run for post-mortem inspection
RUN_KEEP_PLUGIN=1 GOWORK=off go test -v ./...
```

Environment variables:

| Variable | Default | Purpose |
|---|---|---|
| `BIFROST_URL` | `http://localhost:8080` | Bifrost base URL |
| `SC_CHAT_MODEL_OPENAI` | `openai/gpt-4o-mini` | OpenAI chat model |
| `SC_CHAT_MODEL_OPENAI_ALT` | `openai/gpt-4o` | Alternate OpenAI model for cache-by-model cases |
| `SC_EMBED_MODEL_OPENAI` | `text-embedding-3-small` | Embedding model for Phase 2 |
| `SC_CHAT_MODEL_GEMINI` | `gemini/gemini-2.5-flash` | Gemini chat model |
| `SC_CHAT_MODEL_ANTHROPIC` | `anthropic/claude-haiku-4-5` | Anthropic chat model |
| `SC_NAMESPACE` | `BifrostSemanticCachePluginE2E` | Vector store namespace |
| `SC_WRITE_SETTLE_MS` | `500` | Async write settle wait in ms |
| `RUN_FORCE` | unset | `1` to delete pre-existing plugin before run |
| `RUN_KEEP_PLUGIN` | unset | `1` to skip teardown on exit |
| `TRAIL_SESSION_ID` | unset | Stamped onto every log line for trail integration |

## Screenshots/Recordings

N/A

## Breaking changes

- [x] No

## Related issues

N/A

## Security considerations

No secrets are stored in the test suite. API keys are consumed from the existing Bifrost provider configuration and never passed directly through the test harness.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [x] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable
occcat pushed a commit to occcat/bifrost that referenced this pull request Sep 2, 2026
* feat: add granular RBAC checks for API keys, inference, metrics, and filter inaccessible sidebar items (#3295)

This PR improves RBAC granularity in the sidebar by introducing dedicated resource types for `APIKeys`, `Inference`, and `Metrics`, and fixes sidebar visibility logic so that items and groups are hidden when the user lacks access rather than relying on broader, less specific permissions.

- Added three new `RbacResource` enum values: `APIKeys`, `Inference`, and `Metrics` to the fallback RBAC context.
- The API Keys sidebar item now gates access via the new `hasAPIKeyAccess` (`RbacResource.APIKeys`) check instead of the generic `hasSettingsAccess`.
- The MCP Logs sidebar item now correctly gates access via `hasMCPGatewayAccess` instead of the unrelated `hasLogsAccess`.
- Introduced an `accessibleItems` memoized computation that filters out sidebar items and entire groups whose sub-items are all inaccessible, ensuring users never see empty navigation sections. Previously, access filtering only happened during search.
- Removed unused imports (`PanelLeft`, `PanelRight`, `cn`).

- [ ] Bug fix
- [x] Feature
- [x] Refactor
- [ ] Documentation
- [ ] Chore/CI

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [x] UI (React)
- [ ] Docs

1. Log in as a user with restricted RBAC permissions that exclude `APIKeys` and/or `Settings`.
2. Verify the API Keys entry under the Config section is hidden for users without `APIKeys` view permission.
3. Verify the MCP Logs entry is hidden for users without `MCPGateway` view permission.
4. Verify that sidebar groups with no accessible sub-items are hidden entirely rather than showing an empty group.
5. Verify that users with full access see no change in sidebar behavior.

```sh
cd ui
pnpm i || npm i
pnpm build || npm run build
```

_Add before/after screenshots showing sidebar items hidden for restricted users._

- [ ] Yes
- [x] No

_Link related issues here._

Access control checks for API Keys management are now scoped to a dedicated `APIKeys` RBAC resource rather than the broader `Settings` resource, reducing the risk of unintended access to key management for users who have settings visibility but should not manage API keys.

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* fix: hide delete log button instead of disabling it when user lacks delete access (#3314)

The delete button in log tables was always rendered (just disabled) for users without delete access. This PR hides the actions column entirely when the user lacks delete permissions, and fixes the RBAC resource check for MCP logs to use the correct `MCPGateway` resource instead of `Logs`.

- The actions column in both the workspace logs and MCP logs tables is now conditionally included in the column definitions only when `hasDeleteAccess` is `true`, rather than always rendering a disabled button.
- The delete button styling was updated to use more visible destructive colors (`text-destructive/60 border-destructive/60`) instead of the previous muted secondary foreground styles.
- The RBAC resource used to gate delete access on the MCP logs page was corrected from `RbacResource.Logs` to `RbacResource.MCPGateway`.

- [x] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [x] UI (React)
- [ ] Docs

1. Log in as a user **without** delete access on Logs or MCPGateway resources.
2. Navigate to the workspace logs page and the MCP logs page.
3. Verify the delete button/column is not visible.
4. Log in as a user **with** delete access.
5. Verify the delete button appears and is functional.

```sh
cd ui
pnpm i
pnpm test
pnpm build
```

Before: Delete button rendered but disabled for users without access.
After: Delete column is hidden entirely for users without delete access.

- [ ] Yes
- [x] No

The RBAC fix ensures MCP log deletion is gated on the correct `MCPGateway` resource permission, preventing users with only `Logs` delete access from incorrectly being granted delete access to MCP logs.

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* feat: add `MCPLogs` RBAC resource and enforce access control on MCP logs route and sidebar (#3316)

Introduces a dedicated `MCPLogs` RBAC resource, decoupling MCP log access control from the `MCPGateway` resource. This allows permissions for viewing and deleting MCP logs to be managed independently from gateway-level permissions.

- Added `MCPLogs` as a new `RbacResource` enum value in the fallback RBAC context.
- The MCP Logs route now checks `MCPLogs` view permission and renders a `NoPermissionView` when access is denied, rather than rendering the page unconditionally.
- Delete access on the MCP Logs page now checks `RbacResource.MCPLogs` instead of `RbacResource.MCPGateway`.
- The sidebar MCP Logs entry now uses `hasMCPLogsAccess` (derived from `RbacResource.MCPLogs`) to control visibility, rather than reusing `hasMCPGatewayAccess`.

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [x] UI (React)
- [ ] Docs

1. Configure a role that has `MCPGateway` access but **no** `MCPLogs` access.
2. Log in as a user with that role and navigate to the MCP Logs page — the `NoPermissionView` should be displayed and the sidebar entry should be hidden.
3. Grant the role `MCPLogs` view access and confirm the page and sidebar entry become accessible.
4. Verify that delete functionality on the MCP Logs page is gated by `MCPLogs` delete permission independently of `MCPGateway` delete permission.

```sh
cd ui
pnpm i || npm i
pnpm test || npm test
pnpm build || npm run build
```

N/A

- [x] Yes
- [ ] No

Any role configuration that previously relied on `MCPGateway` permissions to grant access to MCP Logs will need to be updated to explicitly grant `MCPLogs` permissions.

N/A

Access to MCP log data (which may contain sensitive tool execution details) is now enforced by a dedicated RBAC resource, reducing the risk of unintended access through overly broad `MCPGateway` permissions.

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* fix: replace unsafe inline jsonb cast with `bifrost_safe_jsonb` PL/pgSQL helper to prevent malformed JSON from aborting list queries (#3407)

## Summary

The `/api/logs` list query was aborting entirely when a single row contained malformed JSON in `input_history` or `responses_input_history`. The previous inline guard only checked the first character before casting to `jsonb`, so rows that appeared array-shaped but contained malformed JSON (unterminated structures, trailing commas, unpaired UTF-16 surrogates, `\u0000` escapes, etc.) would trigger a `22P02`/`22P05` error and kill the entire response. This PR fixes that by introducing a PL/pgSQL helper function (`bifrost_safe_jsonb`) that wraps the cast in an `EXCEPTION` block and falls back to returning the raw text on any parse failure.

## Changes

- Added a new migration `migrationAddSafeJsonbFunction` that installs the `bifrost_safe_jsonb(text)` PL/pgSQL function on Postgres. The function validates the input, attempts the `jsonb` cast inside an `EXCEPTION` block, and returns the last array element on success or the raw text on any failure.
- Replaced the multi-condition inline `CASE` guards in `listSelectColumns` for Postgres with calls to `bifrost_safe_jsonb`, simplifying the SQL and correctly handling all malformed-JSON edge cases that the previous character-check approach missed.
- For SQLite, added `json_valid()`, `json_type()`, and `json_array_length()` guards to the `CASE` expressions to prevent extraction attempts on invalid or empty arrays.
- Added `safe_jsonb_test.go` covering both the SQLite and Postgres dialect branches of `listSelectColumns`, as well as direct invocation of `bifrost_safe_jsonb` across all relevant edge cases (malformed structures, surrogate pairs, `\u0000` escapes, non-array values, SQL `NULL`).

## Type of change

- [x] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
cd framework && docker compose up -d postgres

go test ./framework/logstore/ -run 'MalformedInputHistory|BifrostSafeJsonb' -count=1 -v
```

Insert a row into the logs table with a malformed JSON value in `input_history` (e.g., `[{"key": "val"` — unterminated) and verify that a call to the list endpoint returns successfully without a 500 error, with the malformed row's `input_history` returned as raw text rather than aborting the query.

## Test Coverage

### `TestSearchLogs_MalformedInputHistory_{SQLite,Postgres}` — end-to-end list query

| # | Case | Column | Payload shape | Pre-fix behavior | Path exercised |
| --- | --- | --- | --- | --- | --- |
| 1 | `unterminated_object_in_array` | `input_history` | `[{"role":"user","content":"hi"` | 22P02 aborts query | EXCEPTION fallback |
| 2 | `garbage_after_bracket` | `input_history` | `[abc, not json]` | 22P02 aborts query | EXCEPTION fallback |
| 3 | `trailing_comma` | `input_history` | `[{"role":"user","content":"hi"},]` | 22P02 aborts query | EXCEPTION fallback |
| 4 | `unclosed_array_only` | `input_history` | `[` | 22P02 aborts query | EXCEPTION fallback |
| 5 | `open_bracket_then_brace_unclosed` | `input_history` | `[{` | 22P02 aborts query | EXCEPTION fallback |
| 6 | `nan_value_not_valid_json` | `input_history` | `[NaN]` | 22P02 aborts query | EXCEPTION fallback |
| 7 | `infinity_value_not_valid_json` | `input_history` | `[Infinity]` | 22P02 aborts query | EXCEPTION fallback |
| 8 | `unpaired_high_surrogate` | `input_history` | `[{"...":"bad \uD800 surrogate"}]` | 22P05 aborts query | EXCEPTION fallback |
| 9 | `unpaired_low_surrogate` | `input_history` | `[{"...":"bad \uDC00 low"}]` | 22P05 aborts query | EXCEPTION fallback |
| 10 | `bad_surrogate_pair_high_then_ascii` | `input_history` | `[{"c":"\uD800A"}]` | 22P05 aborts query | EXCEPTION fallback |
| 11 | `u0000_escape_inside_string` | `input_history` | `[{"...":"null byte � here"}]` | 22P05 aborts query | EXCEPTION fallback |
| 12 | `literal_backslash_u0000_valid_jsonb` | `input_history` | `[{"...":"... \\u0000 literal"}]` | OK (degraded to raw by old guard) | Fast path, last-element extraction |
| 13 | `single_element_array` | `input_history` | `[{"role":"user","content":"only one"}]` | OK | Fast path |
| 14 | `array_of_primitives` | `input_history` | `[1,2,3]` | OK | Fast path |
| 15 | `array_with_null_last_element` | `input_history` | `[{...}, null]` | OK | Fast path |
| 16 | `deeply_nested_valid` | `input_history` | `[{"role":"user","content":{"nested":{"deep":{"value":42}}}}]` | OK | Fast path |
| 17 | `unicode_emoji_content` | `input_history` | `[{"...":"hello 🎉 world ✨"}]` | OK | Fast path |
| 18 | `large_valid_array` | `input_history` | 1001-element array | OK | Fast path at scale |
| 19 | `leading_whitespace_then_array` | `input_history` | `   [\t{...}]` | OK | `btrim` + fast path |
| 20 | `top_level_object_not_array` | `input_history` | `{"not":"an array"}` | OK | Non-array fall-through |
| 21 | `null_literal` | `input_history` | `null` | OK | Non-array fall-through |
| 22 | `whitespace_only` | `input_history` | `"   \t  "` | OK | Empty-after-btrim fall-through |
| 23 | `realtime_turn_malformed_passthrough` | `input_history` (object_type=`realtime.turn`) | `[{"role":"user"` | OK (outer CASE bypassed safe fn) | Realtime-turn bypass branch |
| 24 | `malformed_responses_input_history` | `responses_input_history` | `[{"role":"user"` | 22P02 aborts query | Mirror column, EXCEPTION fallback |
| 25 | `valid_responses_input_history` | `responses_input_history` | `[{...},{...}]` | OK | Mirror column, fast path |

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

[https://github.com/maximhq/bifrost/issues/3255](https://github.com/maximhq/bifrost/issues/3255#issuecomment-4427506449)

## Security considerations

None. The function is `IMMUTABLE` and operates only on text values already stored in the database. No new inputs are exposed.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* feat: add required headers input to prompt playground settings panel (#3412)

## Summary

Adds support for server-configured required request headers in the prompt playground. When the server specifies `required_headers` in its client config, users can now provide values for those headers directly in the settings panel, and they are forwarded with every chat completion request.

## Changes

- Added `customHeaders` state and `requiredHeaders` derived from the core config's `client_config.required_headers` to the `PromptContext`, keeping header keys in sync with the server config while preserving user-entered values.
- Exposed a "Required Headers" section in the settings panel that renders an input field for each required header name when any are configured.
- Extended `ExecutionConfig` in the executor to accept `customHeaders`, which are merged into the fetch request headers (skipping any entries with empty names or values).
- Passed `customHeaders` through both `handleSubmit` and `handleSubmitToolResult` execution paths and included it in their respective `useCallback` dependency arrays.

## Type of change

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [x] UI (React)
- [ ] Docs

## How to test

1. Configure `required_headers` in the server's client config (e.g., `["X-My-Custom-Header"]`).
2. Open the prompt playground and navigate to the settings panel.
3. Verify a "Required Headers" section appears with an input for each configured header name.
4. Enter a value for each header and send a chat completion request.
5. Confirm the header is present in the outgoing request.
6. Remove a header from the server config and verify it disappears from the UI without affecting other header values.

```sh
cd ui
pnpm i || npm i
pnpm test || npm test
pnpm build || npm run build
```

## Screenshots/Recordings

_Add before/after screenshots of the settings panel showing the new Required Headers section._

## Breaking changes

- [x] No

## Related issues

## Security considerations

Header values are entered by the user and sent only to the configured backend endpoint. Empty header names or values are explicitly skipped before being added to the request, preventing accidental forwarding of blank headers. Users should be cautious not to enter sensitive credentials unless the connection to the server is secured.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* fix: skip pagination clamp for virtual keys export requests (#3416)

## Summary

When exporting virtual keys, pagination clamping was being applied unnecessarily, which could interfere with retrieving the full dataset. This PR skips the pagination limit/offset clamping when the export flag is set, while still ensuring the offset is non-negative.

## Changes

- Pagination clamping via `ClampPaginationParams` is now bypassed when `params.Export` is `true`, allowing exports to retrieve data without artificially constrained limits
- A minimal guard ensures `params.Offset` is still set to `0` if negative during an export request

## Type of change

- [x] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [x] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

Trigger a virtual keys export request and verify that all keys are returned without being truncated by pagination limits. Compare the export result count against the total number of virtual keys in the system.

```sh
go test ./...
```

## Screenshots/Recordings

N/A

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

https://github.com/maximhq/bifrost/issues/3414

## Security considerations

No additional security implications. Export access is still gated by existing authentication and authorization checks.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* chore: bump `@maximhq/bifrost` to v1.6.3 (#3417)

## Summary

Bumps the `@maximhq/bifrost` NPX package version to `1.6.3` to align the `package.json` and `package-lock.json` version fields, which were previously out of sync.

## Changes

- Updated `package.json` version from `1.6.2` to `1.6.3`
- Corrected `package-lock.json` to reflect `1.6.3` consistently across both the lockfile root and the package entry (previously mismatched at `1.0.6` and `1.0.4`)

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [x] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
cd npx/bifrost
npm install
npm pack --dry-run
# Verify the reported version is 1.6.3
```

## Screenshots/Recordings

N/A

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

N/A

## Security considerations

No security implications.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* feat: add volume histogram chart to MCP logs page and fix drag-select bar click suppression (#3431)

Adds a log volume histogram chart to the MCP Logs page, matching the existing chart behavior on the main Logs page. Also fixes a bug where clicking a bar immediately after a drag-select would overwrite the dragged time range with a single-bucket zoom.

- Added `useGetMCPHistogramQuery` to the MCP Logs page to fetch histogram data with optional polling, and rendered the `LogsVolumeChart` component in the MCP Logs view.
- Added `handleTimeRangeChange`, `handleResetZoom`, and `isZoomed` logic to the MCP Logs page, mirroring the behavior already present on the main Logs page.
- Fixed `isZoomed` on the main Logs page to return `false` when a named `period` (e.g. `"1h"`) is active, so resetting zoom correctly clears the zoomed state.
- When resetting zoom, `period: "1h"` and `polling: true` are now explicitly set in URL state to ensure the page returns to a live-polling relative range.
- Fixed a race condition in `LogsVolumeChart` where Recharts fires a Bar `onClick` event immediately after a drag-select `mouseUp`, which was overwriting the dragged range with a single-bucket zoom. A `suppressNextBarClickRef` ref is set during drag completion and cleared on the next bar click to suppress the spurious event.

- [x] Bug fix
- [x] Feature

- [x] UI (React)

1. Navigate to the MCP Logs page and confirm the log volume histogram chart renders and updates with polling.
2. Click a bar in the histogram and confirm the time range zooms into that bucket.
3. Drag-select a range on the histogram and confirm the time range updates to the dragged selection without immediately snapping to a single bucket.
4. Click "Reset Zoom" and confirm the chart returns to the default 1-hour live-polling view.

```sh
cd ui
pnpm i
pnpm build
```

Before: MCP Logs page had no histogram chart.
After: MCP Logs page displays the log volume histogram with zoom, drag-select, and reset zoom functionality identical to the main Logs page.

- [x] No

None.

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* refactor: semantic cache plugin (#3210)

## Summary

This PR refactors the semantic cache plugin to simplify its internal state management, improves cache lookup correctness, and adds a new `cache_hit_types` filter to the logs API and UI. The direct cache lookup path is now a single deterministic point-fetch by a UUIDv5 `directCacheID` (replacing the previous dual-path of chunk lookup + legacy metadata scan), and several context keys are consolidated. The UI gains a "Local Caching" filter sidebar section and cache hit type badges in the log detail view.

## Changes

- **Semantic cache plugin refactor:**
  - Replaced the dual direct-search path (`performDirectChunkLookup` + `performLegacyDirectSearch`) with a single `performDirectSearch` that does an O(1) `GetChunk` by deterministic `directCacheID` (UUIDv5 derived from provider, model, cacheKey, requestHash, paramsHash).
  - `generateDirectCacheID` now returns an error instead of silently falling back to a string concatenation, making failures explicit.
  - `request_hash` is no longer stored as a top-level metadata field; it is encoded into the `directCacheID` instead.
  - Reduced context keys from ~10 to 4 (`directCacheIDKey`, `paramsHashKey`, `embeddingsKey`, `embeddingsInputTokensKey`), removing stale keys like `requestIDKey`, `requestHashKey`, `isCacheHitKey`, and `cacheHitTypeKey`.
  - `shouldSkipCaching` is extracted into its own method; cache-hit detection now reads `CacheDebug.CacheHit` from the response rather than a context flag.
  - `buildUnifiedMetadata` no longer accepts `requestHash` as a parameter.
  - `addSingleResponse` renamed to `addNonStreamingResponse`.
  - `StreamAccumulator` fields `HasError`, `FinalTimestamp`, and `FinishReason` on `StreamChunk` are removed; error streams are handled by early return in `PostLLMHook`.
  - Streaming replay goroutine now guards every send with `ctx.Done()` to prevent goroutine leaks on dropped consumers.
  - A background `runStreamCleanupLoop` goroutine (started by `Init`, stopped by `Cleanup` via `stopCh`) replaces the one-shot cleanup call, periodically reaping stale stream accumulators.
  - `buildResponseFromResult` now accepts `threshold`, `similarity`, and `inputTokens` as pointers, and `attachCacheDebug` is extracted as a shared helper for both streaming and non-streaming paths.
  - `isExpiredEntry` is extracted as a standalone function.
  - `chunkSortKey` replaces the large inline sort comparator in `processAccumulatedStream`.
  - Tools, stop sequences, modalities, include lists, and other order-insensitive set fields are now hashed with `hashSortedSet` / `sortedStringSet` to prevent MCP's randomized map iteration from perturbing the request hash.
  - `extractAttachmentsForCaching` is extracted so attachment URLs are included in the cache key metadata rather than the embedding text.
  - `extractTextForEmbedding` no longer returns a `paramsHash`; callers compute it once via `buildRequestMetadataForCaching` + `hashMap`.
  - `generateEmbedding` moved from `utils.go` to `search.go`.
  - `generateRequestHash` now accepts prebuilt metadata to avoid recomputing it.
  - `removeField` no longer mutates the input slice's backing array.
  - Added `PronunciationDictionaryLocators`, `TimestampGranularities`, `Include`, `AdditionalFormats`, and `InputImages` to their respective parameter metadata extractors.
  - Public context key names changed from `semantic_cache_*` to `semantic_cache-*` (underscore → hyphen separator after the plugin prefix).
  - `SelectFields` no longer includes `request_hash`.
  - `VectorStoreProperties` no longer includes a `request_hash` entry.
  - `CacheByModel` and `CacheByProvider` default-value log messages added.

- **Log filtering — `cache_hit_types`:**
  - Added `CacheHitTypes []string` to `SearchFilters` in `framework/logstore/tables.go`.
  - `applyFilters` in `rdb.go` applies a JSON path filter on `cache_debug` for both SQLite (`json_extract`) and PostgreSQL (`substring` regex) dialects, restricted to the allowlist `["direct", "semantic"]`.
  - `canUseMatViewFilters` excludes queries with `CacheHitTypes` set from the materialized-view fast path.
  - HTTP handlers (`getLogs`, `getLogsStats`, `parseHistogramFilters`) parse a `cache_hit_types` comma-separated query parameter.

- **UI:**
  - Added a "Local Caching" filter section to `LogsFilterSidebar` with checkboxes for "Direct cache" and "Semantic cache".
  - `cache_hit_types` is added to URL state, filter state, and the `buildFilterParams` API helper.
  - Log detail view shows "Direct Cache" (indigo) and "Semantic Cache" (rose) badges based on `cache_debug.hit_type`.
  - Plugins form now filters the provider dropdown to embedding-capable providers only (`EmbeddingSupportedProviders` for built-ins; `custom_provider_config.allowed_requests.embedding` for custom providers), shows an error message when no embedding provider is configured, and disables the toggle accordingly.
  - Embedding model input replaced with `ModelMultiselect` (single-select mode) scoped to the selected provider.
  - Provider dropdown clears the embedding model when the provider changes.
  - Provider icons rendered in the provider dropdown.
  - `EmbeddingSupportedProviders` constant added to `ui/lib/constants/logs.ts`.

- **Misc:**
  - HTTP request logging in `CorsMiddleware` and an auth debug log are commented out.
  - `transports/bifrost-http/v1.5.x` added to `.gitignore`.
  - Minor formatting fixes in `core/schemas/bifrost.go` and `framework/modelcatalog/sync.go`.
  - Missing newline at end of `sync.go` added.

## Type of change

- [ ] Bug fix
- [x] Feature
- [x] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [x] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [x] UI (React)
- [ ] Docs

## How to test

```sh
# Core/Transports
go test ./plugins/semanticcache/...
go test ./framework/logstore/...
go test ./transports/bifrost-http/...

# UI
cd ui
pnpm i
pnpm build
```

- Configure the semantic cache plugin with a direct and/or semantic cache type and verify that cache hits are recorded with the correct `hit_type` in `cache_debug`.
- Query `/logs?cache_hit_types=direct` and `/logs?cache_hit_types=semantic` and confirm only matching entries are returned.
- In the UI, open the logs filter sidebar and verify the "Local Caching" section appears with "Direct cache" and "Semantic cache" checkboxes that correctly filter the log list.
- Open a log detail for a cache hit and confirm the appropriate badge ("Direct Cache" or "Semantic Cache") is displayed.
- In the plugins form, verify that only embedding-capable providers appear in the provider dropdown and that the embedding model field uses the model multiselect.

## Breaking changes

- [x] Yes

The public semantic cache context key names have changed from `semantic_cache_*` to `semantic_cache-*`. Any caller setting `CacheKey`, `CacheTTLKey`, `CacheThresholdKey`, `CacheTypeKey`, or `CacheNoStoreKey` via the old string values will no longer be recognized by the plugin. Update all call sites to use the exported constants from the plugin package rather than raw string literals.

`request_hash` is no longer stored as a top-level metadata field in the vector store. Existing cache entries written by prior versions will not be found by the new direct-search path (they will be treated as misses and re-populated).

`ClearCacheForRequestID` is documented as currently broken for entries written by the new direct-search path; callers should not rely on it until the TODO is resolved.

## Related issues

N/A

## Security considerations

The `CacheHitTypes` filter allowlists values to `"direct"` and `"semantic"` before interpolating them into SQL, preventing arbitrary input from reaching the JSON path expression.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* feat: remove `cleanup_on_shutdown` from semantic cache plugin config (#3330)

## Summary

Removes the `cleanup_on_shutdown` option from the semantic cache plugin. Cache data now always persists between Bifrost restarts. The previous behavior of deleting all cache entries and the vector store namespace on shutdown is no longer supported.

## Changes

- Removed `CleanUpOnShutdown` field from `Config` struct in `plugins/semanticcache/main.go` and stripped the corresponding shutdown deletion logic from `Cleanup()`
- Removed `cleanup_on_shutdown` from the JSON config schema (`transports/config.schema.json`), Helm values schema (`helm-charts/bifrost/values.schema.json`), Helm template helper (`_helpers.tpl`), and default `values.yaml`
- Removed `cleanup_on_shutdown` from all example Kubernetes values files and documentation code samples
- Added migration guide entry (Breaking Change 16) in `docs/migration-guides/v1.5.0.mdx` describing the removal, how to clear cache data using the existing invalidation endpoints, and how to handle dimension/provider/model rotation without the old escape hatch
- Updated the semantic caching feature docs to remove references to `cleanup_on_shutdown` and the associated warning block
- Removed `TestCleanup_DeletesEntriesAndNamespaceWhenEnabled` test and simplified `newTestPlugin` helper to drop the `cleanupOnShutdown` parameter across all test files

## Type of change

- [ ] Bug fix
- [ ] Feature
- [x] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [x] Docs

## How to test

```sh
go test ./plugins/semanticcache/...
```

Verify that passing `cleanup_on_shutdown` in a semantic cache plugin config is rejected by schema validation. Confirm that restarting Bifrost with a semantic cache configured leaves existing vector store entries intact.

## Breaking changes

- [x] Yes
- [ ] No

The `cleanup_on_shutdown` field is removed from the semantic cache plugin config schema and will be rejected by validation. Remove it from `config.json`, Helm values, and any `PUT /api/config` payloads. To clear cache data, use `DELETE /api/cache/clear/{cacheId}`, `DELETE /api/cache/clear-by-key/{cacheKey}`, or rotate `vector_store_namespace` to a fresh name.

## Related issues

See Breaking Change 16 in the v1.5.0 migration guide.

## Security considerations

None.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [x] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* refactor: semantic cache ui revamp (#3331)

## Summary

Replaces the separate `PluginsForm` component with a fully self-contained `CachingView` that introduces a first-class **Direct / Direct + Semantic** mode toggle for the local cache plugin. Previously, the UI only exposed provider-backed semantic cache settings and had no concept of direct-only (hash-based) caching as a distinct, supported mode. This rewrite makes direct-only mode the default and gates semantic configuration behind an explicit mode selection.

## Changes

- Deleted `pluginsForm.tsx` and consolidated all local cache configuration logic directly into `cachingView.tsx`.
- Introduced a `CacheMode` type (`"direct"` | `"semantic"`) with a tab-based picker. Direct-only mode requires no embedding provider; semantic mode adds vector similarity on top and requires a provider, model, and dimension.
- The enable/disable toggle now immediately calls `updatePlugin` or `createPlugin` (for first-time setup) rather than deferring the enabled-state change to the Save button, decoupling the plugin lifecycle from config edits.
- Added `inferMode` to derive the active mode from a saved config, `isEmptyConfig` to detect zero-value configs from the API, `buildPayload` to strip semantic-only fields when persisting a direct-only config, and `validateForSave` for inline validation surfaced before the user clicks Save.
- Structural change warnings (provider/model/dimension drift vs. server state) are now shown only when the user has actually modified those fields, rather than permanently in semantic mode.
- Removed the Zod `cacheConfigSchema` validation path in favor of the new `validateForSave` function.
- Removed the effect that auto-seeded a default provider/model/dimension on first load, since direct-only mode no longer requires those fields.
- Per-request override documentation expanded to include `x-bf-cache-key` and `x-bf-cache-no-store` with clearer descriptions.

## Type of change

- [ ] Bug fix
- [ ] Feature
- [x] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [x] UI (React)
- [ ] Docs

## How to test

```sh
cd ui
pnpm i || npm i
pnpm build || npm run build
```

1. Navigate to the Workspace → Config → Caching view.
2. Verify the page loads with **Direct only** selected by default and no provider/model/dimension fields visible.
3. Switch to **Direct + Semantic** and confirm provider, model, and dimension fields appear with inline validation.
4. Toggle caching on without a vector store configured and confirm the toggle is disabled.
5. Save a direct-only config and confirm the plugin is created/updated with `dimension: 1` and no provider fields.
6. Save a semantic config with a valid provider, model, and dimension and confirm the full payload is persisted.
7. Reload the page and confirm the saved mode and config are correctly hydrated.

## Screenshots/Recordings

Before/after screenshots recommended showing the mode tab picker, the conditional semantic fields, and the structural change warning banner.

## Breaking changes

- [x] Yes
- [ ] No

The `PluginsForm` component is removed. Any code importing it directly will need to be updated. The enable/disable toggle now persists immediately rather than requiring a Save click, which changes the interaction model for existing users.

## Related issues

N/A

## Security considerations

No new auth, secrets, or PII handling introduced. API keys for embedding providers continue to be inherited from the provider's existing configuration and are not re-entered or stored in the cache config.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* fix: resolve cache plugin at request time to support post-boot loads and plugin reloads (#3423)

## Summary

The `CacheHandler` previously captured a reference to the `semantic_cache` plugin at boot time. This caused two bugs: (1) if the plugin was not present in `config.json` at startup, cache-clear routes were never registered, resulting in HTTP 405 for the entire process lifetime; (2) if the plugin was loaded or reloaded via `/api/plugins` after boot, the handler held a stale (or nil) pointer and would silently misbehave. Additionally, `GET /api/plugins/:name` was returning the raw plugin config without runtime status, causing the UI to see an empty status when refetching a single plugin.

## Changes

- `CacheHandler` now accepts a `CacheClearerResolver` function instead of a concrete plugin pointer. The resolver is called on every cache-clear request, so plugin lifecycle changes via `/api/plugins` are always honored.
- `CacheClearer` and `CacheClearerResolver` are exported so server wiring can supply the resolver without importing the plugin's concrete type.
- Cache routes are registered unconditionally at startup. When no plugin is loaded, requests return HTTP 400 with a descriptive message instead of HTTP 405.
- The server wiring in `RegisterAPIRoutes` uses a closure over `lib.FindPluginAs` to resolve the plugin per request, replacing the boot-time capture.
- `getPlugin` now returns the same response shape as list/create/update (with runtime status merged in), fixing the empty status seen by `useGetPluginQuery` in the UI.
- Tests cover the new "plugin not loaded" path for both `clearCache` and `clearCacheByKey`, and existing tests are updated to use the resolver-based constructor.

## Type of change

- [x] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [x] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./transports/bifrost-http/handlers/...
go test ./transports/bifrost-http/...
```

1. Start the server **without** `semantic_cache` in `config.json`. Issue `DELETE /api/cache/clear/{cacheId}` — expect HTTP 400 with `"semantic_cache plugin is not loaded"` (previously HTTP 405).
2. Load the `semantic_cache` plugin via `POST /api/plugins`. Repeat the request — expect the cache-clear to succeed.
3. Reload or remove the plugin via `PUT`/`DELETE /api/plugins`. Verify the handler reflects the new state on the next request without a server restart.
4. Issue `GET /api/plugins/{name}` for a loaded plugin and confirm the response includes runtime status fields, matching the shape returned by the list endpoint.

## Breaking changes

- [x] Yes
- [ ] No

`NewCacheHandler` now accepts a `CacheClearerResolver` function instead of a `schemas.LLMPlugin`. Any caller constructing a `CacheHandler` directly must be updated to pass a resolver.

## Related issues

## Security considerations

None. The change does not affect authentication, secrets, or PII handling.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* fix: decouple cache telemetry from write decision and guard no-op search paths in semantic cache (#3424)

## Summary

Fixes several correctness issues in the semantic cache plugin's `PostLLMHook` and related helpers: cache telemetry (`cache_debug`) was previously invisible to callers using `no-store`, cache-hit replay detection was fragile, non-positive per-request TTL overrides could silently kill cache writes, and requests with a `cache_type` header narrowed to a path the plugin cannot serve would still produce orphan cache entries.

## Changes

- **Early exit for unsupported search paths in `PreLLMHook`**: When `resolveCacheTypes` resolves to a path the plugin cannot actually serve (e.g. `x-bf-cache-type=semantic` against a direct-only plugin, or an unknown header value), the hook now clears cache state and returns early instead of proceeding to generate an embedding or write an orphan entry under a random request UUID that no future read can match.

- **Separated cache-hit replay handling from write-skip logic**: The `shouldSkipCaching` method (which conflated cache-hit detection with write-skip conditions) is replaced by `shouldSkipCacheWrite`. Cache-hit replay is now handled as a dedicated early return in `PostLLMHook` before any telemetry stamping, while `shouldSkipCacheWrite` gates only the write decision after telemetry is already stamped. This ensures `cache_debug` is always populated for callers using `no-store` or large-payload modes.

- **Telemetry stamped before write decision**: `stampCacheDebugForMiss` is now called before `shouldSkipCacheWrite` is consulted, so observability is not conditional on whether the entry is ultimately written.

- **Non-positive TTL overrides fall back to plugin default**: `resolveTTL` now treats a zero or negative per-request TTL override as "use default" rather than applying it, which would have set `expires_at=now` and silently discarded the cache write.

- **Cleaned up stale comments**: Removed an outdated ordering constraint comment in `PostLLMHook` that no longer applies after the restructuring.

- **Tests updated**: Test cases for `shouldSkipCaching` are renamed and updated to reflect the new `shouldSkipCacheWrite` contract. The cache-hit replay test case is removed from this suite (it is now an early return in `PostLLMHook`, not a condition inside the helper). A new default-is-false test is added.

## Type of change

- [x] Bug fix
- [ ] Feature
- [x] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./plugins/semanticcache/...
```

Validate the following scenarios:
- A request with `x-bf-cache-type=semantic` against a plugin configured with `Provider=""` or `Dimension=1` should log a warning and skip caching entirely — no orphan entry should appear in the store.
- A request with `Cache-Control: no-store` should still produce a populated `cache_debug` field in the response with `cache_hit=false`.
- A per-request TTL override of `0s` should fall back to the plugin's configured default TTL and not silently discard the cache write.

## Breaking changes

- [x] No

## Security considerations

None.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* test: add semantic cache e2e test suite skeleton (#3425)

## Summary

Adds a standalone end-to-end test suite for the `semantic_cache` plugin under `tests/semanticcache`. The suite validates the full caching lifecycle against a live Bifrost instance — plugin creation/teardown, cache miss/hit assertions, cross-provider behavior, streaming, and log cross-checking — without provisioning any infrastructure itself.

## Changes

- **`e2e_test.go`** — `TestMain` entry point: loads config, initializes the report directory, checks Bifrost reachability, enforces plugin-absent precondition (with `RUN_FORCE=1` auto-delete), runs all phases, and performs best-effort teardown on exit.
- **`preconditions_test.go`** — Phase 0 checks: Bifrost reachable, OpenAI configured, optional providers (Gemini, Anthropic) present with warnings if absent.
- **`http_test.go`** — HTTP helpers for all request types: chat completions (streaming and non-streaming), text completions, embeddings, image generation, and the Responses API. Each helper dumps full request/response bodies to the report directory for forensics.
- **`plugin_test.go`** — Plugin lifecycle helpers (`pluginCreate`, `pluginUpdate`, `pluginDelete`, `pluginGet`) mirroring the exact wire format the UI sends to `/api/plugins`.
- **`assert_test.go`** — Assertion helpers (`assertMiss`, `assertHit`, `assertNoCacheDebug`, `assertSameCacheID`, `assertDifferentCacheID`) plus a configurable async write-settle wait (`SC_WRITE_SETTLE_MS`) to account for the plugin's async PostLLMHook store write.
- **`cache_test.go`** — Cache management helpers (`clearByCacheID`, `clearByCacheKey`) wrapping the `/api/cache/clear/*` endpoints.
- **`logs_crosscheck_test.go`** — Cross-checks the persisted log row's `cache_debug` against the in-flight response stamp, with polling to handle Bifrost's async logging pipeline and float epsilon tolerance for JSON encoder differences.
- **`fixtures_test.go`** — Hand-curated paraphrase pairs for Phase 2 semantic cases, designed to land well above (canonical→paraphrase) or well below (canonical→unrelated) the default 0.8 similarity threshold.
- **`log_test.go`** — Structured per-run logging to `reports/<UTC-timestamp>/run.log` with optional `TRAIL_SESSION_ID` stamping for trail integration.
- **`go.mod`** — Standalone module (`github.com/maximhq/bifrost/tests/semanticcache`), consistent with the `tests/governance` pattern, excluded from the repo's `go.work`.
- **`README.md`** — Documents prerequisites, env vars, run commands, trail integration, and report output format.
- **`.gitignore`** — Excludes `reports/` and `*.log` from version control.

Notable design decisions: the suite is intentionally verify-only (no infrastructure provisioning), uses a dedicated vector store namespace (`BifrostSemanticCachePluginE2E`) to isolate test data, and writes full wire-level request/response artifacts per step to support post-mortem debugging without re-running.

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [x] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

Requires a running Bifrost instance with Weaviate configured, OpenAI (required), and optionally Gemini and Anthropic providers.

```sh
cd tests/semanticcache

# All phases
GOWORK=off go test -v ./...

# Single phase
GOWORK=off go test -v -run TestPhase1_DirectOnly ./...

# Auto-delete any pre-existing plugin row before run
RUN_FORCE=1 GOWORK=off go test -v ./...

# Keep plugin after run for post-mortem inspection
RUN_KEEP_PLUGIN=1 GOWORK=off go test -v ./...
```

Environment variables:

| Variable | Default | Purpose |
|---|---|---|
| `BIFROST_URL` | `http://localhost:8080` | Bifrost base URL |
| `SC_CHAT_MODEL_OPENAI` | `openai/gpt-4o-mini` | OpenAI chat model |
| `SC_CHAT_MODEL_OPENAI_ALT` | `openai/gpt-4o` | Alternate OpenAI model for cache-by-model cases |
| `SC_EMBED_MODEL_OPENAI` | `text-embedding-3-small` | Embedding model for Phase 2 |
| `SC_CHAT_MODEL_GEMINI` | `gemini/gemini-2.5-flash` | Gemini chat model |
| `SC_CHAT_MODEL_ANTHROPIC` | `anthropic/claude-haiku-4-5` | Anthropic chat model |
| `SC_NAMESPACE` | `BifrostSemanticCachePluginE2E` | Vector store namespace |
| `SC_WRITE_SETTLE_MS` | `500` | Async write settle wait in ms |
| `RUN_FORCE` | unset | `1` to delete pre-existing plugin before run |
| `RUN_KEEP_PLUGIN` | unset | `1` to skip teardown on exit |
| `TRAIL_SESSION_ID` | unset | Stamped onto every log line for trail integration |

## Screenshots/Recordings

N/A

## Breaking changes

- [x] No

## Related issues

N/A

## Security considerations

No secrets are stored in the test suite. API keys are consumed from the existing Bifrost provider configuration and never passed directly through the test harness.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [x] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* test: add direct cache e2e test suite (#3426)

## Summary

Adds a comprehensive end-to-end test suite (`TestDirect`) for the semantic cache plugin operating in direct-only mode. The suite covers 55 test cases (plan §1.1–1.55) validating cache hit/miss behavior, key isolation, TTL handling, config flag mutations, normalization, streaming, multi-endpoint support, parameter hashing, tool definitions, and cache management operations.

## Changes

- Introduces `tests/semanticcache/direct_test.go` with `TestDirect`, covering:
  - **Basic hit/miss and key isolation** (1.1, 1.2, 1.3, 1.4)
  - **`cache_by_model` and `cache_by_provider` flag behavior** (1.5–1.8), including serial config-mutation cases that restore baseline via `t.Cleanup`
  - **`exclude_system_prompt` flag** (1.9, 1.10)
  - **Conversation threshold boundary conditions** (1.11, 1.12)
  - **TTL expiry, per-request TTL override, invalid TTL fallback, and zero/negative TTL fallback** (1.13, 1.14, 1.15, 1.54)
  - **`no-store` header semantics**, including case-sensitivity and explicit `false` value (1.16, 1.17, 1.45, 1.46)
  - **`cache-type` header behavior** in direct-only mode, including the `semantic` header bug case (1.18, 1.19)
  - **Streaming SSE**: hit/miss, chunk replay order, and non-final chunk cache_debug absence (1.24, 1.25, 1.47)
  - **Multi-endpoint coverage**: text completions, responses API, embeddings, and image generation (1.20–1.23)
  - **Input normalization**: case folding, whitespace trimming, Unicode, and large prompts (1.26–1.29)
  - **Image attachment hashing**: same URL hits, different URL misses (1.30, 1.31)
  - **Edge cases**: nil content messages, empty messages array, unknown cache ID deletion (1.42, 1.43, 1.40)
  - **Parameter hash isolation**: temperature, top_p, seed, max_tokens, top_logprobs, tools (order-independent and name-change), prompt_cache_key, service_tier, store flag (1.32–1.37, 1.48–1.52)
  - **Cache management**: clear by cache ID, clear by key (1.38, 1.39)
  - **Plugin status round-trip** via GET (1.44)
  - **`/api/logs` cross-check**: verifies persisted `cache_debug` matches in-flight response stamp (1.55)
  - **`responses` API `previous_response_id` isolation** (1.53)
  - **Threshold header no-op in direct-only mode** (1.41)
- Adds helper functions: `simpleChat`, `chatWithSystem`, `chatWithImage`, `restoreDirectBaseline`, `assertHitAndReturnCacheDebug`
- Establishes a parallelism contract: cases that mutate plugin config run serially (no `t.Parallel()`); all others run concurrently with unique cache keys to prevent collisions

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [x] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
# Run the full direct-mode suite
go test ./tests/semanticcache/... -run TestDirect -v -timeout 300s

# Skip the expensive image generation case
SC_SKIP_IMAGE_GEN=1 go test ./tests/semanticcache/... -run TestDirect -v -timeout 300s
```

Required environment variables (same as the broader semantic cache e2e suite):
- `OPENAI_MODEL` — primary OpenAI-compatible model (e.g. `openai/gpt-4o-mini`)
- `OPENAI_MODEL_ALT` — secondary model for cross-model isolation cases
- `OPENAI_EMBED` — embedding model name (e.g. `text-embedding-3-small`)
- `ANTHRO_MODEL` — (optional) Anthropic model; cases 1.7 and 1.8 skip if unset
- `SC_SKIP_IMAGE_GEN=1` — (optional) skip case 1.23 to avoid DALL-E costs

## Screenshots/Recordings

N/A — test-only change.

## Breaking changes

- [x] No

## Related issues

## Security considerations

No new auth, secrets, or PII surface. Test prompts are benign and do not contain sensitive data.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* test: add semantic cache e2e test suite (#3427)

## Summary

Adds a comprehensive integration test suite for the semantic cache mode (Phase 2), covering the full lifecycle of semantic similarity-based cache hits and misses using Weaviate as the vector store and OpenAI's `text-embedding-3-small` as the embedding model. This suite validates that the semantic cache behaves correctly across a wide range of real-world scenarios, complementing the existing direct-mode (Phase 1) tests.

## Changes

- Added `TestParaphraseFixtures` to pre-flight all paraphrase pairs against the live embedding model, asserting cosine similarity thresholds before any semantic cache cases run. This prevents flaky downstream failures caused by borderline fixture pairs.
- Added `TestSemantic` containing 44 sub-cases (2.1–2.44) covering:
  - Semantic hit on paraphrase, miss on unrelated content
  - Per-request threshold overrides (relax, tighten, clamp above/below valid range)
  - `x-bf-cache-type` header forcing direct-only or semantic-only lookup paths
  - Cache key and model/provider isolation in semantic mode
  - `cache_by_model=false` and `cache_by_provider=false` cross-model/cross-provider hits
  - Streaming replay of semantic hits, including tool call preservation
  - TTL expiry, per-request TTL, TTL=0 fallback, and `no-store` header semantics
  - Namespace isolation and dimension-change silent miss behavior
  - Embedding endpoint bypass (semantic search skipped for `/v1/embeddings`)
  - Image generation and Responses API semantic hits
  - Text completion semantic hits
  - Gemini provider with OpenAI embedding provider
  - `params_hash` isolation (temperature, service tier, store flag, prompt cache key, previous response ID)
  - `exclude_system_prompt` flag effect on semantic matching
  - Conversation message threshold skipping semantic search
  - Attachment URL changes causing misses
  - `cache_debug` field presence and correctness on hits and misses, including log endpoint cross-check
  - Streaming chunk-level `cache_debug` placement (final chunk only)
- Serial (non-parallel) cases that mutate plugin config restore baseline via `t.Cleanup` to avoid test pollution.
- A dedicated Weaviate namespace (`cfg.Namespace + "Semantic"`) is used to avoid dimension conflicts with the Phase 1 direct-mode namespace.

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [x] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
# Run fixture pre-flight (requires OpenAI embedding access)
go test ./tests/semanticcache/... -run TestParaphraseFixtures -v

# Run full semantic suite
go test ./tests/semanticcache/... -run TestSemantic -v -timeout 10m

# Skip fixture verification if embedding access is unavailable
SC_SKIP_FIXTURE_VERIFY=1 go test ./tests/semanticcache/... -run TestSemantic -v -timeout 10m

# Skip image generation cases if DALL-E is unavailable
SC_SKIP_IMAGE_GEN=1 go test ./tests/semanticcache/... -run TestSemantic -v -timeout 10m
```

Required environment/config:
- `cfg.OpenAIEmbed` — embedding model name (e.g. `text-embedding-3-small`)
- `cfg.OpenAIModel` / `cfg.OpenAIModelAlt` — chat models for isolation tests
- `cfg.AnthroModel` — optional; skipped if empty (case 2.13)
- `cfg.GeminiModel` — optional; skipped if empty (case 2.28)
- `cfg.Namespace` — base Weaviate namespace; suite appends `Semantic` suffix
- `SC_SKIP_FIXTURE_VERIFY=1` — skip embedding pre-flight
- `SC_SKIP_IMAGE_GEN=1` — skip DALL-E case

## Screenshots/Recordings

N/A

## Breaking changes

- [x] No

## Related issues

N/A

## Security considerations

No new auth, secrets, or PII handling introduced. Tests call live external APIs (OpenAI, optionally Anthropic/Gemini) and require valid credentials in the test environment; no credentials are hardcoded.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* test: add semantic cache plugin lifecycle tests (#3428)

## Summary

Adds an end-to-end lifecycle test for the semantic cache plugin, covering the full disable → re-enable → delete → recreate flow and asserting that namespace data persists across each state transition.

## Changes

- Introduces `TestLifecycle` in `tests/semanticcache/lifecycle_test.go`, which runs 10 serial subtests (3.1–3.10) exercising the plugin's lifecycle state machine:
  - **3.1** – Disabling the plugin via PUT sets `enabled=false` and `status=disabled`
  - **3.2** – Requests while disabled bypass the cache pipeline entirely (no `cache_debug` header)
  - **3.3 / 3.4** – Cache-clear endpoints (`/api/cache/clear/{id}` and `/api/cache/clear-by-key/{k}`) return HTTP 400 when the plugin is not loaded
  - **3.5** – Re-enabling via PUT restores `enabled=true` and `status=active`
  - **3.6** – Entries written before disable are still queryable after re-enable
  - **3.7** – DELETE removes both the DB row and the in-memory plugin instance
  - **3.8** – Requests after delete bypass the cache pipeline (no `cache_debug` header)
  - **3.9** – Recreating the plugin with the same config succeeds and surfaces `status=active`
  - **3.10** – Entries written before delete are still queryable after recreate, validating the namespace-persistence contract introduced by the removal of `CleanUpOnShutdown`
- Tests are intentionally serial (no `t.Parallel()`) because each subtest mutates globally shared plugin lifecycle state
- A `t.Cleanup` handler performs best-effort key clearing regardless of which lifecycle state the plugin is left in at teardown

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [x] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./tests/semanticcache/... -run TestLifecycle -v
```

Expected outcome: all 10 subtests (3.1–3.10) pass, with structured log output at each step confirming correct status transitions and cache hit/miss behaviour.

## Breaking changes

- [x] No

## Related issues

## Security considerations

None. Tests run against a local Bifrost instance and do not introduce new auth paths, secrets handling, or PII exposure.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* feat: add `test-semantic-cache` and `test-semantic-cache-complete` Makefile targets (#3429)

## Summary

Adds Makefile targets for running `semantic_cache` plugin unit tests and end-to-end tests, with optional integration of the `trail` CLI for capture-based debugging sessions.

## Changes

- Added `test-semantic-cache` target that runs e2e tests from `tests/semanticcache`, supporting a `CACHE_TYPE` variable (`direct` or `semantic`) to filter which test phases are executed. Automatically wraps the run in `trail run` if the `trail` binary is available on `PATH`.
- Added `test-semantic-cache-complete` target that runs both the plugin unit tests (`plugins/semanticcache`) and the e2e tests in sequence, optionally wrapping the entire session in a single `trail run` invocation.
- Added `_test-semantic-cache-complete-inner` as an internal helper target that performs the actual sequential execution of unit and e2e tests with formatted output banners.
- Registered all three new targets in the `.PHONY` declaration.

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [x] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
# Run all semantic_cache e2e tests
make test-semantic-cache

# Run only direct cache tests
CACHE_TYPE=direct make test-semantic-cache

# Run only semantic cache tests
CACHE_TYPE=semantic make test-semantic-cache

# Run both unit and e2e tests together
make test-semantic-cache-complete

# Force e2e run regardless of preconditions
RUN_FORCE=1 make test-semantic-cache-complete
```

If `trail` is installed and on `PATH`, all commands will automatically wrap execution in a `trail run` session for capture-based debugging.

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

## Security considerations

None.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* harness improvements (#3457)

* makefile diff fixes (#3462)

## Summary

Briefly explain the purpose of this PR and the problem it solves.

## Changes

- What was changed and why
- Any notable design decisions or trade-offs

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

Describe the steps to validate this change. Include commands and expected outcomes.

```sh
# Core/Transports
go version
go test ./...

# UI
cd ui
pnpm i || npm i
pnpm test || npm test
pnpm build || npm run build
```

If adding new configs or environment variables, document them here.

## Screenshots/Recordings

If UI changes, add before/after screenshots or short clips.

## Breaking changes

- [ ] Yes
- [ ] No

If yes, describe impact and migration instructions.

## Related issues

Link related issues and discussions. Example: Closes #123

## Security considerations

Note any security implications (auth, secrets, PII, sandboxing, etc.).

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* Preserve Anthropic output schema refs (#3449)

* feat: use the new parameter json schema compliant to json schema spec (#3444)

* feat: replace log delete button with actions dropdown menu and pin actions column (#3480)

## Summary

Replaces the direct delete button in the logs and MCP logs action columns with a dropdown menu triggered by a `MoreHorizontal` icon. This improves the UI by providing a more scalable actions pattern while keeping the delete functionality accessible. The actions column is also now properly pinned to the right side of the table when the user has delete access.

## Changes

- Replaced the inline destructive `Trash2` button with a `DropdownMenu` containing a "Delete" item for both logs and MCP logs tables
- The actions column trigger is now a ghost `MoreHorizontal` icon button, reducing visual noise in the table
- The actions column is pinned to the right only when `hasDeleteAccess` is true; otherwise no fixed columns are configured
- Fixed `fixedColumnIds` to include `"actions"` so the column receives correct sticky positioning behavior
- Removed `overflow-hidden` from pinned cells in the MCP logs table to prevent the dropdown from being clipped
- Reduced the actions column size from 72 to 56px

## Type of change

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [x] UI (React)
- [ ] Docs

## How to test

1. Navigate to the Logs page as a user with delete access.
2. Confirm the actions column is pinned to the right of the table.
3. Click the `⋯` icon on any row and verify the dropdown appears with a "Delete" option.
4. Click "Delete" and confirm the log is deleted without the row click handler firing.
5. Repeat on the MCP Logs page.
6. Log in as a user without delete access and confirm the actions column is not present.

```sh
cd ui
pnpm i
pnpm build
```

## Screenshots/Recordings

_Add before/after screenshots showing the old delete button vs. the new dropdown._

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

## Security considerations

No new security implications. Delete access gating remains unchanged.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* fix: constrain model catalog table column widths and truncate overflowing text (#3481)

## Summary

Fixes layout overflow issues in the Model Catalog table where long provider names and model badge text would break out of their columns or cause uneven column sizing.

## Changes

- Added `table-fixed` layout with explicit `<colgroup>` column widths (26% / 44% / 16% / 14%) to enforce stable column proportions
- Added `overflow-hidden` and `truncate` to the Provider name cell so long names are clipped cleanly instead of overflowing
- Added `shrink-0` to the "CUSTOM" badge so it doesn't compress when the provider name is long
- Added `max-w-[220px] truncate` to model name badges in `ModelsUsedCell` to prevent …
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants