mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-10 10:03:58 +00:00
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:
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user