diff --git a/package.json b/package.json index 0f440e1ec1..3e36bb0ddf 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,9 @@ "@types/react": "18.3.31", "@types/react-dom": "18.3.7", "csstype": "3.2.3", - "zod": "3.25.76", + "zod": "4.6.7", + "@lmstudio/sdk>zod": "3.25.76", + "knip>zod": "3.25.76", "katex": "0.18.2" } } diff --git a/packages/cloud/package.json b/packages/cloud/package.json index 612b75afe9..b2f035eb69 100644 --- a/packages/cloud/package.json +++ b/packages/cloud/package.json @@ -14,7 +14,7 @@ "dependencies": { "@roo-code/types": "workspace:^", "jwt-decode": "^4.0.0", - "zod": "^3.25.76" + "zod": "^4.0.0" }, "devDependencies": { "@roo-code/config-eslint": "workspace:^", diff --git a/packages/cloud/src/WebAuthService.ts b/packages/cloud/src/WebAuthService.ts index 501bf95bb5..6df4eb0cd9 100644 --- a/packages/cloud/src/WebAuthService.ts +++ b/packages/cloud/src/WebAuthService.ts @@ -61,7 +61,7 @@ const clerkMeResponseSchema = z.object({ }), ) .optional(), - public_metadata: z.record(z.any()).optional(), + public_metadata: z.record(z.string(), z.any()).optional(), }), }) @@ -233,7 +233,7 @@ export class WebAuthService extends EventEmitter implements A return credentials } catch (error) { if (error instanceof z.ZodError) { - this.log("[auth] Invalid credentials format:", error.errors) + this.log("[auth] Invalid credentials format:", error.issues) } else { this.log("[auth] Failed to parse stored credentials:", error) } diff --git a/packages/core/package.json b/packages/core/package.json index bcaa1e5c16..a33cde67f8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -25,7 +25,7 @@ "execa": "^9.5.2", "ignore": "^7.0.3", "openai": "^5.12.2", - "zod": "^3.25.61" + "zod": "^4.0.0" }, "devDependencies": { "@roo-code/config-eslint": "workspace:^", diff --git a/packages/telemetry/package.json b/packages/telemetry/package.json index 045e5cb4c2..b1a708e2e2 100644 --- a/packages/telemetry/package.json +++ b/packages/telemetry/package.json @@ -14,7 +14,7 @@ "dependencies": { "@roo-code/types": "workspace:^", "posthog-node": "^5.0.0", - "zod": "^3.25.61" + "zod": "^4.0.0" }, "devDependencies": { "@roo-code/config-eslint": "workspace:^", diff --git a/packages/types/package.json b/packages/types/package.json index dd459ebc3d..2538ceb442 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -33,7 +33,7 @@ }, "dependencies": { "ai-sdk-provider-poe": "2.0.18", - "zod": "3.25.76" + "zod": "4.6.7" }, "devDependencies": { "@roo-code/config-eslint": "workspace:^", @@ -42,7 +42,6 @@ "globals": "16.5.0", "tsup": "8.5.1", "ajv": "8.20.0", - "vitest": "4.1.11", - "zod-to-json-schema": "3.25.2" + "vitest": "4.1.11" } } diff --git a/packages/types/src/cli.ts b/packages/types/src/cli.ts index ad178f745d..3f0b3397d6 100644 --- a/packages/types/src/cli.ts +++ b/packages/types/src/cli.ts @@ -114,7 +114,7 @@ export type RooCliQueueItem = z.infer export const rooCliToolUseSchema = z.object({ name: z.string(), - input: z.record(z.unknown()).optional(), + input: z.record(z.string(), z.unknown()).optional(), }) export type RooCliToolUse = z.infer diff --git a/packages/types/src/cloud.ts b/packages/types/src/cloud.ts index c991cdb1e6..3b36f05b27 100644 --- a/packages/types/src/cloud.ts +++ b/packages/types/src/cloud.ts @@ -77,6 +77,7 @@ export interface CloudOrganizationMembership { export const organizationAllowListSchema = z.object({ allowAll: z.boolean(), providers: z.record( + z.string(), z.object({ allowAll: z.boolean(), models: z.array(z.string()).optional(), diff --git a/packages/types/src/mode.ts b/packages/types/src/mode.ts index ebbfb5bf38..1ef8e97f48 100644 --- a/packages/types/src/mode.ts +++ b/packages/types/src/mode.ts @@ -83,7 +83,7 @@ const rawGroupEntryArraySchema = z.array(groupEntrySchema).refine( * tool groups (e.g., "browser") before validation, ensuring backward compatibility * with older user configs. * - * The type assertion to `z.ZodType` is + * The type assertion to `z.ZodType` is * required because `z.preprocess` erases the input type to `unknown`, which * propagates through `modeConfigSchema → rooCodeSettingsSchema → createRunSchema` * and breaks `zodResolver` generic inference in downstream consumers. @@ -91,7 +91,7 @@ const rawGroupEntryArraySchema = z.array(groupEntrySchema).refine( export const groupEntryArraySchema = z.preprocess((val) => { if (!Array.isArray(val)) return val return val.filter((entry) => !isDeprecatedGroupEntry(entry)) -}, rawGroupEntryArraySchema) as z.ZodType +}, rawGroupEntryArraySchema) as z.ZodType export const modeConfigSchema = z.object({ slug: z.string().regex(/^[a-zA-Z0-9-]+$/, "Slug must contain only letters numbers and dashes"), diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 224d3d24c8..36009290cb 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -239,7 +239,7 @@ const indexProviderDefinitions = ( const providerDefinitions = indexProviderDefinitions(providerDefinitionList) const defaultSchema = z.object({ - [API_PROVIDER_FIELD]: z.undefined(), + [API_PROVIDER_FIELD]: z.undefined().optional(), }) type ProviderDefinitionSchemas = { @@ -266,7 +266,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion(API_PROV type ProviderSettingsShape = UnionToIntersection<(typeof providerDefinitionList)[number][typeof SETTINGS_SHAPE_FIELD]> -const providerSettingsObjectSchema = providerDefinitionList.reduce( +const providerSettingsObjectSchema = providerDefinitionList.reduce( (schema, definition) => schema.merge(z.object(definition[SETTINGS_SHAPE_FIELD])), z.object({}), ) diff --git a/packages/types/src/provider-settings/common.ts b/packages/types/src/provider-settings/common.ts index 7e4c49bab3..ca78c3a25d 100644 --- a/packages/types/src/provider-settings/common.ts +++ b/packages/types/src/provider-settings/common.ts @@ -17,10 +17,8 @@ export const API_MODEL_ID_FIELD = "apiModelId" // map `null` to `undefined` before validation; the return type strips `null` from // the output because it can never survive the preprocessing step. const nullableOptional = (schema: Schema) => - z.preprocess((value) => (value === null ? undefined : value), schema) as unknown as z.ZodType< - Exclude, null>, - z.ZodTypeDef, - z.input + z.preprocess((value) => (value === null ? undefined : value), schema) as unknown as z.ZodOptional< + z.ZodType, null>, z.input> > export const baseProviderSettingsShape = { @@ -59,7 +57,7 @@ export type ProviderDefinition = { apiProvider: ProviderIdentifier settingsShape: z.ZodRawShape modelIdKey?: string - schema: z.ZodDiscriminatedUnionOption + schema: z.ZodObject getModelId: ProviderModelIdAccessor } diff --git a/packages/types/src/roomodes-schema.ts b/packages/types/src/roomodes-schema.ts index 6283395686..809c3b13ef 100644 --- a/packages/types/src/roomodes-schema.ts +++ b/packages/types/src/roomodes-schema.ts @@ -6,7 +6,6 @@ */ import { z } from "zod" -import { zodToJsonSchema } from "zod-to-json-schema" import { toolGroups, deprecatedToolGroups } from "./tool.js" import { groupOptionsSchema, modeConfigSchema } from "./mode.js" @@ -45,9 +44,9 @@ const roomodesZodSchema = z * Includes metadata fields ($id, title, description). */ export function generateRoomodesJsonSchema(): Record { - const jsonSchema = zodToJsonSchema(roomodesZodSchema, { - $refStrategy: "none", - target: "jsonSchema7", + const jsonSchema = z.toJSONSchema(roomodesZodSchema, { + target: "draft-7", + io: "output", }) as Record jsonSchema["$id"] = "https://github.com/RooCodeInc/Roo-Code/blob/main/schemas/roomodes.json" diff --git a/packages/types/src/tool.ts b/packages/types/src/tool.ts index d89a8107c1..03c10bc3e7 100644 --- a/packages/types/src/tool.ts +++ b/packages/types/src/tool.ts @@ -57,7 +57,7 @@ export type ToolName = z.infer * ToolUsage */ -export const toolUsageSchema = z.record( +export const toolUsageSchema = z.partialRecord( toolNamesSchema, z.object({ attempts: z.number(), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad0f9eda81..bf16401c4c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,7 +20,9 @@ overrides: '@types/react': 18.3.31 '@types/react-dom': 18.3.7 csstype: 3.2.3 - zod: 3.25.76 + zod: 4.6.7 + '@lmstudio/sdk>zod': 3.25.76 + knip>zod: 3.25.76 katex: 0.18.2 importers: @@ -215,8 +217,8 @@ importers: specifier: ^4.0.0 version: 4.0.0 zod: - specifier: 3.25.76 - version: 3.25.76 + specifier: 4.6.7 + version: 4.6.7 devDependencies: '@roo-code/config-eslint': specifier: workspace:^ @@ -288,10 +290,10 @@ importers: version: 7.0.5 openai: specifier: ^5.12.2 - version: 5.23.2(ws@8.21.3)(zod@3.25.76) + version: 5.23.2(ws@8.21.3)(zod@4.6.7) zod: - specifier: 3.25.76 - version: 3.25.76 + specifier: 4.6.7 + version: 4.6.7 devDependencies: '@roo-code/config-eslint': specifier: workspace:^ @@ -340,8 +342,8 @@ importers: specifier: ^5.0.0 version: 5.55.0(rxjs@7.8.2) zod: - specifier: 3.25.76 - version: 3.25.76 + specifier: 4.6.7 + version: 4.6.7 devDependencies: '@roo-code/config-eslint': specifier: workspace:^ @@ -366,10 +368,10 @@ importers: dependencies: ai-sdk-provider-poe: specifier: 2.0.18 - version: 2.0.18(ai@7.0.142(zod@3.25.76))(zod@3.25.76) + version: 2.0.18(ai@7.0.142(zod@4.6.7))(zod@4.6.7) zod: - specifier: 3.25.76 - version: 3.25.76 + specifier: 4.6.7 + version: 4.6.7 devDependencies: '@roo-code/config-eslint': specifier: workspace:^ @@ -392,9 +394,6 @@ importers: vitest: specifier: 4.1.11 version: 4.1.11(@opentelemetry/api@1.9.0)(@types/node@22.20.1)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) - zod-to-json-schema: - specifier: 3.25.2 - version: 3.25.2(zod@3.25.76) packages/vscode-shim: devDependencies: @@ -415,10 +414,10 @@ importers: dependencies: '@anthropic-ai/sdk': specifier: ^0.109.0 - version: 0.109.1(zod@3.25.76) + version: 0.109.1(zod@4.6.7) '@anthropic-ai/vertex-sdk': specifier: ^0.19.0 - version: 0.19.0(zod@3.25.76) + version: 0.19.0(zod@4.6.7) '@aws-sdk/client-bedrock-runtime': specifier: ^3.922.0 version: 3.1078.0 @@ -427,7 +426,7 @@ importers: version: 3.1078.0 '@google/genai': specifier: ^1.29.1 - version: 1.52.0(@modelcontextprotocol/sdk@1.31.0(zod@3.25.76)) + version: 1.52.0(@modelcontextprotocol/sdk@1.31.0(zod@4.6.7)) '@lmstudio/sdk': specifier: ^1.1.1 version: 1.5.0 @@ -436,7 +435,7 @@ importers: version: 1.15.1 '@modelcontextprotocol/sdk': specifier: 1.31.0 - version: 1.31.0(zod@3.25.76) + version: 1.31.0(zod@4.6.7) '@qdrant/js-client-rest': specifier: ^1.14.0 version: 1.18.0(typescript@5.9.3) @@ -460,7 +459,7 @@ importers: version: 4.9.3 ai-sdk-provider-poe: specifier: 2.0.18 - version: 2.0.18(ai@7.0.142(zod@3.25.76))(zod@3.25.76) + version: 2.0.18(ai@7.0.142(zod@4.6.7))(zod@4.6.7) async-mutex: specifier: ^0.5.0 version: 0.5.0 @@ -541,7 +540,7 @@ importers: version: 0.6.3 openai: specifier: ^5.12.2 - version: 5.23.2(ws@8.21.3)(zod@3.25.76) + version: 5.23.2(ws@8.21.3)(zod@4.6.7) os-name: specifier: ^6.0.0 version: 6.1.0 @@ -615,8 +614,8 @@ importers: specifier: ^2.8.0 version: 2.9.0 zod: - specifier: 3.25.76 - version: 3.25.76 + specifier: 4.6.7 + version: 4.6.7 devDependencies: '@roo-code/build': specifier: workspace:^ @@ -659,7 +658,7 @@ importers: version: 3.9.2 ai: specifier: 7.0.142 - version: 7.0.142(zod@3.25.76) + version: 7.0.142(zod@4.6.7) esbuild-wasm: specifier: 0.28.1 version: 0.28.1 @@ -874,8 +873,8 @@ importers: specifier: ^0.2.2 version: 0.2.2(@types/react@18.3.31)(react@18.3.1) zod: - specifier: 3.25.76 - version: 3.25.76 + specifier: 4.6.7 + version: 4.6.7 devDependencies: '@playwright/test': specifier: 1.62.1 @@ -956,43 +955,37 @@ packages: resolution: {integrity: sha512-snoLXB9DmvAmmngbPN/Io8IGzZ9zWpC208EgIIztYf1e1JhwuMkgKCYkL30vGhSen4PrBafu2+sO4G/17wu45A==} engines: {node: '>=18'} peerDependencies: - zod: 3.25.76 + zod: 4.6.7 '@ai-sdk/gateway@4.0.115': resolution: {integrity: sha512-Xn+gDWM4Ti2FzYmxhlZtjEuGBGZlOYaQMpwcVRSN4c9qCMUszD5F5TAbkxgLV/tHKeMfEX+WEuTCl6Kkj2r73A==} engines: {node: '>=22'} peerDependencies: - zod: 3.25.76 + zod: 4.6.7 '@ai-sdk/openai-compatible@2.0.56': resolution: {integrity: sha512-cQrN6OUn/jvsY3OdsU6Wn+ss7vp1iwIcakZKSlSRMnYqShBfyT7Qht+eqmgxs7w9ttrw6FAG6o11AiBs+iEsTA==} engines: {node: '>=18'} peerDependencies: - zod: 3.25.76 - - '@ai-sdk/openai-compatible@3.0.70': - resolution: {integrity: sha512-0TefLzj+V7aMLrIIKdk3CKRji+hOu3oOSHeLKJVzr0+q/7rsH4Q56w6RK7GX7csmAd9Urtt097XBTkBwJbpDjA==} - engines: {node: '>=22'} - peerDependencies: - zod: 3.25.76 + zod: 4.6.7 '@ai-sdk/openai@3.0.27': resolution: {integrity: sha512-pLMxWOypwroXiK9dxNpn60/HGhWWWDEOJ3lo9vZLoxvpJNtKnLKojwVIvlW3yEjlD7ll1+jUO2uzsABNTaP5Yg==} engines: {node: '>=18'} peerDependencies: - zod: 3.25.76 + zod: 4.6.7 '@ai-sdk/provider-utils@4.0.35': resolution: {integrity: sha512-bjYld/2KGPLt78kpqbya+fD4LYS7BqVQJyUjE3qAHrYB0FR2Q90BaWEVIBZaguTWXf/A8L6uG1zO1v9TxVlGWg==} engines: {node: '>=18'} peerDependencies: - zod: 3.25.76 + zod: 4.6.7 '@ai-sdk/provider-utils@5.0.61': resolution: {integrity: sha512-Y+Z3539UXw0ctFDNepTtG23VqBQHr2A6yI2qUxvQcg75DJYgJoD2F9U2Vp5OgQjX7lxto1BLgFtKuOhzrr52vQ==} engines: {node: '>=22'} peerDependencies: - zod: 3.25.76 + zod: 4.6.7 '@ai-sdk/provider@3.0.13': resolution: {integrity: sha512-ZPtVYt5QIJzOta1kdUiDuCx4HhFkvNPv/rvmZ2b1iXwybYjJsCnNYR4PAw4kW7rgVfDARvHXcU64efWuqNp6bw==} @@ -1013,7 +1006,7 @@ packages: resolution: {integrity: sha512-q9OnEKLr5H9nxSuXdgDgJhxfYMiE+AaUEBze2Gk91UcaaLnsN+Lx5fbCYywiqurU/APLdwv23x03Wm6WN3EBsg==} hasBin: true peerDependencies: - zod: 3.25.76 + zod: 4.6.7 peerDependenciesMeta: zod: optional: true @@ -2049,7 +2042,7 @@ packages: engines: {node: '>=18'} peerDependencies: '@cfworker/json-schema': ^4.1.1 - zod: 3.25.76 + zod: 4.6.7 peerDependenciesMeta: '@cfworker/json-schema': optional: true @@ -3853,7 +3846,7 @@ packages: resolution: {integrity: sha512-Tifni6/6nNhCu4GnFEeYYR7CGmJ3Yhm44SGxYrOupqP2aaZwmGGiiB5Zx4hiauJR0AWimcXULejDT+Qsj+SHOQ==} engines: {node: '>=22'} peerDependencies: - zod: 3.25.76 + zod: 4.6.7 ajv-formats@3.0.1: resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} @@ -4396,10 +4389,6 @@ packages: resolution: {integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==} engines: {node: '>= 0.8'} - copy-anything@4.0.5: - resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} - engines: {node: '>=18'} - copy-to-clipboard@3.3.3: resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} @@ -6936,7 +6925,7 @@ packages: hasBin: true peerDependencies: ws: ^8.18.0 - zod: 3.25.76 + zod: 4.6.7 peerDependenciesMeta: ws: optional: true @@ -8904,17 +8893,20 @@ packages: zod-to-json-schema@3.25.2: resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} peerDependencies: - zod: 3.25.76 + zod: 4.6.7 zod-validation-error@3.5.4: resolution: {integrity: sha512-+hEiRIiPobgyuFlEojnqjJnhFvg4r/i3cqgcm67eehZf/WBaK3g6cD02YU9mtdVxZjv8CzCA9n/Rhrs3yAAvAw==} engines: {node: '>=18.0.0'} peerDependencies: - zod: 3.25.76 + zod: 4.6.7 zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.6.7: + resolution: {integrity: sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==} + zustand@5.0.14: resolution: {integrity: sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==} engines: {node: '>=12.20.0'} @@ -8940,52 +8932,46 @@ snapshots: '@adobe/css-tools@4.4.2': {} - '@ai-sdk/anthropic@3.0.42(zod@3.25.76)': + '@ai-sdk/anthropic@3.0.42(zod@4.6.7)': dependencies: '@ai-sdk/provider': 3.0.13 - '@ai-sdk/provider-utils': 4.0.35(zod@3.25.76) - zod: 3.25.76 + '@ai-sdk/provider-utils': 4.0.35(zod@4.6.7) + zod: 4.6.7 - '@ai-sdk/gateway@4.0.115(zod@3.25.76)': + '@ai-sdk/gateway@4.0.115(zod@4.6.7)': dependencies: '@ai-sdk/provider': 4.0.28 - '@ai-sdk/provider-utils': 5.0.61(zod@3.25.76) + '@ai-sdk/provider-utils': 5.0.61(zod@4.6.7) '@vercel/oidc': 3.2.0 - zod: 3.25.76 + zod: 4.6.7 - '@ai-sdk/openai-compatible@2.0.56(zod@3.25.76)': + '@ai-sdk/openai-compatible@2.0.56(zod@4.6.7)': dependencies: '@ai-sdk/provider': 3.0.13 - '@ai-sdk/provider-utils': 4.0.35(zod@3.25.76) - zod: 3.25.76 - - '@ai-sdk/openai-compatible@3.0.70(zod@3.25.76)': - dependencies: - '@ai-sdk/provider': 4.0.28 - '@ai-sdk/provider-utils': 5.0.61(zod@3.25.76) - zod: 3.25.76 + '@ai-sdk/provider-utils': 4.0.35(zod@4.6.7) + zod: 4.6.7 - '@ai-sdk/openai@3.0.27(zod@3.25.76)': + '@ai-sdk/openai@3.0.27(zod@4.6.7)': dependencies: '@ai-sdk/provider': 3.0.13 - '@ai-sdk/provider-utils': 4.0.35(zod@3.25.76) - zod: 3.25.76 + '@ai-sdk/provider-utils': 4.0.35(zod@4.6.7) + zod: 4.6.7 - '@ai-sdk/provider-utils@4.0.35(zod@3.25.76)': + '@ai-sdk/provider-utils@4.0.35(zod@4.6.7)': dependencies: '@ai-sdk/provider': 3.0.13 '@standard-schema/spec': 1.1.0 eventsource-parser: 3.1.0 - zod: 3.25.76 + zod: 4.6.7 - '@ai-sdk/provider-utils@5.0.61(zod@3.25.76)': + '@ai-sdk/provider-utils@5.0.61(zod@4.6.7)': dependencies: '@ai-sdk/provider': 4.0.28 '@standard-schema/spec': 1.1.0 '@workflow/serde': 4.1.0 eventsource-parser: 3.1.0 undici: 6.28.1 - zod: 3.25.76 + zod: 4.6.7 '@ai-sdk/provider@3.0.13': dependencies: @@ -9005,16 +8991,16 @@ snapshots: package-manager-detector: 1.8.0 tinyexec: 1.3.0 - '@anthropic-ai/sdk@0.109.1(zod@3.25.76)': + '@anthropic-ai/sdk@0.109.1(zod@4.6.7)': dependencies: json-schema-to-ts: 3.1.1 standardwebhooks: 1.0.0 optionalDependencies: - zod: 3.25.76 + zod: 4.6.7 - '@anthropic-ai/vertex-sdk@0.19.0(zod@3.25.76)': + '@anthropic-ai/vertex-sdk@0.19.0(zod@4.6.7)': dependencies: - '@anthropic-ai/sdk': 0.109.1(zod@3.25.76) + '@anthropic-ai/sdk': 0.109.1(zod@4.6.7) google-auth-library: 10.9.0 transitivePeerDependencies: - supports-color @@ -10042,14 +10028,14 @@ snapshots: '@floating-ui/utils@0.2.11': {} - '@google/genai@1.52.0(@modelcontextprotocol/sdk@1.31.0(zod@3.25.76))': + '@google/genai@1.52.0(@modelcontextprotocol/sdk@1.31.0(zod@4.6.7))': dependencies: google-auth-library: 10.9.0 p-retry: 4.6.2 protobufjs: 7.6.4 ws: 8.21.3 optionalDependencies: - '@modelcontextprotocol/sdk': 1.31.0(zod@3.25.76) + '@modelcontextprotocol/sdk': 1.31.0(zod@4.6.7) transitivePeerDependencies: - bufferutil - supports-color @@ -10331,13 +10317,13 @@ snapshots: '@mistralai/mistralai@1.15.1': dependencies: ws: 8.21.3 - zod: 3.25.76 - zod-to-json-schema: 3.25.2(zod@3.25.76) + zod: 4.6.7 + zod-to-json-schema: 3.25.2(zod@4.6.7) transitivePeerDependencies: - bufferutil - utf-8-validate - '@modelcontextprotocol/sdk@1.31.0(zod@3.25.76)': + '@modelcontextprotocol/sdk@1.31.0(zod@4.6.7)': dependencies: '@hono/node-server': 1.19.14(hono@4.12.23) ajv: 8.20.0 @@ -10354,8 +10340,8 @@ snapshots: json-schema-typed: 8.0.2 pkce-challenge: 5.0.0 raw-body: 3.0.2 - zod: 3.25.76 - zod-to-json-schema: 3.25.2(zod@3.25.76) + zod: 4.6.7 + zod-to-json-schema: 3.25.2(zod@4.6.7) transitivePeerDependencies: - supports-color @@ -12194,23 +12180,23 @@ snapshots: agent-base@7.1.3: {} - ai-sdk-provider-poe@2.0.18(ai@7.0.142(zod@3.25.76))(zod@3.25.76): + ai-sdk-provider-poe@2.0.18(ai@7.0.142(zod@4.6.7))(zod@4.6.7): dependencies: - '@ai-sdk/anthropic': 3.0.42(zod@3.25.76) - '@ai-sdk/openai': 3.0.27(zod@3.25.76) - '@ai-sdk/openai-compatible': 2.0.56(zod@3.25.76) + '@ai-sdk/anthropic': 3.0.42(zod@4.6.7) + '@ai-sdk/openai': 3.0.27(zod@4.6.7) + '@ai-sdk/openai-compatible': 2.0.56(zod@4.6.7) '@ai-sdk/provider': 3.0.13 - '@ai-sdk/provider-utils': 4.0.35(zod@3.25.76) - ai: 7.0.142(zod@3.25.76) + '@ai-sdk/provider-utils': 4.0.35(zod@4.6.7) + ai: 7.0.142(zod@4.6.7) transitivePeerDependencies: - zod - ai@7.0.142(zod@3.25.76): + ai@7.0.142(zod@4.6.7): dependencies: - '@ai-sdk/gateway': 4.0.115(zod@3.25.76) + '@ai-sdk/gateway': 4.0.115(zod@4.6.7) '@ai-sdk/provider': 4.0.28 - '@ai-sdk/provider-utils': 5.0.61(zod@3.25.76) - zod: 3.25.76 + '@ai-sdk/provider-utils': 5.0.61(zod@4.6.7) + zod: 4.6.7 ajv-formats@3.0.1(ajv@8.20.0): optionalDependencies: @@ -12760,10 +12746,6 @@ snapshots: depd: 2.0.0 keygrip: 1.1.0 - copy-anything@4.0.5: - dependencies: - is-what: 5.5.0 - copy-to-clipboard@3.3.3: dependencies: toggle-selection: 1.0.6 @@ -15648,7 +15630,7 @@ snapshots: mutation-server-protocol@0.4.1: dependencies: - zod: 3.25.76 + zod: 4.6.7 mutation-testing-elements@3.8.4: {} @@ -15841,10 +15823,10 @@ snapshots: is-inside-container: 1.0.0 is-wsl: 3.1.0 - openai@5.23.2(ws@8.21.3)(zod@3.25.76): + openai@5.23.2(ws@8.21.3)(zod@4.6.7): optionalDependencies: ws: 8.21.3 - zod: 3.25.76 + zod: 4.6.7 option@0.2.4: {} @@ -17998,12 +17980,18 @@ snapshots: dependencies: zod: 3.25.76 + zod-to-json-schema@3.25.2(zod@4.6.7): + dependencies: + zod: 4.6.7 + zod-validation-error@3.5.4(zod@3.25.76): dependencies: zod: 3.25.76 zod@3.25.76: {} + zod@4.6.7: {} + zustand@5.0.14(@types/react@18.3.31)(react@19.2.7): optionalDependencies: '@types/react': 18.3.31 diff --git a/schemas/roomodes.json b/schemas/roomodes.json index cff607526b..810a17dc16 100644 --- a/schemas/roomodes.json +++ b/schemas/roomodes.json @@ -1,4 +1,5 @@ { + "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "customModes": { @@ -48,8 +49,6 @@ }, { "type": "array", - "minItems": 2, - "maxItems": 2, "items": [ { "type": "string", @@ -67,7 +66,10 @@ }, "additionalProperties": false } - ] + ], + "additionalItems": false, + "minItems": 2, + "maxItems": 2 } ] } @@ -96,7 +98,6 @@ }, "required": ["customModes"], "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://github.com/RooCodeInc/Roo-Code/blob/main/schemas/roomodes.json", "title": "Roo Code Custom Modes", "description": "Schema for .roomodes configuration files used by Roo Code to define custom modes." diff --git a/src/core/config/CustomModesManager.ts b/src/core/config/CustomModesManager.ts index 2e2e8dc8e2..86bab479d7 100644 --- a/src/core/config/CustomModesManager.ts +++ b/src/core/config/CustomModesManager.ts @@ -406,11 +406,11 @@ export class CustomModesManager { // Validate the mode configuration before saving const validationResult = modeConfigSchema.safeParse(config) if (!validationResult.success) { - const errorMessages = validationResult.error.errors + const errorMessages = validationResult.error.issues .map((err) => `${err.path.join(".")}: ${err.message}`) .join(", ") const errorMessage = `Invalid mode configuration: ${errorMessages}` - logger.error("Mode validation failed", { slug, errors: validationResult.error.errors }) + logger.error("Mode validation failed", { slug, errors: validationResult.error.issues }) vscode.window.showErrorMessage(t("common:customModes.errors.updateFailed", { error: errorMessage })) throw new Error(errorMessage) } @@ -963,11 +963,11 @@ export class CustomModesManager { const validationResult = modeConfigSchema.safeParse(modeConfig) if (!validationResult.success) { logger.error(`Invalid mode configuration for ${modeConfig.slug}`, { - errors: validationResult.error.errors, + errors: validationResult.error.issues, }) return { success: false, - error: `Invalid mode configuration for ${modeConfig.slug}: ${validationResult.error.errors.map((e) => e.message).join(", ")}`, + error: `Invalid mode configuration for ${modeConfig.slug}: ${validationResult.error.issues.map((e) => e.message).join(", ")}`, } } diff --git a/src/core/config/__tests__/importExport.spec.ts b/src/core/config/__tests__/importExport.spec.ts index f50fb29d11..90db81d8a1 100644 --- a/src/core/config/__tests__/importExport.spec.ts +++ b/src/core/config/__tests__/importExport.spec.ts @@ -348,7 +348,10 @@ describe("importExport", () => { customModesManager: mockCustomModesManager, }) - expect(result).toEqual({ success: false, error: "[providerProfiles.currentApiConfigName]: Required" }) + expect(result).toEqual({ + success: false, + error: "[providerProfiles.currentApiConfigName]: Invalid input: expected string, received undefined", + }) expect(fs.readFile).toHaveBeenCalledWith("/mock/path/settings.json", "utf-8") expect(mockProviderSettingsManager.import).not.toHaveBeenCalled() expect(mockContextProxy.setValues).not.toHaveBeenCalled() diff --git a/src/package.json b/src/package.json index 5c3f30b405..54ab407ef0 100644 --- a/src/package.json +++ b/src/package.json @@ -540,7 +540,7 @@ "web-tree-sitter": "^0.25.6", "workerpool": "^9.2.0", "yaml": "^2.8.0", - "zod": "3.25.76" + "zod": "4.6.7" }, "devDependencies": { "@roo-code/build": "workspace:^", diff --git a/src/services/mcp/McpHub.ts b/src/services/mcp/McpHub.ts index 5e608fec73..f0321eed76 100644 --- a/src/services/mcp/McpHub.ts +++ b/src/services/mcp/McpHub.ts @@ -99,7 +99,7 @@ const createServerTypeSchema = () => { command: z.string().min(1, "Command cannot be empty"), args: z.array(z.string()).optional(), cwd: z.string().default(() => vscode.workspace.workspaceFolders?.at(0)?.uri.fsPath ?? process.cwd()), - env: z.record(z.string()).optional(), + env: z.record(z.string(), z.string()).optional(), // Ensure no SSE fields are present url: z.undefined().optional(), headers: z.undefined().optional(), @@ -113,7 +113,7 @@ const createServerTypeSchema = () => { BaseConfigSchema.extend({ type: z.enum(["sse"]).optional(), url: z.string().url("URL must be a valid URL format"), - headers: z.record(z.string()).optional(), + headers: z.record(z.string(), z.string()).optional(), // Ensure no stdio fields are present command: z.undefined().optional(), args: z.undefined().optional(), @@ -128,7 +128,7 @@ const createServerTypeSchema = () => { BaseConfigSchema.extend({ type: z.enum(["streamable-http"]).optional(), url: z.string().url("URL must be a valid URL format"), - headers: z.record(z.string()).optional(), + headers: z.record(z.string(), z.string()).optional(), // Ensure no stdio fields are present command: z.undefined().optional(), args: z.undefined().optional(), @@ -149,7 +149,7 @@ export const ServerConfigSchema = createServerTypeSchema() // Settings schema const McpSettingsSchema = z.object({ - mcpServers: z.record(ServerConfigSchema), + mcpServers: z.record(z.string(), ServerConfigSchema), }) export class McpHub { @@ -270,7 +270,7 @@ export class McpHub { } catch (validationError) { if (validationError instanceof z.ZodError) { // Extract and format validation errors - const errorMessages = validationError.errors + const errorMessages = validationError.issues .map((err) => `${err.path.join(".")}: ${err.message}`) .join("; ") throw new Error( @@ -349,7 +349,7 @@ export class McpHub { const result = McpSettingsSchema.safeParse(config) if (!result.success) { - const errorMessages = result.error.errors + const errorMessages = result.error.issues .map((err) => `${err.path.join(".")}: ${err.message}`) .join("\n") vscode.window.showErrorMessage(t("mcp:errors.invalid_settings_validation", { errorMessages })) @@ -438,7 +438,7 @@ export class McpHub { await this.updateServerConnections(result.data.mcpServers || {}, "project") } else { // Format validation errors for better user feedback - const errorMessages = result.error.errors + const errorMessages = result.error.issues .map((err) => `${err.path.join(".")}: ${err.message}`) .join("\n") console.error("Invalid project MCP settings format:", errorMessages) @@ -589,7 +589,7 @@ export class McpHub { // Pass all servers including disabled ones - they'll be handled in updateServerConnections await this.updateServerConnections(result.data.mcpServers || {}, source, false) } else { - const errorMessages = result.error.errors + const errorMessages = result.error.issues .map((err) => `${err.path.join(".")}: ${err.message}`) .join("\n") console.error(`Invalid ${source} MCP settings format:`, errorMessages) diff --git a/src/utils/json-schema.ts b/src/utils/json-schema.ts index 9773a357d2..fe11ce0090 100644 --- a/src/utils/json-schema.ts +++ b/src/utils/json-schema.ts @@ -123,111 +123,110 @@ const TypeFieldSchema = z.union([JsonSchemaTypeSchema, z.array(JsonSchemaTypeSch * * Uses recursive parsing so transformations apply to all nested schemas automatically. */ -const NormalizedToolSchemaInternal: z.ZodType, z.ZodTypeDef, Record> = z.lazy( - () => - z - .object({ - // Accept both single type and array of types, transform array to anyOf - type: TypeFieldSchema.optional(), - properties: z.record(z.string(), NormalizedToolSchemaInternal).optional(), - items: z.union([NormalizedToolSchemaInternal, z.array(NormalizedToolSchemaInternal)]).optional(), - required: z.array(z.string()).optional(), - // Don't set default here - we'll handle it conditionally in the transform - additionalProperties: z.union([z.boolean(), NormalizedToolSchemaInternal]).optional(), - description: z.string().optional(), - default: z.unknown().optional(), - enum: z.array(JsonSchemaEnumValueSchema).optional(), - const: JsonSchemaEnumValueSchema.optional(), - anyOf: z.array(NormalizedToolSchemaInternal).optional(), - oneOf: z.array(NormalizedToolSchemaInternal).optional(), - allOf: z.array(NormalizedToolSchemaInternal).optional(), - $ref: z.string().optional(), - minimum: z.number().optional(), - maximum: z.number().optional(), - minLength: z.number().optional(), - maxLength: z.number().optional(), - pattern: z.string().optional(), - minItems: z.number().optional(), - maxItems: z.number().optional(), - uniqueItems: z.boolean().optional(), - // Format field - unsupported values will be stripped in transform - format: z.string().optional(), - }) - .passthrough() - .transform((schema) => { - const { - type, - required, - properties, - additionalProperties: _additionalProperties, - format, - items, - minItems, - maxItems, - uniqueItems, - ...rest - } = schema - const result: Record = { ...rest } +const NormalizedToolSchemaInternal: z.ZodType, Record> = z.lazy(() => + z + .object({ + // Accept both single type and array of types, transform array to anyOf + type: TypeFieldSchema.optional(), + properties: z.record(z.string(), NormalizedToolSchemaInternal).optional(), + items: z.union([NormalizedToolSchemaInternal, z.array(NormalizedToolSchemaInternal)]).optional(), + required: z.array(z.string()).optional(), + // Don't set default here - we'll handle it conditionally in the transform + additionalProperties: z.union([z.boolean(), NormalizedToolSchemaInternal]).optional(), + description: z.string().optional(), + default: z.unknown().optional(), + enum: z.array(JsonSchemaEnumValueSchema).optional(), + const: JsonSchemaEnumValueSchema.optional(), + anyOf: z.array(NormalizedToolSchemaInternal).optional(), + oneOf: z.array(NormalizedToolSchemaInternal).optional(), + allOf: z.array(NormalizedToolSchemaInternal).optional(), + $ref: z.string().optional(), + minimum: z.number().optional(), + maximum: z.number().optional(), + minLength: z.number().optional(), + maxLength: z.number().optional(), + pattern: z.string().optional(), + minItems: z.number().optional(), + maxItems: z.number().optional(), + uniqueItems: z.boolean().optional(), + // Format field - unsupported values will be stripped in transform + format: z.string().optional(), + }) + .passthrough() + .transform((schema) => { + const { + type, + required, + properties, + additionalProperties: _additionalProperties, + format, + items, + minItems, + maxItems, + uniqueItems, + ...rest + } = schema + const result: Record = { ...rest } - // Determine if this schema represents an object type - const isObjectType = - type === "object" || (Array.isArray(type) && type.includes("object")) || properties !== undefined + // Determine if this schema represents an object type + const isObjectType = + type === "object" || (Array.isArray(type) && type.includes("object")) || properties !== undefined - // Collect array-specific properties for potential use in type handling - const arrayProps = { items, minItems, maxItems, uniqueItems } + // Collect array-specific properties for potential use in type handling + const arrayProps = { items, minItems, maxItems, uniqueItems } - // If type is an array, convert to anyOf format (JSON Schema 2020-12) - // Array-specific properties must be moved inside the array variant - if (Array.isArray(type)) { - result.anyOf = type.map((t) => { - if (t === "array") { - return applyArrayProperties({ type: t }, arrayProps) - } - return { type: t } - }) - } else if (type !== undefined) { - result.type = type - // For single "array" type, preserve array-specific properties at root - if (type === "array") { - applyArrayProperties(result, arrayProps) + // If type is an array, convert to anyOf format (JSON Schema 2020-12) + // Array-specific properties must be moved inside the array variant + if (Array.isArray(type)) { + result.anyOf = type.map((t) => { + if (t === "array") { + return applyArrayProperties({ type: t }, arrayProps) } + return { type: t } + }) + } else if (type !== undefined) { + result.type = type + // For single "array" type, preserve array-specific properties at root + if (type === "array") { + applyArrayProperties(result, arrayProps) } + } - // Strip unsupported format values for OpenAI compatibility - // Only include format if it's a supported value - if (format && OPENAI_SUPPORTED_FORMATS.has(format)) { - result.format = format - } + // Strip unsupported format values for OpenAI compatibility + // Only include format if it's a supported value + if (format && OPENAI_SUPPORTED_FORMATS.has(format)) { + result.format = format + } - // Handle properties and required for strict mode - if (properties) { - result.properties = properties - if (required) { - const propertyKeys = Object.keys(properties) - const filteredRequired = required.filter((key) => propertyKeys.includes(key)) - if (filteredRequired.length > 0) { - result.required = filteredRequired - } + // Handle properties and required for strict mode + if (properties) { + result.properties = properties + if (required) { + const propertyKeys = Object.keys(properties) + const filteredRequired = required.filter((key) => propertyKeys.includes(key)) + if (filteredRequired.length > 0) { + result.required = filteredRequired } - } else if (result.type === "object" || (Array.isArray(type) && type.includes("object"))) { - // For type: "object" without properties, add empty properties - // This is required by OpenAI strict mode - result.properties = {} } + } else if (result.type === "object" || (Array.isArray(type) && type.includes("object"))) { + // For type: "object" without properties, add empty properties + // This is required by OpenAI strict mode + result.properties = {} + } - // Only add additionalProperties for object-type schemas - // Adding it to primitive types (string, number, etc.) is invalid JSON Schema - if (isObjectType) { - // For strict mode compatibility, we MUST set additionalProperties to false - // Even if the original schema had {} (any) or true, we force false because - // OpenAI/OpenRouter strict mode rejects schemas with additionalProperties != false - // The original schema intent (allowing arbitrary properties) is incompatible with strict mode - result.additionalProperties = false - } - // For non-object types, don't include additionalProperties at all + // Only add additionalProperties for object-type schemas + // Adding it to primitive types (string, number, etc.) is invalid JSON Schema + if (isObjectType) { + // For strict mode compatibility, we MUST set additionalProperties to false + // Even if the original schema had {} (any) or true, we force false because + // OpenAI/OpenRouter strict mode rejects schemas with additionalProperties != false + // The original schema intent (allowing arbitrary properties) is incompatible with strict mode + result.additionalProperties = false + } + // For non-object types, don't include additionalProperties at all - return result - }), + return result + }), ) /** diff --git a/webview-ui/package.json b/webview-ui/package.json index 40be8a186b..aed9fc1d71 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -81,7 +81,7 @@ "use-sound": "^5.0.0", "vscode-material-icons": "^0.1.1", "vscrui": "^0.2.2", - "zod": "^3.25.61" + "zod": "^4.0.0" }, "devDependencies": { "@playwright/test": "1.62.1", diff --git a/webview-ui/src/components/chat/CodeIndexPopover.tsx b/webview-ui/src/components/chat/CodeIndexPopover.tsx index 9ae1114d9c..121ae7f760 100644 --- a/webview-ui/src/components/chat/CodeIndexPopover.tsx +++ b/webview-ui/src/components/chat/CodeIndexPopover.tsx @@ -498,7 +498,7 @@ export const CodeIndexPopover: React.FC = ({ } catch (error) { if (error instanceof z.ZodError) { const errors: Record = {} - error.errors.forEach((err) => { + error.issues.forEach((err) => { if (err.path[0]) { errors[err.path[0] as string] = err.message } diff --git a/webview-ui/src/components/chat/__tests__/CodeIndexPopover.test.tsx b/webview-ui/src/components/chat/__tests__/CodeIndexPopover.test.tsx index 00dec93a81..b12c00190a 100644 --- a/webview-ui/src/components/chat/__tests__/CodeIndexPopover.test.tsx +++ b/webview-ui/src/components/chat/__tests__/CodeIndexPopover.test.tsx @@ -218,4 +218,13 @@ describe("CodeIndexPopover OpenRouter provider lookup", () => { expect(screen.getByPlaceholderText("settings:codeIndex.bedrockRegionPlaceholder")).toHaveValue(expectedRegion) expect(screen.getByPlaceholderText("settings:codeIndex.bedrockProfilePlaceholder")).toHaveValue(expectedProfile) }) + + it("shows a field error when saving a changed provider without its required key", () => { + renderPopover(providerIdentifiers.openai, "embedding-model") + fireEvent.click(screen.getByText("settings:codeIndex.setupConfigLabel")) + fireEvent.change(screen.getAllByRole("combobox")[0], { target: { value: providerIdentifiers.gemini } }) + fireEvent.click(screen.getByText("settings:codeIndex.saveSettings")) + + expect(screen.getByText("settings:codeIndex.validation.geminiApiKeyRequired")).toBeInTheDocument() + }) }) diff --git a/webview-ui/src/components/modes/ModesView.tsx b/webview-ui/src/components/modes/ModesView.tsx index 2459d559b6..d5512009a5 100644 --- a/webview-ui/src/components/modes/ModesView.tsx +++ b/webview-ui/src/components/modes/ModesView.tsx @@ -417,7 +417,7 @@ const ModesView = () => { if (!result.success) { // Map Zod errors to specific fields - result.error.errors.forEach((error) => { + result.error.issues.forEach((error) => { const field = error.path[0] as string const message = error.message diff --git a/webview-ui/src/components/modes/__tests__/ModesView.spec.tsx b/webview-ui/src/components/modes/__tests__/ModesView.spec.tsx index 25dc7fb8e1..0a691d77ba 100644 --- a/webview-ui/src/components/modes/__tests__/ModesView.spec.tsx +++ b/webview-ui/src/components/modes/__tests__/ModesView.spec.tsx @@ -43,6 +43,16 @@ describe("PromptsView", () => { vitest.clearAllMocks() }) + it("shows the schema error when creating a mode without a name", async () => { + renderPromptsView() + fireEvent.click(screen.getByTestId("add-mode-button")) + fireEvent.click(screen.getByText("prompts:createModeDialog.buttons.create")) + + await waitFor(() => { + expect(screen.getByText("Name is required")).toBeInTheDocument() + }) + }) + it("displays the current mode name in the select trigger", () => { renderPromptsView({ mode: "code" }) const selectTrigger = screen.getByTestId("mode-select-trigger") diff --git a/webview-ui/src/components/ui/hooks/__tests__/useOpenRouterModelProviders.spec.tsx b/webview-ui/src/components/ui/hooks/__tests__/useOpenRouterModelProviders.spec.tsx new file mode 100644 index 0000000000..7b3dda7e24 --- /dev/null +++ b/webview-ui/src/components/ui/hooks/__tests__/useOpenRouterModelProviders.spec.tsx @@ -0,0 +1,45 @@ +import React from "react" +import axios from "axios" +import { QueryClient, QueryClientProvider } from "@tanstack/react-query" +import { renderHook, waitFor } from "@testing-library/react" +import { z } from "zod" + +import { useOpenRouterModelProviders } from "../useOpenRouterModelProviders" + +vi.mock("axios") + +const wrapper = ({ children }: React.PropsWithChildren) => ( + + {children} + +) + +describe("useOpenRouterModelProviders", () => { + afterEach(() => { + vi.restoreAllMocks() + }) + + it("logs the zod issues and returns no providers when validation throws", async () => { + const zodError = new z.ZodError([{ code: "custom", path: ["data"], message: "bad data" }]) + vi.mocked(axios.get).mockRejectedValue(zodError) + const consoleError = vi.spyOn(console, "error").mockImplementation(() => {}) + + const { result } = renderHook(() => useOpenRouterModelProviders("vendor/model"), { wrapper }) + + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + expect(result.current.data).toEqual({}) + expect(consoleError).toHaveBeenCalledWith("OpenRouter API response validation failed:", zodError.issues) + }) + + it("logs other errors and returns no providers", async () => { + const failure = new Error("network down") + vi.mocked(axios.get).mockRejectedValue(failure) + const consoleError = vi.spyOn(console, "error").mockImplementation(() => {}) + + const { result } = renderHook(() => useOpenRouterModelProviders("vendor/model"), { wrapper }) + + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + expect(result.current.data).toEqual({}) + expect(consoleError).toHaveBeenCalledWith("Error fetching OpenRouter providers:", failure) + }) +}) diff --git a/webview-ui/src/components/ui/hooks/useOpenRouterModelProviders.ts b/webview-ui/src/components/ui/hooks/useOpenRouterModelProviders.ts index 4ebe32443c..1994bdb9a8 100644 --- a/webview-ui/src/components/ui/hooks/useOpenRouterModelProviders.ts +++ b/webview-ui/src/components/ui/hooks/useOpenRouterModelProviders.ts @@ -87,7 +87,7 @@ async function getOpenRouterProvidersForModel(modelId: string, baseUrl?: string) } } catch (error) { if (error instanceof z.ZodError) { - console.error(`OpenRouter API response validation failed:`, error.errors) + console.error(`OpenRouter API response validation failed:`, error.issues) } else { console.error(`Error fetching OpenRouter providers:`, error) }