Files
sub2api/backend/internal/service/gateway_model_availability.go
李建琦 6d655c9903
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

112 lines
4.1 KiB
Go

package service
import (
"context"
"strings"
"github.com/Wei-Shaw/sub2api/internal/config"
)
// ModelAvailabilityDiagnosis describes whether the requested model can be
// served by any persistently eligible account in the group (active with its
// schedulable setting enabled), ignoring transient state such as rate limits,
// overload, temporary unschedulability, and runtime blocks. Handlers use this
// on the "no available accounts" error path to distinguish 404
// model_not_found from 503 service_unavailable.
type ModelAvailabilityDiagnosis struct {
// HasAccountsInPool is true if the group has at least one persistently
// eligible account on the queried platform (or, for Anthropic/Gemini, on
// the platform plus mixed-scheduled Antigravity accounts).
HasAccountsInPool bool
// HasModelSupport is true if at least one account's model mapping admits
// the requested model.
HasModelSupport bool
}
// ModelAvailabilityDiagnoser is implemented by gateway services that can
// report whether the requested model is configured to be served by any
// account. Both *GatewayService and *OpenAIGatewayService implement this so
// handlers in either package can share a single classifier.
type ModelAvailabilityDiagnoser interface {
DiagnoseModelAvailabilityForPlatform(
ctx context.Context,
groupID *int64,
requestedModel string,
platform string,
) ModelAvailabilityDiagnosis
}
// DiagnoseModelAvailabilityForPlatform inspects accounts enabled for scheduling
// by persistent configuration and returns whether the requested model is
// configured to be served by any of them. The dedicated repository query
// bypasses scheduler snapshots and deliberately ignores transient rate-limit,
// overload, temporary-unschedulable, expiry, quota, and runtime-block state.
//
// Safe to call on the error path: returns {true,true} on any internal failure
// or when the inputs preclude meaningful diagnosis (empty model, etc.), so
// callers stay on the 503 fallback branch.
func (s *GatewayService) DiagnoseModelAvailabilityForPlatform(
ctx context.Context,
groupID *int64,
requestedModel string,
platform string,
) ModelAvailabilityDiagnosis {
if s == nil {
return ModelAvailabilityDiagnosis{HasAccountsInPool: true, HasModelSupport: true}
}
requestedModel = strings.TrimSpace(requestedModel)
if requestedModel == "" {
// No model specified — cannot decide model_not_found. Caller falls back to 503.
return ModelAvailabilityDiagnosis{HasAccountsInPool: true, HasModelSupport: true}
}
if strings.TrimSpace(platform) == "" {
// Without a platform we cannot scope the lookup; bail out to the
// 503 branch rather than make an unscoped scan.
return ModelAvailabilityDiagnosis{HasAccountsInPool: true, HasModelSupport: true}
}
if s.accountRepo == nil {
return ModelAvailabilityDiagnosis{HasAccountsInPool: true, HasModelSupport: true}
}
useMixed := platform == PlatformAnthropic || platform == PlatformGemini
platforms := []string{platform}
if useMixed {
platforms = append(platforms, PlatformAntigravity)
}
queryGroupID := groupID
includeGrouped := false
if useMixed {
// Preserve the generic scheduler's scope rules: an explicit group wins
// for mixed scheduling, while group-less simple mode scans all accounts.
if groupID == nil && s.cfg != nil && s.cfg.RunMode == config.RunModeSimple {
includeGrouped = true
}
} else if s.cfg != nil && s.cfg.RunMode == config.RunModeSimple {
queryGroupID = nil
includeGrouped = true
}
accounts, err := s.accountRepo.ListModelAvailabilityCandidates(ctx, queryGroupID, platforms, includeGrouped)
if err != nil {
// Conservative fallback: pretend everything is fine so the caller
// returns 503 (we don't want to flip to 404 just because a lookup
// hiccup'd).
return ModelAvailabilityDiagnosis{HasAccountsInPool: true, HasModelSupport: true}
}
diag := ModelAvailabilityDiagnosis{}
for i := range accounts {
if useMixed && accounts[i].Platform == PlatformAntigravity && !accounts[i].IsMixedSchedulingEnabled() {
continue
}
diag.HasAccountsInPool = true
if s.isModelSupportedByAccountWithContext(ctx, &accounts[i], requestedModel) {
diag.HasModelSupport = true
return diag
}
}
return diag
}