mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-05 13:10:22 +00:00
fix(providers): stabilize runtime normalization hooks
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
||||||
import { registerAmazonBedrockPlugin } from "./register.runtime.js";
|
import { registerAmazonBedrockPlugin } from "./register.sync.runtime.js";
|
||||||
|
|
||||||
export default definePluginEntry({
|
export default definePluginEntry({
|
||||||
id: "amazon-bedrock",
|
id: "amazon-bedrock",
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import type { StreamFn } from "@mariozechner/pi-agent-core";
|
import type { StreamFn } from "@mariozechner/pi-agent-core";
|
||||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
||||||
|
import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared";
|
||||||
import {
|
import {
|
||||||
createBedrockNoCacheWrapper,
|
createBedrockNoCacheWrapper,
|
||||||
isAnthropicBedrockModel,
|
isAnthropicBedrockModel,
|
||||||
streamWithPayloadPatch,
|
streamWithPayloadPatch,
|
||||||
} from "openclaw/plugin-sdk/provider-stream";
|
} from "openclaw/plugin-sdk/provider-stream";
|
||||||
|
import {
|
||||||
|
mergeImplicitBedrockProvider,
|
||||||
|
resolveBedrockConfigApiKey,
|
||||||
|
resolveImplicitBedrockProvider,
|
||||||
|
} from "./api.js";
|
||||||
|
|
||||||
type GuardrailConfig = {
|
type GuardrailConfig = {
|
||||||
guardrailIdentifier: string;
|
guardrailIdentifier: string;
|
||||||
@@ -38,7 +44,7 @@ function createGuardrailWrapStreamFn(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function registerAmazonBedrockPlugin(api: OpenClawPluginApi): Promise<void> {
|
export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void {
|
||||||
// Keep registration-local constants inside the function so partial module
|
// Keep registration-local constants inside the function so partial module
|
||||||
// initialization during test bootstrap cannot trip TDZ reads.
|
// initialization during test bootstrap cannot trip TDZ reads.
|
||||||
const providerId = "amazon-bedrock";
|
const providerId = "amazon-bedrock";
|
||||||
@@ -48,15 +54,6 @@ export async function registerAmazonBedrockPlugin(api: OpenClawPluginApi): Promi
|
|||||||
/ValidationException.*(?:exceeds? the (?:maximum|max) (?:number of )?(?:input )?tokens)/i,
|
/ValidationException.*(?:exceeds? the (?:maximum|max) (?:number of )?(?:input )?tokens)/i,
|
||||||
/ModelStreamErrorException.*(?:Input is too long|too many input tokens)/i,
|
/ModelStreamErrorException.*(?:Input is too long|too many input tokens)/i,
|
||||||
] as const;
|
] as const;
|
||||||
// Defer provider-owned helper loading until registration so test/plugin-loader
|
|
||||||
// cycles cannot re-enter this module before its constants initialize.
|
|
||||||
const [
|
|
||||||
{ buildProviderReplayFamilyHooks },
|
|
||||||
{ mergeImplicitBedrockProvider, resolveBedrockConfigApiKey, resolveImplicitBedrockProvider },
|
|
||||||
] = await Promise.all([
|
|
||||||
import("openclaw/plugin-sdk/provider-model-shared"),
|
|
||||||
import("./api.js"),
|
|
||||||
]);
|
|
||||||
const anthropicByModelReplayHooks = buildProviderReplayFamilyHooks({
|
const anthropicByModelReplayHooks = buildProviderReplayFamilyHooks({
|
||||||
family: "anthropic-by-model",
|
family: "anthropic-by-model",
|
||||||
});
|
});
|
||||||
@@ -6,6 +6,6 @@ export default definePluginEntry({
|
|||||||
name: "Anthropic Provider",
|
name: "Anthropic Provider",
|
||||||
description: "Bundled Anthropic provider plugin",
|
description: "Bundled Anthropic provider plugin",
|
||||||
register(api) {
|
register(api) {
|
||||||
registerAnthropicPlugin(api);
|
return registerAnthropicPlugin(api);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type {
|
|||||||
} from "openclaw/plugin-sdk/plugin-entry";
|
} from "openclaw/plugin-sdk/plugin-entry";
|
||||||
import {
|
import {
|
||||||
applyAuthProfileConfig,
|
applyAuthProfileConfig,
|
||||||
|
createProviderApiKeyAuthMethod,
|
||||||
ensureApiKeyFromOptionEnvOrPrompt,
|
ensureApiKeyFromOptionEnvOrPrompt,
|
||||||
listProfilesForProvider,
|
listProfilesForProvider,
|
||||||
normalizeApiKeyInput,
|
normalizeApiKeyInput,
|
||||||
@@ -17,11 +18,13 @@ import {
|
|||||||
} from "openclaw/plugin-sdk/provider-auth";
|
} from "openclaw/plugin-sdk/provider-auth";
|
||||||
import { cloneFirstTemplateModel } from "openclaw/plugin-sdk/provider-model-shared";
|
import { cloneFirstTemplateModel } from "openclaw/plugin-sdk/provider-model-shared";
|
||||||
import { fetchClaudeUsage } from "openclaw/plugin-sdk/provider-usage";
|
import { fetchClaudeUsage } from "openclaw/plugin-sdk/provider-usage";
|
||||||
|
import { buildAnthropicCliBackend } from "./cli-backend.js";
|
||||||
import { buildAnthropicCliMigrationResult, hasClaudeCliAuth } from "./cli-migration.js";
|
import { buildAnthropicCliMigrationResult, hasClaudeCliAuth } from "./cli-migration.js";
|
||||||
import {
|
import {
|
||||||
applyAnthropicConfigDefaults,
|
applyAnthropicConfigDefaults,
|
||||||
normalizeAnthropicProviderConfig,
|
normalizeAnthropicProviderConfig,
|
||||||
} from "./config-defaults.js";
|
} from "./config-defaults.js";
|
||||||
|
import { anthropicMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
||||||
import { buildAnthropicReplayPolicy } from "./replay-policy.js";
|
import { buildAnthropicReplayPolicy } from "./replay-policy.js";
|
||||||
import { wrapAnthropicProviderStream } from "./stream-wrappers.js";
|
import { wrapAnthropicProviderStream } from "./stream-wrappers.js";
|
||||||
|
|
||||||
@@ -200,7 +203,7 @@ async function runAnthropicCliMigrationNonInteractive(ctx: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function registerAnthropicPlugin(api: OpenClawPluginApi): Promise<void> {
|
export function registerAnthropicPlugin(api: OpenClawPluginApi): void {
|
||||||
const claudeCliProfileId = "anthropic:claude-cli";
|
const claudeCliProfileId = "anthropic:claude-cli";
|
||||||
const providerId = "anthropic";
|
const providerId = "anthropic";
|
||||||
const defaultAnthropicModel = "anthropic/claude-sonnet-4-6";
|
const defaultAnthropicModel = "anthropic/claude-sonnet-4-6";
|
||||||
@@ -211,45 +214,7 @@ export async function registerAnthropicPlugin(api: OpenClawPluginApi): Promise<v
|
|||||||
"anthropic/claude-sonnet-4-5",
|
"anthropic/claude-sonnet-4-5",
|
||||||
"anthropic/claude-haiku-4-5",
|
"anthropic/claude-haiku-4-5",
|
||||||
] as const;
|
] as const;
|
||||||
let createApiKeyAuthMethod:
|
api.registerCliBackend(buildAnthropicCliBackend());
|
||||||
| (typeof import("openclaw/plugin-sdk/provider-auth-api-key"))["createProviderApiKeyAuthMethod"]
|
|
||||||
| undefined;
|
|
||||||
let mediaUnderstandingProvider:
|
|
||||||
| (typeof import("./media-understanding-provider.js"))["anthropicMediaUnderstandingProvider"]
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
// Avoid touching a partially initialized static binding during cyclic bootstrap.
|
|
||||||
try {
|
|
||||||
const cliBackendModule = await import("./cli-backend.js");
|
|
||||||
const cliBackend =
|
|
||||||
typeof cliBackendModule.buildAnthropicCliBackend === "function"
|
|
||||||
? cliBackendModule.buildAnthropicCliBackend()
|
|
||||||
: undefined;
|
|
||||||
if (cliBackend) {
|
|
||||||
api.registerCliBackend(cliBackend);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Best-effort during test bootstrap; provider registration still proceeds.
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const providerApiKeyAuthModule = await import("openclaw/plugin-sdk/provider-auth-api-key");
|
|
||||||
createApiKeyAuthMethod =
|
|
||||||
typeof providerApiKeyAuthModule.createProviderApiKeyAuthMethod === "function"
|
|
||||||
? providerApiKeyAuthModule.createProviderApiKeyAuthMethod
|
|
||||||
: undefined;
|
|
||||||
} catch {
|
|
||||||
createApiKeyAuthMethod = undefined;
|
|
||||||
}
|
|
||||||
if (!createApiKeyAuthMethod) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const mediaUnderstandingModule = await import("./media-understanding-provider.js");
|
|
||||||
mediaUnderstandingProvider =
|
|
||||||
mediaUnderstandingModule.anthropicMediaUnderstandingProvider ?? undefined;
|
|
||||||
} catch {
|
|
||||||
mediaUnderstandingProvider = undefined;
|
|
||||||
}
|
|
||||||
api.registerProvider({
|
api.registerProvider({
|
||||||
id: providerId,
|
id: providerId,
|
||||||
label: "Anthropic",
|
label: "Anthropic",
|
||||||
@@ -291,7 +256,7 @@ export async function registerAnthropicPlugin(api: OpenClawPluginApi): Promise<v
|
|||||||
runtime: ctx.runtime,
|
runtime: ctx.runtime,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
createApiKeyAuthMethod({
|
createProviderApiKeyAuthMethod({
|
||||||
providerId,
|
providerId,
|
||||||
methodId: "api-key",
|
methodId: "api-key",
|
||||||
label: "Anthropic API key",
|
label: "Anthropic API key",
|
||||||
@@ -337,7 +302,5 @@ export async function registerAnthropicPlugin(api: OpenClawPluginApi): Promise<v
|
|||||||
profileId: ctx.profileId,
|
profileId: ctx.profileId,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (mediaUnderstandingProvider) {
|
api.registerMediaUnderstandingProvider(anthropicMediaUnderstandingProvider);
|
||||||
api.registerMediaUnderstandingProvider(mediaUnderstandingProvider);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,16 @@
|
|||||||
import { definePluginEntry, type ProviderAuthContext } from "openclaw/plugin-sdk/plugin-entry";
|
import { definePluginEntry, type ProviderAuthContext } from "openclaw/plugin-sdk/plugin-entry";
|
||||||
|
import {
|
||||||
|
coerceSecretRef,
|
||||||
|
DEFAULT_COPILOT_API_BASE_URL,
|
||||||
|
ensureAuthProfileStore,
|
||||||
|
fetchCopilotUsage,
|
||||||
|
githubCopilotLoginCommand,
|
||||||
|
listProfilesForProvider,
|
||||||
|
PROVIDER_ID,
|
||||||
|
resolveCopilotApiToken,
|
||||||
|
resolveCopilotForwardCompatModel,
|
||||||
|
wrapCopilotProviderStream,
|
||||||
|
} from "./register.runtime.js";
|
||||||
|
|
||||||
const COPILOT_ENV_VARS = ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"];
|
const COPILOT_ENV_VARS = ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"];
|
||||||
const COPILOT_XHIGH_MODEL_IDS = ["gpt-5.2", "gpt-5.2-codex"] as const;
|
const COPILOT_XHIGH_MODEL_IDS = ["gpt-5.2", "gpt-5.2-codex"] as const;
|
||||||
@@ -15,19 +27,7 @@ export default definePluginEntry({
|
|||||||
id: "github-copilot",
|
id: "github-copilot",
|
||||||
name: "GitHub Copilot Provider",
|
name: "GitHub Copilot Provider",
|
||||||
description: "Bundled GitHub Copilot provider plugin",
|
description: "Bundled GitHub Copilot provider plugin",
|
||||||
async register(api) {
|
register(api) {
|
||||||
const {
|
|
||||||
coerceSecretRef,
|
|
||||||
DEFAULT_COPILOT_API_BASE_URL,
|
|
||||||
ensureAuthProfileStore,
|
|
||||||
fetchCopilotUsage,
|
|
||||||
githubCopilotLoginCommand,
|
|
||||||
listProfilesForProvider,
|
|
||||||
PROVIDER_ID,
|
|
||||||
resolveCopilotApiToken,
|
|
||||||
resolveCopilotForwardCompatModel,
|
|
||||||
wrapCopilotProviderStream,
|
|
||||||
} = await import("./register.runtime.js");
|
|
||||||
function resolveFirstGithubToken(params: { agentDir?: string; env: NodeJS.ProcessEnv }): {
|
function resolveFirstGithubToken(params: { agentDir?: string; env: NodeJS.ProcessEnv }): {
|
||||||
githubToken: string;
|
githubToken: string;
|
||||||
hasProfile: boolean;
|
hasProfile: boolean;
|
||||||
|
|||||||
@@ -1,10 +1,24 @@
|
|||||||
export {
|
import {
|
||||||
coerceSecretRef,
|
coerceSecretRef,
|
||||||
ensureAuthProfileStore,
|
ensureAuthProfileStore,
|
||||||
listProfilesForProvider,
|
listProfilesForProvider,
|
||||||
} from "openclaw/plugin-sdk/provider-auth";
|
} from "openclaw/plugin-sdk/provider-auth";
|
||||||
export { githubCopilotLoginCommand } from "openclaw/plugin-sdk/provider-auth-login";
|
import { githubCopilotLoginCommand } from "openclaw/plugin-sdk/provider-auth-login";
|
||||||
export { PROVIDER_ID, resolveCopilotForwardCompatModel } from "./models.js";
|
import { PROVIDER_ID, resolveCopilotForwardCompatModel } from "./models.js";
|
||||||
export { wrapCopilotAnthropicStream, wrapCopilotProviderStream } from "./stream.js";
|
import { wrapCopilotAnthropicStream, wrapCopilotProviderStream } from "./stream.js";
|
||||||
export { DEFAULT_COPILOT_API_BASE_URL, resolveCopilotApiToken } from "./token.js";
|
import { DEFAULT_COPILOT_API_BASE_URL, resolveCopilotApiToken } from "./token.js";
|
||||||
export { fetchCopilotUsage } from "./usage.js";
|
import { fetchCopilotUsage } from "./usage.js";
|
||||||
|
|
||||||
|
export {
|
||||||
|
coerceSecretRef,
|
||||||
|
DEFAULT_COPILOT_API_BASE_URL,
|
||||||
|
ensureAuthProfileStore,
|
||||||
|
fetchCopilotUsage,
|
||||||
|
githubCopilotLoginCommand,
|
||||||
|
listProfilesForProvider,
|
||||||
|
PROVIDER_ID,
|
||||||
|
resolveCopilotApiToken,
|
||||||
|
resolveCopilotForwardCompatModel,
|
||||||
|
wrapCopilotAnthropicStream,
|
||||||
|
wrapCopilotProviderStream,
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,6 +5,18 @@ import {
|
|||||||
type ProviderRuntimeModel,
|
type ProviderRuntimeModel,
|
||||||
type ProviderWrapStreamFnContext,
|
type ProviderWrapStreamFnContext,
|
||||||
} from "openclaw/plugin-sdk/plugin-entry";
|
} from "openclaw/plugin-sdk/plugin-entry";
|
||||||
|
import {
|
||||||
|
applyOpenrouterConfig,
|
||||||
|
buildOpenrouterProvider,
|
||||||
|
buildProviderReplayFamilyHooks,
|
||||||
|
buildProviderStreamFamilyHooks,
|
||||||
|
createProviderApiKeyAuthMethod,
|
||||||
|
DEFAULT_CONTEXT_TOKENS,
|
||||||
|
getOpenRouterModelCapabilities,
|
||||||
|
loadOpenRouterModelCapabilities,
|
||||||
|
OPENROUTER_DEFAULT_MODEL_REF,
|
||||||
|
openrouterMediaUnderstandingProvider,
|
||||||
|
} from "./register.runtime.js";
|
||||||
|
|
||||||
const PROVIDER_ID = "openrouter";
|
const PROVIDER_ID = "openrouter";
|
||||||
const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
|
const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
|
||||||
@@ -20,19 +32,7 @@ export default definePluginEntry({
|
|||||||
id: "openrouter",
|
id: "openrouter",
|
||||||
name: "OpenRouter Provider",
|
name: "OpenRouter Provider",
|
||||||
description: "Bundled OpenRouter provider plugin",
|
description: "Bundled OpenRouter provider plugin",
|
||||||
async register(api) {
|
register(api) {
|
||||||
const {
|
|
||||||
buildProviderReplayFamilyHooks,
|
|
||||||
buildProviderStreamFamilyHooks,
|
|
||||||
createProviderApiKeyAuthMethod,
|
|
||||||
DEFAULT_CONTEXT_TOKENS,
|
|
||||||
getOpenRouterModelCapabilities,
|
|
||||||
loadOpenRouterModelCapabilities,
|
|
||||||
OPENROUTER_DEFAULT_MODEL_REF,
|
|
||||||
openrouterMediaUnderstandingProvider,
|
|
||||||
applyOpenrouterConfig,
|
|
||||||
buildOpenrouterProvider,
|
|
||||||
} = await import("./register.runtime.js");
|
|
||||||
const PASSTHROUGH_GEMINI_REPLAY_HOOKS = buildProviderReplayFamilyHooks({
|
const PASSTHROUGH_GEMINI_REPLAY_HOOKS = buildProviderReplayFamilyHooks({
|
||||||
family: "passthrough-gemini",
|
family: "passthrough-gemini",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
export { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
|
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
|
||||||
export {
|
import {
|
||||||
buildProviderReplayFamilyHooks,
|
buildProviderReplayFamilyHooks,
|
||||||
DEFAULT_CONTEXT_TOKENS,
|
DEFAULT_CONTEXT_TOKENS,
|
||||||
} from "openclaw/plugin-sdk/provider-model-shared";
|
} from "openclaw/plugin-sdk/provider-model-shared";
|
||||||
export {
|
import {
|
||||||
buildProviderStreamFamilyHooks,
|
buildProviderStreamFamilyHooks,
|
||||||
createOpenRouterSystemCacheWrapper,
|
createOpenRouterSystemCacheWrapper,
|
||||||
createOpenRouterWrapper,
|
createOpenRouterWrapper,
|
||||||
@@ -11,6 +11,22 @@ export {
|
|||||||
isProxyReasoningUnsupported,
|
isProxyReasoningUnsupported,
|
||||||
loadOpenRouterModelCapabilities,
|
loadOpenRouterModelCapabilities,
|
||||||
} from "openclaw/plugin-sdk/provider-stream";
|
} from "openclaw/plugin-sdk/provider-stream";
|
||||||
export { openrouterMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
import { openrouterMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
||||||
export { applyOpenrouterConfig, OPENROUTER_DEFAULT_MODEL_REF } from "./onboard.js";
|
import { applyOpenrouterConfig, OPENROUTER_DEFAULT_MODEL_REF } from "./onboard.js";
|
||||||
export { buildOpenrouterProvider } from "./provider-catalog.js";
|
import { buildOpenrouterProvider } from "./provider-catalog.js";
|
||||||
|
|
||||||
|
export {
|
||||||
|
applyOpenrouterConfig,
|
||||||
|
buildOpenrouterProvider,
|
||||||
|
buildProviderReplayFamilyHooks,
|
||||||
|
buildProviderStreamFamilyHooks,
|
||||||
|
createOpenRouterSystemCacheWrapper,
|
||||||
|
createOpenRouterWrapper,
|
||||||
|
createProviderApiKeyAuthMethod,
|
||||||
|
DEFAULT_CONTEXT_TOKENS,
|
||||||
|
getOpenRouterModelCapabilities,
|
||||||
|
isProxyReasoningUnsupported,
|
||||||
|
loadOpenRouterModelCapabilities,
|
||||||
|
OPENROUTER_DEFAULT_MODEL_REF,
|
||||||
|
openrouterMediaUnderstandingProvider,
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,3 +2,8 @@ export {
|
|||||||
buildChannelConfigSchema,
|
buildChannelConfigSchema,
|
||||||
TelegramConfigSchema,
|
TelegramConfigSchema,
|
||||||
} from "openclaw/plugin-sdk/channel-config-schema";
|
} from "openclaw/plugin-sdk/channel-config-schema";
|
||||||
|
export {
|
||||||
|
normalizeTelegramCommandDescription,
|
||||||
|
normalizeTelegramCommandName,
|
||||||
|
resolveTelegramCustomCommands,
|
||||||
|
} from "./src/command-config.js";
|
||||||
|
|||||||
@@ -357,6 +357,7 @@ export function getSoonestCooldownExpiry(
|
|||||||
): number | null {
|
): number | null {
|
||||||
const ts = options?.now ?? Date.now();
|
const ts = options?.now ?? Date.now();
|
||||||
let soonest: number | null = null;
|
let soonest: number | null = null;
|
||||||
|
let latestMatchingModelCooldown: number | null = null;
|
||||||
for (const id of profileIds) {
|
for (const id of profileIds) {
|
||||||
const stats = store.usageStats?.[id];
|
const stats = store.usageStats?.[id];
|
||||||
if (!stats) {
|
if (!stats) {
|
||||||
@@ -369,11 +370,27 @@ export function getSoonestCooldownExpiry(
|
|||||||
if (typeof until !== "number" || !Number.isFinite(until) || until <= 0) {
|
if (typeof until !== "number" || !Number.isFinite(until) || until <= 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const matchingModelScopedCooldown =
|
||||||
|
options?.forModel &&
|
||||||
|
stats.cooldownReason === "rate_limit" &&
|
||||||
|
stats.cooldownModel === options.forModel &&
|
||||||
|
!isActiveUnusableWindow(stats.disabledUntil, ts);
|
||||||
|
if (matchingModelScopedCooldown) {
|
||||||
|
latestMatchingModelCooldown =
|
||||||
|
latestMatchingModelCooldown === null ? until : Math.max(latestMatchingModelCooldown, until);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (soonest === null || until < soonest) {
|
if (soonest === null || until < soonest) {
|
||||||
soonest = until;
|
soonest = until;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return soonest;
|
if (soonest === null) {
|
||||||
|
return latestMatchingModelCooldown;
|
||||||
|
}
|
||||||
|
if (latestMatchingModelCooldown === null) {
|
||||||
|
return soonest;
|
||||||
|
}
|
||||||
|
return Math.min(soonest, latestMatchingModelCooldown);
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldBypassModelScopedCooldown(
|
function shouldBypassModelScopedCooldown(
|
||||||
|
|||||||
@@ -8,12 +8,20 @@ export type ProviderModelRef = {
|
|||||||
export function resolveConfiguredProviderFallback(params: {
|
export function resolveConfiguredProviderFallback(params: {
|
||||||
cfg: Pick<OpenClawConfig, "models">;
|
cfg: Pick<OpenClawConfig, "models">;
|
||||||
defaultProvider: string;
|
defaultProvider: string;
|
||||||
|
defaultModel?: string;
|
||||||
}): ProviderModelRef | null {
|
}): ProviderModelRef | null {
|
||||||
const configuredProviders = params.cfg.models?.providers;
|
const configuredProviders = params.cfg.models?.providers;
|
||||||
if (!configuredProviders || typeof configuredProviders !== "object") {
|
if (!configuredProviders || typeof configuredProviders !== "object") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (configuredProviders[params.defaultProvider]) {
|
const defaultProviderConfig = configuredProviders[params.defaultProvider];
|
||||||
|
const defaultModel = params.defaultModel?.trim();
|
||||||
|
const defaultProviderHasDefaultModel =
|
||||||
|
!!defaultProviderConfig &&
|
||||||
|
!!defaultModel &&
|
||||||
|
Array.isArray(defaultProviderConfig.models) &&
|
||||||
|
defaultProviderConfig.models.some((model) => model?.id === defaultModel);
|
||||||
|
if (defaultProviderConfig && (!defaultModel || defaultProviderHasDefaultModel)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const availableProvider = Object.entries(configuredProviders).find(
|
const availableProvider = Object.entries(configuredProviders).find(
|
||||||
|
|||||||
@@ -404,6 +404,7 @@ export function resolveConfiguredModelRef(params: {
|
|||||||
const fallbackProvider = resolveConfiguredProviderFallback({
|
const fallbackProvider = resolveConfiguredProviderFallback({
|
||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
defaultProvider: params.defaultProvider,
|
defaultProvider: params.defaultProvider,
|
||||||
|
defaultModel: params.defaultModel,
|
||||||
});
|
});
|
||||||
if (fallbackProvider) {
|
if (fallbackProvider) {
|
||||||
return fallbackProvider;
|
return fallbackProvider;
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { afterEach, beforeEach, vi } from "vitest";
|
import { afterEach, beforeEach, vi } from "vitest";
|
||||||
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
|
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
|
import { resolveBundledPluginsDir } from "../plugins/bundled-dir.js";
|
||||||
|
import { resetPluginLoaderTestStateForTest } from "../plugins/loader.test-fixtures.js";
|
||||||
|
import { resetProviderRuntimeHookCacheForTest } from "../plugins/provider-runtime.js";
|
||||||
import type { MockFn } from "../test-utils/vitest-mock-fn.js";
|
import type { MockFn } from "../test-utils/vitest-mock-fn.js";
|
||||||
|
import { resetModelsJsonReadyCacheForTest } from "./models-config.js";
|
||||||
import { resolveImplicitProviders } from "./models-config.providers.implicit.js";
|
import { resolveImplicitProviders } from "./models-config.providers.implicit.js";
|
||||||
|
|
||||||
export function withModelsTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
export function withModelsTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
||||||
@@ -14,10 +18,16 @@ export function installModelsConfigTestHooks(opts?: { restoreFetch?: boolean })
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
previousHome = process.env.HOME;
|
previousHome = process.env.HOME;
|
||||||
|
resetPluginLoaderTestStateForTest();
|
||||||
|
resetModelsJsonReadyCacheForTest();
|
||||||
|
resetProviderRuntimeHookCacheForTest();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
process.env.HOME = previousHome;
|
process.env.HOME = previousHome;
|
||||||
|
resetPluginLoaderTestStateForTest();
|
||||||
|
resetModelsJsonReadyCacheForTest();
|
||||||
|
resetProviderRuntimeHookCacheForTest();
|
||||||
if (opts?.restoreFetch && originalFetch) {
|
if (opts?.restoreFetch && originalFetch) {
|
||||||
globalThis.fetch = originalFetch;
|
globalThis.fetch = originalFetch;
|
||||||
}
|
}
|
||||||
@@ -103,6 +113,7 @@ export const MODELS_CONFIG_IMPLICIT_ENV_VARS = [
|
|||||||
"OPENROUTER_API_KEY",
|
"OPENROUTER_API_KEY",
|
||||||
"PI_CODING_AGENT_DIR",
|
"PI_CODING_AGENT_DIR",
|
||||||
"QIANFAN_API_KEY",
|
"QIANFAN_API_KEY",
|
||||||
|
"QWEN_API_KEY",
|
||||||
"MODELSTUDIO_API_KEY",
|
"MODELSTUDIO_API_KEY",
|
||||||
"SYNTHETIC_API_KEY",
|
"SYNTHETIC_API_KEY",
|
||||||
"STEPFUN_API_KEY",
|
"STEPFUN_API_KEY",
|
||||||
@@ -113,6 +124,7 @@ export const MODELS_CONFIG_IMPLICIT_ENV_VARS = [
|
|||||||
"KIMI_API_KEY",
|
"KIMI_API_KEY",
|
||||||
"KIMICODE_API_KEY",
|
"KIMICODE_API_KEY",
|
||||||
"GEMINI_API_KEY",
|
"GEMINI_API_KEY",
|
||||||
|
"OPENCLAW_BUNDLED_PLUGINS_DIR",
|
||||||
"GOOGLE_APPLICATION_CREDENTIALS",
|
"GOOGLE_APPLICATION_CREDENTIALS",
|
||||||
"GOOGLE_CLOUD_LOCATION",
|
"GOOGLE_CLOUD_LOCATION",
|
||||||
"GOOGLE_CLOUD_PROJECT",
|
"GOOGLE_CLOUD_PROJECT",
|
||||||
@@ -146,6 +158,12 @@ export function snapshotImplicitProviderEnv(env?: NodeJS.ProcessEnv): NodeJS.Pro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Provider discovery tests can temporarily scrub VITEST/NODE_ENV to exercise
|
||||||
|
// live HTTP paths. Keep the bundled plugin root pinned to the source checkout
|
||||||
|
// so those tests do not fall back to potentially stale dist-runtime wrappers.
|
||||||
|
snapshot.OPENCLAW_BUNDLED_PLUGINS_DIR ??=
|
||||||
|
resolveBundledPluginsDir({ VITEST: "true" } as NodeJS.ProcessEnv) ?? undefined;
|
||||||
|
|
||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
import type { ModelDefinitionConfig } from "../config/types.models.js";
|
import type { ModelDefinitionConfig } from "../config/types.models.js";
|
||||||
import { installModelsConfigTestHooks, withModelsTempHome } from "./models-config.e2e-harness.js";
|
import { installModelsConfigTestHooks, withModelsTempHome } from "./models-config.e2e-harness.js";
|
||||||
import { ensureOpenClawModelsJson } from "./models-config.js";
|
import { ensureOpenClawModelsJson } from "./models-config.js";
|
||||||
import { readGeneratedModelsJson } from "./models-config.test-utils.js";
|
|
||||||
|
|
||||||
function createGoogleModelsConfig(models: ModelDefinitionConfig[]): OpenClawConfig {
|
function createGoogleModelsConfig(models: ModelDefinitionConfig[]): OpenClawConfig {
|
||||||
return {
|
return {
|
||||||
@@ -20,18 +21,19 @@ function createGoogleModelsConfig(models: ModelDefinitionConfig[]): OpenClawConf
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readGeneratedProvider(providerKey: string) {
|
async function readGeneratedProvider(agentDir: string, providerKey: string) {
|
||||||
const parsed = await readGeneratedModelsJson<{
|
const parsed = JSON.parse(await fs.readFile(path.join(agentDir, "models.json"), "utf8")) as {
|
||||||
providers: Record<string, { baseUrl?: string; models: Array<{ id: string }> }>;
|
providers: Record<string, { baseUrl?: string; models: Array<{ id: string }> }>;
|
||||||
}>();
|
};
|
||||||
return parsed.providers[providerKey];
|
return parsed.providers[providerKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function expectGeneratedProvider(
|
async function expectGeneratedProvider(
|
||||||
|
agentDir: string,
|
||||||
providerKey: string,
|
providerKey: string,
|
||||||
params: { ids: string[]; baseUrl?: string },
|
params: { ids: string[]; baseUrl?: string },
|
||||||
) {
|
) {
|
||||||
const provider = await readGeneratedProvider(providerKey);
|
const provider = await readGeneratedProvider(agentDir, providerKey);
|
||||||
expect(provider?.models?.map((model) => model.id)).toEqual(params.ids);
|
expect(provider?.models?.map((model) => model.id)).toEqual(params.ids);
|
||||||
if (params.baseUrl !== undefined) {
|
if (params.baseUrl !== undefined) {
|
||||||
expect(provider?.baseUrl).toBe(params.baseUrl);
|
expect(provider?.baseUrl).toBe(params.baseUrl);
|
||||||
@@ -66,8 +68,8 @@ describe("models-config", () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await ensureOpenClawModelsJson(cfg);
|
const { agentDir } = await ensureOpenClawModelsJson(cfg);
|
||||||
await expectGeneratedProvider("google", {
|
await expectGeneratedProvider(agentDir, "google", {
|
||||||
ids: ["gemini-3-pro-preview", "gemini-3-flash-preview"],
|
ids: ["gemini-3-pro-preview", "gemini-3-flash-preview"],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -88,8 +90,8 @@ describe("models-config", () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await ensureOpenClawModelsJson(cfg);
|
const { agentDir } = await ensureOpenClawModelsJson(cfg);
|
||||||
await expectGeneratedProvider("google", {
|
await expectGeneratedProvider(agentDir, "google", {
|
||||||
ids: ["gemini-3-flash-preview"],
|
ids: ["gemini-3-flash-preview"],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -121,8 +123,8 @@ describe("models-config", () => {
|
|||||||
},
|
},
|
||||||
} satisfies OpenClawConfig;
|
} satisfies OpenClawConfig;
|
||||||
|
|
||||||
await ensureOpenClawModelsJson(cfg);
|
const { agentDir } = await ensureOpenClawModelsJson(cfg);
|
||||||
await expectGeneratedProvider("google-paid", {
|
await expectGeneratedProvider(agentDir, "google-paid", {
|
||||||
ids: ["gemini-3-pro-preview"],
|
ids: ["gemini-3-pro-preview"],
|
||||||
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
||||||
});
|
});
|
||||||
@@ -154,8 +156,8 @@ describe("models-config", () => {
|
|||||||
},
|
},
|
||||||
} satisfies OpenClawConfig;
|
} satisfies OpenClawConfig;
|
||||||
|
|
||||||
await ensureOpenClawModelsJson(cfg);
|
const { agentDir } = await ensureOpenClawModelsJson(cfg);
|
||||||
await expectGeneratedProvider("google", {
|
await expectGeneratedProvider(agentDir, "google", {
|
||||||
ids: ["gemini-3-flash-preview"],
|
ids: ["gemini-3-flash-preview"],
|
||||||
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
import { beforeAll, describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
let normalizeProviderSpecificConfig: typeof import("./models-config.providers.policy.js").normalizeProviderSpecificConfig;
|
normalizeProviderSpecificConfig,
|
||||||
let resolveProviderConfigApiKeyResolver: typeof import("./models-config.providers.policy.js").resolveProviderConfigApiKeyResolver;
|
resolveProviderConfigApiKeyResolver,
|
||||||
|
} from "./models-config.providers.policy.js";
|
||||||
beforeAll(async () => {
|
|
||||||
({ normalizeProviderSpecificConfig, resolveProviderConfigApiKeyResolver } =
|
|
||||||
await import("./models-config.providers.policy.js"));
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("models-config.providers.policy", () => {
|
describe("models-config.providers.policy", () => {
|
||||||
it("resolves config apiKey markers through provider plugin hooks", async () => {
|
it("resolves config apiKey markers through provider plugin hooks", async () => {
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
import { resolveBedrockConfigApiKey } from "../plugin-sdk/amazon-bedrock.js";
|
||||||
|
import {
|
||||||
|
normalizeGoogleProviderConfig,
|
||||||
|
shouldNormalizeGoogleProviderConfig,
|
||||||
|
} from "../plugin-sdk/google.js";
|
||||||
import {
|
import {
|
||||||
applyProviderNativeStreamingUsageCompatWithPlugin,
|
applyProviderNativeStreamingUsageCompatWithPlugin,
|
||||||
normalizeProviderConfigWithPlugin,
|
normalizeProviderConfigWithPlugin,
|
||||||
@@ -32,20 +37,32 @@ export function normalizeProviderSpecificConfig(
|
|||||||
providerKey: string,
|
providerKey: string,
|
||||||
provider: ProviderConfig,
|
provider: ProviderConfig,
|
||||||
): ProviderConfig {
|
): ProviderConfig {
|
||||||
return (
|
const normalized =
|
||||||
normalizeProviderConfigWithPlugin({
|
normalizeProviderConfigWithPlugin({
|
||||||
provider: providerKey,
|
provider: providerKey,
|
||||||
context: {
|
context: {
|
||||||
provider: providerKey,
|
provider: providerKey,
|
||||||
providerConfig: provider,
|
providerConfig: provider,
|
||||||
},
|
},
|
||||||
}) ?? provider
|
}) ?? undefined;
|
||||||
);
|
if (normalized) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
if (shouldNormalizeGoogleProviderConfig(providerKey, provider)) {
|
||||||
|
return normalizeGoogleProviderConfig(providerKey, provider);
|
||||||
|
}
|
||||||
|
return provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveProviderConfigApiKeyResolver(
|
export function resolveProviderConfigApiKeyResolver(
|
||||||
providerKey: string,
|
providerKey: string,
|
||||||
): ((env: NodeJS.ProcessEnv) => string | undefined) | undefined {
|
): ((env: NodeJS.ProcessEnv) => string | undefined) | undefined {
|
||||||
|
if (providerKey.trim() === "amazon-bedrock") {
|
||||||
|
return (env) => {
|
||||||
|
const resolved = resolveBedrockConfigApiKey(env);
|
||||||
|
return resolved?.trim() || undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
if (!resolveProviderRuntimePlugin({ provider: providerKey })?.resolveConfigApiKey) {
|
if (!resolveProviderRuntimePlugin({ provider: providerKey })?.resolveConfigApiKey) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -789,9 +789,7 @@ describe("applyExtraParamsToAgent", () => {
|
|||||||
void agent.streamFn?.(model, context, {});
|
void agent.streamFn?.(model, context, {});
|
||||||
|
|
||||||
expect(payloads).toHaveLength(1);
|
expect(payloads).toHaveLength(1);
|
||||||
expect(payloads[0]).toEqual({
|
expect(payloads[0]).not.toHaveProperty("reasoning");
|
||||||
reasoning: { effort: "none", summary: "auto" },
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("injects parallel_tool_calls for openai-completions payloads when configured", () => {
|
it("injects parallel_tool_calls for openai-completions payloads when configured", () => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
normalizeTelegramCommandDescription,
|
normalizeTelegramCommandDescription,
|
||||||
normalizeTelegramCommandName,
|
normalizeTelegramCommandName,
|
||||||
resolveTelegramCustomCommands,
|
resolveTelegramCustomCommands,
|
||||||
} from "../../extensions/telegram/api.js";
|
} from "../../extensions/telegram/config-api.js";
|
||||||
import { isSafeScpRemoteHost } from "../infra/scp-host.js";
|
import { isSafeScpRemoteHost } from "../infra/scp-host.js";
|
||||||
import { isValidInboundPathRootPattern } from "../media/inbound-path-policy.js";
|
import { isValidInboundPathRootPattern } from "../media/inbound-path-policy.js";
|
||||||
import { ToolPolicySchema } from "./zod-schema.agent-runtime.js";
|
import { ToolPolicySchema } from "./zod-schema.agent-runtime.js";
|
||||||
|
|||||||
@@ -412,7 +412,25 @@ export function normalizeProviderConfigWithPlugin(params: {
|
|||||||
env?: NodeJS.ProcessEnv;
|
env?: NodeJS.ProcessEnv;
|
||||||
context: ProviderNormalizeConfigContext;
|
context: ProviderNormalizeConfigContext;
|
||||||
}): ModelProviderConfig | undefined {
|
}): ModelProviderConfig | undefined {
|
||||||
return resolveProviderHookPlugin(params)?.normalizeConfig?.(params.context) ?? undefined;
|
const hasConfigChange = (normalized: ModelProviderConfig) =>
|
||||||
|
normalized !== params.context.providerConfig;
|
||||||
|
const matchedPlugin = resolveProviderHookPlugin(params);
|
||||||
|
const normalizedMatched = matchedPlugin?.normalizeConfig?.(params.context);
|
||||||
|
if (normalizedMatched && hasConfigChange(normalizedMatched)) {
|
||||||
|
return normalizedMatched;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const candidate of resolveProviderPluginsForHooks(params)) {
|
||||||
|
if (!candidate.normalizeConfig || candidate === matchedPlugin) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const normalized = candidate.normalizeConfig(params.context);
|
||||||
|
if (normalized && hasConfigChange(normalized)) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyProviderNativeStreamingUsageCompatWithPlugin(params: {
|
export function applyProviderNativeStreamingUsageCompatWithPlugin(params: {
|
||||||
|
|||||||
Reference in New Issue
Block a user