This commit is contained in:
Frank
2026-08-18 22:55:47 -04:00
parent 844288b99f
commit 275a28f6ae
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.modelDisabled": "Model is disabled",
"zen.api.error.regionNotAllowed": "zen.api.error.regionNotAllowed":
"The latest version of this model is only available hosted in China and requires explicit opt in: {{consoleGoUrl}}", "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": "zen.api.error.trialEnded":
"Free promotion has ended for {{model}}. You can continue using the model by subscribing to OpenCode Go - {{link}}", "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.', 'Select "OpenCode Go" as the provider in your opencode configuration to use Go models.',
"workspace.lite.providers.title": "Providers", "workspace.lite.providers.title": "Providers",
"workspace.lite.providers.description": "Control which providers are used for routing.", "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.providers.useChina": "Enable models hosted in China",
"workspace.lite.black.message": "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.", "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, timeCreated: LiteTable.timeCreated,
lite: BillingTable.lite, lite: BillingTable.lite,
region: WorkspaceTable.region, region: WorkspaceTable.region,
allowNonZdr: WorkspaceTable.allow_non_zdr,
}) })
.from(BillingTable) .from(BillingTable)
.innerJoin(LiteTable, eq(LiteTable.workspaceID, BillingTable.workspaceID)) .innerJoin(LiteTable, eq(LiteTable.workspaceID, BillingTable.workspaceID))
@@ -54,6 +55,7 @@ export const queryLiteSubscription = query(async (workspaceID: string) => {
return { return {
mine, mine,
useBalance: row.lite?.useBalance ?? false, useBalance: row.lite?.useBalance ?? false,
allowNonZdr: row.allowNonZdr ?? false,
region: region:
row.region ?? (await Workspace.setDefaultRegion({ country: countryFromRequest(getRequestEvent()?.request) })), row.region ?? (await Workspace.setDefaultRegion({ country: countryFromRequest(getRequestEvent()?.request) })),
rollingUsage: Subscription.analyzeRollingUsage({ rollingUsage: Subscription.analyzeRollingUsage({
@@ -154,6 +156,24 @@ const setGoProviderRouting = action(async (form: FormData) => {
) )
}, "go.providerRouting.set") }, "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 } }) { function LiteUsageItem(props: { label: string; usage: { usagePercent: number; resetInSec: number } }) {
const i18n = useI18n() const i18n = useI18n()
@@ -186,6 +206,7 @@ export function LiteSection(props: { lite: LiteSubscription | undefined }) {
const checkoutSubmission = useSubmission(createLiteCheckoutUrl) const checkoutSubmission = useSubmission(createLiteCheckoutUrl)
const useBalanceSubmission = useSubmission(setLiteUseBalance) const useBalanceSubmission = useSubmission(setLiteUseBalance)
const providerRoutingSubmission = useSubmission(setGoProviderRouting) const providerRoutingSubmission = useSubmission(setGoProviderRouting)
const allowNonZdrSubmission = useSubmission(setGoAllowNonZdr)
const [store, setStore] = createStore({ const [store, setStore] = createStore({
loading: undefined as undefined | "session" | "checkout" | "alipay" | "upi", loading: undefined as undefined | "session" | "checkout" | "alipay" | "upi",
showModal: false, showModal: false,
@@ -264,6 +285,20 @@ export function LiteSection(props: { lite: LiteSubscription | undefined }) {
<h3>{i18n.t("workspace.lite.providers.title")}</h3> <h3>{i18n.t("workspace.lite.providers.title")}</h3>
<p>{i18n.t("workspace.lite.providers.description")}</p> <p>{i18n.t("workspace.lite.providers.description")}</p>
</div> </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"> <form action={setGoProviderRouting} method="post" data-slot="setting-row">
<p>{i18n.t("workspace.lite.providers.useChina")}</p> <p>{i18n.t("workspace.lite.providers.useChina")}</p>
<input type="hidden" name="workspaceID" value={params.id} /> <input type="hidden" name="workspaceID" value={params.id} />
@@ -4,6 +4,7 @@ export class MonthlyLimitError extends Error {}
export class UserLimitError extends Error {} export class UserLimitError extends Error {}
export class ModelError extends Error {} export class ModelError extends Error {}
export class RegionError extends Error {} export class RegionError extends Error {}
export class DataPolicyError extends Error {}
class LimitError extends Error { class LimitError extends Error {
retryAfter?: number retryAfter?: number
@@ -22,6 +22,7 @@ import {
UserLimitError, UserLimitError,
ModelError, ModelError,
RegionError, RegionError,
DataPolicyError,
RateLimitError, RateLimitError,
FreeUsageLimitError, FreeUsageLimitError,
GoUsageLimitError, GoUsageLimitError,
@@ -128,6 +129,12 @@ export async function handler(
: createKeyRateLimiter(modelInfo.id, modelInfo.rateLimit, zenApiKey, input.request) : createKeyRateLimiter(modelInfo.id, modelInfo.rateLimit, zenApiKey, input.request)
await rateLimiter?.check() await rateLimiter?.check()
const authInfo = await authenticate(modelInfo, zenApiKey) 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 const allowedRegions = authInfo?.region
? authInfo.region ? authInfo.region
: await (async () => { : await (async () => {
@@ -477,7 +484,7 @@ export async function handler(
} catch {} } catch {}
} }
if (error instanceof RegionError) if (error instanceof RegionError || error instanceof DataPolicyError)
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
type: "error", type: "error",
@@ -708,6 +715,7 @@ export async function handler(
workspace: { workspace: {
id: WorkspaceTable.id, id: WorkspaceTable.id,
region: WorkspaceTable.region, region: WorkspaceTable.region,
allowNonZdr: WorkspaceTable.allow_non_zdr,
isBlocked: WorkspaceTable.is_blocked, isBlocked: WorkspaceTable.is_blocked,
isFlaggedByAnthropic: WorkspaceTable.is_flagged_by_anthropic, isFlaggedByAnthropic: WorkspaceTable.is_flagged_by_anthropic,
isFlaggedByOpenAI: WorkspaceTable.is_flagged_by_openai, isFlaggedByOpenAI: WorkspaceTable.is_flagged_by_openai,
@@ -820,6 +828,7 @@ export async function handler(
apiKeyId: data.apiKey, apiKeyId: data.apiKey,
workspaceID: data.workspace.id, workspaceID: data.workspace.id,
region: data.workspace.region, region: data.workspace.region,
allowNonZdr: data.workspace.allowNonZdr ?? false,
billing: data.billing, billing: data.billing,
user: data.user, user: data.user,
black: data.black, 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), ...workspaceIndexes(table),
uniqueIndex("global_customer_id").on(table.customerID), uniqueIndex("global_customer_id").on(table.customerID),
uniqueIndex("global_subscription_id").on(table.subscriptionID), 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 { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types"
import { workspaceIndexes } from "./workspace.sql" import { workspaceIndexes } from "./workspace.sql"
@@ -31,5 +31,5 @@ export const ReferralRewardTable = mysqlTable(
amount: bigint("amount", { mode: "number" }).notNull(), amount: bigint("amount", { mode: "number" }).notNull(),
timeApplied: utc("time_applied"), 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 }), slug: varchar("slug", { length: 255 }),
name: varchar("name", { length: 255 }).notNull(), name: varchar("name", { length: 255 }).notNull(),
region: json("region").$type<("us" | "eu" | "sg" | "cn")[]>(), region: json("region").$type<("us" | "eu" | "sg" | "cn")[]>(),
allow_non_zdr: boolean(),
is_blocked: boolean(), is_blocked: boolean(),
is_flagged_by_anthropic: boolean(), is_flagged_by_anthropic: boolean(),
is_flagged_by_openai: boolean(), is_flagged_by_openai: boolean(),
+2
View File
@@ -62,6 +62,7 @@ export namespace Workspace {
z.object({ z.object({
name: z.string().min(1).max(255).optional(), name: z.string().min(1).max(255).optional(),
region: z.array(Region).min(1).optional(), region: z.array(Region).min(1).optional(),
allow_non_zdr: z.boolean().optional(),
}), }),
async (input) => { async (input) => {
Actor.assertAdmin() Actor.assertAdmin()
@@ -72,6 +73,7 @@ export namespace Workspace {
.set({ .set({
...("name" in input ? { name: input.name } : {}), ...("name" in input ? { name: input.name } : {}),
...("region" in input ? { region: input.region } : {}), ...("region" in input ? { region: input.region } : {}),
...("allow_non_zdr" in input ? { allow_non_zdr: input.allow_non_zdr } : {}),
}) })
.where(eq(WorkspaceTable.id, workspaceID)), .where(eq(WorkspaceTable.id, workspaceID)),
) )
+7
View File
@@ -71,6 +71,7 @@ The current list of models includes:
- **MiMo-V2.5-Pro** - **MiMo-V2.5-Pro**
- **MiniMax M3** - **MiniMax M3**
- **MiniMax M2.7** - **MiniMax M2.7**
- **Muse Spark 1.2**
- **Qwen3.8 Max** - **Qwen3.8 Max**
- **Qwen3.7 Max** - **Qwen3.7 Max**
- **Qwen3.7 Plus** - **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 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 |
| MiniMax M3 | 3,200 | 8,000 | 16,000 | | MiniMax M3 | 3,200 | 8,000 | 16,000 |
| MiniMax M2.7 | 3,400 | 8,500 | 17,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.8 Max | 160 | 400 | 810 |
| Qwen3.7 Max | 340 | 840 | 1,690 | | Qwen3.7 Max | 340 | 840 | 1,690 |
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 | | 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 - 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 M3 — 510 input, 56,000 cached, 190 output tokens per request
- MiniMax M2.7 — 300 input, 55,000 cached, 125 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 — 830 input, 71,500 cached, 295 output tokens per request
- MiMo-V2.5-Pro — 790 input, 86,000 cached, 305 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.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 M3 | $0.30 | $1.20 | $0.06 | - | $60 |
| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | $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 | | 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.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 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) | $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 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.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` | | 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.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 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.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 | | Qwen3.6 Plus | Not used | 0 days |
| MiniMax M3 | Not used | 0 days | | MiniMax M3 | Not used | 0 days |
| MiniMax M2.7 | 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 Pro | Not used | 0 days\* |
| DeepSeek V4 Flash | Not used | 0 days\* | | DeepSeek V4 Flash | Not used | 0 days\* |
| Hy3 | 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). - **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). - **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. - **DeepSeek:** ZDR agreement is renewed monthly. The current agreement is valid through August 31, 2026.
--- ---