diff --git a/bun.lock b/bun.lock index 399b945c49..4d03c250af 100644 --- a/bun.lock +++ b/bun.lock @@ -56,6 +56,7 @@ "@solidjs/start": "^1.1.0", "solid-js": "catalog:", "vinxi": "^0.5.7", + "zod": "catalog:", }, }, "packages/console/core": { diff --git a/github/sst-env.d.ts b/github/sst-env.d.ts index 6b69016e71..f742a12004 100644 --- a/github/sst-env.d.ts +++ b/github/sst-env.d.ts @@ -6,4 +6,4 @@ /// import "sst" -export {} +export {} \ No newline at end of file diff --git a/infra/console.ts b/infra/console.ts index 6ca842380f..74c478aeea 100644 --- a/infra/console.ts +++ b/infra/console.ts @@ -99,11 +99,7 @@ export const stripeWebhook = new WebhookEndpoint("StripeWebhookEndpoint", { ], }) -const ANTHROPIC_API_KEY = new sst.Secret("ANTHROPIC_API_KEY") -const OPENAI_API_KEY = new sst.Secret("OPENAI_API_KEY") -const XAI_API_KEY = new sst.Secret("XAI_API_KEY") -const BASETEN_API_KEY = new sst.Secret("BASETEN_API_KEY") -const FIREWORKS_API_KEY = new sst.Secret("FIREWORKS_API_KEY") +const ZEN_MODELS = new sst.Secret("ZEN_MODELS") const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY") const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", { properties: { value: auth.url.apply((url) => url!) }, @@ -128,17 +124,7 @@ if ($app.stage === "production" || $app.stage === "frank") { new sst.cloudflare.x.SolidStart("Console", { domain, path: "packages/console/app", - link: [ - database, - AUTH_API_URL, - STRIPE_WEBHOOK_SECRET, - STRIPE_SECRET_KEY, - ANTHROPIC_API_KEY, - OPENAI_API_KEY, - XAI_API_KEY, - BASETEN_API_KEY, - FIREWORKS_API_KEY, - ], + link: [database, AUTH_API_URL, STRIPE_WEBHOOK_SECRET, STRIPE_SECRET_KEY, ZEN_MODELS], environment: { //VITE_DOCS_URL: web.url.apply((url) => url!), //VITE_API_URL: gateway.url.apply((url) => url!), diff --git a/packages/app/sst-env.d.ts b/packages/app/sst-env.d.ts index 0397645b50..b6a7e9066e 100644 --- a/packages/app/sst-env.d.ts +++ b/packages/app/sst-env.d.ts @@ -6,4 +6,4 @@ /// import "sst" -export {} +export {} \ No newline at end of file diff --git a/packages/console/app/package.json b/packages/console/app/package.json index e370915cb0..d5454c117e 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -12,12 +12,13 @@ "dependencies": { "@ibm/plex": "6.4.1", "@openauthjs/openauth": "0.0.0-20250322224806", + "@opencode/console-core": "workspace:*", "@solidjs/meta": "^0.29.4", "@solidjs/router": "^0.15.0", "@solidjs/start": "^1.1.0", "solid-js": "catalog:", "vinxi": "^0.5.7", - "@opencode/console-core": "workspace:*" + "zod": "catalog:" }, "engines": { "node": ">=22" diff --git a/packages/console/app/src/component/workspace/privacy-section.module.css b/packages/console/app/src/component/workspace/privacy-section.module.css new file mode 100644 index 0000000000..0bb5709cb8 --- /dev/null +++ b/packages/console/app/src/component/workspace/privacy-section.module.css @@ -0,0 +1,114 @@ +.root { + [data-slot="section-content"] { + display: flex; + flex-direction: column; + gap: var(--space-3); + } + + [data-slot="reload-error"] { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-4); + padding: var(--space-4); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + + p { + color: var(--color-danger); + font-size: var(--font-size-sm); + line-height: 1.4; + margin: 0; + flex: 1; + } + + [data-slot="create-form"] { + display: flex; + gap: var(--space-2); + margin: 0; + flex-shrink: 0; + } + } + [data-slot="payment"] { + display: flex; + flex-direction: column; + gap: var(--space-3); + padding: var(--space-4); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + min-width: 14.5rem; + width: fit-content; + + @media (max-width: 30rem) { + width: 100%; + } + + [data-slot="credit-card"] { + padding: var(--space-3-5) var(--space-4); + background-color: var(--color-bg-surface); + border-radius: var(--border-radius-sm); + display: flex; + align-items: center; + justify-content: space-between; + + [data-slot="card-icon"] { + display: flex; + align-items: center; + color: var(--color-text-muted); + } + + [data-slot="card-details"] { + display: flex; + align-items: baseline; + gap: var(--space-1); + + [data-slot="secret"] { + position: relative; + bottom: 2px; + font-size: var(--font-size-lg); + color: var(--color-text-muted); + font-weight: 400; + } + + [data-slot="number"] { + font-size: var(--font-size-3xl); + font-weight: 500; + color: var(--color-text); + } + } + } + + [data-slot="button-row"] { + display: flex; + gap: var(--space-2); + align-items: center; + + @media (max-width: 30rem) { + flex-direction: column; + + > button { + width: 100%; + } + } + + [data-slot="create-form"] { + margin: 0; + } + + /* Make Enable Billing button full width when it's the only button */ + > button { + flex: 1; + } + } + } + [data-slot="usage"] { + p { + font-size: var(--font-size-sm); + line-height: 1.5; + color: var(--color-text-secondary); + b { + font-weight: 600; + } + } + } +} diff --git a/packages/console/app/src/component/workspace/privacy-section.tsx b/packages/console/app/src/component/workspace/privacy-section.tsx new file mode 100644 index 0000000000..12dd3b2037 --- /dev/null +++ b/packages/console/app/src/component/workspace/privacy-section.tsx @@ -0,0 +1,98 @@ +import { json, query, action, useParams, createAsync, useSubmission } from "@solidjs/router" +import { withActor } from "~/context/auth.withActor" +import styles from "./billing-section.module.css" +import { Database, eq } from "@opencode/console-core/drizzle/index.js" +import { WorkspaceTable } from "@opencode/console-core/schema/workspace.sql.js" +import { Show } from "solid-js" + +const updateShare = action(async (form: FormData) => { + "use server" + const workspaceID = form.get("workspaceID")?.toString() + if (!workspaceID) return { error: "Workspace ID is required" } + const dataShare = form.get("dataShare")?.toString() === "true" ? true : null + return json( + await withActor(() => { + return Database.use((tx) => + tx + .update(WorkspaceTable) + .set({ + dataShare, + }) + .where(eq(WorkspaceTable.id, workspaceID)), + ) + }, workspaceID), + { revalidate: getWorkspaceInfo.key }, + ) +}, "workspace.disableShare") + +const getWorkspaceInfo = query(async (workspaceID: string) => { + "use server" + return withActor(() => { + return Database.use((tx) => + tx + .select({ + dataShare: WorkspaceTable.dataShare, + }) + .from(WorkspaceTable) + .where(eq(WorkspaceTable.id, workspaceID)) + .then((r) => r[0]), + ) + }, workspaceID) +}, "workspace.get") + +export function PrivacySection() { + const params = useParams() + const workspaceInfo = createAsync(() => getWorkspaceInfo(params.id)) + const updateShareSubmission = useSubmission(updateShare) + + return ( +
+
+

Privacy controls

+

+ Some providers offer data-sharing programs. If you opt in, you voluntarily share your usage data with + them, which they may use to improve their services, including model training. +

+
+

+ By opting in, you gain access to discounted pricing from the provider. You can opt in or out at any + time. +

+
+

+ + Learn more + +

+
+ +
+
+
+ You are currently opted in to the data-sharing program. +
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+ ) +} diff --git a/packages/console/app/src/routes/workspace/[id].tsx b/packages/console/app/src/routes/workspace/[id].tsx index ec21126c89..dc8e28cdbb 100644 --- a/packages/console/app/src/routes/workspace/[id].tsx +++ b/packages/console/app/src/routes/workspace/[id].tsx @@ -2,11 +2,15 @@ import "./[id].css" import { MonthlyLimitSection } from "~/component/workspace/monthly-limit-section" import { NewUserSection } from "~/component/workspace/new-user-section" import { BillingSection } from "~/component/workspace/billing-section" +import { PrivacySection } from "~/component/workspace/privacy-section" import { PaymentSection } from "~/component/workspace/payment-section" import { UsageSection } from "~/component/workspace/usage-section" import { KeySection } from "~/component/workspace/key-section" +import { Show } from "solid-js" +import { useParams } from "@solidjs/router" export default function () { + const params = useParams() return (
@@ -25,9 +29,20 @@ export default function () { + + +
) } + +export function isBeta(workspaceID: string) { + return [ + "wrk_01K46JDFR0E75SG2Q8K172KF3Y", // production + "wrk_01K4NFRR5P7FSYWH88307B4DDS", // dev + "wrk_01K4PJRKJ2WPQZN3FFYRV4673F", // frank + ].includes(workspaceID) +} diff --git a/packages/console/app/src/routes/zen/handler.ts b/packages/console/app/src/routes/zen/handler.ts index 6065e2f76f..a60abeb70c 100644 --- a/packages/console/app/src/routes/zen/handler.ts +++ b/packages/console/app/src/routes/zen/handler.ts @@ -1,37 +1,15 @@ +import { z } from "zod" import type { APIEvent } from "@solidjs/start/server" import path from "node:path" import { and, Database, eq, isNull, lt, or, sql } from "@opencode/console-core/drizzle/index.js" import { KeyTable } from "@opencode/console-core/schema/key.sql.js" -import { BillingTable, PaymentTable, UsageTable } from "@opencode/console-core/schema/billing.sql.js" +import { BillingTable, UsageTable } from "@opencode/console-core/schema/billing.sql.js" import { centsToMicroCents } from "@opencode/console-core/util/price.js" import { Identifier } from "@opencode/console-core/identifier.js" import { Resource } from "@opencode/console-resource" import { Billing } from "../../../../core/src/billing" import { Actor } from "@opencode/console-core/actor.js" - -type ModelCost = { - input: number - output: number - cacheRead?: number - cacheWrite5m?: number - cacheWrite1h?: number -} - -type Model = { - id: string - auth: boolean - cost: ModelCost | ((usage: any) => ModelCost) - headerMappings: Record - providers: Record< - string, - { - api: string - apiKey: string - model: string - weight?: number - } - > -} +import { WorkspaceTable } from "@opencode/console-core/schema/workspace.sql.js" export async function handler( input: APIEvent, @@ -56,184 +34,32 @@ export async function handler( class MonthlyLimitError extends Error {} class ModelError extends Error {} - const MODELS: Record = { - "claude-opus-4-1": { - id: "claude-opus-4-1" as const, - auth: true, - cost: { - input: 0.000015, - output: 0.000075, - cacheRead: 0.0000015, - cacheWrite5m: 0.00001875, - cacheWrite1h: 0.00003, - }, - headerMappings: {}, - providers: { - anthropic: { - api: "https://api.anthropic.com", - apiKey: Resource.ANTHROPIC_API_KEY.value, - model: "claude-opus-4-1-20250805", - }, - }, - }, - "claude-sonnet-4": { - id: "claude-sonnet-4" as const, - auth: true, - cost: (usage: any) => { - const totalInputTokens = - usage.inputTokens + usage.cacheReadTokens + usage.cacheWrite5mTokens + usage.cacheWrite1hTokens - return totalInputTokens <= 200_000 - ? { - input: 0.000003, - output: 0.000015, - cacheRead: 0.0000003, - cacheWrite5m: 0.00000375, - cacheWrite1h: 0.000006, - } - : { - input: 0.000006, - output: 0.0000225, - cacheRead: 0.0000006, - cacheWrite5m: 0.0000075, - cacheWrite1h: 0.000012, - } - }, - headerMappings: {}, - providers: { - anthropic: { - api: "https://api.anthropic.com", - apiKey: Resource.ANTHROPIC_API_KEY.value, - model: "claude-sonnet-4-20250514", - }, - }, - }, - "claude-3-5-haiku": { - id: "claude-3-5-haiku" as const, - auth: true, - cost: { - input: 0.0000008, - output: 0.000004, - cacheRead: 0.00000008, - cacheWrite5m: 0.000001, - cacheWrite1h: 0.0000016, - }, - headerMappings: {}, - providers: { - anthropic: { - api: "https://api.anthropic.com", - apiKey: Resource.ANTHROPIC_API_KEY.value, - model: "claude-3-5-haiku-20241022", - }, - }, - }, - "gpt-5": { - id: "gpt-5" as const, - auth: true, - cost: { - input: 0.00000125, - output: 0.00001, - cacheRead: 0.000000125, - }, - headerMappings: {}, - providers: { - openai: { - api: "https://api.openai.com", - apiKey: Resource.OPENAI_API_KEY.value, - model: "gpt-5", - }, - }, - }, - "qwen3-coder": { - id: "qwen3-coder" as const, - auth: true, - cost: { - input: 0.00000045, - output: 0.0000018, - }, - headerMappings: {}, - providers: { - baseten: { - api: "https://inference.baseten.co", - apiKey: Resource.BASETEN_API_KEY.value, - model: "Qwen/Qwen3-Coder-480B-A35B-Instruct", - weight: 4, - }, - fireworks: { - api: "https://api.fireworks.ai/inference", - apiKey: Resource.FIREWORKS_API_KEY.value, - model: "accounts/fireworks/models/qwen3-coder-480b-a35b-instruct", - weight: 1, - }, - }, - }, - "kimi-k2": { - id: "kimi-k2" as const, - auth: true, - cost: { - input: 0.0000006, - output: 0.0000025, - }, - headerMappings: {}, - providers: { - baseten: { - api: "https://inference.baseten.co", - apiKey: Resource.BASETEN_API_KEY.value, - model: "moonshotai/Kimi-K2-Instruct-0905", - //weight: 4, - }, - //fireworks: { - // api: "https://api.fireworks.ai/inference", - // apiKey: Resource.FIREWORKS_API_KEY.value, - // model: "accounts/fireworks/models/kimi-k2-instruct-0905", - // weight: 1, - //}, - }, - }, - "grok-code": { - id: "grok-code" as const, - auth: false, - cost: { - input: 0, - output: 0, - cacheRead: 0, - }, - headerMappings: { - "x-grok-conv-id": "x-opencode-session", - "x-grok-req-id": "x-opencode-request", - }, - providers: { - xai: { - api: "https://api.x.ai", - apiKey: Resource.XAI_API_KEY.value, - model: "grok-code", - }, - }, - }, - // deprecated - "qwen/qwen3-coder": { - id: "qwen/qwen3-coder" as const, - auth: true, - cost: { - input: 0.00000038, - output: 0.00000153, - }, - headerMappings: {}, - providers: { - baseten: { - api: "https://inference.baseten.co", - apiKey: Resource.BASETEN_API_KEY.value, - model: "Qwen/Qwen3-Coder-480B-A35B-Instruct", - weight: 5, - }, - fireworks: { - api: "https://api.fireworks.ai/inference", - apiKey: Resource.FIREWORKS_API_KEY.value, - model: "accounts/fireworks/models/qwen3-coder-480b-a35b-instruct", - weight: 1, - }, - }, - }, - } + const ModelCostSchema = z.object({ + input: z.number(), + output: z.number(), + cacheRead: z.number().optional(), + cacheWrite5m: z.number().optional(), + cacheWrite1h: z.number().optional(), + }) + + const ModelSchema = z.object({ + cost: ModelCostSchema, + cost200K: ModelCostSchema.optional(), + providers: z.array( + z.object({ + id: z.string(), + api: z.string(), + apiKey: z.string(), + model: z.string(), + weight: z.number().optional(), + allowAnonymous: z.boolean().optional(), + headerMappings: z.record(z.string(), z.string()).optional(), + disabled: z.boolean().optional(), + }), + ), + }) + + type Model = z.infer const FREE_WORKSPACES = [ "wrk_01K46JDFR0E75SG2Q8K172KF3Y", // frank @@ -259,31 +85,28 @@ export async function handler( session: input.request.headers.get("x-opencode-session"), request: input.request.headers.get("x-opencode-request"), }) - const MODEL = validateModel() - const apiKey = await authenticate() - const isFree = FREE_WORKSPACES.includes(apiKey?.workspaceID ?? "") - await checkCreditsAndLimit() - const providerName = selectProvider() - const providerData = MODEL.providers[providerName] - logger.metric({ provider: providerName }) + const authInfo = await authenticate() + const modelInfo = validateModel(body.model, authInfo) + const providerInfo = selectProvider(modelInfo, authInfo) + logger.metric({ provider: providerInfo.id }) // Request to model provider const startTimestamp = Date.now() - const res = await fetch(path.posix.join(providerData.api, url.pathname.replace(/^\/zen/, "") + url.search), { + const res = await fetch(path.posix.join(providerInfo.api, url.pathname.replace(/^\/zen/, "") + url.search), { method: "POST", headers: (() => { const headers = input.request.headers headers.delete("host") headers.delete("content-length") - opts.setAuthHeader(headers, providerData.apiKey) - Object.entries(MODEL.headerMappings ?? {}).forEach(([k, v]) => { + opts.setAuthHeader(headers, providerInfo.apiKey) + Object.entries(providerInfo.headerMappings ?? {}).forEach(([k, v]) => { headers.set(k, headers.get(v)!) }) return headers })(), body: JSON.stringify({ ...(opts.modifyBody?.(body) ?? body), - model: providerData.model, + model: providerInfo.model, }), }) @@ -302,8 +125,8 @@ export async function handler( const body = JSON.stringify(json) logger.metric({ response_length: body.length }) logger.debug(body) - await trackUsage(json.usage) - await reload() + await trackUsage(authInfo, modelInfo, providerInfo.id, json.usage) + await reload(authInfo) return new Response(body, { status: res.status, statusText: res.statusText, @@ -326,8 +149,8 @@ export async function handler( logger.metric({ response_length: responseLength }) const usage = opts.getStreamUsage() if (usage) { - await trackUsage(usage) - await reload() + await trackUsage(authInfo, modelInfo, providerInfo.id, usage) + await reload(authInfo) } c.close() return @@ -337,6 +160,7 @@ export async function handler( logger.metric({ time_to_first_byte: Date.now() - startTimestamp }) } responseLength += value.length + console.log(decoder.decode(value, { stream: true })) buffer += decoder.decode(value, { stream: true }) const parts = buffer.split("\n\n") @@ -363,202 +187,6 @@ export async function handler( statusText: res.statusText, headers: resHeaders, }) - - function validateModel() { - if (!(body.model in MODELS)) { - throw new ModelError(`Model ${body.model} not supported`) - } - const model = MODELS[body.model as keyof typeof MODELS] - logger.metric({ model: model.id }) - return model - } - - async function authenticate() { - try { - const apiKey = opts.parseApiKey(input.request.headers) - if (!apiKey) throw new AuthError("Missing API key.") - - const key = await Database.use((tx) => - tx - .select({ - id: KeyTable.id, - workspaceID: KeyTable.workspaceID, - }) - .from(KeyTable) - .where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted))) - .then((rows) => rows[0]), - ) - - if (!key) throw new AuthError("Invalid API key.") - logger.metric({ - api_key: key.id, - workspace: key.workspaceID, - }) - return key - } catch (e) { - // ignore error if model does not require authentication - if (!MODEL.auth) return - throw e - } - } - - async function checkCreditsAndLimit() { - if (!apiKey || !MODEL.auth || isFree) return - - const billing = await Database.use((tx) => - tx - .select({ - balance: BillingTable.balance, - paymentMethodID: BillingTable.paymentMethodID, - monthlyLimit: BillingTable.monthlyLimit, - monthlyUsage: BillingTable.monthlyUsage, - timeMonthlyUsageUpdated: BillingTable.timeMonthlyUsageUpdated, - }) - .from(BillingTable) - .where(eq(BillingTable.workspaceID, apiKey.workspaceID)) - .then((rows) => rows[0]), - ) - - if (!billing.paymentMethodID) throw new CreditsError("No payment method") - if (billing.balance <= 0) throw new CreditsError("Insufficient balance") - if ( - billing.monthlyLimit && - billing.monthlyUsage && - billing.timeMonthlyUsageUpdated && - billing.monthlyUsage >= centsToMicroCents(billing.monthlyLimit * 100) - ) { - const now = new Date() - const currentYear = now.getUTCFullYear() - const currentMonth = now.getUTCMonth() - const dateYear = billing.timeMonthlyUsageUpdated.getUTCFullYear() - const dateMonth = billing.timeMonthlyUsageUpdated.getUTCMonth() - if (currentYear === dateYear && currentMonth === dateMonth) - throw new MonthlyLimitError(`You have reached your monthly spending limit of $${billing.monthlyLimit}.`) - } - } - - function selectProvider() { - const picks = Object.entries(MODEL.providers).flatMap(([name, provider]) => - Array(provider.weight ?? 1).fill(name), - ) - return picks[Math.floor(Math.random() * picks.length)] - } - - async function trackUsage(usage: any) { - const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } = - opts.normalizeUsage(usage) - - const modelCost = typeof MODEL.cost === "function" ? MODEL.cost(usage) : MODEL.cost - - const inputCost = modelCost.input * inputTokens * 100 - const outputCost = modelCost.output * outputTokens * 100 - const reasoningCost = (() => { - if (!reasoningTokens) return undefined - return modelCost.output * reasoningTokens * 100 - })() - const cacheReadCost = (() => { - if (!cacheReadTokens) return undefined - if (!modelCost.cacheRead) return undefined - return modelCost.cacheRead * cacheReadTokens * 100 - })() - const cacheWrite5mCost = (() => { - if (!cacheWrite5mTokens) return undefined - if (!modelCost.cacheWrite5m) return undefined - return modelCost.cacheWrite5m * cacheWrite5mTokens * 100 - })() - const cacheWrite1hCost = (() => { - if (!cacheWrite1hTokens) return undefined - if (!modelCost.cacheWrite1h) return undefined - return modelCost.cacheWrite1h * cacheWrite1hTokens * 100 - })() - const totalCostInCent = - inputCost + - outputCost + - (reasoningCost ?? 0) + - (cacheReadCost ?? 0) + - (cacheWrite5mCost ?? 0) + - (cacheWrite1hCost ?? 0) - - logger.metric({ - "tokens.input": inputTokens, - "tokens.output": outputTokens, - "tokens.reasoning": reasoningTokens, - "tokens.cache_read": cacheReadTokens, - "tokens.cache_write_5m": cacheWrite5mTokens, - "tokens.cache_write_1h": cacheWrite1hTokens, - "cost.input": Math.round(inputCost), - "cost.output": Math.round(outputCost), - "cost.reasoning": reasoningCost ? Math.round(reasoningCost) : undefined, - "cost.cache_read": cacheReadCost ? Math.round(cacheReadCost) : undefined, - "cost.cache_write_5m": cacheWrite5mCost ? Math.round(cacheWrite5mCost) : undefined, - "cost.cache_write_1h": cacheWrite1hCost ? Math.round(cacheWrite1hCost) : undefined, - "cost.total": Math.round(totalCostInCent), - }) - - if (!apiKey) return - - const cost = isFree ? 0 : centsToMicroCents(totalCostInCent) - await Database.transaction(async (tx) => { - await tx.insert(UsageTable).values({ - workspaceID: apiKey.workspaceID, - id: Identifier.create("usage"), - model: MODEL.id, - provider: providerName, - inputTokens, - outputTokens, - reasoningTokens, - cacheReadTokens, - cacheWrite5mTokens, - cacheWrite1hTokens, - cost, - }) - await tx - .update(BillingTable) - .set({ - balance: sql`${BillingTable.balance} - ${cost}`, - monthlyUsage: sql` - CASE - WHEN MONTH(${BillingTable.timeMonthlyUsageUpdated}) = MONTH(now()) AND YEAR(${BillingTable.timeMonthlyUsageUpdated}) = YEAR(now()) THEN ${BillingTable.monthlyUsage} + ${cost} - ELSE ${cost} - END - `, - timeMonthlyUsageUpdated: sql`now()`, - }) - .where(eq(BillingTable.workspaceID, apiKey.workspaceID)) - }) - - await Database.use((tx) => - tx - .update(KeyTable) - .set({ timeUsed: sql`now()` }) - .where(eq(KeyTable.id, apiKey.id)), - ) - } - - async function reload() { - if (!apiKey) return - - const lock = await Database.use((tx) => - tx - .update(BillingTable) - .set({ - timeReloadLockedTill: sql`now() + interval 1 minute`, - }) - .where( - and( - eq(BillingTable.workspaceID, apiKey.workspaceID), - eq(BillingTable.reload, true), - lt(BillingTable.balance, centsToMicroCents(Billing.CHARGE_THRESHOLD)), - or(isNull(BillingTable.timeReloadLockedTill), lt(BillingTable.timeReloadLockedTill, sql`now()`)), - ), - ), - ) - if (lock.rowsAffected === 0) return - - await Actor.provide("system", { workspaceID: apiKey.workspaceID }, async () => { - await Billing.reload() - }) - } } catch (error: any) { logger.metric({ "error.type": error.constructor.name, @@ -591,4 +219,222 @@ export async function handler( { status: 500 }, ) } + + async function authenticate() { + const apiKey = opts.parseApiKey(input.request.headers) + if (!apiKey) return + + const data = await Database.use((tx) => + tx + .select({ + apiKey: KeyTable.id, + workspaceID: KeyTable.workspaceID, + dataShare: WorkspaceTable.dataShare, + balance: BillingTable.balance, + paymentMethodID: BillingTable.paymentMethodID, + monthlyLimit: BillingTable.monthlyLimit, + monthlyUsage: BillingTable.monthlyUsage, + timeMonthlyUsageUpdated: BillingTable.timeMonthlyUsageUpdated, + }) + .from(KeyTable) + .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID)) + .innerJoin(BillingTable, eq(BillingTable.workspaceID, KeyTable.workspaceID)) + .where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted))) + .then((rows) => rows[0]), + ) + + if (!data) throw new AuthError("Invalid API key.") + logger.metric({ + api_key: data.apiKey, + workspace: data.workspaceID, + }) + + const isFree = FREE_WORKSPACES.includes(data.workspaceID) + if (!isFree) { + if (!data.paymentMethodID) throw new CreditsError("No payment method") + if (data.balance <= 0) throw new CreditsError("Insufficient balance") + if ( + data.monthlyLimit && + data.monthlyUsage && + data.timeMonthlyUsageUpdated && + data.monthlyUsage >= centsToMicroCents(data.monthlyLimit * 100) + ) { + const now = new Date() + const currentYear = now.getUTCFullYear() + const currentMonth = now.getUTCMonth() + const dateYear = data.timeMonthlyUsageUpdated.getUTCFullYear() + const dateMonth = data.timeMonthlyUsageUpdated.getUTCMonth() + if (currentYear === dateYear && currentMonth === dateMonth) + throw new MonthlyLimitError(`You have reached your monthly spending limit of $${data.monthlyLimit}.`) + } + } + + return { + apiKeyId: data.apiKey, + workspaceID: data.workspaceID, + dataShare: data.dataShare, + isFree, + } + } + + function validateModel(reqModel: string, authInfo: Awaited>) { + const json = JSON.parse(Resource.ZEN_MODELS.value) + + const allModels = z + .record( + z.string(), + z.object({ + standard: ModelSchema, + dataShare: ModelSchema.optional(), + }), + ) + .parse(json) + + if (!(reqModel in allModels)) { + throw new ModelError(`Model ${reqModel} not supported`) + } + const modelId = reqModel as keyof typeof allModels + const modelData = authInfo?.dataShare + ? (allModels[modelId].dataShare ?? allModels[modelId].standard) + : allModels[modelId].standard + logger.metric({ model: modelId }) + return { id: modelId, ...modelData } + } + + function selectProvider(model: Model, authInfo: Awaited>) { + let providers = model.providers.filter((provider) => !provider.disabled) + + if (!authInfo) { + providers = providers.filter((provider) => provider.allowAnonymous) + if (providers.length === 0) throw new AuthError("Missing API key.") + } + + const picks = providers.flatMap((provider) => Array(provider.weight ?? 1).fill(provider)) + return picks[Math.floor(Math.random() * picks.length)] + } + + async function trackUsage( + authInfo: Awaited>, + modelInfo: ReturnType, + providerId: string, + usage: any, + ) { + const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } = + opts.normalizeUsage(usage) + + const modelCost = + modelInfo.cost200K && + usage.inputTokens + usage.cacheReadTokens + usage.cacheWrite5mTokens + usage.cacheWrite1hTokens > 200_000 + ? modelInfo.cost200K + : modelInfo.cost + + const inputCost = modelCost.input * inputTokens * 100 + const outputCost = modelCost.output * outputTokens * 100 + const reasoningCost = (() => { + if (!reasoningTokens) return undefined + return modelCost.output * reasoningTokens * 100 + })() + const cacheReadCost = (() => { + if (!cacheReadTokens) return undefined + if (!modelCost.cacheRead) return undefined + return modelCost.cacheRead * cacheReadTokens * 100 + })() + const cacheWrite5mCost = (() => { + if (!cacheWrite5mTokens) return undefined + if (!modelCost.cacheWrite5m) return undefined + return modelCost.cacheWrite5m * cacheWrite5mTokens * 100 + })() + const cacheWrite1hCost = (() => { + if (!cacheWrite1hTokens) return undefined + if (!modelCost.cacheWrite1h) return undefined + return modelCost.cacheWrite1h * cacheWrite1hTokens * 100 + })() + const totalCostInCent = + inputCost + + outputCost + + (reasoningCost ?? 0) + + (cacheReadCost ?? 0) + + (cacheWrite5mCost ?? 0) + + (cacheWrite1hCost ?? 0) + + logger.metric({ + "tokens.input": inputTokens, + "tokens.output": outputTokens, + "tokens.reasoning": reasoningTokens, + "tokens.cache_read": cacheReadTokens, + "tokens.cache_write_5m": cacheWrite5mTokens, + "tokens.cache_write_1h": cacheWrite1hTokens, + "cost.input": Math.round(inputCost), + "cost.output": Math.round(outputCost), + "cost.reasoning": reasoningCost ? Math.round(reasoningCost) : undefined, + "cost.cache_read": cacheReadCost ? Math.round(cacheReadCost) : undefined, + "cost.cache_write_5m": cacheWrite5mCost ? Math.round(cacheWrite5mCost) : undefined, + "cost.cache_write_1h": cacheWrite1hCost ? Math.round(cacheWrite1hCost) : undefined, + "cost.total": Math.round(totalCostInCent), + }) + + if (!authInfo) return + + const cost = authInfo.isFree ? 0 : centsToMicroCents(totalCostInCent) + await Database.transaction(async (tx) => { + await tx.insert(UsageTable).values({ + workspaceID: authInfo.workspaceID, + id: Identifier.create("usage"), + model: modelInfo.id, + provider: providerId, + inputTokens, + outputTokens, + reasoningTokens, + cacheReadTokens, + cacheWrite5mTokens, + cacheWrite1hTokens, + cost, + }) + await tx + .update(BillingTable) + .set({ + balance: sql`${BillingTable.balance} - ${cost}`, + monthlyUsage: sql` + CASE + WHEN MONTH(${BillingTable.timeMonthlyUsageUpdated}) = MONTH(now()) AND YEAR(${BillingTable.timeMonthlyUsageUpdated}) = YEAR(now()) THEN ${BillingTable.monthlyUsage} + ${cost} + ELSE ${cost} + END + `, + timeMonthlyUsageUpdated: sql`now()`, + }) + .where(eq(BillingTable.workspaceID, authInfo.workspaceID)) + }) + + await Database.use((tx) => + tx + .update(KeyTable) + .set({ timeUsed: sql`now()` }) + .where(eq(KeyTable.id, authInfo.apiKeyId)), + ) + } + + async function reload(authInfo: Awaited>) { + if (!authInfo) return + + const lock = await Database.use((tx) => + tx + .update(BillingTable) + .set({ + timeReloadLockedTill: sql`now() + interval 1 minute`, + }) + .where( + and( + eq(BillingTable.workspaceID, authInfo.workspaceID), + eq(BillingTable.reload, true), + lt(BillingTable.balance, centsToMicroCents(Billing.CHARGE_THRESHOLD)), + or(isNull(BillingTable.timeReloadLockedTill), lt(BillingTable.timeReloadLockedTill, sql`now()`)), + ), + ), + ) + if (lock.rowsAffected === 0) return + + await Actor.provide("system", { workspaceID: authInfo.workspaceID }, async () => { + await Billing.reload() + }) + } } diff --git a/packages/console/app/src/routes/zen/v1/chat/completions.ts b/packages/console/app/src/routes/zen/v1/chat/completions.ts index 801557324e..0db14c7ad0 100644 --- a/packages/console/app/src/routes/zen/v1/chat/completions.ts +++ b/packages/console/app/src/routes/zen/v1/chat/completions.ts @@ -5,6 +5,9 @@ type Usage = { prompt_tokens?: number completion_tokens?: number total_tokens?: number + // used by moonshot + cached_tokens?: number + // used by xai prompt_tokens_details?: { text_tokens?: number audio_tokens?: number @@ -48,7 +51,7 @@ export function POST(input: APIEvent) { inputTokens: usage.prompt_tokens ?? 0, outputTokens: usage.completion_tokens ?? 0, reasoningTokens: usage.completion_tokens_details?.reasoning_tokens ?? undefined, - cacheReadTokens: usage.prompt_tokens_details?.cached_tokens ?? undefined, + cacheReadTokens: usage.cached_tokens ?? usage.prompt_tokens_details?.cached_tokens ?? undefined, }), }) } diff --git a/packages/console/app/sst-env.d.ts b/packages/console/app/sst-env.d.ts index bd55882173..9b9de73273 100644 --- a/packages/console/app/sst-env.d.ts +++ b/packages/console/app/sst-env.d.ts @@ -6,4 +6,4 @@ /// import "sst" -export {} +export {} \ No newline at end of file diff --git a/packages/console/core/migrations/0014_demonic_princess_powerful.sql b/packages/console/core/migrations/0014_demonic_princess_powerful.sql new file mode 100644 index 0000000000..c2c228be1e --- /dev/null +++ b/packages/console/core/migrations/0014_demonic_princess_powerful.sql @@ -0,0 +1 @@ +ALTER TABLE `workspace` ADD `data_share` boolean; \ No newline at end of file diff --git a/packages/console/core/migrations/meta/0014_snapshot.json b/packages/console/core/migrations/meta/0014_snapshot.json new file mode 100644 index 0000000000..54fcc4ff0b --- /dev/null +++ b/packages/console/core/migrations/meta/0014_snapshot.json @@ -0,0 +1,681 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "12189a4e-5083-4b17-b8e3-8279c9a3e61a", + "prevId": "28336c91-553c-4d1d-9875-1ee761e47582", + "tables": { + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "email": { + "name": "email", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "billing": { + "name": "billing", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_id": { + "name": "customer_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_method_id": { + "name": "payment_method_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_method_last4": { + "name": "payment_method_last4", + "type": "varchar(4)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "balance": { + "name": "balance", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "monthly_limit": { + "name": "monthly_limit", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "monthly_usage": { + "name": "monthly_usage", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_monthly_usage_updated": { + "name": "time_monthly_usage_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reload": { + "name": "reload", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reload_error": { + "name": "reload_error", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_reload_error": { + "name": "time_reload_error", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_reload_locked_till": { + "name": "time_reload_locked_till", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "global_customer_id": { + "name": "global_customer_id", + "columns": [ + "customer_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "billing_workspace_id_id_pk": { + "name": "billing_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "payment": { + "name": "payment", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_id": { + "name": "customer_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_id": { + "name": "payment_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "amount": { + "name": "amount", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "payment_workspace_id_id_pk": { + "name": "payment_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "usage": { + "name": "usage", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_write_5m_tokens": { + "name": "cache_write_5m_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_write_1h_tokens": { + "name": "cache_write_1h_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost": { + "name": "cost", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "usage_workspace_id_id_pk": { + "name": "usage_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "key": { + "name": "key", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor": { + "name": "actor", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "old_name": { + "name": "old_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_used": { + "name": "time_used", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "global_key": { + "name": "global_key", + "columns": [ + "key" + ], + "isUnique": true + }, + "name": { + "name": "name", + "columns": [ + "workspace_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "key_workspace_id_id_pk": { + "name": "key_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_seen": { + "name": "time_seen", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "user_email": { + "name": "user_email", + "columns": [ + "workspace_id", + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "user_workspace_id_id_pk": { + "name": "user_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "workspace": { + "name": "workspace", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "data_share": { + "name": "data_share", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "slug": { + "name": "slug", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "workspace_id": { + "name": "workspace_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/console/core/migrations/meta/_journal.json b/packages/console/core/migrations/meta/_journal.json index 1b7d45da33..e1eb8da649 100644 --- a/packages/console/core/migrations/meta/_journal.json +++ b/packages/console/core/migrations/meta/_journal.json @@ -99,6 +99,13 @@ "when": 1757956978089, "tag": "0013_absurd_hobgoblin", "breakpoints": true + }, + { + "idx": 14, + "version": "5", + "when": 1758289919722, + "tag": "0014_demonic_princess_powerful", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/packages/console/core/src/schema/workspace.sql.ts b/packages/console/core/src/schema/workspace.sql.ts index 9792554289..67fddb08c2 100644 --- a/packages/console/core/src/schema/workspace.sql.ts +++ b/packages/console/core/src/schema/workspace.sql.ts @@ -1,4 +1,4 @@ -import { primaryKey, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core" +import { primaryKey, mysqlTable, uniqueIndex, varchar, boolean } from "drizzle-orm/mysql-core" import { timestamps, ulid } from "../drizzle/types" export const WorkspaceTable = mysqlTable( @@ -7,6 +7,7 @@ export const WorkspaceTable = mysqlTable( id: ulid("id").notNull().primaryKey(), slug: varchar("slug", { length: 255 }), name: varchar("name", { length: 255 }), + dataShare: boolean("data_share"), ...timestamps, }, (table) => [uniqueIndex("slug").on(table.slug)], diff --git a/packages/console/core/src/workspace.ts b/packages/console/core/src/workspace.ts index a9fb923d6f..79959f708d 100644 --- a/packages/console/core/src/workspace.ts +++ b/packages/console/core/src/workspace.ts @@ -1,6 +1,5 @@ import { z } from "zod" import { fn } from "./util/fn" -import { centsToMicroCents } from "./util/price" import { Actor } from "./actor" import { Database, eq } from "./drizzle" import { Identifier } from "./identifier" diff --git a/packages/console/core/sst-env.d.ts b/packages/console/core/sst-env.d.ts index bd55882173..9b9de73273 100644 --- a/packages/console/core/sst-env.d.ts +++ b/packages/console/core/sst-env.d.ts @@ -6,4 +6,4 @@ /// import "sst" -export {} +export {} \ No newline at end of file diff --git a/packages/console/function/sst-env.d.ts b/packages/console/function/sst-env.d.ts index afa8c6fe78..b07b55f628 100644 --- a/packages/console/function/sst-env.d.ts +++ b/packages/console/function/sst-env.d.ts @@ -6,91 +6,75 @@ import "sst" declare module "sst" { export interface Resource { - ANTHROPIC_API_KEY: { - type: "sst.sst.Secret" - value: string + "AUTH_API_URL": { + "type": "sst.sst.Linkable" + "value": string } - AUTH_API_URL: { - type: "sst.sst.Linkable" - value: string + "Console": { + "type": "sst.cloudflare.SolidStart" + "url": string } - BASETEN_API_KEY: { - type: "sst.sst.Secret" - value: string + "Database": { + "database": string + "host": string + "password": string + "port": number + "type": "sst.sst.Linkable" + "username": string } - Console: { - type: "sst.cloudflare.SolidStart" - url: string + "GITHUB_APP_ID": { + "type": "sst.sst.Secret" + "value": string } - Database: { - database: string - host: string - password: string - port: number - type: "sst.sst.Linkable" - username: string + "GITHUB_APP_PRIVATE_KEY": { + "type": "sst.sst.Secret" + "value": string } - FIREWORKS_API_KEY: { - type: "sst.sst.Secret" - value: string + "GITHUB_CLIENT_ID_CONSOLE": { + "type": "sst.sst.Secret" + "value": string } - GITHUB_APP_ID: { - type: "sst.sst.Secret" - value: string + "GITHUB_CLIENT_SECRET_CONSOLE": { + "type": "sst.sst.Secret" + "value": string } - GITHUB_APP_PRIVATE_KEY: { - type: "sst.sst.Secret" - value: string + "GOOGLE_CLIENT_ID": { + "type": "sst.sst.Secret" + "value": string } - GITHUB_CLIENT_ID_CONSOLE: { - type: "sst.sst.Secret" - value: string + "HONEYCOMB_API_KEY": { + "type": "sst.sst.Secret" + "value": string } - GITHUB_CLIENT_SECRET_CONSOLE: { - type: "sst.sst.Secret" - value: string + "STRIPE_SECRET_KEY": { + "type": "sst.sst.Secret" + "value": string } - GOOGLE_CLIENT_ID: { - type: "sst.sst.Secret" - value: string + "STRIPE_WEBHOOK_SECRET": { + "type": "sst.sst.Linkable" + "value": string } - HONEYCOMB_API_KEY: { - type: "sst.sst.Secret" - value: string + "Web": { + "type": "sst.cloudflare.Astro" + "url": string } - OPENAI_API_KEY: { - type: "sst.sst.Secret" - value: string - } - STRIPE_SECRET_KEY: { - type: "sst.sst.Secret" - value: string - } - STRIPE_WEBHOOK_SECRET: { - type: "sst.sst.Linkable" - value: string - } - Web: { - type: "sst.cloudflare.Astro" - url: string - } - XAI_API_KEY: { - type: "sst.sst.Secret" - value: string + "ZEN_MODELS": { + "type": "sst.sst.Secret" + "value": string } } } -// cloudflare -import * as cloudflare from "@cloudflare/workers-types" +// cloudflare +import * as cloudflare from "@cloudflare/workers-types"; declare module "sst" { export interface Resource { - Api: cloudflare.Service - AuthApi: cloudflare.Service - AuthStorage: cloudflare.KVNamespace - Bucket: cloudflare.R2Bucket - LogProcessor: cloudflare.Service + "Api": cloudflare.Service + "AuthApi": cloudflare.Service + "AuthStorage": cloudflare.KVNamespace + "Bucket": cloudflare.R2Bucket + "LogProcessor": cloudflare.Service } } import "sst" -export {} +export {} \ No newline at end of file diff --git a/packages/console/resource/sst-env.d.ts b/packages/console/resource/sst-env.d.ts index afa8c6fe78..b07b55f628 100644 --- a/packages/console/resource/sst-env.d.ts +++ b/packages/console/resource/sst-env.d.ts @@ -6,91 +6,75 @@ import "sst" declare module "sst" { export interface Resource { - ANTHROPIC_API_KEY: { - type: "sst.sst.Secret" - value: string + "AUTH_API_URL": { + "type": "sst.sst.Linkable" + "value": string } - AUTH_API_URL: { - type: "sst.sst.Linkable" - value: string + "Console": { + "type": "sst.cloudflare.SolidStart" + "url": string } - BASETEN_API_KEY: { - type: "sst.sst.Secret" - value: string + "Database": { + "database": string + "host": string + "password": string + "port": number + "type": "sst.sst.Linkable" + "username": string } - Console: { - type: "sst.cloudflare.SolidStart" - url: string + "GITHUB_APP_ID": { + "type": "sst.sst.Secret" + "value": string } - Database: { - database: string - host: string - password: string - port: number - type: "sst.sst.Linkable" - username: string + "GITHUB_APP_PRIVATE_KEY": { + "type": "sst.sst.Secret" + "value": string } - FIREWORKS_API_KEY: { - type: "sst.sst.Secret" - value: string + "GITHUB_CLIENT_ID_CONSOLE": { + "type": "sst.sst.Secret" + "value": string } - GITHUB_APP_ID: { - type: "sst.sst.Secret" - value: string + "GITHUB_CLIENT_SECRET_CONSOLE": { + "type": "sst.sst.Secret" + "value": string } - GITHUB_APP_PRIVATE_KEY: { - type: "sst.sst.Secret" - value: string + "GOOGLE_CLIENT_ID": { + "type": "sst.sst.Secret" + "value": string } - GITHUB_CLIENT_ID_CONSOLE: { - type: "sst.sst.Secret" - value: string + "HONEYCOMB_API_KEY": { + "type": "sst.sst.Secret" + "value": string } - GITHUB_CLIENT_SECRET_CONSOLE: { - type: "sst.sst.Secret" - value: string + "STRIPE_SECRET_KEY": { + "type": "sst.sst.Secret" + "value": string } - GOOGLE_CLIENT_ID: { - type: "sst.sst.Secret" - value: string + "STRIPE_WEBHOOK_SECRET": { + "type": "sst.sst.Linkable" + "value": string } - HONEYCOMB_API_KEY: { - type: "sst.sst.Secret" - value: string + "Web": { + "type": "sst.cloudflare.Astro" + "url": string } - OPENAI_API_KEY: { - type: "sst.sst.Secret" - value: string - } - STRIPE_SECRET_KEY: { - type: "sst.sst.Secret" - value: string - } - STRIPE_WEBHOOK_SECRET: { - type: "sst.sst.Linkable" - value: string - } - Web: { - type: "sst.cloudflare.Astro" - url: string - } - XAI_API_KEY: { - type: "sst.sst.Secret" - value: string + "ZEN_MODELS": { + "type": "sst.sst.Secret" + "value": string } } } -// cloudflare -import * as cloudflare from "@cloudflare/workers-types" +// cloudflare +import * as cloudflare from "@cloudflare/workers-types"; declare module "sst" { export interface Resource { - Api: cloudflare.Service - AuthApi: cloudflare.Service - AuthStorage: cloudflare.KVNamespace - Bucket: cloudflare.R2Bucket - LogProcessor: cloudflare.Service + "Api": cloudflare.Service + "AuthApi": cloudflare.Service + "AuthStorage": cloudflare.KVNamespace + "Bucket": cloudflare.R2Bucket + "LogProcessor": cloudflare.Service } } import "sst" -export {} +export {} \ No newline at end of file diff --git a/packages/console/scripts/sst-env.d.ts b/packages/console/scripts/sst-env.d.ts index bd55882173..9b9de73273 100644 --- a/packages/console/scripts/sst-env.d.ts +++ b/packages/console/scripts/sst-env.d.ts @@ -6,4 +6,4 @@ /// import "sst" -export {} +export {} \ No newline at end of file diff --git a/packages/function/sst-env.d.ts b/packages/function/sst-env.d.ts index afa8c6fe78..b07b55f628 100644 --- a/packages/function/sst-env.d.ts +++ b/packages/function/sst-env.d.ts @@ -6,91 +6,75 @@ import "sst" declare module "sst" { export interface Resource { - ANTHROPIC_API_KEY: { - type: "sst.sst.Secret" - value: string + "AUTH_API_URL": { + "type": "sst.sst.Linkable" + "value": string } - AUTH_API_URL: { - type: "sst.sst.Linkable" - value: string + "Console": { + "type": "sst.cloudflare.SolidStart" + "url": string } - BASETEN_API_KEY: { - type: "sst.sst.Secret" - value: string + "Database": { + "database": string + "host": string + "password": string + "port": number + "type": "sst.sst.Linkable" + "username": string } - Console: { - type: "sst.cloudflare.SolidStart" - url: string + "GITHUB_APP_ID": { + "type": "sst.sst.Secret" + "value": string } - Database: { - database: string - host: string - password: string - port: number - type: "sst.sst.Linkable" - username: string + "GITHUB_APP_PRIVATE_KEY": { + "type": "sst.sst.Secret" + "value": string } - FIREWORKS_API_KEY: { - type: "sst.sst.Secret" - value: string + "GITHUB_CLIENT_ID_CONSOLE": { + "type": "sst.sst.Secret" + "value": string } - GITHUB_APP_ID: { - type: "sst.sst.Secret" - value: string + "GITHUB_CLIENT_SECRET_CONSOLE": { + "type": "sst.sst.Secret" + "value": string } - GITHUB_APP_PRIVATE_KEY: { - type: "sst.sst.Secret" - value: string + "GOOGLE_CLIENT_ID": { + "type": "sst.sst.Secret" + "value": string } - GITHUB_CLIENT_ID_CONSOLE: { - type: "sst.sst.Secret" - value: string + "HONEYCOMB_API_KEY": { + "type": "sst.sst.Secret" + "value": string } - GITHUB_CLIENT_SECRET_CONSOLE: { - type: "sst.sst.Secret" - value: string + "STRIPE_SECRET_KEY": { + "type": "sst.sst.Secret" + "value": string } - GOOGLE_CLIENT_ID: { - type: "sst.sst.Secret" - value: string + "STRIPE_WEBHOOK_SECRET": { + "type": "sst.sst.Linkable" + "value": string } - HONEYCOMB_API_KEY: { - type: "sst.sst.Secret" - value: string + "Web": { + "type": "sst.cloudflare.Astro" + "url": string } - OPENAI_API_KEY: { - type: "sst.sst.Secret" - value: string - } - STRIPE_SECRET_KEY: { - type: "sst.sst.Secret" - value: string - } - STRIPE_WEBHOOK_SECRET: { - type: "sst.sst.Linkable" - value: string - } - Web: { - type: "sst.cloudflare.Astro" - url: string - } - XAI_API_KEY: { - type: "sst.sst.Secret" - value: string + "ZEN_MODELS": { + "type": "sst.sst.Secret" + "value": string } } } -// cloudflare -import * as cloudflare from "@cloudflare/workers-types" +// cloudflare +import * as cloudflare from "@cloudflare/workers-types"; declare module "sst" { export interface Resource { - Api: cloudflare.Service - AuthApi: cloudflare.Service - AuthStorage: cloudflare.KVNamespace - Bucket: cloudflare.R2Bucket - LogProcessor: cloudflare.Service + "Api": cloudflare.Service + "AuthApi": cloudflare.Service + "AuthStorage": cloudflare.KVNamespace + "Bucket": cloudflare.R2Bucket + "LogProcessor": cloudflare.Service } } import "sst" -export {} +export {} \ No newline at end of file diff --git a/packages/opencode/sst-env.d.ts b/packages/opencode/sst-env.d.ts index 0397645b50..b6a7e9066e 100644 --- a/packages/opencode/sst-env.d.ts +++ b/packages/opencode/sst-env.d.ts @@ -6,4 +6,4 @@ /// import "sst" -export {} +export {} \ No newline at end of file diff --git a/packages/plugin/sst-env.d.ts b/packages/plugin/sst-env.d.ts index 0397645b50..b6a7e9066e 100644 --- a/packages/plugin/sst-env.d.ts +++ b/packages/plugin/sst-env.d.ts @@ -6,4 +6,4 @@ /// import "sst" -export {} +export {} \ No newline at end of file diff --git a/packages/sdk/js/sst-env.d.ts b/packages/sdk/js/sst-env.d.ts index bd55882173..9b9de73273 100644 --- a/packages/sdk/js/sst-env.d.ts +++ b/packages/sdk/js/sst-env.d.ts @@ -6,4 +6,4 @@ /// import "sst" -export {} +export {} \ No newline at end of file diff --git a/packages/web/sst-env.d.ts b/packages/web/sst-env.d.ts index 0397645b50..b6a7e9066e 100644 --- a/packages/web/sst-env.d.ts +++ b/packages/web/sst-env.d.ts @@ -6,4 +6,4 @@ /// import "sst" -export {} +export {} \ No newline at end of file diff --git a/sdks/vscode/sst-env.d.ts b/sdks/vscode/sst-env.d.ts index 0397645b50..b6a7e9066e 100644 --- a/sdks/vscode/sst-env.d.ts +++ b/sdks/vscode/sst-env.d.ts @@ -6,4 +6,4 @@ /// import "sst" -export {} +export {} \ No newline at end of file diff --git a/sst-env.d.ts b/sst-env.d.ts index 22caba969d..c2160547a6 100644 --- a/sst-env.d.ts +++ b/sst-env.d.ts @@ -5,99 +5,83 @@ declare module "sst" { export interface Resource { - ANTHROPIC_API_KEY: { - type: "sst.sst.Secret" - value: string + "AUTH_API_URL": { + "type": "sst.sst.Linkable" + "value": string } - AUTH_API_URL: { - type: "sst.sst.Linkable" - value: string + "Api": { + "type": "sst.cloudflare.Worker" + "url": string } - Api: { - type: "sst.cloudflare.Worker" - url: string + "AuthApi": { + "type": "sst.cloudflare.Worker" + "url": string } - AuthApi: { - type: "sst.cloudflare.Worker" - url: string + "AuthStorage": { + "type": "sst.cloudflare.Kv" } - AuthStorage: { - type: "sst.cloudflare.Kv" + "Bucket": { + "name": string + "type": "sst.cloudflare.Bucket" } - BASETEN_API_KEY: { - type: "sst.sst.Secret" - value: string + "Console": { + "type": "sst.cloudflare.SolidStart" + "url": string } - Bucket: { - name: string - type: "sst.cloudflare.Bucket" + "Database": { + "database": string + "host": string + "password": string + "port": number + "type": "sst.sst.Linkable" + "username": string } - Console: { - type: "sst.cloudflare.SolidStart" - url: string + "GITHUB_APP_ID": { + "type": "sst.sst.Secret" + "value": string } - Database: { - database: string - host: string - password: string - port: number - type: "sst.sst.Linkable" - username: string + "GITHUB_APP_PRIVATE_KEY": { + "type": "sst.sst.Secret" + "value": string } - FIREWORKS_API_KEY: { - type: "sst.sst.Secret" - value: string + "GITHUB_CLIENT_ID_CONSOLE": { + "type": "sst.sst.Secret" + "value": string } - GITHUB_APP_ID: { - type: "sst.sst.Secret" - value: string + "GITHUB_CLIENT_SECRET_CONSOLE": { + "type": "sst.sst.Secret" + "value": string } - GITHUB_APP_PRIVATE_KEY: { - type: "sst.sst.Secret" - value: string + "GOOGLE_CLIENT_ID": { + "type": "sst.sst.Secret" + "value": string } - GITHUB_CLIENT_ID_CONSOLE: { - type: "sst.sst.Secret" - value: string + "HONEYCOMB_API_KEY": { + "type": "sst.sst.Secret" + "value": string } - GITHUB_CLIENT_SECRET_CONSOLE: { - type: "sst.sst.Secret" - value: string + "LogProcessor": { + "type": "sst.cloudflare.Worker" } - GOOGLE_CLIENT_ID: { - type: "sst.sst.Secret" - value: string + "STRIPE_SECRET_KEY": { + "type": "sst.sst.Secret" + "value": string } - HONEYCOMB_API_KEY: { - type: "sst.sst.Secret" - value: string + "STRIPE_WEBHOOK_SECRET": { + "type": "sst.sst.Linkable" + "value": string } - LogProcessor: { - type: "sst.cloudflare.Worker" + "Web": { + "type": "sst.cloudflare.Astro" + "url": string } - OPENAI_API_KEY: { - type: "sst.sst.Secret" - value: string - } - STRIPE_SECRET_KEY: { - type: "sst.sst.Secret" - value: string - } - STRIPE_WEBHOOK_SECRET: { - type: "sst.sst.Linkable" - value: string - } - Web: { - type: "sst.cloudflare.Astro" - url: string - } - XAI_API_KEY: { - type: "sst.sst.Secret" - value: string + "ZEN_MODELS": { + "type": "sst.sst.Secret" + "value": string } } } /// import "sst" -export {} +export {} \ No newline at end of file