Skip to content
Draft
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
5 changes: 5 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ Providers can expose a built-in shorthand, such as `agy` for `google-antigravity
| `baseUrl` | `string` | Upstream API base URL. Most built-in fixed endpoints ignore a mismatch; collision-safe key presets preserve an older same-named custom destination. |
| `requestPacing?` | `{ enabled, requestsPerMinute?, minIntervalMs?, models? }` | Optional client-side outbound request-start pacing, separate from upstream usage, billing, and rate-limit indicators. RPM is converted to an even interval; `minIntervalMs` may impose a longer interval. Provider limits apply across all models, while `models` entries use exact upstream model IDs (for example `nvidia/llama-3.1-nemotron-ultra-253b-v1`) and can only add delay. Queue waits do not consume the upstream response-header timeout. HTTP, Responses WebSocket, and explicit adapter `fetchResponse`/`runTurn` dispatches are covered. |
| `upstreamHttpVersion?` | `"auto" \| "http1.1" \| "h1" \| "http2" \| "h2"` | Pin the HTTP version used for upstream requests to this provider. Defaults to `auto`, which lets Bun negotiate. An explicit pin requires an HTTPS target and fails locally when it cannot be honored. Set `http1.1` when a provider's HTTP/2 SSE stream stalls instead of delivering events — the symptom is a long-running streaming request that produces nothing and eventually times out. For Cursor, `http1.1`/`h1` selects its `RunSSE` + `BidiAppend` compatibility transport for inference and also pins live model discovery. Management `POST`/`PATCH` accept `null` to clear it back to `auto`. |
| `tlsProfile?` | `"antigravity-browser"` | Opt-in browser-compatible TLS transport for the canonical `google-antigravity` OAuth provider. It is accepted only with the Google adapter, Cloud Code Assist mode, and Google's canonical HTTPS Antigravity hosts. Redirects remain manual, configured proxy routing must be preserved or the request fails closed, and omission keeps the normal Bun transport without loading the optional TLS dependency. |
| `responsesPath?` | `string` | Relative resource path for key-auth `openai-responses` requests. It must start with `/` and contain no scheme, query, or fragment. |
| `allowEncryptedV2AgentTasks?` | `boolean` | Disabled by default. Trust a direct key-auth `openai-responses` provider to consume or relay opaque encrypted V2 sub-agent tasks unchanged. Eligible routes skip `agentTaskRecovery`; all other routes keep the existing recovery or fail-closed behavior. OpenCodex does not decrypt, translate, or recover tasks sent through this opt-in. |
| `upstreamWebsocket?` | `boolean` | Opt-in upstream Responses WebSocket transport for `openai-responses` requests (default false). When the upstream supports the Responses WebSocket protocol, streaming POST requests to the configured Responses path (default `/v1/responses`) are dialed as WSS over an HTTPS base URL and re-encoded to SSE for the usual pipeline. Forward providers use `{baseUrl}/responses`; key-auth providers use `responsesPath`, or the legacy `/v1/responses` fallback. This mirrors the canonical ChatGPT backend optimization for OpenAI-compatible gateways (for example sub2api) whose WebSocket ingress is measurably faster than its SSE queue. Plain HTTP remains on SSE; non-Responses paths and `openai-chat` requests stay on HTTP. |
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@
"@types/bun": "1.4.2",
"typescript": "7.0.2"
},
"optionalDependencies": {
"wreq-js": "2.3.1"
},
"overrides": {
"@hono/node-server": "2.1.0",
"fast-uri": "^3.1.7",
Expand Down
2 changes: 2 additions & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -984,6 +984,8 @@
"provider-model-discovery-contract.test.ts": "providers",
"provider-outbound-private-network.test.ts": "providers",
"provider-outbound.test.ts": "providers",
"provider-runtime-fetch.test.ts": "providers",
"provider-tls-profile.test.ts": "providers",
"provider-payload.test.ts": "gui",
"provider-quota-observed-marker.test.ts": "providers",
"provider-quota.test.ts": "providers",
Expand Down
10 changes: 10 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import { recordOwnedConfigPath } from "./lib/config-ownership";
import { assertNotRealHomeUnderTest } from "./lib/test-home-guard";
import { providerDestinationConfigError } from "./lib/destination-policy";
import { redactSecretString } from "./lib/redact";
import { providerTlsProfileConfigError } from "./lib/provider-tls-profile";
import { openRouterRoutingConfigError } from "./providers/openrouter-routing";
import { MODEL_ALIAS_PATTERN } from "./providers/default-aliases";
import { MODEL_DISCOVERY_MAX_MODELS } from "./providers/model-discovery-limits";
Expand Down Expand Up @@ -581,6 +582,7 @@ const providerConfigSchema = z.object({
modelPinnedReasoningEfforts: modelPinnedEffortsSchema.optional(),
adapter: z.string().min(1),
baseUrl: z.string().min(1),
tlsProfile: z.literal("antigravity-browser").optional(),
alias: z.string().optional(),
modelAliases: z.record(z.string(), z.string()).optional(),
modelDisplayNames: modelDisplayNamesSchema.optional(),
Expand Down Expand Up @@ -1420,6 +1422,14 @@ const configSchema = z.object({
message: responsesPathError,
});
}
const tlsProfileError = providerTlsProfileConfigError(name, provider);
if (tlsProfileError) {
ctx.addIssue({
code: "custom",
path: ["providers", redactSecretString(name), "tlsProfile"],
message: tlsProfileError,
});
}
const headersError = providerHeadersConfigError((provider as { headers?: unknown }).headers);
if (headersError) {
ctx.addIssue({
Expand Down
23 changes: 23 additions & 0 deletions src/lib/provider-runtime-fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { OcxProviderConfig } from "../types";

export const RUNTIME_PROVIDER_FETCH = Symbol("opencodex.provider.runtime-fetch");

export interface RuntimeProviderFetch {
providerName: string;
origins: readonly string[];
fetch: typeof globalThis.fetch;
}

/** Return only an explicitly injected executor matching the provider and exact destination origin. */
export function runtimeProviderFetch(
provider: OcxProviderConfig,
providerName: string | undefined,
): typeof globalThis.fetch | undefined {
const runtime = (provider as OcxProviderConfig & { [RUNTIME_PROVIDER_FETCH]?: RuntimeProviderFetch })[RUNTIME_PROVIDER_FETCH];
if (!runtime || runtime.providerName !== providerName) return undefined;
try {
return runtime.origins.includes(new URL(provider.baseUrl).origin) ? runtime.fetch : undefined;
} catch {
return undefined;
}
}
136 changes: 136 additions & 0 deletions src/lib/provider-tls-profile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import type { OcxProviderConfig } from "../types";
import { redactSecretString } from "./redact";
import { runtimeProviderFetch } from "./provider-runtime-fetch";
import { resolveProxyRoute } from "./proxy-env";

export type ProviderTlsProfile = "antigravity-browser";
export type ProviderTlsProfileStatus = "disabled" | "active" | "failed";
export const ANTIGRAVITY_TLS_HOSTS = new Set([
"daily-cloudcode-pa.googleapis.com",
"cloudcode-pa.googleapis.com",
]);
type TlsRuntime = {
fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
resolveProxyRoute?: typeof resolveProxyRoute;
};
let status = new Map<string, ProviderTlsProfileStatus>();
let runtime: TlsRuntime | undefined;

export function isCanonicalAntigravityUrl(input: string | URL): boolean {
try {
const url = new URL(input);
return (
url.protocol === "https:" &&
(url.port === "" || url.port === "443") &&
!url.username &&
!url.password &&
ANTIGRAVITY_TLS_HOSTS.has(url.hostname.toLowerCase())
);
} catch {
return false;
}
}

export function providerTlsProfileConfigError(
providerName: string,
provider: Pick<
OcxProviderConfig,
"adapter" | "authMode" | "googleMode" | "baseUrl" | "tlsProfile"
>,
): string | null {
if (provider.tlsProfile === undefined) return null;
if (provider.tlsProfile !== "antigravity-browser")
return "tlsProfile must be antigravity-browser";
if (
providerName !== "google-antigravity" ||
provider.adapter !== "google" ||
provider.authMode !== "oauth" ||
provider.googleMode !== "cloud-code-assist" ||
!isCanonicalAntigravityUrl(provider.baseUrl)
) {
return "tlsProfile antigravity-browser requires the canonical Google Antigravity OAuth destination";
}
return null;
}

export function getProviderTlsProfileStatus(
name: string,
): ProviderTlsProfileStatus {
return status.get(name) ?? "disabled";
}

export function resetProviderTlsProfileForTests(): void {
status = new Map();
runtime = undefined;
}

export function setProviderTlsRuntimeForTest(
next: TlsRuntime | undefined,
): void {
runtime = next;
}

function preserveTransportError(error: unknown): Error {
const message = redactSecretString(
error instanceof Error ? error.message : "provider TLS transport failed",
);
const name = error instanceof Error ? error.name : "Error";
if (name === "AbortError" || name === "TimeoutError")
return new DOMException(message, name);
const wrapped = new Error(message);
wrapped.name = name;
return wrapped;
}

export function providerTlsFetch(
name: string,
provider: Pick<
OcxProviderConfig,
"adapter" | "authMode" | "googleMode" | "baseUrl" | "tlsProfile"
>,
fallback: typeof globalThis.fetch,
): typeof globalThis.fetch {
if (provider.tlsProfile === undefined) {
status.set(name, "disabled");
return fallback;
}
if (providerTlsProfileConfigError(name, provider)) {
status.set(name, "failed");
return (async () => {
throw new Error("invalid provider TLS profile");
}) as unknown as typeof globalThis.fetch;
}
return (async (input, init) => {
const destination =
typeof input === "string" || input instanceof URL ? input : input.url;
if (!isCanonicalAntigravityUrl(destination))
throw new Error("provider TLS profile refused noncanonical destination");
try {
const configured = runtimeProviderFetch(
provider as OcxProviderConfig,
name,
);
const mod =
runtime ?? ((await import("wreq-js")) as unknown as TlsRuntime);
const proxyRoute = (runtime?.resolveProxyRoute ?? resolveProxyRoute)(
new URL(destination),
);
if (proxyRoute.kind === "fallback")
throw new Error(
"provider TLS profile cannot preserve configured proxy semantics",
);
const response = await (configured ?? mod.fetch)(input, {
...init,
redirect: "manual",
browser: "chrome_142",
os: "windows",
...(proxyRoute.kind === "proxy" ? { proxy: proxyRoute.proxy } : {}),
} as RequestInit & { browser: string; os: string });
status.set(name, "active");
return response;
} catch (error) {
status.set(name, "failed");
throw preserveTransportError(error);
}
}) as typeof globalThis.fetch;
}
1 change: 1 addition & 0 deletions src/server/auth-cors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
desktopExecutor: "redacted",
unsafeAllowNativeLocalExec: "editor",
nativeLocalExec: "editor",
tlsProfile: "editor",
} as const satisfies Record<keyof OcxProviderConfig, ProviderConfigFieldPolicy>;

type ProviderFieldWithPolicy<Policy extends ProviderConfigFieldPolicy> = {
Expand Down
4 changes: 3 additions & 1 deletion src/server/responses/fetch-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { OcxProviderConfig } from "../../types";
import type { WsData } from "../ws-bridge";
import { waitForProviderRequestSlot } from "../../providers/request-pacing";
import { withUpstreamHttpVersion } from "../../lib/upstream-http-version";
import { providerTlsFetch } from "../../lib/provider-tls-profile";
import type { CodexWsQuotaObserver } from "./codex-ws-metadata";

export { withUpstreamHttpVersion };
Expand Down Expand Up @@ -71,14 +72,15 @@ export function providerFetch(
options: ProviderFetchOptions = {},
): ProviderFetch {
const base = (provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? globalThis.fetch;
const transport = options.providerName ? providerTlsFetch(options.providerName, provider, base) : base;
const preconnect = (...args: Parameters<typeof globalThis.fetch.preconnect>): void => {
base.preconnect?.(...args);
};
// Rebuilt dispatches must use the same physical-send boundary as ordinary HTTP sends.
// Return the original 3xx so the response owner retains its retry/health/relay contract.
const dispatch = Object.assign(
(input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit) =>
base(input, { ...init, redirect: "manual" }),
transport(input, { ...init, redirect: "manual" }),
{ preconnect },
) as typeof globalThis.fetch;
const httpFetch = Object.assign(
Expand Down
2 changes: 2 additions & 0 deletions src/types/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,8 @@ export type TierDecision =
* retries are allowed; OAuth/forward credentials and local runtimes are never replayed.
*/
export interface OcxProviderConfig {
/** Optional browser-compatible outbound TLS profile; disabled by default. */
tlsProfile?: "antigravity-browser";
/** Optional short provider namespace used only at request/catalog presentation time. */
alias?: string;
/** Native model id -> short, slash-free request alias. */
Expand Down
2 changes: 2 additions & 0 deletions tests/fixtures/test-layout-expected.json
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,8 @@
"provider-model-discovery-contract.test.ts": "providers",
"provider-outbound-private-network.test.ts": "providers",
"provider-outbound.test.ts": "providers",
"provider-runtime-fetch.test.ts": "providers",
"provider-tls-profile.test.ts": "providers",
"provider-payload.test.ts": "gui",
"provider-quota-observed-marker.test.ts": "providers",
"provider-quota.test.ts": "providers",
Expand Down
10 changes: 10 additions & 0 deletions tests/providers/provider-runtime-fetch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { expect, test } from "bun:test";
import { RUNTIME_PROVIDER_FETCH, runtimeProviderFetch } from "../../src/lib/provider-runtime-fetch";

test("runtime provider fetch is scoped to provider and exact origin", () => {
const fetcher = async () => new Response("ok");
const provider = { adapter: "openai-chat", baseUrl: "https://example.com", [RUNTIME_PROVIDER_FETCH]: { providerName: "p", origins: ["https://example.com"], fetch: fetcher } } as any;
expect(runtimeProviderFetch(provider, "p")).toBe(fetcher);
expect(runtimeProviderFetch(provider, "other")).toBeUndefined();
expect(runtimeProviderFetch({ ...provider, baseUrl: "https://example.net" }, "p")).toBeUndefined();
});
Loading
Loading