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 bd33a1eb..00000000 --- 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 { - 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 d02a9c61..89c91c60 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 00000000..2a6b1106 --- /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 00000000..b9b1bfc8 --- /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 c5ea5021..25fd94a9 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 d064c957..fa401cea 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 eff1f655..0473df14 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"),