summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--cloud/app/src/routes/gateway/v1/chat/completions.ts799
-rw-r--r--cloud/app/src/routes/stripe/webhook.ts7
-rw-r--r--cloud/core/src/billing.ts39
-rw-r--r--cloud/core/src/workspace.ts2
-rw-r--r--cloud/function/src/gateway.ts596
-rw-r--r--cloud/function/sst-env.d.ts11
-rw-r--r--cloud/resource/sst-env.d.ts11
-rw-r--r--infra/cloud.ts22
-rw-r--r--opencode.json19
-rw-r--r--packages/function/sst-env.d.ts11
-rw-r--r--sst-env.d.ts14
-rw-r--r--sst.config.ts11
12 files changed, 318 insertions, 1224 deletions
diff --git a/cloud/app/src/routes/gateway/v1/chat/completions.ts b/cloud/app/src/routes/gateway/v1/chat/completions.ts
index 671b589c5..8a0b54b14 100644
--- a/cloud/app/src/routes/gateway/v1/chat/completions.ts
+++ b/cloud/app/src/routes/gateway/v1/chat/completions.ts
@@ -1,576 +1,305 @@
import { Resource } from "@opencode/cloud-resource"
-import { Billing } from "@opencode/cloud-core/billing.js"
import type { APIEvent } from "@solidjs/start/server"
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 { 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": {
- // 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,
outputTokens,
reasoningTokens,
cacheReadTokens,
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,
+ })
+ }
}
diff --git a/cloud/app/src/routes/stripe/webhook.ts b/cloud/app/src/routes/stripe/webhook.ts
index 61d14a645..592f4568d 100644
--- a/cloud/app/src/routes/stripe/webhook.ts
+++ b/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 (!customerID) throw new Error("Customer ID 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")
+ const chargedAmount = 2000
+
await Actor.provide("system", { workspaceID }, async () => {
const customer = await Billing.get()
if (customer?.customerID && customer.customerID !== customerID) throw new Error("Customer ID mismatch")
@@ -50,7 +53,7 @@ export async function POST(input: APIEvent) {
await tx
.update(BillingTable)
.set({
- balance: sql`${BillingTable.balance} + ${centsToMicroCents(amount)}`,
+ balance: sql`${BillingTable.balance} + ${centsToMicroCents(chargedAmount)}`,
customerID,
paymentMethodID: paymentMethod.id,
paymentMethodLast4: paymentMethod.card!.last4,
@@ -59,7 +62,7 @@ export async function POST(input: APIEvent) {
await tx.insert(PaymentTable).values({
workspaceID,
id: Identifier.create("payment"),
- amount: centsToMicroCents(amount),
+ amount: centsToMicroCents(chargedAmount),
paymentID,
customerID,
})
diff --git a/cloud/core/src/billing.ts b/cloud/core/src/billing.ts
index 298d151f9..6f241ba06 100644
--- a/cloud/core/src/billing.ts
+++ b/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(
z.object({
successUrl: z.string(),
@@ -109,7 +72,7 @@ export namespace Billing {
product_data: {
name: "opencode credits",
},
- unit_amount: 2000, // $20 minimum
+ unit_amount: 2118, // $20 minimum + Stripe fee 4.4% + $0.30
},
quantity: 1,
},
diff --git a/cloud/core/src/workspace.ts b/cloud/core/src/workspace.ts
index 8646362a2..a9fb923d6 100644
--- a/cloud/core/src/workspace.ts
+++ b/cloud/core/src/workspace.ts
@@ -26,7 +26,7 @@ export namespace Workspace {
await tx.insert(BillingTable).values({
workspaceID,
id: Identifier.create("billing"),
- balance: centsToMicroCents(100),
+ balance: 0,
})
})
await Actor.provide(
diff --git a/cloud/function/src/gateway.ts b/cloud/function/src/gateway.ts
deleted file mode 100644
index ffbcc1b89..000000000
--- a/cloud/function/src/gateway.ts
+++ /dev/null
@@ -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
diff --git a/cloud/function/sst-env.d.ts b/cloud/function/sst-env.d.ts
index d0b2bb4a4..5478b4334 100644
--- a/cloud/function/sst-env.d.ts
+++ b/cloud/function/sst-env.d.ts
@@ -14,6 +14,10 @@ declare module "sst" {
"type": "sst.sst.Linkable"
"value": string
}
+ "BASETEN_API_KEY": {
+ "type": "sst.sst.Secret"
+ "value": string
+ }
"Console": {
"type": "sst.cloudflare.SolidStart"
"url": string
@@ -46,10 +50,6 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
- "OPENAI_API_KEY": {
- "type": "sst.sst.Secret"
- "value": string
- }
"STRIPE_SECRET_KEY": {
"type": "sst.sst.Secret"
"value": string
@@ -62,7 +62,7 @@ declare module "sst" {
"type": "sst.cloudflare.Astro"
"url": string
}
- "ZHIPU_API_KEY": {
+ "XAI_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
@@ -76,7 +76,6 @@ declare module "sst" {
"AuthApi": cloudflare.Service
"AuthStorage": cloudflare.KVNamespace
"Bucket": cloudflare.R2Bucket
- "GatewayApi": cloudflare.Service
}
}
diff --git a/cloud/resource/sst-env.d.ts b/cloud/resource/sst-env.d.ts
index d0b2bb4a4..5478b4334 100644
--- a/cloud/resource/sst-env.d.ts
+++ b/cloud/resource/sst-env.d.ts
@@ -14,6 +14,10 @@ declare module "sst" {
"type": "sst.sst.Linkable"
"value": string
}
+ "BASETEN_API_KEY": {
+ "type": "sst.sst.Secret"
+ "value": string
+ }
"Console": {
"type": "sst.cloudflare.SolidStart"
"url": string
@@ -46,10 +50,6 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
- "OPENAI_API_KEY": {
- "type": "sst.sst.Secret"
- "value": string
- }
"STRIPE_SECRET_KEY": {
"type": "sst.sst.Secret"
"value": string
@@ -62,7 +62,7 @@ declare module "sst" {
"type": "sst.cloudflare.Astro"
"url": string
}
- "ZHIPU_API_KEY": {
+ "XAI_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
@@ -76,7 +76,6 @@ declare module "sst" {
"AuthApi": cloudflare.Service
"AuthStorage": cloudflare.KVNamespace
"Bucket": cloudflare.R2Bucket
- "GatewayApi": cloudflare.Service
}
}
diff --git a/infra/cloud.ts b/infra/cloud.ts
index 9dbfcd2cc..b628cde72 100644
--- a/infra/cloud.ts
+++ b/infra/cloud.ts
@@ -102,8 +102,8 @@ export const stripeWebhook = new WebhookEndpoint("StripeWebhook", {
})
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 AUTH_API_URL = new sst.Linkable("AUTH_API_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", {
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
@@ -139,8 +125,8 @@ new sst.cloudflare.x.SolidStart("Console", {
STRIPE_WEBHOOK_SECRET,
STRIPE_SECRET_KEY,
ANTHROPIC_API_KEY,
- OPENAI_API_KEY,
- ZHIPU_API_KEY,
+ XAI_API_KEY,
+ BASETEN_API_KEY,
],
environment: {
//VITE_DOCS_URL: web.url.apply((url) => url!),
diff --git a/opencode.json b/opencode.json
index f416e91bf..6226069a9 100644
--- a/opencode.json
+++ b/opencode.json
@@ -1,6 +1,23 @@
{
"$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": {
"weather": {
"type": "local",
diff --git a/packages/function/sst-env.d.ts b/packages/function/sst-env.d.ts
index d0b2bb4a4..5478b4334 100644
--- a/packages/function/sst-env.d.ts
+++ b/packages/function/sst-env.d.ts
@@ -14,6 +14,10 @@ declare module "sst" {
"type": "sst.sst.Linkable"
"value": string
}
+ "BASETEN_API_KEY": {
+ "type": "sst.sst.Secret"
+ "value": string
+ }
"Console": {
"type": "sst.cloudflare.SolidStart"
"url": string
@@ -46,10 +50,6 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
- "OPENAI_API_KEY": {
- "type": "sst.sst.Secret"
- "value": string
- }
"STRIPE_SECRET_KEY": {
"type": "sst.sst.Secret"
"value": string
@@ -62,7 +62,7 @@ declare module "sst" {
"type": "sst.cloudflare.Astro"
"url": string
}
- "ZHIPU_API_KEY": {
+ "XAI_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
@@ -76,7 +76,6 @@ declare module "sst" {
"AuthApi": cloudflare.Service
"AuthStorage": cloudflare.KVNamespace
"Bucket": cloudflare.R2Bucket
- "GatewayApi": cloudflare.Service
}
}
diff --git a/sst-env.d.ts b/sst-env.d.ts
index a9276330a..534b20a69 100644
--- a/sst-env.d.ts
+++ b/sst-env.d.ts
@@ -24,6 +24,10 @@ declare module "sst" {
"AuthStorage": {
"type": "sst.cloudflare.Kv"
}
+ "BASETEN_API_KEY": {
+ "type": "sst.sst.Secret"
+ "value": string
+ }
"Bucket": {
"name": string
"type": "sst.cloudflare.Bucket"
@@ -60,14 +64,6 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
- "GatewayApi": {
- "type": "sst.cloudflare.Worker"
- "url": string
- }
- "OPENAI_API_KEY": {
- "type": "sst.sst.Secret"
- "value": string
- }
"STRIPE_SECRET_KEY": {
"type": "sst.sst.Secret"
"value": string
@@ -80,7 +76,7 @@ declare module "sst" {
"type": "sst.cloudflare.Astro"
"url": string
}
- "ZHIPU_API_KEY": {
+ "XAI_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
diff --git a/sst.config.ts b/sst.config.ts
index 06cce7f3f..f9ffa579b 100644
--- a/sst.config.ts
+++ b/sst.config.ts
@@ -12,15 +12,14 @@ export default $config({
},
planetscale: "0.4.1",
},
- };
+ }
},
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 {
api: api.url,
- gateway: gateway.url,
auth: auth.url,
- };
+ }
},
-});
+})