File size: 4,921 Bytes
5f24abe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
import { Resource } from "@opencode-ai/console-resource"
import { and, Database, eq, isNotNull, isNull, sql } from "@opencode-ai/console-core/drizzle/index.js"
import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js"
import { ProviderTable } from "@opencode-ai/console-core/schema/provider.sql.js"
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"

const paths: Record<string, string | undefined> = {
  "POST /zen/v1/chat/completions": "/openai/v1/chat/completions",
  "POST /zen/v1/responses": "/openai/v1/responses",
  "POST /zen/v1/messages": "/anthropic/v1/messages",
  "POST /zen/go/v1/chat/completions": "/go/openai/v1/chat/completions",
  "POST /zen/go/v1/responses": "/go/openai/v1/responses",
  "POST /zen/go/v1/messages": "/go/anthropic/v1/messages",
  "GET /zen/v1/models": "/v1/models",
  "GET /zen/go/v1/models": "/go/v1/models",
  "GET /zen/go/v1/usage": "/go/v1/usage",
}

export async function proxyInference(
  request: Request,
  generation?: {
    provider?: "openai" | "anthropic" | "google"
    /** The provider's native model ID, not the public Zen alias. */
    model?: string
    body: (model?: string) => ReadableStream<Uint8Array>
  },
): Promise<Response | undefined> {
  const url = new URL(request.url)
  const path =
    paths[`${request.method} ${url.pathname}`] ??
    (request.method === "POST" &&
    /^\/zen\/v1\/models\/[^/]+:(?:generateContent|streamGenerateContent)$/.test(url.pathname)
      ? url.pathname.replace("/zen/v1/models/", "/google/v1beta/models/")
      : undefined)
  if (!path) return undefined

  const key = url.pathname.endsWith("/messages")
    ? request.headers.get("x-api-key")
    : path.startsWith("/google/")
      ? request.headers.get("x-goog-api-key")
      : request.headers.get("authorization")?.split(" ")[1]
  if (!key || key === "public") return undefined

  // New Console keys are never in the legacy key table; every legacy key is `sk-`.
  const legacy = !key.startsWith("oc_sk_")
  const workspace = legacy ? await migratedWorkspace(key, generation?.provider) : undefined
  if (legacy && !workspace) return undefined
  const model = workspace?.provider ? generation?.model : undefined
  if (workspace?.provider && !model) throw new Error("Legacy BYOK model mapping is unavailable")

  const destination = new URL(Resource.ConsoleMigration.inferenceUrl)
  // Imported connections must use this same workspace/provider-derived ID.
  const target =
    model && workspace
      ? `/custom/conn_${workspace.id.slice(4)}_${workspace.provider}${
          path.startsWith("/google/")
            ? `/models/${encodeURIComponent(model)}${url.pathname.slice(url.pathname.lastIndexOf(":"))}`
            : url.pathname.slice("/zen/v1".length)
        }`
      : path
  destination.pathname = `${destination.pathname.replace(/\/$/, "")}${target}`
  destination.search = url.search
  destination.hash = ""

  // Model extraction has already read part of the body; forward its replay stream.
  const forwarded = new Request(
    destination,
    generation ? new Request(request, { method: request.method, body: generation.body(model) }) : request,
  )
  // Migrated requests use ordinary destination authentication and accounting.
  for (const name of [
    "x-zen",
    "x-zen-model",
    "x-zen-ip",
    "cf-access-client-id",
    "cf-access-client-secret",
    "host",
    "content-length",
  ])
    forwarded.headers.delete(name)
  forwarded.headers.set("authorization", `Bearer ${key}`)
  const ip = request.headers.get("cf-connecting-ip")
  if (ip) forwarded.headers.set("x-real-ip", ip)
  const requestID = request.headers.get("x-opencode-request-id") ?? request.headers.get("x-opencode-request")
  if (requestID) forwarded.headers.set("x-opencode-request-id", requestID)

  return fetch(forwarded, { redirect: "manual" })
}

// Routing only; the destination owns authentication and revocation after cutover.
function migratedWorkspace(key: string, provider?: string) {
  return Database.use((tx) =>
    tx
      .select({ id: WorkspaceTable.id, provider: ProviderTable.provider })
      .from(KeyTable)
      .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID))
      .leftJoin(
        ProviderTable,
        provider
          ? and(
              eq(ProviderTable.workspaceID, KeyTable.workspaceID),
              eq(ProviderTable.provider, provider),
              isNull(ProviderTable.timeDeleted),
              sql`length(${ProviderTable.credentials}) > 0`,
            )
          : sql`false`,
      )
      .where(and(eq(KeyTable.key, key), isNotNull(WorkspaceTable.migrated_at)))
      .limit(1)
      .then((rows) => rows[0]),
  )
}

export function inferenceUnavailable() {
  return Response.json(
    { error: { type: "api_error", message: "Inference routing is unavailable. Please retry later." } },
    { status: 503, headers: { "Cache-Control": "no-store" } },
  )
}