Expand core functionality and update related components

This commit is contained in:
musistudio
2026-07-28 17:44:49 +08:00
parent d305f87e67
commit 6cfd3f226f
8 changed files with 1574 additions and 60 deletions
@@ -3,7 +3,7 @@
*/
import { isGatewayProviderEnabled } from "@ccr/core/contracts/app";
import type { AppConfig, GatewayProviderConfig, GatewayProviderProtocol, VirtualModelProfileConfig } from "@ccr/core/contracts/app";
import { codexDefaultBaseUrl, kimiAccessTokenExpired, kimiIdentityHeaders, readClaudeCodeOauth, readCodexAuth, readGrokAuth, readKimiAuth, resolveGrokAuth, resolveKimiAuth } from "@ccr/core/agents/local-providers/service";
import { codexDefaultBaseUrl, kimiAccessTokenExpired, kimiIdentityHeaders, localAgentProviderApiKey, readClaudeCodeOauth, readCodexAuth, readGrokAuth, readKimiAuth, resolveGrokAuth, resolveKimiAuth } from "@ccr/core/agents/local-providers/service";
import { grokAccessTokenExpired, grokClientVersion } from "@ccr/core/agents/local-providers/grok";
import { pluginService } from "@ccr/core/plugins/service";
import { normalizeRouteSelector, providerRuntimeId } from "@ccr/core/routing/model-registry";
@@ -40,12 +40,18 @@ export async function compileCoreGatewayConfig(
)
: [];
const pluginBillingConfig = isRecord(pluginCoreGatewayConfig.billing) ? pluginCoreGatewayConfig.billing : {};
const configuredProviderPlugins = normalizeClaudeCodeOauthProviderPlugins([
...(config.providerPlugins ?? []).filter(providerPluginEnabled),
...pluginService.getCoreProviderPlugins().filter(providerPluginEnabled)
const allConfiguredProviderPlugins = normalizeClaudeCodeOauthProviderPlugins([
...(config.providerPlugins ?? []),
...pluginService.getCoreProviderPlugins()
]);
const configuredProviderPlugins = allConfiguredProviderPlugins.filter(providerPluginEnabled);
const configuredProviderPluginsWithLocalCodexFallbacks = withMissingCodexOauthProviderPlugins(
configuredProviderPlugins,
allConfiguredProviderPlugins,
config.Providers.filter(isGatewayProviderEnabled)
);
const providerPluginsWithRuntimeDefaults = await withKimiOauthRuntimeDefaults(
await withGrokOauthRuntimeDefaults(withClaudeCodeOauthRuntimeDefaults(withCodexOauthRuntimeDefaults(configuredProviderPlugins)))
await withGrokOauthRuntimeDefaults(withClaudeCodeOauthRuntimeDefaults(withCodexOauthRuntimeDefaults(configuredProviderPluginsWithLocalCodexFallbacks)))
);
const codexOauthProviderNames = codexOauthLocalProviderNames(providerPluginsWithRuntimeDefaults);
const enabledProviders = config.Providers.filter(isGatewayProviderEnabled);
@@ -197,6 +203,88 @@ function providerPluginEnabled(plugin: unknown): boolean {
}
function withMissingCodexOauthProviderPlugins(
providerPlugins: unknown[],
explicitProviderPlugins: unknown[],
providers: GatewayProviderConfig[]
): unknown[] {
const codexAuth = readCodexAuth();
if (!codexAuth?.accessToken && !codexAuth?.refreshToken) {
return providerPlugins;
}
const codexOauthProviderNames = codexOauthLocalProviderNames(explicitProviderPlugins);
const additions: unknown[] = [];
for (const provider of providers) {
if (!isLocalCodexOauthProvider(provider)) {
continue;
}
const runtimeName = providerRuntimeId(provider);
const capabilityName = providerCapabilityInternalName(provider, "openai_responses");
if (
codexOauthProviderNames.has(provider.name) ||
codexOauthProviderNames.has(runtimeName) ||
codexOauthProviderNames.has(capabilityName)
) {
continue;
}
const keyPrefix = `ccr-local-agent-${providerNameSlug(runtimeName)}`;
additions.push({
codexOauth: {
accessToken: codexAuth.accessToken,
...(codexAuth.accountId ? { accountId: codexAuth.accountId } : {}),
refreshIfMissingAccessToken: true,
refreshToken: codexAuth.refreshToken,
required: true
},
key: `${keyPrefix}-codex-oauth-recovered`,
providerName: provider.name
});
}
return additions.length > 0 ? [...providerPlugins, ...additions] : providerPlugins;
}
function isLocalCodexOauthProvider(provider: GatewayProviderConfig): boolean {
const protocol =
normalizeProviderProtocol(provider.type) ??
normalizeProviderProtocol(provider.provider) ??
inferProtocol(provider);
return protocol === "openai_responses" &&
providerApiKeyValue(provider) === localAgentProviderApiKey &&
normalizeCodexProviderBaseUrl(providerBaseUrlValue(provider)) === normalizeCodexProviderBaseUrl(codexDefaultBaseUrl);
}
function providerApiKeyValue(provider: GatewayProviderConfig): string {
return provider.api_key || provider.apiKey || provider.apikey || "";
}
function providerBaseUrlValue(provider: GatewayProviderConfig): string {
return provider.api_base_url || provider.baseurl || provider.baseUrl || "";
}
function normalizeCodexProviderBaseUrl(value: string): string {
try {
const url = new URL(value);
url.hash = "";
url.search = "";
url.pathname = url.pathname.replace(/\/+$/g, "");
return url.toString().replace(/\/+$/g, "");
} catch {
return value.trim().replace(/\/+$/g, "");
}
}
function providerNameSlug(value: string): string {
return value
.trim()
.toLowerCase()
.replace(/[^a-z0-9_.-]+/g, "-")
.replace(/^-+|-+$/g, "") || "provider";
}
export function normalizeCoreGatewayVirtualModelProfiles(profiles: unknown[], config: AppConfig): unknown[] {
return profiles.map((profile) => normalizeCoreGatewayVirtualModelProfile(profile, config));
}
@@ -513,14 +601,19 @@ function withCodexOauthProviderBaseUrl(
provider: GatewayProviderConfig,
codexOauthProviderNames: Set<string>
): GatewayProviderConfig {
if (!codexOauthProviderNames.has(provider.name)) {
return provider;
}
const protocol =
normalizeProviderProtocol(provider.type) ??
normalizeProviderProtocol(provider.provider) ??
inferProtocol(provider);
const names = [
provider.name,
providerRuntimeId(provider),
providerCapabilityInternalName(provider, "openai_responses")
];
if (!names.some((name) => codexOauthProviderNames.has(name))) {
return provider;
}
if (protocol !== "openai_responses") {
return provider;
}
+709 -12
View File
@@ -1,4 +1,4 @@
import { createHash } from "node:crypto";
import { createHash, randomUUID } from "node:crypto";
import type {
GatewayProviderConnectivityCheckReport,
GatewayProviderConnectivityCheckRequest,
@@ -12,6 +12,8 @@ import type {
GatewayProviderCapabilityProtocol,
GatewayProviderProtocol
} from "@ccr/core/contracts/app";
import { codexDefaultBaseUrl, readCodexAuth } from "@ccr/core/agents/local-providers/codex";
import { localAgentProviderApiKey } from "@ccr/core/agents/local-providers/shared";
import { findProviderPresetByBaseUrl, providerApiKeySafetyIssue } from "@ccr/core/providers/presets/index";
import { getProviderCatalogModels } from "@ccr/core/providers/model-catalog";
import { fetchWithSystemProxy } from "@ccr/core/proxy/system-proxy-fetch";
@@ -26,6 +28,8 @@ import {
newApiKeyUsageAccountConfig,
type DetectedProviderKind
} from "@ccr/core/providers/new-api";
import { recordGatewayRequestLog } from "@ccr/core/observability/request-log-store";
import { requestLogSampled } from "@ccr/core/observability/raw-trace-sync";
type ModelSource = NonNullable<GatewayProviderProbeResult["modelSource"]>;
@@ -64,6 +68,15 @@ type ProbeCacheEntry = {
result: GatewayProviderProbeResult;
};
type GatewayProviderConnectivityCheckOptions = {
requestLog?: {
bodyCapturePolicy?: "all" | "errors" | "none";
enabled?: boolean;
maxBodyBytes?: number;
successSampleRate?: number;
};
};
const protocolOrder: GatewayProviderCapabilityProtocol[] = [
"openai_responses",
"openai_chat_completions",
@@ -82,9 +95,23 @@ const protocolProbeCacheMs = 60 * 1000;
const connectivityProbeCacheMs = 15 * 1000;
const failedProbeCacheMs = 10 * 1000;
const maxProbeCacheEntries = 500;
const codexOauthTokenEndpoint = "https://auth.openai.com/oauth/token";
const codexOauthClientId = "app_EMoamEEZ73f0CkXaXp7hrann";
const codexOauthDefaultScope = "openid profile email offline_access";
const codexOauthRequiredScopes = ["api.connectors.read", "api.connectors.invoke"];
const codexOauthDefaultTimeoutMs = 8_000;
const codexProbeOauthCache = new Map<string, CodexProbeOauthRefreshResult>();
const inFlightCodexProbeOauthRefreshes = new Map<string, Promise<CodexProbeOauthRefreshResult>>();
const probeCache = new Map<string, ProbeCacheEntry>();
const inFlightProbes = new Map<string, Promise<GatewayProviderProbeResult>>();
type CodexProbeOauthRefreshResult = {
accessToken?: string;
accountId?: string;
expiresAtMs: number;
refreshToken?: string;
};
export async function probeGatewayProvider(request: GatewayProviderProbeRequest): Promise<GatewayProviderProbeResult> {
pruneProbeCache();
const cacheKey = providerProbeCacheKey(request);
@@ -155,11 +182,14 @@ export async function probeGatewayProviderCandidates(
}
export async function checkGatewayProviderConnectivity(
request: GatewayProviderConnectivityCheckRequest
request: GatewayProviderConnectivityCheckRequest,
options: GatewayProviderConnectivityCheckOptions = {}
): Promise<GatewayProviderConnectivityCheckReport> {
const models = uniqueStrings(request.models);
const checks = await Promise.all(
models.map(async (model) => {
const startedAtMs = Date.now();
const startedAt = new Date(startedAtMs).toISOString();
try {
const result = await probeGatewayProviderCandidates({
apiKey: request.apiKey,
@@ -172,6 +202,7 @@ export async function checkGatewayProviderConnectivity(
});
if (!result) {
return {
durationMs: Date.now() - startedAtMs,
model,
probe: undefined,
report: {
@@ -179,12 +210,14 @@ export async function checkGatewayProviderConnectivity(
model,
protocols: [],
supported: false
}
},
startedAt
};
}
const supported = providerProbeHasSupportedProtocol(result.probe);
return {
durationMs: Date.now() - startedAtMs,
model,
probe: result.probe,
report: {
@@ -194,10 +227,12 @@ export async function checkGatewayProviderConnectivity(
model,
protocols: result.probe.protocols,
supported
}
},
startedAt
};
} catch (error) {
return {
durationMs: Date.now() - startedAtMs,
model,
probe: undefined,
report: {
@@ -205,11 +240,15 @@ export async function checkGatewayProviderConnectivity(
model,
protocols: [],
supported: false
}
},
startedAt
};
}
})
);
for (const check of checks) {
recordProviderConnectivityRequestLog(request, check, options.requestLog);
}
const reports = checks.map((check) => check.report);
return {
failed: reports.filter((item) => !item.supported),
@@ -219,6 +258,82 @@ export async function checkGatewayProviderConnectivity(
};
}
function recordProviderConnectivityRequestLog(
request: GatewayProviderConnectivityCheckRequest,
check: {
durationMs: number;
model: string;
report: GatewayProviderConnectivityCheckReport["results"][number];
startedAt: string;
},
options: GatewayProviderConnectivityCheckOptions["requestLog"]
): void {
if (!options?.enabled) {
return;
}
const protocol = check.report.protocols.find((item) => item.supported) ?? check.report.protocols[0];
const candidate = protocol
? request.candidates.find((item) => item.baseUrl === protocol.baseUrl)
: request.candidates[0];
const successful = check.report.supported;
const requestId = randomUUID();
if (successful && !requestLogSampled(requestId, options.successSampleRate ?? 1)) {
return;
}
const bodyCapturePolicy = options.bodyCapturePolicy ?? "all";
const captureBody = bodyCapturePolicy === "all" || (bodyCapturePolicy === "errors" && !successful);
const responseBodyText = JSON.stringify({
message: check.report.message,
protocols: check.report.protocols.map((item) => ({
endpoint: item.endpoint,
message: item.message,
protocol: item.protocol,
status: item.status,
supported: item.supported
})),
supported: check.report.supported
});
recordGatewayRequestLog({
bodyCapturePolicy,
captureBody,
client: "provider-connectivity-check",
completedAt: new Date(new Date(check.startedAt).getTime() + check.durationMs).toISOString(),
durationMs: check.durationMs,
error: successful ? undefined : check.report.message,
maxBodyBytes: options.maxBodyBytes,
method: "POST",
model: check.model,
path: "/__ccr/provider-connectivity",
providerName: providerProbeCandidateName(candidate),
providerProtocol: protocol?.protocol as GatewayProviderProtocol | undefined,
requestedModel: check.model,
requestBody: Buffer.from(JSON.stringify({
candidates: request.candidates.map((item) => ({
baseUrl: item.baseUrl,
name: providerProbeCandidateName(item),
protocols: item.protocols
})),
model: check.model,
protocols: request.protocols
})),
requestHeaders: {},
requestId,
resolvedModel: check.model,
responseBodyText,
responseHeaders: {},
startedAt: check.startedAt,
statusCode: protocol?.status ?? (successful ? 200 : 599),
url: protocol?.endpoint ?? candidate?.baseUrl ?? "provider-connectivity-check"
});
}
function providerProbeCandidateName(candidate: GatewayProviderProbeCandidate | undefined): string | undefined {
const value: unknown = candidate;
return isRecord(value) ? readString(value.name) : undefined;
}
async function resolveGatewayProviderProbe(request: GatewayProviderProbeRequest): Promise<GatewayProviderProbeResult> {
const mode = request.mode ?? "protocols";
const safetyIssue = providerApiKeySafetyIssue({
@@ -589,10 +704,12 @@ async function probeProtocolConnectivity(
let firstResult: GatewayProviderProbeProtocolResult | undefined;
for (const candidate of endpoints) {
const request = providerProbeAuthRequest(
const request = await providerProbeAuthRequest(
candidate.endpoint,
requestForProtocol(protocol, model, apiKey),
providerPlugins
providerPlugins,
apiKey,
{ model }
);
const result = await requestJson(request.url, request.init);
const message = readResponseMessage(result);
@@ -732,18 +849,105 @@ function mediaProbeBody(protocol: GatewayProviderCapabilityProtocol): Record<str
return {};
}
function providerProbeAuthRequest(
async function providerProbeAuthRequest(
url: string,
init: RequestInit,
providerPlugins: unknown[]
): { init: RequestInit; url: string } {
providerPlugins: unknown[],
apiKey: string | undefined,
context: { model: string }
): Promise<{ init: RequestInit; url: string }> {
const auth = providerPlugins
.map(providerPluginAuth)
.find((item): item is Record<string, unknown> => Boolean(item));
if (!auth) {
return { init, url };
let codexOauth = providerPlugins
.map(providerPluginCodexOauth)
.find((item): item is Record<string, unknown> => Boolean(item));
let requestTransform = providerPlugins
.map(providerPluginRequest)
.find((item): item is Record<string, unknown> => Boolean(item));
const liveCodexAuth = providerProbeLiveCodexOauth(url, apiKey);
if (codexOauth && liveCodexAuth) {
codexOauth = {
...codexOauth,
...liveCodexAuth.codexOauth
};
} else {
codexOauth ??= liveCodexAuth?.codexOauth;
}
requestTransform ??= liveCodexAuth?.request;
if (codexOauth && apiKey === localAgentProviderApiKey && isCodexProbeEndpoint(url)) {
requestTransform = withCodexProbeBackendRequestTransform(requestTransform);
}
let request = { init, url };
if (auth) {
request = providerProbeStaticAuthRequest(request.url, request.init, auth);
}
if (codexOauth) {
request = await providerProbeCodexOauthRequest(request.url, request.init, codexOauth);
}
if (liveCodexAuth?.isFedrampAccount) {
const headers = new Headers(request.init.headers);
headers.set("X-OpenAI-Fedramp", "true");
request = {
...request,
init: {
...request.init,
headers
}
};
}
if (requestTransform) {
request = providerProbeRequestTransformRequest(request.url, request.init, requestTransform, context);
}
return request;
}
function providerProbeLiveCodexOauth(
url: string,
apiKey: string | undefined
): { codexOauth: Record<string, unknown>; isFedrampAccount?: boolean; request: Record<string, unknown> } | undefined {
if (apiKey !== localAgentProviderApiKey || !isCodexProbeEndpoint(url)) {
return undefined;
}
const auth = readCodexAuth();
if (!auth?.accessToken && !auth?.refreshToken) {
return undefined;
}
return {
codexOauth: {
...(auth.accessToken ? { accessToken: auth.accessToken } : {}),
...(auth.accountId ? { accountId: auth.accountId } : {}),
refreshIfMissingAccessToken: true,
...(auth.refreshToken ? { refreshToken: auth.refreshToken } : {}),
required: true
},
isFedrampAccount: auth.isFedrampAccount,
request: codexProbeBackendRequestTransform()
};
}
function isCodexProbeEndpoint(url: string): boolean {
try {
const parsed = new URL(url);
const codexBase = new URL(codexDefaultBaseUrl);
return parsed.origin === codexBase.origin &&
parsed.pathname.toLowerCase().startsWith(codexBase.pathname.toLowerCase());
} catch {
return false;
}
}
function providerProbeStaticAuthRequest(
url: string,
init: RequestInit,
auth: Record<string, unknown>
): { init: RequestInit; url: string } {
const headers = new Headers(init.headers);
for (const header of readStringArray(auth.removeHeaders)) {
headers.delete(header);
@@ -772,6 +976,222 @@ function providerProbeAuthRequest(
};
}
function providerProbeRequestTransformRequest(
url: string,
init: RequestInit,
requestTransform: Record<string, unknown>,
context: { model: string }
): { init: RequestInit; url: string } {
const headers = new Headers(init.headers);
for (const [name, value] of Object.entries(isRecord(requestTransform.headers) ? requestTransform.headers : {})) {
const headerValue = renderProviderProbeTemplate(readString(value), context);
if (headerValue) {
headers.set(name, headerValue);
}
}
const nextUrl = new URL(url);
for (const [name, value] of Object.entries(isRecord(requestTransform.query) ? requestTransform.query : {})) {
const queryValue = renderProviderProbeTemplate(readString(value), context);
if (queryValue) {
nextUrl.searchParams.set(name, queryValue);
}
}
const transformedBody = providerProbeTransformedJsonBody(init.body, requestTransform, context);
return {
init: {
...init,
body: transformedBody ?? init.body,
headers
},
url: nextUrl.toString()
};
}
function providerProbeTransformedJsonBody(
body: BodyInit | null | undefined,
requestTransform: Record<string, unknown>,
context: { model: string }
): string | undefined {
const payload = requestBodyJsonObject(body);
if (!payload) {
return undefined;
}
let changed = false;
for (const path of readStringArray(requestTransform.bodyRemove)) {
changed = deleteJsonPath(payload, path) || changed;
}
const bodyMerge = isRecord(requestTransform.bodyMerge) ? requestTransform.bodyMerge : undefined;
if (bodyMerge) {
const renderedMerge = renderJsonTemplateValues(bodyMerge, context);
if (isRecord(renderedMerge)) {
mergeJsonObject(payload, renderedMerge);
changed = true;
}
}
const bodySet = isRecord(requestTransform.bodySet) ? requestTransform.bodySet : undefined;
if (bodySet) {
for (const [path, value] of Object.entries(bodySet)) {
setJsonPath(payload, path, renderJsonTemplateValues(value, context));
changed = true;
}
}
return changed ? JSON.stringify(payload) : undefined;
}
function requestBodyJsonObject(body: BodyInit | null | undefined): Record<string, unknown> | undefined {
if (typeof body !== "string") {
return undefined;
}
const payload = parseJson(body);
return isRecord(payload) ? { ...payload } : undefined;
}
function deleteJsonPath(target: Record<string, unknown>, path: string): boolean {
const parts = jsonPathParts(path);
if (parts.length === 0) {
return false;
}
const parent = jsonPathParent(target, parts);
const key = parts[parts.length - 1];
if (!parent || key === undefined || !Object.prototype.hasOwnProperty.call(parent, key)) {
return false;
}
delete parent[key];
return true;
}
function setJsonPath(target: Record<string, unknown>, path: string, value: unknown): void {
const parts = jsonPathParts(path);
if (parts.length === 0) {
return;
}
let current = target;
for (const part of parts.slice(0, -1)) {
const next = current[part];
if (!isRecord(next)) {
current[part] = {};
}
current = current[part] as Record<string, unknown>;
}
const key = parts[parts.length - 1];
if (key !== undefined) {
current[key] = value;
}
}
function jsonPathParent(target: Record<string, unknown>, parts: string[]): Record<string, unknown> | undefined {
let current: unknown = target;
for (const part of parts.slice(0, -1)) {
if (!isRecord(current)) {
return undefined;
}
current = current[part];
}
return isRecord(current) ? current : undefined;
}
function jsonPathParts(path: string): string[] {
return path.split(".").map((part) => part.trim()).filter(Boolean);
}
function mergeJsonObject(target: Record<string, unknown>, source: Record<string, unknown>): void {
for (const [key, value] of Object.entries(source)) {
if (isRecord(value) && isRecord(target[key])) {
mergeJsonObject(target[key], value);
} else {
target[key] = value;
}
}
}
function renderJsonTemplateValues(value: unknown, context: { model: string }): unknown {
if (typeof value === "string") {
return renderProviderProbeTemplate(value, context);
}
if (Array.isArray(value)) {
return value.map((item) => renderJsonTemplateValues(item, context));
}
if (isRecord(value)) {
return Object.fromEntries(
Object.entries(value).map(([key, item]) => [key, renderJsonTemplateValues(item, context)])
);
}
return value;
}
function renderProviderProbeTemplate(value: string | undefined, context: { model: string }): string | undefined {
return value
?.split("{{ model }}").join(context.model)
.split("{{ request.body.model }}").join(context.model)
.split("{{ upstreamRequest.body.model }}").join(context.model);
}
function codexProbeBackendRequestTransform(): Record<string, unknown> {
return withCodexProbeBackendRequestTransform();
}
function withCodexProbeBackendRequestTransform(requestTransform: Record<string, unknown> = {}): Record<string, unknown> {
const bodyRemove = readStringArray(requestTransform.bodyRemove);
return {
...requestTransform,
bodyRemove: uniqueStrings([...bodyRemove, "max_output_tokens", "stop"])
};
}
async function providerProbeCodexOauthRequest(
url: string,
init: RequestInit,
codexOauth: Record<string, unknown>
): Promise<{ init: RequestInit; url: string }> {
const requiredScopes = codexRequiredScopes(codexOauth.requiredScopes);
const scope = codexOauthScope(readString(codexOauth.scope), requiredScopes);
let accessToken = readString(codexOauth.accessToken) || readString(codexOauth.access_token);
let refreshToken = readString(codexOauth.refreshToken) || readString(codexOauth.refresh_token);
let accountId = readString(codexOauth.accountId) || readString(codexOauth.account_id);
if (refreshToken && shouldRefreshCodexProbeToken(accessToken, codexOauth, requiredScopes)) {
const refreshed = await refreshCodexProbeAccessToken(codexOauth, refreshToken, scope);
accessToken = refreshed.accessToken || accessToken;
refreshToken = refreshed.refreshToken || refreshToken;
accountId = refreshed.accountId || accountId;
}
const required = readBoolean(codexOauth.required) !== false;
if (!accessToken) {
if (required) {
throw new Error("Codex OAuth access token is required but missing.");
}
return { init, url };
}
const missingScopes = codexMissingRequiredScopes(accessToken, requiredScopes);
if (missingScopes.length > 0 && required) {
throw new Error(`Codex OAuth access token is missing required scopes: ${missingScopes.join(", ")}.`);
}
const headers = new Headers(init.headers);
const authHeader = readString(codexOauth.authHeader) || "authorization";
const authScheme = readString(codexOauth.authScheme) || "Bearer";
headers.set(authHeader, authScheme ? `${authScheme} ${accessToken}` : accessToken);
accountId = accountId || codexAccountIdFromToken(accessToken);
if (accountId) {
headers.set("ChatGPT-Account-Id", accountId);
}
return {
init: {
...init,
headers
},
url
};
}
function providerPluginAuth(plugin: unknown): Record<string, unknown> | undefined {
if (!isRecord(plugin) || !isRecord(plugin.auth)) {
return undefined;
@@ -779,12 +1199,138 @@ function providerPluginAuth(plugin: unknown): Record<string, unknown> | undefine
return plugin.auth;
}
function providerPluginCodexOauth(plugin: unknown): Record<string, unknown> | undefined {
if (!isRecord(plugin) || !isRecord(plugin.codexOauth)) {
return undefined;
}
return plugin.codexOauth;
}
function providerPluginRequest(plugin: unknown): Record<string, unknown> | undefined {
if (!isRecord(plugin) || !isRecord(plugin.request)) {
return undefined;
}
return plugin.request;
}
function readStringArray(value: unknown): string[] {
return Array.isArray(value)
? value.map(readString).filter((item): item is string => Boolean(item))
: [];
}
async function refreshCodexProbeAccessToken(
codexOauth: Record<string, unknown>,
refreshToken: string,
scope: string
): Promise<CodexProbeOauthRefreshResult> {
const tokenEndpoint =
readString(codexOauth.tokenEndpoint) ||
readString(process.env.CODEX_REFRESH_TOKEN_URL_OVERRIDE) ||
codexOauthTokenEndpoint;
const clientId = readString(codexOauth.clientId) || codexOauthClientId;
const cacheKey = [
tokenEndpoint,
clientId,
scope,
hashSensitiveValue(refreshToken)
].join("\n");
const now = Date.now();
const cached = codexProbeOauthCache.get(cacheKey);
if (cached?.accessToken && cached.expiresAtMs > now + 60_000) {
return cached;
}
const inFlight = inFlightCodexProbeOauthRefreshes.get(cacheKey);
if (inFlight) {
return inFlight;
}
const refresh = refreshCodexProbeAccessTokenUncached(codexOauth, refreshToken, scope, tokenEndpoint, clientId, now)
.finally(() => {
if (inFlightCodexProbeOauthRefreshes.get(cacheKey) === refresh) {
inFlightCodexProbeOauthRefreshes.delete(cacheKey);
}
});
inFlightCodexProbeOauthRefreshes.set(cacheKey, refresh);
return refresh;
}
async function refreshCodexProbeAccessTokenUncached(
codexOauth: Record<string, unknown>,
refreshToken: string,
scope: string,
tokenEndpoint: string,
clientId: string,
now: number
): Promise<CodexProbeOauthRefreshResult> {
const timeoutMs = Math.max(1, Number(codexOauth.timeoutMs) || codexOauthDefaultTimeoutMs);
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetchWithSystemProxy(tokenEndpoint, {
body: JSON.stringify({
client_id: clientId,
grant_type: "refresh_token",
refresh_token: refreshToken,
scope
}),
headers: {
"content-type": "application/json"
},
method: "POST",
signal: controller.signal
});
const text = await response.text();
const payload = parseJson(text);
if (!response.ok) {
throw new Error(`Codex OAuth token refresh returned HTTP ${response.status}${codexTokenRefreshErrorMessage(payload, text)}`);
}
if (!isRecord(payload)) {
throw new Error("Codex OAuth token refresh returned an invalid JSON payload.");
}
const accessToken = readString(payload.access_token) || readString(payload.accessToken);
if (!accessToken) {
throw new Error("Codex OAuth token refresh did not return an access token.");
}
const result = {
accessToken,
accountId: readString(payload.account_id) || readString(payload.accountId) || codexAccountIdFromToken(accessToken),
expiresAtMs: codexTokenExpiresAtMs(accessToken) ?? now + 30 * 60 * 1000,
refreshToken: readString(payload.refresh_token) || readString(payload.refreshToken) || refreshToken
};
codexProbeOauthCache.set([
tokenEndpoint,
clientId,
scope,
hashSensitiveValue(refreshToken)
].join("\n"), result);
pruneCodexProbeOauthCache();
return result;
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
throw new Error(`Codex OAuth token refresh timed out after ${timeoutMs}ms.`);
}
throw error;
} finally {
clearTimeout(timer);
}
}
function pruneCodexProbeOauthCache(): void {
if (codexProbeOauthCache.size <= maxProbeCacheEntries) {
return;
}
const oldestEntries = [...codexProbeOauthCache.entries()]
.sort(([, left], [, right]) => left.expiresAtMs - right.expiresAtMs)
.slice(0, codexProbeOauthCache.size - maxProbeCacheEntries);
for (const [key] of oldestEntries) {
codexProbeOauthCache.delete(key);
}
}
async function requestJson(url: string, init: RequestInit): Promise<FetchJsonResult> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), probeTimeoutMs);
@@ -1319,6 +1865,157 @@ function parseJson(value: string): unknown {
}
}
function codexRequiredScopes(value: unknown): string[] {
if (value === undefined) {
return [...codexOauthRequiredScopes];
}
if (!Array.isArray(value)) {
return [...codexOauthRequiredScopes];
}
if (value.length === 0) {
return [];
}
const scopes: string[] = [];
const seen = new Set<string>();
for (const item of value) {
if (typeof item !== "string" || !item.trim()) {
return [...codexOauthRequiredScopes];
}
for (const scope of item.split(/\s+/)) {
const normalized = scope.trim();
if (normalized && !seen.has(normalized)) {
seen.add(normalized);
scopes.push(normalized);
}
}
}
return scopes.length > 0 ? scopes : [...codexOauthRequiredScopes];
}
function codexOauthScope(configuredScope: string | undefined, requiredScopes: string[]): string {
return uniqueStrings([
...((configuredScope || codexOauthDefaultScope).split(/\s+/)),
...requiredScopes
]).join(" ");
}
function shouldRefreshCodexProbeToken(
accessToken: string | undefined,
codexOauth: Record<string, unknown>,
requiredScopes: string[]
): boolean {
if (readBoolean(codexOauth.forceRefresh)) {
return true;
}
if (!accessToken) {
return readBoolean(codexOauth.refreshIfMissingAccessToken) !== false;
}
if (codexAccessTokenExpired(accessToken)) {
return true;
}
return codexMissingRequiredScopes(accessToken, requiredScopes).length > 0;
}
function codexAccessTokenExpired(token: string | undefined): boolean {
const expiresAtMs = codexTokenExpiresAtMs(token);
return expiresAtMs !== undefined && Date.now() >= expiresAtMs - 60_000;
}
function codexTokenExpiresAtMs(token: string | undefined): number | undefined {
const payload = codexJwtPayload(token);
const exp = typeof payload?.exp === "number" && Number.isFinite(payload.exp) ? payload.exp : undefined;
return exp !== undefined ? exp * 1000 : undefined;
}
function codexMissingRequiredScopes(token: string, requiredScopes: string[]): string[] {
if (requiredScopes.length === 0) {
return [];
}
const payload = codexJwtPayload(token);
if (!payload) {
return [];
}
const scopes = codexTokenScopes(payload);
if (scopes.length === 0) {
return [];
}
return requiredScopes.filter((scope) => !scopes.includes(scope));
}
function codexTokenScopes(payload: Record<string, unknown>): string[] {
const scopes: string[] = [];
const pushScope = (value: unknown) => {
if (typeof value === "string") {
scopes.push(...value.split(/\s+/));
return;
}
if (Array.isArray(value)) {
scopes.push(...value.map(readString).filter((item): item is string => Boolean(item)));
}
};
pushScope(payload.scope);
pushScope(payload.scopes);
pushScope(payload.scp);
return uniqueStrings(scopes);
}
function codexAccountIdFromToken(token: string): string | undefined {
const payload = codexJwtPayload(token);
if (!payload) {
return undefined;
}
const auth = isRecord(payload["https://api.openai.com/auth"])
? payload["https://api.openai.com/auth"]
: {};
return readString(auth.chatgpt_account_id) ||
readString(auth.account_id) ||
readString(auth.accountId) ||
readString(payload.account_id) ||
readString(payload.accountId);
}
function codexJwtPayload(token: string | undefined): Record<string, unknown> | undefined {
const encoded = token?.split(".")[1];
if (!encoded) {
return undefined;
}
try {
const padded = encoded.padEnd(encoded.length + ((4 - encoded.length % 4) % 4), "=");
const decoded = Buffer.from(padded.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8");
const payload = JSON.parse(decoded) as unknown;
return isRecord(payload) ? payload : undefined;
} catch {
return undefined;
}
}
function codexTokenRefreshErrorMessage(payload: unknown, text: string): string {
const message = readPayloadMessage(payload);
if (message) {
return `: ${message}`;
}
const trimmed = text.trim();
return trimmed ? `: ${trimmed.slice(0, 500)}` : "";
}
function readBoolean(value: unknown): boolean | undefined {
if (typeof value === "boolean") {
return value;
}
if (typeof value !== "string") {
return undefined;
}
const normalized = value.trim().toLowerCase();
if (normalized === "true") {
return true;
}
if (normalized === "false") {
return false;
}
return undefined;
}
function readString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
+12 -1
View File
@@ -40,6 +40,7 @@ import { ensureProxyCertificateAuthority } from "@ccr/core/proxy/certificates";
import { proxyService } from "@ccr/core/proxy/service";
import { listMcpServerTools } from "@ccr/core/mcp/tool-discovery";
import { closeRequestLogRuntime, getAgentAnalysis, getAgentTracePayload, getRequestLogDetail, getRequestLogs } from "@ccr/core/observability/request-log-store";
import { shouldRecordRequestLogs } from "@ccr/core/observability/raw-trace-sync";
import { getUsageStats } from "@ccr/core/usage/store";
import { gatewayService } from "@ccr/core/gateway/service";
import { shouldRestartGatewayForRuntimeConfigChange } from "@ccr/core/gateway/runtime-change";
@@ -272,7 +273,17 @@ const rpcHandlers: Record<string, RpcHandler> = {
},
applyProfile: async () => applyProfileConfig(await loadAppConfig()),
cancelBotGatewayQrLogin: (request) => cancelBotGatewayQrLogin(request as BotGatewayQrLoginCancelRequest),
checkProviderConnectivity: (request) => checkGatewayProviderConnectivity(request as GatewayProviderConnectivityCheckRequest),
checkProviderConnectivity: async (request) => {
const config = await loadAppConfig();
return checkGatewayProviderConnectivity(request as GatewayProviderConnectivityCheckRequest, {
requestLog: {
bodyCapturePolicy: config.observability.requestLogBodyCapture,
enabled: shouldRecordRequestLogs(config),
maxBodyBytes: config.observability.requestLogMaxBodyBytes,
successSampleRate: config.observability.requestLogSuccessSampleRate
}
});
},
clearProxyNetworkCaptures: () => proxyService.clearNetworkCaptures(),
closeBotGatewayQrWindow: (_request) => ({ closed: false }),
detectProviderIcon: (request) => detectProviderIcon(request as ProviderIconDetectionRequest),
@@ -1,4 +1,7 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { codexDefaultBaseUrl } from "@ccr/core/agents/local-providers/service.ts";
import { createDefaultAppConfig } from "@ccr/core/config/default-config.ts";
@@ -93,6 +96,85 @@ test("Codex OAuth plugins retain the default base URL after runtime identity nor
assert.equal(compiled.providers[0].baseurl, codexDefaultBaseUrl);
});
test("Codex local providers synthesize OAuth plugins when persisted plugins are missing", async (t) => {
const home = useTemporaryCodexHome(t, "ccr-codex-runtime-missing-plugins-");
fs.mkdirSync(path.join(home, ".codex"), { recursive: true });
fs.writeFileSync(path.join(home, ".codex", "auth.json"), JSON.stringify({
tokens: {
access_token: "access-live",
account_id: "acct-live-runtime",
refresh_token: "refresh-live"
}
}));
const config = createDefaultAppConfig();
config.providerPlugins = [];
config.Providers = [
{
api_base_url: codexDefaultBaseUrl,
api_key: "ccr-local-agent-login",
id: "codex-api",
models: ["gpt-5.5"],
name: "Codex API",
type: "openai_responses"
}
];
const compiled = await compileCoreGatewayConfig(
config,
"raw-trace-token",
"billing-usage-token",
"core-auth-token"
);
const codexPlugins = compiled.providerPlugins.filter((item) => String(item.key).includes("codex-oauth"));
assert.equal(compiled.providers[0].baseurl, codexDefaultBaseUrl);
assert.equal(codexPlugins.length, 1);
assert.equal(codexPlugins[0].providerName, "codex-api");
assert.equal(codexPlugins[0].codexOauth.refreshToken, "refresh-live");
});
test("Codex local provider fallback respects disabled OAuth plugins", async (t) => {
const home = useTemporaryCodexHome(t, "ccr-codex-runtime-disabled-plugin-");
fs.mkdirSync(path.join(home, ".codex"), { recursive: true });
fs.writeFileSync(path.join(home, ".codex", "auth.json"), JSON.stringify({
tokens: {
access_token: "access-live",
refresh_token: "refresh-live"
}
}));
const config = createDefaultAppConfig();
config.providerPlugins = [{
codexOauth: {
refreshToken: "refresh-disabled"
},
enabled: false,
key: "ccr-local-agent-codex-api-codex-oauth",
providerName: "Codex API"
}];
config.Providers = [
{
api_base_url: codexDefaultBaseUrl,
api_key: "ccr-local-agent-login",
id: "codex-api",
models: ["gpt-5.5"],
name: "Codex API",
type: "openai_responses"
}
];
const compiled = await compileCoreGatewayConfig(
config,
"raw-trace-token",
"billing-usage-token",
"core-auth-token"
);
const codexPlugins = compiled.providerPlugins.filter((item) => String(item.key).includes("codex-oauth"));
assert.equal(codexPlugins.length, 0);
});
test("credential-free fallback headers use the provider runtime identity", () => {
const provider = {
api_base_url: "https://api.example.test",
@@ -120,3 +202,17 @@ test("credential-free fallback headers use the provider runtime identity", () =>
assert.equal(attempt.headers["x-target-provider"], "provider-runtime-test");
});
function useTemporaryCodexHome(t, prefix) {
const home = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
const previousHome = process.env.CCR_INTERNAL_HOME_DIR;
process.env.CCR_INTERNAL_HOME_DIR = home;
t.after(() => {
if (previousHome === undefined) {
delete process.env.CCR_INTERNAL_HOME_DIR;
} else {
process.env.CCR_INTERNAL_HOME_DIR = previousHome;
}
});
return home;
}
@@ -1,4 +1,7 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import {
newApiKeyUsageFallbackMessageForTest,
@@ -270,6 +273,452 @@ test("connectivity probe applies provider plugin auth for local agent imports",
assert.equal(report.results[0]?.supported, true);
});
test("connectivity probe applies provider plugin request transforms", async (t) => {
const previousFetch = globalThis.fetch;
let called = false;
globalThis.fetch = async (input, init) => {
called = true;
const url = new URL(String(input));
const headers = new Headers(init?.headers);
const body = JSON.parse(String(init?.body ?? "{}"));
assert.equal(url.origin, "http://127.0.0.1:49124");
assert.equal(url.pathname, "/v1/responses");
assert.equal(url.searchParams.get("probe_model"), "probe-model");
assert.equal(headers.get("x-probe-model"), "probe-model");
assert.equal(body.model, "probe-model");
assert.equal(body.max_output_tokens, undefined);
return new Response(JSON.stringify({ id: "ok" }), {
headers: { "content-type": "application/json" },
status: 200
});
};
t.after(() => {
globalThis.fetch = previousFetch;
});
const report = await checkGatewayProviderConnectivity({
apiKey: "sk-test",
candidates: [{
baseUrl: "http://127.0.0.1:49124/v1",
name: "Local Agent",
protocols: ["openai_responses"],
source: "custom"
}],
forceRefresh: true,
models: ["probe-model"],
providerPlugins: [{
request: {
bodyRemove: ["max_output_tokens"],
headers: {
"x-probe-model": "{{ model }}"
},
query: {
probe_model: "{{ request.body.model }}"
}
}
}],
protocols: ["openai_responses"]
});
assert.equal(called, true);
assert.equal(report.passed.length, 1);
assert.equal(report.failed.length, 0);
});
test("connectivity probe refreshes Codex OAuth plugin auth", async (t) => {
const previousFetch = globalThis.fetch;
const accessToken = jwt({
"https://api.openai.com/auth": {
chatgpt_account_id: "acct-refreshed"
},
exp: Math.floor(Date.now() / 1000) + 3600,
scope: "api.connectors.read api.connectors.invoke"
});
const calls = [];
globalThis.fetch = async (input, init) => {
const url = new URL(String(input));
const headers = new Headers(init?.headers);
calls.push({
authorization: headers.get("authorization"),
body: init?.body ? JSON.parse(String(init.body)) : undefined,
chatgptAccountId: headers.get("chatgpt-account-id"),
method: init?.method,
pathname: url.pathname,
url: url.toString()
});
if (url.toString() === "http://127.0.0.1:49122/oauth/token") {
return new Response(JSON.stringify({
access_token: accessToken,
refresh_token: "refresh-next"
}), {
headers: { "content-type": "application/json" },
status: 200
});
}
if (url.origin === "http://127.0.0.1:49122" && url.pathname === "/codex/responses") {
const ok =
headers.get("authorization") === `Bearer ${accessToken}` &&
headers.get("chatgpt-account-id") === "acct-refreshed";
return new Response(JSON.stringify(ok ? { id: "ok" } : { error: { message: "Unauthorized" } }), {
headers: { "content-type": "application/json" },
status: ok ? 200 : 401
});
}
return new Response(JSON.stringify({ error: { message: "Unexpected request" } }), {
headers: { "content-type": "application/json" },
status: 404
});
};
t.after(() => {
globalThis.fetch = previousFetch;
});
const report = await checkGatewayProviderConnectivity({
apiKey: "ccr-local-agent-login",
candidates: [{
baseUrl: "http://127.0.0.1:49122/codex",
name: "Codex API",
protocols: ["openai_responses"],
source: "custom"
}],
forceRefresh: true,
models: ["gpt-5-codex"],
providerPlugins: [{
codexOauth: {
refreshToken: "refresh-current",
tokenEndpoint: "http://127.0.0.1:49122/oauth/token"
},
key: "ccr-local-agent-codex-api-codex-oauth",
providerName: "Codex API"
}],
protocols: ["openai_responses"]
});
assert.equal(report.passed.length, 1);
assert.equal(report.failed.length, 0);
assert.deepEqual(calls.map((call) => call.pathname), ["/oauth/token", "/codex/responses"]);
assert.deepEqual(calls[0]?.body, {
client_id: "app_EMoamEEZ73f0CkXaXp7hrann",
grant_type: "refresh_token",
refresh_token: "refresh-current",
scope: "openid profile email offline_access api.connectors.read api.connectors.invoke"
});
assert.equal(calls[1]?.authorization, `Bearer ${accessToken}`);
assert.equal(calls[1]?.chatgptAccountId, "acct-refreshed");
});
test("connectivity probe applies Codex request defaults for OAuth plugins", async (t) => {
const previousFetch = globalThis.fetch;
const accessToken = jwt({
"https://api.openai.com/auth": {
chatgpt_account_id: "acct-codex-defaults"
},
exp: Math.floor(Date.now() / 1000) + 3600,
scope: "api.connectors.read api.connectors.invoke"
});
let requestBody;
globalThis.fetch = async (input, init) => {
const url = new URL(String(input));
const headers = new Headers(init?.headers);
requestBody = init?.body ? JSON.parse(String(init.body)) : undefined;
if (url.toString() === "https://chatgpt.com/backend-api/codex/responses") {
const ok =
headers.get("authorization") === `Bearer ${accessToken}` &&
headers.get("chatgpt-account-id") === "acct-codex-defaults" &&
requestBody?.max_output_tokens === undefined;
return new Response(JSON.stringify(ok ? { id: "ok" } : { error: { message: "Bad request" } }), {
headers: { "content-type": "application/json" },
status: ok ? 200 : 400
});
}
return new Response(JSON.stringify({ error: { message: "Unexpected request" } }), {
headers: { "content-type": "application/json" },
status: 404
});
};
t.after(() => {
globalThis.fetch = previousFetch;
});
const report = await checkGatewayProviderConnectivity({
apiKey: "ccr-local-agent-login",
candidates: [{
baseUrl: "https://chatgpt.com/backend-api/codex",
name: "Codex API",
protocols: ["openai_responses"],
source: "custom"
}],
forceRefresh: true,
models: ["gpt-5-codex"],
providerPlugins: [{
codexOauth: {
accessToken
},
key: "ccr-local-agent-codex-api-codex-oauth",
providerName: "Codex API"
}],
protocols: ["openai_responses"]
});
assert.equal(report.passed.length, 1);
assert.equal(report.failed.length, 0);
assert.equal(requestBody?.model, "gpt-5-codex");
assert.equal(requestBody?.max_output_tokens, undefined);
});
test("connectivity probe prefers live Codex auth over saved OAuth plugin tokens", async (t) => {
useTemporaryCodexHome(t, "ccr-codex-probe-live-over-plugin-");
const previousFetch = globalThis.fetch;
const savedToken = jwt({
"https://api.openai.com/auth": {
chatgpt_account_id: "acct-saved-plugin"
},
exp: Math.floor(Date.now() / 1000) + 3600,
scope: "api.connectors.read api.connectors.invoke"
});
const liveToken = jwt({
"https://api.openai.com/auth": {
chatgpt_account_id: "acct-live-token"
},
exp: Math.floor(Date.now() / 1000) + 3600,
scope: "api.connectors.read api.connectors.invoke"
});
const calls = [];
fs.mkdirSync(path.join(process.env.CCR_INTERNAL_HOME_DIR, ".codex"), { recursive: true });
fs.writeFileSync(path.join(process.env.CCR_INTERNAL_HOME_DIR, ".codex", "auth.json"), JSON.stringify({
tokens: {
access_token: liveToken,
account_id: "acct-live-file",
refresh_token: "refresh-live"
}
}));
globalThis.fetch = async (input, init) => {
const url = new URL(String(input));
const headers = new Headers(init?.headers);
calls.push({
authorization: headers.get("authorization"),
body: init?.body ? JSON.parse(String(init.body)) : undefined,
chatgptAccountId: headers.get("chatgpt-account-id"),
pathname: url.pathname,
url: url.toString()
});
if (url.toString() === "https://chatgpt.com/backend-api/codex/responses") {
const ok =
headers.get("authorization") === `Bearer ${liveToken}` &&
headers.get("chatgpt-account-id") === "acct-live-file";
return new Response(JSON.stringify(ok ? { id: "ok" } : { error: { message: "Unauthorized" } }), {
headers: { "content-type": "application/json" },
status: ok ? 200 : 401
});
}
return new Response(JSON.stringify({ error: { message: "Unexpected request" } }), {
headers: { "content-type": "application/json" },
status: 404
});
};
t.after(() => {
globalThis.fetch = previousFetch;
});
const report = await checkGatewayProviderConnectivity({
apiKey: "ccr-local-agent-login",
candidates: [{
baseUrl: "https://chatgpt.com/backend-api/codex",
name: "Codex API",
protocols: ["openai_responses"],
source: "custom"
}],
forceRefresh: true,
models: ["gpt-5-codex"],
providerPlugins: [{
codexOauth: {
accessToken: savedToken,
accountId: "acct-saved-plugin"
},
key: "ccr-local-agent-codex-api-codex-oauth",
providerName: "Codex API"
}],
protocols: ["openai_responses"]
});
assert.equal(report.passed.length, 1);
assert.equal(report.failed.length, 0);
assert.deepEqual(calls.map((call) => call.pathname), ["/backend-api/codex/responses"]);
assert.equal(calls[0]?.authorization, `Bearer ${liveToken}`);
assert.equal(calls[0]?.chatgptAccountId, "acct-live-file");
assert.equal(calls[0]?.body?.max_output_tokens, undefined);
});
test("connectivity probe shares concurrent Codex OAuth refreshes", async (t) => {
const previousFetch = globalThis.fetch;
const accessToken = jwt({
"https://api.openai.com/auth": {
chatgpt_account_id: "acct-shared-refresh"
},
exp: Math.floor(Date.now() / 1000) + 3600,
scope: "api.connectors.read api.connectors.invoke"
});
let refreshCalls = 0;
let responseCalls = 0;
globalThis.fetch = async (input, init) => {
const url = new URL(String(input));
const headers = new Headers(init?.headers);
if (url.toString() === "http://127.0.0.1:49125/oauth/token") {
refreshCalls += 1;
await new Promise((resolve) => setTimeout(resolve, 25));
return new Response(JSON.stringify({
access_token: accessToken,
refresh_token: "refresh-shared-next"
}), {
headers: { "content-type": "application/json" },
status: 200
});
}
if (url.origin === "http://127.0.0.1:49125" && url.pathname === "/codex/responses") {
responseCalls += 1;
const ok =
headers.get("authorization") === `Bearer ${accessToken}` &&
headers.get("chatgpt-account-id") === "acct-shared-refresh";
return new Response(JSON.stringify(ok ? { id: "ok" } : { error: { message: "Unauthorized" } }), {
headers: { "content-type": "application/json" },
status: ok ? 200 : 401
});
}
return new Response(JSON.stringify({ error: { message: "Unexpected request" } }), {
headers: { "content-type": "application/json" },
status: 404
});
};
t.after(() => {
globalThis.fetch = previousFetch;
});
const report = await checkGatewayProviderConnectivity({
apiKey: "ccr-local-agent-login",
candidates: [{
baseUrl: "http://127.0.0.1:49125/codex",
name: "Codex API",
protocols: ["openai_responses"],
source: "custom"
}],
forceRefresh: true,
models: ["gpt-5-a", "gpt-5-b"],
providerPlugins: [{
codexOauth: {
refreshToken: "refresh-shared-current",
tokenEndpoint: "http://127.0.0.1:49125/oauth/token"
},
key: "ccr-local-agent-codex-api-codex-oauth",
providerName: "Codex API"
}],
protocols: ["openai_responses"]
});
assert.equal(refreshCalls, 1);
assert.equal(responseCalls, 2);
assert.equal(report.passed.length, 2);
assert.equal(report.failed.length, 0);
});
test("connectivity probe recovers Codex OAuth auth when saved plugin is missing", async (t) => {
useTemporaryCodexHome(t, "ccr-codex-probe-live-auth-");
const previousFetch = globalThis.fetch;
const accessToken = jwt({
"https://api.openai.com/auth": {
chatgpt_account_id: "acct-live-probe"
},
exp: Math.floor(Date.now() / 1000) + 3600,
scope: "api.connectors.read api.connectors.invoke"
});
const calls = [];
fs.mkdirSync(path.join(process.env.CCR_INTERNAL_HOME_DIR, ".codex"), { recursive: true });
fs.writeFileSync(path.join(process.env.CCR_INTERNAL_HOME_DIR, ".codex", "auth.json"), JSON.stringify({
tokens: {
refresh_token: "refresh-live"
}
}));
globalThis.fetch = async (input, init) => {
const url = new URL(String(input));
const headers = new Headers(init?.headers);
calls.push({
authorization: headers.get("authorization"),
body: init?.body ? JSON.parse(String(init.body)) : undefined,
chatgptAccountId: headers.get("chatgpt-account-id"),
pathname: url.pathname,
url: url.toString()
});
if (url.toString() === "https://auth.openai.com/oauth/token") {
return new Response(JSON.stringify({
access_token: accessToken
}), {
headers: { "content-type": "application/json" },
status: 200
});
}
if (url.toString() === "https://chatgpt.com/backend-api/codex/responses") {
const ok =
headers.get("authorization") === `Bearer ${accessToken}` &&
headers.get("chatgpt-account-id") === "acct-live-probe";
return new Response(JSON.stringify(ok ? { id: "ok" } : { error: { message: "Unauthorized" } }), {
headers: { "content-type": "application/json" },
status: ok ? 200 : 401
});
}
return new Response(JSON.stringify({ error: { message: "Unexpected request" } }), {
headers: { "content-type": "application/json" },
status: 404
});
};
t.after(() => {
globalThis.fetch = previousFetch;
});
const report = await checkGatewayProviderConnectivity({
apiKey: "ccr-local-agent-login",
candidates: [{
baseUrl: "https://chatgpt.com/backend-api/codex",
name: "Codex API",
protocols: ["openai_responses"],
source: "custom"
}],
forceRefresh: true,
models: ["gpt-5.5"],
providerPlugins: [],
protocols: ["openai_responses"]
});
assert.equal(report.passed.length, 1);
assert.equal(report.failed.length, 0);
assert.deepEqual(calls.map((call) => call.pathname), ["/oauth/token", "/backend-api/codex/responses"]);
assert.equal(calls[0]?.body?.refresh_token, "refresh-live");
assert.equal(calls[1]?.authorization, `Bearer ${accessToken}`);
assert.equal(calls[1]?.chatgptAccountId, "acct-live-probe");
assert.equal(calls[1]?.body?.max_output_tokens, undefined);
});
test("New API response headers enable key quota account connector", () => {
assert.equal(detectedProviderFromHeaders({ "X-New-Api-Version": "0.8.0" }), "new-api");
assert.equal(detectedProviderFromHeaders({ "x-oneapi-request-id": "req-1" }), "new-api");
@@ -330,3 +779,29 @@ test("New API user self parser returns user balance", () => {
used: 250
}]);
});
function jwt(payload) {
return [
base64url({ alg: "none", typ: "JWT" }),
base64url(payload),
""
].join(".");
}
function base64url(value) {
return Buffer.from(JSON.stringify(value)).toString("base64url");
}
function useTemporaryCodexHome(t, prefix) {
const home = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
const previousHome = process.env.CCR_INTERNAL_HOME_DIR;
process.env.CCR_INTERNAL_HOME_DIR = home;
t.after(() => {
if (previousHome === undefined) {
delete process.env.CCR_INTERNAL_HOME_DIR;
} else {
process.env.CCR_INTERNAL_HOME_DIR = previousHome;
}
});
return home;
}
+5 -38
View File
@@ -26,9 +26,9 @@ import {
persistLanguagePreference, PluginInstallCandidate, PluginMarketplaceEntry, PluginRoutingConfigTarget, PluginSettingsDraft, presetCapabilitiesFromDraft,
probeProviderCandidates, probeProviderDeepLinkPayload, profileAgentLabel, profileAgentOptionsForRuntime, profileDraftWithDetectedAppPath, profileEnvRowsForAgent, ProfileConfig, ProfileOpenSurface, ProfileRuntimeStatus, profileConfigFromDraft, providerAccountApiKeySafetyIssue,
profileOpenCommandFallback, profileOpenSurfaces, ProviderAccountSnapshot, providerApiKeySafetyIssue, ProviderConnectivityCheckReport, ProviderDeepLinkPayload, ProviderDeepLinkRequest, providerIdentitySafetyIssue, providerProbeCandidates,
providerBaseUrl, providerCapabilitiesForProtocols, providerCapabilitiesForSave, providerConnectivityApiKeyFromDraft, providerGlobalBaseUrlForProbe, providerProbeCandidatesApiKeySafetyIssue, providerProbeHasSupportedProtocol, providerProbeInputKey, providerProtocolOptions, providerSelectableProtocolsFromProbe, ProxyNetworkSnapshot,
providerBaseUrl, providerCapabilitiesForProtocols, providerCapabilitiesForSave, providerConnectivityApiKeyFromDraft, providerConnectivityProviderPlugins, providerGlobalBaseUrlForProbe, providerProbeCandidatesApiKeySafetyIssue, providerProbeHasSupportedProtocol, providerProbeInputKey, providerProtocolOptions, providerSelectableProtocolsFromProbe, ProxyNetworkSnapshot,
ProxyStatus, readLanguagePreference, RequestLogListFilter, RequestLogPage, ResolvedLanguage,
ResolvedTheme, resolvePluginInstallPlan, resolveProviderDeepLinkCatalogModels, RouterRule, SettingsPageId,
ResolvedTheme, resolvePluginInstallPlan, resolveProviderDeepLinkCatalogModels, removeLocalAgentProviderPluginsForProvider, RouterRule, SettingsPageId,
routingRewriteFromDraftRow, setProviderPresets, splitLines, translateAppErrorMessage, translateText, TrayBalanceProgressConfig, TrayWidgetConfig,
uniqueProviderProtocols, uniqueRoutingRuleId, updateApiKeyEditableConfig, UsageStatsFilter, UsageStatsRange, UsageStatsSnapshot, useEffect,
useMemo, useReducedMotion, useRef, useState, validateVirtualModelDraft, ViewId,
@@ -128,41 +128,6 @@ function providerPluginKey(value: unknown): string | undefined {
return isPlainRecord(value) && typeof value.key === "string" && value.key.trim() ? value.key.trim() : undefined;
}
function removeLocalAgentProviderPluginsForProvider(
current: unknown[] | undefined,
provider: GatewayProviderConfig | undefined
): unknown[] | undefined {
if (!provider || providerApiKeyValue(provider) !== localAgentProviderApiKey) {
return current;
}
const providerNames = new Set([
provider.name,
provider.type ? `${provider.name}::${provider.type}` : ""
].map((value) => value.trim().toLowerCase()).filter(Boolean));
return (current ?? []).filter((plugin) => !localAgentProviderPluginMatchesProvider(plugin, providerNames));
}
function localAgentProviderPluginMatchesProvider(plugin: unknown, providerNames: Set<string>): boolean {
if (!isPlainRecord(plugin)) {
return false;
}
const key = typeof plugin.key === "string" ? plugin.key.trim().toLowerCase() : "";
if (!key.startsWith("ccr-local-agent-")) {
return false;
}
const pluginProviderName = typeof plugin.providerName === "string"
? plugin.providerName
: typeof plugin.provider === "string"
? plugin.provider
: "";
return providerNames.has(pluginProviderName.trim().toLowerCase());
}
function providerApiKeyValue(provider: GatewayProviderConfig): string {
return provider.api_key || provider.apiKey || provider.apikey || "";
}
function providerNameSlug(value: string): string {
return value
.trim()
@@ -1439,6 +1404,8 @@ function App() {
const apiKey = providerConnectivityApiKeyFromDraft(providerDraft);
const models = mergeProviderModelLists(modelsToCheck ?? mergeProviderModelLists(providerDraft.selectedModels, splitLines(providerDraft.modelsText)));
const protocols = providerDraft.selectedProtocols.length > 0 ? providerDraft.selectedProtocols : [providerDraft.protocol];
const existingProvider = providerEditIndex === undefined ? undefined : draftConfig.Providers[providerEditIndex];
const providerPlugins = providerConnectivityProviderPlugins(providerDraft, draftConfig.providerPlugins, existingProvider);
const candidates = providerProbeCandidates(providerDraft)
.map((candidate) => ({
...candidate,
@@ -1477,7 +1444,7 @@ function App() {
candidates,
forceRefresh: true,
models,
providerPlugins: providerDraft.providerPlugins,
providerPlugins,
protocols
});
if (providerConnectivityRequestId.current !== requestId) {
@@ -751,6 +751,113 @@ export function createProviderDraftFromProvider(provider: GatewayProviderConfig)
};
}
export function providerConnectivityProviderPlugins(
draft: AddProviderDraft,
providerPlugins: unknown[] | undefined,
existingProvider?: GatewayProviderConfig
): unknown[] {
if (draft.providerPlugins.length > 0) {
return draft.providerPlugins.filter(providerPluginEnabled);
}
if (providerConnectivityApiKeyFromDraft(draft) !== localAgentProviderApiKeyValue) {
return [];
}
const names = localAgentProviderPluginNamesForDraft(draft, existingProvider);
return (providerPlugins ?? []).filter((plugin) =>
providerPluginEnabled(plugin) &&
localAgentProviderPluginMatchesNames(plugin, names)
);
}
export function removeLocalAgentProviderPluginsForProvider(
current: unknown[] | undefined,
provider: GatewayProviderConfig | undefined
): unknown[] | undefined {
if (!provider || providerApiKey(provider) !== localAgentProviderApiKeyValue) {
return current;
}
const names = localAgentProviderPluginNamesForProvider(provider);
return (current ?? []).filter((plugin) => !localAgentProviderPluginMatchesNames(plugin, names));
}
function localAgentProviderPluginNamesForDraft(
draft: AddProviderDraft,
existingProvider: GatewayProviderConfig | undefined
): Set<string> {
const existingProviderIdOrName = existingProvider?.id || existingProvider?.name;
return providerPluginMatchNames([
draft.name,
existingProvider?.name,
existingProvider?.id,
existingProviderIdOrName ? providerNameSlug(existingProviderIdOrName) : undefined
], draft.protocol);
}
function localAgentProviderPluginNamesForProvider(provider: GatewayProviderConfig): Set<string> {
const protocol = toProviderProtocol(provider.type) ?? toProviderProtocol(provider.provider);
const providerIdOrName = provider.id || provider.name;
return providerPluginMatchNames([
provider.name,
provider.id,
providerIdOrName ? providerNameSlug(providerIdOrName) : undefined
], protocol);
}
function providerPluginMatchNames(
names: Array<string | undefined>,
protocol: GatewayProviderProtocol | undefined
): Set<string> {
const values = new Set<string>();
for (const name of names) {
const normalized = normalizeProviderPluginName(name);
if (!normalized) {
continue;
}
values.add(normalized);
if (protocol) {
values.add(normalizeProviderPluginName(`${normalized}::${protocol}`) || "");
}
}
values.delete("");
return values;
}
function localAgentProviderPluginMatchesNames(plugin: unknown, names: Set<string>): boolean {
if (!isPlainRecord(plugin)) {
return false;
}
const key = typeof plugin.key === "string" ? plugin.key.trim().toLowerCase() : "";
if (!key.startsWith("ccr-local-agent-")) {
return false;
}
const pluginProviderName = typeof plugin.providerName === "string"
? plugin.providerName
: typeof plugin.provider === "string"
? plugin.provider
: "";
const normalizedProviderName = normalizeProviderPluginName(pluginProviderName);
return Boolean(normalizedProviderName && names.has(normalizedProviderName));
}
function providerPluginEnabled(plugin: unknown): boolean {
return !isPlainRecord(plugin) || plugin.enabled !== false;
}
function normalizeProviderPluginName(value: string | undefined): string | undefined {
const trimmed = value?.trim();
return trimmed ? trimmed.toLowerCase() : undefined;
}
export function providerNameSlug(value: string): string {
return value
.trim()
.toLowerCase()
.replace(/[^a-z0-9_.-]+/g, "-")
.replace(/^-+|-+$/g, "") || "provider";
}
export function createProviderCredentialDraft(index = 0): ProviderCredentialDraft {
return {
apiKey: "",
@@ -22,6 +22,7 @@ import {
providerCapabilitiesForSave,
providerCapabilityBaseUrlForProtocol,
providerConnectivityApiKeyFromDraft,
providerConnectivityProviderPlugins,
providerDisplayIcon,
providerAccountConnectorsTextWithNewApiUserBalanceTemplate,
providerGlobalBaseUrlForProbe,
@@ -29,6 +30,7 @@ import {
providerProtocolOptions,
providerProbeCandidates,
providerSelectableProtocolsFromProbe,
removeLocalAgentProviderPluginsForProvider,
setProviderPresets
} from "@ccr/ui/pages/home/shared/index.tsx";
import { installBrowserGlobals } from "../fixtures/index.ts";
@@ -645,6 +647,72 @@ test("provider connectivity API key follows selected credential mode", () => {
assert.equal(providerConnectivityApiKeyFromDraft({ ...draft, credentialMode: "pool" }), "sk-pool");
});
test("provider connectivity includes saved Codex OAuth plugins while editing", () => {
const provider = {
api_base_url: "https://chatgpt.com/backend-api/codex",
api_key: "ccr-local-agent-login",
id: "codex-api",
models: ["gpt-5.5"],
name: "Codex API",
type: "openai_responses" as const
};
const draft = {
...createProviderDraftFromProvider(provider),
name: "Renamed Codex API"
};
const displayPlugin = {
codexOauth: { refreshToken: "refresh-display" },
key: "ccr-local-agent-codex-api-codex-oauth",
providerName: "Codex API"
};
const runtimePlugin = {
codexOauth: { refreshToken: "refresh-runtime" },
key: "ccr-local-agent-codex-api-codex-oauth-internal",
providerName: "codex-api::openai_responses"
};
const disabledRuntimePlugin = {
codexOauth: { refreshToken: "refresh-disabled" },
enabled: false,
key: "ccr-local-agent-codex-api-codex-oauth-disabled",
providerName: "codex-api::openai_responses"
};
const otherPlugin = {
codexOauth: { refreshToken: "refresh-other" },
key: "ccr-local-agent-other-codex-oauth",
providerName: "Other Codex API"
};
assert.deepEqual(
providerConnectivityProviderPlugins(draft, [displayPlugin, runtimePlugin, disabledRuntimePlugin, otherPlugin], provider),
[displayPlugin, runtimePlugin]
);
assert.deepEqual(
providerConnectivityProviderPlugins({ ...draft, providerPlugins: [otherPlugin] }, [displayPlugin, runtimePlugin], provider),
[otherPlugin]
);
assert.deepEqual(
removeLocalAgentProviderPluginsForProvider([displayPlugin, runtimePlugin, otherPlugin], provider),
[otherPlugin]
);
const poolProvider = {
...provider,
api_key: "",
credentials: [{
api_key: "ccr-local-agent-login",
enabled: true,
id: "login",
name: "Login"
}]
};
const poolDraft = createProviderDraftFromProvider(poolProvider);
assert.equal(poolDraft.credentialMode, "pool");
assert.deepEqual(
providerConnectivityProviderPlugins(poolDraft, [displayPlugin, runtimePlugin, otherPlugin], poolProvider),
[displayPlugin, runtimePlugin]
);
});
test("provider probe keeps catalog model defaults separate from user overrides", () => {
const draft = {
...createProviderDraft([]),