mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-27 20:45:49 +00:00
fix: allow provider timeout overlays (#83990)
* fix: allow provider timeout overlays * test: fix provider overlay fixture types
This commit is contained in:
@@ -325,6 +325,8 @@ describe("microsoft-foundry plugin", () => {
|
||||
models: {
|
||||
providers: {
|
||||
"microsoft-foundry": {
|
||||
baseUrl: "",
|
||||
models: [],
|
||||
timeoutSeconds: 120,
|
||||
},
|
||||
},
|
||||
@@ -338,7 +340,7 @@ describe("microsoft-foundry plugin", () => {
|
||||
agentDir: defaultFoundryAgentDir,
|
||||
});
|
||||
|
||||
expect(config.models?.providers?.["microsoft-foundry"]?.models?.[0]?.id).toBe("gpt-5.4");
|
||||
expect(config.models?.providers?.["microsoft-foundry"]?.models).toEqual([]);
|
||||
expect(config.models?.providers?.["microsoft-foundry"]?.timeoutSeconds).toBe(120);
|
||||
});
|
||||
|
||||
|
||||
@@ -26,7 +26,12 @@ export function buildMicrosoftFoundryProvider(): ProviderPlugin {
|
||||
auth: [entraIdAuthMethod, apiKeyAuthMethod],
|
||||
onModelSelected: async (ctx) => {
|
||||
const providerConfig = ctx.config.models?.providers?.[PROVIDER_ID];
|
||||
if (!providerConfig || !ctx.model.startsWith(`${PROVIDER_ID}/`)) {
|
||||
if (
|
||||
!providerConfig ||
|
||||
!providerConfig.baseUrl?.trim() ||
|
||||
!Array.isArray(providerConfig.models) ||
|
||||
!ctx.model.startsWith(`${PROVIDER_ID}/`)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const selectedModelId = ctx.model.slice(`${PROVIDER_ID}/`.length);
|
||||
|
||||
@@ -146,7 +146,7 @@ vi.mock("./openrouter-model-capabilities.js", () => ({
|
||||
mockLoadOpenRouterModelCapabilities(modelId),
|
||||
}));
|
||||
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { OpenClawConfig, OpenClawConfigInput } from "../../config/config.js";
|
||||
import { COPILOT_INTEGRATION_ID, buildCopilotIdeHeaders } from "../copilot-dynamic-headers.js";
|
||||
import { getModelProviderLocalService } from "../provider-local-service.js";
|
||||
import { getModelProviderRequestTransport } from "../provider-request-config.js";
|
||||
@@ -593,10 +593,11 @@ describe("resolveModel", () => {
|
||||
});
|
||||
|
||||
it("defaults missing model cost before handing models to PI", () => {
|
||||
const cfg = {
|
||||
const cfg: OpenClawConfig = {
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "",
|
||||
api: "openai-responses",
|
||||
models: [
|
||||
{
|
||||
@@ -605,6 +606,7 @@ describe("resolveModel", () => {
|
||||
api: "openai-responses",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 400_000,
|
||||
maxTokens: 128_000,
|
||||
},
|
||||
@@ -612,7 +614,7 @@ describe("resolveModel", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
};
|
||||
|
||||
const result = resolveModelForTest("openai", "gpt-5.5", "/tmp/agent", cfg);
|
||||
|
||||
@@ -663,6 +665,50 @@ describe("resolveModel", () => {
|
||||
expect(result.error).toBe("Unknown model: openai/typo-model");
|
||||
});
|
||||
|
||||
it("does not create fallback models from provider overlays alone", () => {
|
||||
const cfg = {
|
||||
models: {
|
||||
providers: {
|
||||
typoProvider: {
|
||||
timeoutSeconds: 600,
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfigInput;
|
||||
|
||||
const result = resolveModelForTest(
|
||||
"typoProvider",
|
||||
"typoed-model",
|
||||
"/tmp/agent",
|
||||
cfg as unknown as OpenClawConfig,
|
||||
);
|
||||
|
||||
expect(result.model).toBeUndefined();
|
||||
expect(result.error).toBe("Unknown model: typoProvider/typoed-model");
|
||||
});
|
||||
|
||||
it("does not create fallback models from built-in provider api overlays", () => {
|
||||
const cfg = {
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
api: "openai-responses",
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfigInput;
|
||||
|
||||
const result = resolveModelForTest(
|
||||
"openai",
|
||||
"typoed-model",
|
||||
"/tmp/agent",
|
||||
cfg as unknown as OpenClawConfig,
|
||||
);
|
||||
|
||||
expect(result.model).toBeUndefined();
|
||||
expect(result.error).toBe("Unknown model: openai/typoed-model");
|
||||
});
|
||||
|
||||
it("defaults baseUrl-only local custom fallback models to chat completions", () => {
|
||||
const cfg = {
|
||||
agents: {
|
||||
@@ -1174,9 +1220,14 @@ describe("resolveModel", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
} satisfies OpenClawConfigInput;
|
||||
|
||||
const result = resolveModelForTest("openai", "gpt-5.5", "/tmp/agent", cfg);
|
||||
const result = resolveModelForTest(
|
||||
"openai",
|
||||
"gpt-5.5",
|
||||
"/tmp/agent",
|
||||
cfg as unknown as OpenClawConfig,
|
||||
);
|
||||
|
||||
expect(result.error).toBeUndefined();
|
||||
expect((result.model as { requestTimeoutMs?: number } | undefined)?.requestTimeoutMs).toBe(
|
||||
|
||||
@@ -59,6 +59,82 @@ describe("boolean config validation", () => {
|
||||
});
|
||||
|
||||
describe("model provider localService config", () => {
|
||||
it("accepts standalone timeout overlays for bundled model providers", () => {
|
||||
const result = OpenClawSchema.safeParse({
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
timeoutSeconds: 600,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.models?.providers?.openai?.timeoutSeconds).toBe(600);
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects standalone timeout overlays for unknown model providers", () => {
|
||||
const result = OpenClawSchema.safeParse({
|
||||
models: {
|
||||
providers: {
|
||||
anyManifestProvider: {
|
||||
timeoutSeconds: 600,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
const paths = result.error.issues.map((issue) => issue.path.join("."));
|
||||
expect(paths).toEqual(
|
||||
expect.arrayContaining([
|
||||
"models.providers.anyManifestProvider.baseUrl",
|
||||
"models.providers.anyManifestProvider.models",
|
||||
]),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("requires models when a model provider declaration sets baseUrl", () => {
|
||||
const result = OpenClawSchema.safeParse({
|
||||
models: {
|
||||
providers: {
|
||||
custom: {
|
||||
baseUrl: "https://example.test/v1",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
const paths = result.error.issues.map((issue) => issue.path.join("."));
|
||||
expect(paths).toContain("models.providers.custom.models");
|
||||
}
|
||||
});
|
||||
|
||||
it("requires baseUrl when a model provider declaration sets models", () => {
|
||||
const result = OpenClawSchema.safeParse({
|
||||
models: {
|
||||
providers: {
|
||||
custom: {
|
||||
models: [{ id: "custom-model", name: "Custom model", api: "openai-completions" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
const paths = result.error.issues.map((issue) => issue.path.join("."));
|
||||
expect(paths).toContain("models.providers.custom.baseUrl");
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts on-demand local provider service settings", () => {
|
||||
const result = OpenClawSchema.safeParse({
|
||||
models: {
|
||||
|
||||
@@ -932,7 +932,7 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"models.mode":
|
||||
'Controls provider catalog behavior: "merge" keeps built-ins and overlays your custom providers, while "replace" uses only your configured providers. In "merge", matching provider IDs preserve non-empty agent models.json baseUrl values, while apiKey values are preserved only when the provider is not SecretRef-managed in current config/auth-profile context; SecretRef-managed providers refresh apiKey from current source markers, and matching model contextWindow/maxTokens use the higher value between explicit and implicit entries.',
|
||||
"models.providers":
|
||||
"Provider map keyed by provider ID containing connection/auth settings and concrete model definitions. Use stable provider keys so references from agents and tooling remain portable across environments.",
|
||||
"Provider map keyed by provider ID containing connection/auth settings and concrete model definitions. Built-in providers may be tuned with provider-level overlays; custom providers must include baseUrl and models. Use stable provider keys so references from agents and tooling remain portable across environments.",
|
||||
"models.pricing":
|
||||
"Controls the optional background model-pricing bootstrap that fetches remote per-token cost catalogs.",
|
||||
"models.pricing.enabled":
|
||||
@@ -952,7 +952,7 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"models.providers.*.maxTokens":
|
||||
"Default maximum output token budget applied to models under this provider when a model entry does not set maxTokens.",
|
||||
"models.providers.*.timeoutSeconds":
|
||||
"Optional per-provider model request timeout in seconds. Applies to provider HTTP fetches, including connect, headers, body, and total request abort handling, and also raises the LLM idle/stream watchdog ceiling for this provider above the implicit ~120s default. Use this for slow local or self-hosted model servers, or for cloud providers that buffer reasoning tokens silently on the wire (Gemini preview, large-tool-payload Claude/Opus), instead of changing global agent timeouts.",
|
||||
"Optional per-provider model request timeout in seconds. For built-in providers, this can be set as a standalone overlay. For custom providers, set it alongside the provider baseUrl and models. Applies to provider HTTP fetches, including connect, headers, body, and total request abort handling, and also raises the LLM idle/stream watchdog ceiling for this provider above the implicit ~120s default. Use this for slow local or self-hosted model servers, or for cloud providers that buffer reasoning tokens silently on the wire (Gemini preview, large-tool-payload Claude/Opus), instead of changing global agent timeouts.",
|
||||
"models.providers.*.injectNumCtxForOpenAICompat":
|
||||
"Controls whether OpenClaw injects `options.num_ctx` for Ollama providers configured with the OpenAI-compatible adapter (`openai-completions`). Default is true. Set false only if your proxy/upstream rejects unknown `options` payload fields.",
|
||||
"models.providers.*.params":
|
||||
|
||||
@@ -165,6 +165,12 @@ export type ModelProviderConfig = {
|
||||
models: ModelDefinitionConfig[];
|
||||
};
|
||||
|
||||
export type ModelProviderDeclarationConfig = ModelProviderConfig;
|
||||
|
||||
export type ModelProviderConfigInput = Omit<Partial<ModelProviderConfig>, "models"> & {
|
||||
models?: ModelDefinitionConfig[];
|
||||
};
|
||||
|
||||
export type BedrockDiscoveryConfig = {
|
||||
enabled?: boolean;
|
||||
region?: string;
|
||||
@@ -207,3 +213,7 @@ export type ModelsConfig = {
|
||||
*/
|
||||
ollamaDiscovery?: DiscoveryToggleConfig;
|
||||
};
|
||||
|
||||
export type ModelsConfigInput = Omit<ModelsConfig, "providers"> & {
|
||||
providers?: Record<string, ModelProviderConfigInput>;
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ import type {
|
||||
CommandsConfig,
|
||||
MessagesConfig,
|
||||
} from "./types.messages.js";
|
||||
import type { ModelsConfig } from "./types.models.js";
|
||||
import type { ModelsConfig, ModelsConfigInput } from "./types.models.js";
|
||||
import type { NodeHostConfig } from "./types.node-host.js";
|
||||
import type { PluginsConfig } from "./types.plugins.js";
|
||||
import type { SecretsConfig } from "./types.secrets.js";
|
||||
@@ -153,6 +153,10 @@ export type OpenClawConfig = {
|
||||
proxy?: ProxyConfig;
|
||||
};
|
||||
|
||||
export type OpenClawConfigInput = Omit<OpenClawConfig, "models"> & {
|
||||
models?: ModelsConfigInput;
|
||||
};
|
||||
|
||||
declare const openClawConfigStateBrand: unique symbol;
|
||||
|
||||
type BrandedConfigState<TState extends string> = OpenClawConfig & {
|
||||
|
||||
@@ -26,7 +26,10 @@ import {
|
||||
normalizeGooglePreviewModelId,
|
||||
} from "./provider-model-id-normalize.js";
|
||||
|
||||
export type { ModelApi, ModelProviderConfig } from "../config/types.models.js";
|
||||
export type {
|
||||
ModelApi,
|
||||
ModelProviderDeclarationConfig as ModelProviderConfig,
|
||||
} from "../config/types.models.js";
|
||||
export type {
|
||||
UnifiedModelCatalogEntry,
|
||||
UnifiedModelCatalogKind,
|
||||
|
||||
Reference in New Issue
Block a user