iop/apps/edge/internal/service/usage_attribution_dispatch_test.go
toki 04879f2b43 feat(openai): 실제 provider별 사용량 귀속을 기록한다
요청 종료 계수와 실제 provider 시도 사용량을 분리하고, 직접·pool·retry 경로의 attribution을 보존한다. 관련 계약·스펙과 완료된 task archive 정리도 함께 반영한다.
2026-07-31 20:22:23 +09:00

215 lines
5.7 KiB
Go

package service
import (
"context"
"net"
"testing"
toki "git.toki-labs.com/toki/proto-socket/go"
"google.golang.org/protobuf/proto"
edgeevents "iop/apps/edge/internal/events"
edgenode "iop/apps/edge/internal/node"
"iop/packages/go/config"
iop "iop/proto/gen/iop"
)
func newUsageAttributionService(t *testing.T, providerType string) *Service {
t.Helper()
edgeConn, nodeConn := net.Pipe()
t.Cleanup(func() {
_ = edgeConn.Close()
_ = nodeConn.Close()
})
parserMap := toki.ParserMap{
toki.TypeNameOf(&iop.RunRequest{}): func(data []byte) (proto.Message, error) {
message := &iop.RunRequest{}
return message, proto.Unmarshal(data, message)
},
toki.TypeNameOf(&iop.ProviderTunnelRequest{}): func(data []byte) (proto.Message, error) {
message := &iop.ProviderTunnelRequest{}
return message, proto.Unmarshal(data, message)
},
}
edgeClient := toki.NewTcpClient(edgeConn, 0, 0, parserMap)
_ = toki.NewTcpClient(nodeConn, 0, 0, parserMap)
registry := edgenode.NewRegistry()
registry.Register(&edgenode.NodeEntry{
NodeID: "node-a",
LifecycleState: edgenode.LifecycleConnected,
Client: edgeClient,
})
service := New(registry, edgeevents.NewBus())
store := edgenode.NewNodeStore()
store.Add(&edgenode.NodeRecord{
ID: "node-a",
Providers: []config.NodeProviderConf{{
ID: "provider-a",
Type: providerType,
Models: []string{"served-a"},
Health: "available",
Capacity: 1,
}},
})
service.SetNodeStore(store)
service.SetModelCatalog([]config.ModelCatalogEntry{{
ID: "logical-model",
UsageAttribution: config.UsageAttributionModelGroup,
Providers: map[string]string{"provider-a": "served-a"},
}})
return service
}
func assertUsageAttributionDispatch(
t *testing.T,
dispatch RunDispatch,
providerID, target, nodeID, attribution string,
) {
t.Helper()
if dispatch.ProviderID != providerID {
t.Errorf("provider_id = %q, want %q", dispatch.ProviderID, providerID)
}
if dispatch.Target != target {
t.Errorf("target = %q, want %q", dispatch.Target, target)
}
if dispatch.NodeID != nodeID {
t.Errorf("node_id = %q, want %q", dispatch.NodeID, nodeID)
}
if dispatch.UsageAttribution != attribution {
t.Errorf("usage_attribution = %q, want %q", dispatch.UsageAttribution, attribution)
}
}
func TestRunDispatchActualProviderBinding(t *testing.T) {
t.Run("direct normalized", func(t *testing.T) {
service := newUsageAttributionService(t, "ollama")
result, err := service.SubmitRun(context.Background(), SubmitRunRequest{
NodeRef: "node-a",
ProviderID: "provider-direct",
UsageAttribution: config.UsageAttributionProvider,
Adapter: "cli",
Target: "direct-target",
Background: true,
})
if err != nil {
t.Fatalf("SubmitRun: %v", err)
}
defer result.Close()
assertUsageAttributionDispatch(
t,
result.Dispatch(),
"provider-direct",
"direct-target",
"node-a",
config.UsageAttributionProvider,
)
})
t.Run("provider pool normalized", func(t *testing.T) {
service := newUsageAttributionService(t, "ollama")
result, err := service.SubmitProviderPool(context.Background(), ProviderPoolDispatchRequest{
Run: SubmitRunRequest{
ModelGroupKey: "logical-model",
UsageAttribution: config.UsageAttributionModelGroup,
ProviderPool: true,
Background: true,
},
})
if err != nil {
t.Fatalf("SubmitProviderPool: %v", err)
}
if result.Path != ProviderPoolPathNormalized || result.Run == nil {
t.Fatalf("path = %q run=%v, want normalized run", result.Path, result.Run != nil)
}
defer result.Run.Close()
defer service.queue.releaseRun(result.DispatchInfo.RunID, "test-cleanup")
assertUsageAttributionDispatch(
t,
result.DispatchInfo,
"provider-a",
"served-a",
"node-a",
config.UsageAttributionModelGroup,
)
assertUsageAttributionDispatch(
t,
result.Run.Dispatch(),
"provider-a",
"served-a",
"node-a",
config.UsageAttributionModelGroup,
)
})
}
func TestTunnelDispatchActualProviderBinding(t *testing.T) {
t.Run("direct tunnel", func(t *testing.T) {
service := newUsageAttributionService(t, "vllm")
result, err := service.SubmitProviderTunnel(context.Background(), SubmitProviderTunnelRequest{
NodeRef: "node-a",
ProviderID: "provider-direct",
UsageAttribution: config.UsageAttributionProvider,
Adapter: "openai_compat",
Target: "direct-target",
Method: "POST",
Path: "/v1/chat/completions",
})
if err != nil {
t.Fatalf("SubmitProviderTunnel: %v", err)
}
defer result.Close()
assertUsageAttributionDispatch(
t,
result.Dispatch(),
"provider-direct",
"direct-target",
"node-a",
config.UsageAttributionProvider,
)
})
t.Run("provider pool tunnel", func(t *testing.T) {
service := newUsageAttributionService(t, "vllm")
result, err := service.SubmitProviderPool(context.Background(), ProviderPoolDispatchRequest{
Run: SubmitRunRequest{
ModelGroupKey: "logical-model",
UsageAttribution: config.UsageAttributionModelGroup,
ProviderPool: true,
},
Tunnel: SubmitProviderTunnelRequest{
Method: "POST",
Path: "/v1/chat/completions",
},
})
if err != nil {
t.Fatalf("SubmitProviderPool: %v", err)
}
if result.Path != ProviderPoolPathTunnel || result.Tunnel == nil {
t.Fatalf("path = %q tunnel=%v, want provider tunnel", result.Path, result.Tunnel != nil)
}
defer result.Tunnel.Close()
assertUsageAttributionDispatch(
t,
result.DispatchInfo,
"provider-a",
"served-a",
"node-a",
config.UsageAttributionModelGroup,
)
assertUsageAttributionDispatch(
t,
result.Tunnel.Dispatch(),
"provider-a",
"served-a",
"node-a",
config.UsageAttributionModelGroup,
)
})
}