Browse Source

wip: gateway

Frank 6 months ago
parent
commit
40036abb9d

+ 26 - 5
bun.lock

@@ -12,14 +12,19 @@
       "name": "@opencode/function",
       "version": "0.3.128",
       "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",
       },
       "devDependencies": {
         "@cloudflare/workers-types": "4.20250522.0",
         "@types/node": "catalog:",
+        "openai": "5.11.0",
         "typescript": "catalog:",
       },
     },
@@ -157,13 +162,17 @@
 
     "@ai-sdk/amazon-bedrock": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-icLGO7Q0NinnHIPgT+y1QjHVwH4HwV+brWbvM+FfCG2Afpa89PyKa3Ret91kGjZpBgM/xnj1B7K5eM+rRlsXQA=="],
 
-    "@ai-sdk/anthropic": ["@ai-sdk/anthropic@1.2.12", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ=="],
+    "@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-uyyaO4KhxoIKZztREqLPh+6/K3ZJx/rp72JKoUEL9/kC+vfQTThUfPnY/bUryUpcnawx8IY/tSoYNOi/8PCv7w=="],
 
     "@ai-sdk/gateway": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-beta.2", "@ai-sdk/provider-utils": "3.0.0-beta.10" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-felWPMuECZRGx8xnmvH5dW3jywKTkGnw/tXN8szphGzEDr/BfxywuXijfPBG2WBUS6frPXsvSLDRdCm5W38PXA=="],
 
-    "@ai-sdk/provider": ["@ai-sdk/[email protected]", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-vqhtZA7R24q1XnmfmIb1fZSmHMIaJH1BVQ+0kFnNJgqWsc+V8i+yfetZ37gUc4fXATFmBuS/6O7+RPoHsZ2Fqg=="],
+    "@ai-sdk/openai": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-D4zYz2uR90aooKQvX1XnS00Z7PkbrcY+snUvPfm5bCabTG7bzLrVtD56nJ5bSaZG8lmuOMfXpyiEEArYLyWPpw=="],
+
+    "@ai-sdk/openai-compatible": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-luHVcU+yKzwv3ekKgbP3v+elUVxb2Rt+8c6w9qi7g2NYG2/pEL21oIrnaEnc6UtTZLLZX9EFBcpq2N1FQKDIMw=="],
+
+    "@ai-sdk/provider": ["@ai-sdk/[email protected]", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
 
-    "@ai-sdk/provider-utils": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-beta.2", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-e6WSsgM01au04/1L/v5daXHn00eKjPBQXl3jq3BfvQbQ1jo8Rls2pvrdkyVc25jBW4TV4Zm+tw+v6NAh5NPXMA=="],
+    "@ai-sdk/provider-utils": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],
 
     "@ampproject/remapping": ["@ampproject/[email protected]", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
 
@@ -1367,6 +1376,8 @@
 
     "open": ["[email protected]", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "is-wsl": "^3.1.0" } }, "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw=="],
 
+    "openai": ["[email protected]", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-+AuTc5pVjlnTuA9zvn8rA/k+1RluPIx9AD4eDcnutv6JNwHHZxIhkFy+tmMKCvmMFDQzfA/r1ujvPWB19DQkYg=="],
+
     "openapi-types": ["[email protected]", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
 
     "opencode": ["opencode@workspace:packages/opencode"],
@@ -1841,9 +1852,9 @@
 
     "@ai-sdk/amazon-bedrock/aws4fetch": ["[email protected]", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
 
-    "@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/[email protected]", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
+    "@ai-sdk/gateway/@ai-sdk/provider": ["@ai-sdk/[email protected]", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-vqhtZA7R24q1XnmfmIb1fZSmHMIaJH1BVQ+0kFnNJgqWsc+V8i+yfetZ37gUc4fXATFmBuS/6O7+RPoHsZ2Fqg=="],
 
-    "@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="],
+    "@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-beta.2", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-e6WSsgM01au04/1L/v5daXHn00eKjPBQXl3jq3BfvQbQ1jo8Rls2pvrdkyVc25jBW4TV4Zm+tw+v6NAh5NPXMA=="],
 
     "@ampproject/remapping/@jridgewell/trace-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
 
@@ -1875,6 +1886,10 @@
 
     "@rollup/pluginutils/estree-walker": ["[email protected]", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
 
+    "ai/@ai-sdk/provider": ["@ai-sdk/[email protected]", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-vqhtZA7R24q1XnmfmIb1fZSmHMIaJH1BVQ+0kFnNJgqWsc+V8i+yfetZ37gUc4fXATFmBuS/6O7+RPoHsZ2Fqg=="],
+
+    "ai/@ai-sdk/provider-utils": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-beta.2", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-e6WSsgM01au04/1L/v5daXHn00eKjPBQXl3jq3BfvQbQ1jo8Rls2pvrdkyVc25jBW4TV4Zm+tw+v6NAh5NPXMA=="],
+
     "ansi-align/string-width": ["[email protected]", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
 
     "anymatch/picomatch": ["[email protected]", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
@@ -1915,6 +1930,8 @@
 
     "nypm/pathe": ["[email protected]", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
 
+    "opencode/@ai-sdk/anthropic": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ=="],
+
     "opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/[email protected]", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="],
 
     "opencontrol/hono": ["[email protected]", "", {}, "sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg=="],
@@ -2001,6 +2018,10 @@
 
     "gray-matter/js-yaml/argparse": ["[email protected]", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
 
+    "opencode/@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/[email protected]", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
+
+    "opencode/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="],
+
     "opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["[email protected]", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="],
 
     "opencontrol/@modelcontextprotocol/sdk/zod": ["[email protected]", "", {}, "sha512-JMMPMy9ZBk3XFEdbM3iL1brx4NUSejd6xr3ELrrGEfGb355gjhiAWtG3K5o+AViV/3ZfkIrCzXsZn6SbLwTR8Q=="],

+ 12 - 0
infra/app.ts

@@ -46,3 +46,15 @@ new sst.cloudflare.x.Astro("Web", {
     VITE_API_URL: api.url,
   },
 })
+
+const OPENCODE_API_KEY = new sst.Secret("OPENCODE_API_KEY")
+const ANTHROPIC_API_KEY = new sst.Secret("ANTHROPIC_API_KEY")
+const OPENAI_API_KEY = new sst.Secret("OPENAI_API_KEY")
+const ZHIPU_API_KEY = new sst.Secret("ZHIPU_API_KEY")
+
+export const gateway = new sst.cloudflare.Worker("GatewayApi", {
+  domain: `api.gateway.${domain}`,
+  handler: "packages/function/src/gateway.ts",
+  url: true,
+  link: [OPENCODE_API_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY, ZHIPU_API_KEY],
+})

+ 15 - 0
opencode.json

@@ -1,5 +1,20 @@
 {
   "$schema": "https://opencode.ai/config.json",
+  "model": "opencode/anthropic/claude-sonnet-4",
+  "provider": {
+    "opencode": {
+      "name": "opencode",
+      "npm": "@ai-sdk/openai-compatible",
+      "options": {
+        "baseURL": "https://api.gateway.frank.dev.opencode.ai/v1"
+      },
+      "models": {
+        "anthropic/claude-sonnet-4": {},
+        "openai/gpt-4.1": {},
+        "zhipu/glm-4.5-flash": {}
+      }
+    }
+  },
   "mcp": {
     "context7": {
       "type": "remote",

+ 7 - 2
packages/function/package.json

@@ -6,12 +6,17 @@
   "type": "module",
   "devDependencies": {
     "@cloudflare/workers-types": "4.20250522.0",
-    "typescript": "catalog:",
-    "@types/node": "catalog:"
+    "@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"
   }

+ 499 - 0
packages/function/src/gateway.ts

@@ -0,0 +1,499 @@
+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"))

+ 17 - 0
packages/function/sst-env.d.ts

@@ -6,6 +6,10 @@
 import "sst"
 declare module "sst" {
   export interface Resource {
+    "ANTHROPIC_API_KEY": {
+      "type": "sst.sst.Secret"
+      "value": string
+    }
     "GITHUB_APP_ID": {
       "type": "sst.sst.Secret"
       "value": string
@@ -14,10 +18,22 @@ declare module "sst" {
       "type": "sst.sst.Secret"
       "value": string
     }
+    "OPENAI_API_KEY": {
+      "type": "sst.sst.Secret"
+      "value": string
+    }
+    "OPENCODE_API_KEY": {
+      "type": "sst.sst.Secret"
+      "value": string
+    }
     "Web": {
       "type": "sst.cloudflare.Astro"
       "url": string
     }
+    "ZHIPU_API_KEY": {
+      "type": "sst.sst.Secret"
+      "value": string
+    }
   }
 }
 // cloudflare 
@@ -26,6 +42,7 @@ declare module "sst" {
   export interface Resource {
     "Api": cloudflare.Service
     "Bucket": cloudflare.R2Bucket
+    "GatewayApi": cloudflare.Service
   }
 }
 

+ 9 - 0
packages/plugin/sst-env.d.ts

@@ -0,0 +1,9 @@
+/* This file is auto-generated by SST. Do not edit. */
+/* tslint:disable */
+/* eslint-disable */
+/* deno-fmt-ignore-file */
+
+/// <reference path="../../sst-env.d.ts" />
+
+import "sst"
+export {}

+ 9 - 0
packages/sdk/js/sst-env.d.ts

@@ -0,0 +1,9 @@
+/* This file is auto-generated by SST. Do not edit. */
+/* tslint:disable */
+/* eslint-disable */
+/* deno-fmt-ignore-file */
+
+/// <reference path="../../../sst-env.d.ts" />
+
+import "sst"
+export {}

+ 20 - 0
sst-env.d.ts

@@ -5,6 +5,10 @@
 
 declare module "sst" {
   export interface Resource {
+    "ANTHROPIC_API_KEY": {
+      "type": "sst.sst.Secret"
+      "value": string
+    }
     "Api": {
       "type": "sst.cloudflare.Worker"
       "url": string
@@ -20,10 +24,26 @@ declare module "sst" {
       "type": "sst.sst.Secret"
       "value": string
     }
+    "GatewayApi": {
+      "type": "sst.cloudflare.Worker"
+      "url": string
+    }
+    "OPENAI_API_KEY": {
+      "type": "sst.sst.Secret"
+      "value": string
+    }
+    "OPENCODE_API_KEY": {
+      "type": "sst.sst.Secret"
+      "value": string
+    }
     "Web": {
       "type": "sst.cloudflare.Astro"
       "url": string
     }
+    "ZHIPU_API_KEY": {
+      "type": "sst.sst.Secret"
+      "value": string
+    }
   }
 }
 /// <reference path="sst-env.d.ts" />

+ 2 - 1
sst.config.ts

@@ -10,9 +10,10 @@ export default $config({
     }
   },
   async run() {
-    const { api } = await import("./infra/app.js")
+    const { api, gateway } = await import("./infra/app.js")
     return {
       api: api.url,
+      gateway: gateway.url,
     }
   },
 })