summaryrefslogtreecommitdiffhomepage
path: root/cloud
diff options
context:
space:
mode:
authorFrank <[email protected]>2025-09-11 18:30:13 -0400
committerFrank <[email protected]>2025-09-11 18:30:13 -0400
commita52b352b24db0339fbc178741fe764ad62be0589 (patch)
treee3ccd21d4d4e2dba3dd56a7f8a0629f80cfdc37e /cloud
parent79c73267cf83b9d96af50ac60cbda948c26d8a49 (diff)
downloadopencode-a52b352b24db0339fbc178741fe764ad62be0589.tar.gz
opencode-a52b352b24db0339fbc178741fe764ad62be0589.zip
wip: zen
Diffstat (limited to 'cloud')
-rw-r--r--cloud/app/src/routes/gateway/v1/chat/completions.ts308
-rw-r--r--cloud/app/src/util/zen.ts1
-rw-r--r--cloud/core/migrations/0004_first_mockingbird.sql1
-rw-r--r--cloud/core/migrations/meta/0004_snapshot.json617
-rw-r--r--cloud/core/migrations/meta/_journal.json9
-rw-r--r--cloud/core/package.json1
-rw-r--r--cloud/core/src/schema/billing.sql.ts1
7 files changed, 629 insertions, 309 deletions
diff --git a/cloud/app/src/routes/gateway/v1/chat/completions.ts b/cloud/app/src/routes/gateway/v1/chat/completions.ts
deleted file mode 100644
index bd33a1ebf..000000000
--- a/cloud/app/src/routes/gateway/v1/chat/completions.ts
+++ /dev/null
@@ -1,308 +0,0 @@
-/**
- * @deprecated Use zen/v1/chat/completions instead
- */
-import { Resource } from "@opencode/cloud-resource"
-import type { APIEvent } from "@solidjs/start/server"
-import { Database, eq, sql } from "@opencode/cloud-core/drizzle/index.js"
-import { KeyTable } from "@opencode/cloud-core/schema/key.sql.js"
-import { BillingTable, UsageTable } from "@opencode/cloud-core/schema/billing.sql.js"
-import { centsToMicroCents } from "@opencode/cloud-core/util/price.js"
-import { Identifier } from "@opencode/cloud-core/identifier.js"
-
-const MODELS = {
- // "anthropic/claude-sonnet-4": {
- // auth: true,
- // api: "https://api.anthropic.com",
- // apiKey: Resource.ANTHROPIC_API_KEY.value,
- // model: "claude-sonnet-4-20250514",
- // cost: {
- // input: 0.0000015,
- // output: 0.000006,
- // reasoning: 0.0000015,
- // cacheRead: 0.0000001,
- // cacheWrite: 0.0000001,
- // },
- // headerMappings: {},
- // },
- "qwen/qwen3-coder": {
- id: "qwen/qwen3-coder",
- auth: true,
- api: "https://inference.baseten.co",
- apiKey: Resource.BASETEN_API_KEY.value,
- model: "Qwen/Qwen3-Coder-480B-A35B-Instruct",
- cost: {
- input: 0.00000038,
- output: 0.00000153,
- reasoning: 0,
- cacheRead: 0,
- cacheWrite: 0,
- },
- headerMappings: {},
- },
- "grok-code": {
- id: "x-ai/grok-code-fast-1",
- auth: false,
- api: "https://api.x.ai",
- apiKey: Resource.XAI_API_KEY.value,
- model: "grok-code",
- cost: {
- input: 0,
- output: 0,
- reasoning: 0,
- cacheRead: 0,
- cacheWrite: 0,
- },
- headerMappings: {
- "x-grok-conv-id": "x-opencode-session",
- "x-grok-req-id": "x-opencode-request",
- },
- },
-}
-
-class AuthError extends Error {}
-class CreditsError extends Error {}
-class ModelError extends Error {}
-
-export async function POST(input: APIEvent) {
- try {
- const url = new URL(input.request.url)
- const body = await input.request.json()
- const MODEL = validateModel()
- const apiKey = await authenticate()
- await checkCredits()
-
- // Request to model provider
- const res = await fetch(new URL(url.pathname.replace(/^\/gateway/, "") + url.search, MODEL.api), {
- method: "POST",
- headers: (() => {
- const headers = input.request.headers
- headers.delete("host")
- headers.delete("content-length")
- headers.set("authorization", `Bearer ${MODEL.apiKey}`)
- Object.entries(MODEL.headerMappings ?? {}).forEach(([k, v]) => {
- headers.set(k, headers.get(v)!)
- })
- return headers
- })(),
- body: JSON.stringify({
- ...body,
- model: MODEL.model,
- stream_options: {
- include_usage: true,
- },
- }),
- })
-
- // Scrub response headers
- const resHeaders = new Headers()
- const keepHeaders = ["content-type", "cache-control"]
- for (const [k, v] of res.headers.entries()) {
- if (keepHeaders.includes(k.toLowerCase())) {
- resHeaders.set(k, v)
- }
- }
-
- // Handle non-streaming response
- if (!body.stream) {
- const body = await res.json()
- await trackUsage(body)
- return new Response(JSON.stringify(body), {
- status: res.status,
- statusText: res.statusText,
- headers: resHeaders,
- })
- }
-
- // Handle streaming response
- const stream = new ReadableStream({
- start(c) {
- const reader = res.body?.getReader()
- const decoder = new TextDecoder()
- let buffer = ""
-
- function pump(): Promise<void> {
- return (
- reader?.read().then(async ({ done, value }) => {
- if (done) {
- c.close()
- return
- }
-
- buffer += decoder.decode(value, { stream: true })
-
- const parts = buffer.split("\n\n")
- buffer = parts.pop() ?? ""
-
- const usage = parts
- .map((part) => part.trim())
- .filter((part) => part.startsWith("data: "))
- .map((part) => {
- try {
- return JSON.parse(part.slice(6))
- } catch (e) {
- return {}
- }
- })
- .find((part) => part.usage)
- if (usage) await trackUsage(usage)
-
- c.enqueue(value)
-
- return pump()
- }) || Promise.resolve()
- )
- }
-
- return pump()
- },
- })
-
- return new Response(stream, {
- status: res.status,
- statusText: res.statusText,
- headers: resHeaders,
- })
-
- function validateModel() {
- if (!(body.model in MODELS)) {
- throw new ModelError(`Model ${body.model} not supported`)
- }
- return MODELS[body.model as keyof typeof MODELS]
- }
-
- async function authenticate() {
- try {
- const authHeader = input.request.headers.get("authorization")
- if (!authHeader || !authHeader.startsWith("Bearer ")) throw new AuthError("Missing API key.")
-
- const apiKey = authHeader.split(" ")[1]
- const key = await Database.use((tx) =>
- tx
- .select({
- id: KeyTable.id,
- workspaceID: KeyTable.workspaceID,
- })
- .from(KeyTable)
- .where(eq(KeyTable.key, apiKey))
- .then((rows) => rows[0]),
- )
-
- if (!key) throw new AuthError("Invalid API key.")
- return key
- } catch (e) {
- console.log(e)
- // ignore error if model does not require authentication
- if (!MODEL.auth) return
- throw e
- }
- }
-
- async function checkCredits() {
- if (!apiKey || !MODEL.auth) return
-
- const billing = await Database.use((tx) =>
- tx
- .select({
- balance: BillingTable.balance,
- })
- .from(BillingTable)
- .where(eq(BillingTable.workspaceID, apiKey.workspaceID))
- .then((rows) => rows[0]),
- )
-
- if (billing.balance <= 0) throw new CreditsError("Insufficient balance")
- }
-
- async function trackUsage(chunk: any) {
- console.log(`trackUsage ${apiKey}`)
-
- if (!apiKey) return
-
- const usage = chunk.usage
- const inputTokens = usage.prompt_tokens ?? 0
- const outputTokens = usage.completion_tokens ?? 0
- const reasoningTokens = usage.completion_tokens_details?.reasoning_tokens ?? 0
- const cacheReadTokens = usage.prompt_tokens_details?.cached_tokens ?? 0
- //const cacheWriteTokens = providerMetadata?.["anthropic"]?.["cacheCreationInputTokens"] ?? 0
- const cacheWriteTokens = 0
-
- const inputCost = MODEL.cost.input * inputTokens
- const outputCost = MODEL.cost.output * outputTokens
- const reasoningCost = MODEL.cost.reasoning * reasoningTokens
- const cacheReadCost = MODEL.cost.cacheRead * cacheReadTokens
- const cacheWriteCost = MODEL.cost.cacheWrite * cacheWriteTokens
- const costInCents = (inputCost + outputCost + reasoningCost + cacheReadCost + cacheWriteCost) * 100
- const cost = centsToMicroCents(costInCents)
-
- await Database.transaction(async (tx) => {
- await tx.insert(UsageTable).values({
- workspaceID: apiKey.workspaceID,
- id: Identifier.create("usage"),
- model: MODEL.id,
- inputTokens,
- outputTokens,
- reasoningTokens,
- cacheReadTokens,
- cacheWriteTokens,
- cost,
- })
- await tx
- .update(BillingTable)
- .set({
- balance: sql`${BillingTable.balance} - ${cost}`,
- })
- .where(eq(BillingTable.workspaceID, apiKey.workspaceID))
- })
-
- await Database.use((tx) =>
- tx
- .update(KeyTable)
- .set({ timeUsed: sql`now()` })
- .where(eq(KeyTable.id, apiKey.id)),
- )
- }
- } catch (error: any) {
- if (error instanceof AuthError) {
- return new Response(
- JSON.stringify({
- error: {
- message: error.message,
- type: "invalid_request_error",
- param: null,
- code: "unauthorized",
- },
- }),
- {
- status: 401,
- },
- )
- }
-
- if (error instanceof CreditsError) {
- return new Response(
- JSON.stringify({
- error: {
- message: error.message,
- type: "insufficient_quota",
- param: null,
- code: "insufficient_quota",
- },
- }),
- {
- status: 401,
- },
- )
- }
-
- if (error instanceof ModelError) {
- return new Response(JSON.stringify({ error: { message: error.message } }), {
- status: 401,
- })
- }
-
- console.log(error)
- return new Response(JSON.stringify({ error: { message: error.message } }), {
- status: 500,
- })
- }
-}
diff --git a/cloud/app/src/util/zen.ts b/cloud/app/src/util/zen.ts
index d02a9c614..89c91c604 100644
--- a/cloud/app/src/util/zen.ts
+++ b/cloud/app/src/util/zen.ts
@@ -474,6 +474,7 @@ export async function handler(
workspaceID: apiKey.workspaceID,
id: Identifier.create("usage"),
model: MODEL.id,
+ provider: providerName,
inputTokens,
outputTokens,
reasoningTokens,
diff --git a/cloud/core/migrations/0004_first_mockingbird.sql b/cloud/core/migrations/0004_first_mockingbird.sql
new file mode 100644
index 000000000..2a6b11067
--- /dev/null
+++ b/cloud/core/migrations/0004_first_mockingbird.sql
@@ -0,0 +1 @@
+ALTER TABLE `usage` ADD `provider` varchar(255); \ No newline at end of file
diff --git a/cloud/core/migrations/meta/0004_snapshot.json b/cloud/core/migrations/meta/0004_snapshot.json
new file mode 100644
index 000000000..b9b1bfc8c
--- /dev/null
+++ b/cloud/core/migrations/meta/0004_snapshot.json
@@ -0,0 +1,617 @@
+{
+ "version": "5",
+ "dialect": "mysql",
+ "id": "06dc6226-bfbb-4ccc-b4bc-f26070c3bed5",
+ "prevId": "26cebd59-f553-441c-a2b2-2f9578a0ad2b",
+ "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
+ },
+ "reload": {
+ "name": "reload",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "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": false,
+ "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_tokens": {
+ "name": "cache_write_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
+ },
+ "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/cloud/core/migrations/meta/_journal.json b/cloud/core/migrations/meta/_journal.json
index c5ea50210..25fd94a99 100644
--- a/cloud/core/migrations/meta/_journal.json
+++ b/cloud/core/migrations/meta/_journal.json
@@ -29,6 +29,13 @@
"when": 1757600397194,
"tag": "0003_dusty_clint_barton",
"breakpoints": true
+ },
+ {
+ "idx": 4,
+ "version": "5",
+ "when": 1757627357232,
+ "tag": "0004_first_mockingbird",
+ "breakpoints": true
}
]
-}
+} \ No newline at end of file
diff --git a/cloud/core/package.json b/cloud/core/package.json
index d064c957e..fa401cea8 100644
--- a/cloud/core/package.json
+++ b/cloud/core/package.json
@@ -18,6 +18,7 @@
},
"scripts": {
"db": "sst shell drizzle-kit",
+ "db-dev": "sst shell --stage dev -- drizzle-kit",
"db-prod": "sst shell --stage production -- drizzle-kit",
"typecheck": "tsc --noEmit"
},
diff --git a/cloud/core/src/schema/billing.sql.ts b/cloud/core/src/schema/billing.sql.ts
index eff1f6550..0473df142 100644
--- a/cloud/core/src/schema/billing.sql.ts
+++ b/cloud/core/src/schema/billing.sql.ts
@@ -34,6 +34,7 @@ export const UsageTable = mysqlTable(
...workspaceColumns,
...timestamps,
model: varchar("model", { length: 255 }).notNull(),
+ provider: varchar("provider", { length: 255 }),
inputTokens: int("input_tokens").notNull(),
outputTokens: int("output_tokens").notNull(),
reasoningTokens: int("reasoning_tokens"),