diff options
| author | Frank <[email protected]> | 2025-08-08 13:22:54 -0400 |
|---|---|---|
| committer | Frank <[email protected]> | 2025-08-08 13:24:32 -0400 |
| commit | 183e0911b76025a1f2a82e979d9834fec2131d0e (patch) | |
| tree | 9987c1753bd64d1ce1d174ab397f1a8c681f642c /packages/function | |
| parent | c7bb19ad0712469063eab35589aa5d3602b0c5b1 (diff) | |
| download | opencode-183e0911b76025a1f2a82e979d9834fec2131d0e.tar.gz opencode-183e0911b76025a1f2a82e979d9834fec2131d0e.zip | |
wip: gateway
Diffstat (limited to 'packages/function')
| -rw-r--r-- | packages/function/package.json | 5 | ||||
| -rw-r--r-- | packages/function/src/gateway.ts | 499 | ||||
| -rw-r--r-- | packages/function/sst-env.d.ts | 40 |
3 files changed, 39 insertions, 505 deletions
diff --git a/packages/function/package.json b/packages/function/package.json index 1a256447d..99474496c 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -7,16 +7,11 @@ "devDependencies": { "@cloudflare/workers-types": "4.20250522.0", "@types/node": "catalog:", - "openai": "5.11.0", "typescript": "catalog:" }, "dependencies": { - "@ai-sdk/anthropic": "2.0.0", - "@ai-sdk/openai": "2.0.2", - "@ai-sdk/openai-compatible": "1.0.1", "@octokit/auth-app": "8.0.1", "@octokit/rest": "22.0.0", - "ai": "catalog:", "hono": "catalog:", "jose": "6.0.11" } diff --git a/packages/function/src/gateway.ts b/packages/function/src/gateway.ts deleted file mode 100644 index 17e9f5094..000000000 --- a/packages/function/src/gateway.ts +++ /dev/null @@ -1,499 +0,0 @@ -import { Hono, Context, Next } from "hono" -import { Resource } from "sst" -import { generateText, streamText } 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" - -type Env = {} - -const auth = async (c: Context, next: 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] - - // Replace with your validation logic - if (apiKey !== Resource.OPENCODE_API_KEY.value) { - return c.json( - { - error: { - message: "Invalid API key.", - type: "invalid_request_error", - param: null, - code: "unauthorized", - }, - }, - 401, - ) - } - - await next() -} -export default new Hono<{ Bindings: Env }>() - .get("/", (c) => c.text("Hello, world!")) - .post("/v1/chat/completions", auth, async (c) => { - try { - const body = await c.req.json<ChatCompletionCreateParamsBase>() - - console.log(body) - - const model = (() => { - const [provider, ...parts] = body.model.split("/") - const model = parts.join("/") - if (provider === "anthropic" && model === "claude-sonnet-4") { - return createAnthropic({ - apiKey: Resource.ANTHROPIC_API_KEY.value, - })("claude-sonnet-4-20250514") - } - if (provider === "openai" && model === "gpt-4.1") { - return createOpenAI({ - apiKey: Resource.OPENAI_API_KEY.value, - })("gpt-4.1") - } - if (provider === "zhipuai" && model === "glm-4.5-flash") { - return createOpenAICompatible({ - name: "Zhipu AI", - baseURL: "https://api.z.ai/api/paas/v4", - apiKey: Resource.ZHIPU_API_KEY.value, - })("glm-4.5-flash") - } - throw new Error(`Unsupported provider: ${provider}`) - })() - - const requestBody = transformOpenAIRequestToAiSDK() - - return body.stream ? await handleStream() : await handleGenerate() - - async function handleStream() { - const result = await streamText({ - model, - ...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.fullStream) { - // TODO - //console.log("!!! CHUCK !!!", chunk); - switch (chunk.type) { - case "text-delta": { - const data = { - id, - object: "chat.completion.chunk", - created, - model: body.model, - choices: [ - { - index: 0, - delta: { - content: chunk.text, - }, - 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.text, - }, - 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: [ - { - id: chunk.toolCallId, - type: "function", - function: { - name: chunk.toolName, - arguments: JSON.stringify(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, - error: { - message: 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 finishReason = - { - stop: "stop", - length: "length", - "content-filter": "content_filter", - "tool-calls": "tool_calls", - error: "stop", - other: "stop", - unknown: "stop", - }[chunk.finishReason] || "stop" - - const data = { - id, - object: "chat.completion.chunk", - created, - model: body.model, - choices: [ - { - index: 0, - delta: {}, - finish_reason: finishReason, - }, - ], - usage: { - prompt_tokens: chunk.totalUsage.inputTokens, - completion_tokens: chunk.totalUsage.outputTokens, - total_tokens: chunk.totalUsage.totalTokens, - completion_tokens_details: { - reasoning_tokens: chunk.totalUsage.reasoningTokens, - }, - prompt_tokens_details: { - cached_tokens: chunk.totalUsage.cachedInputTokens, - }, - }, - } - 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 "start-step": - case "finish-step": - 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 generateText({ - model, - ...requestBody, - }) - 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() - - 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" } - if (body.response_format.type === "json_schema") - return { - type: "json", - 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" } - throw new Error("Unsupported response format") - })(), - seed: body.seed ?? undefined, - } - - function transformTools() { - const { tools, tool_choice } = body - - if (!tools || tools.length === 0) { - return { tools: undefined, toolChoice: undefined } - } - - const aiSdkTools = tools.reduce( - (acc, tool) => { - acc[tool.function.name] = { - type: "function" as const, - name: tool.function.name, - description: tool.function.description, - inputSchema: tool.function.parameters, - } - return acc - }, - {} as Record<string, any>, - ) - - let aiSdkToolChoice - if (tool_choice == null) { - aiSdkToolChoice = undefined - } else if (tool_choice === "auto") { - aiSdkToolChoice = "auto" - } else if (tool_choice === "none") { - aiSdkToolChoice = "none" - } else if (tool_choice === "required") { - aiSdkToolChoice = "required" - } else if (tool_choice.type === "function") { - aiSdkToolChoice = { - type: "tool", - 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 - } - } - } catch (error: any) { - return c.json({ error: { message: error.message } }, 500) - } - }) - .all("*", (c) => c.text("Not Found")) diff --git a/packages/function/sst-env.d.ts b/packages/function/sst-env.d.ts index 7106662e3..4e2b15921 100644 --- a/packages/function/sst-env.d.ts +++ b/packages/function/sst-env.d.ts @@ -10,6 +10,30 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } + "AUTH_API_URL": { + "type": "sst.sst.Linkable" + "value": string + } + "Console": { + "type": "sst.cloudflare.StaticSite" + "url": string + } + "DATABASE_PASSWORD": { + "type": "sst.sst.Secret" + "value": string + } + "DATABASE_USERNAME": { + "type": "sst.sst.Secret" + "value": string + } + "Database": { + "database": string + "host": string + "password": string + "port": number + "type": "sst.sst.Linkable" + "username": string + } "GITHUB_APP_ID": { "type": "sst.sst.Secret" "value": string @@ -18,14 +42,26 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } + "GITHUB_CLIENT_ID_CONSOLE": { + "type": "sst.sst.Secret" + "value": string + } + "GITHUB_CLIENT_SECRET_CONSOLE": { + "type": "sst.sst.Secret" + "value": string + } "OPENAI_API_KEY": { "type": "sst.sst.Secret" "value": string } - "OPENCODE_API_KEY": { + "STRIPE_SECRET_KEY": { "type": "sst.sst.Secret" "value": string } + "STRIPE_WEBHOOK_SECRET": { + "type": "sst.sst.Linkable" + "value": string + } "Web": { "type": "sst.cloudflare.Astro" "url": string @@ -41,6 +77,8 @@ import * as cloudflare from "@cloudflare/workers-types"; declare module "sst" { export interface Resource { "Api": cloudflare.Service + "AuthApi": cloudflare.Service + "AuthStorage": cloudflare.KVNamespace "Bucket": cloudflare.R2Bucket "GatewayApi": cloudflare.Service } |
