Skip to content

Commit 7081d63

Browse files
committed
semantic routing ui
1 parent a48e26b commit 7081d63

10 files changed

Lines changed: 1124 additions & 158 deletions

File tree

framework/configstore/complexityconfig.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ const (
142142
)
143143

144144
// DefaultComplexitySemanticTimeout bounds per-request embedding generation.
145-
const DefaultComplexitySemanticTimeout = 100 * time.Millisecond
145+
const DefaultComplexitySemanticTimeout = 500 * time.Millisecond
146146

147147
// ComplexitySemanticConfig configures the embedding-based complexity
148148
// classifier. A non-nil value enables semantic classification. The classifier
@@ -180,7 +180,7 @@ type ComplexitySemanticConfig struct {
180180
VectorStore string `json:"vector_store,omitempty"`
181181
}
182182

183-
// UnmarshalJSON accepts Timeout as a duration string ("100ms") or a JSON number
183+
// UnmarshalJSON accepts Timeout as a duration string ("500ms") or a JSON number
184184
// (milliseconds). All other fields decode through the default path via an alias.
185185
func (c *ComplexitySemanticConfig) UnmarshalJSON(data []byte) error {
186186
// alias suppresses ComplexitySemanticConfig's UnmarshalJSON to avoid

framework/configstore/complexityconfig_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func TestComplexitySemanticConfigTimeoutMarshalRoundTrip(t *testing.T) {
7070
func TestComplexitySemanticConfigNormalizedDefaults(t *testing.T) {
7171
normalized := testSemanticConfig().normalized()
7272

73-
assert.Equal(t, DefaultComplexitySemanticTimeout, normalized.Timeout)
73+
assert.Equal(t, 500*time.Millisecond, normalized.Timeout)
7474
assert.Equal(t, ComplexitySemanticFallbackLexical, normalized.Fallback)
7575
assert.Equal(t, ComplexitySemanticVectorStoreEmbedded, normalized.VectorStore)
7676
require.NoError(t, normalized.Validate())

plugins/governance/embedding.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package governance
33
import (
44
"context"
55
"encoding/json"
6+
"errors"
67
"fmt"
78
"time"
89

@@ -13,6 +14,11 @@ import (
1314
"github.com/maximhq/bifrost/plugins/governance/complexity"
1415
)
1516

17+
// ErrEmbeddingRequestExecutorNotConfigured means the HTTP server has not
18+
// finished wiring the governance plugin to Bifrost's embedding request path.
19+
// Configuration clients can retry this transient startup state.
20+
var ErrEmbeddingRequestExecutorNotConfigured = errors.New("embedding request executor is not configured")
21+
1622
// EmbeddingRequestExecutor invokes the embedding endpoint on the bifrost
1723
// client. The plugin calls it to embed request text for semantic complexity
1824
// classification. It mirrors the signature of bifrost.Client.EmbeddingRequest.
@@ -68,7 +74,7 @@ type ComplexityVectorStoreSetter interface {
6874
// warmupEmbeddingTimeout bounds one warmup embedding call, whether that is a
6975
// batch of exemplars or a single-input fallback. Warmup runs in the background
7076
// with no request waiting on it, so it must NOT inherit semantic.Timeout — that
71-
// is the hot-path budget (100ms by default), which a 32-exemplar batch cannot
77+
// is the hot-path budget (500ms by default), which a 32-exemplar batch cannot
7278
// possibly meet. It stays bounded so a hung provider cannot pin the warmup
7379
// worker forever.
7480
const warmupEmbeddingTimeout = 60 * time.Second
@@ -277,7 +283,7 @@ const probeEmbeddingText = "a"
277283
// budget and reported to no telemetry counter.
278284
func (p *GovernancePlugin) ProbeEmbeddingDimension(ctx context.Context, provider schemas.ModelProvider, model string) (int, error) {
279285
if p.embeddingExecutor() == nil {
280-
return 0, fmt.Errorf("embedding request executor is not configured")
286+
return 0, ErrEmbeddingRequestExecutorNotConfigured
281287
}
282288
probeCtx := schemas.NewBifrostContext(ctx, schemas.NoDeadline)
283289
defer probeCtx.Cancel()
@@ -328,7 +334,7 @@ func (p *GovernancePlugin) generateEmbedding(ctx *schemas.BifrostContext, semant
328334
func (p *GovernancePlugin) generateEmbeddings(ctx *schemas.BifrostContext, semantic *complexity.SemanticConfig, texts []string, timeout time.Duration) ([][]float32, int, error) {
329335
executor := p.embeddingExecutor()
330336
if executor == nil {
331-
return nil, 0, fmt.Errorf("embedding request executor is not configured")
337+
return nil, 0, ErrEmbeddingRequestExecutorNotConfigured
332338
}
333339
if semantic == nil || semantic.Provider == "" || semantic.EmbeddingModel == "" {
334340
return nil, 0, fmt.Errorf("semantic classification is not configured")

plugins/governance/embedding_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ func TestProbeEmbeddingDimensionRequiresExecutorAndReportsFailures(t *testing.T)
131131
unwired := &GovernancePlugin{}
132132
_, err := unwired.ProbeEmbeddingDimension(t.Context(), "openai", "text-embedding-3-small")
133133
require.Error(t, err)
134+
assert.ErrorIs(t, err, ErrEmbeddingRequestExecutorNotConfigured)
134135
assert.Contains(t, err.Error(), "executor is not configured")
135136

136137
plugin := &GovernancePlugin{}
@@ -210,7 +211,7 @@ func TestGenerateEmbeddingTimeoutCancelsCall(t *testing.T) {
210211
}
211212

212213
// TestWarmupEmbedsDoNotInheritTheRequestTimeout is a regression guard: warmup
213-
// used to run through semantic.Timeout, the hot-path budget (100ms by default).
214+
// used to run through semantic.Timeout, the hot-path budget (500ms by default).
214215
// A 32-exemplar batch cannot finish in that window, so every warmup failed with
215216
// a 504 and semantic routing silently served its fallback forever.
216217
func TestWarmupEmbedsDoNotInheritTheRequestTimeout(t *testing.T) {

transports/bifrost-http/handlers/governance.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -770,7 +770,6 @@ func (h *GovernanceHandler) reconcileVKModelConfig(ctx context.Context, tx *gorm
770770
}
771771
}
772772

773-
774773
// Resulting budget count: the desired set if provided, else the existing set.
775774
finalBudgetCount := len(mc.Budgets)
776775
if d.budgetsProvided {
@@ -1230,6 +1229,10 @@ func (h *GovernanceHandler) probeComplexityEmbeddingDimension(ctx *fasthttp.Requ
12301229

12311230
dimension, err := prober.ProbeComplexityEmbeddingDimension(ctx, payload.Provider, payload.EmbeddingModel)
12321231
if err != nil {
1232+
if errors.Is(err, governance.ErrEmbeddingRequestExecutorNotConfigured) {
1233+
SendError(ctx, fasthttp.StatusServiceUnavailable, "embedding service is still initializing; retry in a moment")
1234+
return
1235+
}
12331236
// Almost always an operator-correctable cause (unknown model, missing
12341237
// key, provider rejecting the request), so it is reported as a client
12351238
// error the form can render inline.

transports/bifrost-http/handlers/governance_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,8 @@ type mockComplexityGovernanceManager struct {
271271
validationErr error
272272
semanticStatus complexity.SemanticStatusInfo
273273
semanticErr error
274+
probeDimension int
275+
probeErr error
274276
}
275277

276278
func (m *mockComplexityGovernanceManager) ReloadComplexityAnalyzerConfig(_ context.Context, config *complexity.AnalyzerConfig) error {
@@ -291,6 +293,10 @@ func (m *mockComplexityGovernanceManager) GetComplexitySemanticStatus(_ context.
291293
return m.semanticStatus, m.semanticErr
292294
}
293295

296+
func (m *mockComplexityGovernanceManager) ProbeComplexityEmbeddingDimension(_ context.Context, _ schemas.ModelProvider, _ string) (int, error) {
297+
return m.probeDimension, m.probeErr
298+
}
299+
294300
func testComplexityAnalyzerPayload(t *testing.T, cfg complexity.AnalyzerConfig) string {
295301
t.Helper()
296302
body, err := json.Marshal(cfg)
@@ -475,6 +481,23 @@ func TestComplexitySemanticStatusReturnsRuntimeReadiness(t *testing.T) {
475481
}
476482
}
477483

484+
func TestProbeComplexityEmbeddingDimensionReportsExecutorStartupAsUnavailable(t *testing.T) {
485+
SetLogger(&mockLogger{})
486+
handler := &GovernanceHandler{governanceManager: &mockComplexityGovernanceManager{
487+
probeErr: governance.ErrEmbeddingRequestExecutorNotConfigured,
488+
}}
489+
490+
ctx := newTestRequestCtx(`{"provider":"openai","embedding_model":"text-embedding-3-small"}`)
491+
handler.probeComplexityEmbeddingDimension(ctx)
492+
493+
if ctx.Response.StatusCode() != fasthttp.StatusServiceUnavailable {
494+
t.Fatalf("expected status 503, got %d: %s", ctx.Response.StatusCode(), string(ctx.Response.Body()))
495+
}
496+
if !strings.Contains(string(ctx.Response.Body()), "still initializing") {
497+
t.Fatalf("expected retryable startup message, got %s", string(ctx.Response.Body()))
498+
}
499+
}
500+
478501
func TestComplexityAnalyzerConfigGetCanonicalizesLegacyRowWithoutRewritingIt(t *testing.T) {
479502
SetLogger(&mockLogger{})
480503
store := setupPricingOverrideHandlerStore(t)

transports/config.schema.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3652,7 +3652,7 @@
36523652
"description": "Embedding vector dimension"
36533653
},
36543654
"timeout": {
3655-
"description": "Per-request embedding timeout (duration string like '100ms', or milliseconds as a number; default: 100ms). On timeout the fallback classifier is used.",
3655+
"description": "Per-request embedding timeout (duration string like '500ms', or milliseconds as a number; default: 500ms). On timeout the fallback classifier is used.",
36563656
"oneOf": [
36573657
{
36583658
"type": "string",
@@ -3662,7 +3662,8 @@
36623662
"type": "number",
36633663
"exclusiveMinimum": 0
36643664
}
3665-
]
3665+
],
3666+
"default": "500ms"
36663667
},
36673668
"fallback": {
36683669
"type": "string",

0 commit comments

Comments
 (0)