Skip to content

Commit 9819bb7

Browse files
authored
feat: add validation for vllm endpoint address (#202)
Signed-off-by: bitliu <[email protected]>
1 parent 34bb8fa commit 9819bb7

File tree

9 files changed

+672
-12
lines changed

9 files changed

+672
-12
lines changed

config/config.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,12 @@ prompt_guard:
2727
jailbreak_mapping_path: "models/jailbreak_classifier_modernbert-base_model/jailbreak_type_mapping.json"
2828

2929
# vLLM Endpoints Configuration
30+
# IMPORTANT: 'address' field must be a valid IP address (IPv4 or IPv6)
31+
# Supported formats: 127.0.0.1, 192.168.1.1, ::1, 2001:db8::1
32+
# NOT supported: domain names (example.com), protocol prefixes (http://), paths (/api), ports in address (use 'port' field)
3033
vllm_endpoints:
3134
- name: "endpoint1"
32-
address: "127.0.0.1"
35+
address: "127.0.0.1" # IPv4 address - REQUIRED format
3336
port: 8000
3437
models:
3538
- "openai/gpt-oss-20b"

deploy/kubernetes/config.yaml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,25 @@ prompt_guard:
2222
jailbreak_mapping_path: "models/jailbreak_classifier_modernbert-base_model/jailbreak_type_mapping.json"
2323

2424
# vLLM Endpoints Configuration - supports multiple endpoints, each can serve multiple models
25+
# IMPORTANT: 'address' field must be a valid IP address (IPv4 or IPv6)
26+
# Supported formats: 127.0.0.1, 192.168.1.1, ::1, 2001:db8::1
27+
# NOT supported: domain names (example.com), protocol prefixes (http://), paths (/api), ports in address (use 'port' field)
2528
vllm_endpoints:
2629
- name: "endpoint1"
27-
address: "127.0.0.1"
30+
address: "127.0.0.1" # IPv4 address - REQUIRED format
2831
port: 11434
2932
models:
3033
- "phi4"
3134
- "gemma3:27b"
3235
weight: 1 # Load balancing weight
3336
- name: "endpoint2"
34-
address: "127.0.0.1"
37+
address: "127.0.0.1" # IPv4 address - REQUIRED format
3538
port: 11434
3639
models:
3740
- "mistral-small3.1"
3841
weight: 1
3942
- name: "endpoint3"
40-
address: "127.0.0.1"
43+
address: "127.0.0.1" # IPv4 address - REQUIRED format
4144
port: 11434
4245
models:
4346
- "phi4" # Same model can be served by multiple endpoints for redundancy

src/semantic-router/pkg/api/server_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ func TestOpenAIModelsEndpoint(t *testing.T) {
254254
VLLMEndpoints: []config.VLLMEndpoint{
255255
{
256256
Name: "primary",
257-
Address: "localhost",
257+
Address: "127.0.0.1", // Changed from localhost to IP address
258258
Port: 8000,
259259
Models: []string{"gpt-4o-mini", "llama-3.1-8b-instruct"},
260260
Weight: 1,

src/semantic-router/pkg/config/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,11 @@ func validateConfigStructure(cfg *RouterConfig) error {
364364
}
365365
}
366366

367+
// Validate vLLM endpoints address formats
368+
if err := validateVLLMEndpoints(cfg.VLLMEndpoints); err != nil {
369+
return err
370+
}
371+
367372
return nil
368373
}
369374

src/semantic-router/pkg/config/config_test.go

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -998,6 +998,249 @@ default_model: "missing-default-model"
998998
Expect(err.Error()).To(ContainSubstring("missing-default-model"))
999999
})
10001000
})
1001+
1002+
Describe("vLLM Endpoint Address Validation", func() {
1003+
Context("with valid IP addresses", func() {
1004+
It("should accept IPv4 addresses", func() {
1005+
configContent := `
1006+
vllm_endpoints:
1007+
- name: "endpoint1"
1008+
address: "127.0.0.1"
1009+
port: 8000
1010+
models:
1011+
- "test-model"
1012+
weight: 1
1013+
1014+
categories:
1015+
- name: "test"
1016+
model_scores:
1017+
- model: "test-model"
1018+
score: 0.9
1019+
use_reasoning: true
1020+
1021+
default_model: "test-model"
1022+
`
1023+
err := os.WriteFile(configFile, []byte(configContent), 0o644)
1024+
Expect(err).NotTo(HaveOccurred())
1025+
1026+
cfg, err := config.LoadConfig(configFile)
1027+
Expect(err).NotTo(HaveOccurred())
1028+
Expect(cfg.VLLMEndpoints[0].Address).To(Equal("127.0.0.1"))
1029+
})
1030+
1031+
It("should accept IPv6 addresses", func() {
1032+
configContent := `
1033+
vllm_endpoints:
1034+
- name: "endpoint1"
1035+
address: "::1"
1036+
port: 8000
1037+
models:
1038+
- "test-model"
1039+
weight: 1
1040+
1041+
categories:
1042+
- name: "test"
1043+
model_scores:
1044+
- model: "test-model"
1045+
score: 0.9
1046+
use_reasoning: true
1047+
1048+
default_model: "test-model"
1049+
`
1050+
err := os.WriteFile(configFile, []byte(configContent), 0o644)
1051+
Expect(err).NotTo(HaveOccurred())
1052+
1053+
cfg, err := config.LoadConfig(configFile)
1054+
Expect(err).NotTo(HaveOccurred())
1055+
Expect(cfg.VLLMEndpoints[0].Address).To(Equal("::1"))
1056+
})
1057+
})
1058+
1059+
Context("with invalid address formats", func() {
1060+
It("should reject domain names", func() {
1061+
configContent := `
1062+
vllm_endpoints:
1063+
- name: "endpoint1"
1064+
address: "example.com"
1065+
port: 8000
1066+
models:
1067+
- "test-model"
1068+
weight: 1
1069+
1070+
categories:
1071+
- name: "test"
1072+
model_scores:
1073+
- model: "test-model"
1074+
score: 0.9
1075+
use_reasoning: true
1076+
1077+
default_model: "test-model"
1078+
`
1079+
err := os.WriteFile(configFile, []byte(configContent), 0o644)
1080+
Expect(err).NotTo(HaveOccurred())
1081+
1082+
_, err = config.LoadConfig(configFile)
1083+
Expect(err).To(HaveOccurred())
1084+
Expect(err.Error()).To(ContainSubstring("endpoint1"))
1085+
Expect(err.Error()).To(ContainSubstring("address validation failed"))
1086+
Expect(err.Error()).To(ContainSubstring("invalid IP address format"))
1087+
})
1088+
1089+
It("should reject protocol prefixes", func() {
1090+
configContent := `
1091+
vllm_endpoints:
1092+
- name: "endpoint1"
1093+
address: "http://127.0.0.1"
1094+
port: 8000
1095+
models:
1096+
- "test-model"
1097+
weight: 1
1098+
1099+
categories:
1100+
- name: "test"
1101+
model_scores:
1102+
- model: "test-model"
1103+
score: 0.9
1104+
use_reasoning: true
1105+
1106+
default_model: "test-model"
1107+
`
1108+
err := os.WriteFile(configFile, []byte(configContent), 0o644)
1109+
Expect(err).NotTo(HaveOccurred())
1110+
1111+
_, err = config.LoadConfig(configFile)
1112+
Expect(err).To(HaveOccurred())
1113+
Expect(err.Error()).To(ContainSubstring("protocol prefixes"))
1114+
Expect(err.Error()).To(ContainSubstring("are not supported"))
1115+
})
1116+
1117+
It("should reject addresses with paths", func() {
1118+
configContent := `
1119+
vllm_endpoints:
1120+
- name: "endpoint1"
1121+
address: "127.0.0.1/api"
1122+
port: 8000
1123+
models:
1124+
- "test-model"
1125+
weight: 1
1126+
1127+
categories:
1128+
- name: "test"
1129+
model_scores:
1130+
- model: "test-model"
1131+
score: 0.9
1132+
use_reasoning: true
1133+
1134+
default_model: "test-model"
1135+
`
1136+
err := os.WriteFile(configFile, []byte(configContent), 0o644)
1137+
Expect(err).NotTo(HaveOccurred())
1138+
1139+
_, err = config.LoadConfig(configFile)
1140+
Expect(err).To(HaveOccurred())
1141+
Expect(err.Error()).To(ContainSubstring("paths are not supported"))
1142+
})
1143+
1144+
It("should reject addresses with port numbers", func() {
1145+
configContent := `
1146+
vllm_endpoints:
1147+
- name: "endpoint1"
1148+
address: "127.0.0.1:8080"
1149+
port: 8000
1150+
models:
1151+
- "test-model"
1152+
weight: 1
1153+
1154+
categories:
1155+
- name: "test"
1156+
model_scores:
1157+
- model: "test-model"
1158+
score: 0.9
1159+
use_reasoning: true
1160+
1161+
default_model: "test-model"
1162+
`
1163+
err := os.WriteFile(configFile, []byte(configContent), 0o644)
1164+
Expect(err).NotTo(HaveOccurred())
1165+
1166+
_, err = config.LoadConfig(configFile)
1167+
Expect(err).To(HaveOccurred())
1168+
Expect(err.Error()).To(ContainSubstring("port numbers in address are not supported"))
1169+
Expect(err.Error()).To(ContainSubstring("use 'port' field instead"))
1170+
})
1171+
1172+
It("should provide comprehensive error messages", func() {
1173+
configContent := `
1174+
vllm_endpoints:
1175+
- name: "test-endpoint"
1176+
address: "https://example.com"
1177+
port: 8000
1178+
models:
1179+
- "test-model"
1180+
weight: 1
1181+
1182+
categories:
1183+
- name: "test"
1184+
model_scores:
1185+
- model: "test-model"
1186+
score: 0.9
1187+
use_reasoning: true
1188+
1189+
default_model: "test-model"
1190+
`
1191+
err := os.WriteFile(configFile, []byte(configContent), 0o644)
1192+
Expect(err).NotTo(HaveOccurred())
1193+
1194+
_, err = config.LoadConfig(configFile)
1195+
Expect(err).To(HaveOccurred())
1196+
1197+
errorMsg := err.Error()
1198+
Expect(errorMsg).To(ContainSubstring("test-endpoint"))
1199+
Expect(errorMsg).To(ContainSubstring("Supported formats"))
1200+
Expect(errorMsg).To(ContainSubstring("IPv4: 192.168.1.1"))
1201+
Expect(errorMsg).To(ContainSubstring("IPv6: ::1"))
1202+
Expect(errorMsg).To(ContainSubstring("Unsupported formats"))
1203+
Expect(errorMsg).To(ContainSubstring("Domain names: example.com"))
1204+
Expect(errorMsg).To(ContainSubstring("Protocol prefixes: http://"))
1205+
})
1206+
})
1207+
1208+
Context("with multiple endpoints", func() {
1209+
It("should validate all endpoints", func() {
1210+
configContent := `
1211+
vllm_endpoints:
1212+
- name: "endpoint1"
1213+
address: "127.0.0.1"
1214+
port: 8000
1215+
models:
1216+
- "test-model1"
1217+
weight: 1
1218+
- name: "endpoint2"
1219+
address: "example.com"
1220+
port: 8001
1221+
models:
1222+
- "test-model2"
1223+
weight: 1
1224+
1225+
categories:
1226+
- name: "test"
1227+
model_scores:
1228+
- model: "test-model1"
1229+
score: 0.9
1230+
use_reasoning: true
1231+
1232+
default_model: "test-model1"
1233+
`
1234+
err := os.WriteFile(configFile, []byte(configContent), 0o644)
1235+
Expect(err).NotTo(HaveOccurred())
1236+
1237+
_, err = config.LoadConfig(configFile)
1238+
Expect(err).To(HaveOccurred())
1239+
Expect(err.Error()).To(ContainSubstring("endpoint2"))
1240+
Expect(err.Error()).To(ContainSubstring("invalid IP address format"))
1241+
})
1242+
})
1243+
})
10011244
})
10021245

10031246
Describe("Semantic Cache Backend Configuration", func() {
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package config
2+
3+
import (
4+
"fmt"
5+
"net"
6+
"regexp"
7+
"strings"
8+
)
9+
10+
var (
11+
// Pre-compiled regular expressions for better performance
12+
protocolRegex = regexp.MustCompile(`^https?://`)
13+
pathRegex = regexp.MustCompile(`/`)
14+
// Pattern to match IPv4 address followed by port number
15+
ipv4PortRegex = regexp.MustCompile(`^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+$`)
16+
// Pattern to match IPv6 address followed by port number [::1]:8080
17+
ipv6PortRegex = regexp.MustCompile(`^\[.*\]:\d+$`)
18+
)
19+
20+
// validateIPAddress validates IP address format
21+
// Supports IPv4 and IPv6 addresses, rejects domain names, protocol prefixes, paths, etc.
22+
func validateIPAddress(address string) error {
23+
// Check for empty string
24+
trimmed := strings.TrimSpace(address)
25+
if trimmed == "" {
26+
return fmt.Errorf("address cannot be empty")
27+
}
28+
29+
// Check for protocol prefixes (http://, https://)
30+
if protocolRegex.MatchString(trimmed) {
31+
return fmt.Errorf("protocol prefixes (http://, https://) are not supported, got: %s", address)
32+
}
33+
34+
// Check for paths (contains / character)
35+
if pathRegex.MatchString(trimmed) {
36+
return fmt.Errorf("paths are not supported, got: %s", address)
37+
}
38+
39+
// Check for port numbers (IPv4 address followed by port or IPv6 address followed by port)
40+
if ipv4PortRegex.MatchString(trimmed) || ipv6PortRegex.MatchString(trimmed) {
41+
return fmt.Errorf("port numbers in address are not supported, use 'port' field instead, got: %s", address)
42+
}
43+
44+
// Use Go standard library to validate IP address format
45+
ip := net.ParseIP(trimmed)
46+
if ip == nil {
47+
return fmt.Errorf("invalid IP address format, got: %s", address)
48+
}
49+
50+
return nil
51+
}
52+
53+
// validateVLLMEndpoints validates the address format of all vLLM endpoints
54+
func validateVLLMEndpoints(endpoints []VLLMEndpoint) error {
55+
for _, endpoint := range endpoints {
56+
if err := validateIPAddress(endpoint.Address); err != nil {
57+
return fmt.Errorf("vLLM endpoint '%s' address validation failed: %w\n\nSupported formats:\n- IPv4: 192.168.1.1, 127.0.0.1\n- IPv6: ::1, 2001:db8::1\n\nUnsupported formats:\n- Domain names: example.com, localhost\n- Protocol prefixes: http://, https://\n- Paths: /api/v1, /health\n- Ports in address: use 'port' field instead", endpoint.Name, err)
58+
}
59+
}
60+
return nil
61+
}
62+
63+
// isValidIPv4 checks if the address is a valid IPv4 address
64+
func isValidIPv4(address string) bool {
65+
ip := net.ParseIP(address)
66+
return ip != nil && ip.To4() != nil
67+
}
68+
69+
// isValidIPv6 checks if the address is a valid IPv6 address
70+
func isValidIPv6(address string) bool {
71+
ip := net.ParseIP(address)
72+
return ip != nil && ip.To4() == nil
73+
}
74+
75+
// getIPAddressType returns the IP address type information for error messages and debugging
76+
func getIPAddressType(address string) string {
77+
if isValidIPv4(address) {
78+
return "IPv4"
79+
}
80+
if isValidIPv6(address) {
81+
return "IPv6"
82+
}
83+
return "invalid"
84+
}

0 commit comments

Comments
 (0)