diff --git a/open-sse/config/providerModels.ts b/open-sse/config/providerModels.ts index 99a42ea019f7..6351ad72634a 100644 --- a/open-sse/config/providerModels.ts +++ b/open-sse/config/providerModels.ts @@ -61,6 +61,7 @@ export function getModelsByProviderId(providerId: string): RegistryModel[] { const CLAUDE_MODEL_PATTERN = /(?:^|[\/._-])claude(?:[._-]|$)/; const CLAUDE_MAX_EFFORT_UNSUPPORTED_FAMILY_PATTERNS = [/(?:^|[\/._-])haiku(?:[._-]|$)/] as const; +const ANTHROPIC_COMPATIBLE_PREFIX = "anthropic-compatible-"; export function supportsClaudeMaxEffort(modelId: string | null | undefined): boolean { if (typeof modelId !== "string" || modelId.length === 0) return false; @@ -73,19 +74,37 @@ export function supportsClaudeMaxEffort(modelId: string | null | undefined): boo ); } +function resolveProviderModelList(aliasOrId: string): { + alias: string; + models: RegistryModel[] | null; +} { + const resolvedId = aliasOrId.startsWith(ANTHROPIC_COMPATIBLE_PREFIX) ? "claude" : aliasOrId; + const alias = PROVIDER_ID_TO_ALIAS[resolvedId] || resolvedId; + const models = PROVIDER_MODELS[alias] || PROVIDER_MODELS[resolvedId] || null; + return { alias, models }; +} + export function supportsXHighEffort(aliasOrId: string, modelId: string): boolean { - const alias = PROVIDER_ID_TO_ALIAS[aliasOrId] || aliasOrId; - const providerModels = PROVIDER_MODELS[alias] || PROVIDER_MODELS[aliasOrId]; + const { models: providerModels } = resolveProviderModelList(aliasOrId); // Unknown provider (not in registry) โ€” pass through unchanged. if (!providerModels) return true; - const model = getProviderModel(alias, modelId); + const model = providerModels.find((entry) => entry.id === modelId); + + // Keep explicit false entries as the unsupported-model list. Unlisted models + // and models without an explicit flag pass through unchanged. + return model?.supportsXHighEffort !== false; +} + +export function supportsXHighEffortForMaxNormalization( + aliasOrId: string, + modelId: string +): boolean { + const { alias, models: providerModels } = resolveProviderModelList(aliasOrId); + if (!providerModels) return true; + const model = providerModels.find((entry) => entry.id === modelId); - // Claude Code models default to supporting extra-high effort. Keep explicit - // false entries as the unsupported-model list so newly added Claude models do - // not need opt-in flags. if (alias === "cc") { return model?.supportsXHighEffort !== false; } - return model?.supportsXHighEffort === true; } diff --git a/open-sse/executors/base.ts b/open-sse/executors/base.ts index f940e630d6c8..2da49931ff74 100644 --- a/open-sse/executors/base.ts +++ b/open-sse/executors/base.ts @@ -1,6 +1,10 @@ import { HTTP_STATUS, FETCH_TIMEOUT_MS } from "../config/constants.ts"; import { applyFingerprint, isCliCompatEnabled } from "../config/cliFingerprints.ts"; -import { supportsClaudeMaxEffort, supportsXHighEffort } from "../config/providerModels.ts"; +import { + supportsClaudeMaxEffort, + supportsXHighEffort, + supportsXHighEffortForMaxNormalization, +} from "../config/providerModels.ts"; import type { PoolConfig } from "../services/sessionPool/types.ts"; import type { Session } from "../services/sessionPool/session.ts"; import { SessionPool } from "../services/sessionPool/sessionPool.ts"; @@ -25,7 +29,10 @@ import { modelSupportsContext1mBeta, } from "../services/claudeCodeCompatible.ts"; import { getClaudeCodeCompatibleRequestDefaults } from "@/lib/providers/requestDefaults"; -import { cloakThirdPartyToolNames, remapToolNamesInRequest } from "../services/claudeCodeToolRemapper.ts"; +import { + cloakThirdPartyToolNames, + remapToolNamesInRequest, +} from "../services/claudeCodeToolRemapper.ts"; import { obfuscateInBody } from "../services/claudeCodeObfuscation.ts"; import { sanitizeClaudeToolSchemas } from "../translator/helpers/schemaCoercion.ts"; import { sanitizeResponsesInputItems } from "../services/responsesInputSanitizer.ts"; @@ -223,12 +230,11 @@ function hasActiveClaudeThinking(body: Record): boolean { * Each rejection burns a combo fallback attempt before reaching a working * provider. Apply provider-aware sanitation here (after transformRequest, so * reintroductions by per-provider transforms are also caught) before fetch. - * xhigh support is registry-gated: models that genuinely support xhigh pass - * through unchanged, and Claude models default to xhigh support unless marked - * as legacy unsupported entries. max support is Claude/CC-compatible only and + * xhigh support is opt-out: pass through unchanged unless the registry marks + * a model as unsupported. max support is Claude/CC-compatible only and * intentionally separate: older Opus/Sonnet models may support max even when - * they do not support xhigh. For OpenAI-shape providers, normalize max to - * xhigh when that top tier is allowed; otherwise downgrade to high. + * they do not support xhigh. For OpenAI-shape providers, keep the existing + * max normalization behavior. */ const MISTRAL_NO_REASONING_EFFORT_PATTERN = /devstral/i; const GITHUB_NO_REASONING_EFFORT_PATTERN = /(claude|haiku|oswe)/i; @@ -260,10 +266,10 @@ export function sanitizeReasoningEffortForProvider( const supportsXHigh = supportsXHighEffort(provider, modelStr); const shouldDowngradeXHigh = effortStr === "xhigh" && !supportsXHigh; - const shouldNormalizeMaxToXHigh = - effortStr === "max" && !supportsMaxEffortForProvider(provider, modelStr) && supportsXHigh; - const shouldDowngradeMax = - effortStr === "max" && !supportsMaxEffortForProvider(provider, modelStr) && !supportsXHigh; + const supportsXHighForMax = supportsXHighEffortForMaxNormalization(provider, modelStr); + const supportsMax = supportsMaxEffortForProvider(provider, modelStr); + const shouldNormalizeMaxToXHigh = effortStr === "max" && !supportsMax && supportsXHighForMax; + const shouldDowngradeMax = effortStr === "max" && !supportsMax && !supportsXHighForMax; if (shouldNormalizeMaxToXHigh) { log?.info?.( diff --git a/package-lock.json b/package-lock.json index 25c8f8dfc6ca..3b293886a84c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -93,7 +93,7 @@ "@testing-library/react": "^16.3.2", "@types/bcryptjs": "^3.0.0", "@types/better-sqlite3": "^7.6.13", - "@types/bun": "latest", + "@types/bun": "*", "@types/keytar": "^4.4.2", "@types/node": "^25.9.1", "@types/react": "^19.2.15", @@ -1351,9 +1351,9 @@ "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", - "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -1367,9 +1367,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", - "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -1383,9 +1383,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", - "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -1399,9 +1399,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", - "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -1415,9 +1415,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", - "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -1431,9 +1431,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", - "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -1447,9 +1447,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", - "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -1463,9 +1463,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", - "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -1479,9 +1479,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", - "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -1495,9 +1495,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", - "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -1511,9 +1511,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", - "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -1527,9 +1527,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", - "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -1543,9 +1543,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", - "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -1559,9 +1559,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", - "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -1575,9 +1575,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", - "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -1591,9 +1591,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", - "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -1607,9 +1607,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", - "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -1623,9 +1623,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", - "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -1639,9 +1639,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", - "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -1655,9 +1655,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", - "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -1671,9 +1671,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", - "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -1687,9 +1687,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", - "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -1703,9 +1703,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", - "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -1719,9 +1719,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", - "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -1735,9 +1735,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", - "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -1751,9 +1751,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", - "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -10317,9 +10317,9 @@ } }, "node_modules/esbuild": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", - "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -10329,32 +10329,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.0", - "@esbuild/android-arm": "0.28.0", - "@esbuild/android-arm64": "0.28.0", - "@esbuild/android-x64": "0.28.0", - "@esbuild/darwin-arm64": "0.28.0", - "@esbuild/darwin-x64": "0.28.0", - "@esbuild/freebsd-arm64": "0.28.0", - "@esbuild/freebsd-x64": "0.28.0", - "@esbuild/linux-arm": "0.28.0", - "@esbuild/linux-arm64": "0.28.0", - "@esbuild/linux-ia32": "0.28.0", - "@esbuild/linux-loong64": "0.28.0", - "@esbuild/linux-mips64el": "0.28.0", - "@esbuild/linux-ppc64": "0.28.0", - "@esbuild/linux-riscv64": "0.28.0", - "@esbuild/linux-s390x": "0.28.0", - "@esbuild/linux-x64": "0.28.0", - "@esbuild/netbsd-arm64": "0.28.0", - "@esbuild/netbsd-x64": "0.28.0", - "@esbuild/openbsd-arm64": "0.28.0", - "@esbuild/openbsd-x64": "0.28.0", - "@esbuild/openharmony-arm64": "0.28.0", - "@esbuild/sunos-x64": "0.28.0", - "@esbuild/win32-arm64": "0.28.0", - "@esbuild/win32-ia32": "0.28.0", - "@esbuild/win32-x64": "0.28.0" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, "node_modules/escalade": { diff --git a/scripts/build/prepublish.ts b/scripts/build/prepublish.ts index b5c2e14dfba2..afcdca2be355 100644 --- a/scripts/build/prepublish.ts +++ b/scripts/build/prepublish.ts @@ -309,6 +309,15 @@ if (existsSync(opencodePluginSrc) && existsSync(join(opencodePluginSrc, "package } else { console.log(" โœ… @omniroute/opencode-plugin dist/ already present (skipping rebuild)"); } + // Remove plugin node_modules after build โ€” hard links created by npm install on Linux + // (CI runner) end up in the tarball as LINK entries, which npm registry rejects with + // E415 "Hard link is not allowed". The node_modules are only needed for the tsup build; + // they must not ship in the published package. + const pluginNodeModules = join(opencodePluginSrc, "node_modules"); + if (existsSync(pluginNodeModules)) { + rmSync(pluginNodeModules, { recursive: true, force: true }); + console.log(" ๐Ÿงน Removed @omniroute/opencode-plugin/node_modules (hard link guard)"); + } } else { console.log(" โญ๏ธ @omniroute/opencode-plugin not found in workspace (skipping build)"); } diff --git a/tests/unit/base-executor-sanitize-effort.test.ts b/tests/unit/base-executor-sanitize-effort.test.ts index 9f2eea1266e8..fc909674785d 100644 --- a/tests/unit/base-executor-sanitize-effort.test.ts +++ b/tests/unit/base-executor-sanitize-effort.test.ts @@ -11,7 +11,7 @@ function makeLog() { }; } -test("sanitizeReasoningEffortForProvider: xiaomi-mimo downgrades xhigh โ†’ high", () => { +test("sanitizeReasoningEffortForProvider: xiaomi-mimo preserves xhigh by default", () => { const log = makeLog(); const body = { model: "mimo-v2.5-pro", @@ -19,15 +19,60 @@ test("sanitizeReasoningEffortForProvider: xiaomi-mimo downgrades xhigh โ†’ high" messages: [{ role: "user", content: "hi" }], }; const result = sanitizeReasoningEffortForProvider(body, "xiaomi-mimo", "mimo-v2.5-pro", log); + assert.equal(result, body, "xhigh passes through unless the model explicitly opts out"); + assert.equal((result as any).reasoning_effort, "xhigh"); + assert.equal((result as any).model, "mimo-v2.5-pro", "other fields preserved"); + assert.equal(log.messages.length, 0); +}); + +test("sanitizeReasoningEffortForProvider: OpenRouter DeepSeek preserves xhigh", () => { + const body = { + model: "deepseek/deepseek-v4-pro", + reasoning_effort: "xhigh", + messages: [{ role: "user", content: "hi" }], + }; + const result = sanitizeReasoningEffortForProvider( + body, + "openrouter", + "deepseek/deepseek-v4-pro", + null + ); + assert.equal(result, body); + assert.equal((result as any).reasoning_effort, "xhigh"); +}); + +test("sanitizeReasoningEffortForProvider: explicit xhigh opt-out downgrades to high", () => { + const log = makeLog(); + const body = { + model: "claude-opus-4-6", + reasoning_effort: "xhigh", + messages: [{ role: "user", content: "hi" }], + }; + const result = sanitizeReasoningEffortForProvider(body, "claude", "claude-opus-4-6", log); assert.notEqual(result, body, "must return a new object when mutating"); assert.equal((result as any).reasoning_effort, "high"); - assert.equal((result as any).model, "mimo-v2.5-pro", "other fields preserved"); assert.ok( log.messages.some(([tag, m]) => tag === "REASONING_SANITIZE" && /xhigh โ†’ high/.test(m)), "logs the downgrade" ); }); +test("sanitizeReasoningEffortForProvider: Anthropic-compatible dynamic provider honors xhigh opt-out", () => { + const body = { + model: "claude-opus-4-6", + reasoning_effort: "xhigh", + messages: [{ role: "user", content: "hi" }], + }; + const result = sanitizeReasoningEffortForProvider( + body, + "anthropic-compatible-test", + "claude-opus-4-6", + null + ); + assert.notEqual(result, body, "must return a new object when mutating"); + assert.equal((result as any).reasoning_effort, "high"); +}); + test("sanitizeReasoningEffortForProvider: xiaomi-mimo downgrades max โ†’ high", () => { const log = makeLog(); const body = { @@ -125,29 +170,25 @@ test("sanitizeReasoningEffortForProvider: claude preserves max for Opus/Sonnet a assert.equal((haikuResult as any).reasoning_effort, "high"); }); -test("sanitizeReasoningEffortForProvider: xiaomi-mimo downgrades xhigh in nested reasoning.effort", () => { +test("sanitizeReasoningEffortForProvider: xiaomi-mimo preserves nested xhigh by default", () => { const body = { model: "mimo-v2.5-pro", reasoning: { effort: "xhigh", summary: "auto" }, messages: [], }; const result = sanitizeReasoningEffortForProvider(body, "xiaomi-mimo", "mimo-v2.5-pro", null); - assert.equal((result as any).reasoning.effort, "high"); + assert.equal(result, body); + assert.equal((result as any).reasoning.effort, "xhigh"); assert.equal((result as any).reasoning.summary, "auto", "other reasoning fields preserved"); }); -test("sanitizeReasoningEffortForProvider: nested reasoning downgrade preserves Responses shape", () => { +test("sanitizeReasoningEffortForProvider: explicit xhigh opt-out preserves Responses shape", () => { const body = { - model: "responses-only-model", + model: "claude-opus-4-6", reasoning: { effort: "xhigh", summary: "auto" }, input: [], }; - const result = sanitizeReasoningEffortForProvider( - body, - "xiaomi-mimo", - "responses-only-model", - null - ); + const result = sanitizeReasoningEffortForProvider(body, "claude", "claude-opus-4-6", null); assert.equal((result as any).reasoning.effort, "high"); assert.equal((result as any).reasoning_effort, undefined); }); @@ -197,22 +238,14 @@ test("sanitizeReasoningEffortForProvider: mistral/devstral preserves reasoning w assert.deepEqual((result as any).reasoning, { summary: "auto" }); }); -test("sanitizeReasoningEffortForProvider: codex with xhigh passes through unchanged when model supports it", () => { - // codex/gpt-5.5-xhigh is flagged supportsXHighEffort:true in providerRegistry. - // Claude Opus 4.7+ models default to xhigh support unless explicitly opted out. +test("sanitizeReasoningEffortForProvider: codex with xhigh passes through unchanged", () => { const body = { model: "gpt-5.5-xhigh", reasoning_effort: "xhigh", messages: [], }; const result = sanitizeReasoningEffortForProvider(body, "codex", "gpt-5.5-xhigh", null); - // Either passes through unchanged (supportsXHighEffort=true) - // or the registry doesn't flag it โ€” in which case downgrade is acceptable. - // We assert no error and that some reasoning_effort is present. - assert.ok( - (result as any).reasoning_effort === "xhigh" || (result as any).reasoning_effort === "high", - "either preserved (xhigh) or downgraded (high)" - ); + assert.equal((result as any).reasoning_effort, "xhigh"); }); test("sanitizeReasoningEffortForProvider: no-op when reasoning_effort absent", () => { @@ -224,10 +257,8 @@ test("sanitizeReasoningEffortForProvider: no-op when reasoning_effort absent", ( test("sanitizeReasoningEffortForProvider: handles unknown providers as pass-through", () => { const body = { model: "some-model", reasoning_effort: "xhigh", messages: [] }; const result = sanitizeReasoningEffortForProvider(body, "unknown-provider", "some-model", null); - // unknown provider + xhigh + model not in registry โ†’ supportsXHighEffort returns false โ†’ downgrade - // OR unknown provider isn't in the strip list โ†’ returns xhigh - // Both are acceptable behavior; we just assert no exception thrown. - assert.ok(result !== undefined); + assert.equal(result, body); + assert.equal((result as any).reasoning_effort, "xhigh"); }); test("sanitizeReasoningEffortForProvider: non-object body returns unchanged", () => { diff --git a/tests/unit/executor-base-utils.test.ts b/tests/unit/executor-base-utils.test.ts index d3727c205e26..fd869228982d 100644 --- a/tests/unit/executor-base-utils.test.ts +++ b/tests/unit/executor-base-utils.test.ts @@ -122,10 +122,10 @@ test("sanitizeReasoningEffortForProvider passes through body without reasoning_e assert.deepEqual(result, body); }); -test("sanitizeReasoningEffortForProvider clamps xhigh to high for unsupported providers", () => { +test("sanitizeReasoningEffortForProvider preserves xhigh unless explicitly unsupported", () => { const body = { reasoning_effort: "xhigh" }; const result = base.sanitizeReasoningEffortForProvider(body, "openai", "gpt-4o") as any; - assert.equal(result.reasoning_effort, "high"); + assert.equal(result.reasoning_effort, "xhigh"); }); test("sanitizeReasoningEffortForProvider preserves high effort", () => { diff --git a/tests/unit/provider-models-config.test.ts b/tests/unit/provider-models-config.test.ts index 0ebd754469d9..bdfcc5377977 100644 --- a/tests/unit/provider-models-config.test.ts +++ b/tests/unit/provider-models-config.test.ts @@ -12,6 +12,7 @@ import { isValidModel, supportsClaudeMaxEffort, supportsXHighEffort, + supportsXHighEffortForMaxNormalization, } from "../../open-sse/config/providerModels.ts"; test("provider models helpers expose model lists and defaults", () => { @@ -106,7 +107,7 @@ test("Claude max effort support excludes Haiku family and non-Claude IDs", () => assert.equal(supportsClaudeMaxEffort("claude-future-5-0"), true); }); -test("Claude xhigh effort support defaults on for new models and opts out legacy models", () => { +test("xhigh effort support defaults to pass-through and opts out explicit false models", () => { const claudeModels = new Set(getModelsByProviderId("claude").map((model) => model.id)); assert.ok(claudeModels.has("claude-opus-4-8")); @@ -115,4 +116,38 @@ test("Claude xhigh effort support defaults on for new models and opts out legacy assert.equal(supportsXHighEffort("claude", "claude-opus-4-6"), false); assert.equal(supportsXHighEffort("claude", "claude-sonnet-4-6"), false); assert.equal(supportsXHighEffort("claude", "claude-future-5-0"), true); + assert.equal(supportsXHighEffort("anthropic-compatible-test", "claude-opus-4-6"), false); + assert.equal(supportsXHighEffort("anthropic-compatible-test", "claude-opus-4-7"), true); + assert.equal(supportsXHighEffort("anthropic-compatible-cc-test", "claude-opus-4-6"), false); + assert.equal(supportsXHighEffort("anthropic-compatible-cc-test", "claude-opus-4-7"), true); + assert.equal(supportsXHighEffort("openrouter", "deepseek/deepseek-v4-pro"), true); + assert.equal(supportsXHighEffort("deepseek", "deepseek-v4-pro"), true); +}); + +test("max normalization keeps existing xhigh opt-in behavior", () => { + assert.equal( + supportsXHighEffortForMaxNormalization("openai-compatible-free1", "gemini-3.1-pro-preview"), + true + ); + assert.equal(supportsXHighEffortForMaxNormalization("xiaomi-mimo", "mimo-v2.5-pro"), false); + assert.equal( + supportsXHighEffortForMaxNormalization("anthropic-compatible-cc-test", "claude-opus-4-6"), + false + ); + assert.equal( + supportsXHighEffortForMaxNormalization("anthropic-compatible-cc-test", "claude-opus-4-7"), + true + ); + assert.equal( + supportsXHighEffortForMaxNormalization("anthropic-compatible-test", "claude-opus-4-6"), + false + ); + assert.equal( + supportsXHighEffortForMaxNormalization("anthropic-compatible-test", "claude-opus-4-7"), + true + ); + assert.equal( + supportsXHighEffortForMaxNormalization("openrouter", "deepseek/deepseek-v4-pro"), + false + ); });