Compare commits

..

9 Commits

Author SHA1 Message Date
Luke Parker e024e2ef92 fix(app): align debug gutter text (#39782) 2026-07-31 01:46:36 +00:00
Deven Navani ceb4890ca3 docs: add Modal provider setup (#39710) 2026-07-30 13:29:32 -05:00
opencode-agent[bot] 3f239b3834 chore: update nix node_modules hashes 2026-07-30 16:46:38 +00:00
Aiden Cline c1ee3c6e34 fix(opencode): stop MCP SSE error reconnect loops (#39697) 2026-07-30 11:26:48 -05:00
opencode-agent[bot] a1ab489e61 fix(provider): widen interleaved reasoning fields (#39556)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-07-30 11:10:34 -05:00
opencode 8c38d260eb sync release versions for v1.18.10 2026-07-30 14:43:50 +00:00
opencode-agent[bot] a4f25a94b4 fix: use OpenCode model catalog URL (#39672)
Co-authored-by: Dax Raad <d@ironbay.co>
2026-07-30 10:12:08 -04:00
opencode-agent[bot] ff0382e971 chore: generate 2026-07-30 09:31:27 +00:00
OpeOginni ac5380614f fix(app): repair malformed persisted tabs (#39645) 2026-07-30 17:29:40 +08:00
51 changed files with 371 additions and 318 deletions
+28 -28
View File
@@ -29,7 +29,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.18.9",
"version": "1.18.10",
"dependencies": {
"@corvu/drawer": "catalog:",
"@dnd-kit/abstract": "0.5.0",
@@ -96,7 +96,7 @@
},
"packages/cli": {
"name": "@opencode-ai/cli",
"version": "1.18.9",
"version": "1.18.10",
"bin": {
"lildax": "./bin/lildax.cjs",
},
@@ -144,7 +144,7 @@
},
"packages/codemode": {
"name": "@opencode-ai/codemode",
"version": "1.18.9",
"version": "1.18.10",
"dependencies": {
"acorn": "8.15.0",
"effect": "catalog:",
@@ -158,7 +158,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.18.9",
"version": "1.18.10",
"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.9",
"version": "1.18.10",
"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.9",
"version": "1.18.10",
"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.9",
"version": "1.18.10",
"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.9",
"version": "1.18.10",
"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.9",
"version": "1.18.10",
"bin": {
"opencode": "./bin/opencode",
},
@@ -381,7 +381,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.18.9",
"version": "1.18.10",
"dependencies": {
"@zip.js/zip.js": "2.7.62",
"effect": "catalog:",
@@ -435,7 +435,7 @@
},
"packages/effect-drizzle-sqlite": {
"name": "@opencode-ai/effect-drizzle-sqlite",
"version": "1.18.9",
"version": "1.18.10",
"dependencies": {
"drizzle-orm": "catalog:",
"effect": "catalog:",
@@ -449,7 +449,7 @@
},
"packages/effect-sqlite-node": {
"name": "@opencode-ai/effect-sqlite-node",
"version": "1.18.9",
"version": "1.18.10",
"dependencies": {
"effect": "catalog:",
},
@@ -461,7 +461,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.18.9",
"version": "1.18.10",
"dependencies": {
"@hono/standard-validator": "catalog:",
"@opencode-ai/core": "workspace:*",
@@ -493,7 +493,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.18.9",
"version": "1.18.10",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -509,7 +509,7 @@
},
"packages/http-recorder": {
"name": "@opencode-ai/http-recorder",
"version": "1.18.9",
"version": "1.18.10",
"dependencies": {
"@effect/platform-node": "4.0.0-beta.83",
"@effect/platform-node-shared": "4.0.0-beta.83",
@@ -540,7 +540,7 @@
},
"packages/llm": {
"name": "@opencode-ai/llm",
"version": "1.18.9",
"version": "1.18.10",
"dependencies": {
"@opencode-ai/schema": "workspace:*",
"@smithy/eventstream-codec": "4.2.14",
@@ -559,7 +559,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.18.9",
"version": "1.18.10",
"bin": {
"opencode": "./bin/opencode",
},
@@ -690,7 +690,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.18.9",
"version": "1.18.10",
"dependencies": {
"@ai-sdk/provider": "3.0.8",
"@opencode-ai/sdk": "workspace:*",
@@ -766,7 +766,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.18.9",
"version": "1.18.10",
"dependencies": {
"cross-spawn": "catalog:",
},
@@ -781,7 +781,7 @@
},
"packages/server": {
"name": "@opencode-ai/server",
"version": "1.18.9",
"version": "1.18.10",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/protocol": "workspace:*",
@@ -796,7 +796,7 @@
},
"packages/session-ui": {
"name": "@opencode-ai/session-ui",
"version": "1.18.9",
"version": "1.18.10",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz",
@@ -841,7 +841,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.18.9",
"version": "1.18.10",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -854,7 +854,7 @@
},
"packages/stats/app": {
"name": "@opencode-ai/stats-app",
"version": "1.18.9",
"version": "1.18.10",
"dependencies": {
"@ibm/plex": "6.4.1",
"@kobalte/core": "catalog:",
@@ -888,7 +888,7 @@
},
"packages/stats/core": {
"name": "@opencode-ai/stats-core",
"version": "1.18.9",
"version": "1.18.10",
"dependencies": {
"@aws-sdk/client-athena": "3.933.0",
"@planetscale/database": "1.19.0",
@@ -907,7 +907,7 @@
},
"packages/stats/server": {
"name": "@opencode-ai/stats-server",
"version": "1.18.9",
"version": "1.18.10",
"dependencies": {
"@aws-sdk/client-firehose": "3.933.0",
"@effect/platform-node": "catalog:",
@@ -949,7 +949,7 @@
},
"packages/tui": {
"name": "@opencode-ai/tui",
"version": "1.18.9",
"version": "1.18.10",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
@@ -976,7 +976,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.18.9",
"version": "1.18.10",
"dependencies": {
"@kobalte/core": "catalog:",
"@pierre/diffs": "catalog:",
@@ -1028,7 +1028,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.18.9",
"version": "1.18.10",
"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-fK6zAHJC3ut/KUdfqLPxVMH7Z1yv65YZ7qcHF45kPas=",
"aarch64-linux": "sha256-CSWZY/vVUCKzlwfisQQHqHjawUwL0BRc8Y9o2y/QZYA=",
"aarch64-darwin": "sha256-IyFm5NbnU63BaOO/F4/v1exz3VbvkY96yjg9iun+O9Q=",
"x86_64-darwin": "sha256-XnXFoUxqXmEx7AKx5YhaJuLY2NYMtI0ICafaenU5L6k="
"x86_64-linux": "sha256-AD5kt1o035hdvYPyrvjiz2mcsXFJ7UJVEmbE9AJFDFs=",
"aarch64-linux": "sha256-f1gVXUpIRWC/I8dxIigdGGvPsju04MXVjZhtq5EYL64=",
"aarch64-darwin": "sha256-c+JRpjs2gsgQ2czyM6bTbYEdMbX/erOSQdX6TdrI59k=",
"x86_64-darwin": "sha256-iB7l4PtQQ9Mx/XAdtyayiLe6pZKhuXB9lpmrbTr+uWc="
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.18.9",
"version": "1.18.10",
"description": "",
"type": "module",
"exports": {
+36 -18
View File
@@ -66,7 +66,7 @@ function Cell(props: {
<div
classList={{
"flex min-w-0 items-center": true,
"min-h-[20px] w-fit flex-row justify-start gap-1.5 px-1.5 py-0.5 text-left": !!props.inline,
"min-h-[20px] w-fit justify-start px-1.5 py-0.5 text-left": !!props.inline,
"justify-center text-center": !props.inline,
"min-h-[42px] w-full flex-col rounded-[8px] px-0.5 py-1": !props.inline,
"col-span-2": !!props.wide && !props.inline,
@@ -74,21 +74,29 @@ function Cell(props: {
>
<div
classList={{
"text-[10px] leading-none font-black uppercase tracking-[0.04em] opacity-70": true,
"flex min-w-0": true,
"-translate-y-px items-baseline gap-1.5": !!props.inline,
"flex-col items-center": !props.inline,
}}
>
{props.label}
</div>
<div
classList={{
"uppercase leading-none font-bold tabular-nums": true,
"text-[11px]": !!props.inline,
"text-[13px] sm:text-[14px]": !props.inline,
"text-text-on-critical-base": !!props.bad,
"opacity-70": !!props.dim,
}}
>
{props.value}
<div
classList={{
"text-[10px] leading-none font-black uppercase tracking-[0.04em] opacity-70": true,
}}
>
{props.label}
</div>
<div
classList={{
"uppercase leading-none font-bold tabular-nums": true,
"text-[11px]": !!props.inline,
"text-[13px] sm:text-[14px]": !props.inline,
"text-text-on-critical-base": !!props.bad,
"opacity-70": !!props.dim,
}}
>
{props.value}
</div>
</div>
</div>
)
@@ -116,15 +124,25 @@ function FocusCell(props: { active: boolean; inline?: boolean; onClick: () => vo
aria-pressed={props.active}
classList={{
"flex min-w-0 items-center font-mono uppercase hover:bg-surface-raised-base focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-border-focus": true,
"min-h-[20px] w-fit flex-row justify-start gap-1.5 rounded px-1.5 py-0.5 text-left": !!props.inline,
"min-h-[20px] w-fit justify-start rounded px-1.5 py-0.5 text-left": !!props.inline,
"min-h-[42px] w-full flex-col justify-center rounded-[8px] px-0.5 py-1 text-center": !props.inline,
"bg-surface-raised-base text-text-strong": props.active,
}}
onClick={props.onClick}
>
<span class="text-[10px] leading-none font-black tracking-[0.04em] opacity-70">FOCUS</span>
<span classList={{ "leading-none font-bold": true, "text-[11px]": !!props.inline, "text-[13px]": !props.inline }}>
{props.active ? "ON" : "OFF"}
<span
classList={{
flex: true,
"-translate-y-px items-baseline gap-1.5": !!props.inline,
"flex-col items-center": !props.inline,
}}
>
<span class="text-[10px] leading-none font-black tracking-[0.04em] opacity-70">FOCUS</span>
<span
classList={{ "leading-none font-bold": true, "text-[11px]": !!props.inline, "text-[13px]": !props.inline }}
>
{props.active ? "ON" : "OFF"}
</span>
</span>
</button>
)
+23
View File
@@ -0,0 +1,23 @@
import type { ServerConnection } from "./server"
import type { Tab } from "./tabs"
export function migrateTabs(value: unknown, fallback: ServerConnection.Key): Tab[] {
if (!Array.isArray(value)) return []
return value.flatMap<Tab>((tab) => {
if (!tab || typeof tab !== "object") return []
if ("server" in tab && typeof tab.server !== "string") return []
const server = ("server" in tab ? tab.server : fallback) as ServerConnection.Key
if (tab.type === "session" && typeof tab.sessionId === "string") {
return [{ type: tab.type, server, sessionId: tab.sessionId }]
}
if (
tab.type === "draft" &&
typeof tab.draftID === "string" &&
typeof tab.directory === "string" &&
(tab.worktree === undefined || typeof tab.worktree === "string")
) {
return [{ type: tab.type, server, draftID: tab.draftID, directory: tab.directory, worktree: tab.worktree }]
}
return []
})
}
+18
View File
@@ -3,6 +3,7 @@ import { createRoot, getOwner, onCleanup } from "solid-js"
import { createTabMemory } from "./tab-memory"
import { nextTabAfterClose, pushClosedTab, removeClosedTabs, takeClosedTab, type ClosedTab } from "./closed-tabs"
import type { SessionTab, Tab } from "./tabs"
import { migrateTabs } from "./tab-migration"
import type { ServerConnection } from "./server"
const server = "local\nhttp://localhost:4096" as ServerConnection.Key
@@ -11,6 +12,23 @@ function sessionTab(sessionId: string): SessionTab {
return { type: "session", server, sessionId }
}
describe("tab migration", () => {
test("drops null and malformed persisted tabs", () => {
expect(
migrateTabs([null, sessionTab("a"), { type: "session", server }, { type: "unknown", server }, "invalid"], server),
).toEqual([sessionTab("a")])
})
test("adds the fallback server to valid legacy tabs", () => {
expect(migrateTabs([{ type: "session", sessionId: "a", dirBase64: "legacy" }], server)).toEqual([sessionTab("a")])
})
test("replaces invalid top-level persisted data", () => {
expect(migrateTabs(null, server)).toEqual([])
expect(migrateTabs({}, server)).toEqual([])
})
})
describe("tab memory", () => {
test("keeps state until its tab is removed", () => {
createRoot((dispose) => {
+2 -7
View File
@@ -12,6 +12,7 @@ import { sessionHref } from "@/utils/session-route"
import { createTabMemory } from "./tab-memory"
import { nextTabAfterClose, pushClosedTab, removeClosedTabs, takeClosedTab, type ClosedTab } from "./closed-tabs"
import { createDraftPromptSession, type PromptModel } from "./prompt-state"
import { migrateTabs } from "./tab-migration"
export type SessionTab = {
type: "session"
@@ -59,13 +60,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
const [store, setStore, _, ready] = persisted(
{
...Persist.window("tabs"),
migrate: (value: unknown) => {
if (!Array.isArray(value)) return value
return value.map((tab) => {
if (!tab || typeof tab !== "object" || "server" in tab) return tab
return { ...tab, server: fallback }
})
},
migrate: (value: unknown) => migrateTabs(value, fallback),
},
createStore<Tab[]>([]),
)
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/cli",
"version": "1.18.9",
"version": "1.18.10",
"type": "module",
"license": "MIT",
"bin": {
+1 -1
View File
@@ -1,4 +1,4 @@
const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.dev"
const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.opencode.ai"
export const modelsData = process.env.MODELS_DEV_API_JSON
? await Bun.file(process.env.MODELS_DEV_API_JSON).text()
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/codemode",
"version": "1.18.9",
"version": "1.18.10",
"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.9",
"version": "1.18.10",
"type": "module",
"license": "MIT",
"scripts": {
@@ -1,40 +0,0 @@
import type { Stripe } from "stripe"
export function liteCheckoutSubscriptionToProvision(
type: "checkout.session.completed" | "checkout.session.async_payment_succeeded",
session: Pick<Stripe.Checkout.Session, "mode" | "payment_status" | "subscription">,
) {
if (session.mode !== "subscription") return
if (typeof session.subscription !== "string") return
if (type === "checkout.session.completed" && session.payment_status === "unpaid") return
return session.subscription
}
export function liteSubscriptionProvisioningData(
subscription: Pick<
Stripe.Subscription,
"id" | "status" | "customer" | "default_payment_method" | "metadata"
>,
) {
if (subscription.metadata.type !== "lite") return
if (subscription.status !== "active" && subscription.status !== "trialing") return
if (typeof subscription.customer !== "string") throw new Error("Customer ID not found")
const workspaceID = subscription.metadata.workspaceID
const userID = subscription.metadata.userID
if (!workspaceID) throw new Error("Workspace ID not found")
if (!userID) throw new Error("User ID not found")
return {
subscriptionID: subscription.id,
customerID: subscription.customer,
workspaceID,
userID,
userEmail: subscription.metadata.userEmail || undefined,
coupon: subscription.metadata.coupon || undefined,
paymentMethodID:
typeof subscription.default_payment_method === "string"
? subscription.default_payment_method
: subscription.default_payment_method?.id,
}
}
+79 -101
View File
@@ -10,10 +10,6 @@ import { Resource } from "@opencode-ai/console-resource"
import { LiteData } from "@opencode-ai/console-core/lite.js"
import { BlackData } from "@opencode-ai/console-core/black.js"
import { Referral } from "@opencode-ai/console-core/referral.js"
import {
liteCheckoutSubscriptionToProvision,
liteSubscriptionProvisioningData,
} from "./lite-subscription.js"
export async function POST(input: APIEvent) {
const body = await Billing.stripe().webhooks.constructEventAsync(
@@ -24,10 +20,6 @@ export async function POST(input: APIEvent) {
console.log(body.type, JSON.stringify(body, null, 2))
return (async () => {
if (body.type === "checkout.session.completed" || body.type === "checkout.session.async_payment_succeeded") {
const subscriptionID = liteCheckoutSubscriptionToProvision(body.type, body.data.object)
if (subscriptionID) await provisionLiteSubscription(subscriptionID)
}
if (body.type === "customer.updated") {
// check default payment method changed
const prevInvoiceSettings = body.data.previous_attributes?.invoice_settings ?? {}
@@ -113,6 +105,85 @@ export async function POST(input: APIEvent) {
})
})
}
if (body.type === "customer.subscription.created") {
const type = body.data.object.metadata?.type
if (type === "lite") {
const workspaceID = body.data.object.metadata?.workspaceID
const userID = body.data.object.metadata?.userID
const userEmail = body.data.object.metadata?.userEmail
const coupon = body.data.object.metadata?.coupon
const customerID = body.data.object.customer as string
const invoiceID = body.data.object.latest_invoice as string
const subscriptionID = body.data.object.id as string
const paymentMethodID = body.data.object.default_payment_method as string
if (!workspaceID) throw new Error("Workspace ID not found")
if (!userID) throw new Error("User ID not found")
if (!customerID) throw new Error("Customer ID not found")
if (!invoiceID) throw new Error("Invoice ID not found")
if (!subscriptionID) throw new Error("Subscription ID not found")
if (!paymentMethodID) throw new Error("Payment method ID not found")
// get payment method for the payment intent
const paymentMethod = await Billing.stripe().paymentMethods.retrieve(paymentMethodID)
await Actor.provide("system", { workspaceID }, async () => {
// look up current billing
const billing = await Billing.get()
if (!billing) throw new Error(`Workspace with ID ${workspaceID} not found`)
if (billing.customerID && billing.customerID !== customerID) throw new Error("Customer ID mismatch")
// set customer metadata
if (!billing?.customerID) {
await Billing.stripe().customers.update(customerID, {
metadata: {
workspaceID,
},
})
}
await Database.transaction(async (tx) => {
await tx
.update(BillingTable)
.set({
customerID,
liteSubscriptionID: subscriptionID,
lite: {},
paymentMethodID: paymentMethod.id,
paymentMethodLast4: paymentMethod.card?.last4 ?? null,
paymentMethodType: paymentMethod.type,
})
.where(eq(BillingTable.workspaceID, workspaceID))
await tx.insert(LiteTable).values({
workspaceID,
id: Identifier.create("lite"),
userID: userID,
})
if (userEmail) {
if (coupon === LiteData.firstMonth50Coupon) {
await Billing.redeemCoupon(userEmail, "GO1MONTH50")
} else if (coupon === LiteData.firstMonth100Coupon) {
await Billing.redeemCoupon(userEmail, "GOFREEMONTH")
} else if (coupon === LiteData.threeMonths100Coupon) {
await Billing.redeemCoupon(userEmail, "GO3MONTHS100")
} else if (coupon === LiteData.sixMonths100Coupon) {
await Billing.redeemCoupon(userEmail, "GO6MONTHS100")
} else if (coupon === LiteData.twelveMonths100Coupon) {
await Billing.redeemCoupon(userEmail, "GO12MONTHS100")
}
}
})
await Referral.completeFromLiteSubscription({
workspaceID,
userID,
}).catch((error) => {
console.error("Referral sync failed", error)
})
})
}
}
if (body.type === "customer.subscription.updated" && body.data.object.status === "incomplete_expired") {
const subscriptionID = body.data.object.id
if (!subscriptionID) throw new Error("Subscription ID not found")
@@ -150,10 +221,6 @@ export async function POST(input: APIEvent) {
if (!invoiceID) throw new Error("Invoice ID not found")
if (!subscriptionID) throw new Error("Subscription ID not found")
if (body.data.object.billing_reason === "subscription_create" && productID === LiteData.productID()) {
await provisionLiteSubscription(subscriptionID)
}
// get coupon id from subscription
const invoice = await Billing.stripe().invoices.retrieve(invoiceID, {
expand: ["discounts", "payments"],
@@ -311,92 +378,3 @@ export async function POST(input: APIEvent) {
return Response.json({ message: error.message }, { status: 500 })
})
}
async function provisionLiteSubscription(subscriptionID: string) {
const stripe = Billing.stripe()
const subscription = liteSubscriptionProvisioningData(await stripe.subscriptions.retrieve(subscriptionID))
if (!subscription) return
await Actor.provide("system", { workspaceID: subscription.workspaceID }, async () => {
const billing = await Billing.get()
if (!billing) throw new Error(`Workspace with ID ${subscription.workspaceID} not found`)
if (billing.customerID && billing.customerID !== subscription.customerID) throw new Error("Customer ID mismatch")
if (billing.liteSubscriptionID === subscription.subscriptionID) return
const paymentMethod = subscription.paymentMethodID
? await stripe.paymentMethods.retrieve(subscription.paymentMethodID)
: undefined
if (!billing.customerID) {
await stripe.customers.update(subscription.customerID, {
metadata: {
workspaceID: subscription.workspaceID,
},
})
}
const provisioned = await Database.transaction(async (tx) => {
const current = await tx
.select({
customerID: BillingTable.customerID,
liteSubscriptionID: BillingTable.liteSubscriptionID,
})
.from(BillingTable)
.where(eq(BillingTable.workspaceID, subscription.workspaceID))
.for("update")
.then((rows) => rows[0])
if (!current) throw new Error(`Workspace with ID ${subscription.workspaceID} not found`)
if (current.customerID && current.customerID !== subscription.customerID) throw new Error("Customer ID mismatch")
if (current.liteSubscriptionID === subscription.subscriptionID) return false
if (current.liteSubscriptionID) throw new Error("Workspace already has another Go subscription")
await tx
.update(BillingTable)
.set({
customerID: subscription.customerID,
liteSubscriptionID: subscription.subscriptionID,
lite: {},
...(paymentMethod
? {
paymentMethodID: paymentMethod.id,
paymentMethodLast4: paymentMethod.card?.last4 ?? null,
paymentMethodType: paymentMethod.type,
}
: {}),
})
.where(eq(BillingTable.workspaceID, subscription.workspaceID))
await tx
.insert(LiteTable)
.ignore()
.values({
workspaceID: subscription.workspaceID,
id: Identifier.create("lite"),
userID: subscription.userID,
})
if (subscription.userEmail) {
if (subscription.coupon === LiteData.firstMonth50Coupon) {
await Billing.redeemCoupon(subscription.userEmail, "GO1MONTH50")
} else if (subscription.coupon === LiteData.firstMonth100Coupon) {
await Billing.redeemCoupon(subscription.userEmail, "GOFREEMONTH")
} else if (subscription.coupon === LiteData.threeMonths100Coupon) {
await Billing.redeemCoupon(subscription.userEmail, "GO3MONTHS100")
} else if (subscription.coupon === LiteData.sixMonths100Coupon) {
await Billing.redeemCoupon(subscription.userEmail, "GO6MONTHS100")
} else if (subscription.coupon === LiteData.twelveMonths100Coupon) {
await Billing.redeemCoupon(subscription.userEmail, "GO12MONTHS100")
}
}
return true
})
if (!provisioned) return
await Referral.completeFromLiteSubscription({
workspaceID: subscription.workspaceID,
userID: subscription.userID,
}).catch((error) => {
console.error("Referral sync failed", error)
})
})
}
@@ -1,66 +0,0 @@
import { expect, test } from "bun:test"
import {
liteCheckoutSubscriptionToProvision,
liteSubscriptionProvisioningData,
} from "../src/routes/stripe/lite-subscription"
test("waits for an unpaid Checkout Session's asynchronous payment", () => {
const session = {
mode: "subscription" as const,
payment_status: "unpaid" as const,
subscription: "sub_go",
}
expect(liteCheckoutSubscriptionToProvision("checkout.session.completed", session)).toBeUndefined()
expect(liteCheckoutSubscriptionToProvision("checkout.session.async_payment_succeeded", session)).toBe("sub_go")
})
test("provisions a paid Checkout Session immediately", () => {
expect(
liteCheckoutSubscriptionToProvision("checkout.session.completed", {
mode: "subscription",
payment_status: "paid",
subscription: "sub_go",
}),
).toBe("sub_go")
})
test("provisions an active Go subscription without a default payment method", () => {
expect(
liteSubscriptionProvisioningData({
id: "sub_go",
status: "active",
customer: "cus_go",
default_payment_method: null,
metadata: {
type: "lite",
workspaceID: "wrk_go",
userID: "usr_go",
},
}),
).toEqual({
subscriptionID: "sub_go",
customerID: "cus_go",
workspaceID: "wrk_go",
userID: "usr_go",
userEmail: undefined,
coupon: undefined,
paymentMethodID: undefined,
})
})
test("waits for an asynchronous Go subscription to become active", () => {
expect(
liteSubscriptionProvisioningData({
id: "sub_go",
status: "incomplete",
customer: "cus_go",
default_payment_method: null,
metadata: {
type: "lite",
workspaceID: "wrk_go",
userID: "usr_go",
},
}),
).toBeUndefined()
})
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.18.9",
"version": "1.18.10",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.18.9",
"version": "1.18.10",
"$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.9",
"version": "1.18.10",
"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.9",
"version": "1.18.10",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.18.9",
"version": "1.18.10",
"name": "@opencode-ai/core",
"type": "module",
"license": "MIT",
+10 -4
View File
@@ -15,6 +15,11 @@ import { httpClient } from "./effect/app-node-platform"
export const CatalogModelStatus = Schema.Literals(["alpha", "beta", "deprecated"])
export type CatalogModelStatus = typeof CatalogModelStatus.Type
const InterleavedField = Schema.Union([
Schema.Literals(["reasoning", "reasoning_content", "reasoning_text"]),
Schema.String,
])
const USER_AGENT = `opencode/${InstallationChannel}/${InstallationVersion}/${Flag.OPENCODE_CLIENT}`
const CostTier = Schema.Struct({
@@ -71,9 +76,10 @@ export const Model = Schema.Struct({
reasoning_options: Schema.optional(Schema.Array(ReasoningOption)),
interleaved: Schema.optional(
Schema.Union([
Schema.Literal(true),
Schema.Boolean,
InterleavedField,
Schema.Struct({
field: Schema.Literals(["reasoning", "reasoning_content", "reasoning_details"]),
field: InterleavedField,
}),
]),
),
@@ -151,10 +157,10 @@ const layer = Layer.effect(
),
)
const source = Flag.OPENCODE_MODELS_URL || "https://models.dev"
const source = Flag.OPENCODE_MODELS_URL || "https://models.opencode.ai"
const filepath = path.join(
Global.Path.cache,
source === "https://models.dev" ? "models.json" : `models-${Hash.fast(source)}.json`,
source === "https://models.opencode.ai" ? "models.json" : `models-${Hash.fast(source)}.json`,
)
const ttl = Duration.minutes(5)
const lockKey = `models-dev:${filepath}`
+8 -2
View File
@@ -5,6 +5,11 @@ import { PositiveInt } from "../../schema"
export const ModelStatus = Schema.Literals(["alpha", "beta", "deprecated", "active"])
const InterleavedField = Schema.Union([
Schema.Literals(["reasoning", "reasoning_content", "reasoning_text"]),
Schema.String,
])
export const Model = Schema.Struct({
id: Schema.optional(Schema.String),
name: Schema.optional(Schema.String),
@@ -16,9 +21,10 @@ export const Model = Schema.Struct({
tool_call: Schema.optional(Schema.Boolean),
interleaved: Schema.optional(
Schema.Union([
Schema.Literal(true),
Schema.Boolean,
InterleavedField,
Schema.Struct({
field: Schema.Literals(["reasoning", "reasoning_content", "reasoning_details"]),
field: InterleavedField,
}),
]),
),
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "1.18.9",
"version": "1.18.10",
"type": "module",
"license": "MIT",
"homepage": "https://opencode.ai",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.18.9",
"version": "1.18.10",
"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.9",
"version": "1.18.10",
"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.9",
"version": "1.18.10",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.18.9",
"version": "1.18.10",
"$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.9",
"version": "1.18.10",
"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.9",
"version": "1.18.10",
"name": "@opencode-ai/llm",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -2,7 +2,7 @@ import * as fs from "node:fs/promises"
import * as path from "node:path"
const RECORDINGS_DIR = path.resolve(import.meta.dir, "..", "test", "fixtures", "recordings")
const MODELS_DEV_URL = "https://models.dev/api.json"
const MODELS_DEV_URL = "https://models.opencode.ai/api.json"
type JsonRecord = Record<string, unknown>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.18.9",
"version": "1.18.10",
"name": "opencode",
"type": "module",
"license": "MIT",
+8 -3
View File
@@ -976,10 +976,15 @@ const ProviderModalities = Schema.Struct({
pdf: Schema.Boolean,
})
const ProviderInterleavedField = Schema.Union([
Schema.Literals(["reasoning", "reasoning_content", "reasoning_text"]),
Schema.String,
])
const ProviderInterleaved = Schema.Union([
Schema.Boolean,
Schema.Struct({
field: Schema.Literals(["reasoning", "reasoning_content", "reasoning_details"]),
field: ProviderInterleavedField,
}),
])
@@ -1243,7 +1248,7 @@ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model
video: model.modalities?.output?.includes("video") ?? false,
pdf: model.modalities?.output?.includes("pdf") ?? false,
},
interleaved: model.interleaved ?? false,
interleaved: typeof model.interleaved === "string" ? { field: model.interleaved } : (model.interleaved ?? false),
},
release_date: model.release_date ?? "",
variants: {},
@@ -1475,7 +1480,7 @@ const layer = Layer.effect(
pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false,
},
interleaved:
model.interleaved ??
(typeof model.interleaved === "string" ? { field: model.interleaved } : model.interleaved) ??
existingModel?.capabilities.interleaved ??
(!existingModel && apiNpm === "@ai-sdk/openai-compatible" && apiID.includes("deepseek")
? { field: "reasoning_content" }
@@ -0,0 +1,38 @@
import { expect, test } from "bun:test"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
test("does not reconnect an SSE stream after a JSON-RPC error response", async () => {
let requests = 0
const transport = new StreamableHTTPClientTransport(new URL("http://mcp.invalid"), {
fetch: async () => {
requests += 1
return new Response(
new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode("id: prime\nretry: 1\ndata:\n\n"))
controller.enqueue(
new TextEncoder().encode(
'id: error\ndata: {"jsonrpc":"2.0","error":{"code":-32601,"message":"Method not found"},"id":1}\n\n',
),
)
controller.close()
},
}),
{ status: 200, headers: { "content-type": "text/event-stream" } },
)
},
reconnectionOptions: {
initialReconnectionDelay: 1,
maxReconnectionDelay: 1,
reconnectionDelayGrowFactor: 1,
maxRetries: 2,
},
})
await transport.start()
await transport.send({ jsonrpc: "2.0", method: "resources/list", id: 1 })
await Bun.sleep(25)
await transport.close()
expect(requests).toBe(1)
})
@@ -252,6 +252,8 @@ it.instance(
const provider = providers[ProviderV2.ID.make("custom-provider")]
expect(provider.models["deepseek-r1"].capabilities.interleaved).toEqual({ field: "reasoning_content" })
expect(provider.models["deepseek-details"].capabilities.interleaved).toEqual({ field: "reasoning_details" })
expect(provider.models["deepseek-text"].capabilities.interleaved).toEqual({ field: "reasoning_text" })
expect(provider.models["custom-reasoning"].capabilities.interleaved).toEqual({ field: "vendor_reasoning" })
expect(provider.models["custom-model"].capabilities.interleaved).toBe(false)
expect(
providers[ProviderV2.ID.make("custom-anthropic-provider")].models["deepseek-r1"].capabilities.interleaved,
@@ -267,6 +269,8 @@ it.instance(
models: {
"deepseek-r1": { name: "DeepSeek R1" },
"deepseek-details": { name: "DeepSeek Details", interleaved: { field: "reasoning_details" } },
"deepseek-text": { name: "DeepSeek Text", interleaved: "reasoning_text" },
"custom-reasoning": { name: "Custom Reasoning", interleaved: { field: "vendor_reasoning" } },
"custom-model": { name: "Custom Model" },
},
options: { apiKey: "custom-key" },
@@ -1462,6 +1466,7 @@ test("models.dev normalization fills required response fields", () => {
id: "gpt-5.4",
name: "GPT-5.4",
family: "gpt",
interleaved: "reasoning_text",
cost: { input: 2.5, output: 15 },
limit: { context: 1_050_000, input: 922_000, output: 128_000 },
},
@@ -1474,6 +1479,7 @@ test("models.dev normalization fills required response fields", () => {
expect(model.capabilities.reasoning).toBe(false)
expect(model.capabilities.attachment).toBe(false)
expect(model.capabilities.toolcall).toBe(true)
expect(model.capabilities.interleaved).toEqual({ field: "reasoning_text" })
expect(model.release_date).toBe("")
})
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.18.9",
"version": "1.18.10",
"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.9",
"version": "1.18.10",
"type": "module",
"license": "MIT",
"scripts": {
+7 -3
View File
@@ -1768,9 +1768,13 @@ export type ProviderConfig = {
temperature?: boolean
tool_call?: boolean
interleaved?:
| true
| boolean
| "reasoning"
| "reasoning_content"
| "reasoning_text"
| string
| {
field: "reasoning" | "reasoning_content" | "reasoning_details"
field: "reasoning" | "reasoning_content" | "reasoning_text" | string
}
cost?: {
input: number
@@ -2057,7 +2061,7 @@ export type Model = {
interleaved:
| boolean
| {
field: "reasoning" | "reasoning_content" | "reasoning_details"
field: "reasoning" | "reasoning_content" | "reasoning_text" | string
}
}
cost: {
+26 -6
View File
@@ -20820,15 +20820,28 @@
"interleaved": {
"anyOf": [
{
"type": "boolean",
"enum": [true]
"type": "boolean"
},
{
"type": "string",
"enum": ["reasoning", "reasoning_content", "reasoning_text"]
},
{
"type": "string"
},
{
"type": "object",
"properties": {
"field": {
"type": "string",
"enum": ["reasoning", "reasoning_content", "reasoning_details"]
"anyOf": [
{
"type": "string",
"enum": ["reasoning", "reasoning_content", "reasoning_text"]
},
{
"type": "string"
}
]
}
},
"required": ["field"],
@@ -21641,8 +21654,15 @@
"type": "object",
"properties": {
"field": {
"type": "string",
"enum": ["reasoning", "reasoning_content", "reasoning_details"]
"anyOf": [
{
"type": "string",
"enum": ["reasoning", "reasoning_content", "reasoning_text"]
},
{
"type": "string"
}
]
}
},
"required": ["field"],
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/server",
"version": "1.18.9",
"version": "1.18.10",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/session-ui",
"version": "1.18.9",
"version": "1.18.10",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.18.9",
"version": "1.18.10",
"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.9",
"version": "1.18.10",
"private": true,
"type": "module",
"license": "MIT",
@@ -1,8 +1,8 @@
import { query } from "@solidjs/router"
export const modelCatalogSourceUrl = "https://models.dev/catalog.json"
export const modelCatalogPricingUrl = "https://models.dev/api.json"
export const modelCatalogLabSourceUrl = "https://models.dev/labs"
export const modelCatalogSourceUrl = "https://models.opencode.ai/catalog.json"
export const modelCatalogPricingUrl = "https://models.opencode.ai/api.json"
export const modelCatalogLabSourceUrl = "https://models.opencode.ai/labs"
export type ModelCatalogCost = {
input: number
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/stats-core",
"version": "1.18.9",
"version": "1.18.10",
"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.9",
"version": "1.18.10",
"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.9",
"version": "1.18.10",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.18.9",
"version": "1.18.10",
"type": "module",
"license": "MIT",
"repository": {
+1 -1
View File
@@ -45,7 +45,7 @@ function providerIconsPlugin() {
}
async function fetchProviderIcons() {
const url = process.env.OPENCODE_MODELS_URL || "https://models.dev"
const url = process.env.OPENCODE_MODELS_URL || "https://models.opencode.ai"
const providers = await fetch(`${url}/api.json`)
.then((res) => res.json())
.then((json) => Object.keys(json))
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "@opencode-ai/web",
"type": "module",
"license": "MIT",
"version": "1.18.9",
"version": "1.18.10",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
@@ -1505,6 +1505,30 @@ To use Kimi K2 from Moonshot AI:
---
### Modal
1. Create a [shared Endpoint](https://modal.com/endpoints) for the model you want to use.
2. Create a [proxy token](https://modal.com/docs/guide/endpoints#proxy-tokens), then join its ID and secret with a period:
```txt
wk-<id>.ws-<secret>
```
3. Run the `/connect` command, search for **Modal**, and enter the combined proxy token.
```txt
/connect
```
4. Run the `/models` command to select one of the endpoints in your Modal workspace.
```txt
/models
```
---
### NVIDIA
NVIDIA provides access to Nemotron models and many other open models through [build.nvidia.com](https://build.nvidia.com) for free.
@@ -141,6 +141,15 @@ diff --git a/dist/cjs/client/streamableHttp.js b/dist/cjs/client/streamableHttp.
index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eedff5443c9 100644
--- a/dist/cjs/client/streamableHttp.js
+++ b/dist/cjs/client/streamableHttp.js
@@ -204,7 +204,7 @@ class StreamableHTTPClientTransport {
if (!event.event || event.event === 'message') {
try {
const message = types_js_1.JSONRPCMessageSchema.parse(JSON.parse(event.data));
- if ((0, types_js_1.isJSONRPCResultResponse)(message)) {
+ if ((0, types_js_1.isJSONRPCResultResponse)(message) || (0, types_js_1.isJSONRPCErrorResponse)(message)) {
// Mark that we received a response - no need to reconnect for this request
receivedResponse = true;
if (replayMessageId !== undefined) {
@@ -290,7 +290,38 @@ class StreamableHTTPClientTransport {
this.onclose?.();
}
@@ -518,10 +527,19 @@ index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da88
@@ -1,5 +1,5 @@
import { createFetchWithInit, normalizeHeaders } from '../shared/transport.js';
-import { isInitializedNotification, isJSONRPCRequest, isJSONRPCResultResponse, JSONRPCMessageSchema } from '../types.js';
+import { isInitializedNotification, isInitializeRequest, isJSONRPCRequest, isJSONRPCResultResponse, JSONRPCMessageSchema } from '../types.js';
+import { isInitializedNotification, isInitializeRequest, isJSONRPCErrorResponse, isJSONRPCRequest, isJSONRPCResultResponse, JSONRPCMessageSchema } from '../types.js';
import { auth, extractWWWAuthenticateParams, UnauthorizedError } from './auth.js';
import { EventSourceParserStream } from 'eventsource-parser/stream';
// Default reconnection options for StreamableHTTP connections
@@ -200,7 +200,7 @@ export class StreamableHTTPClientTransport {
if (!event.event || event.event === 'message') {
try {
const message = JSONRPCMessageSchema.parse(JSON.parse(event.data));
- if (isJSONRPCResultResponse(message)) {
+ if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) {
// Mark that we received a response - no need to reconnect for this request
receivedResponse = true;
if (replayMessageId !== undefined) {
@@ -286,7 +286,38 @@ export class StreamableHTTPClientTransport {
this.onclose?.();
}
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "1.18.9",
"version": "1.18.10",
"publisher": "sst-dev",
"repository": {
"type": "git",