Skip to content
Open
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
18 changes: 15 additions & 3 deletions src/adapters/openai-responses.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createHash } from "node:crypto";
import type { IncomingMeta, ProviderAdapter } from "./base";
import { namespacedToolName, type AdapterEvent, type OcxParsedRequest, type OcxProviderConfig, type OcxUsage } from "../types";
import { namespacedToolName, type AdapterEvent, type OcxParsedRequest, type OcxProviderConfig, type OcxUsage, type TierDecision } from "../types";
import { catalogModelSupportsReasoningSummaries } from "../codex/catalog";
import { COMPACT_PROMPT, decodeCompactionSummary, SUMMARY_PREFIX } from "../responses/compaction";
import { collectResponsesToolGroups } from "../responses/tool-groups";
Expand Down Expand Up @@ -764,6 +764,15 @@ function stripPreviousResponseId(body: unknown, strip: boolean): unknown {
return rest;
}

/** Apply the settled tier only to a fresh outbound object; `_rawBody` remains caller-owned. */
function applyTierDecisionToResponsesBody(body: unknown, decision: TierDecision | undefined): unknown {
if (!decision || decision.kind === "forward-caller" || !isPlainObject(body)) return body;
const next: Record<string, unknown> = { ...body };
if (decision.kind === "set") next.service_tier = decision.value;
else delete next.service_tier;
return next;
}

/**
* Drop request parameters a stateless Responses upstream cannot implement, and pin
* `store` false.
Expand All @@ -778,8 +787,8 @@ function stripPreviousResponseId(body: unknown, strip: boolean): unknown {
* `prompt` is a reference to a server-stored prompt template — the most stateful
* field in the accepted schema.
*
* `service_tier` is deliberately NOT dropped: the server writes it for fast mode
* (`responses/core.ts`), and silently deleting a configured knob inside an adapter is
* `service_tier` is deliberately NOT dropped: the final TierDecision is applied to a
* detached outbound body before this sanitizer chain, and silently deleting a configured knob is
* worse than forwarding a parameter the upstream ignores.
*
* MUST run before the composed sanitize chain below: `stripItemIdsWhenUnstored` keys
Expand Down Expand Up @@ -1367,6 +1376,9 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
parsed._rawBody,
forward || parsed._previousResponseInputExpanded === true,
);
// stripPreviousResponseId() intentionally returns its input on a no-op. Detach before the
// tier write so a force-fast/default decision can never mutate parsed._rawBody.
outBody = applyTierDecisionToResponsesBody(outBody, parsed.options?.tierDecision);
const stateless = provider.statelessResponses === true;
if (stateless) outBody = stripStatefulResponsesParams(outBody);
// A replay miss can leave a function_call_output whose paired function_call sat
Expand Down
12 changes: 6 additions & 6 deletions src/codex/catalog/provider-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, mo
import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider } from "../../generated/model-metadata";
import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
import {
captureServiceTierAdapterAuthority,
captureFastPolicyAuthority,
serviceTierSupportForModel,
type CapturedServiceTierAdapterAuthority,
} from "../../providers/service-tier";
import type { FastPolicyAuthority } from "../../providers/fastwire";
import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport } from "../../providers/registry";
import { parseAntigravityAvailableModels } from "../../providers/antigravity-models";
import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
Expand Down Expand Up @@ -155,7 +155,7 @@ interface CapturedProviderGather {
readonly discovery: ResolvedProviderModelDiscovery;
readonly policy: CatalogProviderDiscoveryPolicySnapshot;
readonly request: CapturedModelsRequest;
readonly serviceTierAdapterAuthority: CapturedServiceTierAdapterAuthority;
readonly fastPolicyAuthority: FastPolicyAuthority;
readonly observedAuth?: ModelsAuthResolution;
/**
* Configured model ids this provider must keep even when live discovery omits
Expand Down Expand Up @@ -408,7 +408,7 @@ function captureProviderGather(
const enriched = detachedClone(withCanonicalOpenAiForwardAuthDefault(name, configured));
enrichProviderFromRegistry(name, enriched);
const registryTransportMatch = providerMatchesRegistryTransport(name, enriched);
const serviceTierAdapterAuthority = captureServiceTierAdapterAuthority(
const fastPolicyAuthority = captureFastPolicyAuthority(
name,
enriched,
registryTransportMatch,
Expand Down Expand Up @@ -449,7 +449,7 @@ function captureProviderGather(
discovery,
policy,
request,
serviceTierAdapterAuthority,
fastPolicyAuthority,
...(observedAuth ? { observedAuth: Object.freeze({ ...observedAuth }) } : {}),
...(retainConfiguredModelIds && retainConfiguredModelIds.size > 0
? { retainConfiguredModelIds }
Expand Down Expand Up @@ -518,7 +518,7 @@ function captureGatherFlight(
// It is the one member of a provider row that is legitimately a function,
// so it is dropped here rather than allowed to break every encode.
provider: omitProviderTransportExecutor(provider.provider),
serviceTierAdapterAuthority: provider.serviceTierAdapterAuthority,
fastPolicyAuthority: provider.fastPolicyAuthority,
// Combo retention is capture-time state, not a provider-row field. Two
// gathers that share providers but differ in combo targets must not join.
retainConfiguredModelIds: [...(provider.retainConfiguredModelIds ?? [])].sort(),
Expand Down
87 changes: 84 additions & 3 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,11 @@ import {
type OcxConfig,
type OcxApiKeyEntry,
type OcxProviderConfig,
type FastWire,
type ProviderCostOverlay,
} from "./types";
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "./providers/openai-tiers";
import { fastWireDeclarationError, hasFastWireCapabilityConflict } from "./providers/fastwire";
import {
getProviderRegistryEntry,
providerMatchesRegistryTransport,
Expand Down Expand Up @@ -662,12 +664,14 @@ function resolveRuntimePortPath(): string {
}

const warnedConfigFallbacks = new Set<string>();
const warnedInheritedFastWireConflicts = new Set<string>();
let lastWarningReconciledGeneration = 0;

export function reconcileConfigWarningMemos(generation: number): number {
if (generation <= lastWarningReconciledGeneration) return 0;
const removed = warnedConfigFallbacks.size;
const removed = warnedConfigFallbacks.size + warnedInheritedFastWireConflicts.size;
warnedConfigFallbacks.clear();
warnedInheritedFastWireConflicts.clear();
lastWarningReconciledGeneration = generation;
return removed;
}
Expand Down Expand Up @@ -716,6 +720,16 @@ export function requestPacingConfigError(value: unknown): string | null {
return "requestPacing must contain enabled and a valid requestsPerMinute/minIntervalMs provider rule or model overrides";
}

const fastWireSchema = z.object({
kind: z.string(),
canonicalToWire: z.record(z.string().trim(), z.string().trim()),
foreignCallerTiers: z.string(),
betas: z.array(z.string().trim()).optional(),
}).strict().superRefine((fastWire, ctx) => {
const error = fastWireDeclarationError({ fastWire });
if (error) ctx.addIssue({ code: "custom", message: error });
}).transform(fastWire => fastWire as FastWire);

/**
* Zod schema for one provider entry: known fields are validated strictly while unknown
* fields pass through (preserved for runtime extensions).
Expand All @@ -731,6 +745,7 @@ const providerConfigSchema = z.object({
responsesPath: z.string().min(1).optional(),
statelessResponses: z.boolean().optional(),
requiresAdjacentResponsesToolResults: z.boolean().optional(),
fastWire: fastWireSchema.nullable().optional(),
supportsServiceTier: z.boolean().optional(),
modelSupportsServiceTier: z.record(z.string().min(1), z.boolean()).optional(),
preserveResponsesReasoningContent: z.boolean().optional(),
Expand All @@ -753,7 +768,15 @@ const providerConfigSchema = z.object({
repairInvalidIds: z.boolean().optional(),
}).strict().optional(),
responsesSnapshotRepair: z.boolean().optional(),
}).passthrough();
}).passthrough().superRefine((provider, ctx) => {
if (hasFastWireCapabilityConflict(provider)) {
ctx.addIssue({
code: "custom",
path: ["fastWire"],
message: "fastWire=null conflicts with supportsServiceTier=true",
});
}
});
Comment thread
olddonkey marked this conversation as resolved.

const RESERVED_PROVIDER_NAMES = new Set([
// JavaScript prototype-pollution guards.
Expand Down Expand Up @@ -2153,6 +2176,50 @@ function warnDegradedNativeSubagentConfig(rawParsed: unknown, config: OcxConfig)
}
}

/**
* Registry metadata can gain service-tier capability after a config was written. An explicit
* `fastWire: null` remains authoritative on load; rejecting the file would discard unrelated
* providers and API keys. Live writes remain strict through validateConfigCandidate().
*/
function inheritedFastWireConflictProviderNames(
config: Pick<OcxConfig, "providers">,
): string[] {
const conflicts: string[] = [];
for (const [name, provider] of Object.entries(config.providers)) {
if (provider.fastWire !== null || provider.supportsServiceTier === false) continue;
const registry = providerMatchesRegistryTransport(name, provider)
? getProviderRegistryEntry(name)
: undefined;
if (!registry) continue;
const effectiveProviderCapability = provider.supportsServiceTier ?? registry.supportsServiceTier;
const effectiveModelCapabilities = {
...(registry.modelSupportsServiceTier ?? {}),
...(provider.modelSupportsServiceTier ?? {}),
};
if (
effectiveProviderCapability === true
|| Object.values(effectiveModelCapabilities).some(value => value === true)
) {
conflicts.push(name);
}
}
return conflicts;
}

function inheritedFastWireConflictWarning(name: string): string {
return `providers.${redactSecretString(name)}.fastWire=null overrides service-tier capability inherited from the matching registry entry`;
}

function warnInheritedFastWireConflicts(configPath: string, config: OcxConfig): void {
const names = inheritedFastWireConflictProviderNames(config);
if (names.length === 0 || warnedInheritedFastWireConflicts.has(configPath)) return;
warnedInheritedFastWireConflicts.add(configPath);
console.warn(
`⚠️ config.json ${names.map(inheritedFastWireConflictWarning).join("; ")}. `
+ "The persisted providers and API keys were preserved.",
);
}

/**
* Load and validate config.json into an OcxConfig. Missing files reset to
* defaults and clear stale overlays. Broken existing files also fall back to
Expand All @@ -2177,6 +2244,7 @@ export function loadConfig(): OcxConfig {
const result = configSchema.safeParse(parsed);
if (result.success) {
const config = normalizeApiKeyIds(result.data as OcxConfig);
warnInheritedFastWireConflicts(configPath, config);
warnDegradedStreamMode(parsed, config);
warnDegradedHostname(parsed, config);
warnDegradedApiKeys(parsed, config);
Expand All @@ -2201,6 +2269,7 @@ export function loadConfig(): OcxConfig {
if (retryResult.success) {
warnConfigRepaired(configPath, result.error);
const config = normalizeApiKeyIds(retryResult.data as OcxConfig);
warnInheritedFastWireConflicts(configPath, config);
warnDegradedHostname(parsed, config);
warnDegradedApiKeys(parsed, config);
warnDegradedCodexAccountPriorities(parsed, config);
Expand All @@ -2220,6 +2289,7 @@ export function loadConfig(): OcxConfig {
{
warnDroppedConfigSections(configPath, salvaged.dropped, salvaged.issues);
const config = normalizeApiKeyIds(salvaged.parsed);
warnInheritedFastWireConflicts(configPath, config);
warnDegradedHostname(parsed, config);
warnDegradedApiKeys(parsed, config);
warnDegradedCodexAccountPriorities(parsed, config);
Expand Down Expand Up @@ -2279,6 +2349,7 @@ function validFileConfigDiagnostics(config: OcxConfig, rawParsed: unknown): Conf
const rawEffort = rawClaudeSubagentEffort(rawParsed);
const normalized = normalizeClaudeSubagentEffort(normalizeNativeSubagentSync(config, rawParsed), rawParsed);
const warnings = configPlaceholderWarnings(normalized);
warnings.push(...inheritedFastWireConflictProviderNames(normalized).map(inheritedFastWireConflictWarning));
warnings.push(...degradedCodexAccountPriorityWarnings(rawParsed, normalized));
if (rawEffort !== undefined && !isClaudeSubagentEffort(rawEffort)) {
warnings.push(`claudeCode.subagentEffort ignored: expected one of ${CLAUDE_SUBAGENT_EFFORTS.join(", ")}`);
Expand Down Expand Up @@ -2492,7 +2563,17 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
?? loopbackListenerPortError(value);
if (boundaryError) return { ok: false, error: boundaryError };
const result = configSchema.safeParse(value);
if (result.success) return { ok: true, config: normalizeApiKeyIds(result.data as OcxConfig) };
if (result.success) {
const config = normalizeApiKeyIds(result.data as OcxConfig);
const inheritedConflicts = inheritedFastWireConflictProviderNames(config);
if (inheritedConflicts.length > 0) {
return {
ok: false,
error: `schema_invalid: ${inheritedFastWireConflictWarning(inheritedConflicts[0]!)}`,
};
}
return { ok: true, config };
}
return { ok: false, error: schemaDiagnosticsError(result.error) };
}

Expand Down
7 changes: 7 additions & 0 deletions src/providers/derive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,13 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
}
// Registry-only metadata (never seeded into saved config): backfill straight from
// the entry so an explicit user value stays distinguishable from the default.
if (prov.fastWire === undefined && entry.fastWire !== undefined) {
prov.fastWire = entry.fastWire === null ? null : {
...entry.fastWire,
canonicalToWire: { ...entry.fastWire.canonicalToWire },
...(entry.fastWire.betas ? { betas: [...entry.fastWire.betas] } : {}),
};
}
if (prov.supportsServiceTier === undefined && entry.supportsServiceTier !== undefined) prov.supportsServiceTier = entry.supportsServiceTier;
if (prov.preserveResponsesReasoningContent === undefined && entry.preserveResponsesReasoningContent !== undefined) prov.preserveResponsesReasoningContent = entry.preserveResponsesReasoningContent;
applyReasoningSummaryDefaults(prov, entry.modelSupportsReasoningSummaries);
Expand Down
Loading
Loading