Skip to content

Commit 7da96b0

Browse files
authored
Merge branch 'dev' into feat/python_sdk
2 parents a7d13df + dfebf40 commit 7da96b0

File tree

36 files changed

+2491
-413
lines changed

36 files changed

+2491
-413
lines changed

STATS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,4 @@
121121
| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) |
122122
| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) |
123123
| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
124+
| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) |

bun.lock

Lines changed: 11 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/console/app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
88
"build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
99
"start": "vinxi start",
10-
"version": "0.15.18"
10+
"version": "0.15.19"
1111
},
1212
"dependencies": {
1313
"@ibm/plex": "6.4.1",
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export class AuthError extends Error {}
2+
export class CreditsError extends Error {}
3+
export class MonthlyLimitError extends Error {}
4+
export class UserLimitError extends Error {}
5+
export class ModelError extends Error {}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type Format = "anthropic" | "openai" | "oa-compat"

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

Lines changed: 58 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,41 @@
1-
import { z } from "zod"
21
import type { APIEvent } from "@solidjs/start/server"
3-
import path from "node:path"
42
import { and, Database, eq, isNull, lt, or, sql } from "@opencode-ai/console-core/drizzle/index.js"
53
import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js"
64
import { BillingTable, UsageTable } from "@opencode-ai/console-core/schema/billing.sql.js"
75
import { centsToMicroCents } from "@opencode-ai/console-core/util/price.js"
86
import { Identifier } from "@opencode-ai/console-core/identifier.js"
9-
import { Resource } from "@opencode-ai/console-resource"
10-
import { Billing } from "../../../../core/src/billing"
7+
import { Billing } from "@opencode-ai/console-core/billing.js"
118
import { Actor } from "@opencode-ai/console-core/actor.js"
129
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
1310
import { ZenData } from "@opencode-ai/console-core/model.js"
1411
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
1512
import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js"
1613
import { ProviderTable } from "@opencode-ai/console-core/schema/provider.sql.js"
14+
import { logger } from "./logger"
15+
import { AuthError, CreditsError, MonthlyLimitError, UserLimitError, ModelError } from "./error"
16+
import { createBodyConverter, createStreamPartConverter, createResponseConverter } from "./provider/provider"
17+
import { Format } from "./format"
18+
import { anthropicHelper } from "./provider/anthropic"
19+
import { openaiHelper } from "./provider/openai"
20+
import { oaCompatHelper } from "./provider/openai-compatible"
21+
22+
type ZenData = Awaited<ReturnType<typeof ZenData.list>>
23+
type Model = ZenData["models"][string]
1724

1825
export async function handler(
1926
input: APIEvent,
2027
opts: {
21-
modifyBody?: (body: any) => any
22-
setAuthHeader: (headers: Headers, apiKey: string) => void
28+
format: Format
2329
parseApiKey: (headers: Headers) => string | undefined
24-
onStreamPart: (chunk: string) => void
25-
getStreamUsage: () => any
26-
normalizeUsage: (body: any) => {
27-
inputTokens: number
28-
outputTokens: number
29-
reasoningTokens?: number
30-
cacheReadTokens?: number
31-
cacheWrite5mTokens?: number
32-
cacheWrite1hTokens?: number
33-
}
3430
},
3531
) {
36-
class AuthError extends Error {}
37-
class CreditsError extends Error {}
38-
class MonthlyLimitError extends Error {}
39-
class UserLimitError extends Error {}
40-
class ModelError extends Error {}
41-
42-
type ZenData = Awaited<ReturnType<typeof ZenData.list>>
43-
type Model = ZenData["models"][string]
44-
4532
const FREE_WORKSPACES = [
4633
"wrk_01K46JDFR0E75SG2Q8K172KF3Y", // frank
4734
"wrk_01K6W1A3VE0KMNVSCQT43BG2SX", // opencode bench
4835
]
4936

50-
const logger = {
51-
metric: (values: Record<string, any>) => {
52-
console.log(`_metric:${JSON.stringify(values)}`)
53-
},
54-
log: console.log,
55-
debug: (message: string) => {
56-
if (Resource.App.stage === "production") return
57-
console.debug(message)
58-
},
59-
}
60-
6137
try {
62-
const url = new URL(input.request.url)
6338
const body = await input.request.json()
64-
logger.debug(JSON.stringify(body))
6539
logger.metric({
6640
is_tream: !!body.stream,
6741
session: input.request.headers.get("x-opencode-session"),
@@ -78,22 +52,28 @@ export async function handler(
7852

7953
// Request to model provider
8054
const startTimestamp = Date.now()
81-
const res = await fetch(path.posix.join(providerInfo.api, url.pathname.replace(/^\/zen\/v1/, "") + url.search), {
55+
const reqUrl = providerInfo.modifyUrl(providerInfo.api)
56+
const reqBody = JSON.stringify(
57+
providerInfo.modifyBody({
58+
...createBodyConverter(opts.format, providerInfo.format)(body),
59+
model: providerInfo.model,
60+
}),
61+
)
62+
logger.debug("REQUEST URL: " + reqUrl)
63+
logger.debug("REQUEST: " + reqBody)
64+
const res = await fetch(reqUrl, {
8265
method: "POST",
8366
headers: (() => {
8467
const headers = input.request.headers
8568
headers.delete("host")
8669
headers.delete("content-length")
87-
opts.setAuthHeader(headers, providerInfo.apiKey)
70+
providerInfo.modifyHeaders(headers, body, providerInfo.apiKey)
8871
Object.entries(providerInfo.headerMappings ?? {}).forEach(([k, v]) => {
8972
headers.set(k, headers.get(v)!)
9073
})
9174
return headers
9275
})(),
93-
body: JSON.stringify({
94-
...(opts.modifyBody?.(body) ?? body),
95-
model: providerInfo.model,
96-
}),
76+
body: reqBody,
9777
})
9878

9979
// Scrub response headers
@@ -104,14 +84,19 @@ export async function handler(
10484
resHeaders.set(k, v)
10585
}
10686
}
87+
logger.debug("STATUS: " + res.status + " " + res.statusText)
88+
if (res.status === 400 || res.status === 503) {
89+
logger.debug("RESPONSE: " + (await res.text()))
90+
}
10791

10892
// Handle non-streaming response
10993
if (!body.stream) {
94+
const responseConverter = createResponseConverter(providerInfo.format, opts.format)
11095
const json = await res.json()
111-
const body = JSON.stringify(json)
96+
const body = JSON.stringify(responseConverter(json))
11297
logger.metric({ response_length: body.length })
113-
logger.debug(body)
114-
await trackUsage(authInfo, modelInfo, providerInfo.id, json.usage)
98+
logger.debug("RESPONSE: " + body)
99+
await trackUsage(authInfo, modelInfo, providerInfo, json.usage)
115100
await reload(authInfo)
116101
return new Response(body, {
117102
status: res.status,
@@ -121,10 +106,13 @@ export async function handler(
121106
}
122107

123108
// Handle streaming response
109+
const streamConverter = createStreamPartConverter(providerInfo.format, opts.format)
110+
const usageParser = providerInfo.createUsageParser()
124111
const stream = new ReadableStream({
125112
start(c) {
126113
const reader = res.body?.getReader()
127114
const decoder = new TextDecoder()
115+
const encoder = new TextEncoder()
128116
let buffer = ""
129117
let responseLength = 0
130118

@@ -136,9 +124,9 @@ export async function handler(
136124
response_length: responseLength,
137125
"timestamp.last_byte": Date.now(),
138126
})
139-
const usage = opts.getStreamUsage()
127+
const usage = usageParser.retrieve()
140128
if (usage) {
141-
await trackUsage(authInfo, modelInfo, providerInfo.id, usage)
129+
await trackUsage(authInfo, modelInfo, providerInfo, usage)
142130
await reload(authInfo)
143131
}
144132
c.close()
@@ -158,12 +146,21 @@ export async function handler(
158146
const parts = buffer.split("\n\n")
159147
buffer = parts.pop() ?? ""
160148

161-
for (const part of parts) {
162-
logger.debug(part)
163-
opts.onStreamPart(part.trim())
149+
for (let part of parts) {
150+
logger.debug("PART: " + part)
151+
152+
part = part.trim()
153+
usageParser.parse(part)
154+
155+
if (providerInfo.format !== opts.format) {
156+
part = streamConverter(part)
157+
c.enqueue(encoder.encode(part + "\n\n"))
158+
}
164159
}
165160

166-
c.enqueue(value)
161+
if (providerInfo.format === opts.format) {
162+
c.enqueue(value)
163+
}
167164

168165
return pump()
169166
}) || Promise.resolve()
@@ -235,7 +232,11 @@ export async function handler(
235232
throw new ModelError(`Provider ${provider.id} not supported`)
236233
}
237234

238-
return { ...provider, ...zenData.providers[provider.id] }
235+
return {
236+
...provider,
237+
...zenData.providers[provider.id],
238+
...(provider.id === "anthropic" ? anthropicHelper : provider.id === "openai" ? openaiHelper : oaCompatHelper),
239+
}
239240
}
240241

241242
async function authenticate(
@@ -356,11 +357,11 @@ export async function handler(
356357
async function trackUsage(
357358
authInfo: Awaited<ReturnType<typeof authenticate>>,
358359
modelInfo: ReturnType<typeof validateModel>,
359-
providerId: string,
360+
providerInfo: Awaited<ReturnType<typeof selectProvider>>,
360361
usage: any,
361362
) {
362363
const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } =
363-
opts.normalizeUsage(usage)
364+
providerInfo.normalizeUsage(usage)
364365

365366
const modelCost =
366367
modelInfo.cost200K &&
@@ -421,7 +422,7 @@ export async function handler(
421422
workspaceID: authInfo.workspaceID,
422423
id: Identifier.create("usage"),
423424
model: modelInfo.id,
424-
provider: providerId,
425+
provider: providerInfo.id,
425426
inputTokens,
426427
outputTokens,
427428
reasoningTokens,
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Resource } from "@opencode-ai/console-resource"
2+
3+
export const logger = {
4+
metric: (values: Record<string, any>) => {
5+
console.log(`_metric:${JSON.stringify(values)}`)
6+
},
7+
log: console.log,
8+
debug: (message: string) => {
9+
if (Resource.App.stage === "production") return
10+
console.debug(message)
11+
},
12+
}

0 commit comments

Comments
 (0)