summaryrefslogtreecommitdiffhomepage
path: root/cloud/app
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/app
parent79c73267cf83b9d96af50ac60cbda948c26d8a49 (diff)
downloadopencode-a52b352b24db0339fbc178741fe764ad62be0589.tar.gz
opencode-a52b352b24db0339fbc178741fe764ad62be0589.zip
wip: zen
Diffstat (limited to 'cloud/app')
-rw-r--r--cloud/app/src/routes/gateway/v1/chat/completions.ts308
-rw-r--r--cloud/app/src/util/zen.ts1
2 files changed, 1 insertions, 308 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,