mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-12 04:09:48 +08:00
feat(pricing): support processing tier multipliers
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user