Flatten backends package structure

This commit is contained in:
2025-10-19 15:50:42 +02:00
parent f209bc88b6
commit 2a7010d0e1
14 changed files with 128 additions and 151 deletions

View File

@@ -8,3 +8,15 @@ const (
BackendTypeVllm BackendType = "vllm"
// BackendTypeMlxVlm BackendType = "mlx_vlm" // Future expansion
)
type Options struct {
BackendType BackendType `json:"backend_type"`
BackendOptions map[string]any `json:"backend_options,omitempty"`
Nodes map[string]struct{} `json:"-"`
// Backend-specific options
LlamaServerOptions *LlamaServerOptions `json:"-"`
MlxServerOptions *MlxServerOptions `json:"-"`
VllmServerOptions *VllmServerOptions `json:"-"`
}

View File

@@ -1,15 +1,14 @@
package llamacpp
package backends
import (
"encoding/json"
"llamactl/pkg/backends"
"reflect"
"strconv"
)
// multiValuedFlags defines flags that should be repeated for each value rather than comma-separated
// llamaMultiValuedFlags defines flags that should be repeated for each value rather than comma-separated
// Used for both parsing (with underscores) and building (with dashes)
var multiValuedFlags = map[string]bool{
var llamaMultiValuedFlags = map[string]bool{
// Parsing keys (with underscores)
"override_tensor": true,
"override_kv": true,
@@ -338,8 +337,8 @@ func (o *LlamaServerOptions) UnmarshalJSON(data []byte) error {
// BuildCommandArgs converts InstanceOptions to command line arguments
func (o *LlamaServerOptions) BuildCommandArgs() []string {
// Llama uses multiple flags for arrays by default (not comma-separated)
// Use package-level multiValuedFlags variable
return backends.BuildCommandArgs(o, multiValuedFlags)
// Use package-level llamaMultiValuedFlags variable
return BuildCommandArgs(o, llamaMultiValuedFlags)
}
func (o *LlamaServerOptions) BuildDockerArgs() []string {
@@ -356,10 +355,10 @@ func (o *LlamaServerOptions) BuildDockerArgs() []string {
func ParseLlamaCommand(command string) (*LlamaServerOptions, error) {
executableNames := []string{"llama-server"}
var subcommandNames []string // Llama has no subcommands
// Use package-level multiValuedFlags variable
// Use package-level llamaMultiValuedFlags variable
var llamaOptions LlamaServerOptions
if err := backends.ParseCommand(command, executableNames, subcommandNames, multiValuedFlags, &llamaOptions); err != nil {
if err := ParseCommand(command, executableNames, subcommandNames, llamaMultiValuedFlags, &llamaOptions); err != nil {
return nil, err
}

View File

@@ -1,16 +1,16 @@
package llamacpp_test
package backends_test
import (
"encoding/json"
"fmt"
"llamactl/pkg/backends/llamacpp"
"llamactl/pkg/backends"
"reflect"
"slices"
"testing"
)
func TestBuildCommandArgs_BasicFields(t *testing.T) {
options := llamacpp.LlamaServerOptions{
func TestLlamaCppBuildCommandArgs_BasicFields(t *testing.T) {
options := backends.LlamaServerOptions{
Model: "/path/to/model.gguf",
Port: 8080,
Host: "localhost",
@@ -42,30 +42,30 @@ func TestBuildCommandArgs_BasicFields(t *testing.T) {
}
}
func TestBuildCommandArgs_BooleanFields(t *testing.T) {
func TestLlamaCppBuildCommandArgs_BooleanFields(t *testing.T) {
tests := []struct {
name string
options llamacpp.LlamaServerOptions
options backends.LlamaServerOptions
expected []string
excluded []string
}{
{
name: "verbose true",
options: llamacpp.LlamaServerOptions{
options: backends.LlamaServerOptions{
Verbose: true,
},
expected: []string{"--verbose"},
},
{
name: "verbose false",
options: llamacpp.LlamaServerOptions{
options: backends.LlamaServerOptions{
Verbose: false,
},
excluded: []string{"--verbose"},
},
{
name: "multiple booleans",
options: llamacpp.LlamaServerOptions{
options: backends.LlamaServerOptions{
Verbose: true,
FlashAttn: true,
Mlock: false,
@@ -95,8 +95,8 @@ func TestBuildCommandArgs_BooleanFields(t *testing.T) {
}
}
func TestBuildCommandArgs_NumericFields(t *testing.T) {
options := llamacpp.LlamaServerOptions{
func TestLlamaCppBuildCommandArgs_NumericFields(t *testing.T) {
options := backends.LlamaServerOptions{
Port: 8080,
Threads: 4,
CtxSize: 2048,
@@ -125,8 +125,8 @@ func TestBuildCommandArgs_NumericFields(t *testing.T) {
}
}
func TestBuildCommandArgs_ZeroValues(t *testing.T) {
options := llamacpp.LlamaServerOptions{
func TestLlamaCppBuildCommandArgs_ZeroValues(t *testing.T) {
options := backends.LlamaServerOptions{
Port: 0, // Should be excluded
Threads: 0, // Should be excluded
Temperature: 0, // Should be excluded
@@ -152,8 +152,8 @@ func TestBuildCommandArgs_ZeroValues(t *testing.T) {
}
}
func TestBuildCommandArgs_ArrayFields(t *testing.T) {
options := llamacpp.LlamaServerOptions{
func TestLlamaCppBuildCommandArgs_ArrayFields(t *testing.T) {
options := backends.LlamaServerOptions{
Lora: []string{"adapter1.bin", "adapter2.bin"},
OverrideTensor: []string{"tensor1", "tensor2", "tensor3"},
DrySequenceBreaker: []string{".", "!", "?"},
@@ -177,8 +177,8 @@ func TestBuildCommandArgs_ArrayFields(t *testing.T) {
}
}
func TestBuildCommandArgs_EmptyArrays(t *testing.T) {
options := llamacpp.LlamaServerOptions{
func TestLlamaCppBuildCommandArgs_EmptyArrays(t *testing.T) {
options := backends.LlamaServerOptions{
Lora: []string{}, // Empty array should not generate args
OverrideTensor: []string{}, // Empty array should not generate args
}
@@ -193,9 +193,9 @@ func TestBuildCommandArgs_EmptyArrays(t *testing.T) {
}
}
func TestBuildCommandArgs_FieldNameConversion(t *testing.T) {
func TestLlamaCppBuildCommandArgs_FieldNameConversion(t *testing.T) {
// Test snake_case to kebab-case conversion
options := llamacpp.LlamaServerOptions{
options := backends.LlamaServerOptions{
CtxSize: 4096,
GPULayers: 32,
ThreadsBatch: 2,
@@ -223,7 +223,7 @@ func TestBuildCommandArgs_FieldNameConversion(t *testing.T) {
}
}
func TestUnmarshalJSON_StandardFields(t *testing.T) {
func TestLlamaCppUnmarshalJSON_StandardFields(t *testing.T) {
jsonData := `{
"model": "/path/to/model.gguf",
"port": 8080,
@@ -234,7 +234,7 @@ func TestUnmarshalJSON_StandardFields(t *testing.T) {
"temp": 0.7
}`
var options llamacpp.LlamaServerOptions
var options backends.LlamaServerOptions
err := json.Unmarshal([]byte(jsonData), &options)
if err != nil {
t.Fatalf("Unmarshal failed: %v", err)
@@ -263,16 +263,16 @@ func TestUnmarshalJSON_StandardFields(t *testing.T) {
}
}
func TestUnmarshalJSON_AlternativeFieldNames(t *testing.T) {
func TestLlamaCppUnmarshalJSON_AlternativeFieldNames(t *testing.T) {
tests := []struct {
name string
jsonData string
checkFn func(llamacpp.LlamaServerOptions) error
checkFn func(backends.LlamaServerOptions) error
}{
{
name: "threads alternatives",
jsonData: `{"t": 4, "tb": 2}`,
checkFn: func(opts llamacpp.LlamaServerOptions) error {
checkFn: func(opts backends.LlamaServerOptions) error {
if opts.Threads != 4 {
return fmt.Errorf("expected threads 4, got %d", opts.Threads)
}
@@ -285,7 +285,7 @@ func TestUnmarshalJSON_AlternativeFieldNames(t *testing.T) {
{
name: "context size alternatives",
jsonData: `{"c": 2048}`,
checkFn: func(opts llamacpp.LlamaServerOptions) error {
checkFn: func(opts backends.LlamaServerOptions) error {
if opts.CtxSize != 2048 {
return fmt.Errorf("expected ctx_size 4096, got %d", opts.CtxSize)
}
@@ -295,7 +295,7 @@ func TestUnmarshalJSON_AlternativeFieldNames(t *testing.T) {
{
name: "gpu layers alternatives",
jsonData: `{"ngl": 16}`,
checkFn: func(opts llamacpp.LlamaServerOptions) error {
checkFn: func(opts backends.LlamaServerOptions) error {
if opts.GPULayers != 16 {
return fmt.Errorf("expected gpu_layers 32, got %d", opts.GPULayers)
}
@@ -305,7 +305,7 @@ func TestUnmarshalJSON_AlternativeFieldNames(t *testing.T) {
{
name: "model alternatives",
jsonData: `{"m": "/path/model.gguf"}`,
checkFn: func(opts llamacpp.LlamaServerOptions) error {
checkFn: func(opts backends.LlamaServerOptions) error {
if opts.Model != "/path/model.gguf" {
return fmt.Errorf("expected model '/path/model.gguf', got %q", opts.Model)
}
@@ -315,7 +315,7 @@ func TestUnmarshalJSON_AlternativeFieldNames(t *testing.T) {
{
name: "temperature alternatives",
jsonData: `{"temp": 0.8}`,
checkFn: func(opts llamacpp.LlamaServerOptions) error {
checkFn: func(opts backends.LlamaServerOptions) error {
if opts.Temperature != 0.8 {
return fmt.Errorf("expected temperature 0.8, got %f", opts.Temperature)
}
@@ -326,7 +326,7 @@ func TestUnmarshalJSON_AlternativeFieldNames(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var options llamacpp.LlamaServerOptions
var options backends.LlamaServerOptions
err := json.Unmarshal([]byte(tt.jsonData), &options)
if err != nil {
t.Fatalf("Unmarshal failed: %v", err)
@@ -339,24 +339,24 @@ func TestUnmarshalJSON_AlternativeFieldNames(t *testing.T) {
}
}
func TestUnmarshalJSON_InvalidJSON(t *testing.T) {
func TestLlamaCppUnmarshalJSON_InvalidJSON(t *testing.T) {
invalidJSON := `{"port": "not-a-number", "invalid": syntax}`
var options llamacpp.LlamaServerOptions
var options backends.LlamaServerOptions
err := json.Unmarshal([]byte(invalidJSON), &options)
if err == nil {
t.Error("Expected error for invalid JSON")
}
}
func TestUnmarshalJSON_ArrayFields(t *testing.T) {
func TestLlamaCppUnmarshalJSON_ArrayFields(t *testing.T) {
jsonData := `{
"lora": ["adapter1.bin", "adapter2.bin"],
"override_tensor": ["tensor1", "tensor2"],
"dry_sequence_breaker": [".", "!", "?"]
}`
var options llamacpp.LlamaServerOptions
var options backends.LlamaServerOptions
err := json.Unmarshal([]byte(jsonData), &options)
if err != nil {
t.Fatalf("Unmarshal failed: %v", err)
@@ -423,7 +423,7 @@ func TestParseLlamaCommand(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := llamacpp.ParseLlamaCommand(tt.command)
result, err := backends.ParseLlamaCommand(tt.command)
if tt.expectErr {
if err == nil {
@@ -446,7 +446,7 @@ func TestParseLlamaCommand(t *testing.T) {
func TestParseLlamaCommandValues(t *testing.T) {
command := "llama-server --model /test/model.gguf --gpu-layers 32 --temp 0.7 --verbose --no-mmap"
result, err := llamacpp.ParseLlamaCommand(command)
result, err := backends.ParseLlamaCommand(command)
if err != nil {
t.Fatalf("unexpected error: %v", err)
@@ -475,7 +475,7 @@ func TestParseLlamaCommandValues(t *testing.T) {
func TestParseLlamaCommandArrays(t *testing.T) {
command := "llama-server --model test.gguf --lora adapter1.bin --lora=adapter2.bin"
result, err := llamacpp.ParseLlamaCommand(command)
result, err := backends.ParseLlamaCommand(command)
if err != nil {
t.Fatalf("unexpected error: %v", err)

View File

@@ -1,8 +1,4 @@
package mlx
import (
"llamactl/pkg/backends"
)
package backends
type MlxServerOptions struct {
// Basic connection options
@@ -33,7 +29,7 @@ type MlxServerOptions struct {
// BuildCommandArgs converts to command line arguments
func (o *MlxServerOptions) BuildCommandArgs() []string {
multipleFlags := map[string]bool{} // MLX doesn't currently have []string fields
return backends.BuildCommandArgs(o, multipleFlags)
return BuildCommandArgs(o, multipleFlags)
}
// ParseMlxCommand parses a mlx_lm.server command string into MlxServerOptions
@@ -48,7 +44,7 @@ func ParseMlxCommand(command string) (*MlxServerOptions, error) {
multiValuedFlags := map[string]bool{} // MLX has no multi-valued flags
var mlxOptions MlxServerOptions
if err := backends.ParseCommand(command, executableNames, subcommandNames, multiValuedFlags, &mlxOptions); err != nil {
if err := ParseCommand(command, executableNames, subcommandNames, multiValuedFlags, &mlxOptions); err != nil {
return nil, err
}

View File

@@ -1,7 +1,7 @@
package mlx_test
package backends_test
import (
"llamactl/pkg/backends/mlx"
"llamactl/pkg/backends"
"testing"
)
@@ -50,7 +50,7 @@ func TestParseMlxCommand(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := mlx.ParseMlxCommand(tt.command)
result, err := backends.ParseMlxCommand(tt.command)
if tt.expectErr {
if err == nil {
@@ -73,7 +73,7 @@ func TestParseMlxCommand(t *testing.T) {
func TestParseMlxCommandValues(t *testing.T) {
command := "mlx_lm.server --model /test/model.mlx --port 8080 --temp 0.7 --trust-remote-code --log-level DEBUG"
result, err := mlx.ParseMlxCommand(command)
result, err := backends.ParseMlxCommand(command)
if err != nil {
t.Fatalf("unexpected error: %v", err)
@@ -100,8 +100,8 @@ func TestParseMlxCommandValues(t *testing.T) {
}
}
func TestBuildCommandArgs(t *testing.T) {
options := &mlx.MlxServerOptions{
func TestMlxBuildCommandArgs(t *testing.T) {
options := &backends.MlxServerOptions{
Model: "/test/model.mlx",
Host: "127.0.0.1",
Port: 8080,

View File

@@ -1,11 +1,7 @@
package vllm
package backends
import (
"llamactl/pkg/backends"
)
// multiValuedFlags defines flags that should be repeated for each value rather than comma-separated
var multiValuedFlags = map[string]bool{
// vllmMultiValuedFlags defines flags that should be repeated for each value rather than comma-separated
var vllmMultiValuedFlags = map[string]bool{
"api-key": true,
"allowed-origins": true,
"allowed-methods": true,
@@ -155,7 +151,7 @@ func (o *VllmServerOptions) BuildCommandArgs() []string {
// Use package-level multipleFlags variable
flagArgs := backends.BuildCommandArgs(&optionsCopy, multiValuedFlags)
flagArgs := BuildCommandArgs(&optionsCopy, vllmMultiValuedFlags)
args = append(args, flagArgs...)
return args
@@ -165,7 +161,7 @@ func (o *VllmServerOptions) BuildDockerArgs() []string {
var args []string
// Use package-level multipleFlags variable
flagArgs := backends.BuildCommandArgs(o, multiValuedFlags)
flagArgs := BuildCommandArgs(o, vllmMultiValuedFlags)
args = append(args, flagArgs...)
return args
@@ -192,7 +188,7 @@ func ParseVllmCommand(command string) (*VllmServerOptions, error) {
}
var vllmOptions VllmServerOptions
if err := backends.ParseCommand(command, executableNames, subcommandNames, multiValuedFlags, &vllmOptions); err != nil {
if err := ParseCommand(command, executableNames, subcommandNames, multiValuedFlags, &vllmOptions); err != nil {
return nil, err
}

View File

@@ -1,8 +1,7 @@
package vllm_test
package backends_test
import (
"llamactl/pkg/backends/vllm"
"slices"
"llamactl/pkg/backends"
"testing"
)
@@ -46,7 +45,7 @@ func TestParseVllmCommand(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := vllm.ParseVllmCommand(tt.command)
result, err := backends.ParseVllmCommand(tt.command)
if tt.expectErr {
if err == nil {
@@ -69,7 +68,7 @@ func TestParseVllmCommand(t *testing.T) {
func TestParseVllmCommandValues(t *testing.T) {
command := "vllm serve test-model --tensor-parallel-size 4 --gpu-memory-utilization 0.8 --enable-log-outputs"
result, err := vllm.ParseVllmCommand(command)
result, err := backends.ParseVllmCommand(command)
if err != nil {
t.Fatalf("unexpected error: %v", err)
@@ -89,8 +88,8 @@ func TestParseVllmCommandValues(t *testing.T) {
}
}
func TestBuildCommandArgs(t *testing.T) {
options := vllm.VllmServerOptions{
func TestVllmBuildCommandArgs(t *testing.T) {
options := backends.VllmServerOptions{
Model: "microsoft/DialoGPT-medium",
Port: 8080,
Host: "localhost",
@@ -137,17 +136,3 @@ func TestBuildCommandArgs(t *testing.T) {
t.Errorf("Expected 2 --allowed-origins flags, got %d", allowedOriginsCount)
}
}
// Helper functions
func contains(slice []string, item string) bool {
return slices.Contains(slice, item)
}
func containsFlagWithValue(args []string, flag, value string) bool {
for i, arg := range args {
if arg == flag && i+1 < len(args) && args[i+1] == value {
return true
}
}
return false
}