mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-07 07:11:06 +00:00
fix(providers): stabilize runtime normalization hooks
This commit is contained in:
@@ -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