Skip to content

refactor(admin): push handler business logic into feature services#478

Merged
SantiagoDePolonia merged 1 commit into
mainfrom
refactor/admin-logic-pushdowns
Jul 4, 2026
Merged

refactor(admin): push handler business logic into feature services#478
SantiagoDePolonia merged 1 commit into
mainfrom
refactor/admin-logic-pushdowns

Conversation

@SantiagoDePolonia

@SantiagoDePolonia SantiagoDePolonia commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Next architecture-review item (doc in #472, §6/admin): four blocks of business logic lived inside admin HTTP handlers instead of the feature services they belong to. Each moves to its owning package; the handlers keep only decode → gate → delegate → encode.

Was in Now in
GenerateFailoverRules handler loop (registry iteration, category filter, source matching, resolver ranking, View assembly) failover.GenerateSuggestions(registry, rules, primaryModel)
Budget ratio math + clampBudgetRatio budget.CheckResult.UsageRatio() (deliberately unclamped — >1 reads as exceeded) and .PeriodRatio(now) (clamped)
Audit×usage load-then-summarize dance usage.SummarizeUsageForRequestIDs(ctx, loader, ids) on a narrow RequestUsageLoader seam
"Preserve stored Enabled on omit/rename" upsert rule virtualmodels.Service.ResolveUpsertEnabled(source, oldSource, requested)

Each move gets unit tests in its new home (suggestion generation incl. category filter + primary-model filter, ratio edge cases incl. clamping bounds, loader error/nil paths, and all four enabled-resolution branches). The admin handler tests pass unchanged — no behavior change.

User-visible impact

None.

Testing

Full go build ./... and go test ./...; golangci-lint 0 issues on all touched packages; pre-commit make test-race on the commit.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Admin audit logs now include usage summaries when available.
    • Failover suggestion generation is now available through the admin interface with broader model support.
    • Budget views now show usage and period progress ratios.
  • Bug Fixes

    • Improved handling when usage details can’t be loaded, so audit logs still return.
    • Virtual model updates now preserve enabled status more consistently, including rename flows.
    • Budget progress values are now calculated more reliably and consistently.

Four blocks the architecture review flagged as business logic living in
admin HTTP handlers move to their owning packages, with unit tests in the
new homes; handlers keep only decode/gate/delegate/encode:

- failover.GenerateSuggestions owns dashboard suggestion generation
  (registry iteration, text-generation category filter, source matching,
  resolver ranking, View assembly); the handler parses params and encodes.
- budget.CheckResult gains UsageRatio (deliberately unclamped so >1 reads
  as exceeded) and PeriodRatio (clamped elapsed-period fraction), replacing
  inline math and clampBudgetRatio in the handler.
- usage.SummarizeUsageForRequestIDs collapses the load-then-summarize dance
  behind one call on a narrow RequestUsageLoader seam; the audit handler
  keeps its warn-and-continue enrichment behavior.
- virtualmodels.Service.ResolveUpsertEnabled owns the preserve-stored-
  Enabled-on-omit/rename rule the upsert mapping previously encoded.

No behavior change; admin handler tests pass unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR extracts previously inline logic in admin handlers into reusable package-level helpers: usage summarization (usage.SummarizeUsageForRequestIDs), budget ratio calculation (CheckResult.UsageRatio/PeriodRatio), failover suggestion generation (failover.GenerateSuggestions), and virtual model enabled resolution (Service.ResolveUpsertEnabled), each with corresponding unit tests.

Changes

Usage Summarization Helper

Layer / File(s) Summary
Usage summarization helper and audit handler wiring
internal/usage/request_summary.go, internal/usage/request_summary_loader_test.go, internal/admin/handler_audit.go
Adds RequestUsageLoader interface and SummarizeUsageForRequestIDs function; auditLogResponse now calls this helper unconditionally and logs a warning instead of failing on error.

Budget Ratio Calculation

Layer / File(s) Summary
CheckResult ratio methods and handler usage
internal/budget/types.go, internal/budget/types_test.go, internal/admin/handler_budgets.go
Adds UsageRatio() and PeriodRatio(now) methods on CheckResult; budgetStatusResponses now uses them directly, and the now-unused clampBudgetRatio helper is removed.

Failover Suggestion Generation

Layer / File(s) Summary
GenerateSuggestions implementation and handler delegation
internal/failover/suggest.go, internal/failover/suggest_test.go, internal/admin/handler_failover.go
Adds GenerateSuggestions with sourceMatchesModel/modelSupportsCategory helpers; GenerateFailoverRules delegates to it, removing local resolver construction and filtering helpers.

Virtual Model Enabled Resolution

Layer / File(s) Summary
ResolveUpsertEnabled helper and handler wiring
internal/virtualmodels/service.go, internal/virtualmodels/service_test.go, internal/admin/handler_virtualmodels.go
Adds Service.ResolveUpsertEnabled; buildVirtualModelUpsert now delegates Enabled computation to it instead of inline lookups.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AuditHandler as auditLogResponse
  participant Usage as SummarizeUsageForRequestIDs
  participant Loader as RequestUsageLoader

  AuditHandler->>Usage: SummarizeUsageForRequestIDs(ctx, usageReader, requestIDs)
  Usage->>Loader: GetUsageByRequestIDs(ctx, requestIDs)
  Loader-->>Usage: usage entries or error
  Usage-->>AuditHandler: summaries map or error
  AuditHandler->>AuditHandler: log warning and continue on error
Loading
sequenceDiagram
  participant AdminHandler as GenerateFailoverRules
  participant Failover as failover.GenerateSuggestions
  participant Resolver as failover resolver
  participant Registry as Registry

  AdminHandler->>Failover: GenerateSuggestions(registry, rules, primaryModel)
  Failover->>Resolver: create resolver from FailoverConfig
  Failover->>Registry: iterate text-generation models
  Failover->>Resolver: SuggestFailovers(OperationChatCompletions)
  Resolver-->>Failover: candidate targets
  Failover-->>AdminHandler: []View suggestions
Loading

Possibly related PRs

  • ENTERPILOT/GoModel#336: Shares the SummarizeUsageByRequestID summarization path used by the new SummarizeUsageForRequestIDs helper.
  • ENTERPILOT/GoModel#423: Both touch virtual-model enablement resolution in handler_virtualmodels.go/virtualmodels.Service.

Poem

A rabbit hops through code so neat,
Pulling logic out, making it complete,
Ratios, suggestions, usage, and flags,
Now tucked in helpers, no more zigzags,
Tests all green, my paws applaud 🐰✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main refactor: moving admin handler business logic into feature services.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ 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 refactor/admin-logic-pushdowns

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-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 88.23529% with 10 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/failover/suggest.go 80.00% 5 Missing and 5 partials ⚠️

📢 Thoughts on this report? Let us know!

@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: 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 `@internal/virtualmodels/service_test.go`:
- Around line 497-523: Refactor TestService_ResolveUpsertEnabled into a
table-driven test to cover the four precedence cases in a clearer, more
idiomatic way. Keep the existing setup with newTestService, svc.Upsert, and
ResolveUpsertEnabled, but move the explicit override, omit-preserves,
rename-preserves, and default-enabled checks into named table entries with
expected outcomes and failure messages. This will make the scenarios easier to
read and extend while keeping the same behavior coverage.
🪄 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

Run ID: 5c147044-2aa2-4a23-b9b6-0f68dd32f5f4

📥 Commits

Reviewing files that changed from the base of the PR and between e864064 and 04a29c2.

📒 Files selected for processing (12)
  • internal/admin/handler_audit.go
  • internal/admin/handler_budgets.go
  • internal/admin/handler_failover.go
  • internal/admin/handler_virtualmodels.go
  • internal/budget/types.go
  • internal/budget/types_test.go
  • internal/failover/suggest.go
  • internal/failover/suggest_test.go
  • internal/usage/request_summary.go
  • internal/usage/request_summary_loader_test.go
  • internal/virtualmodels/service.go
  • internal/virtualmodels/service_test.go

Comment on lines +497 to +523
func TestService_ResolveUpsertEnabled(t *testing.T) {
t.Parallel()
svc := newTestService(t)
ctx := context.Background()

if err := svc.Upsert(ctx, VirtualModel{
Source: "fast",
Targets: []Target{{Provider: "openai", Model: "gpt-4o"}},
Enabled: false,
}); err != nil {
t.Fatalf("Upsert() error = %v", err)
}

enabled := true
if got := svc.ResolveUpsertEnabled("fast", "", &enabled); !got {
t.Fatal("explicit request value must win over the stored flag")
}
if got := svc.ResolveUpsertEnabled("fast", "", nil); got {
t.Fatal("omitted flag must preserve the stored (disabled) value")
}
if got := svc.ResolveUpsertEnabled("renamed", "fast", nil); got {
t.Fatal("rename must preserve the flag of the row being renamed")
}
if got := svc.ResolveUpsertEnabled("brand-new", "", nil); !got {
t.Fatal("new rows must default to enabled")
}
}

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.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a table-driven test for the four scenarios.

The test exercises four distinct precedence branches (explicit override, omit-preserves, rename-preserves, default-enabled) via sequential if blocks. A table-driven structure would make each case's inputs/expectations easier to scan and extend.

♻️ Proposed table-driven refactor
 func TestService_ResolveUpsertEnabled(t *testing.T) {
 	t.Parallel()
 	svc := newTestService(t)
 	ctx := context.Background()
 
 	if err := svc.Upsert(ctx, VirtualModel{
 		Source:  "fast",
 		Targets: []Target{{Provider: "openai", Model: "gpt-4o"}},
 		Enabled: false,
 	}); err != nil {
 		t.Fatalf("Upsert() error = %v", err)
 	}
 
 	enabled := true
-	if got := svc.ResolveUpsertEnabled("fast", "", &enabled); !got {
-		t.Fatal("explicit request value must win over the stored flag")
-	}
-	if got := svc.ResolveUpsertEnabled("fast", "", nil); got {
-		t.Fatal("omitted flag must preserve the stored (disabled) value")
-	}
-	if got := svc.ResolveUpsertEnabled("renamed", "fast", nil); got {
-		t.Fatal("rename must preserve the flag of the row being renamed")
-	}
-	if got := svc.ResolveUpsertEnabled("brand-new", "", nil); !got {
-		t.Fatal("new rows must default to enabled")
-	}
+	tests := []struct {
+		name      string
+		source    string
+		oldSource string
+		requested *bool
+		want      bool
+	}{
+		{"explicit value wins", "fast", "", &enabled, true},
+		{"omitted preserves stored disabled", "fast", "", nil, false},
+		{"rename preserves source row flag", "renamed", "fast", nil, false},
+		{"new row defaults to enabled", "brand-new", "", nil, true},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := svc.ResolveUpsertEnabled(tt.source, tt.oldSource, tt.requested); got != tt.want {
+				t.Fatalf("ResolveUpsertEnabled() = %v, want %v", got, tt.want)
+			}
+		})
+	}
 }

As per coding guidelines, **/*.go code should "use idiomatic Go, prefer clear names, small interfaces, simple structs, and table-driven tests".

📝 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
func TestService_ResolveUpsertEnabled(t *testing.T) {
t.Parallel()
svc := newTestService(t)
ctx := context.Background()
if err := svc.Upsert(ctx, VirtualModel{
Source: "fast",
Targets: []Target{{Provider: "openai", Model: "gpt-4o"}},
Enabled: false,
}); err != nil {
t.Fatalf("Upsert() error = %v", err)
}
enabled := true
if got := svc.ResolveUpsertEnabled("fast", "", &enabled); !got {
t.Fatal("explicit request value must win over the stored flag")
}
if got := svc.ResolveUpsertEnabled("fast", "", nil); got {
t.Fatal("omitted flag must preserve the stored (disabled) value")
}
if got := svc.ResolveUpsertEnabled("renamed", "fast", nil); got {
t.Fatal("rename must preserve the flag of the row being renamed")
}
if got := svc.ResolveUpsertEnabled("brand-new", "", nil); !got {
t.Fatal("new rows must default to enabled")
}
}
func TestService_ResolveUpsertEnabled(t *testing.T) {
t.Parallel()
svc := newTestService(t)
ctx := context.Background()
if err := svc.Upsert(ctx, VirtualModel{
Source: "fast",
Targets: []Target{{Provider: "openai", Model: "gpt-4o"}},
Enabled: false,
}); err != nil {
t.Fatalf("Upsert() error = %v", err)
}
enabled := true
tests := []struct {
name string
source string
oldSource string
requested *bool
want bool
}{
{"explicit value wins", "fast", "", &enabled, true},
{"omitted preserves stored disabled", "fast", "", nil, false},
{"rename preserves source row flag", "renamed", "fast", nil, false},
{"new row defaults to enabled", "brand-new", "", nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := svc.ResolveUpsertEnabled(tt.source, tt.oldSource, tt.requested); got != tt.want {
t.Fatalf("ResolveUpsertEnabled() = %v, want %v", got, tt.want)
}
})
}
}
🤖 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/virtualmodels/service_test.go` around lines 497 - 523, Refactor
TestService_ResolveUpsertEnabled into a table-driven test to cover the four
precedence cases in a clearer, more idiomatic way. Keep the existing setup with
newTestService, svc.Upsert, and ResolveUpsertEnabled, but move the explicit
override, omit-preserves, rename-preserves, and default-enabled checks into
named table entries with expected outcomes and failure messages. This will make
the scenarios easier to read and extend while keeping the same behavior
coverage.

Source: Coding guidelines

@greptile-apps

greptile-apps Bot commented Jul 4, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

Safe to merge with low risk.

The changed code mainly moves existing logic into feature-owned helpers and preserves the handler validation and delegation flow. No verified runtime bugs or security issues were found in the changed paths. Added tests cover the extracted failover, budget, usage, and virtual model behavior.

No files require special attention.

T-Rex T-Rex Logs

What T-Rex did

  • T-Rex captured the Go toolchain version as go1.26.4 on linux/amd64.
  • T-Rex confirmed the target packages were resolved by go list.
  • T-Rex executed focused tests for internal/budget, internal/failover, internal/usage, and internal/virtualmodels, and they exited with code 0.
  • T-Rex ran the admin package tests and they exited with code 0.
  • T-Rex built the gomodel command, performed a full Go build, and attempted a full Go test, but the test did not complete due to an unrelated gomodel/internal/providers failure before the wrapper timed out.

View all artifacts

T-Rex Ran code and verified through T-Rex

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Admin as Admin HTTP handler
participant Failover as failover.GenerateSuggestions
participant Budget as budget.CheckResult
participant Usage as usage.SummarizeUsageForRequestIDs
participant VM as virtualmodels.Service

Admin->>Failover: registry, rules, primaryModel
Failover-->>Admin: []failover.View suggestions
Admin->>Budget: UsageRatio() / PeriodRatio(now)
Budget-->>Admin: dashboard ratios
Admin->>Usage: ctx, usageReader, requestIDs
Usage-->>Admin: request usage summaries
Admin->>VM: ResolveUpsertEnabled(source, oldSource, requested)
VM-->>Admin: enabled flag to persist
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Admin as Admin HTTP handler
participant Failover as failover.GenerateSuggestions
participant Budget as budget.CheckResult
participant Usage as usage.SummarizeUsageForRequestIDs
participant VM as virtualmodels.Service

Admin->>Failover: registry, rules, primaryModel
Failover-->>Admin: []failover.View suggestions
Admin->>Budget: UsageRatio() / PeriodRatio(now)
Budget-->>Admin: dashboard ratios
Admin->>Usage: ctx, usageReader, requestIDs
Usage-->>Admin: request usage summaries
Admin->>VM: ResolveUpsertEnabled(source, oldSource, requested)
VM-->>Admin: enabled flag to persist
Loading

Reviews (1): Last reviewed commit: "refactor(admin): push handler business l..." | Re-trigger Greptile

@SantiagoDePolonia SantiagoDePolonia merged commit ef59c77 into main Jul 4, 2026
21 checks passed
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.

2 participants