You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The CLI and the server MUST behave identically. Same defaults, same flags, same escape hatches, same failure modes. There is no acceptable reason for the server to apply a max_tokens cap and the CLI to skip it, or for the server to hard-code permissive: false while the CLI exposes --permissive. This is a parity bug across two surfaces, not a "discussion point".
This ticket replaces the original discussion-style framing. Concrete actions are below the analyses.
What is broken (v1.3.1)
Policy
CLI
Server
Parity?
max_tokens when not specified
nil → no cap → can run to [context overflow]
512 (from BodyLimits.defaultMaxResponseTokens)
NO
Permissive guardrails
--permissive flag exposes .permissiveContentTransformations
defaultOutputReserveTokens (used by the context trimmer) is 512.
I matched the new defaultMaxResponseTokens to 512 for "architectural consistency" — but the trimmer's reserve and the response cap are two different concepts. The trimmer's 512 reserves space for output; the cap limits how much output the model produces. They can coincide but should not be coupled by definition.
OpenAI's own historical default for max_tokens was 16 (and is now effectively the model context max minus the prompt). Anthropic requires it explicitly. There is no industry standard.
🔴 Red Hat (intuition / feelings)
512 feels arbitrary because it is arbitrary — picked for code symmetry, not for user need.
It conflates "trim reservation" with "response cap".
It will surprise users whose first useful response gets cut at 512 tokens with finish_reason: length.
⚫ Black Hat (risks)
Silent truncation for any reply longer than 512 tokens. Users who didn't read the new README section will think the model is dumb.
Coupling bug: if anyone ever changes defaultOutputReserveTokens for trimmer reasons, they will silently change the response cap too. Bad coupling.
Doesn't actually solve the real problem, which is "the on-device model has no graceful overflow recovery". A cap at 512 only papers over the symptom.
Hostile to good use cases: structured JSON output, code generation, summarisation — all routinely > 512 tokens.
Sets finish_reason: "length" so clients can detect truncation and ask for more.
Conservative — under 512 covers the majority of chat-style replies.
🟢 Green Hat (alternatives, in roughly increasing engineering effort)
Bigger fixed default. 1024 or 2048. Covers more legitimate cases. Still safe for typical prompts.
No default; reject the request. Return 400 with a message: "max_tokens is required because the on-device model cannot recover from context overflow". Forces the client to make the call. Honest about the limit.
Dynamic default: max(64, 4096 − estimatedInputTokens − safetyBuffer). Uses the entire remaining window. Requires TokenCounter on the input before sampling — already available.
Per-mode default: 512 for plain chat, larger for response_format=json_object, larger for tool-calling continuations.
Decouple from defaultOutputReserveTokens: even if the value stays 512, name it for what it is (responseCap) and document why, so future contributors don't think they are the same thing.
Fix the root cause: catch the overflow in the streaming path, end the stream cleanly with finish_reason: "length", and return what was generated up to that point. Then the cap becomes optional rather than load-bearing.
🔵 Blue Hat (process)
The 512 was a 5-minute patch for a real production bug. It deserves a proper second pass.
Whatever value we pick, the CLI must apply the same one.
The decision should be backed by a microbenchmark across short / medium / long prompts so the number has a basis.
Alternatives analysis: what should the cap actually be?
Recommend (3) dynamic default as the right long-term answer, with (5) decoupling the constant as a no-brainer immediate cleanup. Rationale:
Option
Pros
Cons
Verdict
Keep 512
Already shipped, conservative
Truncates valid use cases
Insufficient
1024 / 2048 fixed
Less truncation
Still arbitrary, still couples to nothing meaningful
Better than 512 but not the right shape
Reject without max_tokens
Honest, forces explicit choice
Breaks every "drop-in OpenAI client" promise we make
Breaks the product pitch
Dynamic 4096 − input − buffer
Uses the full window, no surprise truncation, correct shape
Needs token-counting before sampling (we already have it)
If we keep a fixed number for now, 1024 is a defensible compromise — covers typical structured JSON and short-to-medium replies, still leaves 3072 tokens for input.
Permissive mode: what must happen
Server clients have NO way to enable permissive guardrails today. This is a real product gap, not a missing knob — Apple's default guardrails block plenty of benign creative-writing, technical-explanation, and code prompts that the CLI handles fine with --permissive.
The server MUST expose this. Two compatible mechanisms; we should ship both:
x_permissive: true request extension — per-request opt-in on the JSON body. Mirrors the existing x_context_* extensions. Cheapest change, immediate parity for OpenAI-SDK callers.
--permissive server flag — process-wide opt-in for the rare server operator who wants every request permissive (matches CLI semantics exactly).
Hard-coded permissive: false at Handlers.swift:82 must be deleted in the same PR that ships either of the above.
Actions (the actual work)
In rough order:
Decouple the constant. Rename defaultMaxResponseTokens to something honest (fallbackResponseCap?) and document that it has nothing to do with defaultOutputReserveTokens. Quick win, lands first.
Apply the same default to the CLI.Sources/main.swift:143 should use the same fallback the server uses. No more "the CLI streams to stdout in real time" hand-wave — that defence does not hold for piped, scripted, JSON-output, launchd, or large-input invocations. Same bug, same fix, same surface.
Switch the cap to a dynamic value based on 4096 − estimatedInputTokens − safetyBuffer, with a floor (e.g. 64 tokens) so a too-large input fails loudly rather than silently allowing 0 output. Or, if we keep it fixed for now, raise it to 1024 with explicit rationale in the README.
Expose permissive mode on the server. Add x_permissive request extension and a --permissive server flag. Delete the hard-coded false at Handlers.swift:82.
Refactor SessionOptions construction into one shared "apply CLI/server defaults" function. The architectural root cause is that both surfaces hand-roll their own SessionOptions. Today: max_tokens. Tomorrow: the next field someone forgets to wire on one of the two sides.
Tests asserting parity. A unit test that constructs SessionOptions from "no flags" CLI input and "no fields" server input and asserts they are byte-equal except for the configuration fields (retry, server-only knobs).
Update the README's Default response cap (max_tokens) section when the above lands. The current text justifies the divergence; that justification needs to disappear.
Microbenchmark the cap. Measure typical chat / JSON / code response lengths so any future change to the default is data-driven, not vibes.
Acceptance criteria
A future PR that closes this ticket must:
Make the CLI and the server apply the samemax_tokens fallback (whatever the value), proven by a parity test.
Make --permissive reachable from server clients (via extension and/or server flag), proven by an integration test that flips permissive on and verifies a guardrail-loosened reply.
Land a single shared applyDefaults(SessionOptions) (or equivalent) so future divergences are mechanically prevented.
Update the README to reflect actual parity.
Severity
P1 — Real user impact (silent truncation, no permissive escape hatch on the server, repro of #128 still possible from the CLI). Not a P0 only because both bugs have explicit-flag workarounds. Block any v1.4 release on closing this.
TL;DR
The CLI and the server MUST behave identically. Same defaults, same flags, same escape hatches, same failure modes. There is no acceptable reason for the server to apply a
max_tokenscap and the CLI to skip it, or for the server to hard-codepermissive: falsewhile the CLI exposes--permissive. This is a parity bug across two surfaces, not a "discussion point".This ticket replaces the original discussion-style framing. Concrete actions are below the analyses.
What is broken (v1.3.1)
max_tokenswhen not specified[context overflow]BodyLimits.defaultMaxResponseTokens)--permissiveflag exposes.permissiveContentTransformationspermissive: falseatHandlers.swift:82ContextConfig.permissivepropagationCode refs (commit
10650d8, v1.3.1):SessionOptionsatmain.swift:141-149SessionOptionsatHandlers.swift:78-86permissive→SystemLanguageModelguardrails atSession.swift:44-48512lives atBodyLimits.swift:17Six Thinking Hats: is 512 the right cap?
Franz called out the arbitrary "500-something". Honest analysis:
⚪ White Hat (facts only)
defaultOutputReserveTokens(used by the context trimmer) is 512.defaultMaxResponseTokensto 512 for "architectural consistency" — but the trimmer's reserve and the response cap are two different concepts. The trimmer's 512 reserves space for output; the cap limits how much output the model produces. They can coincide but should not be coupled by definition.max_tokenswas16(and is now effectively the model context max minus the prompt). Anthropic requires it explicitly. There is no industry standard.🔴 Red Hat (intuition / feelings)
finish_reason: length.⚫ Black Hat (risks)
defaultOutputReserveTokensfor trimmer reasons, they will silently change the response cap too. Bad coupling.🟡 Yellow Hat (benefits)
finish_reason: "length"so clients can detect truncation and ask for more.🟢 Green Hat (alternatives, in roughly increasing engineering effort)
400with a message: "max_tokens is required because the on-device model cannot recover from context overflow". Forces the client to make the call. Honest about the limit.max(64, 4096 − estimatedInputTokens − safetyBuffer). Uses the entire remaining window. RequiresTokenCounteron the input before sampling — already available.512for plain chat, larger forresponse_format=json_object, larger for tool-calling continuations.defaultOutputReserveTokens: even if the value stays 512, name it for what it is (responseCap) and document why, so future contributors don't think they are the same thing.finish_reason: "length", and return what was generated up to that point. Then the cap becomes optional rather than load-bearing.🔵 Blue Hat (process)
Alternatives analysis: what should the cap actually be?
Recommend (3) dynamic default as the right long-term answer, with (5) decoupling the constant as a no-brainer immediate cleanup. Rationale:
max_tokens4096 − input − bufferIf we keep a fixed number for now, 1024 is a defensible compromise — covers typical structured JSON and short-to-medium replies, still leaves 3072 tokens for input.
Permissive mode: what must happen
Server clients have NO way to enable permissive guardrails today. This is a real product gap, not a missing knob — Apple's default guardrails block plenty of benign creative-writing, technical-explanation, and code prompts that the CLI handles fine with
--permissive.The server MUST expose this. Two compatible mechanisms; we should ship both:
x_permissive: truerequest extension — per-request opt-in on the JSON body. Mirrors the existingx_context_*extensions. Cheapest change, immediate parity for OpenAI-SDK callers.--permissiveserver flag — process-wide opt-in for the rare server operator who wants every request permissive (matches CLI semantics exactly).Hard-coded
permissive: falseatHandlers.swift:82must be deleted in the same PR that ships either of the above.Actions (the actual work)
In rough order:
defaultMaxResponseTokensto something honest (fallbackResponseCap?) and document that it has nothing to do withdefaultOutputReserveTokens. Quick win, lands first.Sources/main.swift:143should use the same fallback the server uses. No more "the CLI streams to stdout in real time" hand-wave — that defence does not hold for piped, scripted, JSON-output, launchd, or large-input invocations. Same bug, same fix, same surface.4096 − estimatedInputTokens − safetyBuffer, with a floor (e.g. 64 tokens) so a too-large input fails loudly rather than silently allowing 0 output. Or, if we keep it fixed for now, raise it to 1024 with explicit rationale in the README.x_permissiverequest extension and a--permissiveserver flag. Delete the hard-codedfalseatHandlers.swift:82.SessionOptionsconstruction into one shared "apply CLI/server defaults" function. The architectural root cause is that both surfaces hand-roll their ownSessionOptions. Today:max_tokens. Tomorrow: the next field someone forgets to wire on one of the two sides.SessionOptionsfrom "no flags" CLI input and "no fields" server input and asserts they are byte-equal except for the configuration fields (retry, server-only knobs).Default response cap (max_tokens)section when the above lands. The current text justifies the divergence; that justification needs to disappear.Acceptance criteria
A future PR that closes this ticket must:
max_tokensfallback (whatever the value), proven by a parity test.--permissivereachable from server clients (via extension and/or server flag), proven by an integration test that flips permissive on and verifies a guardrail-loosened reply.applyDefaults(SessionOptions)(or equivalent) so future divergences are mechanically prevented.Severity
P1 — Real user impact (silent truncation, no permissive escape hatch on the server, repro of #128 still possible from the CLI). Not a P0 only because both bugs have explicit-flag workarounds. Block any v1.4 release on closing this.
cc @franzenzenhofer
Updated 2026-04-26 after Franz's review on v1.3.1. Original framing was too discussion-heavy; this version states what must happen.