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/providers/fastwire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type {
} from "../types";
import { MODEL_ADAPTER_OVERRIDE_ALLOWED } from "../types";
import { sanitizeLogMetadataString } from "../lib/redact";
import type { InboundWire, ModelWireDefault } from "./registry";
import type { InboundWire, ModelWireDefault, ProviderAuthKind } from "./registry";

const SERVICE_TIER_ADAPTERS = new Set(["openai-chat", "openai-responses"]);
const FAST_WIRE_ADAPTERS: Readonly<Record<FastWire["kind"], ReadonlySet<string>>> = {
Expand All @@ -31,6 +31,7 @@ export type FastPolicyAuthTransport =

export interface FastPolicyAuthority {
readonly providerAdapter: string;
readonly providerAuthMode?: ProviderAuthKind;
readonly fastWireDeclaration: FastWire | null | undefined;
readonly modelWireOverrideAllowed: boolean;
readonly authTransport: FastPolicyAuthTransport;
Expand Down Expand Up @@ -114,12 +115,18 @@ function registryDefaultForModel(
defaults: Readonly<Record<string, ModelWireDefault>>,
modelId: string,
inbound: InboundWire,
authMode: ProviderAuthKind | undefined,
): string | undefined {
const normalizedModelId = modelId.trim().toLowerCase();
if (!Object.hasOwn(defaults, normalizedModelId)) return undefined;
const declared = defaults[normalizedModelId];
if (declared === undefined) return undefined;
if (typeof declared !== "string" && !declared.inbound.includes(inbound)) return undefined;
if (typeof declared !== "string") {
if (!declared.inbound.includes(inbound)) return undefined;
if (declared.authModes && (authMode === undefined || !declared.authModes.includes(authMode))) {
return undefined;
}
}
const wire = typeof declared === "string" ? declared : declared.wire;
return MODEL_ADAPTER_OVERRIDE_ALLOWED.has(wire) ? wire : undefined;
}
Expand All @@ -143,7 +150,12 @@ function resolvePolicyAdapter(
return { adapter: configured, hardPinned: false };
}
if (MODEL_ADAPTER_OVERRIDE_ALLOWED.has(authority.providerAdapter)) {
const registryDefault = registryDefaultForModel(authority.registryWireDefaults, modelId, inbound);
const registryDefault = registryDefaultForModel(
authority.registryWireDefaults,
modelId,
inbound,
authority.providerAuthMode,
);
if (registryDefault !== undefined) return { adapter: registryDefault, hardPinned: false };
}
}
Expand Down
24 changes: 20 additions & 4 deletions src/providers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,13 @@ export type InboundWire = "responses" | "chat" | "anthropic";

/**
* A per-model wire default: a bare string applies to every inbound, while the object
* form applies only to the listed inbound protocols.
* form may scope the default to listed inbound protocols and authentication modes.
*/
export type ModelWireDefault = string | { wire: string; inbound: readonly InboundWire[] };
export type ModelWireDefault = string | {
wire: string;
inbound: readonly InboundWire[];
authModes?: readonly ProviderAuthKind[];
};

export interface ResponsesTerminalRepairPolicy {
/** Quiet time after a structurally complete output graph before synthesizing completion. */
Expand Down Expand Up @@ -1011,6 +1015,14 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
// 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"],
defaultModel: "grok-4.5",
// The current Grok CLI catalog declares both subscription models as native Responses
// backends. Keep API-key and translated Chat/Anthropic callers on their existing wire;
// Codex Responses traffic can relay xAI's SSE as it arrives instead of waiting for the
// Chat Completions compatibility stream to flush at the end of a reasoning turn.
modelWireDefaults: {
"grok-4.6": { wire: "openai-responses", inbound: ["responses"], authModes: ["oauth"] },
"grok-4.5": { wire: "openai-responses", inbound: ["responses"], authModes: ["oauth"] },
},
// Vision lineup per docs.x.ai model-capabilities/images/understanding: the grok-4.x chat
// models accept image input (JPEG/PNG, URL or base64). Without this the catalog leaves
// inputModalities undefined, and deriveComboCatalogModel defaults an undefined member to
Expand Down Expand Up @@ -2634,8 +2646,12 @@ export function providerModelWireDefault(
if (!entry?.modelWireDefaults || !providerMatchesRegistryTransport(id, provider)) return undefined;
const declared = entry.modelWireDefaults[modelId.trim().toLowerCase()];
if (declared === undefined) return undefined;
// A bare string applies to every inbound; the object form only to the listed ones.
if (typeof declared !== "string" && !declared.inbound.includes(inbound)) return undefined;
// A bare string applies to every inbound/auth mode; the object form may narrow either.
if (typeof declared !== "string") {
if (!declared.inbound.includes(inbound)) return undefined;
const authMode = provider.authMode ?? entry.authKind;
if (declared.authModes && !declared.authModes.includes(authMode)) return undefined;
}
const wire = typeof declared === "string" ? declared : declared.wire;
return wire !== undefined && allowedWires.has(wire) ? wire : undefined;
}
Expand Down
9 changes: 8 additions & 1 deletion src/providers/service-tier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,13 @@ function cloneRegistryWireDefaults(
for (const [modelId, declaration] of Object.entries(defaults)) {
clone[modelId.trim().toLowerCase()] = typeof declaration === "string"
? declaration
: Object.freeze({ wire: declaration.wire, inbound: Object.freeze([...declaration.inbound]) });
: Object.freeze({
wire: declaration.wire,
inbound: Object.freeze([...declaration.inbound]),
...(declaration.authModes
? { authModes: Object.freeze([...declaration.authModes]) }
: {}),
});
}
return Object.freeze(clone);
}
Expand All @@ -61,6 +67,7 @@ function buildFastPolicyAuthority(
const registry = registryTransportMatch ? getProviderRegistryEntry(providerName) : undefined;
const authority: FastPolicyAuthority = Object.freeze({
providerAdapter: provider.adapter,
providerAuthMode: provider.authMode ?? registry?.authKind ?? "key",
fastWireDeclaration: cloneFastWire(
provider.fastWire !== undefined ? provider.fastWire : registry?.fastWire,
{ freeze: true },
Expand Down
92 changes: 92 additions & 0 deletions src/server/responses/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2621,6 +2621,98 @@ async function handleResponsesInner(
request.releaseBodyObservation?.();
}

// Native Responses providers return before the generic adapter recovery loop below. Keep
// their OAuth contract identical: one pre-stream 401 forces a credential refresh and one
// rebuilt replay. xAI's current subscription models use this branch now that their official
// Grok CLI catalog declares the Responses backend.
if (upstreamResponse.status === 401 && isOAuth401ReplayProvider && sentOAuthSnapshot) {
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
let refreshed: OAuthAccessSnapshot;
try {
refreshed = await forceRefreshOAuthAccessSnapshot(sentOAuthSnapshot);
} catch (err) {
upstream.abort();
releaseCodexAuthContextProbeLease(authCtx);
return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(err));
}
sentOAuthSnapshot = refreshed;
replayOAuthCredentialSnapshot = {
accountId: refreshed.accountId,
generation: refreshed.generation,
};
if (route.providerName === "kiro") {
parsed._kiroAuthContext = { ...(refreshed.kiro ?? {}) };
}
const refreshedProvider = resolveProviderTransport(
route.providerName,
{ ...route.provider, apiKey: refreshed.accessToken },
parsed.options.promptCacheKey,
route.providerName === "github-copilot" ? getOAuthCredentialApiBaseUrl(route.providerName) : undefined,
);
route.provider = refreshedProvider;
const refreshedAdapter = resolveAdapter(
resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire),
config.cacheRetention,
);
if (!("passthrough" in refreshedAdapter) || !refreshedAdapter.passthrough) {
upstream.abort();
return formatErrorResponse(502, "upstream_error", "OAuth refresh changed the provider wire unexpectedly");
}
bindRouteReasoningReplayScope({
parsed,
providerName: route.providerName,
provider: refreshedProvider,
adapterName: refreshedAdapter.name,
oauthCredentialSnapshot: replayOAuthCredentialSnapshot,
});
logCtx.providerAdapter = refreshedAdapter.name;
sealRequestAttemptIdentity(
logCtx.activeAttempt,
logCtx.provider,
refreshedAdapter.name,
logCtx.accountLogLabel,
);
try {
request = await refreshedAdapter.buildRequest(parsed, {
headers: selectedForwardHeaders,
translatorBudget,
});
recordAdapterReasoning(logCtx, request);
recordAdapterTier(logCtx, request);
} catch (err) {
upstream.abort();
if (options.abortSignal?.aborted) return clientCancelledResponse();
const msg = err instanceof Error ? err.message : String(err);
return formatErrorResponse(400, "invalid_request_error", redactSecretString(msg));
}
try {
upstreamResponse = await fetchWithTransientRetry(
recovery => {
noteAttemptSend(logCtx.activeAttempt, passthroughEstimate, recovery ?? "oauth-401");
return fetchWithHeaderTimeout(request.url, applyUpstreamRecoveryInit({
method: request.method,
headers: request.headers,
body: request.body,
}, recovery), upstream.signal, connectMs, parsed.stream,
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
providerName: route.providerName,
modelId: route.modelId,
}),
route.provider.authMode === "forward")
.then(res => {
settleObservedHostResponse();
return res;
});
},
{ abortSignal: upstream.signal, label: safeHostLabel(request.url) },
);
} catch (err) {
return transportFailureResponse(err);
} finally {
request.releaseBodyObservation?.();
}
}

// Same-target 429 wait-and-retry (opt-in `retryOn429`) for key-auth providers on the
// passthrough wire. This branch returns before the recovery loop below, so Responses-shaped
// key-auth gateways (e.g. the built-in DeepSeek preset) would otherwise surface 429
Expand Down
18 changes: 14 additions & 4 deletions structure/04_transports-and-sidecars.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,16 @@ Registry `modelWireDefaults` select an evidence-backed upstream protocol for an
changing the provider-wide adapter. Explicit, allowed `modelAdapters` configuration always wins,
including an entry that opts the model back into the provider-wide wire. Defaults are applied only
while the configured provider still matches the registry transport, so reusing a preset name for a
different custom destination does not inherit its upstream assumptions.
different custom destination does not inherit its upstream assumptions. Object-form defaults may
also narrow the decision by inbound protocol and authentication mode; an auth-scoped default must
not leak from a subscription transport into an API-key or forwarded-credential route.

xAI keeps `openai-chat` as its provider-wide compatibility wire. The official Grok CLI catalog
declares the Grok 4.5 and 4.6 subscription models as Responses backends, so only OAuth-backed native
Responses traffic for those exact models selects `openai-responses`. API-key requests, translated
Chat/Anthropic callers, other Grok models, and explicit model adapter overrides retain their
existing wire. This lets Codex receive native xAI SSE deltas as they arrive without widening the
credential or compatibility boundary.

OpenCode Go documents `gpt-5.6-luna` on `/zen/go/v1/responses` while sibling models use its Chat or
Anthropic endpoints. The built-in preset therefore selects `openai-responses` only for Luna and
Expand Down Expand Up @@ -601,9 +610,10 @@ Grounded in the open-sourced official client (xai-org/grok-build); unit + eviden
`auth.json` load-merge-persist (`src/oauth/store.ts`); generation-guarded persist
(`expectedGeneration` → superseded adoption), conditional `needsReauth`, bounded jittered
retry for transient token-endpoint failures.
- **Reactive 401 replay:** the serving recovery loop force-refreshes once (singleflight,
generation-checked) and replays OAuth-backed xAI requests exactly once with a re-resolved
transport; API-key/BYOK paths excluded (`src/server/responses.ts`).
- **Reactive 401 replay:** both the adapter recovery loop and native Responses passthrough branch
force-refresh once (singleflight, generation-checked) and replay OAuth-backed xAI requests
exactly once with a re-resolved transport; API-key/BYOK paths are excluded
(`src/server/responses/core.ts`).
- **Header parity:** per-attempt `x-grok-req-id` (fresh UUID inside the transport fetch
wrapper), stable session/conv affinity headers, always-set User-Agent, and a single
compatibility profile const for the Grok client version (`src/providers/xai-transport.ts`);
Expand Down
32 changes: 32 additions & 0 deletions tests/adapter-resolve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,38 @@ describe("per-model wire override (#404)", () => {
});

describe("registry per-model wire defaults", () => {
function xai(authMode: "oauth" | "key", overrides: Partial<OcxProviderConfig> = {}): OcxProviderConfig {
return gateway({
baseUrl: "https://api.x.ai/v1",
authMode,
...overrides,
});
}

test("routes current xAI subscription models through Responses for native Codex traffic", () => {
for (const model of ["grok-4.6", "grok-4.5"]) {
expect(resolveWireProtocolOverride("xai", model, xai("oauth"), "responses").adapter)
.toBe("openai-responses");
}
});

test("keeps xAI key auth and translated callers on their existing Chat wire", () => {
expect(resolveWireProtocolOverride("xai", "grok-4.6", xai("key"), "responses").adapter)
.toBe("openai-chat");
expect(resolveWireProtocolOverride("xai", "grok-4.6", xai("oauth"), "chat").adapter)
.toBe("openai-chat");
expect(resolveWireProtocolOverride("xai", "grok-4.6", xai("oauth"), "anthropic").adapter)
.toBe("openai-chat");
expect(resolveWireProtocolOverride("xai", "grok-4.3", xai("oauth"), "responses").adapter)
.toBe("openai-chat");
});

test("an explicit xAI Chat override opts out of the subscription Responses default", () => {
const provider = xai("oauth", { modelAdapters: { "grok-4.6": "openai-chat" } });
expect(resolveWireProtocolOverride("xai", "grok-4.6", provider, "responses").adapter)
.toBe("openai-chat");
});

function deepseek(overrides: Partial<OcxProviderConfig> = {}): OcxProviderConfig {
return gateway({
baseUrl: "https://api.deepseek.com",
Expand Down
39 changes: 39 additions & 0 deletions tests/fastwire-policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,27 @@ describe("resolveFastPolicy matrix", () => {
expect(resolveFastPolicy(authority, MODEL, "responses").adapter).toBe("openai-responses");
});

test("registry defaults retain their auth-mode constraint", () => {
const base: FastPolicyAuthority = {
...authorityForMatrix({
source: "provider-adapter",
declaration: "undefined",
overrideAllowed: true,
capability: "true",
chatForeignTierForward: true,
}),
providerAdapter: "openai-chat",
registryWireDefaults: {
[MODEL]: { wire: "openai-responses", inbound: ["responses"], authModes: ["oauth"] },
},
};
expect(resolveFastPolicy({ ...base, providerAuthMode: "oauth" }, MODEL).adapter)
.toBe("openai-responses");
expect(resolveFastPolicy({ ...base, providerAuthMode: "key" }, MODEL).adapter)
.toBe("openai-chat");
expect(resolveFastPolicy(base, MODEL).adapter).toBe("openai-chat");
});

test("hard pins and configured overrides retain exact runtime model-key semantics", () => {
const authority: FastPolicyAuthority = {
...authorityForMatrix({
Expand Down Expand Up @@ -235,6 +256,24 @@ describe("resolveFastPolicy matrix", () => {
expect(fastPolicyForModel(provider, MODEL, "fixture").capability).toBe(false);
});

test("captured xAI registry defaults keep OAuth and key transports separate", () => {
const oauthProvider = Object.freeze({
adapter: "openai-chat",
baseUrl: "https://api.x.ai/v1",
authMode: "oauth" as const,
});
const keyProvider = Object.freeze({
adapter: "openai-chat",
baseUrl: "https://api.x.ai/v1",
authMode: "key" as const,
});

expect(fastPolicyForModel(oauthProvider, "grok-4.6", "xai").adapter)
.toBe("openai-responses");
expect(fastPolicyForModel(keyProvider, "grok-4.6", "xai").adapter)
.toBe("openai-chat");
});

test("prototype-named providers and models use only own wire-policy rows", () => {
expect(captureWireAdapterHardPins("toString")).toEqual({});
expect(isWirePinnedModel("toString", MODEL)).toBe(false);
Expand Down
Loading
Loading