wip: gateway

This commit is contained in:
Frank
2025-08-10 00:28:59 -04:00
parent bd4319f2bc
commit 34ac0e895d

View File

@@ -4,11 +4,11 @@ import { cors } from "hono/cors"
import { HTTPException } from "hono/http-exception" import { HTTPException } from "hono/http-exception"
import { zValidator } from "@hono/zod-validator" import { zValidator } from "@hono/zod-validator"
import { Resource } from "sst" import { Resource } from "sst"
import { generateText, streamText } from "ai" import { type ProviderMetadata, type LanguageModelUsage, generateText, streamText, Tool } from "ai"
import { createAnthropic } from "@ai-sdk/anthropic" import { createAnthropic } from "@ai-sdk/anthropic"
import { createOpenAI } from "@ai-sdk/openai" import { createOpenAI } from "@ai-sdk/openai"
import { createOpenAICompatible } from "@ai-sdk/openai-compatible" import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
import type { LanguageModelV2Usage, LanguageModelV2Prompt } from "@ai-sdk/provider" import type { LanguageModelV2Prompt } from "@ai-sdk/provider"
import { type ChatCompletionCreateParamsBase } from "openai/resources/chat/completions" import { type ChatCompletionCreateParamsBase } from "openai/resources/chat/completions"
import { Actor } from "@opencode/cloud-core/actor.js" import { Actor } from "@opencode/cloud-core/actor.js"
import { and, Database, eq, sql } from "@opencode/cloud-core/drizzle/index.js" import { and, Database, eq, sql } from "@opencode/cloud-core/drizzle/index.js"
@@ -181,8 +181,6 @@ const app = new Hono<{ Bindings: Env; Variables: { keyRecord?: { id: string; wor
try { try {
const body = await c.req.json<ChatCompletionCreateParamsBase>() const body = await c.req.json<ChatCompletionCreateParamsBase>()
console.log(body)
const model = SUPPORTED_MODELS[body.model as keyof typeof SUPPORTED_MODELS]?.model() const model = SUPPORTED_MODELS[body.model as keyof typeof SUPPORTED_MODELS]?.model()
if (!model) throw new Error(`Unsupported model: ${body.model}`) if (!model) throw new Error(`Unsupported model: ${body.model}`)
@@ -191,7 +189,7 @@ const app = new Hono<{ Bindings: Env; Variables: { keyRecord?: { id: string; wor
return body.stream ? await handleStream() : await handleGenerate() return body.stream ? await handleStream() : await handleGenerate()
async function handleStream() { async function handleStream() {
const result = await streamText({ const result = streamText({
model, model,
...requestBody, ...requestBody,
}) })
@@ -204,8 +202,7 @@ const app = new Hono<{ Bindings: Env; Variables: { keyRecord?: { id: string; wor
try { try {
for await (const chunk of result.fullStream) { for await (const chunk of result.fullStream) {
// TODO console.log("!!! CHUNK !!! : " + chunk.type)
//console.log("!!! CHUCK !!!", chunk);
switch (chunk.type) { switch (chunk.type) {
case "text-delta": { case "text-delta": {
const data = { const data = {
@@ -282,8 +279,15 @@ const app = new Hono<{ Bindings: Env; Variables: { keyRecord?: { id: string; wor
object: "chat.completion.chunk", object: "chat.completion.chunk",
created, created,
model: body.model, model: body.model,
choices: [
{
index: 0,
delta: {},
finish_reason: "stop",
},
],
error: { error: {
message: chunk.error, message: typeof chunk.error === "string" ? chunk.error : JSON.stringify(chunk.error),
type: "server_error", type: "server_error",
}, },
} }
@@ -294,17 +298,6 @@ const app = new Hono<{ Bindings: Env; Variables: { keyRecord?: { id: string; wor
} }
case "finish": { case "finish": {
const finishReason =
{
stop: "stop",
length: "length",
"content-filter": "content_filter",
"tool-calls": "tool_calls",
error: "stop",
other: "stop",
unknown: "stop",
}[chunk.finishReason] || "stop"
const data = { const data = {
id, id,
object: "chat.completion.chunk", object: "chat.completion.chunk",
@@ -314,7 +307,16 @@ const app = new Hono<{ Bindings: Env; Variables: { keyRecord?: { id: string; wor
{ {
index: 0, index: 0,
delta: {}, delta: {},
finish_reason: finishReason, finish_reason:
{
stop: "stop",
length: "length",
"content-filter": "content_filter",
"tool-calls": "tool_calls",
error: "stop",
other: "stop",
unknown: "stop",
}[chunk.finishReason] || "stop",
}, },
], ],
usage: { usage: {
@@ -335,10 +337,13 @@ const app = new Hono<{ Bindings: Env; Variables: { keyRecord?: { id: string; wor
break break
} }
case "finish-step": {
await trackUsage(body.model, chunk.usage, chunk.providerMetadata)
}
//case "stream-start": //case "stream-start":
//case "response-metadata": //case "response-metadata":
case "start-step": case "start-step":
case "finish-step":
case "text-start": case "text-start":
case "text-end": case "text-end":
case "reasoning-start": case "reasoning-start":
@@ -373,7 +378,7 @@ const app = new Hono<{ Bindings: Env; Variables: { keyRecord?: { id: string; wor
model, model,
...requestBody, ...requestBody,
}) })
await trackUsage(body.model, response.usage) await trackUsage(body.model, response.usage, response.providerMetadata)
return c.json({ return c.json({
id: `chatcmpl-${Date.now()}`, id: `chatcmpl-${Date.now()}`,
object: "chat.completion" as const, object: "chat.completion" as const,
@@ -427,6 +432,7 @@ const app = new Hono<{ Bindings: Env; Variables: { keyRecord?: { id: string; wor
function transformOpenAIRequestToAiSDK() { function transformOpenAIRequestToAiSDK() {
const prompt = transformMessages() const prompt = transformMessages()
const tools = transformTools()
return { return {
prompt, prompt,
@@ -456,6 +462,8 @@ const app = new Hono<{ Bindings: Env; Variables: { keyRecord?: { id: string; wor
throw new Error("Unsupported response format") throw new Error("Unsupported response format")
})(), })(),
seed: body.seed ?? undefined, seed: body.seed ?? undefined,
//tools: tools.tools,
//toolChoice: tools.toolChoice,
} }
function transformTools() { function transformTools() {
@@ -468,7 +476,6 @@ const app = new Hono<{ Bindings: Env; Variables: { keyRecord?: { id: string; wor
const aiSdkTools = tools.reduce( const aiSdkTools = tools.reduce(
(acc, tool) => { (acc, tool) => {
acc[tool.function.name] = { acc[tool.function.name] = {
type: "function" as const,
name: tool.function.name, name: tool.function.name,
description: tool.function.description, description: tool.function.description,
inputSchema: tool.function.parameters, inputSchema: tool.function.parameters,
@@ -482,14 +489,14 @@ const app = new Hono<{ Bindings: Env; Variables: { keyRecord?: { id: string; wor
if (tool_choice == null) { if (tool_choice == null) {
aiSdkToolChoice = undefined aiSdkToolChoice = undefined
} else if (tool_choice === "auto") { } else if (tool_choice === "auto") {
aiSdkToolChoice = "auto" aiSdkToolChoice = "auto" as const
} else if (tool_choice === "none") { } else if (tool_choice === "none") {
aiSdkToolChoice = "none" aiSdkToolChoice = "none" as const
} else if (tool_choice === "required") { } else if (tool_choice === "required") {
aiSdkToolChoice = "required" aiSdkToolChoice = "required" as const
} else if (tool_choice.type === "function") { } else if (tool_choice.type === "function") {
aiSdkToolChoice = { aiSdkToolChoice = {
type: "tool", type: "tool" as const,
toolName: tool_choice.function.name, toolName: tool_choice.function.name,
} }
} }
@@ -604,30 +611,39 @@ const app = new Hono<{ Bindings: Env; Variables: { keyRecord?: { id: string; wor
} }
} }
async function trackUsage(model: string, usage: LanguageModelV2Usage) { async function trackUsage(model: string, usage: LanguageModelUsage, providerMetadata?: ProviderMetadata) {
const keyRecord = c.get("keyRecord") const keyRecord = c.get("keyRecord")
if (!keyRecord) return if (!keyRecord) return
const modelData = SUPPORTED_MODELS[model as keyof typeof SUPPORTED_MODELS] const modelData = SUPPORTED_MODELS[model as keyof typeof SUPPORTED_MODELS]
if (!modelData) throw new Error(`Unsupported model: ${model}`) if (!modelData) throw new Error(`Unsupported model: ${model}`)
const inputCost = modelData.input * (usage.inputTokens ?? 0) const inputTokens = usage.inputTokens ?? 0
const outputCost = modelData.output * (usage.outputTokens ?? 0) const outputTokens = usage.outputTokens ?? 0
const reasoningCost = modelData.reasoning * (usage.reasoningTokens ?? 0) const reasoningTokens = usage.reasoningTokens ?? 0
const cacheReadCost = modelData.cacheRead * (usage.cachedInputTokens ?? 0) const cacheReadTokens = usage.cachedInputTokens ?? 0
const cacheWriteCost = modelData.cacheWrite * (usage.outputTokens ?? 0) const cacheWriteTokens =
providerMetadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
// @ts-expect-error
providerMetadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ??
0
const totalCost = inputCost + outputCost + reasoningCost + cacheReadCost + cacheWriteCost 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 Actor.provide("system", { workspaceID: keyRecord.workspaceID }, async () => { await Actor.provide("system", { workspaceID: keyRecord.workspaceID }, async () => {
await Billing.consume({ await Billing.consume({
model, model,
inputTokens: usage.inputTokens ?? 0, inputTokens,
outputTokens: usage.outputTokens ?? 0, outputTokens,
reasoningTokens: usage.reasoningTokens ?? 0, reasoningTokens,
cacheReadTokens: usage.cachedInputTokens ?? 0, cacheReadTokens,
cacheWriteTokens: usage.outputTokens ?? 0, cacheWriteTokens,
costInCents: totalCost * 100, costInCents,
}) })
}) })
@@ -699,7 +715,7 @@ const app = new Hono<{ Bindings: Env; Variables: { keyRecord?: { id: string; wor
price_data: { price_data: {
currency: "usd", currency: "usd",
product_data: { product_data: {
name: "OpenControl credits", name: "opencode credits",
}, },
unit_amount: 2000, // $20 minimum unit_amount: 2000, // $20 minimum
}, },