feat(models): support anthropic sonnet 4.6

This commit is contained in:
Peter Steinberger
2026-02-18 00:00:20 +01:00
parent a333d92013
commit ae2c8f2cf0
12 changed files with 93 additions and 10 deletions

View File

@@ -33,6 +33,7 @@ Docs: https://docs.openclaw.ai
- Plugins: add `before_agent_start` model/provider overrides before resolution. (#18568) Thanks @natefikru. - 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. - 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 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. - 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. - 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. - 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.

View File

@@ -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) - `pnpm test:live` (or `OPENCLAW_LIVE_TEST=1` if invoking Vitest directly)
- `OPENCLAW_LIVE_CLI_BACKEND=1` - `OPENCLAW_LIVE_CLI_BACKEND=1`
- Defaults: - Defaults:
- Model: `claude-cli/claude-sonnet-4-5` - Model: `claude-cli/claude-sonnet-4-6`
- Command: `claude` - Command: `claude`
- Args: `["-p","--output-format","json","--dangerously-skip-permissions"]` - Args: `["-p","--output-format","json","--dangerously-skip-permissions"]`
- Overrides (optional): - Overrides (optional):
@@ -219,7 +219,7 @@ Example:
```bash ```bash
OPENCLAW_LIVE_CLI_BACKEND=1 \ 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 pnpm test:live src/gateway/gateway-cli-backend.live.test.ts
``` ```

View File

@@ -1,8 +1,8 @@
import { type Api, completeSimple, type Model } from "@mariozechner/pi-ai";
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import { type Api, completeSimple, type Model } from "@mariozechner/pi-ai";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { import {
ANTHROPIC_SETUP_TOKEN_PREFIX, ANTHROPIC_SETUP_TOKEN_PREFIX,
@@ -142,6 +142,7 @@ function pickModel(models: Array<Model<Api>>, raw?: string): Model<Api> | null {
const preferred = [ const preferred = [
"claude-opus-4-5", "claude-opus-4-5",
"claude-sonnet-4-6",
"claude-sonnet-4-5", "claude-sonnet-4-5",
"claude-sonnet-4-0", "claude-sonnet-4-0",
"claude-haiku-3-5", "claude-haiku-3-5",

View File

@@ -20,9 +20,11 @@ const CLAUDE_MODEL_ALIASES: Record<string, string> = {
"claude-opus-4-5": "opus", "claude-opus-4-5": "opus",
"claude-opus-4": "opus", "claude-opus-4": "opus",
sonnet: "sonnet", sonnet: "sonnet",
"sonnet-4.6": "sonnet",
"sonnet-4.5": "sonnet", "sonnet-4.5": "sonnet",
"sonnet-4.1": "sonnet", "sonnet-4.1": "sonnet",
"sonnet-4.0": "sonnet", "sonnet-4.0": "sonnet",
"claude-sonnet-4-6": "sonnet",
"claude-sonnet-4-5": "sonnet", "claude-sonnet-4-5": "sonnet",
"claude-sonnet-4-1": "sonnet", "claude-sonnet-4-1": "sonnet",
"claude-sonnet-4-0": "sonnet", "claude-sonnet-4-0": "sonnet",

View File

@@ -5,6 +5,7 @@ export type ModelRef = {
const ANTHROPIC_PREFIXES = [ const ANTHROPIC_PREFIXES = [
"claude-opus-4-6", "claude-opus-4-6",
"claude-sonnet-4-6",
"claude-opus-4-5", "claude-opus-4-5",
"claude-sonnet-4-5", "claude-sonnet-4-5",
"claude-haiku-4-5", "claude-haiku-4-5",

View File

@@ -1,8 +1,8 @@
import type { Api, Model } from "@mariozechner/pi-ai"; import type { Api, Model } from "@mariozechner/pi-ai";
import type { ModelRegistry } from "./pi-model-discovery.js";
import { DEFAULT_CONTEXT_TOKENS } from "./defaults.js"; import { DEFAULT_CONTEXT_TOKENS } from "./defaults.js";
import { normalizeModelCompat } from "./model-compat.js"; import { normalizeModelCompat } from "./model-compat.js";
import { normalizeProviderId } from "./model-selection.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_GPT_53_MODEL_ID = "gpt-5.3-codex";
const OPENAI_CODEX_TEMPLATE_MODEL_IDS = ["gpt-5.2-codex"] as const; 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_MODEL_ID = "claude-opus-4-6";
const ANTHROPIC_OPUS_46_DOT_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_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_MODEL_ID = "glm-5";
const ZAI_GLM5_TEMPLATE_MODEL_IDS = ["glm-4.7"] as const; 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<Api> | 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. // 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. // When a user configures zai/glm-5 without a models.json entry, clone glm-4.7 as a forward-compat fallback.
function resolveZaiGlm5ForwardCompatModel( function resolveZaiGlm5ForwardCompatModel(
@@ -243,6 +284,7 @@ export function resolveForwardCompatModel(
return ( return (
resolveOpenAICodexGpt53FallbackModel(provider, modelId, modelRegistry) ?? resolveOpenAICodexGpt53FallbackModel(provider, modelId, modelRegistry) ??
resolveAnthropicOpus46ForwardCompatModel(provider, modelId, modelRegistry) ?? resolveAnthropicOpus46ForwardCompatModel(provider, modelId, modelRegistry) ??
resolveAnthropicSonnet46ForwardCompatModel(provider, modelId, modelRegistry) ??
resolveZaiGlm5ForwardCompatModel(provider, modelId, modelRegistry) ?? resolveZaiGlm5ForwardCompatModel(provider, modelId, modelRegistry) ??
resolveAntigravityOpus46ForwardCompatModel(provider, modelId, modelRegistry) resolveAntigravityOpus46ForwardCompatModel(provider, modelId, modelRegistry)
); );

View File

@@ -45,6 +45,14 @@ describe("model-selection", () => {
provider: "anthropic", provider: "anthropic",
model: "claude-opus-4-6", 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", () => { it("should use default provider if none specified", () => {

View File

@@ -1,7 +1,7 @@
import type { OpenClawConfig } from "../config/config.js"; import type { OpenClawConfig } from "../config/config.js";
import type { ModelCatalogEntry } from "./model-catalog.js";
import { resolveAgentModelPrimary } from "./agent-scope.js"; import { resolveAgentModelPrimary } from "./agent-scope.js";
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js"; import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js";
import type { ModelCatalogEntry } from "./model-catalog.js";
import { normalizeGoogleModelId } from "./models-config.providers.js"; import { normalizeGoogleModelId } from "./models-config.providers.js";
export type ModelRef = { export type ModelRef = {
@@ -19,6 +19,7 @@ export type ModelAliasIndex = {
const ANTHROPIC_MODEL_ALIASES: Record<string, string> = { const ANTHROPIC_MODEL_ALIASES: Record<string, string> = {
"opus-4.6": "claude-opus-4-6", "opus-4.6": "claude-opus-4-6",
"opus-4.5": "claude-opus-4-5", "opus-4.5": "claude-opus-4-5",
"sonnet-4.6": "claude-sonnet-4-6",
"sonnet-4.5": "claude-sonnet-4-5", "sonnet-4.5": "claude-sonnet-4-5",
}; };
const OPENAI_CODEX_OAUTH_MODEL_PREFIXES = ["gpt-5.3-codex"] as const; const OPENAI_CODEX_OAUTH_MODEL_PREFIXES = ["gpt-5.3-codex"] as const;

View File

@@ -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", () => { it("builds an antigravity forward-compat fallback for claude-opus-4-6-thinking", () => {
mockDiscoveredModel({ mockDiscoveredModel({
provider: "google-antigravity", provider: "google-antigravity",

View File

@@ -1,7 +1,7 @@
import { ensureAuthProfileStore } from "../agents/auth-profiles.js";
import type { OpenClawConfig, GatewayAuthConfig } from "../config/config.js"; import type { OpenClawConfig, GatewayAuthConfig } from "../config/config.js";
import type { RuntimeEnv } from "../runtime.js"; import type { RuntimeEnv } from "../runtime.js";
import type { WizardPrompter } from "../wizard/prompts.js"; import type { WizardPrompter } from "../wizard/prompts.js";
import { ensureAuthProfileStore } from "../agents/auth-profiles.js";
import { promptAuthChoiceGrouped } from "./auth-choice-prompt.js"; import { promptAuthChoiceGrouped } from "./auth-choice-prompt.js";
import { applyAuthChoice, resolvePreferredProviderForAuthChoice } from "./auth-choice.js"; import { applyAuthChoice, resolvePreferredProviderForAuthChoice } from "./auth-choice.js";
import { import {
@@ -30,6 +30,7 @@ function sanitizeTokenValue(value: string | undefined): string | undefined {
const ANTHROPIC_OAUTH_MODEL_KEYS = [ const ANTHROPIC_OAUTH_MODEL_KEYS = [
"anthropic/claude-opus-4-6", "anthropic/claude-opus-4-6",
"anthropic/claude-sonnet-4-6",
"anthropic/claude-opus-4-5", "anthropic/claude-opus-4-5",
"anthropic/claude-sonnet-4-5", "anthropic/claude-sonnet-4-5",
"anthropic/claude-haiku-4-5", "anthropic/claude-haiku-4-5",

View File

@@ -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 { DEFAULT_CONTEXT_TOKENS } from "../agents/defaults.js";
import { parseModelRef } from "../agents/model-selection.js"; import { parseModelRef } from "../agents/model-selection.js";
import { DEFAULT_AGENT_MAX_CONCURRENT, DEFAULT_SUBAGENT_MAX_CONCURRENT } from "./agent-limits.js"; import { DEFAULT_AGENT_MAX_CONCURRENT, DEFAULT_SUBAGENT_MAX_CONCURRENT } from "./agent-limits.js";
import { resolveTalkApiKey } from "./talk.js"; import { resolveTalkApiKey } from "./talk.js";
import type { OpenClawConfig } from "./types.js";
import type { ModelDefinitionConfig } from "./types.models.js";
type WarnState = { warned: boolean }; type WarnState = { warned: boolean };
@@ -14,7 +14,7 @@ type AnthropicAuthDefaultsMode = "api_key" | "oauth";
const DEFAULT_MODEL_ALIASES: Readonly<Record<string, string>> = { const DEFAULT_MODEL_ALIASES: Readonly<Record<string, string>> = {
// Anthropic (pi-ai catalog uses "latest" ids without date suffix) // Anthropic (pi-ai catalog uses "latest" ids without date suffix)
opus: "anthropic/claude-opus-4-6", opus: "anthropic/claude-opus-4-6",
sonnet: "anthropic/claude-sonnet-4-5", sonnet: "anthropic/claude-sonnet-4-6",
// OpenAI // OpenAI
gpt: "openai/gpt-5.2", gpt: "openai/gpt-5.2",

View File

@@ -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 CLI_RESUME = isTruthyEnvValue(process.env.OPENCLAW_LIVE_CLI_BACKEND_RESUME_PROBE);
const describeLive = LIVE && CLI_LIVE ? describe : describe.skip; 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_CLAUDE_ARGS = ["-p", "--output-format", "json", "--dangerously-skip-permissions"];
const DEFAULT_CODEX_ARGS = [ const DEFAULT_CODEX_ARGS = [
"exec", "exec",