This commit is contained in:
Frank
2025-09-11 01:31:49 -04:00
parent 7aa57accf5
commit dedfa563c2
3 changed files with 139 additions and 37 deletions

View File

@@ -6,9 +6,7 @@ export function POST(input: APIEvent) {
return handler(input, { return handler(input, {
modifyBody: (body: any) => ({ modifyBody: (body: any) => ({
...body, ...body,
stream_options: { ...(body.stream ? { stream_options: { include_usage: true } } : {}),
include_usage: true,
},
}), }),
setAuthHeader: (headers: Headers, apiKey: string) => { setAuthHeader: (headers: Headers, apiKey: string) => {
headers.set("authorization", `Bearer ${apiKey}`) headers.set("authorization", `Bearer ${apiKey}`)

View File

@@ -1,4 +1,5 @@
import type { APIEvent } from "@solidjs/start/server" import type { APIEvent } from "@solidjs/start/server"
import path from "node:path"
import { Database, eq, sql } from "@opencode/cloud-core/drizzle/index.js" import { Database, eq, sql } from "@opencode/cloud-core/drizzle/index.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 { BillingTable, UsageTable } from "@opencode/cloud-core/schema/billing.sql.js"
@@ -6,6 +7,29 @@ import { centsToMicroCents } from "@opencode/cloud-core/util/price.js"
import { Identifier } from "@opencode/cloud-core/identifier.js" import { Identifier } from "@opencode/cloud-core/identifier.js"
import { Resource } from "@opencode/cloud-resource" import { Resource } from "@opencode/cloud-resource"
type ModelCost = {
input: number
output: number
cacheRead: number
cacheWrite5m: number
cacheWrite1h: number
}
type Model = {
id: string
auth: boolean
cost: ModelCost | ((usage: any) => ModelCost)
headerMappings: Record<string, string>
providers: Record<
string,
{
api: string
apiKey: string
model: string
weight?: number
}
>
}
export async function handler( export async function handler(
input: APIEvent, input: APIEvent,
opts: { opts: {
@@ -28,13 +52,10 @@ export async function handler(
class CreditsError extends Error {} class CreditsError extends Error {}
class ModelError extends Error {} class ModelError extends Error {}
const MODELS = { const MODELS: Record<string, Model> = {
"claude-opus-4-1": { "claude-opus-4-1": {
id: "claude-opus-4-1" as const, id: "claude-opus-4-1" as const,
auth: true, auth: true,
api: "https://api.anthropic.com",
apiKey: Resource.ANTHROPIC_API_KEY.value,
model: "claude-opus-4-1-20250805",
cost: { cost: {
input: 0.000015, input: 0.000015,
output: 0.000075, output: 0.000075,
@@ -43,13 +64,17 @@ export async function handler(
cacheWrite1h: 0.00003, cacheWrite1h: 0.00003,
}, },
headerMappings: {}, headerMappings: {},
providers: {
anthropic: {
api: "https://api.anthropic.com",
apiKey: Resource.ANTHROPIC_API_KEY.value,
model: "claude-opus-4-1-20250805",
},
},
}, },
"claude-sonnet-4": { "claude-sonnet-4": {
id: "claude-sonnet-4" as const, id: "claude-sonnet-4" as const,
auth: true, auth: true,
api: "https://api.anthropic.com",
apiKey: Resource.ANTHROPIC_API_KEY.value,
model: "claude-sonnet-4-20250514",
cost: (usage: any) => { cost: (usage: any) => {
const totalInputTokens = const totalInputTokens =
usage.inputTokens + usage.cacheReadTokens + usage.cacheWrite5mTokens + usage.cacheWrite1hTokens usage.inputTokens + usage.cacheReadTokens + usage.cacheWrite5mTokens + usage.cacheWrite1hTokens
@@ -70,13 +95,17 @@ export async function handler(
} }
}, },
headerMappings: {}, headerMappings: {},
providers: {
anthropic: {
api: "https://api.anthropic.com",
apiKey: Resource.ANTHROPIC_API_KEY.value,
model: "claude-sonnet-4-20250514",
},
},
}, },
"claude-3-5-haiku": { "claude-3-5-haiku": {
id: "claude-3-5-haiku" as const, id: "claude-3-5-haiku" as const,
auth: true, auth: true,
api: "https://api.anthropic.com",
apiKey: Resource.ANTHROPIC_API_KEY.value,
model: "claude-3-5-haiku-20241022",
cost: { cost: {
input: 0.0000008, input: 0.0000008,
output: 0.000004, output: 0.000004,
@@ -85,13 +114,17 @@ export async function handler(
cacheWrite1h: 0.0000016, cacheWrite1h: 0.0000016,
}, },
headerMappings: {}, headerMappings: {},
providers: {
anthropic: {
api: "https://api.anthropic.com",
apiKey: Resource.ANTHROPIC_API_KEY.value,
model: "claude-3-5-haiku-20241022",
},
},
}, },
"gpt-5": { "gpt-5": {
id: "gpt-5" as const, id: "gpt-5" as const,
auth: true, auth: true,
api: "https://api.openai.com",
apiKey: Resource.OPENAI_API_KEY.value,
model: "gpt-5",
cost: { cost: {
input: 0.00000125, input: 0.00000125,
output: 0.00001, output: 0.00001,
@@ -100,28 +133,43 @@ export async function handler(
cacheWrite1h: 0, cacheWrite1h: 0,
}, },
headerMappings: {}, headerMappings: {},
providers: {
openai: {
api: "https://api.openai.com",
apiKey: Resource.OPENAI_API_KEY.value,
model: "gpt-5",
},
},
}, },
"qwen3-coder": { "qwen3-coder": {
id: "qwen3-coder" as const, id: "qwen3-coder" as const,
auth: true, auth: true,
api: "https://inference.baseten.co",
apiKey: Resource.BASETEN_API_KEY.value,
model: "Qwen/Qwen3-Coder-480B-A35B-Instruct",
cost: { cost: {
input: 0.00000038, input: 0.00000045,
output: 0.00000153, output: 0.0000018,
cacheRead: 0, cacheRead: 0,
cacheWrite5m: 0, cacheWrite5m: 0,
cacheWrite1h: 0, cacheWrite1h: 0,
}, },
headerMappings: {}, headerMappings: {},
providers: {
baseten: {
api: "https://inference.baseten.co",
apiKey: Resource.BASETEN_API_KEY.value,
model: "Qwen/Qwen3-Coder-480B-A35B-Instruct",
weight: 4,
},
fireworks: {
api: "https://api.fireworks.ai/inference",
apiKey: Resource.FIREWORKS_API_KEY.value,
model: "accounts/fireworks/models/qwen3-coder-480b-a35b-instruct",
weight: 1,
},
},
}, },
"kimi-k2": { "kimi-k2": {
id: "kimi-k2" as const, id: "kimi-k2" as const,
auth: true, auth: true,
api: "https://inference.baseten.co",
apiKey: Resource.BASETEN_API_KEY.value,
model: "moonshotai/Kimi-K2-Instruct-0905",
cost: { cost: {
input: 0.0000006, input: 0.0000006,
output: 0.0000025, output: 0.0000025,
@@ -130,13 +178,24 @@ export async function handler(
cacheWrite1h: 0, cacheWrite1h: 0,
}, },
headerMappings: {}, headerMappings: {},
providers: {
baseten: {
api: "https://inference.baseten.co",
apiKey: Resource.BASETEN_API_KEY.value,
model: "moonshotai/Kimi-K2-Instruct-0905",
weight: 4,
},
fireworks: {
api: "https://api.fireworks.ai/inference",
apiKey: Resource.FIREWORKS_API_KEY.value,
model: "accounts/fireworks/models/kimi-k2-instruct-0905",
weight: 1,
},
},
}, },
"grok-code": { "grok-code": {
id: "grok-code" as const, id: "grok-code" as const,
auth: false, auth: false,
api: "https://api.x.ai",
apiKey: Resource.XAI_API_KEY.value,
model: "grok-code",
cost: { cost: {
input: 0, input: 0,
output: 0, output: 0,
@@ -148,14 +207,18 @@ export async function handler(
"x-grok-conv-id": "x-opencode-session", "x-grok-conv-id": "x-opencode-session",
"x-grok-req-id": "x-opencode-request", "x-grok-req-id": "x-opencode-request",
}, },
providers: {
xai: {
api: "https://api.x.ai",
apiKey: Resource.XAI_API_KEY.value,
model: "grok-code",
},
},
}, },
// deprecated // deprecated
"qwen/qwen3-coder": { "qwen/qwen3-coder": {
id: "qwen/qwen3-coder" as const, id: "qwen/qwen3-coder" as const,
auth: true, auth: true,
api: "https://inference.baseten.co",
apiKey: Resource.BASETEN_API_KEY.value,
model: "Qwen/Qwen3-Coder-480B-A35B-Instruct",
cost: { cost: {
input: 0.00000038, input: 0.00000038,
output: 0.00000153, output: 0.00000153,
@@ -164,6 +227,20 @@ export async function handler(
cacheWrite1h: 0, cacheWrite1h: 0,
}, },
headerMappings: {}, headerMappings: {},
providers: {
baseten: {
api: "https://inference.baseten.co",
apiKey: Resource.BASETEN_API_KEY.value,
model: "Qwen/Qwen3-Coder-480B-A35B-Instruct",
weight: 5,
},
fireworks: {
api: "https://api.fireworks.ai/inference",
apiKey: Resource.FIREWORKS_API_KEY.value,
model: "accounts/fireworks/models/qwen3-coder-480b-a35b-instruct",
weight: 1,
},
},
}, },
} }
@@ -195,15 +272,19 @@ export async function handler(
const apiKey = await authenticate() const apiKey = await authenticate()
const isFree = FREE_WORKSPACES.includes(apiKey?.workspaceID ?? "") const isFree = FREE_WORKSPACES.includes(apiKey?.workspaceID ?? "")
await checkCredits() await checkCredits()
const providerName = selectProvider()
const providerData = MODEL.providers[providerName]
logger.metric({ provider: providerName })
// Request to model provider // Request to model provider
const res = await fetch(new URL(url.pathname.replace(/^\/zen/, "") + url.search, MODEL.api), { const startTimestamp = Date.now()
const res = await fetch(path.posix.join(providerData.api, url.pathname.replace(/^\/zen/, "") + url.search), {
method: "POST", method: "POST",
headers: (() => { headers: (() => {
const headers = input.request.headers const headers = input.request.headers
headers.delete("host") headers.delete("host")
headers.delete("content-length") headers.delete("content-length")
opts.setAuthHeader(headers, MODEL.apiKey) opts.setAuthHeader(headers, providerData.apiKey)
Object.entries(MODEL.headerMappings ?? {}).forEach(([k, v]) => { Object.entries(MODEL.headerMappings ?? {}).forEach(([k, v]) => {
headers.set(k, headers.get(v)!) headers.set(k, headers.get(v)!)
}) })
@@ -211,7 +292,7 @@ export async function handler(
})(), })(),
body: JSON.stringify({ body: JSON.stringify({
...(opts.modifyBody?.(body) ?? body), ...(opts.modifyBody?.(body) ?? body),
model: MODEL.model, model: providerData.model,
}), }),
}) })
@@ -245,7 +326,6 @@ export async function handler(
const decoder = new TextDecoder() const decoder = new TextDecoder()
let buffer = "" let buffer = ""
let responseLength = 0 let responseLength = 0
let startTimestamp = Date.now()
function pump(): Promise<void> { function pump(): Promise<void> {
return ( return (
@@ -262,7 +342,6 @@ export async function handler(
logger.metric({ time_to_first_byte: Date.now() - startTimestamp }) logger.metric({ time_to_first_byte: Date.now() - startTimestamp })
} }
responseLength += value.length responseLength += value.length
buffer += decoder.decode(value, { stream: true }) buffer += decoder.decode(value, { stream: true })
const parts = buffer.split("\n\n") const parts = buffer.split("\n\n")
@@ -344,6 +423,13 @@ export async function handler(
if (billing.balance <= 0) throw new CreditsError("Insufficient balance") if (billing.balance <= 0) throw new CreditsError("Insufficient balance")
} }
function selectProvider() {
const picks = Object.entries(MODEL.providers).flatMap(([name, provider]) =>
Array<string>(provider.weight ?? 1).fill(name),
)
return picks[Math.floor(Math.random() * picks.length)]
}
async function trackUsage(usage: any) { async function trackUsage(usage: any) {
const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } = const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } =
opts.normalizeUsage(usage) opts.normalizeUsage(usage)
@@ -416,9 +502,25 @@ export async function handler(
"error.message": error.message, "error.message": error.message,
}) })
// Note: both top level "type" and "error.type" fields are used by the @ai-sdk/anthropic client to render the error message.
if (error instanceof AuthError || error instanceof CreditsError || error instanceof ModelError) if (error instanceof AuthError || error instanceof CreditsError || error instanceof ModelError)
return new Response(JSON.stringify({ error: { message: error.message } }), { status: 401 }) return new Response(
JSON.stringify({
type: "error",
error: { type: error.constructor.name, message: error.message },
}),
{ status: 401 },
)
return new Response(JSON.stringify({ error: { message: error.message } }), { status: 500 }) return new Response(
JSON.stringify({
type: "error",
error: {
type: "error",
message: error.message,
},
}),
{ status: 500 },
)
} }
} }

View File

@@ -103,6 +103,7 @@ const ANTHROPIC_API_KEY = new sst.Secret("ANTHROPIC_API_KEY")
const OPENAI_API_KEY = new sst.Secret("OPENAI_API_KEY") const OPENAI_API_KEY = new sst.Secret("OPENAI_API_KEY")
const XAI_API_KEY = new sst.Secret("XAI_API_KEY") const XAI_API_KEY = new sst.Secret("XAI_API_KEY")
const BASETEN_API_KEY = new sst.Secret("BASETEN_API_KEY") const BASETEN_API_KEY = new sst.Secret("BASETEN_API_KEY")
const FIREWORKS_API_KEY = new sst.Secret("FIREWORKS_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!) },
@@ -136,6 +137,7 @@ new sst.cloudflare.x.SolidStart("Console", {
OPENAI_API_KEY, OPENAI_API_KEY,
XAI_API_KEY, XAI_API_KEY,
BASETEN_API_KEY, BASETEN_API_KEY,
FIREWORKS_API_KEY,
], ],
environment: { environment: {
//VITE_DOCS_URL: web.url.apply((url) => url!), //VITE_DOCS_URL: web.url.apply((url) => url!),