From 0b40d6476aa915171842cf4ffea605969ffd76a5 Mon Sep 17 00:00:00 2001 From: binjamshid <143622128+binjamshid@users.noreply.github.com> Date: Wed, 11 Nov 2026 08:52:45 +0400 Subject: [PATCH 1/2] fix(claude): version errors from a custom endpoint no longer say to run claude update --- .../Adapters/ClaudeAdapterV2.test.ts | 46 ++++++++++++++++++- .../Adapters/ClaudeAdapterV2.ts | 43 ++++++++++++++++- apps/server/src/provider/ClaudeProvider.ts | 13 +++--- .../src/provider/Drivers/ClaudeSkills.ts | 11 +++++ 4 files changed, 104 insertions(+), 9 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index 1add2822a01e9..6bc16f1cdffe0 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -6587,9 +6587,42 @@ describe("ClaudeAdapterV2 background wake turns", () => { expected: "Claude stopped: the prompt exceeds the model's context window. Run /compact or start a new thread.", }, + ...[ + { + name: "a model version error rewritten by a custom endpoint", + baseUrl: "https://gateway.example.test/v1", + installed: "2.1.322", + expected: + "The custom API endpoint gateway.example.test reported Claude Code 2.1.280, but this session runs Claude Code 2.1.322. Running claude update will not fix this; the endpoint or a proxy behind it rewrote the version.", + }, + { name: "a model version error without a custom endpoint", installed: "2.1.322" }, + { + name: "a model version error from an outdated Claude Code", + baseUrl: "https://gateway.example.test/v1", + installed: "2.1.280", + }, + ].map((row) => { + const result = + "API Error: 400 Claude Code 2.1.280 does not support this model; version 2.1.290 or newer is required. Run 'claude update', or update the Claude desktop app, then try again."; + return { + ...row, + isError: true, + result, + errors: undefined, + apiErrorStatus: undefined, + terminalReason: undefined, + environment: { + CLAUDE_CONFIG_DIR: "/synthetic/no-claude-config", + ...(row.baseUrl === undefined ? {} : { ANTHROPIC_BASE_URL: row.baseUrl }), + }, + expected: row.expected ?? result, + }; + }), ])("reports terminal API error text for $name", (input) => Effect.gen(function* () { - const harness = yield* makeWakeHarness; + const harness = yield* makeWakeHarnessWithOptions( + "environment" in input ? { environment: input.environment } : undefined, + ); const now = yield* DateTime.now; yield* harness.runtime.startTurn( makeClaudeTestTurnInput({ @@ -6602,6 +6635,17 @@ describe("ClaudeAdapterV2 background wake turns", () => { }), ); const apiErrorStatus = input.apiErrorStatus ?? 400; + if ("installed" in input) { + yield* harness.offerAndWait( + claudeSdkFrame({ + type: "system", + subtype: "init", + uuid: "00000000-0000-4000-8000-000000000698", + session_id: WAKE_NATIVE_SESSION, + claude_code_version: input.installed, + }), + ); + } yield* Queue.offer( harness.sdkMessages, makeResultFrame({ diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 7b63b7efc0b65..e377cce59e1c0 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -84,6 +84,7 @@ import { import { AgentScope } from "@t3tools/shared/AgentScope"; import * as HostProcess from "@t3tools/shared/HostProcess"; import { applyClaudePromptEffortPrefix } from "@t3tools/shared/model"; +import { compareSemverVersions } from "@t3tools/shared/semver"; import { CLAUDE_RESUME_COMPACTION_COMPACT_ANSWER, CLAUDE_RESUME_COMPACTION_NEVER_ANSWER, @@ -165,7 +166,9 @@ import { } from "../../provider/Drivers/ClaudeSkillDispatch.ts"; import { claudeLaunchArgDirectories, + customClaudeApiEndpointHost, discoverClaudeSkills, + readClaudeSettingsEnv, } from "../../provider/Drivers/ClaudeSkills.ts"; import { compileClaudeModelSelection, @@ -4106,6 +4109,23 @@ function gatewayCooldownResetAt( : undefined; } +const CLAUDE_UNSUPPORTED_VERSION_ERROR = + /^API Error: 400 Claude Code (\d+\.\d+\.\d+) does not support this model; version \d+\.\d+\.\d+ or newer is required\./; + +/** The version a gateway sent in place of the session's own, when a model error names an older one. */ +function claudeRewrittenVersion( + message: SDKResultMessage, + sessionVersion: string | undefined, +): string | undefined { + if (message.subtype !== "success" || message.terminal_reason !== "api_error") return undefined; + const reported = CLAUDE_UNSUPPORTED_VERSION_ERROR.exec(message.result.trim())?.[1]; + return reported !== undefined && + sessionVersion !== undefined && + compareSemverVersions(reported, sessionVersion) < 0 + ? reported + : undefined; +} + function providerFailureFromResult( message: SDKResultMessage, receivedAt: DateTime.Utc, @@ -4446,6 +4466,7 @@ interface ClaudeLiveQueryContext { receivedInit: boolean; // From system/init; feature-detects control requests such as MCP Apps. capabilities: ReadonlySet; + claudeCodeVersion: string | undefined; permissionMode: PermissionMode; stopping: boolean; readonly subagentsFromEarlierProcesses: ReadonlySet; @@ -10083,12 +10104,30 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( // The CLI also labels 403 authentication_failed; show its text because login cannot grant access. const permissionDenied = message.subtype === "success" && message.api_error_status === 403; + const rewrittenVersion = claudeRewrittenVersion(message, liveQuery.claudeCodeVersion); + const endpointHost = + rewrittenVersion === undefined + ? undefined + : customClaudeApiEndpointHost( + (yield* readClaudeSettingsEnv( + adapterOptions.settings, + context.input.runtimePolicy.cwd ?? undefined, + adapterOptions.environment, + ).pipe( + Effect.provideService(FileSystem.FileSystem, fileSystem), + Effect.provideService(Path.Path, path), + )).ANTHROPIC_BASE_URL ?? adapterOptions.environment.ANTHROPIC_BASE_URL, + ); + const rewrittenVersionError = + endpointHost === undefined + ? undefined + : `The custom API endpoint ${endpointHost} reported Claude Code ${rewrittenVersion}, but this session runs Claude Code ${liveQuery.claudeCodeVersion}. Running claude update will not fix this; the endpoint or a proxy behind it rewrote the version.`; const failureHint = permissionDenied ? message.result : (context.authenticationFailureMessage ?? (usageLimited ? "Claude usage limit reached. Send the message again once the limit resets." - : undefined)); + : rewrittenVersionError)); // Usage-limited /compact reports success; fail it so queued messages wait for the limit to clear. const limitedCompaction = usageLimited && message.subtype === "success" && message.local_command === "compact" @@ -11077,6 +11116,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( openedPermissionMode: queryOptions.permissionMode, receivedInit: false, capabilities: new Set(), + claudeCodeVersion: undefined, permissionMode: queryOptions.permissionMode, stopping: false, handedOffSubagents: new Set(), @@ -11117,6 +11157,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( message.permissionMode ?? context.openedPermissionMode; context.receivedInit = true; context.capabilities = new Set(message.capabilities ?? []); + context.claudeCodeVersion = message.claude_code_version; } if ( message.type === "system" && diff --git a/apps/server/src/provider/ClaudeProvider.ts b/apps/server/src/provider/ClaudeProvider.ts index 72221ff3187b4..47eaf7aa14864 100644 --- a/apps/server/src/provider/ClaudeProvider.ts +++ b/apps/server/src/provider/ClaudeProvider.ts @@ -45,7 +45,11 @@ import { hasClaudeManagedMcpConfig, makeClaudeEnvironment, } from "./Drivers/ClaudeHome.ts"; -import { discoverClaudeSkills, readClaudeSettingsEnv } from "./Drivers/ClaudeSkills.ts"; +import { + customClaudeApiEndpointHost, + discoverClaudeSkills, + readClaudeSettingsEnv, +} from "./Drivers/ClaudeSkills.ts"; import type { ProviderWorkspaceSnapshot } from "@t3tools/provider-core/server/driver"; import { makeUnavailableUsageLimits } from "@t3tools/provider-core/server/usageLimits"; import { @@ -671,12 +675,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( const baseUrl = (yield* readClaudeSettingsEnv(claudeSettings, cwd, resolvedEnvironment)).ANTHROPIC_BASE_URL ?? resolvedEnvironment.ANTHROPIC_BASE_URL; - const endpoint = URL.parse(baseUrl?.trim() ?? ""); - const customApiEndpoint = - endpoint !== null && - (endpoint.protocol === "http:" || endpoint.protocol === "https:") && - endpoint.hostname !== "" && - endpoint.hostname !== "api.anthropic.com"; + const customApiEndpoint = customClaudeApiEndpointHost(baseUrl) !== undefined; if (!customApiEndpoint && claudeAuthStatus(capabilities) === "unauthenticated") { const path = yield* Path.Path; const configDir = (yield* makeClaudeEnvironment(claudeSettings, resolvedEnvironment)) diff --git a/apps/server/src/provider/Drivers/ClaudeSkills.ts b/apps/server/src/provider/Drivers/ClaudeSkills.ts index 0d62109cce828..ada8a39e1ed5b 100644 --- a/apps/server/src/provider/Drivers/ClaudeSkills.ts +++ b/apps/server/src/provider/Drivers/ClaudeSkills.ts @@ -472,6 +472,17 @@ export const readClaudeSettingsEnv = Effect.fn("readClaudeSettingsEnv")(function return settingsEnv; }); +/** The host of an `ANTHROPIC_BASE_URL` that is not Anthropic's own API. */ +export function customClaudeApiEndpointHost(baseUrl: string | undefined): string | undefined { + const endpoint = URL.parse(baseUrl?.trim() ?? ""); + return endpoint !== null && + (endpoint.protocol === "http:" || endpoint.protocol === "https:") && + endpoint.hostname !== "" && + endpoint.hostname !== "api.anthropic.com" + ? endpoint.hostname + : undefined; +} + // Claude's --add-dir is repeatable and variadic; parseCliArgs keeps only one value. export function claudeLaunchArgDirectories(launchArgs?: string): ReadonlyArray { const directories: string[] = []; From dcb7261dfff6329dc18e47763e3c87dd78819b4b Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Nov 2026 05:20:42 +0000 Subject: [PATCH 2/2] fix(claude): keep claude update advice when the session is also below the required version AI-assisted; verified with the tests and tools listed above. --- .../Adapters/ClaudeAdapterV2.test.ts | 4 ++-- .../orchestration-v2/Adapters/ClaudeAdapterV2.ts | 14 ++++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index 6bc16f1cdffe0..3791d87abc171 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -6597,9 +6597,9 @@ describe("ClaudeAdapterV2 background wake turns", () => { }, { name: "a model version error without a custom endpoint", installed: "2.1.322" }, { - name: "a model version error from an outdated Claude Code", + name: "a model version error from a Claude Code still below the required version", baseUrl: "https://gateway.example.test/v1", - installed: "2.1.280", + installed: "2.1.285", }, ].map((row) => { const result = diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index e377cce59e1c0..20e5e6abc77f2 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -4110,18 +4110,20 @@ function gatewayCooldownResetAt( } const CLAUDE_UNSUPPORTED_VERSION_ERROR = - /^API Error: 400 Claude Code (\d+\.\d+\.\d+) does not support this model; version \d+\.\d+\.\d+ or newer is required\./; + /^API Error: 400 Claude Code (\d+\.\d+\.\d+) does not support this model; version (\d+\.\d+\.\d+) or newer is required\./; -/** The version a gateway sent in place of the session's own, when a model error names an older one. */ +/** The version a gateway sent in place of the session's own, when the session itself meets the requirement. */ function claudeRewrittenVersion( message: SDKResultMessage, sessionVersion: string | undefined, ): string | undefined { if (message.subtype !== "success" || message.terminal_reason !== "api_error") return undefined; - const reported = CLAUDE_UNSUPPORTED_VERSION_ERROR.exec(message.result.trim())?.[1]; - return reported !== undefined && - sessionVersion !== undefined && - compareSemverVersions(reported, sessionVersion) < 0 + const [, reported, required] = CLAUDE_UNSUPPORTED_VERSION_ERROR.exec(message.result.trim()) ?? []; + if (reported === undefined || required === undefined || sessionVersion === undefined) { + return undefined; + } + return compareSemverVersions(reported, sessionVersion) < 0 && + compareSemverVersions(sessionVersion, required) >= 0 ? reported : undefined; }