Add TTFT & ITL Metrics for LLM Inference #28

Merged
rock merged 11 commits from feat/sse-optimization-31-32-33 into main 2026-09-15 08:54:00 +00:00
4 changed files with 91 additions and 8 deletions
Showing only changes of commit 65928b109b - Show all commits
+4
View File
@@ -36,6 +36,10 @@ type ModelUpstream struct {
Address string Address string
// Path is the upstream path for this model (e.g., "/v1/chat/completions"). // Path is the upstream path for this model (e.g., "/v1/chat/completions").
Path string Path string
// UpstreamModel is the model name to send to the upstream server.
// If empty, the client-provided model name (Name) is used as-is.
// Use this when the upstream expects a different model name than clients send.
UpstreamModel string
// AuthRequired indicates whether this model requires JWT authentication. // AuthRequired indicates whether this model requires JWT authentication.
AuthRequired bool AuthRequired bool
} }
+10 -8
View File
@@ -37,10 +37,11 @@ type rawRoute struct {
// rawModel represents a single model entry in the YAML configuration. // rawModel represents a single model entry in the YAML configuration.
type rawModel struct { type rawModel struct {
Name string `yaml:"name"` Name string `yaml:"name"`
Address string `yaml:"address"` Address string `yaml:"address"`
Path string `yaml:"path"` Path string `yaml:"path"`
AuthRequired *bool `yaml:"authRequired"` UpstreamModel string `yaml:"upstreamModel"`
AuthRequired *bool `yaml:"authRequired"`
} }
// rawAdapter represents a service adapter in the YAML configuration. // rawAdapter represents a service adapter in the YAML configuration.
@@ -132,10 +133,11 @@ func LoadRoutesAndModelsFromFile(path string) (map[string]*Route, map[string]*Mo
authRequired = *rawModel.AuthRequired authRequired = *rawModel.AuthRequired
} }
models[rawModel.Name] = &ModelUpstream{ models[rawModel.Name] = &ModelUpstream{
Name: rawModel.Name, Name: rawModel.Name,
Address: rawModel.Address, Address: rawModel.Address,
Path: rawModel.Path, Path: rawModel.Path,
AuthRequired: authRequired, UpstreamModel: rawModel.UpstreamModel,
AuthRequired: authRequired,
} }
} }
+63
View File
@@ -381,3 +381,66 @@ func TestBodySizeCappedDispatch(t *testing.T) {
t.Errorf("expected 200 for reasonable body, got %d", resp.StatusCode) t.Errorf("expected 200 for reasonable body, got %d", resp.StatusCode)
} }
} }
// TestUpstreamModelRewrite verifies that the model field is rewritten when upstreamModel is set.
func TestUpstreamModelRewrite(t *testing.T) {
var receivedModel string
upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
var payload map[string]interface{}
json.Unmarshal(body, &payload)
receivedModel = payload["model"].(string)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{}`)
}))
defer upstreamServer.Close()
upstreamAddr := strings.TrimPrefix(upstreamServer.URL, "http://")
cfg := &config.Config{
Models: map[string]*config.ModelUpstream{
// Client sends "ornith:35b", upstream expects "qwen2.5:72b-instruct"
"ornith:35b": {
Name: "ornith:35b",
Address: upstreamAddr,
UpstreamModel: "qwen2.5:72b-instruct",
},
// No rewrite - upstream model same as client model
"reasoning": {
Name: "reasoning",
Address: upstreamAddr,
},
},
Routes: make(map[string]*config.Route),
}
handler := New(cfg)
defer handler.Close()
server := httptest.NewServer(handler)
defer server.Close()
// Test 1: Model should be rewritten
requestBody := `{"model":"ornith:35b","messages":[{"role":"user","content":"hi"}]}`
resp, err := http.Post(server.URL+"/v1/chat/completions", "application/json", strings.NewReader(requestBody))
if err != nil {
t.Fatalf("request failed: %v", err)
}
resp.Body.Close()
if receivedModel != "qwen2.5:72b-instruct" {
t.Errorf("expected upstream to receive model 'qwen2.5:72b-instruct', got '%s'", receivedModel)
}
// Test 2: No rewrite when upstreamModel is empty
receivedModel = ""
requestBody = `{"model":"reasoning","messages":[{"role":"user","content":"hi"}]}`
resp, _ = http.Post(server.URL+"/v1/chat/completions", "application/json", strings.NewReader(requestBody))
resp.Body.Close()
if receivedModel != "reasoning" {
t.Errorf("expected upstream to receive model 'reasoning', got '%s'", receivedModel)
}
}
+14
View File
@@ -122,6 +122,20 @@ func (h *Handler) routeByModel(r *http.Request, path string) (*Route, error) {
} }
} }
// If upstream expects a different model name, rewrite the body
if modelUpstream.UpstreamModel != "" && modelUpstream.UpstreamModel != modelName {
payload["model"] = modelUpstream.UpstreamModel
newBody, err := json.Marshal(payload)
if err != nil {
return nil, &modelValidationError{
Kind: "invalid_request",
Message: fmt.Sprintf("failed to rewrite model name: %v", err),
}
}
r.Body = io.NopCloser(bytes.NewReader(newBody))
r.ContentLength = int64(len(newBody))
}
// Determine the upstream path based on the request path // Determine the upstream path based on the request path
upstreamPath := path upstreamPath := path
if path == "/v1/rerank" { if path == "/v1/rerank" {