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() 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