Files
sub2api/backend/internal/service/gateway_model_availability.go
T

84 lines
3.2 KiB
Go

package service
import (
"context"
"strings"
)
// ModelAvailabilityDiagnosis describes whether the requested model can be
// served by any configured account in the group, ignoring transient state
// (rate limits, quota auto-pause, 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 schedulable
// 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 schedulable accounts of the
// given platform and returns whether the requested model is configured to be
// served by any of them. It deliberately ignores schedulability, rate limits,
// quotas, and runtime blocks — those are transient.
//
// 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}
}
// hasForcePlatform=false so Anthropic/Gemini also surface mixed-scheduled
// Antigravity accounts, matching what selection would consider.
accounts, _, err := s.listSchedulableAccounts(ctx, groupID, platform, false)
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 {
diag.HasAccountsInPool = true
if s.isModelSupportedByAccountWithContext(ctx, &accounts[i], requestedModel) {
diag.HasModelSupport = true
return diag
}
}
return diag
}