feat: add app-owned Copilot and Grok subscription connections
This commit is contained in:
+14
-4
@@ -1,12 +1,14 @@
|
||||
/** Client-safe accounting folds. Rates and calculation live on the server,
|
||||
* using Pi's catalog and calculateCost; no copied pricing table lives here. */
|
||||
import type { CallUsage, ChatRecord, JobRecord, TurnUsage } from "./contracts.ts";
|
||||
import { NATIVE_SUBSCRIPTIONS } from "./contracts/subscriptions.ts";
|
||||
|
||||
export type CallPricer = (usage: Partial<CallUsage>, model: string) => number | null;
|
||||
export interface SessionPricing {
|
||||
cost: number;
|
||||
unpriced: number;
|
||||
subscription?: number;
|
||||
subscriptionProviders?: string[];
|
||||
/** Derived at read time, keyed by the persisted turn's id. Never stored. */
|
||||
turnCosts: Record<string, number | null>;
|
||||
}
|
||||
@@ -14,6 +16,14 @@ export interface SessionPricing {
|
||||
export function isChatGPTPlan(model: string | undefined): boolean {
|
||||
return model?.startsWith("chatgpt:") === true;
|
||||
}
|
||||
export function subscriptionLabel(model: string | undefined): string | undefined {
|
||||
if (isChatGPTPlan(model)) return "ChatGPT";
|
||||
return NATIVE_SUBSCRIPTIONS.find((p) => model?.startsWith(`${p.logicalId}:`))?.label;
|
||||
}
|
||||
export function isSubscriptionModel(model: string | undefined): boolean { return subscriptionLabel(model) !== undefined; }
|
||||
export function subscriptionLabels(models: readonly (string | undefined)[]): string[] {
|
||||
return [...new Set(models.flatMap((model) => { const label = subscriptionLabel(model); return label ? [label] : []; }))];
|
||||
}
|
||||
|
||||
/** Absent stays absent. Long-cache precision is unknown if any write-bearing
|
||||
* step lacks the split; never silently turn that missing detail into zero. */
|
||||
@@ -32,7 +42,7 @@ export function sumSteps(steps: readonly CallUsage[]): CallUsage {
|
||||
}
|
||||
|
||||
export function turnCost(turn: Pick<TurnUsage, "model" | "steps">, price: CallPricer): number | null {
|
||||
if (isChatGPTPlan(turn.model) || turn.steps.length === 0) return null;
|
||||
if (isSubscriptionModel(turn.model) || turn.steps.length === 0) return null;
|
||||
let total = 0;
|
||||
for (const step of turn.steps) {
|
||||
const cost = price(step, turn.model);
|
||||
@@ -50,16 +60,16 @@ export function sessionCost(record: Pick<ChatRecord, "turns">, price: CallPricer
|
||||
for (const turn of record.turns ?? []) {
|
||||
const dollars = turnCost(turn, price);
|
||||
turnCosts[turn.id] = dollars;
|
||||
if (isChatGPTPlan(turn.model)) subscription++;
|
||||
if (isSubscriptionModel(turn.model)) subscription++;
|
||||
else if (dollars === null) unpriced++;
|
||||
else cost += dollars;
|
||||
}
|
||||
return { cost, unpriced, ...(subscription ? { subscription } : {}), turnCosts };
|
||||
return { cost, unpriced, ...(subscription ? { subscription, subscriptionProviders: subscriptionLabels((record.turns ?? []).map((turn) => turn.model)) } : {}), turnCosts };
|
||||
}
|
||||
|
||||
export function jobCost(job: Pick<JobRecord, "usage">, price: CallPricer): number | null {
|
||||
const usage = job.usage;
|
||||
if (!usage || isChatGPTPlan(usage.model)) return null;
|
||||
if (!usage || isSubscriptionModel(usage.model)) return null;
|
||||
if (usage.calls && usage.calls.length === usage.steps) {
|
||||
return turnCost({ model: usage.model, steps: usage.calls }, price);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user