From f72686a74a9a90d01b2c65e34796aa50b668c63e Mon Sep 17 00:00:00 2001 From: Gab Date: Thu, 9 Apr 2026 22:59:51 +1000 Subject: [PATCH] feat: cherry picks --- packages/tfcode/src/acp/agent.ts | 2 +- .../tfcode/src/cli/cmd/tui/context/exit.tsx | 1 + .../src/cli/cmd/tui/routes/session/header.tsx | 6 +-- .../cli/cmd/tui/routes/session/sidebar.tsx | 6 +-- packages/tfcode/src/session/index.ts | 37 +++++-------------- packages/tfcode/src/tool/read.ts | 2 +- packages/tfcode/src/tool/task.ts | 17 ++++++--- packages/tfcode/src/tool/webfetch.ts | 20 ++++++---- 8 files changed, 39 insertions(+), 52 deletions(-) diff --git a/packages/tfcode/src/acp/agent.ts b/packages/tfcode/src/acp/agent.ts index 2a6bbbb1e..5ee0aa994 100644 --- a/packages/tfcode/src/acp/agent.ts +++ b/packages/tfcode/src/acp/agent.ts @@ -115,7 +115,7 @@ export namespace ACP { sessionUpdate: "usage_update", used, size, - cost: { amount: totalCost, currency: "USD" }, + cost: { amount: Math.round(totalCost), currency: "UoI" }, }, }) .catch((error) => { diff --git a/packages/tfcode/src/cli/cmd/tui/context/exit.tsx b/packages/tfcode/src/cli/cmd/tui/context/exit.tsx index 3ed4ae3d2..236320cf0 100644 --- a/packages/tfcode/src/cli/cmd/tui/context/exit.tsx +++ b/packages/tfcode/src/cli/cmd/tui/context/exit.tsx @@ -53,6 +53,7 @@ export const { use: useExit, provider: ExitProvider } = createSimpleContext({ message: store, }, ) + process.on("SIGHUP", () => exit()) return exit }, }) diff --git a/packages/tfcode/src/cli/cmd/tui/routes/session/header.tsx b/packages/tfcode/src/cli/cmd/tui/routes/session/header.tsx index f64dbe533..1f566baa1 100644 --- a/packages/tfcode/src/cli/cmd/tui/routes/session/header.tsx +++ b/packages/tfcode/src/cli/cmd/tui/routes/session/header.tsx @@ -52,10 +52,8 @@ export function Header() { messages(), sumBy((x) => (x.role === "assistant" ? x.cost : 0)), ) - return new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - }).format(total) + const uoi = Math.round(total) + return uoi.toLocaleString() + " UoI" }) const context = createMemo(() => { diff --git a/packages/tfcode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/tfcode/src/cli/cmd/tui/routes/session/sidebar.tsx index d433e2a92..f25e63872 100644 --- a/packages/tfcode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/tfcode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -42,10 +42,8 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) { const cost = createMemo(() => { const total = messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0) - return new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - }).format(total) + const uoi = Math.round(total) + return uoi.toLocaleString() + " UoI" }) const context = createMemo(() => { diff --git a/packages/tfcode/src/session/index.ts b/packages/tfcode/src/session/index.ts index 7d86caf3d..691b0b04e 100644 --- a/packages/tfcode/src/session/index.ts +++ b/packages/tfcode/src/session/index.ts @@ -31,7 +31,6 @@ import { ModelID, ProviderID } from "@/provider/schema" import { Permission } from "@/permission" import { Global } from "@/global" import type { LanguageModelV2Usage } from "@ai-sdk/provider" -import { iife } from "@/util/iife" export namespace Session { const log = Log.create({ service: "session" }) @@ -813,27 +812,11 @@ export namespace Session { 0) as number, ) - // OpenRouter provides inputTokens as the total count of input tokens (including cached). - // AFAIK other providers (OpenRouter/OpenAI/Gemini etc.) do it the same way e.g. vercel/ai#8794 (comment) - // Anthropic does it differently though - inputTokens doesn't include cached tokens. - // It looks like OpenCode's cost calculation assumes all providers return inputTokens the same way Anthropic does (I'm guessing getUsage logic was originally implemented with anthropic), so it's causing incorrect cost calculation for OpenRouter and others. - const excludesCachedTokens = !!(input.metadata?.["anthropic"] || input.metadata?.["bedrock"]) - const adjustedInputTokens = safe( - excludesCachedTokens ? inputTokens : inputTokens - cacheReadInputTokens - cacheWriteInputTokens, - ) + // AI SDK v6 normalized inputTokens to include cached tokens across all providers. + // Always subtract cache tokens to get non-cached input for separate cost calculation. + const adjustedInputTokens = safe(inputTokens - cacheReadInputTokens - cacheWriteInputTokens) - const total = iife(() => { - // Anthropic doesn't provide total_tokens, also ai sdk will vastly undercount if we - // don't compute from components - if ( - input.model.api.npm === "@ai-sdk/anthropic" || - input.model.api.npm === "@ai-sdk/amazon-bedrock" || - input.model.api.npm === "@ai-sdk/google-vertex/anthropic" - ) { - return adjustedInputTokens + outputTokens + cacheReadInputTokens + cacheWriteInputTokens - } - return input.usage.totalTokens - }) + const total = input.usage.totalTokens const tokens = { total, @@ -853,13 +836,11 @@ export namespace Session { 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)) + .add(new Decimal(tokens.input).mul(costInfo?.input ?? 0).div(10_000)) + .add(new Decimal(tokens.output).mul(costInfo?.output ?? 0).div(10_000)) + .add(new Decimal(tokens.cache.read).mul(costInfo?.cache?.read ?? 0).div(10_000)) + .add(new Decimal(tokens.cache.write).mul(costInfo?.cache?.write ?? 0).div(10_000)) + .add(new Decimal(tokens.reasoning).mul(costInfo?.output ?? 0).div(10_000)) .toNumber(), ), tokens, diff --git a/packages/tfcode/src/tool/read.ts b/packages/tfcode/src/tool/read.ts index 84c93bc8a..477dd1f06 100644 --- a/packages/tfcode/src/tool/read.ts +++ b/packages/tfcode/src/tool/read.ts @@ -200,7 +200,7 @@ export const ReadTool = Tool.define("read", { }) const preview = raw.slice(0, 20).join("\n") - let output = [`${filepath}`, `file`, ""].join("\n") + let output = [`${filepath}`, `file`, "" + "\n"].join("\n") output += content.join("\n") const totalLines = lines diff --git a/packages/tfcode/src/tool/task.ts b/packages/tfcode/src/tool/task.ts index e3781126d..f4c2c2d8d 100644 --- a/packages/tfcode/src/tool/task.ts +++ b/packages/tfcode/src/tool/task.ts @@ -64,6 +64,7 @@ export const TaskTool = Tool.define("task", async (ctx) => { if (!agent) throw new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`) const hasTaskPermission = agent.permission.some((rule) => rule.permission === "task") + const hasTodoWritePermission = agent.permission.some((rule) => rule.permission === "todowrite") const session = await iife(async () => { if (params.task_id) { @@ -75,11 +76,15 @@ export const TaskTool = Tool.define("task", async (ctx) => { parentID: ctx.sessionID, title: params.description + ` (@${agent.name} subagent)`, permission: [ - { - permission: "todowrite", - pattern: "*", - action: "deny", - }, + ...(hasTodoWritePermission + ? [] + : [ + { + permission: "todowrite" as const, + pattern: "*" as const, + action: "deny" as const, + }, + ]), { permission: "todoread", pattern: "*", @@ -136,7 +141,7 @@ export const TaskTool = Tool.define("task", async (ctx) => { }, agent: agent.name, tools: { - todowrite: false, + ...(hasTodoWritePermission ? {} : { todowrite: false }), todoread: false, ...(hasTaskPermission ? {} : { task: false }), ...Object.fromEntries((config.experimental?.primary_tools ?? []).map((t) => [t, false])), diff --git a/packages/tfcode/src/tool/webfetch.ts b/packages/tfcode/src/tool/webfetch.ts index a66e66c09..559afd677 100644 --- a/packages/tfcode/src/tool/webfetch.ts +++ b/packages/tfcode/src/tool/webfetch.ts @@ -3,6 +3,7 @@ import { Tool } from "./tool" import TurndownService from "turndown" import DESCRIPTION from "./webfetch.txt" import { abortAfterAny } from "../util/abort" +import { iife } from "@/util/iife" const MAX_RESPONSE_SIZE = 5 * 1024 * 1024 // 5MB const DEFAULT_TIMEOUT = 30 * 1000 // 30 seconds @@ -62,15 +63,18 @@ export const WebFetchTool = Tool.define("webfetch", { "Accept-Language": "en-US,en;q=0.9", } - const initial = await fetch(params.url, { signal, headers }) + const response = await iife(async () => { + try { + const initial = await fetch(params.url, { signal, headers }) - // Retry with honest UA if blocked by Cloudflare bot detection (TLS fingerprint mismatch) - const response = - initial.status === 403 && initial.headers.get("cf-mitigated") === "challenge" - ? await fetch(params.url, { signal, headers: { ...headers, "User-Agent": "opencode" } }) - : initial - - clearTimeout() + // Retry with honest UA if blocked by Cloudflare bot detection (TLS fingerprint mismatch) + return initial.status === 403 && initial.headers.get("cf-mitigated") === "challenge" + ? await fetch(params.url, { signal, headers: { ...headers, "User-Agent": "opencode" } }) + : initial + } finally { + clearTimeout() + } + }) if (!response.ok) { throw new Error(`Request failed with status code: ${response.status}`)