Skip to content
Closed
10 changes: 10 additions & 0 deletions src/adapters/base.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { AdapterEvent, OcxParsedRequest } from "../types";
import type { TranslatorBudget } from "../lib/translator-budget";
import type { AdapterTierMetadata } from "../providers/fastwire";

/** Metadata about the caller's incoming request, for auth-forwarding adapters. */
export interface IncomingMeta {
Expand Down Expand Up @@ -39,6 +40,9 @@ export interface ProviderAdapter {
incoming: IncomingMeta,
emit: (event: AdapterEvent) => void,
): Promise<void>;

/** Exact no-field observation for runTurn adapters, which expose no AdapterRequest object. */
tierLogForRunTurn?(parsed: OcxParsedRequest): AdapterTierMetadata | undefined;
}

export interface AdapterRequest {
Expand Down Expand Up @@ -67,6 +71,12 @@ export interface AdapterRequest {
wireField: "reasoning_effort" | "reasoning.effort" | "thinking.type";
wireValue: string;
};
/**
* Exact tier outcome seeded after this adapter serialized the outbound request.
* This is a live shared observer: response-phase methods mutate `outcome`, so retain
* the reference rather than cloning or snapshotting it.
*/
tierLog?: AdapterTierMetadata;
usageLog?: {
inputTokens?: number;
estimated?: boolean;
Expand Down
11 changes: 11 additions & 0 deletions src/adapters/openai-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import { peekReasoningForCall } from "../responses/reasoning-replay-cache";
import { buildNonOpenAIToolCatalogNudgeForTools, shouldInjectNonOpenAIToolCatalogNudge } from "./tool-catalog-nudge";
import { openRouterProviderPayload, resolveOpenRouterRouting } from "../providers/openrouter-routing";
import { canSerializeServiceTierForChatModel } from "../providers/service-tier";
import {
createAdapterTierMetadata,
} from "../providers/fastwire";
import { openaiChatCompletionsUrl } from "./openai-chat-url";
import { stripResponsesOnlyEncryptedMarker } from "./responses-tool-schema";
import {
Expand Down Expand Up @@ -1430,6 +1433,13 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
if (parsed.stream) body.stream_options = { include_usage: true };

const bodyJson = JSON.stringify(body);
const actualServiceTier = typeof body.service_tier === "string" ? body.service_tier : null;
const tierLog = createAdapterTierMetadata(
parsed.options.tierObservation,
parsed.options.tierDecision,
actualServiceTier === null ? null : "service-tier",
actualServiceTier,
);
if (isDebugEnabled()) {
let host = "upstream";
try { host = new URL(url).host; } catch { /* keep fallback */ }
Expand All @@ -1450,6 +1460,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
headers,
body: bodyJson,
...(reasoningLog ? { reasoningLog } : {}),
...(tierLog ? { tierLog } : {}),
};
},

Expand Down
36 changes: 31 additions & 5 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 All @@ -12,6 +12,9 @@ import { modelRecordValue } from "../reasoning-effort";
import type { TranslatorBudget } from "../lib/translator-budget";
import { rewriteRoutedCustomToolsForUpstream } from "../responses/custom-tool-compat";
import { openaiResponsesUrl } from "./openai-responses-url";
import {
createAdapterTierMetadata,
} from "../providers/fastwire";

// Headers relayed verbatim from the caller in OAuth-passthrough ("forward") mode.
// Exported so the web-search sidecar reuses the exact same forwarded-auth set for its ChatGPT call.
Expand Down Expand Up @@ -764,6 +767,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 +790,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 +1379,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 Expand Up @@ -1414,11 +1429,21 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
convertedRoutedCustomToolNames = rewritten.names;
}
const sanitizedBody = normalizeToolSchemas(stripSparkCompatibility(stripUnsupportedReasoningParams(stripItemIdsWhenUnstored(stripInvalidItemIds(stripUnsupportedHostedTools(sanitizeReasoningInputContent(scrubOcxCompactionItems(outBody), { preserveRawReasoningContent: provider.preserveResponsesReasoningContent === true })))))));
const body = JSON.stringify(stripDisabledReasoningSummaries(
const finalBody = stripDisabledReasoningSummaries(
normalizeConfiguredReasoningSummaryDelivery(sanitizedBody, provider, parsed.modelId),
provider,
parsed.modelId,
));
);
const actualServiceTier = isPlainObject(finalBody) && typeof finalBody.service_tier === "string"
? finalBody.service_tier
: null;
const tierLog = createAdapterTierMetadata(
parsed.options?.tierObservation,
parsed.options?.tierDecision,
actualServiceTier === null ? null : "service-tier",
actualServiceTier,
);
const body = JSON.stringify(finalBody);
const releaseBodyObservation = translatorBudget.observeExternallyCapped(
"passthrough_serialization",
new TextEncoder().encode(body).byteLength,
Expand All @@ -1430,6 +1455,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
body,
releaseBodyObservation,
...(convertedRoutedCustomToolNames ? { convertedRoutedCustomToolNames } : {}),
...(tierLog ? { tierLog } : {}),
};
},

Expand Down
31 changes: 30 additions & 1 deletion src/adapters/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { createMimoFreeAdapter } from "./mimo-free";
import { createOpenAIChatAdapter } from "./openai-chat";
import { createResponsesPassthroughAdapter } from "./openai-responses";
import type { OcxProviderConfig } from "../types";
import { createAdapterTierMetadata } from "../providers/fastwire";

export type AdapterCacheRetention = "none" | "short" | "long";

Expand Down Expand Up @@ -142,5 +143,33 @@ export function createRegisteredAdapter(
): ProviderAdapter {
const definition = getAdapterDefinition(provider.adapter);
if (!definition) throw new Error(`Unknown adapter: ${provider.adapter}`);
return definition.create(provider, context);
const adapter = definition.create(provider, context);
const buildRequest = adapter.buildRequest.bind(adapter);
adapter.buildRequest = (parsed, incoming) => {
const attachTierMetadata = (request: Awaited<ReturnType<ProviderAdapter["buildRequest"]>>) => {
// OpenAI-family adapters report the exact emitted field themselves. Other adapters
// still report an exact absence at this serialization boundary, which makes a routed
// Fast downgrade observable without asking core to infer an outbound body shape.
request.tierLog ??= createAdapterTierMetadata(
parsed.options.tierObservation,
parsed.options.tierDecision,
null,
null,
);
return request;
};
const request = buildRequest(parsed, incoming);
return request instanceof Promise
? request.then(attachTierMetadata)
: attachTierMetadata(request);
};
if (adapter.runTurn && !adapter.tierLogForRunTurn) {
adapter.tierLogForRunTurn = parsed => createAdapterTierMetadata(
parsed.options.tierObservation,
parsed.options.tierDecision,
null,
null,
);
}
return adapter;
}
16 changes: 8 additions & 8 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, registerAntigravityDiscoveredWireModels } 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,12 +408,12 @@ function captureProviderGather(
const enriched = detachedClone(withCanonicalOpenAiForwardAuthDefault(name, configured));
enrichProviderFromRegistry(name, enriched);
const registryTransportMatch = providerMatchesRegistryTransport(name, enriched);
const serviceTierAdapterAuthority = captureServiceTierAdapterAuthority(
const provider = recursivelyFreeze(enriched);
const fastPolicyAuthority = captureFastPolicyAuthority(
name,
enriched,
provider,
registryTransportMatch,
);
const provider = recursivelyFreeze(enriched);
const observedAuth = authResolver.kind === "observed"
&& provider.authMode !== "forward"
&& provider.liveModels !== false
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
Loading
Loading