studio: allow a per-connection max tokens limit on every provider by mahiatlinux · Pull Request #8944 · unslothai/unsloth · GitHub
Skip to content

studio: allow a per-connection max tokens limit on every provider - #8944

Merged
danielhanchen merged 7 commits into
unslothai:mainfrom
mahiatlinux:fix/studio-connected-max-tokens
Aug 16, 2026
Merged

studio: allow a per-connection max tokens limit on every provider#8944
danielhanchen merged 7 commits into
unslothai:mainfrom
mahiatlinux:fix/studio-connected-max-tokens

Conversation

@mahiatlinux

Copy link
Copy Markdown
Collaborator

Fixes #8910

Studio pinned Max Tokens at 32,768 for any connected model whose output cap is not in
EXTERNAL_MAX_OUTPUT_TOKENS_BY_MODEL, and the only way to raise it was the per-connection Max
Tokens limit, which supportsCustomMaxOutputTokens offered on generic Custom connections alone.
MiniMax M3 through OpenRouter hit that, along with most of the router's catalog: the slider sat at
its maximum and the model kept stopping at "reached the Max Tokens limit".

The override now applies to every connection type except ChatGPT subscriptions, whose routing,
model list and output cap are all fixed, so a value stored against one would never be read.

How the ceiling resolves

getExternalMaxOutputTokens takes the documented per-model cap and the connection's own limit and
returns:

  • documented cap present: Math.min(documented, override). The override lowers a published cap,
    which a gateway or spend policy below it needs, and never raises past one. That matters because
    a single OpenRouter connection fronts models of every size, so a 262,144 limit set for MiniMax
    must not raise the slider for openai/gpt-5.3.
  • no documented cap: the override outright, then the 32,768 fallback.
  • either way, floored at getExternalMinOutputTokens. Kimi's floor is 16,000 so a thinking model's
    reasoning_content and answer both fit; without the floor an 8,000 limit handed ParamSlider
    min 16,000 against max 8,000 and chat-adapter sent 8,000 on every request.

The connections dialog rejects a value below that floor rather than storing one the resolver
raises anyway.

Code paths

  • studio/frontend/src/features/chat/provider-capabilities.ts: getExternalMaxOutputTokens
    restructured around a new _documentedMaxOutputTokens lookup.
  • studio/frontend/src/features/chat/external-providers.ts: normalizeCustomMaxOutputTokens ->
    normalizeProviderMaxOutputTokens (the providerType gate is gone),
    supportsCustomMaxOutputTokens -> supportsProviderMaxOutputTokens,
    CUSTOM_MAX_OUTPUT_TOKENS_MIN -> PROVIDER_MAX_OUTPUT_TOKENS_MIN.
  • studio/frontend/src/features/chat/sync-external-providers.ts: carries
    config.max_output_tokens for every connection instead of Custom only.
  • studio/frontend/src/features/chat/chat-providers-dialog.tsx: the Max Tokens limit row renders
    for every non-Codex connection, and parseMaxOutputTokens floors at the provider minimum.
  • studio/backend/routes/providers.py: _validate_max_output_tokens_contract refuses a non-null
    override only for openai_codex. An explicit null is still accepted everywhere, since a blank
    field serialises as null rather than as an omission.

No schema change. llm_providers.max_output_tokens already exists from #8512, and no non-Custom
row can hold a stale value because the old contract rejected one.

Checks

  • npm test 2703 passed, 0 failed
  • npm run typecheck clean
  • pytest tests/test_provider_max_output_tokens_contract.py tests/test_credential_routes.py 59
    passed
  • ruff check and scripts/run_ruff_format.py clean
  • End to end against a real Studio backend and a fake OpenAI-compatible upstream: created an
    OpenRouter connection with a 262,144 limit, read it back through GET /api/providers, resolved
    the slider ceiling through the real syncExternalProvidersFromBackend and
    getExternalMaxOutputTokens (262,144 for minimax/minimax-m3, 16,384 for openai/gpt-5.3),
    sent a chat completion and confirmed max_tokens: 262144 reached the upstream, then cleared the
    override and confirmed the connection returned to the default. The same run against main fails
    at connection creation with Max Tokens limit can only be overridden for generic Custom providers.
  • Each regression test was mutation-checked: reverting the fix it covers makes it fail.

Studio pinned Max Tokens at the 32,768 fallback for any external model with no
documented per-model cap, and the only escape hatch was the generic Custom
connection type. A MiniMax M3 connection through OpenRouter, or any of the
hundreds of router models the capability table does not list, had no way to
raise it.

The per-connection Max Tokens limit now applies to every connection type except
ChatGPT subscriptions, whose routing, model list and output cap are all fixed.
A documented per-model cap still wins over the override: one connection fronts
many models on a router, so a limit set for a 256k-output model must not raise
the slider past what a smaller model on the same connection accepts.

Fixes unslothai#8910
Review of the previous commit turned up two ways the override resolved wrongly.

A documented per-model cap replaced the override outright, so a limit set below a
published cap did nothing: typing 8192 on an OpenAI connection saved cleanly and
left the slider and the outgoing max_tokens at 128000. getExternalMaxOutputTokens
now takes Math.min of the two, so an override lowers a documented cap and still
cannot raise past one.

The other end had no guard at all. EXTERNAL_MIN_OUTPUT_TOKENS_BY_PROVIDER floors
Kimi at 16000 so a thinking model's reasoning_content and answer both fit, and
before this the override could sit under it: an 8000 limit on a Kimi connection
handed the Max Tokens slider min 16000 against max 8000, and chat-adapter sent
8000 on every request. The resolved ceiling is now floored at the provider
minimum, and the connections dialog rejects a sub-floor value rather than storing
one that gets silently raised.

Also here: sync-external-providers.ts and the localStorage load path in
external-providers.ts both deliver the override to the UI and neither was pinned,
so re-gating either on the Custom provider type made the feature a no-op with the
suite green. The Codex rejection test asserted only status 400, which a Codex
create returns from the auth contract too, and passed with the guard deleted.
Both still said the override only replaces the 32,768-token fallback. It also
lowers a documented per-model cap now.
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 16, 2026
chatgpt-codex-connector[bot]

This comment was marked as resolved.

@mahiatlinux

Copy link
Copy Markdown
Collaborator Author

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b04f5e4628

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

exist is a no-op.
"""
if field_was_set and value is not None and provider_type != "custom":
if field_was_set and value is not None and provider_type == "openai_codex":

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject Kimi limits below its output floor

For a Kimi connection, a POST or PUT with max_output_tokens from 64 through 15,999 now passes both the Pydantic ge=64 constraint and this Codex-only check, so that value is persisted as the connection's maximum. On the frontend, however, getExternalMaxOutputTokens raises the effective cap to Kimi's 16,000-token minimum, and the adapter consequently sends up to 16,000 tokens, exceeding the configured maximum. Reject Kimi values below its provider floor in the backend as the dialog already does, rather than accepting a cap that cannot be enforced.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not reachable from the product: the connections dialog floors the value at getExternalMinOutputTokens(providerType), so a Kimi row cannot be given 64-15,999 through Studio. Only a hand-written REST call gets there, and the floor then keeps that connection safe rather than honouring a cap that truncates reasoning_content mid-stream. Enforcing it server-side would need EXTERNAL_MIN_OUTPUT_TOKENS_BY_PROVIDER copied into Python, a second source of truth for every provider floor that is free to drift from the frontend table that actually resolves the ceiling.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: b04f5e4628

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@danielhanchen

Copy link
Copy Markdown
Member

Before/after evidence for this one, from two isolated Studio installs: BEFORE is the merge base 6f443b5cc, AFTER is the head b04f5e462. Both are install.sh --local builds of their own tree, so each half is really serving the bundle from that commit. Same flow on both sides: Settings > Connections > Add connection, connection type OpenRouter.

Settings > Connections > Add connection with OpenRouter selected, before and after

BEFORE runs Connection > API key > the Studio-tools note. AFTER inserts the Max Tokens limit row between the API key field and that note, with the 32,768 placeholder, the help text describing how the override interacts with a model's documented cap, and the amber warning about an upstream that will not accept the value. The Connection select reads OpenRouter on both halves, so the difference is the field and not a missed pick.

The numeric half, read from the same two servers that were photographed. POST /api/providers/ with provider_type: openrouter and max_output_tokens: 262144:

BEFORE AFTER
create status 400 201
detail Max Tokens limit can only be overridden for generic Custom providers. -
value read back on GET /api/providers/ null 262144
Max Tokens limit field on the form absent present

So the 262,144 limit #8910 asks for is both settable and durable after this change, and the connection it is stored against is the one the resolver reads it from.

…ts (unslothai#8944)

parseMaxOutputTokens throws below the provider floor, and the edit form seeds its
draft from the stored value, so a row holding a value under that floor failed every
unrelated save. Seed the draft at the floor instead, which is what
getExternalMaxOutputTokens already resolves such a value to.

Also covers the ChatGPT subscription rejection on create, which was asserted on the
update path only.
@danielhanchen

Copy link
Copy Markdown
Member

Ran the before/after simulations on this one, plus a compatibility sweep, and pushed two small changes in 737521f.

What happened before, what happens now

Before: the per-connection Max Tokens limit existed only on generic Custom connections. Every other connection resolved its ceiling from the documented per-model table, or 32,768 when the model was not in it. MiniMax M3 through OpenRouter is not in it, so the slider sat at 32,768 at its maximum and generation stopped there, which is #8910.

After: any connection except a ChatGPT subscription can carry the limit. A documented cap now bounds the override rather than being replaced by it, so an OpenRouter connection set to 262,144 gives MiniMax M3 262,144 while openai/gpt-5.3 on the same connection stays at its documented 16,384.

The issue is real, not theoretical: the reporter's screenshot shows a generation cut off at the ceiling, and I reproduced the resolver behaviour on both sides.

Backwards compatibility

Swept getExternalMaxOutputTokens over 15,351 cells (21 provider types x 43 model ids x 19 override values) on the merge base and on the head, and diffed cell by cell.

cells swept per side 15,351
cells that moved 8,305
cells that moved with no override set 0

Every single change requires the user to have explicitly set a limit. Nobody who upgrades and changes nothing sees a different ceiling. In particular the new unconditional floor is a no-op today, since the only floor is Kimi's 16,000 and Kimi has no documented cap, so the unfloored result was already 32,768.

The write contract, hammered over 240 create/update/clear cases against both live backends:

  • accepted on the base but refused on the head: 0. The contract only relaxes.
  • Custom connection behaviour: byte for byte unchanged.
  • bounds (63, 0, -1, 2^53, floats, strings, booleans) rejected identically on both sides.
  • ChatGPT subscriptions still refuse an override on create and on update, and still accept an explicit null so a blank field does not break unrelated edits.
  • a spoofed provider_type on update is ignored: the row keeps its type, so an override cannot be laundered onto a Codex row.

Old installs: started from the original llm_providers schema, from the intermediate one, and from the current one, then ran the head build against each. The column is added on demand by _ensure_schema, all rows read, and a pre-existing Custom override of 131,072 survived untouched. No migration is owed.

Platforms and browsers

No hardware path is involved: this is which integer goes in max_tokens on an outbound HTTPS call, so there is no CUDA, ROCm, Metal or CPU-only variation to test, and the local model path never calls the resolver. The backend side is pure Python plus sqlite with no platform-specific filesystem, subprocess or path handling in the changed lines.

What does vary by platform is the browser, so I drove the dialog in all three engines behind the four browsers named (Chromium covers Chrome and Edge, Gecko covers Firefox, WebKit covers Safari):

Chromium Firefox WebKit
field renders, placeholder 32768 yes yes yes
131,072 retained in the box yes yes yes
grouped value rejected with a message yes yes yes
10 rejected naming the 64 minimum yes yes yes
262144 saved and stored yes yes yes

That first row is the one worth having: type="text" was chosen precisely because a type="number" input runs the WHATWG sanitisation algorithm and would blank a grouped value, silently clearing the override. All three engines keep the text and reject it with a message, so the reasoning in the code comment holds in Gecko and WebKit and not just in Chromium.

On the provider floor asymmetry

Several passes flagged that the backend accepts a Kimi value below the 16,000 floor the frontend enforces, and suggested enforcing it server side. I traced it end to end before agreeing, and I do not think the backend should change:

  • nothing outside routes/providers.py, models/providers.py and storage/providers_db.py reads llm_providers.max_output_tokens. The max_output_tokens in routes/inference.py is the Responses API request body field, a different thing with the same name, so there is no backend-routed path that reads the stored cap and bypasses the floor.
  • with 8,000 stored on a Kimi row, getExternalMaxOutputTokens returns 16,000 and the chat-adapter sends 16,000 for every settings value. The sub-floor number never reaches the wire.
  • enforcing it server side means a copy of the provider floor table in Python, free to drift from the frontend table that actually resolves the ceiling.

There was one real consequence though, which is what 737521f fixes.

What changed in 737521f

parseMaxOutputTokens throws below the floor and the edit form seeds its draft from the stored value, so a row holding a sub-floor value could not be saved at all: renaming it, rotating its key or changing its models all failed on a number the user never typed. That is the same shape as the bug #8512 had to follow up on. Seeding the draft at the floor fixes it without a second source of truth, and matches what the resolver already resolves such a value to.

The second change covers the ChatGPT subscription rejection on create, which was asserted on the update path only.

Both are mutation checked: reverting the dialog line fails the new frontend assertion, and neutering the Codex condition fails the new backend one. Gates after the change: frontend typecheck clean, the three affected test files 22 passed, ruff check clean, backend test_provider_max_output_tokens_contract.py and test_credential_routes.py 59 passed.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: 737521faae

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 16, 2026
@unslothai unslothai deleted a comment from mahiatlinux Aug 16, 2026
@unslothai unslothai deleted a comment from mahiatlinux Aug 16, 2026
@unslothai unslothai deleted a comment from mahiatlinux Aug 16, 2026
@danielhanchen
danielhanchen merged commit c41d491 into unslothai:main Aug 16, 2026
47 of 55 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Connected/Cloud LLM Responses are limited to 32768

2 participants