fix(edge): 프로필 backing 중복을 단일화한다

This commit is contained in:
toki 2026-08-02 12:43:48 +09:00
parent 1789f4007c
commit 7d8551b440
2 changed files with 128 additions and 1 deletions

View file

@ -19,7 +19,8 @@ import (
// Providers with a non-empty Adapter field and no protocol profile reference a
// legacy instance and are skipped during provider-first compile. A profile-
// backed provider is compiled under its provider id from the referenced HTTP
// adapter while the legacy instance is retained below.
// adapter. When that backing instance has the same key, it is consumed instead
// of being emitted a second time.
//
// After config normalisation, OllamaInstances, VllmInstances, and
// OpenAICompatInstances already contain the promoted legacy single-instance
@ -45,6 +46,11 @@ func BuildConfigPayload(rec *NodeRecord) (*iop.NodeConfigPayload, error) {
// flat namespace to detect conflicts between provider ids and legacy adapter keys
seenFlat := make(map[string]struct{})
// A profile-backed provider may intentionally reuse the exact legacy
// adapter key that supplies its transport settings. Config validation has
// already required both definitions to agree; emit only the provider-backed
// adapter so the Node receives one unambiguous instance key.
shadowedLegacy := make(map[string]struct{})
for _, p := range rec.Providers {
if p.Adapter != "" && p.RuntimeProfile == nil {
@ -62,6 +68,18 @@ func BuildConfigPayload(rec *NodeRecord) (*iop.NodeConfigPayload, error) {
var err error
if p.RuntimeProfile != nil && strings.TrimSpace(p.Adapter) != "" {
ac, err = profileBackedProviderToAdapterConfig(rec, p)
if err == nil && strings.TrimSpace(p.Adapter) == p.ID {
for _, inst := range rec.Adapters.VllmInstances {
if inst.Name == p.ID {
shadowedLegacy["vllm:"+p.ID] = struct{}{}
}
}
for _, inst := range rec.Adapters.OpenAICompatInstances {
if inst.Name == p.ID {
shadowedLegacy["openai_compat:"+p.ID] = struct{}{}
}
}
}
} else {
ac, err = providerToAdapterConfig(p)
}
@ -83,6 +101,9 @@ func BuildConfigPayload(rec *NodeRecord) (*iop.NodeConfigPayload, error) {
}
seen[key] = struct{}{}
if _, conflict := seenFlat[inst.Name]; conflict {
if _, shadowed := shadowedLegacy[key]; shadowed {
continue
}
return nil, fmt.Errorf("ollama adapter instance key %q conflicts with provider id", inst.Name)
}
payload.Adapters = append(payload.Adapters, &iop.AdapterConfig{
@ -112,6 +133,9 @@ func BuildConfigPayload(rec *NodeRecord) (*iop.NodeConfigPayload, error) {
}
seen[key] = struct{}{}
if _, conflict := seenFlat[inst.Name]; conflict {
if _, shadowed := shadowedLegacy[key]; shadowed {
continue
}
return nil, fmt.Errorf("vllm adapter instance key %q conflicts with provider id", inst.Name)
}
payload.Adapters = append(payload.Adapters, &iop.AdapterConfig{
@ -140,6 +164,9 @@ func BuildConfigPayload(rec *NodeRecord) (*iop.NodeConfigPayload, error) {
}
seen[key] = struct{}{}
if _, conflict := seenFlat[inst.Name]; conflict {
if _, shadowed := shadowedLegacy[key]; shadowed {
continue
}
return nil, fmt.Errorf("openai_compat adapter instance key %q conflicts with provider id", inst.Name)
}
payload.Adapters = append(payload.Adapters, &iop.AdapterConfig{

View file

@ -249,6 +249,106 @@ func TestProtocolProfileBuildConfigPayloadNoBase(t *testing.T) {
_ = openaiCfg.GetProtocolProfile()
}
func TestProtocolProfileProviderReusingBackingKeyEmitsOneAdapter(t *testing.T) {
profile := config.ConcreteProtocolProfile{
ID: "openai_chat",
ProtocolProfileConf: config.ProtocolProfileConf{
Driver: config.ProtocolDriverOpenAIChat,
},
}
rec := &NodeRecord{
ID: "node-profile-reuse",
Alias: "profile-reuse",
Token: "token",
Providers: []config.NodeProviderConf{
{
ID: "provider-a",
Type: "vllm",
Adapter: "provider-a",
Capacity: 2,
RequestTimeoutMS: 120000,
RuntimeProfile: &profile,
},
},
Adapters: config.AdaptersConf{
OpenAICompatInstances: []config.OpenAICompatInstanceConf{
{
Name: "provider-a",
Enabled: true,
Provider: "vllm",
Endpoint: "http://127.0.0.1:8000/v1",
Capacity: 2,
RequestTimeoutMS: 120000,
},
},
},
}
payload, err := BuildConfigPayload(rec)
if err != nil {
t.Fatalf("BuildConfigPayload: %v", err)
}
if len(payload.GetAdapters()) != 1 {
t.Fatalf("adapter count = %d, want 1", len(payload.GetAdapters()))
}
adapter := payload.GetAdapters()[0]
if adapter.GetName() != "provider-a" {
t.Fatalf("adapter name = %q, want provider-a", adapter.GetName())
}
openAICompat := adapter.GetOpenaiCompat()
if openAICompat == nil {
t.Fatal("expected OpenAI-compatible adapter")
}
if openAICompat.GetEndpoint() != "http://127.0.0.1:8000/v1" {
t.Fatalf("endpoint = %q", openAICompat.GetEndpoint())
}
if openAICompat.GetProtocolProfile() == nil {
t.Fatal("expected resolved protocol profile")
}
}
func TestProtocolProfileProviderReusingBackingKeyRejectsOtherLegacyType(t *testing.T) {
profile := config.ConcreteProtocolProfile{
ID: "openai_chat",
ProtocolProfileConf: config.ProtocolProfileConf{
Driver: config.ProtocolDriverOpenAIChat,
},
}
rec := &NodeRecord{
ID: "node-profile-reuse-conflict",
Alias: "profile-reuse-conflict",
Token: "token",
Providers: []config.NodeProviderConf{
{
ID: "provider-a",
Type: "vllm",
Adapter: "provider-a",
Capacity: 2,
RuntimeProfile: &profile,
},
},
Adapters: config.AdaptersConf{
OllamaInstances: []config.OllamaInstanceConf{
{Name: "provider-a", Enabled: true, Capacity: 2},
},
OpenAICompatInstances: []config.OpenAICompatInstanceConf{
{
Name: "provider-a",
Enabled: true,
Provider: "vllm",
Endpoint: "http://127.0.0.1:8000/v1",
Capacity: 2,
},
},
},
}
_, err := BuildConfigPayload(rec)
if err == nil || !strings.Contains(err.Error(), "ollama adapter instance key") {
t.Fatalf("BuildConfigPayload error = %v, want ollama key conflict", err)
}
}
func TestProtocolProfileAdapterBackingCompile(t *testing.T) {
profile := config.ConcreteProtocolProfile{
ID: "custom", ProtocolProfileConf: config.ProtocolProfileConf{