fix(agent-model): omit synthesized maxTokens fallback when nothing was configured (#98312)

* fix(agent-model): omit synthesized maxTokens fallback (Fixes #98295)

The configured-fallback resolver used DEFAULT_CONTEXT_TOKENS (200_000) as
the last-resort value for model.maxTokens when no configured, provider,
or bundled-catalog value was known. For strict OpenAI-compatible providers,
model.maxTokens is forwarded as max_completion_tokens on every request,
so the synthesized 200k value became a wire-level output cap that exceeded
the provider ceiling and produced HTTP 400 (Param Incorrect) on providers
like Xiaomi MiMo (131_072 max) — reported for mimo-v2.5 / mimo-v2.5-pro
added via 'models add' on a custom provider entry.

The sibling applyConfiguredProviderOverrides path already omits maxTokens
when nothing resolves; align resolveConfiguredFallbackModel with that
behavior. When the transport sees an undefined model.maxTokens, it omits
max_completion_tokens entirely and the provider applies its own default,
which is the correct behavior when the user has configured no output limit.

contextWindow retains DEFAULT_CONTEXT_TOKENS as its local budgeting
fallback (it is not shipped on the wire).

* test(agents): prove unknown output cap omission

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Sanjay Santhanam
2026-07-06 19:15:03 -07:00
committed by GitHub
parent 486033b183
commit 00a8dae28b
3 changed files with 68 additions and 6 deletions

View File

@@ -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",

View File

@@ -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,

View File

@@ -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(
{