mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-22 18:24:21 +01:00
wip: gateway
This commit is contained in:
@@ -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
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user