Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
c31d624
feat(ext): add route selector extension point and adaptive strategy
SantiagoDePolonia Jul 28, 2026
189beca
fix(ext): address route-selector review findings
SantiagoDePolonia Jul 28, 2026
b3c07a4
test(virtualmodels): cover every pricing pointer in the isolation test
SantiagoDePolonia Jul 28, 2026
454b7cc
feat(dashboard): server-driven virtual-model strategy dropdown
SantiagoDePolonia Jul 29, 2026
fe6a0e6
fix(dashboard): describe adaptive as extension-defined
SantiagoDePolonia Jul 29, 2026
17e6c4b
feat(ext): expose the detected session ID to request rewriters
SantiagoDePolonia Jul 28, 2026
d96c808
test(server): table-driven session propagation coverage
SantiagoDePolonia Jul 28, 2026
3662589
test(server): assert the rewriter ran in the no-session case
SantiagoDePolonia Jul 29, 2026
38263ff
Merge remote-tracking branch 'origin/main' into feat/ext-route-selector
SantiagoDePolonia Jul 30, 2026
abcfe55
Merge branch 'feat/ext-route-selector' into feat/ext-session-id
SantiagoDePolonia Jul 30, 2026
094d1d1
fix(dashboard): gate adaptive help text on server support
SantiagoDePolonia Jul 30, 2026
56b0dcd
Merge branch 'feat/ext-route-selector' into feat/ext-session-id
SantiagoDePolonia Jul 30, 2026
c6ec988
Merge remote-tracking branch 'origin/main' into feat/ext-route-selector
SantiagoDePolonia Jul 30, 2026
2d56255
Merge branch 'feat/ext-route-selector' into feat/ext-session-id
SantiagoDePolonia Jul 30, 2026
8879ea9
fix(admin): preserve VIRTUAL_MODEL_STRATEGIES through runtime-config …
SantiagoDePolonia Jul 30, 2026
176d0ba
Merge branch 'feat/ext-route-selector' into feat/ext-session-id
SantiagoDePolonia Jul 30, 2026
9a51eb7
Merge remote-tracking branch 'origin/main' into feat/ext-session-id
SantiagoDePolonia Jul 30, 2026
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
4 changes: 4 additions & 0 deletions ext/ext.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ type Input struct {
UserPath string
// RequestID is the request correlation ID (X-Request-ID).
RequestID string
// SessionID is the detected client session, when present, already scoped
// by the effective user path. Session detection runs before rewriters, so
// a rewriter can keep its decisions stable across a conversation.
SessionID string
}

// Result carries a rewritten body and response-header annotations.
Expand Down
1 change: 1 addition & 0 deletions internal/server/request_rewrite.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func RequestRewriteMiddleware(rewriters []ext.RequestRewriter, auditLogger audit
Header: redactCredentialHeaders(c.Request().Header),
UserPath: core.UserPathFromContext(c.Request().Context()),
RequestID: core.GetRequestID(c.Request().Context()),
SessionID: core.SessionIDFromContext(c.Request().Context()),
}

changed := false
Expand Down
53 changes: 53 additions & 0 deletions internal/server/request_rewrite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,59 @@ func TestRequestRewriteMiddlewareRewritesChatCompletions(t *testing.T) {
}
}

func TestRequestRewriteMiddlewareExposesSessionID(t *testing.T) {
tests := []struct {
name string
session string // stamped into the context before rewriters; "" = not detected
want string
}{
{name: "detected session propagates", session: "sess-42", want: "sess-42"},
{name: "no detected session yields empty", session: "", want: ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
provider := newRewriteTestProvider()
var seenSession string
capturing := &stubRewriter{
name: "capture-session",
rewrite: func(in ext.Input) (*ext.Result, error) {
seenSession = in.SessionID
return nil, nil
},
}
// Session detection runs before rewriters and stamps the request
// context; ExtraMiddleware runs even earlier, so it stands in for
// the detector here.
stampSession := func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c *echo.Context) error {
if tt.session != "" {
req := c.Request()
c.SetRequest(req.WithContext(core.WithSessionID(req.Context(), tt.session)))
}
return next(c)
}
}
srv := New(provider, &Config{
RequestRewriters: []ext.RequestRewriter{capturing},
ExtraMiddleware: []echo.MiddlewareFunc{stampSession},
})

rec := postJSON(t, srv, "/v1/chat/completions",
`{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}`)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d (%s)", rec.Code, rec.Body.String())
}
// The empty-session expectation must not pass vacuously.
if capturing.calls != 1 {
t.Fatalf("rewriter called %d times, want 1", capturing.calls)
}
if seenSession != tt.want {
t.Errorf("rewriter saw SessionID %q, want %q", seenSession, tt.want)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
})
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func TestRequestRewriteMiddlewareRewritesMessages(t *testing.T) {
provider := newRewriteTestProvider()
srv := New(provider, &Config{
Expand Down