diff --git a/apis/cloudflare/package.json b/apis/cloudflare/package.json index 8289c4ae..b5812a52 100644 --- a/apis/cloudflare/package.json +++ b/apis/cloudflare/package.json @@ -26,6 +26,6 @@ "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-metrics": "^2.1.0", "dotenv": "^16.3.1", - "zod": "3.25.34" + "zod": "4.2.1" } } diff --git a/apis/vercel/next-env.d.ts b/apis/vercel/next-env.d.ts index fd36f949..725dd6f2 100644 --- a/apis/vercel/next-env.d.ts +++ b/apis/vercel/next-env.d.ts @@ -3,4 +3,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/package.json b/package.json index 9d752ccd..1f4d4a97 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,6 @@ }, "packageManager": "pnpm@8.15.5", "resolutions": { - "zod": "3.25.34" + "zod": "4.2.1" } } diff --git a/packages/proxy/package.json b/packages/proxy/package.json index 8d4e1ef0..0dd82400 100644 --- a/packages/proxy/package.json +++ b/packages/proxy/package.json @@ -120,6 +120,6 @@ "openai": "4.104.0", "openapi-json-schema": "^2.0.0", "uuid": "^9.0.1", - "zod": "^3.25.34" + "zod": "4.2.1" } } diff --git a/packages/proxy/schema/audio.ts b/packages/proxy/schema/audio.ts index 15c636f7..74bd1aa7 100644 --- a/packages/proxy/schema/audio.ts +++ b/packages/proxy/schema/audio.ts @@ -25,8 +25,8 @@ export const pcmAudioFormatSchema = z .discriminatedUnion("name", [ z.object({ name: z.literal("pcm"), - byte_order: z.enum(["little", "big"]).default("little"), - number_encoding: z.enum(["int", "float"]).default("int"), + byte_order: z.enum(["little", "big"]).prefault("little"), + number_encoding: z.enum(["int", "float"]).prefault("int"), bits_per_sample: z.union([z.literal(8), z.literal(16)]), }), z.object({ diff --git a/packages/proxy/schema/index.ts b/packages/proxy/schema/index.ts index ee93455e..f7bafea5 100644 --- a/packages/proxy/schema/index.ts +++ b/packages/proxy/schema/index.ts @@ -690,14 +690,14 @@ export const anthropicSupportedMediaTypes = [ export const anthropicTextBlockSchema = z.object({ type: z.literal("text").optional(), - text: z.string().default(""), + text: z.string().prefault(""), }); export const anthropicImageBlockSchema = z.object({ type: z.literal("image").optional(), source: z.object({ type: z.enum(["base64"]).optional(), media_type: z.enum(["image/jpeg", "image/png", "image/gif", "image/webp"]), - data: z.string().default(""), + data: z.string().prefault(""), }), }); const anthropicContentBlockSchema = z.union([ @@ -706,7 +706,7 @@ const anthropicContentBlockSchema = z.union([ ]); const anthropicContentBlocksSchema = z.array(anthropicContentBlockSchema); const anthropicContentSchema = z.union([ - z.string().default(""), + z.string().prefault(""), anthropicContentBlocksSchema, ]); diff --git a/packages/proxy/schema/models.test.ts b/packages/proxy/schema/models.test.ts index d6d22400..de148633 100644 --- a/packages/proxy/schema/models.test.ts +++ b/packages/proxy/schema/models.test.ts @@ -5,7 +5,7 @@ import { ModelSchema } from "./models"; import { z } from "zod"; it("parse model list", () => { - const models = z.record(z.unknown()).parse(raw_models); + const models = z.record(z.string(), z.unknown()).parse(raw_models); for (const [key, value] of Object.entries(models)) { const result = ModelSchema.safeParse(value); if (!result.success) { diff --git a/packages/proxy/schema/models.ts b/packages/proxy/schema/models.ts index 1e0e8e08..c84936e4 100644 --- a/packages/proxy/schema/models.ts +++ b/packages/proxy/schema/models.ts @@ -87,14 +87,19 @@ export const ModelSchema = z.object({ export type ModelSpec = z.infer; import modelListJson from "./model_list.json"; -const modelListJsonTyped = z.record(ModelSchema).parse(modelListJson); +const modelListJsonTyped = z + .record(z.string(), ModelSchema) + .parse(modelListJson); // Because this file can be included and bundled in various ways, it's important to // really inject these variables into the global scope, rather than let the bundler // have its way with them. declare global { + // eslint-disable-next-line no-var var _proxy_availableModels: { [name: string]: ModelSpec } | undefined; + // eslint-disable-next-line no-var var _proxy_cachedModels: { [name: string]: ModelSpec } | null; + // eslint-disable-next-line no-var var _proxy_cacheTimestamp: number | null; } @@ -158,6 +163,7 @@ async function loadModelsFromControlPlane( throw new Error(`Failed to fetch models: ${response.statusText}`); } const data = await response.json(); + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions globalThis._proxy_cachedModels = data as { [name: string]: ModelSpec }; globalThis._proxy_cacheTimestamp = Date.now(); } catch (error) { diff --git a/packages/proxy/schema/openai-realtime.ts b/packages/proxy/schema/openai-realtime.ts index d0a3f745..e6c8d202 100644 --- a/packages/proxy/schema/openai-realtime.ts +++ b/packages/proxy/schema/openai-realtime.ts @@ -29,7 +29,7 @@ export const toolDefinitionTypeSchema = z.object({ type: z.literal("function").optional(), name: z.string(), description: z.string(), - parameters: z.record(z.unknown()), + parameters: z.record(z.string(), z.unknown()), }); export const sessionResourceTypeSchema = z.object({ diff --git a/packages/proxy/schema/secrets.ts b/packages/proxy/schema/secrets.ts index 4ab582b8..2d8d9cf8 100644 --- a/packages/proxy/schema/secrets.ts +++ b/packages/proxy/schema/secrets.ts @@ -1,30 +1,26 @@ import { z } from "zod"; import { ModelSchema } from "./models"; -export const BaseMetadataSchema = z - .object({ - models: z.array(z.string()).nullish(), - customModels: z.record(ModelSchema).nullish(), - excludeDefaultModels: z.boolean().nullish(), - additionalHeaders: z.record(z.string(), z.string()).nullish(), - supportsStreaming: z.boolean().default(true), - }) - .strict(); - -export const AzureMetadataSchema = BaseMetadataSchema.merge( - z.object({ - api_base: z.string().url(), - api_version: z.string().default("2023-07-01-preview"), - deployment: z.string().nullish(), - auth_type: z.enum(["api_key", "entra_api"]).default("api_key"), - no_named_deployment: z - .boolean() - .default(false) - .describe( - "If true, the deployment name will not be used in the request path.", - ), - }), -).strict(); +export const BaseMetadataSchema = z.strictObject({ + models: z.array(z.string()).nullish(), + customModels: z.record(z.string(), ModelSchema).nullish(), + excludeDefaultModels: z.boolean().nullish(), + additionalHeaders: z.record(z.string(), z.string()).nullish(), + supportsStreaming: z.boolean().prefault(true), +}); + +export const AzureMetadataSchema = BaseMetadataSchema.extend({ + api_base: z.url(), + api_version: z.string().prefault("2023-07-01-preview"), + deployment: z.string().nullish(), + auth_type: z.enum(["api_key", "entra_api"]).prefault("api_key"), + no_named_deployment: z + .boolean() + .prefault(false) + .describe( + "If true, the deployment name will not be used in the request path.", + ), +}); export const AzureEntraSecretSchema = z.object({ client_id: z.string().min(1, "Client ID cannot be empty"), @@ -34,30 +30,24 @@ export const AzureEntraSecretSchema = z.object({ }); export type AzureEntraSecret = z.infer; -export const BedrockMetadataSchema = BaseMetadataSchema.merge( - z.object({ - region: z.string().min(1, "Region cannot be empty"), - access_key: z.string().min(1, "Access key cannot be empty"), - session_token: z.string().nullish(), - api_base: z.union([z.string().url(), z.string().length(0)]).nullish(), - }), -).strict(); +export const BedrockMetadataSchema = BaseMetadataSchema.extend({ + region: z.string().min(1, "Region cannot be empty"), + access_key: z.string().min(1, "Access key cannot be empty"), + session_token: z.string().nullish(), + api_base: z.union([z.url(), z.string().length(0)]).nullish(), +}); export type BedrockMetadata = z.infer; -export const VertexMetadataSchema = BaseMetadataSchema.merge( - z.object({ - project: z.string().min(1, "Project cannot be empty"), - authType: z.enum(["access_token", "service_account_key"]), - api_base: z.union([z.string().url(), z.string().length(0)]).nullish(), - }), -).strict(); +export const VertexMetadataSchema = BaseMetadataSchema.extend({ + project: z.string().min(1, "Project cannot be empty"), + authType: z.enum(["access_token", "service_account_key"]), + api_base: z.union([z.url(), z.string().length(0)]).nullish(), +}); -export const DatabricksMetadataSchema = BaseMetadataSchema.merge( - z.object({ - api_base: z.string().url(), - auth_type: z.enum(["pat", "service_principal_oauth"]).default("pat"), - }), -).strict(); +export const DatabricksMetadataSchema = BaseMetadataSchema.extend({ + api_base: z.url(), + auth_type: z.enum(["pat", "service_principal_oauth"]).prefault("pat"), +}); export const DatabricksOAuthSecretSchema = z.object({ client_id: z.string().min(1, "Client ID cannot be empty"), @@ -65,90 +55,66 @@ export const DatabricksOAuthSecretSchema = z.object({ }); export type DatabricksOAuthSecret = z.infer; -export const OpenAIMetadataSchema = BaseMetadataSchema.merge( - z.object({ - api_base: z.union([ - z.string().url().optional(), - z.string().length(0), - z.null(), - ]), - organization_id: z.string().nullish(), - }), -).strict(); +export const OpenAIMetadataSchema = BaseMetadataSchema.extend({ + api_base: z.union([z.url().optional(), z.string().length(0), z.null()]), + organization_id: z.string().nullish(), +}); -export const MistralMetadataSchema = BaseMetadataSchema.merge( - z.object({ - api_base: z.union([z.string().url(), z.string().length(0)]).nullish(), - }), -).strict(); +export const MistralMetadataSchema = BaseMetadataSchema.extend({ + api_base: z.union([z.url(), z.string().length(0)]).nullish(), +}); -const APISecretBaseSchema = z - .object({ - id: z.string().uuid().nullish(), - org_name: z.string().nullish(), - name: z.string().nullish(), - secret: z.string(), - metadata: z.record(z.unknown()).nullish(), - }) - .strict(); +const APISecretBaseSchema = z.strictObject({ + id: z.string().uuid().nullish(), + org_name: z.string().nullish(), + name: z.string().nullish(), + secret: z.string(), + metadata: z.record(z.string(), z.unknown()).nullish(), +}); export const APISecretSchema = z.union([ - APISecretBaseSchema.merge( - z.object({ - type: z.enum([ - "perplexity", - "anthropic", - "google", - "replicate", - "together", - "baseten", - "ollama", - "groq", - "lepton", - "fireworks", - "cerebras", - "xAI", - "js", - ]), - metadata: BaseMetadataSchema.nullish(), - }), - ), - APISecretBaseSchema.merge( - z.object({ - type: z.literal("openai"), - metadata: OpenAIMetadataSchema.nullish(), - }), - ), - APISecretBaseSchema.merge( - z.object({ - type: z.literal("azure"), - metadata: AzureMetadataSchema.nullish(), - }), - ), - APISecretBaseSchema.merge( - z.object({ - type: z.literal("bedrock"), - metadata: BedrockMetadataSchema.nullish(), - }), - ), - APISecretBaseSchema.merge( - z.object({ - type: z.literal("vertex"), - metadata: VertexMetadataSchema.nullish(), - }), - ), - APISecretBaseSchema.merge( - z.object({ - type: z.literal("databricks"), - metadata: DatabricksMetadataSchema.nullish(), - }), - ), - APISecretBaseSchema.merge( - z.object({ - type: z.literal("mistral"), - metadata: MistralMetadataSchema.nullish(), - }), - ), + APISecretBaseSchema.extend({ + type: z.enum([ + "perplexity", + "anthropic", + "google", + "replicate", + "together", + "baseten", + "ollama", + "groq", + "lepton", + "fireworks", + "cerebras", + "xAI", + "js", + ]), + metadata: BaseMetadataSchema.nullish(), + }), + APISecretBaseSchema.extend({ + type: z.literal("openai"), + metadata: OpenAIMetadataSchema.nullish(), + }), + APISecretBaseSchema.extend({ + type: z.literal("azure"), + metadata: AzureMetadataSchema.nullish(), + }), + APISecretBaseSchema.extend({ + type: z.literal("bedrock"), + metadata: BedrockMetadataSchema.nullish(), + }), + APISecretBaseSchema.extend({ + type: z.literal("vertex"), + metadata: VertexMetadataSchema.nullish(), + }), + APISecretBaseSchema.extend({ + type: z.literal("databricks"), + metadata: DatabricksMetadataSchema.nullish(), + }), + APISecretBaseSchema.extend({ + type: z.literal("mistral"), + metadata: MistralMetadataSchema.nullish(), + }), ]); export type APISecret = z.infer; @@ -157,10 +123,10 @@ export const proxyLoggingParamSchema = z .object({ parent: z.string().optional(), project_name: z.string().optional(), - compress_audio: z.boolean().default(true), + compress_audio: z.boolean().prefault(true), }) .refine((data) => data.parent || data.project_name, { - message: "Either 'parent' or 'project_name' must be provided", + error: "Either 'parent' or 'project_name' must be provided", }) .describe( "If present, proxy will log requests to the given Braintrust project or parent span.", @@ -179,7 +145,7 @@ export const credentialsRequestSchema = z ttl_seconds: z .number() .max(60 * 60 * 24) - .default(60 * 10) + .prefault(60 * 10) .describe("TTL of the temporary credential. 10 minutes by default."), logging: proxyLoggingParamSchema.nullish(), }) diff --git a/packages/proxy/scripts/generate_types.ts b/packages/proxy/scripts/generate_types.ts index c6448c37..f8ab625b 100644 --- a/packages/proxy/scripts/generate_types.ts +++ b/packages/proxy/scripts/generate_types.ts @@ -5,6 +5,7 @@ import { } from "openapi-zod-client"; import * as fs from "fs/promises"; import path from "node:path"; +import * as ts from "typescript"; const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); const OPENAPI_SPEC_PATH = path.join(SCRIPT_DIR, "../generated_types.json"); @@ -17,6 +18,7 @@ const OUTPUT_PATH = path.join(SCRIPT_DIR, "../src/generated_types.ts"); async function main() { const openApiDoc = JSON.parse(await fs.readFile(OPENAPI_SPEC_PATH, "utf-8")); const handlebars = getHandlebars(); + // only outputs as zod v3, support for v4 still in progress. await generateZodClientFromOpenAPI({ openApiDoc, templatePath: TEMPLATE_PATH, @@ -28,11 +30,302 @@ async function main() { additionalPropertiesDefaultValue: false, }, }); + // Read generated code. Optionally skip post-processing when debugging raw output. + let code = await fs.readFile(OUTPUT_PATH, "utf8"); + + // If SKIP_CODMOD=1 is set in the environment, write the generated file as-is + // (with the generated banner) and exit. Useful to inspect upstream generator output + // before applying our v3->v4 codemod. + if (process.env.SKIP_CODMOD === "1") { + const internalGitSha = openApiDoc.info["x-internal-git-sha"] || "UNKNOWN"; + const banner = `// Auto-generated file (internal git SHA ${internalGitSha}) -- do not modify\n\n`; + await fs.writeFile(OUTPUT_PATH, banner + code); + return; + } + + const fixGenerated = (s: string) => { + // Robust AST transform using TypeScript Transformer API. This reliably + // rewrites single-arg `z.record(valueSchema)` into `z.record(z.string(), valueSchema)` + // and unwraps parenthesized expressions. If anything goes wrong, fall back to the cleaned string. + try { + const sf = ts.createSourceFile( + "generated_types.ts", + s, + ts.ScriptTarget.Latest, + /*setParentNodes*/ true, + ts.ScriptKind.TS, + ); + + const isZCall = (expr: ts.Expression, name: string) => + ts.isPropertyAccessExpression(expr) && + ts.isIdentifier(expr.expression) && + expr.expression.text === "z" && + expr.name.text === name; + + const makeZCallExpr = (name: string) => + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier("z"), + name, + ), + undefined, + [], + ); + + const transformer: ts.TransformerFactory = (context) => { + const visit: ts.Visitor = (node) => { + if (ts.isCallExpression(node)) { + const expr = node.expression; + + // Handle z.record(...) general case + if (isZCall(expr, "record")) { + const args = node.arguments.slice(); + if (args.length === 1) { + // single-arg -> insert z.string() as key schema + const valueArg = ts.isParenthesizedExpression(args[0]) + ? args[0].expression + : args[0]; + return ts.factory.updateCallExpression( + node, + node.expression, + node.typeArguments, + [makeZCallExpr("string"), valueArg], + ); + } + + // If call already has 2+ args, just unwrap parentheses on second arg + if (args.length >= 2) { + const second = args[1]; + const newSecond = ts.isParenthesizedExpression(second) + ? second.expression + : second; + const newArgs = [args[0], newSecond, ...args.slice(2)]; + return ts.factory.updateCallExpression( + node, + node.expression, + node.typeArguments, + newArgs, + ); + } + } + } + return ts.visitEachChild(node, visit, context); + }; + return (node) => ts.visitNode(node, visit) as ts.SourceFile; + }; + + const result = ts.transform(sf, [transformer]); + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Transformer API returns Node[]; we expect a SourceFile here + const transformed = result.transformed[0] as ts.SourceFile; + const printer = ts.createPrinter({ removeComments: false }); + const printed = printer.printFile(transformed); + result.dispose(); + return printed; + } catch { + return s; + } + }; + + // Token-level repair: balance parentheses for `z.record(...)` calls, insert missing + // `z.unknown()` value argument when the call only had a single key schema, and + // remove duplicated trailing ')' tokens following the call. + const repairRecordCalls = (src: string) => { + const needle = "z.record("; + let out = ""; + let i = 0; + while (true) { + const p = src.indexOf(needle, i); + if (p === -1) { + out += src.slice(i); + break; + } + out += src.slice(i, p); + let k = p + needle.length; + let depth = 1; + let inSingle = false; + let inDouble = false; + let inBack = false; + let escaped = false; + let end = -1; + for (; k < src.length; k++) { + const ch = src[k]; + if (escaped) { + escaped = false; + continue; + } + if (ch === "\\") { + escaped = true; + continue; + } + if (inSingle) { + if (ch === "'") { + inSingle = false; + } + continue; + } + if (inDouble) { + if (ch === '"') { + inDouble = false; + } + continue; + } + if (inBack) { + if (ch === "`") { + inBack = false; + } + continue; + } + if (ch === "'") { + inSingle = true; + continue; + } + if (ch === '"') { + inDouble = true; + continue; + } + if (ch === "`") { + inBack = true; + continue; + } + if (ch === "(") { + depth++; + continue; + } + if (ch === ")") { + depth--; + if (depth === 0) { + end = k; + break; + } + continue; + } + } + if (end === -1) { + // malformed remainder; append rest and break + out += src.slice(p); + break; + } + + const inner = src.slice(p + needle.length, end); + + // determine if there is a top-level comma in `inner` + let hasTopComma = false; + let pd = 0, + bd = 0, + cd = 0; // paren, bracket, brace depth for detection + inSingle = inDouble = inBack = escaped = false; + for (let m = 0; m < inner.length; m++) { + const ch = inner[m]; + if (escaped) { + escaped = false; + continue; + } + if (ch === "\\") { + escaped = true; + continue; + } + if (inSingle) { + if (ch === "'") inSingle = false; + continue; + } + if (inDouble) { + if (ch === '"') inDouble = false; + continue; + } + if (inBack) { + if (ch === "`") inBack = false; + continue; + } + if (ch === "'") { + inSingle = true; + continue; + } + if (ch === '"') { + inDouble = true; + continue; + } + if (ch === "`") { + inBack = true; + continue; + } + if (ch === "(") { + pd++; + continue; + } + if (ch === ")") { + if (pd > 0) pd--; + continue; + } + if (ch === "[") { + bd++; + continue; + } + if (ch === "]") { + if (bd > 0) bd--; + continue; + } + if (ch === "{") { + cd++; + continue; + } + if (ch === "}") { + if (cd > 0) cd--; + continue; + } + if (ch === "," && pd === 0 && bd === 0 && cd === 0) { + hasTopComma = true; + break; + } + } + + let replacedCall = null; + if (!hasTopComma) { + // single-arg form: insert z.unknown() as the second arg + replacedCall = `${needle}${inner.trim()}, z.unknown())`; + } else { + // keep original text for call + replacedCall = src.slice(p, end + 1); + } + + // append the replacement + out += replacedCall; + + // advance k to after end, and collapse any immediately following duplicated ')' tokens down to one + let j = end + 1; + // count consecutive ) characters + let closeCount = 0; + while (j < src.length && src[j] === ")") { + closeCount++; + j++; + } + if (closeCount > 0) { + // we already emitted one ')' as part of replacement; if extra ) found, skip them + // (effectively collapsing duplicates) + } + + i = j; + } + return out; + }; + + code = fixGenerated(code); + // Apply token-level repairs for z.record(...) cases + code = repairRecordCalls(code); - const code = await fs.readFile(OUTPUT_PATH, "utf8"); const internalGitSha = openApiDoc.info["x-internal-git-sha"] || "UNKNOWN"; const banner = `// Auto-generated file (internal git SHA ${internalGitSha}) -- do not modify\n\n`; await fs.writeFile(OUTPUT_PATH, banner + code); + + // Format the generated file with prettier + const { execSync } = await import("node:child_process"); + try { + execSync(`./node_modules/.bin/prettier --write "${OUTPUT_PATH}"`, { + cwd: path.join(SCRIPT_DIR, "../../.."), + stdio: "inherit", + }); + } catch (error) { + console.warn("Warning: Could not format generated file with prettier"); + } } main(); diff --git a/packages/proxy/scripts/sync_models.ts b/packages/proxy/scripts/sync_models.ts index f0fd5b77..6303ce05 100644 --- a/packages/proxy/scripts/sync_models.ts +++ b/packages/proxy/scripts/sync_models.ts @@ -75,7 +75,7 @@ const liteLLMModelDetailSchema = z }) .passthrough(); -const liteLLMModelListSchema = z.record(liteLLMModelDetailSchema); +const liteLLMModelListSchema = z.record(z.string(), liteLLMModelDetailSchema); type LiteLLMModelDetail = z.infer; type LiteLLMModelList = z.infer; @@ -114,7 +114,7 @@ async function fetchRemoteModels(url: string): Promise { if (error instanceof z.ZodError) { console.error( "Zod validation errors in remote data:", - error.errors, + error.issues, ); reject( new Error( @@ -142,12 +142,12 @@ async function readLocalModels(filePath: string): Promise { const fileContent = await fs.promises.readFile(filePath, "utf-8"); const localData = JSON.parse(fileContent); // Validate local data with the imported ModelSchema - return z.record(ModelSchema).parse(localData); + return z.record(z.string(), ModelSchema).parse(localData); } catch (error) { if (error instanceof z.ZodError) { console.error( "Zod validation errors in local model_list.json:", - error.errors, + error.issues, ); throw new Error("Local model_list.json failed Zod validation."); } diff --git a/packages/proxy/src/generated_types.ts b/packages/proxy/src/generated_types.ts index 8b8a40bf..9004c0f3 100644 --- a/packages/proxy/src/generated_types.ts +++ b/packages/proxy/src/generated_types.ts @@ -1,7 +1,6 @@ // Auto-generated file (internal git SHA e6490ffb3d42f5dbc59d748bb5016f6d8f11cac2) -- do not modify import { z } from "zod"; - export const AclObjectType = z.union([ z.enum([ "organization", @@ -539,7 +538,7 @@ export const ExperimentEvent = z.object({ expected: z.unknown().optional(), error: z.unknown().optional(), scores: z - .union([z.record(z.union([z.number(), z.null()])), z.null()]) + .union([z.record(z.string(), z.union([z.number(), z.null()])), z.null()]) .optional(), metadata: z .union([ @@ -551,7 +550,7 @@ export const ExperimentEvent = z.object({ ]) .optional(), tags: z.union([z.array(z.string()), z.null()]).optional(), - metrics: z.union([z.record(z.number()), z.null()]).optional(), + metrics: z.union([z.record(z.string(), z.number()), z.null()]).optional(), context: z .union([ z @@ -687,7 +686,7 @@ export const PromptParserNullish = z.union([ z.object({ type: z.literal("llm_classifier"), use_cot: z.boolean(), - choice_scores: z.record(z.number().gte(0).lte(1)), + choice_scores: z.record(z.string(), z.number().gte(0).lte(1)), }), z.null(), ]); @@ -809,8 +808,8 @@ export const GraphEdge = z.object({ export type GraphEdgeType = z.infer; export const GraphData = z.object({ type: z.literal("graph"), - nodes: z.record(GraphNode), - edges: z.record(GraphEdge), + nodes: z.record(z.string(), GraphNode), + edges: z.record(z.string(), GraphEdge), }); export type GraphDataType = z.infer; export const FunctionData = z.union([ @@ -1162,7 +1161,7 @@ export const ProjectLogsEvent = z.object({ expected: z.unknown().optional(), error: z.unknown().optional(), scores: z - .union([z.record(z.union([z.number(), z.null()])), z.null()]) + .union([z.record(z.string(), z.union([z.number(), z.null()])), z.null()]) .optional(), metadata: z .union([ @@ -1174,7 +1173,7 @@ export const ProjectLogsEvent = z.object({ ]) .optional(), tags: z.union([z.array(z.string()), z.null()]).optional(), - metrics: z.union([z.record(z.number()), z.null()]).optional(), + metrics: z.union([z.record(z.string(), z.number()), z.null()]).optional(), context: z .union([ z @@ -1216,7 +1215,7 @@ export const ProjectScoreCategory = z.object({ export type ProjectScoreCategoryType = z.infer; export const ProjectScoreCategories = z.union([ z.array(ProjectScoreCategory), - z.record(z.number()), + z.record(z.string(), z.number()), z.array(z.string()), z.null(), ]); @@ -1459,7 +1458,7 @@ export const ViewOptions = z.union([ frameStart: z.union([z.string(), z.null()]), frameEnd: z.union([z.string(), z.null()]), tzUTC: z.union([z.boolean(), z.null()]), - chartVisibility: z.union([z.record(z.boolean()), z.null()]), + chartVisibility: z.union([z.record(z.string(), z.boolean()), z.null()]), projectId: z.union([z.string(), z.null()]), type: z.union([z.enum(["project", "experiment"]), z.null()]), groupBy: z.union([z.string(), z.null()]), @@ -1468,9 +1467,9 @@ export const ViewOptions = z.union([ }), z .object({ - columnVisibility: z.union([z.record(z.boolean()), z.null()]), + columnVisibility: z.union([z.record(z.string(), z.boolean()), z.null()]), columnOrder: z.union([z.array(z.string()), z.null()]), - columnSizing: z.union([z.record(z.number()), z.null()]), + columnSizing: z.union([z.record(z.string(), z.number()), z.null()]), grouping: z.union([z.string(), z.null()]), rowHeight: z.union([z.string(), z.null()]), tallGroupRows: z.union([z.boolean(), z.null()]), diff --git a/packages/proxy/src/providers/anthropic.ts b/packages/proxy/src/providers/anthropic.ts index 9aa32d52..174a0764 100644 --- a/packages/proxy/src/providers/anthropic.ts +++ b/packages/proxy/src/providers/anthropic.ts @@ -140,7 +140,7 @@ export const anthropicStreamEventSchema = z.discriminatedUnion("type", [ type: z.literal("tool_use"), id: z.string(), name: z.string(), - input: z.record(z.unknown()), + input: z.record(z.string(), z.unknown()), }), ]), }), diff --git a/packages/proxy/src/providers/google.ts b/packages/proxy/src/providers/google.ts index a6b9f923..7a4ae1c7 100644 --- a/packages/proxy/src/providers/google.ts +++ b/packages/proxy/src/providers/google.ts @@ -188,27 +188,27 @@ export async function openAIMessagesToGoogleMessages( return sortedContent; } -const finishReason = finishReasonSchema.Enum; +// finishReasonSchema.Enum (v3) is not available in Zod v4; switch on string literals instead function translateFinishReason( reason?: FinishReason | null, ): OpenAIChatCompletionChoice["finish_reason"] | null { // "length" | "stop" | "tool_calls" | "content_filter" | "function_call" switch (reason) { - case finishReason.MAX_TOKENS: + case "MAX_TOKENS": return "length"; - case finishReason.SAFETY: - case finishReason.PROHIBITED_CONTENT: - case finishReason.SPII: - case finishReason.BLOCKLIST: + case "SAFETY": + case "PROHIBITED_CONTENT": + case "SPII": + case "BLOCKLIST": return "content_filter"; - case finishReason.STOP: + case "STOP": return "stop"; - case finishReason.RECITATION: - case finishReason.LANGUAGE: - case finishReason.OTHER: - case finishReason.FINISH_REASON_UNSPECIFIED: - case finishReason.MALFORMED_FUNCTION_CALL: + case "RECITATION": + case "LANGUAGE": + case "OTHER": + case "FINISH_REASON_UNSPECIFIED": + case "MALFORMED_FUNCTION_CALL": return "content_filter"; case undefined: default: @@ -367,13 +367,10 @@ function convertGeminiPartsToOpenAIContent( } } - // If only text content (single part), return as string for backwards compatibility - if ( - !hasNonTextContent && - contentParts.length === 1 && - contentParts[0].type === "text" - ) { - return contentParts[0].text ?? ""; + // If only text content (no images), return as string for backwards compatibility + // Concatenate all text parts into a single string + if (!hasNonTextContent && contentParts.every((p) => p.type === "text")) { + return contentParts.map((p) => p.text ?? "").join(""); } // If no content parts, return empty string diff --git a/packages/proxy/types/anthropic.ts b/packages/proxy/types/anthropic.ts index 2f9b405b..f2f672df 100644 --- a/packages/proxy/types/anthropic.ts +++ b/packages/proxy/types/anthropic.ts @@ -51,7 +51,7 @@ const anthropicToolUseContentPartSchema = z.object({ type: z.literal("tool_use"), id: z.string(), name: z.string(), - input: z.record(z.any()), + input: z.record(z.string(), z.unknown()), cache_control: cacheControlSchema.optional(), }); @@ -59,7 +59,7 @@ const anthropicServerToolUseContentPartSchema = z.object({ type: z.literal("server_tool_use"), id: z.string(), name: z.enum(["web_search", "code_execution"]), - input: z.record(z.any()), + input: z.record(z.string(), z.unknown()), cache_control: cacheControlSchema.optional(), }); @@ -129,7 +129,7 @@ const anthropicMCPToolUseContentPartSchema = z.object({ type: z.literal("mcp_tool_use"), id: z.string(), name: z.string(), - input: z.record(z.any()), + input: z.record(z.string(), z.unknown()), server_name: z.string(), cache_control: cacheControlSchema.nullish(), }); @@ -145,7 +145,7 @@ const anthropicMCPToolResultContentPartSchema = z.object({ type: z.literal("text"), text: z.string(), // This is a simplification of the strict citation schema - citations: z.array(z.record(z.any())).nullish(), + citations: z.array(z.record(z.string(), z.unknown())).nullish(), cache_control: cacheControlSchema.nullish(), }), ), diff --git a/packages/proxy/types/google.ts b/packages/proxy/types/google.ts index 33795fc2..578d7951 100644 --- a/packages/proxy/types/google.ts +++ b/packages/proxy/types/google.ts @@ -101,14 +101,14 @@ const fileDataSchema = z.object({ const functionCallSchema = z.object({ id: z.string().nullish(), - args: z.record(z.unknown()).nullish(), + args: z.record(z.string(), z.unknown()).nullish(), name: z.string().nullish(), }); const functionResponseSchema = z.object({ id: z.string().nullish(), name: z.string().nullish(), - response: z.record(z.unknown()).nullish(), + response: z.record(z.string(), z.unknown()).nullish(), }); const blobSchema = z.object({ @@ -495,7 +495,7 @@ const schemaSchema: z.ZodSchema = z.object({ minimum: z.number().nullish(), nullable: z.boolean().nullish(), pattern: z.string().nullish(), - properties: z.lazy(() => z.record(schemaSchema)).nullish(), + properties: z.lazy(() => z.record(z.string(), schemaSchema)).nullish(), propertyOrdering: z.array(z.string()).nullish(), required: z.array(z.string()).nullish(), title: z.string().nullish(), @@ -627,7 +627,7 @@ const generateContentConfigSchema = z.object({ safetySettings: z.array(safetySettingSchema).nullish(), tools: toolListUnionSchema.nullish(), toolConfig: toolConfigSchema.nullish(), - labels: z.record(z.string()).nullish(), + labels: z.record(z.string(), z.string()).nullish(), cachedContent: z.string().nullish(), responseModalities: z.array(z.string()).nullish(), mediaResolution: mediaResolutionSchema.nullish(), diff --git a/packages/proxy/utils/index.ts b/packages/proxy/utils/index.ts index ebcf2380..8d8775ce 100644 --- a/packages/proxy/utils/index.ts +++ b/packages/proxy/utils/index.ts @@ -18,10 +18,12 @@ export function getCurrentUnixTimestamp(): number { } export const effortToBudgetMultiplier = { + none: 0, minimal: 0, low: 0.2, medium: 0.5, high: 0.8, + xhigh: 1.0, } as const; export const getBudgetMultiplier = ( diff --git a/packages/proxy/utils/tempCredentials.test.ts b/packages/proxy/utils/tempCredentials.test.ts index d441b532..d38c842e 100644 --- a/packages/proxy/utils/tempCredentials.test.ts +++ b/packages/proxy/utils/tempCredentials.test.ts @@ -162,7 +162,7 @@ test("verifyTempCredentials wrong payload type", async () => { }); await expect( verifyTempCredentials({ jwt: jwtWrongSchema, cacheGet }), - ).rejects.toThrow("invalid_literal"); + ).rejects.toThrow("invalid_value"); // Non object. const jwtWrongType = jwtSign("not an object", "auth token", { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8ee12257..12475bfb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,7 +5,7 @@ settings: excludeLinksFromLockfile: false overrides: - zod: 3.25.34 + zod: 4.2.1 importers: @@ -49,16 +49,16 @@ importers: version: 2.1.0(@opentelemetry/api@1.9.0) braintrust: specifier: ^0.3.7 - version: 0.3.7(zod@3.25.34) + version: 0.3.7(zod@4.2.1) dotenv: specifier: ^16.3.1 version: 16.3.1 openai: specifier: ^6.3.0 - version: 6.3.0(zod@3.25.34) + version: 6.3.0(zod@4.2.1) zod: - specifier: 3.25.34 - version: 3.25.34 + specifier: 4.2.1 + version: 4.2.1 devDependencies: '@cloudflare/workers-types': specifier: ^4.20250810.0 @@ -116,7 +116,7 @@ importers: version: 4.19.2 openai: specifier: ^4.104.0 - version: 4.104.0(zod@3.25.34) + version: 4.104.0(zod@4.2.1) redis: specifier: ^4.6.8 version: 4.6.8 @@ -256,7 +256,7 @@ importers: version: 9.0.2 openai: specifier: 4.104.0 - version: 4.104.0(zod@3.25.34) + version: 4.104.0(zod@4.2.1) openapi-json-schema: specifier: ^2.0.0 version: 2.0.0 @@ -264,8 +264,8 @@ importers: specifier: ^9.0.1 version: 9.0.1 zod: - specifier: 3.25.34 - version: 3.25.34 + specifier: 4.2.1 + version: 4.2.1 devDependencies: '@types/content-disposition': specifier: ^0.5.8 @@ -317,7 +317,7 @@ importers: version: 17.7.2 zod-to-json-schema: specifier: ^3.24.6 - version: 3.24.6(zod@3.25.34) + version: 3.24.6(zod@4.2.1) packages: @@ -4561,7 +4561,7 @@ packages: dependencies: '@vue/compiler-ssr': 3.5.22 '@vue/shared': 3.5.22 - vue: 3.5.22(typescript@5.5.4) + vue: 3.5.22(typescript@5.3.3) dev: false /@vue/shared@3.5.13: @@ -4572,14 +4572,14 @@ packages: resolution: {integrity: sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==} dev: false - /@zodios/core@10.9.6(axios@1.13.1)(zod@3.25.34): + /@zodios/core@10.9.6(axios@1.13.1)(zod@4.2.1): resolution: {integrity: sha512-aH4rOdb3AcezN7ws8vDgBfGboZMk2JGGzEq/DtW65MhnRxyTGRuLJRWVQ/2KxDgWvV2F5oTkAS+5pnjKbl0n+A==} peerDependencies: axios: ^0.x || ^1.0.0 - zod: 3.25.34 + zod: 4.2.1 dependencies: axios: 1.13.1 - zod: 3.25.34 + zod: 4.2.1 dev: true /abbrev@1.1.1: @@ -5086,11 +5086,11 @@ packages: fill-range: 7.1.1 dev: true - /braintrust@0.3.7(zod@3.25.34): + /braintrust@0.3.7(zod@4.2.1): resolution: {integrity: sha512-P6nJLgM98IOiBvAsgfUn9dbrLhmTfvED/+2ouacImTgs9adlZoGrIib9BkRAm+keSTfKLBPFqMmUi7QZ4ClemQ==} hasBin: true peerDependencies: - zod: 3.25.34 + zod: 4.2.1 dependencies: '@ai-sdk/provider': 1.1.3 '@next/env': 14.2.3 @@ -5112,8 +5112,8 @@ packages: slugify: 1.6.6 source-map: 0.7.4 uuid: 9.0.1 - zod: 3.25.34 - zod-to-json-schema: 3.23.5(zod@3.25.34) + zod: 4.2.1 + zod-to-json-schema: 3.23.5(zod@4.2.1) transitivePeerDependencies: - '@aws-sdk/credential-provider-web-identity' - supports-color @@ -8070,7 +8070,7 @@ packages: workerd: 1.20251008.0 ws: 8.18.0 youch: 4.1.0-beta.10 - zod: 3.25.34 + zod: 4.2.1 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -8474,12 +8474,12 @@ packages: mimic-fn: 2.1.0 dev: true - /openai@4.104.0(zod@3.25.34): + /openai@4.104.0(zod@4.2.1): resolution: {integrity: sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==} hasBin: true peerDependencies: ws: ^8.18.0 - zod: 3.25.34 + zod: 4.2.1 peerDependenciesMeta: ws: optional: true @@ -8493,24 +8493,24 @@ packages: form-data-encoder: 1.7.2 formdata-node: 4.4.1 node-fetch: 2.7.0 - zod: 3.25.34 + zod: 4.2.1 transitivePeerDependencies: - encoding dev: false - /openai@6.3.0(zod@3.25.34): + /openai@6.3.0(zod@4.2.1): resolution: {integrity: sha512-E6vOGtZvdcb4yXQ5jXvDlUG599OhIkb/GjBLZXS+qk0HF+PJReIldEc9hM8Ft81vn+N6dRdFRb7BZNK8bbvXrw==} hasBin: true peerDependencies: ws: ^8.18.0 - zod: 3.25.34 + zod: 4.2.1 peerDependenciesMeta: ws: optional: true zod: optional: true dependencies: - zod: 3.25.34 + zod: 4.2.1 dev: false /openapi-json-schema@2.0.0: @@ -8527,7 +8527,7 @@ packages: dependencies: '@apidevtools/swagger-parser': 10.1.1(openapi-types@12.1.3) '@liuli-util/fs-extra': 0.1.0 - '@zodios/core': 10.9.6(axios@1.13.1)(zod@3.25.34) + '@zodios/core': 10.9.6(axios@1.13.1)(zod@4.2.1) axios: 1.13.1 cac: 6.7.14 handlebars: 4.7.8 @@ -8538,7 +8538,7 @@ packages: tanu: 0.1.13 ts-pattern: 5.8.0 whence: 2.1.0 - zod: 3.25.34 + zod: 4.2.1 transitivePeerDependencies: - debug - react @@ -10002,7 +10002,7 @@ packages: peerDependencies: vue: '>=3.2.26 < 4' dependencies: - vue: 3.5.22(typescript@5.5.4) + vue: 3.5.22(typescript@5.3.3) dev: false /tailwindcss@3.2.7(postcss@8.4.38): @@ -11236,21 +11236,21 @@ packages: youch-core: 0.3.3 dev: true - /zod-to-json-schema@3.23.5(zod@3.25.34): + /zod-to-json-schema@3.23.5(zod@4.2.1): resolution: {integrity: sha512-5wlSS0bXfF/BrL4jPAbz9da5hDlDptdEppYfe+x4eIJ7jioqKG9uUxOwPzqof09u/XeVdrgFu29lZi+8XNDJtA==} peerDependencies: - zod: 3.25.34 + zod: 4.2.1 dependencies: - zod: 3.25.34 + zod: 4.2.1 dev: false - /zod-to-json-schema@3.24.6(zod@3.25.34): + /zod-to-json-schema@3.24.6(zod@4.2.1): resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==} peerDependencies: - zod: 3.25.34 + zod: 4.2.1 dependencies: - zod: 3.25.34 + zod: 4.2.1 dev: true - /zod@3.25.34: - resolution: {integrity: sha512-lZHvSc2PpWdcfpHlyB33HA9nqP16GpC9IpiG4lYq9jZCJVLZNnWd6Y1cj79bcLSBKTkxepfpjckPv5Y5VOPlwA==} + /zod@4.2.1: + resolution: {integrity: sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==}