diff --git a/src/agents/embedded-agent-runner/model.test.ts b/src/agents/embedded-agent-runner/model.test.ts index 42e054ddbb12..f19691768ad0 100644 --- a/src/agents/embedded-agent-runner/model.test.ts +++ b/src/agents/embedded-agent-runner/model.test.ts @@ -1243,6 +1243,39 @@ describe("resolveModel", () => { }); }); + it("leaves maxTokens undefined when no configured or catalog value is available (regression: #98295)", () => { + // Regression for https://github.com/openclaw/openclaw/issues/98295. + // A custom provider entry without maxTokens (and no matching bundled + // static catalog row) must not synthesize an oversized output cap from + // DEFAULT_CONTEXT_TOKENS. Leaving maxTokens undefined lets the transport + // omit `max_completion_tokens` so the provider applies its own default, + // avoiding HTTP 400 (Param Incorrect) from strict OpenAI-compatible + // servers whose completion-token ceiling is below the synthesized value. + resolveBundledStaticCatalogModelMock.mockReturnValueOnce(undefined); + const cfg = { + models: { + providers: { + xiaomi: { + baseUrl: "https://api.xiaomimimo.com/v1", + models: [ + { + id: "mimo-v2.5-pro", + name: "mimo-v2.5-pro", + }, + ], + }, + }, + }, + } as unknown as OpenClawConfig; + + const result = resolveModelForTest("xiaomi", "mimo-v2.5-pro", "/tmp/agent", cfg); + const model = expectResolvedModel(result); + + expect(model.id).toBe("mimo-v2.5-pro"); + expect(model.baseUrl).toBe("https://api.xiaomimimo.com/v1"); + expect(model.maxTokens).toBeUndefined(); + }); + it("inherits bundled static transport for configured provider fallback models", () => { resolveBundledStaticCatalogModelMock.mockReturnValueOnce({ provider: "deepseek", diff --git a/src/agents/embedded-agent-runner/model.ts b/src/agents/embedded-agent-runner/model.ts index 9ea0fb05540c..723ac2e2517c 100644 --- a/src/agents/embedded-agent-runner/model.ts +++ b/src/agents/embedded-agent-runner/model.ts @@ -1330,6 +1330,11 @@ function resolveConfiguredFallbackModel(params: { compat: fallbackCompat, reasoning: metadataModel?.reasoning, }); + const resolvedFallbackMaxTokens = + configuredModel?.maxTokens ?? + providerConfig?.maxTokens ?? + providerConfig?.models?.[0]?.maxTokens ?? + staticCatalogModel?.maxTokens; return normalizeResolvedModel({ provider, cfg, @@ -1365,12 +1370,11 @@ function resolveConfiguredFallbackModel(params: { providerConfig?.contextTokens ?? providerConfig?.models?.[0]?.contextTokens ?? staticCatalogModel?.contextTokens, - maxTokens: - configuredModel?.maxTokens ?? - providerConfig?.maxTokens ?? - providerConfig?.models?.[0]?.maxTokens ?? - staticCatalogModel?.maxTokens ?? - DEFAULT_CONTEXT_TOKENS, + // maxTokens is a wire-level output cap, not a context-budget fallback. + // Omit an unknown cap so strict providers can apply their own limit. + ...(resolvedFallbackMaxTokens !== undefined + ? { maxTokens: resolvedFallbackMaxTokens } + : {}), ...(resolvedParams ? { params: resolvedParams } : {}), ...(requestTimeoutMs !== undefined ? { requestTimeoutMs } : {}), headers: requestConfig.headers, diff --git a/src/agents/openai-transport-stream.test.ts b/src/agents/openai-transport-stream.test.ts index be45f61c18af..9ed5047c0d43 100644 --- a/src/agents/openai-transport-stream.test.ts +++ b/src/agents/openai-transport-stream.test.ts @@ -7667,6 +7667,31 @@ describe("openai transport stream", () => { expect(params).not.toHaveProperty("max_tokens"); }); + it("omits output-token fields when the resolved model has no known cap", () => { + const params = buildOpenAICompletionsParams( + { + id: "mimo-v2.5-pro", + name: "MiMo V2.5 Pro", + api: "openai-completions", + provider: "xiaomi", + baseUrl: "https://api.xiaomimimo.com/v1", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 1_048_576, + } as Model<"openai-completions">, + { + systemPrompt: "system", + messages: [], + tools: [], + } as never, + undefined, + ); + + expect(params).not.toHaveProperty("max_completion_tokens"); + expect(params).not.toHaveProperty("max_tokens"); + }); + it("uses model params max_completion_tokens for OpenAI completions before model maxTokens", () => { const params = buildOpenAICompletionsParams( {