iop/apps/node/internal/adapters/factory.go
toki 898da6e3af feat(node): openai compatible adapter implementation and test fixes
- Add openai_compat adapter package
- Move 02+01 OpenAI compatible adapter files to archive
- Fix adapter factory and test files
- Update blackbox tests
2026-06-15 14:12:40 +09:00

331 lines
8.4 KiB
Go

package adapters
import (
"fmt"
"go.uber.org/zap"
"google.golang.org/protobuf/types/known/structpb"
"iop/apps/node/internal/adapters/cli"
"iop/apps/node/internal/adapters/mock"
"iop/apps/node/internal/adapters/ollama"
"iop/apps/node/internal/adapters/openai_compat"
"iop/apps/node/internal/adapters/vllm"
"iop/packages/go/config"
iop "iop/proto/gen/iop"
)
// BuildFromPayload creates a Registry from a NodeConfigPayload received from edge.
func BuildFromPayload(payload *iop.NodeConfigPayload, logger *zap.Logger) (*Registry, error) {
reg := NewRegistry()
for _, ac := range payload.GetAdapters() {
if !ac.GetEnabled() {
continue
}
instanceKey := ac.GetName()
typeName := ac.GetType()
switch typeName {
case "mock":
reg.RegisterKeyed(instanceKey, typeName, mock.New(logger))
case "ollama":
var cfg config.OllamaConf
if m := ac.GetOllama(); m != nil {
cfg = ollamaConfFromProto(m)
} else {
cfg = ollamaConfFromStruct(ac.GetSettings())
}
reg.RegisterKeyed(instanceKey, typeName, ollama.New(cfg, logger, instanceKey))
case "vllm":
var cfg config.VllmConf
if m := ac.GetVllm(); m != nil {
cfg = vllmConfFromProto(m)
} else {
cfg = vllmConfFromStruct(ac.GetSettings())
}
reg.RegisterKeyed(instanceKey, typeName, vllm.New(cfg, logger, instanceKey))
case "openai_compat":
var cfg config.OpenAICompatConf
if m := ac.GetOpenaiCompat(); m != nil {
cfg = openAICompatConfFromProto(m)
} else {
cfg = openAICompatConfFromStruct(ac.GetSettings())
}
reg.RegisterKeyed(instanceKey, typeName, openai_compat.New(cfg, logger, instanceKey))
case "cli":
var cfg config.CLIConf
if m := ac.GetCli(); m != nil {
cfg = cliConfFromProto(m)
} else {
cfg = cliConfFromStruct(ac.GetSettings())
}
reg.RegisterKeyed(instanceKey, typeName, cli.New(cfg, logger))
default:
return nil, fmt.Errorf("adapters: unknown adapter type %q", typeName)
}
}
return reg, nil
}
func ollamaConfFromProto(m *iop.OllamaAdapterConfig) config.OllamaConf {
return config.OllamaConf{
Enabled: true,
BaseURL: m.GetBaseUrl(),
ContextSize: int(m.GetContextSize()),
Capacity: int(m.GetCapacity()),
MaxQueue: int(m.GetMaxQueue()),
QueueTimeoutMS: int(m.GetQueueTimeoutMs()),
RequestTimeoutMS: int(m.GetRequestTimeoutMs()),
}
}
func vllmConfFromProto(m *iop.VllmAdapterConfig) config.VllmConf {
return config.VllmConf{
Enabled: true,
Endpoint: m.GetEndpoint(),
Capacity: int(m.GetCapacity()),
MaxQueue: int(m.GetMaxQueue()),
QueueTimeoutMS: int(m.GetQueueTimeoutMs()),
RequestTimeoutMS: int(m.GetRequestTimeoutMs()),
}
}
func openAICompatConfFromProto(m *iop.OpenAICompatAdapterConfig) config.OpenAICompatConf {
return config.OpenAICompatConf{
Enabled: true,
Provider: m.GetProvider(),
Endpoint: m.GetEndpoint(),
Headers: m.GetHeaders(),
Capacity: int(m.GetCapacity()),
MaxQueue: int(m.GetMaxQueue()),
QueueTimeoutMS: int(m.GetQueueTimeoutMs()),
RequestTimeoutMS: int(m.GetRequestTimeoutMs()),
}
}
func cliConfFromProto(m *iop.CLIAdapterConfig) config.CLIConf {
cfg := config.CLIConf{Enabled: true, Profiles: make(map[string]config.CLIProfileConf)}
for name, p := range m.GetProfiles() {
cfg.Profiles[name] = cliProfileFromProto(p)
}
return cfg
}
func cliProfileFromProto(p *iop.CLIProfileConfig) config.CLIProfileConf {
if p == nil {
return config.CLIProfileConf{}
}
prof := config.CLIProfileConf{
Command: p.GetCommand(),
Args: append([]string(nil), p.GetArgs()...),
Env: append([]string(nil), p.GetEnv()...),
Persistent: p.GetPersistent(),
Terminal: p.GetTerminal(),
ResponseIdleTimeoutMS: int(p.GetResponseIdleTimeoutMs()),
StartupIdleTimeoutMS: int(p.GetStartupIdleTimeoutMs()),
OutputFormat: p.GetOutputFormat(),
Mode: p.GetMode(),
ResumeArgs: append([]string(nil), p.GetResumeArgs()...),
}
if marker := p.GetCompletionMarker(); marker != nil {
prof.CompletionMarker = config.CompletionMarkerConf{
Line: marker.GetLine(),
Regex: marker.GetRegex(),
}
}
return prof
}
// Legacy structpb fallback paths for payloads from older edge versions
// that populate AdapterConfig.settings instead of the typed oneof.
func ollamaConfFromStruct(s *structpb.Struct) config.OllamaConf {
cfg := config.OllamaConf{Enabled: true}
if s == nil {
return cfg
}
m := s.AsMap()
if v, ok := m["base_url"].(string); ok {
cfg.BaseURL = v
}
if v, ok := intSetting(m, "context_size"); ok {
cfg.ContextSize = v
} else if v, ok := intSetting(m, "num_ctx"); ok {
cfg.ContextSize = v
}
if v, ok := intSetting(m, "capacity"); ok {
cfg.Capacity = v
}
if v, ok := intSetting(m, "max_queue"); ok {
cfg.MaxQueue = v
}
if v, ok := intSetting(m, "queue_timeout_ms"); ok {
cfg.QueueTimeoutMS = v
}
if v, ok := intSetting(m, "request_timeout_ms"); ok {
cfg.RequestTimeoutMS = v
}
return cfg
}
func intSetting(m map[string]any, key string) (int, bool) {
v, ok := m[key]
if !ok {
return 0, false
}
switch n := v.(type) {
case int:
return n, true
case int32:
return int(n), true
case int64:
return int(n), true
case float64:
return int(n), true
default:
return 0, false
}
}
func vllmConfFromStruct(s *structpb.Struct) config.VllmConf {
cfg := config.VllmConf{Enabled: true}
if s == nil {
return cfg
}
m := s.AsMap()
if v, ok := m["endpoint"].(string); ok {
cfg.Endpoint = v
}
if v, ok := intSetting(m, "capacity"); ok {
cfg.Capacity = v
}
if v, ok := intSetting(m, "max_queue"); ok {
cfg.MaxQueue = v
}
if v, ok := intSetting(m, "queue_timeout_ms"); ok {
cfg.QueueTimeoutMS = v
}
if v, ok := intSetting(m, "request_timeout_ms"); ok {
cfg.RequestTimeoutMS = v
}
return cfg
}
func openAICompatConfFromStruct(s *structpb.Struct) config.OpenAICompatConf {
cfg := config.OpenAICompatConf{Enabled: true}
if s == nil {
return cfg
}
m := s.AsMap()
if v, ok := m["provider"].(string); ok {
cfg.Provider = v
}
if v, ok := m["endpoint"].(string); ok {
cfg.Endpoint = v
}
if headers, ok := m["headers"].(map[string]any); ok {
cfg.Headers = make(map[string]string, len(headers))
for k, hv := range headers {
if s, ok := hv.(string); ok {
cfg.Headers[k] = s
}
}
}
if v, ok := intSetting(m, "capacity"); ok {
cfg.Capacity = v
}
if v, ok := intSetting(m, "max_queue"); ok {
cfg.MaxQueue = v
}
if v, ok := intSetting(m, "queue_timeout_ms"); ok {
cfg.QueueTimeoutMS = v
}
if v, ok := intSetting(m, "request_timeout_ms"); ok {
cfg.RequestTimeoutMS = v
}
return cfg
}
func cliConfFromStruct(s *structpb.Struct) config.CLIConf {
cfg := config.CLIConf{Enabled: true, Profiles: make(map[string]config.CLIProfileConf)}
if s == nil {
return cfg
}
m := s.AsMap()
profiles, ok := m["profiles"].(map[string]any)
if !ok {
return cfg
}
for name, pAny := range profiles {
p, ok := pAny.(map[string]any)
if !ok {
continue
}
prof := config.CLIProfileConf{}
if v, ok := p["command"].(string); ok {
prof.Command = v
}
prof.Args = stringsFromAny(p["args"])
prof.Env = stringsFromAny(p["env"])
if v, ok := boolFromAny(p["persistent"]); ok {
prof.Persistent = v
}
if v, ok := boolFromAny(p["terminal"]); ok {
prof.Terminal = v
}
if v, ok := intFromAny(p["response_idle_timeout_ms"]); ok {
prof.ResponseIdleTimeoutMS = v
}
if v, ok := intFromAny(p["startup_idle_timeout_ms"]); ok {
prof.StartupIdleTimeoutMS = v
}
if v, ok := p["output_format"].(string); ok {
prof.OutputFormat = v
}
if v, ok := p["mode"].(string); ok {
prof.Mode = v
}
prof.ResumeArgs = stringsFromAny(p["resume_args"])
if marker, ok := p["completion_marker"].(map[string]any); ok {
if v, ok := marker["line"].(string); ok {
prof.CompletionMarker.Line = v
}
if v, ok := marker["regex"].(string); ok {
prof.CompletionMarker.Regex = v
}
}
cfg.Profiles[name] = prof
}
return cfg
}
func stringsFromAny(v any) []string {
arr, ok := v.([]any)
if !ok {
return nil
}
out := make([]string, 0, len(arr))
for _, a := range arr {
if s, ok := a.(string); ok {
out = append(out, s)
}
}
return out
}
func boolFromAny(v any) (bool, bool) {
if b, ok := v.(bool); ok {
return b, true
}
return false, false
}
func intFromAny(v any) (int, bool) {
if f, ok := v.(float64); ok {
return int(f), true
}
if i, ok := v.(int); ok {
return i, true
}
return 0, false
}