iop/packages/go/config/config.go

1170 lines
49 KiB
Go

package config
import (
"encoding/hex"
"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)
}
}
// NormalizeProviderType returns the canonical driver name for provider type.
func NormalizeProviderType(t string) string {
switch strings.ToLower(strings.TrimSpace(t)) {
case "openai_compat", "openai_api", "vllm", "lemonade", "sglang", "seulgivibe_claude", "seulgivibe_openai":
return "openai_compat"
case "ollama":
return "ollama"
case "cli":
return "cli"
default:
return t
}
}
type NodeConfig struct {
Transport TransportConf `mapstructure:"transport" yaml:"transport"`
Reconnect ReconnectConf `mapstructure:"reconnect" yaml:"reconnect"`
Logging LoggingConf `mapstructure:"logging" yaml:"logging"`
Metrics MetricsConf `mapstructure:"metrics" yaml:"metrics"`
}
// ReconnectConf controls Edge reconnect behaviour after a disconnect.
type ReconnectConf struct {
IntervalSec int `mapstructure:"interval_sec" yaml:"interval_sec"`
MaxAttempts int `mapstructure:"max_attempts" yaml:"max_attempts"`
}
// EdgeRefreshConf configures the Edge-local refresh admin HTTP server.
// When Enabled is false (default) the admin server is not started and
// config refresh is not available without a restart.
type EdgeRefreshConf struct {
Enabled bool `mapstructure:"enabled" yaml:"enabled"`
Listen string `mapstructure:"listen" yaml:"listen"`
}
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"`
Refresh EdgeRefreshConf `mapstructure:"refresh" yaml:"refresh,omitempty"`
// LongContextThresholdTokens is the input-token estimate at or above which
// a request is classified as long-context for admission policy.
LongContextThresholdTokens int `mapstructure:"long_context_threshold_tokens" yaml:"long_context_threshold_tokens,omitempty"`
Nodes []NodeDefinition `mapstructure:"nodes" yaml:"nodes"`
// Models is the top-level model catalog. Each entry defines a canonical
// routing key (ID) and its provider-pool mapping. When set it takes
// precedence over the legacy openai.model_routes for runtime dispatch.
Models []ModelCatalogEntry `mapstructure:"models" yaml:"models,omitempty"`
}
// 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"`
Providers []NodeProviderConf `mapstructure:"providers" yaml:"providers,omitempty"`
Runtime RuntimeConf `mapstructure:"runtime" yaml:"runtime"`
}
// Category represents the provider category.
type Category string
const (
CategoryAPI Category = "api"
CategoryCLI Category = "cli"
CategoryLocalInference Category = "local_inference"
)
// validateCategory returns nil when category is valid.
func validateCategory(c Category) error {
switch c {
case CategoryAPI, CategoryCLI, CategoryLocalInference:
return nil
default:
return fmt.Errorf("invalid provider category %q (allowed: %q, %q, %q)", c, CategoryAPI, CategoryCLI, CategoryLocalInference)
}
}
// NodeProviderConf defines a resource/provider candidate that belongs to a
// node. Provider-first resources can declare type-specific execution fields
// directly, while Adapter remains available for legacy/compat overrides.
type NodeProviderConf struct {
// ID uniquely identifies this provider within the node and is referenced
// by models[].providers keys.
ID string `mapstructure:"id" yaml:"id"`
// Type is the runtime type (e.g. "ollama", "vllm", "lemonade", "sglang", "openai_api", "cli").
Type string `mapstructure:"type" yaml:"type"`
// Category classifies the resource; MVP values: api, cli, local_inference.
Category Category `mapstructure:"category" yaml:"category"`
// Adapter is the optional concrete enabled adapter instance key used by
// legacy/compat provider resources. A legacy type-name route is valid only
// when exactly one enabled adapter instance of that type exists on the same
// node.
Adapter string `mapstructure:"adapter" yaml:"adapter,omitempty"`
// Models lists the served model names this provider can actually serve.
Models []string `mapstructure:"models" yaml:"models,omitempty"`
// Health is the observed provider health state.
Health string `mapstructure:"health" yaml:"health,omitempty"`
// Capacity is the provider/resource maximum concurrent execution slots.
// Provider-pool dispatch treats 0 as not dispatchable rather than falling
// back to node runtime concurrency metadata.
Capacity int `mapstructure:"capacity" yaml:"capacity,omitempty"`
// TotalContextTokens is the provider runtime's total KV/context budget used
// as the long-context admission budget. Runtime-specific options (e.g.
// ctx_size) map onto this value. Must satisfy
// total_context_tokens >= context_window_tokens * long_context_capacity for
// every model group that references this provider.
TotalContextTokens int `mapstructure:"total_context_tokens" yaml:"total_context_tokens,omitempty"`
// LongContextCapacity is the number of concurrent long-context slots this
// provider allows (context_window_tokens-sized requests). Long requests
// occupy both a normal capacity slot and a long slot. Zero means the
// provider declares no dedicated long-slot limit.
LongContextCapacity int `mapstructure:"long_context_capacity" yaml:"long_context_capacity,omitempty"`
// MaxQueue is the maximum queue depth.
MaxQueue int `mapstructure:"max_queue" yaml:"max_queue,omitempty"`
// QueueTimeoutMS is the queue timeout in milliseconds. Zero means no queue
// timeout when a queue policy is configured.
QueueTimeoutMS int `mapstructure:"queue_timeout_ms" yaml:"queue_timeout_ms,omitempty"`
// Priority is the provider dispatch priority. Lower values are preferred
// when in_flight counts are equal. Must be non-negative; zero is the
// default priority.
Priority int `mapstructure:"priority" yaml:"priority,omitempty"`
// LifecycleCapabilities lists coarse lifecycle capabilities.
LifecycleCapabilities []string `mapstructure:"lifecycle_capabilities" yaml:"lifecycle_capabilities,omitempty"`
// Enabled controls whether this provider participates in the dispatch pool.
// Nil (omitted) means enabled; false disables the provider without removing it.
Enabled *bool `mapstructure:"enabled" yaml:"enabled,omitempty"`
// Provider-First fields (G06)
Provider string `mapstructure:"provider" yaml:"provider,omitempty"`
Endpoint string `mapstructure:"endpoint" yaml:"endpoint,omitempty"`
BaseURL string `mapstructure:"base_url" yaml:"base_url,omitempty"`
Headers map[string]string `mapstructure:"headers" yaml:"headers,omitempty"`
Command string `mapstructure:"command" yaml:"command,omitempty"`
Args []string `mapstructure:"args" yaml:"args,omitempty"`
Env []string `mapstructure:"env" yaml:"env,omitempty"`
Mode string `mapstructure:"mode" yaml:"mode,omitempty"`
ResumeArgs []string `mapstructure:"resume_args" yaml:"resume_args,omitempty"`
OutputFormat string `mapstructure:"output_format" yaml:"output_format,omitempty"`
ContextSize int `mapstructure:"context_size" yaml:"context_size,omitempty"`
RequestTimeoutMS int `mapstructure:"request_timeout_ms" yaml:"request_timeout_ms,omitempty"`
}
// Validate checks internal consistency of the provider candidate config.
func (p NodeProviderConf) Validate() error {
id := strings.TrimSpace(p.ID)
if id == "" {
return fmt.Errorf("nodes[].providers[].id must not be empty")
}
if strings.TrimSpace(p.Type) == "" {
return fmt.Errorf("nodes[].providers[%q]: type must not be empty", id)
}
if err := validateCategory(p.Category); err != nil {
return fmt.Errorf("nodes[].providers[%q]: %w", id, err)
}
for i, m := range p.Models {
if strings.TrimSpace(m) == "" {
return fmt.Errorf("nodes[].providers[%q].models[%d]: served model name must not be empty", id, i)
}
}
if p.Capacity < 0 {
return fmt.Errorf("nodes[].providers[%q].capacity must be non-negative", id)
}
if p.MaxQueue < 0 {
return fmt.Errorf("nodes[].providers[%q].max_queue must be non-negative", id)
}
if p.QueueTimeoutMS < 0 {
return fmt.Errorf("nodes[].providers[%q].queue_timeout_ms must be non-negative", id)
}
if p.RequestTimeoutMS < 0 {
return fmt.Errorf("nodes[].providers[%q].request_timeout_ms must be non-negative", id)
}
if p.ContextSize < 0 {
return fmt.Errorf("nodes[].providers[%q].context_size must be non-negative", id)
}
if p.Priority < 0 {
return fmt.Errorf("nodes[].providers[%q].priority must be non-negative", id)
}
if p.TotalContextTokens < 0 {
return fmt.Errorf("nodes[].providers[%q].total_context_tokens must be non-negative", id)
}
if p.LongContextCapacity < 0 {
return fmt.Errorf("nodes[].providers[%q].long_context_capacity must be non-negative", id)
}
if p.LongContextCapacity > 0 && p.TotalContextTokens <= 0 {
return fmt.Errorf("nodes[].providers[%q].total_context_tokens must be positive when long_context_capacity > 0", id)
}
return nil
}
// ProviderEnabled reports whether a provider is enabled.
// Returns true when Enabled is nil (omitted) or *Enabled is true.
// Returns false only when Enabled is explicitly set to false.
func ProviderEnabled(p NodeProviderConf) bool {
return p.Enabled == nil || *p.Enabled
}
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"`
MaxQueue int `mapstructure:"max_queue" yaml:"max_queue,omitempty"`
QueueTimeoutMS int `mapstructure:"queue_timeout_ms" yaml:"queue_timeout_ms,omitempty"`
WorkspaceRequired bool `mapstructure:"workspace_required" yaml:"workspace_required,omitempty"`
}
// ModelCatalogEntry is a top-level Edge config entry that defines a canonical
// routing key (`ID`) and its provider-pool mapping. Each provider id key
// maps to the concrete served model name that the provider actually exposes.
type ModelCatalogEntry struct {
// ID is the canonical routing key (e.g. "qwen3.6:35b") that matches the
// external OpenAI-compatible model field.
ID string `mapstructure:"id" yaml:"id"`
// DisplayName is a human-readable name shown in operation dashboards.
DisplayName string `mapstructure:"display_name" yaml:"display_name,omitempty"`
// ContextWindowTokens is the model group's single-request maximum context
// contract shared by every provider in the group.
ContextWindowTokens int `mapstructure:"context_window_tokens" yaml:"context_window_tokens,omitempty"`
// DefaultMaxTokens is injected when OpenAI-compatible requests omit an
// output-token limit for this model group.
DefaultMaxTokens int `mapstructure:"default_max_tokens" yaml:"default_max_tokens,omitempty"`
// MinMaxTokens raises too-small OpenAI-compatible output-token limits for
// this model group.
MinMaxTokens int `mapstructure:"min_max_tokens" yaml:"min_max_tokens,omitempty"`
// DefaultThinkingTokenBudget bounds reasoning tokens for think-capable
// providers when callers do not explicitly set a budget.
DefaultThinkingTokenBudget int `mapstructure:"default_thinking_token_budget" yaml:"default_thinking_token_budget,omitempty"`
// Providers maps provider id to the concrete served model name that the
// provider actually exposes. Keys must match nodes[].providers[].id.
Providers map[string]string `mapstructure:"providers" yaml:"providers"`
}
// buildProviderServedModelsIndex aggregates all nodes[].providers[].models
// keyed by provider id. Returns the index for Validate.
func buildProviderServedModelsIndex(nodes []NodeDefinition) map[string]map[string]struct{} {
serveModels := make(map[string]map[string]struct{})
for i := range nodes {
for _, p := range nodes[i].Providers {
if _, ok := serveModels[p.ID]; !ok {
serveModels[p.ID] = make(map[string]struct{})
}
for _, m := range p.Models {
serveModels[p.ID][m] = struct{}{}
}
}
}
return serveModels
}
// Validate checks the structure and internal references of the model catalog entry.
// When serveModels is non-nil, it also verifies that each models[].providers[pid]=model
// refers to a served model that exists in nodes[].providers[id].models.
func (e ModelCatalogEntry) Validate(resolvedProviderIDs map[string]struct{}, serveModels map[string]map[string]struct{}) error {
id := strings.TrimSpace(e.ID)
if id == "" {
return fmt.Errorf("models[].id must not be empty")
}
if e.DefaultMaxTokens < 0 {
return fmt.Errorf("models[%q].default_max_tokens must be non-negative", id)
}
if e.MinMaxTokens < 0 {
return fmt.Errorf("models[%q].min_max_tokens must be non-negative", id)
}
if e.DefaultThinkingTokenBudget < 0 {
return fmt.Errorf("models[%q].default_thinking_token_budget must be non-negative", id)
}
if e.ContextWindowTokens < 0 {
return fmt.Errorf("models[%q].context_window_tokens must be non-negative", id)
}
if e.DefaultMaxTokens > 0 && e.MinMaxTokens > 0 && e.DefaultMaxTokens < e.MinMaxTokens {
return fmt.Errorf("models[%q].default_max_tokens must be greater than or equal to min_max_tokens", id)
}
if len(e.Providers) == 0 {
return fmt.Errorf("models[%q].providers must not be empty", id)
}
for pid, model := range e.Providers {
p := strings.TrimSpace(pid)
if p == "" {
return fmt.Errorf("models[%q].providers: provider id must not be empty", id)
}
m := strings.TrimSpace(model)
if m == "" {
return fmt.Errorf("models[%q].providers[%q]: served model name must not be empty", id, p)
}
if _, ok := resolvedProviderIDs[p]; !ok {
return fmt.Errorf("models[%q].providers[%q]: provider id %q does not resolve to any nodes[].providers[].id", id, p, p)
}
// Serve model membership validation (REVIEW_API-1):
// The served model must be listed in the provider's own models[].
if serveModels != nil {
servedSet, exists := serveModels[p]
if !exists {
// No providers list yet (should not happen in modern configs).
return fmt.Errorf("models[%q].providers[%q]: provider %q has no models[] defined", id, p, p)
}
if _, ok := servedSet[m]; !ok {
return fmt.Errorf("models[%q].providers[%q]: served model %q is not listed in nodes[].providers[%q].models", id, p, m, p)
}
}
}
return nil
}
type EdgeOpenAIConf struct {
Enabled bool `mapstructure:"enabled" yaml:"enabled"`
Listen string `mapstructure:"listen" yaml:"listen"`
BearerToken string `mapstructure:"bearer_token" yaml:"bearer_token"`
PrincipalTokens []OpenAIPrincipalTokenConf `mapstructure:"principal_tokens" yaml:"principal_tokens,omitempty"`
ProviderAuth EdgeOpenAIProviderAuthConf `mapstructure:"provider_auth" yaml:"provider_auth,omitempty"`
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"`
}
// EdgeOpenAIProviderAuthConf configures request-time provider token forwarding.
// It only describes which caller header to forward and never stores raw token
// values in config.
type EdgeOpenAIProviderAuthConf struct {
Enabled bool `mapstructure:"enabled" yaml:"enabled,omitempty"`
FromHeader string `mapstructure:"from_header" yaml:"from_header,omitempty"`
TargetHeader string `mapstructure:"target_header" yaml:"target_header,omitempty"`
Scheme string `mapstructure:"scheme" yaml:"scheme,omitempty"`
Required bool `mapstructure:"required" yaml:"required,omitempty"`
}
// OpenAIPrincipalTokenConf maps a hashed OpenAI-compatible bearer token to the
// IOP bearer-token-operation identity it authenticates as. Raw tokens are
// never stored in config; only the SHA-256 hash is persisted.
type OpenAIPrincipalTokenConf struct {
TokenRef string `mapstructure:"token_ref" yaml:"token_ref"`
TokenHashSHA256 string `mapstructure:"token_hash_sha256" yaml:"token_hash_sha256"`
PrincipalRef string `mapstructure:"principal_ref" yaml:"principal_ref"`
PrincipalAlias string `mapstructure:"principal_alias" yaml:"principal_alias,omitempty"`
}
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 is legacy compatibility metadata. Runtime admission must use
// provider/resource capacity rather than treating this as a node-wide gate.
Concurrency int `mapstructure:"concurrency" yaml:"concurrency"`
}
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"`
Mock MockConf `mapstructure:"mock" yaml:"mock"`
// 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"` // zero means no queue timeout
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"` // zero means no queue timeout
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"` // zero means no queue timeout
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"` // zero means no queue timeout
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"` // zero means no queue timeout
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"` // zero means no queue timeout
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"`
}
// MockConf defines the mock adapter instance. It has no execution fields and
// only serves as a flag to enable the mock adapter in the payload.
type MockConf struct {
Enabled bool `mapstructure:"enabled" yaml:"enabled"`
}
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
}
if err := validateOpenAIPrincipalTokens(cfg.OpenAI.PrincipalTokens); err != nil {
return nil, err
}
if err := normalizeOpenAIProviderAuth(v, &cfg.OpenAI.ProviderAuth); err != nil {
return nil, err
}
if cfg.LongContextThresholdTokens <= 0 {
return nil, fmt.Errorf("long_context_threshold_tokens must be positive")
}
// Collect all provider IDs from nodes[].providers[] for cross-referencing
// and validate uniqueness within each node.
providerIDs := make(map[string]struct{})
providerByID := make(map[string]NodeProviderConf)
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)
}
// Validate unique provider IDs within this node and across all nodes.
seenProviderIDs := make(map[string]struct{}, len(cfg.Nodes[i].Providers))
for j, p := range cfg.Nodes[i].Providers {
if err := p.Validate(); err != nil {
return nil, fmt.Errorf("nodes[%d].providers[%d]: %w", i, j, err)
}
if _, dup := seenProviderIDs[p.ID]; dup {
return nil, fmt.Errorf("nodes[%d].providers: duplicate provider id %q within node", i, p.ID)
}
seenProviderIDs[p.ID] = struct{}{}
if _, globalDup := providerIDs[p.ID]; globalDup {
return nil, fmt.Errorf("nodes[%d].providers[%d]: duplicate provider id %q across nodes (global uniqueness violation)", i, j, p.ID)
}
providerIDs[p.ID] = struct{}{}
providerByID[p.ID] = p
}
}
for i := range cfg.Nodes {
if err := CheckProviderLegacyConflict(i, cfg.Nodes[i].Alias, &cfg.Nodes[i]); err != nil {
return nil, err
}
}
// Build the provider->servedModels index so Validate can check membership.
serveModels := buildProviderServedModelsIndex(cfg.Nodes)
// Validate models[].providers reference valid provider IDs.
seenModelIDs := make(map[string]struct{}, len(cfg.Models))
for i, m := range cfg.Models {
id := strings.TrimSpace(m.ID)
if id == "" {
return nil, fmt.Errorf("models[%d]: id must not be empty", i)
}
if _, dup := seenModelIDs[id]; dup {
return nil, fmt.Errorf("models: duplicate model id %q", id)
}
seenModelIDs[id] = struct{}{}
if err := m.Validate(providerIDs, serveModels); err != nil {
return nil, fmt.Errorf("models[%d]: %w", i, err)
}
if err := validateProviderLongContextBudget(m, providerByID); err != nil {
return nil, fmt.Errorf("models[%d]: %w", i, err)
}
}
return &cfg, nil
}
// validateProviderLongContextBudget enforces the long-context admission budget:
// every provider referenced by a model group with a positive context window must
// have total_context_tokens >= context_window_tokens * long_context_capacity.
// Providers with long_context_capacity == 0 declare no long-slot budget and are
// therefore unconstrained by this check.
func validateProviderLongContextBudget(m ModelCatalogEntry, providerByID map[string]NodeProviderConf) error {
if m.ContextWindowTokens <= 0 {
return nil
}
for pid := range m.Providers {
p := strings.TrimSpace(pid)
prov, ok := providerByID[p]
if !ok {
continue
}
if prov.LongContextCapacity <= 0 {
continue
}
required := m.ContextWindowTokens * prov.LongContextCapacity
if prov.TotalContextTokens < required {
return fmt.Errorf("providers[%q].total_context_tokens (%d) must be >= context_window_tokens (%d) * long_context_capacity (%d) = %d",
p, prov.TotalContextTokens, m.ContextWindowTokens, prov.LongContextCapacity, required)
}
}
return 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{}{}
if r.MaxQueue < 0 {
return fmt.Errorf("openai.model_routes[%d].max_queue must be non-negative", i)
}
if r.QueueTimeoutMS < 0 {
return fmt.Errorf("openai.model_routes[%d].queue_timeout_ms must be non-negative", i)
}
}
return nil
}
// validateOpenAIPrincipalTokens validates the raw-token-free bearer token
// operation mapping. Each entry must resolve to a unique, 64-char lowercase
// hex SHA-256 hash and a non-empty principal_ref; token_ref must be unique.
func validateOpenAIPrincipalTokens(tokens []OpenAIPrincipalTokenConf) error {
seenTokenRef := make(map[string]struct{}, len(tokens))
seenHash := make(map[string]struct{}, len(tokens))
for i, t := range tokens {
ref := strings.TrimSpace(t.TokenRef)
if ref == "" {
return fmt.Errorf("openai.principal_tokens[%d]: token_ref must not be empty", i)
}
if _, dup := seenTokenRef[ref]; dup {
return fmt.Errorf("openai.principal_tokens: duplicate token_ref %q", ref)
}
seenTokenRef[ref] = struct{}{}
hash := strings.ToLower(strings.TrimSpace(t.TokenHashSHA256))
if len(hash) != 64 {
return fmt.Errorf("openai.principal_tokens[%q]: token_hash_sha256 must be a 64-character hex SHA-256 hash", ref)
}
if _, err := hex.DecodeString(hash); err != nil {
return fmt.Errorf("openai.principal_tokens[%q]: token_hash_sha256 must be hex-encoded: %w", ref, err)
}
if _, dup := seenHash[hash]; dup {
return fmt.Errorf("openai.principal_tokens: duplicate token_hash_sha256 for token_ref %q", ref)
}
seenHash[hash] = struct{}{}
if strings.TrimSpace(t.PrincipalRef) == "" {
return fmt.Errorf("openai.principal_tokens[%q]: principal_ref must not be empty", ref)
}
}
return nil
}
func normalizeOpenAIProviderAuth(v *viper.Viper, auth *EdgeOpenAIProviderAuthConf) error {
if !auth.Enabled {
return nil
}
if v.InConfig("openai.provider_auth.from_header") {
auth.FromHeader = strings.TrimSpace(auth.FromHeader)
if auth.FromHeader == "" {
return fmt.Errorf("openai.provider_auth.from_header must not be empty when provider_auth is enabled")
}
} else {
auth.FromHeader = "X-IOP-Provider-Authorization"
}
if v.InConfig("openai.provider_auth.target_header") {
auth.TargetHeader = strings.TrimSpace(auth.TargetHeader)
if auth.TargetHeader == "" {
return fmt.Errorf("openai.provider_auth.target_header must not be empty when provider_auth is enabled")
}
} else {
auth.TargetHeader = "Authorization"
}
auth.Scheme = strings.TrimSpace(auth.Scheme)
if auth.Scheme == "" {
auth.Scheme = "Bearer"
}
if !v.InConfig("openai.provider_auth.required") {
auth.Required = true
}
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
}
// CheckProviderLegacyConflict validates that when a provider-first resource
// declares an ID matching an enabled legacy adapter instance name, the
// provider's execution fields match the legacy adapter's fields. A mismatch
// produces a validation error to prevent silent precedence between the two
// config paths.
func CheckProviderLegacyConflict(nodeIdx int, nodeName string, node *NodeDefinition) error {
// Build a map of enabled adapter instance names to their configs per type.
legacyByKey := make(map[string]*legacyAdapterRef)
for _, inst := range node.Adapters.OllamaInstances {
if inst.Enabled {
legacyByKey[inst.Name] = &legacyAdapterRef{
Type: "ollama",
BaseURL: inst.BaseURL,
ContextSize: inst.ContextSize,
Capacity: inst.Capacity,
}
}
}
for _, inst := range node.Adapters.VllmInstances {
if inst.Enabled {
legacyByKey[inst.Name] = &legacyAdapterRef{
Type: "vllm",
Endpoint: inst.Endpoint,
Capacity: inst.Capacity,
}
}
}
for _, inst := range node.Adapters.OpenAICompatInstances {
if inst.Enabled {
legacyByKey[inst.Name] = &legacyAdapterRef{
Type: "openai_compat",
Provider: inst.Provider,
Endpoint: inst.Endpoint,
Headers: inst.Headers,
Capacity: inst.Capacity,
MaxQueue: inst.MaxQueue,
QueueTimeoutMS: inst.QueueTimeoutMS,
}
}
}
// CLI legacy: enabled cli adapter with its profiles.
if node.Adapters.CLI.Enabled {
for name, profile := range node.Adapters.CLI.Profiles {
legacyByKey[name] = &legacyAdapterRef{
Type: "cli",
Command: profile.Command,
Args: profile.Args,
}
}
}
for j, p := range node.Providers {
legacy, exists := legacyByKey[p.ID]
if !exists {
continue
}
normType := NormalizeProviderType(p.Type)
switch normType {
case "ollama":
if legacy.Type != "ollama" {
continue
}
if p.BaseURL != "" && p.BaseURL != legacy.BaseURL {
return fmt.Errorf("nodes[%d].providers[%d] %q conflicts with adapters.ollama instance %q: base_url mismatch (provider=%q, legacy=%q)",
nodeIdx, j, p.ID, p.ID, p.BaseURL, legacy.BaseURL)
}
if p.ContextSize != 0 && p.ContextSize != legacy.ContextSize {
return fmt.Errorf("nodes[%d].providers[%d] %q conflicts with adapters.ollama instance %q: context_size mismatch (provider=%d, legacy=%d)",
nodeIdx, j, p.ID, p.ID, p.ContextSize, legacy.ContextSize)
}
if p.Capacity != legacy.Capacity {
return fmt.Errorf("nodes[%d].providers[%d] %q conflicts with adapters.ollama instance %q: capacity mismatch (provider=%d, legacy=%d)",
nodeIdx, j, p.ID, p.ID, p.Capacity, legacy.Capacity)
}
case "vllm", "openai_compat":
if legacy.Type != "vllm" && legacy.Type != "openai_compat" {
continue
}
if p.Endpoint != "" && p.Endpoint != legacy.Endpoint {
return fmt.Errorf("nodes[%d].providers[%d] %q conflicts with adapters.%s instance %q: endpoint mismatch (provider=%q, legacy=%q)",
nodeIdx, j, p.ID, legacy.Type, p.ID, p.Endpoint, legacy.Endpoint)
}
if p.Capacity != legacy.Capacity {
return fmt.Errorf("nodes[%d].providers[%d] %q conflicts with adapters.%s instance %q: capacity mismatch (provider=%d, legacy=%d)",
nodeIdx, j, p.ID, legacy.Type, p.ID, p.Capacity, legacy.Capacity)
}
case "cli":
if legacy.Type != "cli" {
continue
}
if p.Command != "" && p.Command != legacy.Command {
return fmt.Errorf("nodes[%d].providers[%d] %q conflicts with adapters.cli profile %q: command mismatch (provider=%q, legacy=%q)",
nodeIdx, j, p.ID, p.ID, p.Command, legacy.Command)
}
}
}
return nil
}
type legacyAdapterRef struct {
Type string
Endpoint string
BaseURL string
Provider string
Headers map[string]string
ContextSize int
Capacity int
MaxQueue int
QueueTimeoutMS int
Command string
Args []string
}
func setDefaults(v *viper.Viper) {
v.SetDefault("transport.edge_addr", "localhost:9090")
v.SetDefault("reconnect.interval_sec", 10)
v.SetDefault("reconnect.max_attempts", 10)
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.bearer_token", "")
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)
v.SetDefault("refresh.enabled", false)
v.SetDefault("refresh.listen", "127.0.0.1:19093")
v.SetDefault("long_context_threshold_tokens", 100000)
}