summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorFrank <[email protected]>2025-11-18 14:28:27 -0500
committerFrank <[email protected]>2025-11-18 14:28:31 -0500
commit7283bfa480b163da7ea66250f4782747293afdfd (patch)
treece86b0e55f81392f0651de54339689ec5135e972 /packages
parent37d5099728613f73edf773d0ea3b88c97a124206 (diff)
downloadopencode-7283bfa480b163da7ea66250f4782747293afdfd.tar.gz
opencode-7283bfa480b163da7ea66250f4782747293afdfd.zip
zen: gemini
Diffstat (limited to 'packages')
-rw-r--r--packages/console/app/src/routes/zen/util/handler.ts17
-rw-r--r--packages/console/app/src/routes/zen/util/provider/anthropic.ts1
-rw-r--r--packages/console/app/src/routes/zen/util/provider/google.ts74
-rw-r--r--packages/console/app/src/routes/zen/util/provider/openai-compatible.ts1
-rw-r--r--packages/console/app/src/routes/zen/util/provider/openai.ts1
-rw-r--r--packages/console/app/src/routes/zen/util/provider/provider.ts3
-rw-r--r--packages/console/app/src/routes/zen/v1/chat/completions.ts2
-rw-r--r--packages/console/app/src/routes/zen/v1/messages.ts2
-rw-r--r--packages/console/app/src/routes/zen/v1/models/[model].ts13
-rw-r--r--packages/console/app/src/routes/zen/v1/responses.ts2
-rw-r--r--packages/console/core/src/model.ts2
11 files changed, 111 insertions, 7 deletions
diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts
index 70df7d7cd..3453a6d38 100644
--- a/packages/console/app/src/routes/zen/util/handler.ts
+++ b/packages/console/app/src/routes/zen/util/handler.ts
@@ -15,6 +15,7 @@ import { logger } from "./logger"
import { AuthError, CreditsError, MonthlyLimitError, UserLimitError, ModelError, RateLimitError } from "./error"
import { createBodyConverter, createStreamPartConverter, createResponseConverter } from "./provider/provider"
import { anthropicHelper } from "./provider/anthropic"
+import { googleHelper } from "./provider/google"
import { openaiHelper } from "./provider/openai"
import { oaCompatHelper } from "./provider/openai-compatible"
import { createRateLimiter } from "./rateLimiter"
@@ -30,6 +31,8 @@ export async function handler(
opts: {
format: ZenData.Format
parseApiKey: (headers: Headers) => string | undefined
+ parseModel: (url: string, body: any) => string
+ parseIsStream: (url: string, body: any) => boolean
},
) {
type AuthInfo = Awaited<ReturnType<typeof authenticate>>
@@ -43,15 +46,18 @@ export async function handler(
]
try {
+ const url = input.request.url
const body = await input.request.json()
const ip = input.request.headers.get("x-real-ip") ?? ""
+ const model = opts.parseModel(url, body)
+ const isStream = opts.parseIsStream(url, body)
logger.metric({
- is_tream: !!body.stream,
+ is_tream: isStream,
session: input.request.headers.get("x-opencode-session"),
request: input.request.headers.get("x-opencode-request"),
})
const zenData = ZenData.list()
- const modelInfo = validateModel(zenData, body.model)
+ const modelInfo = validateModel(zenData, model)
const rateLimiter = createRateLimiter(modelInfo.id, modelInfo.rateLimit, ip)
await rateLimiter?.check()
@@ -64,7 +70,7 @@ export async function handler(
logger.metric({ provider: providerInfo.id })
const startTimestamp = Date.now()
- const reqUrl = providerInfo.modifyUrl(providerInfo.api)
+ const reqUrl = providerInfo.modifyUrl(providerInfo.api, providerInfo.model, isStream)
const reqBody = JSON.stringify(
providerInfo.modifyBody({
...createBodyConverter(opts.format, providerInfo.format)(body),
@@ -114,7 +120,7 @@ export async function handler(
logger.debug("STATUS: " + res.status + " " + res.statusText)
// Handle non-streaming response
- if (!body.stream) {
+ if (!isStream) {
const responseConverter = createResponseConverter(providerInfo.format, opts.format)
const json = await res.json()
const body = JSON.stringify(responseConverter(json))
@@ -169,7 +175,7 @@ export async function handler(
responseLength += value.length
buffer += decoder.decode(value, { stream: true })
- const parts = buffer.split("\n\n")
+ const parts = buffer.split(providerInfo.streamSeparator)
buffer = parts.pop() ?? ""
for (let part of parts) {
@@ -283,6 +289,7 @@ export async function handler(
...(() => {
const format = zenData.providers[provider.id].format
if (format === "anthropic") return anthropicHelper
+ if (format === "google") return googleHelper
if (format === "openai") return openaiHelper
return oaCompatHelper
})(),
diff --git a/packages/console/app/src/routes/zen/util/provider/anthropic.ts b/packages/console/app/src/routes/zen/util/provider/anthropic.ts
index d8d1cd741..887a6e4b5 100644
--- a/packages/console/app/src/routes/zen/util/provider/anthropic.ts
+++ b/packages/console/app/src/routes/zen/util/provider/anthropic.ts
@@ -30,6 +30,7 @@ export const anthropicHelper = {
service_tier: "standard_only",
}
},
+ streamSeparator: "\n\n",
createUsageParser: () => {
let usage: Usage
diff --git a/packages/console/app/src/routes/zen/util/provider/google.ts b/packages/console/app/src/routes/zen/util/provider/google.ts
new file mode 100644
index 000000000..afde42096
--- /dev/null
+++ b/packages/console/app/src/routes/zen/util/provider/google.ts
@@ -0,0 +1,74 @@
+import { ProviderHelper } from "./provider"
+
+/*
+{
+ promptTokenCount: 11453,
+ candidatesTokenCount: 71,
+ totalTokenCount: 11625,
+ cachedContentTokenCount: 8100,
+ promptTokensDetails: [
+ {modality: "TEXT",tokenCount: 11453}
+ ],
+ cacheTokensDetails: [
+ {modality: "TEXT",tokenCount: 8100}
+ ],
+ thoughtsTokenCount: 101
+}
+*/
+
+type Usage = {
+ promptTokenCount?: number
+ candidatesTokenCount?: number
+ totalTokenCount?: number
+ cachedContentTokenCount?: number
+ promptTokensDetails?: { modality: string; tokenCount: number }[]
+ cacheTokensDetails?: { modality: string; tokenCount: number }[]
+ thoughtsTokenCount?: number
+}
+
+export const googleHelper = {
+ format: "google",
+ modifyUrl: (providerApi: string, model?: string, isStream?: boolean) =>
+ `${providerApi}/models/${model}:${isStream ? "streamGenerateContent?alt=sse" : "generateContent"}`,
+ modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
+ headers.set("x-goog-api-key", apiKey)
+ },
+ modifyBody: (body: Record<string, any>) => {
+ return body
+ },
+ streamSeparator: "\r\n\r\n",
+ createUsageParser: () => {
+ let usage: Usage
+
+ return {
+ parse: (chunk: string) => {
+ if (!chunk.startsWith("data: ")) return
+
+ let json
+ try {
+ json = JSON.parse(chunk.slice(6)) as { usageMetadata?: Usage }
+ } catch (e) {
+ return
+ }
+
+ if (!json.usageMetadata) return
+ usage = json.usageMetadata
+ },
+ retrieve: () => usage,
+ }
+ },
+ normalizeUsage: (usage: Usage) => {
+ const inputTokens = usage.promptTokenCount ?? 0
+ const outputTokens = usage.candidatesTokenCount ?? 0
+ const reasoningTokens = usage.thoughtsTokenCount ?? 0
+ const cacheReadTokens = usage.cachedContentTokenCount ?? 0
+ return {
+ inputTokens: inputTokens - cacheReadTokens,
+ outputTokens,
+ reasoningTokens,
+ cacheReadTokens,
+ cacheWrite5mTokens: undefined,
+ cacheWrite1hTokens: undefined,
+ }
+ },
+} satisfies ProviderHelper
diff --git a/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts b/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts
index 8a9170ef1..5771ed4fa 100644
--- a/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts
+++ b/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts
@@ -33,6 +33,7 @@ export const oaCompatHelper = {
...(body.stream ? { stream_options: { include_usage: true } } : {}),
}
},
+ streamSeparator: "\n\n",
createUsageParser: () => {
let usage: Usage
diff --git a/packages/console/app/src/routes/zen/util/provider/openai.ts b/packages/console/app/src/routes/zen/util/provider/openai.ts
index e79e83579..dff6e13fb 100644
--- a/packages/console/app/src/routes/zen/util/provider/openai.ts
+++ b/packages/console/app/src/routes/zen/util/provider/openai.ts
@@ -21,6 +21,7 @@ export const openaiHelper = {
modifyBody: (body: Record<string, any>) => {
return body
},
+ streamSeparator: "\n\n",
createUsageParser: () => {
let usage: Usage
diff --git a/packages/console/app/src/routes/zen/util/provider/provider.ts b/packages/console/app/src/routes/zen/util/provider/provider.ts
index d0f123968..8366f3a63 100644
--- a/packages/console/app/src/routes/zen/util/provider/provider.ts
+++ b/packages/console/app/src/routes/zen/util/provider/provider.ts
@@ -26,9 +26,10 @@ import {
export type ProviderHelper = {
format: ZenData.Format
- modifyUrl: (providerApi: string) => string
+ modifyUrl: (providerApi: string, model?: string, isStream?: boolean) => string
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => void
modifyBody: (body: Record<string, any>) => Record<string, any>
+ streamSeparator: string
createUsageParser: () => {
parse: (chunk: string) => void
retrieve: () => any
diff --git a/packages/console/app/src/routes/zen/v1/chat/completions.ts b/packages/console/app/src/routes/zen/v1/chat/completions.ts
index 44326e79e..655459129 100644
--- a/packages/console/app/src/routes/zen/v1/chat/completions.ts
+++ b/packages/console/app/src/routes/zen/v1/chat/completions.ts
@@ -5,5 +5,7 @@ export function POST(input: APIEvent) {
return handler(input, {
format: "oa-compat",
parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1],
+ parseModel: (url: string, body: any) => body.model,
+ parseIsStream: (url: string, body: any) => !!body.stream,
})
}
diff --git a/packages/console/app/src/routes/zen/v1/messages.ts b/packages/console/app/src/routes/zen/v1/messages.ts
index 4478b6444..54d223f95 100644
--- a/packages/console/app/src/routes/zen/v1/messages.ts
+++ b/packages/console/app/src/routes/zen/v1/messages.ts
@@ -5,5 +5,7 @@ export function POST(input: APIEvent) {
return handler(input, {
format: "anthropic",
parseApiKey: (headers: Headers) => headers.get("x-api-key") ?? undefined,
+ parseModel: (url: string, body: any) => body.model,
+ parseIsStream: (url: string, body: any) => !!body.stream,
})
}
diff --git a/packages/console/app/src/routes/zen/v1/models/[model].ts b/packages/console/app/src/routes/zen/v1/models/[model].ts
new file mode 100644
index 000000000..b20378e37
--- /dev/null
+++ b/packages/console/app/src/routes/zen/v1/models/[model].ts
@@ -0,0 +1,13 @@
+import type { APIEvent } from "@solidjs/start/server"
+import { handler } from "~/routes/zen/util/handler"
+
+export function POST(input: APIEvent) {
+ return handler(input, {
+ format: "google",
+ parseApiKey: (headers: Headers) => headers.get("x-goog-api-key") ?? undefined,
+ parseModel: (url: string, body: any) => url.split("/").pop()?.split(":")?.[0] ?? "",
+ parseIsStream: (url: string, body: any) =>
+ // ie. url: https://opencode.ai/zen/v1/models/gemini-3-pro:streamGenerateContent?alt=sse'
+ url.split("/").pop()?.split(":")?.[1]?.startsWith("streamGenerateContent") ?? false,
+ })
+}
diff --git a/packages/console/app/src/routes/zen/v1/responses.ts b/packages/console/app/src/routes/zen/v1/responses.ts
index eadc5bc8e..a82a667cc 100644
--- a/packages/console/app/src/routes/zen/v1/responses.ts
+++ b/packages/console/app/src/routes/zen/v1/responses.ts
@@ -5,5 +5,7 @@ export function POST(input: APIEvent) {
return handler(input, {
format: "openai",
parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1],
+ parseModel: (url: string, body: any) => body.model,
+ parseIsStream: (url: string, body: any) => !!body.stream,
})
}
diff --git a/packages/console/core/src/model.ts b/packages/console/core/src/model.ts
index 222bdd0f8..bff999e61 100644
--- a/packages/console/core/src/model.ts
+++ b/packages/console/core/src/model.ts
@@ -8,7 +8,7 @@ import { Actor } from "./actor"
import { Resource } from "@opencode-ai/console-resource"
export namespace ZenData {
- const FormatSchema = z.enum(["anthropic", "openai", "oa-compat"])
+ const FormatSchema = z.enum(["anthropic", "google", "openai", "oa-compat"])
export type Format = z.infer<typeof FormatSchema>
const ModelCostSchema = z.object({