| 1 | import type { |
| 2 | EnvironmentId, |
| 3 | ServerSettingsPatch, |
| 4 | UsageModelPriceOverride, |
| 5 | } from "@t3tools/contracts"; |
| 6 | |
| 7 | export interface UsagePriceTarget { |
| 8 | readonly environmentId: EnvironmentId; |
| 9 | readonly label: string; |
| 10 | readonly prices: Readonly<Record<string, UsageModelPriceOverride>> | null; |
| 11 | /** Model mappings. `null` when they are not loaded or the server cannot store them. */ |
| 12 | readonly aliases: Readonly<Record<string, string>> | null; |
| 13 | readonly unavailable: string | null; |
| 14 | } |
| 15 | |
| 16 | /** `null` removes the model's custom price or mapping. */ |
| 17 | export type UsagePriceChange = |
| 18 | | { readonly model: string; readonly price: UsageModelPriceOverride | null } |
| 19 | | { readonly model: string; readonly alias: string | null }; |
| 20 | |
| 21 | export type UsagePriceWriteResult = |
| 22 | | { readonly status: "saved" } |
| 23 | | { readonly status: "failed"; readonly error: string }; |
| 24 | |
| 25 | /** |
| 26 | * Each destination settles independently; retry callers pass only the failed destinations. |
| 27 | * Prices and mappings for one destination go out in a single patch. |
| 28 | */ |
| 29 | export async function writeUsagePrices(input: { |
| 30 | readonly targets: readonly UsagePriceTarget[]; |
| 31 | readonly changes: ReadonlyMap<EnvironmentId, readonly UsagePriceChange[]>; |
| 32 | readonly write: (input: { |
| 33 | environmentId: EnvironmentId; |
| 34 | input: { patch: ServerSettingsPatch }; |
| 35 | }) => Promise<{ readonly _tag: "Success" | "Failure" }>; |
| 36 | readonly onResult: (environmentId: EnvironmentId, result: UsagePriceWriteResult) => void; |
| 37 | }) { |
| 38 | await Promise.all( |
| 39 | input.targets.map(async (target) => { |
| 40 | let result: UsagePriceWriteResult; |
| 41 | const changes = input.changes.get(target.environmentId) ?? []; |
| 42 | const prices = changes.flatMap((change) => |
| 43 | "price" in change ? [[change.model, change.price] as const] : [], |
| 44 | ); |
| 45 | const aliases = changes.flatMap((change) => |
| 46 | "alias" in change ? [[change.model, change.alias] as const] : [], |
| 47 | ); |
| 48 | if (target.unavailable !== null) { |
| 49 | result = { status: "failed", error: target.unavailable }; |
| 50 | } else if (aliases.length > 0 && target.aliases === null) { |
| 51 | result = { status: "failed", error: "Update server to map models" }; |
| 52 | } else { |
| 53 | try { |
| 54 | const saved = |
| 55 | changes.length === 0 |
| 56 | ? { _tag: "Success" as const } |
| 57 | : await input.write({ |
| 58 | environmentId: target.environmentId, |
| 59 | input: { |
| 60 | patch: { |
| 61 | ...(prices.length > 0 |
| 62 | ? { usagePriceOverrides: Object.fromEntries(prices) } |
| 63 | : {}), |
| 64 | ...(aliases.length > 0 |
| 65 | ? { usageModelAliases: Object.fromEntries(aliases) } |
| 66 | : {}), |
| 67 | }, |
| 68 | }, |
| 69 | }); |
| 70 | result = |
| 71 | saved._tag === "Success" |
| 72 | ? { status: "saved" } |
| 73 | : { status: "failed", error: "Could not save. Try again." }; |
| 74 | } catch { |
| 75 | result = { status: "failed", error: "Could not save. Try again." }; |
| 76 | } |
| 77 | } |
| 78 | input.onResult(target.environmentId, result); |
| 79 | }), |
| 80 | ); |
| 81 | } |