feat(pricing): support processing tier multipliers

This commit is contained in:
elky
2026-07-16 23:30:42 +08:00
parent 312583f055
commit 0be380243b
40 changed files with 5012 additions and 565 deletions
@@ -1,4 +1,9 @@
import type { ModelCreate, ModelUpdate, TieredPricingConfig } from '@/api/endpoints'
import type {
ModelCreate,
ModelUpdate,
ProviderTieredPricingConfig,
TieredPricingConfig,
} from '@/api/endpoints'
interface EmbeddingMetadataCarrier {
supported_capabilities?: string[] | null
@@ -15,9 +20,10 @@ function isEmbeddingApiFormat(format: unknown): boolean {
export interface ProviderModelCreatePayloadInput {
globalModelId: string
providerModelName: string
finalTieredPricing: TieredPricingConfig | null
finalTieredPricing: ProviderTieredPricingConfig | null
tieredPricingModified: boolean
pricePerRequest?: number
pricePerRequestModified: boolean
cleanConfig?: Record<string, unknown>
configTouched: boolean
supportsVision?: boolean
@@ -29,9 +35,12 @@ export interface ProviderModelCreatePayloadInput {
}
export interface ProviderModelUpdatePayloadInput {
finalTieredPricing: TieredPricingConfig | null
finalTieredPricing: ProviderTieredPricingConfig | null
tieredPricingModified: boolean
pricePerRequest?: number
pricePerRequestModified: boolean
cleanConfig?: Record<string, unknown>
configTouched: boolean
supportsVision?: boolean
supportsFunctionCalling?: boolean
supportsStreaming?: boolean
@@ -53,12 +62,180 @@ export function modelSupportsEmbedding(model: EmbeddingMetadataCarrier | null |
|| (Array.isArray(config.api_formats) && config.api_formats.some(isEmbeddingApiFormat))
}
const STANDARD_PRICING_KEYS = new Set([
'tiers',
'image_output_prices',
'image_output_price_default',
'image_output_price_ranges',
'image_output_price_per_image',
'image_output_price_matrix',
'image_prices',
])
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value)
}
function cloneJson<T>(value: T): T {
return JSON.parse(JSON.stringify(value)) as T
}
function hasOwn(object: object, key: PropertyKey): boolean {
return Object.prototype.hasOwnProperty.call(object, key)
}
function valueHasEntries(value: unknown): boolean {
return (Array.isArray(value) && value.length > 0)
|| (isRecord(value) && Object.keys(value).length > 0)
}
function hasStandardPricingData(pricing: ProviderTieredPricingConfig): boolean {
return (Array.isArray(pricing.tiers) && pricing.tiers.length > 0)
|| (typeof pricing.image_output_price_default === 'number'
&& Number.isFinite(pricing.image_output_price_default))
|| [
'image_output_prices',
'image_output_price_ranges',
'image_output_price_per_image',
'image_output_price_matrix',
'image_prices',
].some(key => valueHasEntries(pricing[key]))
}
function pricingRoot(pricing: ProviderTieredPricingConfig): Record<string, unknown> {
return Object.fromEntries(
Object.entries(pricing).filter(([key]) => key !== 'processing_tiers'),
)
}
function processingTierEntries(pricing: ProviderTieredPricingConfig | null | undefined) {
return isRecord(pricing?.processing_tiers)
? Object.entries(pricing.processing_tiers)
: []
}
function jsonValuesEqual(left: unknown, right: unknown): boolean {
if (Object.is(left, right)) return true
if (Array.isArray(left) || Array.isArray(right)) {
return Array.isArray(left)
&& Array.isArray(right)
&& left.length === right.length
&& left.every((value, index) => jsonValuesEqual(value, right[index]))
}
if (!isRecord(left) || !isRecord(right)) return false
const leftKeys = Object.keys(left).sort()
const rightKeys = Object.keys(right).sort()
return leftKeys.length === rightKeys.length
&& leftKeys.every((key, index) => (
key === rightKeys[index]
&& jsonValuesEqual(left[key], right[key])
))
}
/**
* Build the complete catalog shown by the editor from the two independent
* runtime sources: GlobalModel Standard/default overlays and Provider overrides.
*/
export function mergeProviderTieredPricingForEditing(
globalDefault: ProviderTieredPricingConfig | null | undefined,
providerOverride: ProviderTieredPricingConfig | null | undefined,
): TieredPricingConfig | null {
if (!providerOverride) {
return Array.isArray(globalDefault?.tiers)
? cloneJson(globalDefault) as TieredPricingConfig
: null
}
const providerHasStandard = hasStandardPricingData(providerOverride)
const providerRoot = pricingRoot(providerOverride)
let mergedRoot: Record<string, unknown>
if (providerHasStandard) {
mergedRoot = providerRoot
} else if (globalDefault) {
const providerMetadata = Object.fromEntries(
Object.entries(providerRoot).filter(([key]) => !STANDARD_PRICING_KEYS.has(key)),
)
mergedRoot = {
...pricingRoot(globalDefault),
...providerMetadata,
}
} else {
return null
}
if (!Array.isArray(mergedRoot.tiers)) return null
const mergedProcessingTiers = Object.fromEntries([
...processingTierEntries(globalDefault),
...processingTierEntries(providerOverride),
])
if (Object.keys(mergedProcessingTiers).length > 0) {
mergedRoot.processing_tiers = mergedProcessingTiers
}
return cloneJson(mergedRoot) as TieredPricingConfig
}
/**
* Project the complete editor catalog back to the smallest Provider override.
* Unchanged Standard and processing-tier values keep inheriting from GlobalModel.
*/
export function buildProviderTieredPricingOverride(
finalPricing: TieredPricingConfig | null,
originalEditorPricing: TieredPricingConfig | null,
originalProviderOverride: ProviderTieredPricingConfig | null | undefined,
): ProviderTieredPricingConfig | null {
if (!finalPricing) return null
if (!originalEditorPricing) return cloneJson(finalPricing)
const originalProcessingTiers = originalProviderOverride?.processing_tiers
const preservedProcessingTiers = hasOwn(originalProviderOverride || {}, 'processing_tiers')
? originalProcessingTiers === undefined
? undefined
: cloneJson(originalProcessingTiers)
: undefined
let result = cloneJson(originalProviderOverride || {})
if (!jsonValuesEqual(pricingRoot(finalPricing), pricingRoot(originalEditorPricing))) {
result = cloneJson(pricingRoot(finalPricing)) as ProviderTieredPricingConfig
if (preservedProcessingTiers !== undefined || originalProcessingTiers === null) {
result.processing_tiers = preservedProcessingTiers ?? null
}
}
const finalProcessingTiers = new Map(processingTierEntries(finalPricing))
const baselineProcessingTiers = new Map(processingTierEntries(originalEditorPricing))
const nextProcessingTiers = new Map(processingTierEntries(result))
let processingTiersChanged = false
const keys = new Set([
...finalProcessingTiers.keys(),
...baselineProcessingTiers.keys(),
])
for (const key of keys) {
const finalOverlay = finalProcessingTiers.get(key)
const baselineOverlay = baselineProcessingTiers.get(key)
if (jsonValuesEqual(finalOverlay, baselineOverlay)) continue
processingTiersChanged = true
if (finalOverlay === undefined) nextProcessingTiers.delete(key)
else nextProcessingTiers.set(key, cloneJson(finalOverlay))
}
if (processingTiersChanged) {
if (nextProcessingTiers.size > 0) {
result.processing_tiers = Object.fromEntries(nextProcessingTiers)
} else {
delete result.processing_tiers
}
}
return Object.keys(result).length > 0 ? result : null
}
export function buildProviderModelCreatePayload(input: ProviderModelCreatePayloadInput): ModelCreate {
return {
global_model_id: input.globalModelId,
provider_model_name: input.providerModelName,
tiered_pricing: input.tieredPricingModified && input.finalTieredPricing ? input.finalTieredPricing : undefined,
price_per_request: input.pricePerRequest,
price_per_request: input.pricePerRequestModified ? input.pricePerRequest : undefined,
config: input.configTouched ? input.cleanConfig : undefined,
supports_vision: input.supportsVision,
supports_function_calling: input.supportsFunctionCalling,
@@ -71,9 +248,11 @@ export function buildProviderModelCreatePayload(input: ProviderModelCreatePayloa
export function buildProviderModelUpdatePayload(input: ProviderModelUpdatePayloadInput): ModelUpdate {
return {
tiered_pricing: input.finalTieredPricing,
price_per_request: input.pricePerRequest ?? null,
config: input.cleanConfig || null,
...(input.tieredPricingModified ? { tiered_pricing: input.finalTieredPricing } : {}),
...(input.pricePerRequestModified
? { price_per_request: input.pricePerRequest ?? null }
: {}),
...(input.configTouched ? { config: input.cleanConfig || null } : {}),
supports_vision: input.supportsVision,
supports_function_calling: input.supportsFunctionCalling,
supports_streaming: input.supportsStreaming,