Frank 5 kuukautta sitten
vanhempi
sitoutus
4e629c5b64

+ 264 - 535
cloud/app/src/routes/gateway/v1/chat/completions.ts

@@ -1,576 +1,305 @@
 import { Resource } from "@opencode/cloud-resource"
 import { Resource } from "@opencode/cloud-resource"
-import { Billing } from "@opencode/cloud-core/billing.js"
 import type { APIEvent } from "@solidjs/start/server"
 import type { APIEvent } from "@solidjs/start/server"
 import { Database, eq, sql } from "@opencode/cloud-core/drizzle/index.js"
 import { Database, eq, sql } from "@opencode/cloud-core/drizzle/index.js"
-import { BillingTable, PaymentTable } from "@opencode/cloud-core/schema/billing.sql.js"
-import { Identifier } from "@opencode/cloud-core/identifier.js"
-import { centsToMicroCents } from "@opencode/cloud-core/util/price.js"
-import { Actor } from "@opencode/cloud-core/actor.js"
 import { KeyTable } from "@opencode/cloud-core/schema/key.sql.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 SUPPORTED_MODELS = {
+const MODELS = {
   //  "anthropic/claude-sonnet-4": {
   //  "anthropic/claude-sonnet-4": {
-  //    input: 0.0000015,
-  //    output: 0.000006,
-  //    reasoning: 0.0000015,
-  //    cacheRead: 0.0000001,
-  //    cacheWrite: 0.0000001,
-  //    model: () =>
-  //      createAnthropic({
-  //        apiKey: Resource.ANTHROPIC_API_KEY.value,
-  //      })("claude-sonnet-4-20250514"),
-  //  },
-  //  "openai/gpt-4.1": {
-  //    input: 0.0000015,
-  //    output: 0.000006,
-  //    reasoning: 0.0000015,
-  //    cacheRead: 0.0000001,
-  //    cacheWrite: 0.0000001,
-  //    model: () =>
-  //      createOpenAI({
-  //        apiKey: Resource.OPENAI_API_KEY.value,
-  //      })("gpt-4.1"),
-  //  },
-  //  "zhipuai/glm-4.5-flash": {
-  //    input: 0,
-  //    output: 0,
-  //    reasoning: 0,
-  //    cacheRead: 0,
-  //    cacheWrite: 0,
-  //    model: () =>
-  //      createOpenAICompatible({
-  //        name: "Zhipu AI",
-  //        baseURL: "https://api.z.ai/api/paas/v4",
-  //        apiKey: Resource.ZHIPU_API_KEY.value,
-  //      })("glm-4.5-flash"),
+  //    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: {},
+  },
+  "x-ai/grok-code-fast-1": {
+    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",
+    },
+  },
 }
 }
 
 
-export async function POST(input: APIEvent) {
-  // Check auth header
-  const authHeader = input.request.headers.get("authorization")
-  if (!authHeader || !authHeader.startsWith("Bearer "))
-    return Response.json(
-      {
-        error: {
-          message: "Missing API key.",
-          type: "invalid_request_error",
-          param: null,
-          code: "unauthorized",
-        },
-      },
-      { status: 401 },
-    )
-  const apiKey = authHeader.split(" ")[1]
-
-  // Check against KeyTable
-  const keyRecord = await Database.use((tx) =>
-    tx
-      .select({
-        id: KeyTable.id,
-        workspaceID: KeyTable.workspaceID,
-      })
-      .from(KeyTable)
-      .where(eq(KeyTable.key, apiKey))
-      .then((rows) => rows[0]),
-  )
-
-  if (!keyRecord)
-    return Response.json(
-      {
-        error: {
-          message: "Invalid API key.",
-          type: "invalid_request_error",
-          param: null,
-          code: "unauthorized",
-        },
-      },
-      { status: 401 },
-    )
-
-  /*
-  return await Actor.provide("system", { workspaceID: keyRecord.workspaceID }, async () => {
-    try {
-      // Check balance
-      const customer = await Billing.get()
-      if (customer.balance <= 0) {
-        return Response.json(
-          {
-            error: {
-              message: "Insufficient balance",
-              type: "insufficient_quota",
-              param: null,
-              code: "insufficient_quota",
-            },
-          },
-          { status: 401 },
-        )
-      }
-
-      const body = await input.request.json<ChatCompletionCreateParamsBase>()
-      const model = SUPPORTED_MODELS[body.model as keyof typeof SUPPORTED_MODELS]?.model()
-      if (!model) throw new Error(`Unsupported model: ${body.model}`)
-
-      const requestBody = transformOpenAIRequestToAiSDK()
-
-      return body.stream ? await handleStream() : await handleGenerate()
-
-      async function handleStream() {
-        const result = await model.doStream({
-          ...requestBody,
-        })
-
-        const encoder = new TextEncoder()
-        const stream = new ReadableStream({
-          async start(controller) {
-            const id = `chatcmpl-${Date.now()}`
-            const created = Math.floor(Date.now() / 1000)
-
-            try {
-              for await (const chunk of result.stream) {
-                console.log("!!! CHUNK !!! : " + chunk.type)
-                switch (chunk.type) {
-                  case "text-delta": {
-                    const data = {
-                      id,
-                      object: "chat.completion.chunk",
-                      created,
-                      model: body.model,
-                      choices: [
-                        {
-                          index: 0,
-                          delta: {
-                            content: chunk.delta,
-                          },
-                          finish_reason: null,
-                        },
-                      ],
-                    }
-                    controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
-                    break
-                  }
-
-                  case "reasoning-delta": {
-                    const data = {
-                      id,
-                      object: "chat.completion.chunk",
-                      created,
-                      model: body.model,
-                      choices: [
-                        {
-                          index: 0,
-                          delta: {
-                            reasoning_content: chunk.delta,
-                          },
-                          finish_reason: null,
-                        },
-                      ],
-                    }
-                    controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
-                    break
-                  }
-
-                  case "tool-call": {
-                    const data = {
-                      id,
-                      object: "chat.completion.chunk",
-                      created,
-                      model: body.model,
-                      choices: [
-                        {
-                          index: 0,
-                          delta: {
-                            tool_calls: [
-                              {
-                                index: 0,
-                                id: chunk.toolCallId,
-                                type: "function",
-                                function: {
-                                  name: chunk.toolName,
-                                  arguments: chunk.input,
-                                },
-                              },
-                            ],
-                          },
-                          finish_reason: null,
-                        },
-                      ],
-                    }
-                    controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
-                    break
-                  }
-
-                  case "error": {
-                    const data = {
-                      id,
-                      object: "chat.completion.chunk",
-                      created,
-                      model: body.model,
-                      choices: [
-                        {
-                          index: 0,
-                          delta: {},
-                          finish_reason: "stop",
-                        },
-                      ],
-                      error: {
-                        message: typeof chunk.error === "string" ? chunk.error : chunk.error,
-                        type: "server_error",
-                      },
-                    }
-                    controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
-                    controller.enqueue(encoder.encode("data: [DONE]\n\n"))
-                    controller.close()
-                    break
-                  }
+class AuthError extends Error {}
+class CreditsError extends Error {}
+class ModelError extends Error {}
 
 
-                  case "finish": {
-                    const data = {
-                      id,
-                      object: "chat.completion.chunk",
-                      created,
-                      model: body.model,
-                      choices: [
-                        {
-                          index: 0,
-                          delta: {},
-                          finish_reason:
-                            {
-                              stop: "stop",
-                              length: "length",
-                              "content-filter": "content_filter",
-                              "tool-calls": "tool_calls",
-                              error: "stop",
-                              other: "stop",
-                              unknown: "stop",
-                            }[chunk.finishReason] || "stop",
-                        },
-                      ],
-                      usage: {
-                        prompt_tokens: chunk.usage.inputTokens,
-                        completion_tokens: chunk.usage.outputTokens,
-                        total_tokens: chunk.usage.totalTokens,
-                        completion_tokens_details: {
-                          reasoning_tokens: chunk.usage.reasoningTokens,
-                        },
-                        prompt_tokens_details: {
-                          cached_tokens: chunk.usage.cachedInputTokens,
-                        },
-                      },
-                    }
-                    await trackUsage(body.model, chunk.usage, chunk.providerMetadata)
-                    controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
-                    controller.enqueue(encoder.encode("data: [DONE]\n\n"))
-                    controller.close()
-                    break
-                  }
-
-                  //case "stream-start":
-                  //case "response-metadata":
-                  case "text-start":
-                  case "text-end":
-                  case "reasoning-start":
-                  case "reasoning-end":
-                  case "tool-input-start":
-                  case "tool-input-delta":
-                  case "tool-input-end":
-                  case "raw":
-                  default:
-                    // Log unknown chunk types for debugging
-                    console.warn(`Unknown chunk type: ${(chunk as any).type}`)
-                    break
-                }
-              }
-            } catch (error) {
-              controller.error(error)
-            }
-          },
-        })
-
-        return new Response(stream, {
-          headers: {
-            "Content-Type": "text/plain; charset=utf-8",
-            "Cache-Control": "no-cache",
-            Connection: "keep-alive",
-          },
-        })
-      }
-
-      async function handleGenerate() {
-        const response = await model.doGenerate({
-          ...requestBody,
-        })
-        await trackUsage(body.model, response.usage, response.providerMetadata)
-        return c.json({
-          id: `chatcmpl-${Date.now()}`,
-          object: "chat.completion" as const,
-          created: Math.floor(Date.now() / 1000),
-          model: body.model,
-          choices: [
-            {
-              index: 0,
-              message: {
-                role: "assistant" as const,
-                content: response.content?.find((c) => c.type === "text")?.text ?? "",
-                reasoning_content: response.content?.find((c) => c.type === "reasoning")?.text,
-                tool_calls: response.content
-                  ?.filter((c) => c.type === "tool-call")
-                  .map((toolCall) => ({
-                    id: toolCall.toolCallId,
-                    type: "function" as const,
-                    function: {
-                      name: toolCall.toolName,
-                      arguments: toolCall.input,
-                    },
-                  })),
-              },
-              finish_reason:
-                (
-                  {
-                    stop: "stop",
-                    length: "length",
-                    "content-filter": "content_filter",
-                    "tool-calls": "tool_calls",
-                    error: "stop",
-                    other: "stop",
-                    unknown: "stop",
-                  } as const
-                )[response.finishReason] || "stop",
-            },
-          ],
-          usage: {
-            prompt_tokens: response.usage?.inputTokens,
-            completion_tokens: response.usage?.outputTokens,
-            total_tokens: response.usage?.totalTokens,
-            completion_tokens_details: {
-              reasoning_tokens: response.usage?.reasoningTokens,
-            },
-            prompt_tokens_details: {
-              cached_tokens: response.usage?.cachedInputTokens,
-            },
-          },
+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)
       }
       }
+    }
 
 
-      function transformOpenAIRequestToAiSDK() {
-        const prompt = transformMessages()
-        const tools = transformTools()
+    // 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,
+      })
+    }
 
 
-        return {
-          prompt,
-          maxOutputTokens: body.max_tokens ?? body.max_completion_tokens ?? undefined,
-          temperature: body.temperature ?? undefined,
-          topP: body.top_p ?? undefined,
-          frequencyPenalty: body.frequency_penalty ?? undefined,
-          presencePenalty: body.presence_penalty ?? undefined,
-          providerOptions: body.reasoning_effort
-            ? {
-                anthropic: {
-                  reasoningEffort: body.reasoning_effort,
-                },
+    // 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
               }
               }
-            : undefined,
-          stopSequences: (typeof body.stop === "string" ? [body.stop] : body.stop) ?? undefined,
-          responseFormat: (() => {
-            if (!body.response_format) return { type: "text" as const }
-            if (body.response_format.type === "json_schema")
-              return {
-                type: "json" as const,
-                schema: body.response_format.json_schema.schema,
-                name: body.response_format.json_schema.name,
-                description: body.response_format.json_schema.description,
-              }
-            if (body.response_format.type === "json_object") return { type: "json" as const }
-            throw new Error("Unsupported response format")
-          })(),
-          seed: body.seed ?? undefined,
-          tools: tools.tools,
-          toolChoice: tools.toolChoice,
-        }
-
-        function transformTools() {
-          const { tools, tool_choice } = body
 
 
-          if (!tools || tools.length === 0) {
-            return { tools: undefined, toolChoice: undefined }
-          }
+              buffer += decoder.decode(value, { stream: true })
 
 
-          const aiSdkTools = tools.map((tool) => {
-            return {
-              type: tool.type,
-              name: tool.function.name,
-              description: tool.function.description,
-              inputSchema: tool.function.parameters!,
-            }
-          })
-
-          let aiSdkToolChoice
-          if (tool_choice == null) {
-            aiSdkToolChoice = undefined
-          } else if (tool_choice === "auto") {
-            aiSdkToolChoice = { type: "auto" as const }
-          } else if (tool_choice === "none") {
-            aiSdkToolChoice = { type: "none" as const }
-          } else if (tool_choice === "required") {
-            aiSdkToolChoice = { type: "required" as const }
-          } else if (tool_choice.type === "function") {
-            aiSdkToolChoice = {
-              type: "tool" as const,
-              toolName: tool_choice.function.name,
-            }
-          }
-
-          return { tools: aiSdkTools, toolChoice: aiSdkToolChoice }
-        }
-
-        function transformMessages() {
-          const { messages } = body
-          const prompt: LanguageModelV2Prompt = []
+              const parts = buffer.split("\n\n")
+              buffer = parts.pop() ?? ""
 
 
-          for (const message of messages) {
-            switch (message.role) {
-              case "system": {
-                prompt.push({
-                  role: "system",
-                  content: message.content as string,
+              const usage = parts
+                .map((part) => part.trim())
+                .filter((part) => part.startsWith("data: "))
+                .map((part) => {
+                  try {
+                    return JSON.parse(part.slice(6))
+                  } catch (e) {
+                    return {}
+                  }
                 })
                 })
-                break
-              }
+                .find((part) => part.usage)
+              if (usage) await trackUsage(usage)
 
 
-              case "user": {
-                if (typeof message.content === "string") {
-                  prompt.push({
-                    role: "user",
-                    content: [{ type: "text", text: message.content }],
-                  })
-                } else {
-                  const content = message.content.map((part) => {
-                    switch (part.type) {
-                      case "text":
-                        return { type: "text" as const, text: part.text }
-                      case "image_url":
-                        return {
-                          type: "file" as const,
-                          mediaType: "image/jpeg" as const,
-                          data: part.image_url.url,
-                        }
-                      default:
-                        throw new Error(`Unsupported content part type: ${(part as any).type}`)
-                    }
-                  })
-                  prompt.push({
-                    role: "user",
-                    content,
-                  })
-                }
-                break
-              }
+              c.enqueue(value)
 
 
-              case "assistant": {
-                const content: Array<
-                  | { type: "text"; text: string }
-                  | {
-                      type: "tool-call"
-                      toolCallId: string
-                      toolName: string
-                      input: any
-                    }
-                > = []
+              return pump()
+            }) || Promise.resolve()
+          )
+        }
 
 
-                if (message.content) {
-                  content.push({
-                    type: "text",
-                    text: message.content as string,
-                  })
-                }
+        return pump()
+      },
+    })
 
 
-                if (message.tool_calls) {
-                  for (const toolCall of message.tool_calls) {
-                    content.push({
-                      type: "tool-call",
-                      toolCallId: toolCall.id,
-                      toolName: toolCall.function.name,
-                      input: JSON.parse(toolCall.function.arguments),
-                    })
-                  }
-                }
+    return new Response(stream, {
+      status: res.status,
+      statusText: res.statusText,
+      headers: resHeaders,
+    })
 
 
-                prompt.push({
-                  role: "assistant",
-                  content,
-                })
-                break
-              }
+    function validateModel() {
+      if (!(body.model in MODELS)) {
+        throw new ModelError(`Model ${body.model} not supported`)
+      }
+      return MODELS[body.model as keyof typeof MODELS]
+    }
 
 
-              case "tool": {
-                prompt.push({
-                  role: "tool",
-                  content: [
-                    {
-                      type: "tool-result",
-                      toolName: "placeholder",
-                      toolCallId: message.tool_call_id,
-                      output: {
-                        type: "text",
-                        value: message.content as string,
-                      },
-                    },
-                  ],
-                })
-                break
-              }
+    async function authenticate() {
+      try {
+        const authHeader = input.request.headers.get("authorization")
+        if (!authHeader || !authHeader.startsWith("Bearer ")) throw new AuthError("Missing API key.")
 
 
-              default: {
-                throw new Error(`Unsupported message role: ${message.role}`)
-              }
-            }
-          }
+        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]),
+        )
 
 
-          return prompt
-        }
+        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 trackUsage(model: string, usage: LanguageModelUsage, providerMetadata?: ProviderMetadata) {
-        const modelData = SUPPORTED_MODELS[model as keyof typeof SUPPORTED_MODELS]
-        if (!modelData) throw new Error(`Unsupported model: ${model}`)
+    async function checkCredits() {
+      if (!apiKey || !MODEL.auth) return
 
 
-        const inputTokens = usage.inputTokens ?? 0
-        const outputTokens = usage.outputTokens ?? 0
-        const reasoningTokens = usage.reasoningTokens ?? 0
-        const cacheReadTokens = usage.cachedInputTokens ?? 0
-        const cacheWriteTokens =
-          providerMetadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
-          // @ts-expect-error
-          providerMetadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ??
-          0
+      const billing = await Database.use((tx) =>
+        tx
+          .select({
+            balance: BillingTable.balance,
+          })
+          .from(BillingTable)
+          .where(eq(BillingTable.workspaceID, apiKey.workspaceID))
+          .then((rows) => rows[0]),
+      )
 
 
-        const inputCost = modelData.input * inputTokens
-        const outputCost = modelData.output * outputTokens
-        const reasoningCost = modelData.reasoning * reasoningTokens
-        const cacheReadCost = modelData.cacheRead * cacheReadTokens
-        const cacheWriteCost = modelData.cacheWrite * cacheWriteTokens
-        const costInCents = (inputCost + outputCost + reasoningCost + cacheReadCost + cacheWriteCost) * 100
+      if (billing.balance <= 0) throw new CreditsError("Insufficient balance")
+    }
 
 
-        await Billing.consume({
-          model,
+    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,
           inputTokens,
           outputTokens,
           outputTokens,
           reasoningTokens,
           reasoningTokens,
           cacheReadTokens,
           cacheReadTokens,
           cacheWriteTokens,
           cacheWriteTokens,
-          costInCents,
+          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, keyRecord.id)),
-        )
-      }
-    } catch (error: any) {
-      return Response.json({ error: { message: error.message } }, { status: 500 })
+      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,
+    })
+  }
 }
 }

+ 5 - 2
cloud/app/src/routes/stripe/webhook.ts

@@ -24,8 +24,11 @@ export async function POST(input: APIEvent) {
     if (!workspaceID) throw new Error("Workspace ID not found")
     if (!workspaceID) throw new Error("Workspace ID not found")
     if (!customerID) throw new Error("Customer ID not found")
     if (!customerID) throw new Error("Customer ID not found")
     if (!amount) throw new Error("Amount not found")
     if (!amount) throw new Error("Amount not found")
+    if (amount !== 2118) throw new Error("Amount mismatch")
     if (!paymentID) throw new Error("Payment ID not found")
     if (!paymentID) throw new Error("Payment ID not found")
 
 
+    const chargedAmount = 2000
+
     await Actor.provide("system", { workspaceID }, async () => {
     await Actor.provide("system", { workspaceID }, async () => {
       const customer = await Billing.get()
       const customer = await Billing.get()
       if (customer?.customerID && customer.customerID !== customerID) throw new Error("Customer ID mismatch")
       if (customer?.customerID && customer.customerID !== customerID) throw new Error("Customer ID mismatch")
@@ -50,7 +53,7 @@ export async function POST(input: APIEvent) {
         await tx
         await tx
           .update(BillingTable)
           .update(BillingTable)
           .set({
           .set({
-            balance: sql`${BillingTable.balance} + ${centsToMicroCents(amount)}`,
+            balance: sql`${BillingTable.balance} + ${centsToMicroCents(chargedAmount)}`,
             customerID,
             customerID,
             paymentMethodID: paymentMethod.id,
             paymentMethodID: paymentMethod.id,
             paymentMethodLast4: paymentMethod.card!.last4,
             paymentMethodLast4: paymentMethod.card!.last4,
@@ -59,7 +62,7 @@ export async function POST(input: APIEvent) {
         await tx.insert(PaymentTable).values({
         await tx.insert(PaymentTable).values({
           workspaceID,
           workspaceID,
           id: Identifier.create("payment"),
           id: Identifier.create("payment"),
-          amount: centsToMicroCents(amount),
+          amount: centsToMicroCents(chargedAmount),
           paymentID,
           paymentID,
           customerID,
           customerID,
         })
         })

+ 1 - 38
cloud/core/src/billing.ts

@@ -52,43 +52,6 @@ export namespace Billing {
     )
     )
   }
   }
 
 
-  export const consume = fn(
-    z.object({
-      requestID: z.string().optional(),
-      model: z.string(),
-      inputTokens: z.number(),
-      outputTokens: z.number(),
-      reasoningTokens: z.number().optional(),
-      cacheReadTokens: z.number().optional(),
-      cacheWriteTokens: z.number().optional(),
-      costInCents: z.number(),
-    }),
-    async (input) => {
-      const workspaceID = Actor.workspace()
-      const cost = centsToMicroCents(input.costInCents)
-
-      return await Database.transaction(async (tx) => {
-        await tx.insert(UsageTable).values({
-          workspaceID,
-          id: Identifier.create("usage"),
-          model: input.model,
-          inputTokens: input.inputTokens,
-          outputTokens: input.outputTokens,
-          reasoningTokens: input.reasoningTokens,
-          cacheReadTokens: input.cacheReadTokens,
-          cacheWriteTokens: input.cacheWriteTokens,
-          cost,
-        })
-        await tx
-          .update(BillingTable)
-          .set({
-            balance: sql`${BillingTable.balance} - ${cost}`,
-          })
-          .where(eq(BillingTable.workspaceID, workspaceID))
-      })
-    },
-  )
-
   export const generateCheckoutUrl = fn(
   export const generateCheckoutUrl = fn(
     z.object({
     z.object({
       successUrl: z.string(),
       successUrl: z.string(),
@@ -109,7 +72,7 @@ export namespace Billing {
               product_data: {
               product_data: {
                 name: "opencode credits",
                 name: "opencode credits",
               },
               },
-              unit_amount: 2000, // $20 minimum
+              unit_amount: 2118, // $20 minimum + Stripe fee 4.4% + $0.30
             },
             },
             quantity: 1,
             quantity: 1,
           },
           },

+ 1 - 1
cloud/core/src/workspace.ts

@@ -26,7 +26,7 @@ export namespace Workspace {
       await tx.insert(BillingTable).values({
       await tx.insert(BillingTable).values({
         workspaceID,
         workspaceID,
         id: Identifier.create("billing"),
         id: Identifier.create("billing"),
-        balance: centsToMicroCents(100),
+        balance: 0,
       })
       })
     })
     })
     await Actor.provide(
     await Actor.provide(

+ 0 - 596
cloud/function/src/gateway.ts

@@ -1,596 +0,0 @@
-import { Hono, MiddlewareHandler } from "hono"
-import { type ProviderMetadata, type LanguageModelUsage } from "ai"
-import { createAnthropic } from "@ai-sdk/anthropic"
-import { createOpenAI } from "@ai-sdk/openai"
-import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
-import type { LanguageModelV2Prompt } from "@ai-sdk/provider"
-import { type ChatCompletionCreateParamsBase } from "openai/resources/chat/completions"
-import { Actor } from "@opencode/cloud-core/actor.js"
-import { Database, eq, sql } from "@opencode/cloud-core/drizzle/index.js"
-import { KeyTable } from "@opencode/cloud-core/schema/key.sql.js"
-import { Billing } from "@opencode/cloud-core/billing.js"
-import { Resource } from "@opencode/cloud-resource"
-
-type Env = {}
-
-const SUPPORTED_MODELS = {
-  "anthropic/claude-sonnet-4": {
-    input: 0.0000015,
-    output: 0.000006,
-    reasoning: 0.0000015,
-    cacheRead: 0.0000001,
-    cacheWrite: 0.0000001,
-    model: () =>
-      createAnthropic({
-        apiKey: Resource.ANTHROPIC_API_KEY.value,
-      })("claude-sonnet-4-20250514"),
-  },
-  "openai/gpt-4.1": {
-    input: 0.0000015,
-    output: 0.000006,
-    reasoning: 0.0000015,
-    cacheRead: 0.0000001,
-    cacheWrite: 0.0000001,
-    model: () =>
-      createOpenAI({
-        apiKey: Resource.OPENAI_API_KEY.value,
-      })("gpt-4.1"),
-  },
-  "zhipuai/glm-4.5-flash": {
-    input: 0,
-    output: 0,
-    reasoning: 0,
-    cacheRead: 0,
-    cacheWrite: 0,
-    model: () =>
-      createOpenAICompatible({
-        name: "Zhipu AI",
-        baseURL: "https://api.z.ai/api/paas/v4",
-        apiKey: Resource.ZHIPU_API_KEY.value,
-      })("glm-4.5-flash"),
-  },
-}
-
-const GatewayAuth: MiddlewareHandler = async (c, next) => {
-  const authHeader = c.req.header("authorization")
-
-  if (!authHeader || !authHeader.startsWith("Bearer ")) {
-    return c.json(
-      {
-        error: {
-          message: "Missing API key.",
-          type: "invalid_request_error",
-          param: null,
-          code: "unauthorized",
-        },
-      },
-      401,
-    )
-  }
-
-  const apiKey = authHeader.split(" ")[1]
-
-  // Check against KeyTable
-  const keyRecord = await Database.use((tx) =>
-    tx
-      .select({
-        id: KeyTable.id,
-        workspaceID: KeyTable.workspaceID,
-      })
-      .from(KeyTable)
-      .where(eq(KeyTable.key, apiKey))
-      .then((rows) => rows[0]),
-  )
-
-  if (!keyRecord) {
-    return c.json(
-      {
-        error: {
-          message: "Invalid API key.",
-          type: "invalid_request_error",
-          param: null,
-          code: "unauthorized",
-        },
-      },
-      401,
-    )
-  }
-
-  c.set("keyRecord", keyRecord)
-  await next()
-}
-
-const app = new Hono<{ Bindings: Env; Variables: { keyRecord?: { id: string; workspaceID: string } } }>()
-  .get("/", (c) => c.text("Hello, world!"))
-  .post("/v1/chat/completions", GatewayAuth, async (c) => {
-    const keyRecord = c.get("keyRecord")!
-
-    return await Actor.provide("system", { workspaceID: keyRecord.workspaceID }, async () => {
-      try {
-        // Check balance
-        const customer = await Billing.get()
-        if (customer.balance <= 0) {
-          return c.json(
-            {
-              error: {
-                message: "Insufficient balance",
-                type: "insufficient_quota",
-                param: null,
-                code: "insufficient_quota",
-              },
-            },
-            401,
-          )
-        }
-
-        const body = await c.req.json<ChatCompletionCreateParamsBase>()
-        const model = SUPPORTED_MODELS[body.model as keyof typeof SUPPORTED_MODELS]?.model()
-        if (!model) throw new Error(`Unsupported model: ${body.model}`)
-
-        const requestBody = transformOpenAIRequestToAiSDK()
-
-        return body.stream ? await handleStream() : await handleGenerate()
-
-        async function handleStream() {
-          const result = await model.doStream({
-            ...requestBody,
-          })
-
-          const encoder = new TextEncoder()
-          const stream = new ReadableStream({
-            async start(controller) {
-              const id = `chatcmpl-${Date.now()}`
-              const created = Math.floor(Date.now() / 1000)
-
-              try {
-                for await (const chunk of result.stream) {
-                  console.log("!!! CHUNK !!! : " + chunk.type)
-                  switch (chunk.type) {
-                    case "text-delta": {
-                      const data = {
-                        id,
-                        object: "chat.completion.chunk",
-                        created,
-                        model: body.model,
-                        choices: [
-                          {
-                            index: 0,
-                            delta: {
-                              content: chunk.delta,
-                            },
-                            finish_reason: null,
-                          },
-                        ],
-                      }
-                      controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
-                      break
-                    }
-
-                    case "reasoning-delta": {
-                      const data = {
-                        id,
-                        object: "chat.completion.chunk",
-                        created,
-                        model: body.model,
-                        choices: [
-                          {
-                            index: 0,
-                            delta: {
-                              reasoning_content: chunk.delta,
-                            },
-                            finish_reason: null,
-                          },
-                        ],
-                      }
-                      controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
-                      break
-                    }
-
-                    case "tool-call": {
-                      const data = {
-                        id,
-                        object: "chat.completion.chunk",
-                        created,
-                        model: body.model,
-                        choices: [
-                          {
-                            index: 0,
-                            delta: {
-                              tool_calls: [
-                                {
-                                  index: 0,
-                                  id: chunk.toolCallId,
-                                  type: "function",
-                                  function: {
-                                    name: chunk.toolName,
-                                    arguments: chunk.input,
-                                  },
-                                },
-                              ],
-                            },
-                            finish_reason: null,
-                          },
-                        ],
-                      }
-                      controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
-                      break
-                    }
-
-                    case "error": {
-                      const data = {
-                        id,
-                        object: "chat.completion.chunk",
-                        created,
-                        model: body.model,
-                        choices: [
-                          {
-                            index: 0,
-                            delta: {},
-                            finish_reason: "stop",
-                          },
-                        ],
-                        error: {
-                          message: typeof chunk.error === "string" ? chunk.error : chunk.error,
-                          type: "server_error",
-                        },
-                      }
-                      controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
-                      controller.enqueue(encoder.encode("data: [DONE]\n\n"))
-                      controller.close()
-                      break
-                    }
-
-                    case "finish": {
-                      const data = {
-                        id,
-                        object: "chat.completion.chunk",
-                        created,
-                        model: body.model,
-                        choices: [
-                          {
-                            index: 0,
-                            delta: {},
-                            finish_reason:
-                              {
-                                stop: "stop",
-                                length: "length",
-                                "content-filter": "content_filter",
-                                "tool-calls": "tool_calls",
-                                error: "stop",
-                                other: "stop",
-                                unknown: "stop",
-                              }[chunk.finishReason] || "stop",
-                          },
-                        ],
-                        usage: {
-                          prompt_tokens: chunk.usage.inputTokens,
-                          completion_tokens: chunk.usage.outputTokens,
-                          total_tokens: chunk.usage.totalTokens,
-                          completion_tokens_details: {
-                            reasoning_tokens: chunk.usage.reasoningTokens,
-                          },
-                          prompt_tokens_details: {
-                            cached_tokens: chunk.usage.cachedInputTokens,
-                          },
-                        },
-                      }
-                      await trackUsage(body.model, chunk.usage, chunk.providerMetadata)
-                      controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
-                      controller.enqueue(encoder.encode("data: [DONE]\n\n"))
-                      controller.close()
-                      break
-                    }
-
-                    //case "stream-start":
-                    //case "response-metadata":
-                    case "text-start":
-                    case "text-end":
-                    case "reasoning-start":
-                    case "reasoning-end":
-                    case "tool-input-start":
-                    case "tool-input-delta":
-                    case "tool-input-end":
-                    case "raw":
-                    default:
-                      // Log unknown chunk types for debugging
-                      console.warn(`Unknown chunk type: ${(chunk as any).type}`)
-                      break
-                  }
-                }
-              } catch (error) {
-                controller.error(error)
-              }
-            },
-          })
-
-          return new Response(stream, {
-            headers: {
-              "Content-Type": "text/plain; charset=utf-8",
-              "Cache-Control": "no-cache",
-              Connection: "keep-alive",
-            },
-          })
-        }
-
-        async function handleGenerate() {
-          const response = await model.doGenerate({
-            ...requestBody,
-          })
-          await trackUsage(body.model, response.usage, response.providerMetadata)
-          return c.json({
-            id: `chatcmpl-${Date.now()}`,
-            object: "chat.completion" as const,
-            created: Math.floor(Date.now() / 1000),
-            model: body.model,
-            choices: [
-              {
-                index: 0,
-                message: {
-                  role: "assistant" as const,
-                  content: response.content?.find((c) => c.type === "text")?.text ?? "",
-                  reasoning_content: response.content?.find((c) => c.type === "reasoning")?.text,
-                  tool_calls: response.content
-                    ?.filter((c) => c.type === "tool-call")
-                    .map((toolCall) => ({
-                      id: toolCall.toolCallId,
-                      type: "function" as const,
-                      function: {
-                        name: toolCall.toolName,
-                        arguments: toolCall.input,
-                      },
-                    })),
-                },
-                finish_reason:
-                  (
-                    {
-                      stop: "stop",
-                      length: "length",
-                      "content-filter": "content_filter",
-                      "tool-calls": "tool_calls",
-                      error: "stop",
-                      other: "stop",
-                      unknown: "stop",
-                    } as const
-                  )[response.finishReason] || "stop",
-              },
-            ],
-            usage: {
-              prompt_tokens: response.usage?.inputTokens,
-              completion_tokens: response.usage?.outputTokens,
-              total_tokens: response.usage?.totalTokens,
-              completion_tokens_details: {
-                reasoning_tokens: response.usage?.reasoningTokens,
-              },
-              prompt_tokens_details: {
-                cached_tokens: response.usage?.cachedInputTokens,
-              },
-            },
-          })
-        }
-
-        function transformOpenAIRequestToAiSDK() {
-          const prompt = transformMessages()
-          const tools = transformTools()
-
-          return {
-            prompt,
-            maxOutputTokens: body.max_tokens ?? body.max_completion_tokens ?? undefined,
-            temperature: body.temperature ?? undefined,
-            topP: body.top_p ?? undefined,
-            frequencyPenalty: body.frequency_penalty ?? undefined,
-            presencePenalty: body.presence_penalty ?? undefined,
-            providerOptions: body.reasoning_effort
-              ? {
-                  anthropic: {
-                    reasoningEffort: body.reasoning_effort,
-                  },
-                }
-              : undefined,
-            stopSequences: (typeof body.stop === "string" ? [body.stop] : body.stop) ?? undefined,
-            responseFormat: (() => {
-              if (!body.response_format) return { type: "text" as const }
-              if (body.response_format.type === "json_schema")
-                return {
-                  type: "json" as const,
-                  schema: body.response_format.json_schema.schema,
-                  name: body.response_format.json_schema.name,
-                  description: body.response_format.json_schema.description,
-                }
-              if (body.response_format.type === "json_object") return { type: "json" as const }
-              throw new Error("Unsupported response format")
-            })(),
-            seed: body.seed ?? undefined,
-            tools: tools.tools,
-            toolChoice: tools.toolChoice,
-          }
-
-          function transformTools() {
-            const { tools, tool_choice } = body
-
-            if (!tools || tools.length === 0) {
-              return { tools: undefined, toolChoice: undefined }
-            }
-
-            const aiSdkTools = tools.map((tool) => {
-              return {
-                type: tool.type,
-                name: tool.function.name,
-                description: tool.function.description,
-                inputSchema: tool.function.parameters!,
-              }
-            })
-
-            let aiSdkToolChoice
-            if (tool_choice == null) {
-              aiSdkToolChoice = undefined
-            } else if (tool_choice === "auto") {
-              aiSdkToolChoice = { type: "auto" as const }
-            } else if (tool_choice === "none") {
-              aiSdkToolChoice = { type: "none" as const }
-            } else if (tool_choice === "required") {
-              aiSdkToolChoice = { type: "required" as const }
-            } else if (tool_choice.type === "function") {
-              aiSdkToolChoice = {
-                type: "tool" as const,
-                toolName: tool_choice.function.name,
-              }
-            }
-
-            return { tools: aiSdkTools, toolChoice: aiSdkToolChoice }
-          }
-
-          function transformMessages() {
-            const { messages } = body
-            const prompt: LanguageModelV2Prompt = []
-
-            for (const message of messages) {
-              switch (message.role) {
-                case "system": {
-                  prompt.push({
-                    role: "system",
-                    content: message.content as string,
-                  })
-                  break
-                }
-
-                case "user": {
-                  if (typeof message.content === "string") {
-                    prompt.push({
-                      role: "user",
-                      content: [{ type: "text", text: message.content }],
-                    })
-                  } else {
-                    const content = message.content.map((part) => {
-                      switch (part.type) {
-                        case "text":
-                          return { type: "text" as const, text: part.text }
-                        case "image_url":
-                          return {
-                            type: "file" as const,
-                            mediaType: "image/jpeg" as const,
-                            data: part.image_url.url,
-                          }
-                        default:
-                          throw new Error(`Unsupported content part type: ${(part as any).type}`)
-                      }
-                    })
-                    prompt.push({
-                      role: "user",
-                      content,
-                    })
-                  }
-                  break
-                }
-
-                case "assistant": {
-                  const content: Array<
-                    | { type: "text"; text: string }
-                    | {
-                        type: "tool-call"
-                        toolCallId: string
-                        toolName: string
-                        input: any
-                      }
-                  > = []
-
-                  if (message.content) {
-                    content.push({
-                      type: "text",
-                      text: message.content as string,
-                    })
-                  }
-
-                  if (message.tool_calls) {
-                    for (const toolCall of message.tool_calls) {
-                      content.push({
-                        type: "tool-call",
-                        toolCallId: toolCall.id,
-                        toolName: toolCall.function.name,
-                        input: JSON.parse(toolCall.function.arguments),
-                      })
-                    }
-                  }
-
-                  prompt.push({
-                    role: "assistant",
-                    content,
-                  })
-                  break
-                }
-
-                case "tool": {
-                  prompt.push({
-                    role: "tool",
-                    content: [
-                      {
-                        type: "tool-result",
-                        toolName: "placeholder",
-                        toolCallId: message.tool_call_id,
-                        output: {
-                          type: "text",
-                          value: message.content as string,
-                        },
-                      },
-                    ],
-                  })
-                  break
-                }
-
-                default: {
-                  throw new Error(`Unsupported message role: ${message.role}`)
-                }
-              }
-            }
-
-            return prompt
-          }
-        }
-
-        async function trackUsage(model: string, usage: LanguageModelUsage, providerMetadata?: ProviderMetadata) {
-          const modelData = SUPPORTED_MODELS[model as keyof typeof SUPPORTED_MODELS]
-          if (!modelData) throw new Error(`Unsupported model: ${model}`)
-
-          const inputTokens = usage.inputTokens ?? 0
-          const outputTokens = usage.outputTokens ?? 0
-          const reasoningTokens = usage.reasoningTokens ?? 0
-          const cacheReadTokens = usage.cachedInputTokens ?? 0
-          const cacheWriteTokens =
-            providerMetadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
-            // @ts-expect-error
-            providerMetadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ??
-            0
-
-          const inputCost = modelData.input * inputTokens
-          const outputCost = modelData.output * outputTokens
-          const reasoningCost = modelData.reasoning * reasoningTokens
-          const cacheReadCost = modelData.cacheRead * cacheReadTokens
-          const cacheWriteCost = modelData.cacheWrite * cacheWriteTokens
-          const costInCents = (inputCost + outputCost + reasoningCost + cacheReadCost + cacheWriteCost) * 100
-
-          await Billing.consume({
-            model,
-            inputTokens,
-            outputTokens,
-            reasoningTokens,
-            cacheReadTokens,
-            cacheWriteTokens,
-            costInCents,
-          })
-
-          await Database.use((tx) =>
-            tx
-              .update(KeyTable)
-              .set({ timeUsed: sql`now()` })
-              .where(eq(KeyTable.id, keyRecord.id)),
-          )
-        }
-      } catch (error: any) {
-        return c.json({ error: { message: error.message } }, 500)
-      }
-    })
-  })
-  .all("*", (c) => c.text("Not Found"))
-
-export type ApiType = typeof app
-
-export default app

+ 5 - 6
cloud/function/sst-env.d.ts

@@ -14,6 +14,10 @@ declare module "sst" {
       "type": "sst.sst.Linkable"
       "type": "sst.sst.Linkable"
       "value": string
       "value": string
     }
     }
+    "BASETEN_API_KEY": {
+      "type": "sst.sst.Secret"
+      "value": string
+    }
     "Console": {
     "Console": {
       "type": "sst.cloudflare.SolidStart"
       "type": "sst.cloudflare.SolidStart"
       "url": string
       "url": string
@@ -46,10 +50,6 @@ declare module "sst" {
       "type": "sst.sst.Secret"
       "type": "sst.sst.Secret"
       "value": string
       "value": string
     }
     }
-    "OPENAI_API_KEY": {
-      "type": "sst.sst.Secret"
-      "value": string
-    }
     "STRIPE_SECRET_KEY": {
     "STRIPE_SECRET_KEY": {
       "type": "sst.sst.Secret"
       "type": "sst.sst.Secret"
       "value": string
       "value": string
@@ -62,7 +62,7 @@ declare module "sst" {
       "type": "sst.cloudflare.Astro"
       "type": "sst.cloudflare.Astro"
       "url": string
       "url": string
     }
     }
-    "ZHIPU_API_KEY": {
+    "XAI_API_KEY": {
       "type": "sst.sst.Secret"
       "type": "sst.sst.Secret"
       "value": string
       "value": string
     }
     }
@@ -76,7 +76,6 @@ declare module "sst" {
     "AuthApi": cloudflare.Service
     "AuthApi": cloudflare.Service
     "AuthStorage": cloudflare.KVNamespace
     "AuthStorage": cloudflare.KVNamespace
     "Bucket": cloudflare.R2Bucket
     "Bucket": cloudflare.R2Bucket
-    "GatewayApi": cloudflare.Service
   }
   }
 }
 }
 
 

+ 5 - 6
cloud/resource/sst-env.d.ts

@@ -14,6 +14,10 @@ declare module "sst" {
       "type": "sst.sst.Linkable"
       "type": "sst.sst.Linkable"
       "value": string
       "value": string
     }
     }
+    "BASETEN_API_KEY": {
+      "type": "sst.sst.Secret"
+      "value": string
+    }
     "Console": {
     "Console": {
       "type": "sst.cloudflare.SolidStart"
       "type": "sst.cloudflare.SolidStart"
       "url": string
       "url": string
@@ -46,10 +50,6 @@ declare module "sst" {
       "type": "sst.sst.Secret"
       "type": "sst.sst.Secret"
       "value": string
       "value": string
     }
     }
-    "OPENAI_API_KEY": {
-      "type": "sst.sst.Secret"
-      "value": string
-    }
     "STRIPE_SECRET_KEY": {
     "STRIPE_SECRET_KEY": {
       "type": "sst.sst.Secret"
       "type": "sst.sst.Secret"
       "value": string
       "value": string
@@ -62,7 +62,7 @@ declare module "sst" {
       "type": "sst.cloudflare.Astro"
       "type": "sst.cloudflare.Astro"
       "url": string
       "url": string
     }
     }
-    "ZHIPU_API_KEY": {
+    "XAI_API_KEY": {
       "type": "sst.sst.Secret"
       "type": "sst.sst.Secret"
       "value": string
       "value": string
     }
     }
@@ -76,7 +76,6 @@ declare module "sst" {
     "AuthApi": cloudflare.Service
     "AuthApi": cloudflare.Service
     "AuthStorage": cloudflare.KVNamespace
     "AuthStorage": cloudflare.KVNamespace
     "Bucket": cloudflare.R2Bucket
     "Bucket": cloudflare.R2Bucket
-    "GatewayApi": cloudflare.Service
   }
   }
 }
 }
 
 

+ 4 - 18
infra/cloud.ts

@@ -102,8 +102,8 @@ export const stripeWebhook = new WebhookEndpoint("StripeWebhook", {
 })
 })
 
 
 const ANTHROPIC_API_KEY = new sst.Secret("ANTHROPIC_API_KEY")
 const ANTHROPIC_API_KEY = new sst.Secret("ANTHROPIC_API_KEY")
-const OPENAI_API_KEY = new sst.Secret("OPENAI_API_KEY")
-const ZHIPU_API_KEY = new sst.Secret("ZHIPU_API_KEY")
+const XAI_API_KEY = new sst.Secret("XAI_API_KEY")
+const BASETEN_API_KEY = new sst.Secret("BASETEN_API_KEY")
 const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
 const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
 const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
 const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
   properties: { value: auth.url.apply((url) => url!) },
   properties: { value: auth.url.apply((url) => url!) },
@@ -111,20 +111,6 @@ const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
 const STRIPE_WEBHOOK_SECRET = new sst.Linkable("STRIPE_WEBHOOK_SECRET", {
 const STRIPE_WEBHOOK_SECRET = new sst.Linkable("STRIPE_WEBHOOK_SECRET", {
   properties: { value: stripeWebhook.secret },
   properties: { value: stripeWebhook.secret },
 })
 })
-export const gateway = new sst.cloudflare.Worker("GatewayApi", {
-  domain: `api.gateway.${domain}`,
-  handler: "cloud/function/src/gateway.ts",
-  url: true,
-  link: [
-    database,
-    AUTH_API_URL,
-    STRIPE_WEBHOOK_SECRET,
-    STRIPE_SECRET_KEY,
-    ANTHROPIC_API_KEY,
-    OPENAI_API_KEY,
-    ZHIPU_API_KEY,
-  ],
-})
 
 
 ////////////////
 ////////////////
 // CONSOLE
 // CONSOLE
@@ -139,8 +125,8 @@ new sst.cloudflare.x.SolidStart("Console", {
     STRIPE_WEBHOOK_SECRET,
     STRIPE_WEBHOOK_SECRET,
     STRIPE_SECRET_KEY,
     STRIPE_SECRET_KEY,
     ANTHROPIC_API_KEY,
     ANTHROPIC_API_KEY,
-    OPENAI_API_KEY,
-    ZHIPU_API_KEY,
+    XAI_API_KEY,
+    BASETEN_API_KEY,
   ],
   ],
   environment: {
   environment: {
     //VITE_DOCS_URL: web.url.apply((url) => url!),
     //VITE_DOCS_URL: web.url.apply((url) => url!),

+ 18 - 1
opencode.json

@@ -1,6 +1,23 @@
 {
 {
   "$schema": "https://opencode.ai/config.json",
   "$schema": "https://opencode.ai/config.json",
-
+  "provider": {
+    "frank": {
+      "npm": "@ai-sdk/openai-compatible",
+      "name": "My AI ProviderDisplay Name",
+      "env": ["OPENCODE_API_KEY"],
+      "options": {
+        "baseURL": "https://console.frank.dev.opencode.ai/gateway/v1"
+      },
+      "models": {
+        "x-ai/grok-code-fast-1": {
+          "name": "Grok Code Fast 1"
+        },
+        "qwen/qwen3-coder": {
+          "name": "Qwen 3 Coder"
+        }
+      }
+    }
+  },
   "mcp": {
   "mcp": {
     "weather": {
     "weather": {
       "type": "local",
       "type": "local",

+ 5 - 6
packages/function/sst-env.d.ts

@@ -14,6 +14,10 @@ declare module "sst" {
       "type": "sst.sst.Linkable"
       "type": "sst.sst.Linkable"
       "value": string
       "value": string
     }
     }
+    "BASETEN_API_KEY": {
+      "type": "sst.sst.Secret"
+      "value": string
+    }
     "Console": {
     "Console": {
       "type": "sst.cloudflare.SolidStart"
       "type": "sst.cloudflare.SolidStart"
       "url": string
       "url": string
@@ -46,10 +50,6 @@ declare module "sst" {
       "type": "sst.sst.Secret"
       "type": "sst.sst.Secret"
       "value": string
       "value": string
     }
     }
-    "OPENAI_API_KEY": {
-      "type": "sst.sst.Secret"
-      "value": string
-    }
     "STRIPE_SECRET_KEY": {
     "STRIPE_SECRET_KEY": {
       "type": "sst.sst.Secret"
       "type": "sst.sst.Secret"
       "value": string
       "value": string
@@ -62,7 +62,7 @@ declare module "sst" {
       "type": "sst.cloudflare.Astro"
       "type": "sst.cloudflare.Astro"
       "url": string
       "url": string
     }
     }
-    "ZHIPU_API_KEY": {
+    "XAI_API_KEY": {
       "type": "sst.sst.Secret"
       "type": "sst.sst.Secret"
       "value": string
       "value": string
     }
     }
@@ -76,7 +76,6 @@ declare module "sst" {
     "AuthApi": cloudflare.Service
     "AuthApi": cloudflare.Service
     "AuthStorage": cloudflare.KVNamespace
     "AuthStorage": cloudflare.KVNamespace
     "Bucket": cloudflare.R2Bucket
     "Bucket": cloudflare.R2Bucket
-    "GatewayApi": cloudflare.Service
   }
   }
 }
 }
 
 

+ 5 - 9
sst-env.d.ts

@@ -24,6 +24,10 @@ declare module "sst" {
     "AuthStorage": {
     "AuthStorage": {
       "type": "sst.cloudflare.Kv"
       "type": "sst.cloudflare.Kv"
     }
     }
+    "BASETEN_API_KEY": {
+      "type": "sst.sst.Secret"
+      "value": string
+    }
     "Bucket": {
     "Bucket": {
       "name": string
       "name": string
       "type": "sst.cloudflare.Bucket"
       "type": "sst.cloudflare.Bucket"
@@ -60,14 +64,6 @@ declare module "sst" {
       "type": "sst.sst.Secret"
       "type": "sst.sst.Secret"
       "value": string
       "value": string
     }
     }
-    "GatewayApi": {
-      "type": "sst.cloudflare.Worker"
-      "url": string
-    }
-    "OPENAI_API_KEY": {
-      "type": "sst.sst.Secret"
-      "value": string
-    }
     "STRIPE_SECRET_KEY": {
     "STRIPE_SECRET_KEY": {
       "type": "sst.sst.Secret"
       "type": "sst.sst.Secret"
       "value": string
       "value": string
@@ -80,7 +76,7 @@ declare module "sst" {
       "type": "sst.cloudflare.Astro"
       "type": "sst.cloudflare.Astro"
       "url": string
       "url": string
     }
     }
-    "ZHIPU_API_KEY": {
+    "XAI_API_KEY": {
       "type": "sst.sst.Secret"
       "type": "sst.sst.Secret"
       "value": string
       "value": string
     }
     }

+ 5 - 6
sst.config.ts

@@ -12,15 +12,14 @@ export default $config({
         },
         },
         planetscale: "0.4.1",
         planetscale: "0.4.1",
       },
       },
-    };
+    }
   },
   },
   async run() {
   async run() {
-    const { api } = await import("./infra/app.js");
-    const { auth, gateway } = await import("./infra/cloud.js");
+    const { api } = await import("./infra/app.js")
+    const { auth } = await import("./infra/cloud.js")
     return {
     return {
       api: api.url,
       api: api.url,
-      gateway: gateway.url,
       auth: auth.url,
       auth: auth.url,
-    };
+    }
   },
   },
-});
+})