Compare commits

...

21 Commits

Author SHA1 Message Date
Aiden Cline 3b29b7e3af fix(opencode): default xAI OAuth to device flow 2026-08-04 02:36:43 +00:00
opencode 7fe993879f sync release versions for v1.18.12 2026-08-04 00:55:26 +00:00
Frank def14d96ef Revert "fix slow queries" 2026-08-03 20:14:17 -04:00
opencode-agent[bot] b0b1149233 test(opencode): cover Azure completion reasoning (#40340)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-08-04 09:50:17 +10:00
Aarav Sareen 7becbbf711 feat(app): refine diff viewer (#40285)
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
2026-08-03 22:49:49 +00:00
Frederik 87481f2bb6 fix: gpt-5.5+ in combination with azure fails with reasoningEffort (#40265) 2026-08-03 17:42:30 -05:00
opencode-agent[bot] 9535a8f929 fix(ui): update OpenRouter logo (#40313)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-08-04 07:08:09 +10:00
YB e917c12c80 docs: remove obsolete vouch system section (#40266) 2026-08-03 14:17:27 -05:00
opencode-agent[bot] 89130db6b0 chore: update nix node_modules hashes 2026-08-03 10:07:49 +00:00
Frank 9465a6c97d Merge branch 'dev' of github.com:anomalyco/opencode into dev 2026-08-03 05:54:55 -04:00
Frank 53fd02970d fix slow queries 2026-08-03 05:54:41 -04:00
Luke Parker 6ff0adef22 perf(app): fix composer lag via buffered blob draft storage (#40207) 2026-08-03 19:49:08 +10:00
opencode-agent[bot] 3307f99f1f fix(ci): reuse imported macOS keychain (#40221)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
2026-08-03 19:48:32 +10:00
Frank 8de7a3c739 fix slow queries 2026-08-03 05:41:46 -04:00
opencode-agent[bot] 28d0637406 chore: generate 2026-08-03 08:48:38 +00:00
Frank ef7f712867 fix slow queries 2026-08-03 04:47:10 -04:00
Joshua 3016830e25 fix(app): search every known project in the open project dialog (#40202) 2026-08-03 14:54:00 +08:00
Jack e9e7472456 docs(go): add Qwen3.8 Max (#40228) 2026-08-03 14:48:57 +08:00
Brendan Allan 9f9fbf815c fix(app): clear stale timeline errors (#39465) 2026-08-03 14:18:52 +08:00
opencode-agent[bot] c88facb2cc chore: generate 2026-08-03 06:09:39 +00:00
twinkle d4b85f8d83 fix(app): skip legacy config reads for v2 (#40211) 2026-08-03 14:08:22 +08:00
105 changed files with 4258 additions and 334 deletions
+1 -2
View File
@@ -339,8 +339,7 @@ jobs:
env:
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
GH_TOKEN: ${{ steps.committer.outputs.token }}
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
CSC_KEYCHAIN: build.keychain
APPLE_API_KEY: ${{ runner.temp }}/apple-api-key.p8
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
-27
View File
@@ -252,33 +252,6 @@ These are not strictly enforced, they are just general guidelines:
For net-new functionality, start with a design conversation. Open an issue describing the problem, your proposed approach (optional), and why it belongs in OpenCode. The core team will help decide whether it should move forward; please wait for that approval instead of opening a feature PR directly.
## Trust & Vouch System
This project uses [vouch](https://github.com/mitchellh/vouch) to manage contributor trust. The vouch list is maintained in [`.github/VOUCHED.td`](.github/VOUCHED.td).
### How it works
- **Vouched users** are explicitly trusted contributors.
- **Denounced users** are explicitly blocked. Issues and pull requests from denounced users are automatically closed. If you have been denounced, you can request to be unvouched by reaching out to a maintainer on [Discord](https://opencode.ai/discord)
- **Everyone else** can participate normally — you don't need to be vouched to open issues or PRs.
### For maintainers
Collaborators with write access can manage the vouch list by commenting on any issue:
- `vouch` — vouch for the issue author
- `vouch @username` — vouch for a specific user
- `denounce` — denounce the issue author
- `denounce @username` — denounce a specific user
- `denounce @username <reason>` — denounce with a reason
- `unvouch` / `unvouch @username` — remove someone from the list
Changes are committed automatically to `.github/VOUCHED.td`.
### Denouncement policy
Denouncement is reserved for users who repeatedly submit low-quality AI-generated contributions, spam, or otherwise act in bad faith. It is not used for disagreements or honest mistakes.
## Issue Requirements
All issues **must** use one of our issue templates:
+29 -28
View File
@@ -29,7 +29,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.18.11",
"version": "1.18.12",
"dependencies": {
"@corvu/drawer": "catalog:",
"@dnd-kit/abstract": "0.5.0",
@@ -96,7 +96,7 @@
},
"packages/cli": {
"name": "@opencode-ai/cli",
"version": "1.18.11",
"version": "1.18.12",
"bin": {
"lildax": "./bin/lildax.cjs",
},
@@ -144,7 +144,7 @@
},
"packages/codemode": {
"name": "@opencode-ai/codemode",
"version": "1.18.11",
"version": "1.18.12",
"dependencies": {
"acorn": "8.15.0",
"effect": "catalog:",
@@ -158,7 +158,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.18.11",
"version": "1.18.12",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -194,7 +194,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.18.11",
"version": "1.18.12",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -221,7 +221,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.18.11",
"version": "1.18.12",
"dependencies": {
"@ai-sdk/anthropic": "3.0.82",
"@ai-sdk/openai": "3.0.48",
@@ -243,7 +243,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.18.11",
"version": "1.18.12",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -267,7 +267,7 @@
},
"packages/console/support": {
"name": "@opencode-ai/console-support",
"version": "1.18.11",
"version": "1.18.12",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@opencode-ai/console-core": "workspace:*",
@@ -287,7 +287,7 @@
},
"packages/core": {
"name": "@opencode-ai/core",
"version": "1.18.11",
"version": "1.18.12",
"bin": {
"opencode": "./bin/opencode",
},
@@ -381,9 +381,10 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.18.11",
"version": "1.18.12",
"dependencies": {
"@zip.js/zip.js": "2.7.62",
"drizzle-orm": "catalog:",
"effect": "catalog:",
"electron-context-menu": "4.1.2",
"electron-log": "^5",
@@ -434,7 +435,7 @@
},
"packages/effect-drizzle-sqlite": {
"name": "@opencode-ai/effect-drizzle-sqlite",
"version": "1.18.11",
"version": "1.18.12",
"dependencies": {
"drizzle-orm": "catalog:",
"effect": "catalog:",
@@ -448,7 +449,7 @@
},
"packages/effect-sqlite-node": {
"name": "@opencode-ai/effect-sqlite-node",
"version": "1.18.11",
"version": "1.18.12",
"dependencies": {
"effect": "catalog:",
},
@@ -460,7 +461,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.18.11",
"version": "1.18.12",
"dependencies": {
"@hono/standard-validator": "catalog:",
"@opencode-ai/core": "workspace:*",
@@ -492,7 +493,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.18.11",
"version": "1.18.12",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -508,7 +509,7 @@
},
"packages/http-recorder": {
"name": "@opencode-ai/http-recorder",
"version": "1.18.11",
"version": "1.18.12",
"dependencies": {
"@effect/platform-node": "4.0.0-beta.83",
"@effect/platform-node-shared": "4.0.0-beta.83",
@@ -539,7 +540,7 @@
},
"packages/llm": {
"name": "@opencode-ai/llm",
"version": "1.18.11",
"version": "1.18.12",
"dependencies": {
"@opencode-ai/schema": "workspace:*",
"@smithy/eventstream-codec": "4.2.14",
@@ -558,7 +559,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.18.11",
"version": "1.18.12",
"bin": {
"opencode": "./bin/opencode",
},
@@ -689,7 +690,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.18.11",
"version": "1.18.12",
"dependencies": {
"@ai-sdk/provider": "3.0.8",
"@opencode-ai/sdk": "workspace:*",
@@ -765,7 +766,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.18.11",
"version": "1.18.12",
"dependencies": {
"cross-spawn": "catalog:",
},
@@ -780,7 +781,7 @@
},
"packages/server": {
"name": "@opencode-ai/server",
"version": "1.18.11",
"version": "1.18.12",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/protocol": "workspace:*",
@@ -795,7 +796,7 @@
},
"packages/session-ui": {
"name": "@opencode-ai/session-ui",
"version": "1.18.11",
"version": "1.18.12",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz",
@@ -839,7 +840,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.18.11",
"version": "1.18.12",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -852,7 +853,7 @@
},
"packages/stats/app": {
"name": "@opencode-ai/stats-app",
"version": "1.18.11",
"version": "1.18.12",
"dependencies": {
"@ibm/plex": "6.4.1",
"@kobalte/core": "catalog:",
@@ -886,7 +887,7 @@
},
"packages/stats/core": {
"name": "@opencode-ai/stats-core",
"version": "1.18.11",
"version": "1.18.12",
"dependencies": {
"@aws-sdk/client-athena": "3.933.0",
"@planetscale/database": "1.19.0",
@@ -905,7 +906,7 @@
},
"packages/stats/server": {
"name": "@opencode-ai/stats-server",
"version": "1.18.11",
"version": "1.18.12",
"dependencies": {
"@aws-sdk/client-firehose": "3.933.0",
"@effect/platform-node": "catalog:",
@@ -947,7 +948,7 @@
},
"packages/tui": {
"name": "@opencode-ai/tui",
"version": "1.18.11",
"version": "1.18.12",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
@@ -974,7 +975,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.18.11",
"version": "1.18.12",
"dependencies": {
"@kobalte/core": "catalog:",
"@pierre/diffs": "catalog:",
@@ -1026,7 +1027,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.18.11",
"version": "1.18.12",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-SiJd6uXrL/MqqFGN/uUcHh0Wzdlafnpx++VZa5gUCoE=",
"aarch64-linux": "sha256-pKcT34NYIHVasraRTx0ASTzyuFuIzBTxXg6+KSKvTps=",
"aarch64-darwin": "sha256-hzrym0KpiyYAv80eT/DjcSIJWYBUq4QDgDf6Wtot7jU=",
"x86_64-darwin": "sha256-Or5dSTdajUwij0XpovXSXjJ0LmRUcpcW5TJ4q4B0k0A="
"x86_64-linux": "sha256-a7NyYa9vRUEqDfZNDPXXmFO58RDEgioyuGSl5CPBvxo=",
"aarch64-linux": "sha256-l4OJtSEllHvRhktjcaJYwkXBSaJvsIoyoLusbZfYMcM=",
"aarch64-darwin": "sha256-IIl0BQGs1/HLFh0auiQjiwfSQ2nfHcK2G2BAphYW59c=",
"x86_64-darwin": "sha256-vVeuPyd4ZIRYrHouphTuEb4rkRZLKKTAHe840jNh9rU="
}
}
@@ -0,0 +1,60 @@
import { expect, test } from "@playwright/test"
import type { Page } from "@playwright/test"
import { fixture, pageMessages } from "../smoke/session-timeline.fixture"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectAppVisible } from "../utils/waits"
const NAMES = ["alpha-service", "bravo-web", "charlie-api", "delta-tools", "echo-infra", "foxtrot-docs"]
const worktrees = NAMES.map((name) => `/opencode-demo/${name}`)
// The sixth project sits outside the five-item recent cap, so it is only reachable if the
// dialog hands every recent project to the list filter instead of a pre-truncated slice.
const OUTSIDE_CAP = "foxtrot-docs"
// Dialog rows carry data-directory-path; the sidebar project list does not, so this
// scopes assertions to the picker instead of matching the sidebar entry of the same name.
const rows = (page: Page) => page.locator("[data-directory-path]")
const row = (page: Page, name: string) => page.locator(`[data-directory-path*="${name}"]`)
async function openProjectDialog(page: Page) {
await mockOpenCodeServer(page, {
sessions: fixture.sessions,
provider: fixture.provider,
directory: fixture.directory,
project: fixture.project,
pageMessages,
fileList: () => [],
findFiles: () => [],
})
await page.addInitScript((dirs) => {
localStorage.setItem(
"opencode.global.dat:server",
JSON.stringify({
projects: { local: dirs.map((worktree: string) => ({ worktree, expanded: false })) },
lastProject: {},
}),
)
}, worktrees)
await page.goto("/")
const add = page.getByRole("button", { name: "Add project" }).first()
await expectAppVisible(add)
await add.click()
await expect(rows(page)).toHaveCount(5)
return page.getByRole("textbox").last()
}
test("searches every recent project, not just the five most recent", async ({ page }) => {
const search = await openProjectDialog(page)
await expect(row(page, OUTSIDE_CAP)).toHaveCount(0)
await search.fill("foxtrot")
await expect(row(page, OUTSIDE_CAP)).toHaveCount(1)
})
test("still caps the idle recent list at five projects", async ({ page }) => {
await openProjectDialog(page)
await expect(row(page, NAMES[4])).toHaveCount(1)
await expect(row(page, OUTSIDE_CAP)).toHaveCount(0)
})
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.18.11",
"version": "1.18.12",
"description": "",
"type": "module",
"exports": {
@@ -18,6 +18,8 @@ interface DialogSelectDirectoryProps {
server: ServerConnection.Any
}
const RECENT_PROJECT_LIMIT = 5
type Row = {
absolute: string
search: string
@@ -102,7 +104,6 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
return projects
.map((project, index) => ({ project, at: byProject.get(project.worktree) ?? 0, index }))
.sort((a, b) => b.at - a.at || a.index - b.index)
.slice(0, 5)
.map(({ project }) => {
const row = toRow(project.worktree, home(), "recent")
const name = project.name || getFilename(project.worktree)
@@ -116,7 +117,10 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
const items = async (value: string) => {
const results = await directories(value)
const directoryRows = results.map((absolute) => toRow(absolute, home(), "folders"))
return uniqueRows([...recentProjects(), ...directoryRows])
// Cap the idle list only. Once a query narrows the results, every project stays searchable.
const recent = recentProjects()
const visible = value ? recent : recent.slice(0, RECENT_PROJECT_LIMIT)
return uniqueRows([...visible, ...directoryRows])
}
function resolve(absolute: string) {
@@ -344,7 +344,7 @@ export function usePromptInputV2Controller(props: PromptInputV2ControllerProps):
if (item?.commentID) comments.remove(item.path, item.commentID)
},
openAttachment: (attachment) =>
dialog.show(() => <ImagePreview src={attachment.dataUrl} alt={attachment.filename} />),
dialog.show(() => <ImagePreview src={attachment.blob.url} alt={attachment.filename} />),
openContext(key) {
const item = controller.contextItem(key)
if (item) openComment(item, props, sync, layout, files, comments)
@@ -377,6 +377,7 @@ export function usePromptInputV2Controller(props: PromptInputV2ControllerProps):
}),
readClipboardImage: platform.readClipboardImage,
getPathForFile: platform.getPathForFile,
store: platform.draftStore?.putBlob,
},
view: {
placeholder: designPlaceholder,
+1 -1
View File
@@ -1489,7 +1489,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<PromptImageAttachments
attachments={imageAttachments()}
onOpen={(attachment) =>
dialog.show(() => <ImagePreview src={attachment.dataUrl} alt={attachment.filename} />)
dialog.show(() => <ImagePreview src={attachment.blob.url} alt={attachment.filename} />)
}
onRemove={removeAttachment}
removeLabel={language.t("prompt.attachment.remove")}
@@ -3,28 +3,13 @@ import { makeEventListener } from "@solid-primitives/event-listener"
import { showToast } from "@/utils/toast"
import { type ContentPart, type ImageAttachmentPart, type usePrompt } from "@/context/prompt"
import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
import { uuid } from "@/utils/uuid"
import { getCursorPosition } from "./editor-dom"
import { createBlobReference, type DraftStore } from "@/utils/draft-store"
import { attachmentMime } from "./files"
import { normalizePaste, pasteMode } from "./paste"
function dataUrl(file: File, mime: string) {
return new Promise<string>((resolve) => {
const reader = new FileReader()
reader.addEventListener("error", () => resolve(""))
reader.addEventListener("load", () => {
const value = typeof reader.result === "string" ? reader.result : ""
const idx = value.indexOf(",")
if (idx === -1) {
resolve(value)
return
}
resolve(`data:${mime};base64,${value.slice(idx + 1)}`)
})
reader.readAsDataURL(file)
})
}
type PromptTarget = Pick<ReturnType<ReturnType<typeof usePrompt>["capture"]>, "current" | "cursor" | "set">
type AttachmentTarget = { prompt: PromptTarget; cursor: number | undefined }
@@ -36,6 +21,7 @@ type PromptAttachmentsCoreInput = {
warn?: () => void
readClipboardImage?: () => Promise<File | null>
getPathForFile?: (file: File) => string
draftStore?: DraftStore
}
export type PromptAttachmentsInput = {
@@ -65,16 +51,13 @@ export function createPromptAttachmentsCore(input: PromptAttachmentsCoreInput) {
return false
}
const url = await dataUrl(file, mime)
if (!url) return false
const attachment: ImageAttachmentPart = {
type: "image",
id: uuid(),
filename: file.name,
sourcePath: input.getPathForFile?.(file) || undefined,
mime,
dataUrl: url,
blob: input.draftStore ? await input.draftStore.putBlob(file) : await createBlobReference(file),
}
target.prompt.set([...target.prompt.current(), attachment], target.cursor)
return true
@@ -166,8 +149,10 @@ export function createPromptAttachmentsCore(input: PromptAttachmentsCoreInput) {
export function createPromptAttachments(input: PromptAttachmentsInput) {
const language = useLanguage()
const platform = usePlatform()
const attachments = createPromptAttachmentsCore({
...input,
draftStore: platform.draftStore,
capture: input.prompt.capture,
warn: () => {
showToast({
@@ -22,7 +22,7 @@ type ContextFile = {
type BuildRequestPartsInput = {
prompt: Prompt
context: ContextFile[]
images: ImageAttachmentPart[]
images: (Omit<ImageAttachmentPart, "blob"> & { dataUrl: string })[]
text: string
messageID: string
sessionID: string
@@ -1,7 +1,13 @@
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
import type { Prompt } from "@/context/prompt"
import { Persist, persisted } from "@/utils/persist"
import { prependHistoryEntry, type PromptHistoryComment, type PromptHistoryStoredEntry } from "./history"
import {
clonePromptHistoryComments,
clonePromptParts,
prependHistoryEntry,
type PromptHistoryComment,
type PromptHistoryStoredEntry,
} from "./history"
export type PromptInputHistory = {
entries: (mode: "normal" | "shell") => PromptHistoryStoredEntry[]
@@ -35,13 +41,23 @@ export function createPromptInputHistory(): PromptInputHistory {
}
export function createPersistedPromptInputHistory() {
const [normal, setNormal] = persisted(
Persist.global("prompt-history", ["prompt-history.v1"]),
const [normal, setNormal, normalInit] = persisted(
Persist.prompt(Persist.global("prompt-history", ["prompt-history.v1"])),
createStore<PromptHistoryState>({ entries: [] }),
)
const [shell, setShell] = persisted(
Persist.global("prompt-history-shell", ["prompt-history-shell.v1"]),
const [shell, setShell, shellInit] = persisted(
Persist.prompt(Persist.global("prompt-history-shell", ["prompt-history-shell.v1"])),
createStore<PromptHistoryState>({ entries: [] }),
)
return createPromptInputHistoryStore(normal, setNormal, shell, setShell)
const history = createPromptInputHistoryStore(normal, setNormal, shell, setShell)
return {
...history,
add(prompt: Prompt, mode: "normal" | "shell", comments: PromptHistoryComment[]) {
const ready = mode === "shell" ? shellInit : normalInit
if (!(ready instanceof Promise)) return history.add(prompt, mode, comments)
const saved = clonePromptParts(prompt)
const metadata = clonePromptHistoryComments(comments)
void ready.then(() => history.add(saved, mode, metadata))
},
}
}
@@ -112,7 +112,7 @@ describe("prompt-input history", () => {
end: 12,
selection: { startLine: 1, startChar: 1, endLine: 2, endChar: 1 },
},
{ type: "image", id: "1", filename: "img.png", mime: "image/png", dataUrl: "data:image/png;base64,abc" },
{ type: "image", id: "1", filename: "img.png", mime: "image/png", blob: { id: "blob", url: "blob:test" } },
]
const copy = clonePromptParts(original)
expect(copy).not.toBe(original)
@@ -100,7 +100,7 @@ export const PromptImageAttachments: Component<PromptImageAttachmentsProps> = (p
}
>
<img
src={attachment.dataUrl}
src={attachment.blob.url}
alt={attachment.filename}
class={props.newLayoutDesigns ? imageClassV2 : imageClass}
onClick={() => props.onOpen(attachment)}
@@ -22,6 +22,7 @@ import { ScopedKey } from "@/utils/server-scope"
import { createPromptSubmissionState } from "./submission-state"
import { normalizeSessionInfo } from "@/utils/session"
import { Event } from "@opencode-ai/schema/event"
import { blobDataUrl } from "@/utils/draft-store"
type PendingPrompt = {
abort: AbortController
@@ -95,10 +96,12 @@ export async function sendFollowupDraft(input: FollowupSendInput) {
providerID: input.draft.model.providerID,
variant: input.draft.variant,
},
files: images.map((attachment) => ({
uri: attachment.dataUrl,
name: attachment.filename,
})),
files: await Promise.all(
images.map(async (attachment) => ({
uri: await blobDataUrl(attachment.blob, attachment.mime),
name: attachment.filename,
})),
),
})
return true
} catch (err) {
@@ -108,10 +111,16 @@ export async function sendFollowupDraft(input: FollowupSendInput) {
}
const messageID = input.messageID ?? Identifier.ascending("message")
const encodedImages = await Promise.all(
images.map(async (attachment) => ({
...attachment,
dataUrl: await blobDataUrl(attachment.blob, attachment.mime),
})),
)
const { requestParts, optimisticParts } = buildRequestParts({
prompt: input.draft.prompt,
context: input.draft.context,
images,
images: encodedImages,
text,
sessionID: input.draft.sessionID,
messageID,
@@ -516,10 +525,12 @@ export function createPromptSubmit(input: PromptSubmitInput) {
arguments: args.join(" "),
agent,
model: { id: model.modelID, providerID: model.providerID, variant },
files: images.map((attachment) => ({
uri: attachment.dataUrl,
name: attachment.filename,
})),
files: await Promise.all(
images.map(async (attachment) => ({
uri: await blobDataUrl(attachment.blob, attachment.mime),
name: attachment.filename,
})),
),
})
.catch((err) => {
serverSync().session.set("session_status", session.id, { type: "idle" })
@@ -8,6 +8,7 @@ import {
bootstrapDirectory,
loadAgentsQuery,
loadCommands,
loadGlobalConfigQuery,
loadPathQuery,
loadProjectsQuery,
loadProvidersQuery,
@@ -76,6 +77,7 @@ function directoryState() {
describe("bootstrapDirectory", () => {
test("uses legacy MCP endpoints while refreshing a v1 directory", async () => {
const legacyConfigReads: string[] = []
const mcpReads: string[] = []
const [store, setStore] = directoryState()
@@ -91,7 +93,12 @@ describe("bootstrapDirectory", () => {
},
sdk: {
app: { agents: async () => ({ data: [{ name: "build", mode: "primary" }] }) },
config: { get: async () => ({ data: {} }) },
config: {
get: async () => {
legacyConfigReads.push("directory")
return { data: {} }
},
},
session: { status: async () => ({ data: {} }) },
vcs: { get: async () => ({ data: undefined }) },
command: {
@@ -134,8 +141,88 @@ describe("bootstrapDirectory", () => {
await new Promise((resolve) => setTimeout(resolve, 80))
expect(store.status).toBe("complete")
expect(legacyConfigReads).toEqual(["directory"])
expect(mcpReads.sort()).toEqual(["command", "resource", "status"])
})
test("skips legacy config while refreshing a v2 directory", async () => {
const [store, setStore] = directoryState()
await bootstrapDirectory({
directory: "/project",
scope: ServerScope.local,
mcp: false,
global: {
config: {} satisfies Config,
path: { state: "", config: "", worktree: "/project", directory: "/project", home: "/home" },
project: [{ id: "project", worktree: "/project" } as Project],
provider,
},
sdk: {
config: {
get: async () => {
throw new Error("legacy directory config should not be called")
},
},
} as unknown as OpencodeClient,
api,
store,
setStore,
vcsCache: { setStore() {} } as unknown as VcsCache,
loadSessions() {},
translate: (key) => key,
queryClient: new QueryClient(),
protocol: Promise.resolve("v2"),
})
expect(store.status).toBe("partial")
await new Promise((resolve) => setTimeout(resolve, 80))
expect(store.status).toBe("complete")
})
})
describe("config queries", () => {
test("skips legacy global config for v2 servers", async () => {
const sdk = {
global: {
config: {
get: async () => {
throw new Error("legacy global config should not be called")
},
},
},
} as unknown as OpencodeClient
const result = await new QueryClient().fetchQuery(
loadGlobalConfigQuery(ServerScope.local, sdk, Promise.resolve("v2")),
)
expect(result).toEqual({})
})
test("loads legacy global config for v1 servers", async () => {
const calls: string[] = []
const config = { shell: "zsh" } satisfies Config
const sdk = {
global: {
config: {
get: async () => {
calls.push("global")
return { data: config }
},
},
},
} as unknown as OpencodeClient
const result = await new QueryClient().fetchQuery(
loadGlobalConfigQuery(ServerScope.local, sdk, Promise.resolve("v1")),
)
expect(result).toEqual(config)
expect(calls).toEqual(["global"])
})
})
describe("query keys", () => {
@@ -105,10 +105,13 @@ function showErrors(input: {
})
}
export const loadGlobalConfigQuery = (scope: ServerScope, sdk: OpencodeClient) =>
export const loadGlobalConfigQuery = (scope: ServerScope, sdk: OpencodeClient, protocol?: Promise<ServerProtocol>) =>
queryOptions({
queryKey: [scope, "config"],
queryFn: () => retry(() => sdk.global.config.get().then((x) => x.data!)),
queryFn: async () => {
if ((await protocol) !== "v1") return {}
return retry(() => sdk.global.config.get().then((x) => x.data!))
},
})
type ProjectApi = {
@@ -149,7 +152,7 @@ export async function bootstrapGlobal(input: {
queryClient: QueryClient
}) {
const slow = [
() => input.queryClient.fetchQuery(loadGlobalConfigQuery(input.scope, input.serverSDK)),
() => input.queryClient.fetchQuery(loadGlobalConfigQuery(input.scope, input.serverSDK, input.protocol)),
() =>
input.queryClient.fetchQuery(
loadProvidersQuery(input.scope, null, input.serverAPI, input.serverSDK, input.protocol),
@@ -376,7 +379,10 @@ export async function bootstrapDirectory(input: {
.ensureQueryData(loadAgentsQuery(input.scope, input.directory, input.api.agent, input.sdk, input.protocol))
.then((data) => input.setStore("agent", data)),
() =>
retry(() => input.sdk.config.get().then((x) => input.setStore("config", reconcile(x.data!, { merge: false })))),
retry(async () => {
if ((await input.protocol) !== "v1") return
return input.sdk.config.get().then((x) => input.setStore("config", reconcile(x.data!, { merge: false })))
}),
() =>
retry(() =>
(async () => {
+4
View File
@@ -5,6 +5,7 @@ import type { DesktopMenuAction } from "../desktop-menu"
import { ServerConnection } from "./server"
import type { WslServersPlatform } from "../wsl/types"
import type { UpdaterPlatform } from "../updater"
import type { DraftStore } from "@/utils/draft-store"
type PickerPaths = string | string[] | null
type OpenDirectoryPickerOptions = { title?: string; multiple?: boolean }
@@ -64,6 +65,9 @@ type PlatformBase = {
/** Storage mechanism, defaults to localStorage */
storage?: (name?: string) => SyncStorage | AsyncStorage
/** Prompt drafts, history, and their blobs. */
draftStore?: DraftStore
/** Stable platform window identity for window-scoped persistence */
windowID?: string
+5 -4
View File
@@ -5,6 +5,7 @@ import { createStore, type SetStoreFunction } from "solid-js/store"
import type { FileSelection } from "@/context/file"
import { Persist, persisted } from "@/utils/persist"
import type { ServerScope } from "@/utils/server-scope"
import type { BlobReference } from "@/utils/draft-store"
interface PartBase {
content: string
@@ -37,7 +38,7 @@ export interface ImageAttachmentPart {
filename: string
sourcePath?: string
mime: string
dataUrl: string
blob: BlobReference
}
export type ContentPart = TextPart | FileAttachmentPart | AgentPart | ImageAttachmentPart
@@ -168,9 +169,9 @@ function createPromptActions(setStore: SetStoreFunction<PromptStore>) {
}
function promptTarget(serverScope: ServerScope, scope: PromptScope) {
if ("draftID" in scope) return Persist.draft(scope.draftID, "prompt")
if ("draftID" in scope) return Persist.prompt(Persist.draft(scope.draftID, "prompt"))
const legacy = `${scope.dir}/prompt${scope.id ? "/" + scope.id : ""}.v2`
return Persist.serverScoped(serverScope, scope.dir, scope.id, "prompt", [legacy])
return Persist.prompt(Persist.serverScoped(serverScope, scope.dir, scope.id, "prompt", [legacy]))
}
function promptStore(initial?: InitialPrompt): PromptStore {
@@ -245,7 +246,7 @@ export function createPromptSession(serverScope: ServerScope, scope: PromptScope
}
export function createDraftPromptSession(draftID: string, initial?: InitialPrompt) {
return createPersistedPrompt(Persist.draft(draftID, "prompt"), initial)
return createPersistedPrompt(Persist.prompt(Persist.draft(draftID, "prompt")), initial)
}
export type PromptSession = ReturnType<typeof createPromptSession>
+1 -1
View File
@@ -184,7 +184,7 @@ function makeQueryOptionsApi(
protocol: Promise<"v1" | "v2">,
) {
return {
globalConfig: () => loadGlobalConfigQuery(scope, serverSDK()),
globalConfig: () => loadGlobalConfigQuery(scope, serverSDK(), protocol),
projects: () => loadProjectsQuery(scope, serverAPI.project),
providers: (directory: PathKey | null) =>
loadProvidersQuery(scope, directory, serverAPI, directory ? sdkFor(directory) : serverSDK(), protocol),
+4 -1
View File
@@ -101,7 +101,10 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
}
const removeDraftPersisted = (draftID: string) => {
for (const key of draftPersistedKeys()) removePersisted(Persist.draft(draftID, key), platform)
for (const key of draftPersistedKeys()) {
const target = Persist.draft(draftID, key)
removePersisted(key === "prompt" ? Persist.prompt(target) : target, platform)
}
}
const removeInfo = (key: string) => {
+2
View File
@@ -4,6 +4,7 @@ import * as Sentry from "@sentry/solid"
import { render } from "solid-js/web"
import { AppBaseProviders, AppInterface } from "@/app"
import { type Platform, PlatformProvider } from "@/context/platform"
import { createBrowserDraftStore } from "@/utils/draft-store"
import { dict as en } from "@/i18n/en"
import { dict as zh } from "@/i18n/zh"
import { authFromToken } from "@/utils/server"
@@ -116,6 +117,7 @@ const clearAuthToken = () => {
const platform: Platform = {
platform: "web",
draftStore: createBrowserDraftStore(),
version: pkg.version,
openExternal,
restart,
+1
View File
@@ -27,3 +27,4 @@ export {
type WslServersState,
} from "./wsl/types"
export { ServerConnection } from "./context/server"
export { createDraftStore, type DraftStore } from "./utils/draft-store"
@@ -167,4 +167,41 @@ describe("current session timeline rows", () => {
"thinking:msg_2",
])
})
test("removes a failed assistant error when the turn continues streaming", () => {
const source = [
{ id: "msg_user", type: "user", text: "recover", time: { created: 1 } },
{
id: "msg_failed",
type: "assistant",
agent: "build",
model: { id: "model", providerID: "provider" },
content: [],
error: { type: "ProviderError", message: "temporary failure" },
time: { created: 2, completed: 3 },
},
{
id: "msg_recovery",
type: "assistant",
agent: "build",
model: { id: "model", providerID: "provider" },
content: [{ type: "text", text: "streaming again" }],
time: { created: 4 },
},
] satisfies SessionMessageInfo[]
const normalized = normalizeSessionMessages("ses_1", source)
const messages = new Map(normalized.messages.map((message) => [message.id, message]))
const result = Timeline.constructSessionMessageRows(
source,
(messageID) => messages.get(messageID),
(messageID) => normalized.parts.get(messageID) ?? [],
true,
"busy",
true,
normalized.messages.filter((message) => message.role === "user"),
)
expect(result.rows.map((row) => row._tag)).toEqual(["UserMessage", "AssistantPart"])
})
})
@@ -116,7 +116,8 @@ export namespace Timeline {
const compaction = userParts.some((p) => p.type === "compaction")
const interruptedMessageIndex = assistantMessages.findIndex((m) => m.error?.name === "MessageAbortedError")
const interrupted = interruptedMessageIndex !== -1
const error = assistantMessages.find((m) => m.error && m.error.name !== "MessageAbortedError")?.error
const latestError = assistantMessages.at(-1)?.error
const error = latestError?.name === "MessageAbortedError" ? undefined : latestError
const assistantPartRefs = assistantMessages.flatMap((message, messageIndex) =>
getMessageParts(message.id)
+171
View File
@@ -0,0 +1,171 @@
import type { AsyncStorage } from "@solid-primitives/storage"
export type BlobReference = { id: string; url: string }
type Driver = {
get(key: string): Promise<string | null>
set(key: string, value: string): Promise<void>
remove(key: string): Promise<void>
putBlob(blob: Blob): Promise<string>
getBlob(id: string): Promise<Blob | null>
}
export type DraftStore = AsyncStorage & { putBlob(blob: Blob): Promise<BlobReference> }
const urls = new Map<string, string>()
function blobUrl(id: string, blob: Blob) {
const existing = urls.get(id)
if (existing) return existing
const url = URL.createObjectURL(blob)
urls.set(id, url)
return url
}
async function blobID(blob: Blob) {
const id = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", await blob.arrayBuffer())))
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("")
return id
}
export async function createBlobReference(blob: Blob): Promise<BlobReference> {
const id = await blobID(blob)
return { id, url: blobUrl(id, blob) }
}
export function createDraftStore(driver: Driver): DraftStore {
const versions = new Map<string, number>()
const putBlob = async (blob: Blob) => {
const id = await driver.putBlob(blob)
return { id, url: blobUrl(id, blob) }
}
const encode = async (value: unknown): Promise<unknown> => {
if (Array.isArray(value)) return Promise.all(value.map(encode))
if (!value || typeof value !== "object") return value
const item = value as Record<string, unknown>
if (item.type === "image" && typeof item.dataUrl === "string") {
const blob = await fetch(item.dataUrl).then((response) => response.blob())
const { dataUrl: _, ...rest } = item
return { ...rest, blob: { id: await driver.putBlob(blob) } }
}
if ("blob" in item && item.blob && typeof item.blob === "object") {
const blob = item.blob as Record<string, unknown>
if (typeof blob.id === "string" && blob.id.startsWith("data:")) {
const data = await fetch(blob.id).then((response) => response.blob())
return { ...item, blob: { id: await driver.putBlob(data) } }
}
return { ...item, blob: { id: blob.id } }
}
return Object.fromEntries(
await Promise.all(Object.entries(item).map(async ([key, entry]) => [key, await encode(entry)])),
)
}
const decode = async (value: unknown): Promise<unknown> => {
if (Array.isArray(value)) return Promise.all(value.map(decode))
if (!value || typeof value !== "object") return value
const item = value as Record<string, unknown>
if (item.blob && typeof item.blob === "object") {
const ref = item.blob as Record<string, unknown>
if (typeof ref.id === "string") {
const blob = await driver.getBlob(ref.id)
if (blob) return { ...item, blob: { id: ref.id, url: blobUrl(ref.id, blob) } }
}
}
return Object.fromEntries(
await Promise.all(Object.entries(item).map(async ([key, entry]) => [key, await decode(entry)])),
)
}
return {
getItem: async (key) => {
const value = await driver.get(key)
return value === null ? null : JSON.stringify(await decode(JSON.parse(value)))
},
setItem: async (key, value) => {
const version = (versions.get(key) ?? 0) + 1
versions.set(key, version)
const encoded = JSON.stringify(await encode(JSON.parse(value)))
if (versions.get(key) === version) await driver.set(key, encoded)
},
removeItem: async (key) => {
versions.set(key, (versions.get(key) ?? 0) + 1)
await driver.remove(key)
},
putBlob,
}
}
export function createBrowserDraftStore(): DraftStore {
const request = indexedDB.open("opencode-drafts", 1)
request.addEventListener("upgradeneeded", () => {
request.result.createObjectStore("documents")
request.result.createObjectStore("blobs")
})
const db = new Promise<IDBDatabase>((resolve, reject) => {
request.addEventListener("success", () => {
const database = request.result
const transaction = database.transaction(["documents", "blobs"], "readwrite")
const documents = transaction.objectStore("documents").getAll()
documents.addEventListener("success", () => {
const used = new Set<string>()
JSON.parse(`[${documents.result.join(",")}]`, (_key, item) => {
if (item?.blob && typeof item.blob.id === "string") used.add(item.blob.id)
return item
})
const blobs = transaction.objectStore("blobs").openKeyCursor()
blobs.addEventListener("success", () => {
const cursor = blobs.result
if (!cursor) return
if (!used.has(String(cursor.key))) cursor.delete()
cursor.continue()
})
})
transaction.addEventListener("complete", () => resolve(database))
transaction.addEventListener("abort", () => resolve(database))
})
request.addEventListener("error", () => reject(request.error))
})
const get = async (store: string, key: string) => {
const result = (await db).transaction(store).objectStore(store).get(key)
return new Promise<unknown>((resolve, reject) => {
result.addEventListener("success", () => resolve(result.result))
result.addEventListener("error", () => reject(result.error))
})
}
const write = async (store: string, key: string, value?: unknown) => {
const transaction = (await db).transaction(store, "readwrite")
if (value === undefined) transaction.objectStore(store).delete(key)
else transaction.objectStore(store).put(value, key)
return new Promise<void>((resolve, reject) => {
transaction.addEventListener("complete", () => resolve())
transaction.addEventListener("error", () => reject(transaction.error))
})
}
return createDraftStore({
get: async (key) => ((await get("documents", key)) as string | undefined) ?? null,
set: (key, value) => write("documents", key, value),
remove: (key) => write("documents", key),
putBlob: async (blob) => {
const id = await blobID(blob)
await write("blobs", id, blob)
return id
},
getBlob: async (id) => ((await get("blobs", id)) as Blob | undefined) ?? null,
})
}
export async function blobDataUrl(blob: BlobReference, mime: string) {
const data = await fetch(blob.url).then((response) => response.blob())
return new Promise<string>((resolve, reject) => {
const reader = new FileReader()
reader.addEventListener("error", () => reject(reader.error))
reader.addEventListener("load", () => {
const value = typeof reader.result === "string" ? reader.result : ""
resolve(`data:${mime};base64,${value.slice(value.indexOf(",") + 1)}`)
})
reader.readAsDataURL(data)
})
}
export function createLegacyBlobReference(dataUrl: string): BlobReference {
return { id: dataUrl, url: dataUrl }
}
+47 -5
View File
@@ -15,6 +15,7 @@ type PersistedWithReady<T> = [
]
type PersistTarget = {
draft?: boolean
storage?: string
scope?: "window"
legacyStorageNames?: string[]
@@ -295,6 +296,14 @@ async function removeAsync(storage: AsyncStorage, key: string) {
} catch {}
}
function toAsyncStorage(storage: SyncStorage | AsyncStorage): AsyncStorage {
return {
getItem: async (key) => storage.getItem(key),
setItem: async (key, value) => storage.setItem(key, value),
removeItem: async (key) => storage.removeItem(key),
}
}
async function migrateLegacyAsync(input: {
current: AsyncStorage
legacyStore?: AsyncStorage
@@ -513,6 +522,9 @@ export const Persist = {
if (session) return Persist.serverSession(scope, dir, session, key, legacy)
return Persist.serverWorkspace(scope, dir, key, legacy)
},
prompt(target: PersistTarget): PersistTarget {
return { ...target, draft: true }
},
}
function resolveTarget(target: PersistTarget, platform: Platform): PersistTarget {
@@ -526,9 +538,12 @@ function resolveTarget(target: PersistTarget, platform: Platform): PersistTarget
}
export function removePersisted(
target: { storage?: string; legacyStorageNames?: string[]; key: string },
target: { draft?: boolean; storage?: string; legacyStorageNames?: string[]; key: string },
platform?: Platform,
) {
if (target.draft && platform?.draftStore) {
void platform.draftStore.removeItem(`${target.storage ?? "default"}:${target.key}`)
}
const isDesktop = platform?.platform === "desktop" && !!platform.storage
if (isDesktop) {
@@ -561,8 +576,17 @@ export function persisted<T>(
const legacy = config.legacy ?? []
const isDesktop = platform.platform === "desktop" && !!platform.storage
const draft = config.draft ? platform.draftStore : undefined
const currentStorage = (() => {
if (draft) {
const prefix = `${config.storage ?? "default"}:`
return {
getItem: (key: string) => draft.getItem(prefix + key),
setItem: (key: string, value: string) => draft.setItem(prefix + key, value),
removeItem: (key: string) => draft.removeItem(prefix + key),
} satisfies AsyncStorage
}
if (isDesktop) return platform.storage?.(config.storage)
if (!config.storage) return localStorageDirect()
return localStorageWithPrefix(config.storage)
@@ -577,7 +601,7 @@ export function persisted<T>(
const legacyStorageNames = config.legacyStorageNames ?? []
const storage = (() => {
if (!isDesktop) {
if (!isDesktop && !draft) {
const current = currentStorage as SyncStorage
const legacyStore = legacyStorage as SyncStorage
const legacyStores = legacyStorageNames.map(localStorageWithPrefix)
@@ -609,15 +633,26 @@ export function persisted<T>(
const current = currentStorage as AsyncStorage
const legacyStore = legacyStorage as AsyncStorage | undefined
const legacyStores = legacyStorageNames
.map((name) => platform.storage?.(name) as AsyncStorage | undefined)
const oldCurrent = draft
? isDesktop
? platform.storage?.(config.storage)
: config.storage
? localStorageWithPrefix(config.storage)
: localStorageDirect()
: undefined
const legacyStores = [
oldCurrent,
...legacyStorageNames.map((name) => (isDesktop ? platform.storage?.(name) : localStorageWithPrefix(name))),
]
.filter((x) => !!x)
.map(toAsyncStorage)
let draftLatest: string | undefined
const api: AsyncStorage = {
getItem: async (key) => {
const value = await readCurrentAsync({ storage: current, key, defaults, migrate: config.migrate })
if (value !== undefined) return value
return migrateLegacyAsync({
const migrated = await migrateLegacyAsync({
current,
legacyStore,
stores: legacyStores,
@@ -626,8 +661,15 @@ export function persisted<T>(
defaults,
migrate: config.migrate,
})
if (draftLatest === undefined) {
if (draft && migrated !== null) return (await current.getItem(key)) ?? migrated
return migrated
}
await current.setItem(key, draftLatest)
return draftLatest
},
setItem: async (key, value) => {
if (draft) draftLatest = value
await current.setItem(key, value)
},
removeItem: async (key) => {
+12 -2
View File
@@ -37,8 +37,18 @@ describe("extractPromptFromParts", () => {
expect(result).toHaveLength(3)
expect(result[0]).toMatchObject({ type: "text", content: "check these" })
expect(result.slice(1)).toMatchObject([
{ type: "image", filename: "a.png", mime: "image/png", dataUrl: "data:image/png;base64,AAA" },
{ type: "image", filename: "b.pdf", mime: "application/pdf", dataUrl: "data:application/pdf;base64,BBB" },
{
type: "image",
filename: "a.png",
mime: "image/png",
blob: expect.objectContaining({ id: expect.any(String) }),
},
{
type: "image",
filename: "b.pdf",
mime: "application/pdf",
blob: expect.objectContaining({ id: expect.any(String) }),
},
])
})
})
+2 -1
View File
@@ -1,5 +1,6 @@
import type { AgentPart as MessageAgentPart, FilePart, Part, TextPart } from "@opencode-ai/sdk/v2"
import type { AgentPart, FileAttachmentPart, ImageAttachmentPart, Prompt } from "@/context/prompt"
import { createLegacyBlobReference } from "@/utils/draft-store"
type Inline =
| {
@@ -107,7 +108,7 @@ export function extractPromptFromParts(parts: Part[], opts?: { directory?: strin
id: filePart.id,
filename: filePart.filename ?? attachmentName,
mime: filePart.mime,
dataUrl: filePart.url,
blob: createLegacyBlobReference(filePart.url),
})
}
}
@@ -2,6 +2,7 @@ import { beforeAll, describe, expect, mock, test } from "bun:test"
import type { AsyncStorage } from "@solid-primitives/storage"
import { createEffect, createRoot } from "solid-js"
import { ServerScope } from "@/utils/server-scope"
import { createDraftStore } from "@/utils/draft-store"
let Prompt: typeof import("@/context/prompt")
let read: ((value: string | null) => void) | undefined
@@ -30,7 +31,7 @@ beforeAll(async () => {
}),
}))
mock.module("@/context/platform", () => ({
usePlatform: () => ({ platform: "desktop", storage: () => storage }),
usePlatform: () => ({ platform: "desktop", storage: () => storage, draftStore: storage }),
}))
Prompt = await import("@/context/prompt")
@@ -69,3 +70,50 @@ describe("prompt persistence", () => {
})
})
})
test("moves legacy image data URLs into blobs and hydrates object URLs", async () => {
const documents = new Map<string, string>()
const blobs = new Map<string, Blob>()
const store = createDraftStore({
get: async (key) => documents.get(key) ?? null,
set: async (key, value) => void documents.set(key, value),
remove: async (key) => void documents.delete(key),
putBlob: async (blob) => {
const id = String(blob.size)
blobs.set(id, blob)
return id
},
getBlob: async (id) => blobs.get(id) ?? null,
})
await store.setItem("prompt", JSON.stringify({ prompt: [{ type: "image", dataUrl: "data:image/png;base64,YQ==" }] }))
expect(documents.get("prompt")).not.toContain("dataUrl")
const value = JSON.parse((await store.getItem("prompt"))!)
expect(value.prompt[0].blob.id).toBe("1")
expect(value.prompt[0].blob.url).toStartWith("blob:")
})
test("does not let delayed blob migration overwrite a newer draft", async () => {
const documents = new Map<string, string>()
const migration = Promise.withResolvers<void>()
const store = createDraftStore({
get: async () => null,
set: async (key, value) => void documents.set(key, value),
remove: async () => undefined,
putBlob: async () => {
await migration.promise
return "blob"
},
getBlob: async () => null,
})
const older = store.setItem(
"prompt",
JSON.stringify({ prompt: [{ type: "image", dataUrl: "data:image/png;base64,YQ==" }] }),
)
await Bun.sleep(0)
await store.setItem("prompt", JSON.stringify({ prompt: [{ type: "text", content: "latest" }] }))
migration.resolve()
await older
expect(documents.get("prompt")).toContain("latest")
})
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/cli",
"version": "1.18.11",
"version": "1.18.12",
"type": "module",
"license": "MIT",
"bin": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/codemode",
"version": "1.18.11",
"version": "1.18.12",
"description": "Effect-native confined code execution over schema-described tools",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.18.11",
"version": "1.18.12",
"type": "module",
"license": "MIT",
"scripts": {
@@ -32,6 +32,7 @@ const models = [
{ name: "Kimi K2.6", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" },
{ name: "MiMo-V2.5-Pro", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" },
{ name: "MiMo-V2.5", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" },
{ name: "Qwen3.8 Max", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" },
{ name: "Qwen3.7 Max", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" },
{ name: "Qwen3.7 Plus", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" },
{ name: "Qwen3.6 Plus", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" },
@@ -67,6 +68,7 @@ function LimitsGraph(props: { href: string }) {
const graph = [
{ id: "grok-4.5", name: "Grok 4.5", req: 120, d: "50ms" },
{ id: "kimi-k3", name: "Kimi K3", req: 110, d: "75ms" },
{ id: "qwen3.8-max", name: "Qwen3.8 Max", req: 160, d: "90ms" },
{ id: "glm-5.2", name: "GLM-5.2", req: 880, d: "100ms" },
{ id: "minimax-m3", name: "MiniMax M3", req: 3200, d: "210ms" },
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", req: 3450, d: "270ms" },
@@ -313,6 +313,7 @@ export function LiteSection(props: { lite: LiteSubscription | undefined }) {
<li>Kimi K2.6</li>
<li>MiniMax M3</li>
<li>MiniMax M2.7</li>
<li>Qwen3.8 Max</li>
<li>Qwen3.7 Max</li>
<li>Qwen3.7 Plus</li>
<li>Qwen3.6 Plus</li>
@@ -0,0 +1,2 @@
CREATE UNIQUE INDEX `global_lite_subscription_id` ON `billing` (`lite_subscription_id`);--> statement-breakpoint
CREATE INDEX `referral_id` ON `referral_reward` (`referral_id`);
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.18.11",
"version": "1.18.12",
"private": true,
"type": "module",
"license": "MIT",
@@ -53,6 +53,7 @@ export const BillingTable = mysqlTable(
...workspaceIndexes(table),
uniqueIndex("global_customer_id").on(table.customerID),
uniqueIndex("global_subscription_id").on(table.subscriptionID),
uniqueIndex("global_lite_subscription_id").on(table.liteSubscriptionID),
],
)
@@ -1,4 +1,4 @@
import { bigint, mysqlTable, primaryKey, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
import { bigint, index, mysqlTable, primaryKey, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types"
import { workspaceIndexes } from "./workspace.sql"
@@ -31,5 +31,8 @@ export const ReferralRewardTable = mysqlTable(
amount: bigint("amount", { mode: "number" }).notNull(),
timeApplied: utc("time_applied"),
},
(table) => [primaryKey({ columns: [table.workspaceID, table.referralID] })],
(table) => [
primaryKey({ columns: [table.workspaceID, table.referralID] }),
index("referral_id").on(table.referralID),
],
)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.18.11",
"version": "1.18.12",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.18.11",
"version": "1.18.12",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-support",
"version": "1.18.11",
"version": "1.18.12",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.18.11",
"version": "1.18.12",
"name": "@opencode-ai/core",
"type": "module",
"license": "MIT",
+3 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "1.18.11",
"version": "1.18.12",
"type": "module",
"license": "MIT",
"homepage": "https://opencode.ai",
@@ -30,7 +30,8 @@
"electron-log": "^5",
"electron-store": "11.0.2",
"electron-updater": "6.8.9",
"electron-window-state": "^5.0.3"
"electron-window-state": "^5.0.3",
"drizzle-orm": "catalog:"
},
"devDependencies": {
"@actions/artifact": "4.0.0",
@@ -0,0 +1,16 @@
import { expect, test } from "bun:test"
import { createDesktopDraftStore } from "./draft-store"
test("flushes the latest buffered draft and stores blobs", () => {
const store = createDesktopDraftStore(":memory:")
store.set("prompt", "first")
store.set("prompt", "latest")
expect(store.get("prompt")).toBe("latest")
store.flush()
expect(store.get("prompt")).toBe("latest")
const bytes = new TextEncoder().encode("image")
const id = store.putBlob(bytes)
expect(store.getBlob(id)).toEqual(bytes)
store.close()
})
+82
View File
@@ -0,0 +1,82 @@
import { createHash } from "node:crypto"
import { DatabaseSync } from "node:sqlite"
import { eq } from "drizzle-orm"
import { drizzle } from "drizzle-orm/node-sqlite"
import { blob, sqliteTable, text } from "drizzle-orm/sqlite-core"
const documents = sqliteTable("document", {
key: text().primaryKey(),
value: text().notNull(),
})
const blobs = sqliteTable("blob", {
id: text().primaryKey(),
data: blob({ mode: "buffer" }).notNull(),
})
export function createDesktopDraftStore(filename: string) {
const native = new DatabaseSync(filename)
native.exec(
"PRAGMA journal_mode=WAL; CREATE TABLE IF NOT EXISTS document (key TEXT PRIMARY KEY, value TEXT NOT NULL); CREATE TABLE IF NOT EXISTS blob (id TEXT PRIMARY KEY, data BLOB NOT NULL);",
)
const db = drizzle({ client: native })
const used = new Set<string>()
db.select({ value: documents.value })
.from(documents)
.all()
.forEach(({ value }) =>
JSON.parse(value, (_key, item) => {
if (item?.blob && typeof item.blob.id === "string") used.add(item.blob.id)
return item
}),
)
db.select({ id: blobs.id })
.from(blobs)
.all()
.filter(({ id }) => !used.has(id))
.forEach(({ id }) => db.delete(blobs).where(eq(blobs.id, id)).run())
const pending = new Map<string, string | null>()
let timer: ReturnType<typeof setTimeout> | undefined
const flush = () => {
if (timer) clearTimeout(timer)
timer = undefined
const writes = [...pending]
pending.clear()
db.transaction((tx) => {
writes.forEach(([key, value]) => {
if (value === null) tx.delete(documents).where(eq(documents.key, key)).run()
else
tx.insert(documents)
.values({ key, value })
.onConflictDoUpdate({ target: documents.key, set: { value } })
.run()
})
})
}
const schedule = () => {
if (!timer) timer = setTimeout(flush, 500)
}
return {
get: (key: string) =>
pending.has(key)
? (pending.get(key) ?? null)
: (db.select({ value: documents.value }).from(documents).where(eq(documents.key, key)).get()?.value ?? null),
set(key: string, value: string | null) {
pending.set(key, value)
schedule()
},
putBlob(data: Uint8Array) {
const id = createHash("sha256").update(data).digest("hex")
db.insert(blobs)
.values({ id, data: Buffer.from(data) })
.onConflictDoNothing()
.run()
return id
},
getBlob: (id: string) => db.select({ data: blobs.data }).from(blobs).where(eq(blobs.id, id)).get()?.data ?? null,
flush,
close() {
flush()
native.close()
},
}
}
+2 -2
View File
@@ -171,7 +171,7 @@ const main = Effect.gen(function* () {
setAppQuitting()
void stopSidecars().finally(() => {
app.relaunch()
app.exit(0)
app.quit()
})
}
@@ -245,7 +245,7 @@ const main = Effect.gen(function* () {
for (const signal of ["SIGINT", "SIGTERM"] as const) {
process.on(signal, () => {
setAppQuitting()
void stopSidecars().finally(() => app.exit(0))
void stopSidecars().finally(() => app.quit())
})
}
+14 -1
View File
@@ -1,6 +1,6 @@
import { execFile } from "node:child_process"
import { stat } from "node:fs/promises"
import { basename } from "node:path"
import { basename, join } from "node:path"
import { app, BrowserWindow, clipboard, dialog, ipcMain, shell } from "electron"
import type { IpcMainEvent, IpcMainInvokeEvent } from "electron"
import type { DesktopMenuAction } from "@opencode-ai/app/desktop-menu"
@@ -21,6 +21,7 @@ import {
} from "./windows"
import type { UpdaterController } from "./updater-controller"
import { createUpdaterSubscriptions } from "./updater-subscriptions"
import { createDesktopDraftStore } from "./draft-store"
const pickerFilters = (ext?: string[]) => {
if (!ext || ext.length === 0) return undefined
@@ -51,8 +52,12 @@ type Deps = {
}
export function registerIpcHandlers(deps: Deps) {
const drafts = createDesktopDraftStore(join(app.getPath("userData"), "drafts.sqlite"))
const updaterSubscriptions = createUpdaterSubscriptions()
app.once("will-quit", updaterSubscriptions.clear)
app.on("before-quit", () => drafts.flush())
app.once("will-quit", () => drafts.close())
app.on("browser-window-created", (_event, win) => win.on("session-end", () => drafts.flush()))
ipcMain.handle("kill-sidecar", () => deps.killSidecar())
ipcMain.handle("await-initialization", () => deps.awaitInitialization())
@@ -123,6 +128,14 @@ export function registerIpcHandlers(deps: Deps) {
const store = getStore(name)
return Object.keys(store.store).length
})
ipcMain.handle("draft-get", (_event, key: string) => drafts.get(key))
ipcMain.handle("draft-set", (_event, key: string, value: string) => drafts.set(key, value))
ipcMain.handle("draft-delete", (_event, key: string) => drafts.set(key, null))
ipcMain.handle("draft-blob-put", (_event, data: ArrayBuffer) => drafts.putBlob(new Uint8Array(data)))
ipcMain.handle("draft-blob-get", (_event, id: string) => {
const data = drafts.getBlob(id)
return data ? data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) : null
})
ipcMain.handle(
"open-directory-picker",
+5
View File
@@ -73,6 +73,11 @@ const api: ElectronAPI = {
storeClear: (name) => ipcRenderer.invoke("store-clear", name),
storeKeys: (name) => ipcRenderer.invoke("store-keys", name),
storeLength: (name) => ipcRenderer.invoke("store-length", name),
draftGet: (key) => ipcRenderer.invoke("draft-get", key),
draftSet: (key, value) => ipcRenderer.invoke("draft-set", key, value),
draftDelete: (key) => ipcRenderer.invoke("draft-delete", key),
draftBlobPut: (data) => ipcRenderer.invoke("draft-blob-put", data),
draftBlobGet: (id) => ipcRenderer.invoke("draft-blob-get", id),
getWindowID: () => ipcRenderer.invoke("get-window-id"),
onMenuCommand: (cb) => {
+5
View File
@@ -63,6 +63,11 @@ export type ElectronAPI = {
storeClear: (name: string) => Promise<void>
storeKeys: (name: string) => Promise<string[]>
storeLength: (name: string) => Promise<number>
draftGet: (key: string) => Promise<string | null>
draftSet: (key: string, value: string) => Promise<void>
draftDelete: (key: string) => Promise<void>
draftBlobPut: (data: ArrayBuffer) => Promise<string>
draftBlobGet: (id: string) => Promise<ArrayBuffer | null>
getWindowID: () => Promise<string>
onMenuCommand: (cb: (id: string) => void) => () => void
+8
View File
@@ -9,6 +9,7 @@ import {
type Locale,
type Platform,
PlatformProvider,
createDraftStore,
ServerConnection,
useCommand,
useWslServers,
@@ -226,6 +227,13 @@ const createPlatform = (windowState: DesktopWindowState): Platform => {
},
storage,
draftStore: createDraftStore({
get: window.api.draftGet,
set: window.api.draftSet,
remove: window.api.draftDelete,
putBlob: (blob) => blob.arrayBuffer().then(window.api.draftBlobPut),
getBlob: (id) => window.api.draftBlobGet(id).then((data) => data && new Blob([data])),
}),
updater: {
state: updaterState,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.18.11",
"version": "1.18.12",
"name": "@opencode-ai/effect-drizzle-sqlite",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.18.11",
"version": "1.18.12",
"name": "@opencode-ai/effect-sqlite-node",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.18.11",
"version": "1.18.12",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.18.11",
"version": "1.18.12",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.18.11",
"version": "1.18.12",
"name": "@opencode-ai/http-recorder",
"description": "Record and replay Effect HTTP client traffic with deterministic cassettes",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.18.11",
"version": "1.18.12",
"name": "@opencode-ai/llm",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.18.11",
"version": "1.18.12",
"name": "opencode",
"type": "module",
"license": "MIT",
+26 -34
View File
@@ -551,6 +551,32 @@ export async function XaiAuthPlugin(input: PluginInput, options: XaiAuthPluginOp
{
label: "xAI Grok OAuth (SuperGrok Subscription)",
type: "oauth",
authorize: async () => {
const device = await requestDeviceCode(options)
const browserUrl = device.verification_uri_complete ?? device.verification_uri
return {
url: browserUrl,
instructions: `Open ${device.verification_uri} on any device and enter code: ${device.user_code}`,
method: "auto" as const,
callback: async () => {
try {
const tokens = await pollDeviceCodeToken(device, options)
return {
type: "success" as const,
refresh: tokens.refresh_token,
access: tokens.access_token,
expires: Date.now() + (tokens.expires_in ?? 3600) * 1000,
}
} catch (err) {
return { type: "failed" as const }
}
},
}
},
},
{
label: "xAI Grok OAuth (Local Callback)",
type: "oauth",
authorize: async () => {
await startOAuthServer()
const pkce = await generatePKCE()
@@ -582,40 +608,6 @@ export async function XaiAuthPlugin(input: PluginInput, options: XaiAuthPluginOp
}
},
},
{
// RFC 8628 device-code flow. The CLI prints a verification URL
// and a short user_code that the user enters in a browser on
// any device. No loopback callback server runs on the CLI host,
// so this works on VPS / SSH / Docker / CI / WSL / any
// environment where 127.0.0.1:56121 isn't reachable from the
// user's browser. Defends the only attack surface (the polling
// loop) with the standard authorization_pending / slow_down
// backoff and a hard deadline from xAI's `expires_in`.
label: "xAI Grok OAuth (Headless / Remote / VPS)",
type: "oauth",
authorize: async () => {
const device = await requestDeviceCode(options)
const browserUrl = device.verification_uri_complete ?? device.verification_uri
return {
url: browserUrl,
instructions: `Open ${device.verification_uri} on any device and enter code: ${device.user_code}`,
method: "auto" as const,
callback: async () => {
try {
const tokens = await pollDeviceCodeToken(device, options)
return {
type: "success" as const,
refresh: tokens.refresh_token,
access: tokens.access_token,
expires: Date.now() + (tokens.expires_in ?? 3600) * 1000,
}
} catch (err) {
return { type: "failed" as const }
}
},
}
},
},
{
label: "Manually enter API Key",
type: "api",
+8 -2
View File
@@ -1270,8 +1270,14 @@ export function options(input: {
result["gateway"] = { caching: "auto" }
}
if (input.model.api.npm === "@ai-sdk/azure" && input.model.api.id.includes("gpt-5.5")) {
result["reasoningSummary"] = "auto"
// Any gpt version above 5.4 in combination with azure does not support reasoningEffort
// so we should return early here.
const [, gptMajorVersion, gptMinorVersion] = input.model.api.id.match(/gpt-(\d+)\.(\d+)/) ?? []
const isGpt55OrNewer = Number(gptMajorVersion) > 5 || (Number(gptMajorVersion) === 5 && Number(gptMinorVersion) >= 5)
if (input.model.api.npm === "@ai-sdk/azure" && input.providerOptions?.useCompletionUrls) {
if (!isGpt55OrNewer) {
result["reasoningEffort"] = "medium"
}
return result
}
+10 -10
View File
@@ -111,7 +111,7 @@ describe("plugin.xai", () => {
).toEqual({})
expect(hooks.auth!.methods.map((m) => [m.type, m.label])).toEqual([
["oauth", "xAI Grok OAuth (SuperGrok Subscription)"],
["oauth", "xAI Grok OAuth (Headless / Remote / VPS)"],
["oauth", "xAI Grok OAuth (Local Callback)"],
["api", "Manually enter API Key"],
])
})
@@ -424,11 +424,11 @@ describe("plugin.xai", () => {
return new Response("unexpected request", { status: 500 })
})
const hooks = await XaiAuthPlugin({} as any, serverOptions(server))
const headless = hooks.auth!.methods.find(
const oauth = hooks.auth!.methods.find(
(m): m is Extract<typeof m, { type: "oauth" }> =>
m.type === "oauth" && m.label === "xAI Grok OAuth (Headless / Remote / VPS)",
m.type === "oauth" && m.label === "xAI Grok OAuth (SuperGrok Subscription)",
)!
const result = await headless.authorize!()
const result = await oauth.authorize!()
expect(result.method).toBe("auto")
expect(result.url).toBe("https://x.ai/device?user_code=ABCD-1234")
@@ -448,11 +448,11 @@ describe("plugin.xai", () => {
}
return new Response("unexpected request", { status: 500 })
})
const headless = (await XaiAuthPlugin({} as any, serverOptions(server))).auth!.methods.find(
const oauth = (await XaiAuthPlugin({} as any, serverOptions(server))).auth!.methods.find(
(m): m is Extract<typeof m, { type: "oauth" }> =>
m.type === "oauth" && m.label === "xAI Grok OAuth (Headless / Remote / VPS)",
m.type === "oauth" && m.label === "xAI Grok OAuth (SuperGrok Subscription)",
)!
expect((await headless.authorize!()).url).toBe("https://x.ai/device")
expect((await oauth.authorize!()).url).toBe("https://x.ai/device")
})
test("requestDeviceCode posts form body, validates fields, and surfaces endpoint errors", async () => {
@@ -610,11 +610,11 @@ describe("plugin.xai", () => {
}
return Response.json({ error: "access_denied" }, { status: 400 })
})
const headless = (await XaiAuthPlugin({} as any, serverOptions(server))).auth!.methods.find(
const oauth = (await XaiAuthPlugin({} as any, serverOptions(server))).auth!.methods.find(
(m): m is Extract<typeof m, { type: "oauth" }> =>
m.type === "oauth" && m.label === "xAI Grok OAuth (Headless / Remote / VPS)",
m.type === "oauth" && m.label === "xAI Grok OAuth (SuperGrok Subscription)",
)!
expect(await ((await headless.authorize!()) as any).callback()).toEqual({ type: "failed" })
expect(await ((await oauth.authorize!()) as any).callback()).toEqual({ type: "failed" })
})
})
})
@@ -681,14 +681,34 @@ describe("ProviderTransform.options - gpt-5 reasoningEffort", () => {
expect(result.reasoningEffort).toBeUndefined()
})
test("gpt-5.5 should NOT set reasoningEffort", () => {
test("gpt-5.5 should NOT set reasoningEffort for the completions API", () => {
const result = ProviderTransform.options({
model: createModel("gpt-5.5"),
sessionID,
providerOptions: { useCompletionUrls: true },
})
expect(result.reasoningEffort).toBeUndefined()
})
test("gpt-5.6 should NOT set reasoningEffort for the completions API", () => {
const result = ProviderTransform.options({
model: createModel("gpt-5.6"),
sessionID,
providerOptions: { useCompletionUrls: true },
})
expect(result.reasoningEffort).toBeUndefined()
})
test("gpt-5.6 should set reasoningEffort for the responses API", () => {
const result = ProviderTransform.options({
model: createModel("gpt-5.6"),
sessionID,
providerOptions: {},
})
expect(result.reasoningEffort).toBeUndefined()
expect(result.reasoningEffort).toBe("medium")
})
})
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.18.11",
"version": "1.18.12",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.18.11",
"version": "1.18.12",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/server",
"version": "1.18.11",
"version": "1.18.12",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/session-ui",
"version": "1.18.11",
"version": "1.18.12",
"private": true,
"type": "module",
"license": "MIT",
+32 -5
View File
@@ -255,8 +255,16 @@ function useFileViewer(config: ViewerConfig) {
if (!dragMoved) {
pendingSelectionEnd = false
const selected = config.buildClickSelection()
if (selected) config.setSelectedLines(selected)
config.onLineSelectionEnd(lastSelection)
const next =
selected &&
lastSelection?.start === selected.start &&
lastSelection.end === selected.end &&
lastSelection.side === selected.side &&
(lastSelection.endSide ?? lastSelection.side) === (selected.endSide ?? selected.side)
? null
: selected
if (next !== undefined) config.setSelectedLines(next)
config.onLineSelectionEnd(next === undefined ? lastSelection : next)
dragStart = undefined
dragEnd = undefined
dragMoved = false
@@ -291,10 +299,29 @@ function useFileViewer(config: ViewerConfig) {
createEffect(() => {
rendered()
const ranges = config.commentedLines()
requestAnimationFrame(() => {
const root = getRoot()
if (!root) return
const root = getRoot()
if (!root) return
if (ranges.length === 0) {
config.markCommented(root, ranges)
return
}
let frame: number | undefined
const mark = () => {
if (frame !== undefined) cancelAnimationFrame(frame)
frame = requestAnimationFrame(() => {
frame = undefined
config.markCommented(root, ranges)
})
}
const observer = new MutationObserver(mark)
observer.observe(root, { childList: true, subtree: true })
mark()
onCleanup(() => {
observer.disconnect()
if (frame !== undefined) cancelAnimationFrame(frame)
})
})
+61 -29
View File
@@ -26,30 +26,19 @@ const unsafeCSS = `
color-mix(in lab, var(--diffs-bg) 33.333%, var(--diffs-deletion-base)),
color-mix(in lab, var(--diffs-bg) 60%, var(--diffs-deletion-base))
);
--diffs-bg-deletion-number-override: var(--diffs-bg-deletion-override);
--diffs-bg-addition-override: light-dark(
color-mix(in lab, var(--diffs-bg) 33.333%, var(--diffs-addition-base)),
color-mix(in lab, var(--diffs-bg) 60%, var(--diffs-addition-base))
);
--diffs-selection-base: var(--surface-warning-strong);
--diffs-selection-border: var(--border-warning-base);
--diffs-selection-number-fg: #1c1917;
--diffs-bg-addition-number-override: var(--diffs-bg-addition-override);
--diffs-selection-base: var(--v2-background-bg-accent);
--diffs-selection-number-fg: var(--v2-text-text-accent);
--diffs-comment-bg: rgb(from var(--v2-background-bg-accent) r g b / 0.06);
/* Use explicit alpha instead of color-mix(..., transparent) to avoid Safari's non-premultiplied interpolation bugs. */
--diffs-bg-selection: var(--diffs-bg-selection-override, rgb(from var(--surface-warning-base) r g b / 0.65));
--diffs-bg-selection-number: var(
--diffs-bg-selection-number-override,
rgb(from var(--surface-warning-base) r g b / 0.85)
);
--diffs-bg-selection-text: rgb(from var(--surface-warning-strong) r g b / 0.2);
}
:host([data-color-scheme='dark']) [data-diff],
:host([data-color-scheme='dark']) [data-file] {
--diffs-selection-number-fg: #fdfbfb;
--diffs-bg-selection: var(--diffs-bg-selection-override, rgb(from var(--solaris-dark-6) r g b / 0.65));
--diffs-bg-selection-number: var(
--diffs-bg-selection-number-override,
rgb(from var(--solaris-dark-6) r g b / 0.85)
);
--diffs-bg-selection: var(--diffs-bg-selection-override, rgb(from var(--diffs-selection-base) r g b / 0.2));
--diffs-bg-selection-number: var(--diffs-bg-selection-number-override, var(--diffs-bg-selection));
--diffs-bg-selection-text: rgb(from var(--diffs-selection-base) r g b / 0.2);
}
[data-diff] ::selection,
@@ -57,6 +46,34 @@ const unsafeCSS = `
background-color: var(--diffs-bg-selection-text);
}
[data-indicators='bars'] [data-column-number][data-line-type='change-addition']::before,
[data-indicators='bars'] [data-column-number][data-line-type='change-deletion']::before {
width: 2px;
}
[data-indicators='bars'] [data-column-number][data-line-type='change-deletion']::before {
background-image: none;
background-color: var(--diffs-deletion-base);
}
[data-background] [data-column-number] {
--mix-light: 88%;
--mix-dark: 80%;
}
[data-diff-type='split'] [data-additions],
[data-diff-type='split'] [data-additions] [data-gutter],
[data-diff-type='split'] [data-deletions],
[data-diff-type='split'] [data-deletions] [data-content] {
border-left: 0;
border-right: 0;
}
[data-content-buffer] {
background-image: none;
background-color: var(--diffs-bg-context-gutter);
}
::highlight(opencode-find) {
background-color: rgb(from var(--surface-warning-base) r g b / 0.35);
}
@@ -83,22 +100,24 @@ const unsafeCSS = `
color: var(--diffs-selection-number-fg);
}
[data-diff] [data-line-annotation][data-comment-selected]:not([data-selected-line]) [data-annotation-content] {
box-shadow: inset 0 0 0 9999px var(--diffs-bg-selection);
}
[data-file] [data-line-annotation][data-comment-selected]:not([data-selected-line]) [data-annotation-content] {
box-shadow: inset 0 0 0 9999px var(--diffs-bg-selection);
[data-diff] [data-line-annotation],
[data-diff] [data-gutter-buffer='annotation'],
[data-file] [data-line-annotation],
[data-file] [data-gutter-buffer='annotation'] {
--diffs-annotation-bg: var(--diffs-comment-bg);
--diffs-computed-decoration-bg: var(--diffs-comment-bg);
--diffs-computed-diff-line-bg: var(--diffs-comment-bg);
--diffs-computed-selected-line-bg: var(--diffs-comment-bg);
--diffs-line-bg: var(--diffs-comment-bg);
background-color: var(--diffs-comment-bg);
}
[data-diff] [data-line][data-selected-line] {
background-color: var(--diffs-bg-selection);
box-shadow: inset 2px 0 0 var(--diffs-selection-border);
}
[data-file] [data-line][data-selected-line] {
background-color: var(--diffs-bg-selection);
box-shadow: inset 2px 0 0 var(--diffs-selection-border);
}
[data-diff] [data-column-number][data-selected-line] {
@@ -118,6 +137,19 @@ const unsafeCSS = `
color: var(--diffs-selection-number-fg);
}
@media (pointer: fine) {
[data-gutter-utility-slot] {
opacity: 0;
pointer-events: none;
}
[data-column-number][data-hovered] [data-gutter-utility-slot],
[data-gutter-utility-slot]:focus-within {
opacity: 1;
pointer-events: auto;
}
}
/* The deletion word-diff emphasis is stronger than additions; soften it while selected so the selection highlight reads consistently. */
[data-diff] [data-line][data-line-type='change-deletion'][data-selected-line] {
--diffs-bg-deletion-emphasis: light-dark(
@@ -133,7 +165,6 @@ const unsafeCSS = `
height: 24px;
}
[data-column-number] {
background-color: var(--diffs-bg);
cursor: default !important;
}
@@ -182,5 +213,6 @@ export const styleVariables = {
"--diffs-font-features": "var(--font-family-mono--font-feature-settings)",
"--diffs-header-font-family": "var(--font-family-sans)",
"--diffs-gap-block": 0,
"--diffs-min-number-column-width": "4ch",
"--diffs-gap-style": 0,
"--diffs-min-number-column-width": "3ch",
}
@@ -151,7 +151,10 @@ export function createLineCommentControllerV2<T extends LineCommentShape>(props:
}
: undefined
},
onMouseEnter: () => note.hoverComment(comment.selection),
onMouseEnter: () => {
if (note.commenting()) return
note.hoverComment(comment.selection)
},
onClick: () => {
if (note.isEditing(comment.id)) return
note.toggleComment(comment.id, comment.selection)
@@ -164,10 +167,14 @@ export function createLineCommentControllerV2<T extends LineCommentShape>(props:
},
selection: formatSelectedLineLabel(range, i18n.t),
onInput: note.setDraft,
onCancel: note.cancelDraft,
onCancel: () => {
note.cancelDraft()
note.select(null)
},
onSubmit: (comment) => {
props.onSubmit({ comment, selection: cloneSelectedLineRange(range) })
note.cancelDraft()
note.select(null)
},
cancelLabel: i18n.t("ui.lineComment.cancel"),
submitLabel: i18n.t("ui.lineComment.submit"),
@@ -78,6 +78,7 @@ export type PromptInputV2AttachmentConfig = {
onError: (error: unknown) => void
readClipboardImage?: () => Promise<File | null>
getPathForFile?: (file: File) => string
store?: (file: File) => Promise<{ id: string; url: string }>
}
export function createPromptInputV2Attachments(
@@ -102,8 +103,7 @@ export function createPromptInputV2Attachments(
if (toast) input.warn()
return false
}
const url = await dataUrl(file, mime)
if (!url) return false
const blob = input.store ? await input.store(file) : await blobReference(file)
const sourcePath = input.getPathForFile?.(file) || undefined
// Native clipboard images arrive with a fresh timestamped filename on every paste, so identical
// clipboard content is matched on bytes alone.
@@ -112,7 +112,7 @@ export function createPromptInputV2Attachments(
.some(
(part) =>
part.type === "image" &&
part.dataUrl === url &&
part.blob.id === blob.id &&
(sourcePath
? part.sourcePath === sourcePath
: !part.sourcePath && (clipboard || part.filename === file.name)),
@@ -127,7 +127,7 @@ export function createPromptInputV2Attachments(
filename: file.name,
sourcePath,
mime,
dataUrl: url,
blob,
}
target.prompt.set([...target.prompt.current(), attachment], target.cursor)
return true
@@ -219,20 +219,14 @@ export function createPromptInputV2Attachments(
}
}
function dataUrl(file: File, mime: string) {
return new Promise<string>((resolve) => {
const reader = new FileReader()
reader.addEventListener("error", () => resolve(""))
reader.addEventListener("load", () => {
const value = typeof reader.result === "string" ? reader.result : ""
const index = value.indexOf(",")
resolve(index === -1 ? value : `data:${mime};base64,${value.slice(index + 1)}`)
})
reader.readAsDataURL(file)
})
}
const imageMimes = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"])
async function blobReference(file: File) {
const id = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", await file.arrayBuffer())))
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("")
return { id, url: URL.createObjectURL(file) }
}
const imageExtensions = new Map([
["gif", "image/gif"],
["jpeg", "image/jpeg"],
@@ -425,7 +425,7 @@ export function PromptInputV2Attachments(props: {
}
>
<img
src={attachment.dataUrl}
src={attachment.blob.url}
alt={attachment.filename}
class="w-[58px] h-[46px] rounded-[6px] object-cover"
onClick={() => props.onAttachmentClick?.(attachment)}
@@ -121,7 +121,7 @@ function ControlledPromptInput() {
id: "attachment-1",
filename: "requirements.md",
mime: "text/markdown",
dataUrl: "data:text/markdown;base64,IyBSZXF1aXJlbWVudHM=",
blob: { id: "requirements", url: "data:text/markdown;base64,IyBSZXF1aXJlbWVudHM=" },
},
],
cursor: 0,
@@ -199,7 +199,7 @@ function ControlledPromptInput() {
id: `attachment-${store.state.prompt.filter((part) => part.type === "image").length + 1}`,
filename,
mime,
dataUrl: `data:${mime};base64,`,
blob: { id: filename, url: `data:${mime};base64,` },
})
}
@@ -13,7 +13,7 @@ function createPromptStore() {
id: "attachment-1",
filename: "notes.txt",
mime: "text/plain",
dataUrl: "data:text/plain;base64,",
blob: { id: "a", url: "blob:a" },
},
],
cursor: 3,
@@ -50,7 +50,7 @@ describe("prompt input v2 store", () => {
id: "attachment-1",
filename: "notes.txt",
mime: "text/plain",
dataUrl: "data:text/plain;base64,",
blob: { id: "a", url: "blob:a" },
},
])
expect(prompt.state.cursor).toBe(7)
@@ -31,7 +31,7 @@ export type PromptInputV2Attachment = {
filename: string
sourcePath?: string
mime: string
dataUrl: string
blob: { id: string; url: string }
}
export type PromptInputV2Prompt = (
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.18.11",
"version": "1.18.12",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/stats-app",
"version": "1.18.11",
"version": "1.18.12",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/stats-core",
"version": "1.18.11",
"version": "1.18.12",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/stats-server",
"version": "1.18.11",
"version": "1.18.12",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/tui",
"version": "1.18.11",
"version": "1.18.12",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.18.11",
"version": "1.18.12",
"type": "module",
"license": "MIT",
"repository": {
@@ -1,8 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.10913 12.07C3.65512 12.07 5.76627 11.5988 6.85825 10.98C7.95023 10.3612 7.95023 10.3612 10.207 8.75965C13.0642 6.73196 15.0845 7.41088 18.3968 7.41088" fill="currentColor"/>
<path d="M3.10913 12.07C3.65512 12.07 5.76627 11.5988 6.85825 10.98C7.95023 10.3612 7.95023 10.3612 10.207 8.75965C13.0642 6.73196 15.0845 7.41088 18.3968 7.41088" stroke="currentColor" stroke-width="3.27593"/>
<path d="M21.6 7.43108L16.0037 10.6622V4.20001L21.6 7.43108Z" fill="currentColor" stroke="currentColor" stroke-width="0.0363992"/>
<path d="M3 12.072C3.54599 12.072 5.65714 12.5432 6.74912 13.162C7.8411 13.7808 7.8411 13.7808 10.0978 15.3823C12.9551 17.41 14.9753 16.7311 18.2877 16.7311" fill="currentColor"/>
<path d="M3 12.072C3.54599 12.072 5.65714 12.5432 6.74912 13.162C7.8411 13.7808 7.8411 13.7808 10.0978 15.3823C12.9551 17.41 14.9753 16.7311 18.2877 16.7311" stroke="currentColor" stroke-width="3.27593"/>
<path d="M21.4909 16.7109L15.8945 13.4798V19.942L21.4909 16.7109Z" fill="currentColor" stroke="currentColor" stroke-width="0.0363992"/>
<path d="M728.039 234C819.325 234 893.323 308.62 893.323 400.668C893.323 492.716 819.325 567.336 728.039 567.336L891.984 732.656C912.81 753.655 898.061 789.56 868.613 789.56H397.472C245.333 789.56 122 665.193 122 511.78C122 358.367 245.333 234 397.472 234H728.039ZM397.472 345.112C306.189 345.112 232.189 419.732 232.189 511.78C232.189 603.828 306.189 678.448 397.472 678.448C488.756 678.448 562.756 603.828 562.756 511.78C562.756 419.732 488.756 345.112 397.472 345.112Z" fill="currentColor" transform="scale(0.0234375)"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 627 B

@@ -366,34 +366,9 @@
</symbol>
<symbol viewBox="0 0 24 24" fill="none" id="openrouter">
<path
d="M3.10913 12.07C3.65512 12.07 5.76627 11.5988 6.85825 10.98C7.95023 10.3612 7.95023 10.3612 10.207 8.75965C13.0642 6.73196 15.0845 7.41088 18.3968 7.41088"
d="M728.039 234C819.325 234 893.323 308.62 893.323 400.668C893.323 492.716 819.325 567.336 728.039 567.336L891.984 732.656C912.81 753.655 898.061 789.56 868.613 789.56H397.472C245.333 789.56 122 665.193 122 511.78C122 358.367 245.333 234 397.472 234H728.039ZM397.472 345.112C306.189 345.112 232.189 419.732 232.189 511.78C232.189 603.828 306.189 678.448 397.472 678.448C488.756 678.448 562.756 603.828 562.756 511.78C562.756 419.732 488.756 345.112 397.472 345.112Z"
fill="currentColor"
></path>
<path
d="M3.10913 12.07C3.65512 12.07 5.76627 11.5988 6.85825 10.98C7.95023 10.3612 7.95023 10.3612 10.207 8.75965C13.0642 6.73196 15.0845 7.41088 18.3968 7.41088"
stroke="currentColor"
stroke-width="3.27593"
></path>
<path
d="M21.6 7.43108L16.0037 10.6622V4.20001L21.6 7.43108Z"
fill="currentColor"
stroke="currentColor"
stroke-width="0.0363992"
></path>
<path
d="M3 12.072C3.54599 12.072 5.65714 12.5432 6.74912 13.162C7.8411 13.7808 7.8411 13.7808 10.0978 15.3823C12.9551 17.41 14.9753 16.7311 18.2877 16.7311"
fill="currentColor"
></path>
<path
d="M3 12.072C3.54599 12.072 5.65714 12.5432 6.74912 13.162C7.8411 13.7808 7.8411 13.7808 10.0978 15.3823C12.9551 17.41 14.9753 16.7311 18.2877 16.7311"
stroke="currentColor"
stroke-width="3.27593"
></path>
<path
d="M21.4909 16.7109L15.8945 13.4798V19.942L21.4909 16.7109Z"
fill="currentColor"
stroke="currentColor"
stroke-width="0.0363992"
transform="scale(0.0234375)"
></path>
</symbol>
<symbol viewBox="0 0 24 24" fill="none" id="opencode">

Before

Width:  |  Height:  |  Size: 276 KiB

After

Width:  |  Height:  |  Size: 275 KiB

+4 -2
View File
@@ -41,7 +41,8 @@
"syntax-type": "var(--v2-purple-800)",
"syntax-constant": "#007b80",
"syntax-critical": "var(--v2-red-800)",
"syntax-diff-delete": "#ff8c00",
"syntax-diff-add": "var(--v2-state-fg-success)",
"syntax-diff-delete": "var(--v2-state-fg-danger)",
"syntax-diff-unknown": "#a753ae",
"surface-critical-base": "#FFF2F0"
},
@@ -275,7 +276,8 @@
"syntax-type": "var(--v2-purple-400)",
"syntax-constant": "#93e9f6",
"syntax-critical": "var(--v2-red-400)",
"syntax-diff-delete": "#fab283",
"syntax-diff-add": "var(--v2-state-fg-success)",
"syntax-diff-delete": "var(--v2-state-fg-danger)",
"syntax-diff-unknown": "#edb2f1",
"surface-critical-base": "#1F0603"
},
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "@opencode-ai/web",
"type": "module",
"license": "MIT",
"version": "1.18.11",
"version": "1.18.12",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
+7 -1
View File
@@ -60,6 +60,7 @@ OpenCode Go هو اشتراك منخفض التكلفة — **$5 للشهر ال
- **MiMo-V2.5-Pro**
- **MiniMax M3**
- **MiniMax M2.7**
- **Qwen3.8 Max**
- **Qwen3.7 Max**
- **Qwen3.7 Plus**
- **Qwen3.6 Plus**
@@ -96,7 +97,8 @@ OpenCode Go هو اشتراك منخفض التكلفة — **$5 للشهر ال
| 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 |
| Qwen3.7 Max | 950 | 2,390 | 4,770 |
| Qwen3.8 Max | 160 | 400 | 810 |
| Qwen3.7 Max | 340 | 840 | 1,690 |
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 |
@@ -114,6 +116,7 @@ OpenCode Go هو اشتراك منخفض التكلفة — **$5 للشهر ال
- DeepSeek V4 Flash — 790 input، و68,000 cached، و280 output tokens لكل طلب
- MiniMax M3 — 510 input، و56,000 cached، و190 output tokens لكل طلب
- MiniMax M2.7 — 300 input، و55,000 cached، و125 output tokens لكل طلب
- Qwen3.8 Max — 420 input، و66,000 cached، و200 output tokens لكل طلب
- Qwen3.7 Max — 420 input، و66,000 cached، و200 output tokens لكل طلب
- Qwen3.7 Plus — 500 input، و57,000 cached، و190 output tokens لكل طلب
- Qwen3.6 Plus — 500 input، و57,000 cached، و190 output tokens لكل طلب
@@ -138,6 +141,7 @@ OpenCode Go هو اشتراك منخفض التكلفة — **$5 للشهر ال
| 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 |
| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 |
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 |
| 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 |
@@ -195,6 +199,7 @@ OpenCode Go هو اشتراك منخفض التكلفة — **$5 للشهر ال
| 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` |
| Qwen3.8 Max | qwen3.8-max | `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` |
@@ -227,6 +232,7 @@ https://opencode.ai/zen/go/v1/models
| Kimi K2.6 | غير مستخدَمة | 0 أيام |
| MiMo-V2.5-Pro | غير مستخدَمة | 0 أيام |
| MiMo-V2.5 | غير مستخدَمة | 0 أيام |
| Qwen3.8 Max | غير مستخدَمة | 0 أيام |
| Qwen3.7 Max | غير مستخدَمة | 0 أيام |
| Qwen3.7 Plus | غير مستخدَمة | 0 أيام |
| Qwen3.6 Plus | غير مستخدَمة | 0 أيام |
+7 -1
View File
@@ -70,6 +70,7 @@ Trenutna lista modela uključuje:
- **MiMo-V2.5-Pro**
- **MiniMax M3**
- **MiniMax M2.7**
- **Qwen3.8 Max**
- **Qwen3.7 Max**
- **Qwen3.7 Plus**
- **Qwen3.6 Plus**
@@ -106,7 +107,8 @@ Tabela ispod pruža procijenjeni broj zahtjeva na osnovu tipičnih obrazaca kori
| 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 |
| Qwen3.7 Max | 950 | 2,390 | 4,770 |
| Qwen3.8 Max | 160 | 400 | 810 |
| Qwen3.7 Max | 340 | 840 | 1,690 |
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 |
@@ -124,6 +126,7 @@ Procjene se zasnivaju na zapaženim obrascima zahtjeva:
- DeepSeek V4 Flash — 790 ulaznih, 68,000 keširanih, 280 izlaznih tokena po zahtjevu
- MiniMax M3 — 510 ulaznih, 56,000 keširanih, 190 izlaznih tokena po zahtjevu
- MiniMax M2.7 — 300 ulaznih, 55,000 keširanih, 125 izlaznih tokena po zahtjevu
- Qwen3.8 Max — 420 ulaznih, 66,000 keširanih, 200 izlaznih tokena po zahtjevu
- Qwen3.7 Max — 420 ulaznih, 66,000 keširanih, 200 izlaznih tokena po zahtjevu
- Qwen3.7 Plus — 500 ulaznih, 57,000 keširanih, 190 izlaznih tokena po zahtjevu
- Qwen3.6 Plus — 500 ulaznih, 57,000 keširanih, 190 izlaznih tokena po zahtjevu
@@ -148,6 +151,7 @@ Procjene se također zasnivaju na sljedećim cijenama po 1M tokena i mjesečnoj
| 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 |
| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 |
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 |
| 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 |
@@ -207,6 +211,7 @@ Također možete pristupiti Go modelima putem sljedećih API endpointa.
| 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` |
| Qwen3.8 Max | qwen3.8-max | `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` |
@@ -241,6 +246,7 @@ https://opencode.ai/zen/go/v1/models
| Kimi K2.6 | Ne koristi se | 0 dana |
| MiMo-V2.5-Pro | Ne koristi se | 0 dana |
| MiMo-V2.5 | Ne koristi se | 0 dana |
| Qwen3.8 Max | Ne koristi se | 0 dana |
| Qwen3.7 Max | Ne koristi se | 0 dana |
| Qwen3.7 Plus | Ne koristi se | 0 dana |
| Qwen3.6 Plus | Ne koristi se | 0 dana |
+7 -1
View File
@@ -70,6 +70,7 @@ Den nuværende liste over modeller inkluderer:
- **MiMo-V2.5-Pro**
- **MiniMax M3**
- **MiniMax M2.7**
- **Qwen3.8 Max**
- **Qwen3.7 Max**
- **Qwen3.7 Plus**
- **Qwen3.6 Plus**
@@ -106,7 +107,8 @@ Tabellen nedenfor giver et estimeret antal anmodninger baseret på typiske Go-fo
| 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 |
| Qwen3.7 Max | 950 | 2,390 | 4,770 |
| Qwen3.8 Max | 160 | 400 | 810 |
| Qwen3.7 Max | 340 | 840 | 1,690 |
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 |
@@ -124,6 +126,7 @@ Estimaterne er baseret på observerede anmodningsmønstre:
- DeepSeek V4 Flash — 790 input, 68.000 cachelagrede, 280 output-tokens pr. anmodning
- MiniMax M3 — 510 input, 56.000 cachelagrede, 190 output-tokens pr. anmodning
- MiniMax M2.7 — 300 input, 55.000 cachelagrede, 125 output-tokens pr. anmodning
- Qwen3.8 Max — 420 input, 66.000 cachelagrede, 200 output-tokens pr. anmodning
- Qwen3.7 Max — 420 input, 66.000 cachelagrede, 200 output-tokens pr. anmodning
- Qwen3.7 Plus — 500 input, 57.000 cachelagrede, 190 output-tokens pr. anmodning
- Qwen3.6 Plus — 500 input, 57.000 cachelagrede, 190 output-tokens pr. anmodning
@@ -148,6 +151,7 @@ Estimaterne er også baseret på følgende priser pr. 1M tokens og det månedlig
| 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 |
| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 |
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 |
| 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 |
@@ -207,6 +211,7 @@ Du kan også få adgang til Go-modeller gennem følgende API-endpoints.
| 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` |
| Qwen3.8 Max | qwen3.8-max | `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` |
@@ -241,6 +246,7 @@ https://opencode.ai/zen/go/v1/models
| Kimi K2.6 | Ikke brugt | 0 dage |
| MiMo-V2.5-Pro | Ikke brugt | 0 dage |
| MiMo-V2.5 | Ikke brugt | 0 dage |
| Qwen3.8 Max | Ikke brugt | 0 dage |
| Qwen3.7 Max | Ikke brugt | 0 dage |
| Qwen3.7 Plus | Ikke brugt | 0 dage |
| Qwen3.6 Plus | Ikke brugt | 0 dage |
+7 -1
View File
@@ -62,6 +62,7 @@ Die aktuelle Liste der Modelle umfasst:
- **MiMo-V2.5-Pro**
- **MiniMax M3**
- **MiniMax M2.7**
- **Qwen3.8 Max**
- **Qwen3.7 Max**
- **Qwen3.7 Plus**
- **Qwen3.6 Plus**
@@ -98,7 +99,8 @@ Die folgende Tabelle zeigt eine geschätzte Anzahl von Anfragen basierend auf ty
| 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 |
| Qwen3.7 Max | 950 | 2,390 | 4,770 |
| Qwen3.8 Max | 160 | 400 | 810 |
| Qwen3.7 Max | 340 | 840 | 1,690 |
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 |
@@ -116,6 +118,7 @@ Die Schätzungen basieren auf beobachteten Anfragemustern:
- DeepSeek V4 Flash — 790 Input-, 68.000 Cached-, 280 Output-Tokens pro Anfrage
- MiniMax M3 — 510 Input-, 56.000 Cached-, 190 Output-Tokens pro Anfrage
- MiniMax M2.7 — 300 Input-, 55.000 Cached-, 125 Output-Tokens pro Anfrage
- Qwen3.8 Max — 420 Input-, 66.000 Cached-, 200 Output-Tokens pro Anfrage
- Qwen3.7 Max — 420 Input-, 66.000 Cached-, 200 Output-Tokens pro Anfrage
- Qwen3.7 Plus — 500 Input-, 57.000 Cached-, 190 Output-Tokens pro Anfrage
- Qwen3.6 Plus — 500 Input-, 57.000 Cached-, 190 Output-Tokens pro Anfrage
@@ -140,6 +143,7 @@ Die Schätzungen basieren außerdem auf den folgenden Preisen pro 1M Tokens und
| 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 |
| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 |
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 |
| 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 |
@@ -197,6 +201,7 @@ Du kannst auf die Go-Modelle auch über die folgenden API-Endpunkte zugreifen.
| 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` |
| Qwen3.8 Max | qwen3.8-max | `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` |
@@ -229,6 +234,7 @@ https://opencode.ai/zen/go/v1/models
| Kimi K2.6 | Nicht verwendet | 0 Tage |
| MiMo-V2.5-Pro | Nicht verwendet | 0 Tage |
| MiMo-V2.5 | Nicht verwendet | 0 Tage |
| Qwen3.8 Max | Nicht verwendet | 0 Tage |
| Qwen3.7 Max | Nicht verwendet | 0 Tage |
| Qwen3.7 Plus | Nicht verwendet | 0 Tage |
| Qwen3.6 Plus | Nicht verwendet | 0 Tage |
+7 -1
View File
@@ -70,6 +70,7 @@ La lista actual de modelos incluye:
- **MiMo-V2.5-Pro**
- **MiniMax M3**
- **MiniMax M2.7**
- **Qwen3.8 Max**
- **Qwen3.7 Max**
- **Qwen3.7 Plus**
- **Qwen3.6 Plus**
@@ -106,7 +107,8 @@ La siguiente tabla proporciona una cantidad estimada de peticiones basada en los
| 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 |
| Qwen3.7 Max | 950 | 2,390 | 4,770 |
| Qwen3.8 Max | 160 | 400 | 810 |
| Qwen3.7 Max | 340 | 840 | 1,690 |
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 |
@@ -124,6 +126,7 @@ Las estimaciones se basan en los patrones de peticiones observados:
- DeepSeek V4 Flash — 790 tokens de entrada, 68,000 en caché, 280 tokens de salida por petición
- MiniMax M3 — 510 tokens de entrada, 56,000 en caché, 190 tokens de salida por petición
- MiniMax M2.7 — 300 tokens de entrada, 55,000 en caché, 125 tokens de salida por petición
- Qwen3.8 Max — 420 tokens de entrada, 66,000 en caché, 200 tokens de salida por petición
- Qwen3.7 Max — 420 tokens de entrada, 66,000 en caché, 200 tokens de salida por petición
- Qwen3.7 Plus — 500 tokens de entrada, 57,000 en caché, 190 tokens de salida por petición
- Qwen3.6 Plus — 500 tokens de entrada, 57,000 en caché, 190 tokens de salida por petición
@@ -148,6 +151,7 @@ Las estimaciones también se basan en los siguientes precios por 1M tokens y en
| 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 |
| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 |
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 |
| 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 |
@@ -207,6 +211,7 @@ También puedes acceder a los modelos de Go a través de los siguientes endpoint
| 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` |
| Qwen3.8 Max | qwen3.8-max | `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` |
@@ -241,6 +246,7 @@ https://opencode.ai/zen/go/v1/models
| Kimi K2.6 | No utilizado | 0 días |
| MiMo-V2.5-Pro | No utilizado | 0 días |
| MiMo-V2.5 | No utilizado | 0 días |
| Qwen3.8 Max | No utilizado | 0 días |
| Qwen3.7 Max | No utilizado | 0 días |
| Qwen3.7 Plus | No utilizado | 0 días |
| Qwen3.6 Plus | No utilizado | 0 días |
+7 -1
View File
@@ -60,6 +60,7 @@ La liste actuelle des modèles comprend :
- **MiMo-V2.5-Pro**
- **MiniMax M3**
- **MiniMax M2.7**
- **Qwen3.8 Max**
- **Qwen3.7 Max**
- **Qwen3.7 Plus**
- **Qwen3.6 Plus**
@@ -96,7 +97,8 @@ Le tableau ci-dessous fournit une estimation du nombre de requêtes basée sur d
| 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 |
| Qwen3.7 Max | 950 | 2,390 | 4,770 |
| Qwen3.8 Max | 160 | 400 | 810 |
| Qwen3.7 Max | 340 | 840 | 1,690 |
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 |
@@ -114,6 +116,7 @@ Les estimations sont basées sur les schémas de requêtes observés :
- DeepSeek V4 Flash — 790 tokens en entrée, 68,000 en cache, 280 tokens en sortie par requête
- MiniMax M3 — 510 tokens en entrée, 56,000 en cache, 190 tokens en sortie par requête
- MiniMax M2.7 — 300 tokens en entrée, 55,000 en cache, 125 tokens en sortie par requête
- Qwen3.8 Max — 420 tokens en entrée, 66,000 en cache, 200 tokens en sortie par requête
- Qwen3.7 Max — 420 tokens en entrée, 66,000 en cache, 200 tokens en sortie par requête
- Qwen3.7 Plus — 500 tokens en entrée, 57,000 en cache, 190 tokens en sortie par requête
- Qwen3.6 Plus — 500 tokens en entrée, 57,000 en cache, 190 tokens en sortie par requête
@@ -138,6 +141,7 @@ Les estimations sont également basées sur les prix suivants par 1M tokens et s
| 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 |
| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 |
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 |
| 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 |
@@ -195,6 +199,7 @@ Vous pouvez également accéder aux modèles Go via les points de terminaison d'
| 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` |
| Qwen3.8 Max | qwen3.8-max | `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` |
@@ -227,6 +232,7 @@ https://opencode.ai/zen/go/v1/models
| Kimi K2.6 | Non utilisé | 0 jour |
| MiMo-V2.5-Pro | Non utilisé | 0 jour |
| MiMo-V2.5 | Non utilisé | 0 jour |
| Qwen3.8 Max | Non utilisé | 0 jour |
| Qwen3.7 Max | Non utilisé | 0 jour |
| Qwen3.7 Plus | Non utilisé | 0 jour |
| Qwen3.6 Plus | Non utilisé | 0 jour |
+7 -1
View File
@@ -70,6 +70,7 @@ The current list of models includes:
- **MiMo-V2.5-Pro**
- **MiniMax M3**
- **MiniMax M2.7**
- **Qwen3.8 Max**
- **Qwen3.7 Max**
- **Qwen3.7 Plus**
- **Qwen3.6 Plus**
@@ -106,7 +107,8 @@ The table below provides an estimated request count based on typical Go usage pa
| 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 |
| Qwen3.7 Max | 950 | 2,390 | 4,770 |
| Qwen3.8 Max | 160 | 400 | 810 |
| Qwen3.7 Max | 340 | 840 | 1,690 |
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 |
@@ -126,6 +128,7 @@ The estimates are based on observed request patterns:
- MiniMax M2.7 — 300 input, 55,000 cached, 125 output tokens per request
- MiMo-V2.5 — 830 input, 71,500 cached, 295 output tokens per request
- 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.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
@@ -148,6 +151,7 @@ The estimates are also based on the following prices per 1M tokens and the month
| 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 |
| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 |
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 |
| 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 |
@@ -207,6 +211,7 @@ You can also access Go models through the following API endpoints.
| 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` |
| Qwen3.8 Max | qwen3.8-max | `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` |
@@ -241,6 +246,7 @@ https://opencode.ai/zen/go/v1/models
| Kimi K2.6 | Not used | 0 days |
| MiMo-V2.5-Pro | Not used | 0 days |
| MiMo-V2.5 | Not used | 0 days |
| Qwen3.8 Max | 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 |
+7 -1
View File
@@ -68,6 +68,7 @@ L'elenco attuale dei modelli include:
- **MiMo-V2.5-Pro**
- **MiniMax M3**
- **MiniMax M2.7**
- **Qwen3.8 Max**
- **Qwen3.7 Max**
- **Qwen3.7 Plus**
- **Qwen3.6 Plus**
@@ -104,7 +105,8 @@ La tabella seguente fornisce una stima del conteggio delle richieste in base a p
| 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 |
| Qwen3.7 Max | 950 | 2,390 | 4,770 |
| Qwen3.8 Max | 160 | 400 | 810 |
| Qwen3.7 Max | 340 | 840 | 1,690 |
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 |
@@ -122,6 +124,7 @@ Le stime si basano sui pattern di richieste osservati:
- DeepSeek V4 Flash — 790 di input, 68.000 in cache, 280 token di output per richiesta
- MiniMax M3 — 510 di input, 56.000 in cache, 190 token di output per richiesta
- MiniMax M2.7 — 300 di input, 55.000 in cache, 125 token di output per richiesta
- Qwen3.8 Max — 420 di input, 66.000 in cache, 200 token di output per richiesta
- Qwen3.7 Max — 420 di input, 66.000 in cache, 200 token di output per richiesta
- Qwen3.7 Plus — 500 di input, 57.000 in cache, 190 token di output per richiesta
- Qwen3.6 Plus — 500 di input, 57.000 in cache, 190 token di output per richiesta
@@ -146,6 +149,7 @@ Le stime si basano anche sui seguenti prezzi per 1M token e sull'utilizzo mensil
| 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 |
| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 |
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 |
| 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 |
@@ -205,6 +209,7 @@ Puoi anche accedere ai modelli Go tramite i seguenti endpoint API.
| 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` |
| Qwen3.8 Max | qwen3.8-max | `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` |
@@ -239,6 +244,7 @@ https://opencode.ai/zen/go/v1/models
| Kimi K2.6 | Non utilizzato | 0 giorni |
| MiMo-V2.5-Pro | Non utilizzato | 0 giorni |
| MiMo-V2.5 | Non utilizzato | 0 giorni |
| Qwen3.8 Max | Non utilizzato | 0 giorni |
| Qwen3.7 Max | Non utilizzato | 0 giorni |
| Qwen3.7 Plus | Non utilizzato | 0 giorni |
| Qwen3.6 Plus | Non utilizzato | 0 giorni |
+7 -1
View File
@@ -60,6 +60,7 @@ OpenCode Goをサブスクライブできるのは、1つのワークスペー
- **MiMo-V2.5-Pro**
- **MiniMax M3**
- **MiniMax M2.7**
- **Qwen3.8 Max**
- **Qwen3.7 Max**
- **Qwen3.7 Plus**
- **Qwen3.6 Plus**
@@ -96,7 +97,8 @@ OpenCode Goには以下の制限が含まれています:
| 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 |
| Qwen3.7 Max | 950 | 2,390 | 4,770 |
| Qwen3.8 Max | 160 | 400 | 810 |
| Qwen3.7 Max | 340 | 840 | 1,690 |
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 |
@@ -114,6 +116,7 @@ OpenCode Goには以下の制限が含まれています:
- DeepSeek V4 Flash — リクエストあたり 入力 790トークン、キャッシュ 68,000トークン、出力 280トークン
- MiniMax M3 — リクエストあたり 入力 510トークン、キャッシュ 56,000トークン、出力 190トークン
- MiniMax M2.7 — リクエストあたり 入力 300トークン、キャッシュ 55,000トークン、出力 125トークン
- Qwen3.8 Max — リクエストあたり 入力 420トークン、キャッシュ 66,000トークン、出力 200トークン
- Qwen3.7 Max — リクエストあたり 入力 420トークン、キャッシュ 66,000トークン、出力 200トークン
- Qwen3.7 Plus — リクエストあたり 入力 500トークン、キャッシュ 57,000トークン、出力 190トークン
- Qwen3.6 Plus — リクエストあたり 入力 500トークン、キャッシュ 57,000トークン、出力 190トークン
@@ -138,6 +141,7 @@ OpenCode Goには以下の制限が含まれています:
| 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 |
| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 |
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 |
| 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 |
@@ -195,6 +199,7 @@ Goでは月額$10を支払い、その6倍の利用枠を提供することを
| 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` |
| Qwen3.8 Max | qwen3.8-max | `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` |
@@ -227,6 +232,7 @@ https://opencode.ai/zen/go/v1/models
| Kimi K2.6 | 使用なし | 0日 |
| MiMo-V2.5-Pro | 使用なし | 0日 |
| MiMo-V2.5 | 使用なし | 0日 |
| Qwen3.8 Max | 使用なし | 0日 |
| Qwen3.7 Max | 使用なし | 0日 |
| Qwen3.7 Plus | 使用なし | 0日 |
| Qwen3.6 Plus | 使用なし | 0日 |
+7 -1
View File
@@ -60,6 +60,7 @@ workspace당 한 명의 멤버만 OpenCode Go를 구독할 수 있습니다.
- **MiMo-V2.5-Pro**
- **MiniMax M3**
- **MiniMax M2.7**
- **Qwen3.8 Max**
- **Qwen3.7 Max**
- **Qwen3.7 Plus**
- **Qwen3.6 Plus**
@@ -96,7 +97,8 @@ OpenCode Go에는 다음과 같은 한도가 포함됩니다.
| 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 |
| Qwen3.7 Max | 950 | 2,390 | 4,770 |
| Qwen3.8 Max | 160 | 400 | 810 |
| Qwen3.7 Max | 340 | 840 | 1,690 |
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 |
@@ -114,6 +116,7 @@ OpenCode Go에는 다음과 같은 한도가 포함됩니다.
- DeepSeek V4 Flash — 요청당 입력 790, 캐시 68,000, 출력 토큰 280
- MiniMax M3 — 요청당 입력 510, 캐시 56,000, 출력 토큰 190
- MiniMax M2.7 — 요청당 입력 300, 캐시 55,000, 출력 토큰 125
- Qwen3.8 Max — 요청당 입력 420, 캐시 66,000, 출력 토큰 200
- Qwen3.7 Max — 요청당 입력 420, 캐시 66,000, 출력 토큰 200
- Qwen3.7 Plus — 요청당 입력 500, 캐시 57,000, 출력 토큰 190
- Qwen3.6 Plus — 요청당 입력 500, 캐시 57,000, 출력 토큰 190
@@ -138,6 +141,7 @@ OpenCode Go에는 다음과 같은 한도가 포함됩니다.
| 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 |
| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 |
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 |
| 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 |
@@ -195,6 +199,7 @@ Go에서는 월 $10를 지불하며, 저희는 그 6배의 사용량을 제공
| 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` |
| Qwen3.8 Max | qwen3.8-max | `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` |
@@ -227,6 +232,7 @@ https://opencode.ai/zen/go/v1/models
| Kimi K2.6 | 사용되지 않음 | 0일 |
| MiMo-V2.5-Pro | 사용되지 않음 | 0일 |
| MiMo-V2.5 | 사용되지 않음 | 0일 |
| Qwen3.8 Max | 사용되지 않음 | 0일 |
| Qwen3.7 Max | 사용되지 않음 | 0일 |
| Qwen3.7 Plus | 사용되지 않음 | 0일 |
| Qwen3.6 Plus | 사용되지 않음 | 0일 |
+7 -1
View File
@@ -70,6 +70,7 @@ Den nåværende listen over modeller inkluderer:
- **MiMo-V2.5-Pro**
- **MiniMax M3**
- **MiniMax M2.7**
- **Qwen3.8 Max**
- **Qwen3.7 Max**
- **Qwen3.7 Plus**
- **Qwen3.6 Plus**
@@ -106,7 +107,8 @@ Tabellen nedenfor gir et estimert antall forespørsler basert på typiske bruksm
| 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 |
| Qwen3.7 Max | 950 | 2,390 | 4,770 |
| Qwen3.8 Max | 160 | 400 | 810 |
| Qwen3.7 Max | 340 | 840 | 1,690 |
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 |
@@ -124,6 +126,7 @@ Estimatene er basert på observerte forespørselsmønstre:
- DeepSeek V4 Flash — 790 input, 68 000 bufret, 280 output-tokens per forespørsel
- MiniMax M3 — 510 input, 56 000 bufret, 190 output-tokens per forespørsel
- MiniMax M2.7 — 300 input, 55 000 bufret, 125 output-tokens per forespørsel
- Qwen3.8 Max — 420 input, 66 000 bufret, 200 output-tokens per forespørsel
- Qwen3.7 Max — 420 input, 66 000 bufret, 200 output-tokens per forespørsel
- Qwen3.7 Plus — 500 input, 57 000 bufret, 190 output-tokens per forespørsel
- Qwen3.6 Plus — 500 input, 57 000 bufret, 190 output-tokens per forespørsel
@@ -148,6 +151,7 @@ Estimatene er også basert på følgende priser per 1M tokens og den månedlige
| 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 |
| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 |
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 |
| 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 |
@@ -207,6 +211,7 @@ Du kan også få tilgang til Go-modeller gjennom følgende API-endepunkter.
| 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` |
| Qwen3.8 Max | qwen3.8-max | `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` |
@@ -241,6 +246,7 @@ https://opencode.ai/zen/go/v1/models
| Kimi K2.6 | Brukes ikke | 0 dager |
| MiMo-V2.5-Pro | Brukes ikke | 0 dager |
| MiMo-V2.5 | Brukes ikke | 0 dager |
| Qwen3.8 Max | Brukes ikke | 0 dager |
| Qwen3.7 Max | Brukes ikke | 0 dager |
| Qwen3.7 Plus | Brukes ikke | 0 dager |
| Qwen3.6 Plus | Brukes ikke | 0 dager |
+7 -1
View File
@@ -64,6 +64,7 @@ Obecna lista modeli obejmuje:
- **MiMo-V2.5-Pro**
- **MiniMax M3**
- **MiniMax M2.7**
- **Qwen3.8 Max**
- **Qwen3.7 Max**
- **Qwen3.7 Plus**
- **Qwen3.6 Plus**
@@ -100,7 +101,8 @@ Poniższa tabela przedstawia szacunkową liczbę żądań na podstawie typowych
| 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 |
| Qwen3.7 Max | 950 | 2,390 | 4,770 |
| Qwen3.8 Max | 160 | 400 | 810 |
| Qwen3.7 Max | 340 | 840 | 1,690 |
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 |
@@ -118,6 +120,7 @@ Szacunki te opierają się na zaobserwowanych wzorcach żądań:
- DeepSeek V4 Flash — 790 tokenów wejściowych, 68 000 w pamięci podręcznej, 280 tokenów wyjściowych na żądanie
- MiniMax M3 — 510 tokenów wejściowych, 56 000 w pamięci podręcznej, 190 tokenów wyjściowych na żądanie
- MiniMax M2.7 — 300 tokenów wejściowych, 55 000 w pamięci podręcznej, 125 tokenów wyjściowych na żądanie
- Qwen3.8 Max — 420 tokenów wejściowych, 66 000 w pamięci podręcznej, 200 tokenów wyjściowych na żądanie
- Qwen3.7 Max — 420 tokenów wejściowych, 66 000 w pamięci podręcznej, 200 tokenów wyjściowych na żądanie
- Qwen3.7 Plus — 500 tokenów wejściowych, 57 000 w pamięci podręcznej, 190 tokenów wyjściowych na żądanie
- Qwen3.6 Plus — 500 tokenów wejściowych, 57 000 w pamięci podręcznej, 190 tokenów wyjściowych na żądanie
@@ -142,6 +145,7 @@ Szacunki opierają się również na następujących cenach za 1M tokenów oraz
| 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 |
| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 |
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 |
| 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 |
@@ -199,6 +203,7 @@ Możesz również uzyskać dostęp do modeli Go za pośrednictwem następującyc
| 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` |
| Qwen3.8 Max | qwen3.8-max | `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` |
@@ -233,6 +238,7 @@ https://opencode.ai/zen/go/v1/models
| Kimi K2.6 | Niewykorzystywane | 0 dni |
| MiMo-V2.5-Pro | Niewykorzystywane | 0 dni |
| MiMo-V2.5 | Niewykorzystywane | 0 dni |
| Qwen3.8 Max | Niewykorzystywane | 0 dni |
| Qwen3.7 Max | Niewykorzystywane | 0 dni |
| Qwen3.7 Plus | Niewykorzystywane | 0 dni |
| Qwen3.6 Plus | Niewykorzystywane | 0 dni |
+10 -14
View File
@@ -2244,9 +2244,9 @@ Some useful routing options:
### xAI
Three ways to authenticate: a SuperGrok subscription via browser OAuth, the same SuperGrok subscription via a headless device-code flow (for VPS / SSH / Docker), or a pay-as-you-go API key from the xAI console.
Three ways to authenticate: a SuperGrok subscription via device-code OAuth, the same SuperGrok subscription via a local callback, or a pay-as-you-go API key from the xAI console.
#### Option A — SuperGrok OAuth (browser login)
#### Option A — SuperGrok OAuth
1. Run the `/connect` command and search for **xAI**.
@@ -2254,9 +2254,11 @@ Three ways to authenticate: a SuperGrok subscription via browser OAuth, the same
/connect
```
2. Select **xAI Grok OAuth (SuperGrok Subscription)**. OpenCode opens xAI's consent screen in your browser and waits for the callback on `http://127.0.0.1:56121/callback`.
2. Select **xAI Grok OAuth (SuperGrok Subscription)**. OpenCode prints a verification URL and a short user code.
3. Run the `/models` command to select a Grok model.
3. Open the URL, enter the code, and approve the consent screen. OpenCode polls xAI's token endpoint and stores the resulting OAuth tokens once you approve.
4. Run the `/models` command to select a Grok model.
```txt
/models
@@ -2264,23 +2266,17 @@ Three ways to authenticate: a SuperGrok subscription via browser OAuth, the same
OpenCode refreshes the OAuth access token automatically. Any Grok or X Premium plan that includes Grok API access works; you do not need a separate `XAI_API_KEY`.
#### Option B — SuperGrok device-code (headless / remote server / VPS)
#### Option B — SuperGrok local callback
Use this when OpenCode is running somewhere a browser can't reach the loopback redirect: a VPS, a remote dev box over SSH, inside Docker, in CI, etc. No callback port is opened on the host running OpenCode — instead xAI hands the CLI a short code that you type into a browser on any other device (laptop, phone, …).
Use this only if you prefer a local browser callback and the browser can reach OpenCode on the same machine.
1. Run the `/connect` command on the remote host and search for **xAI**.
1. Run the `/connect` command and search for **xAI**.
```txt
/connect
```
2. Select **xAI Grok OAuth (Headless / Remote / VPS)**. OpenCode prints a verification URL and a short user code.
```txt
Open https://x.ai/device on any device and enter code: ABCD-1234
```
3. Open the URL on a device that has a browser (your laptop or phone), enter the code, and approve the consent screen. OpenCode polls xAI's token endpoint and stores the resulting OAuth tokens once you approve. Token refresh works the same as Option A.
2. Select **xAI Grok OAuth (Local Callback)**. OpenCode opens xAI's consent screen and waits for the callback on `http://127.0.0.1:56121/callback`. Token refresh works the same as Option A.
#### Option C — API key
+7 -1
View File
@@ -70,6 +70,7 @@ A lista atual de modelos inclui:
- **MiMo-V2.5-Pro**
- **MiniMax M3**
- **MiniMax M2.7**
- **Qwen3.8 Max**
- **Qwen3.7 Max**
- **Qwen3.7 Plus**
- **Qwen3.6 Plus**
@@ -106,7 +107,8 @@ A tabela abaixo fornece uma contagem estimada de requisições com base nos padr
| 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 |
| Qwen3.7 Max | 950 | 2,390 | 4,770 |
| Qwen3.8 Max | 160 | 400 | 810 |
| Qwen3.7 Max | 340 | 840 | 1,690 |
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 |
@@ -124,6 +126,7 @@ As estimativas se baseiam nos padrões de requisições observados:
- DeepSeek V4 Flash — 790 tokens de entrada, 68.000 em cache, 280 tokens de saída por requisição
- MiniMax M3 — 510 tokens de entrada, 56.000 em cache, 190 tokens de saída por requisição
- MiniMax M2.7 — 300 tokens de entrada, 55.000 em cache, 125 tokens de saída por requisição
- Qwen3.8 Max — 420 tokens de entrada, 66.000 em cache, 200 tokens de saída por requisição
- Qwen3.7 Max — 420 tokens de entrada, 66.000 em cache, 200 tokens de saída por requisição
- Qwen3.7 Plus — 500 tokens de entrada, 57.000 em cache, 190 tokens de saída por requisição
- Qwen3.6 Plus — 500 tokens de entrada, 57.000 em cache, 190 tokens de saída por requisição
@@ -148,6 +151,7 @@ As estimativas também se baseiam nos seguintes preços por 1M tokens e no uso m
| 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 |
| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 |
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 |
| 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 |
@@ -207,6 +211,7 @@ Você também pode acessar os modelos do Go através dos seguintes endpoints de
| 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` |
| Qwen3.8 Max | qwen3.8-max | `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` |
@@ -241,6 +246,7 @@ https://opencode.ai/zen/go/v1/models
| Kimi K2.6 | Não usado | 0 dias |
| MiMo-V2.5-Pro | Não usado | 0 dias |
| MiMo-V2.5 | Não usado | 0 dias |
| Qwen3.8 Max | Não usado | 0 dias |
| Qwen3.7 Max | Não usado | 0 dias |
| Qwen3.7 Plus | Não usado | 0 dias |
| Qwen3.6 Plus | Não usado | 0 dias |
+7 -1
View File
@@ -70,6 +70,7 @@ OpenCode Go работает так же, как и любой другой пр
- **MiMo-V2.5-Pro**
- **MiniMax M3**
- **MiniMax M2.7**
- **Qwen3.8 Max**
- **Qwen3.7 Max**
- **Qwen3.7 Plus**
- **Qwen3.6 Plus**
@@ -106,7 +107,8 @@ OpenCode Go включает следующие лимиты:
| 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 |
| Qwen3.7 Max | 950 | 2,390 | 4,770 |
| Qwen3.8 Max | 160 | 400 | 810 |
| Qwen3.7 Max | 340 | 840 | 1,690 |
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 |
@@ -124,6 +126,7 @@ OpenCode Go включает следующие лимиты:
- DeepSeek V4 Flash — 790 входных, 68,000 кешированных, 280 выходных токенов на запрос
- MiniMax M3 — 510 входных, 56,000 кешированных, 190 выходных токенов на запрос
- MiniMax M2.7 — 300 входных, 55,000 кешированных, 125 выходных токенов на запрос
- Qwen3.8 Max — 420 входных, 66,000 кешированных, 200 выходных токенов на запрос
- Qwen3.7 Max — 420 входных, 66,000 кешированных, 200 выходных токенов на запрос
- Qwen3.7 Plus — 500 входных, 57,000 кешированных, 190 выходных токенов на запрос
- Qwen3.6 Plus — 500 входных, 57,000 кешированных, 190 выходных токенов на запрос
@@ -148,6 +151,7 @@ OpenCode Go включает следующие лимиты:
| 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 |
| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 |
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 |
| 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 |
@@ -207,6 +211,7 @@ OpenCode Go включает следующие лимиты:
| 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` |
| Qwen3.8 Max | qwen3.8-max | `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` |
@@ -241,6 +246,7 @@ https://opencode.ai/zen/go/v1/models
| Kimi K2.6 | Не используется | 0 дней |
| MiMo-V2.5-Pro | Не используется | 0 дней |
| MiMo-V2.5 | Не используется | 0 дней |
| Qwen3.8 Max | Не используется | 0 дней |
| Qwen3.7 Max | Не используется | 0 дней |
| Qwen3.7 Plus | Не используется | 0 дней |
| Qwen3.6 Plus | Не используется | 0 дней |

Some files were not shown because too many files have changed in this diff Show More