From d536e451fd1a194ded66194c40b97721dca9c2f1 Mon Sep 17 00:00:00 2001 From: Alex Promakh Date: Mon, 19 Jan 2026 22:43:08 +0000 Subject: [PATCH] feat: for openrouter session uses cost from metadata --- packages/opencode/src/session/index.ts | 73 ++++++++++++++++++++------ 1 file changed, 58 insertions(+), 15 deletions(-) diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 3fcdab5238c..275dd9682ef 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -20,6 +20,7 @@ import { Command } from "../command" import { Snapshot } from "@/snapshot" import type { Provider } from "@/provider/provider" +import type { JSONObject } from "@ai-sdk/provider" import { PermissionNext } from "@/permission/next" import { Global } from "@/global" @@ -409,6 +410,58 @@ export namespace Session { return part }) + function extractProviderCost(metadata?: ProviderMetadata): number | null { + if (!metadata) return null + + if (typeof metadata.openrouter?.usage == "object") { + const usage = metadata.openrouter?.usage as JSONObject; + if (typeof usage.cost == "number") { + return usage.cost; + } + } + + return null + } + + type UsageTokens = { + input: number; + output: number; + reasoning: number; + cache: { + write: number; + read: number; + }; + } + + const getCost = fn(z.object({ + model: z.custom(), + tokens: z.custom(), + metadata: z.custom().optional(), + }), (input) => { + const {tokens, metadata} = input; + const providerCost = extractProviderCost(metadata); + if (providerCost) { + return providerCost; + } + + // Approximated cost + const costInfo = + input.model.cost?.experimentalOver200K && tokens.input + tokens.cache.read > 200_000 + ? input.model.cost.experimentalOver200K + : input.model.cost; + const appoximatedCost = new Decimal(0) + .add(new Decimal(tokens.input).mul(costInfo?.input ?? 0).div(1_000_000)) + .add(new Decimal(tokens.output).mul(costInfo?.output ?? 0).div(1_000_000)) + .add(new Decimal(tokens.cache.read).mul(costInfo?.cache?.read ?? 0).div(1_000_000)) + .add(new Decimal(tokens.cache.write).mul(costInfo?.cache?.write ?? 0).div(1_000_000)) + // TODO: update models.dev to have better pricing model, for now: + // charge reasoning tokens at the same rate as output tokens + .add(new Decimal(tokens.reasoning).mul(costInfo?.output ?? 0).div(1_000_000)) + .toNumber(); + + return Number.isFinite(appoximatedCost) ? appoximatedCost : 0; + }) + export const getUsage = fn( z.object({ model: z.custom(), @@ -441,23 +494,13 @@ export namespace Session { }, } - const costInfo = - input.model.cost?.experimentalOver200K && tokens.input + tokens.cache.read > 200_000 - ? input.model.cost.experimentalOver200K - : input.model.cost return { - cost: safe( - new Decimal(0) - .add(new Decimal(tokens.input).mul(costInfo?.input ?? 0).div(1_000_000)) - .add(new Decimal(tokens.output).mul(costInfo?.output ?? 0).div(1_000_000)) - .add(new Decimal(tokens.cache.read).mul(costInfo?.cache?.read ?? 0).div(1_000_000)) - .add(new Decimal(tokens.cache.write).mul(costInfo?.cache?.write ?? 0).div(1_000_000)) - // TODO: update models.dev to have better pricing model, for now: - // charge reasoning tokens at the same rate as output tokens - .add(new Decimal(tokens.reasoning).mul(costInfo?.output ?? 0).div(1_000_000)) - .toNumber(), - ), tokens, + cost: getCost({ + model: input.model, + tokens: tokens, + metadata: input.metadata + }), } }, )