Implement router enhancements

This commit is contained in:
musistudio
2026-08-24 19:02:41 +08:00
parent ccd00632ac
commit 9e7d4f782c
26 changed files with 4147 additions and 11 deletions
+35
View File
@@ -1516,6 +1516,7 @@ function parseProviderModelMetadata(value: unknown): ProviderModelMetadata | und
const effectiveContextWindowPercent = readPercentage(value.effectiveContextWindowPercent ?? value.effective_context_window_percent);
const maxContextWindow = readPositiveInteger(value.maxContextWindow ?? value.max_context_window);
const maxOutputTokens = readPositiveInteger(value.maxOutputTokens ?? value.max_output_tokens ?? value.outputTokens ?? value.output_tokens);
const openRouterDiscountRouting = parseOpenRouterDiscountRouting(value.openRouterDiscountRouting ?? value.open_router_discount_routing);
const pricing = parseProviderModelPricing(value.pricing);
const metadata: ProviderModelMetadata = {
...(Array.isArray(value.additionalSpeedTiers) ? { additionalSpeedTiers: value.additionalSpeedTiers } : {}),
@@ -1531,6 +1532,7 @@ function parseProviderModelMetadata(value: unknown): ProviderModelMetadata | und
...(effectiveContextWindowPercent ? { effectiveContextWindowPercent } : {}),
...(maxContextWindow ? { maxContextWindow } : {}),
...(maxOutputTokens ? { maxOutputTokens } : {}),
...(openRouterDiscountRouting ? { openRouterDiscountRouting } : {}),
...(pricing ? { pricing } : {}),
...(Array.isArray(value.serviceTiers) ? { serviceTiers: value.serviceTiers } : {}),
...(Array.isArray(value.service_tiers) ? { serviceTiers: value.service_tiers } : {}),
@@ -1543,6 +1545,34 @@ function parseProviderModelMetadata(value: unknown): ProviderModelMetadata | und
return Object.keys(metadata).length > 0 ? metadata : undefined;
}
function parseOpenRouterDiscountRouting(value: unknown): ProviderModelMetadata["openRouterDiscountRouting"] {
if (value === true) {
return { enabled: true };
}
if (value === false || !isObject(value)) {
return undefined;
}
const providerBlacklist = parseStringList(value.providerBlacklist ?? value.provider_blacklist ?? value.ignoredProviders ?? value.ignored_providers ?? value.ignore);
const routing: NonNullable<ProviderModelMetadata["openRouterDiscountRouting"]> = {
...(typeof value.allowFallbacks === "boolean" ? { allowFallbacks: value.allowFallbacks } : {}),
...(typeof value.allow_fallbacks === "boolean" ? { allowFallbacks: value.allow_fallbacks } : {}),
...(readNonNegativeNumber(value.cacheHitRate ?? value.cache_hit_rate) !== undefined ? { cacheHitRate: readNonNegativeNumber(value.cacheHitRate ?? value.cache_hit_rate) } : {}),
...(typeof value.enabled === "boolean" ? { enabled: value.enabled } : {}),
...(readPositiveInteger(value.endpointTtlMs ?? value.endpoint_ttl_ms ?? value.priceTtlMs ?? value.price_ttl_ms) ? { endpointTtlMs: readPositiveInteger(value.endpointTtlMs ?? value.endpoint_ttl_ms ?? value.priceTtlMs ?? value.price_ttl_ms) } : {}),
...(readPositiveInteger(value.minOutputTokens ?? value.min_output_tokens) ? { minOutputTokens: readPositiveInteger(value.minOutputTokens ?? value.min_output_tokens) } : {}),
...(readNonNegativeNumber(value.minSavingsRatio ?? value.min_savings_ratio) !== undefined ? { minSavingsRatio: readNonNegativeNumber(value.minSavingsRatio ?? value.min_savings_ratio) } : {}),
...(readNonNegativeNumber(value.minSavingsUsd ?? value.min_savings_usd) !== undefined ? { minSavingsUsd: readNonNegativeNumber(value.minSavingsUsd ?? value.min_savings_usd) } : {}),
...(readNonNegativeNumber(value.minUptime5m ?? value.min_uptime_5m) !== undefined ? { minUptime5m: readNonNegativeNumber(value.minUptime5m ?? value.min_uptime_5m) } : {}),
...(readNonNegativeNumber(value.outputTokenRatio ?? value.output_token_ratio) !== undefined ? { outputTokenRatio: readNonNegativeNumber(value.outputTokenRatio ?? value.output_token_ratio) } : {}),
...(providerBlacklist.length > 0 ? { providerBlacklist } : {}),
...(typeof value.requireParameters === "boolean" ? { requireParameters: value.requireParameters } : {}),
...(typeof value.require_parameters === "boolean" ? { requireParameters: value.require_parameters } : {}),
...(typeof value.respectExistingProviderOrder === "boolean" ? { respectExistingProviderOrder: value.respectExistingProviderOrder } : {}),
...(typeof value.respect_existing_provider_order === "boolean" ? { respectExistingProviderOrder: value.respect_existing_provider_order } : {})
};
return Object.keys(routing).length > 0 ? routing : undefined;
}
function parseProviderModelCapabilities(value: unknown): ProviderModelCapabilities | undefined {
if (!isObject(value)) {
return undefined;
@@ -3287,6 +3317,11 @@ function gatewayPluginPermissionAlias(value: string): string {
case "route":
case "routes":
return "gateway-routes";
case "gateway-request-transform":
case "gateway-request-transforms":
case "request-transform":
case "request-transforms":
return "gateway-request-transforms";
case "proxy":
case "proxy-route":
return "proxy-routes";
+36
View File
@@ -210,6 +210,21 @@ export type ProviderModelPricing = {
outputUsdPerMillionTokens?: number;
};
export type ProviderModelOpenRouterDiscountRoutingConfig = {
allowFallbacks?: boolean;
cacheHitRate?: number;
enabled?: boolean;
endpointTtlMs?: number;
minOutputTokens?: number;
minSavingsRatio?: number;
minSavingsUsd?: number;
minUptime5m?: number;
outputTokenRatio?: number;
providerBlacklist?: string[];
requireParameters?: boolean;
respectExistingProviderOrder?: boolean;
};
export type ProviderModelMetadata = {
additionalSpeedTiers?: unknown[];
capabilities?: ProviderModelCapabilities;
@@ -219,6 +234,7 @@ export type ProviderModelMetadata = {
effectiveContextWindowPercent?: number;
maxContextWindow?: number;
maxOutputTokens?: number;
openRouterDiscountRouting?: ProviderModelOpenRouterDiscountRoutingConfig;
pricing?: ProviderModelPricing;
serviceTiers?: unknown[];
supportsFastMode?: boolean;
@@ -477,6 +493,25 @@ export type ProviderCatalogModelsResult = {
providerName?: string;
};
export type OpenRouterProviderCatalogRequest = {
apiKey?: string;
baseUrl?: string;
model?: string;
};
export type OpenRouterProviderCatalogItem = {
name: string;
quantizations?: string[];
slug: string;
tokensYesterday?: number;
uptimePercent?: number;
};
export type OpenRouterProviderCatalogResult = {
loadedFrom?: string;
providers: OpenRouterProviderCatalogItem[];
};
export type ProviderAccountTestRequest = {
apiKey?: string;
baseUrl: string;
@@ -837,6 +872,7 @@ export const GATEWAY_PLUGIN_PERMISSION_IDS = [
"proxy-routes",
"http-backends",
"provider-account-connectors",
"gateway-request-transforms",
"core-gateway-config",
"core-provider-plugins",
"virtual-model-profiles",
+63
View File
@@ -646,6 +646,7 @@ function normalizeDeepLinkModelMetadata(value: unknown): ProviderModelMetadata |
const effectiveContextWindowPercent = percentage(value.effectiveContextWindowPercent ?? value.effective_context_window_percent);
const maxContextWindow = positiveInteger(value.maxContextWindow ?? value.max_context_window);
const maxOutputTokens = positiveInteger(value.maxOutputTokens ?? value.max_output_tokens ?? value.outputTokens ?? value.output_tokens);
const openRouterDiscountRouting = normalizeDeepLinkOpenRouterDiscountRouting(value.openRouterDiscountRouting ?? value.open_router_discount_routing);
const pricing = normalizeDeepLinkModelPricing(value.pricing);
const defaultReasoningLevelValue = value.defaultReasoningLevel ?? value.default_reasoning_level;
const defaultReasoningLevel = defaultReasoningLevelValue === null
@@ -665,6 +666,7 @@ function normalizeDeepLinkModelMetadata(value: unknown): ProviderModelMetadata |
...(effectiveContextWindowPercent ? { effectiveContextWindowPercent } : {}),
...(maxContextWindow ? { maxContextWindow } : {}),
...(maxOutputTokens ? { maxOutputTokens } : {}),
...(openRouterDiscountRouting ? { openRouterDiscountRouting } : {}),
...(pricing ? { pricing } : {}),
...(Array.isArray(value.serviceTiers) ? { serviceTiers: value.serviceTiers } : {}),
...(Array.isArray(value.service_tiers) ? { serviceTiers: value.service_tiers } : {}),
@@ -675,6 +677,59 @@ function normalizeDeepLinkModelMetadata(value: unknown): ProviderModelMetadata |
return Object.keys(metadata).length > 0 ? metadata : undefined;
}
function normalizeDeepLinkOpenRouterDiscountRouting(value: unknown): ProviderModelMetadata["openRouterDiscountRouting"] {
if (value === true) {
return { enabled: true };
}
if (value === false || !isRecord(value)) {
return undefined;
}
const routing: NonNullable<ProviderModelMetadata["openRouterDiscountRouting"]> = {};
for (const [camelKey, snakeKey] of [
["allowFallbacks", "allow_fallbacks"],
["enabled", "enabled"],
["requireParameters", "require_parameters"],
["respectExistingProviderOrder", "respect_existing_provider_order"]
] as const) {
const raw = value[camelKey] ?? value[snakeKey];
if (typeof raw === "boolean") {
routing[camelKey] = raw;
}
}
for (const [camelKey, snakeKey] of [
["cacheHitRate", "cache_hit_rate"],
["minSavingsRatio", "min_savings_ratio"],
["minSavingsUsd", "min_savings_usd"],
["minUptime5m", "min_uptime_5m"],
["outputTokenRatio", "output_token_ratio"]
] as const) {
const raw = value[camelKey] ?? value[snakeKey];
const parsed = nonNegativeNumber(raw);
if (parsed !== undefined) {
routing[camelKey] = parsed;
}
}
for (const [camelKey, snakeKey] of [
["endpointTtlMs", "endpoint_ttl_ms"],
["minOutputTokens", "min_output_tokens"]
] as const) {
const raw = value[camelKey] ?? value[snakeKey];
const parsed = positiveInteger(raw);
if (parsed !== undefined) {
routing[camelKey] = parsed;
}
}
const priceTtlMs = positiveInteger(value.priceTtlMs ?? value.price_ttl_ms);
if (routing.endpointTtlMs === undefined && priceTtlMs !== undefined) {
routing.endpointTtlMs = priceTtlMs;
}
const providerBlacklist = normalizedStringList(value.providerBlacklist ?? value.provider_blacklist ?? value.ignoredProviders ?? value.ignored_providers ?? value.ignore);
if (providerBlacklist.length > 0) {
routing.providerBlacklist = providerBlacklist;
}
return Object.keys(routing).length > 0 ? routing : undefined;
}
function normalizeDeepLinkReasoningLevels(value: unknown): ProviderModelMetadata["supportedReasoningLevels"] {
if (!Array.isArray(value)) {
return undefined;
@@ -700,6 +755,14 @@ function normalizedString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function normalizedStringList(value: unknown): string[] {
if (Array.isArray(value)) {
return value.flatMap((item) => normalizedStringList(item));
}
const text = normalizedString(value);
return text ? splitModelValue(text) : [];
}
function normalizeDeepLinkModelCapabilities(value: unknown): ProviderModelCapabilities | undefined {
if (!isRecord(value)) {
return undefined;
@@ -16,6 +16,7 @@ export const IPC_CHANNELS = {
appGetLocalAgentProviderCandidates: "ccr:app:get-local-agent-provider-candidates",
appGetProviderAccountSnapshots: "ccr:app:get-provider-account-snapshots",
appGetProviderCatalogModels: "ccr:app:get-provider-catalog-models",
appGetOpenRouterProviderCatalog: "ccr:app:get-openrouter-provider-catalog",
appGetProviderPresets: "ccr:app:get-provider-presets",
appGetProxyCertificateStatus: "ccr:app:get-proxy-certificate-status",
appGetProxyNetworkCaptures: "ccr:app:get-proxy-network-captures",
@@ -263,6 +263,8 @@ export const localObservabilityHeaderNames = new Set([
"x-ccr-claude-app-model-rewrite",
"x-ccr-codex-patch-bridge",
"x-ccr-claude-model-discovery",
"x-ccr-openrouter-discount-model",
"x-ccr-openrouter-discount-provider-id",
"x-ccr-cursor-openai-compat",
"x-ccr-logical-provider",
"x-ccr-provider-credential-chain",
+90 -6
View File
@@ -34,7 +34,7 @@ import { codexMultiAgentBridgeResponseStream, prepareCodexMultiAgentBridgeReques
import { rewriteAnthropicMessageStartModelStream, shouldRewriteAnthropicMessageStartModel } from "@ccr/core/gateway/features/anthropic-response-model";
import { prepareCursorOpenAICompatChatBody } from "@ccr/core/gateway/features/cursor-compat";
import { filteredResponseHeaders, formatError, formatUpstreamErrorForLog, forwardHeaders, inferGatewayClient, readRequestBody, sendJson, shouldCaptureGatewayUsage, shouldSendBody, stripLocalGatewayAuthHeaders } from "@ccr/core/gateway/http/io";
import { serializeJsonBody, takeJsonObject } from "@ccr/core/gateway/http/body";
import { parseJsonObjectSafe, serializeJsonBody, takeJsonObject } from "@ccr/core/gateway/http/body";
import { createGatewayModelsResponse, prepareClaudeAppDiscoveredModelRequest, prepareClaudeCodeDiscoveredModelRequest, shouldServeGatewayModelsResponse } from "@ccr/core/gateway/features/model-discovery";
import { resolveProviderLogName, resolveResponseProviderProtocol, sanitizeHeaderValue } from "@ccr/core/providers/runtime-topology";
import { createBodySampler, requestLogSampled, shouldRecordRequestLogs } from "@ccr/core/observability/raw-trace-sync";
@@ -47,6 +47,8 @@ import { cancelResponseBody, destroyResponseStreams, fetchUpstreamWithFallback,
import { requestProtocolForPath, shouldApplyGatewayRouting } from "@ccr/core/routing/protocol-endpoints";
import { createClaudeCodeWebSearchContinuationContext, createHostedWebSearchProtocolContext, hostedWebSearchProtocolResponseStream, hostedWebSearchUnavailableMessage, prepareClaudeCodeWebSearchContinuationRequestBody, prepareHostedWebSearchProtocolRequestBody, selectClaudeCodeWebSearchContinuationRecords, selectHostedWebSearchProtocolRecords } from "@ccr/core/gateway/features/hosted-web-search/index";
import { isModelAllowedForProfile, profileForApiKey } from "@ccr/core/profiles/model-allowlist";
import { pluginService } from "@ccr/core/plugins/service";
import { finalizeOpenRouterDiscountProviderRouterSelection } from "@ccr/core/plugins/built-ins/openrouter-discount-provider-router";
export type GatewayRequestPipelineDependencies = {
getBrowserWebSearchMcpIntegration: () => BrowserWebSearchMcpIntegration | undefined;
@@ -158,8 +160,24 @@ export class GatewayRequestPipeline {
}
let routeFallback = this.config.Router.fallback;
let routedModel: string | undefined;
let routedSessionId: string | undefined;
let routedTokenCount: number | undefined;
let codexApplyPatchBridgeActive = false;
let codexMultiAgentBridgeActive = false;
const pluginResponseHeaders = new Headers();
let openRouterDiscountSelectionFinalized = false;
let openRouterDiscountUsedCcrFallback = false;
const finalizeOpenRouterDiscountSelection = (ok: boolean) => {
if (openRouterDiscountSelectionFinalized) {
return;
}
openRouterDiscountSelectionFinalized = true;
finalizeOpenRouterDiscountProviderRouterSelection(requestId, {
ok,
routedModel,
usedCcrFallback: openRouterDiscountUsedCcrFallback
});
};
const authenticatedProfile = profileForApiKey(activeConfig, apiKey);
const claudeModelRewriteStartedAt = Date.now();
const claudeModelRewrite = prepareClaudeCodeDiscoveredModelRequest(this.config, request.headers, method, path, bodyToForward);
@@ -336,6 +354,8 @@ export class GatewayRequestPipeline {
headers["x-ccr-route-diagnostics"] = String(routed.decision.diagnostics.length);
}
routeFallback = routed.decision.fallback ?? routeFallback;
routedSessionId = routed.decision.sessionId;
routedTokenCount = routed.decision.tokenCount;
if (routed.decision.model) {
headers["x-ccr-routed-model"] = sanitizeHeaderValue(routed.decision.model);
routedModel = routed.decision.model;
@@ -625,6 +645,62 @@ export class GatewayRequestPipeline {
});
}
const pluginTransformStartedAt = Date.now();
const pluginTransform = await pluginService.applyGatewayRequestTransforms({
body: parseJsonObjectSafe(bodyToForward),
headers,
method,
path: upstreamPath,
requestId,
...(routedModel ? { routedModel } : {}),
...(routedSessionId ? { sessionId: routedSessionId } : {}),
...(routedTokenCount !== undefined ? { tokenCount: routedTokenCount } : {}),
url: request.url ?? path
});
if (pluginTransform.applied.length > 0) {
if (pluginTransform.body) {
bodyToForward = serializeJsonBody(pluginTransform.body);
}
for (const name of Object.keys(headers)) {
delete headers[name];
}
Object.assign(headers, pluginTransform.headers);
if (pluginTransform.body) {
headers["content-type"] = "application/json";
}
routedModel = pluginTransform.routedModel ?? routedModel;
for (const [name, value] of Object.entries(pluginTransform.responseHeaders)) {
pluginResponseHeaders.set(name, value);
}
for (const applied of pluginTransform.applied) {
routeTrace?.capture({
changes: applied.changes,
decision: {
reason: `plugin:${applied.id}`,
source: "plugin"
},
durationMs: Date.now() - pluginTransformStartedAt,
kind: "mutation",
name: `plugin.request-transform:${applied.pluginId}:${applied.id}`,
phase: "routing",
startedAtMs: pluginTransformStartedAt,
target: pluginTransform.routedModel ? { model: pluginTransform.routedModel } : undefined
});
}
}
const effectiveModelAfterPlugin = routedModel ?? requestLogRequestedModel(bodyToForward ?? requestBody, path);
const deniedPluginModel = profileDeniedModel(activeConfig, authenticatedProfile, modelBeforeRouting, effectiveModelAfterPlugin);
if (deniedPluginModel) {
finalizeOpenRouterDiscountSelection(false);
sendJson(response, 403, {
error: {
code: "profile_model_not_allowed",
message: `Model "${deniedPluginModel}" is not allowed for this profile.`
}
});
return;
}
const contentLengthHeader = headers["content-length"];
delete headers["content-length"];
const upstreamPreparationChanges: RequestRouteTraceChange[] = contentLengthHeader === undefined
@@ -649,6 +725,7 @@ export class GatewayRequestPipeline {
upstreamUrl
});
} catch (error) {
finalizeOpenRouterDiscountSelection(false);
failContextArchiveRequest(contextArchiveRecord, contextArchiveRequestConfig);
const failedAttempts = error instanceof UpstreamRequestError ? error.failedAttempts : [];
const message = formatUpstreamErrorForLog(error, {
@@ -664,9 +741,10 @@ export class GatewayRequestPipeline {
routedModel = error.attempt?.model ?? routedModel;
}
if (clientDisconnected || upstreamAbortController.signal.aborted) {
writeRequestLog(clientClosedRequestStatusCode, new Headers(), "", false, clientDisconnectMessage);
writeRequestLog(clientClosedRequestStatusCode, new Headers(pluginResponseHeaders), "", false, clientDisconnectMessage);
return;
}
const errorResponseHeaders = new Headers(pluginResponseHeaders);
if (shouldCaptureUsage) {
recordUsage({
bodyText: "",
@@ -675,14 +753,14 @@ export class GatewayRequestPipeline {
fallbackModel: routedModel,
method,
path,
providerName: resolveProviderLogName(new Headers(), this.config, routedModel),
providerProtocol: resolveResponseProviderProtocol(new Headers(), this.config),
providerName: resolveProviderLogName(errorResponseHeaders, this.config, routedModel),
providerProtocol: resolveResponseProviderProtocol(errorResponseHeaders, this.config),
requestId,
responseHeaders: new Headers(),
responseHeaders: errorResponseHeaders,
statusCode: 502
});
}
writeRequestLog(502, new Headers(), "", false, message);
writeRequestLog(502, errorResponseHeaders, "", false, message);
throw error;
}
@@ -706,6 +784,7 @@ export class GatewayRequestPipeline {
bodyToForward = upstreamResult.attempt.body ?? bodyToForward;
routedModel = upstreamResult.attempt.model ?? routedModel;
}
openRouterDiscountUsedCcrFallback = upstreamResult.failedAttempts.length > 0;
const responseHeaders = rewriteCapabilityResponseHeaders(
// Copy into a mutable Headers instance: upstream fetch Response.headers
// can be immutable (TypeError: immutable on .delete/.set), and
@@ -716,6 +795,7 @@ export class GatewayRequestPipeline {
new Headers(mergeFallbackResponseHeaders(upstreamResponseHeaders(upstreamResult), upstreamResult)),
this.config
);
pluginResponseHeaders.forEach((value, name) => responseHeaders.set(name, value));
const upstreamResponse = upstreamResult.response;
if (upstreamResponse.ok) {
finalizeContextArchiveRequest(contextArchiveRecord, {
@@ -730,6 +810,7 @@ export class GatewayRequestPipeline {
}
if (clientDisconnected || upstreamAbortController.signal.aborted) {
await cancelResponseBody(upstreamResponse);
finalizeOpenRouterDiscountSelection(false);
writeRequestLog(clientClosedRequestStatusCode, responseHeaders, "", false, clientDisconnectMessage);
return;
}
@@ -766,11 +847,13 @@ export class GatewayRequestPipeline {
recordProviderCredentialOutcome(this.config, method, upstreamResult.attempt, upstreamResponse.status, responseHeaders);
if (clientDisconnected || response.destroyed) {
await cancelResponseBody(upstreamResponse);
finalizeOpenRouterDiscountSelection(false);
writeRequestLog(clientClosedRequestStatusCode, responseHeaders, "", false, clientDisconnectMessage);
return;
}
response.writeHead(upstreamResponse.status, Object.fromEntries(filteredResponseHeaders(responseHeaders)));
if (!upstreamResponse.body) {
finalizeOpenRouterDiscountSelection(upstreamResponse.ok);
if (shouldCaptureUsage) {
recordUsage({
bodyText: "",
@@ -843,6 +926,7 @@ export class GatewayRequestPipeline {
terminalEventSeen: sseErrorDetector.hasTerminalEvent(),
upstreamStatus: upstreamResponse.status
});
finalizeOpenRouterDiscountSelection(outcome.statusCode >= 200 && outcome.statusCode < 400 && !outcome.error);
writeRequestLog(
outcome.statusCode,
responseHeaders,
+56 -2
View File
@@ -24,6 +24,8 @@ import type { ApiKeyLimitUsage, ProviderCredentialRoutingTarget, UpstreamAttempt
import type { RouteTraceObserver } from "@ccr/core/observability/route-trace";
const providerCredentialSpilloverThreshold = 0.8;
const openRouterDiscountModelHeader = "x-ccr-openrouter-discount-model";
const openRouterDiscountProviderHeader = "x-ccr-openrouter-discount-provider-id";
export function applyProviderCapabilityRouting(input: {
@@ -355,7 +357,10 @@ export async function fetchUpstreamWithFallback(input: {
let cachedAttemptRouting = attemptRoutingCache.get(plannedAttempt.model);
if (!cachedAttemptRouting) {
const routedHeaders = { ...input.headers };
const sourceBody = buildAttemptBody(input.body, input.path, plannedAttempt.model);
const sourceBody = buildAttemptBody(input.body, input.path, plannedAttempt.model, {
discountModel: input.headers[openRouterDiscountModelHeader],
discountProvider: input.headers[openRouterDiscountProviderHeader]
});
const routing = applyProviderCapabilityRouting({
body: sourceBody,
config: input.config,
@@ -1073,7 +1078,11 @@ function buildUpstreamAttempts(
function buildAttemptBody(
body: Buffer | undefined,
path: string,
model: string | undefined
model: string | undefined,
options: {
discountModel?: string;
discountProvider?: string;
} = {}
): Buffer | undefined {
if (!body || !model || requestProtocolForPath(path) === "gemini_generate_content") {
return body;
@@ -1082,9 +1091,54 @@ function buildAttemptBody(
if (!parsedBody || stringValue(parsedBody.model) === model) {
return body;
}
if (shouldRemoveOpenRouterDiscountProvider(parsedBody, model, options)) {
const { provider: _provider, ...rest } = parsedBody;
return serializeJsonBody({ ...rest, model });
}
return serializeJsonBodyWithModel(parsedBody, model);
}
function shouldRemoveOpenRouterDiscountProvider(
body: Record<string, unknown>,
model: string,
options: {
discountModel?: string;
discountProvider?: string;
}
): boolean {
if (!options.discountModel || !isRecord(body.provider)) {
return false;
}
return !selectorMatchesOpenRouterDiscountTarget(model, options.discountModel, options.discountProvider);
}
function selectorMatchesOpenRouterDiscountTarget(
model: string,
discountModel: string,
discountProvider: string | undefined
): boolean {
const normalizedModel = normalizeRouteSelector(model)?.toLowerCase() ?? "";
const normalizedDiscountModel = normalizeRouteSelector(discountModel)?.toLowerCase() ?? "";
if (!normalizedModel || !normalizedDiscountModel) {
return false;
}
if (normalizedModel === normalizedDiscountModel) {
return true;
}
if (!normalizedModel.endsWith(`/${normalizedDiscountModel}`)) {
return false;
}
const providerPrefix = normalizedModel.slice(0, normalizedModel.length - normalizedDiscountModel.length - 1);
const providerKey = normalizeProviderHint(discountProvider);
const normalizedProviderPrefix = normalizeProviderHint(providerPrefix);
return normalizedProviderPrefix.includes("openrouter") ||
Boolean(providerKey && normalizedProviderPrefix.includes(providerKey));
}
function normalizeProviderHint(value: unknown): string {
return (stringValue(value) ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "");
}
function routeTraceChange(
scope: RequestRouteTraceChange["scope"],
File diff suppressed because it is too large Load Diff
+5
View File
@@ -536,6 +536,11 @@ function pluginPermissionAlias(value: string): string {
case "route":
case "routes":
return "gateway-routes";
case "gateway-request-transform":
case "gateway-request-transforms":
case "request-transform":
case "request-transforms":
return "gateway-request-transforms";
case "proxy":
case "proxy-route":
return "proxy-routes";
+261
View File
@@ -16,6 +16,7 @@ import {
type ProviderAccountMeter,
type ProviderAccountPluginConnectorConfig,
type ProviderAccountSnapshot,
type RequestRouteTraceChange,
CLAUDE_DESIGN_PLUGIN_ID,
CLAUDE_SHIP_PLUGIN_ID,
GATEWAY_PLUGIN_PERMISSION_IDS,
@@ -23,6 +24,7 @@ import {
knownGatewayPluginDefaultSurfaces
} from "@ccr/core/contracts/app";
import { backendService, type RegisteredHttpBackend, type SqliteStore, type SqliteStoreOptions } from "@ccr/core/plugins/backend-service";
import { openRouterDiscountProviderRouterTransform } from "@ccr/core/plugins/built-ins/openrouter-discount-provider-router";
import { CONFIGDIR, DATADIR } from "@ccr/core/config/constants";
import { isDesktopAppRuntime } from "@ccr/core/runtime/desktop-app";
import type { ProviderAccountWebContentFetchRequest } from "@ccr/core/providers/account-webcontent";
@@ -79,6 +81,55 @@ export type GatewayPluginProviderAccountConnector = {
resolve: (request: GatewayPluginProviderAccountRequest) => MaybePromise<ProviderAccountMeter[] | ProviderAccountSnapshot | undefined>;
};
export type GatewayPluginRequestTransformInput = {
body?: Record<string, unknown>;
headers: Record<string, string>;
method: string;
path: string;
requestId: string;
routedModel?: string;
sessionId?: string;
tokenCount?: number;
url: string;
};
export type GatewayPluginRequestTransformResult = {
body?: Record<string, unknown>;
headers?: Record<string, string | number | boolean | null | undefined>;
responseHeaders?: Record<string, string | number | boolean | null | undefined>;
routedModel?: string;
};
export type GatewayPluginRequestTransformContext = Pick<
GatewayPluginContext,
"config" | "logger" | "openSqliteStore" | "paths" | "permissions" | "pluginConfig" | "pluginId"
>;
export type GatewayPluginRequestTransformHandler = (
input: GatewayPluginRequestTransformInput,
context: GatewayPluginRequestTransformContext
) => MaybePromise<GatewayPluginRequestTransformResult | null | undefined | false>;
export type GatewayPluginRequestTransformRegistration = {
id?: string;
transform: GatewayPluginRequestTransformHandler;
};
export type GatewayPluginRequestTransformApplied = {
changes: RequestRouteTraceChange[];
id: string;
pluginId: string;
responseHeaders: Record<string, string>;
};
export type GatewayPluginRequestTransformOutput = {
applied: GatewayPluginRequestTransformApplied[];
body?: Record<string, unknown>;
headers: Record<string, string>;
responseHeaders: Record<string, string>;
routedModel?: string;
};
export type GatewayPluginStopReason = "disabled" | "reload" | "stop";
export type GatewayPluginStopEvent = {
@@ -95,6 +146,7 @@ export type GatewayPluginRegistration = {
virtualModelProfiles?: unknown[];
};
gatewayRoutes?: GatewayPluginRouteRegistration[];
gatewayRequestTransforms?: GatewayPluginRequestTransformRegistration[];
onStop?: GatewayPluginStopHandler;
providerAccountConnectors?: GatewayPluginProviderAccountConnector[];
proxyRoutes?: GatewayPluginProxyRouteRegistration[];
@@ -118,6 +170,7 @@ export type GatewayPluginContext = {
registerCoreGatewayVirtualModelProfile: (profile: unknown) => void;
registerApp: (app: GatewayPluginAppConfig) => void;
registerGatewayRoute: (route: GatewayPluginRouteRegistration) => void;
registerGatewayRequestTransform: (transform: GatewayPluginRequestTransformRegistration) => void;
registerHttpBackend: (backend: GatewayPluginHttpBackendRegistration) => Promise<RegisteredHttpBackend>;
registerProviderAccountConnector: (connector: GatewayPluginProviderAccountConnector) => void;
registerProxyRoute: (route: GatewayPluginProxyRouteRegistration) => void;
@@ -161,6 +214,10 @@ type RegisteredProxyRoute = Omit<GatewayPluginProxyRouteRegistration, "host" | "
pluginId: string;
};
type RegisteredGatewayRequestTransform = Required<Pick<GatewayPluginRequestTransformRegistration, "id" | "transform">> & {
pluginId: string;
};
type LoadedPlugin = {
activate?: (context: GatewayPluginContext) => MaybePromise<GatewayPluginRegistration | void>;
setup?: (context: GatewayPluginContext) => MaybePromise<GatewayPluginRegistration | void>;
@@ -182,6 +239,7 @@ type PluginServiceStateSnapshot = {
apps: InstalledBrowserApp[];
coreGatewayConfig: Record<string, unknown>;
coreProviderPlugins: unknown[];
gatewayRequestTransforms: RegisteredGatewayRequestTransform[];
gatewayRoutes: RegisteredGatewayRoute[];
providerAccountConnectors: Map<string, GatewayPluginProviderAccountConnector>;
proxyRoutes: RegisteredProxyRoute[];
@@ -197,6 +255,7 @@ class GatewayPluginService {
private coreGatewayConfig: Record<string, unknown> = {};
private coreProviderPlugins: unknown[] = [];
private apps: InstalledBrowserApp[] = [];
private gatewayRequestTransforms: RegisteredGatewayRequestTransform[] = [];
private gatewayRoutes: RegisteredGatewayRoute[] = [];
private proxyRoutes: RegisteredProxyRoute[] = [];
private providerAccountConnectors = new Map<string, GatewayPluginProviderAccountConnector>();
@@ -207,6 +266,7 @@ class GatewayPluginService {
async start(config: AppConfig): Promise<void> {
await this.stop({ nextConfig: config });
this.config = config;
this.registerBuiltInGatewayRequestTransforms();
for (const pluginConfig of config.plugins ?? []) {
if (pluginConfig.enabled === false) {
@@ -249,6 +309,7 @@ class GatewayPluginService {
this.apps = [];
this.coreGatewayConfig = {};
this.coreProviderPlugins = [];
this.gatewayRequestTransforms = [];
this.gatewayRoutes = [];
this.proxyRoutes = [];
this.providerAccountConnectors.clear();
@@ -259,6 +320,81 @@ class GatewayPluginService {
return this.gatewayRoutes.length > 0;
}
async applyGatewayRequestTransforms(input: GatewayPluginRequestTransformInput): Promise<GatewayPluginRequestTransformOutput> {
let body = cloneJsonObject(input.body);
let headers = { ...input.headers };
let routedModel = input.routedModel;
const responseHeaders: Record<string, string> = {};
const applied: GatewayPluginRequestTransformApplied[] = [];
for (const transform of this.gatewayRequestTransforms) {
const beforeBody = body;
const beforeHeaders = headers;
const beforeRoutedModel = routedModel;
let result: GatewayPluginRequestTransformResult | null | undefined | false;
try {
result = await transform.transform({
body: cloneJsonObject(body),
headers: { ...headers },
method: input.method,
path: input.path,
requestId: input.requestId,
...(routedModel ? { routedModel } : {}),
...(input.sessionId ? { sessionId: input.sessionId } : {}),
...(input.tokenCount !== undefined ? { tokenCount: input.tokenCount } : {}),
url: input.url
}, this.createRequestTransformContext(transform.pluginId));
} catch (error) {
console.warn(`[plugin:${transform.pluginId}] Request transform ${transform.id} failed: ${formatError(error)}`);
continue;
}
if (!result || !isRecord(result)) {
continue;
}
const changes: RequestRouteTraceChange[] = [];
const nextBody = isRecord(result.body) ? cloneJsonObject(result.body) : body;
if (nextBody && nextBody !== beforeBody && JSON.stringify(nextBody) !== JSON.stringify(beforeBody)) {
body = nextBody;
changes.push({ operation: beforeBody ? "replace" : "add", path: "/body", scope: "body" });
}
const nextHeaders = applyHeaderPatch(headers, result.headers);
headers = nextHeaders.headers;
changes.push(...nextHeaders.changes(beforeHeaders));
if (typeof result.routedModel === "string" && result.routedModel.trim() && result.routedModel !== beforeRoutedModel) {
routedModel = result.routedModel.trim();
changes.push({
...(beforeRoutedModel ? { before: beforeRoutedModel } : {}),
after: routedModel,
operation: beforeRoutedModel ? "replace" : "add",
path: "/routing/model",
scope: "routing"
});
}
const transformResponseHeaders = normalizedStringHeaders(result.responseHeaders);
Object.assign(responseHeaders, transformResponseHeaders);
if (changes.length > 0 || Object.keys(transformResponseHeaders).length > 0) {
applied.push({
changes,
id: transform.id,
pluginId: transform.pluginId,
responseHeaders: transformResponseHeaders
});
}
}
return {
applied,
...(body ? { body } : {}),
headers,
responseHeaders,
...(routedModel ? { routedModel } : {})
};
}
getCoreGatewayConfig(): Record<string, unknown> {
return { ...this.coreGatewayConfig };
}
@@ -408,6 +544,13 @@ class GatewayPluginService {
for (const route of registration.gatewayRoutes ?? []) {
this.registerGatewayRoute(pluginId, route);
}
if ((registration.gatewayRequestTransforms ?? []).length > 0) {
this.requirePluginSurface(pluginConfig, "gateway", "register gateway request transforms");
this.requirePluginPermission(permissions, "gateway-request-transforms", "register gateway request transforms");
}
for (const transform of registration.gatewayRequestTransforms ?? []) {
this.registerGatewayRequestTransform(pluginId, transform);
}
if ((registration.proxyRoutes ?? []).length > 0) {
this.requirePluginSurface(pluginConfig, "gateway", "register proxy routes");
this.requirePluginPermission(permissions, "proxy-routes", "register proxy routes");
@@ -514,6 +657,25 @@ class GatewayPluginService {
});
}
private registerGatewayRequestTransform(pluginId: string, transform: GatewayPluginRequestTransformRegistration): void {
if (typeof transform.transform !== "function") {
throw new Error(`Plugin ${pluginId} registered an invalid gateway request transform.`);
}
this.gatewayRequestTransforms.push({
id: transform.id?.trim() || `${pluginId}:request-transform:${this.gatewayRequestTransforms.length + 1}`,
pluginId,
transform: transform.transform
});
}
private registerBuiltInGatewayRequestTransforms(): void {
this.gatewayRequestTransforms.push({
id: "openrouter-discount-provider-router",
pluginId: "openrouter",
transform: openRouterDiscountProviderRouterTransform
});
}
private registerProxyRoute(pluginId: string, route: GatewayPluginProxyRouteRegistration): void {
const host = route.host.trim().toLowerCase();
if (!host) {
@@ -570,6 +732,11 @@ class GatewayPluginService {
this.requirePluginPermission(permissions, "gateway-routes", "register gateway routes");
this.registerGatewayRoute(pluginConfig.id, route);
},
registerGatewayRequestTransform: (transform) => {
this.requirePluginSurface(pluginConfig, "gateway", "register gateway request transforms");
this.requirePluginPermission(permissions, "gateway-request-transforms", "register gateway request transforms");
this.registerGatewayRequestTransform(pluginConfig.id, transform);
},
registerHttpBackend: (backend) => {
this.requirePluginSurface(pluginConfig, "gateway", "register HTTP backends");
this.requirePluginPermission(permissions, "http-backends", "register HTTP backends");
@@ -599,6 +766,30 @@ class GatewayPluginService {
});
}
private createRequestTransformContext(pluginId: string): GatewayPluginRequestTransformContext {
const pluginConfig = this.config?.plugins.find((plugin) => plugin.id === pluginId);
const permissions = pluginPermissionAccess(pluginConfig ?? { id: pluginId });
const pluginPermissions = pluginPermissionList(permissions);
const pluginDataDir = path.join(DATADIR, "plugins", sanitizeFileSegment(pluginId));
const logger = createPluginLogger(pluginId);
return {
config: this.config ?? ({} as AppConfig),
logger,
paths: {
configDir: CONFIGDIR,
dataDir: DATADIR,
pluginDataDir
},
permissions: pluginPermissions,
pluginConfig: pluginConfig?.config,
pluginId,
openSqliteStore: (options) => {
this.requirePluginPermission(permissions, "sqlite-store", "open a SQLite store");
return this.openSqliteStore(pluginId, pluginDataDir, options);
}
};
}
private createRouteContext(pluginId: string): GatewayPluginRouteContext {
const pluginConfig = this.config?.plugins.find((plugin) => plugin.id === pluginId);
const permissions = pluginPermissionAccess(pluginConfig ?? { id: pluginId });
@@ -695,6 +886,7 @@ class GatewayPluginService {
apps: [...this.apps],
coreGatewayConfig: { ...this.coreGatewayConfig },
coreProviderPlugins: [...this.coreProviderPlugins],
gatewayRequestTransforms: [...this.gatewayRequestTransforms],
gatewayRoutes: [...this.gatewayRoutes],
providerAccountConnectors: new Map(this.providerAccountConnectors),
proxyRoutes: [...this.proxyRoutes],
@@ -709,6 +901,7 @@ class GatewayPluginService {
this.apps = snapshot.apps;
this.coreGatewayConfig = snapshot.coreGatewayConfig;
this.coreProviderPlugins = snapshot.coreProviderPlugins;
this.gatewayRequestTransforms = snapshot.gatewayRequestTransforms;
this.gatewayRoutes = snapshot.gatewayRoutes;
this.providerAccountConnectors = snapshot.providerAccountConnectors;
this.proxyRoutes = snapshot.proxyRoutes;
@@ -989,6 +1182,74 @@ function sanitizeFileSegment(value: string): string {
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "plugin";
}
function cloneJsonObject(value: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
if (!value) {
return undefined;
}
return JSON.parse(JSON.stringify(value)) as Record<string, unknown>;
}
function applyHeaderPatch(
headers: Record<string, string>,
patch: GatewayPluginRequestTransformResult["headers"]
): { changes: (before: Record<string, string>) => RequestRouteTraceChange[]; headers: Record<string, string> } {
const next = { ...headers };
if (!isRecord(patch)) {
return { changes: () => [], headers: next };
}
const touched = new Set<string>();
for (const [rawName, rawValue] of Object.entries(patch)) {
const name = rawName.trim().toLowerCase();
if (!name) {
continue;
}
touched.add(name);
if (rawValue === undefined || rawValue === null) {
delete next[name];
} else {
next[name] = String(rawValue);
}
}
return {
headers: next,
changes: (before) => [...touched].flatMap((name) => {
const beforeValue = before[name];
const afterValue = next[name];
if (Object.is(beforeValue, afterValue)) {
return [];
}
return [{
...(afterValue === undefined ? {} : { after: afterValue }),
...(beforeValue === undefined ? {} : { before: beforeValue }),
operation: beforeValue === undefined ? "add" : afterValue === undefined ? "remove" : "replace",
path: `/headers/${escapeJsonPointer(name)}`,
scope: "headers"
} satisfies RequestRouteTraceChange];
})
};
}
function normalizedStringHeaders(value: GatewayPluginRequestTransformResult["responseHeaders"]): Record<string, string> {
if (!isRecord(value)) {
return {};
}
const headers: Record<string, string> = {};
for (const [rawName, rawValue] of Object.entries(value)) {
const name = rawName.trim().toLowerCase();
if (!name || rawValue === undefined || rawValue === null) {
continue;
}
headers[name] = String(rawValue);
}
return headers;
}
function escapeJsonPointer(value: string): string {
return value.replace(/~/g, "~0").replace(/\//g, "~1");
}
function providerAccountConnectorKey(pluginId: string, connectorId: string): string {
return `${pluginId.trim()}:${connectorId.trim()}`;
}
@@ -0,0 +1,308 @@
import type {
OpenRouterProviderCatalogItem,
OpenRouterProviderCatalogRequest,
OpenRouterProviderCatalogResult
} from "@ccr/core/contracts/app";
const providerCache = new Map<string, { fetchedAt: number; providers: OpenRouterProviderCatalogItem[] }>();
const providerCacheTtlMs = 10 * 60_000;
const activityCache = new Map<string, { fetchedAt: number; totals: Map<string, number> }>();
export async function getOpenRouterProviderCatalog(
request: OpenRouterProviderCatalogRequest
): Promise<OpenRouterProviderCatalogResult> {
const apiKey = stringValue(request.apiKey);
const apiRoot = openRouterApiRoot(request.baseUrl);
const model = stringValue(request.model);
const cacheKey = model ? `${apiRoot}:model:${model}` : `${apiRoot}:providers`;
const cached = providerCache.get(cacheKey);
const now = Date.now();
if (cached && now - cached.fetchedAt < providerCacheTtlMs) {
return {
loadedFrom: apiRoot,
providers: cached.providers
};
}
const endpointPath = model
? modelEndpointsPath(model)
: "/api/v1/providers";
const response = await fetch(`${trimRight(apiRoot, "/")}${endpointPath}`);
if (!response.ok) {
const text = await response.text().catch(() => "");
throw new Error(`OpenRouter providers request failed (${response.status}): ${text.slice(0, 200)}`);
}
const payload = await response.json() as unknown;
const record = isRecord(payload) ? payload : {};
const data = model && isRecord(record.data) && Array.isArray(record.data.endpoints)
? record.data.endpoints
: Array.isArray(record.data)
? record.data
: [];
const providers = model
? mergeProviderActivity(
normalizeOpenRouterEndpointProviders(data),
await loadYesterdayProviderTokenTotals(apiRoot, apiKey, model)
)
: normalizeOpenRouterProviders(data);
providerCache.set(cacheKey, { fetchedAt: now, providers });
return {
loadedFrom: apiRoot,
providers
};
}
function normalizeOpenRouterProviders(values: unknown[]): OpenRouterProviderCatalogItem[] {
const seen = new Set<string>();
return values.flatMap((value) => {
if (!isRecord(value)) {
return [];
}
const slug = stringValue(value.slug);
const name = stringValue(value.name) || slug;
if (!slug || seen.has(slug)) {
return [];
}
seen.add(slug);
return [{ name, slug }];
}).sort((left, right) => left.name.localeCompare(right.name));
}
function normalizeOpenRouterEndpointProviders(values: unknown[]): OpenRouterProviderCatalogItem[] {
const bySlug = new Map<string, OpenRouterProviderCatalogItem>();
for (const value of values) {
if (!isRecord(value)) {
continue;
}
const explicitName = stringValue(value.provider_name) || stringValue(value.name);
const slug = baseProviderSlug(
stringValue(value.provider_slug) ||
stringValue(value.provider_tag) ||
stringValue(value.tag) ||
slugify(explicitName)
);
const name = explicitName || providerNameFromSlug(slug);
if (!slug) {
continue;
}
const uptimePercent = firstFiniteNumber([
value.uptime_last_1d,
value.uptime_last_30m,
value.uptime_last_5m
]);
const existing = bySlug.get(slug);
const bestUptimePercent = bestPercent(existing?.uptimePercent, uptimePercent);
const quantizations = uniqueDisplayStrings([
...(existing?.quantizations ?? []),
value.quantization
]);
bySlug.set(slug, {
name: (existing?.name ?? name) || slug,
...(quantizations.length > 0 ? { quantizations } : {}),
slug,
...(bestUptimePercent !== undefined
? { uptimePercent: bestUptimePercent }
: {})
});
}
return [...bySlug.values()].sort((left, right) => left.name.localeCompare(right.name));
}
async function loadYesterdayProviderTokenTotals(
apiRoot: string,
apiKey: string,
model: string
): Promise<Map<string, number> | undefined> {
if (!apiKey) {
return undefined;
}
const date = previousUtcDate();
const cacheKey = `${apiRoot}:activity:${date}:${model}`;
const cached = activityCache.get(cacheKey);
const now = Date.now();
if (cached && now - cached.fetchedAt < providerCacheTtlMs) {
return cached.totals;
}
const url = new URL(`${trimRight(apiRoot, "/")}/api/v1/activity`);
url.searchParams.set("date", date);
try {
const response = await fetch(url, {
headers: {
authorization: `Bearer ${apiKey}`
}
});
if (!response.ok) {
return undefined;
}
const payload = await response.json() as unknown;
const record = isRecord(payload) ? payload : {};
const data = Array.isArray(record.data) ? record.data : [];
const totals = normalizeOpenRouterActivity(data, model);
activityCache.set(cacheKey, { fetchedAt: now, totals });
return totals;
} catch {
return undefined;
}
}
function normalizeOpenRouterActivity(values: unknown[], model: string): Map<string, number> {
const totals = new Map<string, number>();
const normalizedModel = model.toLowerCase();
for (const value of values) {
if (!isRecord(value)) {
continue;
}
const activityModel = (stringValue(value.model_permaslug) || stringValue(value.model)).toLowerCase();
if (!activityModel || (activityModel !== normalizedModel && !activityModel.startsWith(`${normalizedModel}-`))) {
continue;
}
const slug = baseProviderSlug(
stringValue(value.provider_slug) ||
stringValue(value.provider_tag) ||
stringValue(value.tag) ||
stringValue(value.provider_name)
);
if (!slug) {
continue;
}
const totalTokens = firstFiniteNumber([
value.total_tokens,
value.tokens
]) ?? sumFiniteNumbers([
value.prompt_tokens,
value.completion_tokens,
value.input_tokens,
value.output_tokens
]);
if (totalTokens <= 0) {
continue;
}
totals.set(slug, (totals.get(slug) ?? 0) + totalTokens);
}
return totals;
}
function mergeProviderActivity(
providers: OpenRouterProviderCatalogItem[],
totals: Map<string, number> | undefined
): OpenRouterProviderCatalogItem[] {
if (!totals || totals.size === 0) {
return providers;
}
return providers.map((provider) => {
const tokensYesterday = totals.get(provider.slug);
return tokensYesterday !== undefined
? { ...provider, tokensYesterday }
: provider;
});
}
function previousUtcDate(): string {
const date = new Date();
date.setUTCDate(date.getUTCDate() - 1);
return date.toISOString().slice(0, 10);
}
function bestPercent(left: number | undefined, right: number | undefined): number | undefined {
if (left === undefined) {
return right;
}
if (right === undefined) {
return left;
}
return Math.max(left, right);
}
function modelEndpointsPath(model: string): string {
const [author, ...slugParts] = model.split("/");
const slug = slugParts.join("/");
if (!author || !slug) {
throw new Error(`OpenRouter model id must be author/slug: ${model}`);
}
return `/api/v1/models/${encodeURIComponent(author)}/${encodeURIComponent(slug)}/endpoints`;
}
function baseProviderSlug(value: string): string {
return slugify(value.split("/")[0] ?? "");
}
function providerNameFromSlug(slug: string): string {
return slug.split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
}
function uniqueDisplayStrings(values: unknown[]): string[] {
const byKey = new Map<string, string>();
for (const value of values) {
const text = stringValue(value);
const key = text.toLowerCase();
if (!text || byKey.has(key)) {
continue;
}
byKey.set(key, text);
}
return [...byKey.values()].sort((left, right) => left.localeCompare(right));
}
function slugify(value: string): string {
return stringValue(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
}
function openRouterApiRoot(baseUrl: unknown): string {
const raw = stringValue(baseUrl) || "https://openrouter.ai/api/v1";
try {
const url = new URL(raw);
url.pathname = url.pathname.replace(/\/api\/v1\/?$/i, "") || "/";
url.search = "";
url.hash = "";
return `${url.origin}${url.pathname === "/" ? "" : trimRight(url.pathname, "/")}`;
} catch {
return "https://openrouter.ai";
}
}
function stringValue(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
}
function firstFiniteNumber(values: unknown[]): number | undefined {
for (const value of values) {
const number = finiteNumber(value);
if (number !== undefined) {
return number;
}
}
return undefined;
}
function sumFiniteNumbers(values: unknown[]): number {
let total = 0;
for (const value of values) {
total += finiteNumber(value) ?? 0;
}
return total;
}
function finiteNumber(value: unknown): number | undefined {
if (typeof value === "number" && Number.isFinite(value)) {
return value;
}
if (typeof value === "string" && value.trim()) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : undefined;
}
return undefined;
}
function trimRight(value: string, char: string): string {
let result = value;
while (result.endsWith(char)) {
result = result.slice(0, -1);
}
return result;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
@@ -31,6 +31,7 @@ import { detectProviderIcon } from "@ccr/core/providers/icons";
import { fetchProviderManifest } from "@ccr/core/providers/manifest-service";
import { getLocalAgentProviderCandidates, importLocalAgentProvider, probeLocalAgentProvider } from "@ccr/core/agents/local-providers/service";
import { getProviderCatalogModels } from "@ccr/core/providers/model-catalog";
import { getOpenRouterProviderCatalog } from "@ccr/core/providers/openrouter-provider-catalog";
import { getProviderPresets } from "@ccr/core/providers/presets/index";
import { checkGatewayProviderConnectivity, probeGatewayProvider, probeGatewayProviderCandidates } from "@ccr/core/providers/probe";
import { stopProviderModelAutoRefreshService, syncProviderModelAutoRefreshService } from "@ccr/core/providers/model-auto-refresh";
@@ -68,6 +69,7 @@ import type {
GatewayStatus,
LocalAgentProviderImportRequest,
LocalAgentProviderProbeRequest,
OpenRouterProviderCatalogRequest,
PluginDependency,
PluginDirectorySelection,
ProfileApplyResult,
@@ -330,6 +332,7 @@ const rpcHandlers: Record<string, RpcHandler> = {
getProfileRuntimeStatus: () => getProfileRuntimeStatus(),
getProviderAccountSnapshots: (provider, options) => getProviderAccountSnapshots(provider as string | undefined, options as ProviderAccountSnapshotRequestOptions | undefined),
getProviderCatalogModels: (request) => getProviderCatalogModels(request as ProviderCatalogModelsRequest),
getOpenRouterProviderCatalog: (request) => getOpenRouterProviderCatalog(request as OpenRouterProviderCatalogRequest),
getProviderPresets: () => getProviderPresets(),
getProxyCertificateStatus: () => proxyService.getCertificateStatus(),
getProxyNetworkCaptures: () => proxyService.getNetworkCaptures(),
@@ -1133,6 +1136,11 @@ function pluginPermissionAlias(value: string): string {
case "route":
case "routes":
return "gateway-routes";
case "gateway-request-transform":
case "gateway-request-transforms":
case "request-transform":
case "request-transforms":
return "gateway-request-transforms";
case "proxy":
case "proxy-route":
return "proxy-routes";
@@ -21,6 +21,7 @@ test("plugin service skips failed plugins and rolls back their registrations", a
module.exports = async function brokenPlugin(context) {
context.registerApp({ id: "broken-context-app", name: "Broken context", url: "http://broken-context.local" });
context.registerGatewayRoute({ id: "broken-context-route", path: "/broken-context", handler(_request, response) { response.end("broken"); } });
context.registerGatewayRequestTransform({ id: "broken-transform", transform(input) { return { headers: { "x-broken-transform": input.requestId } }; } });
context.registerCoreGatewayProviderPlugin({ key: "broken-context-provider" });
context.registerCoreGatewayVirtualModelProfile({ id: "broken-context-vm" });
context.registerProviderAccountConnector({ id: "broken-account", resolve() { return []; } });
@@ -57,6 +58,30 @@ module.exports = {
}]
},
gatewayRoutes: [{ id: "good-route", path: "/good", handler(_request, response) { response.end("good"); } }],
gatewayRequestTransforms: [{
id: "good-transform",
transform(input) {
return {
body: {
...input.body,
model: "OpenRouter/z-ai/glm-5.2",
provider: {
order: ["deepinfra"],
allow_fallbacks: false,
require_parameters: true
}
},
headers: {
...input.headers,
"x-good-transform": String(input.tokenCount) + ":" + input.sessionId
},
responseHeaders: {
"x-good-savings": "0.0001"
},
routedModel: "OpenRouter/z-ai/glm-5.2"
};
}
}],
providerAccountConnectors: [{ id: "good-account", resolve() { return []; } }],
proxyRoutes: [{ host: "good.local", upstream: "http://127.0.0.1" }]
};
@@ -96,6 +121,31 @@ module.exports = {
assert.deepEqual(pluginService.getApps().map((app) => app.id), ["good-app"]);
assert.equal(pluginService.matchGatewayRoute("GET", "/broken-context"), undefined);
assert.equal(pluginService.matchGatewayRoute("GET", "/good")?.id, "good-route");
const transformed = await pluginService.applyGatewayRequestTransforms({
body: { model: "OpenRouter/z-ai/glm-5.2" },
headers: { "content-type": "application/json" },
method: "POST",
path: "/v1/chat/completions",
requestId: "req-1",
routedModel: "OpenRouter/z-ai/glm-5.2",
sessionId: "session-1",
tokenCount: 123,
url: "/v1/chat/completions"
});
assert.equal(transformed.body.model, "OpenRouter/z-ai/glm-5.2");
assert.deepEqual(transformed.body.provider, {
order: ["deepinfra"],
allow_fallbacks: false,
require_parameters: true
});
assert.equal(transformed.headers["x-good-transform"], "123:session-1");
assert.equal(transformed.responseHeaders["x-good-savings"], "0.0001");
assert.equal(transformed.routedModel, "OpenRouter/z-ai/glm-5.2");
assert.deepEqual(transformed.applied.map((item) => item.id), ["good-transform"]);
assert.equal(transformed.applied[0].pluginId, "good");
assert.equal(transformed.applied[0].responseHeaders["x-good-savings"], "0.0001");
assert.ok(transformed.applied[0].changes.some((change) => change.scope === "body"));
assert.ok(transformed.applied[0].changes.some((change) => change.path === "/headers/x-good-transform"));
assert.deepEqual(pluginService.getProxyRouteTargets(), [{ host: "good.local", paths: undefined }]);
assert.deepEqual(pluginService.getCoreProviderPlugins().map((plugin) => plugin.key), ["good-context-provider", "good-provider"]);
assert.deepEqual(pluginService.getVirtualModelProfiles().map((profile) => profile.id), ["good-context-vm", "good-vm"]);
@@ -175,6 +225,126 @@ module.exports = {
}
});
test("built-in OpenRouter discount routing only runs for enabled provider models", async () => {
const originalFetch = globalThis.fetch;
try {
globalThis.fetch = async () => new Response(JSON.stringify({
data: {
endpoints: [
{
provider_name: "Expensive",
provider_slug: "expensive",
status: 0,
supports_implicit_caching: true,
uptime_last_30m: 100,
pricing: {
cache_read: "0.000001",
completion: "0.00002",
prompt: "0.00001"
}
},
{
provider_name: "Mid",
provider_slug: "mid",
status: 0,
supports_implicit_caching: true,
uptime_last_30m: 100,
pricing: {
cache_read: "0.000001",
completion: "0.00001",
prompt: "0.000005"
}
},
{
provider_name: "Cheap",
status: 0,
supports_implicit_caching: true,
uptime_last_30m: 100,
pricing: {
cache_read: "0.000001",
completion: "0.0000048",
prompt: "0.0000024"
}
}
]
}
}), {
headers: { "content-type": "application/json" },
status: 200
});
const config = createDefaultAppConfig();
config.gateway.enabled = false;
config.Providers = [{
apiKey: "sk-or-v1-test",
baseUrl: "https://openrouter.ai/api/v1",
modelMetadata: {
"z-ai/glm-disabled": {
openRouterDiscountRouting: { enabled: false }
},
"z-ai/glm-enabled": {
openRouterDiscountRouting: {
cacheHitRate: 0.75,
enabled: true,
minSavingsRatio: 0,
minSavingsUsd: 0,
providerBlacklist: ["Cheap"]
}
}
},
models: ["z-ai/glm-disabled", "z-ai/glm-enabled"],
name: "OpenRouter",
type: "openai_chat_completions"
}];
await pluginService.start(config);
const skipped = await pluginService.applyGatewayRequestTransforms({
body: { model: "OpenRouter/z-ai/glm-disabled" },
headers: { "content-type": "application/json" },
method: "POST",
path: "/v1/chat/completions",
requestId: "disabled-req",
routedModel: "OpenRouter/z-ai/glm-disabled",
sessionId: "session-disabled",
tokenCount: 1000,
url: "/v1/chat/completions"
});
assert.deepEqual(skipped.applied, []);
const transformed = await pluginService.applyGatewayRequestTransforms({
body: {
model: "OpenRouter/z-ai/glm-enabled",
max_tokens: 500,
provider: { order: ["expensive"] }
},
headers: { "content-type": "application/json" },
method: "POST",
path: "/v1/chat/completions",
requestId: "enabled-req",
routedModel: "OpenRouter/z-ai/glm-enabled",
sessionId: "session-enabled",
tokenCount: 1000,
url: "/v1/chat/completions"
});
assert.deepEqual(transformed.body.provider, {
allow_fallbacks: true,
ignore: ["cheap"],
order: ["mid"],
require_parameters: true
});
assert.deepEqual(transformed.applied.map((item) => item.id), ["openrouter-discount-provider-router"]);
assert.equal(transformed.applied[0].pluginId, "openrouter");
assert.equal(transformed.responseHeaders["x-ccr-openrouter-discount-ignored-providers"], "cheap");
assert.equal(transformed.responseHeaders["x-ccr-openrouter-discount-selected-provider"], "mid");
assert.equal(transformed.responseHeaders["x-ccr-openrouter-discount-cheapest-off-pct"], "50.00");
assert.equal(transformed.responseHeaders["x-ccr-openrouter-discount-savings-usd"], "0.00325");
} finally {
globalThis.fetch = originalFetch;
await pluginService.stop();
}
});
test("plugin gateway route failures are contained to the plugin response", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-plugin-route-failure-"));
const warnings = [];
@@ -81,6 +81,32 @@ test("provider model metadata config preserves custom pricing, image, web search
});
});
test("provider model metadata config preserves OpenRouter discount routing settings", () => {
assert.deepEqual(providerModelMetadataFromConfigForTest({
open_router_discount_routing: {
allow_fallbacks: true,
cache_hit_rate: 0.8,
enabled: true,
min_savings_ratio: 0.1,
min_savings_usd: 0.0002,
price_ttl_ms: 120000,
provider_blacklist: ["cheap-provider", "other-provider"],
require_parameters: false
}
}), {
openRouterDiscountRouting: {
allowFallbacks: true,
cacheHitRate: 0.8,
enabled: true,
endpointTtlMs: 120000,
minSavingsRatio: 0.1,
minSavingsUsd: 0.0002,
providerBlacklist: ["cheap-provider", "other-provider"],
requireParameters: false
}
});
});
test("provider model metadata config drops invalid custom prices", () => {
assert.deepEqual(providerModelMetadataFromConfigForTest({
pricing: {
@@ -42,6 +42,14 @@ test("parseProviderDeepLinkPayload reads payload JSON, models, descriptions, dis
capabilities: { image_input: true, web_search: true },
context_window: 128000,
max_output_tokens: 64000,
open_router_discount_routing: {
enabled: true,
min_savings_ratio: 0.1,
min_savings_usd: 0.0002,
price_ttl_ms: 120000,
provider_blacklist: ["cheap-provider", "other-provider"],
require_parameters: false
},
pricing: {
cache_write_1h_usd_per_million_tokens: 6,
cache_write_5m_usd_per_million_tokens: 3.75,
@@ -83,6 +91,14 @@ test("parseProviderDeepLinkPayload reads payload JSON, models, descriptions, dis
capabilities: { imageInput: true, webSearch: true },
contextWindow: 128000,
maxOutputTokens: 64000,
openRouterDiscountRouting: {
enabled: true,
endpointTtlMs: 120000,
minSavingsRatio: 0.1,
minSavingsUsd: 0.0002,
providerBlacklist: ["cheap-provider", "other-provider"],
requireParameters: false
},
pricing: {
cacheWrite1hUsdPerMillionTokens: 6,
cacheWrite5mUsdPerMillionTokens: 3.75,
@@ -68,6 +68,68 @@ test("retry backoff stops after client aborts a network error", async () => {
});
});
test("OpenRouter discount provider constraints are removed from different fallback model attempts", async () => {
const originalFetch = globalThis.fetch;
const bodies = [];
try {
globalThis.fetch = async (_url, init) => {
bodies.push(JSON.parse(String(init.body)));
return new Response("{}", {
headers: { "content-type": "application/json" },
status: bodies.length === 1 ? 503 : 200
});
};
const result = await fetchUpstreamWithFallback({
body: Buffer.from(JSON.stringify({
messages: [],
model: "OpenRouter/z-ai/glm-primary",
provider: {
ignore: ["legacy"],
order: ["cheap"]
}
})),
config: {
Providers: [],
Router: {
fallback: {
mode: "model-chain",
models: ["OpenRouter/z-ai/glm-fallback"],
retryCount: 0
},
rules: []
},
virtualModelProfiles: []
},
coreAuthToken: "core-token",
fallback: {
mode: "model-chain",
models: ["OpenRouter/z-ai/glm-fallback"],
retryCount: 0
},
headers: {
"x-ccr-openrouter-discount-model": "z-ai/glm-primary",
"x-ccr-openrouter-discount-provider-id": "openrouter"
},
method: "POST",
path: "/v1/chat/completions",
routedModel: "OpenRouter/z-ai/glm-primary",
upstreamUrl: "http://127.0.0.1:3456/v1/chat/completions"
});
assert.equal(result.response.status, 200);
assert.equal(result.failedAttempts.length, 1);
assert.deepEqual(bodies[0].provider, {
ignore: ["legacy"],
order: ["cheap"]
});
assert.equal(bodies[1].model, "OpenRouter/z-ai/glm-fallback");
assert.equal(bodies[1].provider, undefined);
} finally {
globalThis.fetch = originalFetch;
}
});
test("target-provider routing preserves slash-namespaced model ids", () => {
const cases = [
{
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,120 @@
import assert from "node:assert/strict";
import test from "node:test";
import { getOpenRouterProviderCatalog } from "@ccr/core/providers/openrouter-provider-catalog.ts";
test("OpenRouter provider catalog loads provider names and slugs", async () => {
const originalFetch = globalThis.fetch;
let requestedUrl = "";
try {
globalThis.fetch = async (url, init) => {
requestedUrl = String(url);
assert.equal(init?.headers?.authorization, undefined);
return new Response(JSON.stringify({
data: [
{ name: "OpenAI", slug: "openai" },
{ name: "Google Vertex", slug: "google-vertex" },
{ name: "", slug: "" }
]
}), {
headers: { "content-type": "application/json" },
status: 200
});
};
const result = await getOpenRouterProviderCatalog({
baseUrl: "https://openrouter.ai/api/v1"
});
assert.equal(requestedUrl, "https://openrouter.ai/api/v1/providers");
assert.deepEqual(result.providers, [
{ name: "Google Vertex", slug: "google-vertex" },
{ name: "OpenAI", slug: "openai" }
]);
} finally {
globalThis.fetch = originalFetch;
}
});
test("OpenRouter provider catalog loads endpoint providers for a model", async () => {
const originalFetch = globalThis.fetch;
const requestedUrls = [];
try {
globalThis.fetch = async (url, init) => {
requestedUrls.push(String(url));
if (String(url).includes("/api/v1/activity")) {
assert.equal(init?.headers?.authorization, "Bearer sk-or-test");
return new Response(JSON.stringify({
data: [
{ completion_tokens: 500, model: "anthropic/claude-fable-5", prompt_tokens: 1000, provider_name: "OpenAI" },
{ completion_tokens: 15, model: "anthropic/claude-fable-5", prompt_tokens: 10, provider_name: "Google Vertex" },
{ completion_tokens: 1, model: "openai/gpt-5", prompt_tokens: 1, provider_name: "OpenAI" }
]
}), {
headers: { "content-type": "application/json" },
status: 200
});
}
assert.equal(init?.headers?.authorization, undefined);
return new Response(JSON.stringify({
data: {
endpoints: [
{ provider_name: "OpenAI", quantization: "fp16", tag: "openai/flexible", uptime_last_1d: 0.98 },
{ provider_name: "Google Vertex", provider_slug: "google-vertex", quantization: "bf16", uptime_last_30m: 0.991 },
{ provider_name: "OpenAI", quantization: "fp8", tag: "openai/standard", uptime_last_5m: 0.995 }
]
}
}), {
headers: { "content-type": "application/json" },
status: 200
});
};
const result = await getOpenRouterProviderCatalog({
apiKey: "sk-or-test",
baseUrl: "https://openrouter.ai/api/v1",
model: "anthropic/claude-fable-5"
});
assert.equal(requestedUrls[0], "https://openrouter.ai/api/v1/models/anthropic/claude-fable-5/endpoints");
assert.match(requestedUrls[1], /^https:\/\/openrouter\.ai\/api\/v1\/activity\?date=\d{4}-\d{2}-\d{2}$/);
assert.deepEqual(result.providers, [
{ name: "Google Vertex", quantizations: ["bf16"], slug: "google-vertex", tokensYesterday: 25, uptimePercent: 0.991 },
{ name: "OpenAI", quantizations: ["fp16", "fp8"], slug: "openai", tokensYesterday: 1500, uptimePercent: 0.995 }
]);
} finally {
globalThis.fetch = originalFetch;
}
});
test("OpenRouter provider catalog loads model endpoints without an API key", async () => {
const originalFetch = globalThis.fetch;
const requestedUrls = [];
try {
globalThis.fetch = async (url, init) => {
requestedUrls.push(String(url));
assert.equal(init?.headers?.authorization, undefined);
return new Response(JSON.stringify({
data: {
endpoints: [
{ provider_name: "Alibaba", provider_slug: "alibaba", quantization: "fp8", uptime_last_5m: 100 }
]
}
}), {
headers: { "content-type": "application/json" },
status: 200
});
};
const result = await getOpenRouterProviderCatalog({
baseUrl: "https://openrouter-no-key.test/api/v1",
model: "z-ai/glm-5.2"
});
assert.deepEqual(requestedUrls, ["https://openrouter-no-key.test/api/v1/models/z-ai/glm-5.2/endpoints"]);
assert.deepEqual(result.providers, [
{ name: "Alibaba", quantizations: ["fp8"], slug: "alibaba", uptimePercent: 100 }
]);
} finally {
globalThis.fetch = originalFetch;
}
});
+8 -1
View File
@@ -37,6 +37,7 @@ import { fetchProviderManifest } from "@ccr/core/providers/manifest-service";
import { getLocalAgentProviderCandidates, importLocalAgentProvider, probeLocalAgentProvider } from "@ccr/core/agents/local-providers/service";
import { isLaunchAtLoginSupported, syncLaunchAtLogin } from "./launch-at-login";
import { getProviderCatalogModels } from "@ccr/core/providers/model-catalog";
import { getOpenRouterProviderCatalog } from "@ccr/core/providers/openrouter-provider-catalog";
import { getProviderPresets } from "@ccr/core/providers/presets/index";
import { checkGatewayProviderConnectivity, probeGatewayProvider, probeGatewayProviderCandidates } from "@ccr/core/providers/probe";
import { syncProviderModelAutoRefreshService } from "@ccr/core/providers/model-auto-refresh";
@@ -54,7 +55,7 @@ import { getUsageStats } from "@ccr/core/usage/store";
import { applyNativeThemePreference } from "./native-theme";
import { registerProviderAccountWebContentFetchHandler } from "./provider-account-webcontent";
import windowsManager from "./windows";
import { CLAUDE_DESIGN_PLUGIN_ID, GATEWAY_PLUGIN_PERMISSION_IDS, GATEWAY_PLUGIN_SURFACE_IDS, type AgentAnalysisFilter, type AgentAnalysisTracePayloadRequest, type ApiKeyConfig, type AppCaptureElementPngRequest, type AppCaptureElementPngResult, type AppConfig, type AppDataExportResult, type AppImageExportTargetRequest, type AppImageExportTargetResult, type AppInfo, type AppRenderHtmlPngRequest, type AppRenderHtmlPngResult, type AppSaveConfigOptions, type BotGatewayQrLoginCancelRequest, type BotGatewayQrLoginStartRequest, type BotGatewayQrLoginWaitRequest, type BotGatewayQrWindowCloseRequest, type BotGatewayQrWindowOpenRequest, type ChromeLoginImportRequest, type GatewayPluginAppConfig, type GatewayPluginPermission, type GatewayPluginSurface, type GatewayProviderConnectivityCheckRequest, type GatewayProviderProbeCandidatesRequest, type GatewayProviderProbeRequest, type GatewayStatus, type LocalAgentProviderImportRequest, type PluginDependency, type PluginDirectorySelection, type ProfileApplyResult, type ProfileOpenRequest, type ProfileOpenResult, type ProviderAccountResetRequest, type ProviderAccountSnapshotRequestOptions, type ProviderAccountTestRequest, type ProviderCatalogModelsRequest, type ProviderIconDetectionRequest, type ProviderManifestFetchRequest, type RequestLogListFilter, type RouteScriptTestRequest, type RouteScriptValidationRequest, type UsageStatsFilter, type UsageStatsRange } from "@ccr/core/contracts/app";
import { CLAUDE_DESIGN_PLUGIN_ID, GATEWAY_PLUGIN_PERMISSION_IDS, GATEWAY_PLUGIN_SURFACE_IDS, type AgentAnalysisFilter, type AgentAnalysisTracePayloadRequest, type ApiKeyConfig, type AppCaptureElementPngRequest, type AppCaptureElementPngResult, type AppConfig, type AppDataExportResult, type AppImageExportTargetRequest, type AppImageExportTargetResult, type AppInfo, type AppRenderHtmlPngRequest, type AppRenderHtmlPngResult, type AppSaveConfigOptions, type BotGatewayQrLoginCancelRequest, type BotGatewayQrLoginStartRequest, type BotGatewayQrLoginWaitRequest, type BotGatewayQrWindowCloseRequest, type BotGatewayQrWindowOpenRequest, type ChromeLoginImportRequest, type GatewayPluginAppConfig, type GatewayPluginPermission, type GatewayPluginSurface, type GatewayProviderConnectivityCheckRequest, type GatewayProviderProbeCandidatesRequest, type GatewayProviderProbeRequest, type GatewayStatus, type LocalAgentProviderImportRequest, type OpenRouterProviderCatalogRequest, type PluginDependency, type PluginDirectorySelection, type ProfileApplyResult, type ProfileOpenRequest, type ProfileOpenResult, type ProviderAccountResetRequest, type ProviderAccountSnapshotRequestOptions, type ProviderAccountTestRequest, type ProviderCatalogModelsRequest, type ProviderIconDetectionRequest, type ProviderManifestFetchRequest, type RequestLogListFilter, type RouteScriptTestRequest, type RouteScriptValidationRequest, type UsageStatsFilter, type UsageStatsRange } from "@ccr/core/contracts/app";
const imageExportTargets = new Map<string, string>();
const gatewayPluginPermissionIdSet = new Set<string>(GATEWAY_PLUGIN_PERMISSION_IDS);
const gatewayPluginSurfaceIdSet = new Set<string>(GATEWAY_PLUGIN_SURFACE_IDS);
@@ -115,6 +116,7 @@ ipcMain.handle(IPC_CHANNELS.appGetProfileRuntimeStatus, () => {
});
ipcMain.handle(IPC_CHANNELS.appGetProviderAccountSnapshots, (_event, provider?: string, options?: ProviderAccountSnapshotRequestOptions) => getProviderAccountSnapshots(provider, options));
ipcMain.handle(IPC_CHANNELS.appGetProviderCatalogModels, (_event, request: ProviderCatalogModelsRequest) => getProviderCatalogModels(request));
ipcMain.handle(IPC_CHANNELS.appGetOpenRouterProviderCatalog, (_event, request: OpenRouterProviderCatalogRequest) => getOpenRouterProviderCatalog(request));
ipcMain.handle(IPC_CHANNELS.appGetProviderPresets, () => getProviderPresets());
ipcMain.handle(IPC_CHANNELS.appGetAgentAnalysis, (_event, filter?: AgentAnalysisFilter) => getAgentAnalysis(filter));
ipcMain.handle(IPC_CHANNELS.appGetAgentTracePayload, (_event, request: AgentAnalysisTracePayloadRequest) => getAgentTracePayload(request));
@@ -1345,6 +1347,11 @@ function pluginPermissionAlias(value: string): string {
case "route":
case "routes":
return "gateway-routes";
case "gateway-request-transform":
case "gateway-request-transforms":
case "request-transform":
case "request-transforms":
return "gateway-request-transforms";
case "proxy":
case "proxy-route":
return "proxy-routes";
+3
View File
@@ -45,6 +45,8 @@ import type {
LocalAgentProviderImportResult,
LocalAgentProviderProbeRequest,
LocalAgentProviderProbeResult,
OpenRouterProviderCatalogRequest,
OpenRouterProviderCatalogResult,
PluginDirectorySelection,
PluginMarketplaceEntry,
ProfileOpenCommandResult,
@@ -125,6 +127,7 @@ contextBridge.exposeInMainWorld("ccr", {
getProfileRuntimeStatus: () => invoke(IPC_CHANNELS.appGetProfileRuntimeStatus) as Promise<ProfileRuntimeStatus>,
getProviderAccountSnapshots: (provider?: string, options?: ProviderAccountSnapshotRequestOptions) => invoke(IPC_CHANNELS.appGetProviderAccountSnapshots, provider, options) as Promise<ProviderAccountSnapshot[]>,
getProviderCatalogModels: (request: ProviderCatalogModelsRequest) => invoke(IPC_CHANNELS.appGetProviderCatalogModels, request) as Promise<ProviderCatalogModelsResult>,
getOpenRouterProviderCatalog: (request: OpenRouterProviderCatalogRequest) => invoke(IPC_CHANNELS.appGetOpenRouterProviderCatalog, request) as Promise<OpenRouterProviderCatalogResult>,
getProviderPresets: () => invoke(IPC_CHANNELS.appGetProviderPresets) as Promise<ProviderPreset[]>,
getPluginMarketplace: () => invoke(IPC_CHANNELS.appGetPluginMarketplace) as Promise<PluginMarketplaceEntry[]>,
getProxyCertificateStatus: () => invoke(IPC_CHANNELS.appGetProxyCertificateStatus) as Promise<ProxyCertificateStatus>,
@@ -21,7 +21,7 @@ import {
import { PopoverPortal } from "@/components/ui/popover";
import { Tooltip, TooltipPortal } from "@/components/ui/tooltip";
import { providerUrlWithDefaultScheme } from "@ccr/core/providers/url";
import type { ChromeLoginImportJob, LocalAgentProviderCandidate, ProviderAccountHttpJsonConnectorConfig, ProviderAccountWebContentJsonConnectorConfig } from "@ccr/core/contracts/app";
import type { ChromeLoginImportJob, LocalAgentProviderCandidate, OpenRouterProviderCatalogItem, OpenRouterProviderCatalogRequest, ProviderAccountHttpJsonConnectorConfig, ProviderAccountWebContentJsonConnectorConfig } from "@ccr/core/contracts/app";
import type { ReactNode } from "react";
const useClientLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
@@ -2354,6 +2354,13 @@ export function AddProviderForm({
onQueryChange={(modelSearch) => onChange({ modelSearch })}
onRefresh={onRefreshModels}
onSelectedChange={updateConfiguredModels}
openRouterDiscountRouting={draft.presetId === "openrouter"}
openRouterProviderCatalogRequest={draft.presetId === "openrouter"
? {
apiKey: providerConnectivityApiKeyFromDraft(draft),
baseUrl: draft.baseUrl
}
: undefined}
query={draft.modelSearch}
selected={configuredModels}
/>
@@ -3867,6 +3874,8 @@ function ProviderModelPicker({
onQueryChange,
onRefresh,
onSelectedChange,
openRouterDiscountRouting = false,
openRouterProviderCatalogRequest,
query,
selected
}: {
@@ -3879,6 +3888,8 @@ function ProviderModelPicker({
onQueryChange: (value: string) => void;
onRefresh?: () => void | Promise<unknown>;
onSelectedChange: (value: string[]) => void;
openRouterDiscountRouting?: boolean;
openRouterProviderCatalogRequest?: OpenRouterProviderCatalogRequest;
query: string;
selected: string[];
}) {
@@ -4215,6 +4226,8 @@ function ProviderModelPicker({
models={visibleAddedModels}
onChange={onMetadataChange}
onRemoveModel={removeModel}
openRouterDiscountRouting={openRouterDiscountRouting}
openRouterProviderCatalogRequest={openRouterProviderCatalogRequest}
sourceModels={catalog}
/>
</div>
@@ -4223,6 +4236,369 @@ function ProviderModelPicker({
);
}
function OpenRouterProviderBlacklistSelect({
onChange,
request,
value
}: {
onChange: (value: string[]) => void;
request?: OpenRouterProviderCatalogRequest;
value: string[];
}) {
const t = useAppText();
const formatError = useAppErrorText();
const [open, setOpen] = useState(false);
const [query, setQuery] = useState("");
const [providers, setProviders] = useState<OpenRouterProviderCatalogItem[]>([]);
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const [popoverLayout, setPopoverLayout] = useState<{
left: number;
listHeight: number;
maxHeight: number;
offset: number;
placement: "above" | "below";
width: number;
}>();
const panelRef = useRef<HTMLDivElement>(null);
const rootRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const selectedValues = normalizeOpenRouterProviderValues(value);
const selectedSet = new Set(selectedValues);
const options = useMemo(() => openRouterProviderOptions(providers, selectedValues), [providers, selectedValues.join("\n")]);
const normalizedQuery = query.trim().toLowerCase();
const filteredOptions = normalizedQuery
? options.filter((option) => openRouterProviderOptionMatchesQuery(option, normalizedQuery))
: options;
const label = selectedValues.length === 0
? t("No providers blocked")
: `${selectedValues.length} ${t("blocked")}`;
const requestKey = `${request?.baseUrl ?? ""}\n${request?.apiKey ?? ""}\n${request?.model ?? ""}`;
useEffect(() => {
setProviders([]);
setError("");
setLoading(false);
}, [requestKey]);
useClientLayoutEffect(() => {
if (!open) {
setPopoverLayout(undefined);
return;
}
function updatePopoverLayout() {
const root = rootRef.current;
if (!root) {
return;
}
const anchor = root.getBoundingClientRect();
const margin = 12;
const gap = 6;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const availableWidth = Math.max(240, viewportWidth - margin * 2);
const width = Math.min(Math.max(anchor.width, 280), availableWidth);
const left = Math.min(Math.max(margin, anchor.left), viewportWidth - margin - width);
const below = Math.max(0, viewportHeight - anchor.bottom - margin - gap);
const above = Math.max(0, anchor.top - margin - gap);
const placement = below < 280 && above > below ? "above" : "below";
const availableHeight = Math.max(144, placement === "above" ? above : below);
const maxHeight = Math.min(360, availableHeight);
const listHeight = Math.max(120, Math.min(260, maxHeight - 82));
setPopoverLayout({
left,
listHeight,
maxHeight,
offset: placement === "above" ? viewportHeight - anchor.top + gap : anchor.bottom + gap,
placement,
width
});
}
updatePopoverLayout();
window.addEventListener("resize", updatePopoverLayout);
window.addEventListener("scroll", updatePopoverLayout, true);
return () => {
window.removeEventListener("resize", updatePopoverLayout);
window.removeEventListener("scroll", updatePopoverLayout, true);
};
}, [open]);
useEffect(() => {
if (!open) {
return;
}
void loadProviders(false);
}, [open, requestKey]);
useEffect(() => {
if (!open) {
return;
}
const focusTimer = window.setTimeout(() => inputRef.current?.focus(), 0);
const handlePointerDown = (event: MouseEvent) => {
const target = event.target as Node;
if (!rootRef.current?.contains(target) && !panelRef.current?.contains(target)) {
setOpen(false);
}
};
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setOpen(false);
}
};
document.addEventListener("mousedown", handlePointerDown);
document.addEventListener("keydown", handleKeyDown);
return () => {
window.clearTimeout(focusTimer);
document.removeEventListener("mousedown", handlePointerDown);
document.removeEventListener("keydown", handleKeyDown);
};
}, [open]);
function toggleProvider(slug: string) {
const normalized = normalizeOpenRouterProviderValue(slug);
if (!normalized) {
return;
}
const next = selectedSet.has(normalized)
? selectedValues.filter((item) => item !== normalized)
: [...selectedValues, normalized].sort((left, right) => left.localeCompare(right));
onChange(next);
}
async function loadProviders(force = false) {
if (!window.ccr?.getOpenRouterProviderCatalog) {
return;
}
if (!force && (providers.length > 0 || loading)) {
return;
}
if (!request?.model?.trim()) {
setError(t("Select a model to load its OpenRouter providers."));
return;
}
setLoading(true);
setError("");
try {
const result = await window.ccr.getOpenRouterProviderCatalog(request);
setProviders(result.providers);
} catch (errorValue) {
setProviders([]);
setError(formatError(errorValue));
} finally {
setLoading(false);
}
}
return (
<div className="space-y-1.5">
<div className="flex min-w-0 items-center justify-between gap-2">
<Label className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">{t("Provider blacklist")}</Label>
{selectedValues.length > 0 ? (
<Button className="h-6 px-2 text-[10px]" onClick={() => onChange([])} type="button" variant="ghost">
{t("Clear blacklist")}
</Button>
) : null}
</div>
<div className="relative min-w-0" ref={rootRef}>
<button
aria-expanded={open}
aria-haspopup="listbox"
className={cn(
"flex h-9 w-full min-w-0 items-center gap-2 rounded-md border border-input bg-background px-2.5 text-left text-[12px] outline-none transition-colors hover:bg-muted/40 focus-visible:ring-2 focus-visible:ring-ring/25",
open && "border-ring/35 bg-muted/30"
)}
onClick={() => {
setQuery("");
setOpen((current) => !current);
}}
type="button"
>
<span className="min-w-0 flex-1 truncate">{label}</span>
{loading ? <LoaderCircle className="h-3.5 w-3.5 shrink-0 animate-spin text-muted-foreground" /> : null}
<ChevronDown className={cn("h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform", open && "rotate-180")} />
</button>
<PopoverPortal open={open}>
<AnimatedPopover
className="fixed z-[150]"
placement={popoverLayout?.placement ?? "below"}
style={popoverLayout
? {
left: `${popoverLayout.left}px`,
maxHeight: `${popoverLayout.maxHeight}px`,
width: `${popoverLayout.width}px`,
...(popoverLayout.placement === "above"
? { bottom: `${popoverLayout.offset}px` }
: { top: `${popoverLayout.offset}px` })
}
: undefined}
>
<PopoverContent className="w-full overflow-hidden p-1" ref={panelRef}>
<div className="mb-1 flex items-center gap-1">
<div className="relative min-w-0 flex-1">
<Search className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
<Input
aria-label={t("Search providers")}
className="h-8 pl-8"
onChange={(event) => setQuery(event.target.value)}
placeholder={t("Search providers")}
ref={inputRef}
value={query}
/>
</div>
<Button
aria-label={t("Refresh providers")}
className="h-8 w-8 shrink-0"
disabled={loading}
onClick={() => void loadProviders(true)}
size="iconSm"
title={t("Refresh providers")}
type="button"
variant="ghost"
>
<RefreshCw className={cn("h-3.5 w-3.5", loading && "animate-spin")} />
</Button>
</div>
{error ? (
<div className="mb-1 rounded-md border border-amber-500/30 bg-amber-500/5 px-2 py-1.5 text-[11px] leading-4 text-amber-700 dark:text-amber-300">
{error}
</div>
) : null}
<div className="overflow-auto" role="listbox" style={{ maxHeight: `${popoverLayout?.listHeight ?? 240}px` }}>
{loading && options.length === 0 ? (
<div className="flex items-center justify-center gap-2 px-2 py-6 text-[12px] text-muted-foreground">
<LoaderCircle className="h-3.5 w-3.5 animate-spin" />
<span>{t("Loading providers")}</span>
</div>
) : filteredOptions.length > 0 ? (
filteredOptions.map((option) => {
const checked = selectedSet.has(option.slug);
const metricText = openRouterProviderMetricText(option, t);
return (
<button
aria-selected={checked}
className={cn(
"flex min-h-9 w-full min-w-0 items-center gap-2 rounded-[5px] px-2 py-1.5 text-left text-[12px] outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring/25",
checked ? "bg-primary/10 text-primary" : "text-foreground hover:bg-muted"
)}
key={option.slug}
onClick={() => toggleProvider(option.slug)}
role="option"
type="button"
>
<Checkbox checked={checked} readOnly tabIndex={-1} />
<span className="min-w-0 flex-1">
<span className="block truncate font-medium">{option.name}</span>
{metricText ? <span className="block truncate text-[10px] text-muted-foreground">{metricText}</span> : null}
</span>
{checked ? <Check className="h-3.5 w-3.5 shrink-0" /> : null}
</button>
);
})
) : (
<div className="px-2 py-6 text-center text-[12px] text-muted-foreground">{t("No providers found")}</div>
)}
</div>
</PopoverContent>
</AnimatedPopover>
</PopoverPortal>
</div>
<div className="text-[10px] leading-4 text-muted-foreground/75">{t("Excluded OpenRouter providers will be skipped by discount routing and OpenRouter fallbacks.")}</div>
</div>
);
}
function openRouterProviderOptions(
providers: OpenRouterProviderCatalogItem[],
selectedValues: string[]
): OpenRouterProviderCatalogItem[] {
const bySlug = new Map<string, OpenRouterProviderCatalogItem>();
for (const provider of providers) {
const slug = normalizeOpenRouterProviderValue(provider.slug);
if (slug) {
bySlug.set(slug, { ...provider, name: provider.name || slug, slug });
}
}
for (const value of selectedValues) {
if (!bySlug.has(value)) {
bySlug.set(value, { name: value, slug: value });
}
}
return [...bySlug.values()].sort((left, right) => left.name.localeCompare(right.name));
}
function openRouterProviderOptionMatchesQuery(option: OpenRouterProviderCatalogItem, query: string): boolean {
return option.name.toLowerCase().includes(query) ||
option.slug.toLowerCase().includes(query) ||
(option.quantizations ?? []).some((value) => value.toLowerCase().includes(query));
}
function openRouterProviderMetricText(option: OpenRouterProviderCatalogItem, t: (key: string) => string): string {
const parts = [
formatOpenRouterQuantizations(option.quantizations, t),
formatOpenRouterUptime(option.uptimePercent, t),
formatOpenRouterTokens(option.tokensYesterday, t)
].filter(Boolean);
return parts.join(" · ");
}
function formatOpenRouterQuantizations(values: string[] | undefined, t: (key: string) => string): string {
const quantizations = [...new Map((values ?? [])
.map((value) => value.trim())
.filter(Boolean)
.map((value) => [value.toLowerCase(), value])).values()]
.sort((left, right) => left.localeCompare(right));
return quantizations.length > 0 ? `${t("Quantization")} ${quantizations.join(", ")}` : "";
}
function formatOpenRouterUptime(value: number | undefined, t: (key: string) => string): string {
if (value === undefined || !Number.isFinite(value)) {
return "";
}
const percent = value <= 1 ? value * 100 : value;
return `${t("Uptime")} ${percent.toLocaleString(undefined, {
maximumFractionDigits: percent >= 99 ? 2 : 1,
minimumFractionDigits: 0
})}%`;
}
function formatOpenRouterTokens(value: number | undefined, t: (key: string) => string): string {
if (value === undefined || !Number.isFinite(value) || value < 0) {
return "";
}
return `${t("Yesterday")} ${formatCompactNumber(value)} ${t("tokens")}`;
}
function formatCompactNumber(value: number): string {
return value.toLocaleString(undefined, {
maximumFractionDigits: value >= 1_000 ? 1 : 0,
notation: value >= 10_000 ? "compact" : "standard"
});
}
function normalizeOpenRouterProviderValues(values: string[]): string[] {
const seen = new Set<string>();
const result: string[] = [];
for (const value of values) {
const normalized = normalizeOpenRouterProviderValue(value);
if (!normalized || seen.has(normalized)) {
continue;
}
seen.add(normalized);
result.push(normalized);
}
return result;
}
function normalizeOpenRouterProviderValue(value: string): string {
return value.trim().toLowerCase();
}
function ProviderModelListSkeleton() {
const t = useAppText();
@@ -4265,6 +4641,8 @@ function ModelMetadataEditor({
models,
onChange,
onRemoveModel,
openRouterDiscountRouting = false,
openRouterProviderCatalogRequest,
sourceModels
}: {
className?: string;
@@ -4276,6 +4654,8 @@ function ModelMetadataEditor({
models: string[];
onChange: (value: AddProviderDraft["modelMetadata"]) => void;
onRemoveModel?: (model: string) => void;
openRouterDiscountRouting?: boolean;
openRouterProviderCatalogRequest?: OpenRouterProviderCatalogRequest;
sourceModels?: string[];
}) {
const t = useAppText();
@@ -4420,6 +4800,36 @@ function ModelMetadataEditor({
});
}
function updateOpenRouterDiscountRouting(model: string, checked: boolean) {
updateMetadata(model, (current) => {
const next = { ...current };
if (checked) {
next.openRouterDiscountRouting = {
...(current.openRouterDiscountRouting ?? {}),
enabled: true
};
} else {
delete next.openRouterDiscountRouting;
}
return next;
});
}
function updateOpenRouterProviderBlacklist(model: string, providerBlacklist: string[]) {
updateMetadata(model, (current) => {
const routing = { ...(current.openRouterDiscountRouting ?? {}), enabled: true };
if (providerBlacklist.length > 0) {
routing.providerBlacklist = providerBlacklist;
} else {
delete routing.providerBlacklist;
}
return {
...current,
openRouterDiscountRouting: routing
};
});
}
function toggleExpanded(model: string) {
setExpandedModels((current) => {
const next = new Set(current);
@@ -4461,6 +4871,7 @@ function ModelMetadataEditor({
modelMetadata?.maxContextWindow ||
modelMetadata?.pricing ||
modelMetadata?.capabilities ||
modelMetadata?.openRouterDiscountRouting ||
modelMetadata?.supportsFastMode !== undefined ||
modelMetadata?.supportedReasoningLevels !== undefined ||
modelMetadata?.supportsReasoningSummaries !== undefined
@@ -4506,6 +4917,27 @@ function ModelMetadataEditor({
<span className="block truncate text-[11px] font-medium uppercase tracking-wide text-muted-foreground">{t("Model settings")}</span>
<span className="shrink-0 text-[11px] leading-4 text-muted-foreground/75">{t("Context, pricing, reasoning, Fast Mode, web search, and image")}</span>
</div>
{openRouterDiscountRouting ? (
<div className="space-y-2">
<Label className="flex min-w-0 items-center gap-2 text-[12px] font-medium">
<Checkbox
checked={modelMetadata?.openRouterDiscountRouting?.enabled ?? false}
onCheckedChange={(checked) => updateOpenRouterDiscountRouting(model, checked)}
/>
<span>{t("OpenRouter discount routing")}</span>
</Label>
<div className="text-[10px] leading-4 text-muted-foreground/75">{t("Automatically choose the cheapest live OpenRouter provider endpoint when savings exceed estimated cache loss.")}</div>
{modelMetadata?.openRouterDiscountRouting?.enabled ? (
<OpenRouterProviderBlacklistSelect
onChange={(providerBlacklist) => updateOpenRouterProviderBlacklist(model, providerBlacklist)}
request={openRouterProviderCatalogRequest
? { ...openRouterProviderCatalogRequest, model }
: undefined}
value={modelMetadata.openRouterDiscountRouting.providerBlacklist ?? []}
/>
) : null}
</div>
) : null}
<div className="space-y-2">
<div className="flex min-w-0 items-center justify-between gap-2">
<Label className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">{t("Context window (tokens)")}</Label>
@@ -197,6 +197,11 @@ function pluginPermissionAlias(value: string): string {
case "route":
case "routes":
return "gateway-routes";
case "gateway-request-transform":
case "gateway-request-transforms":
case "request-transform":
case "request-transforms":
return "gateway-request-transforms";
case "proxy":
case "proxy-route":
return "proxy-routes";
@@ -179,6 +179,9 @@ import type {
OverviewWidgetSize,
OverviewWidgetType,
OverviewWidgetVariant,
OpenRouterProviderCatalogItem,
OpenRouterProviderCatalogRequest,
OpenRouterProviderCatalogResult,
PluginDependency,
PluginDirectorySelection,
PluginMarketplaceEntry,
@@ -566,7 +569,7 @@ export type {
LucideIcon, AgentAnalysisFilter, AgentAnalysisSessionSelection, AgentAnalysisSnapshot, AgentAnalysisTrace, AgentAnalysisTracePayloadFullResult, AgentAnalysisTracePayloadRequest, AgentAnalysisTraceRun, AgentAnalysisTraceRunKind, AgentKind, AppConfig, AppInfo, AppSaveConfigOptions, AppUpdateStatus, ApiKeyConfig,
ApiKeyLimitConfig, BotGatewayQrLoginCancelRequest, BotGatewayQrLoginCancelResult, BotGatewayQrLoginStartRequest, BotGatewayQrLoginStartResult, BotGatewayQrLoginWaitRequest, BotGatewayQrLoginWaitResult, BotGatewayQrWindowOpenResult, BotGatewayRuntimeConfig, BotGatewaySavedConfig, BotHandoffScanTarget, GatewayProviderConfig, GatewayProviderCapability, GatewayPluginAppConfig, GatewayProviderProbeResult, GatewayProviderProtocol, GatewayMcpServerConfig,
GatewayMcpServerTransport, GatewayMcpStdioMessageMode, GatewayMcpToolInfo, GatewayStatus, OverviewMetricKind, OverviewWidgetConfig, OverviewWidgetSize, OverviewWidgetType,
OverviewWidgetVariant, PluginDependency, PluginDirectorySelection, PluginMarketplaceEntry, ProviderAccountConfig, ProviderAccountConnectorConfig, ProviderAccountHttpJsonConnectorConfig,
OverviewWidgetVariant, OpenRouterProviderCatalogItem, OpenRouterProviderCatalogRequest, OpenRouterProviderCatalogResult, PluginDependency, PluginDirectorySelection, PluginMarketplaceEntry, ProviderAccountConfig, ProviderAccountConnectorConfig, ProviderAccountHttpJsonConnectorConfig,
ProviderAccountMeter, ProviderAccountStandardConnectorConfig, ProviderAccountSnapshot, ProviderAccountTestPath, ProviderAccountTestResult, ProviderDeepLinkPayload, ProviderDeepLinkRequest,
ProviderCredentialConfig,
ProfileConfig, ProfileOpenSurface, ProfileRuntimeStatus, CodexProfileConfigFormat, ProfileScope, ProfileSurface, ProxyCertificateInstallResult, ProxyCertificateStatus, ProxyNetworkBody,
@@ -263,6 +263,22 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Back": "Back",
"Auto fetch latest models": "Auto fetch latest models",
"Poll the provider models endpoint every 10 minutes and add newly discovered models automatically.": "Poll the provider models endpoint every 10 minutes and add newly discovered models automatically.",
"OpenRouter discount routing": "OpenRouter discount routing",
"Automatically choose the cheapest live OpenRouter provider endpoint when savings exceed estimated cache loss.": "Automatically choose the cheapest live OpenRouter provider endpoint when savings exceed estimated cache loss.",
"Provider blacklist": "Provider blacklist",
"No providers blocked": "No providers blocked",
"blocked": "blocked",
"Clear blacklist": "Clear blacklist",
"Search providers": "Search providers",
"Refresh providers": "Refresh providers",
"Loading providers": "Loading providers",
"No providers found": "No providers found",
"Quantization": "Quantization",
"Uptime": "Uptime",
"Yesterday": "Yesterday",
"Enter an OpenRouter API key to load providers.": "Enter an OpenRouter API key to load providers.",
"Select a model to load its OpenRouter providers.": "Select a model to load its OpenRouter providers.",
"Excluded OpenRouter providers will be skipped by discount routing and OpenRouter fallbacks.": "Excluded OpenRouter providers will be skipped by discount routing and OpenRouter fallbacks.",
"Auto detect protocols": "Auto detect protocols",
"Auto detect protocols description": "When enabled, CCR probes the endpoint while editing and uses the detected protocols and models to update this provider. Turn it off to keep manually selected protocols and custom model IDs unchanged.",
"Auto detect protocols info": "Auto detect protocols info",
@@ -901,6 +917,21 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Auto": "自动",
"Auto fetch latest models": "自动拉取最新模型",
"Poll the provider models endpoint every 10 minutes and add newly discovered models automatically.": "每 10 分钟请求一次供应商 models 接口,并自动加入新发现的模型。",
"OpenRouter discount routing": "OpenRouter 折扣路由",
"Automatically choose the cheapest live OpenRouter provider endpoint when savings exceed estimated cache loss.": "当节省金额超过预估缓存失效损失时,自动选择最便宜的 OpenRouter 实时供应商端点。",
"Provider blacklist": "供应商黑名单",
"No providers blocked": "未屏蔽供应商",
"blocked": "已屏蔽",
"Clear blacklist": "清空黑名单",
"Refresh providers": "刷新供应商",
"Loading providers": "正在加载供应商",
"No providers found": "未找到供应商",
"Quantization": "量化",
"Uptime": "可用率",
"Yesterday": "昨日",
"Enter an OpenRouter API key to load providers.": "请输入 OpenRouter API key 后加载供应商。",
"Select a model to load its OpenRouter providers.": "请选择模型以加载它的 OpenRouter 供应商。",
"Excluded OpenRouter providers will be skipped by discount routing and OpenRouter fallbacks.": "黑名单中的 OpenRouter 供应商会被折扣路由和 OpenRouter fallback 跳过。",
"Auto detect protocols": "自动探测协议",
"Auto detect protocols description": "开启后,CCR 会在编辑时探测接口,并用探测到的协议和模型更新此供应商。关闭后,手动选择的协议和自定义模型 ID 会保持不变。",
"Auto detect protocols info": "自动探测协议说明",
+3
View File
@@ -44,6 +44,8 @@ import type {
LocalAgentProviderImportResult,
LocalAgentProviderProbeRequest,
LocalAgentProviderProbeResult,
OpenRouterProviderCatalogRequest,
OpenRouterProviderCatalogResult,
PluginDirectorySelection,
PluginMarketplaceEntry,
ProfileOpenCommandResult,
@@ -113,6 +115,7 @@ declare global {
getProfileRuntimeStatus: () => Promise<ProfileRuntimeStatus>;
getProviderAccountSnapshots: (provider?: string, options?: ProviderAccountSnapshotRequestOptions) => Promise<ProviderAccountSnapshot[]>;
getProviderCatalogModels: (request: ProviderCatalogModelsRequest) => Promise<ProviderCatalogModelsResult>;
getOpenRouterProviderCatalog: (request: OpenRouterProviderCatalogRequest) => Promise<OpenRouterProviderCatalogResult>;
getProviderPresets: () => Promise<ProviderPreset[]>;
getPluginMarketplace: () => Promise<PluginMarketplaceEntry[]>;
getProxyCertificateStatus: () => Promise<ProxyCertificateStatus>;
+1
View File
@@ -128,6 +128,7 @@ const webClientBridge: CcrApi = {
getProfileRuntimeStatus: () => rpc("getProfileRuntimeStatus") as ReturnType<CcrApi["getProfileRuntimeStatus"]>,
getProviderAccountSnapshots: (provider, options) => rpc("getProviderAccountSnapshots", [provider, options]) as ReturnType<CcrApi["getProviderAccountSnapshots"]>,
getProviderCatalogModels: (request) => rpc("getProviderCatalogModels", [request]) as ReturnType<CcrApi["getProviderCatalogModels"]>,
getOpenRouterProviderCatalog: (request) => rpc("getOpenRouterProviderCatalog", [request]) as ReturnType<CcrApi["getOpenRouterProviderCatalog"]>,
getProviderPresets: () => rpc("getProviderPresets") as ReturnType<CcrApi["getProviderPresets"]>,
getProxyCertificateStatus: () => rpc("getProxyCertificateStatus") as ReturnType<CcrApi["getProxyCertificateStatus"]>,
getProxyNetworkCaptures: () => rpc("getProxyNetworkCaptures") as ReturnType<CcrApi["getProxyNetworkCaptures"]>,