mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 16:18:29 +08:00
Group pricing (rate multiplier, peak windows, per-user overrides) and account cost (accounts.rate_multiplier) already live side by side, but nothing stops the scheduler from handing a request to an account whose cost multiplier exceeds what the group's pricing can profitably serve. Add an opt-in per-group profit gate that filters scheduling candidates by a margin rule, while ordering, scoring, stickiness and breakers keep working unchanged among qualified accounts. Admission rule: an account qualifies iff U <= D * (1 - min_margin - safety_buffer) within a small relative epsilon, where U is accounts.rate_multiplier (0 is legal; missing/negative/NaN/Inf are conservatively rejected as invalid) and D is the requester's effective downstream multiplier (user-group override ?? group default, times the group peak factor) frozen at the request's pricing instant. - groups gain profit_control_enabled / profit_min_margin / profit_safety_buffer (migration 191); the durable auth-cache invalidation trigger additionally watches the profit and pricing columns (migration 192) so out-of-band group edits cannot leave stale auth snapshots; GetByKeyForAuth explicitly projects the new columns and the API-key auth snapshot version is bumped to force a refresh of pre-existing snapshots - request-level pricing instant: token entry points install pricingAt into ctx; the profit threshold D and the RecordUsage peak factor read the same instant, so one request never changes price mid-flight across waits/retries/failover (media and unwired paths keep the existing record-time semantics) - the gate covers token requests on openai, anthropic, gemini, grok and antigravity groups: OpenAI-family handlers via WithOpenAIRequestPricingContext (responses incl. WS bridge, chat completions, messages, embeddings, alpha search), the shared gateway via WithGatewayTokenRequestPricing (messages, chat completions, responses, gemini model actions); composite groups cannot enable it directly; image/video/models/usage/count_tokens stay ungated and an explicit image-generation intent suppresses the gate end to end - post-slot recheck: after a slot is acquired the account is re-read via SchedulerSnapshotService.GetAccount (scheduler cache, then DB; only when both fail the check fails open with WARN + metric); a vetoed account releases its slot and joins the request's exclusion set for reselection; sticky bindings are written only after the final check passes, and an over-threshold sticky account is skipped, not deleted, so it comes back once its rate recovers - sticky-session cache contract: GatewayCache.GetSessionAccountID now returns ErrStickySessionNotFound on a miss (mapped from redis.Nil in the repository implementation, mirroring ErrRefreshTokenNotFound) so the profit sticky path can distinguish "no binding yet" from a real read failure without importing the cache driver in service code - cross-group re-entry (composite parent -> member group) resolves the gate against the member group and clears a stale parent gate instead of letting a foreign threshold veto accounts - per-platform/group activity counters (installs, threshold vetoes, invalid-rate vetoes, refresh failures) for observability - admin UI: profit-control section on the five platforms' group forms with percent input, validation and platform-switch reset; group create/update/duplicate normalize and validate the config at a single choke point - cmd/profit-preview: offline what-if tool that replays the production admission semantics over an exported config/account/override/model dump, reports per-model admitted-account counts under the default and the worst-case (lowest user override) D, and surfaces probe-sync staleness as warnings without affecting admission Tests: service unit coverage for gate resolution/veto/threshold epsilon/pricing instant/suppress marker/scheduler filtering and post-slot recheck (incl. -race on the profit surface), unit-tagged handler slot-recheck and capability-mapping regressions, sqlmock and real-PostgreSQL integration regressions for the GetByKeyForAuth projection and the migration-192 trigger watch list, API contract update, and frontend specs for the five-platform form helpers.
48 lines
2.1 KiB
PL/PgSQL
48 lines
2.1 KiB
PL/PgSQL
-- Profit-control fields are part of the API-key auth snapshot and gate the
|
|
-- scheduling admission filter; the profit threshold D additionally depends on
|
|
-- group pricing and peak-window fields. Extend the durable invalidation
|
|
-- trigger so out-of-band group edits (direct SQL, crash between update and
|
|
-- app-level invalidation) cannot leave cached snapshots using stale
|
|
-- profit-control inputs. Normal admin saves already invalidate via
|
|
-- InvalidateAuthCacheByGroupID; this trigger is the durable backstop. Based on
|
|
-- the latest function body from 186_group_auth_cache_image_generation.sql.
|
|
|
|
CREATE OR REPLACE FUNCTION enqueue_group_auth_cache_invalidation()
|
|
RETURNS TRIGGER
|
|
LANGUAGE plpgsql
|
|
AS $$
|
|
DECLARE
|
|
target_group_id BIGINT;
|
|
BEGIN
|
|
target_group_id := OLD.id;
|
|
IF TG_OP = 'UPDATE'
|
|
AND OLD.status IS NOT DISTINCT FROM NEW.status
|
|
AND OLD.is_exclusive IS NOT DISTINCT FROM NEW.is_exclusive
|
|
AND OLD.allow_image_generation IS NOT DISTINCT FROM NEW.allow_image_generation
|
|
AND OLD.platform IS NOT DISTINCT FROM NEW.platform
|
|
AND OLD.subscription_type IS NOT DISTINCT FROM NEW.subscription_type
|
|
AND OLD.rate_multiplier IS NOT DISTINCT FROM NEW.rate_multiplier
|
|
AND OLD.peak_rate_enabled IS NOT DISTINCT FROM NEW.peak_rate_enabled
|
|
AND OLD.peak_start IS NOT DISTINCT FROM NEW.peak_start
|
|
AND OLD.peak_end IS NOT DISTINCT FROM NEW.peak_end
|
|
AND OLD.peak_rate_multiplier IS NOT DISTINCT FROM NEW.peak_rate_multiplier
|
|
AND OLD.profit_control_enabled IS NOT DISTINCT FROM NEW.profit_control_enabled
|
|
AND OLD.profit_min_margin IS NOT DISTINCT FROM NEW.profit_min_margin
|
|
AND OLD.profit_safety_buffer IS NOT DISTINCT FROM NEW.profit_safety_buffer
|
|
AND OLD.deleted_at IS NOT DISTINCT FROM NEW.deleted_at THEN
|
|
RETURN NEW;
|
|
END IF;
|
|
|
|
INSERT INTO auth_cache_invalidation_outbox (cache_key)
|
|
SELECT encode(sha256(convert_to(k.key, 'UTF8')), 'hex')
|
|
FROM api_keys AS k
|
|
WHERE k.group_id = target_group_id
|
|
AND k.deleted_at IS NULL
|
|
AND k.key <> '';
|
|
IF TG_OP = 'DELETE' THEN
|
|
RETURN OLD;
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$;
|