refactor: align provider and task contracts

This commit is contained in:
Qiufeng
2026-09-03 08:18:37 +08:00
parent b16390dd41
commit ca2e4b360e
13 changed files with 807 additions and 363 deletions
+136 -21
View File
@@ -1,6 +1,7 @@
export const CONTRACT_VERSION = "2026-08-20.v1" as const;
export const API_PREFIX = "/api/v1" as const;
export const WS_PATH = "/api/v1/ws/tasks" as const;
export const DOMAIN_SNAPSHOT_VERSION = 2 as const;
export const ErrorCodes = {
Unauthorized: "UNAUTHORIZED",
@@ -91,13 +92,16 @@ export type CapabilitySchema = {
async: boolean;
};
/** Values are stored by capability so management forms never need a raw JSON
* editor while a task can still snapshot the exact public contract. */
export type CapabilityParameters = Partial<Record<ModelCapability, Record<string, string | number | boolean | string[]>>>;
export type PublicResolution = {
id: string;
label: string;
width: number;
height: number;
priceMultiplier: number;
/** Standard image ratios enabled for this billing preset; `auto` is implicit. */
/** Standard image ratios enabled for this public model; `auto` is implicit. */
ratios?: string[];
};
@@ -130,19 +134,122 @@ export type PublicModelProduct = {
name: string;
tier: "basic" | "advanced" | "flagship";
capabilities: ModelCapability[];
capabilityParameters?: CapabilityParameters;
resolutions: PublicResolution[];
basePrice: number;
currencyUnit: string;
enabled: boolean;
version?: number;
capabilitiesSchema?: CapabilitySchema[];
publicErrorMessages?: Partial<Record<ErrorCode, string>>;
maxCount?: number;
maxConcurrent?: number;
};
export type InternalModelProduct = PublicModelProduct & {
channelGroupId?: string;
export type InternalModelProduct = PublicModelProduct;
export type PricingRuleTier = {
minQuantity: number;
maxQuantity?: number;
unitPrice: number;
};
export type PricingRule = {
id: string;
publicModelId: string;
resolutionId?: string;
billingUnit: "task" | "output";
tiers: PricingRuleTier[];
multiplier: number;
balanceUnitVersion: number;
effectiveFrom?: string;
effectiveTo?: string;
enabled: boolean;
version: number;
};
export type ChannelModel = {
requestModelId: string;
source: "discovered" | "manual";
enabled: boolean;
lastSeenAt?: string;
};
export type ModelRouteChannel = {
channelId: string;
priorityTier: number;
orderWithinTier: number;
retryBudget: number;
};
export type ModelRoute = {
publicModelId: string;
channels: ModelRouteChannel[];
strategy: "strict" | "smart-within-priority";
retryBudget: number;
version: number;
};
export type ChannelHealthStatus = "healthy" | "degraded" | "open" | "half-open";
export type ChannelHealthSampleStatus = "success" | "slow" | "network_error" | "http_error" | "provider_error" | "empty" | "probing";
export type ChannelHealthSample = {
id: string;
channelId: string;
source: "probe" | "task";
status: ChannelHealthSampleStatus;
latencyMs?: number;
httpStatus?: number;
requestPath?: string;
requestAttempted: boolean;
errorCode?: string;
createdAt: string;
};
export type CapacitySnapshot = {
current: number;
limit: number;
available: number;
};
export type RouteSnapshotCandidate = ModelRouteChannel & {
requestModelId: string;
};
export type RouteSnapshot = {
groupId: string;
groupVersion: number;
publicModelId: string;
routeVersion: number;
orderedCandidates: RouteSnapshotCandidate[];
strategy: ModelRoute["strategy"];
totalRetryBudget: number;
};
export type PricingSnapshot = {
ruleId: string;
ruleVersion: number;
publicModelId: string;
resolutionId?: string;
unitPrice: number;
quantity: number;
billingUnit: PricingRule["billingUnit"];
multiplier: number;
balanceUnitVersion: number;
};
export type ModelSnapshot = {
publicModelId: string;
modelVersion: number;
name: string;
tier: PublicModelProduct["tier"];
capabilities: ModelCapability[];
capabilityParameters?: CapabilityParameters;
resolutionId?: string;
resolution?: PublicResolution;
};
export type PlanSnapshot = {
planId?: string;
version?: number;
queuePriority: number;
maxConcurrent: number;
};
export type BalanceSummary = {
@@ -242,18 +349,16 @@ export type GenerationTask = OrganizationScope & {
createdAt: string;
updatedAt: string;
eventSequence: number;
channelGroupId: string;
routeSnapshotVersion: number;
attempts: GenerationAttempt[];
outputs: Array<{ id: string; mimeType: string; url?: string; status: "succeeded" | "failed"; unitPriceSnapshot?: number; chargedAmount?: number; metadata?: ImageOutputMetadata }>;
publicErrorCode?: ErrorCode;
prompt?: string;
resolutionPresetId?: string;
count?: number;
pricingSnapshot?: { basePrice: number; multiplier: number; unitVersion: number };
modelSnapshot?: { publicModelId: string; version: number };
routeSnapshot?: { groupId: string; version: number; channelIds: string[]; retryBudget?: number; channelRetryBudgets?: Record<string, number> };
planSnapshot?: { planId?: string; version?: number; queuePriority: number; maxConcurrent: number };
pricingSnapshot: PricingSnapshot;
modelSnapshot: ModelSnapshot;
routeSnapshot: RouteSnapshot;
planSnapshot: PlanSnapshot;
leaseExpiresAt?: string;
/** Fences a late worker/provider response after a lease is recovered. */
leaseToken?: string;
@@ -405,7 +510,9 @@ export type AdminSessionResponse = {
export type AdminPage<T> = {
items: T[];
total?: number;
total: number;
page?: number;
pageSize?: number;
nextCursor?: string;
};
@@ -413,19 +520,27 @@ export type AdminChannelSummary = {
id: string;
label: string;
enabled: boolean;
priority: number;
health: "healthy" | "degraded" | "open";
health: ChannelHealthStatus | "disabled";
version: number;
retryBudget: number;
consecutiveFailures: number;
providerType?: "generic" | "openai-images";
providerName?: string;
providerModelId?: string;
enabledModelIds?: string[];
models?: ChannelModel[];
modelMappings?: Array<{ displayModelId: string; requestModelId: string }>;
resolutionModelMap?: Record<string, string>;
baseUrl?: string;
secretConfigured?: boolean;
capacity?: CapacitySnapshot;
circuitResetAt?: string;
groupIds?: string[];
samples?: Array<ChannelHealthSample | null>;
};
export type AdminChannelGroupSummary = {
id: string;
name?: string;
enabled: boolean;
version: number;
routes: ModelRoute[];
};
export type AdminTaskSummary = {