Skip to content

feat(routetrace): add POST /v1/route/trace dry-run route explanation#247

Open
gr3enarr0w wants to merge 6 commits into
ferro-labs:mainfrom
gr3enarr0w:feat/route-trace-dry-run-238
Open

feat(routetrace): add POST /v1/route/trace dry-run route explanation#247
gr3enarr0w wants to merge 6 commits into
ferro-labs:mainfrom
gr3enarr0w:feat/route-trace-dry-run-238

Conversation

@gr3enarr0w

@gr3enarr0w gr3enarr0w commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements #238. Adds an authenticated, client-facing dry-run endpoint that explains how the active routing strategy would SELECT a target for a request WITHOUT calling any provider — zero token spend, no circuit perturbation. Unblocks Pi's Ferro router UX and any client that needs a safe "why would this route here?" probe.

POST /v1/route/trace
Body: { "model": "...", "messages": [...], "stream": false, "metadata": {...} }
Auth: Bearer MASTER_KEY / key-store key (same authed /v1/* group as /v1/models + /v1/chat/completions)
Always returns: dry_run: true

Response contract

{
  "strategy": "conditional",
  "requested_model": "qwen3.5:397b-cloud",
  "resolved_model": "qwen3.5:397b-cloud",
  "selected_target_key": "ollama-cloud",
  "selected_model": "qwen3.5:397b-cloud",
  "candidate_targets": [
    { "target_key": "ollama-cloud", "matched": true, "reason": "model condition matched",
      "healthy": true, "circuit_open": false, "supports_model": true,
      "weight": 1.0, "estimated_cost_usd": 0.0, "p50_latency_ms": 0, "has_latency_samples": false }
  ],
  "catalog": { "model_found": true, "priced": false, "context_window": 128000, "mode": "chat", "status": "ga" },
  "trace_id": "...",
  "dry_run": true
}
  • selected_target_key / selected_model are empty when no target is selectable (mirrors the gateway's "no provider supports model" error path).
  • candidate_targets[] is ordered by the strategy's preference; each carries matched, reason, healthy, circuit_open, supports_model, weight, estimated_cost_usd, p50_latency_ms, has_latency_samples.

Selection faithfulness

The tracer reproduces each strategy's Execute() selection rule from internal/strategies/:

Strategy Dry-run pick
single first target (only Targets[0] is a candidate)
fallback first healthy+model-supporting target
loadbalance deterministic first-eligible pick (real selection is probabilistic; dry-run avoids RNG)
conditional rule-matched target, else configured Targets[0] fallback
cost-optimized lowest-eligible-cost target (via models.Calculate zero-usage probe — same primitive as the live strategy)
least-latency lowest-P50-with-samples; unsampled eligible candidates sink last

If a strategy's selection rules change, the matching branch in routetrace.Trace() should be updated to keep the dry-run faithful (called out in the package doc).

Non-goals enforced (per #238)

  • No Pi-specific logic lands here. The tracer only consumes Ferro's existing routing primitives: targets/conditions/aliases, provider SupportsModel, circuit-breaker State, latency tracker P50, models.Calculate.
  • SynapseRouter is not reintroduced as a routing dependency.
  • Provider quotas untouched. Complete is never invoked; circuit counters are never incremented (verified by integration test asserting the stub provider's Complete call count stays at 0).

Gateway exposure

Two small MU-safe accessors added to *Gateway:

  • CircuitState(name) circuitbreaker.State — returns StateClosed when no breaker is configured; never perturbs counters.
  • LatencyTracker() *latency.Tracker — returns the tracker (nil when least-latency routing is inactive).

Tests

Suite Coverage
internal/routetrace/trace_test.go (9 unit) all 5 supported strategy modes + alias resolution + no-selection path + catalog-missing path
internal/handler/route_trace_test.go (4 handler) fallback selection, unknown model, missing-model 400, malformed-JSON 400 — asserts Complete is never called
test/integration/http/route_trace_test.go (3 e2e) production router: auth requires Bearer key (401 without), X-Request-Id header emitted, zero provider calls, structured response shape

go vet ./... clean; golangci-lint not run locally (not installed) — CI will gate.

Docs

  • README gets a "Dry-run route trace (no tokens spent)" example with curl + response description.
  • AGENTS.md endpoint table lists POST /v1/route/trace.

Closes #238.

Summary by CodeRabbit

  • New Features

    • Added an authenticated POST /v1/route/trace dry-run endpoint that previews which model/target would be selected—without contacting providers or consuming quota.
    • Responses include strategy and selected target/model, per-candidate support/health notes, circuit status, optional latency signals, and estimated cost/catalog match details.
    • Supports alias resolution and multiple routing strategies (fallback, conditional, cost-optimized, latency-aware).
  • Documentation

    • Documented the new dry-run route trace endpoint and full response fields.
  • Tests

    • Added unit and integration coverage for success, validation errors, and auth behavior.

Clark Everson added 3 commits May 21, 2026 13:01
…erro-labs#238)

Adds an authenticated, client-facing dry-run endpoint that explains how the
active routing strategy would SELECT a target for a request WITHOUT calling any
provider (zero token spend, no circuit perturbation). Unblocks Pi's Ferro
router UX and any client that needs a safe "why would this route here?" probe.

Endpoint
  POST /v1/route/trace
  Body: chat/completions-shaped { model, messages?, stream?, metadata? }
  Auth: same authed /v1/* group as /v1/models + /v1/chat/completions
        (Bearer MASTER_KEY / key-store key; inherits auth, audit, OTel, X-Request-ID).
  Always returns dry_run: true.

Response contract (TraceResponse):
  - strategy, requested_model, resolved_model (after alias resolution)
  - selected_target_key / selected_model (empty when no target is selectable,
    mirroring the gateway's "no provider supports model" error path)
  - candidate_targets[] ordered by the strategy's preference, each with:
      matched, reason, healthy, circuit_open, supports_model,
      weight, estimated_cost_usd, p50_latency_ms, has_latency_samples
  - catalog: { model_found, priced, context_window, mode, status, provider }

Selection mirrors the per-strategy Execute() in internal/strategies/: single
(first target), fallback (first healthy+supporting), loadbalance (eligible
deterministic pick), conditional (rule-matched target, else Targets[0]),
cost-optimized (lowest eligible cost via models.Calculate zero-usage probe),
least-latency (lowest P50-with-samples; unsampled eligible last). If a
strategy's selection rules change, update the matching branch in Trace().

No Pi-specific logic lands here; the tracer only consumes Ferro's existing
routing primitives (targets/conditions/aliases, provider SupportsModel,
circuit-breaker State, latency tracker P50, models.Calculate). SynapseRouter
is not reintroduced as a dependency.

Gateway exposure:
  - CircuitState(name) — MU-safe accessor (never perturbs counters).
  - LatencyTracker() — returns the latency tracker (nil when least-latency
    routing is inactive).

Tests:
  - internal/routetrace/trace_test.go: 9 unit tests covering all 5 supported
    strategy modes + alias resolution + no-selection path + catalog-missing path.
  - internal/handler/route_trace_test.go: 4 handler tests (fallback selection,
    unknown model, missing-model 400, malformed-JSON 400) asserting Complete is
    never called.
  - test/integration/http/route_trace_test.go: 3 end-to-end tests through the
    production router asserting auth (401), X-Request-Id header, zero provider
    calls, and the structured response.

Docs: README gets a "Dry-run route trace" example; AGENTS.md endpoint table
lists the new route.

Closes ferro-labs#238.
@gr3enarr0w

Copy link
Copy Markdown
Contributor Author

Preparing review...

@gr3enarr0w

Copy link
Copy Markdown
Contributor Author

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis ✅

238 - PR Code Verified

Compliant requirements:

  • Endpoint performs no upstream model call and consumes no provider quota.
  • Returns selected target/model and ordered candidate explanations.
  • Includes provider health/circuit/retry-relevant state when available.
  • Includes model-catalog pricing/capability match state when available.
  • Works for conditional, fallback, cost-optimized, least-latency, and single routing modes.
  • Do not add Pi-specific logic to Ferro.
  • Do not reintroduce SynapseRouter as a routing dependency.

Requires further human verification:

  • Emits normal auth, audit, and OTel/request IDs like other admin/client-visible routes (auth and request ID parity are covered by integration tests; audit log and OTel span emission should be verified in a running environment).
⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ No major issues detected

@MitulShah1

Copy link
Copy Markdown
Contributor

Really nice work on this, and thanks for the thorough tests and docs. Pulled the branch, it builds clean and all three suites pass locally. The package split and the mu-safe accessors are exactly the right shape, and I like that the no-provider-call guarantee is asserted in a test.

Two things on selection faithfulness I would like to sort out before we merge, since the endpoint's whole value is matching what the gateway would really do:

  1. Cost-optimized: the dry-run probes cost with zero usage, so every priced target comes back at 0 and the "cheapest" ordering falls back to declared order. The live CostOptimized strategy estimates prompt tokens from the message body and ranks on InputUSD, so for differently priced providers the trace can name a different target than the gateway would pick. Could we pass the messages through and reuse the same token estimate so the cost ranking matches?

  2. Least-latency: when some providers have no samples yet, the live strategy routes to an unseen provider to profile it, but the trace sinks unsampled candidates last and selects a sampled one, so the cold-start pick is inverted. Worth either mirroring that or clearly marking the selected target as "warmup pick pending" in that state.

Small stuff: estimated_cost_usd comes back as 0.0 rather than null for unpriced models, and the branch is carrying some unrelated local tooling (.superset/, CODEX.md, and the matching .gitignore entries) that should come out so the PR stays focused on the route trace change.

None of this touches real routing or spend, it is just about the explain output being trustworthy. Happy to merge once the cost/latency selection lines up. Thanks again for unblocking the Pi router surface.

@MitulShah1

Copy link
Copy Markdown
Contributor

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: be0db260-1584-41c5-bd75-db31a4b3b60c

📥 Commits

Reviewing files that changed from the base of the PR and between 2652178 and 65354e7.

📒 Files selected for processing (1)
  • .gitignore

📝 Walkthrough

Walkthrough

Adds an authenticated POST /v1/route/trace endpoint that explains model routing without provider calls. It evaluates strategies, candidates, health, circuits, latency, cost, aliases, and catalog data, with gateway wiring, documentation, tests, and local ignore rules.

Changes

Dry-run route trace endpoint

Layer / File(s) Summary
Route trace evaluation engine
internal/routetrace/trace.go, internal/routetrace/trace_test.go
Defines trace contracts and evaluates candidate selection across single, fallback, conditional, cost, and latency strategies, including aliases and catalog edge cases.
Gateway snapshot and HTTP handler
gateway.go, internal/handler/route_trace.go, internal/handler/route_trace_test.go
Exposes circuit and latency state, builds trace configuration from gateway state, validates requests, and returns dry-run JSON responses.
Endpoint wiring, documentation, and integration validation
internal/httpserver/router.go, AGENTS.md, README.md, test/integration/http/route_trace_test.go
Registers the authenticated endpoint, documents its behavior, and verifies routing, authentication, validation, request IDs, and zero provider calls.

Local development ignore rules

Layer / File(s) Summary
Local artifact ignore rules
.gitignore
Ignores environment files, dependencies, virtual environments, build/cache outputs, Superset runtime files, and local tooling artifacts while allowing .env.example.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Router
  participant RouteTrace
  participant Gateway
  participant Trace
  Client->>Router: POST /v1/route/trace
  Router->>RouteTrace: authenticate and dispatch
  RouteTrace->>Gateway: snapshot routing state
  Gateway-->>RouteTrace: routing inputs and lookup hooks
  RouteTrace->>Trace: Trace(cfg, requestedModel)
  Trace-->>RouteTrace: TraceResponse with candidates
  RouteTrace-->>Client: dry_run JSON response
Loading

Poem

I’m a rabbit with routes in my bright little den,
No tokens were spent, yet we traced where they’d end.
Circuits and costs hopped neatly in line,
While latency whispered, “This target is mine.”
The endpoint now tells every path with a grin—
And I tucked local clutter safely within.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The .gitignore updates add generic local/dev ignores unrelated to the route-trace endpoint, making the PR broader than the linked issue scope. Move generic ignore-rule cleanup to a separate housekeeping PR unless it is explicitly needed for this feature; keep the endpoint change focused on route tracing.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding a dry-run POST /v1/route/trace route explanation endpoint.
Linked Issues check ✅ Passed The PR implements the dry-run route trace endpoint, no-provider-call behavior, candidate explanations, catalog/health/circuit data, and the required routing modes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@codecov

codecov Bot commented Jul 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.10373% with 60 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/routetrace/trace.go 76.70% 36 Missing and 5 partials ⚠️
gateway.go 40.00% 9 Missing ⚠️
internal/handler/route_trace.go 86.95% 5 Missing and 1 partial ⚠️
internal/httpserver/router.go 0.00% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.superset/scripts/context-gen.sh (1)

1-17: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Missing workspace cd, inconsistent with sibling scripts.

run.sh, setup.sh, and teardown.sh all cd "${SUPERSET_WORKSPACE_PATH:-$PWD}" first; this script doesn't, so a standalone invocation from a different CWD writes context files and reads git state for the wrong directory.

🔧 Proposed fix
 #!/usr/bin/env bash
 set -euo pipefail
 
+cd "${SUPERSET_WORKSPACE_PATH:-$PWD}"
+
 mkdir -p .superset/context
🤖 Prompt for 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.

In @.superset/scripts/context-gen.sh around lines 1 - 17, The workspace context
script is reading and writing relative to the caller’s current directory instead
of the configured workspace path. Update context-gen.sh to cd into
"${SUPERSET_WORKSPACE_PATH:-$PWD}" at the start, matching run.sh, setup.sh, and
teardown.sh, so the git status and generated current-status.txt are always based
on the intended workspace. Refer to the script’s top-level setup before the
mkdir/git status block when applying the fix.
♻️ Duplicate comments (1)
.superset/ports.json (1)

1-6: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unrelated tooling scope already flagged.

This entire .superset/ addition (and matching .gitignore entries) was already called out in review as unrelated local tooling that should be removed to keep the PR focused on the route-trace endpoint.

Separately: this static port map isn't read by any script here — run.sh/teardown.sh source .superset/runtime/ports.env, and setup.sh generates that file dynamically from a workspace-name hash rather than this file. If kept, this file appears dead.

🤖 Prompt for 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.

In @.superset/ports.json around lines 1 - 6, Remove the unrelated .superset port
map from the change set and keep the PR focused on the route-trace endpoint. The
.superset/ports.json file appears unused because run.sh, teardown.sh, and
setup.sh rely on .superset/runtime/ports.env generated at runtime, so delete
this static mapping (and any matching .gitignore entries) unless you also update
the runtime scripts to consume it.
🤖 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 @.superset/teardown.sh:
- Around line 12-16: The teardown logic in the PID handling block uses an
unquoted $pids expansion in the kill path, which is fragile even though multiple
PIDs must be passed separately. Update the teardown.sh logic around the
lsof/kill sequence to handle multiple PIDs explicitly and robustly, preferably
by piping the PID list through xargs before invoking kill, while preserving the
existing TERM signal behavior and the conditional check around pids.

In `@internal/handler/route_trace.go`:
- Line 44: The request body in the trace route handler is decoded without any
size limit, so cap it before calling the JSON decoder. Update the handler that
uses json.NewDecoder(r.Body).Decode(&req) to read through a bounded body wrapper
(for example, the same pattern used by ChatCompletions or other request handlers
if present) and keep the limit consistent across handlers. Ensure the fix is
applied in the route handling path that parses the request into req, not just in
the decoder call itself.

In `@internal/routetrace/trace_test.go`:
- Line 256: The LatencyHasSamps stub in the route trace test triggers the
unused-parameter lint because the callback does not reference its name argument.
Update the anonymous function in the test setup to avoid an unused parameter,
either by removing the parameter entirely if the signature allows it or by
naming it with the blank identifier, and keep the change localized around the
LatencyHasSamps field in trace_test.go.

In `@internal/routetrace/trace.go`:
- Around line 29-36: Add a doc comment for the exported StrategyMode constants
block in trace.go so static analysis no longer flags ModeSingle, ModeFallback,
ModeLoadBalance, ModeConditional, ModeLatency, and ModeCostOptimized as
undocumented. Place a single comment above the const block describing that these
constants define the supported routing strategy modes, and keep the existing
names unchanged.
- Around line 332-359: The bare-model fallback in catalogMatch is
nondeterministic because it returns the first matching map entry from iteration,
so Provider/Priced/Status can vary across runs. Update the shared lookup path in
models.Catalog.Get or adjust catalogMatch to avoid ranged map tie-breaking by
requiring provider/model keys for ambiguous ids, or by using a deterministic
selection rule when multiple entries share the same ModelID. Use catalogMatch
and models.Catalog.Get as the main symbols to locate the lookup logic.
- Around line 264-278: The CandidateTarget reordering in the mode switch for
RouteTrace is incorrect for ModeSingle because runtime routing always uses the
first declared target, even when later targets are healthy. Update the logic in
the function handling mode-specific candidate ordering so that ModeSingle
preserves the original declared order and only ModeConditional applies the
matched-target-to-front behavior based on conditionalMatchKey, keeping the trace
aligned with actual routing.

---

Outside diff comments:
In @.superset/scripts/context-gen.sh:
- Around line 1-17: The workspace context script is reading and writing relative
to the caller’s current directory instead of the configured workspace path.
Update context-gen.sh to cd into "${SUPERSET_WORKSPACE_PATH:-$PWD}" at the
start, matching run.sh, setup.sh, and teardown.sh, so the git status and
generated current-status.txt are always based on the intended workspace. Refer
to the script’s top-level setup before the mkdir/git status block when applying
the fix.

---

Duplicate comments:
In @.superset/ports.json:
- Around line 1-6: Remove the unrelated .superset port map from the change set
and keep the PR focused on the route-trace endpoint. The .superset/ports.json
file appears unused because run.sh, teardown.sh, and setup.sh rely on
.superset/runtime/ports.env generated at runtime, so delete this static mapping
(and any matching .gitignore entries) unless you also update the runtime scripts
to consume it.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: 645ba94a-39ab-435b-bb22-5a9ef4b7b896

📥 Commits

Reviewing files that changed from the base of the PR and between 17998ae and d879083.

📒 Files selected for processing (16)
  • .gitignore
  • .superset/ports.json
  • .superset/run.sh
  • .superset/scripts/context-gen.sh
  • .superset/setup.sh
  • .superset/teardown.sh
  • AGENTS.md
  • CODEX.md
  • README.md
  • gateway.go
  • internal/handler/route_trace.go
  • internal/handler/route_trace_test.go
  • internal/httpserver/router.go
  • internal/routetrace/trace.go
  • internal/routetrace/trace_test.go
  • test/integration/http/route_trace_test.go

Comment thread .superset/teardown.sh Outdated
Comment on lines +12 to +16
pids="$(lsof -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null || true)"
if [ -n "$pids" ]; then
kill -TERM $pids 2>/dev/null || true
fi
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Shellcheck SC2086: unquoted $pids expansion.

Intentional here (multiple PIDs need to split into separate args), but relying on unquoted word-splitting is fragile. Consider xargs for clarity and robustness.

🔧 Proposed fix
-      pids="$(lsof -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null || true)"
-      if [ -n "$pids" ]; then
-        kill -TERM $pids 2>/dev/null || true
-      fi
+      pids="$(lsof -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null || true)"
+      if [ -n "$pids" ]; then
+        printf '%s\n' "$pids" | xargs -r kill -TERM 2>/dev/null || true
+      fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pids="$(lsof -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null || true)"
if [ -n "$pids" ]; then
kill -TERM $pids 2>/dev/null || true
fi
fi
pids="$(lsof -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null || true)"
if [ -n "$pids" ]; then
printf '%s\n' "$pids" | xargs -r kill -TERM 2>/dev/null || true
fi
fi
🧰 Tools
🪛 Shellcheck (0.11.0)

[info] 14-14: Double quote to prevent globbing and word splitting.

(SC2086)

🤖 Prompt for 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.

In @.superset/teardown.sh around lines 12 - 16, The teardown logic in the PID
handling block uses an unquoted $pids expansion in the kill path, which is
fragile even though multiple PIDs must be passed separately. Update the
teardown.sh logic around the lsof/kill sequence to handle multiple PIDs
explicitly and robustly, preferably by piping the PID list through xargs before
invoking kill, while preserving the existing TERM signal behavior and the
conditional check around pids.

Source: Linters/SAST tools

func RouteTrace(gw *aigateway.Gateway) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req routeTraceRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider capping request body size.

json.NewDecoder(r.Body).Decode(...) has no size limit, letting a client send an arbitrarily large body to this authenticated endpoint before the JSON parser rejects it.

🛡️ Proposed fix
+		r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1MiB cap
 		var req routeTraceRequest
 		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {

Please confirm whether other handlers in this codebase (e.g. ChatCompletions) already apply a similar cap, so this stays consistent.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1MiB cap
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
🤖 Prompt for 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.

In `@internal/handler/route_trace.go` at line 44, The request body in the trace
route handler is decoded without any size limit, so cap it before calling the
JSON decoder. Update the handler that uses json.NewDecoder(r.Body).Decode(&req)
to read through a bounded body wrapper (for example, the same pattern used by
ChatCompletions or other request handlers if present) and keep the limit
consistent across handlers. Ensure the fix is applied in the route handling path
that parses the request into req, not just in the decoder call itself.

Comment thread internal/routetrace/trace_test.go Outdated
Comment thread internal/routetrace/trace.go
Comment thread internal/routetrace/trace.go
Comment thread internal/routetrace/trace.go
@MitulShah1

Copy link
Copy Markdown
Contributor

@gr3enarr0w let me know when you able to fix those issue so i can plan for release.

…lookup, lint

- orderCandidates no longer reorders ModeSingle candidates: runtime single-mode
  routing always uses Targets[0] regardless of a later target's health, so
  reordering made the trace disagree with actual routing.
- catalogMatch's bare-model-id fallback now picks deterministically (lowest
  catalog key) instead of relying on Go's randomized map iteration order.
- Add doc comment on the exported StrategyMode const block and rename an
  unused test closure parameter to satisfy revive.
- Drop .superset/ and CODEX.md local tooling scaffold from the diff (picked
  up from an unrelated local dev-tooling daemon, not part of this feature).
@gr3enarr0w

Copy link
Copy Markdown
Contributor Author

Pushed fixes for lint and the CodeRabbit findings:

  • Lint: added the missing doc comment on the exported StrategyMode const block, renamed the unused test closure parameter to _.
  • ModeSingle ordering bug (real correctness issue, not just style): orderCandidates was bringing a later healthy/matched candidate to the front for single-mode, but runtime single-mode routing (strategies.Single) always uses Targets[0] regardless of any other target's health — there's no fallback within single mode. So the trace could report a different target as selected than what the gateway would actually route to. Fixed by excluding ModeSingle from the reorder case; declared order (and therefore Targets[0]) is now preserved, matching selectIndex's existing "only index 0" check.
  • Nondeterministic catalog lookup: catalogMatch's bare-model-id fallback iterated a Go map and returned the first hit — non-deterministic between runs when two providers share a bare model ID (same bug that exists in models.Catalog.Get, which this mirrors). Fixed the local copy in catalogMatch to break ties deterministically (lowest catalog key), scoped to this file rather than touching the shared models package.
  • Dropped .superset//CODEX.md — unrelated local dev-tooling scaffold, not part of this feature.

Full test suite (including race) passes. Let me know if you'd rather see the catalog nondeterminism fixed at the models.Catalog.Get level instead — I kept it local to this PR's file since that's a separate pre-existing bug affecting real routing decisions too, not just the trace.

…ace handler

Use httptest.NewRequestWithContext instead of httptest.NewRequest in
route_trace_test.go, and preallocate the conditions slice with its known
final capacity in route_trace.go. Pure lint fixes, no behavior change.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 @.gitignore:
- Line 73: Update the CODEX.md entry in the root ignore configuration to
/CODEX.md so only the repository-root local artifact is ignored, while nested
CODEX.md documentation remains trackable.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: ff12e2db-57ff-43a1-b04d-53c20e874708

📥 Commits

Reviewing files that changed from the base of the PR and between d879083 and 2652178.

📒 Files selected for processing (5)
  • .gitignore
  • internal/handler/route_trace.go
  • internal/handler/route_trace_test.go
  • internal/routetrace/trace.go
  • internal/routetrace/trace_test.go

Comment thread .gitignore Outdated
Unanchored CODEX.md in .gitignore would ignore any file with that
name anywhere in the tree, not just the repo-root dev-tooling
scaffold file it's meant for. Root-anchor it to /CODEX.md so a
legitimate nested CODEX.md could still be tracked.
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.

feat: add dry-run route trace/explain endpoint

2 participants