mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-08-30 17:11:12 +08:00
Refactor router configuration and request handling
This commit is contained in:
Generated
+75
-630
File diff suppressed because it is too large
Load Diff
+16
-3
@@ -78,7 +78,8 @@
|
||||
"electron-updater": "^6.8.9",
|
||||
"node-forge": "^1.4.0",
|
||||
"openai": "^6.27.0",
|
||||
"undici": "^7.27.2"
|
||||
"pm2": "^7.0.3",
|
||||
"undici": "^7.29.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
@@ -96,7 +97,7 @@
|
||||
"electron": "^42.3.3",
|
||||
"electron-builder": "^26.8.1",
|
||||
"esbuild": "^0.27.7",
|
||||
"js-yaml": "^4.2.0",
|
||||
"js-yaml": "^4.3.1",
|
||||
"lucide-react": "^1.17.0",
|
||||
"motion": "^12.40.0",
|
||||
"react": "^18.3.1",
|
||||
@@ -109,6 +110,18 @@
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"overrides": {
|
||||
"@types/react": "$@types/react"
|
||||
"@types/react": "$@types/react",
|
||||
"fast-uri@3.1.2": "3.1.5",
|
||||
"find-my-way@9.6.0": "9.7.0",
|
||||
"js-yaml": "$js-yaml",
|
||||
"minimatch@10.2.5": {
|
||||
"brace-expansion": "5.0.9"
|
||||
},
|
||||
"pm2": {
|
||||
"js-yaml": "4.3.1"
|
||||
},
|
||||
"socks": {
|
||||
"ip-address": "10.5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,6 @@
|
||||
"@the-next-ai/bot-gateway-sdk": "^0.1.0",
|
||||
"better-sqlite3": "^12.11.1",
|
||||
"node-forge": "^1.4.0",
|
||||
"undici": "^7.27.2"
|
||||
"undici": "^7.29.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@the-next-ai/bot-gateway-sdk": "^0.1.0",
|
||||
"better-sqlite3": "^12.11.1",
|
||||
"node-forge": "^1.4.0",
|
||||
"pm2": "^6.0.13",
|
||||
"undici": "^7.27.2"
|
||||
"pm2": "^7.0.3",
|
||||
"undici": "^7.29.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,14 @@ import { randomUUID } from "node:crypto";
|
||||
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
||||
import type { ApiKeyConfig, AppConfig, GatewayStatus, RouteScriptTestRequest, RouteScriptTestResult, RouteScriptValidationRequest, RouteScriptValidationResult, RouterRule } from "@ccr/core/contracts/app";
|
||||
import { NO_AVAILABLE_GATEWAY_MODELS_MESSAGE, hasAvailableGatewayModels } from "@ccr/core/contracts/app";
|
||||
import { loadAppConfig } from "@ccr/core/config/config";
|
||||
import { backendService } from "@ccr/core/plugins/backend-service";
|
||||
import { getSystemProxyUrlForProtocol } from "@ccr/core/proxy/system-proxy-fetch";
|
||||
import { pluginService } from "@ccr/core/plugins/service";
|
||||
import { proxyService } from "@ccr/core/proxy/service";
|
||||
import { ClaudeCodeRouterPlugin } from "@ccr/core/gateway/claude-code-router-plugin";
|
||||
import { compileCoreGatewayConfig } from "@ccr/core/gateway/core-runtime/config-compiler";
|
||||
import { isAddressInUseMessage, probeExistingCcrGateway } from "@ccr/core/gateway/existing-gateway-probe";
|
||||
import { isAddressInUseMessage, probeExistingCcrGateway, reloadExistingCcrGatewayConfig } from "@ccr/core/gateway/existing-gateway-probe";
|
||||
import { closeServer, formatError } from "@ccr/core/gateway/http/io";
|
||||
import { RawTraceSynchronizer } from "@ccr/core/observability/raw-trace-sync";
|
||||
import { GatewayBillingSynchronizer } from "@ccr/core/usage/billing-sync";
|
||||
@@ -21,6 +22,8 @@ import { coreGatewayAuthHeader } from "@ccr/core/gateway/internal/shared";
|
||||
import type { BrowserAutomationMcpIntegration, BrowserWebSearchMcpIntegration, GatewayStopOptions } from "@ccr/core/gateway/internal/shared";
|
||||
import { GatewayRequestPipeline } from "@ccr/core/gateway/request/pipeline";
|
||||
import { GatewayHttpRequestHandler } from "@ccr/core/gateway/http/request-handler";
|
||||
import { gatewayRuntimeConfigRevision } from "@ccr/core/gateway/runtime-config-control";
|
||||
import { shouldRestartGatewayForRuntimeConfigChange } from "@ccr/core/gateway/runtime-change";
|
||||
import { RouteScriptRuntime } from "@ccr/core/routing/route-script-runtime";
|
||||
import { buildRouteScriptInput } from "@ccr/core/routing/route-script-context";
|
||||
import { compileRouterConfig } from "@ccr/core/routing/config-compiler";
|
||||
@@ -61,6 +64,10 @@ class GatewayService {
|
||||
getBrowserAutomationMcpIntegration: () => this.browserAutomationMcpIntegration,
|
||||
getConfig: () => this.config,
|
||||
getPlugin: () => this.plugin,
|
||||
getRuntimeConfigControlStatus: () => ({
|
||||
...(this.runtimeConfigReloadError ? { lastError: this.runtimeConfigReloadError } : {}),
|
||||
revision: gatewayRuntimeConfigRevision(this.config)
|
||||
}),
|
||||
getStatus: () => ({
|
||||
coreEndpoint: this.status.coreEndpoint,
|
||||
coreManagedExternally: this.status.coreManagedExternally,
|
||||
@@ -70,6 +77,9 @@ class GatewayService {
|
||||
handleRawTraceSync: (request, response) => this.rawTraceSynchronizer.handle(request, response),
|
||||
handleBillingUsageSync: (request, response) => this.billingSynchronizer.handle(request, response),
|
||||
proxyRequest: (request, response, path, apiKey) => this.proxyRequest(request, response, path, apiKey),
|
||||
requestRuntimeConfigReload: (expectedRevision, forceRestart) => {
|
||||
this.schedulePersistedRuntimeConfigReload(expectedRevision, forceRestart);
|
||||
},
|
||||
replayContextArchive: (input) => this.requestPipeline.replayContextArchive(input)
|
||||
});
|
||||
|
||||
@@ -90,11 +100,14 @@ class GatewayService {
|
||||
private child?: ChildProcess;
|
||||
private config?: AppConfig;
|
||||
private coreAuthToken = "";
|
||||
private externalGatewayApiKey?: string;
|
||||
private plugin?: ClaudeCodeRouterPlugin;
|
||||
private readonly rawTraceSynchronizer = new RawTraceSynchronizer({
|
||||
getConfig: () => this.config
|
||||
});
|
||||
private readonly routeScriptRuntime = new RouteScriptRuntime();
|
||||
private runtimeConfigReloadError?: string;
|
||||
private runtimeConfigReloadQueue: Promise<void> = Promise.resolve();
|
||||
private server?: Server;
|
||||
private status: GatewayStatus = {
|
||||
coreEndpoint: "",
|
||||
@@ -216,6 +229,7 @@ class GatewayService {
|
||||
pid: this.child?.pid,
|
||||
state: "running"
|
||||
};
|
||||
this.runtimeConfigReloadError = undefined;
|
||||
return this.status;
|
||||
} catch (error) {
|
||||
await this.stop();
|
||||
@@ -235,7 +249,12 @@ class GatewayService {
|
||||
if (currentStatus.gatewayManagedExternally) {
|
||||
const existingGateway = await probeExistingCcrGateway(config);
|
||||
if (existingGateway.state === "usable") {
|
||||
this.markExternalGatewayRunning(config, existingGateway.endpoint);
|
||||
this.markExternalGatewayRunning(config, existingGateway.endpoint, existingGateway.apiKey);
|
||||
try {
|
||||
await this.reloadExternalGatewayConfig(config, false);
|
||||
} catch {
|
||||
return this.getStatus();
|
||||
}
|
||||
return this.getStatus();
|
||||
}
|
||||
}
|
||||
@@ -255,14 +274,28 @@ class GatewayService {
|
||||
return status;
|
||||
}
|
||||
|
||||
this.markExternalGatewayRunning(config, existingGateway.endpoint);
|
||||
this.markExternalGatewayRunning(config, existingGateway.endpoint, existingGateway.apiKey);
|
||||
try {
|
||||
await this.reloadExternalGatewayConfig(config, false);
|
||||
} catch {
|
||||
return this.getStatus();
|
||||
}
|
||||
return this.getStatus();
|
||||
}
|
||||
|
||||
async restart(config: AppConfig): Promise<GatewayStatus> {
|
||||
if (this.status.gatewayManagedExternally) {
|
||||
await this.reloadExternalGatewayConfig(config, true);
|
||||
return this.getStatus();
|
||||
}
|
||||
return this.start(config);
|
||||
}
|
||||
|
||||
async stop(options: GatewayStopOptions = {}): Promise<GatewayStatus> {
|
||||
const child = this.child;
|
||||
this.child = undefined;
|
||||
this.coreAuthToken = "";
|
||||
this.externalGatewayApiKey = undefined;
|
||||
if (child && !child.killed) {
|
||||
child.kill();
|
||||
}
|
||||
@@ -311,7 +344,7 @@ class GatewayService {
|
||||
async updateConfig(config: AppConfig): Promise<void> {
|
||||
assertLoopbackCoreHost(config.gateway.coreHost);
|
||||
if (this.status.gatewayManagedExternally) {
|
||||
this.markExternalGatewayRunning(config, endpoint(config.gateway.host, config.gateway.port));
|
||||
await this.reloadExternalGatewayConfig(config, false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -452,11 +485,65 @@ class GatewayService {
|
||||
return this.requestPipeline.proxyRequest(request, response, path, apiKey);
|
||||
}
|
||||
|
||||
private markExternalGatewayRunning(config: AppConfig, externalEndpoint: string): void {
|
||||
private async reloadExternalGatewayConfig(config: AppConfig, forceRestart: boolean): Promise<void> {
|
||||
const currentEndpoint = this.status.endpoint || endpoint(config.gateway.host, config.gateway.port);
|
||||
try {
|
||||
const externalGateway = await reloadExistingCcrGatewayConfig(
|
||||
currentEndpoint,
|
||||
config,
|
||||
this.externalGatewayApiKey,
|
||||
{ forceRestart }
|
||||
);
|
||||
this.markExternalGatewayRunning(config, externalGateway.endpoint, externalGateway.apiKey);
|
||||
} catch (error) {
|
||||
const message = `Failed to update the externally managed CCR gateway: ${formatError(error)}`;
|
||||
this.status = {
|
||||
...this.status,
|
||||
lastError: message,
|
||||
state: "error"
|
||||
};
|
||||
throw new Error(message, { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
private schedulePersistedRuntimeConfigReload(expectedRevision: string, forceRestart: boolean): void {
|
||||
this.runtimeConfigReloadError = undefined;
|
||||
this.runtimeConfigReloadQueue = this.runtimeConfigReloadQueue.then(
|
||||
() => this.reloadPersistedRuntimeConfig(expectedRevision, forceRestart),
|
||||
() => this.reloadPersistedRuntimeConfig(expectedRevision, forceRestart)
|
||||
);
|
||||
}
|
||||
|
||||
private async reloadPersistedRuntimeConfig(expectedRevision: string, forceRestart: boolean): Promise<void> {
|
||||
try {
|
||||
const nextConfig = await loadAppConfig();
|
||||
const actualRevision = gatewayRuntimeConfigRevision(nextConfig);
|
||||
if (actualRevision !== expectedRevision) {
|
||||
throw new Error(`Persisted configuration revision ${actualRevision || "(missing)"} does not match the requested revision ${expectedRevision}.`);
|
||||
}
|
||||
const restartRequired = forceRestart || !this.config ||
|
||||
shouldRestartGatewayForRuntimeConfigChange(this.config, nextConfig);
|
||||
if (restartRequired) {
|
||||
const status = await this.start(nextConfig);
|
||||
if (status.state === "error") {
|
||||
throw new Error(status.lastError || "CCR gateway failed to restart with the updated configuration.");
|
||||
}
|
||||
} else {
|
||||
await this.updateConfig(nextConfig);
|
||||
}
|
||||
this.runtimeConfigReloadError = undefined;
|
||||
} catch (error) {
|
||||
this.runtimeConfigReloadError = formatError(error);
|
||||
console.error(`[gateway] Failed to reload persisted runtime configuration: ${this.runtimeConfigReloadError}`);
|
||||
}
|
||||
}
|
||||
|
||||
private markExternalGatewayRunning(config: AppConfig, externalEndpoint: string, apiKey?: string): void {
|
||||
this.config = config;
|
||||
this.child = undefined;
|
||||
this.server = undefined;
|
||||
this.coreAuthToken = "";
|
||||
this.externalGatewayApiKey = apiKey;
|
||||
this.status = {
|
||||
coreEndpoint: endpoint(config.gateway.coreHost, config.gateway.corePort),
|
||||
endpoint: externalEndpoint,
|
||||
|
||||
@@ -522,7 +522,15 @@ function mergeNoProxy(current: string | undefined, values: string[]): string {
|
||||
}
|
||||
|
||||
export function endpoint(host: string, port: number): string {
|
||||
const endpointHost = host === "0.0.0.0" ? "127.0.0.1" : host;
|
||||
const unwrappedHost = host.startsWith("[") && host.endsWith("]")
|
||||
? host.slice(1, -1)
|
||||
: host;
|
||||
const connectHost = !unwrappedHost || unwrappedHost === "0.0.0.0"
|
||||
? "127.0.0.1"
|
||||
: unwrappedHost === "::"
|
||||
? "::1"
|
||||
: unwrappedHost;
|
||||
const endpointHost = connectHost.includes(":") ? `[${connectHost}]` : connectHost;
|
||||
return `http://${endpointHost}:${port}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { ApiKeyConfig, AppConfig } from "@ccr/core/contracts/app";
|
||||
import { endpoint as gatewayEndpoint } from "@ccr/core/gateway/core-runtime/supervisor";
|
||||
import { gatewayRuntimeConfigControlPath, gatewayRuntimeConfigRevision } from "@ccr/core/gateway/runtime-config-control";
|
||||
|
||||
export type ExistingCcrGatewayProbe =
|
||||
| { endpoint: string; reason?: string; state: "unavailable" }
|
||||
@@ -14,6 +16,7 @@ type ExistingGatewayHttpProbe = {
|
||||
};
|
||||
|
||||
const existingGatewayFetchAttempts = 3;
|
||||
const runtimeConfigReloadTimeoutMs = 20_000;
|
||||
|
||||
export async function probeExistingCcrGateway(
|
||||
config: Pick<AppConfig, "APIKEY" | "APIKEYS" | "gateway">
|
||||
@@ -71,7 +74,78 @@ export async function probeExistingCcrGateway(
|
||||
|
||||
export function publicGatewayEndpoint(config: Pick<AppConfig, "gateway">): string {
|
||||
const host = probeGatewayHost(config.gateway.host);
|
||||
return `http://${formatEndpointHost(host)}:${config.gateway.port}/`;
|
||||
return gatewayEndpoint(host, config.gateway.port);
|
||||
}
|
||||
|
||||
export async function reloadExistingCcrGatewayConfig(
|
||||
currentEndpoint: string,
|
||||
config: AppConfig,
|
||||
currentApiKey: string | undefined,
|
||||
options: { forceRestart?: boolean } = {}
|
||||
): Promise<{ apiKey: string; endpoint: string }> {
|
||||
const configRevision = gatewayRuntimeConfigRevision(config);
|
||||
if (!configRevision) {
|
||||
throw new Error("Cannot determine the saved CCR configuration revision.");
|
||||
}
|
||||
const currentKey = currentApiKey?.trim();
|
||||
if (!currentKey) {
|
||||
throw new Error("The API key accepted by the externally managed CCR gateway is unavailable.");
|
||||
}
|
||||
|
||||
if (options.forceRestart !== true) {
|
||||
const currentStatus = await fetchExistingGateway(currentEndpoint, gatewayRuntimeConfigControlPath, {
|
||||
headers: { authorization: `Bearer ${currentKey}` }
|
||||
}, 1, 700);
|
||||
if (currentStatus.status === 200 && isRecord(currentStatus.payload) &&
|
||||
currentStatus.payload.revision === configRevision) {
|
||||
return { apiKey: currentKey, endpoint: publicGatewayEndpoint(config) };
|
||||
}
|
||||
}
|
||||
|
||||
const submission = await fetchExistingGateway(currentEndpoint, gatewayRuntimeConfigControlPath, {
|
||||
body: JSON.stringify({
|
||||
configRevision,
|
||||
forceRestart: options.forceRestart === true
|
||||
}),
|
||||
headers: {
|
||||
authorization: `Bearer ${currentKey}`,
|
||||
"content-type": "application/json"
|
||||
},
|
||||
method: "POST"
|
||||
});
|
||||
if (submission.status === 404) {
|
||||
throw new Error("The running CCR gateway does not support runtime configuration reloads. Restart that gateway process once, then try again.");
|
||||
}
|
||||
if (submission.status !== 200 && submission.status !== 202) {
|
||||
const detail = readGatewayErrorMessage(submission.payload) || submission.reason ||
|
||||
`HTTP ${submission.status ?? 0}`;
|
||||
throw new Error(`The running CCR gateway rejected the configuration reload request: ${detail}`);
|
||||
}
|
||||
|
||||
const expectedEndpoint = publicGatewayEndpoint(config);
|
||||
const apiKeys = externalGatewayReloadApiKeys(config, currentKey);
|
||||
const deadline = Date.now() + runtimeConfigReloadTimeoutMs;
|
||||
let lastReason: string | undefined;
|
||||
while (Date.now() < deadline) {
|
||||
for (const apiKey of apiKeys) {
|
||||
const status = await fetchExistingGateway(expectedEndpoint, gatewayRuntimeConfigControlPath, {
|
||||
headers: { authorization: `Bearer ${apiKey}` }
|
||||
}, 1, 700);
|
||||
if (status.status === 200 && isRecord(status.payload)) {
|
||||
if (status.payload.revision === configRevision) {
|
||||
return { apiKey, endpoint: expectedEndpoint };
|
||||
}
|
||||
if (typeof status.payload.lastError === "string" && status.payload.lastError.trim()) {
|
||||
throw new Error(`The running CCR gateway could not apply the saved configuration: ${status.payload.lastError}`);
|
||||
}
|
||||
}
|
||||
if (status.status !== 401 && status.status !== 403) {
|
||||
lastReason = status.reason || (status.status ? `HTTP ${status.status}` : lastReason);
|
||||
}
|
||||
}
|
||||
await wait(100);
|
||||
}
|
||||
throw new Error(`Timed out waiting for the running CCR gateway to load configuration revision ${configRevision}${lastReason ? ` (${lastReason})` : ""}.`);
|
||||
}
|
||||
|
||||
export function isAddressInUseMessage(message: string | undefined): boolean {
|
||||
@@ -81,12 +155,14 @@ export function isAddressInUseMessage(message: string | undefined): boolean {
|
||||
async function fetchExistingGateway(
|
||||
endpoint: string,
|
||||
pathname: string,
|
||||
init: RequestInit = {}
|
||||
init: RequestInit = {},
|
||||
attempts = existingGatewayFetchAttempts,
|
||||
timeoutMs = 1200
|
||||
): Promise<ExistingGatewayHttpProbe> {
|
||||
let reason: string | undefined;
|
||||
for (let attempt = 0; attempt < existingGatewayFetchAttempts; attempt += 1) {
|
||||
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 1200);
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
const response = await fetch(new URL(pathname, endpoint).toString(), {
|
||||
...init,
|
||||
@@ -105,6 +181,17 @@ async function fetchExistingGateway(
|
||||
return { reason };
|
||||
}
|
||||
|
||||
function externalGatewayReloadApiKeys(
|
||||
config: Pick<AppConfig, "APIKEY" | "APIKEYS">,
|
||||
currentApiKey: string
|
||||
): string[] {
|
||||
const apiKeys = existingGatewayApiKeyCandidates(config).map((candidate) => candidate.key);
|
||||
if (!apiKeys.includes(currentApiKey)) {
|
||||
apiKeys.push(currentApiKey);
|
||||
}
|
||||
return apiKeys;
|
||||
}
|
||||
|
||||
function existingGatewayApiKeyCandidates(config: Pick<AppConfig, "APIKEY" | "APIKEYS">): ApiKeyConfig[] {
|
||||
const candidates = [
|
||||
...(Array.isArray(config.APIKEYS) ? config.APIKEYS : []),
|
||||
@@ -175,10 +262,6 @@ function probeGatewayHost(host: string): string {
|
||||
return host;
|
||||
}
|
||||
|
||||
function formatEndpointHost(host: string): string {
|
||||
return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
@@ -186,3 +269,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
function formatError(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
async function wait(milliseconds: number): Promise<void> {
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
type ContextArchiveReplayExecutor
|
||||
} from "@ccr/core/gateway/context-archive";
|
||||
import { ccrRemoteControlPathPrefix, ccrRemoteControlService } from "@ccr/core/gateway/remote-control-service";
|
||||
import { gatewayRuntimeConfigControlPath } from "@ccr/core/gateway/runtime-config-control";
|
||||
import { authorize, claudeCodeWifTokenPath, handleClaudeCodeWifTokenRequest, reserveApiKeyLimits } from "@ccr/core/gateway/auth/api-key-authorizer";
|
||||
import { parseJsonObject, readRequestBody, sendJson } from "@ccr/core/gateway/http/io";
|
||||
import { shouldRecordRequestLogs } from "@ccr/core/observability/raw-trace-sync";
|
||||
@@ -27,10 +28,12 @@ export type GatewayHttpRequestHandlerDependencies = {
|
||||
getBrowserAutomationMcpIntegration: () => BrowserAutomationMcpIntegration | undefined;
|
||||
getConfig: () => AppConfig | undefined;
|
||||
getPlugin: () => ClaudeCodeRouterPlugin | undefined;
|
||||
getRuntimeConfigControlStatus: () => { lastError?: string; revision?: string };
|
||||
getStatus: () => { coreEndpoint: string; coreManagedExternally?: boolean; endpoint: string; state: string };
|
||||
handleBillingUsageSync: (request: IncomingMessage, response: ServerResponse) => Promise<void>;
|
||||
handleRawTraceSync: (request: IncomingMessage, response: ServerResponse) => Promise<void>;
|
||||
proxyRequest: (request: IncomingMessage, response: ServerResponse, path: string, apiKey?: ApiKeyConfig) => Promise<void>;
|
||||
requestRuntimeConfigReload: (expectedRevision: string, forceRestart: boolean) => void;
|
||||
replayContextArchive: ContextArchiveReplayExecutor;
|
||||
};
|
||||
|
||||
@@ -40,10 +43,12 @@ export class GatewayHttpRequestHandler {
|
||||
private get browserAutomationMcpIntegration() { return this.dependencies.getBrowserAutomationMcpIntegration(); }
|
||||
private get config() { return this.dependencies.getConfig(); }
|
||||
private get plugin() { return this.dependencies.getPlugin(); }
|
||||
private get runtimeConfigControlStatus() { return this.dependencies.getRuntimeConfigControlStatus(); }
|
||||
private get status() { return this.dependencies.getStatus(); }
|
||||
private handleBillingUsageSync(request: IncomingMessage, response: ServerResponse) { return this.dependencies.handleBillingUsageSync(request, response); }
|
||||
private handleRawTraceSync(request: IncomingMessage, response: ServerResponse) { return this.dependencies.handleRawTraceSync(request, response); }
|
||||
private proxyRequest(request: IncomingMessage, response: ServerResponse, path: string, apiKey?: ApiKeyConfig) { return this.dependencies.proxyRequest(request, response, path, apiKey); }
|
||||
private requestRuntimeConfigReload(expectedRevision: string, forceRestart: boolean) { return this.dependencies.requestRuntimeConfigReload(expectedRevision, forceRestart); }
|
||||
private replayContextArchive(input: Parameters<ContextArchiveReplayExecutor>[0]) { return this.dependencies.replayContextArchive(input); }
|
||||
|
||||
async handleRequest(request: IncomingMessage, response: ServerResponse): Promise<void> {
|
||||
@@ -75,6 +80,46 @@ export class GatewayHttpRequestHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
if (path === gatewayRuntimeConfigControlPath) {
|
||||
const authorization = await authorize(request, response, this.config);
|
||||
if (!authorization.ok) {
|
||||
return;
|
||||
}
|
||||
if (!this.config.APIKEY || authorization.apiKey?.key !== this.config.APIKEY) {
|
||||
sendJson(response, 403, { error: { message: "The primary CCR API key is required for runtime configuration control." } });
|
||||
return;
|
||||
}
|
||||
if (request.method === "GET") {
|
||||
sendJson(response, 200, {
|
||||
...this.runtimeConfigControlStatus,
|
||||
state: this.status.state
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (request.method !== "POST") {
|
||||
sendJson(response, 405, { error: { message: "Method not allowed." } });
|
||||
return;
|
||||
}
|
||||
const body = parseJsonObject(await readRequestBody(request));
|
||||
const expectedRevision = typeof body.configRevision === "string"
|
||||
? body.configRevision.trim()
|
||||
: "";
|
||||
if (!/^[a-f0-9]{64}$/i.test(expectedRevision)) {
|
||||
sendJson(response, 400, { error: { message: "A valid configRevision is required." } });
|
||||
return;
|
||||
}
|
||||
const forceRestart = body.forceRestart === true;
|
||||
response.once("finish", () => {
|
||||
this.requestRuntimeConfigReload(expectedRevision, forceRestart);
|
||||
});
|
||||
sendJson(response, 202, {
|
||||
accepted: true,
|
||||
configRevision: expectedRevision,
|
||||
restarting: forceRestart
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path === ccrRemoteControlPathPrefix || path.startsWith(`${ccrRemoteControlPathPrefix}/`)) {
|
||||
const authorization = await authorize(request, response, this.config);
|
||||
if (!authorization.ok) {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import type { AppConfig } from "@ccr/core/contracts/app";
|
||||
|
||||
export const gatewayRuntimeConfigControlPath = "/__ccr/runtime/config";
|
||||
|
||||
export function gatewayRuntimeConfigRevision(config: AppConfig | undefined): string | undefined {
|
||||
if (!config) {
|
||||
return undefined;
|
||||
}
|
||||
return createHash("sha256")
|
||||
.update(stableJson(config))
|
||||
.digest("hex");
|
||||
}
|
||||
|
||||
function stableJson(value: unknown): string {
|
||||
if (Array.isArray(value)) {
|
||||
return `[${value.map(stableJson).join(",")}]`;
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
const record = value as Record<string, unknown>;
|
||||
return `{${Object.keys(record)
|
||||
.sort()
|
||||
.filter((key) => record[key] !== undefined)
|
||||
.map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`)
|
||||
.join(",")}}`;
|
||||
}
|
||||
return JSON.stringify(value) ?? "null";
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { findRunningOpenCodeAppPid, launchOpenCodeAppProfile, openCodeAppLaunchS
|
||||
import { writeOpenCodeGatewayConfig } from "@ccr/core/agents/opencode/profile-config";
|
||||
import { codexCliMiddlewareRuntimeScript } from "@ccr/core/agents/codex/cli-middleware-runtime";
|
||||
import { CONFIGDIR } from "@ccr/core/config/constants";
|
||||
import { endpoint } from "@ccr/core/gateway/core-runtime/supervisor";
|
||||
import { gatewayService } from "@ccr/core/gateway/service";
|
||||
import { TOOL_HUB_MCP_RUNTIME_FILE_NAME, bundledToolHubMcpEntryPathCandidates } from "@ccr/core/mcp/toolhub-config";
|
||||
import { mediaToolsGatewayEndpoint } from "@ccr/core/mcp/grok-media-config";
|
||||
@@ -737,7 +738,7 @@ function isAddressInUseError(message: string | undefined): boolean {
|
||||
|
||||
function profileGatewayEndpoint(config: AppConfig): string {
|
||||
const host = probeGatewayHost(config.gateway.host);
|
||||
return `http://${formatEndpointHost(host)}:${config.gateway.port}/`;
|
||||
return endpoint(host, config.gateway.port);
|
||||
}
|
||||
|
||||
function probeGatewayHost(host: string): string {
|
||||
@@ -750,10 +751,6 @@ function probeGatewayHost(host: string): string {
|
||||
return host;
|
||||
}
|
||||
|
||||
function formatEndpointHost(host: string): string {
|
||||
return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
||||
}
|
||||
|
||||
function profileGatewayConfigFor(config: AppConfig, profile: ReturnType<typeof findProfileForOpen>): AppConfig {
|
||||
const token = findProfileApiKey(config, profile);
|
||||
if (!token) {
|
||||
|
||||
@@ -265,7 +265,17 @@ const rpcHandlers: Record<string, RpcHandler> = {
|
||||
const synced = await syncClaudeAppGatewayConfig(baseConfig);
|
||||
const savedConfig = synced.config;
|
||||
let runtimeStatus = gatewayService.getStatus();
|
||||
if (synced.configChanged || shouldRestartGatewayForRuntimeConfigChange(previousConfig, savedConfig) || runtimeStatus.state !== "running") {
|
||||
const restartRequired = synced.configChanged ||
|
||||
shouldRestartGatewayForRuntimeConfigChange(previousConfig, savedConfig) ||
|
||||
runtimeStatus.state !== "running";
|
||||
if (runtimeStatus.gatewayManagedExternally) {
|
||||
if (restartRequired) {
|
||||
runtimeStatus = await gatewayService.restart(savedConfig);
|
||||
} else {
|
||||
await gatewayService.updateConfig(savedConfig);
|
||||
runtimeStatus = gatewayService.getStatus();
|
||||
}
|
||||
} else if (restartRequired) {
|
||||
runtimeStatus = await gatewayService.start(savedConfig);
|
||||
} else {
|
||||
await gatewayService.updateConfig(savedConfig);
|
||||
@@ -382,14 +392,14 @@ const rpcHandlers: Record<string, RpcHandler> = {
|
||||
restartGateway: async () => {
|
||||
const syncedClaudeAppConfig = await syncClaudeAppGatewayConfig(await loadAppConfig());
|
||||
const config = syncedClaudeAppConfig.config;
|
||||
const status = await gatewayService.start(config);
|
||||
const status = await gatewayService.restart(config);
|
||||
await applyProfileIfServiceRunning(config, status);
|
||||
return status;
|
||||
},
|
||||
restartProxy: async () => {
|
||||
const syncedClaudeAppConfig = await syncClaudeAppGatewayConfig(await loadAppConfig());
|
||||
const config = syncedClaudeAppConfig.config;
|
||||
const status = await gatewayService.start(config);
|
||||
const status = await gatewayService.restart(config);
|
||||
await applyProfileIfServiceRunning(config, status);
|
||||
return proxyService.getStatus();
|
||||
},
|
||||
@@ -420,7 +430,16 @@ const rpcHandlers: Record<string, RpcHandler> = {
|
||||
const syncedClaudeAppConfig = await syncClaudeAppGatewayConfig(savedConfig);
|
||||
savedConfig = syncedClaudeAppConfig.config;
|
||||
let runtimeStatus = gatewayService.getStatus();
|
||||
if (syncedClaudeAppConfig.configChanged || shouldRestartGatewayForRuntimeConfigChange(previousConfig, savedConfig)) {
|
||||
const restartRequired = syncedClaudeAppConfig.configChanged ||
|
||||
shouldRestartGatewayForRuntimeConfigChange(previousConfig, savedConfig);
|
||||
if (runtimeStatus.gatewayManagedExternally) {
|
||||
if (restartRequired) {
|
||||
runtimeStatus = await gatewayService.restart(savedConfig);
|
||||
} else {
|
||||
await gatewayService.updateConfig(savedConfig);
|
||||
runtimeStatus = gatewayService.getStatus();
|
||||
}
|
||||
} else if (restartRequired) {
|
||||
runtimeStatus = await gatewayService.start(savedConfig);
|
||||
} else {
|
||||
await gatewayService.updateConfig(savedConfig);
|
||||
|
||||
@@ -11,6 +11,8 @@ import { waitForTcpListener } from "../../support/loopback-listener.mjs";
|
||||
test("gateway HTTP model discovery and request access honor profile model allowlists", async (t) => {
|
||||
const config = createProfileAllowlistHttpTestConfig();
|
||||
const plugin = new ClaudeCodeRouterPlugin(config);
|
||||
const runtimeConfigRevision = "a".repeat(64);
|
||||
const runtimeConfigReloads = [];
|
||||
const status = {
|
||||
coreEndpoint: "http://127.0.0.1:1",
|
||||
endpoint: "http://127.0.0.1:0",
|
||||
@@ -27,10 +29,14 @@ test("gateway HTTP model discovery and request access honor profile model allowl
|
||||
getBrowserAutomationMcpIntegration: () => undefined,
|
||||
getConfig: () => config,
|
||||
getPlugin: () => plugin,
|
||||
getRuntimeConfigControlStatus: () => ({ revision: runtimeConfigRevision }),
|
||||
getStatus: () => status,
|
||||
handleBillingUsageSync: unsupportedHandler,
|
||||
handleRawTraceSync: unsupportedHandler,
|
||||
proxyRequest: (request, response, path, apiKey) => pipeline.proxyRequest(request, response, path, apiKey),
|
||||
requestRuntimeConfigReload: (expectedRevision, forceRestart) => {
|
||||
runtimeConfigReloads.push({ expectedRevision, forceRestart });
|
||||
},
|
||||
replayContextArchive: async () => ({ ok: false, statusCode: 404, message: "not configured" })
|
||||
});
|
||||
const server = createServer((request, response) => {
|
||||
@@ -56,6 +62,29 @@ test("gateway HTTP model discovery and request access honor profile model allowl
|
||||
const endpoint = `http://127.0.0.1:${serverPort(server)}`;
|
||||
status.endpoint = endpoint;
|
||||
|
||||
const runtimeConfigStatus = await fetchJson(`${endpoint}/__ccr/runtime/config`, {
|
||||
headers: authHeaders("gateway-key")
|
||||
});
|
||||
assert.equal(runtimeConfigStatus.revision, runtimeConfigRevision);
|
||||
assert.equal(runtimeConfigStatus.state, "running");
|
||||
|
||||
const deniedRuntimeConfigStatus = await fetch(`${endpoint}/__ccr/runtime/config`, {
|
||||
headers: authHeaders("profile-alpha-key")
|
||||
});
|
||||
assert.equal(deniedRuntimeConfigStatus.status, 403);
|
||||
|
||||
const runtimeConfigReload = await fetch(`${endpoint}/__ccr/runtime/config`, {
|
||||
body: JSON.stringify({ configRevision: runtimeConfigRevision, forceRestart: true }),
|
||||
headers: {
|
||||
...authHeaders("gateway-key"),
|
||||
"content-type": "application/json"
|
||||
},
|
||||
method: "POST"
|
||||
});
|
||||
assert.equal(runtimeConfigReload.status, 202, await runtimeConfigReload.text());
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.deepEqual(runtimeConfigReloads, [{ expectedRevision: runtimeConfigRevision, forceRestart: true }]);
|
||||
|
||||
const alphaModels = await fetchJson(`${endpoint}/v1/models`, {
|
||||
headers: authHeaders("profile-alpha-key")
|
||||
});
|
||||
|
||||
@@ -11,8 +11,16 @@ import {
|
||||
replacePersistedRuntimeState
|
||||
} from "@ccr/core/config/config-repository.ts";
|
||||
import { createDefaultAppConfig } from "@ccr/core/config/default-config.ts";
|
||||
import { endpoint } from "@ccr/core/gateway/core-runtime/supervisor.ts";
|
||||
import { gatewayService } from "@ccr/core/gateway/service.ts";
|
||||
|
||||
test("gateway endpoints normalize wildcard and IPv6 hosts", () => {
|
||||
assert.equal(endpoint("0.0.0.0", 3456), "http://127.0.0.1:3456");
|
||||
assert.equal(endpoint("::", 3456), "http://[::1]:3456");
|
||||
assert.equal(endpoint("::1", 3456), "http://[::1]:3456");
|
||||
assert.equal(endpoint("[::1]", 3456), "http://[::1]:3456");
|
||||
});
|
||||
|
||||
test("gateway start persists preflight validation failures for status polling", async () => {
|
||||
await gatewayService.stop();
|
||||
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { CLAUDE_CODE_AUTH_MODE_ENV } from "@ccr/core/agents/claude-code/auth-mode.ts";
|
||||
import { createDefaultAppConfig } from "@ccr/core/config/default-config.ts";
|
||||
import { ensureProfileGateway } from "@ccr/core/profiles/launch-service.ts";
|
||||
|
||||
function claudeProfileConfig() {
|
||||
function claudeProfileConfig(authMode = "api-key-helper") {
|
||||
const profile = {
|
||||
agent: "claude-code",
|
||||
enabled: true,
|
||||
env: {},
|
||||
env: {
|
||||
[CLAUDE_CODE_AUTH_MODE_ENV]: authMode
|
||||
},
|
||||
id: "claude-main",
|
||||
model: "Provider/model",
|
||||
name: "Claude Main",
|
||||
@@ -99,6 +102,47 @@ test("existing profile gateway falls back to the root identity response", async
|
||||
}
|
||||
});
|
||||
|
||||
test("existing profile gateway rejects WIF profiles when the root identity lacks the token endpoint", async () => {
|
||||
const previousFetch = globalThis.fetch;
|
||||
const paths = [];
|
||||
globalThis.fetch = async (input) => {
|
||||
const url = new URL(String(input));
|
||||
paths.push(url.pathname);
|
||||
if (url.pathname === "/health") {
|
||||
return Response.json({
|
||||
core: "http://127.0.0.1:3467",
|
||||
status: "running",
|
||||
timestamp: "2026-01-01T00:00:00.000Z"
|
||||
});
|
||||
}
|
||||
if (url.pathname === "/v1/models") {
|
||||
return Response.json({ data: [], object: "list" });
|
||||
}
|
||||
if (url.pathname === "/") {
|
||||
return Response.json({
|
||||
endpoints: ["GET /v1/models"],
|
||||
name: "claude-code-router"
|
||||
});
|
||||
}
|
||||
throw new Error(`Unexpected gateway probe: ${url.pathname}`);
|
||||
};
|
||||
|
||||
try {
|
||||
const { config, profile } = claudeProfileConfig("wif");
|
||||
await assert.rejects(
|
||||
ensureProfileGateway(config, profile, "Local subscription router", {
|
||||
reuseExisting: true,
|
||||
startIfMissing: false
|
||||
}),
|
||||
/does not advertise the Claude Code WIF token endpoint/
|
||||
);
|
||||
|
||||
assert.deepEqual(paths, ["/health", "/v1/models", "/"]);
|
||||
} finally {
|
||||
globalThis.fetch = previousFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("existing profile gateway retries a transient transport failure", async () => {
|
||||
const previousFetch = globalThis.fetch;
|
||||
const paths = [];
|
||||
|
||||
@@ -69,6 +69,7 @@ test("web RPC ignores Origin and Referer when the auth token is valid", async ()
|
||||
test("startGateway reuses an already healthy CCR gateway on the configured port", async () => {
|
||||
const { saveAppConfig } = await import("@ccr/core/config/config.ts");
|
||||
const { createDefaultAppConfig } = await import("@ccr/core/config/default-config.ts");
|
||||
const { gatewayRuntimeConfigRevision } = await import("@ccr/core/gateway/runtime-config-control.ts");
|
||||
const { gatewayService } = await import("@ccr/core/gateway/service.ts");
|
||||
const { startWebManagementServer } = await import("@ccr/core/web/management-server.ts");
|
||||
await gatewayService.stop();
|
||||
@@ -86,7 +87,11 @@ test("startGateway reuses an already healthy CCR gateway on the configured port"
|
||||
type: "openai_chat_completions"
|
||||
}];
|
||||
const savedConfig = await saveAppConfig(config);
|
||||
const externalGateway = createHealthyCcrGateway(savedConfig.APIKEY);
|
||||
const externalGatewayState = {
|
||||
reloadRequests: [],
|
||||
revision: gatewayRuntimeConfigRevision(savedConfig)
|
||||
};
|
||||
const externalGateway = createHealthyCcrGateway(savedConfig.APIKEY, externalGatewayState);
|
||||
await listen(externalGateway, gatewayPort);
|
||||
const runtime = await startWebManagementServer({
|
||||
authToken: webAuthToken,
|
||||
@@ -100,10 +105,25 @@ test("startGateway reuses an already healthy CCR gateway on the configured port"
|
||||
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.value.state, "running", payload.value.lastError);
|
||||
assert.equal(payload.value.endpoint, `http://127.0.0.1:${gatewayPort}/`);
|
||||
assert.equal(payload.value.endpoint, `http://127.0.0.1:${gatewayPort}`);
|
||||
assert.equal(payload.value.gatewayManagedExternally, true);
|
||||
assert.equal(gatewayService.getStatus().state, "running");
|
||||
assert.equal(gatewayService.getStatus().gatewayManagedExternally, true);
|
||||
|
||||
const secondStart = await rpc(runtime.url, webAuthToken, "startGateway");
|
||||
assert.equal(secondStart.ok, true);
|
||||
assert.equal(secondStart.value.endpoint, `http://127.0.0.1:${gatewayPort}`);
|
||||
assert.equal(secondStart.value.gatewayManagedExternally, true);
|
||||
|
||||
const nextConfig = structuredClone(savedConfig);
|
||||
nextConfig.Providers[0].name = "Updated Test Provider";
|
||||
const reloadCountBeforeSave = externalGatewayState.reloadRequests.length;
|
||||
const saveResult = await rpc(runtime.url, webAuthToken, "saveConfig", [nextConfig, { applyProfile: false }]);
|
||||
assert.equal(saveResult.ok, true);
|
||||
assert.equal(externalGatewayState.reloadRequests.length, reloadCountBeforeSave + 1);
|
||||
assert.equal(externalGatewayState.reloadRequests.at(-1).forceRestart, true);
|
||||
assert.equal(externalGatewayState.revision, gatewayRuntimeConfigRevision(saveResult.value));
|
||||
assert.equal(gatewayService.getStatus().gatewayManagedExternally, true);
|
||||
} finally {
|
||||
await runtime.close();
|
||||
await gatewayService.stop();
|
||||
@@ -124,7 +144,7 @@ async function rpc(baseUrl, authToken, method, args = []) {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function createHealthyCcrGateway(apiKey) {
|
||||
function createHealthyCcrGateway(apiKey, state) {
|
||||
return createServer((request, response) => {
|
||||
const url = new URL(request.url || "/", "http://127.0.0.1");
|
||||
if (url.pathname === "/health") {
|
||||
@@ -152,10 +172,40 @@ function createHealthyCcrGateway(apiKey) {
|
||||
sendJson(response, 200, { data: [{ id: "test-model", object: "model" }], object: "list" });
|
||||
return;
|
||||
}
|
||||
if (url.pathname === "/__ccr/runtime/config") {
|
||||
if (request.headers.authorization !== `Bearer ${apiKey}`) {
|
||||
sendJson(response, 401, { error: { message: "Invalid API key." } });
|
||||
return;
|
||||
}
|
||||
if (request.method === "GET") {
|
||||
sendJson(response, 200, { revision: state.revision, state: "running" });
|
||||
return;
|
||||
}
|
||||
if (request.method === "POST") {
|
||||
void readJsonRequest(request).then((body) => {
|
||||
state.reloadRequests.push(body);
|
||||
state.revision = body.configRevision;
|
||||
sendJson(response, 202, {
|
||||
accepted: true,
|
||||
configRevision: body.configRevision,
|
||||
restarting: body.forceRestart === true
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
sendJson(response, 404, { error: { message: "Not found." } });
|
||||
});
|
||||
}
|
||||
|
||||
async function readJsonRequest(request) {
|
||||
const chunks = [];
|
||||
for await (const chunk of request) {
|
||||
chunks.push(Buffer.from(chunk));
|
||||
}
|
||||
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
||||
}
|
||||
|
||||
function sendJson(response, statusCode, payload) {
|
||||
response.writeHead(statusCode, { "content-type": "application/json" });
|
||||
response.end(JSON.stringify(payload));
|
||||
|
||||
@@ -220,8 +220,18 @@ ipcMain.handle(IPC_CHANNELS.appApplyClaudeAppGateway, async (_event, config?: Ap
|
||||
const synced = await syncClaudeAppGatewayConfig(baseConfig);
|
||||
const savedConfig = synced.config;
|
||||
let runtimeStatus = gatewayService.getStatus();
|
||||
const restartRequired = synced.configChanged ||
|
||||
shouldRestartGatewayForRuntimeConfigChange(previousConfig, savedConfig) ||
|
||||
runtimeStatus.state !== "running";
|
||||
|
||||
if (synced.configChanged || shouldRestartGatewayForRuntimeConfigChange(previousConfig, savedConfig) || runtimeStatus.state !== "running") {
|
||||
if (runtimeStatus.gatewayManagedExternally) {
|
||||
if (restartRequired) {
|
||||
runtimeStatus = await gatewayService.restart(savedConfig);
|
||||
} else {
|
||||
await gatewayService.updateConfig(savedConfig);
|
||||
runtimeStatus = gatewayService.getStatus();
|
||||
}
|
||||
} else if (restartRequired) {
|
||||
runtimeStatus = await gatewayService.start(savedConfig);
|
||||
} else {
|
||||
await gatewayService.updateConfig(savedConfig);
|
||||
@@ -323,7 +333,16 @@ ipcMain.handle(IPC_CHANNELS.appSaveConfig, async (_event, config: AppConfig, opt
|
||||
const syncedClaudeAppConfig = await syncClaudeAppGatewayConfig(savedConfig);
|
||||
savedConfig = syncedClaudeAppConfig.config;
|
||||
let runtimeStatus = gatewayService.getStatus();
|
||||
if (syncedClaudeAppConfig.configChanged || shouldRestartGatewayForRuntimeConfigChange(previousConfig, savedConfig)) {
|
||||
const restartRequired = syncedClaudeAppConfig.configChanged ||
|
||||
shouldRestartGatewayForRuntimeConfigChange(previousConfig, savedConfig);
|
||||
if (runtimeStatus.gatewayManagedExternally) {
|
||||
if (restartRequired) {
|
||||
runtimeStatus = await gatewayService.restart(savedConfig);
|
||||
} else {
|
||||
await gatewayService.updateConfig(savedConfig);
|
||||
runtimeStatus = gatewayService.getStatus();
|
||||
}
|
||||
} else if (restartRequired) {
|
||||
runtimeStatus = await gatewayService.start(savedConfig);
|
||||
} else {
|
||||
await gatewayService.updateConfig(savedConfig);
|
||||
@@ -360,7 +379,7 @@ ipcMain.handle(IPC_CHANNELS.appSetOnboardingFinished, async () => {
|
||||
ipcMain.handle(IPC_CHANNELS.appRestartGateway, async () => {
|
||||
const syncedClaudeAppConfig = await syncClaudeAppGatewayConfig(await loadAppConfig());
|
||||
const config = syncedClaudeAppConfig.config;
|
||||
const status = await gatewayService.start(config);
|
||||
const status = await gatewayService.restart(config);
|
||||
await applyProfileIfServiceRunning(config, status);
|
||||
await builtInBrowserService.syncProxy(config);
|
||||
return status;
|
||||
@@ -391,7 +410,7 @@ ipcMain.handle(IPC_CHANNELS.appShowMainWindow, () => {
|
||||
ipcMain.handle(IPC_CHANNELS.appRestartProxy, async () => {
|
||||
const syncedClaudeAppConfig = await syncClaudeAppGatewayConfig(await loadAppConfig());
|
||||
const config = syncedClaudeAppConfig.config;
|
||||
const status = await gatewayService.start(config);
|
||||
const status = await gatewayService.restart(config);
|
||||
await applyProfileIfServiceRunning(config, status);
|
||||
await builtInBrowserService.syncProxy(config);
|
||||
return proxyService.getStatus();
|
||||
|
||||
@@ -997,7 +997,7 @@ export function AddProfileForm({
|
||||
/>
|
||||
{validation.allowedModels ? <ProfileFieldHint>{t(validation.allowedModels)}</ProfileFieldHint> : null}
|
||||
</Field>
|
||||
{draft.agent === "codex" && draft.agent !== "workbuddy" ? (
|
||||
{draft.agent === "codex" ? (
|
||||
<div className="flex items-center justify-between gap-3 rounded-md border border-border bg-muted/20 px-3 py-2">
|
||||
<span className="text-[12px] font-medium">{t("Show all sessions")}</span>
|
||||
<Toggle checked={draft.showAllSessions} onChange={(showAllSessions) => onChange({ showAllSessions })} />
|
||||
|
||||
Reference in New Issue
Block a user