feat(sse): add STRICT_ZERO_COST opt-in free-access policy (#10965)

5 — freeAccessPolicy "strict" opt-in (default off): verifica candidatos de auto-combo contra estado de quota ao vivo + segurança econômica por conexão antes do dispatch (fail-closed — estado desconhecido/stale/incompleto é excluído). Zero mudança de comportamento com o default "off". 82 testes focados, eslint/prettier/typecheck limpos, docs em docs/routing/STRICT_ZERO_COST.md.
This commit is contained in:
mymusicmyspace
2026-08-21 19:59:11 +02:00
committed by GitHub
parent 9469b9c79e
commit 3caa59107e
14 changed files with 1579 additions and 13 deletions

View File

@@ -238,6 +238,11 @@ export async function getSettings() {
// (`:free` suffix, zero-price pricing, or FREE_MODEL_BUDGETS membership). Default
// false preserves prior behaviour; opt-in only.
hidePaidModels: false,
// Opt-in, default off: same shape as hidePaidModels above, but requires a
// live hard-stop-guaranteed quota check for non-keyless free candidates.
// See open-sse/services/autoCombo/strictZeroCostFilter.ts.
freeAccessPolicy: "off",
excludeTosAvoid: false,
// #9418: Opt-in filter that hides auto/* virtual combos from the /v1/models catalog.
// User-defined combos are unaffected; routing still works for hidden ids sent explicitly.
hideAutoCombos: false,

View File

@@ -128,6 +128,16 @@ export const updateSettingsSchema = z.object({
blockedProviders: z.array(z.string().max(100)).optional(),
noAuthFallbackDisabledProviders: z.array(z.string().max(100)).optional(),
hidePaidModels: z.boolean().optional(),
// STRICT_ZERO_COST (opt-in, default "off"): stricter than hidePaidModels — a
// candidate must be keyless (no credential exists, so no request against it
// can ever be billed) OR pass a live, fresh, hard-stop-guaranteed quota
// check, per candidate, before ranking/dispatch. See
// open-sse/services/autoCombo/strictZeroCostFilter.ts.
freeAccessPolicy: z.enum(["off", "strict"]).optional(),
// Separate from freeAccessPolicy on purpose: excludes candidates whose
// curated `tos` verdict is "avoid" (proxy/self-hosted use conflicts with the
// provider's own terms) — a contractual concern, not an economic one.
excludeTosAvoid: z.boolean().optional(),
hideHealthCheckLogs: z.boolean().optional(),
hideEndpointCloudflaredTunnel: z.boolean().optional(),
hideEndpointTailscaleFunnel: z.boolean().optional(),

View File

@@ -2453,6 +2453,18 @@ export async function markAccountUnavailable(
try {
await currentMutex;
// STRICT_ZERO_COST: this connection just failed (whatever the reason) —
// drop any cached "SAFE" free-allowance reading for it immediately rather
// than waiting out the TTL, so the very next candidate-pool build reads a
// clean cache miss (UNKNOWN → excluded) instead of a stale SAFE. Cheap,
// idempotent, and correct to over-invalidate on non-quota failures too —
// worst case is one extra background refresh.
if (provider) {
const { invalidateFreeAccessState } =
await import("@omniroute/open-sse/services/autoCombo/freeAccessQuota.ts");
invalidateFreeAccessState(provider, connectionId);
}
const resourceBypass = getResource404Bypass(status, errorText, connectionId, log);
if (resourceBypass) return resourceBypass;