summaryrefslogtreecommitdiffhomepage
path: root/packages/cloud/app/src/routes/zen
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cloud/app/src/routes/zen')
-rw-r--r--packages/cloud/app/src/routes/zen/handler.ts594
-rw-r--r--packages/cloud/app/src/routes/zen/v1/chat/completions.ts54
-rw-r--r--packages/cloud/app/src/routes/zen/v1/messages.ts61
-rw-r--r--packages/cloud/app/src/routes/zen/v1/responses.ts52
4 files changed, 0 insertions, 761 deletions
diff --git a/packages/cloud/app/src/routes/zen/handler.ts b/packages/cloud/app/src/routes/zen/handler.ts
deleted file mode 100644
index ab1fc6599..000000000
--- a/packages/cloud/app/src/routes/zen/handler.ts
+++ /dev/null
@@ -1,594 +0,0 @@
-import type { APIEvent } from "@solidjs/start/server"
-import path from "node:path"
-import { and, Database, eq, isNull, lt, or, sql } from "@opencode/cloud-core/drizzle/index.js"
-import { KeyTable } from "@opencode/cloud-core/schema/key.sql.js"
-import { BillingTable, PaymentTable, UsageTable } from "@opencode/cloud-core/schema/billing.sql.js"
-import { centsToMicroCents } from "@opencode/cloud-core/util/price.js"
-import { Identifier } from "@opencode/cloud-core/identifier.js"
-import { Resource } from "@opencode/cloud-resource"
-import { Billing } from "../../../../core/src/billing"
-import { Actor } from "@opencode/cloud-core/actor.js"
-
-type ModelCost = {
- input: number
- output: number
- cacheRead?: number
- cacheWrite5m?: number
- cacheWrite1h?: number
-}
-
-type Model = {
- id: string
- auth: boolean
- cost: ModelCost | ((usage: any) => ModelCost)
- headerMappings: Record<string, string>
- providers: Record<
- string,
- {
- api: string
- apiKey: string
- model: string
- weight?: number
- }
- >
-}
-
-export async function handler(
- input: APIEvent,
- opts: {
- modifyBody?: (body: any) => any
- setAuthHeader: (headers: Headers, apiKey: string) => void
- parseApiKey: (headers: Headers) => string | undefined
- onStreamPart: (chunk: string) => void
- getStreamUsage: () => any
- normalizeUsage: (body: any) => {
- inputTokens: number
- outputTokens: number
- reasoningTokens?: number
- cacheReadTokens?: number
- cacheWrite5mTokens?: number
- cacheWrite1hTokens?: number
- }
- },
-) {
- class AuthError extends Error {}
- class CreditsError extends Error {}
- class MonthlyLimitError extends Error {}
- class ModelError extends Error {}
-
- const MODELS: Record<string, Model> = {
- "claude-opus-4-1": {
- id: "claude-opus-4-1" as const,
- auth: true,
- cost: {
- input: 0.000015,
- output: 0.000075,
- cacheRead: 0.0000015,
- cacheWrite5m: 0.00001875,
- cacheWrite1h: 0.00003,
- },
- headerMappings: {},
- providers: {
- anthropic: {
- api: "https://api.anthropic.com",
- apiKey: Resource.ANTHROPIC_API_KEY.value,
- model: "claude-opus-4-1-20250805",
- },
- },
- },
- "claude-sonnet-4": {
- id: "claude-sonnet-4" as const,
- auth: true,
- cost: (usage: any) => {
- const totalInputTokens =
- usage.inputTokens + usage.cacheReadTokens + usage.cacheWrite5mTokens + usage.cacheWrite1hTokens
- return totalInputTokens <= 200_000
- ? {
- input: 0.000003,
- output: 0.000015,
- cacheRead: 0.0000003,
- cacheWrite5m: 0.00000375,
- cacheWrite1h: 0.000006,
- }
- : {
- input: 0.000006,
- output: 0.0000225,
- cacheRead: 0.0000006,
- cacheWrite5m: 0.0000075,
- cacheWrite1h: 0.000012,
- }
- },
- headerMappings: {},
- providers: {
- anthropic: {
- api: "https://api.anthropic.com",
- apiKey: Resource.ANTHROPIC_API_KEY.value,
- model: "claude-sonnet-4-20250514",
- },
- },
- },
- "claude-3-5-haiku": {
- id: "claude-3-5-haiku" as const,
- auth: true,
- cost: {
- input: 0.0000008,
- output: 0.000004,
- cacheRead: 0.00000008,
- cacheWrite5m: 0.000001,
- cacheWrite1h: 0.0000016,
- },
- headerMappings: {},
- providers: {
- anthropic: {
- api: "https://api.anthropic.com",
- apiKey: Resource.ANTHROPIC_API_KEY.value,
- model: "claude-3-5-haiku-20241022",
- },
- },
- },
- "gpt-5": {
- id: "gpt-5" as const,
- auth: true,
- cost: {
- input: 0.00000125,
- output: 0.00001,
- cacheRead: 0.000000125,
- },
- headerMappings: {},
- providers: {
- openai: {
- api: "https://api.openai.com",
- apiKey: Resource.OPENAI_API_KEY.value,
- model: "gpt-5",
- },
- },
- },
- "qwen3-coder": {
- id: "qwen3-coder" as const,
- auth: true,
- cost: {
- input: 0.00000045,
- output: 0.0000018,
- },
- headerMappings: {},
- providers: {
- baseten: {
- api: "https://inference.baseten.co",
- apiKey: Resource.BASETEN_API_KEY.value,
- model: "Qwen/Qwen3-Coder-480B-A35B-Instruct",
- weight: 4,
- },
- fireworks: {
- api: "https://api.fireworks.ai/inference",
- apiKey: Resource.FIREWORKS_API_KEY.value,
- model: "accounts/fireworks/models/qwen3-coder-480b-a35b-instruct",
- weight: 1,
- },
- },
- },
- "kimi-k2": {
- id: "kimi-k2" as const,
- auth: true,
- cost: {
- input: 0.0000006,
- output: 0.0000025,
- },
- headerMappings: {},
- providers: {
- baseten: {
- api: "https://inference.baseten.co",
- apiKey: Resource.BASETEN_API_KEY.value,
- model: "moonshotai/Kimi-K2-Instruct-0905",
- //weight: 4,
- },
- //fireworks: {
- // api: "https://api.fireworks.ai/inference",
- // apiKey: Resource.FIREWORKS_API_KEY.value,
- // model: "accounts/fireworks/models/kimi-k2-instruct-0905",
- // weight: 1,
- //},
- },
- },
- "grok-code": {
- id: "grok-code" as const,
- auth: false,
- cost: {
- input: 0,
- output: 0,
- cacheRead: 0,
- },
- headerMappings: {
- "x-grok-conv-id": "x-opencode-session",
- "x-grok-req-id": "x-opencode-request",
- },
- providers: {
- xai: {
- api: "https://api.x.ai",
- apiKey: Resource.XAI_API_KEY.value,
- model: "grok-code",
- },
- },
- },
- // deprecated
- "qwen/qwen3-coder": {
- id: "qwen/qwen3-coder" as const,
- auth: true,
- cost: {
- input: 0.00000038,
- output: 0.00000153,
- },
- headerMappings: {},
- providers: {
- baseten: {
- api: "https://inference.baseten.co",
- apiKey: Resource.BASETEN_API_KEY.value,
- model: "Qwen/Qwen3-Coder-480B-A35B-Instruct",
- weight: 5,
- },
- fireworks: {
- api: "https://api.fireworks.ai/inference",
- apiKey: Resource.FIREWORKS_API_KEY.value,
- model: "accounts/fireworks/models/qwen3-coder-480b-a35b-instruct",
- weight: 1,
- },
- },
- },
- }
-
- const FREE_WORKSPACES = [
- "wrk_01K46JDFR0E75SG2Q8K172KF3Y", // frank
- ]
-
- const logger = {
- metric: (values: Record<string, any>) => {
- console.log(`_metric:${JSON.stringify(values)}`)
- },
- log: console.log,
- debug: (message: string) => {
- if (Resource.App.stage === "production") return
- console.debug(message)
- },
- }
-
- try {
- const url = new URL(input.request.url)
- const body = await input.request.json()
- logger.debug(JSON.stringify(body))
- logger.metric({
- is_tream: !!body.stream,
- session: input.request.headers.get("x-opencode-session"),
- request: input.request.headers.get("x-opencode-request"),
- })
- const MODEL = validateModel()
- const apiKey = await authenticate()
- const isFree = FREE_WORKSPACES.includes(apiKey?.workspaceID ?? "")
- await checkCreditsAndLimit()
- const providerName = selectProvider()
- const providerData = MODEL.providers[providerName]
- logger.metric({ provider: providerName })
-
- // Request to model provider
- const startTimestamp = Date.now()
- const res = await fetch(path.posix.join(providerData.api, url.pathname.replace(/^\/zen/, "") + url.search), {
- method: "POST",
- headers: (() => {
- const headers = input.request.headers
- headers.delete("host")
- headers.delete("content-length")
- opts.setAuthHeader(headers, providerData.apiKey)
- Object.entries(MODEL.headerMappings ?? {}).forEach(([k, v]) => {
- headers.set(k, headers.get(v)!)
- })
- return headers
- })(),
- body: JSON.stringify({
- ...(opts.modifyBody?.(body) ?? body),
- model: providerData.model,
- }),
- })
-
- // Scrub response headers
- const resHeaders = new Headers()
- const keepHeaders = ["content-type", "cache-control"]
- for (const [k, v] of res.headers.entries()) {
- if (keepHeaders.includes(k.toLowerCase())) {
- resHeaders.set(k, v)
- }
- }
-
- // Handle non-streaming response
- if (!body.stream) {
- const json = await res.json()
- const body = JSON.stringify(json)
- logger.metric({ response_length: body.length })
- logger.debug(body)
- await trackUsage(json.usage)
- await reload()
- return new Response(body, {
- status: res.status,
- statusText: res.statusText,
- headers: resHeaders,
- })
- }
-
- // Handle streaming response
- const stream = new ReadableStream({
- start(c) {
- const reader = res.body?.getReader()
- const decoder = new TextDecoder()
- let buffer = ""
- let responseLength = 0
-
- function pump(): Promise<void> {
- return (
- reader?.read().then(async ({ done, value }) => {
- if (done) {
- logger.metric({ response_length: responseLength })
- const usage = opts.getStreamUsage()
- if (usage) {
- await trackUsage(usage)
- await reload()
- }
- c.close()
- return
- }
-
- if (responseLength === 0) {
- logger.metric({ time_to_first_byte: Date.now() - startTimestamp })
- }
- responseLength += value.length
- buffer += decoder.decode(value, { stream: true })
-
- const parts = buffer.split("\n\n")
- buffer = parts.pop() ?? ""
-
- for (const part of parts) {
- logger.debug(part)
- opts.onStreamPart(part.trim())
- }
-
- c.enqueue(value)
-
- return pump()
- }) || Promise.resolve()
- )
- }
-
- return pump()
- },
- })
-
- return new Response(stream, {
- status: res.status,
- statusText: res.statusText,
- headers: resHeaders,
- })
-
- function validateModel() {
- if (!(body.model in MODELS)) {
- throw new ModelError(`Model ${body.model} not supported`)
- }
- const model = MODELS[body.model as keyof typeof MODELS]
- logger.metric({ model: model.id })
- return model
- }
-
- async function authenticate() {
- try {
- const apiKey = opts.parseApiKey(input.request.headers)
- if (!apiKey) throw new AuthError("Missing API key.")
-
- const key = await Database.use((tx) =>
- tx
- .select({
- id: KeyTable.id,
- workspaceID: KeyTable.workspaceID,
- })
- .from(KeyTable)
- .where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted)))
- .then((rows) => rows[0]),
- )
-
- if (!key) throw new AuthError("Invalid API key.")
- logger.metric({
- api_key: key.id,
- workspace: key.workspaceID,
- })
- return key
- } catch (e) {
- // ignore error if model does not require authentication
- if (!MODEL.auth) return
- throw e
- }
- }
-
- async function checkCreditsAndLimit() {
- if (!apiKey || !MODEL.auth || isFree) return
-
- const billing = await Database.use((tx) =>
- tx
- .select({
- balance: BillingTable.balance,
- paymentMethodID: BillingTable.paymentMethodID,
- monthlyLimit: BillingTable.monthlyLimit,
- monthlyUsage: BillingTable.monthlyUsage,
- timeMonthlyUsageUpdated: BillingTable.timeMonthlyUsageUpdated,
- })
- .from(BillingTable)
- .where(eq(BillingTable.workspaceID, apiKey.workspaceID))
- .then((rows) => rows[0]),
- )
-
- if (!billing.paymentMethodID) throw new CreditsError("No payment method")
- if (billing.balance <= 0) throw new CreditsError("Insufficient balance")
- if (
- billing.monthlyLimit &&
- billing.monthlyUsage &&
- billing.timeMonthlyUsageUpdated &&
- billing.monthlyUsage >= centsToMicroCents(billing.monthlyLimit * 100)
- ) {
- const now = new Date()
- const currentYear = now.getUTCFullYear()
- const currentMonth = now.getUTCMonth()
- const dateYear = billing.timeMonthlyUsageUpdated.getUTCFullYear()
- const dateMonth = billing.timeMonthlyUsageUpdated.getUTCMonth()
- if (currentYear === dateYear && currentMonth === dateMonth)
- throw new MonthlyLimitError(`You have reached your monthly spending limit of $${billing.monthlyLimit}.`)
- }
- }
-
- function selectProvider() {
- const picks = Object.entries(MODEL.providers).flatMap(([name, provider]) =>
- Array<string>(provider.weight ?? 1).fill(name),
- )
- return picks[Math.floor(Math.random() * picks.length)]
- }
-
- async function trackUsage(usage: any) {
- const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } =
- opts.normalizeUsage(usage)
-
- const modelCost = typeof MODEL.cost === "function" ? MODEL.cost(usage) : MODEL.cost
-
- const inputCost = modelCost.input * inputTokens * 100
- const outputCost = modelCost.output * outputTokens * 100
- const reasoningCost = (() => {
- if (!reasoningTokens) return undefined
- return modelCost.output * reasoningTokens * 100
- })()
- const cacheReadCost = (() => {
- if (!cacheReadTokens) return undefined
- if (!modelCost.cacheRead) return undefined
- return modelCost.cacheRead * cacheReadTokens * 100
- })()
- const cacheWrite5mCost = (() => {
- if (!cacheWrite5mTokens) return undefined
- if (!modelCost.cacheWrite5m) return undefined
- return modelCost.cacheWrite5m * cacheWrite5mTokens * 100
- })()
- const cacheWrite1hCost = (() => {
- if (!cacheWrite1hTokens) return undefined
- if (!modelCost.cacheWrite1h) return undefined
- return modelCost.cacheWrite1h * cacheWrite1hTokens * 100
- })()
- const totalCostInCent =
- inputCost +
- outputCost +
- (reasoningCost ?? 0) +
- (cacheReadCost ?? 0) +
- (cacheWrite5mCost ?? 0) +
- (cacheWrite1hCost ?? 0)
-
- logger.metric({
- "tokens.input": inputTokens,
- "tokens.output": outputTokens,
- "tokens.reasoning": reasoningTokens,
- "tokens.cache_read": cacheReadTokens,
- "tokens.cache_write_5m": cacheWrite5mTokens,
- "tokens.cache_write_1h": cacheWrite1hTokens,
- "cost.input": Math.round(inputCost),
- "cost.output": Math.round(outputCost),
- "cost.reasoning": reasoningCost ? Math.round(reasoningCost) : undefined,
- "cost.cache_read": cacheReadCost ? Math.round(cacheReadCost) : undefined,
- "cost.cache_write_5m": cacheWrite5mCost ? Math.round(cacheWrite5mCost) : undefined,
- "cost.cache_write_1h": cacheWrite1hCost ? Math.round(cacheWrite1hCost) : undefined,
- "cost.total": Math.round(totalCostInCent),
- })
-
- if (!apiKey) return
-
- const cost = isFree ? 0 : centsToMicroCents(totalCostInCent)
- await Database.transaction(async (tx) => {
- await tx.insert(UsageTable).values({
- workspaceID: apiKey.workspaceID,
- id: Identifier.create("usage"),
- model: MODEL.id,
- provider: providerName,
- inputTokens,
- outputTokens,
- reasoningTokens,
- cacheReadTokens,
- cacheWrite5mTokens,
- cacheWrite1hTokens,
- cost,
- })
- await tx
- .update(BillingTable)
- .set({
- balance: sql`${BillingTable.balance} - ${cost}`,
- monthlyUsage: sql`
- CASE
- WHEN MONTH(${BillingTable.timeMonthlyUsageUpdated}) = MONTH(now()) AND YEAR(${BillingTable.timeMonthlyUsageUpdated}) = YEAR(now()) THEN ${BillingTable.monthlyUsage} + ${cost}
- ELSE ${cost}
- END
- `,
- timeMonthlyUsageUpdated: sql`now()`,
- })
- .where(eq(BillingTable.workspaceID, apiKey.workspaceID))
- })
-
- await Database.use((tx) =>
- tx
- .update(KeyTable)
- .set({ timeUsed: sql`now()` })
- .where(eq(KeyTable.id, apiKey.id)),
- )
- }
-
- async function reload() {
- if (!apiKey) return
-
- const lock = await Database.use((tx) =>
- tx
- .update(BillingTable)
- .set({
- timeReloadLockedTill: sql`now() + interval 1 minute`,
- })
- .where(
- and(
- eq(BillingTable.workspaceID, apiKey.workspaceID),
- eq(BillingTable.reload, true),
- lt(BillingTable.balance, centsToMicroCents(Billing.CHARGE_THRESHOLD)),
- or(isNull(BillingTable.timeReloadLockedTill), lt(BillingTable.timeReloadLockedTill, sql`now()`)),
- ),
- ),
- )
- if (lock.rowsAffected === 0) return
-
- await Actor.provide("system", { workspaceID: apiKey.workspaceID }, async () => {
- await Billing.reload()
- })
- }
- } catch (error: any) {
- logger.metric({
- "error.type": error.constructor.name,
- "error.message": error.message,
- })
-
- // Note: both top level "type" and "error.type" fields are used by the @ai-sdk/anthropic client to render the error message.
- if (
- error instanceof AuthError ||
- error instanceof CreditsError ||
- error instanceof MonthlyLimitError ||
- error instanceof ModelError
- )
- return new Response(
- JSON.stringify({
- type: "error",
- error: { type: error.constructor.name, message: error.message },
- }),
- { status: 401 },
- )
-
- return new Response(
- JSON.stringify({
- type: "error",
- error: {
- type: "error",
- message: error.message,
- },
- }),
- { status: 500 },
- )
- }
-}
diff --git a/packages/cloud/app/src/routes/zen/v1/chat/completions.ts b/packages/cloud/app/src/routes/zen/v1/chat/completions.ts
deleted file mode 100644
index 801557324..000000000
--- a/packages/cloud/app/src/routes/zen/v1/chat/completions.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import type { APIEvent } from "@solidjs/start/server"
-import { handler } from "~/routes/zen/handler"
-
-type Usage = {
- prompt_tokens?: number
- completion_tokens?: number
- total_tokens?: number
- prompt_tokens_details?: {
- text_tokens?: number
- audio_tokens?: number
- image_tokens?: number
- cached_tokens?: number
- }
- completion_tokens_details?: {
- reasoning_tokens?: number
- audio_tokens?: number
- accepted_prediction_tokens?: number
- rejected_prediction_tokens?: number
- }
-}
-
-export function POST(input: APIEvent) {
- let usage: Usage
- return handler(input, {
- modifyBody: (body: any) => ({
- ...body,
- ...(body.stream ? { stream_options: { include_usage: true } } : {}),
- }),
- setAuthHeader: (headers: Headers, apiKey: string) => {
- headers.set("authorization", `Bearer ${apiKey}`)
- },
- parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1],
- onStreamPart: (chunk: string) => {
- if (!chunk.startsWith("data: ")) return
-
- let json
- try {
- json = JSON.parse(chunk.slice(6)) as { usage?: Usage }
- } catch (e) {
- return
- }
-
- if (!json.usage) return
- usage = json.usage
- },
- getStreamUsage: () => usage,
- normalizeUsage: (usage: Usage) => ({
- inputTokens: usage.prompt_tokens ?? 0,
- outputTokens: usage.completion_tokens ?? 0,
- reasoningTokens: usage.completion_tokens_details?.reasoning_tokens ?? undefined,
- cacheReadTokens: usage.prompt_tokens_details?.cached_tokens ?? undefined,
- }),
- })
-}
diff --git a/packages/cloud/app/src/routes/zen/v1/messages.ts b/packages/cloud/app/src/routes/zen/v1/messages.ts
deleted file mode 100644
index 1fd85d5c7..000000000
--- a/packages/cloud/app/src/routes/zen/v1/messages.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import type { APIEvent } from "@solidjs/start/server"
-import { handler } from "~/routes/zen/handler"
-
-type Usage = {
- cache_creation?: {
- ephemeral_5m_input_tokens?: number
- ephemeral_1h_input_tokens?: number
- }
- cache_creation_input_tokens?: number
- cache_read_input_tokens?: number
- input_tokens?: number
- output_tokens?: number
- server_tool_use?: {
- web_search_requests?: number
- }
-}
-
-export function POST(input: APIEvent) {
- let usage: Usage
- return handler(input, {
- modifyBody: (body: any) => ({
- ...body,
- service_tier: "standard_only",
- }),
- setAuthHeader: (headers: Headers, apiKey: string) => headers.set("x-api-key", apiKey),
- parseApiKey: (headers: Headers) => headers.get("x-api-key") ?? undefined,
- onStreamPart: (chunk: string) => {
- const data = chunk.split("\n")[1]
- if (!data.startsWith("data: ")) return
-
- let json
- try {
- json = JSON.parse(data.slice(6)) as { usage?: Usage }
- } catch (e) {
- return
- }
-
- if (!json.usage) return
- usage = {
- ...usage,
- ...json.usage,
- cache_creation: {
- ...usage?.cache_creation,
- ...json.usage.cache_creation,
- },
- server_tool_use: {
- ...usage?.server_tool_use,
- ...json.usage.server_tool_use,
- },
- }
- },
- getStreamUsage: () => usage,
- normalizeUsage: (usage: Usage) => ({
- inputTokens: usage.input_tokens ?? 0,
- outputTokens: usage.output_tokens ?? 0,
- cacheReadTokens: usage.cache_read_input_tokens ?? undefined,
- cacheWrite5mTokens: usage.cache_creation?.ephemeral_5m_input_tokens ?? undefined,
- cacheWrite1hTokens: usage.cache_creation?.ephemeral_1h_input_tokens ?? undefined,
- }),
- })
-}
diff --git a/packages/cloud/app/src/routes/zen/v1/responses.ts b/packages/cloud/app/src/routes/zen/v1/responses.ts
deleted file mode 100644
index 486c129b9..000000000
--- a/packages/cloud/app/src/routes/zen/v1/responses.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import type { APIEvent } from "@solidjs/start/server"
-import { handler } from "~/routes/zen/handler"
-
-type Usage = {
- input_tokens?: number
- input_tokens_details?: {
- cached_tokens?: number
- }
- output_tokens?: number
- output_tokens_details?: {
- reasoning_tokens?: number
- }
- total_tokens?: number
-}
-
-export function POST(input: APIEvent) {
- let usage: Usage
- return handler(input, {
- setAuthHeader: (headers: Headers, apiKey: string) => {
- headers.set("authorization", `Bearer ${apiKey}`)
- },
- parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1],
- onStreamPart: (chunk: string) => {
- const [event, data] = chunk.split("\n")
- if (event !== "event: response.completed") return
- if (!data.startsWith("data: ")) return
-
- let json
- try {
- json = JSON.parse(data.slice(6)) as { response?: { usage?: Usage } }
- } catch (e) {
- return
- }
-
- if (!json.response?.usage) return
- usage = json.response.usage
- },
- getStreamUsage: () => usage,
- normalizeUsage: (usage: Usage) => {
- const inputTokens = usage.input_tokens ?? 0
- const outputTokens = usage.output_tokens ?? 0
- const reasoningTokens = usage.output_tokens_details?.reasoning_tokens ?? undefined
- const cacheReadTokens = usage.input_tokens_details?.cached_tokens ?? undefined
- return {
- inputTokens: inputTokens - (cacheReadTokens ?? 0),
- outputTokens: outputTokens - (reasoningTokens ?? 0),
- reasoningTokens,
- cacheReadTokens,
- }
- },
- })
-}