- Migrate config contract files to archive (01_config_contract) - Update edge config.go for new configuration structure - Refactor node/mapper.go and mapper_test.go for field mapping - Update node/store_test.go tests - Add new fields to configs/edge.yaml - Extend packages/go/config with new configuration options - Update protobuf definitions in runtime.proto and generated code
582 lines
24 KiB
Go
582 lines
24 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
const AgentKindGenericNode = "generic-node"
|
|
|
|
// NormalizeAgentKind returns the canonical agent kind for a node definition.
|
|
// An empty value defaults to generic-node; any other unsupported value is
|
|
// rejected so misconfigured kinds fail at config load time.
|
|
func NormalizeAgentKind(kind string) (string, error) {
|
|
switch kind {
|
|
case "":
|
|
return AgentKindGenericNode, nil
|
|
case AgentKindGenericNode:
|
|
return kind, nil
|
|
default:
|
|
return "", fmt.Errorf("invalid agent_kind %q (allowed: %q)", kind, AgentKindGenericNode)
|
|
}
|
|
}
|
|
|
|
type NodeConfig struct {
|
|
Transport TransportConf `mapstructure:"transport" yaml:"transport"`
|
|
Logging LoggingConf `mapstructure:"logging" yaml:"logging"`
|
|
Metrics MetricsConf `mapstructure:"metrics" yaml:"metrics"`
|
|
}
|
|
|
|
type EdgeConfig struct {
|
|
Edge EdgeInfo `mapstructure:"edge" yaml:"edge"`
|
|
Server EdgeServerConf `mapstructure:"server" yaml:"server"`
|
|
Bootstrap EdgeBootstrapConf `mapstructure:"bootstrap" yaml:"bootstrap"`
|
|
OpenAI EdgeOpenAIConf `mapstructure:"openai" yaml:"openai"`
|
|
A2A EdgeA2AConf `mapstructure:"a2a" yaml:"a2a"`
|
|
TLS TLSConf `mapstructure:"tls" yaml:"tls"`
|
|
Logging LoggingConf `mapstructure:"logging" yaml:"logging"`
|
|
Metrics MetricsConf `mapstructure:"metrics" yaml:"metrics"`
|
|
Console EdgeConsoleConf `mapstructure:"console" yaml:"console"`
|
|
ControlPlane EdgeControlPlaneConf `mapstructure:"control_plane" yaml:"control_plane"`
|
|
Nodes []NodeDefinition `mapstructure:"nodes" yaml:"nodes"`
|
|
}
|
|
|
|
// EdgeControlPlaneConf holds the outbound connector settings for the
|
|
// Control Plane-Edge wire. When Enabled is false or WireAddr is empty the
|
|
// connector is a no-op and existing Edge behaviour is unaffected.
|
|
type EdgeControlPlaneConf struct {
|
|
Enabled bool `mapstructure:"enabled" yaml:"enabled"`
|
|
WireAddr string `mapstructure:"wire_addr" yaml:"wire_addr"`
|
|
ReconnectIntervalSec int `mapstructure:"reconnect_interval_sec" yaml:"reconnect_interval_sec"`
|
|
}
|
|
|
|
// EdgeInfo carries this edge instance's stable identity for loading and logging.
|
|
// It is not used for Control Plane registration.
|
|
type EdgeInfo struct {
|
|
ID string `mapstructure:"id" yaml:"id"`
|
|
Name string `mapstructure:"name" yaml:"name"`
|
|
}
|
|
|
|
// NodeDefinition is the edge-side record for a pre-registered node.
|
|
type NodeDefinition struct {
|
|
ID string `mapstructure:"id" yaml:"id"` // stable node identity; if empty, a UUID v4 is auto-assigned (dev fallback only)
|
|
Alias string `mapstructure:"alias" yaml:"alias"`
|
|
Token string `mapstructure:"token" yaml:"token"`
|
|
AgentKind string `mapstructure:"agent_kind" yaml:"agent_kind"` // generic-node (default)
|
|
Adapters AdaptersConf `mapstructure:"adapters" yaml:"adapters"`
|
|
Runtime RuntimeConf `mapstructure:"runtime" yaml:"runtime"`
|
|
}
|
|
|
|
type NodeInfo struct {
|
|
ID string `mapstructure:"id" yaml:"id"`
|
|
Name string `mapstructure:"name" yaml:"name"`
|
|
}
|
|
|
|
type EdgeServerConf struct {
|
|
Listen string `mapstructure:"listen" yaml:"listen"`
|
|
AdvertiseHost string `mapstructure:"advertise_host" yaml:"advertise_host"`
|
|
}
|
|
|
|
type EdgeBootstrapConf struct {
|
|
ArtifactBaseURL string `mapstructure:"artifact_base_url" yaml:"artifact_base_url"`
|
|
Listen string `mapstructure:"listen" yaml:"listen"`
|
|
ArtifactDir string `mapstructure:"artifact_dir" yaml:"artifact_dir"`
|
|
}
|
|
|
|
// OpenAIRouteEntry maps an external model id to an internal adapter/target routing.
|
|
// Fields not set here fall back to the top-level EdgeOpenAIConf defaults.
|
|
type OpenAIRouteEntry struct {
|
|
Model string `mapstructure:"model" yaml:"model"`
|
|
NodeRef string `mapstructure:"node" yaml:"node,omitempty"`
|
|
Adapter string `mapstructure:"adapter" yaml:"adapter,omitempty"`
|
|
Target string `mapstructure:"target" yaml:"target"`
|
|
SessionID string `mapstructure:"session_id" yaml:"session_id,omitempty"`
|
|
TimeoutSec int `mapstructure:"timeout_sec" yaml:"timeout_sec,omitempty"`
|
|
WorkspaceRequired bool `mapstructure:"workspace_required" yaml:"workspace_required,omitempty"`
|
|
}
|
|
|
|
type EdgeOpenAIConf struct {
|
|
Enabled bool `mapstructure:"enabled" yaml:"enabled"`
|
|
Listen string `mapstructure:"listen" yaml:"listen"`
|
|
NodeRef string `mapstructure:"node" yaml:"node"`
|
|
Adapter string `mapstructure:"adapter" yaml:"adapter"`
|
|
Target string `mapstructure:"target" yaml:"target"`
|
|
Models []string `mapstructure:"models" yaml:"models"`
|
|
ModelRoutes []OpenAIRouteEntry `mapstructure:"model_routes" yaml:"model_routes,omitempty"`
|
|
SessionID string `mapstructure:"session_id" yaml:"session_id"`
|
|
TimeoutSec int `mapstructure:"timeout_sec" yaml:"timeout_sec"`
|
|
StrictOutput bool `mapstructure:"strict_output" yaml:"strict_output"`
|
|
StrictStreamBuffer bool `mapstructure:"strict_stream_buffer" yaml:"strict_stream_buffer"`
|
|
}
|
|
|
|
type EdgeA2AConf struct {
|
|
Enabled bool `mapstructure:"enabled" yaml:"enabled"`
|
|
Listen string `mapstructure:"listen" yaml:"listen"`
|
|
Path string `mapstructure:"path" yaml:"path"`
|
|
NodeRef string `mapstructure:"node" yaml:"node"`
|
|
Adapter string `mapstructure:"adapter" yaml:"adapter"`
|
|
Target string `mapstructure:"target" yaml:"target"`
|
|
SessionID string `mapstructure:"session_id" yaml:"session_id"`
|
|
TimeoutSec int `mapstructure:"timeout_sec" yaml:"timeout_sec"`
|
|
BearerToken string `mapstructure:"bearer_token" yaml:"bearer_token"`
|
|
}
|
|
|
|
type EdgeConsoleConf struct {
|
|
Adapter string `mapstructure:"adapter" yaml:"adapter"`
|
|
Target string `mapstructure:"target" yaml:"target"`
|
|
Agent string `mapstructure:"agent" yaml:"agent"` // legacy alias for target
|
|
Model string `mapstructure:"model" yaml:"model"` // legacy alias for target
|
|
SessionID string `mapstructure:"session_id" yaml:"session_id"`
|
|
Background bool `mapstructure:"background" yaml:"background"`
|
|
TimeoutSec int `mapstructure:"timeout_sec" yaml:"timeout_sec"`
|
|
}
|
|
|
|
func (c EdgeConsoleConf) ResolveTarget() string {
|
|
if c.Target != "" {
|
|
return c.Target
|
|
}
|
|
if c.Agent != "" {
|
|
return c.Agent
|
|
}
|
|
return c.Model
|
|
}
|
|
|
|
// ResolveAgent is kept for callers still using the legacy console.agent name.
|
|
func (c EdgeConsoleConf) ResolveAgent() string {
|
|
return c.ResolveTarget()
|
|
}
|
|
|
|
type TransportConf struct {
|
|
EdgeAddr string `mapstructure:"edge_addr" yaml:"edge_addr"`
|
|
Token string `mapstructure:"token" yaml:"token"`
|
|
}
|
|
|
|
type TLSConf struct {
|
|
Enabled bool `mapstructure:"enabled" yaml:"enabled"`
|
|
Cert string `mapstructure:"cert" yaml:"cert"`
|
|
Key string `mapstructure:"key" yaml:"key"`
|
|
CA string `mapstructure:"ca" yaml:"ca"`
|
|
}
|
|
|
|
type RuntimeConf struct {
|
|
Concurrency int `mapstructure:"concurrency" yaml:"concurrency"`
|
|
WorkspaceRoot string `mapstructure:"workspace_root" yaml:"workspace_root"`
|
|
}
|
|
|
|
type SQLiteConf struct {
|
|
DSN string `mapstructure:"dsn" yaml:"dsn"`
|
|
}
|
|
|
|
type LoggingConf struct {
|
|
Level string `mapstructure:"level" yaml:"level"`
|
|
Pretty bool `mapstructure:"pretty" yaml:"pretty"`
|
|
Path string `mapstructure:"path" yaml:"path"`
|
|
}
|
|
|
|
type MetricsConf struct {
|
|
Port int `mapstructure:"port" yaml:"port"`
|
|
}
|
|
|
|
type AdaptersConf struct {
|
|
// Legacy single-instance fields. When non-empty they are normalised into
|
|
// OllamaInstances / VllmInstances / OpenAICompatInstances during config load
|
|
// so that all downstream code only needs to inspect the slice fields.
|
|
Ollama OllamaConf `mapstructure:"ollama" yaml:"ollama"`
|
|
Vllm VllmConf `mapstructure:"vllm" yaml:"vllm"`
|
|
OpenAICompat OpenAICompatConf `mapstructure:"openai_compat" yaml:"openai_compat"`
|
|
CLI CLIConf `mapstructure:"cli" yaml:"cli"`
|
|
|
|
// Multi-instance collections. Each entry carries a unique Name that acts as
|
|
// the stable adapter instance identity within the node. Names must be unique
|
|
// within each type collection; duplicate names are rejected at load time.
|
|
OllamaInstances []OllamaInstanceConf `mapstructure:"ollama_instances" yaml:"ollama_instances,omitempty"`
|
|
VllmInstances []VllmInstanceConf `mapstructure:"vllm_instances" yaml:"vllm_instances,omitempty"`
|
|
OpenAICompatInstances []OpenAICompatInstanceConf `mapstructure:"openai_compat_instances" yaml:"openai_compat_instances,omitempty"`
|
|
}
|
|
|
|
// OllamaInstanceConf is one named Ollama engine instance within a node.
|
|
type OllamaInstanceConf struct {
|
|
Name string `mapstructure:"name" yaml:"name"`
|
|
Enabled bool `mapstructure:"enabled" yaml:"enabled"`
|
|
BaseURL string `mapstructure:"base_url" yaml:"base_url"`
|
|
ContextSize int `mapstructure:"context_size" yaml:"context_size"`
|
|
Capacity int `mapstructure:"capacity" yaml:"capacity"`
|
|
MaxQueue int `mapstructure:"max_queue" yaml:"max_queue"`
|
|
QueueTimeoutMS int `mapstructure:"queue_timeout_ms" yaml:"queue_timeout_ms"`
|
|
RequestTimeoutMS int `mapstructure:"request_timeout_ms" yaml:"request_timeout_ms"`
|
|
}
|
|
|
|
// VllmInstanceConf is one named vLLM engine instance within a node.
|
|
type VllmInstanceConf struct {
|
|
Name string `mapstructure:"name" yaml:"name"`
|
|
Enabled bool `mapstructure:"enabled" yaml:"enabled"`
|
|
Endpoint string `mapstructure:"endpoint" yaml:"endpoint"`
|
|
Capacity int `mapstructure:"capacity" yaml:"capacity"`
|
|
MaxQueue int `mapstructure:"max_queue" yaml:"max_queue"`
|
|
QueueTimeoutMS int `mapstructure:"queue_timeout_ms" yaml:"queue_timeout_ms"`
|
|
RequestTimeoutMS int `mapstructure:"request_timeout_ms" yaml:"request_timeout_ms"`
|
|
}
|
|
|
|
type OpenAICompatConf struct {
|
|
Enabled bool `mapstructure:"enabled" yaml:"enabled"`
|
|
Provider string `mapstructure:"provider" yaml:"provider"`
|
|
Endpoint string `mapstructure:"endpoint" yaml:"endpoint"`
|
|
Headers map[string]string `mapstructure:"headers" yaml:"headers"`
|
|
Capacity int `mapstructure:"capacity" yaml:"capacity"`
|
|
MaxQueue int `mapstructure:"max_queue" yaml:"max_queue"`
|
|
QueueTimeoutMS int `mapstructure:"queue_timeout_ms" yaml:"queue_timeout_ms"`
|
|
RequestTimeoutMS int `mapstructure:"request_timeout_ms" yaml:"request_timeout_ms"`
|
|
}
|
|
|
|
type OpenAICompatInstanceConf struct {
|
|
Name string `mapstructure:"name" yaml:"name"`
|
|
Enabled bool `mapstructure:"enabled" yaml:"enabled"`
|
|
Provider string `mapstructure:"provider" yaml:"provider"`
|
|
Endpoint string `mapstructure:"endpoint" yaml:"endpoint"`
|
|
Headers map[string]string `mapstructure:"headers" yaml:"headers"`
|
|
Capacity int `mapstructure:"capacity" yaml:"capacity"`
|
|
MaxQueue int `mapstructure:"max_queue" yaml:"max_queue"`
|
|
QueueTimeoutMS int `mapstructure:"queue_timeout_ms" yaml:"queue_timeout_ms"`
|
|
RequestTimeoutMS int `mapstructure:"request_timeout_ms" yaml:"request_timeout_ms"`
|
|
}
|
|
|
|
type OllamaConf struct {
|
|
Enabled bool `mapstructure:"enabled" yaml:"enabled"`
|
|
BaseURL string `mapstructure:"base_url" yaml:"base_url"`
|
|
ContextSize int `mapstructure:"context_size" yaml:"context_size"`
|
|
Capacity int `mapstructure:"capacity" yaml:"capacity"`
|
|
MaxQueue int `mapstructure:"max_queue" yaml:"max_queue"`
|
|
QueueTimeoutMS int `mapstructure:"queue_timeout_ms" yaml:"queue_timeout_ms"`
|
|
RequestTimeoutMS int `mapstructure:"request_timeout_ms" yaml:"request_timeout_ms"`
|
|
}
|
|
|
|
type VllmConf struct {
|
|
Enabled bool `mapstructure:"enabled" yaml:"enabled"`
|
|
Endpoint string `mapstructure:"endpoint" yaml:"endpoint"`
|
|
Capacity int `mapstructure:"capacity" yaml:"capacity"`
|
|
MaxQueue int `mapstructure:"max_queue" yaml:"max_queue"`
|
|
QueueTimeoutMS int `mapstructure:"queue_timeout_ms" yaml:"queue_timeout_ms"`
|
|
RequestTimeoutMS int `mapstructure:"request_timeout_ms" yaml:"request_timeout_ms"`
|
|
}
|
|
|
|
type CLIConf struct {
|
|
Enabled bool `mapstructure:"enabled" yaml:"enabled"`
|
|
Profiles map[string]CLIProfileConf `mapstructure:"profiles" yaml:"profiles"`
|
|
}
|
|
|
|
type CLIProfileConf struct {
|
|
Command string `mapstructure:"command" yaml:"command"`
|
|
Args []string `mapstructure:"args" yaml:"args"`
|
|
Env []string `mapstructure:"env" yaml:"env"`
|
|
Persistent bool `mapstructure:"persistent" yaml:"persistent"`
|
|
Terminal bool `mapstructure:"terminal" yaml:"terminal"`
|
|
ResponseIdleTimeoutMS int `mapstructure:"response_idle_timeout_ms" yaml:"response_idle_timeout_ms"`
|
|
StartupIdleTimeoutMS int `mapstructure:"startup_idle_timeout_ms" yaml:"startup_idle_timeout_ms"`
|
|
OutputFormat string `mapstructure:"output_format" yaml:"output_format"`
|
|
CompletionMarker CompletionMarkerConf `mapstructure:"completion_marker" yaml:"completion_marker"`
|
|
Mode string `mapstructure:"mode" yaml:"mode"`
|
|
ResumeArgs []string `mapstructure:"resume_args" yaml:"resume_args"`
|
|
}
|
|
|
|
type CompletionMarkerConf struct {
|
|
Line string `mapstructure:"line" yaml:"line"`
|
|
Regex string `mapstructure:"regex" yaml:"regex"`
|
|
}
|
|
|
|
func (m CompletionMarkerConf) Empty() bool {
|
|
return m.Line == "" && m.Regex == ""
|
|
}
|
|
|
|
func Load(cfgFile string) (*NodeConfig, error) {
|
|
v := viper.New()
|
|
v.SetConfigFile(cfgFile)
|
|
setDefaults(v)
|
|
if err := v.ReadInConfig(); err != nil {
|
|
return nil, err
|
|
}
|
|
var cfg NodeConfig
|
|
if err := v.Unmarshal(&cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
return &cfg, nil
|
|
}
|
|
|
|
func LoadEdge(cfgFile string) (*EdgeConfig, error) {
|
|
v := viper.New()
|
|
v.SetConfigFile(cfgFile)
|
|
setEdgeDefaults(v)
|
|
if err := v.ReadInConfig(); err != nil {
|
|
return nil, err
|
|
}
|
|
var cfg EdgeConfig
|
|
if err := v.Unmarshal(&cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
if !v.InConfig("console.target") {
|
|
if v.InConfig("console.agent") {
|
|
cfg.Console.Target = cfg.Console.Agent
|
|
} else if v.InConfig("console.model") {
|
|
cfg.Console.Target = cfg.Console.Model
|
|
}
|
|
}
|
|
if err := validateOpenAIRoutes(cfg.OpenAI.ModelRoutes); err != nil {
|
|
return nil, err
|
|
}
|
|
for i := range cfg.Nodes {
|
|
kind, err := NormalizeAgentKind(cfg.Nodes[i].AgentKind)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("nodes[%d] alias=%q: %w", i, cfg.Nodes[i].Alias, err)
|
|
}
|
|
cfg.Nodes[i].AgentKind = kind
|
|
|
|
if err := normalizeAdapters(&cfg.Nodes[i].Adapters); err != nil {
|
|
name := cfg.Nodes[i].ID
|
|
if name == "" {
|
|
name = cfg.Nodes[i].Alias
|
|
}
|
|
return nil, fmt.Errorf("nodes[%d] %q adapters: %w", i, name, err)
|
|
}
|
|
}
|
|
return &cfg, nil
|
|
}
|
|
|
|
// NormalizeAdapters promotes legacy single-instance Ollama/Vllm fields into the
|
|
// typed instance slices and validates that all instance names are unique.
|
|
func NormalizeAdapters(a *AdaptersConf) error {
|
|
return normalizeAdapters(a)
|
|
}
|
|
|
|
func normalizeAdapters(a *AdaptersConf) error {
|
|
if a.Ollama.Enabled {
|
|
existing := findOllamaInstance(a.OllamaInstances, "ollama")
|
|
if existing == nil {
|
|
a.OllamaInstances = append([]OllamaInstanceConf{{
|
|
Name: "ollama",
|
|
Enabled: a.Ollama.Enabled,
|
|
BaseURL: a.Ollama.BaseURL,
|
|
ContextSize: a.Ollama.ContextSize,
|
|
Capacity: a.Ollama.Capacity,
|
|
MaxQueue: a.Ollama.MaxQueue,
|
|
QueueTimeoutMS: a.Ollama.QueueTimeoutMS,
|
|
RequestTimeoutMS: a.Ollama.RequestTimeoutMS,
|
|
}}, a.OllamaInstances...)
|
|
} else if !sameOllamaInstance(*existing, a.Ollama) {
|
|
return fmt.Errorf("ollama: legacy field conflicts with explicit instance %q: enabled/base_url/context_size/capacity/max_queue/queue_timeout_ms/request_timeout_ms mismatch", "ollama")
|
|
}
|
|
}
|
|
if a.Vllm.Enabled {
|
|
existing := findVllmInstance(a.VllmInstances, "vllm")
|
|
if existing == nil {
|
|
a.VllmInstances = append([]VllmInstanceConf{{
|
|
Name: "vllm",
|
|
Enabled: a.Vllm.Enabled,
|
|
Endpoint: a.Vllm.Endpoint,
|
|
Capacity: a.Vllm.Capacity,
|
|
MaxQueue: a.Vllm.MaxQueue,
|
|
QueueTimeoutMS: a.Vllm.QueueTimeoutMS,
|
|
RequestTimeoutMS: a.Vllm.RequestTimeoutMS,
|
|
}}, a.VllmInstances...)
|
|
} else if !sameVllmInstance(*existing, a.Vllm) {
|
|
return fmt.Errorf("vllm: legacy field conflicts with explicit instance %q: enabled/endpoint/capacity/max_queue/queue_timeout_ms/request_timeout_ms mismatch", "vllm")
|
|
}
|
|
}
|
|
if a.OpenAICompat.Enabled {
|
|
existing := findOpenAICompatInstance(a.OpenAICompatInstances, "openai_compat")
|
|
if existing == nil {
|
|
a.OpenAICompatInstances = append([]OpenAICompatInstanceConf{{
|
|
Name: "openai_compat",
|
|
Enabled: a.OpenAICompat.Enabled,
|
|
Provider: a.OpenAICompat.Provider,
|
|
Endpoint: a.OpenAICompat.Endpoint,
|
|
Headers: a.OpenAICompat.Headers,
|
|
Capacity: a.OpenAICompat.Capacity,
|
|
MaxQueue: a.OpenAICompat.MaxQueue,
|
|
QueueTimeoutMS: a.OpenAICompat.QueueTimeoutMS,
|
|
RequestTimeoutMS: a.OpenAICompat.RequestTimeoutMS,
|
|
}}, a.OpenAICompatInstances...)
|
|
} else if !sameOpenAICompatInstance(*existing, a.OpenAICompat) {
|
|
return fmt.Errorf("openai_compat: legacy field conflicts with explicit instance %q: enabled/provider/endpoint/headers/capacity/max_queue/queue_timeout_ms/request_timeout_ms mismatch", "openai_compat")
|
|
}
|
|
}
|
|
|
|
if err := checkUniqueNames("ollama_instances", func(i int) string { return a.OllamaInstances[i].Name }, len(a.OllamaInstances)); err != nil {
|
|
return err
|
|
}
|
|
if err := checkUniqueNames("vllm_instances", func(i int) string { return a.VllmInstances[i].Name }, len(a.VllmInstances)); err != nil {
|
|
return err
|
|
}
|
|
if err := checkUniqueNames("openai_compat_instances", func(i int) string { return a.OpenAICompatInstances[i].Name }, len(a.OpenAICompatInstances)); err != nil {
|
|
return err
|
|
}
|
|
for i, inst := range a.OllamaInstances {
|
|
field := fmt.Sprintf("ollama_instances[%d]", i)
|
|
if err := validateProviderQueueConfig(field, inst.Capacity, inst.MaxQueue, inst.QueueTimeoutMS, inst.RequestTimeoutMS); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for i, inst := range a.VllmInstances {
|
|
field := fmt.Sprintf("vllm_instances[%d]", i)
|
|
if err := validateProviderQueueConfig(field, inst.Capacity, inst.MaxQueue, inst.QueueTimeoutMS, inst.RequestTimeoutMS); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for i, inst := range a.OpenAICompatInstances {
|
|
field := fmt.Sprintf("openai_compat_instances[%d]", i)
|
|
if err := validateProviderQueueConfig(field, inst.Capacity, inst.MaxQueue, inst.QueueTimeoutMS, inst.RequestTimeoutMS); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateProviderQueueConfig(field string, capacity, maxQueue, queueTimeoutMS, requestTimeoutMS int) error {
|
|
if capacity < 0 {
|
|
return fmt.Errorf("%s.capacity must be non-negative", field)
|
|
}
|
|
if maxQueue < 0 {
|
|
return fmt.Errorf("%s.max_queue must be non-negative", field)
|
|
}
|
|
if queueTimeoutMS < 0 {
|
|
return fmt.Errorf("%s.queue_timeout_ms must be non-negative", field)
|
|
}
|
|
if requestTimeoutMS < 0 {
|
|
return fmt.Errorf("%s.request_timeout_ms must be non-negative", field)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func sameOllamaInstance(inst OllamaInstanceConf, legacy OllamaConf) bool {
|
|
return inst.Enabled == legacy.Enabled &&
|
|
inst.BaseURL == legacy.BaseURL &&
|
|
inst.ContextSize == legacy.ContextSize &&
|
|
inst.Capacity == legacy.Capacity &&
|
|
inst.MaxQueue == legacy.MaxQueue &&
|
|
inst.QueueTimeoutMS == legacy.QueueTimeoutMS &&
|
|
inst.RequestTimeoutMS == legacy.RequestTimeoutMS
|
|
}
|
|
|
|
func sameVllmInstance(inst VllmInstanceConf, legacy VllmConf) bool {
|
|
return inst.Enabled == legacy.Enabled &&
|
|
inst.Endpoint == legacy.Endpoint &&
|
|
inst.Capacity == legacy.Capacity &&
|
|
inst.MaxQueue == legacy.MaxQueue &&
|
|
inst.QueueTimeoutMS == legacy.QueueTimeoutMS &&
|
|
inst.RequestTimeoutMS == legacy.RequestTimeoutMS
|
|
}
|
|
|
|
func findOllamaInstance(instances []OllamaInstanceConf, name string) *OllamaInstanceConf {
|
|
for i := range instances {
|
|
if instances[i].Name == name {
|
|
return &instances[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func findVllmInstance(instances []VllmInstanceConf, name string) *VllmInstanceConf {
|
|
for i := range instances {
|
|
if instances[i].Name == name {
|
|
return &instances[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func sameOpenAICompatInstance(inst OpenAICompatInstanceConf, legacy OpenAICompatConf) bool {
|
|
if inst.Enabled != legacy.Enabled ||
|
|
inst.Provider != legacy.Provider ||
|
|
inst.Endpoint != legacy.Endpoint ||
|
|
inst.Capacity != legacy.Capacity ||
|
|
inst.MaxQueue != legacy.MaxQueue ||
|
|
inst.QueueTimeoutMS != legacy.QueueTimeoutMS ||
|
|
inst.RequestTimeoutMS != legacy.RequestTimeoutMS {
|
|
return false
|
|
}
|
|
if len(inst.Headers) != len(legacy.Headers) {
|
|
return false
|
|
}
|
|
for k, v := range inst.Headers {
|
|
if lv, ok := legacy.Headers[k]; !ok || lv != v {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func findOpenAICompatInstance(instances []OpenAICompatInstanceConf, name string) *OpenAICompatInstanceConf {
|
|
for i := range instances {
|
|
if instances[i].Name == name {
|
|
return &instances[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateOpenAIRoutes rejects duplicate and empty model ids in the route catalog.
|
|
func validateOpenAIRoutes(routes []OpenAIRouteEntry) error {
|
|
seen := make(map[string]struct{}, len(routes))
|
|
for i, r := range routes {
|
|
model := strings.TrimSpace(r.Model)
|
|
if model == "" {
|
|
return fmt.Errorf("openai.model_routes[%d]: model must not be empty", i)
|
|
}
|
|
if _, dup := seen[model]; dup {
|
|
return fmt.Errorf("openai.model_routes: duplicate model %q", model)
|
|
}
|
|
seen[model] = struct{}{}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func checkUniqueNames(field string, name func(int) string, n int) error {
|
|
seen := make(map[string]struct{}, n)
|
|
for i := 0; i < n; i++ {
|
|
k := name(i)
|
|
if k == "" {
|
|
return fmt.Errorf("%s[%d]: name must not be empty", field, i)
|
|
}
|
|
if _, dup := seen[k]; dup {
|
|
return fmt.Errorf("%s: duplicate name %q", field, k)
|
|
}
|
|
seen[k] = struct{}{}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func setDefaults(v *viper.Viper) {
|
|
v.SetDefault("transport.edge_addr", "localhost:9090")
|
|
v.SetDefault("logging.level", "info")
|
|
v.SetDefault("metrics.port", 9091)
|
|
}
|
|
|
|
func setEdgeDefaults(v *viper.Viper) {
|
|
v.SetDefault("server.listen", "0.0.0.0:9090")
|
|
v.SetDefault("bootstrap.listen", "0.0.0.0:18080")
|
|
v.SetDefault("bootstrap.artifact_dir", "artifacts")
|
|
v.SetDefault("openai.enabled", false)
|
|
v.SetDefault("openai.listen", "0.0.0.0:18081")
|
|
v.SetDefault("openai.adapter", "ollama")
|
|
v.SetDefault("openai.session_id", "openai")
|
|
v.SetDefault("openai.timeout_sec", 120)
|
|
v.SetDefault("openai.strict_output", true)
|
|
v.SetDefault("openai.strict_stream_buffer", false)
|
|
v.SetDefault("a2a.enabled", false)
|
|
v.SetDefault("a2a.listen", "0.0.0.0:8081")
|
|
v.SetDefault("a2a.path", "/a2a")
|
|
v.SetDefault("a2a.adapter", "cli")
|
|
v.SetDefault("a2a.session_id", "a2a")
|
|
v.SetDefault("a2a.timeout_sec", 120)
|
|
v.SetDefault("logging.level", "info")
|
|
v.SetDefault("metrics.port", 19092)
|
|
v.SetDefault("tls.enabled", false)
|
|
v.SetDefault("console.adapter", "cli")
|
|
v.SetDefault("console.target", "claude")
|
|
v.SetDefault("console.session_id", "default")
|
|
v.SetDefault("console.background", false)
|
|
v.SetDefault("console.timeout_sec", 120)
|
|
v.SetDefault("control_plane.enabled", false)
|
|
v.SetDefault("control_plane.wire_addr", "")
|
|
v.SetDefault("control_plane.reconnect_interval_sec", 5)
|
|
}
|