Skip to content
This repository was archived by the owner on Apr 1, 2026. It is now read-only.

Commit 7283bfa

Browse files
author
Frank
committed
zen: gemini
1 parent 37d5099 commit 7283bfa

11 files changed

Lines changed: 111 additions & 7 deletions

File tree

packages/console/app/src/routes/zen/util/handler.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { logger } from "./logger"
1515
import { AuthError, CreditsError, MonthlyLimitError, UserLimitError, ModelError, RateLimitError } from "./error"
1616
import { createBodyConverter, createStreamPartConverter, createResponseConverter } from "./provider/provider"
1717
import { anthropicHelper } from "./provider/anthropic"
18+
import { googleHelper } from "./provider/google"
1819
import { openaiHelper } from "./provider/openai"
1920
import { oaCompatHelper } from "./provider/openai-compatible"
2021
import { createRateLimiter } from "./rateLimiter"
@@ -30,6 +31,8 @@ export async function handler(
3031
opts: {
3132
format: ZenData.Format
3233
parseApiKey: (headers: Headers) => string | undefined
34+
parseModel: (url: string, body: any) => string
35+
parseIsStream: (url: string, body: any) => boolean
3336
},
3437
) {
3538
type AuthInfo = Awaited<ReturnType<typeof authenticate>>
@@ -43,15 +46,18 @@ export async function handler(
4346
]
4447

4548
try {
49+
const url = input.request.url
4650
const body = await input.request.json()
4751
const ip = input.request.headers.get("x-real-ip") ?? ""
52+
const model = opts.parseModel(url, body)
53+
const isStream = opts.parseIsStream(url, body)
4854
logger.metric({
49-
is_tream: !!body.stream,
55+
is_tream: isStream,
5056
session: input.request.headers.get("x-opencode-session"),
5157
request: input.request.headers.get("x-opencode-request"),
5258
})
5359
const zenData = ZenData.list()
54-
const modelInfo = validateModel(zenData, body.model)
60+
const modelInfo = validateModel(zenData, model)
5561
const rateLimiter = createRateLimiter(modelInfo.id, modelInfo.rateLimit, ip)
5662
await rateLimiter?.check()
5763

@@ -64,7 +70,7 @@ export async function handler(
6470
logger.metric({ provider: providerInfo.id })
6571

6672
const startTimestamp = Date.now()
67-
const reqUrl = providerInfo.modifyUrl(providerInfo.api)
73+
const reqUrl = providerInfo.modifyUrl(providerInfo.api, providerInfo.model, isStream)
6874
const reqBody = JSON.stringify(
6975
providerInfo.modifyBody({
7076
...createBodyConverter(opts.format, providerInfo.format)(body),
@@ -114,7 +120,7 @@ export async function handler(
114120
logger.debug("STATUS: " + res.status + " " + res.statusText)
115121

116122
// Handle non-streaming response
117-
if (!body.stream) {
123+
if (!isStream) {
118124
const responseConverter = createResponseConverter(providerInfo.format, opts.format)
119125
const json = await res.json()
120126
const body = JSON.stringify(responseConverter(json))
@@ -169,7 +175,7 @@ export async function handler(
169175
responseLength += value.length
170176
buffer += decoder.decode(value, { stream: true })
171177

172-
const parts = buffer.split("\n\n")
178+
const parts = buffer.split(providerInfo.streamSeparator)
173179
buffer = parts.pop() ?? ""
174180

175181
for (let part of parts) {
@@ -283,6 +289,7 @@ export async function handler(
283289
...(() => {
284290
const format = zenData.providers[provider.id].format
285291
if (format === "anthropic") return anthropicHelper
292+
if (format === "google") return googleHelper
286293
if (format === "openai") return openaiHelper
287294
return oaCompatHelper
288295
})(),

packages/console/app/src/routes/zen/util/provider/anthropic.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export const anthropicHelper = {
3030
service_tier: "standard_only",
3131
}
3232
},
33+
streamSeparator: "\n\n",
3334
createUsageParser: () => {
3435
let usage: Usage
3536

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { ProviderHelper } from "./provider"
2+
3+
/*
4+
{
5+
promptTokenCount: 11453,
6+
candidatesTokenCount: 71,
7+
totalTokenCount: 11625,
8+
cachedContentTokenCount: 8100,
9+
promptTokensDetails: [
10+
{modality: "TEXT",tokenCount: 11453}
11+
],
12+
cacheTokensDetails: [
13+
{modality: "TEXT",tokenCount: 8100}
14+
],
15+
thoughtsTokenCount: 101
16+
}
17+
*/
18+
19+
type Usage = {
20+
promptTokenCount?: number
21+
candidatesTokenCount?: number
22+
totalTokenCount?: number
23+
cachedContentTokenCount?: number
24+
promptTokensDetails?: { modality: string; tokenCount: number }[]
25+
cacheTokensDetails?: { modality: string; tokenCount: number }[]
26+
thoughtsTokenCount?: number
27+
}
28+
29+
export const googleHelper = {
30+
format: "google",
31+
modifyUrl: (providerApi: string, model?: string, isStream?: boolean) =>
32+
`${providerApi}/models/${model}:${isStream ? "streamGenerateContent?alt=sse" : "generateContent"}`,
33+
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
34+
headers.set("x-goog-api-key", apiKey)
35+
},
36+
modifyBody: (body: Record<string, any>) => {
37+
return body
38+
},
39+
streamSeparator: "\r\n\r\n",
40+
createUsageParser: () => {
41+
let usage: Usage
42+
43+
return {
44+
parse: (chunk: string) => {
45+
if (!chunk.startsWith("data: ")) return
46+
47+
let json
48+
try {
49+
json = JSON.parse(chunk.slice(6)) as { usageMetadata?: Usage }
50+
} catch (e) {
51+
return
52+
}
53+
54+
if (!json.usageMetadata) return
55+
usage = json.usageMetadata
56+
},
57+
retrieve: () => usage,
58+
}
59+
},
60+
normalizeUsage: (usage: Usage) => {
61+
const inputTokens = usage.promptTokenCount ?? 0
62+
const outputTokens = usage.candidatesTokenCount ?? 0
63+
const reasoningTokens = usage.thoughtsTokenCount ?? 0
64+
const cacheReadTokens = usage.cachedContentTokenCount ?? 0
65+
return {
66+
inputTokens: inputTokens - cacheReadTokens,
67+
outputTokens,
68+
reasoningTokens,
69+
cacheReadTokens,
70+
cacheWrite5mTokens: undefined,
71+
cacheWrite1hTokens: undefined,
72+
}
73+
},
74+
} satisfies ProviderHelper

packages/console/app/src/routes/zen/util/provider/openai-compatible.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export const oaCompatHelper = {
3333
...(body.stream ? { stream_options: { include_usage: true } } : {}),
3434
}
3535
},
36+
streamSeparator: "\n\n",
3637
createUsageParser: () => {
3738
let usage: Usage
3839

packages/console/app/src/routes/zen/util/provider/openai.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const openaiHelper = {
2121
modifyBody: (body: Record<string, any>) => {
2222
return body
2323
},
24+
streamSeparator: "\n\n",
2425
createUsageParser: () => {
2526
let usage: Usage
2627

packages/console/app/src/routes/zen/util/provider/provider.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ import {
2626

2727
export type ProviderHelper = {
2828
format: ZenData.Format
29-
modifyUrl: (providerApi: string) => string
29+
modifyUrl: (providerApi: string, model?: string, isStream?: boolean) => string
3030
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => void
3131
modifyBody: (body: Record<string, any>) => Record<string, any>
32+
streamSeparator: string
3233
createUsageParser: () => {
3334
parse: (chunk: string) => void
3435
retrieve: () => any

packages/console/app/src/routes/zen/v1/chat/completions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,7 @@ export function POST(input: APIEvent) {
55
return handler(input, {
66
format: "oa-compat",
77
parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1],
8+
parseModel: (url: string, body: any) => body.model,
9+
parseIsStream: (url: string, body: any) => !!body.stream,
810
})
911
}

packages/console/app/src/routes/zen/v1/messages.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,7 @@ export function POST(input: APIEvent) {
55
return handler(input, {
66
format: "anthropic",
77
parseApiKey: (headers: Headers) => headers.get("x-api-key") ?? undefined,
8+
parseModel: (url: string, body: any) => body.model,
9+
parseIsStream: (url: string, body: any) => !!body.stream,
810
})
911
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { APIEvent } from "@solidjs/start/server"
2+
import { handler } from "~/routes/zen/util/handler"
3+
4+
export function POST(input: APIEvent) {
5+
return handler(input, {
6+
format: "google",
7+
parseApiKey: (headers: Headers) => headers.get("x-goog-api-key") ?? undefined,
8+
parseModel: (url: string, body: any) => url.split("/").pop()?.split(":")?.[0] ?? "",
9+
parseIsStream: (url: string, body: any) =>
10+
// ie. url: https://opencode.ai/zen/v1/models/gemini-3-pro:streamGenerateContent?alt=sse'
11+
url.split("/").pop()?.split(":")?.[1]?.startsWith("streamGenerateContent") ?? false,
12+
})
13+
}

packages/console/app/src/routes/zen/v1/responses.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,7 @@ export function POST(input: APIEvent) {
55
return handler(input, {
66
format: "openai",
77
parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1],
8+
parseModel: (url: string, body: any) => body.model,
9+
parseIsStream: (url: string, body: any) => !!body.stream,
810
})
911
}

0 commit comments

Comments
 (0)