Skip to content
Closed
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
2 changes: 1 addition & 1 deletion src/adapters/openai-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1696,7 +1696,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
if (!isCanonicalOpenAiForwardProvider(provider)) {
outBody = promoteClientLoadedTools(outBody);
}
if (provider.authMode !== "forward") {
if (!isCanonicalOpenAiForwardProvider(provider)) {
const rewritten = rewriteRoutedCustomToolsForUpstream(outBody);
outBody = rewritten.body;
convertedRoutedCustomToolNames = rewritten.names;
Expand Down
33 changes: 31 additions & 2 deletions src/providers/openai-tiers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { CodexAccountMode, OcxConfig, OcxProviderConfig, ProviderCostOverlay } from "../types";
import { OPENAI_PROVIDER_TIER_VERSION } from "../types";
import { openaiResponsesUrl } from "../adapters/openai-responses-url";
import { MAX_COST4_RATE } from "../usage/expected-prices";

export const OPENAI_CODEX_PROVIDER_ID = "openai";
Expand Down Expand Up @@ -36,7 +37,35 @@ export function isCanonicalOpenAiForwardProvider(provider: OcxProviderConfig): b
&& normalizedBaseUrl(provider.baseUrl) === CODEX_FORWARD_BASE_URL;
}

const OPENAI_API_BASE_URL = "https://api.openai.com/v1";
const OPENAI_API_ORIGIN = "https://api.openai.com";
const OPENAI_API_BASE_URL = `${OPENAI_API_ORIGIN}/v1`;
const OPENAI_API_RESPONSES_URL = `${OPENAI_API_BASE_URL}/responses`;

/**
* The Responses endpoint the adapter would actually POST key-auth traffic to, normalized.
*
* Mirrors the adapter's own construction (`src/adapters/openai-responses.ts`): a configured
* `responsesPath` is appended to the base verbatim, and only the default branch runs the
* `/v1/responses` suffix normalization. Classifying on the base URL alone would call
* `baseUrl: "https://api.openai.com"` with `responsesPath: "/other"` official even though that
* request never reaches the official Responses endpoint.
*/
function resolvedResponsesEndpoint(provider: OcxProviderConfig): string | undefined {
try {
const raw = provider.responsesPath === undefined
? openaiResponsesUrl(provider.baseUrl)
: `${provider.baseUrl.replace(/\/$/, "")}${provider.responsesPath}`;
return normalizedBaseUrl(raw);
} catch {
return undefined;
}
}

function isOfficialOpenAiResponsesDestination(provider: OcxProviderConfig): boolean {
// Exact normalized URL keeps lookalike/suffix hosts out of this set: `api.openai.com.evil.test`
// resolves to its own origin, never to the official one.
return resolvedResponsesEndpoint(provider) === OPENAI_API_RESPONSES_URL;
}

/**
* Whether this provider can serve `POST /responses/compact`. The canonical ChatGPT
Expand Down Expand Up @@ -65,7 +94,7 @@ export function supportsNativeResponsesCompactEndpoint(
export function isOpenAiOperatedResponsesDestination(provider: OcxProviderConfig): boolean {
if (isCanonicalOpenAiForwardProvider(provider)) return true;
return provider.adapter === "openai-responses"
&& normalizedBaseUrl(provider.baseUrl) === OPENAI_API_BASE_URL;
&& isOfficialOpenAiResponsesDestination(provider);
}

/**
Expand Down
42 changes: 30 additions & 12 deletions src/responses/reasoning-replay-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,32 +133,51 @@ function sweepExpiredServingIdentities(at: number): void {
}
}

function servingIdentityFor(
scope: OcxReasoningReplayScopeRef | undefined,
): { threadId: string; identity: string } | undefined {
const threadId = scope?.clientThreadId;
const identityTuple = tupleForServingIdentity(scope?.current);
if (!nonEmpty(threadId) || !identityTuple) return undefined;
return { threadId, identity: JSON.stringify(identityTuple) };
}

/**
* Compare this request's route with the last route recorded for its client thread, then
* record the current route. A live mismatch means replayed opaque reasoning was minted by
* another backend and must not be forwarded to this one.
* Compare this request's route with the last successfully serving route for its client thread.
* A live mismatch means replayed opaque reasoning was minted by another backend and must not be
* forwarded to this one. Comparison deliberately does not refresh or replace the recorded route:
* a failed candidate request did not serve the thread.
*
* Serving provenance uses restart-stable destination and credential dimensions so token
* generations and other volatile credential material cannot create false route changes. Missing
* durable identity, expired, or evicted state is deliberately unknown rather than a mismatch.
* This store is process-local, so a backend switch spanning a proxy restart is not detected.
*/
export function updateReasoningReplayServingIdentity(
export function reasoningReplayServingIdentityChanged(
scope: OcxReasoningReplayScopeRef | undefined,
): boolean {
const threadId = scope?.clientThreadId;
const identityTuple = tupleForServingIdentity(scope?.current);
if (!nonEmpty(threadId) || !identityTuple) return false;
const identity = JSON.stringify(identityTuple);
const current = servingIdentityFor(scope);
if (!current) return false;
const at = now();
sweepExpiredServingIdentities(at);
const previous = servingIdentities.get(current.threadId);
return previous !== undefined && previous.identity !== current.identity;
}

/** Record the route only after it has successfully served the client thread. */
export function commitReasoningReplayServingIdentity(
scope: OcxReasoningReplayScopeRef | undefined,
): void {
const current = servingIdentityFor(scope);
if (!current) return;
const at = now();
sweepExpiredServingIdentities(at);
const previous = servingIdentities.get(threadId);
const changed = previous !== undefined && previous.identity !== identity;
const previous = servingIdentities.get(current.threadId);
const { threadId, identity } = current;
const bytes = Buffer.byteLength(JSON.stringify([threadId, identity]), "utf8");
if (bytes > MAX_TOTAL_BYTES) {
deleteServingIdentity(threadId);
return false;
return;
}

if (previous) deleteServingIdentity(threadId);
Expand All @@ -179,7 +198,6 @@ export function updateReasoningReplayServingIdentity(
if (oldestThreadId === undefined) break;
deleteServingIdentity(oldestThreadId);
}
return changed;
}

function processLocalIdentity(domain: string, material: string): string {
Expand Down
Loading
Loading