feat: cherry picks

This commit is contained in:
Gab
2026-04-09 22:59:51 +10:00
parent c8231976f3
commit f72686a74a
8 changed files with 39 additions and 52 deletions

View File

@@ -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) => {

View File

@@ -53,6 +53,7 @@ export const { use: useExit, provider: ExitProvider } = createSimpleContext({
message: store,
},
)
process.on("SIGHUP", () => exit())
return exit
},
})

View File

@@ -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(() => {

View File

@@ -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(() => {

View File

@@ -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,

View File

@@ -200,7 +200,7 @@ export const ReadTool = Tool.define("read", {
})
const preview = raw.slice(0, 20).join("\n")
let output = [`<path>${filepath}</path>`, `<type>file</type>`, "<content>"].join("\n")
let output = [`<path>${filepath}</path>`, `<type>file</type>`, "<content>" + "\n"].join("\n")
output += content.join("\n")
const totalLines = lines

View File

@@ -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])),

View File

@@ -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}`)