Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions ext/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ func (t RouteTarget) Qualified() string { return t.Provider + "/" + t.Model }
// scores slower than a target that succeeds at once.
type RouteOutcome struct {
RouteTarget
// Source is the virtual model originally addressed when this attempt was
// selected through one. SessionID is the detected client session. Together
// they let selectors update cache affinity only after a successful attempt.
Source string
SessionID string
// Endpoint is the upstream API endpoint (e.g. "/chat/completions").
Endpoint string
// StatusCode is the final upstream HTTP status; 0 on a network error.
Expand Down
14 changes: 13 additions & 1 deletion internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,13 @@ func routeSelectorHooks(selector ext.RouteSelector) llmclient.Hooks {
})
return ctx
},
OnRequestEnd: func(_ context.Context, info llmclient.ResponseInfo) {
OnRequestEnd: func(ctx context.Context, info llmclient.ResponseInfo) {
observe("attempt_end", func() {
source, sessionID := routeAffinityContext(ctx)
selector.OnAttemptEnd(ext.RouteOutcome{
RouteTarget: ext.RouteTarget{Provider: info.Provider, Model: info.Model},
Source: source,
SessionID: sessionID,
Endpoint: info.Endpoint,
StatusCode: info.StatusCode,
Duration: info.Duration,
Expand All @@ -161,6 +164,15 @@ func routeSelectorHooks(selector ext.RouteSelector) llmclient.Hooks {
}
}

func routeAffinityContext(ctx context.Context) (source, sessionID string) {
sessionID = core.SessionIDFromContext(ctx)
workflow := core.GetWorkflow(ctx)
if workflow == nil || workflow.Resolution == nil || !workflow.Resolution.AliasApplied {
return "", sessionID
}
return workflow.Resolution.RequestedQualifiedModel(), sessionID
}

// selectorLabel returns the selector's name for logs, tolerating a panicking
// Name implementation, so recovery paths never re-enter extension code.
func selectorLabel(selector ext.RouteSelector) (name string) {
Expand Down
30 changes: 30 additions & 0 deletions internal/app/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,40 @@ import (
"github.com/enterpilot/gomodel/internal/core"
"github.com/enterpilot/gomodel/internal/guardrails"
"github.com/enterpilot/gomodel/internal/live"
"github.com/enterpilot/gomodel/internal/llmclient"
"github.com/enterpilot/gomodel/internal/providers"
"github.com/enterpilot/gomodel/internal/server"
)

type routeObservationSelector struct {
outcome ext.RouteOutcome
}

func (*routeObservationSelector) Name() string { return "observer" }
func (*routeObservationSelector) Select(ext.RouteRequest) (string, bool) { return "", false }
func (*routeObservationSelector) OnAttemptStart(ext.RouteTarget) {}
func (s *routeObservationSelector) OnAttemptEnd(outcome ext.RouteOutcome) {
s.outcome = outcome
}

func TestRouteSelectorHooksExposeSuccessfulRouteAffinityContext(t *testing.T) {
selector := &routeObservationSelector{}
hooks := routeSelectorHooks(selector)
ctx := core.WithSessionID(context.Background(), "session-a")
ctx = core.WithWorkflow(ctx, &core.Workflow{Resolution: &core.RequestModelResolution{
Requested: core.NewRequestedModelSelector("smart", ""),
ResolvedSelector: core.ModelSelector{Provider: "openai", Model: "gpt"},
AliasApplied: true,
}})
ctx = hooks.OnRequestStart(ctx, llmclient.RequestInfo{Provider: "openai", Model: "gpt"})
hooks.OnRequestEnd(ctx, llmclient.ResponseInfo{Provider: "openai", Model: "gpt", StatusCode: http.StatusOK})

if selector.outcome.Source != "smart" || selector.outcome.SessionID != "session-a" {
t.Fatalf("route affinity context = %q/%q, want smart/session-a",
selector.outcome.Source, selector.outcome.SessionID)
}
}
Comment on lines +36 to +52

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

Add cases for workflows without an applied alias.

This test covers only the alias-applied path. Add table-driven cases for a missing workflow, a missing resolution, and AliasApplied == false. Verify that Source is empty and SessionID remains available.

As per coding guidelines, “Tests should cover request translation, response normalization, error handling, default configuration, and provider-specific parameter mapping.”

🤖 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/app/app_test.go` around lines 36 - 52, Add table-driven cases to
TestRouteSelectorHooksExposeSuccessfulRouteAffinityContext for a missing
workflow, missing resolution, and AliasApplied == false. For each case, invoke
routeSelectorHooks.OnRequestStart and OnRequestEnd with the existing request
context, then verify outcome.Source is empty while outcome.SessionID remains
"session-a"; retain the current alias-applied case and its expected "smart"
source.

Source: Coding guidelines


type runtimeRefreshMockProvider struct {
models *core.ModelsResponse
err error
Expand Down
Loading