mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-10 08:41:13 +00:00
refactor(providers): move defaults and error policy into plugins
This commit is contained in:
@@ -46,6 +46,11 @@ function createGuardrailWrapStreamFn(
|
||||
|
||||
const PROVIDER_ID = "amazon-bedrock";
|
||||
const CLAUDE_46_MODEL_RE = /claude-(?:opus|sonnet)-4(?:\.|-)6(?:$|[-.])/i;
|
||||
const BEDROCK_CONTEXT_OVERFLOW_PATTERNS = [
|
||||
/ValidationException.*(?:input is too long|max input token|input token.*exceed)/i,
|
||||
/ValidationException.*(?:exceeds? the (?:maximum|max) (?:number of )?(?:input )?tokens)/i,
|
||||
/ModelStreamErrorException.*(?:Input is too long|too many input tokens)/i,
|
||||
] as const;
|
||||
|
||||
export async function registerAmazonBedrockPlugin(api: OpenClawPluginApi): Promise<void> {
|
||||
const guardrail = (api.pluginConfig as Record<string, unknown> | undefined)?.guardrail as
|
||||
@@ -86,6 +91,17 @@ export async function registerAmazonBedrockPlugin(api: OpenClawPluginApi): Promi
|
||||
resolveConfigApiKey: ({ env }) => resolveBedrockConfigApiKey(env),
|
||||
buildReplayPolicy: ({ modelId }) => buildAnthropicReplayPolicyForModel(modelId),
|
||||
wrapStreamFn,
|
||||
matchesContextOverflowError: ({ errorMessage }) =>
|
||||
BEDROCK_CONTEXT_OVERFLOW_PATTERNS.some((pattern) => pattern.test(errorMessage)),
|
||||
classifyFailoverReason: ({ errorMessage }) => {
|
||||
if (/ThrottlingException|Too many concurrent requests/i.test(errorMessage)) {
|
||||
return "rate_limit";
|
||||
}
|
||||
if (/ModelNotReadyException/i.test(errorMessage)) {
|
||||
return "overloaded";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
resolveDefaultThinkingLevel: ({ modelId }) =>
|
||||
CLAUDE_46_MODEL_RE.test(modelId.trim()) ? "adaptive" : undefined,
|
||||
});
|
||||
|
||||
225
extensions/anthropic/config-defaults.ts
Normal file
225
extensions/anthropic/config-defaults.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
|
||||
const ANTHROPIC_PROVIDER_API = "anthropic-messages";
|
||||
|
||||
function resolveAnthropicDefaultAuthMode(
|
||||
config: OpenClawConfig,
|
||||
env: NodeJS.ProcessEnv,
|
||||
): "api_key" | "oauth" | null {
|
||||
const profiles = config.auth?.profiles ?? {};
|
||||
const anthropicProfiles = Object.entries(profiles).filter(
|
||||
([, profile]) => profile?.provider === "anthropic",
|
||||
);
|
||||
|
||||
const order = config.auth?.order?.anthropic ?? [];
|
||||
for (const profileId of order) {
|
||||
const entry = profiles[profileId];
|
||||
if (!entry || entry.provider !== "anthropic") {
|
||||
continue;
|
||||
}
|
||||
if (entry.mode === "api_key") {
|
||||
return "api_key";
|
||||
}
|
||||
if (entry.mode === "oauth" || entry.mode === "token") {
|
||||
return "oauth";
|
||||
}
|
||||
}
|
||||
|
||||
const hasApiKey = anthropicProfiles.some(([, profile]) => profile?.mode === "api_key");
|
||||
const hasOauth = anthropicProfiles.some(
|
||||
([, profile]) => profile?.mode === "oauth" || profile?.mode === "token",
|
||||
);
|
||||
if (hasApiKey && !hasOauth) {
|
||||
return "api_key";
|
||||
}
|
||||
if (hasOauth && !hasApiKey) {
|
||||
return "oauth";
|
||||
}
|
||||
|
||||
if (env.ANTHROPIC_OAUTH_TOKEN?.trim()) {
|
||||
return "oauth";
|
||||
}
|
||||
if (env.ANTHROPIC_API_KEY?.trim()) {
|
||||
return "api_key";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveModelPrimaryValue(
|
||||
value: string | { primary?: string; fallbacks?: string[] } | undefined,
|
||||
): string | undefined {
|
||||
if (typeof value === "string") {
|
||||
const trimmed = value.trim();
|
||||
return trimmed || undefined;
|
||||
}
|
||||
const primary = value?.primary;
|
||||
if (typeof primary !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = primary.trim();
|
||||
return trimmed || undefined;
|
||||
}
|
||||
|
||||
function resolveAnthropicPrimaryModelRef(raw?: string): string | null {
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
const aliasKey = trimmed.toLowerCase();
|
||||
if (aliasKey === "opus") {
|
||||
return "anthropic/claude-opus-4-6";
|
||||
}
|
||||
if (aliasKey === "sonnet") {
|
||||
return "anthropic/claude-sonnet-4-6";
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function parseProviderModelRef(
|
||||
raw: string,
|
||||
defaultProvider: string,
|
||||
): { provider: string; model: string } | null {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
const slashIndex = trimmed.indexOf("/");
|
||||
if (slashIndex <= 0) {
|
||||
return { provider: defaultProvider, model: trimmed };
|
||||
}
|
||||
const provider = trimmed.slice(0, slashIndex).trim();
|
||||
const model = trimmed.slice(slashIndex + 1).trim();
|
||||
if (!provider || !model) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
provider: normalizeProviderId(provider),
|
||||
model,
|
||||
};
|
||||
}
|
||||
|
||||
function isAnthropicCacheRetentionTarget(
|
||||
parsed: { provider: string; model: string } | null | undefined,
|
||||
) {
|
||||
return Boolean(
|
||||
parsed &&
|
||||
(parsed.provider === "anthropic" ||
|
||||
(parsed.provider === "amazon-bedrock" &&
|
||||
parsed.model.toLowerCase().includes("anthropic.claude"))),
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeAnthropicProviderConfig<T extends { api?: string; models?: unknown[] }>(
|
||||
providerConfig: T,
|
||||
): T {
|
||||
if (
|
||||
providerConfig.api ||
|
||||
!Array.isArray(providerConfig.models) ||
|
||||
providerConfig.models.length === 0
|
||||
) {
|
||||
return providerConfig;
|
||||
}
|
||||
return { ...providerConfig, api: ANTHROPIC_PROVIDER_API };
|
||||
}
|
||||
|
||||
export function applyAnthropicConfigDefaults(params: {
|
||||
config: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): OpenClawConfig {
|
||||
const defaults = params.config.agents?.defaults;
|
||||
if (!defaults) {
|
||||
return params.config;
|
||||
}
|
||||
|
||||
const authMode = resolveAnthropicDefaultAuthMode(params.config, params.env);
|
||||
if (!authMode) {
|
||||
return params.config;
|
||||
}
|
||||
|
||||
let mutated = false;
|
||||
const nextDefaults = { ...defaults };
|
||||
const contextPruning = defaults.contextPruning ?? {};
|
||||
const heartbeat = defaults.heartbeat ?? {};
|
||||
|
||||
if (defaults.contextPruning?.mode === undefined) {
|
||||
nextDefaults.contextPruning = {
|
||||
...contextPruning,
|
||||
mode: "cache-ttl",
|
||||
ttl: defaults.contextPruning?.ttl ?? "1h",
|
||||
};
|
||||
mutated = true;
|
||||
}
|
||||
|
||||
if (defaults.heartbeat?.every === undefined) {
|
||||
nextDefaults.heartbeat = {
|
||||
...heartbeat,
|
||||
every: authMode === "oauth" ? "1h" : "30m",
|
||||
};
|
||||
mutated = true;
|
||||
}
|
||||
|
||||
if (authMode === "api_key") {
|
||||
const nextModels = defaults.models ? { ...defaults.models } : {};
|
||||
let modelsMutated = false;
|
||||
|
||||
for (const [key, entry] of Object.entries(nextModels)) {
|
||||
const parsed = parseProviderModelRef(key, "anthropic");
|
||||
if (!isAnthropicCacheRetentionTarget(parsed)) {
|
||||
continue;
|
||||
}
|
||||
const current = entry ?? {};
|
||||
const paramsValue = (current as { params?: Record<string, unknown> }).params ?? {};
|
||||
if (typeof paramsValue.cacheRetention === "string") {
|
||||
continue;
|
||||
}
|
||||
nextModels[key] = {
|
||||
...(current as Record<string, unknown>),
|
||||
params: { ...paramsValue, cacheRetention: "short" },
|
||||
};
|
||||
modelsMutated = true;
|
||||
}
|
||||
|
||||
const primary = resolveAnthropicPrimaryModelRef(
|
||||
resolveModelPrimaryValue(
|
||||
defaults.model as string | { primary?: string; fallbacks?: string[] } | undefined,
|
||||
),
|
||||
);
|
||||
if (primary) {
|
||||
const parsedPrimary = parseProviderModelRef(primary, "anthropic");
|
||||
if (isAnthropicCacheRetentionTarget(parsedPrimary)) {
|
||||
const key = `${parsedPrimary.provider}/${parsedPrimary.model}`;
|
||||
const entry = nextModels[key];
|
||||
const current = entry ?? {};
|
||||
const paramsValue = (current as { params?: Record<string, unknown> }).params ?? {};
|
||||
if (typeof paramsValue.cacheRetention !== "string") {
|
||||
nextModels[key] = {
|
||||
...(current as Record<string, unknown>),
|
||||
params: { ...paramsValue, cacheRetention: "short" },
|
||||
};
|
||||
modelsMutated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (modelsMutated) {
|
||||
nextDefaults.models = nextModels;
|
||||
mutated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!mutated) {
|
||||
return params.config;
|
||||
}
|
||||
|
||||
return {
|
||||
...params.config,
|
||||
agents: {
|
||||
...params.config.agents,
|
||||
defaults: nextDefaults,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -35,4 +35,51 @@ describe("anthropic provider replay hooks", () => {
|
||||
dropThinkingBlocks: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("defaults provider api through plugin config normalization", () => {
|
||||
const provider = registerSingleProviderPlugin(anthropicPlugin);
|
||||
|
||||
expect(
|
||||
provider.normalizeConfig?.({
|
||||
provider: "anthropic",
|
||||
providerConfig: {
|
||||
models: [{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" }],
|
||||
},
|
||||
} as never),
|
||||
).toMatchObject({
|
||||
api: "anthropic-messages",
|
||||
});
|
||||
});
|
||||
|
||||
it("applies Anthropic pruning defaults through plugin hooks", () => {
|
||||
const provider = registerSingleProviderPlugin(anthropicPlugin);
|
||||
|
||||
const next = provider.applyConfigDefaults?.({
|
||||
provider: "anthropic",
|
||||
env: {},
|
||||
config: {
|
||||
auth: {
|
||||
profiles: {
|
||||
"anthropic:api": { provider: "anthropic", mode: "api_key" },
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "anthropic/claude-opus-4-5" },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never);
|
||||
|
||||
expect(next?.agents?.defaults?.contextPruning).toMatchObject({
|
||||
mode: "cache-ttl",
|
||||
ttl: "1h",
|
||||
});
|
||||
expect(next?.agents?.defaults?.heartbeat).toMatchObject({
|
||||
every: "30m",
|
||||
});
|
||||
expect(
|
||||
next?.agents?.defaults?.models?.["anthropic/claude-opus-4-5"]?.params?.cacheRetention,
|
||||
).toBe("short");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,6 +30,10 @@ import { composeProviderStreamWrappers } from "openclaw/plugin-sdk/provider-stre
|
||||
import { fetchClaudeUsage } from "openclaw/plugin-sdk/provider-usage";
|
||||
import { buildAnthropicCliBackend } from "./cli-backend.js";
|
||||
import { buildAnthropicCliMigrationResult, hasClaudeCliAuth } from "./cli-migration.js";
|
||||
import {
|
||||
applyAnthropicConfigDefaults,
|
||||
normalizeAnthropicProviderConfig,
|
||||
} from "./config-defaults.js";
|
||||
import { anthropicMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
||||
import { buildAnthropicReplayPolicy } from "./replay-policy.js";
|
||||
import {
|
||||
@@ -445,6 +449,8 @@ export async function registerAnthropicPlugin(api: OpenClawPluginApi): Promise<v
|
||||
},
|
||||
}),
|
||||
],
|
||||
normalizeConfig: ({ providerConfig }) => normalizeAnthropicProviderConfig(providerConfig),
|
||||
applyConfigDefaults: ({ config, env }) => applyAnthropicConfigDefaults({ config, env }),
|
||||
resolveDynamicModel: (ctx) => resolveAnthropicForwardCompatModel(ctx),
|
||||
buildReplayPolicy: (ctx) => buildAnthropicReplayPolicy(ctx),
|
||||
isModernModelRef: ({ modelId }) => matchesAnthropicModernModel(modelId),
|
||||
|
||||
@@ -246,6 +246,8 @@ export default definePluginEntry({
|
||||
return null;
|
||||
},
|
||||
},
|
||||
classifyFailoverReason: ({ errorMessage }) =>
|
||||
/\bworkers?_ai\b.*\b(?:rate|limit|quota)\b/i.test(errorMessage) ? "rate_limit" : undefined,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -34,5 +34,7 @@ export default defineSingleProviderPluginEntry({
|
||||
catalog: {
|
||||
buildProvider: buildDeepSeekProvider,
|
||||
},
|
||||
matchesContextOverflowError: ({ errorMessage }) =>
|
||||
/\bdeepseek\b.*(?:input.*too long|context.*exceed)/i.test(errorMessage),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -89,6 +89,8 @@ export default defineSingleProviderPluginEntry({
|
||||
buildProvider: buildMistralProvider,
|
||||
allowExplicitBaseUrl: true,
|
||||
},
|
||||
matchesContextOverflowError: ({ errorMessage }) =>
|
||||
/\bmistral\b.*(?:input.*too long|token limit.*exceeded)/i.test(errorMessage),
|
||||
normalizeResolvedModel: ({ model }) => applyMistralModelCompat(model),
|
||||
contributeResolvedModelCompat: ({ modelId, model }) =>
|
||||
shouldContributeMistralCompat({ modelId, model }) ? MISTRAL_MODEL_COMPAT_PATCH : undefined,
|
||||
|
||||
@@ -168,6 +168,9 @@ export default definePluginEntry({
|
||||
client,
|
||||
};
|
||||
},
|
||||
matchesContextOverflowError: ({ errorMessage }) =>
|
||||
/\bollama\b.*(?:context length|too many tokens|context window)/i.test(errorMessage) ||
|
||||
/\btruncating input\b.*\btoo long\b/i.test(errorMessage),
|
||||
resolveSyntheticAuth: ({ providerConfig }) => {
|
||||
const hasApiConfig =
|
||||
Boolean(providerConfig?.api?.trim()) ||
|
||||
|
||||
@@ -259,6 +259,8 @@ export function buildOpenAIProvider(): ProviderPlugin {
|
||||
normalizeProviderId(ctx.provider) === PROVIDER_ID
|
||||
? wrapOpenAIProviderStream(ctx)
|
||||
: wrapAzureOpenAIProviderStream(ctx),
|
||||
matchesContextOverflowError: ({ errorMessage }) =>
|
||||
/content_filter.*(?:prompt|input).*(?:too long|exceed)/i.test(errorMessage),
|
||||
resolveTransportTurnState: (ctx) => resolveOpenAITransportTurnState(ctx),
|
||||
resolveWebSocketSessionPolicy: (ctx) => resolveOpenAIWebSocketSessionPolicy(ctx),
|
||||
resolveReasoningOutputMode: () => "native",
|
||||
|
||||
@@ -30,5 +30,9 @@ export default defineSingleProviderPluginEntry({
|
||||
catalog: {
|
||||
buildProvider: buildTogetherProvider,
|
||||
},
|
||||
classifyFailoverReason: ({ errorMessage }) =>
|
||||
/\bconcurrency limit\b.*\b(?:breached|reached)\b/i.test(errorMessage)
|
||||
? "rate_limit"
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -59,6 +59,12 @@ describe("classifyProviderSpecificError", () => {
|
||||
expect(classifyProviderSpecificError("concurrency limit reached")).toBe("rate_limit");
|
||||
});
|
||||
|
||||
it("classifies Cloudflare Workers AI quota errors as rate_limit", () => {
|
||||
expect(classifyProviderSpecificError("workers_ai gateway error: quota limit exceeded")).toBe(
|
||||
"rate_limit",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not match generic 'model is not ready' without Bedrock prefix", () => {
|
||||
expect(classifyProviderSpecificError("model is not ready")).toBeNull();
|
||||
});
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
/**
|
||||
* Provider-specific error patterns that improve failover classification accuracy.
|
||||
* Provider-owned error-pattern dispatch plus legacy fallback patterns.
|
||||
*
|
||||
* Many providers return errors in non-standard formats. Without these patterns,
|
||||
* errors get misclassified (e.g., a context overflow classified as "format"),
|
||||
* causing the failover engine to choose wrong recovery strategies.
|
||||
* Most provider-specific failover classification now lives on provider-plugin
|
||||
* hooks. This module keeps only fallback patterns for providers that do not
|
||||
* yet ship a dedicated provider plugin hook surface.
|
||||
*/
|
||||
|
||||
import {
|
||||
classifyProviderFailoverReasonWithPlugin,
|
||||
matchesProviderContextOverflowWithPlugin,
|
||||
} from "../../plugins/provider-runtime.js";
|
||||
import type { FailoverReason } from "./types.js";
|
||||
|
||||
type ProviderErrorPattern = {
|
||||
@@ -21,30 +25,9 @@ type ProviderErrorPattern = {
|
||||
* to catch provider-specific wording that the generic regex misses.
|
||||
*/
|
||||
export const PROVIDER_CONTEXT_OVERFLOW_PATTERNS: readonly RegExp[] = [
|
||||
// AWS Bedrock
|
||||
/ValidationException.*(?:input is too long|max input token|input token.*exceed)/i,
|
||||
/ValidationException.*(?:exceeds? the (?:maximum|max) (?:number of )?(?:input )?tokens)/i,
|
||||
/ModelStreamErrorException.*(?:Input is too long|too many input tokens)/i,
|
||||
|
||||
// Azure OpenAI (sometimes wraps OpenAI errors differently)
|
||||
/content_filter.*(?:prompt|input).*(?:too long|exceed)/i,
|
||||
|
||||
// Ollama / local models
|
||||
/\bollama\b.*(?:context length|too many tokens|context window)/i,
|
||||
/\btruncating input\b.*\btoo long\b/i,
|
||||
|
||||
// Mistral
|
||||
/\bmistral\b.*(?:input.*too long|token limit.*exceeded)/i,
|
||||
|
||||
// Cohere
|
||||
// Cohere does not currently ship a bundled provider hook.
|
||||
/\btotal tokens?.*exceeds? (?:the )?(?:model(?:'s)? )?(?:max|maximum|limit)/i,
|
||||
|
||||
// DeepSeek
|
||||
/\bdeepseek\b.*(?:input.*too long|context.*exceed)/i,
|
||||
|
||||
// Google Vertex / Gemini: INVALID_ARGUMENT with token-related messages is context overflow.
|
||||
/INVALID_ARGUMENT.*(?:exceeds? the (?:maximum|max)|input.*too (?:long|large))/i,
|
||||
|
||||
// Generic "input too long" pattern that isn't covered by existing checks
|
||||
/\binput (?:is )?too long for (?:the )?model\b/i,
|
||||
];
|
||||
@@ -55,38 +38,11 @@ export const PROVIDER_CONTEXT_OVERFLOW_PATTERNS: readonly RegExp[] = [
|
||||
* produce wrong results for specific providers.
|
||||
*/
|
||||
export const PROVIDER_SPECIFIC_PATTERNS: readonly ProviderErrorPattern[] = [
|
||||
// AWS Bedrock: ThrottlingException is rate limit
|
||||
{
|
||||
test: /ThrottlingException|Too many concurrent requests/i,
|
||||
reason: "rate_limit",
|
||||
},
|
||||
|
||||
// AWS Bedrock: ModelNotReadyException (require class prefix to avoid false positives)
|
||||
{
|
||||
test: /ModelNotReadyException/i,
|
||||
reason: "overloaded",
|
||||
},
|
||||
|
||||
// Azure: content_policy_violation should not trigger failover
|
||||
// (it's a content moderation rejection, not a transient error)
|
||||
|
||||
// Groq: model_deactivated is permanent
|
||||
// Groq does not currently ship a bundled provider hook.
|
||||
{
|
||||
test: /model(?:_is)?_deactivated|model has been deactivated/i,
|
||||
reason: "model_not_found",
|
||||
},
|
||||
|
||||
// Together AI / Fireworks: specific rate limit messages
|
||||
{
|
||||
test: /\bconcurrency limit\b.*\breached\b/i,
|
||||
reason: "rate_limit",
|
||||
},
|
||||
|
||||
// Cloudflare Workers AI
|
||||
{
|
||||
test: /\bworkers?_ai\b.*\b(?:rate|limit|quota)\b/i,
|
||||
reason: "rate_limit",
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -94,7 +50,11 @@ export const PROVIDER_SPECIFIC_PATTERNS: readonly ProviderErrorPattern[] = [
|
||||
* Called from `isContextOverflowError()` to catch provider-specific wording.
|
||||
*/
|
||||
export function matchesProviderContextOverflow(errorMessage: string): boolean {
|
||||
return PROVIDER_CONTEXT_OVERFLOW_PATTERNS.some((pattern) => pattern.test(errorMessage));
|
||||
return (
|
||||
matchesProviderContextOverflowWithPlugin({
|
||||
context: { errorMessage },
|
||||
}) || PROVIDER_CONTEXT_OVERFLOW_PATTERNS.some((pattern) => pattern.test(errorMessage))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,6 +62,12 @@ export function matchesProviderContextOverflow(errorMessage: string): boolean {
|
||||
* Returns null if no provider-specific pattern matches (fall through to generic classification).
|
||||
*/
|
||||
export function classifyProviderSpecificError(errorMessage: string): FailoverReason | null {
|
||||
const pluginReason = classifyProviderFailoverReasonWithPlugin({
|
||||
context: { errorMessage },
|
||||
});
|
||||
if (pluginReason) {
|
||||
return pluginReason;
|
||||
}
|
||||
for (const pattern of PROVIDER_SPECIFIC_PATTERNS) {
|
||||
if (pattern.test.test(errorMessage)) {
|
||||
return pattern.reason;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { DEFAULT_CONTEXT_TOKENS } from "../agents/defaults.js";
|
||||
import { normalizeProviderId, parseModelRef } from "../agents/model-selection.js";
|
||||
import { normalizeProviderId } from "../agents/model-selection.js";
|
||||
import { normalizeProviderSpecificConfig } from "../agents/models-config.providers.policy.js";
|
||||
import { applyProviderConfigDefaultsWithPlugin } from "../plugins/provider-runtime.js";
|
||||
import { DEFAULT_AGENT_MAX_CONCURRENT, DEFAULT_SUBAGENT_MAX_CONCURRENT } from "./agent-limits.js";
|
||||
import { resolveAgentModelPrimaryValue } from "./model-input.js";
|
||||
import {
|
||||
LEGACY_TALK_PROVIDER_ID,
|
||||
normalizeTalkConfig,
|
||||
@@ -16,8 +17,6 @@ type WarnState = { warned: boolean };
|
||||
|
||||
let defaultWarnState: WarnState = { warned: false };
|
||||
|
||||
type AnthropicAuthDefaultsMode = "api_key" | "oauth";
|
||||
|
||||
const DEFAULT_MODEL_ALIASES: Readonly<Record<string, string>> = {
|
||||
// Anthropic (pi-ai catalog uses "latest" ids without date suffix)
|
||||
opus: "anthropic/claude-opus-4-6",
|
||||
@@ -54,16 +53,6 @@ const MISTRAL_SAFE_MAX_TOKENS_BY_MODEL = {
|
||||
type ModelDefinitionLike = Partial<ModelDefinitionConfig> &
|
||||
Pick<ModelDefinitionConfig, "id" | "name">;
|
||||
|
||||
function resolveDefaultProviderApi(
|
||||
providerId: string,
|
||||
providerApi: ModelDefinitionConfig["api"] | undefined,
|
||||
): ModelDefinitionConfig["api"] | undefined {
|
||||
if (providerApi) {
|
||||
return providerApi;
|
||||
}
|
||||
return normalizeProviderId(providerId) === "anthropic" ? "anthropic-messages" : undefined;
|
||||
}
|
||||
|
||||
function isPositiveNumber(value: unknown): value is number {
|
||||
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
||||
}
|
||||
@@ -98,58 +87,6 @@ export function resolveNormalizedProviderModelMaxTokens(params: {
|
||||
return Math.min(safeMaxTokens, params.contextWindow);
|
||||
}
|
||||
|
||||
function resolveAnthropicDefaultAuthMode(cfg: OpenClawConfig): AnthropicAuthDefaultsMode | null {
|
||||
const profiles = cfg.auth?.profiles ?? {};
|
||||
const anthropicProfiles = Object.entries(profiles).filter(
|
||||
([, profile]) => profile?.provider === "anthropic",
|
||||
);
|
||||
|
||||
const order = cfg.auth?.order?.anthropic ?? [];
|
||||
for (const profileId of order) {
|
||||
const entry = profiles[profileId];
|
||||
if (!entry || entry.provider !== "anthropic") {
|
||||
continue;
|
||||
}
|
||||
if (entry.mode === "api_key") {
|
||||
return "api_key";
|
||||
}
|
||||
if (entry.mode === "oauth" || entry.mode === "token") {
|
||||
return "oauth";
|
||||
}
|
||||
}
|
||||
|
||||
const hasApiKey = anthropicProfiles.some(([, profile]) => profile?.mode === "api_key");
|
||||
const hasOauth = anthropicProfiles.some(
|
||||
([, profile]) => profile?.mode === "oauth" || profile?.mode === "token",
|
||||
);
|
||||
if (hasApiKey && !hasOauth) {
|
||||
return "api_key";
|
||||
}
|
||||
if (hasOauth && !hasApiKey) {
|
||||
return "oauth";
|
||||
}
|
||||
|
||||
if (process.env.ANTHROPIC_OAUTH_TOKEN?.trim()) {
|
||||
return "oauth";
|
||||
}
|
||||
if (process.env.ANTHROPIC_API_KEY?.trim()) {
|
||||
return "api_key";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolvePrimaryModelRef(raw?: string): string | null {
|
||||
if (!raw || typeof raw !== "string") {
|
||||
return null;
|
||||
}
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
const aliasKey = trimmed.toLowerCase();
|
||||
return DEFAULT_MODEL_ALIASES[aliasKey] ?? trimmed;
|
||||
}
|
||||
|
||||
export type SessionDefaultsOptions = {
|
||||
warn?: (message: string) => void;
|
||||
warnState?: WarnState;
|
||||
@@ -242,15 +179,19 @@ export function applyModelDefaults(cfg: OpenClawConfig): OpenClawConfig {
|
||||
if (providerConfig) {
|
||||
const nextProviders = { ...providerConfig };
|
||||
for (const [providerId, provider] of Object.entries(providerConfig)) {
|
||||
const models = provider.models;
|
||||
const normalizedProvider = normalizeProviderSpecificConfig(providerId, provider);
|
||||
const models = normalizedProvider.models;
|
||||
if (!Array.isArray(models) || models.length === 0) {
|
||||
if (normalizedProvider !== provider) {
|
||||
nextProviders[providerId] = normalizedProvider;
|
||||
mutated = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const providerApi = resolveDefaultProviderApi(providerId, provider.api);
|
||||
let nextProvider = provider;
|
||||
if (providerApi && provider.api !== providerApi) {
|
||||
const providerApi = normalizedProvider.api;
|
||||
let nextProvider = normalizedProvider;
|
||||
if (nextProvider !== provider) {
|
||||
mutated = true;
|
||||
nextProvider = { ...nextProvider, api: providerApi };
|
||||
}
|
||||
let providerMutated = false;
|
||||
const nextModels = models.map((model) => {
|
||||
@@ -434,105 +375,16 @@ export function applyLoggingDefaults(cfg: OpenClawConfig): OpenClawConfig {
|
||||
}
|
||||
|
||||
export function applyContextPruningDefaults(cfg: OpenClawConfig): OpenClawConfig {
|
||||
const defaults = cfg.agents?.defaults;
|
||||
if (!defaults) {
|
||||
return cfg;
|
||||
}
|
||||
|
||||
const authMode = resolveAnthropicDefaultAuthMode(cfg);
|
||||
if (!authMode) {
|
||||
return cfg;
|
||||
}
|
||||
|
||||
let mutated = false;
|
||||
const nextDefaults = { ...defaults };
|
||||
const contextPruning = defaults.contextPruning ?? {};
|
||||
const heartbeat = defaults.heartbeat ?? {};
|
||||
|
||||
if (defaults.contextPruning?.mode === undefined) {
|
||||
nextDefaults.contextPruning = {
|
||||
...contextPruning,
|
||||
mode: "cache-ttl",
|
||||
ttl: defaults.contextPruning?.ttl ?? "1h",
|
||||
};
|
||||
mutated = true;
|
||||
}
|
||||
|
||||
if (defaults.heartbeat?.every === undefined) {
|
||||
nextDefaults.heartbeat = {
|
||||
...heartbeat,
|
||||
every: authMode === "oauth" ? "1h" : "30m",
|
||||
};
|
||||
mutated = true;
|
||||
}
|
||||
|
||||
if (authMode === "api_key") {
|
||||
const nextModels = defaults.models ? { ...defaults.models } : {};
|
||||
let modelsMutated = false;
|
||||
const isAnthropicCacheRetentionTarget = (
|
||||
parsed: { provider: string; model: string } | null | undefined,
|
||||
): parsed is { provider: string; model: string } =>
|
||||
Boolean(
|
||||
parsed &&
|
||||
(parsed.provider === "anthropic" ||
|
||||
(parsed.provider === "amazon-bedrock" &&
|
||||
parsed.model.toLowerCase().includes("anthropic.claude"))),
|
||||
);
|
||||
|
||||
for (const [key, entry] of Object.entries(nextModels)) {
|
||||
const parsed = parseModelRef(key, "anthropic");
|
||||
if (!isAnthropicCacheRetentionTarget(parsed)) {
|
||||
continue;
|
||||
}
|
||||
const current = entry ?? {};
|
||||
const params = (current as { params?: Record<string, unknown> }).params ?? {};
|
||||
if (typeof params.cacheRetention === "string") {
|
||||
continue;
|
||||
}
|
||||
nextModels[key] = {
|
||||
...(current as Record<string, unknown>),
|
||||
params: { ...params, cacheRetention: "short" },
|
||||
};
|
||||
modelsMutated = true;
|
||||
}
|
||||
|
||||
const primary = resolvePrimaryModelRef(
|
||||
resolveAgentModelPrimaryValue(defaults.model) ?? undefined,
|
||||
);
|
||||
if (primary) {
|
||||
const parsedPrimary = parseModelRef(primary, "anthropic");
|
||||
if (isAnthropicCacheRetentionTarget(parsedPrimary)) {
|
||||
const key = `${parsedPrimary.provider}/${parsedPrimary.model}`;
|
||||
const entry = nextModels[key];
|
||||
const current = entry ?? {};
|
||||
const params = (current as { params?: Record<string, unknown> }).params ?? {};
|
||||
if (typeof params.cacheRetention !== "string") {
|
||||
nextModels[key] = {
|
||||
...(current as Record<string, unknown>),
|
||||
params: { ...params, cacheRetention: "short" },
|
||||
};
|
||||
modelsMutated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (modelsMutated) {
|
||||
nextDefaults.models = nextModels;
|
||||
mutated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!mutated) {
|
||||
return cfg;
|
||||
}
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: nextDefaults,
|
||||
},
|
||||
};
|
||||
return (
|
||||
applyProviderConfigDefaultsWithPlugin({
|
||||
provider: "anthropic",
|
||||
context: {
|
||||
provider: "anthropic",
|
||||
config: cfg,
|
||||
env: process.env,
|
||||
},
|
||||
}) ?? cfg
|
||||
);
|
||||
}
|
||||
|
||||
export function applyCompactionDefaults(cfg: OpenClawConfig): OpenClawConfig {
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
ProviderAuthMethod,
|
||||
ProviderAuthMethodNonInteractiveContext,
|
||||
ProviderAuthResult,
|
||||
ProviderApplyConfigDefaultsContext,
|
||||
ProviderBuildMissingAuthMessageContext,
|
||||
ProviderBuildUnknownModelHintContext,
|
||||
ProviderBuiltInModelSuppressionContext,
|
||||
@@ -28,6 +29,7 @@ import type {
|
||||
ProviderDeferSyntheticProfileAuthContext,
|
||||
ProviderDefaultThinkingPolicyContext,
|
||||
ProviderDiscoveryContext,
|
||||
ProviderFailoverErrorContext,
|
||||
ProviderFetchUsageSnapshotContext,
|
||||
ProviderModernModelPolicyContext,
|
||||
ProviderNormalizeConfigContext,
|
||||
@@ -77,6 +79,7 @@ export type {
|
||||
ProviderCatalogResult,
|
||||
ProviderDeferSyntheticProfileAuthContext,
|
||||
ProviderAugmentModelCatalogContext,
|
||||
ProviderApplyConfigDefaultsContext,
|
||||
ProviderBuiltInModelSuppressionContext,
|
||||
ProviderBuiltInModelSuppressionResult,
|
||||
ProviderBuildMissingAuthMessageContext,
|
||||
@@ -84,6 +87,7 @@ export type {
|
||||
ProviderCacheTtlEligibilityContext,
|
||||
ProviderDefaultThinkingPolicyContext,
|
||||
ProviderFetchUsageSnapshotContext,
|
||||
ProviderFailoverErrorContext,
|
||||
ProviderModernModelPolicyContext,
|
||||
ProviderNormalizeConfigContext,
|
||||
ProviderNormalizeToolSchemasContext,
|
||||
|
||||
@@ -34,7 +34,10 @@ let buildProviderAuthDoctorHintWithPlugin: typeof import("./provider-runtime.js"
|
||||
let buildProviderMissingAuthMessageWithPlugin: typeof import("./provider-runtime.js").buildProviderMissingAuthMessageWithPlugin;
|
||||
let buildProviderUnknownModelHintWithPlugin: typeof import("./provider-runtime.js").buildProviderUnknownModelHintWithPlugin;
|
||||
let applyProviderNativeStreamingUsageCompatWithPlugin: typeof import("./provider-runtime.js").applyProviderNativeStreamingUsageCompatWithPlugin;
|
||||
let applyProviderConfigDefaultsWithPlugin: typeof import("./provider-runtime.js").applyProviderConfigDefaultsWithPlugin;
|
||||
let formatProviderAuthProfileApiKeyWithPlugin: typeof import("./provider-runtime.js").formatProviderAuthProfileApiKeyWithPlugin;
|
||||
let classifyProviderFailoverReasonWithPlugin: typeof import("./provider-runtime.js").classifyProviderFailoverReasonWithPlugin;
|
||||
let matchesProviderContextOverflowWithPlugin: typeof import("./provider-runtime.js").matchesProviderContextOverflowWithPlugin;
|
||||
let normalizeProviderConfigWithPlugin: typeof import("./provider-runtime.js").normalizeProviderConfigWithPlugin;
|
||||
let normalizeProviderModelIdWithPlugin: typeof import("./provider-runtime.js").normalizeProviderModelIdWithPlugin;
|
||||
let applyProviderResolvedModelCompatWithPlugins: typeof import("./provider-runtime.js").applyProviderResolvedModelCompatWithPlugins;
|
||||
@@ -253,9 +256,12 @@ describe("provider-runtime", () => {
|
||||
buildProviderMissingAuthMessageWithPlugin,
|
||||
buildProviderUnknownModelHintWithPlugin,
|
||||
applyProviderNativeStreamingUsageCompatWithPlugin,
|
||||
applyProviderConfigDefaultsWithPlugin,
|
||||
applyProviderResolvedModelCompatWithPlugins,
|
||||
applyProviderResolvedTransportWithPlugin,
|
||||
classifyProviderFailoverReasonWithPlugin,
|
||||
formatProviderAuthProfileApiKeyWithPlugin,
|
||||
matchesProviderContextOverflowWithPlugin,
|
||||
normalizeProviderConfigWithPlugin,
|
||||
normalizeProviderModelIdWithPlugin,
|
||||
normalizeProviderTransportWithPlugin,
|
||||
@@ -383,6 +389,78 @@ describe("provider-runtime", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves provider config defaults through owner plugins", () => {
|
||||
resolveOwningPluginIdsForProviderMock.mockReturnValue(["anthropic"]);
|
||||
resolvePluginProvidersMock.mockReturnValue([
|
||||
{
|
||||
id: "anthropic",
|
||||
label: "Anthropic",
|
||||
auth: [],
|
||||
applyConfigDefaults: ({ config }) => ({
|
||||
...config,
|
||||
agents: {
|
||||
defaults: {
|
||||
heartbeat: { every: "1h" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
]);
|
||||
|
||||
expect(
|
||||
applyProviderConfigDefaultsWithPlugin({
|
||||
provider: "anthropic",
|
||||
context: {
|
||||
provider: "anthropic",
|
||||
env: {},
|
||||
config: {},
|
||||
},
|
||||
}),
|
||||
).toMatchObject({
|
||||
agents: {
|
||||
defaults: {
|
||||
heartbeat: {
|
||||
every: "1h",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves failover classification through hook-only aliases", () => {
|
||||
resolvePluginProvidersMock.mockReturnValue([
|
||||
{
|
||||
id: "openai",
|
||||
label: "OpenAI",
|
||||
hookAliases: ["azure-openai-responses"],
|
||||
auth: [],
|
||||
matchesContextOverflowError: ({ errorMessage }) =>
|
||||
/\bcontent_filter\b.*\btoo long\b/i.test(errorMessage),
|
||||
classifyFailoverReason: ({ errorMessage }) =>
|
||||
/\bquota exceeded\b/i.test(errorMessage) ? "rate_limit" : undefined,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(
|
||||
matchesProviderContextOverflowWithPlugin({
|
||||
provider: "azure-openai-responses",
|
||||
context: {
|
||||
provider: "azure-openai-responses",
|
||||
errorMessage: "content_filter prompt too long",
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
classifyProviderFailoverReasonWithPlugin({
|
||||
provider: "azure-openai-responses",
|
||||
context: {
|
||||
provider: "azure-openai-responses",
|
||||
errorMessage: "quota exceeded",
|
||||
},
|
||||
}),
|
||||
).toBe("rate_limit");
|
||||
});
|
||||
|
||||
it("resolves stream wrapper hooks through hook-only aliases without provider ownership", () => {
|
||||
const wrappedStreamFn = vi.fn();
|
||||
resolvePluginProvidersMock.mockReturnValue([
|
||||
|
||||
@@ -21,6 +21,7 @@ import type {
|
||||
ProviderCreateStreamFnContext,
|
||||
ProviderDefaultThinkingPolicyContext,
|
||||
ProviderFetchUsageSnapshotContext,
|
||||
ProviderFailoverErrorContext,
|
||||
ProviderNormalizeToolSchemasContext,
|
||||
ProviderNormalizeConfigContext,
|
||||
ProviderNormalizeModelIdContext,
|
||||
@@ -34,6 +35,7 @@ import type {
|
||||
ProviderPrepareExtraParamsContext,
|
||||
ProviderPrepareDynamicModelContext,
|
||||
ProviderPrepareRuntimeAuthContext,
|
||||
ProviderApplyConfigDefaultsContext,
|
||||
ProviderResolveConfigApiKeyContext,
|
||||
ProviderSanitizeReplayHistoryContext,
|
||||
ProviderResolveUsageAuthContext,
|
||||
@@ -593,6 +595,47 @@ export async function resolveProviderUsageSnapshotWithPlugin(params: {
|
||||
return await resolveProviderRuntimePlugin(params)?.fetchUsageSnapshot?.(params.context);
|
||||
}
|
||||
|
||||
export function matchesProviderContextOverflowWithPlugin(params: {
|
||||
provider?: string;
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
context: ProviderFailoverErrorContext;
|
||||
}): boolean {
|
||||
const plugins = params.provider
|
||||
? [resolveProviderHookPlugin({ ...params, provider: params.provider })].filter(
|
||||
(plugin): plugin is ProviderPlugin => Boolean(plugin),
|
||||
)
|
||||
: resolveProviderPluginsForHooks(params);
|
||||
for (const plugin of plugins) {
|
||||
if (plugin.matchesContextOverflowError?.(params.context)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function classifyProviderFailoverReasonWithPlugin(params: {
|
||||
provider?: string;
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
context: ProviderFailoverErrorContext;
|
||||
}) {
|
||||
const plugins = params.provider
|
||||
? [resolveProviderHookPlugin({ ...params, provider: params.provider })].filter(
|
||||
(plugin): plugin is ProviderPlugin => Boolean(plugin),
|
||||
)
|
||||
: resolveProviderPluginsForHooks(params);
|
||||
for (const plugin of plugins) {
|
||||
const reason = plugin.classifyFailoverReason?.(params.context);
|
||||
if (reason) {
|
||||
return reason;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function formatProviderAuthProfileApiKeyWithPlugin(params: {
|
||||
provider: string;
|
||||
config?: OpenClawConfig;
|
||||
@@ -663,6 +706,16 @@ export function resolveProviderDefaultThinkingLevel(params: {
|
||||
return resolveProviderRuntimePlugin(params)?.resolveDefaultThinkingLevel?.(params.context);
|
||||
}
|
||||
|
||||
export function applyProviderConfigDefaultsWithPlugin(params: {
|
||||
provider: string;
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
context: ProviderApplyConfigDefaultsContext;
|
||||
}) {
|
||||
return resolveProviderRuntimePlugin(params)?.applyConfigDefaults?.(params.context) ?? undefined;
|
||||
}
|
||||
|
||||
export function resolveProviderModernModelRef(params: {
|
||||
provider: string;
|
||||
config?: OpenClawConfig;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||
import type { StreamFn } from "@mariozechner/pi-agent-core";
|
||||
import type { Api, Model } from "@mariozechner/pi-ai";
|
||||
import type { ModelRegistry } from "@mariozechner/pi-coding-agent";
|
||||
import type { Command } from "commander";
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import type {
|
||||
ApiKeyCredential,
|
||||
AuthProfileCredential,
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
AuthProfileStore,
|
||||
} from "../agents/auth-profiles/types.js";
|
||||
import type { ModelCatalogEntry } from "../agents/model-catalog.js";
|
||||
import type { FailoverReason } from "../agents/pi-embedded-helpers/types.js";
|
||||
import type { ProviderRequestTransportOverrides } from "../agents/provider-request-config.js";
|
||||
import type { AnyAgentTool } from "../agents/tools/common.js";
|
||||
import type { ThinkLevel } from "../auto-reply/thinking.js";
|
||||
@@ -748,6 +749,30 @@ export type ProviderResolveWebSocketSessionPolicyContext = {
|
||||
sessionId?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Provider-owned failover error classification input.
|
||||
*
|
||||
* Use this when provider-specific transport or API errors need classification
|
||||
* hints that generic string matching cannot express safely.
|
||||
*/
|
||||
export type ProviderFailoverErrorContext = {
|
||||
provider?: string;
|
||||
modelId?: string;
|
||||
errorMessage: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Provider-owned config-default application input.
|
||||
*
|
||||
* Use this when a provider needs to add global config defaults that depend on
|
||||
* provider auth mode or provider-specific model families.
|
||||
*/
|
||||
export type ProviderApplyConfigDefaultsContext = {
|
||||
provider: string;
|
||||
config: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generic embedding provider shape returned by provider plugins.
|
||||
*
|
||||
@@ -1288,6 +1313,20 @@ export type ProviderPlugin = {
|
||||
fetchUsageSnapshot?: (
|
||||
ctx: ProviderFetchUsageSnapshotContext,
|
||||
) => Promise<ProviderUsageSnapshot | null | undefined> | ProviderUsageSnapshot | null | undefined;
|
||||
/**
|
||||
* Provider-owned failover context-overflow matcher.
|
||||
*
|
||||
* Return true when the provider recognizes the raw error as a context-window
|
||||
* overflow shape that generic heuristics would miss.
|
||||
*/
|
||||
matchesContextOverflowError?: (ctx: ProviderFailoverErrorContext) => boolean | undefined;
|
||||
/**
|
||||
* Provider-owned failover error classification.
|
||||
*
|
||||
* Return a failover reason when the provider recognizes a provider-specific
|
||||
* raw error shape. Return undefined to fall back to generic classification.
|
||||
*/
|
||||
classifyFailoverReason?: (ctx: ProviderFailoverErrorContext) => FailoverReason | null | undefined;
|
||||
/**
|
||||
* Provider-owned cache TTL eligibility.
|
||||
*
|
||||
@@ -1359,6 +1398,15 @@ export type ProviderPlugin = {
|
||||
resolveDefaultThinkingLevel?: (
|
||||
ctx: ProviderDefaultThinkingPolicyContext,
|
||||
) => "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "adaptive" | null | undefined;
|
||||
/**
|
||||
* Provider-owned global config defaults.
|
||||
*
|
||||
* Use this when config materialization needs provider-specific defaults that
|
||||
* depend on auth mode, env, or provider model-family semantics.
|
||||
*/
|
||||
applyConfigDefaults?: (
|
||||
ctx: ProviderApplyConfigDefaultsContext,
|
||||
) => OpenClawConfig | null | undefined;
|
||||
/**
|
||||
* Provider-owned "modern model" matcher used by live profile/smoke filters.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user