feat(batch): add provider id and reuse policy
This commit is contained in:
63
internal/batch/provider_id.go
Normal file
63
internal/batch/provider_id.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package batch
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func NormalizeProviderID(baseURL string) string {
|
||||
parsedURL, err := url.Parse(strings.TrimSpace(baseURL))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if parsedURL.Scheme == "" || parsedURL.Host == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
normalizedHost := normalizeProviderHost(parsedURL.Hostname())
|
||||
normalizedPath := path.Clean(strings.TrimSpace(parsedURL.EscapedPath()))
|
||||
if normalizedPath == "." {
|
||||
normalizedPath = ""
|
||||
}
|
||||
|
||||
normalizedURL := parsedURL.Scheme + "://" + strings.ToLower(parsedURL.Hostname()) + normalizedPath
|
||||
digest := sha256.Sum256([]byte(normalizedURL))
|
||||
hash := hex.EncodeToString(digest[:])
|
||||
if len(hash) > 8 {
|
||||
hash = hash[len(hash)-8:]
|
||||
}
|
||||
|
||||
if normalizedHost == "" {
|
||||
normalizedHost = "provider"
|
||||
}
|
||||
return fmt.Sprintf("%s-%s", normalizedHost, hash)
|
||||
}
|
||||
|
||||
func normalizeProviderHost(host string) string {
|
||||
host = strings.TrimSpace(strings.ToLower(host))
|
||||
if host == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
parts := strings.Split(host, ".")
|
||||
filtered := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, part)
|
||||
}
|
||||
|
||||
if len(filtered) > 1 {
|
||||
filtered = filtered[:len(filtered)-1]
|
||||
}
|
||||
if len(filtered) == 0 {
|
||||
return strings.ReplaceAll(host, ".", "-")
|
||||
}
|
||||
return strings.Join(filtered, "-")
|
||||
}
|
||||
20
internal/batch/provider_id_test.go
Normal file
20
internal/batch/provider_id_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package batch
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNormalizeProviderID(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
first := NormalizeProviderID("https://api.deepseek.com/v1")
|
||||
second := NormalizeProviderID("https://api.deepseek.com/proxy/v1")
|
||||
|
||||
if first == second {
|
||||
t.Fatalf("NormalizeProviderID() = %q for both URLs, want distinct ids", first)
|
||||
}
|
||||
if first == "" || second == "" {
|
||||
t.Fatalf("NormalizeProviderID() returned empty ids: %q %q", first, second)
|
||||
}
|
||||
if got := first[:12]; got != "api-deepseek" {
|
||||
t.Fatalf("NormalizeProviderID() prefix = %q, want api-deepseek*", first)
|
||||
}
|
||||
}
|
||||
95
internal/batch/reuse_policy.go
Normal file
95
internal/batch/reuse_policy.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package batch
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"sub2api-cn-relay-manager/internal/probe"
|
||||
)
|
||||
|
||||
type ReuseInput struct {
|
||||
ProviderID string
|
||||
CanonicalModelFamilies []string
|
||||
MatchedAccountID int64
|
||||
MatchedAccountState MatchedAccountState
|
||||
ExistingProviderID string
|
||||
ExistingAccessStatus AccessStatus
|
||||
ExistingCanonicalFamilys []string
|
||||
}
|
||||
|
||||
type ReuseDecision struct {
|
||||
ProvisionReused bool
|
||||
ReusedFromProviderID string
|
||||
ReusedFromAccountID int64
|
||||
MatchedAccountState MatchedAccountState
|
||||
AccountResolution AccountResolution
|
||||
FamilyCovered bool
|
||||
}
|
||||
|
||||
func DecideReuse(input ReuseInput) ReuseDecision {
|
||||
decision := ReuseDecision{
|
||||
MatchedAccountState: input.MatchedAccountState,
|
||||
AccountResolution: AccountResolutionCreated,
|
||||
FamilyCovered: canonicalFamiliesCovered(input.CanonicalModelFamilies, input.ExistingCanonicalFamilys),
|
||||
}
|
||||
|
||||
if input.MatchedAccountState == "" {
|
||||
decision.MatchedAccountState = MatchedAccountStateNone
|
||||
}
|
||||
|
||||
if !sameProvider(input.ProviderID, input.ExistingProviderID) || !decision.FamilyCovered {
|
||||
return decision
|
||||
}
|
||||
|
||||
if input.ExistingAccessStatus == AccessStatusBroken || input.MatchedAccountState == MatchedAccountStateBroken {
|
||||
decision.AccountResolution = AccountResolutionReplaced
|
||||
return decision
|
||||
}
|
||||
|
||||
switch input.MatchedAccountState {
|
||||
case MatchedAccountStateDisabled, MatchedAccountStateDeprecated:
|
||||
decision.ProvisionReused = true
|
||||
decision.ReusedFromProviderID = strings.TrimSpace(input.ExistingProviderID)
|
||||
decision.ReusedFromAccountID = input.MatchedAccountID
|
||||
decision.AccountResolution = AccountResolutionReactivated
|
||||
return decision
|
||||
case MatchedAccountStateActive, MatchedAccountStateNone, "":
|
||||
decision.ProvisionReused = true
|
||||
decision.ReusedFromProviderID = strings.TrimSpace(input.ExistingProviderID)
|
||||
decision.ReusedFromAccountID = input.MatchedAccountID
|
||||
decision.AccountResolution = AccountResolutionReused
|
||||
return decision
|
||||
default:
|
||||
return decision
|
||||
}
|
||||
}
|
||||
|
||||
func canonicalFamiliesCovered(requested []string, existing []string) bool {
|
||||
if len(requested) == 0 || len(existing) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
existingSet := make(map[string]struct{}, len(existing))
|
||||
for _, family := range existing {
|
||||
canonical := probe.CanonicalModelFamily(family)
|
||||
if canonical == "" {
|
||||
continue
|
||||
}
|
||||
existingSet[canonical] = struct{}{}
|
||||
}
|
||||
|
||||
for _, family := range requested {
|
||||
canonical := probe.CanonicalModelFamily(family)
|
||||
if canonical == "" {
|
||||
return false
|
||||
}
|
||||
if _, ok := existingSet[canonical]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func sameProvider(left, right string) bool {
|
||||
return strings.TrimSpace(left) != "" && strings.TrimSpace(left) == strings.TrimSpace(right)
|
||||
}
|
||||
113
internal/batch/reuse_policy_test.go
Normal file
113
internal/batch/reuse_policy_test.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package batch
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDecideReuse(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("active provider with covered family is reused", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
decision := DecideReuse(ReuseInput{
|
||||
ProviderID: "api-deepseek-12345678",
|
||||
CanonicalModelFamilies: []string{"kimi-k2.6"},
|
||||
MatchedAccountID: 101,
|
||||
MatchedAccountState: MatchedAccountStateActive,
|
||||
ExistingProviderID: "api-deepseek-12345678",
|
||||
ExistingAccessStatus: AccessStatusActive,
|
||||
ExistingCanonicalFamilys: []string{"kimi 2.6"},
|
||||
})
|
||||
|
||||
if !decision.ProvisionReused {
|
||||
t.Fatal("ProvisionReused = false, want true")
|
||||
}
|
||||
if decision.AccountResolution != AccountResolutionReused {
|
||||
t.Fatalf("AccountResolution = %q, want %q", decision.AccountResolution, AccountResolutionReused)
|
||||
}
|
||||
if decision.MatchedAccountState != MatchedAccountStateActive {
|
||||
t.Fatalf("MatchedAccountState = %q, want %q", decision.MatchedAccountState, MatchedAccountStateActive)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("disabled or deprecated account is reactivated", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, state := range []MatchedAccountState{MatchedAccountStateDisabled, MatchedAccountStateDeprecated} {
|
||||
state := state
|
||||
t.Run(string(state), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
decision := DecideReuse(ReuseInput{
|
||||
ProviderID: "api-kimi-12345678",
|
||||
CanonicalModelFamilies: []string{"kimi-k2.6"},
|
||||
MatchedAccountID: 202,
|
||||
MatchedAccountState: state,
|
||||
ExistingProviderID: "api-kimi-12345678",
|
||||
ExistingAccessStatus: AccessStatusActive,
|
||||
ExistingCanonicalFamilys: []string{"kimi-2.6"},
|
||||
})
|
||||
|
||||
if !decision.ProvisionReused {
|
||||
t.Fatal("ProvisionReused = false, want true")
|
||||
}
|
||||
if decision.AccountResolution != AccountResolutionReactivated {
|
||||
t.Fatalf("AccountResolution = %q, want %q", decision.AccountResolution, AccountResolutionReactivated)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("broken provider or account is replaced", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
brokenProvider := DecideReuse(ReuseInput{
|
||||
ProviderID: "api-deepseek-12345678",
|
||||
CanonicalModelFamilies: []string{"deepseek-v4-pro"},
|
||||
MatchedAccountState: MatchedAccountStateActive,
|
||||
ExistingProviderID: "api-deepseek-12345678",
|
||||
ExistingAccessStatus: AccessStatusBroken,
|
||||
ExistingCanonicalFamilys: []string{"deepseek-v4-pro"},
|
||||
})
|
||||
if brokenProvider.ProvisionReused {
|
||||
t.Fatal("ProvisionReused = true, want false for broken provider")
|
||||
}
|
||||
if brokenProvider.AccountResolution != AccountResolutionReplaced {
|
||||
t.Fatalf("AccountResolution = %q, want %q", brokenProvider.AccountResolution, AccountResolutionReplaced)
|
||||
}
|
||||
|
||||
brokenAccount := DecideReuse(ReuseInput{
|
||||
ProviderID: "api-deepseek-12345678",
|
||||
CanonicalModelFamilies: []string{"deepseek-v4-pro"},
|
||||
MatchedAccountState: MatchedAccountStateBroken,
|
||||
ExistingProviderID: "api-deepseek-12345678",
|
||||
ExistingAccessStatus: AccessStatusActive,
|
||||
ExistingCanonicalFamilys: []string{"deepseek-v4-pro"},
|
||||
})
|
||||
if brokenAccount.ProvisionReused {
|
||||
t.Fatal("ProvisionReused = true, want false for broken account")
|
||||
}
|
||||
if brokenAccount.AccountResolution != AccountResolutionReplaced {
|
||||
t.Fatalf("AccountResolution = %q, want %q", brokenAccount.AccountResolution, AccountResolutionReplaced)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("same family different alias counts as covered", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
decision := DecideReuse(ReuseInput{
|
||||
ProviderID: "api-kimi-12345678",
|
||||
CanonicalModelFamilies: []string{"kimi-k2.6"},
|
||||
MatchedAccountState: MatchedAccountStateActive,
|
||||
ExistingProviderID: "api-kimi-12345678",
|
||||
ExistingAccessStatus: AccessStatusActive,
|
||||
ExistingCanonicalFamilys: []string{"kimi 2.6"},
|
||||
})
|
||||
|
||||
if !decision.ProvisionReused {
|
||||
t.Fatal("ProvisionReused = false, want true")
|
||||
}
|
||||
if !decision.FamilyCovered {
|
||||
t.Fatal("FamilyCovered = false, want true")
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user