diff --git a/CHANGELOG.md b/CHANGELOG.md index 108861360ef..e2191180ed0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ Docs: https://docs.openclaw.ai - Plugins: add `before_agent_start` model/provider overrides before resolution. (#18568) Thanks @natefikru. - Memory/Search: add FTS fallback plus query expansion for memory search. (#18304) Thanks @irchelper. - Agents/Models: support per-model `thinkingDefault` overrides in model config. (#18152) Thanks @wu-tian807. +- Agents/Models: support Anthropic Sonnet 4.6 (`anthropic/claude-sonnet-4-6`) across aliases/defaults with forward-compat fallback when upstream catalogs still only expose Sonnet 4.5. - Agents: enable `llms.txt` discovery in default behavior. (#18158) Thanks @yolo-maxi. - Feishu: add Bitable create-app/create-field tools for automation workflows. (#17963) Thanks @gaowanqi08141999. - Cron/Gateway: separate per-job webhook delivery (`delivery.mode = "webhook"`) from announce delivery, enforce valid HTTP(S) webhook URLs, and keep a temporary legacy `notify + cron.webhook` fallback for stored jobs. (#17901) Thanks @advaitpaliwal. diff --git a/docs/help/testing.md b/docs/help/testing.md index a0ab38f7843..325ca16f3e9 100644 --- a/docs/help/testing.md +++ b/docs/help/testing.md @@ -200,7 +200,7 @@ OPENCLAW_LIVE_SETUP_TOKEN=1 OPENCLAW_LIVE_SETUP_TOKEN_PROFILE=anthropic:setup-to - `pnpm test:live` (or `OPENCLAW_LIVE_TEST=1` if invoking Vitest directly) - `OPENCLAW_LIVE_CLI_BACKEND=1` - Defaults: - - Model: `claude-cli/claude-sonnet-4-5` + - Model: `claude-cli/claude-sonnet-4-6` - Command: `claude` - Args: `["-p","--output-format","json","--dangerously-skip-permissions"]` - Overrides (optional): @@ -219,7 +219,7 @@ Example: ```bash OPENCLAW_LIVE_CLI_BACKEND=1 \ - OPENCLAW_LIVE_CLI_BACKEND_MODEL="claude-cli/claude-sonnet-4-5" \ + OPENCLAW_LIVE_CLI_BACKEND_MODEL="claude-cli/claude-sonnet-4-6" \ pnpm test:live src/gateway/gateway-cli-backend.live.test.ts ``` diff --git a/src/agents/anthropic.setup-token.live.test.ts b/src/agents/anthropic.setup-token.live.test.ts index a8a7859c7f4..182a20e5c2a 100644 --- a/src/agents/anthropic.setup-token.live.test.ts +++ b/src/agents/anthropic.setup-token.live.test.ts @@ -1,8 +1,8 @@ +import { type Api, completeSimple, type Model } from "@mariozechner/pi-ai"; import { randomUUID } from "node:crypto"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { type Api, completeSimple, type Model } from "@mariozechner/pi-ai"; import { describe, expect, it } from "vitest"; import { ANTHROPIC_SETUP_TOKEN_PREFIX, @@ -142,6 +142,7 @@ function pickModel(models: Array>, raw?: string): Model | null { const preferred = [ "claude-opus-4-5", + "claude-sonnet-4-6", "claude-sonnet-4-5", "claude-sonnet-4-0", "claude-haiku-3-5", diff --git a/src/agents/cli-backends.ts b/src/agents/cli-backends.ts index 2f1db0f87a6..cf3cdb4bb18 100644 --- a/src/agents/cli-backends.ts +++ b/src/agents/cli-backends.ts @@ -20,9 +20,11 @@ const CLAUDE_MODEL_ALIASES: Record = { "claude-opus-4-5": "opus", "claude-opus-4": "opus", sonnet: "sonnet", + "sonnet-4.6": "sonnet", "sonnet-4.5": "sonnet", "sonnet-4.1": "sonnet", "sonnet-4.0": "sonnet", + "claude-sonnet-4-6": "sonnet", "claude-sonnet-4-5": "sonnet", "claude-sonnet-4-1": "sonnet", "claude-sonnet-4-0": "sonnet", diff --git a/src/agents/live-model-filter.ts b/src/agents/live-model-filter.ts index 97d22da9742..dbaba0c7df1 100644 --- a/src/agents/live-model-filter.ts +++ b/src/agents/live-model-filter.ts @@ -5,6 +5,7 @@ export type ModelRef = { const ANTHROPIC_PREFIXES = [ "claude-opus-4-6", + "claude-sonnet-4-6", "claude-opus-4-5", "claude-sonnet-4-5", "claude-haiku-4-5", diff --git a/src/agents/model-forward-compat.ts b/src/agents/model-forward-compat.ts index 9694c548d0f..dc566f9da5c 100644 --- a/src/agents/model-forward-compat.ts +++ b/src/agents/model-forward-compat.ts @@ -1,8 +1,8 @@ import type { Api, Model } from "@mariozechner/pi-ai"; +import type { ModelRegistry } from "./pi-model-discovery.js"; import { DEFAULT_CONTEXT_TOKENS } from "./defaults.js"; import { normalizeModelCompat } from "./model-compat.js"; import { normalizeProviderId } from "./model-selection.js"; -import type { ModelRegistry } from "./pi-model-discovery.js"; const OPENAI_CODEX_GPT_53_MODEL_ID = "gpt-5.3-codex"; const OPENAI_CODEX_TEMPLATE_MODEL_IDS = ["gpt-5.2-codex"] as const; @@ -10,6 +10,9 @@ const OPENAI_CODEX_TEMPLATE_MODEL_IDS = ["gpt-5.2-codex"] as const; const ANTHROPIC_OPUS_46_MODEL_ID = "claude-opus-4-6"; const ANTHROPIC_OPUS_46_DOT_MODEL_ID = "claude-opus-4.6"; const ANTHROPIC_OPUS_TEMPLATE_MODEL_IDS = ["claude-opus-4-5", "claude-opus-4.5"] as const; +const ANTHROPIC_SONNET_46_MODEL_ID = "claude-sonnet-4-6"; +const ANTHROPIC_SONNET_46_DOT_MODEL_ID = "claude-sonnet-4.6"; +const ANTHROPIC_SONNET_TEMPLATE_MODEL_IDS = ["claude-sonnet-4-5", "claude-sonnet-4.5"] as const; const ZAI_GLM5_MODEL_ID = "glm-5"; const ZAI_GLM5_TEMPLATE_MODEL_IDS = ["glm-4.7"] as const; @@ -139,6 +142,44 @@ function resolveAnthropicOpus46ForwardCompatModel( }); } +function resolveAnthropicSonnet46ForwardCompatModel( + provider: string, + modelId: string, + modelRegistry: ModelRegistry, +): Model | undefined { + const normalizedProvider = normalizeProviderId(provider); + if (normalizedProvider !== "anthropic") { + return undefined; + } + + const trimmedModelId = modelId.trim(); + const lower = trimmedModelId.toLowerCase(); + const isSonnet46 = + lower === ANTHROPIC_SONNET_46_MODEL_ID || + lower === ANTHROPIC_SONNET_46_DOT_MODEL_ID || + lower.startsWith(`${ANTHROPIC_SONNET_46_MODEL_ID}-`) || + lower.startsWith(`${ANTHROPIC_SONNET_46_DOT_MODEL_ID}-`); + if (!isSonnet46) { + return undefined; + } + + const templateIds: string[] = []; + if (lower.startsWith(ANTHROPIC_SONNET_46_MODEL_ID)) { + templateIds.push(lower.replace(ANTHROPIC_SONNET_46_MODEL_ID, "claude-sonnet-4-5")); + } + if (lower.startsWith(ANTHROPIC_SONNET_46_DOT_MODEL_ID)) { + templateIds.push(lower.replace(ANTHROPIC_SONNET_46_DOT_MODEL_ID, "claude-sonnet-4.5")); + } + templateIds.push(...ANTHROPIC_SONNET_TEMPLATE_MODEL_IDS); + + return cloneFirstTemplateModel({ + normalizedProvider, + trimmedModelId, + templateIds, + modelRegistry, + }); +} + // Z.ai's GLM-5 may not be present in pi-ai's built-in model catalog yet. // When a user configures zai/glm-5 without a models.json entry, clone glm-4.7 as a forward-compat fallback. function resolveZaiGlm5ForwardCompatModel( @@ -243,6 +284,7 @@ export function resolveForwardCompatModel( return ( resolveOpenAICodexGpt53FallbackModel(provider, modelId, modelRegistry) ?? resolveAnthropicOpus46ForwardCompatModel(provider, modelId, modelRegistry) ?? + resolveAnthropicSonnet46ForwardCompatModel(provider, modelId, modelRegistry) ?? resolveZaiGlm5ForwardCompatModel(provider, modelId, modelRegistry) ?? resolveAntigravityOpus46ForwardCompatModel(provider, modelId, modelRegistry) ); diff --git a/src/agents/model-selection.e2e.test.ts b/src/agents/model-selection.e2e.test.ts index 6638d5720b1..d04517d0166 100644 --- a/src/agents/model-selection.e2e.test.ts +++ b/src/agents/model-selection.e2e.test.ts @@ -45,6 +45,14 @@ describe("model-selection", () => { provider: "anthropic", model: "claude-opus-4-6", }); + expect(parseModelRef("anthropic/sonnet-4.6", "openai")).toEqual({ + provider: "anthropic", + model: "claude-sonnet-4-6", + }); + expect(parseModelRef("sonnet-4.6", "anthropic")).toEqual({ + provider: "anthropic", + model: "claude-sonnet-4-6", + }); }); it("should use default provider if none specified", () => { diff --git a/src/agents/model-selection.ts b/src/agents/model-selection.ts index 04a87ece072..4cba18fcc9a 100644 --- a/src/agents/model-selection.ts +++ b/src/agents/model-selection.ts @@ -1,7 +1,7 @@ import type { OpenClawConfig } from "../config/config.js"; +import type { ModelCatalogEntry } from "./model-catalog.js"; import { resolveAgentModelPrimary } from "./agent-scope.js"; import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js"; -import type { ModelCatalogEntry } from "./model-catalog.js"; import { normalizeGoogleModelId } from "./models-config.providers.js"; export type ModelRef = { @@ -19,6 +19,7 @@ export type ModelAliasIndex = { const ANTHROPIC_MODEL_ALIASES: Record = { "opus-4.6": "claude-opus-4-6", "opus-4.5": "claude-opus-4-5", + "sonnet-4.6": "claude-sonnet-4-6", "sonnet-4.5": "claude-sonnet-4-5", }; const OPENAI_CODEX_OAUTH_MODEL_PREFIXES = ["gpt-5.3-codex"] as const; diff --git a/src/agents/pi-embedded-runner/model.test.ts b/src/agents/pi-embedded-runner/model.test.ts index 68fab01ad6e..1c3cebce8d0 100644 --- a/src/agents/pi-embedded-runner/model.test.ts +++ b/src/agents/pi-embedded-runner/model.test.ts @@ -217,6 +217,32 @@ describe("resolveModel", () => { }); }); + it("builds an anthropic forward-compat fallback for claude-sonnet-4-6", () => { + mockDiscoveredModel({ + provider: "anthropic", + modelId: "claude-sonnet-4-5", + templateModel: buildForwardCompatTemplate({ + id: "claude-sonnet-4-5", + name: "Claude Sonnet 4.5", + provider: "anthropic", + api: "anthropic-messages", + baseUrl: "https://api.anthropic.com", + }), + }); + + expectResolvedForwardCompatFallback({ + provider: "anthropic", + id: "claude-sonnet-4-6", + expectedModel: { + provider: "anthropic", + id: "claude-sonnet-4-6", + api: "anthropic-messages", + baseUrl: "https://api.anthropic.com", + reasoning: true, + }, + }); + }); + it("builds an antigravity forward-compat fallback for claude-opus-4-6-thinking", () => { mockDiscoveredModel({ provider: "google-antigravity", diff --git a/src/commands/configure.gateway-auth.ts b/src/commands/configure.gateway-auth.ts index fa37d876f0d..a5560cd63ac 100644 --- a/src/commands/configure.gateway-auth.ts +++ b/src/commands/configure.gateway-auth.ts @@ -1,7 +1,7 @@ -import { ensureAuthProfileStore } from "../agents/auth-profiles.js"; import type { OpenClawConfig, GatewayAuthConfig } from "../config/config.js"; import type { RuntimeEnv } from "../runtime.js"; import type { WizardPrompter } from "../wizard/prompts.js"; +import { ensureAuthProfileStore } from "../agents/auth-profiles.js"; import { promptAuthChoiceGrouped } from "./auth-choice-prompt.js"; import { applyAuthChoice, resolvePreferredProviderForAuthChoice } from "./auth-choice.js"; import { @@ -30,6 +30,7 @@ function sanitizeTokenValue(value: string | undefined): string | undefined { const ANTHROPIC_OAUTH_MODEL_KEYS = [ "anthropic/claude-opus-4-6", + "anthropic/claude-sonnet-4-6", "anthropic/claude-opus-4-5", "anthropic/claude-sonnet-4-5", "anthropic/claude-haiku-4-5", diff --git a/src/config/defaults.ts b/src/config/defaults.ts index cecc8d9e226..c8c1c841926 100644 --- a/src/config/defaults.ts +++ b/src/config/defaults.ts @@ -1,9 +1,9 @@ +import type { OpenClawConfig } from "./types.js"; +import type { ModelDefinitionConfig } from "./types.models.js"; import { DEFAULT_CONTEXT_TOKENS } from "../agents/defaults.js"; import { parseModelRef } from "../agents/model-selection.js"; import { DEFAULT_AGENT_MAX_CONCURRENT, DEFAULT_SUBAGENT_MAX_CONCURRENT } from "./agent-limits.js"; import { resolveTalkApiKey } from "./talk.js"; -import type { OpenClawConfig } from "./types.js"; -import type { ModelDefinitionConfig } from "./types.models.js"; type WarnState = { warned: boolean }; @@ -14,7 +14,7 @@ type AnthropicAuthDefaultsMode = "api_key" | "oauth"; const DEFAULT_MODEL_ALIASES: Readonly> = { // Anthropic (pi-ai catalog uses "latest" ids without date suffix) opus: "anthropic/claude-opus-4-6", - sonnet: "anthropic/claude-sonnet-4-5", + sonnet: "anthropic/claude-sonnet-4-6", // OpenAI gpt: "openai/gpt-5.2", diff --git a/src/gateway/gateway-cli-backend.live.test.ts b/src/gateway/gateway-cli-backend.live.test.ts index f8a8439ce72..4724ee29fd8 100644 --- a/src/gateway/gateway-cli-backend.live.test.ts +++ b/src/gateway/gateway-cli-backend.live.test.ts @@ -18,7 +18,7 @@ const CLI_IMAGE = isTruthyEnvValue(process.env.OPENCLAW_LIVE_CLI_BACKEND_IMAGE_P const CLI_RESUME = isTruthyEnvValue(process.env.OPENCLAW_LIVE_CLI_BACKEND_RESUME_PROBE); const describeLive = LIVE && CLI_LIVE ? describe : describe.skip; -const DEFAULT_MODEL = "claude-cli/claude-sonnet-4-5"; +const DEFAULT_MODEL = "claude-cli/claude-sonnet-4-6"; const DEFAULT_CLAUDE_ARGS = ["-p", "--output-format", "json", "--dangerously-skip-permissions"]; const DEFAULT_CODEX_ARGS = [ "exec",