Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions src/adapters/openai-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,27 @@ function stripDisabledReasoningSummaries(
};
}

/**
* Hide a no-op Responses verbosity control from the wire as well as the catalog. This runs at
* final serialization so a stale catalog or direct caller cannot bypass the capability. Other
* `text` settings (notably structured-output `format`) remain untouched.
*/
function stripDisabledVerbosity(
body: unknown,
provider: OcxProviderConfig,
modelId: string,
): unknown {
if (modelRecordValue(provider.modelSupportsVerbosity, modelId) !== false || !isPlainObject(body)) {
return body;
Comment on lines +381 to +382

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 Propagate the provider-wide opt-out to the wire adapter

When an xAI model ID arrives through live discovery and is absent from XAI_MODELS, the catalog correctly inherits ProviderRegistryEntry.supportsVerbosity = false, but routedProviderConfig supplies the adapter only the enumerated modelSupportsVerbosity map. Consequently, if that discovered model is explicitly routed through openai-responses, this check sees undefined and forwards a stale or direct caller's text.verbosity, contradicting the final-wire guard described above it. Propagate the provider-wide default into the routed request policy or materialize the effective value for the routed model ID.

AGENTS.md reference: src/AGENTS.md:L18-L18

Useful? React with 👍 / 👎.

}
if (!isPlainObject(body.text) || !Object.hasOwn(body.text, "verbosity")) return body;
const { verbosity: _verbosity, ...rest } = body.text;
return {
...body,
...(Object.keys(rest).length > 0 ? { text: rest } : { text: undefined }),
};
}

/**
* Normalize only the delivery enum Codex already emitted. Do not inject a field into callers that
* did not request summaries, and leave every unconfigured provider/model byte-for-byte unchanged.
Expand Down Expand Up @@ -1787,8 +1808,12 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
dropNullContentChannel: !isOpenAiOperatedResponsesDestination(provider),
stripEncryptedContent: threadServingIdentityChanged,
})))))));
const finalBody = stripDisabledReasoningSummaries(
normalizeConfiguredReasoningSummaryDelivery(sanitizedBody, provider, parsed.modelId),
const finalBody = stripDisabledVerbosity(
stripDisabledReasoningSummaries(
normalizeConfiguredReasoningSummaryDelivery(sanitizedBody, provider, parsed.modelId),
provider,
parsed.modelId,
),
provider,
parsed.modelId,
);
Expand Down
6 changes: 6 additions & 0 deletions src/codex/catalog/aggregation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,12 @@ export function deriveComboCatalogModel(
? { supportsServiceTier: false }
: {}),
...(members.some(member => member.supportsReasoningSummaries === false) ? { supportsReasoningSummaries: false } : {}),
// A combo is only as capable as its least capable member. One member that cannot honour
// text.verbosity is enough to make the control a no-op for the whole combo, so the
// conservative false propagates — the same rule supportsReasoningSummaries uses above.
// Without this, routing a combo through an xAI or Kiro member re-advertised a control the
// upstream accepts and ignores.
...(members.some(member => member.supportsVerbosity === false) ? { supportsVerbosity: false } : {}),
...(members.every(member => member.codexToolMode === "shell")
? { codexToolMode: "shell" as const }
: {}),
Expand Down
22 changes: 19 additions & 3 deletions src/codex/catalog/provider-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import {
serviceTierSupportFromPolicy,
} from "../../providers/service-tier";
import type { FastPolicyAuthority } from "../../providers/fastwire";
import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport } from "../../providers/registry";
import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../../providers/registry";
import { parseAntigravityAvailableModels, registerAntigravityDiscoveredWireModels } from "../../providers/antigravity-models";
import { applyProviderContextCap, providerContextCap, resolveUnknownRoutedContextWindow } from "../../providers/context-cap";
import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget";
Expand Down Expand Up @@ -577,6 +577,7 @@ function providerCatalogFingerprint(name: string, prov: OcxProviderConfig): Reco
re: prov.modelReasoningEfforts ?? null,
defRe: prov.modelDefaultReasoningEfforts ?? null,
rsSum: prov.modelSupportsReasoningSummaries ?? null,
verbosity: prov.modelSupportsVerbosity ?? null,
rsDel: prov.modelReasoningSummaryDelivery ?? null,
serviceTier: prov.modelSupportsServiceTier ?? null,
noVis: [...(prov.noVisionModels ?? [])].sort(),
Expand Down Expand Up @@ -645,8 +646,22 @@ function configuredReasoningSummarySupport(prov: OcxProviderConfig | undefined,
return modelRecordValue(prov.modelReasoningSummaryDelivery, id) !== undefined ? true : undefined;
}

function configuredVerbositySupport(name: string, prov: OcxProviderConfig | undefined, id: string): boolean | undefined {
const explicit = prov ? modelRecordValue(prov.modelSupportsVerbosity, id) : undefined;
if (explicit !== undefined) return explicit;
if (!prov) return undefined;
const entry = (providerMatchesRegistryTransport(name, prov) ? getProviderRegistryEntry(name) : undefined)
?? registryEntryForProviderDestination(prov);
if (!entry) return undefined;
const perModel = modelRecordValue(entry.modelSupportsVerbosity, id);
if (perModel !== undefined) return perModel;
// Provider-wide fallback. `modelSupportsVerbosity` only enumerates the ids present when the
// registry row was written, so a live-discovered model used to fall through here and
// re-advertise a control the upstream accepts and ignores. A per-model entry still wins.
return entry.supportsVerbosity;
}

export function applyProviderConfigHints(name: string, prov: OcxProviderConfig, model: CatalogModel, providerCap?: number): CatalogModel {
void name;
const configuredCap = configuredContextWindow(prov, model.id);
const configuredMaxInput = configuredMaxInputTokens(prov, model.id);
const configuredAutoCompact = configuredAutoCompactTokenLimit(prov, model.id);
Expand All @@ -662,6 +677,7 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
const reasoningEfforts = configuredReasoningEfforts(prov, model.id);
const defaultReasoningEffort = modelRecordValue(prov.modelDefaultReasoningEfforts, model.id) ?? model.defaultReasoningEffort;
const supportsReasoningSummaries = configuredReasoningSummarySupport(prov, model.id);
const supportsVerbosity = configuredVerbositySupport(name, prov, model.id);
const fastPolicy = fastPolicyForModel(prov, model.id, name);
const supportsServiceTier = serviceTierSupportFromPolicy(fastPolicy);
const {
Expand Down Expand Up @@ -690,11 +706,11 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
: {}),
...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
...(typeof supportsReasoningSummaries === "boolean" ? { supportsReasoningSummaries } : {}),
...(typeof supportsVerbosity === "boolean" ? { supportsVerbosity } : {}),
...(typeof supportsServiceTier === "boolean" ? { supportsServiceTier } : {}),
...(supportsServiceTier === true && fastPolicy.fastTierDescription !== undefined
? { fastTierDescription: fastPolicy.fastTierDescription }
: {}),
...(prov.adapter === "kiro" ? { supportsVerbosity: false } : {}),
// Default-on for openai-chat providers (explicit false opts out); other adapters
// advertise only on explicit opt-in.
...(prov.parallelToolCalls === true || (prov.adapter === "openai-chat" && prov.parallelToolCalls !== false)
Expand Down
11 changes: 11 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1127,6 +1127,17 @@ const configSchema = z.object({
message: reasoningSummariesError,
});
}
const verbositySupportError = booleanRecordConfigError(
(provider as { modelSupportsVerbosity?: unknown }).modelSupportsVerbosity,
"modelSupportsVerbosity",
);
Comment on lines +1130 to +1133

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 Validate verbosity overrides at the management write boundary

Although file loading now validates modelSupportsVerbosity, providerManagementConfigError does not validate the new field, so POST /api/providers accepts and persists values such as an array or {model: "false"}. The next restart rejects that same config under the validation added here and falls back instead of loading the user's providers. Mirror the booleanRecordConfigError check at the management persistence boundary, as is already done for the adjacent reasoning-summary map.

AGENTS.md reference: src/AGENTS.md:L20-L20

Useful? React with 👍 / 👎.

if (verbositySupportError) {
ctx.addIssue({
code: "custom",
path: ["providers", redactSecretString(name), "modelSupportsVerbosity"],
message: verbositySupportError,
});
}
const serviceTierModelsError = booleanRecordConfigError(
(provider as { modelSupportsServiceTier?: unknown }).modelSupportsServiceTier,
"modelSupportsServiceTier",
Expand Down
35 changes: 34 additions & 1 deletion src/providers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,18 @@ export interface ProviderRegistryEntry {
preserveResponsesReasoningContent?: boolean;
/** Registry defaults for per-model Codex reasoning propagation; explicit user keys win during enrichment. */
modelSupportsReasoningSummaries?: Record<string, boolean>;
/** Registry defaults for per-model Codex Responses verbosity support. */
modelSupportsVerbosity?: Record<string, boolean>;
/**
* Registry default applied to EVERY model of this provider, including ids that arrive from
* live discovery after this table was written.
*
* `modelSupportsVerbosity` only covers the ids enumerated here, so a newly discovered model
* fell through and re-advertised a control the upstream accepts and ignores. Where the opt-out
* is a property of the provider's API rather than of one model, declare it here; a per-model
* entry still wins over it.
*/
supportsVerbosity?: boolean;
modelDiscovery?: ProviderModelDiscoverySpec;
contextWindow?: number;
modelContextWindows?: Record<string, number>;
Expand Down Expand Up @@ -416,6 +428,15 @@ const OPENAI_DAYBREAK_REASONING_EFFORTS: Record<string, string[]> = Object.fromE
OPENAI_DAYBREAK_MODELS.map(id => [id, [] as string[]]),
);
const OPENROUTER_GPT56_MODELS = OPENAI_GPT56_MODELS.map(id => `openai/${id}`);
const XAI_MODELS = [
"grok-4.6",
"grok-4.5",
"grok-4.3",
"grok-4.20-0309-reasoning",
"grok-4.20-0309-non-reasoning",
"grok-build-0.1",
"grok-composer-2.5-fast",
];
// OpenRouter's live /endpoints routes report 1,050,000; keep this separate from the
// unverified OpenAI API-key seed. Evidence: devlog/_plan/260710_provider_hardening/003_research_aggregators.md.
const OPENROUTER_GPT56_CONTEXT_WINDOW = 1_050_000;
Expand Down Expand Up @@ -1067,7 +1088,18 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
// transport returns 400 ("Multi Agent requests are not allowed on chat completions").
// 260813: grok-4.6 added per docs.x.ai/developers/grok-4-6. Context/vision still match
// grok-4.5; the reasoning ladder does not — 4.6 adds the documented xhigh rung.
models: ["grok-4.6", "grok-4.5", "grok-4.3", "grok-4.20-0309-reasoning", "grok-4.20-0309-non-reasoning", "grok-build-0.1", "grok-composer-2.5-fast"],
models: XAI_MODELS,
// Measured only on grok-4.6 against cli-chat-proxy.grok.com: even an invalid
// `text.verbosity` value is accepted and low/high/omitted output length is non-monotonic.
// Apply the resulting opt-out to the whole xAI lineup because `text.verbosity` is an OpenAI
// Responses parameter absent from xAI's documented API, not because every model was probed.
// Keep this separate from reasoning-summary support: that bit gates Codex's
// entire Responses reasoning object, including reasoning.effort.
modelSupportsVerbosity: Object.fromEntries(XAI_MODELS.map(id => [id, false])),
// Provider-wide, not merely per-model: `text.verbosity` is an OpenAI Responses parameter
// absent from xAI's documented API, so a model discovered later has no more support for it
// than the seeded ones do.
supportsVerbosity: false,
defaultModel: "grok-4.5",
// Keep Codex Responses callers on the compatibility Chat wire until xAI can replay
// opaque reasoning continuation and compaction state across later turns. The scoped
Expand Down Expand Up @@ -1237,6 +1269,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
// Per-model context metadata is maintained next to the Kiro model list.
modelContextWindows: KIRO_MODEL_CONTEXT_WINDOWS,
modelReasoningEfforts: KIRO_MODEL_REASONING_EFFORTS,
modelSupportsVerbosity: Object.fromEntries(KIRO_MODELS.map(id => [id, false])),
},
{
// Nous Portal — Nous Research subscription gateway (same backend Hermes Agent
Expand Down
6 changes: 6 additions & 0 deletions src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export function knownModelIdsForProvider(
registry?.modelReasoningEffortMap,
registry?.modelMaxOutputTokens,
registry?.modelSupportsServiceTier,
registry?.modelSupportsVerbosity,
]) {
for (const id of Object.keys(map ?? {})) ids.add(id);
}
Expand Down Expand Up @@ -312,6 +313,10 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
: undefined,
provider.modelSupportsServiceTier,
);
const modelSupportsVerbosity = mergeRecordFill(
registryEntry.modelSupportsVerbosity,
provider.modelSupportsVerbosity,
);
const noVisionModels = mergeStringArray(registryEntry.noVisionModels, provider.noVisionModels);
const noReasoningModels = mergeStringArray(registryEntry.noReasoningModels, provider.noReasoningModels);
const noTemperatureModels = mergeStringArray(registryEntry.noTemperatureModels, provider.noTemperatureModels);
Expand Down Expand Up @@ -426,6 +431,7 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
...(modelMaxInputTokens ? { modelMaxInputTokens } : {}),
...(modelMaxOutputTokens ? { modelMaxOutputTokens } : {}),
...(modelSupportsServiceTier ? { modelSupportsServiceTier } : {}),
...(modelSupportsVerbosity ? { modelSupportsVerbosity } : {}),
...(modelReasoningEfforts ? { modelReasoningEfforts } : {}),
...(modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts } : {}),
...(reasoningEffortMap ? { reasoningEffortMap } : {}),
Expand Down
6 changes: 6 additions & 0 deletions src/types/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,12 @@ export interface OcxProviderConfig {
* Responses backend rejects Codex summary-delivery fields for that model.
*/
modelSupportsReasoningSummaries?: Record<string, boolean>;
/**
* Model-specific Codex Responses verbosity capability. Set false when the upstream ignores
* `text.verbosity`; the catalog hides the no-op picker and the Responses adapter strips stale
* or caller-supplied values while preserving other `text` fields.
*/
modelSupportsVerbosity?: Record<string, boolean>;
/**
* Per-model wire value for Responses `stream_options.reasoning_summary_delivery`.
* Presence also advertises reasoning-summary support for that routed model.
Expand Down
74 changes: 74 additions & 0 deletions tests/codex-catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4391,6 +4391,63 @@ describe("Codex catalog routed normalization", () => {
expect(routed?.default_reasoning_summary).toBe("none");
});

test("xAI and Kiro routed rows disable verbosity without changing other providers", async () => {
const models = await gatherRoutedModels({
providers: {
xai: {
adapter: "openai-chat",
baseUrl: "https://api.x.ai/v1",
authMode: "oauth",
liveModels: false,
models: ["grok-4.6"],
},
kiro: {
adapter: "kiro",
baseUrl: "https://runtime.us-east-1.kiro.dev",
authMode: "oauth",
liveModels: false,
models: ["gpt-5.6-sol"],
},
plain: {
adapter: "openai-responses",
baseUrl: "https://plain.example.test/v1",
authMode: "key",
liveModels: false,
models: ["plain-model"],
},
},
});
const entries = buildCatalogEntries(null, [], models);

expect(models.find(model => model.provider === "xai" && model.id === "grok-4.6")?.supportsVerbosity).toBe(false);
expect(entries.find(entry => entry.slug === "xai/grok-4.6")?.support_verbosity).toBe(false);
expect(models.find(model => model.provider === "kiro" && model.id === "gpt-5.6-sol")?.supportsVerbosity).toBe(false);
expect(entries.find(entry => entry.slug === "kiro/gpt-5.6-sol")?.support_verbosity).toBe(false);
expect(models.find(model => model.provider === "plain" && model.id === "plain-model")?.supportsVerbosity).toBeUndefined();
expect(entries.find(entry => entry.slug === "plain/plain-model")?.support_verbosity).toBe(true);
});

test("a live-discovered xAI id inherits the provider-wide verbosity opt-out", async () => {
// modelSupportsVerbosity only enumerates the ids present when the registry row was written.
// A model that arrives later from live discovery used to fall through and re-advertise a
// control xAI accepts and ignores.
const models = await gatherRoutedModels({
providers: {
xai: {
adapter: "openai-chat",
baseUrl: "https://api.x.ai/v1",
authMode: "oauth",
liveModels: false,
models: ["grok-9.9-not-in-the-registry"],
},
},
});
const entries = buildCatalogEntries(null, [], models);

expect(models.find(model => model.provider === "xai")?.supportsVerbosity).toBe(false);
expect(entries.find(entry => entry.slug === "xai/grok-9.9-not-in-the-registry")?.support_verbosity).toBe(false);
});

test("a routed model never inherits the native template's context window (#992)", () => {
// /models returns only the id: the routed entry must fall to the
// conservative 128k triple, never the native template's larger window.
Expand Down Expand Up @@ -4700,6 +4757,23 @@ describe("Codex catalog routed normalization", () => {
};
enrichProviderFromCatalog("deepseek", submitted);
expect(submitted.modelSupportsReasoningSummaries).toEqual({ "deepseek-v4-flash": false });

const xai: OcxConfig["providers"][string] = {
adapter: "openai-chat",
baseUrl: "https://api.x.ai/v1",
authMode: "key",
};
enrichProviderFromCatalog("xai", xai);
expect(xai.modelSupportsVerbosity).toBeUndefined();

const submittedVerbosity: OcxConfig["providers"][string] = {
adapter: "openai-chat",
baseUrl: "https://api.x.ai/v1",
authMode: "key",
modelSupportsVerbosity: { "grok-4.6": true },
};
enrichProviderFromCatalog("xai", submittedVerbosity);
expect(submittedVerbosity.modelSupportsVerbosity).toEqual({ "grok-4.6": true });
});

test("explicit per-model overrides survive registry backfill", () => {
Expand Down
31 changes: 31 additions & 0 deletions tests/codex-tool-mode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,34 @@ describe("Codex tool mode configuration (#2106)", () => {
});



describe("#2503 combo derivation preserves a member's verbosity opt-out", () => {
const { deriveComboCatalogModel } = require("../src/codex/catalog/aggregation");
const combo = {
name: "mixed-combo",
targets: [
{ provider: "xai", model: "grok-4.6" },
{ provider: "openai", model: "gpt-5.6-sol" },
],
};

test("one member that cannot honour text.verbosity makes the combo advertise false", () => {
// A combo is only as capable as its least capable member: routing a turn to the xAI member
// would re-advertise a control the upstream accepts and ignores. Same conservative rule
// supportsReasoningSummaries already uses.
const derived = deriveComboCatalogModel("mixed-combo", combo, [
{ id: "grok-4.6", provider: "xai", contextWindow: 256000, supportsVerbosity: false },
{ id: "gpt-5.6-sol", provider: "openai", contextWindow: 272000 },
]);
expect(derived?.supportsVerbosity).toBe(false);
});

test("a combo whose members all support verbosity does not force the flag", () => {
const derived = deriveComboCatalogModel("mixed-combo", combo, [
{ id: "a", provider: "openai", contextWindow: 272000 },
{ id: "b", provider: "openai", contextWindow: 272000, supportsVerbosity: true },
]);
expect(derived?.supportsVerbosity).toBeUndefined();
});
});

Loading
Loading