Compare commits

...
5 Commits
12 changed files with 627 additions and 128 deletions
+26 -9
View File
@@ -64,6 +64,14 @@ const OpenAIChatTool = Schema.Struct({
})
type OpenAIChatTool = Schema.Schema.Type<typeof OpenAIChatTool>
// Gemini's OpenAI-compatible surface carries thought signatures in tool call
// `extra_content` and rejects replayed parallel calls without them:
// https://ai.google.dev/gemini-api/docs/thinking#signatures
const ExtraContent = Schema.Struct({
google: Schema.Struct({ thought_signature: Schema.String }),
})
const decodeExtraContent = (value: unknown) => Option.getOrUndefined(Schema.decodeUnknownOption(ExtraContent)(value))
const OpenAIChatAssistantToolCall = Schema.Struct({
id: Schema.String,
type: Schema.tag("function"),
@@ -71,6 +79,7 @@ const OpenAIChatAssistantToolCall = Schema.Struct({
name: Schema.String,
arguments: Schema.String,
}),
extra_content: Schema.optional(ExtraContent),
})
type OpenAIChatAssistantToolCall = Schema.Schema.Type<typeof OpenAIChatAssistantToolCall>
@@ -112,12 +121,6 @@ const decodeReasoningDetail = Schema.decodeUnknownOption(ReasoningDetail)
const knownReasoningDetails = (details: ReadonlyArray<unknown>) =>
details.flatMap((detail) => Option.toArray(decodeReasoningDetail(detail)))
// Intentionally omit Gemini's provider-specific `extra_content.google.thought_signature`
// extension until direct Google OpenAI-compatible routing is supported here:
// https://github.com/vercel/ai/issues/11590
// https://github.com/vercel/ai/pull/11745
// https://ai.google.dev/gemini-api/docs/thought-signatures#openai
const OpenAIChatUserContent = Schema.Union([
Schema.Struct({
type: Schema.Literal("text"),
@@ -242,6 +245,7 @@ const OpenAIChatToolCallDelta = Schema.Struct({
index: optionalNull(Schema.Number),
id: optionalNull(Schema.String),
function: optionalNull(OpenAIChatToolCallDeltaFunction),
extra_content: optionalNull(Schema.Unknown),
})
type OpenAIChatToolCallDelta = Schema.Schema.Type<typeof OpenAIChatToolCallDelta>
@@ -294,6 +298,7 @@ interface PendingToolDelta {
readonly id?: string
readonly name?: string
readonly input: string
readonly extraContent?: Schema.Schema.Type<typeof ExtraContent>
}
export interface ParserState {
@@ -347,13 +352,17 @@ const lowerToolChoice = (toolChoice: NonNullable<LLMRequest["toolChoice"]>) =>
tool: (name) => ({ type: "function" as const, function: { name } }),
})
const lowerToolCall = (part: ToolCallPart, options: LoweringOptions): OpenAIChatAssistantToolCall => ({
const lowerToolCall = (
part: ToolCallPart,
options: LoweringOptions & { readonly providerMetadataKey: string },
): OpenAIChatAssistantToolCall => ({
id: options.toolCallID?.(part.id) ?? part.id,
type: "function",
function: {
name: part.name,
arguments: ProviderShared.encodeJson(part.input === undefined ? {} : part.input),
},
extra_content: decodeExtraContent(part.providerMetadata?.[options.providerMetadataKey]?.extraContent),
})
const lowerMedia = Effect.fn("OpenAIChat.lowerMedia")(function* (part: MediaPart) {
@@ -721,7 +730,9 @@ const detectSupportsStore = (provider: string, baseURL: string | undefined): boo
p === "vercel-ai-gateway" || url.includes("ai-gateway.vercel.sh") || url.includes("vercel.sh")
const isAntLing = p === "ant-ling" || url.includes("api.ant-ling.com")
const isOpencode = p === "opencode" || url.includes("opencode.ai")
const isGemini = url.includes("generativelanguage.googleapis.com")
const isNonStandard =
isGemini ||
isNvidia ||
isCerebras ||
isXai ||
@@ -1114,12 +1125,13 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
const id = current?.id ?? pending?.id ?? (tool.id || undefined)
const name = current?.name ?? pending?.name ?? (tool.function?.name || undefined)
const text = `${pending?.input ?? ""}${tool.function?.arguments ?? ""}`
const extraContent = pending?.extraContent ?? decodeExtraContent(tool.extra_content)
latestToolIndex = index
nextToolIndex = Math.max(nextToolIndex, index + 1)
if (!current && (!id || !name)) {
pendingTools = {
...pendingTools,
[index]: { id: id || undefined, name: name || undefined, input: text },
[index]: { id: id || undefined, name: name || undefined, input: text, extraContent },
}
continue
}
@@ -1131,7 +1143,12 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
ADAPTER,
tools,
index,
{ id: id || undefined, name: name || undefined, text },
{
id: id || undefined,
name: name || undefined,
text,
providerMetadata: extraContent && { [state.providerMetadataKey]: { extraContent } },
},
"OpenAI Chat tool call delta is missing id or name",
)
if (ToolStream.isError(result))
@@ -147,7 +147,12 @@ export const appendOrStart = <K extends StreamKey>(
route: string,
tools: State<K>,
key: K,
delta: { readonly id?: string; readonly name?: string; readonly text: string },
delta: {
readonly id?: string
readonly name?: string
readonly text: string
readonly providerMetadata?: ProviderMetadata
},
missingToolMessage: string,
): AppendOutcome<K> | AIError => {
const current = tools[key]
@@ -161,7 +166,7 @@ export const appendOrStart = <K extends StreamKey>(
namespace: current?.namespace,
input: `${current?.input ?? ""}${delta.text}`,
providerExecuted: current?.providerExecuted,
providerMetadata: current?.providerMetadata,
providerMetadata: current?.providerMetadata ?? delta.providerMetadata,
}
if (current && delta.text.length === 0 && current.id === id && current.name === name)
return { tools, tool: current, events: [] }
@@ -0,0 +1,54 @@
{
"version": 1,
"metadata": {
"model": "gemini-3.8-flash",
"tags": [
"prefix:openai-compatible-chat",
"provider:google",
"protocol:openai-chat",
"tool",
"tool-loop",
"continuation"
],
"name": "gemini-parallel-tool-signatures",
"recordedAt": "2026-09-28T03:12:05.083Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"gemini-3.8-flash\",\"messages\":[{\"role\":\"system\",\"content\":\"Call get_weather for every requested city in parallel, then answer in one short sentence.\"},{\"role\":\"user\",\"content\":\"What is the weather in Paris and in Tokyo?\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}}],\"stream\":true,\"stream_options\":{\"include_usage\":true}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream"
},
"body": "data: {\"choices\":[{\"delta\":{\"role\":\"assistant\",\"tool_calls\":[{\"extra_content\":{\"google\":{\"thought_signature\":\"ErYCCrMCAWkUfRNGh+/Zbc8YUSzk1yfWAfROcjA4HyF1x69jz6167w8zd4n6kZQQ5FDeBZ5HZMEbEkQ4ENOpzsQL8roCR6wONkhXpiduWrTD6XwbP8KGNkf6D1tX/JlBh7G5Cl+0rdjiSOl/mdY1lcjbkfyCRFs5T8odNWMG7WD3rCrXJDFQ/5QfOl+tqVTceKGz2yyXBhOhvsQDU33ulR9tHQJo/Fmx3HNyDdwvmyKUXm+kgqHsYZnkdv6y6xZwy9zBXGUO4QwxelMw3Rrc24Mp2rNbEDZS3YEeP72Jn/hIP5a8XWOx6+zop/4/CjYxxSfN/tJRY5d48NAKRNFzUN1Az8SvAs/N5X2I3/5ZMaDnQWW/BVdS9fm2KFYJ2baqtBiRRnJxMq4ELArkKjGZzHpd97XG8YjV6g==\"}},\"function\":{\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\",\"name\":\"get_weather\"},\"id\":\"call_723181\",\"type\":\"function\"}]},\"index\":0}],\"created\":1790565123,\"id\":\"A9u5aoufBp3rz7IPke_3oAo\",\"model\":\"gemini-3.8-flash\",\"object\":\"chat.completion.chunk\",\"usage\":{\"completion_tokens\":16,\"prompt_tokens\":74,\"total_tokens\":132}}\n\ndata: {\"choices\":[{\"delta\":{\"role\":\"assistant\",\"tool_calls\":[{\"function\":{\"arguments\":\"{\\\"city\\\":\\\"Tokyo\\\"}\",\"name\":\"get_weather\"},\"id\":\"call_723184\",\"type\":\"function\"}]},\"index\":0}],\"created\":1790565123,\"id\":\"A9u5aoufBp3rz7IPke_3oAo\",\"model\":\"gemini-3.8-flash\",\"object\":\"chat.completion.chunk\",\"usage\":{\"completion_tokens\":32,\"prompt_tokens\":74,\"total_tokens\":148}}\n\ndata: {\"choices\":[{\"delta\":{\"role\":\"assistant\"},\"finish_reason\":\"stop\",\"index\":0}],\"created\":1790565124,\"id\":\"A9u5aoufBp3rz7IPke_3oAo\",\"model\":\"gemini-3.8-flash\",\"object\":\"chat.completion.chunk\",\"usage\":{\"completion_tokens\":32,\"prompt_tokens\":74,\"total_tokens\":148}}\n\ndata: [DONE]\n\n"
}
},
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"gemini-3.8-flash\",\"messages\":[{\"role\":\"system\",\"content\":\"Call get_weather for every requested city in parallel, then answer in one short sentence.\"},{\"role\":\"user\",\"content\":\"What is the weather in Paris and in Tokyo?\"},{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"id\":\"call_723181\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"},\"extra_content\":{\"google\":{\"thought_signature\":\"ErYCCrMCAWkUfRNGh+/Zbc8YUSzk1yfWAfROcjA4HyF1x69jz6167w8zd4n6kZQQ5FDeBZ5HZMEbEkQ4ENOpzsQL8roCR6wONkhXpiduWrTD6XwbP8KGNkf6D1tX/JlBh7G5Cl+0rdjiSOl/mdY1lcjbkfyCRFs5T8odNWMG7WD3rCrXJDFQ/5QfOl+tqVTceKGz2yyXBhOhvsQDU33ulR9tHQJo/Fmx3HNyDdwvmyKUXm+kgqHsYZnkdv6y6xZwy9zBXGUO4QwxelMw3Rrc24Mp2rNbEDZS3YEeP72Jn/hIP5a8XWOx6+zop/4/CjYxxSfN/tJRY5d48NAKRNFzUN1Az8SvAs/N5X2I3/5ZMaDnQWW/BVdS9fm2KFYJ2baqtBiRRnJxMq4ELArkKjGZzHpd97XG8YjV6g==\"}}},{\"id\":\"call_723184\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\":\\\"Tokyo\\\"}\"}}]},{\"role\":\"tool\",\"tool_call_id\":\"call_723181\",\"content\":\"{\\\"temperature\\\":22,\\\"condition\\\":\\\"sunny\\\"}\"},{\"role\":\"tool\",\"tool_call_id\":\"call_723184\",\"content\":\"{\\\"temperature\\\":0,\\\"condition\\\":\\\"unknown\\\"}\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}}],\"stream\":true,\"stream_options\":{\"include_usage\":true}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream"
},
"body": "data: {\"choices\":[{\"delta\":{\"content\":\"It is currently sunny and 22°C in Paris,\",\"role\":\"assistant\"},\"index\":0}],\"created\":1790565125,\"id\":\"BNu5auWeB-2fz7IPxY6l4QY\",\"model\":\"gemini-3.8-flash\",\"object\":\"chat.completion.chunk\",\"usage\":{\"completion_tokens\":13,\"prompt_tokens\":150,\"total_tokens\":226}}\n\ndata: {\"choices\":[{\"delta\":{\"content\":\" while Tokyo is 0°C.\",\"role\":\"assistant\"},\"index\":0}],\"created\":1790565125,\"id\":\"BNu5auWeB-2fz7IPxY6l4QY\",\"model\":\"gemini-3.8-flash\",\"object\":\"chat.completion.chunk\",\"usage\":{\"completion_tokens\":21,\"prompt_tokens\":150,\"total_tokens\":234}}\n\ndata: {\"choices\":[{\"delta\":{\"extra_content\":{\"google\":{\"thought_signature\":\"EpcDCpQDAWkUfRM325TAUtfFiOeQIEWn/TsCU9oi2js4VPHFeeLfAu+2k8PJm0fN/OaF0y4ovau7S9QIAsuOPI2w2aIyQ2kMGj1XvUyRTvz30DOZgtq1km6W6YGzZyyTCNSeBcpwtJtziHZVVWq9xEI/HHB8Ta1Ot215xnFyDL7iUGEwgGu45/mInpk+SOCYBy9biDddpDxcDi14BGoleArY9XEFAzYLxXssl7HMWjpfee5095im7gD125Nripq1Jf3nGY/2TxqjgQAdJpQybwct63p74O1szGHQxrkBt7AwphDgbOWtLpUP/QJBRdl8qhrozqRe611NQ6V5lMSwpO7OhQ/IDRtWMwOyrrKblZfmMnnPl2/9xDfZRsYnfmWq+7PeAptJl1cDRlMBKhj5iRn31xvN43EiuvWwWPsSndiWvxrMvVBd89TR4u0+z0pYCYZcsFkYKFlPA7pZsdnh6CON24AA5WUkO4dQNoqYdik6aO5hE9jLHG5FHTdr0W69qgPNozbxnO0ptRcOtkGpB9xYUVoTxcSXXZE=\"}},\"role\":\"assistant\"},\"finish_reason\":\"stop\",\"index\":0}],\"created\":1790565125,\"id\":\"BNu5auWeB-2fz7IPxY6l4QY\",\"model\":\"gemini-3.8-flash\",\"object\":\"chat.completion.chunk\",\"usage\":{\"completion_tokens\":21,\"prompt_tokens\":192,\"total_tokens\":276}}\n\ndata: [DONE]\n\n"
}
}
]
}
@@ -0,0 +1,68 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { LLM, LLMEvent, LLMRequest, Message, ToolRuntime, toDefinitions } from "../../src/index.js"
import * as OpenAICompatible from "../../src/providers/openai-compatible.js"
import { LLMClient } from "../../src/route.js"
import { compileRequest } from "../../src/route/client.js"
import { recordedTests } from "../recorded-test.js"
import { weatherRuntimeTool, weatherToolName } from "../recorded-scenarios.js"
const model = OpenAICompatible.configure({
provider: "google",
baseURL: "https://generativelanguage.googleapis.com/v1beta/openai",
apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY ?? "fixture",
}).model("gemini-3.8-flash")
const recorded = recordedTests({
prefix: "openai-compatible-chat",
provider: "google",
protocol: "openai-chat",
requires: ["GOOGLE_GENERATIVE_AI_API_KEY"],
tags: ["tool", "tool-loop", "continuation"],
metadata: { model: model.id },
})
describe("Gemini OpenAI-compatible Chat recorded", () => {
recorded.effect.with(
"replays thought signatures through a parallel tool loop",
{ cassette: "openai-compatible-chat/gemini-parallel-tool-signatures" },
() =>
Effect.gen(function* () {
const tools = { [weatherToolName]: weatherRuntimeTool }
const request = LLM.request({
model,
system: "Call get_weather for every requested city in parallel, then answer in one short sentence.",
prompt: "What is the weather in Paris and in Tokyo?",
tools: toDefinitions(tools),
cache: "none",
})
const first = yield* LLMClient.generate(request)
const calls = first.events.filter(LLMEvent.is.toolCall)
expect(calls.map((call) => call.input)).toEqual([{ city: "Paris" }, { city: "Tokyo" }])
const extraContent = calls[0]?.providerMetadata?.google?.extraContent
expect(extraContent).toEqual({ google: { thought_signature: expect.any(String) } })
const results = yield* Effect.forEach(calls, (call) => ToolRuntime.dispatch(tools, call))
const continuation = LLMRequest.update(request, {
messages: [
...request.messages,
first.message,
...calls.map((call, index) =>
Message.tool({ id: call.id, name: call.name, result: results[index]!.result }),
),
],
})
const prepared = yield* compileRequest(continuation)
const assistant = prepared.body.messages.find((message) => message.role === "assistant")
expect(assistant?.role === "assistant" ? assistant.tool_calls?.[0]?.extra_content : undefined).toEqual(
extraContent,
)
const second = yield* LLMClient.generate(continuation)
expect(second.events.filter(LLMEvent.is.toolCall)).toHaveLength(0)
expect(second.text).toMatch(/Paris/)
expect(second.text).toMatch(/Tokyo/)
}),
60_000,
)
})
@@ -472,6 +472,45 @@ describe("OpenAI Chat route", () => {
}),
)
it.effect("replays Gemini thought signatures as tool call extra content", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
Message.user("Weather in Paris and Tokyo?"),
Message.assistant([
ToolCallPart.make({
id: "call_1",
name: "lookup",
input: { city: "Paris" },
providerMetadata: { openai: { extraContent: { google: { thought_signature: "sig_1" } } } },
}),
ToolCallPart.make({ id: "call_2", name: "lookup", input: { city: "Tokyo" } }),
]),
Message.tool({ id: "call_1", name: "lookup", result: "Sunny" }),
Message.tool({ id: "call_2", name: "lookup", result: "Rainy" }),
],
}),
)
const assistant = prepared.body.messages[1]
expect(assistant?.role === "assistant" ? assistant.tool_calls : undefined).toEqual([
{
id: "call_1",
type: "function",
function: { name: "lookup", arguments: encodeJson({ city: "Paris" }) },
extra_content: { google: { thought_signature: "sig_1" } },
},
{
id: "call_2",
type: "function",
function: { name: "lookup", arguments: encodeJson({ city: "Tokyo" }) },
},
])
}),
)
it.effect("limits OpenAI and Azure Chat tool call IDs to 40 characters", () =>
Effect.gen(function* () {
const id = `call_${"a".repeat(48)}`
@@ -1805,6 +1844,78 @@ describe("OpenAI Chat route", () => {
}),
)
it.effect("preserves Gemini thought signatures on streamed parallel tool calls", () =>
Effect.gen(function* () {
// Gemini's OpenAI-compatible endpoint omits `index`, streams each call whole,
// and signs only the first call of a parallel batch.
const body = sseEvents(
deltaChunk({
role: "assistant",
tool_calls: [
{
extra_content: { google: { thought_signature: "sig_1" } },
id: "call_1",
type: "function",
function: { name: "lookup", arguments: '{"city":"Paris"}' },
},
],
}),
deltaChunk({
role: "assistant",
tool_calls: [{ id: "call_2", type: "function", function: { name: "lookup", arguments: '{"city":"Tokyo"}' } }],
}),
deltaChunk({}, "stop"),
)
const response = yield* LLMClient.generate(
LLMRequest.update(request, {
tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })],
}),
).pipe(Effect.provide(fixedResponse(body)))
expect(response.events.filter(LLMEvent.is.toolCall)).toEqual([
{
type: "tool-call",
id: "call_1",
name: "lookup",
input: { city: "Paris" },
providerExecuted: undefined,
providerMetadata: { openai: { extraContent: { google: { thought_signature: "sig_1" } } } },
},
{
type: "tool-call",
id: "call_2",
name: "lookup",
input: { city: "Tokyo" },
providerExecuted: undefined,
providerMetadata: undefined,
},
])
}),
)
it.effect("keeps extra content that arrives before the tool identity", () =>
Effect.gen(function* () {
const body = sseEvents(
deltaChunk({
tool_calls: [
{ index: 0, extra_content: { google: { thought_signature: "sig_1" } }, function: { arguments: "{" } },
],
}),
deltaChunk({ tool_calls: [{ index: 0, id: "call_1", function: { name: "lookup", arguments: "}" } }] }),
deltaChunk({}, "tool_calls"),
)
const response = yield* LLMClient.generate(
LLMRequest.update(request, {
tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })],
}),
).pipe(Effect.provide(fixedResponse(body)))
expect(response.events.filter(LLMEvent.is.toolCall).map((event) => event.providerMetadata)).toEqual([
{ openai: { extraContent: { google: { thought_signature: "sig_1" } } } },
])
}),
)
it.effect("does not finalize streamed tool calls when content is filtered", () =>
Effect.gen(function* () {
const body = sseEvents(
+2 -1
View File
@@ -145,8 +145,9 @@ function parts(input: string) {
.filter(Boolean)
}
// cachePath makes each `:`-separated host part a directory.
function safeHost(input: string) {
return Boolean(input) && !input.startsWith("-") && !/[\s/\\]/.test(input)
return Boolean(input) && !input.startsWith("-") && input.split(":").every(safeSegment)
}
function safeSegment(input: string) {
+18
View File
@@ -41,6 +41,12 @@ describe("Repository", () => {
})
})
test("caches a host with a port under a directory per host part", () => {
expect(Repository.cachePath("/cache", Repository.parseRemote("ssh://git@example.com:2222/owner/repo"))).toBe(
path.join("/cache", "example.com", "2222", "owner", "repo"),
)
})
test("keeps local file repositories distinct from remote repositories", () => {
const localPath = path.resolve("repo.git")
const reference = Repository.parse(pathToFileURL(localPath).href)
@@ -62,6 +68,18 @@ describe("Repository", () => {
expect(() => Repository.validateBranch("bad branch")).toThrow(Repository.InvalidBranchError)
})
test.each([
"..:repo",
"git@..:repo",
"../owner/repo",
"ssh://../repo",
"ssh://..:22/repo",
"https://%2e%2e/owner/repo",
".:repo",
])("rejects %s because its host contains a relative path segment", (input) => {
expect(() => Repository.parseRemote(input)).toThrow(Repository.InvalidReferenceError)
})
test("compares cache identity independent of input spelling", () => {
const shorthand = Repository.parseRemote("owner/repo")
+1 -1
View File
@@ -2808,7 +2808,7 @@ function Shell(props: ToolProps) {
command={stringValue(props.input.command)}
workdir={stringValue(props.input.workdir)}
status={props.part.state.status}
background={Boolean(stringValue(props.metadata.shellID)) && props.part.state.status !== "running"}
background={props.part.state.status === "completed" && props.metadata.status === "running"}
output={stringValue(props.metadata.shellID) ? undefined : props.output}
/>
)
@@ -5,6 +5,7 @@ import Card from "./Card.astro"
import CardGroup from "./CardGroup.astro"
import CodeBlock from "./CodeBlock.astro"
import CodeTabs from "./CodeTabs.astro"
import PlanTabs from "./PlanTabs.astro"
import DocsLayout from "../layouts/DocsLayout.astro"
interface Props {
@@ -22,5 +23,5 @@ const rendered = await render(entry)
headings={rendered.headings}
showTableOfContents={entry.data.tableOfContents !== false}
>
<rendered.Content components={{ Callout, Card, CardGroup, CodeBlock, CodeTabs }} />
<rendered.Content components={{ Callout, Card, CardGroup, CodeBlock, CodeTabs, PlanTabs }} />
</DocsLayout>
@@ -0,0 +1,103 @@
---
interface Props {
id: string
label: string
syncKey: string
}
const plans = [
{ id: "go", label: "Go" },
{ id: "go-plus", label: "Go Plus" },
] as const
---
<div class="docs-plan-tabs" data-plan-tabs data-sync-key={Astro.props.syncKey}>
<div class="docs-plan-tabs-list" role="tablist" aria-label={Astro.props.label}>
{
plans.map((plan, index) => (
<button
type="button"
role="tab"
id={`${Astro.props.id}-tab-${plan.id}`}
aria-controls={`${Astro.props.id}-panel-${plan.id}`}
aria-selected={index === 0 ? "true" : "false"}
tabindex={index === 0 ? 0 : -1}
data-plan-tab={plan.id}
>
{plan.label}
</button>
))
}
</div>
{
plans.map((plan, index) => (
<div
role="tabpanel"
id={`${Astro.props.id}-panel-${plan.id}`}
aria-labelledby={`${Astro.props.id}-tab-${plan.id}`}
hidden={index !== 0}
data-plan-panel={plan.id}
>
<slot name={plan.id} />
</div>
))
}
</div>
<script>
const selectPlan = (tabs: HTMLElement, plan: string) => {
tabs.querySelectorAll<HTMLButtonElement>("[data-plan-tab]").forEach((button) => {
const selected = button.dataset.planTab === plan
button.setAttribute("aria-selected", String(selected))
button.tabIndex = selected ? 0 : -1
})
tabs.querySelectorAll<HTMLElement>("[data-plan-panel]").forEach((panel) => {
panel.hidden = panel.dataset.planPanel !== plan
})
}
const selectSyncedPlan = (source: HTMLElement, plan: string) => {
const syncKey = source.dataset.syncKey
if (!syncKey) return
document.querySelectorAll<HTMLElement>("[data-plan-tabs]").forEach((tabs) => {
if (tabs.dataset.syncKey === syncKey) selectPlan(tabs, plan)
})
localStorage.setItem(`docs-plan-tabs:${syncKey}`, plan)
}
document.querySelectorAll<HTMLElement>("[data-plan-tabs]").forEach((tabs) => {
const syncKey = tabs.dataset.syncKey
const plan = syncKey ? localStorage.getItem(`docs-plan-tabs:${syncKey}`) : undefined
if (plan) selectPlan(tabs, plan)
})
document.addEventListener("click", (event) => {
if (!(event.target instanceof Element)) return
const button = event.target.closest<HTMLButtonElement>("[data-plan-tab]")
const tabs = button?.closest<HTMLElement>("[data-plan-tabs]")
if (!button || !tabs || !button.dataset.planTab) return
selectSyncedPlan(tabs, button.dataset.planTab)
})
document.addEventListener("keydown", (event) => {
if (!(event.target instanceof HTMLButtonElement) || !event.target.matches("[data-plan-tab]")) return
const tabs = event.target.closest<HTMLElement>("[data-plan-tabs]")
if (!tabs) return
const buttons = [...tabs.querySelectorAll<HTMLButtonElement>("[data-plan-tab]")]
const selected = buttons.indexOf(event.target)
const next =
event.key === "Home"
? buttons[0]
: event.key === "End"
? buttons.at(-1)
: event.key === "ArrowRight"
? buttons[(selected + 1) % buttons.length]
: event.key === "ArrowLeft"
? buttons[(selected - 1 + buttons.length) % buttons.length]
: undefined
if (!next?.dataset.planTab) return
event.preventDefault()
selectSyncedPlan(tabs, next.dataset.planTab)
next.focus()
})
</script>
+193 -114
View File
@@ -1,17 +1,22 @@
---
title: "Go"
description: "Low cost subscription for open coding models."
description: "Reliable access to open coding models with two usage tiers."
---
OpenCode Go is a low cost **$10/month subscription** that gives you reliable access to popular open coding models.
OpenCode Go gives you reliable access to popular open coding models, with two monthly plans:
| Plan | Price | Included usage |
| ---- | ----- | -------------- |
| **Go** | **$10/month** | Lower-cost access to the models below |
| **Go Plus** | **$40/month** | Higher usage limits across the models below |
Go works like any other provider in OpenCode. You subscribe to OpenCode Go and get your API key. It's **completely optional** and you don't need it to use OpenCode.
It is designed primarily for international users and provides stable global access.
The service is designed primarily for international users and provides stable global access.
## How it works
1. Sign in to the [OpenCode console](https://opencode.ai/console), subscribe to Go, add your billing details, and copy your API key.
1. Sign in to the [OpenCode Console](https://opencode.ai/console), subscribe to Go or Go Plus, add your billing details, and copy your API key.
2. Run `/connect` in the TUI, select **OpenCode Go**, and paste your API key.
```text
@@ -24,7 +29,7 @@ It is designed primarily for international users and provides stable global acce
/models
```
<Callout>Only one member per workspace can subscribe to OpenCode Go.</Callout>
<Callout>Only one member per workspace can subscribe to OpenCode Go or Go Plus.</Callout>
The current list of models includes:
@@ -33,13 +38,13 @@ The current list of models includes:
- **GLM-5.3-Flash**
- **GLM-5.3**
- **GLM-5.2**
- **GLM-5.1**
- **GPT 6 Luna**
- **GPT 5.6 Luna**
- **Kimi K3**
- **Kimi K2.7 Code**
- **Kimi K2.6**
- **LongCat-2.0**
- **LongCat 2.5 Preview Free** (limited time)
- **MiMo-V2.6-Flash**
- **MiMo-V2.6-Pro**
- **MiMo-V2.5**
@@ -50,9 +55,7 @@ The current list of models includes:
- **Muse Spark 1.2 Contributor** ([limited regions](https://ai.developer.meta.com/legal/geographic-use-policy))
- **Qwen3.8 Max**
- **Qwen3.8 Flash**
- **Qwen3.7 Max**
- **Qwen3.7 Plus**
- **Qwen3.6 Plus**
- **DeepSeek V4.1 Flash**
- **DeepSeek V4 Pro**
- **DeepSeek V4 Flash**
@@ -60,7 +63,6 @@ The current list of models includes:
- **Hy4 preview**
- **Hy3**
- **Space Bunny Free** (limited time)
- **LongCat 2.5 Preview Free** (limited time)
The list of models may change as we test and add new ones.
@@ -109,127 +111,212 @@ investigated. The linked reports track fixes and workarounds.
## Usage limits
Usage limits are defined as monthly dollar amounts. The table below shows the
monthly limit and token costs for each model.
monthly limit for each plan and the token costs for each model. Token pricing is
the same for Go and Go Plus.
Each model has the following usage limits: 5-hour — 20% of the monthly limit;
weekly — 50%; and monthly — 100%.
For example, if a model has a $60 monthly limit, you can spend up to:
For example, if a model has a $60 monthly limit on Go and a $120 monthly limit
on Go Plus, you can spend up to:
- **5-hour limit** — $12 of usage
- **Weekly limit** — $30 of usage
- **Monthly limit** — $60 of usage
- **5-hour limit** — $12 on Go or $24 on Go Plus
- **Weekly limit** — $30 on Go or $60 on Go Plus
- **Monthly limit** — $60 on Go or $120 on Go Plus
Across models, Go has $12 five-hour, $30 weekly, and $60 monthly allowances;
Go Plus has $48 five-hour, $120 weekly, and $240 monthly allowances. Each
model's monthly limit below determines how its usage counts toward those
allowances.
Token prices are per 1M tokens.
<div class="docs-table-scroll" role="region" aria-label="Go model pricing" tabIndex={0}>
<PlanTabs id="go-pricing" label="Go plan" syncKey="go-plan">
<div slot="go">
| Model | Input | Output | Cached Read | Cached Write | Monthly limit |
| --------------------------------------- | ------ | ------ | ----------- | ------------ | ---------------------------------------------------- |
| GLM-5.3-Flash | $0.15 | $0.50 | $0.03 | - | **$60** |
| GLM-5.3 | $1.40 | $4.40 | $0.26 | - | **$15** |
| GLM-5.2 | $1.40 | $4.40 | $0.26 | - | **$60** |
| GLM-5.1 | $1.40 | $4.40 | $0.26 | - | **$60** |
| Kimi K3 | $3.00 | $15.00 | $0.30 | - | **$15** |
| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | **$60** |
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | **$60** |
| LongCat-2.0 | $0.30 | $1.20 | $0.006 | - | **$60** |
| MiMo-V2.6-Flash | $0.14 | $0.28 | $0.0028 | - | **$60** |
| MiMo-V2.6-Pro | $0.435 | $0.87 | $0.003625 | - | **$15** |
| MiMo-V2.5 | $0.14 | $0.28 | $0.0028 | - | **$60** |
| MiMo-V2.5-Pro | $0.435 | $0.87 | $0.003625 | - | **$15** |
| MiniMax M3 | $0.30 | $1.20 | $0.06 | - | **$60** |
| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | **$60** |
| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | **$60** |
| Muse Spark 1.3 Contributor | $0.10 | $0.20 | $0.002 | - | **$60** |
| Muse Spark 1.2 Contributor | $0.10 | $0.20 | $0.002 | - | **$60** |
| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | **$15** |
| Qwen3.8 Flash | $0.15 | $0.47 | $0.016 | $0.20 | **$30** |
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | **$30** |
| Qwen3.7 Plus (≤ 256K tokens) | $0.40 | $1.60 | $0.04 | $0.50 | **$60** |
| Qwen3.7 Plus (> 256K tokens) | $1.20 | $4.80 | $0.12 | $1.50 | **$60** |
| Qwen3.6 Plus (≤ 256K tokens) | $0.50 | $3.00 | $0.05 | $0.625 | **$60** |
| Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | **$60** |
| DeepSeek V4.1 Flash (Off-Peak) | $0.15 | $0.60 | $0.003 | - | **$60** |
| DeepSeek V4.1 Flash (Peak) | $0.30 | $1.20 | $0.006 | - | **$60** |
| DeepSeek V4 Pro (Off-Peak) | $0.66 | $1.98 | $0.022 | - | **$15** |
| DeepSeek V4 Pro (Peak) | $1.32 | $3.96 | $0.044 | - | **$15** |
| DeepSeek V4 Flash (Off-Peak) | $0.15 | $0.60 | $0.003 | - | **$30** |
| DeepSeek V4 Flash (Peak) | $0.30 | $1.20 | $0.006 | - | **$30** |
| DeepSeek V4 Flash Vision Exp (Off-Peak) | $0.15 | $0.60 | $0.003 | - | **$15** |
| DeepSeek V4 Flash Vision Exp (Peak) | $0.30 | $1.20 | $0.006 | - | **$15** |
| Hy4 preview | $0.834 | $2.501 | $0.042 | - | **$30** |
| Hy3 | $0.14 | $0.58 | $0.035 | - | **$60** |
| Space Bunny Free | Free | Free | Free | - | **Unlimited**<br /><small>limited time</small> |
| LongCat 2.5 Preview Free | Free | Free | Free | - | **Unlimited**<br /><small>limited time</small> |
| Grok 4.7 (≤ 200K tokens) | $2.00 | $6.00 | $0.50 | - | **$15** |
| Grok 4.7 (> 200K tokens) | $4.00 | $12.00 | $1.00 | - | **$15** |
| Grok 4.6 (≤ 200K tokens) | $2.00 | $6.00 | $0.50 | - | **$15** |
| Grok 4.6 (> 200K tokens) | $4.00 | $12.00 | $1.00 | - | **$15** |
| GPT 6 Luna (≤ 272K tokens) | $0.10 | $0.50 | $0.01 | $0.125 | **$15** |
| GPT 6 Luna (> 272K tokens) | $0.20 | $0.75 | $0.02 | $0.25 | **$15** |
| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | **$15** |
| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | **$15** |
| Model | Input | Output | Cached Read | Cached Write | Monthly limit |
| --------------------------------------- | ------ | ------ | ----------- | ------------ | ---------------------------------------------- |
| GLM-5.3-Flash | $0.15 | $0.50 | $0.03 | - | **$60** |
| GLM-5.3 | $1.40 | $4.40 | $0.26 | - | **$15** |
| GLM-5.2 | $1.40 | $4.40 | $0.26 | - | **$60** |
| Kimi K3 | $3.00 | $15.00 | $0.30 | - | **$15** |
| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | **$60** |
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | **$60** |
| LongCat-2.0 | $0.30 | $1.20 | $0.006 | - | **$60** |
| LongCat 2.5 Preview Free | Free | Free | Free | - | **Unlimited**<br /><small>limited time</small> |
| MiMo-V2.6-Flash | $0.14 | $0.28 | $0.0028 | - | **$60** |
| MiMo-V2.6-Pro | $0.435 | $0.87 | $0.003625 | - | **$15** |
| MiMo-V2.5 | $0.14 | $0.28 | $0.0028 | - | **$60** |
| MiMo-V2.5-Pro | $0.435 | $0.87 | $0.003625 | - | **$15** |
| MiniMax M3 | $0.30 | $1.20 | $0.06 | - | **$60** |
| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | **$60** |
| Muse Spark 1.3 Contributor | $0.10 | $0.20 | $0.002 | - | **$60** |
| Muse Spark 1.2 Contributor | $0.10 | $0.20 | $0.002 | - | **$60** |
| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | **$15** |
| Qwen3.8 Flash | $0.15 | $0.47 | $0.016 | $0.20 | **$30** |
| Qwen3.7 Plus (≤ 256K tokens) | $0.40 | $1.60 | $0.04 | $0.50 | **$60** |
| Qwen3.7 Plus (> 256K tokens) | $1.20 | $4.80 | $0.12 | $1.50 | **$60** |
| DeepSeek V4.1 Flash (Off-Peak) | $0.15 | $0.60 | $0.003 | - | **$60** |
| DeepSeek V4.1 Flash (Peak) | $0.30 | $1.20 | $0.006 | - | **$60** |
| DeepSeek V4 Pro (Off-Peak) | $0.66 | $1.98 | $0.022 | - | **$15** |
| DeepSeek V4 Pro (Peak) | $1.32 | $3.96 | $0.044 | - | **$15** |
| DeepSeek V4 Flash (Off-Peak) | $0.15 | $0.60 | $0.003 | - | **$30** |
| DeepSeek V4 Flash (Peak) | $0.30 | $1.20 | $0.006 | - | **$30** |
| DeepSeek V4 Flash Vision Exp (Off-Peak) | $0.15 | $0.60 | $0.003 | - | **$15** |
| DeepSeek V4 Flash Vision Exp (Peak) | $0.30 | $1.20 | $0.006 | - | **$15** |
| Hy4 preview | $0.834 | $2.501 | $0.042 | - | **$30** |
| Hy3 | $0.14 | $0.58 | $0.035 | - | **$60** |
| Space Bunny Free | Free | Free | Free | - | **Unlimited**<br /><small>limited time</small> |
| Grok 4.7 (≤ 200K tokens) | $2.00 | $6.00 | $0.50 | - | **$15** |
| Grok 4.7 (> 200K tokens) | $4.00 | $12.00 | $1.00 | - | **$15** |
| Grok 4.6 (≤ 200K tokens) | $2.00 | $6.00 | $0.50 | - | **$15** |
| Grok 4.6 (> 200K tokens) | $4.00 | $12.00 | $1.00 | - | **$15** |
| GPT 6 Luna (≤ 272K tokens) | $0.10 | $0.50 | $0.01 | $0.125 | **$15** |
| GPT 6 Luna (> 272K tokens) | $0.20 | $0.75 | $0.02 | $0.25 | **$15** |
| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | **$15** |
| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | **$15** |
</div>
</div>
<div slot="go-plus">
| Model | Input | Output | Cached Read | Cached Write | Monthly limit |
| --------------------------------------- | ------ | ------ | ----------- | ------------ | ---------------------------------------------- |
| GLM-5.3-Flash | $0.15 | $0.50 | $0.03 | - | **$180** |
| GLM-5.3 | $1.40 | $4.40 | $0.26 | - | **$120** |
| GLM-5.2 | $1.40 | $4.40 | $0.26 | - | **$180** |
| Kimi K3 | $3.00 | $15.00 | $0.30 | - | **$60** |
| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | **$180** |
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | **$240** |
| LongCat-2.0 | $0.30 | $1.20 | $0.006 | - | **$240** |
| LongCat 2.5 Preview Free | Free | Free | Free | - | **Unlimited**<br /><small>limited time</small> |
| MiMo-V2.6-Flash | $0.14 | $0.28 | $0.0028 | - | **$120** |
| MiMo-V2.6-Pro | $0.435 | $0.87 | $0.003625 | - | **$60** |
| MiMo-V2.5 | $0.14 | $0.28 | $0.0028 | - | **$120** |
| MiMo-V2.5-Pro | $0.435 | $0.87 | $0.003625 | - | **$60** |
| MiniMax M3 | $0.30 | $1.20 | $0.06 | - | **$180** |
| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | **$240** |
| Muse Spark 1.3 Contributor | $0.10 | $0.20 | $0.002 | - | **$120** |
| Muse Spark 1.2 Contributor | $0.10 | $0.20 | $0.002 | - | **$120** |
| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | **$60** |
| Qwen3.8 Flash | $0.15 | $0.47 | $0.016 | $0.20 | **$90** |
| Qwen3.7 Plus (≤ 256K tokens) | $0.40 | $1.60 | $0.04 | $0.50 | **$180** |
| Qwen3.7 Plus (> 256K tokens) | $1.20 | $4.80 | $0.12 | $1.50 | **$180** |
| DeepSeek V4.1 Flash (Off-Peak) | $0.15 | $0.60 | $0.003 | - | **$120** |
| DeepSeek V4.1 Flash (Peak) | $0.30 | $1.20 | $0.006 | - | **$120** |
| DeepSeek V4 Pro (Off-Peak) | $0.66 | $1.98 | $0.022 | - | **$60** |
| DeepSeek V4 Pro (Peak) | $1.32 | $3.96 | $0.044 | - | **$60** |
| DeepSeek V4 Flash (Off-Peak) | $0.15 | $0.60 | $0.003 | - | **$120** |
| DeepSeek V4 Flash (Peak) | $0.30 | $1.20 | $0.006 | - | **$120** |
| DeepSeek V4 Flash Vision Exp (Off-Peak) | $0.15 | $0.60 | $0.003 | - | **$60** |
| DeepSeek V4 Flash Vision Exp (Peak) | $0.30 | $1.20 | $0.006 | - | **$60** |
| Hy4 preview | $0.834 | $2.501 | $0.042 | - | **$120** |
| Hy3 | $0.14 | $0.58 | $0.035 | - | **$240** |
| Space Bunny Free | Free | Free | Free | - | **Unlimited**<br /><small>limited time</small> |
| Grok 4.7 (≤ 200K tokens) | $2.00 | $6.00 | $0.50 | - | **$60** |
| Grok 4.7 (> 200K tokens) | $4.00 | $12.00 | $1.00 | - | **$60** |
| Grok 4.6 (≤ 200K tokens) | $2.00 | $6.00 | $0.50 | - | **$60** |
| Grok 4.6 (> 200K tokens) | $4.00 | $12.00 | $1.00 | - | **$60** |
| GPT 6 Luna (≤ 272K tokens) | $0.10 | $0.50 | $0.01 | $0.125 | **$60** |
| GPT 6 Luna (> 272K tokens) | $0.20 | $0.75 | $0.02 | $0.25 | **$60** |
| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | **$60** |
| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | **$60** |
</div>
</PlanTabs>
**Space Bunny Free:** Free for a limited time.
**LongCat 2.5 Preview Free:** Free for a limited time.
**DeepSeek V4.1 Flash / V4 Pro / V4 Flash Vision Exp:** Peak hours are 01:00-04:00 and 06:00-10:00 UTC, Monday through Friday; all other hours, including weekends, are Off-Peak. [Learn more](https://api-docs.deepseek.com/quick_start/pricing/).
**DeepSeek V4.1 Flash / V4 Pro / V4 Flash / V4 Flash Vision Exp:** Peak hours are 01:00-04:00 and 06:00-10:00 UTC, Monday through Friday; all other hours, including weekends, are Off-Peak. [Learn more](https://api-docs.deepseek.com/quick_start/pricing/).
**DeepSeek V4 Flash Vision Exp:** Images are converted into tokens based on their dimensions and billed as input tokens alongside text tokens. [Learn more](https://api-docs.deepseek.com/quick_start/pricing/).
### Estimated requests
The table below provides an estimated request count based on typical Go usage patterns:
The tables below estimate request counts based on typical Go usage patterns. Go
Plus estimates scale with each model's higher usage limit.
<div class="docs-table-scroll" role="region" aria-label="Go estimated requests" tabIndex={0}>
<PlanTabs id="go-requests" label="Go plan" syncKey="go-plan">
<div slot="go">
| Model | requests per 5 hour | requests per week | requests per month |
| -------------------------------------------------------- | ------------------------- | -------------------------- | --------------------------- |
| GLM-5.3-Flash | 6,320 | 15,790 | 31,580 |
| GLM-5.3 | 220 | 540 | 1,080 |
| GLM-5.2 | 880 | 2,150 | 4,300 |
| GLM-5.1 | 880 | 2,150 | 4,300 |
| Kimi K3 | 110 | 250 | 490 |
| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 |
| Kimi K2.6 | 1,150 | 2,880 | 5,750 |
| LongCat-2.0 | 11,400 | 28,600 | 57,200 |
| MiMo-V2.6-Flash | 30,100 | 75,200 | 150,400 |
| MiMo-V2.6-Pro | 3,250 | 8,150 | 16,300 |
| MiMo-V2.5 | 30,100 | 75,200 | 150,400 |
| MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 |
| MiniMax M3 | 3,200 | 8,000 | 16,000 |
| MiniMax M2.7 | 3,400 | 8,500 | 17,000 |
| Muse Spark 1.3 Contributor | 45,300 | 113,300 | 226,600 |
| Muse Spark 1.2 Contributor | 45,300 | 113,300 | 226,600 |
| Qwen3.8 Max | 160 | 400 | 810 |
| Qwen3.8 Flash | 5,400 | 13,500 | 27,000 |
| Qwen3.7 Max | 170 | 420 | 840 |
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4.1 Flash | 26,000 | 65,000 | 130,000 |
| DeepSeek V4 Pro | 1,050 | 2,600 | 5,200 |
| DeepSeek V4 Flash | 13,000 | 32,500 | 65,000 |
| DeepSeek V4 Flash Vision Exp | 6,500 | 16,250 | 32,500 |
| Hy4 preview | 1,350 | 3,380 | 6,770 |
| Hy3 | 4,300 | 10,750 | 21,500 |
| Space Bunny Free | Unlimited | Unlimited | Unlimited |
| LongCat 2.5 Preview Free | Unlimited | Unlimited | Unlimited |
| Grok 4.7 | 169 | 423 | 845 |
| Grok 4.6 | 169 | 423 | 845 |
| GPT 6 Luna | 4,230 | 10,560 | 21,130 |
| GPT 5.6 Luna | 2,050 | 5,100 | 10,250 |
| Model | Requests per 5 hours | Requests per week | Requests per month |
| ---------------------------- | -------------------- | ----------------- | ------------------ |
| GLM-5.3-Flash | 6,320 | 15,790 | 31,580 |
| GLM-5.3 | 220 | 540 | 1,080 |
| GLM-5.2 | 880 | 2,150 | 4,300 |
| Kimi K3 | 110 | 250 | 490 |
| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 |
| Kimi K2.6 | 1,150 | 2,880 | 5,750 |
| LongCat-2.0 | 11,400 | 28,600 | 57,200 |
| LongCat 2.5 Preview Free | Unlimited | Unlimited | Unlimited |
| MiMo-V2.6-Flash | 30,100 | 75,200 | 150,400 |
| MiMo-V2.6-Pro | 3,250 | 8,150 | 16,300 |
| MiMo-V2.5 | 30,100 | 75,200 | 150,400 |
| MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 |
| MiniMax M3 | 3,200 | 8,000 | 16,000 |
| MiniMax M2.7 | 3,400 | 8,500 | 17,000 |
| Muse Spark 1.3 Contributor | 45,300 | 113,300 | 226,600 |
| Muse Spark 1.2 Contributor | 45,300 | 113,300 | 226,600 |
| Qwen3.8 Max | 160 | 400 | 810 |
| Qwen3.8 Flash | 5,400 | 13,500 | 27,000 |
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| DeepSeek V4.1 Flash | 26,000 | 65,000 | 130,000 |
| DeepSeek V4 Pro | 1,050 | 2,600 | 5,200 |
| DeepSeek V4 Flash | 13,000 | 32,500 | 65,000 |
| DeepSeek V4 Flash Vision Exp | 6,500 | 16,250 | 32,500 |
| Hy4 preview | 1,350 | 3,380 | 6,770 |
| Hy3 | 4,300 | 10,750 | 21,500 |
| Space Bunny Free | Unlimited | Unlimited | Unlimited |
| Grok 4.7 | 169 | 423 | 845 |
| Grok 4.6 | 169 | 423 | 845 |
| GPT 6 Luna | 4,230 | 10,560 | 21,130 |
| GPT 5.6 Luna | 2,050 | 5,100 | 10,250 |
</div>
</div>
<div slot="go-plus">
| Model | Requests per 5 hours | Requests per week | Requests per month |
| ---------------------------- | -------------------- | ----------------- | ------------------ |
| GLM-5.3-Flash | 18,960 | 47,370 | 94,740 |
| GLM-5.3 | 1,760 | 4,320 | 8,640 |
| GLM-5.2 | 2,640 | 6,450 | 12,900 |
| Kimi K3 | 440 | 1,000 | 1,960 |
| Kimi K2.7 Code | 4,050 | 10,140 | 20,250 |
| Kimi K2.6 | 4,600 | 11,520 | 23,000 |
| LongCat-2.0 | 45,600 | 114,400 | 228,800 |
| LongCat 2.5 Preview Free | Unlimited | Unlimited | Unlimited |
| MiMo-V2.6-Flash | 60,200 | 150,400 | 300,800 |
| MiMo-V2.6-Pro | 13,000 | 32,600 | 65,200 |
| MiMo-V2.5 | 60,200 | 150,400 | 300,800 |
| MiMo-V2.5-Pro | 13,000 | 32,600 | 65,200 |
| MiniMax M3 | 9,600 | 24,000 | 48,000 |
| MiniMax M2.7 | 13,600 | 34,000 | 68,000 |
| Muse Spark 1.3 Contributor | 90,600 | 226,600 | 453,200 |
| Muse Spark 1.2 Contributor | 90,600 | 226,600 | 453,200 |
| Qwen3.8 Max | 640 | 1,600 | 3,240 |
| Qwen3.8 Flash | 16,200 | 40,500 | 81,000 |
| Qwen3.7 Plus | 12,900 | 32,400 | 64,800 |
| DeepSeek V4.1 Flash | 52,000 | 130,000 | 260,000 |
| DeepSeek V4 Pro | 4,200 | 10,400 | 20,800 |
| DeepSeek V4 Flash | 52,000 | 130,000 | 260,000 |
| DeepSeek V4 Flash Vision Exp | 26,000 | 65,000 | 130,000 |
| Hy4 preview | 5,400 | 13,520 | 27,080 |
| Hy3 | 17,200 | 43,000 | 86,000 |
| Space Bunny Free | Unlimited | Unlimited | Unlimited |
| Grok 4.7 | 676 | 1,692 | 3,380 |
| Grok 4.6 | 676 | 1,692 | 3,380 |
| GPT 6 Luna | 16,920 | 42,240 | 84,520 |
| GPT 5.6 Luna | 8,200 | 20,400 | 41,000 |
</div>
</PlanTabs>
The estimates use the following token counts per request; actual usage varies.
- Grok 4.7/4.6 — 390 input, 32,500 cached, 120 output tokens per request
- GLM-5.3-Flash — 1,000 input, 55,000 cached, 200 output tokens per request
- GLM-5.3/5.2/5.1 — 700 input, 52,000 cached, 150 output tokens per request
- GLM-5.3/5.2 — 700 input, 52,000 cached, 150 output tokens per request
- GPT 6 Luna — 1,000 input, 50,000 cached, 220 output tokens per request
- GPT 5.6 Luna — 1,000 input, 50,000 cached, 220 output tokens per request
- Kimi K3 — 1,050 input, 76,500 cached, 300 output tokens per request
@@ -249,9 +336,7 @@ The estimates use the following token counts per request; actual usage varies.
- MiMo-V2.5-Pro — 790 input, 86,000 cached, 305 output tokens per request
- Qwen3.8 Max — 420 input, 66,000 cached, 200 output tokens per request
- Qwen3.8 Flash — 600 input, 58,000 cached, 200 output tokens per request
- Qwen3.7 Max — 420 input, 66,000 cached, 200 output tokens per request
- Qwen3.7 Plus — 500 input, 57,000 cached, 190 output tokens per request
- Qwen3.6 Plus — 500 input, 57,000 cached, 190 output tokens per request
- Hy4 preview — 830 input, 71,500 cached, 295 output tokens per request
- Hy3 — 830 input, 71,500 cached, 295 output tokens per request
@@ -261,6 +346,7 @@ You can track your current usage in the [console](https://opencode.ai/console).
Usage limits may change as we learn from early usage and feedback.
---
### Usage beyond limits
@@ -271,7 +357,7 @@ after you've reached your usage limits instead of blocking requests.
### Why some models have lower usage
With Go, you pay $10/month, and the included monthly usage varies by model.
With Go, the included monthly usage varies by model.
For most models, we make this work through bulk discounts and reserved GPU capacity. We then pass those savings on to you as higher monthly usage.
@@ -294,7 +380,6 @@ You can also access Go models through the following API endpoints.
| GLM-5.3-Flash | glm-5.3-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| GLM-5.3 | glm-5.3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| GLM-5.2 | glm-5.2 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Kimi K3 | kimi-k3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -309,14 +394,11 @@ You can also access Go models through the following API endpoints.
| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
| Muse Spark 1.3 Contributor | muse-spark-1.3-contributor | `https://opencode.ai/zen/go/v1/responses` | `@ai-sdk/openai` |
| Muse Spark 1.2 Contributor | muse-spark-1.2-contributor | `https://opencode.ai/zen/go/v1/responses` | `@ai-sdk/openai` |
| Qwen3.8 Max | qwen3.8-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
| Qwen3.8 Flash | qwen3.8-flash | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
| Hy4 preview | hy4-preview | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Space Bunny Free | space-bunny-free | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -353,7 +435,6 @@ curl https://opencode.ai/zen/go/v1/models
| GLM-5.3-Flash | Not used | 0 days |
| GLM-5.3 | Not used | 0 days |
| GLM-5.2 | Not used | 0 days |
| GLM-5.1 | Not used | 0 days |
| Kimi K3 | Not used | 0 days |
| Kimi K2.7 Code | Not used | 0 days |
| Kimi K2.6 | Not used | 0 days |
@@ -364,9 +445,7 @@ curl https://opencode.ai/zen/go/v1/models
| MiMo-V2.5 | Not used | 0 days |
| Qwen3.8 Max | Not used | 0 days |
| Qwen3.8 Flash | Not used | 0 days |
| Qwen3.7 Max | Not used | 0 days |
| Qwen3.7 Plus | Not used | 0 days |
| Qwen3.6 Plus | Not used | 0 days |
| MiniMax M3 | Not used | 0 days |
| MiniMax M2.7 | Not used | 0 days |
| Muse Spark 1.3 Contributor | Yes | Not ZDR |
@@ -384,7 +463,7 @@ curl https://opencode.ai/zen/go/v1/models
- **GPT 6 Luna / GPT 5.6 Luna:** Abuse monitoring logs are generated for all API feature usage and retained for up to 30 days. [Learn more](https://developers.openai.com/api/docs/guides/your-data#data-retention-controls-for-abuse-monitoring).
- **Muse Spark 1.3 Contributor:** Heavily discounted token pricing in exchange for permission to use your prompts and completions to train future Meta models. Availability is limited to regions permitted by Meta's [Geographic Use Policy](https://ai.developer.meta.com/legal/geographic-use-policy). [Learn more](https://dev.meta.ai/docs/pricing-rate-limits#contributor-tier).
- **Muse Spark 1.2 Contributor:** Heavily discounted token pricing in exchange for permission to use your prompts and completions to train future Meta models. Availability is limited to regions permitted by Meta's [Geographic Use Policy](https://ai.developer.meta.com/legal/geographic-use-policy). [Learn more](https://dev.meta.ai/docs/pricing-rate-limits#contributor-tier).
- **DeepSeek:** ZDR agreement is renewed monthly. The current agreement is valid through September 30, 2026.
- **DeepSeek:** ZDR agreement is renewed monthly. The current agreement is valid through October 31, 2026.
## Background
@@ -400,7 +479,7 @@ To fix this, we did a couple of things:
2. We worked with a few providers to make sure these were being served correctly.
3. We benchmarked the combination of the model/provider and came up with a list that we feel good recommending.
OpenCode Go gives you access to these models for **$10/month**.
Both Go and Go Plus provide access to these models; choose the plan that fits how much you use them.
## Goals
+42
View File
@@ -912,6 +912,48 @@ main {
overflow-x: auto;
}
.docs-plan-tabs {
margin-bottom: 1.5rem;
}
.docs-plan-tabs-list {
display: flex;
gap: 0;
margin-bottom: 1rem;
border-bottom: 1px solid var(--border);
}
.docs-plan-tabs-list button {
margin-bottom: -1px;
padding: 0.5rem 1rem;
border: 0;
border-bottom: 2px solid transparent;
background: transparent;
color: var(--muted);
font: inherit;
font-weight: 600;
cursor: pointer;
}
.docs-plan-tabs-list button[aria-selected="true"] {
border-bottom-color: var(--foreground);
color: var(--foreground);
}
.docs-plan-tabs-list button:focus-visible {
outline: 2px solid var(--link);
outline-offset: 2px;
}
.docs-plan-tabs [role="tabpanel"] {
overflow-x: auto;
}
.docs-plan-tabs [role="tabpanel"] > :last-child,
.docs-plan-tabs [role="tabpanel"] > :last-child > :last-child {
margin-bottom: 0;
}
.prose table {
width: 100%;
margin-bottom: 1.5rem;