mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 03:13:12 +00:00
fix(cliproxy): 按 backend 生成 provider 路由
This commit is contained in:
1 parent
b811cacaef
commit
99ed8090b1
13 files changed
+450
-106
No files matched your search
@@ -1,14 +1,13 @@
|
||||
/**
|
||||
* Regression tests for the `ccs api create --cliproxy-provider claude` bridge path.
|
||||
* Regression tests for the `ccs api create --cliproxy-provider <provider>` bridge path.
|
||||
*
|
||||
* PR #1554 fixed the main CLIProxy env-builder to use the root URL for the built-in
|
||||
* claude provider. This file locks the same rule on the parallel api-create bridge path
|
||||
* The bridge path must use the same route rules as the main CLIProxy env-builder
|
||||
* (resolveCliproxyBridgeProfile / listCliproxyBridgeProviders) so both paths stay
|
||||
* consistent.
|
||||
* consistent across backends.
|
||||
*
|
||||
* Background: CLIProxyAPI registers /v1/messages at the ROOT. The /api/provider/<x>
|
||||
* prefix is a Plus-only route for non-Claude providers. Using /api/provider/claude
|
||||
* returns 404 on the base CLIProxyAPI installation.
|
||||
* Background: the original CLIProxyAPI backend registers Claude-compatible routes at
|
||||
* the root. The /api/provider/<x> prefix is a Plus-only route. Using provider-scoped
|
||||
* routes against the original backend returns 404.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
@@ -23,7 +22,7 @@ import { resolveCliproxyBridgeMetadata } from '../cliproxy-profile-bridge';
|
||||
import { invalidateConfigCache } from '../../../config/config-loader-facade';
|
||||
import { clearConfigCache } from '../../../cliproxy/config/base-config-loader';
|
||||
|
||||
describe('cliproxy-profile-bridge: claude provider uses root URL', () => {
|
||||
describe('cliproxy-profile-bridge: backend-aware provider route paths', () => {
|
||||
let tempHome: string;
|
||||
let originalCcsHome: string | undefined;
|
||||
|
||||
@@ -37,12 +36,28 @@ describe('cliproxy-profile-bridge: claude provider uses root URL', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env.CCS_HOME = originalCcsHome;
|
||||
if (originalCcsHome !== undefined) {
|
||||
process.env.CCS_HOME = originalCcsHome;
|
||||
} else {
|
||||
delete process.env.CCS_HOME;
|
||||
}
|
||||
invalidateConfigCache();
|
||||
clearConfigCache();
|
||||
fs.rmSync(tempHome, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function writeBackendConfig(backend: 'original' | 'plus'): void {
|
||||
const ccsDir = path.join(tempHome, '.ccs');
|
||||
fs.mkdirSync(ccsDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(ccsDir, 'config.yaml'),
|
||||
['version: 1', 'cliproxy:', ` backend: ${backend}`, ''].join('\n'),
|
||||
'utf8'
|
||||
);
|
||||
invalidateConfigCache();
|
||||
clearConfigCache();
|
||||
}
|
||||
|
||||
// ── resolveCliproxyBridgeProfile ──────────────────────────────────────────
|
||||
|
||||
it('resolveCliproxyBridgeProfile(claude) produces root base URL', () => {
|
||||
@@ -65,13 +80,20 @@ describe('cliproxy-profile-bridge: claude provider uses root URL', () => {
|
||||
expect(profile.models.haiku).toBe('');
|
||||
});
|
||||
|
||||
it('resolveCliproxyBridgeProfile(gemini) still uses scoped /api/provider path', () => {
|
||||
it('resolveCliproxyBridgeProfile(gemini) uses root URL on the original backend', () => {
|
||||
const profile = resolveCliproxyBridgeProfile('gemini');
|
||||
expect(profile.baseUrl).toContain('/api/provider/gemini');
|
||||
expect(profile.routePath).toBe('/api/provider/gemini');
|
||||
expect(profile.baseUrl).toBe('http://127.0.0.1:8317/');
|
||||
expect(profile.routePath).toBe('/');
|
||||
});
|
||||
|
||||
it('resolveCliproxyBridgeProfile(codex) still uses scoped /api/provider path', () => {
|
||||
it('resolveCliproxyBridgeProfile(codex) uses root URL on the original backend', () => {
|
||||
const profile = resolveCliproxyBridgeProfile('codex');
|
||||
expect(profile.baseUrl).toBe('http://127.0.0.1:8317/');
|
||||
expect(profile.routePath).toBe('/');
|
||||
});
|
||||
|
||||
it('resolveCliproxyBridgeProfile(codex) uses scoped path on the plus backend', () => {
|
||||
writeBackendConfig('plus');
|
||||
const profile = resolveCliproxyBridgeProfile('codex');
|
||||
expect(profile.baseUrl).toContain('/api/provider/codex');
|
||||
expect(profile.routePath).toBe('/api/provider/codex');
|
||||
@@ -87,7 +109,15 @@ describe('cliproxy-profile-bridge: claude provider uses root URL', () => {
|
||||
expect(claudeInfo?.routePath).not.toContain('/api/provider/claude');
|
||||
});
|
||||
|
||||
it('listCliproxyBridgeProviders keeps scoped routePaths for non-claude providers', () => {
|
||||
it('listCliproxyBridgeProviders shows root routePaths for original-backend providers', () => {
|
||||
const providers = listCliproxyBridgeProviders();
|
||||
for (const info of providers) {
|
||||
expect(info.routePath).toBe('/');
|
||||
}
|
||||
});
|
||||
|
||||
it('listCliproxyBridgeProviders keeps scoped routePaths for plus-backend non-claude providers', () => {
|
||||
writeBackendConfig('plus');
|
||||
const providers = listCliproxyBridgeProviders();
|
||||
for (const info of providers) {
|
||||
if (info.provider === 'claude') continue;
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
loadConfigSafe,
|
||||
mutateConfig,
|
||||
} from '../../config/config-loader-facade';
|
||||
import { ProfileError } from '../../errors/error-types';
|
||||
|
||||
/** Check if URL is an OpenRouter endpoint */
|
||||
function isOpenRouterUrl(baseUrl: string): boolean {
|
||||
@@ -333,19 +334,27 @@ export function createCliproxyBridgeProfile(
|
||||
resolved.target,
|
||||
provider
|
||||
);
|
||||
const detectedBridge = resolveCliproxyBridgeMetadata({
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: resolved.baseUrl,
|
||||
ANTHROPIC_AUTH_TOKEN: resolved.apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
name: resolved.name,
|
||||
provider,
|
||||
target: resolved.target,
|
||||
cliproxyBridge:
|
||||
resolveCliproxyBridgeMetadata({
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: resolved.baseUrl,
|
||||
ANTHROPIC_AUTH_TOKEN: resolved.apiKey,
|
||||
},
|
||||
}) ?? null,
|
||||
cliproxyBridge: detectedBridge ?? {
|
||||
provider,
|
||||
providerDisplayName: resolved.providerDisplayName,
|
||||
routePath: resolved.routePath,
|
||||
currentBaseUrl: resolved.baseUrl,
|
||||
source: resolved.source,
|
||||
usesCurrentTarget: true,
|
||||
usesCurrentAuthToken: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -361,7 +370,7 @@ export function updateApiProfileTarget(
|
||||
if (isUnifiedMode()) {
|
||||
mutateConfig((config) => {
|
||||
if (!config.profiles[name]) {
|
||||
throw new Error(`API profile not found: ${name}`);
|
||||
throw new ProfileError(`API profile not found: ${name}`, name);
|
||||
}
|
||||
|
||||
if (target === 'claude') {
|
||||
@@ -412,7 +421,7 @@ function removeApiProfileUnified(name: string): void {
|
||||
const profile = config.profiles[name];
|
||||
|
||||
if (!profile) {
|
||||
throw new Error(`API profile not found: ${name}`);
|
||||
throw new ProfileError(`API profile not found: ${name}`, name);
|
||||
}
|
||||
|
||||
if (profile.settings) {
|
||||
|
||||
@@ -3,6 +3,8 @@ import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
||||
import { ensureProviderSettings, getEffectiveEnvVars } from '../env-builder';
|
||||
import { invalidateConfigCache } from '../../../config/config-loader-facade';
|
||||
import { clearConfigCache } from '../base-config-loader';
|
||||
|
||||
interface EnvSettings {
|
||||
ANTHROPIC_BASE_URL: string;
|
||||
@@ -30,14 +32,35 @@ describe('getEffectiveEnvVars local provider URL normalization', () => {
|
||||
originalCcsHome = process.env.CCS_HOME;
|
||||
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-env-url-'));
|
||||
settingsPath = path.join(tempHome, 'codex.settings.json');
|
||||
process.env.CCS_HOME = tempHome;
|
||||
invalidateConfigCache();
|
||||
clearConfigCache();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env.CCS_HOME = originalCcsHome;
|
||||
if (originalCcsHome !== undefined) {
|
||||
process.env.CCS_HOME = originalCcsHome;
|
||||
} else {
|
||||
delete process.env.CCS_HOME;
|
||||
}
|
||||
invalidateConfigCache();
|
||||
clearConfigCache();
|
||||
fs.rmSync(tempHome, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('rewrites local root URL to provider endpoint without stripping codex effort suffixes', () => {
|
||||
function writeBackendConfig(backend: 'original' | 'plus'): void {
|
||||
const ccsDir = path.join(tempHome, '.ccs');
|
||||
fs.mkdirSync(ccsDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(ccsDir, 'config.yaml'),
|
||||
['version: 1', 'cliproxy:', ` backend: ${backend}`, ''].join('\n'),
|
||||
'utf8'
|
||||
);
|
||||
invalidateConfigCache();
|
||||
clearConfigCache();
|
||||
}
|
||||
|
||||
it('keeps local root URL for the original backend without stripping codex effort suffixes', () => {
|
||||
writeSettings(settingsPath, {
|
||||
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317',
|
||||
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
|
||||
@@ -48,7 +71,7 @@ describe('getEffectiveEnvVars local provider URL normalization', () => {
|
||||
});
|
||||
|
||||
const env = getEffectiveEnvVars('codex', 8317, settingsPath);
|
||||
expect(env.ANTHROPIC_BASE_URL).toBe('http://127.0.0.1:8317/api/provider/codex');
|
||||
expect(env.ANTHROPIC_BASE_URL).toBe('http://127.0.0.1:8317');
|
||||
expect(env.ANTHROPIC_MODEL).toBe('gpt-5.3-codex-xhigh');
|
||||
expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.3-codex-xhigh');
|
||||
expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.3-codex-high');
|
||||
@@ -63,7 +86,37 @@ describe('getEffectiveEnvVars local provider URL normalization', () => {
|
||||
expect(persisted.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5.4-mini-medium');
|
||||
});
|
||||
|
||||
it('rewrites wrong local provider path to the requested provider', () => {
|
||||
it('rewrites local root URL to provider endpoint for the plus backend', () => {
|
||||
writeBackendConfig('plus');
|
||||
writeSettings(settingsPath, {
|
||||
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317',
|
||||
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
|
||||
ANTHROPIC_MODEL: 'gpt-5.3-codex-xhigh',
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: 'gpt-5.3-codex-xhigh',
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: 'gpt-5.3-codex-high',
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gpt-5.4-mini-medium',
|
||||
});
|
||||
|
||||
const env = getEffectiveEnvVars('codex', 8317, settingsPath);
|
||||
expect(env.ANTHROPIC_BASE_URL).toBe('http://127.0.0.1:8317/api/provider/codex');
|
||||
});
|
||||
|
||||
it('rewrites wrong local provider path to root for the original backend', () => {
|
||||
writeSettings(settingsPath, {
|
||||
ANTHROPIC_BASE_URL: 'http://localhost:8317/api/provider/my-codex-variant?debug=1',
|
||||
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
|
||||
ANTHROPIC_MODEL: 'gpt-5.3-codex-xhigh',
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: 'gpt-5.3-codex-xhigh',
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: 'gpt-5.3-codex-high',
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gpt-5.4-mini-medium',
|
||||
});
|
||||
|
||||
const env = getEffectiveEnvVars('codex', 8317, settingsPath);
|
||||
expect(env.ANTHROPIC_BASE_URL).toBe('http://localhost:8317');
|
||||
});
|
||||
|
||||
it('rewrites wrong local provider path to the requested provider for the plus backend', () => {
|
||||
writeBackendConfig('plus');
|
||||
writeSettings(settingsPath, {
|
||||
ANTHROPIC_BASE_URL: 'http://localhost:8317/api/provider/my-codex-variant?debug=1',
|
||||
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
|
||||
@@ -285,7 +338,6 @@ describe('getEffectiveEnvVars local provider URL normalization', () => {
|
||||
});
|
||||
|
||||
it('repairs existing provider settings files that are missing env keys', () => {
|
||||
process.env.CCS_HOME = tempHome;
|
||||
const agySettingsPath = path.join(tempHome, '.ccs', 'agy.settings.json');
|
||||
fs.mkdirSync(path.dirname(agySettingsPath), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
@@ -308,7 +360,7 @@ describe('getEffectiveEnvVars local provider URL normalization', () => {
|
||||
hooks?: Record<string, unknown>;
|
||||
};
|
||||
expect(repaired.hooks?.PreToolUse).toBeDefined();
|
||||
expect(repaired.env?.ANTHROPIC_BASE_URL).toBe('http://127.0.0.1:8317/api/provider/agy');
|
||||
expect(repaired.env?.ANTHROPIC_BASE_URL).toBe('http://127.0.0.1:8317');
|
||||
expect(repaired.env?.ANTHROPIC_MODEL).toBeDefined();
|
||||
expect(repaired.env?.ANTHROPIC_DEFAULT_OPUS_MODEL).toBeDefined();
|
||||
expect(repaired.env?.ANTHROPIC_DEFAULT_SONNET_MODEL).toBeDefined();
|
||||
@@ -316,7 +368,7 @@ describe('getEffectiveEnvVars local provider URL normalization', () => {
|
||||
});
|
||||
|
||||
it('imports legacy cursor settings into the dedicated provider path without reusing legacy transport auth', () => {
|
||||
process.env.CCS_HOME = tempHome;
|
||||
writeBackendConfig('plus');
|
||||
const legacySettingsPath = path.join(tempHome, '.ccs', 'cursor.settings.json');
|
||||
const providerSettingsPath = path.join(
|
||||
tempHome,
|
||||
@@ -375,7 +427,6 @@ describe('getEffectiveEnvVars local provider URL normalization', () => {
|
||||
});
|
||||
|
||||
it('migrates deprecated agy sonnet 4.6 thinking IDs during ensureProviderSettings', () => {
|
||||
process.env.CCS_HOME = tempHome;
|
||||
const agySettingsPath = path.join(tempHome, '.ccs', 'agy.settings.json');
|
||||
fs.mkdirSync(path.dirname(agySettingsPath), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
@@ -422,7 +473,6 @@ describe('getEffectiveEnvVars local provider URL normalization', () => {
|
||||
});
|
||||
|
||||
it('preserves codex effort-suffixed IDs during ensureProviderSettings', () => {
|
||||
process.env.CCS_HOME = tempHome;
|
||||
const codexSettingsPath = path.join(tempHome, '.ccs', 'codex.settings.json');
|
||||
fs.mkdirSync(path.dirname(codexSettingsPath), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
@@ -458,6 +508,7 @@ describe('getEffectiveEnvVars local provider URL normalization', () => {
|
||||
env?: Record<string, string>;
|
||||
presets?: Array<Record<string, string>>;
|
||||
};
|
||||
expect(repaired.env?.ANTHROPIC_BASE_URL).toBe('http://127.0.0.1:8317');
|
||||
expect(repaired.env?.ANTHROPIC_MODEL).toBe('gpt-5.3-codex-xhigh');
|
||||
expect(repaired.env?.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.3-codex-xhigh');
|
||||
expect(repaired.env?.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.3-codex-high');
|
||||
@@ -469,7 +520,6 @@ describe('getEffectiveEnvVars local provider URL normalization', () => {
|
||||
});
|
||||
|
||||
it('recovers malformed provider settings files by writing defaults and backup copy', () => {
|
||||
process.env.CCS_HOME = tempHome;
|
||||
const agySettingsPath = path.join(tempHome, '.ccs', 'agy.settings.json');
|
||||
fs.mkdirSync(path.dirname(agySettingsPath), { recursive: true });
|
||||
fs.writeFileSync(agySettingsPath, '{"env": {"ANTHROPIC_MODEL": "claude-sonnet-4-6-thinking",}');
|
||||
|
||||
@@ -36,6 +36,10 @@ import {
|
||||
getOutputLimitsEnv,
|
||||
getCcsDir,
|
||||
} from '../../config/config-loader-facade';
|
||||
import { buildCliproxyProviderPath, buildLocalProviderBaseUrl } from './provider-route';
|
||||
import { ConfigError } from '../../errors/error-types';
|
||||
|
||||
export { buildCliproxyProviderPath, buildLocalProviderBaseUrl } from './provider-route';
|
||||
|
||||
/** Settings file structure for user overrides */
|
||||
interface ProviderSettings {
|
||||
@@ -205,14 +209,12 @@ export function getModelMapping(provider: CLIProxyProvider): ProviderModelMappin
|
||||
|
||||
/**
|
||||
* Get environment variables for Claude CLI (bundled defaults)
|
||||
* Uses provider-specific endpoints (e.g., /api/provider/gemini) for explicit routing
|
||||
* except for the built-in claude provider.
|
||||
* Uses the backend-aware route path for the selected provider.
|
||||
*
|
||||
* Root-URL exception: the claude provider always uses the CLIProxy ROOT endpoint
|
||||
* (http://127.0.0.1:<port>) instead of /api/provider/claude. CLIProxyAPI's Claude Code
|
||||
* contract registers /v1/messages at the root; the /api/provider/ prefix is a Plus-only
|
||||
* feature for non-Claude providers. buildCliproxyProviderPath() encodes this rule and is
|
||||
* used here and by the api-create bridge path so both remain consistent.
|
||||
* Root-URL rule: the original CLIProxy backend uses the root endpoint
|
||||
* (http://127.0.0.1:<port>) and routes by model. The /api/provider/<x> prefix is
|
||||
* only used for Plus-backend non-Claude providers. buildCliproxyProviderPath()
|
||||
* encodes this rule and is reused by the api-create bridge path.
|
||||
*
|
||||
* For the claude built-in provider the model env vars are intentionally omitted so that
|
||||
* the user's own Claude Code /model selection is honored end-to-end (model-neutral passthrough).
|
||||
@@ -375,25 +377,6 @@ function ensureRequiredEnvVars(
|
||||
/** Localhost hostnames used for local CLIProxy endpoints */
|
||||
const LOCALHOST_NAMES = new Set(['127.0.0.1', 'localhost', '0.0.0.0']);
|
||||
|
||||
/**
|
||||
* Return the CLIProxy route path for a provider.
|
||||
*
|
||||
* - claude uses the root path (empty string → "/" after buildProxyUrl normalises it)
|
||||
* because CLIProxyAPI's Claude Code contract registers /v1/messages at the root; the
|
||||
* /api/provider/ prefix is Plus-only and only for non-Claude providers.
|
||||
* - all other providers use the scoped /api/provider/<x> path.
|
||||
*
|
||||
* Exported so the profile-bridge can reuse the same rule (DRY).
|
||||
*/
|
||||
export function buildCliproxyProviderPath(provider: CLIProxyProvider): string {
|
||||
return provider === 'claude' ? '' : `/api/provider/${provider}`;
|
||||
}
|
||||
|
||||
function buildLocalProviderBaseUrl(provider: CLIProxyProvider, port: number): string {
|
||||
const rootUrl = `http://127.0.0.1:${port}`;
|
||||
return provider === 'claude' ? rootUrl : `${rootUrl}/api/provider/${provider}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize local CLIProxy endpoint to the expected provider route.
|
||||
* Only rewrites localhost URLs that target the active local port.
|
||||
@@ -415,11 +398,11 @@ function normalizeLocalProviderBaseUrl(
|
||||
: 80;
|
||||
if (!Number.isFinite(effectivePort) || effectivePort !== port) return baseUrl;
|
||||
|
||||
if (provider === 'claude') {
|
||||
const expectedPath = buildCliproxyProviderPath(provider);
|
||||
if (expectedPath === '') {
|
||||
return parsed.origin;
|
||||
}
|
||||
|
||||
const expectedPath = `/api/provider/${provider}`;
|
||||
if (parsed.pathname === expectedPath && !parsed.search && !parsed.hash) return baseUrl;
|
||||
|
||||
parsed.pathname = expectedPath;
|
||||
@@ -437,7 +420,6 @@ function normalizeLocalProviderBaseUrl(
|
||||
*/
|
||||
function rewriteLocalhostUrls(
|
||||
envVars: NodeJS.ProcessEnv,
|
||||
provider: CLIProxyProvider,
|
||||
remoteConfig: RemoteProxyRewriteConfig
|
||||
): NodeJS.ProcessEnv {
|
||||
const result = { ...envVars };
|
||||
@@ -458,10 +440,7 @@ function rewriteLocalhostUrls(
|
||||
const standardWebPort = normalizedProtocol === 'https' ? 443 : 80;
|
||||
const portSuffix = effectivePort === standardWebPort ? '' : `:${effectivePort}`;
|
||||
const remoteRootUrl = `${normalizedProtocol}://${remoteConfig.host}${portSuffix}`;
|
||||
const remoteBaseUrl =
|
||||
provider === 'claude' ? remoteRootUrl : `${remoteRootUrl}/api/provider/${provider}`;
|
||||
|
||||
result.ANTHROPIC_BASE_URL = remoteBaseUrl;
|
||||
result.ANTHROPIC_BASE_URL = remoteRootUrl;
|
||||
|
||||
// Update auth token if provided
|
||||
if (remoteConfig.authToken) {
|
||||
@@ -517,7 +496,7 @@ export function getEffectiveEnvVars(
|
||||
envVars = ensureRequiredEnvVars(envVars, provider, port);
|
||||
// Apply remote rewrite if configured
|
||||
if (remoteRewriteConfig) {
|
||||
envVars = rewriteLocalhostUrls(envVars, provider, remoteRewriteConfig);
|
||||
envVars = rewriteLocalhostUrls(envVars, remoteRewriteConfig);
|
||||
}
|
||||
return envVars;
|
||||
}
|
||||
@@ -553,7 +532,7 @@ export function getEffectiveEnvVars(
|
||||
envVars = ensureRequiredEnvVars(envVars, provider, port);
|
||||
// Apply remote rewrite if configured
|
||||
if (remoteRewriteConfig) {
|
||||
envVars = rewriteLocalhostUrls(envVars, provider, remoteRewriteConfig);
|
||||
envVars = rewriteLocalhostUrls(envVars, remoteRewriteConfig);
|
||||
}
|
||||
return envVars;
|
||||
}
|
||||
@@ -696,10 +675,11 @@ export function ensureProviderSettings(provider: CLIProxyProvider): void {
|
||||
let parsed: Record<string, unknown>;
|
||||
try {
|
||||
const value = JSON.parse(rawContent) as unknown;
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
throw new Error('settings root must be an object');
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
parsed = value as Record<string, unknown>;
|
||||
} else {
|
||||
throw new SyntaxError('settings root must be an object');
|
||||
}
|
||||
parsed = value as Record<string, unknown>;
|
||||
} catch {
|
||||
// Preserve corrupt payload for manual inspection, then recover with defaults.
|
||||
const backupPath = `${settingsPath}.corrupt-${Date.now()}`;
|
||||
@@ -744,7 +724,7 @@ export function ensureProviderSettings(provider: CLIProxyProvider): void {
|
||||
}
|
||||
}
|
||||
|
||||
if (provider === 'claude' && typeof mergedEnv.ANTHROPIC_BASE_URL === 'string') {
|
||||
if (typeof mergedEnv.ANTHROPIC_BASE_URL === 'string') {
|
||||
const normalizedBaseUrl = normalizeLocalProviderBaseUrl(
|
||||
mergedEnv.ANTHROPIC_BASE_URL,
|
||||
provider,
|
||||
@@ -1002,7 +982,7 @@ export function getCompositeEnvVars(
|
||||
|
||||
// If default tier is missing, we cannot proceed meaningfully
|
||||
if (!defaultModel) {
|
||||
throw new Error(`Missing model for default tier '${defaultTier}'`);
|
||||
throw new ConfigError(`Missing model for default tier '${defaultTier}'`);
|
||||
}
|
||||
|
||||
// Determine base URL and auth token based on remote vs local mode
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { loadOrCreateUnifiedConfig } from '../../config/config-loader-facade';
|
||||
import { DEFAULT_BACKEND } from '../binary/platform-detector';
|
||||
import type { CLIProxyBackend, CLIProxyProvider } from '../types';
|
||||
|
||||
export function getConfiguredCliproxyBackend(): CLIProxyBackend {
|
||||
try {
|
||||
const backend = loadOrCreateUnifiedConfig().cliproxy?.backend;
|
||||
return backend === 'plus' || backend === 'original' ? backend : DEFAULT_BACKEND;
|
||||
} catch {
|
||||
return DEFAULT_BACKEND;
|
||||
}
|
||||
}
|
||||
|
||||
export function usesScopedProviderRoutes(
|
||||
backend: CLIProxyBackend = getConfiguredCliproxyBackend()
|
||||
): boolean {
|
||||
return backend === 'plus';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the CLIProxy route path for a provider.
|
||||
*
|
||||
* The original backend routes Claude-compatible traffic at the root and relies
|
||||
* on model-based provider selection. The Plus backend exposes provider-scoped
|
||||
* routes for non-Claude providers.
|
||||
*/
|
||||
export function buildCliproxyProviderPath(
|
||||
provider: CLIProxyProvider,
|
||||
backend: CLIProxyBackend = getConfiguredCliproxyBackend()
|
||||
): string {
|
||||
if (provider === 'claude') return '';
|
||||
return usesScopedProviderRoutes(backend) ? `/api/provider/${provider}` : '';
|
||||
}
|
||||
|
||||
export function buildLocalProviderBaseUrl(
|
||||
provider: CLIProxyProvider,
|
||||
port: number,
|
||||
backend: CLIProxyBackend = getConfiguredCliproxyBackend()
|
||||
): string {
|
||||
const rootUrl = `http://127.0.0.1:${port}`;
|
||||
return `${rootUrl}${buildCliproxyProviderPath(provider, backend)}`;
|
||||
}
|
||||
@@ -1,11 +1,27 @@
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { afterEach, describe, expect, it } from 'bun:test';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
||||
import { buildClaudeEnvironment, resolveCliproxyImageAnalysisEnv } from '../env-resolver';
|
||||
import type { ImageAnalysisStatus } from '../../../utils/hooks';
|
||||
import { invalidateConfigCache } from '../../../config/config-loader-facade';
|
||||
import { clearConfigCache } from '../../config/base-config-loader';
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
let originalCcsHome: string | undefined;
|
||||
let configHome = '';
|
||||
|
||||
function writeBackendConfig(backend: 'original' | 'plus'): void {
|
||||
const ccsDir = path.join(configHome, '.ccs');
|
||||
fs.mkdirSync(ccsDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(ccsDir, 'config.yaml'),
|
||||
['version: 1', 'cliproxy:', ` backend: ${backend}`, ''].join('\n'),
|
||||
'utf8'
|
||||
);
|
||||
invalidateConfigCache();
|
||||
clearConfigCache();
|
||||
}
|
||||
|
||||
function createCodexSettingsFile(models: {
|
||||
defaultModel: string;
|
||||
@@ -70,7 +86,23 @@ function createImageAnalysisStatus(
|
||||
}
|
||||
|
||||
describe('buildClaudeEnvironment codex fallback normalization', () => {
|
||||
beforeEach(() => {
|
||||
originalCcsHome = process.env.CCS_HOME;
|
||||
configHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-env-resolver-home-'));
|
||||
tempDirs.push(configHome);
|
||||
process.env.CCS_HOME = configHome;
|
||||
invalidateConfigCache();
|
||||
clearConfigCache();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalCcsHome !== undefined) {
|
||||
process.env.CCS_HOME = originalCcsHome;
|
||||
} else {
|
||||
delete process.env.CCS_HOME;
|
||||
}
|
||||
invalidateConfigCache();
|
||||
clearConfigCache();
|
||||
while (tempDirs.length > 0) {
|
||||
const tempDir = tempDirs.pop();
|
||||
if (tempDir) {
|
||||
@@ -99,6 +131,7 @@ describe('buildClaudeEnvironment codex fallback normalization', () => {
|
||||
expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.3-codex(xhigh)');
|
||||
expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.3-codex(high)');
|
||||
expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5.4-mini(medium)');
|
||||
expect(env.ANTHROPIC_BASE_URL).toBe('http://127.0.0.1:8317');
|
||||
});
|
||||
|
||||
it('keeps codex effort aliases when reasoning proxy is active', () => {
|
||||
@@ -122,6 +155,27 @@ describe('buildClaudeEnvironment codex fallback normalization', () => {
|
||||
expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.3-codex-xhigh');
|
||||
expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.3-codex-high');
|
||||
expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5.4-mini-medium');
|
||||
expect(env.ANTHROPIC_BASE_URL).toBe('http://127.0.0.1:9444');
|
||||
});
|
||||
|
||||
it('keeps scoped codex reasoning URL when the plus backend is active', () => {
|
||||
writeBackendConfig('plus');
|
||||
const settingsPath = createCodexSettingsFile({
|
||||
defaultModel: 'gpt-5.3-codex-high',
|
||||
opusModel: 'gpt-5.3-codex-xhigh',
|
||||
sonnetModel: 'gpt-5.3-codex-high',
|
||||
haikuModel: 'gpt-5.4-mini-medium',
|
||||
});
|
||||
|
||||
const env = buildClaudeEnvironment({
|
||||
provider: 'codex',
|
||||
useRemoteProxy: false,
|
||||
localPort: 8317,
|
||||
customSettingsPath: settingsPath,
|
||||
codexReasoningPort: 9444,
|
||||
verbose: false,
|
||||
});
|
||||
|
||||
expect(env.ANTHROPIC_BASE_URL).toBe('http://127.0.0.1:9444/api/provider/codex');
|
||||
});
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
import type { ProxyTarget } from '../proxy/proxy-target-resolver';
|
||||
import { getEffectiveApiKey } from '../auth/auth-token-manager';
|
||||
import { isSettings, type Settings } from '../../types/config';
|
||||
import { buildCliproxyProviderPath } from '../config/provider-route';
|
||||
|
||||
export interface RemoteProxyConfig {
|
||||
host: string;
|
||||
@@ -373,7 +374,10 @@ export function buildClaudeEnvironment(config: ProxyChainConfig): Record<string,
|
||||
|
||||
if (codexReasoningPort) {
|
||||
// Codex reasoning proxy is the outermost layer for codex provider
|
||||
finalBaseUrl = `http://127.0.0.1:${codexReasoningPort}/api/provider/codex`;
|
||||
const providerPath = useRemoteProxy
|
||||
? '/api/provider/codex'
|
||||
: buildCliproxyProviderPath('codex');
|
||||
finalBaseUrl = `http://127.0.0.1:${codexReasoningPort}${providerPath}`;
|
||||
}
|
||||
|
||||
const effectiveEnvVars = {
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
import { shouldDisableCodexReasoning } from './thinking-override-resolver';
|
||||
import type { CLIProxyProvider, ExecutorConfig, ResolvedProxyConfig } from '../types';
|
||||
import type { ThinkingConfig } from '../../config/unified-config-types';
|
||||
import { buildCliproxyProviderPath } from '../config/provider-route';
|
||||
|
||||
// ── Proxy constructor types (for dependency injection in tests) ───────────────
|
||||
|
||||
@@ -162,9 +163,10 @@ export async function buildProxyChain(context: ProxyChainContext): Promise<Proxy
|
||||
stripPathPrefix,
|
||||
});
|
||||
codexReasoningPort = await codexReasoningProxy.start();
|
||||
log(
|
||||
`Codex reasoning proxy active: http://127.0.0.1:${codexReasoningPort}/api/provider/codex`
|
||||
);
|
||||
const providerPath = useRemoteProxy
|
||||
? '/api/provider/codex'
|
||||
: buildCliproxyProviderPath('codex');
|
||||
log(`Codex reasoning proxy active: http://127.0.0.1:${codexReasoningPort}${providerPath}`);
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
codexReasoningProxy = null;
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
} from '../web-server/services/compatible-cli-toml-file-service';
|
||||
import { getModelMaxLevel } from '../cliproxy/model-catalog';
|
||||
import { parseCodexModelTuningAlias } from '../cliproxy/ai-providers/model-id-normalizer';
|
||||
import { buildLocalProviderBaseUrl } from '../cliproxy/config/provider-route';
|
||||
import { ConfigError } from '../errors/error-types';
|
||||
|
||||
export const CCSXP_CLIPROXY_SHORTCUT_ENV = 'CCSXP_CLIPROXY_SHORTCUT';
|
||||
export const CODEX_CLIPROXY_PROVIDER_ID = 'cliproxy';
|
||||
@@ -37,7 +39,7 @@ function resolveCodexConfigPath(env: NodeJS.ProcessEnv = process.env): {
|
||||
}
|
||||
|
||||
export function buildCodexCliproxyProviderBaseUrl(port: number): string {
|
||||
return `http://127.0.0.1:${port}/api/provider/codex`;
|
||||
return buildLocalProviderBaseUrl('codex', port);
|
||||
}
|
||||
|
||||
export function isCcsxpCliproxyShortcut(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
@@ -70,10 +72,66 @@ function resolveProviderEnvKey(provider: Record<string, unknown> | null): string
|
||||
return CODEX_CLIPROXY_PROVIDER_ENV_KEY;
|
||||
}
|
||||
|
||||
function isProviderReady(provider: Record<string, unknown>, envKey: string): boolean {
|
||||
const LOCALHOST_NAMES = new Set(['127.0.0.1', 'localhost', '0.0.0.0']);
|
||||
|
||||
function getEffectiveUrlPort(url: URL): number {
|
||||
if (url.port) return Number.parseInt(url.port, 10);
|
||||
return url.protocol === 'https:' ? 443 : 80;
|
||||
}
|
||||
|
||||
function isManagedLocalUrl(url: URL): boolean {
|
||||
return ['http:', 'https:'].includes(url.protocol) && LOCALHOST_NAMES.has(url.hostname);
|
||||
}
|
||||
|
||||
function shouldUseManagedLocalBaseUrl(current: URL, expected: URL): boolean {
|
||||
if (!isManagedLocalUrl(current) || !isManagedLocalUrl(expected)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentPort = getEffectiveUrlPort(current);
|
||||
const expectedPort = getEffectiveUrlPort(expected);
|
||||
if (currentPort !== expectedPort) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const currentPath = current.pathname.replace(/\/+$/, '') || '/';
|
||||
const expectedPath = expected.pathname.replace(/\/+$/, '') || '/';
|
||||
return currentPath !== expectedPath;
|
||||
}
|
||||
|
||||
function resolveProviderBaseUrl(
|
||||
provider: Record<string, unknown>,
|
||||
fallbackBaseUrl: string
|
||||
): string {
|
||||
const baseUrl = provider.base_url;
|
||||
if (!isValidCodexCliproxyBaseUrl(baseUrl)) {
|
||||
return fallbackBaseUrl;
|
||||
}
|
||||
|
||||
const trimmed = baseUrl.trim();
|
||||
try {
|
||||
const current = new URL(trimmed);
|
||||
const expected = new URL(fallbackBaseUrl);
|
||||
if (shouldUseManagedLocalBaseUrl(current, expected)) {
|
||||
return fallbackBaseUrl;
|
||||
}
|
||||
} catch {
|
||||
return fallbackBaseUrl;
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function isProviderReady(
|
||||
provider: Record<string, unknown>,
|
||||
envKey: string,
|
||||
expectedBaseUrl: string
|
||||
): boolean {
|
||||
return (
|
||||
provider.name === CODEX_CLIPROXY_PROVIDER_NAME &&
|
||||
isValidCodexCliproxyBaseUrl(provider.base_url) &&
|
||||
typeof provider.base_url === 'string' &&
|
||||
resolveProviderBaseUrl(provider, expectedBaseUrl) === provider.base_url.trim() &&
|
||||
provider.env_key === envKey &&
|
||||
provider.wire_api === 'responses' &&
|
||||
provider.requires_openai_auth === false &&
|
||||
@@ -92,17 +150,6 @@ function buildProviderConfig(baseUrl: string, envKey: string): Record<string, un
|
||||
};
|
||||
}
|
||||
|
||||
function resolveProviderBaseUrl(
|
||||
provider: Record<string, unknown>,
|
||||
fallbackBaseUrl: string
|
||||
): string {
|
||||
const baseUrl = provider.base_url;
|
||||
if (isValidCodexCliproxyBaseUrl(baseUrl)) {
|
||||
return baseUrl.trim();
|
||||
}
|
||||
return fallbackBaseUrl;
|
||||
}
|
||||
|
||||
function appendProviderBlock(rawText: string, baseUrl: string): string {
|
||||
const prefix = rawText.trimEnd();
|
||||
const providerBlock = stringifyTomlObject({
|
||||
@@ -135,10 +182,16 @@ export async function ensureCodexCliproxyProviderConfig(
|
||||
const fileProbe = await probeTomlObjectFile(configPath, 'Codex user config', displayPath);
|
||||
|
||||
if (fileProbe.diagnostics.readError) {
|
||||
throw new Error(`Cannot repair ${displayPath}: ${fileProbe.diagnostics.readError}`);
|
||||
throw new ConfigError(
|
||||
`Cannot repair ${displayPath}: ${fileProbe.diagnostics.readError}`,
|
||||
configPath
|
||||
);
|
||||
}
|
||||
if (fileProbe.diagnostics.parseError) {
|
||||
throw new Error(`Cannot repair ${displayPath}: ${fileProbe.diagnostics.parseError}`);
|
||||
throw new ConfigError(
|
||||
`Cannot repair ${displayPath}: ${fileProbe.diagnostics.parseError}`,
|
||||
configPath
|
||||
);
|
||||
}
|
||||
|
||||
const config = fileProbe.config ?? {};
|
||||
@@ -148,7 +201,10 @@ export async function ensureCodexCliproxyProviderConfig(
|
||||
const normalizedModelAlias = normalizeTopLevelCodexModelAlias(config);
|
||||
|
||||
if (modelProvidersValue !== undefined && !providers) {
|
||||
throw new Error(`Cannot repair ${displayPath}: [model_providers] must be a table.`);
|
||||
throw new ConfigError(
|
||||
`Cannot repair ${displayPath}: [model_providers] must be a table.`,
|
||||
configPath
|
||||
);
|
||||
}
|
||||
|
||||
if (!providers || !Object.prototype.hasOwnProperty.call(providers, CODEX_CLIPROXY_PROVIDER_ID)) {
|
||||
@@ -174,13 +230,14 @@ export async function ensureCodexCliproxyProviderConfig(
|
||||
|
||||
const currentProvider = asObject(providers[CODEX_CLIPROXY_PROVIDER_ID]);
|
||||
if (!currentProvider) {
|
||||
throw new Error(
|
||||
`Cannot repair ${displayPath}: [model_providers.${CODEX_CLIPROXY_PROVIDER_ID}] must be a table.`
|
||||
throw new ConfigError(
|
||||
`Cannot repair ${displayPath}: [model_providers.${CODEX_CLIPROXY_PROVIDER_ID}] must be a table.`,
|
||||
configPath
|
||||
);
|
||||
}
|
||||
|
||||
const envKey = resolveProviderEnvKey(currentProvider);
|
||||
const providerReady = isProviderReady(currentProvider, envKey);
|
||||
const providerReady = isProviderReady(currentProvider, envKey, expectedBaseUrl);
|
||||
|
||||
if (!providerReady) {
|
||||
providers[CODEX_CLIPROXY_PROVIDER_ID] = {
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
resolveCliproxyBridgeProfile,
|
||||
suggestCliproxyBridgeName,
|
||||
} from '../../../src/api/services/cliproxy-profile-bridge';
|
||||
import { invalidateConfigCache } from '../../../src/config/config-loader-facade';
|
||||
import { clearConfigCache } from '../../../src/cliproxy/config/base-config-loader';
|
||||
|
||||
describe('cliproxy-profile-bridge', () => {
|
||||
let tempHome = '';
|
||||
@@ -17,6 +19,8 @@ describe('cliproxy-profile-bridge', () => {
|
||||
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-cliproxy-bridge-'));
|
||||
originalCcsHome = process.env.CCS_HOME;
|
||||
process.env.CCS_HOME = tempHome;
|
||||
invalidateConfigCache();
|
||||
clearConfigCache();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -25,18 +29,32 @@ describe('cliproxy-profile-bridge', () => {
|
||||
} else {
|
||||
process.env.CCS_HOME = originalCcsHome;
|
||||
}
|
||||
invalidateConfigCache();
|
||||
clearConfigCache();
|
||||
|
||||
if (tempHome && fs.existsSync(tempHome)) {
|
||||
fs.rmSync(tempHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('resolves routed profile payload for a local CLIProxy provider', () => {
|
||||
function writeBackendConfig(backend: 'original' | 'plus'): void {
|
||||
const ccsDir = path.join(tempHome, '.ccs');
|
||||
fs.mkdirSync(ccsDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(ccsDir, 'config.yaml'),
|
||||
['version: 1', 'cliproxy:', ` backend: ${backend}`, ''].join('\n'),
|
||||
'utf8'
|
||||
);
|
||||
invalidateConfigCache();
|
||||
clearConfigCache();
|
||||
}
|
||||
|
||||
it('resolves routed profile payload for a local original-backend CLIProxy provider', () => {
|
||||
const bridge = resolveCliproxyBridgeProfile('gemini');
|
||||
|
||||
expect(bridge.name).toBe('gemini-api');
|
||||
expect(bridge.baseUrl).toBe('http://127.0.0.1:8317/api/provider/gemini');
|
||||
expect(bridge.routePath).toBe('/api/provider/gemini');
|
||||
expect(bridge.baseUrl).toBe('http://127.0.0.1:8317/');
|
||||
expect(bridge.routePath).toBe('/');
|
||||
expect(bridge.models.default.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
@@ -49,6 +67,7 @@ describe('cliproxy-profile-bridge', () => {
|
||||
});
|
||||
|
||||
it('detects CLIProxy-backed profile metadata and normalizes localhost loopback URLs', () => {
|
||||
writeBackendConfig('plus');
|
||||
const metadata = resolveCliproxyBridgeMetadata({
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: 'http://localhost:8317/api/provider/gemini',
|
||||
|
||||
@@ -6,24 +6,61 @@ import {
|
||||
buildCodexCliproxyProviderBaseUrl,
|
||||
ensureCodexCliproxyProviderConfig,
|
||||
} from '../../../src/targets/codex-cliproxy-provider-config';
|
||||
import { invalidateConfigCache } from '../../../src/config/config-loader-facade';
|
||||
import { clearConfigCache } from '../../../src/cliproxy/config/base-config-loader';
|
||||
|
||||
describe('codex cliproxy provider config repair', () => {
|
||||
let tempHome: string;
|
||||
let codexHome: string;
|
||||
let configPath: string;
|
||||
let env: NodeJS.ProcessEnv;
|
||||
let originalCcsHome: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
originalCcsHome = process.env.CCS_HOME;
|
||||
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-codex-provider-config-'));
|
||||
codexHome = path.join(tempHome, '.codex');
|
||||
configPath = path.join(codexHome, 'config.toml');
|
||||
env = { CODEX_HOME: codexHome } as NodeJS.ProcessEnv;
|
||||
process.env.CCS_HOME = tempHome;
|
||||
invalidateConfigCache();
|
||||
clearConfigCache();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalCcsHome !== undefined) {
|
||||
process.env.CCS_HOME = originalCcsHome;
|
||||
} else {
|
||||
delete process.env.CCS_HOME;
|
||||
}
|
||||
invalidateConfigCache();
|
||||
clearConfigCache();
|
||||
fs.rmSync(tempHome, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function writeBackendConfig(backend: 'original' | 'plus'): void {
|
||||
const ccsDir = path.join(tempHome, '.ccs');
|
||||
fs.mkdirSync(ccsDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(ccsDir, 'config.yaml'),
|
||||
['version: 1', 'cliproxy:', ` backend: ${backend}`, ''].join('\n'),
|
||||
'utf8'
|
||||
);
|
||||
invalidateConfigCache();
|
||||
clearConfigCache();
|
||||
}
|
||||
|
||||
it('uses the original backend root URL by default', () => {
|
||||
expect(buildCodexCliproxyProviderBaseUrl(8317)).toBe('http://127.0.0.1:8317');
|
||||
});
|
||||
|
||||
it('uses the plus backend scoped Codex URL when configured', () => {
|
||||
writeBackendConfig('plus');
|
||||
expect(buildCodexCliproxyProviderBaseUrl(8317)).toBe(
|
||||
'http://127.0.0.1:8317/api/provider/codex'
|
||||
);
|
||||
});
|
||||
|
||||
it('creates the cliproxy model provider when config.toml is missing', async () => {
|
||||
const result = await ensureCodexCliproxyProviderConfig(8317, env);
|
||||
|
||||
@@ -32,6 +69,7 @@ describe('codex cliproxy provider config repair', () => {
|
||||
const rawText = fs.readFileSync(configPath, 'utf8');
|
||||
expect(rawText).toContain('[model_providers.cliproxy]');
|
||||
expect(rawText).toContain('name = "CLIProxy Codex"');
|
||||
expect(rawText).toContain('base_url = "http://127.0.0.1:8317"');
|
||||
expect(rawText).toContain('env_key = "CLIPROXY_API_KEY"');
|
||||
expect(rawText).not.toContain('model_provider = "cliproxy"');
|
||||
});
|
||||
@@ -78,7 +116,7 @@ wire_api = "responses"
|
||||
expect(result.changed).toBe(true);
|
||||
expect(result.envKey).toBe('CLIPROXY_API_KEY');
|
||||
const rawText = fs.readFileSync(configPath, 'utf8');
|
||||
expect(rawText).toContain('base_url = "http://localhost:8317/api/provider/codex"');
|
||||
expect(rawText).toContain('base_url = "http://127.0.0.1:9321"');
|
||||
expect(rawText).toContain('env_key = "CLIPROXY_API_KEY"');
|
||||
expect(rawText).toContain('requires_openai_auth = false');
|
||||
expect(rawText).toContain('supports_websockets = false');
|
||||
@@ -142,7 +180,29 @@ supports_websockets = false
|
||||
expect(fs.readFileSync(configPath, 'utf8')).toBe(rawText);
|
||||
});
|
||||
|
||||
it('leaves a ready localhost provider unchanged', async () => {
|
||||
it('repairs a stale ready localhost provider to the original backend root URL', async () => {
|
||||
fs.mkdirSync(codexHome, { recursive: true });
|
||||
const rawText = `[model_providers.cliproxy]
|
||||
name = "CLIProxy Codex"
|
||||
base_url = "http://localhost:8317/api/provider/codex"
|
||||
env_key = "CLIPROXY_API_KEY"
|
||||
wire_api = "responses"
|
||||
requires_openai_auth = false
|
||||
supports_websockets = false
|
||||
`;
|
||||
fs.writeFileSync(configPath, rawText, 'utf8');
|
||||
|
||||
const result = await ensureCodexCliproxyProviderConfig(8317, env);
|
||||
|
||||
expect(result.changed).toBe(true);
|
||||
expect(result.envKey).toBe('CLIPROXY_API_KEY');
|
||||
const repairedText = fs.readFileSync(configPath, 'utf8');
|
||||
expect(repairedText).toContain('base_url = "http://127.0.0.1:8317"');
|
||||
expect(repairedText).not.toContain('/api/provider/codex');
|
||||
});
|
||||
|
||||
it('leaves a ready plus-backend localhost provider unchanged', async () => {
|
||||
writeBackendConfig('plus');
|
||||
fs.mkdirSync(codexHome, { recursive: true });
|
||||
const rawText = `[model_providers.cliproxy]
|
||||
name = "CLIProxy Codex"
|
||||
@@ -188,7 +248,7 @@ supports_websockets = false
|
||||
|
||||
[model_providers.cliproxy]
|
||||
name = "CLIProxy Codex"
|
||||
base_url = "http://localhost:8317/api/provider/codex"
|
||||
base_url = "http://127.0.0.1:8317"
|
||||
env_key = "CLIPROXY_API_KEY"
|
||||
wire_api = "responses"
|
||||
requires_openai_auth = false
|
||||
@@ -239,7 +299,7 @@ supports_websockets = false
|
||||
|
||||
[model_providers.cliproxy]
|
||||
name = "CLIProxy Codex"
|
||||
base_url = "http://localhost:8317/api/provider/codex"
|
||||
base_url = "http://127.0.0.1:8317"
|
||||
env_key = "CLIPROXY_API_KEY"
|
||||
wire_api = "responses"
|
||||
requires_openai_auth = false
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { afterEach, describe, expect, it } from 'bun:test';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import type { UnifiedConfig } from '../../../../src/config/unified-config-types';
|
||||
import { runBrowserSetup, type BrowserSetupDeps } from '../../../../src/utils/browser/browser-setup';
|
||||
import {
|
||||
runBrowserSetup,
|
||||
type BrowserSetupDeps,
|
||||
} from '../../../../src/utils/browser/browser-setup';
|
||||
|
||||
function createUnifiedConfig(userDataDir: string): UnifiedConfig {
|
||||
return {
|
||||
@@ -96,8 +99,36 @@ function createUnifiedConfig(userDataDir: string): UnifiedConfig {
|
||||
|
||||
describe('browser setup', () => {
|
||||
let tempDir = '';
|
||||
let originalBrowserUserDataDir: string | undefined;
|
||||
let originalBrowserProfileDir: string | undefined;
|
||||
let originalBrowserDevtoolsPort: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
originalBrowserUserDataDir = process.env.CCS_BROWSER_USER_DATA_DIR;
|
||||
originalBrowserProfileDir = process.env.CCS_BROWSER_PROFILE_DIR;
|
||||
originalBrowserDevtoolsPort = process.env.CCS_BROWSER_DEVTOOLS_PORT;
|
||||
delete process.env.CCS_BROWSER_USER_DATA_DIR;
|
||||
delete process.env.CCS_BROWSER_PROFILE_DIR;
|
||||
delete process.env.CCS_BROWSER_DEVTOOLS_PORT;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalBrowserUserDataDir !== undefined) {
|
||||
process.env.CCS_BROWSER_USER_DATA_DIR = originalBrowserUserDataDir;
|
||||
} else {
|
||||
delete process.env.CCS_BROWSER_USER_DATA_DIR;
|
||||
}
|
||||
if (originalBrowserProfileDir !== undefined) {
|
||||
process.env.CCS_BROWSER_PROFILE_DIR = originalBrowserProfileDir;
|
||||
} else {
|
||||
delete process.env.CCS_BROWSER_PROFILE_DIR;
|
||||
}
|
||||
if (originalBrowserDevtoolsPort !== undefined) {
|
||||
process.env.CCS_BROWSER_DEVTOOLS_PORT = originalBrowserDevtoolsPort;
|
||||
} else {
|
||||
delete process.env.CCS_BROWSER_DEVTOOLS_PORT;
|
||||
}
|
||||
|
||||
if (tempDir) {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
tempDir = '';
|
||||
|
||||
@@ -5,6 +5,8 @@ import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import type { Server } from 'http';
|
||||
import profileRoutes from '../../../src/web-server/routes/profile-routes';
|
||||
import { invalidateConfigCache } from '../../../src/config/config-loader-facade';
|
||||
import { clearConfigCache } from '../../../src/cliproxy/config/base-config-loader';
|
||||
|
||||
describe('profile-routes cliproxy bridge', () => {
|
||||
let server: Server;
|
||||
@@ -42,6 +44,8 @@ describe('profile-routes cliproxy bridge', () => {
|
||||
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-profile-routes-cliproxy-bridge-'));
|
||||
originalCcsHome = process.env.CCS_HOME;
|
||||
process.env.CCS_HOME = tempHome;
|
||||
invalidateConfigCache();
|
||||
clearConfigCache();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -50,6 +54,8 @@ describe('profile-routes cliproxy bridge', () => {
|
||||
} else {
|
||||
delete process.env.CCS_HOME;
|
||||
}
|
||||
invalidateConfigCache();
|
||||
clearConfigCache();
|
||||
|
||||
if (tempHome && fs.existsSync(tempHome)) {
|
||||
fs.rmSync(tempHome, { recursive: true, force: true });
|
||||
|
||||
Reference in new issue
Block a user