This commit is contained in:
Frank
2026-08-18 22:55:47 -04:00
parent 8b65fa2ef6
commit 64b4f7df4c
11 changed files with 3308 additions and 3 deletions
+3
View File
@@ -390,6 +390,8 @@ export const dict = {
"zen.api.error.modelDisabled": "Model is disabled",
"zen.api.error.regionNotAllowed":
"The latest version of this model is only available hosted in China and requires explicit opt in: {{consoleGoUrl}}",
"zen.api.error.nonZdrNotAllowed":
"This model collects data used to improve its quality and requires explicit opt in: {{consoleGoUrl}}",
"zen.api.error.trialEnded":
"Free promotion has ended for {{model}}. You can continue using the model by subscribing to OpenCode Go - {{link}}",
@@ -671,6 +673,7 @@ export const dict = {
'Select "OpenCode Go" as the provider in your opencode configuration to use Go models.',
"workspace.lite.providers.title": "Providers",
"workspace.lite.providers.description": "Control which providers are used for routing.",
"workspace.lite.providers.allowNonZdr": "Enable models without zero data retention",
"workspace.lite.providers.useChina": "Enable models hosted in China",
"workspace.lite.black.message":
"You're currently subscribed to OpenCode Black or on the waitlist. Please unsubscribe first if you'd like to switch to Go.",
@@ -39,6 +39,7 @@ export const queryLiteSubscription = query(async (workspaceID: string) => {
timeCreated: LiteTable.timeCreated,
lite: BillingTable.lite,
region: WorkspaceTable.region,
allowNonZdr: WorkspaceTable.allow_non_zdr,
})
.from(BillingTable)
.innerJoin(LiteTable, eq(LiteTable.workspaceID, BillingTable.workspaceID))
@@ -54,6 +55,7 @@ export const queryLiteSubscription = query(async (workspaceID: string) => {
return {
mine,
useBalance: row.lite?.useBalance ?? false,
allowNonZdr: row.allowNonZdr ?? false,
region:
row.region ?? (await Workspace.setDefaultRegion({ country: countryFromRequest(getRequestEvent()?.request) })),
rollingUsage: Subscription.analyzeRollingUsage({
@@ -154,6 +156,24 @@ const setGoProviderRouting = action(async (form: FormData) => {
)
}, "go.providerRouting.set")
const setGoAllowNonZdr = action(async (form: FormData) => {
"use server"
const workspaceID = form.get("workspaceID") as string | null
if (!workspaceID) return { error: formError.workspaceRequired }
const allowNonZdr = (form.get("allowNonZdr") as string | null) === "true"
return json(
await withActor(
() =>
Workspace.update({ allow_non_zdr: allowNonZdr })
.then(() => ({ error: undefined }))
.catch((e) => ({ error: e.message as string })),
workspaceID,
),
{ revalidate: queryLiteSubscription.key },
)
}, "go.allowNonZdr.set")
function LiteUsageItem(props: { label: string; usage: { usagePercent: number; resetInSec: number } }) {
const i18n = useI18n()
@@ -186,6 +206,7 @@ export function LiteSection(props: { lite: LiteSubscription | undefined }) {
const checkoutSubmission = useSubmission(createLiteCheckoutUrl)
const useBalanceSubmission = useSubmission(setLiteUseBalance)
const providerRoutingSubmission = useSubmission(setGoProviderRouting)
const allowNonZdrSubmission = useSubmission(setGoAllowNonZdr)
const [store, setStore] = createStore({
loading: undefined as undefined | "session" | "checkout" | "alipay" | "upi",
showModal: false,
@@ -264,6 +285,20 @@ export function LiteSection(props: { lite: LiteSubscription | undefined }) {
<h3>{i18n.t("workspace.lite.providers.title")}</h3>
<p>{i18n.t("workspace.lite.providers.description")}</p>
</div>
<form action={setGoAllowNonZdr} method="post" data-slot="setting-row">
<p>{i18n.t("workspace.lite.providers.allowNonZdr")}</p>
<input type="hidden" name="workspaceID" value={params.id} />
<input type="hidden" name="allowNonZdr" value={sub().allowNonZdr ? "false" : "true"} />
<label data-slot="toggle-label">
<input
type="checkbox"
checked={sub().allowNonZdr}
disabled={allowNonZdrSubmission.pending}
onChange={(e) => e.currentTarget.form?.requestSubmit()}
/>
<span></span>
</label>
</form>
<form action={setGoProviderRouting} method="post" data-slot="setting-row">
<p>{i18n.t("workspace.lite.providers.useChina")}</p>
<input type="hidden" name="workspaceID" value={params.id} />
@@ -4,6 +4,7 @@ export class MonthlyLimitError extends Error {}
export class UserLimitError extends Error {}
export class ModelError extends Error {}
export class RegionError extends Error {}
export class DataPolicyError extends Error {}
class LimitError extends Error {
retryAfter?: number
@@ -22,6 +22,7 @@ import {
UserLimitError,
ModelError,
RegionError,
DataPolicyError,
RateLimitError,
FreeUsageLimitError,
GoUsageLimitError,
@@ -128,6 +129,12 @@ export async function handler(
: createKeyRateLimiter(modelInfo.id, modelInfo.rateLimit, zenApiKey, input.request)
await rateLimiter?.check()
const authInfo = await authenticate(modelInfo, zenApiKey)
if (authInfo && opts.modelList === "lite" && modelInfo.id === "muse-spark-1.2" && !authInfo.allowNonZdr)
throw new DataPolicyError(
t("zen.api.error.nonZdrNotAllowed", {
consoleGoUrl: `https://opencode.ai/workspace/${authInfo.workspaceID}/go`,
}),
)
const allowedRegions = authInfo?.region
? authInfo.region
: await (async () => {
@@ -477,7 +484,7 @@ export async function handler(
} catch {}
}
if (error instanceof RegionError)
if (error instanceof RegionError || error instanceof DataPolicyError)
return new Response(
JSON.stringify({
type: "error",
@@ -708,6 +715,7 @@ export async function handler(
workspace: {
id: WorkspaceTable.id,
region: WorkspaceTable.region,
allowNonZdr: WorkspaceTable.allow_non_zdr,
isBlocked: WorkspaceTable.is_blocked,
isFlaggedByAnthropic: WorkspaceTable.is_flagged_by_anthropic,
isFlaggedByOpenAI: WorkspaceTable.is_flagged_by_openai,
@@ -820,6 +828,7 @@ export async function handler(
apiKeyId: data.apiKey,
workspaceID: data.workspace.id,
region: data.workspace.region,
allowNonZdr: data.workspace.allowNonZdr ?? false,
billing: data.billing,
user: data.user,
black: data.black,
@@ -0,0 +1 @@
ALTER TABLE `workspace` ADD `allow_non_zdr` boolean;
File diff suppressed because it is too large Load Diff
@@ -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,5 @@ 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)],
)
@@ -8,6 +8,7 @@ export const WorkspaceTable = mysqlTable(
slug: varchar("slug", { length: 255 }),
name: varchar("name", { length: 255 }).notNull(),
region: json("region").$type<("us" | "eu" | "sg" | "cn")[]>(),
allow_non_zdr: boolean(),
is_blocked: boolean(),
is_flagged_by_anthropic: boolean(),
is_flagged_by_openai: boolean(),
+2
View File
@@ -62,6 +62,7 @@ export namespace Workspace {
z.object({
name: z.string().min(1).max(255).optional(),
region: z.array(Region).min(1).optional(),
allow_non_zdr: z.boolean().optional(),
}),
async (input) => {
Actor.assertAdmin()
@@ -72,6 +73,7 @@ export namespace Workspace {
.set({
...("name" in input ? { name: input.name } : {}),
...("region" in input ? { region: input.region } : {}),
...("allow_non_zdr" in input ? { allow_non_zdr: input.allow_non_zdr } : {}),
})
.where(eq(WorkspaceTable.id, workspaceID)),
)
+7
View File
@@ -71,6 +71,7 @@ The current list of models includes:
- **MiMo-V2.5-Pro**
- **MiniMax M3**
- **MiniMax M2.7**
- **Muse Spark 1.2**
- **Qwen3.8 Max**
- **Qwen3.7 Max**
- **Qwen3.7 Plus**
@@ -109,6 +110,7 @@ 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 |
| Muse Spark 1.2 | 45,300 | 113,300 | 226,600 |
| Qwen3.8 Max | 160 | 400 | 810 |
| Qwen3.7 Max | 340 | 840 | 1,690 |
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
@@ -128,6 +130,7 @@ The estimates are based on observed request patterns:
- DeepSeek V4 Flash — 410 input, 71,300 cached, 310 output tokens per request
- MiniMax M3 — 510 input, 56,000 cached, 190 output tokens per request
- MiniMax M2.7 — 300 input, 55,000 cached, 125 output tokens per request
- Muse Spark 1.2 — 620 input, 71,400 cached, 300 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
@@ -154,6 +157,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 |
| Muse Spark 1.2 | $0.10 | $0.20 | $0.002 | - | $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 |
@@ -219,6 +223,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` |
| Muse Spark 1.2 | muse-spark-1.2 | `https://opencode.ai/zen/go/v1/responses` | `@ai-sdk/openai` |
| Qwen3.8 Max | qwen3.8-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
| Qwen3.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` |
@@ -261,12 +266,14 @@ https://opencode.ai/zen/go/v1/models
| Qwen3.6 Plus | Not used | 0 days |
| MiniMax M3 | Not used | 0 days |
| MiniMax M2.7 | Not used | 0 days |
| Muse Spark 1.2 | May be used | Not ZDR |
| DeepSeek V4 Pro | Not used | 0 days\* |
| DeepSeek V4 Flash | Not used | 0 days\* |
| Hy3 | Not used | 0 days |
- **Grok 4.5:** ZDR disables important API features that depend on stored data, including the stateful Responses API, Files and Collections, and the Batch API. [Learn more](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr).
- **GPT 5.6 Luna:** Abuse monitoring logs are generated for all API feature usage and retained for up to 30 days. [Learn more](https://developers.openai.com/api/docs/guides/your-data#data-retention-controls-for-abuse-monitoring).
- **Muse Spark 1.2:** Go uses Meta's Contributor tier, which is not ZDR and allows prompts and completions to be used to train future Meta models. [Learn more](https://dev.meta.ai/docs/pricing-rate-limits/#contributor-tier).
- **DeepSeek:** ZDR agreement is renewed monthly. The current agreement is valid through August 31, 2026.
---