pull/8821/head
Frank 2026-01-16 01:07:00 -05:00
parent de2de099b4
commit f66e6d7033
16 changed files with 228 additions and 80 deletions

View File

@ -81,6 +81,8 @@
"@opencode-ai/console-mail": "workspace:*", "@opencode-ai/console-mail": "workspace:*",
"@opencode-ai/console-resource": "workspace:*", "@opencode-ai/console-resource": "workspace:*",
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
"@smithy/eventstream-codec": "4.2.7",
"@smithy/util-utf8": "4.2.0",
"@solidjs/meta": "catalog:", "@solidjs/meta": "catalog:",
"@solidjs/router": "catalog:", "@solidjs/router": "catalog:",
"@solidjs/start": "catalog:", "@solidjs/start": "catalog:",
@ -1522,7 +1524,7 @@
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.5", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ=="], "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.5", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ=="],
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.5", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA=="], "@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.7", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DrpkEoM3j9cBBWhufqBwnbbn+3nf1N9FP6xuVJ+e220jbactKuQgaZwjwP5CP1t+O94brm2JgVMD2atMGX3xIQ=="],
"@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.5", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-HohfmCQZjppVnKX2PnXlf47CW3j92Ki6T/vkAT2DhBR47e89pen3s4fIa7otGTtrVxmj7q+IhH0RnC5kpR8wtw=="], "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.5", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-HohfmCQZjppVnKX2PnXlf47CW3j92Ki6T/vkAT2DhBR47e89pen3s4fIa7otGTtrVxmj7q+IhH0RnC5kpR8wtw=="],
@ -3966,6 +3968,8 @@
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.57", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-DREpYqW2pylgaj69gZ+K8u92bo9DaMgFdictYnY+IwYeY3bawQ4zI7l/o1VkDsBDljAx8iYz5lPURwVZNu+Xpg=="], "@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.57", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-DREpYqW2pylgaj69gZ+K8u92bo9DaMgFdictYnY+IwYeY3bawQ4zI7l/o1VkDsBDljAx8iYz5lPURwVZNu+Xpg=="],
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.5", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA=="],
"@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], "@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
"@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "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=="], "@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "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=="],
@ -4236,6 +4240,10 @@
"@slack/web-api/p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="], "@slack/web-api/p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="],
"@smithy/eventstream-codec/@smithy/types": ["@smithy/types@4.11.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA=="],
"@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.5", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA=="],
"@solidjs/start/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], "@solidjs/start/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
"@solidjs/start/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], "@solidjs/start/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],

View File

@ -20,6 +20,8 @@
"@opencode-ai/console-mail": "workspace:*", "@opencode-ai/console-mail": "workspace:*",
"@opencode-ai/console-resource": "workspace:*", "@opencode-ai/console-resource": "workspace:*",
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
"@smithy/eventstream-codec": "4.2.7",
"@smithy/util-utf8": "4.2.0",
"@solidjs/meta": "catalog:", "@solidjs/meta": "catalog:",
"@solidjs/router": "catalog:", "@solidjs/router": "catalog:",
"@solidjs/start": "catalog:", "@solidjs/start": "catalog:",

View File

@ -81,12 +81,13 @@ export async function handler(
const isTrial = await trialLimiter?.isTrial() const isTrial = await trialLimiter?.isTrial()
const rateLimiter = createRateLimiter(modelInfo.rateLimit, ip) const rateLimiter = createRateLimiter(modelInfo.rateLimit, ip)
await rateLimiter?.check() await rateLimiter?.check()
const stickyTracker = createStickyTracker(modelInfo.stickyProvider ?? false, sessionId) const stickyTracker = createStickyTracker(modelInfo.stickyProvider, sessionId)
const stickyProvider = await stickyTracker?.get() const stickyProvider = await stickyTracker?.get()
const authInfo = await authenticate(modelInfo) const authInfo = await authenticate(modelInfo)
const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => { const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => {
const providerInfo = selectProvider( const providerInfo = selectProvider(
model,
zenData, zenData,
authInfo, authInfo,
modelInfo, modelInfo,
@ -101,7 +102,7 @@ export async function handler(
logger.metric({ provider: providerInfo.id }) logger.metric({ provider: providerInfo.id })
const startTimestamp = Date.now() const startTimestamp = Date.now()
const reqUrl = providerInfo.modifyUrl(providerInfo.api, providerInfo.model, isStream) const reqUrl = providerInfo.modifyUrl(providerInfo.api, isStream)
const reqBody = JSON.stringify( const reqBody = JSON.stringify(
providerInfo.modifyBody({ providerInfo.modifyBody({
...createBodyConverter(opts.format, providerInfo.format)(body), ...createBodyConverter(opts.format, providerInfo.format)(body),
@ -135,7 +136,7 @@ export async function handler(
// ie. openai 404 error: Item with id 'msg_0ead8b004a3b165d0069436a6b6834819896da85b63b196a3f' not found. // ie. openai 404 error: Item with id 'msg_0ead8b004a3b165d0069436a6b6834819896da85b63b196a3f' not found.
res.status !== 404 && res.status !== 404 &&
// ie. cannot change codex model providers mid-session // ie. cannot change codex model providers mid-session
!modelInfo.stickyProvider && modelInfo.stickyProvider !== "strict" &&
modelInfo.fallbackProvider && modelInfo.fallbackProvider &&
providerInfo.id !== modelInfo.fallbackProvider providerInfo.id !== modelInfo.fallbackProvider
) { ) {
@ -194,17 +195,19 @@ export async function handler(
// Handle streaming response // Handle streaming response
const streamConverter = createStreamPartConverter(providerInfo.format, opts.format) const streamConverter = createStreamPartConverter(providerInfo.format, opts.format)
const usageParser = providerInfo.createUsageParser() const usageParser = providerInfo.createUsageParser()
const binaryDecoder = providerInfo.createBinaryStreamDecoder()
const stream = new ReadableStream({ const stream = new ReadableStream({
start(c) { start(c) {
const reader = res.body?.getReader() const reader = res.body?.getReader()
const decoder = new TextDecoder() const decoder = new TextDecoder()
const encoder = new TextEncoder() const encoder = new TextEncoder()
let buffer = "" let buffer = ""
let responseLength = 0 let responseLength = 0
function pump(): Promise<void> { function pump(): Promise<void> {
return ( return (
reader?.read().then(async ({ done, value }) => { reader?.read().then(async ({ done, value: rawValue }) => {
if (done) { if (done) {
logger.metric({ logger.metric({
response_length: responseLength, response_length: responseLength,
@ -230,6 +233,10 @@ export async function handler(
"timestamp.first_byte": now, "timestamp.first_byte": now,
}) })
} }
const value = binaryDecoder ? binaryDecoder(rawValue) : rawValue
if (!value) return
responseLength += value.length responseLength += value.length
buffer += decoder.decode(value, { stream: true }) buffer += decoder.decode(value, { stream: true })
dataDumper?.provideStream(buffer) dataDumper?.provideStream(buffer)
@ -331,6 +338,7 @@ export async function handler(
} }
function selectProvider( function selectProvider(
reqModel: string,
zenData: ZenData, zenData: ZenData,
authInfo: AuthInfo, authInfo: AuthInfo,
modelInfo: ModelInfo, modelInfo: ModelInfo,
@ -339,7 +347,7 @@ export async function handler(
retry: RetryOptions, retry: RetryOptions,
stickyProvider: string | undefined, stickyProvider: string | undefined,
) { ) {
const provider = (() => { const modelProvider = (() => {
if (authInfo?.provider?.credentials) { if (authInfo?.provider?.credentials) {
return modelInfo.providers.find((provider) => provider.id === modelInfo.byokProvider) return modelInfo.providers.find((provider) => provider.id === modelInfo.byokProvider)
} }
@ -372,18 +380,19 @@ export async function handler(
return providers[index || 0] return providers[index || 0]
})() })()
if (!provider) throw new ModelError("No provider available") if (!modelProvider) throw new ModelError("No provider available")
if (!(provider.id in zenData.providers)) throw new ModelError(`Provider ${provider.id} not supported`) if (!(modelProvider.id in zenData.providers)) throw new ModelError(`Provider ${modelProvider.id} not supported`)
return { return {
...provider, ...modelProvider,
...zenData.providers[provider.id], ...zenData.providers[modelProvider.id],
...(() => { ...(() => {
const format = zenData.providers[provider.id].format const format = zenData.providers[modelProvider.id].format
if (format === "anthropic") return anthropicHelper const providerModel = modelProvider.model
if (format === "google") return googleHelper if (format === "anthropic") return anthropicHelper({ reqModel, providerModel })
if (format === "openai") return openaiHelper if (format === "google") return googleHelper({ reqModel, providerModel })
return oaCompatHelper if (format === "openai") return openaiHelper({ reqModel, providerModel })
return oaCompatHelper({ reqModel, providerModel })
})(), })(),
} }
} }

View File

@ -1,4 +1,6 @@
import { EventStreamCodec } from "@smithy/eventstream-codec"
import { ProviderHelper, CommonRequest, CommonResponse, CommonChunk } from "./provider" import { ProviderHelper, CommonRequest, CommonResponse, CommonChunk } from "./provider"
import { fromUtf8, toUtf8 } from "@smithy/util-utf8"
type Usage = { type Usage = {
cache_creation?: { cache_creation?: {
@ -14,65 +16,164 @@ type Usage = {
} }
} }
export const anthropicHelper = { export const anthropicHelper: ProviderHelper = ({ reqModel, providerModel }) => {
format: "anthropic", const isBedrockModelArn = providerModel.startsWith("arn:aws:bedrock:")
modifyUrl: (providerApi: string) => providerApi + "/messages", const isBedrockModelID = providerModel.startsWith("global.anthropic.")
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => { const isBedrock = isBedrockModelArn || isBedrockModelID
headers.set("x-api-key", apiKey) const isSonnet = reqModel.includes("sonnet")
headers.set("anthropic-version", headers.get("anthropic-version") ?? "2023-06-01") return {
if (body.model.startsWith("claude-sonnet-")) { format: "anthropic",
headers.set("anthropic-beta", "context-1m-2025-08-07") modifyUrl: (providerApi: string, isStream?: boolean) =>
} isBedrock
}, ? `${providerApi}/model/${isBedrockModelArn ? encodeURIComponent(providerModel) : providerModel}/${isStream ? "invoke-with-response-stream" : "invoke"}`
modifyBody: (body: Record<string, any>) => { : providerApi + "/messages",
return { modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
if (isBedrock) {
headers.set("Authorization", `Bearer ${apiKey}`)
} else {
headers.set("x-api-key", apiKey)
headers.set("anthropic-version", headers.get("anthropic-version") ?? "2023-06-01")
if (body.model.startsWith("claude-sonnet-")) {
headers.set("anthropic-beta", "context-1m-2025-08-07")
}
}
},
modifyBody: (body: Record<string, any>) => ({
...body, ...body,
service_tier: "standard_only", ...(isBedrock
} ? {
}, anthropic_version: "bedrock-2023-05-31",
streamSeparator: "\n\n", anthropic_beta: isSonnet ? "context-1m-2025-08-07" : undefined,
createUsageParser: () => { model: undefined,
let usage: Usage stream: undefined,
}
: {
service_tier: "standard_only",
}),
}),
createBinaryStreamDecoder: () => {
if (!isBedrock) return undefined
return { const decoder = new TextDecoder()
parse: (chunk: string) => { const encoder = new TextEncoder()
const data = chunk.split("\n")[1] const codec = new EventStreamCodec(toUtf8, fromUtf8)
if (!data.startsWith("data: ")) return let buffer = new Uint8Array(0)
return (value: Uint8Array) => {
const newBuffer = new Uint8Array(buffer.length + value.length)
newBuffer.set(buffer)
newBuffer.set(value, buffer.length)
buffer = newBuffer
if (buffer.length < 4) return
// The first 4 bytes are the total length (big-endian).
const totalLength = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength).getUint32(0, false)
// If we don't have the full message yet, wait for more chunks.
if (buffer.length < totalLength) return
let json
try { try {
json = JSON.parse(data.slice(6)) // Decode exactly the sub-slice for this event.
} catch (e) { const subView = buffer.subarray(0, totalLength)
return const decoded = codec.decode(subView)
}
const usageUpdate = json.usage ?? json.message?.usage // Slice the used bytes out of the buffer, removing this message.
if (!usageUpdate) return buffer = buffer.slice(totalLength)
usage = {
...usage, // Process message
...usageUpdate, /* Example of Bedrock data
cache_creation: { ```
...usage?.cache_creation, {
...usageUpdate.cache_creation, bytes: 'eyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtb3B1cy00LTUtMjAyNTExMDEiLCJpZCI6Im1zZ19iZHJrXzAxMjVGdHRGb2lkNGlwWmZ4SzZMbktxeCIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjo0LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjEsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjoxMTk2MywiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MSwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjF9fX0=',
}, p: '...'
server_tool_use: {
...usage?.server_tool_use,
...usageUpdate.server_tool_use,
},
} }
}, ```
retrieve: () => usage,
} Decoded bytes
}, ```
normalizeUsage: (usage: Usage) => ({ {
inputTokens: usage.input_tokens ?? 0, type: 'message_start',
outputTokens: usage.output_tokens ?? 0, message: {
reasoningTokens: undefined, model: 'claude-opus-4-5-20251101',
cacheReadTokens: usage.cache_read_input_tokens ?? undefined, id: 'msg_bdrk_0125FttFoid4ipZfxK6LnKqx',
cacheWrite5mTokens: usage.cache_creation?.ephemeral_5m_input_tokens ?? undefined, type: 'message',
cacheWrite1hTokens: usage.cache_creation?.ephemeral_1h_input_tokens ?? undefined, role: 'assistant',
}), content: [],
} satisfies ProviderHelper stop_reason: null,
stop_sequence: null,
usage: {
input_tokens: 4,
cache_creation_input_tokens: 1,
cache_read_input_tokens: 11963,
cache_creation: [Object],
output_tokens: 1
}
}
}
```
*/
/* Example of Anthropic data
```
event: message_delta
data: {"type":"message_start","message":{"model":"claude-opus-4-5-20251101","id":"msg_01ETvwVWSKULxzPdkQ1xAnk2","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":11543,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":11543,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard"}}}
```
*/
if (decoded.headers[":message-type"]?.value !== "event") return
const data = decoder.decode(decoded.body, { stream: true })
const parsedDataResult = JSON.parse(data)
delete parsedDataResult.p
const utf8 = atob(parsedDataResult.bytes)
return encoder.encode(["event: message_start", "\n", "data: " + utf8, "\n\n"].join(""))
} catch (e) {
console.log(e)
}
}
},
streamSeparator: "\n\n",
createUsageParser: () => {
let usage: Usage
return {
parse: (chunk: string) => {
const data = chunk.split("\n")[1]
if (!data.startsWith("data: ")) return
let json
try {
json = JSON.parse(data.slice(6))
} catch (e) {
return
}
const usageUpdate = json.usage ?? json.message?.usage
if (!usageUpdate) return
usage = {
...usage,
...usageUpdate,
cache_creation: {
...usage?.cache_creation,
...usageUpdate.cache_creation,
},
server_tool_use: {
...usage?.server_tool_use,
...usageUpdate.server_tool_use,
},
}
},
retrieve: () => usage,
}
},
normalizeUsage: (usage: Usage) => ({
inputTokens: usage.input_tokens ?? 0,
outputTokens: usage.output_tokens ?? 0,
reasoningTokens: undefined,
cacheReadTokens: usage.cache_read_input_tokens ?? undefined,
cacheWrite5mTokens: usage.cache_creation?.ephemeral_5m_input_tokens ?? undefined,
cacheWrite1hTokens: usage.cache_creation?.ephemeral_1h_input_tokens ?? undefined,
}),
}
}
export function fromAnthropicRequest(body: any): CommonRequest { export function fromAnthropicRequest(body: any): CommonRequest {
if (!body || typeof body !== "object") return body if (!body || typeof body !== "object") return body

View File

@ -26,16 +26,17 @@ type Usage = {
thoughtsTokenCount?: number thoughtsTokenCount?: number
} }
export const googleHelper = { export const googleHelper: ProviderHelper = ({ providerModel }) => ({
format: "google", format: "google",
modifyUrl: (providerApi: string, model?: string, isStream?: boolean) => modifyUrl: (providerApi: string, isStream?: boolean) =>
`${providerApi}/models/${model}:${isStream ? "streamGenerateContent?alt=sse" : "generateContent"}`, `${providerApi}/models/${providerModel}:${isStream ? "streamGenerateContent?alt=sse" : "generateContent"}`,
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => { modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
headers.set("x-goog-api-key", apiKey) headers.set("x-goog-api-key", apiKey)
}, },
modifyBody: (body: Record<string, any>) => { modifyBody: (body: Record<string, any>) => {
return body return body
}, },
createBinaryStreamDecoder: () => undefined,
streamSeparator: "\r\n\r\n", streamSeparator: "\r\n\r\n",
createUsageParser: () => { createUsageParser: () => {
let usage: Usage let usage: Usage
@ -71,4 +72,4 @@ export const googleHelper = {
cacheWrite1hTokens: undefined, cacheWrite1hTokens: undefined,
} }
}, },
} satisfies ProviderHelper })

View File

@ -21,7 +21,7 @@ type Usage = {
} }
} }
export const oaCompatHelper = { export const oaCompatHelper: ProviderHelper = () => ({
format: "oa-compat", format: "oa-compat",
modifyUrl: (providerApi: string) => providerApi + "/chat/completions", modifyUrl: (providerApi: string) => providerApi + "/chat/completions",
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => { modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
@ -33,6 +33,7 @@ export const oaCompatHelper = {
...(body.stream ? { stream_options: { include_usage: true } } : {}), ...(body.stream ? { stream_options: { include_usage: true } } : {}),
} }
}, },
createBinaryStreamDecoder: () => undefined,
streamSeparator: "\n\n", streamSeparator: "\n\n",
createUsageParser: () => { createUsageParser: () => {
let usage: Usage let usage: Usage
@ -68,7 +69,7 @@ export const oaCompatHelper = {
cacheWrite1hTokens: undefined, cacheWrite1hTokens: undefined,
} }
}, },
} satisfies ProviderHelper })
export function fromOaCompatibleRequest(body: any): CommonRequest { export function fromOaCompatibleRequest(body: any): CommonRequest {
if (!body || typeof body !== "object") return body if (!body || typeof body !== "object") return body

View File

@ -12,7 +12,7 @@ type Usage = {
total_tokens?: number total_tokens?: number
} }
export const openaiHelper = { export const openaiHelper: ProviderHelper = () => ({
format: "openai", format: "openai",
modifyUrl: (providerApi: string) => providerApi + "/responses", modifyUrl: (providerApi: string) => providerApi + "/responses",
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => { modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
@ -21,6 +21,7 @@ export const openaiHelper = {
modifyBody: (body: Record<string, any>) => { modifyBody: (body: Record<string, any>) => {
return body return body
}, },
createBinaryStreamDecoder: () => undefined,
streamSeparator: "\n\n", streamSeparator: "\n\n",
createUsageParser: () => { createUsageParser: () => {
let usage: Usage let usage: Usage
@ -58,7 +59,7 @@ export const openaiHelper = {
cacheWrite1hTokens: undefined, cacheWrite1hTokens: undefined,
} }
}, },
} satisfies ProviderHelper })
export function fromOpenaiRequest(body: any): CommonRequest { export function fromOpenaiRequest(body: any): CommonRequest {
if (!body || typeof body !== "object") return body if (!body || typeof body !== "object") return body

View File

@ -33,11 +33,12 @@ export type UsageInfo = {
cacheWrite1hTokens?: number cacheWrite1hTokens?: number
} }
export type ProviderHelper = { export type ProviderHelper = (input: { reqModel: string; providerModel: string }) => {
format: ZenData.Format format: ZenData.Format
modifyUrl: (providerApi: string, model?: string, isStream?: boolean) => string modifyUrl: (providerApi: string, isStream?: boolean) => string
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => void modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => void
modifyBody: (body: Record<string, any>) => Record<string, any> modifyBody: (body: Record<string, any>) => Record<string, any>
createBinaryStreamDecoder: () => ((chunk: Uint8Array) => Uint8Array | undefined) | undefined
streamSeparator: string streamSeparator: string
createUsageParser: () => { createUsageParser: () => {
parse: (chunk: string) => void parse: (chunk: string) => void

View File

@ -1,6 +1,6 @@
import { Resource } from "@opencode-ai/console-resource" import { Resource } from "@opencode-ai/console-resource"
export function createStickyTracker(stickyProvider: boolean, session: string) { export function createStickyTracker(stickyProvider: "strict" | "prefer" | undefined, session: string) {
if (!stickyProvider) return if (!stickyProvider) return
if (!session) return if (!session) return
const key = `sticky:${session}` const key = `sticky:${session}`

View File

@ -35,7 +35,7 @@ export namespace ZenData {
cost200K: ModelCostSchema.optional(), cost200K: ModelCostSchema.optional(),
allowAnonymous: z.boolean().optional(), allowAnonymous: z.boolean().optional(),
byokProvider: z.enum(["openai", "anthropic", "google"]).optional(), byokProvider: z.enum(["openai", "anthropic", "google"]).optional(),
stickyProvider: z.boolean().optional(), stickyProvider: z.enum(["strict", "prefer"]).optional(),
trial: TrialSchema.optional(), trial: TrialSchema.optional(),
rateLimit: z.number().optional(), rateLimit: z.number().optional(),
fallbackProvider: z.string().optional(), fallbackProvider: z.string().optional(),

View File

@ -134,6 +134,10 @@ declare module "sst" {
"type": "sst.sst.Secret" "type": "sst.sst.Secret"
"value": string "value": string
} }
"ZEN_MODELS8": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_SESSION_SECRET": { "ZEN_SESSION_SECRET": {
"type": "sst.sst.Secret" "type": "sst.sst.Secret"
"value": string "value": string

View File

@ -134,6 +134,10 @@ declare module "sst" {
"type": "sst.sst.Secret" "type": "sst.sst.Secret"
"value": string "value": string
} }
"ZEN_MODELS8": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_SESSION_SECRET": { "ZEN_SESSION_SECRET": {
"type": "sst.sst.Secret" "type": "sst.sst.Secret"
"value": string "value": string

View File

@ -134,6 +134,10 @@ declare module "sst" {
"type": "sst.sst.Secret" "type": "sst.sst.Secret"
"value": string "value": string
} }
"ZEN_MODELS8": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_SESSION_SECRET": { "ZEN_SESSION_SECRET": {
"type": "sst.sst.Secret" "type": "sst.sst.Secret"
"value": string "value": string

View File

@ -134,6 +134,10 @@ declare module "sst" {
"type": "sst.sst.Secret" "type": "sst.sst.Secret"
"value": string "value": string
} }
"ZEN_MODELS8": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_SESSION_SECRET": { "ZEN_SESSION_SECRET": {
"type": "sst.sst.Secret" "type": "sst.sst.Secret"
"value": string "value": string

View File

@ -134,6 +134,10 @@ declare module "sst" {
"type": "sst.sst.Secret" "type": "sst.sst.Secret"
"value": string "value": string
} }
"ZEN_MODELS8": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_SESSION_SECRET": { "ZEN_SESSION_SECRET": {
"type": "sst.sst.Secret" "type": "sst.sst.Secret"
"value": string "value": string

4
sst-env.d.ts vendored
View File

@ -160,6 +160,10 @@ declare module "sst" {
"type": "sst.sst.Secret" "type": "sst.sst.Secret"
"value": string "value": string
} }
"ZEN_MODELS8": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_SESSION_SECRET": { "ZEN_SESSION_SECRET": {
"type": "sst.sst.Secret" "type": "sst.sst.Secret"
"value": string "value": string