iop/apps/node/internal/adapters/factory.go
toki f01c7e5ecd refactor(node): CLI 실행 경계를 정리한다
터미널 세션 core와 CLI mode executor 경계를 분리해 후속 remote terminal bridge가 재사용할 내부 실행 기반을 만든다.

adapter 기본값과 vLLM experimental surface도 명시해 production 실행 경로가 mock fallback이나 미구현 실행으로 숨지 않게 한다.
2026-06-06 14:57:49 +09:00

232 lines
5.6 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/packages/go/config"
iop "iop/proto/gen/iop"
)
const vllmDisabledError = "adapters: vllm adapter is experimental and disabled until streaming execution is implemented"
// 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
}
switch ac.GetType() {
case "mock":
reg.Register(mock.New(logger))
case "ollama":
var cfg config.OllamaConf
if m := ac.GetOllama(); m != nil {
cfg = ollamaConfFromProto(m)
} else {
cfg = ollamaConfFromStruct(ac.GetSettings())
}
reg.Register(ollama.New(cfg, logger))
case "vllm":
return nil, fmt.Errorf(vllmDisabledError)
case "cli":
var cfg config.CLIConf
if m := ac.GetCli(); m != nil {
cfg = cliConfFromProto(m)
} else {
cfg = cliConfFromStruct(ac.GetSettings())
}
reg.Register(cli.New(cfg, logger))
default:
return nil, fmt.Errorf("adapters: unknown adapter type %q", ac.GetType())
}
}
return reg, nil
}
func ollamaConfFromProto(m *iop.OllamaAdapterConfig) config.OllamaConf {
return config.OllamaConf{
Enabled: true,
BaseURL: m.GetBaseUrl(),
ContextSize: int(m.GetContextSize()),
}
}
func vllmConfFromProto(m *iop.VllmAdapterConfig) config.VllmConf {
return config.VllmConf{Enabled: true, Endpoint: m.GetEndpoint()}
}
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
}
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
}
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
}