fix(cliproxy): use root URL for Claude provider (#1554)

Built-in Claude provider now uses the CLIProxy root URL instead of /api/provider/claude (which 404'd since base CLIProxy registers /v1/messages at root). Repairs stale local Claude settings, keeps non-Claude providers on their scoped routes, and applies the same root-URL rule to the ccs api create --cliproxy-provider claude bridge path via a shared buildCliproxyProviderPath helper. Closes #1545.
This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-06-16 10:23:48 -04:00
1 parent 81e9bdea5d
commit 49a7b9dc85
6 files changed
+293 -31

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
{
"env": {
"ANTHROPIC_BASE_URL": "http://127.0.0.1:8317/api/provider/claude",
"ANTHROPIC_BASE_URL": "http://127.0.0.1:8317",
"ANTHROPIC_AUTH_TOKEN": "ccs-internal-managed"
}
}
@@ -0,0 +1,131 @@
/**
* Regression tests for the `ccs api create --cliproxy-provider claude` 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
* (resolveCliproxyBridgeProfile / listCliproxyBridgeProviders) so both paths stay
* consistent.
*
* 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.
*/
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import {
resolveCliproxyBridgeProfile,
listCliproxyBridgeProviders,
} from '../cliproxy-profile-bridge';
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', () => {
let tempHome: string;
let originalCcsHome: string | undefined;
beforeEach(() => {
originalCcsHome = process.env.CCS_HOME;
// Empty temp dir → loadOrCreateUnifiedConfig defaults to local target (127.0.0.1:8317).
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-bridge-test-'));
process.env.CCS_HOME = tempHome;
invalidateConfigCache();
clearConfigCache();
});
afterEach(() => {
process.env.CCS_HOME = originalCcsHome;
invalidateConfigCache();
clearConfigCache();
fs.rmSync(tempHome, { recursive: true, force: true });
});
// ── resolveCliproxyBridgeProfile ──────────────────────────────────────────
it('resolveCliproxyBridgeProfile(claude) produces root base URL', () => {
const profile = resolveCliproxyBridgeProfile('claude');
// Must be the root URL — NOT /api/provider/claude.
expect(profile.baseUrl).toBe('http://127.0.0.1:8317/');
expect(profile.baseUrl).not.toContain('/api/provider/claude');
});
it('resolveCliproxyBridgeProfile(claude) reports root routePath', () => {
const profile = resolveCliproxyBridgeProfile('claude');
expect(profile.routePath).toBe('/');
});
it('resolveCliproxyBridgeProfile(claude) does not leak model pins (model-neutral)', () => {
const profile = resolveCliproxyBridgeProfile('claude');
expect(profile.models.default).toBe('');
expect(profile.models.opus).toBe('');
expect(profile.models.sonnet).toBe('');
expect(profile.models.haiku).toBe('');
});
it('resolveCliproxyBridgeProfile(gemini) still uses scoped /api/provider path', () => {
const profile = resolveCliproxyBridgeProfile('gemini');
expect(profile.baseUrl).toContain('/api/provider/gemini');
expect(profile.routePath).toBe('/api/provider/gemini');
});
it('resolveCliproxyBridgeProfile(codex) still uses scoped /api/provider path', () => {
const profile = resolveCliproxyBridgeProfile('codex');
expect(profile.baseUrl).toContain('/api/provider/codex');
expect(profile.routePath).toBe('/api/provider/codex');
});
// ── listCliproxyBridgeProviders ───────────────────────────────────────────
it('listCliproxyBridgeProviders shows root routePath for claude', () => {
const providers = listCliproxyBridgeProviders();
const claudeInfo = providers.find((p) => p.provider === 'claude');
expect(claudeInfo).toBeDefined();
expect(claudeInfo?.routePath).toBe('/');
expect(claudeInfo?.routePath).not.toContain('/api/provider/claude');
});
it('listCliproxyBridgeProviders keeps scoped routePaths for non-claude providers', () => {
const providers = listCliproxyBridgeProviders();
for (const info of providers) {
if (info.provider === 'claude') continue;
expect(info.routePath).toBe(`/api/provider/${info.provider}`);
}
});
// ── resolveCliproxyBridgeMetadata fallback behaviour under root URL ───────
//
// When a claude profile stores the fixed root URL (http://127.0.0.1:8317/),
// extractProviderFromPathname('/') returns null (no /api/provider/ segment),
// so resolveCliproxyBridgeMetadata returns null for that settings object.
// Dashboard routes fall back to mapExternalProviderName(profile.name) or the
// profile's cliproxyProvider field — both benign paths that still identify the
// provider correctly. This test locks the null-return so a future change to
// extractProviderFromPathname cannot silently introduce a regression.
it('resolveCliproxyBridgeMetadata returns null for a root-URL claude settings object (benign fallback locked)', () => {
const settings = {
env: {
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/',
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
},
};
// extractProviderFromPathname cannot identify the provider from a root path.
// The caller falls back to profile.name / cliproxyBridge from other sources.
expect(resolveCliproxyBridgeMetadata(settings)).toBeNull();
});
it('resolveCliproxyBridgeMetadata still resolves non-claude providers from scoped URL', () => {
const settings = {
env: {
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/gemini',
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
},
};
const meta = resolveCliproxyBridgeMetadata(settings);
expect(meta).not.toBeNull();
expect(meta?.provider).toBe('gemini');
});
});
+20 -9
View File
@@ -2,6 +2,7 @@ import * as fs from 'fs';
import * as path from 'path';
import { buildProxyUrl, getProxyTarget } from '../../cliproxy/proxy/proxy-target-resolver';
import { buildCliproxyProviderPath } from '../../cliproxy/config/env-builder';
import { getEffectiveApiKey } from '../../cliproxy/auth/auth-token-manager';
import { getModelMappingFromConfig } from '../../cliproxy/config/base-config-loader';
import {
@@ -107,13 +108,18 @@ function resolveBridgeModelMapping(provider: CLIProxyProvider): ModelMapping {
}
export function listCliproxyBridgeProviders(): CliproxyBridgeProviderInfo[] {
return CLIPROXY_PROVIDER_IDS.map((provider) => ({
provider,
displayName: getProviderDisplayName(provider),
description: getProviderDescription(provider),
defaultProfileName: getDefaultCliproxyBridgeName(provider),
routePath: `/api/provider/${provider}`,
}));
return CLIPROXY_PROVIDER_IDS.map((provider) => {
const providerPath = buildCliproxyProviderPath(provider);
return {
provider,
displayName: getProviderDisplayName(provider),
description: getProviderDescription(provider),
defaultProfileName: getDefaultCliproxyBridgeName(provider),
// claude uses root path (CLIProxyAPI registers /v1/messages at root);
// all other providers use their scoped /api/provider/<x> route.
routePath: providerPath === '' ? '/' : providerPath,
};
});
}
export function resolveCliproxyBridgeProfile(
@@ -125,8 +131,13 @@ export function resolveCliproxyBridgeProfile(
): ResolvedCliproxyBridgeProfile {
const target = getProxyTarget();
const profileName = options.name?.trim() || suggestCliproxyBridgeName(provider);
const baseUrl = buildProxyUrl(target, `/api/provider/${provider}`);
// Use the shared path helper so the claude provider always resolves to the
// CLIProxy root URL (same rule as buildLocalProviderBaseUrl in env-builder).
const providerPath = buildCliproxyProviderPath(provider);
const baseUrl = buildProxyUrl(target, providerPath);
const apiKey = target.authToken ?? getEffectiveApiKey();
// Expose the canonical route path: root for claude, scoped path for others.
const routePath = providerPath === '' ? '/' : providerPath;
return {
name: profileName,
@@ -136,7 +147,7 @@ export function resolveCliproxyBridgeProfile(
apiKey,
models: resolveBridgeModelMapping(provider),
target: options.target || 'claude',
routePath: `/api/provider/${provider}`,
routePath,
source: target.isRemote ? 'remote' : 'local',
};
}
@@ -10,7 +10,12 @@ import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import { getClaudeEnvVars, ensureProviderSettings, getRemoteEnvVars } from '../env-builder';
import {
getClaudeEnvVars,
ensureProviderSettings,
getEffectiveEnvVars,
getRemoteEnvVars,
} from '../env-builder';
import { clearConfigCache } from '../base-config-loader';
const MODEL_KEYS = [
@@ -45,10 +50,10 @@ describe('claude provider model-neutral passthrough (Gap 1)', () => {
}
});
it('still sets ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN for claude', () => {
it('sets root ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN for claude', () => {
const env = getClaudeEnvVars('claude');
expect(env.ANTHROPIC_BASE_URL).toBe('http://127.0.0.1:8317/api/provider/claude');
expect(env.ANTHROPIC_BASE_URL).toBe('http://127.0.0.1:8317');
expect(env.ANTHROPIC_AUTH_TOKEN).toBeDefined();
});
@@ -83,10 +88,57 @@ describe('claude provider model-neutral passthrough (Gap 1)', () => {
}
// Transport keys must be present
expect(written.env.ANTHROPIC_BASE_URL).toBe('http://127.0.0.1:8317/api/provider/claude');
expect(written.env.ANTHROPIC_BASE_URL).toBe('http://127.0.0.1:8317');
expect(written.env.ANTHROPIC_AUTH_TOKEN).toBeDefined();
});
it('normalizes stale claude provider-scoped base URL to CLIProxy root at read level', () => {
process.env.CCS_HOME = tempHome;
const ccsDir = path.join(tempHome, '.ccs');
fs.mkdirSync(ccsDir, { recursive: true });
const settingsPath = path.join(ccsDir, 'claude.settings.json');
fs.writeFileSync(
settingsPath,
JSON.stringify({
env: {
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/claude',
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
},
}),
'utf-8'
);
const env = getEffectiveEnvVars('claude', 8317, settingsPath);
expect(env.ANTHROPIC_BASE_URL).toBe('http://127.0.0.1:8317');
});
it('repairs stale claude provider-scoped base URL in stored default settings', () => {
process.env.CCS_HOME = tempHome;
const ccsDir = path.join(tempHome, '.ccs');
fs.mkdirSync(ccsDir, { recursive: true });
const settingsPath = path.join(ccsDir, 'claude.settings.json');
fs.writeFileSync(
settingsPath,
JSON.stringify({
env: {
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/claude',
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
},
}),
'utf-8'
);
ensureProviderSettings('claude');
const repaired = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')) as {
env: Record<string, string | undefined>;
};
expect(repaired.env.ANTHROPIC_BASE_URL).toBe('http://127.0.0.1:8317');
});
// ── Upgrade-path: existing claude.settings.json with stale default model pins ──
it('strips stale default model pins from existing claude.settings.json on ensureProviderSettings', () => {
@@ -107,7 +159,7 @@ describe('claude provider model-neutral passthrough (Gap 1)', () => {
settingsPath,
JSON.stringify({
env: {
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/claude',
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317',
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
...stalePins,
},
@@ -128,7 +180,7 @@ describe('claude provider model-neutral passthrough (Gap 1)', () => {
}
// Transport keys must still be present
expect(repaired.env.ANTHROPIC_BASE_URL).toBe('http://127.0.0.1:8317/api/provider/claude');
expect(repaired.env.ANTHROPIC_BASE_URL).toBe('http://127.0.0.1:8317');
expect(repaired.env.ANTHROPIC_AUTH_TOKEN).toBeDefined();
});
@@ -143,7 +195,7 @@ describe('claude provider model-neutral passthrough (Gap 1)', () => {
settingsPath,
JSON.stringify({
env: {
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/claude',
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317',
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
ANTHROPIC_MODEL: 'claude-opus-4-7', // customised — not the stale sonnet default
},
@@ -183,7 +235,7 @@ describe('claude provider model-neutral passthrough (Gap 1)', () => {
settingsPath,
JSON.stringify({
env: {
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/claude',
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317',
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
...stalePins,
},
@@ -217,7 +269,7 @@ describe('claude provider model-neutral passthrough (Gap 1)', () => {
const originalContent = JSON.stringify(
{
env: {
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/claude',
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317',
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
},
},
@@ -252,7 +304,7 @@ describe('claude provider model-neutral passthrough (Gap 1)', () => {
settingsPath,
JSON.stringify({
env: {
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/claude',
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317',
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
ANTHROPIC_MODEL: 'claude-sonnet-4-5-20250929',
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4-5-20251101',
@@ -285,7 +337,7 @@ describe('claude provider model-neutral passthrough (Gap 1)', () => {
settingsPath,
JSON.stringify({
env: {
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/claude',
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317',
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
ANTHROPIC_MODEL: 'claude-sonnet-4-6',
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4-6',
@@ -318,7 +370,7 @@ describe('claude provider model-neutral passthrough (Gap 1)', () => {
settingsPath,
JSON.stringify({
env: {
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/claude',
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317',
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
ANTHROPIC_MODEL: 'claude-opus-4-8',
},
@@ -400,7 +452,7 @@ describe('claude provider model-neutral passthrough (Gap 1)', () => {
settingsPath,
JSON.stringify({
env: {
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/claude',
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317',
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
ANTHROPIC_MODEL: 'claude-sonnet-4-5-20250929',
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4-5-20251101',
@@ -464,7 +516,7 @@ describe('claude provider model-neutral passthrough (Gap 1)', () => {
const originalContent =
JSON.stringify({
env: {
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/claude',
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317',
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
ANTHROPIC_MODEL: 'claude-sonnet-4-6',
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4-7',
@@ -502,7 +554,7 @@ describe('claude provider model-neutral passthrough (Gap 1)', () => {
settingsPath,
JSON.stringify({
env: {
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/claude',
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317',
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
ANTHROPIC_MODEL: 'claude-opus-4-8', // custom — not a historical default
},
@@ -533,7 +585,7 @@ describe('claude provider model-neutral passthrough (Gap 1)', () => {
customPath,
JSON.stringify({
env: {
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/claude',
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317',
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
ANTHROPIC_MODEL: 'claude-sonnet-4-6', // equals a stale default, but explicit
},
@@ -568,7 +620,7 @@ describe('claude provider model-neutral passthrough (Gap 1)', () => {
settingsPath,
JSON.stringify({
env: {
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/claude',
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317',
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
ANTHROPIC_MODEL: 'claude-sonnet-4-6', // equals a stale default, but post-migration = explicit
},
+47 -4
View File
@@ -205,8 +205,14 @@ export function getModelMapping(provider: CLIProxyProvider): ProviderModelMappin
/**
* Get environment variables for Claude CLI (bundled defaults)
* Uses provider-specific endpoint (e.g., /api/provider/gemini) for explicit routing.
* This enables concurrent gemini/codex usage - each session routes to its provider via URL path.
* Uses provider-specific endpoints (e.g., /api/provider/gemini) for explicit routing
* except for the built-in claude 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.
*
* 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).
@@ -231,7 +237,7 @@ export function getClaudeEnvVars(
// Core transport env vars set dynamically for all providers
const coreEnvVars: NodeJS.ProcessEnv = {
ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}/api/provider/${provider}`,
ANTHROPIC_BASE_URL: buildLocalProviderBaseUrl(provider, port),
ANTHROPIC_AUTH_TOKEN: getEffectiveApiKey(),
};
@@ -369,6 +375,25 @@ 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.
@@ -390,6 +415,10 @@ function normalizeLocalProviderBaseUrl(
: 80;
if (!Number.isFinite(effectivePort) || effectivePort !== port) return baseUrl;
if (provider === 'claude') {
return parsed.origin;
}
const expectedPath = `/api/provider/${provider}`;
if (parsed.pathname === expectedPath && !parsed.search && !parsed.hash) return baseUrl;
@@ -428,7 +457,9 @@ function rewriteLocalhostUrls(
// Omit port suffix for standard web ports (80/443) for cleaner URLs
const standardWebPort = normalizedProtocol === 'https' ? 443 : 80;
const portSuffix = effectivePort === standardWebPort ? '' : `:${effectivePort}`;
const remoteBaseUrl = `${normalizedProtocol}://${remoteConfig.host}${portSuffix}/api/provider/${provider}`;
const remoteRootUrl = `${normalizedProtocol}://${remoteConfig.host}${portSuffix}`;
const remoteBaseUrl =
provider === 'claude' ? remoteRootUrl : `${remoteRootUrl}/api/provider/${provider}`;
result.ANTHROPIC_BASE_URL = remoteBaseUrl;
@@ -713,6 +744,18 @@ export function ensureProviderSettings(provider: CLIProxyProvider): void {
}
}
if (provider === 'claude' && typeof mergedEnv.ANTHROPIC_BASE_URL === 'string') {
const normalizedBaseUrl = normalizeLocalProviderBaseUrl(
mergedEnv.ANTHROPIC_BASE_URL,
provider,
CLIPROXY_DEFAULT_PORT
);
if (normalizedBaseUrl !== mergedEnv.ANTHROPIC_BASE_URL) {
mergedEnv.ANTHROPIC_BASE_URL = normalizedBaseUrl;
mutated = true;
}
}
// Canonicalize provider-specific model aliases (e.g., AGY Sonnet 4.6 thinking legacy IDs).
for (const key of MODEL_ENV_VAR_KEYS) {
const current = mergedEnv[key];
@@ -654,6 +654,31 @@ describe('persist command Claude extension parity', () => {
expect(renderedLogs).not.toContain('Native Codex target:');
});
it('persists claude CLIProxy profile with root CLIProxy base URL', async () => {
await writeUnifiedConfig();
const settingsPath = path.join(tempRoot, '.claude', 'settings.json');
await fs.promises.mkdir(path.dirname(settingsPath), { recursive: true });
const originalConsoleLog = console.log;
console.log = () => {};
try {
await withScopedHome(() => handlePersistCommand(['claude', '--yes']));
} finally {
console.log = originalConsoleLog;
}
const persisted = JSON.parse(await fs.promises.readFile(settingsPath, 'utf8')) as {
env: Record<string, string | undefined>;
};
expect(persisted.env.ANTHROPIC_BASE_URL).toBe('http://127.0.0.1:8317');
expect(persisted.env.ANTHROPIC_BASE_URL).not.toContain('/api/provider/claude');
expect(persisted.env.ANTHROPIC_AUTH_TOKEN).toBeDefined();
expect(persisted.env.ANTHROPIC_MODEL).toBeUndefined();
});
it('blocks Codex CLIProxy profiles from Claude settings persistence', async () => {
await writeUnifiedConfig();