Implement common ParseCommand interface

This commit is contained in:
2025-10-25 18:41:46 +02:00
parent 0a7420c9f9
commit bd6436840e
8 changed files with 36 additions and 27 deletions

View File

@@ -23,6 +23,7 @@ type backend interface {
SetPort(int) SetPort(int)
GetHost() string GetHost() string
Validate() error Validate() error
ParseCommand(string) (any, error)
} }
var backendConstructors = map[BackendType]func() backend{ var backendConstructors = map[BackendType]func() backend{

View File

@@ -378,19 +378,19 @@ func (o *LlamaServerOptions) BuildDockerArgs() []string {
return o.BuildCommandArgs() return o.BuildCommandArgs()
} }
// ParseLlamaCommand parses a llama-server command string into LlamaServerOptions // ParseCommand parses a llama-server command string into LlamaServerOptions
// Supports multiple formats: // Supports multiple formats:
// 1. Full command: "llama-server --model file.gguf" // 1. Full command: "llama-server --model file.gguf"
// 2. Full path: "/usr/local/bin/llama-server --model file.gguf" // 2. Full path: "/usr/local/bin/llama-server --model file.gguf"
// 3. Args only: "--model file.gguf --gpu-layers 32" // 3. Args only: "--model file.gguf --gpu-layers 32"
// 4. Multiline commands with backslashes // 4. Multiline commands with backslashes
func ParseLlamaCommand(command string) (*LlamaServerOptions, error) { func (o *LlamaServerOptions) ParseCommand(command string) (any, error) {
executableNames := []string{"llama-server"} executableNames := []string{"llama-server"}
var subcommandNames []string // Llama has no subcommands var subcommandNames []string // Llama has no subcommands
// Use package-level llamaMultiValuedFlags variable // Use package-level llamaMultiValuedFlags variable
var llamaOptions LlamaServerOptions var llamaOptions LlamaServerOptions
if err := ParseCommand(command, executableNames, subcommandNames, llamaMultiValuedFlags, &llamaOptions); err != nil { if err := parseCommand(command, executableNames, subcommandNames, llamaMultiValuedFlags, &llamaOptions); err != nil {
return nil, err return nil, err
} }

View File

@@ -385,7 +385,9 @@ func TestParseLlamaCommand(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
result, err := backends.ParseLlamaCommand(tt.command) var opts backends.LlamaServerOptions
resultAny, err := opts.ParseCommand(tt.command)
result, _ := resultAny.(*backends.LlamaServerOptions)
if tt.expectErr { if tt.expectErr {
if err == nil { if err == nil {
@@ -413,7 +415,9 @@ func TestParseLlamaCommand(t *testing.T) {
func TestParseLlamaCommandArrays(t *testing.T) { func TestParseLlamaCommandArrays(t *testing.T) {
command := "llama-server --model test.gguf --lora adapter1.bin --lora=adapter2.bin" command := "llama-server --model test.gguf --lora adapter1.bin --lora=adapter2.bin"
result, err := backends.ParseLlamaCommand(command) var opts backends.LlamaServerOptions
resultAny, err := opts.ParseCommand(command)
result, _ := resultAny.(*backends.LlamaServerOptions)
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
@@ -429,4 +433,4 @@ func TestParseLlamaCommandArrays(t *testing.T) {
t.Errorf("expected lora[%d]=%s got %s", i, v, result.Lora[i]) t.Errorf("expected lora[%d]=%s got %s", i, v, result.Lora[i])
} }
} }
} }

View File

@@ -70,19 +70,19 @@ func (o *MlxServerOptions) BuildDockerArgs() []string {
return []string{} return []string{}
} }
// ParseMlxCommand parses a mlx_lm.server command string into MlxServerOptions // ParseCommand parses a mlx_lm.server command string into MlxServerOptions
// Supports multiple formats: // Supports multiple formats:
// 1. Full command: "mlx_lm.server --model model/path" // 1. Full command: "mlx_lm.server --model model/path"
// 2. Full path: "/usr/local/bin/mlx_lm.server --model model/path" // 2. Full path: "/usr/local/bin/mlx_lm.server --model model/path"
// 3. Args only: "--model model/path --host 0.0.0.0" // 3. Args only: "--model model/path --host 0.0.0.0"
// 4. Multiline commands with backslashes // 4. Multiline commands with backslashes
func ParseMlxCommand(command string) (*MlxServerOptions, error) { func (o *MlxServerOptions) ParseCommand(command string) (any, error) {
executableNames := []string{"mlx_lm.server"} executableNames := []string{"mlx_lm.server"}
var subcommandNames []string // MLX has no subcommands var subcommandNames []string // MLX has no subcommands
multiValuedFlags := map[string]bool{} // MLX has no multi-valued flags multiValuedFlags := map[string]bool{} // MLX has no multi-valued flags
var mlxOptions MlxServerOptions var mlxOptions MlxServerOptions
if err := ParseCommand(command, executableNames, subcommandNames, multiValuedFlags, &mlxOptions); err != nil { if err := parseCommand(command, executableNames, subcommandNames, multiValuedFlags, &mlxOptions); err != nil {
return nil, err return nil, err
} }

View File

@@ -96,7 +96,9 @@ func TestParseMlxCommand(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
result, err := backends.ParseMlxCommand(tt.command) var opts backends.MlxServerOptions
resultAny, err := opts.ParseCommand(tt.command)
result, _ := resultAny.(*backends.MlxServerOptions)
if tt.expectErr { if tt.expectErr {
if err == nil { if err == nil {
@@ -174,11 +176,11 @@ func TestMlxBuildCommandArgs_BooleanFields(t *testing.T) {
func TestMlxBuildCommandArgs_ZeroValues(t *testing.T) { func TestMlxBuildCommandArgs_ZeroValues(t *testing.T) {
options := backends.MlxServerOptions{ options := backends.MlxServerOptions{
Port: 0, // Should be excluded Port: 0, // Should be excluded
TopK: 0, // Should be excluded TopK: 0, // Should be excluded
Temp: 0, // Should be excluded Temp: 0, // Should be excluded
Model: "", // Should be excluded Model: "", // Should be excluded
LogLevel: "", // Should be excluded LogLevel: "", // Should be excluded
TrustRemoteCode: false, // Should be excluded TrustRemoteCode: false, // Should be excluded
} }
@@ -199,4 +201,4 @@ func TestMlxBuildCommandArgs_ZeroValues(t *testing.T) {
t.Errorf("Zero value argument %q should not be present in %v", excludedArg, args) t.Errorf("Zero value argument %q should not be present in %v", excludedArg, args)
} }
} }
} }

View File

@@ -9,8 +9,8 @@ import (
"strings" "strings"
) )
// ParseCommand parses a command string into a target struct // parseCommand parses a command string into a target struct
func ParseCommand(command string, executableNames []string, subcommandNames []string, multiValuedFlags map[string]bool, target any) error { func parseCommand(command string, executableNames []string, subcommandNames []string, multiValuedFlags map[string]bool, target any) error {
// Normalize multiline commands // Normalize multiline commands
command = normalizeCommand(command) command = normalizeCommand(command)
if command == "" { if command == "" {

View File

@@ -202,14 +202,14 @@ func (o *VllmServerOptions) BuildDockerArgs() []string {
return args return args
} }
// ParseVllmCommand parses a vLLM serve command string into VllmServerOptions // ParseCommand parses a vLLM serve command string into VllmServerOptions
// Supports multiple formats: // Supports multiple formats:
// 1. Full command: "vllm serve --model MODEL_NAME --other-args" // 1. Full command: "vllm serve --model MODEL_NAME --other-args"
// 2. Full path: "/usr/local/bin/vllm serve --model MODEL_NAME" // 2. Full path: "/usr/local/bin/vllm serve --model MODEL_NAME"
// 3. Serve only: "serve --model MODEL_NAME --other-args" // 3. Serve only: "serve --model MODEL_NAME --other-args"
// 4. Args only: "--model MODEL_NAME --other-args" // 4. Args only: "--model MODEL_NAME --other-args"
// 5. Multiline commands with backslashes // 5. Multiline commands with backslashes
func ParseVllmCommand(command string) (*VllmServerOptions, error) { func (o *VllmServerOptions) ParseCommand(command string) (any, error) {
executableNames := []string{"vllm"} executableNames := []string{"vllm"}
subcommandNames := []string{"serve"} subcommandNames := []string{"serve"}
multiValuedFlags := map[string]bool{ multiValuedFlags := map[string]bool{
@@ -223,7 +223,7 @@ func ParseVllmCommand(command string) (*VllmServerOptions, error) {
} }
var vllmOptions VllmServerOptions var vllmOptions VllmServerOptions
if err := ParseCommand(command, executableNames, subcommandNames, multiValuedFlags, &vllmOptions); err != nil { if err := parseCommand(command, executableNames, subcommandNames, multiValuedFlags, &vllmOptions); err != nil {
return nil, err return nil, err
} }

View File

@@ -92,7 +92,9 @@ func TestParseVllmCommand(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
result, err := backends.ParseVllmCommand(tt.command) var opts backends.VllmServerOptions
resultAny, err := opts.ParseCommand(tt.command)
result, _ := resultAny.(*backends.VllmServerOptions)
if tt.expectErr { if tt.expectErr {
if err == nil { if err == nil {
@@ -173,11 +175,11 @@ func TestVllmBuildCommandArgs_BooleanFields(t *testing.T) {
func TestVllmBuildCommandArgs_ZeroValues(t *testing.T) { func TestVllmBuildCommandArgs_ZeroValues(t *testing.T) {
options := backends.VllmServerOptions{ options := backends.VllmServerOptions{
Port: 0, // Should be excluded Port: 0, // Should be excluded
TensorParallelSize: 0, // Should be excluded TensorParallelSize: 0, // Should be excluded
GPUMemoryUtilization: 0, // Should be excluded GPUMemoryUtilization: 0, // Should be excluded
Model: "", // Should be excluded (positional arg) Model: "", // Should be excluded (positional arg)
Host: "", // Should be excluded Host: "", // Should be excluded
EnableLogOutputs: false, // Should be excluded EnableLogOutputs: false, // Should be excluded
} }