Skip to content

Commit ed42141

Browse files
authored
Merge pull request #98 from pavelhov/development
chore(release): promote 0.1.27 to main
2 parents af19c91 + 88f9ae8 commit ed42141

21 files changed

Lines changed: 619 additions & 60 deletions

‎docs-site/src/content/docs/reference/architecture.md‎

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -118,14 +118,18 @@ Anthropic SSE comments and `ping` events count as upstream activity, including w
118118
thinking during context compaction. Proxy-generated keep-alives do not reset the stall deadline;
119119
an upstream connection that stops sending activity still times out.
120120

121-
For Opus 5.5 through the canonical Anthropic API, ordinary routed turns default to a
122-
128,000-token total output allowance when the caller omits a limit. This uses the model's
123-
[documented capacity for long coding turns](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/prompting-claude-opus-5-5#calibrate-effort)
124-
and preserves the requested reasoning effort. Adaptive thinking and answer text share the
125-
allowance; effort does not enforce a separate thinking budget. Explicit limits remain
126-
authoritative, and other models and custom Anthropic-compatible endpoints retain their defaults.
127-
128-
Routed context compaction for the same model and endpoint uses at most
121+
Ordinary routed turns use known provider/model output allowances by default, including
122+
requests with smaller caller-supplied caps. Set the global `outputTokenPolicy` to `request`
123+
to honor explicit caps and configured fallback budgets. Output metadata is matched to the
124+
provider destination before selecting an allowance.
125+
126+
Native Claude Fable 5.2 and Opus 5.5 have 128,000-token output allowances. Adaptive thinking
127+
and answer text share the allowance, while effort controls reasoning depth. Protocols
128+
that expose no output-budget field leave generation limits to the upstream service.
129+
See [output token allowances](/reference/configuration/providers/#output-token-allowances)
130+
for configuration and coverage.
131+
132+
Routed context compaction for Opus 5.5 through the canonical Anthropic API uses at most
129133
`medium` reasoning effort and defaults to a 64,000-token total output limit when the caller
130134
omits one. Adaptive thinking and summary text share that limit; effort does not reserve a
131135
fixed number of tokens for the summary. Explicit output limits and lower reasoning efforts

‎docs-site/src/content/docs/reference/configuration/providers.md‎

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ authenticated.
1414
| `disabledModels?` | `string[]` | — | Models hidden from Codex's catalog and `/v1/models`, but not blocked from direct proxy calls. A routed id is removed from listings. An account-qualified native id hides only that selector row; a bare native GPT id hides the bare row and every account-selector row for that model. The dashboard Models page exposes only routed and bare native rows; use this configuration field directly to hide one selector-qualified row. |
1515
| `providerContextCaps?` | `Record<string, number>` | `{}` | Per-provider Codex-visible context caps. A cap only lowers a known context window. |
1616
| `contextCapValue?` | `number` | `350000` | Value used by the dashboard context-cap controls; changing it updates every enabled `providerContextCaps` entry. |
17+
| `outputTokenPolicy?` | `"request" \| "model-max"` | `"model-max"` | Uses the known output allowance for ordinary requests, including requests with a smaller caller-supplied cap. Select `request` to honor caller caps and configured fallback budgets. |
1718
| `codexAccounts?` | `CodexAccount[]` | `[]` | ChatGPT/Codex pool account metadata managed by Codex Auth. Secrets live separately in `codex-accounts.json`. |
1819
| `pausedCodexAccountIds?` | `string[]` | `[]` | Accounts excluded from Pool selection until resumed, including the main `__main__` account when paused. |
1920
| `codexAccountNamespaces?` | `Record<string, string>` | — | Optional map from an arbitrary public model selector to a stored Codex account target. Each selector whose target is present adds separate `<selector>/<native-openai-model>` rows to the Codex picker; each row uses only that account. With any selector active, bare native rows are hidden in the picker, but their ids remain routable and listed by raw `/v1/models` unless explicitly disabled. |
@@ -38,6 +39,31 @@ configured pool ids or selector targets also cannot reuse a selector. Keep raw a
3839
emails private; the selector is the public name. See [Routing Configuration](/reference/configuration/routing/)
3940
for exact-selection behavior and precedence.
4041

42+
## Output token allowances
43+
44+
Ordinary requests use the known output allowance for their selected provider and model by
45+
default. This also applies when an existing configuration omits `outputTokenPolicy`.
46+
47+
```json
48+
{
49+
"outputTokenPolicy": "request"
50+
}
51+
```
52+
53+
With the default `model-max` policy, a smaller caller-supplied output cap is raised when CodexCommander has
54+
matching output-limit metadata for the route. For example, a 32,768-token request for native
55+
Claude Fable 5.2 can use its 128,000-token allowance. Thinking and visible output share that
56+
allowance.
57+
58+
The `request` setting shown above honors explicit caller caps. Requests that omit a cap can
59+
use known model allowances after configured fallback budgets. Unknown capacities use the
60+
request or adapter fallback. Adapters whose upstream protocol has no output-limit field use
61+
the upstream's allowance.
62+
63+
Context compaction keeps its maintenance budgets. Provider output ceilings and context
64+
windows still apply, and reaching a ceiling produces an incomplete response. A larger
65+
allowance can permit longer generation and higher usage.
66+
4167
## Reserved OpenAI providers
4268

4369
`openai` and `openai-apikey` are fixed reserved ids. `openai.codexAccountMode` is `"pool"` by default
@@ -67,8 +93,8 @@ metadata, and Pro virtual ids rewrite to the base wire model with `reasoning.mod
6793
| `modelContextWindows?` | `Record<string, number>` | Per-model context caps. These override `contextWindow` and never raise smaller live metadata. |
6894
| `modelInputModalities?` | `Record<string, string[]>` | Per-model input hints such as `["text"]` or `["text", "image"]`. |
6995
| `modelMaxInputTokens?` | `Record<string, number>` | Positive per-model max input limits used for catalog auto-compaction hints. |
70-
| `defaultMaxOutputTokens?` | `number` | Provider-wide `openai-chat` fallback when the client omits `max_output_tokens`. |
71-
| `modelMaxOutputTokens?` | `Record<string, number>` | Positive per-model `openai-chat` fallback budgets; exact/pattern matches beat the provider default. |
96+
| `defaultMaxOutputTokens?` | `number` | Provider-wide fallback for translated Anthropic, Chat Completions, Google, and Command Code requests when the client omits an output cap. |
97+
| `modelMaxOutputTokens?` | `Record<string, number>` | Positive per-model fallback budgets for translated requests. Exact/pattern matches beat the provider default. The `model-max` policy uses known model allowances ahead of these budgets. |
7298
| `chatCompletionTokenField?` | `"max_tokens" \| "max_completion_tokens"` | Chat Completions output-budget field. Defaults to `max_tokens`; canonical Kimi Coding Plan presets use `max_completion_tokens`. |
7399
| `headers?` | `Record<string, string>` | Extra upstream headers. Authorization, cookies, API-key headers, embedded newlines, and invalid names are rejected. |
74100
| `openRouterRouting?` | `OpenRouterProviderRouting` | Default OpenRouter `order`, `only`, and `allowFallbacks` preferences; valid only for canonical OpenRouter with `openai-chat`. |

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codexcommander",
3-
"version": "0.1.26",
3+
"version": "0.1.27",
44
"private": true,
55
"description": "CodexCommander — universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
66
"type": "module",

‎src/adapters/anthropic.ts‎

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { IncomingMeta, ProviderAdapter } from "./base";
22
import { debugDroppedFrame } from "../lib/debug";
33
import { isCanonicalAnthropicEndpoint } from "../lib/anthropic-endpoint";
4+
import { knownModelOutputTokenLimit, resolveOutputTokenBudget } from "../output-budget";
45
import type {
56
AdapterEvent,
67
CodexCommanderAssistantMessage,
@@ -39,7 +40,7 @@ function toAnthropicContentPart(p: CodexCommanderContentPart): unknown {
3940

4041
/** Default `max_tokens` when Codex omits `max_output_tokens`. */
4142
const DEFAULT_MAX_TOKENS = 8192;
42-
/** Safe ceiling for `max_tokens` (thinking + visible output) across current Claude 4.x models. */
43+
/** Compatibility fallback when a legacy thinking model has no verified output limit. */
4344
const REASONING_MAX_TOKENS_CEILING = 32_000;
4445
/** Ordinary adaptive-thinking sizing: heuristic max effort allowance (32k) + 8192.
4546
* Adaptive effort is soft guidance, so this total does not reserve a hard visible-output portion. */
@@ -857,11 +858,8 @@ export function createAnthropicAdapter(provider: CodexCommanderProviderConfig, c
857858
enforceAnthropicImageLimits(messages);
858859
const tools = toolsToAnthropicFormat(parsed, toolNames);
859860

860-
// Adaptive effort has no fixed thinking reservation: the legacy effort heuristic
861-
// can exhaust its shared total before an ordinary Opus 5.5 turn finishes. Use the
862-
// documented long-turn allowance only for this known model and native endpoint;
863-
// compatible gateways may have lower limits. Compaction keeps its smaller summary
864-
// allowance and effort cap, while an explicit caller total always wins.
861+
// Native Opus maintenance keeps its smaller summary allowance and effort cap.
862+
// Ordinary turns resolve their total independently from reasoning effort.
865863
const version = claudeFamilyVersion(parsed.modelId);
866864
const nativeOpus55 = version?.family === "opus" && version.major === 5 && version.minor === 5
867865
&& isCanonicalAnthropicEndpoint(provider.baseUrl);
@@ -871,8 +869,8 @@ export function createAnthropicAdapter(provider: CodexCommanderProviderConfig, c
871869
&& requestedEffort !== "none" && requestedEffort !== "minimal" && requestedEffort !== "low"
872870
? "medium"
873871
: requestedEffort;
874-
const maxOutputTokens = parsed.options.maxOutputTokens
875-
?? (nativeOpus55 ? (opusCompaction ? OPUS_5_5_COMPACTION_MAX_TOKENS : OPUS_5_5_MAX_TOKENS) : undefined);
872+
const maxOutputTokens = resolveOutputTokenBudget(provider, parsed,
873+
nativeOpus55 ? (opusCompaction ? OPUS_5_5_COMPACTION_MAX_TOKENS : OPUS_5_5_MAX_TOKENS) : undefined);
876874
// Do not mutate parsed.options: requested-effort diagnostics describe the client.
877875
// Anthropic has no reasoningLog field for output_config.effort in the shared contract.
878876

@@ -927,12 +925,16 @@ export function createAnthropicAdapter(provider: CodexCommanderProviderConfig, c
927925
// visible output) and budget_tokens >= 1024. Codex sends the SAME value for both, which
928926
// 400s ("max_tokens must be greater than thinking.budget_tokens"). Size them so max_tokens
929927
// always exceeds the budget within a model-safe ceiling, reserving room for visible output.
930-
const maxOut = maxOutputTokens ?? DEFAULT_MAX_TOKENS;
931928
const wantBudget = reasoningBudget(reasoningEffort);
932-
const maxTokens = Math.min(REASONING_MAX_TOKENS_CEILING, Math.max(maxOut, wantBudget + OUTPUT_HEADROOM));
933-
const budget = Math.max(MIN_THINKING_BUDGET, Math.min(wantBudget, maxTokens - OUTPUT_FLOOR));
929+
const knownLimit = knownModelOutputTokenLimit(provider, parsed.modelId);
930+
const maxTokens = knownLimit !== undefined && maxOutputTokens !== undefined
931+
? maxOutputTokens
932+
: Math.min(REASONING_MAX_TOKENS_CEILING, Math.max(maxOutputTokens ?? DEFAULT_MAX_TOKENS, wantBudget + OUTPUT_HEADROOM));
934933
body.max_tokens = maxTokens;
935-
body.thinking = { type: "enabled", budget_tokens: budget };
934+
if (maxTokens > MIN_THINKING_BUDGET) {
935+
const budget = Math.max(MIN_THINKING_BUDGET, Math.min(wantBudget, maxTokens - OUTPUT_FLOOR));
936+
body.thinking = { type: "enabled", budget_tokens: budget };
937+
}
936938
}
937939
// Extended thinking disallows temperature != 1 and top_p — drop both or the API 400s.
938940
delete body.temperature;

‎src/adapters/command-code.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { dispatchHttpFetch, cleanupResponseDispatch, closeAdapterObservation, observeAdapterEvent, observeDispatch, responseDispatch } from "../usage/dispatch-http";
2+
import { resolveOutputTokenBudget } from "../output-budget";
23
import { randomUUID } from "node:crypto";
34
import { execFile as execFileCallback } from "node:child_process";
45
import { promisify } from "node:util";
@@ -356,7 +357,7 @@ export function createCommandCodeAdapter(provider: CodexCommanderProviderConfig)
356357
messages: wireMessages(parsed.context.messages),
357358
tools: wireTools(tools),
358359
system,
359-
max_tokens: parsed.options.maxOutputTokens ?? provider.defaultMaxOutputTokens ?? 64_000,
360+
max_tokens: resolveOutputTokenBudget(provider, parsed, provider.defaultMaxOutputTokens ?? 64_000),
360361
// The proprietary /alpha/generate endpoint is NDJSON-stream-only; the proxy converts
361362
// the buffered/streamed events to the client's requested shape (parsed.stream).
362363
stream: true,

‎src/adapters/google.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base";
2+
import { resolveOutputTokenBudget } from "../output-budget";
23
import { debugDroppedFrame } from "../lib/debug";
34
import { createHash } from "node:crypto";
45
import { createImageBudget, materializeInlineImage, MAX_ENCODED_BYTES_PER_IMAGE, artifactHttpUrl } from "../images/artifacts";
@@ -335,7 +336,8 @@ export function createGoogleAdapter(provider: CodexCommanderProviderConfig): Pro
335336
if (toolConfig) body.toolConfig = toolConfig;
336337

337338
const generationConfig: Record<string, unknown> = {};
338-
if (parsed.options.maxOutputTokens) generationConfig.maxOutputTokens = parsed.options.maxOutputTokens;
339+
const maxOutputTokens = resolveOutputTokenBudget(provider, parsed);
340+
if (maxOutputTokens !== undefined) generationConfig.maxOutputTokens = maxOutputTokens;
339341
if (parsed.options.temperature !== undefined) generationConfig.temperature = parsed.options.temperature;
340342
if (parsed.options.topP !== undefined) generationConfig.topP = parsed.options.topP;
341343
if (parsed.options.stopSequences) generationConfig.stopSequences = parsed.options.stopSequences;

‎src/adapters/openai-chat.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { AdapterRequest, ProviderAdapter } from "./base";
22
import type { AdapterEvent, CodexCommanderAssistantMessage, CodexCommanderContentPart, CodexCommanderMessage, CodexCommanderParsedRequest, CodexCommanderProviderConfig, CodexCommanderTextContent, CodexCommanderThinkingContent, CodexCommanderToolCall, CodexCommanderUsage } from "../types";
33
import { isAllowedToolChoice, modelInList, namespacedToolName, resolveToolChoiceWireName, toolAllowedByChoice } from "../types";
44
import { mapReasoningEffort, modelRecordValue } from "../reasoning-effort";
5+
import { resolveOutputTokenBudget } from "../output-budget";
56
import { debugProviderDiagnostic } from "../lib/debug";
67
import { isDebugEnabled } from "../lib/debug-settings";
78
import { isCyberPolicyCode } from "../lib/errors";
@@ -881,7 +882,8 @@ export function createOpenAIChatAdapter(provider: CodexCommanderProviderConfig):
881882
stream: parsed.stream,
882883
};
883884
if (modelInList(provider.reasoningSplitModels, parsed.modelId)) body.reasoning_split = true;
884-
const maxTokens = resolveMaxTokens(provider, parsed);
885+
const requestedMaxTokens = resolveMaxTokens(provider, parsed);
886+
const maxTokens = resolveOutputTokenBudget(provider, parsed, requestedMaxTokens);
885887
const openRouterRouting = resolveOpenRouterRouting(provider, parsed.modelId);
886888
if (openRouterRouting) body.provider = openRouterProviderPayload(openRouterRouting);
887889
if (tools) body.tools = tools;
@@ -921,7 +923,7 @@ export function createOpenAIChatAdapter(provider: CodexCommanderProviderConfig):
921923
wireValue: reasoningEffort,
922924
};
923925
} else if (modelInList(provider.thinkingBudgetModels, parsed.modelId)) {
924-
const budget = thinkingBudgetForEffort(parsed, reasoningEffort, maxTokens);
926+
const budget = thinkingBudgetForEffort(parsed, reasoningEffort, requestedMaxTokens);
925927
if (budget !== undefined) {
926928
body.thinking_budget = budget;
927929
reasoningLog = {

‎src/adapters/openai-responses.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { isNativeResponsesProvider, NATIVE_RESPONSES_HEADERS } from "../responses/native-policy";
2+
import { knownModelOutputTokenLimit, resolveOutputTokenBudget } from "../output-budget";
23
import { createHash } from "node:crypto";
34
import type { AdapterRequest, IncomingMeta, ProviderAdapter } from "./base";
45
import { namespacedToolName, type AdapterEvent, type CodexCommanderParsedRequest, type CodexCommanderProviderConfig, type CodexCommanderUsage } from "../types";
@@ -1235,6 +1236,11 @@ export function createResponsesPassthroughAdapter(provider: CodexCommanderProvid
12351236
);
12361237
outBody = normalizeImageGenClientTools(outBody);
12371238
}
1239+
if (!forward && provider._outputTokenPolicy === "model-max"
1240+
&& knownModelOutputTokenLimit(provider, parsed.modelId) !== undefined
1241+
&& parsed._compactionRequest !== true && isPlainObject(outBody)) {
1242+
outBody = { ...outBody, max_output_tokens: resolveOutputTokenBudget(provider, parsed) };
1243+
}
12381244
if (!native && (forward || parsed._previousResponseInputExpanded === true)) {
12391245
outBody = repairOversizedReplayCallIds(outBody);
12401246
}

‎src/config.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1107,6 +1107,7 @@ const configSchema = z.object({
11071107
.optional(),
11081108
providers: z.record(z.string(), providerConfigSchema),
11091109
defaultProvider: z.string().min(1),
1110+
outputTokenPolicy: z.enum(["request", "model-max"]).optional(),
11101111
claudeCode: claudeCodeSchema.optional(),
11111112
clientIntegrations: clientIntegrationsSchema.optional(),
11121113
subagentModels: subagentRosterSchema.optional(),
@@ -2524,6 +2525,7 @@ export function getDefaultConfig(): CodexCommanderConfig {
25242525
},
25252526
},
25262527
defaultProvider: "openai",
2528+
outputTokenPolicy: "model-max",
25272529
subagentModels: DEFAULT_SUBAGENT_MODELS.map(model => ({ model })),
25282530
multiAgentGuidanceEnabled: true,
25292531
websockets: false,

0 commit comments

Comments
 (0)