mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-06 17:33:55 +02: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 { registerAmazonBedrockPlugin } from "./register.runtime.js";
|
||||
import { registerAmazonBedrockPlugin } from "./register.sync.runtime.js";
|
||||
|
||||
export default definePluginEntry({
|
||||
id: "amazon-bedrock",
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import type { StreamFn } from "@mariozechner/pi-agent-core";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import {
|
||||
createBedrockNoCacheWrapper,
|
||||
isAnthropicBedrockModel,
|
||||
streamWithPayloadPatch,
|
||||
} from "openclaw/plugin-sdk/provider-stream";
|
||||
import {
|
||||
mergeImplicitBedrockProvider,
|
||||
resolveBedrockConfigApiKey,
|
||||
resolveImplicitBedrockProvider,
|
||||
} from "./api.js";
|
||||
|
||||
type GuardrailConfig = {
|
||||
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
|
||||
// initialization during test bootstrap cannot trip TDZ reads.
|
||||
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,
|
||||
/ModelStreamErrorException.*(?:Input is too long|too many input tokens)/i,
|
||||
] 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({
|
||||
family: "anthropic-by-model",
|
||||
});
|
||||
@@ -6,6 +6,6 @@ export default definePluginEntry({
|
||||
name: "Anthropic Provider",
|
||||
description: "Bundled Anthropic provider plugin",
|
||||
register(api) {
|
||||
registerAnthropicPlugin(api);
|
||||
return registerAnthropicPlugin(api);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
} from "openclaw/plugin-sdk/plugin-entry";
|
||||
import {
|
||||
applyAuthProfileConfig,
|
||||
createProviderApiKeyAuthMethod,
|
||||
ensureApiKeyFromOptionEnvOrPrompt,
|
||||
listProfilesForProvider,
|
||||
normalizeApiKeyInput,
|
||||
@@ -17,11 +18,13 @@ import {
|
||||
} from "openclaw/plugin-sdk/provider-auth";
|
||||
import { cloneFirstTemplateModel } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
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 { 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 providerId = "anthropic";
|
||||
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-haiku-4-5",
|
||||
] as const;
|
||||
let createApiKeyAuthMethod:
|
||||
| (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.registerCliBackend(buildAnthropicCliBackend());
|
||||
api.registerProvider({
|
||||
id: providerId,
|
||||
label: "Anthropic",
|
||||
@@ -291,7 +256,7 @@ export async function registerAnthropicPlugin(api: OpenClawPluginApi): Promise<v
|
||||
runtime: ctx.runtime,
|
||||
}),
|
||||
},
|
||||
createApiKeyAuthMethod({
|
||||
createProviderApiKeyAuthMethod({
|
||||
providerId,
|
||||
methodId: "api-key",
|
||||
label: "Anthropic API key",
|
||||
@@ -337,7 +302,5 @@ export async function registerAnthropicPlugin(api: OpenClawPluginApi): Promise<v
|
||||
profileId: ctx.profileId,
|
||||
}),
|
||||
});
|
||||
if (mediaUnderstandingProvider) {
|
||||
api.registerMediaUnderstandingProvider(mediaUnderstandingProvider);
|
||||
}
|
||||
api.registerMediaUnderstandingProvider(anthropicMediaUnderstandingProvider);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
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_XHIGH_MODEL_IDS = ["gpt-5.2", "gpt-5.2-codex"] as const;
|
||||
@@ -15,19 +27,7 @@ export default definePluginEntry({
|
||||
id: "github-copilot",
|
||||
name: "GitHub Copilot Provider",
|
||||
description: "Bundled GitHub Copilot provider plugin",
|
||||
async register(api) {
|
||||
const {
|
||||
coerceSecretRef,
|
||||
DEFAULT_COPILOT_API_BASE_URL,
|
||||
ensureAuthProfileStore,
|
||||
fetchCopilotUsage,
|
||||
githubCopilotLoginCommand,
|
||||
listProfilesForProvider,
|
||||
PROVIDER_ID,
|
||||
resolveCopilotApiToken,
|
||||
resolveCopilotForwardCompatModel,
|
||||
wrapCopilotProviderStream,
|
||||
} = await import("./register.runtime.js");
|
||||
register(api) {
|
||||
function resolveFirstGithubToken(params: { agentDir?: string; env: NodeJS.ProcessEnv }): {
|
||||
githubToken: string;
|
||||
hasProfile: boolean;
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
export {
|
||||
import {
|
||||
coerceSecretRef,
|
||||
ensureAuthProfileStore,
|
||||
listProfilesForProvider,
|
||||
} from "openclaw/plugin-sdk/provider-auth";
|
||||
export { githubCopilotLoginCommand } from "openclaw/plugin-sdk/provider-auth-login";
|
||||
export { PROVIDER_ID, resolveCopilotForwardCompatModel } from "./models.js";
|
||||
export { wrapCopilotAnthropicStream, wrapCopilotProviderStream } from "./stream.js";
|
||||
export { DEFAULT_COPILOT_API_BASE_URL, resolveCopilotApiToken } from "./token.js";
|
||||
export { fetchCopilotUsage } from "./usage.js";
|
||||
import { githubCopilotLoginCommand } from "openclaw/plugin-sdk/provider-auth-login";
|
||||
import { PROVIDER_ID, resolveCopilotForwardCompatModel } from "./models.js";
|
||||
import { wrapCopilotAnthropicStream, wrapCopilotProviderStream } from "./stream.js";
|
||||
import { DEFAULT_COPILOT_API_BASE_URL, resolveCopilotApiToken } from "./token.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 ProviderWrapStreamFnContext,
|
||||
} 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 OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
|
||||
@@ -20,19 +32,7 @@ export default definePluginEntry({
|
||||
id: "openrouter",
|
||||
name: "OpenRouter Provider",
|
||||
description: "Bundled OpenRouter provider plugin",
|
||||
async register(api) {
|
||||
const {
|
||||
buildProviderReplayFamilyHooks,
|
||||
buildProviderStreamFamilyHooks,
|
||||
createProviderApiKeyAuthMethod,
|
||||
DEFAULT_CONTEXT_TOKENS,
|
||||
getOpenRouterModelCapabilities,
|
||||
loadOpenRouterModelCapabilities,
|
||||
OPENROUTER_DEFAULT_MODEL_REF,
|
||||
openrouterMediaUnderstandingProvider,
|
||||
applyOpenrouterConfig,
|
||||
buildOpenrouterProvider,
|
||||
} = await import("./register.runtime.js");
|
||||
register(api) {
|
||||
const PASSTHROUGH_GEMINI_REPLAY_HOOKS = buildProviderReplayFamilyHooks({
|
||||
family: "passthrough-gemini",
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
|
||||
export {
|
||||
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
|
||||
import {
|
||||
buildProviderReplayFamilyHooks,
|
||||
DEFAULT_CONTEXT_TOKENS,
|
||||
} from "openclaw/plugin-sdk/provider-model-shared";
|
||||
export {
|
||||
import {
|
||||
buildProviderStreamFamilyHooks,
|
||||
createOpenRouterSystemCacheWrapper,
|
||||
createOpenRouterWrapper,
|
||||
@@ -11,6 +11,22 @@ export {
|
||||
isProxyReasoningUnsupported,
|
||||
loadOpenRouterModelCapabilities,
|
||||
} from "openclaw/plugin-sdk/provider-stream";
|
||||
export { openrouterMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
||||
export { applyOpenrouterConfig, OPENROUTER_DEFAULT_MODEL_REF } from "./onboard.js";
|
||||
export { buildOpenrouterProvider } from "./provider-catalog.js";
|
||||
import { openrouterMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
||||
import { applyOpenrouterConfig, OPENROUTER_DEFAULT_MODEL_REF } from "./onboard.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,
|
||||
TelegramConfigSchema,
|
||||
} 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 {
|
||||
const ts = options?.now ?? Date.now();
|
||||
let soonest: number | null = null;
|
||||
let latestMatchingModelCooldown: number | null = null;
|
||||
for (const id of profileIds) {
|
||||
const stats = store.usageStats?.[id];
|
||||
if (!stats) {
|
||||
@@ -369,11 +370,27 @@ export function getSoonestCooldownExpiry(
|
||||
if (typeof until !== "number" || !Number.isFinite(until) || until <= 0) {
|
||||
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) {
|
||||
soonest = until;
|
||||
}
|
||||
}
|
||||
return soonest;
|
||||
if (soonest === null) {
|
||||
return latestMatchingModelCooldown;
|
||||
}
|
||||
if (latestMatchingModelCooldown === null) {
|
||||
return soonest;
|
||||
}
|
||||
return Math.min(soonest, latestMatchingModelCooldown);
|
||||
}
|
||||
|
||||
function shouldBypassModelScopedCooldown(
|
||||
|
||||
@@ -8,12 +8,20 @@ export type ProviderModelRef = {
|
||||
export function resolveConfiguredProviderFallback(params: {
|
||||
cfg: Pick<OpenClawConfig, "models">;
|
||||
defaultProvider: string;
|
||||
defaultModel?: string;
|
||||
}): ProviderModelRef | null {
|
||||
const configuredProviders = params.cfg.models?.providers;
|
||||
if (!configuredProviders || typeof configuredProviders !== "object") {
|
||||
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;
|
||||
}
|
||||
const availableProvider = Object.entries(configuredProviders).find(
|
||||
|
||||
@@ -404,6 +404,7 @@ export function resolveConfiguredModelRef(params: {
|
||||
const fallbackProvider = resolveConfiguredProviderFallback({
|
||||
cfg: params.cfg,
|
||||
defaultProvider: params.defaultProvider,
|
||||
defaultModel: params.defaultModel,
|
||||
});
|
||||
if (fallbackProvider) {
|
||||
return fallbackProvider;
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { afterEach, beforeEach, vi } from "vitest";
|
||||
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.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 { resetModelsJsonReadyCacheForTest } from "./models-config.js";
|
||||
import { resolveImplicitProviders } from "./models-config.providers.implicit.js";
|
||||
|
||||
export function withModelsTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
||||
@@ -14,10 +18,16 @@ export function installModelsConfigTestHooks(opts?: { restoreFetch?: boolean })
|
||||
|
||||
beforeEach(() => {
|
||||
previousHome = process.env.HOME;
|
||||
resetPluginLoaderTestStateForTest();
|
||||
resetModelsJsonReadyCacheForTest();
|
||||
resetProviderRuntimeHookCacheForTest();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env.HOME = previousHome;
|
||||
resetPluginLoaderTestStateForTest();
|
||||
resetModelsJsonReadyCacheForTest();
|
||||
resetProviderRuntimeHookCacheForTest();
|
||||
if (opts?.restoreFetch && originalFetch) {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
@@ -103,6 +113,7 @@ export const MODELS_CONFIG_IMPLICIT_ENV_VARS = [
|
||||
"OPENROUTER_API_KEY",
|
||||
"PI_CODING_AGENT_DIR",
|
||||
"QIANFAN_API_KEY",
|
||||
"QWEN_API_KEY",
|
||||
"MODELSTUDIO_API_KEY",
|
||||
"SYNTHETIC_API_KEY",
|
||||
"STEPFUN_API_KEY",
|
||||
@@ -113,6 +124,7 @@ export const MODELS_CONFIG_IMPLICIT_ENV_VARS = [
|
||||
"KIMI_API_KEY",
|
||||
"KIMICODE_API_KEY",
|
||||
"GEMINI_API_KEY",
|
||||
"OPENCLAW_BUNDLED_PLUGINS_DIR",
|
||||
"GOOGLE_APPLICATION_CREDENTIALS",
|
||||
"GOOGLE_CLOUD_LOCATION",
|
||||
"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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { ModelDefinitionConfig } from "../config/types.models.js";
|
||||
import { installModelsConfigTestHooks, withModelsTempHome } from "./models-config.e2e-harness.js";
|
||||
import { ensureOpenClawModelsJson } from "./models-config.js";
|
||||
import { readGeneratedModelsJson } from "./models-config.test-utils.js";
|
||||
|
||||
function createGoogleModelsConfig(models: ModelDefinitionConfig[]): OpenClawConfig {
|
||||
return {
|
||||
@@ -20,18 +21,19 @@ function createGoogleModelsConfig(models: ModelDefinitionConfig[]): OpenClawConf
|
||||
};
|
||||
}
|
||||
|
||||
async function readGeneratedProvider(providerKey: string) {
|
||||
const parsed = await readGeneratedModelsJson<{
|
||||
async function readGeneratedProvider(agentDir: string, providerKey: string) {
|
||||
const parsed = JSON.parse(await fs.readFile(path.join(agentDir, "models.json"), "utf8")) as {
|
||||
providers: Record<string, { baseUrl?: string; models: Array<{ id: string }> }>;
|
||||
}>();
|
||||
};
|
||||
return parsed.providers[providerKey];
|
||||
}
|
||||
|
||||
async function expectGeneratedProvider(
|
||||
agentDir: string,
|
||||
providerKey: 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);
|
||||
if (params.baseUrl !== undefined) {
|
||||
expect(provider?.baseUrl).toBe(params.baseUrl);
|
||||
@@ -66,8 +68,8 @@ describe("models-config", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
await ensureOpenClawModelsJson(cfg);
|
||||
await expectGeneratedProvider("google", {
|
||||
const { agentDir } = await ensureOpenClawModelsJson(cfg);
|
||||
await expectGeneratedProvider(agentDir, "google", {
|
||||
ids: ["gemini-3-pro-preview", "gemini-3-flash-preview"],
|
||||
});
|
||||
});
|
||||
@@ -88,8 +90,8 @@ describe("models-config", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
await ensureOpenClawModelsJson(cfg);
|
||||
await expectGeneratedProvider("google", {
|
||||
const { agentDir } = await ensureOpenClawModelsJson(cfg);
|
||||
await expectGeneratedProvider(agentDir, "google", {
|
||||
ids: ["gemini-3-flash-preview"],
|
||||
});
|
||||
});
|
||||
@@ -121,8 +123,8 @@ describe("models-config", () => {
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
await ensureOpenClawModelsJson(cfg);
|
||||
await expectGeneratedProvider("google-paid", {
|
||||
const { agentDir } = await ensureOpenClawModelsJson(cfg);
|
||||
await expectGeneratedProvider(agentDir, "google-paid", {
|
||||
ids: ["gemini-3-pro-preview"],
|
||||
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
||||
});
|
||||
@@ -154,8 +156,8 @@ describe("models-config", () => {
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
await ensureOpenClawModelsJson(cfg);
|
||||
await expectGeneratedProvider("google", {
|
||||
const { agentDir } = await ensureOpenClawModelsJson(cfg);
|
||||
await expectGeneratedProvider(agentDir, "google", {
|
||||
ids: ["gemini-3-flash-preview"],
|
||||
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
||||
});
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
|
||||
let normalizeProviderSpecificConfig: typeof import("./models-config.providers.policy.js").normalizeProviderSpecificConfig;
|
||||
let resolveProviderConfigApiKeyResolver: typeof import("./models-config.providers.policy.js").resolveProviderConfigApiKeyResolver;
|
||||
|
||||
beforeAll(async () => {
|
||||
({ normalizeProviderSpecificConfig, resolveProviderConfigApiKeyResolver } =
|
||||
await import("./models-config.providers.policy.js"));
|
||||
});
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
normalizeProviderSpecificConfig,
|
||||
resolveProviderConfigApiKeyResolver,
|
||||
} from "./models-config.providers.policy.js";
|
||||
|
||||
describe("models-config.providers.policy", () => {
|
||||
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 {
|
||||
applyProviderNativeStreamingUsageCompatWithPlugin,
|
||||
normalizeProviderConfigWithPlugin,
|
||||
@@ -32,20 +37,32 @@ export function normalizeProviderSpecificConfig(
|
||||
providerKey: string,
|
||||
provider: ProviderConfig,
|
||||
): ProviderConfig {
|
||||
return (
|
||||
const normalized =
|
||||
normalizeProviderConfigWithPlugin({
|
||||
provider: providerKey,
|
||||
context: {
|
||||
provider: providerKey,
|
||||
providerConfig: provider,
|
||||
},
|
||||
}) ?? provider
|
||||
);
|
||||
}) ?? undefined;
|
||||
if (normalized) {
|
||||
return normalized;
|
||||
}
|
||||
if (shouldNormalizeGoogleProviderConfig(providerKey, provider)) {
|
||||
return normalizeGoogleProviderConfig(providerKey, provider);
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
export function resolveProviderConfigApiKeyResolver(
|
||||
providerKey: string,
|
||||
): ((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) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -789,9 +789,7 @@ describe("applyExtraParamsToAgent", () => {
|
||||
void agent.streamFn?.(model, context, {});
|
||||
|
||||
expect(payloads).toHaveLength(1);
|
||||
expect(payloads[0]).toEqual({
|
||||
reasoning: { effort: "none", summary: "auto" },
|
||||
});
|
||||
expect(payloads[0]).not.toHaveProperty("reasoning");
|
||||
});
|
||||
|
||||
it("injects parallel_tool_calls for openai-completions payloads when configured", () => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
normalizeTelegramCommandDescription,
|
||||
normalizeTelegramCommandName,
|
||||
resolveTelegramCustomCommands,
|
||||
} from "../../extensions/telegram/api.js";
|
||||
} from "../../extensions/telegram/config-api.js";
|
||||
import { isSafeScpRemoteHost } from "../infra/scp-host.js";
|
||||
import { isValidInboundPathRootPattern } from "../media/inbound-path-policy.js";
|
||||
import { ToolPolicySchema } from "./zod-schema.agent-runtime.js";
|
||||
|
||||
@@ -412,7 +412,25 @@ export function normalizeProviderConfigWithPlugin(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
context: ProviderNormalizeConfigContext;
|
||||
}): 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: {
|
||||
|
||||
Reference in New Issue
Block a user