Compare commits

..

1 Commits

Author SHA1 Message Date
Rhys Sullivan e73ec6d2d7 feat(model): add provider fast mode toggle 2026-03-16 11:46:41 -07:00
26 changed files with 636 additions and 169 deletions
@@ -1023,6 +1023,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
})
const variants = createMemo(() => ["default", ...local.model.variant.list()])
const fast = createMemo(() => local.model.fast.available())
const fastLabel = createMemo(() =>
language.t(local.model.fast.current() ? "command.model.fast.disable" : "command.model.fast.enable"),
)
const accepting = createMemo(() => {
const id = params.id
if (!id) return permission.isAutoAcceptingDirectory(sdk.directory)
@@ -1534,6 +1538,25 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
/>
</TooltipKeybind>
</div>
<Show when={fast()}>
<Tooltip placement="top" gutter={8} value={fastLabel()}>
<Button
data-action="prompt-fast"
variant="ghost"
onClick={() => local.model.fast.toggle()}
class="h-7 px-2 shrink-0 text-13-medium"
classList={{
"text-text-base": !local.model.fast.current(),
"text-icon-warning-base bg-surface-warning-base": local.model.fast.current(),
}}
style={control()}
aria-label={fastLabel()}
aria-pressed={local.model.fast.current()}
>
{language.t("command.model.fast.label")}
</Button>
</Tooltip>
</Show>
<TooltipKeybind
placement="top"
gutter={8}
@@ -34,6 +34,7 @@ export type FollowupDraft = {
agent: string
model: { providerID: string; modelID: string }
variant?: string
fast?: boolean
}
type FollowupSendInput = {
@@ -88,6 +89,7 @@ export async function sendFollowupDraft(input: FollowupSendInput) {
agent: input.draft.agent,
model: `${input.draft.model.providerID}/${input.draft.model.modelID}`,
variant: input.draft.variant,
fast: input.draft.fast,
parts: images.map((attachment) => ({
id: Identifier.ascending("part"),
type: "file" as const,
@@ -122,6 +124,7 @@ export async function sendFollowupDraft(input: FollowupSendInput) {
agent: input.draft.agent,
model: input.draft.model,
variant: input.draft.variant,
fast: input.draft.fast,
}
const add = () =>
@@ -156,6 +159,7 @@ export async function sendFollowupDraft(input: FollowupSendInput) {
messageID,
parts: requestParts,
variant: input.draft.variant,
fast: input.draft.fast,
})
return true
} catch (err) {
@@ -297,6 +301,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
const currentModel = local.model.current()
const currentAgent = local.agent.current()
const variant = local.model.variant.current()
const fast = local.model.fast.current()
if (!currentModel || !currentAgent) {
showToast({
title: language.t("prompt.toast.modelAgentRequired.title"),
@@ -398,6 +403,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
agent,
model,
variant,
fast,
}
const clearInput = () => {
@@ -461,6 +467,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
agent,
model: `${model.providerID}/${model.modelID}`,
variant,
fast,
parts: images.map((attachment) => ({
id: Identifier.ascending("part"),
type: "file" as const,
@@ -326,7 +326,7 @@ export function SessionHeader() {
<div class="flex h-[24px] box-border items-center rounded-md border border-border-weak-base bg-surface-panel overflow-hidden">
<Button
variant="ghost"
class="rounded-none h-full px-0.5 border-none shadow-none disabled:!cursor-default"
class="rounded-none h-full py-0 pr-1.5 pl-px gap-1.5 border-none shadow-none disabled:!cursor-default"
classList={{
"bg-surface-raised-base-active": opening(),
}}
@@ -339,6 +339,7 @@ export function SessionHeader() {
<Spinner class="size-3.5" style={{ color: tint() ?? "var(--icon-base)" }} />
</Show>
</div>
<span class="text-12-regular text-text-strong">{language.t("common.open")}</span>
</Button>
<DropdownMenu
gutter={4}
+40 -2
View File
@@ -8,6 +8,7 @@ import { useProviders } from "@/hooks/use-providers"
import { modelEnabled, modelProbe } from "@/testing/model-selection"
import { Persist, persisted } from "@/utils/persist"
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
import * as Fast from "./model-fast"
import { useSDK } from "./sdk"
import { useSync } from "./sync"
@@ -17,6 +18,7 @@ type State = {
agent?: string
model?: ModelKey
variant?: string | null
fast?: boolean
}
type Saved = {
@@ -79,10 +81,11 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
current?: string
draft?: State
last?: {
type: "agent" | "model" | "variant"
type: "agent" | "model" | "variant" | "fast"
agent?: string
model?: ModelKey | null
variant?: string | null
fast?: boolean
}
}>({
current: list()[0]?.name,
@@ -191,11 +194,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
agent: item.name,
model: item.model,
variant: item.variant ?? null,
fast: scope()?.fast,
})
const next = {
agent: item.name,
model: item.model,
variant: item.variant,
fast: scope()?.fast,
} satisfies State
const session = id()
if (session) {
@@ -249,6 +254,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
agent: agent.current()?.name,
model: model ? { providerID: model.provider.id, modelID: model.id } : undefined,
variant: selected(),
fast: !!scope()?.fast && Fast.enabled(model),
} satisfies State
}
@@ -296,6 +302,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
agent: agent.current()?.name,
model: item ?? null,
variant: selected(),
fast: model.fast.current(),
})
write({ model: item })
if (!item) return
@@ -333,6 +340,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
agent: agent.current()?.name,
model: model ? { providerID: model.provider.id, modelID: model.id } : null,
variant: value ?? null,
fast: !!scope()?.fast && Fast.enabled(model),
})
write({ variant: value ?? null })
})
@@ -349,6 +357,34 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
)
},
},
fast: {
selected() {
return scope()?.fast === true
},
current() {
return this.selected() && this.available()
},
available() {
return Fast.enabled(current())
},
set(value: boolean) {
if (value && !this.available()) return
const model = current()
batch(() => {
setStore("last", {
type: "fast",
agent: agent.current()?.name,
model: model ? { providerID: model.provider.id, modelID: model.id } : null,
variant: selected(),
fast: value,
})
write({ fast: value || undefined })
})
},
toggle() {
this.set(!this.current())
},
},
}
const result = {
@@ -372,7 +408,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
handoff.set(handoffKey(dir, session), next)
setStore("draft", undefined)
},
restore(msg: { sessionID: string; agent: string; model: ModelKey; variant?: string }) {
restore(msg: { sessionID: string; agent: string; model: ModelKey; variant?: string; fast?: boolean }) {
const session = id()
if (!session) return
if (msg.sessionID !== session) return
@@ -383,6 +419,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
agent: msg.agent,
model: msg.model,
variant: msg.variant ?? null,
fast: msg.fast === true,
})
},
},
@@ -405,6 +442,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
: undefined,
variant: result.model.variant.current() ?? null,
fast: result.model.fast.current(),
selected: result.model.variant.selected(),
configured: result.model.variant.configured(),
pick: scope(),
+28
View File
@@ -0,0 +1,28 @@
type Model = {
id: string
provider: {
id: string
}
}
function lower(model: Model) {
return model.id.toLowerCase()
}
export function kind(model: Model | undefined) {
if (!model) return
const id = lower(model)
if (
model.provider.id === "anthropic" &&
(id.includes("claude-opus-4-6") || id.includes("claude-opus-4.6") || id.includes("opus-4-6"))
) {
return "claude"
}
if (model.provider.id === "openai" && id.includes("gpt-5.4")) {
return "codex"
}
}
export function enabled(model: Model | undefined) {
return !!kind(model)
}
+4
View File
@@ -67,6 +67,10 @@ export const dict = {
"command.agent.cycle.description": "Switch to the next agent",
"command.agent.cycle.reverse": "Cycle agent backwards",
"command.agent.cycle.reverse.description": "Switch to the previous agent",
"command.model.fast.label": "Fast",
"command.model.fast.enable": "Enable fast mode",
"command.model.fast.disable": "Disable fast mode",
"command.model.fast.description": "Toggle provider fast mode for supported Claude and Codex models",
"command.model.variant.cycle": "Cycle thinking effort",
"command.model.variant.cycle.description": "Switch to the next effort level",
"command.prompt.mode.shell": "Shell",
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"
import type { UserMessage } from "@opencode-ai/sdk/v2"
import { resetSessionModel, syncSessionModel } from "./session-model-helpers"
const message = (input?: Partial<Pick<UserMessage, "agent" | "model" | "variant">>) =>
const message = (input?: Partial<Pick<UserMessage, "agent" | "model" | "variant" | "fast">>) =>
({
id: "msg",
sessionID: "session",
@@ -31,6 +31,24 @@ describe("syncSessionModel", () => {
expect(calls).toEqual([message({ variant: "high" })])
})
test("restores fast mode from the last message", () => {
const calls: unknown[] = []
syncSessionModel(
{
session: {
restore(value) {
calls.push(value)
},
reset() {},
},
},
message({ fast: true }),
)
expect(calls).toEqual([message({ fast: true })])
})
})
describe("resetSessionModel", () => {
@@ -353,6 +353,14 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
slash: "model",
onSelect: () => dialog.show(() => <DialogSelectModel model={local.model} />),
}),
modelCommand({
id: "model.fast.toggle",
title: language.t(local.model.fast.current() ? "command.model.fast.disable" : "command.model.fast.enable"),
description: language.t("command.model.fast.description"),
slash: "fast",
disabled: !local.model.fast.available(),
onSelect: () => local.model.fast.toggle(),
}),
mcpCommand({
id: "mcp.toggle",
title: language.t("command.mcp.toggle"),
+4 -1
View File
@@ -7,20 +7,23 @@ type State = {
agent?: string
model?: ModelKey | null
variant?: string | null
fast?: boolean
}
export type ModelProbeState = {
dir?: string
sessionID?: string
last?: {
type: "agent" | "model" | "variant"
type: "agent" | "model" | "variant" | "fast"
agent?: string
model?: ModelKey | null
variant?: string | null
fast?: boolean
}
agent?: string
model?: (ModelKey & { name?: string }) | undefined
variant?: string | null
fast?: boolean
selected?: string | null
configured?: string
pick?: State
@@ -164,7 +164,8 @@ export function Prompt(props: PromptProps) {
if (msg.agent && isPrimaryAgent) {
local.agent.set(msg.agent)
if (msg.model) local.model.set(msg.model)
if (msg.variant) local.model.variant.set(msg.variant)
local.model.variant.set(msg.variant)
local.model.fast.set(msg.fast === true)
}
}
})
@@ -330,6 +331,19 @@ export function Prompt(props: PromptProps) {
input.cursorOffset = Bun.stringWidth(content)
},
},
{
title: local.model.fast.current() ? "Disable fast mode" : "Enable fast mode",
value: "model.fast",
category: "Model",
enabled: local.model.fast.available(),
slash: {
name: "fast",
},
onSelect: (dialog) => {
local.model.fast.toggle()
dialog.clear()
},
},
{
title: "Skills",
value: "prompt.skills",
@@ -586,6 +600,7 @@ export function Prompt(props: PromptProps) {
// Capture mode before it gets reset
const currentMode = store.mode
const variant = local.model.variant.current()
const fast = local.model.fast.current()
if (store.mode === "shell") {
sdk.client.session.shell({
@@ -621,6 +636,7 @@ export function Prompt(props: PromptProps) {
model: `${selectedModel.providerID}/${selectedModel.modelID}`,
messageID,
variant,
fast,
parts: nonTextParts
.filter((x) => x.type === "file")
.map((x) => ({
@@ -637,6 +653,7 @@ export function Prompt(props: PromptProps) {
agent: local.agent.current().name,
model: selectedModel,
variant,
fast,
parts: [
{
id: PartID.ascending(),
@@ -765,6 +782,8 @@ export function Prompt(props: PromptProps) {
return !!current
})
const showFast = createMemo(() => local.model.fast.current())
const placeholderText = createMemo(() => {
if (props.sessionID) return undefined
if (store.mode === "shell") {
@@ -1028,6 +1047,12 @@ export function Prompt(props: PromptProps) {
<span style={{ fg: theme.warning, bold: true }}>{local.model.variant.current()}</span>
</text>
</Show>
<Show when={showFast()}>
<text fg={theme.textMuted}>·</text>
<text>
<span style={{ fg: theme.info, bold: true }}>fast</span>
</text>
</Show>
</box>
</Show>
</box>
@@ -13,6 +13,7 @@ import { useArgs } from "./args"
import { useSDK } from "./sdk"
import { RGBA } from "@opentui/core"
import { Filesystem } from "@/util/filesystem"
import * as Fast from "@/provider/fast"
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
name: "Local",
@@ -112,12 +113,14 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
modelID: string
}[]
variant: Record<string, string | undefined>
fast: Record<string, boolean | undefined>
}>({
ready: false,
model: {},
recent: [],
favorite: [],
variant: {},
fast: {},
})
const filePath = path.join(Global.Path.state, "model.json")
@@ -135,6 +138,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
recent: modelStore.recent,
favorite: modelStore.favorite,
variant: modelStore.variant,
fast: modelStore.fast,
})
}
@@ -143,6 +147,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
if (Array.isArray(x.recent)) setModelStore("recent", x.recent)
if (Array.isArray(x.favorite)) setModelStore("favorite", x.favorite)
if (typeof x.variant === "object" && x.variant !== null) setModelStore("variant", x.variant)
if (typeof x.fast === "object" && x.fast !== null) setModelStore("fast", x.fast)
})
.catch(() => {})
.finally(() => {
@@ -358,6 +363,36 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
this.set(variants[index + 1])
},
},
fast: {
selected() {
const m = currentModel()
if (!m) return false
const key = `${m.providerID}/${m.modelID}`
return modelStore.fast[key] === true
},
current() {
return this.selected() && this.available()
},
available() {
const m = currentModel()
if (!m) return false
const provider = sync.data.provider.find((x) => x.id === m.providerID)
const info = provider?.models[m.modelID]
if (!info) return false
return Fast.enabled(info, { codex: info.providerID === "openai" })
},
set(value: boolean) {
const m = currentModel()
if (!m) return
if (value && !this.available()) return
const key = `${m.providerID}/${m.modelID}`
setModelStore("fast", key, value || undefined)
save()
},
toggle() {
this.set(!this.current())
},
},
}
})
+5
View File
@@ -47,6 +47,11 @@ process.on("uncaughtException", (e) => {
})
})
// Ensure the process exits on terminal hangup (eg. closing the terminal tab).
// Without this, long-running commands like `serve` block on a never-resolving
// promise and survive as orphaned processes.
process.on("SIGHUP", () => process.exit())
let cli = yargs(hideBin(process.argv))
.parserConfiguration({ "populate--": true })
.scriptName("opencode")
+210
View File
@@ -0,0 +1,210 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import { SessionID, MessageID } from "@/session/schema"
import z from "zod"
import { Log } from "../util/log"
import { Plugin } from "../plugin"
import { Instance } from "../project/instance"
import { Wildcard } from "../util/wildcard"
import { PermissionID } from "./schema"
export namespace Permission {
const log = Log.create({ service: "permission" })
function toKeys(pattern: Info["pattern"], type: string): string[] {
return pattern === undefined ? [type] : Array.isArray(pattern) ? pattern : [pattern]
}
function covered(keys: string[], approved: Map<string, boolean>): boolean {
return keys.every((k) => {
for (const p of approved.keys()) {
if (Wildcard.match(k, p)) return true
}
return false
})
}
export const Info = z
.object({
id: PermissionID.zod,
type: z.string(),
pattern: z.union([z.string(), z.array(z.string())]).optional(),
sessionID: SessionID.zod,
messageID: MessageID.zod,
callID: z.string().optional(),
message: z.string(),
metadata: z.record(z.string(), z.any()),
time: z.object({
created: z.number(),
}),
})
.meta({
ref: "Permission",
})
export type Info = z.infer<typeof Info>
interface PendingEntry {
info: Info
resolve: () => void
reject: (e: any) => void
}
export const Event = {
Updated: BusEvent.define("permission.updated", Info),
Replied: BusEvent.define(
"permission.replied",
z.object({
sessionID: SessionID.zod,
permissionID: PermissionID.zod,
response: z.string(),
}),
),
}
const state = Instance.state(
() => ({
pending: new Map<SessionID, Map<PermissionID, PendingEntry>>(),
approved: new Map<SessionID, Map<string, boolean>>(),
}),
async (state) => {
for (const session of state.pending.values()) {
for (const item of session.values()) {
item.reject(new RejectedError(item.info.sessionID, item.info.id, item.info.callID, item.info.metadata))
}
}
},
)
export function pending() {
return state().pending
}
export function list() {
const { pending } = state()
const result: Info[] = []
for (const session of pending.values()) {
for (const item of session.values()) {
result.push(item.info)
}
}
return result.sort((a, b) => String(a.id).localeCompare(String(b.id)))
}
export async function ask(input: {
type: Info["type"]
message: Info["message"]
pattern?: Info["pattern"]
callID?: Info["callID"]
sessionID: Info["sessionID"]
messageID: Info["messageID"]
metadata: Info["metadata"]
}) {
const { pending, approved } = state()
log.info("asking", {
sessionID: input.sessionID,
messageID: input.messageID,
toolCallID: input.callID,
pattern: input.pattern,
})
const approvedForSession = approved.get(input.sessionID)
const keys = toKeys(input.pattern, input.type)
if (approvedForSession && covered(keys, approvedForSession)) return
const info: Info = {
id: PermissionID.ascending(),
type: input.type,
pattern: input.pattern,
sessionID: input.sessionID,
messageID: input.messageID,
callID: input.callID,
message: input.message,
metadata: input.metadata,
time: {
created: Date.now(),
},
}
switch (
await Plugin.trigger("permission.ask", info, {
status: "ask",
}).then((x) => x.status)
) {
case "deny":
throw new RejectedError(info.sessionID, info.id, info.callID, info.metadata)
case "allow":
return
}
if (!pending.has(input.sessionID)) pending.set(input.sessionID, new Map())
return new Promise<void>((resolve, reject) => {
pending.get(input.sessionID)!.set(info.id, {
info,
resolve,
reject,
})
Bus.publish(Event.Updated, info)
})
}
export const Response = z.enum(["once", "always", "reject"])
export type Response = z.infer<typeof Response>
export function respond(input: { sessionID: Info["sessionID"]; permissionID: Info["id"]; response: Response }) {
log.info("response", input)
const { pending, approved } = state()
const session = pending.get(input.sessionID)
const match = session?.get(input.permissionID)
if (!session || !match) return
session.delete(input.permissionID)
if (session.size === 0) pending.delete(input.sessionID)
Bus.publish(Event.Replied, {
sessionID: input.sessionID,
permissionID: input.permissionID,
response: input.response,
})
if (input.response === "reject") {
match.reject(new RejectedError(input.sessionID, input.permissionID, match.info.callID, match.info.metadata))
return
}
match.resolve()
if (input.response === "always") {
if (!approved.has(input.sessionID)) approved.set(input.sessionID, new Map())
const approvedSession = approved.get(input.sessionID)!
const approveKeys = toKeys(match.info.pattern, match.info.type)
for (const k of approveKeys) {
approvedSession.set(k, true)
}
const items = pending.get(input.sessionID)
if (!items) return
const toRespond: Info[] = []
for (const item of items.values()) {
const itemKeys = toKeys(item.info.pattern, item.info.type)
if (covered(itemKeys, approvedSession)) {
toRespond.push(item.info)
}
}
for (const item of toRespond) {
respond({
sessionID: item.sessionID,
permissionID: item.id,
response: input.response,
})
}
}
}
export class RejectedError extends Error {
constructor(
public readonly sessionID: SessionID,
public readonly permissionID: PermissionID,
public readonly toolCallID?: string,
public readonly metadata?: Record<string, any>,
public readonly reason?: string,
) {
super(
reason !== undefined
? reason
: `The user rejected permission to use this specific tool call. You may try again with different parameters.`,
)
}
}
}
+45
View File
@@ -0,0 +1,45 @@
type Model = {
providerID: string
api: {
id: string
npm: string
}
}
function lower(model: Pick<Model, "api">) {
return model.api.id.toLowerCase()
}
type Input = {
codex?: boolean
}
export function kind(model: Pick<Model, "providerID" | "api">, input?: Input) {
const id = lower(model)
if (
model.providerID === "anthropic" &&
model.api.npm === "@ai-sdk/anthropic" &&
(id.includes("claude-opus-4-6") || id.includes("claude-opus-4.6") || id.includes("opus-4-6"))
) {
return "claude"
}
if (
model.providerID === "openai" &&
input?.codex === true &&
model.api.npm === "@ai-sdk/openai" &&
id.includes("gpt-5.4")
) {
return "codex"
}
}
export function enabled(model: Pick<Model, "providerID" | "api">, input?: Input) {
return !!kind(model, input)
}
export function options(model: Pick<Model, "providerID" | "api">, input?: Input) {
const mode = kind(model, input)
if (mode === "claude") return { speed: "fast" }
if (mode === "codex") return { serviceTier: "priority" }
return {}
}
@@ -6,6 +6,7 @@ import type { Provider } from "./provider"
import type { ModelsDev } from "./models"
import { iife } from "@/util/iife"
import { Flag } from "@/flag/flag"
import * as Fast from "./fast"
type Modality = NonNullable<ModelsDev.Model["modalities"]>["input"][number]
@@ -905,6 +906,10 @@ export namespace ProviderTransform {
return { [key]: options }
}
export function fast(model: Provider.Model, input?: { codex?: boolean }) {
return Fast.options(model, input)
}
export function maxOutputTokens(model: Provider.Model): number {
return Math.min(model.limit.output, OUTPUT_TOKEN_MAX) || OUTPUT_TOKEN_MAX
}
+1 -6
View File
@@ -129,12 +129,7 @@ export class QuestionService extends ServiceMap.Service<QuestionService, Questio
pending.set(id, { info, deferred })
Bus.publish(Event.Asked, info)
return yield* Effect.ensuring(
Deferred.await(deferred),
Effect.sync(() => {
pending.delete(id)
}),
)
return yield* Deferred.await(deferred)
})
const reply = Effect.fn("QuestionService.reply")(function* (input: { requestID: QuestionID; answers: Answer[] }) {
@@ -141,6 +141,7 @@ export namespace SessionCompaction {
mode: "compaction",
agent: "compaction",
variant: userMessage.variant,
fast: userMessage.fast,
summary: true,
path: {
cwd: Instance.directory,
+8 -4
View File
@@ -101,11 +101,15 @@ export namespace LLM {
sessionID: input.sessionID,
providerOptions: provider.options,
})
const fast = (
input.small || !input.user.fast ? {} : ProviderTransform.fast(input.model, { codex: isCodex })
) as Record<string, any>
const options: Record<string, any> = pipe(
base,
mergeDeep(input.model.options),
mergeDeep(input.agent.options),
mergeDeep(variant),
base as Record<string, any>,
mergeDeep(input.model.options as Record<string, any>),
mergeDeep(input.agent.options as Record<string, any>),
mergeDeep(variant as Record<string, any>),
mergeDeep(fast),
)
if (isCodex) {
options.instructions = SystemPrompt.instructions()
@@ -369,6 +369,7 @@ export namespace MessageV2 {
system: z.string().optional(),
tools: z.record(z.string(), z.boolean()).optional(),
variant: z.string().optional(),
fast: z.boolean().optional(),
}).meta({
ref: "UserMessage",
})
@@ -437,6 +438,7 @@ export namespace MessageV2 {
}),
structured: z.any().optional(),
variant: z.string().optional(),
fast: z.boolean().optional(),
finish: z.string().optional(),
}).meta({
ref: "AssistantMessage",
+7
View File
@@ -111,6 +111,7 @@ export namespace SessionPrompt {
format: MessageV2.Format.optional(),
system: z.string().optional(),
variant: z.string().optional(),
fast: z.boolean().optional(),
parts: z.array(
z.discriminatedUnion("type", [
MessageV2.TextPart.omit({
@@ -363,6 +364,7 @@ export namespace SessionPrompt {
mode: task.agent,
agent: task.agent,
variant: lastUser.variant,
fast: lastUser.fast,
path: {
cwd: Instance.directory,
root: Instance.worktree,
@@ -575,6 +577,7 @@ export namespace SessionPrompt {
mode: agent.name,
agent: agent.name,
variant: lastUser.variant,
fast: lastUser.fast,
path: {
cwd: Instance.directory,
root: Instance.worktree,
@@ -984,6 +987,7 @@ export namespace SessionPrompt {
system: input.system,
format: input.format,
variant,
fast: input.fast,
}
using _ = defer(() => InstructionPrompt.clear(info.id))
@@ -1310,6 +1314,7 @@ export namespace SessionPrompt {
model: input.model,
messageID: input.messageID,
variant: input.variant,
fast: input.fast,
},
{
message: info,
@@ -1727,6 +1732,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
arguments: z.string(),
command: z.string(),
variant: z.string().optional(),
fast: z.boolean().optional(),
parts: z
.array(
z.discriminatedUnion("type", [
@@ -1884,6 +1890,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
agent: userAgent,
parts,
variant: input.variant,
fast: input.fast,
})) as MessageV2.WithParts
Bus.publish(Command.Event.Executed, {
@@ -1,88 +0,0 @@
/**
* Repro for: @parcel/watcher native callback loses AsyncLocalStorage context
*
* Background:
* opencode uses AsyncLocalStorage (ALS) to track which project directory
* is active. Bus.publish reads Instance.directory from ALS to route events
* to the right instance. This works for normal JS async code (setTimeout,
* Promises, etc.) because Node propagates ALS through those.
*
* But @parcel/watcher is a native C++ addon. Its callback re-enters JS
* from C++ via libuv, bypassing Node's async hooks — so ALS is empty.
* Bus.publish silently throws Context.NotFound, and the event vanishes.
*
* What this breaks:
* The git HEAD watcher (always active, no experimental flag) should detect
* branch switches and update the TUI. But because events never arrive,
* the branch indicator never live-updates.
*
* This test:
* 1. Creates a tmp git repo and boots an instance with the file watcher
* 2. Listens on GlobalBus for watcher events (Bus.publish emits to GlobalBus)
* 3. Runs `git checkout -b` to change .git/HEAD — the watcher WILL detect
* this change and fire the callback, but Bus.publish will fail silently
* 4. Times out after 5s because the event never reaches GlobalBus
*
* Fix: Instance.bind(fn) captures ALS context at subscription time and
* restores it in the callback. See #17601.
*/
import { $ } from "bun"
import { afterEach, expect, test } from "bun:test"
import { tmpdir } from "../fixture/fixture"
async function load() {
const { FileWatcher } = await import("../../src/file/watcher")
const { GlobalBus } = await import("../../src/bus/global")
const { Instance } = await import("../../src/project/instance")
return { FileWatcher, GlobalBus, Instance }
}
afterEach(async () => {
const { Instance } = await load()
await Instance.disposeAll()
})
test("git HEAD watcher publishes events via Bus (ALS context test)", async () => {
const { FileWatcher, GlobalBus, Instance } = await load()
// 1. Create a temp git repo and start the file watcher inside an instance.
// The watcher subscribes to .git/HEAD changes via @parcel/watcher.
// At this point we're inside Instance.provide, so ALS is active.
await using tmp = await tmpdir({ git: true })
await Instance.provide({
directory: tmp.path,
fn: async () => {
FileWatcher.init()
await Bun.sleep(200) // wait for native watcher to finish subscribing
},
})
// 2. Listen on GlobalBus and trigger a branch switch.
// When .git/HEAD changes, @parcel/watcher fires our callback from C++.
// The callback calls Bus.publish, which needs ALS to read Instance.directory.
// Without Instance.bind, ALS is empty → Bus.publish throws → event never arrives.
const got = await new Promise<any>((resolve, reject) => {
const timeout = setTimeout(() => {
GlobalBus.off("event", on)
reject(new Error("timed out — native callback likely lost ALS context"))
}, 5000)
function on(evt: any) {
if (evt.directory !== tmp.path) return
if (evt.payload?.type !== FileWatcher.Event.Updated.type) return
clearTimeout(timeout)
GlobalBus.off("event", on)
resolve(evt.payload.properties)
}
GlobalBus.on("event", on)
// This changes .git/HEAD, which the native watcher will detect
$`git checkout -b test-branch`.cwd(tmp.path).quiet().nothrow()
})
// 3. If we get here, the event arrived — ALS context was preserved.
// On the unfixed code, we never get here (the promise rejects with timeout).
expect(got).toBeDefined()
expect(got.event).toBe("change")
})
@@ -176,6 +176,70 @@ describe("ProviderTransform.options - gpt-5 textVerbosity", () => {
})
})
describe("ProviderTransform.fast", () => {
const createModel = (input: { providerID: string; modelID: string; npm: string }) =>
({
id: input.modelID,
providerID: input.providerID,
api: {
id: input.modelID,
url: "https://example.com",
npm: input.npm,
},
name: input.modelID,
capabilities: {
temperature: true,
reasoning: true,
attachment: true,
toolcall: true,
input: { text: true, audio: false, image: true, video: false, pdf: false },
output: { text: true, audio: false, image: false, video: false, pdf: false },
interleaved: false,
},
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
limit: { context: 200000, output: 8192 },
status: "active",
options: {},
headers: {},
}) as any
test("uses speed fast for anthropic opus 4.6", () => {
const model = createModel({
providerID: "anthropic",
modelID: "claude-opus-4-6",
npm: "@ai-sdk/anthropic",
})
expect(ProviderTransform.fast(model)).toEqual({ speed: "fast" })
})
test("uses priority service tier for openai gpt-5 codex models", () => {
const model = createModel({
providerID: "openai",
modelID: "gpt-5.4",
npm: "@ai-sdk/openai",
})
expect(ProviderTransform.fast(model, { codex: true })).toEqual({ serviceTier: "priority" })
})
test("returns empty options for unsupported models", () => {
const model = createModel({
providerID: "anthropic",
modelID: "claude-sonnet-4-6",
npm: "@ai-sdk/anthropic",
})
expect(ProviderTransform.fast(model)).toEqual({})
})
test("returns empty options for openai api mode", () => {
const model = createModel({
providerID: "openai",
modelID: "gpt-5.4",
npm: "@ai-sdk/openai",
})
expect(ProviderTransform.fast(model)).toEqual({})
})
})
describe("ProviderTransform.options - gateway", () => {
const sessionID = "test-session-123"
+6
View File
@@ -1841,6 +1841,7 @@ export class Session2 extends HeyApiClient {
format?: OutputFormat
system?: string
variant?: string
fast?: boolean
parts?: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
},
options?: Options<never, ThrowOnError>,
@@ -1861,6 +1862,7 @@ export class Session2 extends HeyApiClient {
{ in: "body", key: "format" },
{ in: "body", key: "system" },
{ in: "body", key: "variant" },
{ in: "body", key: "fast" },
{ in: "body", key: "parts" },
],
},
@@ -1973,6 +1975,7 @@ export class Session2 extends HeyApiClient {
format?: OutputFormat
system?: string
variant?: string
fast?: boolean
parts?: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
},
options?: Options<never, ThrowOnError>,
@@ -1993,6 +1996,7 @@ export class Session2 extends HeyApiClient {
{ in: "body", key: "format" },
{ in: "body", key: "system" },
{ in: "body", key: "variant" },
{ in: "body", key: "fast" },
{ in: "body", key: "parts" },
],
},
@@ -2026,6 +2030,7 @@ export class Session2 extends HeyApiClient {
arguments?: string
command?: string
variant?: string
fast?: boolean
parts?: Array<{
id?: string
type: "file"
@@ -2051,6 +2056,7 @@ export class Session2 extends HeyApiClient {
{ in: "body", key: "arguments" },
{ in: "body", key: "command" },
{ in: "body", key: "variant" },
{ in: "body", key: "fast" },
{ in: "body", key: "parts" },
],
},
+5
View File
@@ -238,6 +238,7 @@ export type UserMessage = {
[key: string]: boolean
}
variant?: string
fast?: boolean
}
export type ProviderAuthError = {
@@ -340,6 +341,7 @@ export type AssistantMessage = {
}
structured?: unknown
variant?: string
fast?: boolean
finish?: string
}
@@ -3284,6 +3286,7 @@ export type SessionPromptData = {
format?: OutputFormat
system?: string
variant?: string
fast?: boolean
parts: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
}
path: {
@@ -3484,6 +3487,7 @@ export type SessionPromptAsyncData = {
format?: OutputFormat
system?: string
variant?: string
fast?: boolean
parts: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
}
path: {
@@ -3526,6 +3530,7 @@ export type SessionCommandData = {
arguments: string
command: string
variant?: string
fast?: boolean
parts?: Array<{
id?: string
type: "file"
+77 -63
View File
@@ -1,23 +1,23 @@
---
title: Introducción
description: Comience a usar OpenCode.
description: Comience con OpenCode.
---
import { Tabs, TabItem } from "@astrojs/starlight/components"
import config from "../../../../config.mjs"
export const console = config.console
[**OpenCode**](/) es un agente de codigo de IA de código abierto. Está disponible como interfaz basada en terminal, aplicación de escritorio o extensión IDE.
[**OpenCode**](/) es un agente de codificación de IA de código abierto. Está disponible como interfaz basada en terminal, aplicación de escritorio o extensión IDE.
![OpenCode TUI con el tema opencode](../../../assets/lander/screenshot.png)
Comencemos.
Empecemos.
---
#### Requisitos previos
Para usar OpenCode en la terminal, necesitará:
Para usar OpenCode en su terminal, necesitará:
1. Un emulador de terminal moderno como:
- [WezTerm](https://wezterm.org), multiplataforma
@@ -25,7 +25,7 @@ Para usar OpenCode en la terminal, necesitará:
- [Ghostty](https://ghostty.org), Linux y macOS
- [Kitty](https://sw.kovidgoyal.net/kitty/), Linux y macOS
2. Claves de API de los proveedores de LLM que quiera usar.
2. Claves API para los LLM proveedores que desea utilizar.
---
@@ -37,7 +37,7 @@ La forma más sencilla de instalar OpenCode es mediante el script de instalació
curl -fsSL https://opencode.ai/install | bash
```
También puedes instalarlo con alguno de los siguientes métodos:
También puedes instalarlo con los siguientes comandos:
- **Usando Node.js**
@@ -91,7 +91,7 @@ También puedes instalarlo con alguno de los siguientes métodos:
#### Windows
:::tip[Recomendado: Usar WSL]
Para obtener la mejor experiencia en Windows, recomendamos utilizar [Windows Subsystem for Linux (WSL)](/docs/windows-wsl). Ofrece mejor rendimiento y compatibilidad total con las funciones de OpenCode.
Para obtener la mejor experiencia en Windows, recomendamos utilizar [Windows Subsystem for Linux (WSL)](/docs/windows-wsl). Proporciona un mejor rendimiento y compatibilidad total con las funciones de OpenCode.
:::
- **Usando Chocolatey**
@@ -124,28 +124,28 @@ Para obtener la mejor experiencia en Windows, recomendamos utilizar [Windows Sub
docker run -it --rm ghcr.io/anomalyco/opencode
```
El soporte para instalar OpenCode en Windows usando Bun todavía está en desarrollo.
Actualmente se encuentra en progreso el soporte para instalar OpenCode en Windows usando Bun.
También puede obtener el binario desde [Versiones](https://github.com/anomalyco/opencode/releases).
También puede obtener el binario de [Versiones](https://github.com/anomalyco/opencode/releases).
---
## Configuración
Con OpenCode, puede usar cualquier proveedor de LLM configurando sus claves de API.
Con OpenCode puedes usar cualquier proveedor LLM configurando sus claves API.
Si es nuevo en el uso de proveedores de LLM, le recomendamos usar [OpenCode Zen](/docs/zen).
Es una selección de modelos probados y verificados por el equipo de OpenCode.
Si es nuevo en el uso de proveedores LLM, le recomendamos usar [OpenCode Zen](/docs/zen).
Es una lista seleccionada de modelos que han sido probados y verificados por el equipo de OpenCode.
1. Ejecute el comando `/connect` en la TUI, seleccione opencode y diríjase a [opencode.ai/auth](https://opencode.ai/auth).
1. Ejecute el comando `/connect` en TUI, seleccione opencode y diríjase a [opencode.ai/auth](https://opencode.ai/auth).
```txt
/connect
```
2. Inicie sesión, agregue sus datos de facturación y copie su clave de API.
2. Inicie sesión, agregue sus datos de facturación y copie su clave API.
3. Pega tu clave de API.
3. Pega tu clave API.
```txt
┌ API key
@@ -154,45 +154,50 @@ Es una selección de modelos probados y verificados por el equipo de OpenCode.
└ enter
```
También puede seleccionar otro proveedor. [Más información](/docs/providers#directory).
Alternativamente, puede seleccionar uno de los otros proveedores. [Más información](/docs/providers#directory).
---
## Inicializar
Ahora que ya configuró un proveedor, vaya al proyecto en el que quiera trabajar.
Ahora que ha configurado un proveedor, puede navegar a un proyecto que
quieres trabajar.
```bash
cd /path/to/project
```
Luego, ejecute OpenCode.
Y ejecute OpenCode.
```bash
opencode
```
A continuación, inicialice OpenCode para el proyecto con el siguiente comando:
A continuación, inicialice OpenCode para el proyecto ejecutando el siguiente comando.
```bash frame="none"
/init
```
OpenCode analizará su proyecto y creará un archivo AGENTS.md en la raíz.
Esto hará que OpenCode analice su proyecto y cree un archivo `AGENTS.md` en
la raíz del proyecto.
:::tip
Asegúrese de versionar en Git el archivo AGENTS.md de su proyecto.
Debes enviar el archivo `AGENTS.md` de tu proyecto a Git.
:::
Esto ayuda a OpenCode a comprender la estructura del proyecto y los patrones de código que se usan en él.
Esto ayuda a OpenCode a comprender la estructura del proyecto y los patrones de codificación.
usado.
---
## Usar
Ahora ya está listo para usar OpenCode en su proyecto. Puede pedirle desde explicaciones del código hasta cambios concretos.
Ahora está listo para usar OpenCode para trabajar en su proyecto. No dudes en preguntarle
¡cualquier cosa!
Si es la primera vez que usa un agente de codigo con IA, estos ejemplos pueden servirle como punto de partida.
Si es nuevo en el uso de un agente de codificación de IA, aquí hay algunos ejemplos que podrían
ayuda.
---
@@ -201,117 +206,126 @@ Si es la primera vez que usa un agente de codigo con IA, estos ejemplos pueden s
Puede pedirle a OpenCode que le explique el código base.
:::tip
Utilice la tecla `@` para realizar una búsqueda aproximada de archivos dentro del proyecto.
Utilice la tecla `@` para realizar una búsqueda aproximada de archivos en el proyecto.
:::
```txt frame="none" "@packages/functions/src/api/index.ts"
¿Cómo se maneja la autenticación en @packages/functions/src/api/index.ts
How is authentication handled in @packages/functions/src/api/index.ts
```
Esto resulta útil cuando hay una parte del código base en la que usted no ha trabajado.
Esto es útil si hay una parte del código base en la que no trabajaste.
---
### Agregar funcionalidades
### Agregar funciones
Puede pedirle a OpenCode que agregue nuevas funcionalidades a su proyecto. Aun así, primero recomendamos pedirle que cree un plan.
Puede pedirle a OpenCode que agregue nuevas funciones a su proyecto. Aunque primero recomendamos pedirle que cree un plan.
1. **Crear un plan**
1. **Crea un plan**
OpenCode tiene un modo Plan que desactiva temporalmente su capacidad de hacer cambios y, en su lugar, propone _cómo_ implementará la funcionalidad.
OpenCode tiene un _Modo Plan_ que desactiva su capacidad para realizar cambios y
en su lugar, sugiera _cómo_ implementará la función.
Cambie a este modo con la tecla **Tab.** Verá un indicador en la esquina inferior derecha.
Cambie a él usando la tecla **Tab**. Verás un indicador para esto en la esquina inferior derecha.
```bash frame="none" title="Switch to Plan mode"
<TAB>
```
Ahora describa lo que quiere que haga.
Ahora describamos lo que queremos que haga.
```txt frame="none"
Cuando un usuario elimine una nota, queremos marcarla como eliminada en la base de datos.
Luego, cree una pantalla que muestre todas las notas eliminadas recientemente.
Desde esa pantalla, el usuario podrá restaurar una nota o eliminarla de forma permanente.
When a user deletes a note, we'd like to flag it as deleted in the database.
Then create a screen that shows all the recently deleted notes.
From this screen, the user can undelete a note or permanently delete it.
```
Procure darle a OpenCode suficiente contexto para que entienda exactamente lo que necesita. Ayuda hablarle como si estuviera hablando con un desarrollador junior de su equipo.
Quiere darle a OpenCode suficientes detalles para entender lo que quiere. ayuda
hablar con él como si estuviera hablando con un desarrollador junior de su equipo.
:::tip
Déle a OpenCode todo el contexto y los ejemplos que pueda para ayudarle a comprender lo que desea.
Dale a OpenCode mucho contexto y ejemplos para ayudarlo a comprender lo que
desear.
:::
2. **Iterar sobre el plan**
2. **Repetir el plan**
Una vez que OpenCode le proponga un plan, puede darle comentarios o agregar más detalles.
Una vez que le proporcione un plan, puede enviarle comentarios o agregar más detalles.
```txt frame="none"
Queremos diseñar esta nueva pantalla usando un diseño que ya hemos usado antes.
[Imagen #1] Revise esta imagen y úsela como referencia.
We'd like to design this new screen using a design I've used before.
[Image #1] Take a look at this image and use it as a reference.
```
:::tip
Arrastre y suelte imágenes en la terminal para agregarlas al mensaje.
:::
OpenCode puede analizar cualquier imagen que usted le proporcione y añadirla al contexto del mensaje. Puede hacerlo arrastrando y soltando una imagen en la terminal.
OpenCode puede escanear cualquier imagen que le proporcione y agregarla al mensaje. Puede
Haga esto arrastrando y soltando una imagen en la terminal.
3. **Implementar la funcionalidad**
3. **Crea la función**
Cuando esté conforme con el plan, vuelva al modo _Build_ presionando de nuevo la tecla Tab.
Una vez que se sienta cómodo con el plan, vuelva al _Modo Build_
presionando la tecla **Tab** nuevamente.
```bash frame="none"
<TAB>
```
Luego, pídale que haga los cambios.
Y pidiéndole que haga los cambios.
```bash frame="none"
Perfecto. Continúe y realice los cambios.
Sounds good! Go ahead and make the changes.
```
---
### Realizar cambios
Para cambios más sencillos, puede pedirle a OpenCode que los implemente directamente, sin revisar antes un plan.
Para cambios más sencillos, puede pedirle a OpenCode que lo construya directamente.
sin tener que revisar el plan primero.
```txt frame="none" "@packages/functions/src/settings.ts" "@packages/functions/src/notes.ts"
Necesitamos agregar autenticación a la ruta /settings. Revise cómo se maneja esto
en la ruta /notes en @packages/functions/src/notes.ts e implemente
la misma lógica en @packages/functions/src/settings.ts.
We need to add authentication to the /settings route. Take a look at how this is
handled in the /notes route in @packages/functions/src/notes.ts and implement
the same logic in @packages/functions/src/settings.ts
```
Procure dar suficientes detalles para que OpenCode pueda tomar las decisiones correctas al hacer los cambios
Desea asegurarse de proporcionar una buena cantidad de detalles para que OpenCode tome la decisión correcta.
cambios.
---
### Deshacer cambios
Supongamos que le pide a OpenCode que haga algunos cambios.
Digamos que le pides a OpenCode que haga algunos cambios.
```txt frame="none" "@packages/functions/src/api/index.ts"
¿Puede refactorizar la funcn en @packages/functions/src/api/index.ts?
Can you refactor the function in @packages/functions/src/api/index.ts?
```
Pero luego se da cuenta de que no era lo que quería. Puede **deshacer** los cambios usando el comando `/undo`.
Pero te das cuenta de que no es lo que querías. Puedes **deshacer** los cambios
usando el comando `/undo`.
```bash frame="none"
/undo
```
OpenCode revertirá los cambios que hizo y volverá a mostrar su mensaje original.
OpenCode ahora revertirá los cambios que realizó y mostrará su mensaje original
de nuevo.
```txt frame="none" "@packages/functions/src/api/index.ts"
¿Puede refactorizar la funcn en @packages/functions/src/api/index.ts?
Can you refactor the function in @packages/functions/src/api/index.ts?
```
Desde ahí, puede modificar el mensaje y pedirle a OpenCode que lo intente de nuevo.
Desde aquí puedes modificar el mensaje y pedirle a OpenCode que vuelva a intentarlo.
:::tip
Puede ejecutar `/undo` varias veces para deshacer varios cambios.
:::
También puede rehacer los cambios usando el comando `/redo.`
O **puedes rehacer** los cambios usando el comando `/redo`.
```bash frame="none"
/redo
@@ -321,7 +335,7 @@ También puede rehacer los cambios usando el comando `/redo.`
## Compartir
Las conversaciones que tenga con OpenCode pueden [compartirse con su
Las conversaciones que tengas con OpenCode pueden ser [compartidas con tu
equipo](/docs/share).
```bash frame="none"
@@ -334,12 +348,12 @@ Esto creará un enlace a la conversación actual y lo copiará en su portapapele
Las conversaciones no se comparten de forma predeterminada.
:::
Aquí tiene una [conversación de ejemplo](https://opencode.ai/s/4XP1fce5) con OpenCode.
Aquí hay una [conversación de ejemplo](https://opencode.ai/s/4XP1fce5) con OpenCode.
---
## Personalizar
Y eso es todo. Ya conoce lo básico para empezar a usar OpenCode.
¡Y eso es todo! Ahora eres un profesional en el uso de OpenCode.
Para personalizarlo, recomendamos [elegir un tema](/docs/themes), [personalizar las combinaciones de teclas](/docs/keybinds), [configurar formateadores de código](/docs/formatters), [crear comandos personalizados](/docs/commands) o explorar la [configuración OpenCode](/docs/config).
Para personalizarlo, recomendamos [elegir un tema](/docs/themes), [personalizar las combinaciones de teclas](/docs/keybinds), [configurar formateadores de código](/docs/formatters), [crear comandos personalizados](/docs/commands) o jugar con la [configuración OpenCode](/docs/config).
+4 -2
View File
@@ -137,10 +137,12 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**.
| Kimi K2 Thinking | $0.40 | $2.50 | - | - |
| Kimi K2 | $0.40 | $2.50 | - | - |
| Qwen3 Coder 480B | $0.45 | $1.50 | - | - |
| Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 |
| Claude Opus 4.6 (≤ 200K tokens) | $5.00 | $25.00 | $0.50 | $6.25 |
| Claude Opus 4.6 (> 200K tokens) | $10.00 | $37.50 | $1.00 | $12.50 |
| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 |
| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 |
| Claude Sonnet 4.6 | $3.00 | $15.00 | $0.30 | $3.75 |
| Claude Sonnet 4.6 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 |
| Claude Sonnet 4.6 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 |
| Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 |
| Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 |
| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 |