Compare commits

..

1 Commits

Author SHA1 Message Date
Jack 6cc915bcf7 fix(console): recover delayed Go subscriptions 2026-07-30 09:32:06 +00:00
50 changed files with 300 additions and 335 deletions
+28 -28
View File
@@ -29,7 +29,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.18.10",
"version": "1.18.9",
"dependencies": {
"@corvu/drawer": "catalog:",
"@dnd-kit/abstract": "0.5.0",
@@ -96,7 +96,7 @@
},
"packages/cli": {
"name": "@opencode-ai/cli",
"version": "1.18.10",
"version": "1.18.9",
"bin": {
"lildax": "./bin/lildax.cjs",
},
@@ -144,7 +144,7 @@
},
"packages/codemode": {
"name": "@opencode-ai/codemode",
"version": "1.18.10",
"version": "1.18.9",
"dependencies": {
"acorn": "8.15.0",
"effect": "catalog:",
@@ -158,7 +158,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.18.10",
"version": "1.18.9",
"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.10",
"version": "1.18.9",
"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.10",
"version": "1.18.9",
"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.10",
"version": "1.18.9",
"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.10",
"version": "1.18.9",
"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.10",
"version": "1.18.9",
"bin": {
"opencode": "./bin/opencode",
},
@@ -381,7 +381,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.18.10",
"version": "1.18.9",
"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.10",
"version": "1.18.9",
"dependencies": {
"drizzle-orm": "catalog:",
"effect": "catalog:",
@@ -449,7 +449,7 @@
},
"packages/effect-sqlite-node": {
"name": "@opencode-ai/effect-sqlite-node",
"version": "1.18.10",
"version": "1.18.9",
"dependencies": {
"effect": "catalog:",
},
@@ -461,7 +461,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.18.10",
"version": "1.18.9",
"dependencies": {
"@hono/standard-validator": "catalog:",
"@opencode-ai/core": "workspace:*",
@@ -493,7 +493,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.18.10",
"version": "1.18.9",
"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.10",
"version": "1.18.9",
"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.10",
"version": "1.18.9",
"dependencies": {
"@opencode-ai/schema": "workspace:*",
"@smithy/eventstream-codec": "4.2.14",
@@ -559,7 +559,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.18.10",
"version": "1.18.9",
"bin": {
"opencode": "./bin/opencode",
},
@@ -690,7 +690,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.18.10",
"version": "1.18.9",
"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.10",
"version": "1.18.9",
"dependencies": {
"cross-spawn": "catalog:",
},
@@ -781,7 +781,7 @@
},
"packages/server": {
"name": "@opencode-ai/server",
"version": "1.18.10",
"version": "1.18.9",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/protocol": "workspace:*",
@@ -796,7 +796,7 @@
},
"packages/session-ui": {
"name": "@opencode-ai/session-ui",
"version": "1.18.10",
"version": "1.18.9",
"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.10",
"version": "1.18.9",
"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.10",
"version": "1.18.9",
"dependencies": {
"@ibm/plex": "6.4.1",
"@kobalte/core": "catalog:",
@@ -888,7 +888,7 @@
},
"packages/stats/core": {
"name": "@opencode-ai/stats-core",
"version": "1.18.10",
"version": "1.18.9",
"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.10",
"version": "1.18.9",
"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.10",
"version": "1.18.9",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
@@ -976,7 +976,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.18.10",
"version": "1.18.9",
"dependencies": {
"@kobalte/core": "catalog:",
"@pierre/diffs": "catalog:",
@@ -1028,7 +1028,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.18.10",
"version": "1.18.9",
"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-AD5kt1o035hdvYPyrvjiz2mcsXFJ7UJVEmbE9AJFDFs=",
"aarch64-linux": "sha256-f1gVXUpIRWC/I8dxIigdGGvPsju04MXVjZhtq5EYL64=",
"aarch64-darwin": "sha256-c+JRpjs2gsgQ2czyM6bTbYEdMbX/erOSQdX6TdrI59k=",
"x86_64-darwin": "sha256-iB7l4PtQQ9Mx/XAdtyayiLe6pZKhuXB9lpmrbTr+uWc="
"x86_64-linux": "sha256-fK6zAHJC3ut/KUdfqLPxVMH7Z1yv65YZ7qcHF45kPas=",
"aarch64-linux": "sha256-CSWZY/vVUCKzlwfisQQHqHjawUwL0BRc8Y9o2y/QZYA=",
"aarch64-darwin": "sha256-IyFm5NbnU63BaOO/F4/v1exz3VbvkY96yjg9iun+O9Q=",
"x86_64-darwin": "sha256-XnXFoUxqXmEx7AKx5YhaJuLY2NYMtI0ICafaenU5L6k="
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.18.10",
"version": "1.18.9",
"description": "",
"type": "module",
"exports": {
-23
View File
@@ -1,23 +0,0 @@
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,7 +3,6 @@ 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
@@ -12,23 +11,6 @@ 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) => {
+7 -2
View File
@@ -12,7 +12,6 @@ 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"
@@ -60,7 +59,13 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
const [store, setStore, _, ready] = persisted(
{
...Persist.window("tabs"),
migrate: (value: unknown) => migrateTabs(value, fallback),
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 }
})
},
},
createStore<Tab[]>([]),
)
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/cli",
"version": "1.18.10",
"version": "1.18.9",
"type": "module",
"license": "MIT",
"bin": {
+1 -1
View File
@@ -1,4 +1,4 @@
const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.opencode.ai"
const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.dev"
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.10",
"version": "1.18.9",
"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.10",
"version": "1.18.9",
"type": "module",
"license": "MIT",
"scripts": {
@@ -0,0 +1,40 @@
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,
}
}
+101 -79
View File
@@ -10,6 +10,10 @@ 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(
@@ -20,6 +24,10 @@ 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 ?? {}
@@ -105,85 +113,6 @@ 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")
@@ -221,6 +150,10 @@ 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"],
@@ -378,3 +311,92 @@ 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)
})
})
}
@@ -0,0 +1,66 @@
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.10",
"version": "1.18.9",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.18.10",
"version": "1.18.9",
"$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.10",
"version": "1.18.9",
"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.10",
"version": "1.18.9",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.18.10",
"version": "1.18.9",
"name": "@opencode-ai/core",
"type": "module",
"license": "MIT",
+4 -10
View File
@@ -15,11 +15,6 @@ 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({
@@ -76,10 +71,9 @@ export const Model = Schema.Struct({
reasoning_options: Schema.optional(Schema.Array(ReasoningOption)),
interleaved: Schema.optional(
Schema.Union([
Schema.Boolean,
InterleavedField,
Schema.Literal(true),
Schema.Struct({
field: InterleavedField,
field: Schema.Literals(["reasoning", "reasoning_content", "reasoning_details"]),
}),
]),
),
@@ -157,10 +151,10 @@ const layer = Layer.effect(
),
)
const source = Flag.OPENCODE_MODELS_URL || "https://models.opencode.ai"
const source = Flag.OPENCODE_MODELS_URL || "https://models.dev"
const filepath = path.join(
Global.Path.cache,
source === "https://models.opencode.ai" ? "models.json" : `models-${Hash.fast(source)}.json`,
source === "https://models.dev" ? "models.json" : `models-${Hash.fast(source)}.json`,
)
const ttl = Duration.minutes(5)
const lockKey = `models-dev:${filepath}`
+2 -8
View File
@@ -5,11 +5,6 @@ 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),
@@ -21,10 +16,9 @@ export const Model = Schema.Struct({
tool_call: Schema.optional(Schema.Boolean),
interleaved: Schema.optional(
Schema.Union([
Schema.Boolean,
InterleavedField,
Schema.Literal(true),
Schema.Struct({
field: InterleavedField,
field: Schema.Literals(["reasoning", "reasoning_content", "reasoning_details"]),
}),
]),
),
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "1.18.10",
"version": "1.18.9",
"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.10",
"version": "1.18.9",
"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.10",
"version": "1.18.9",
"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.10",
"version": "1.18.9",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.18.10",
"version": "1.18.9",
"$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.10",
"version": "1.18.9",
"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.10",
"version": "1.18.9",
"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.opencode.ai/api.json"
const MODELS_DEV_URL = "https://models.dev/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.10",
"version": "1.18.9",
"name": "opencode",
"type": "module",
"license": "MIT",
+3 -8
View File
@@ -976,15 +976,10 @@ 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: ProviderInterleavedField,
field: Schema.Literals(["reasoning", "reasoning_content", "reasoning_details"]),
}),
])
@@ -1248,7 +1243,7 @@ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model
video: model.modalities?.output?.includes("video") ?? false,
pdf: model.modalities?.output?.includes("pdf") ?? false,
},
interleaved: typeof model.interleaved === "string" ? { field: model.interleaved } : (model.interleaved ?? false),
interleaved: model.interleaved ?? false,
},
release_date: model.release_date ?? "",
variants: {},
@@ -1480,7 +1475,7 @@ const layer = Layer.effect(
pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false,
},
interleaved:
(typeof model.interleaved === "string" ? { field: model.interleaved } : model.interleaved) ??
model.interleaved ??
existingModel?.capabilities.interleaved ??
(!existingModel && apiNpm === "@ai-sdk/openai-compatible" && apiID.includes("deepseek")
? { field: "reasoning_content" }
@@ -1,38 +0,0 @@
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,8 +252,6 @@ 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,
@@ -269,8 +267,6 @@ 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" },
@@ -1466,7 +1462,6 @@ 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 },
},
@@ -1479,7 +1474,6 @@ 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.10",
"version": "1.18.9",
"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.10",
"version": "1.18.9",
"type": "module",
"license": "MIT",
"scripts": {
+3 -7
View File
@@ -1768,13 +1768,9 @@ export type ProviderConfig = {
temperature?: boolean
tool_call?: boolean
interleaved?:
| boolean
| "reasoning"
| "reasoning_content"
| "reasoning_text"
| string
| true
| {
field: "reasoning" | "reasoning_content" | "reasoning_text" | string
field: "reasoning" | "reasoning_content" | "reasoning_details"
}
cost?: {
input: number
@@ -2061,7 +2057,7 @@ export type Model = {
interleaved:
| boolean
| {
field: "reasoning" | "reasoning_content" | "reasoning_text" | string
field: "reasoning" | "reasoning_content" | "reasoning_details"
}
}
cost: {
+6 -26
View File
@@ -20820,28 +20820,15 @@
"interleaved": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "string",
"enum": ["reasoning", "reasoning_content", "reasoning_text"]
},
{
"type": "string"
"type": "boolean",
"enum": [true]
},
{
"type": "object",
"properties": {
"field": {
"anyOf": [
{
"type": "string",
"enum": ["reasoning", "reasoning_content", "reasoning_text"]
},
{
"type": "string"
}
]
"type": "string",
"enum": ["reasoning", "reasoning_content", "reasoning_details"]
}
},
"required": ["field"],
@@ -21654,15 +21641,8 @@
"type": "object",
"properties": {
"field": {
"anyOf": [
{
"type": "string",
"enum": ["reasoning", "reasoning_content", "reasoning_text"]
},
{
"type": "string"
}
]
"type": "string",
"enum": ["reasoning", "reasoning_content", "reasoning_details"]
}
},
"required": ["field"],
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/server",
"version": "1.18.10",
"version": "1.18.9",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/session-ui",
"version": "1.18.10",
"version": "1.18.9",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.18.10",
"version": "1.18.9",
"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.10",
"version": "1.18.9",
"private": true,
"type": "module",
"license": "MIT",
@@ -1,8 +1,8 @@
import { query } from "@solidjs/router"
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 const modelCatalogSourceUrl = "https://models.dev/catalog.json"
export const modelCatalogPricingUrl = "https://models.dev/api.json"
export const modelCatalogLabSourceUrl = "https://models.dev/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.10",
"version": "1.18.9",
"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.10",
"version": "1.18.9",
"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.10",
"version": "1.18.9",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.18.10",
"version": "1.18.9",
"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.opencode.ai"
const url = process.env.OPENCODE_MODELS_URL || "https://models.dev"
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.10",
"version": "1.18.9",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
@@ -1505,30 +1505,6 @@ 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,15 +141,6 @@ 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?.();
}
@@ -527,19 +518,10 @@ 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, isJSONRPCErrorResponse, isJSONRPCRequest, isJSONRPCResultResponse, JSONRPCMessageSchema } from '../types.js';
+import { isInitializedNotification, isInitializeRequest, 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.10",
"version": "1.18.9",
"publisher": "sst-dev",
"repository": {
"type": "git",