Merge pull request #1137 from kaitranntt/kai/refactor/1135-structural-maintainability

refactor(cliproxy): flatten module structure and colocate tests
This commit is contained in:
Kai (Tam Nhu) Tran
2026-04-30 10:05:05 -04:00
committed by GitHub
238 changed files with 1530 additions and 1211 deletions
+16
View File
@@ -46,3 +46,19 @@ logs/
# Test coverage
ui/coverage/
# UI build artifacts (generated by tsc -b in pre-commit hooks)
ui/src/lib/*.js
ui/src/lib/*.d.ts
ui/src/lib/*.js.map
ui/src/lib/*.d.ts.map
# Test mock build artifacts
tests/mocks/*.js
tests/mocks/*.d.ts
tests/mocks/*.js.map
tests/mocks/*.d.ts.map
tests/mocks/fixtures/*.js
tests/mocks/fixtures/*.d.ts
tests/mocks/fixtures/*.js.map
tests/mocks/fixtures/*.d.ts.map
+1
View File
@@ -5,6 +5,7 @@ import prettier from 'eslint-config-prettier';
export default [
{
files: ['src/**/*.ts'],
ignores: ['src/**/__tests__/**'],
languageOptions: {
parser: tsparser,
parserOptions: {
+4 -4
View File
@@ -1,16 +1,16 @@
import * as fs from 'fs';
import * as path from 'path';
import { getCcsDir, loadConfigSafe } from '../../utils/config-manager';
import { buildProxyUrl, getProxyTarget } from '../../cliproxy/proxy-target-resolver';
import { getEffectiveApiKey } from '../../cliproxy/auth-token-manager';
import { getModelMappingFromConfig } from '../../cliproxy/base-config-loader';
import { buildProxyUrl, getProxyTarget } from '../../cliproxy/proxy/proxy-target-resolver';
import { getEffectiveApiKey } from '../../cliproxy/auth/auth-token-manager';
import { getModelMappingFromConfig } from '../../cliproxy/config/base-config-loader';
import {
CLIPROXY_PROVIDER_IDS,
getProviderDescription,
getProviderDisplayName,
mapExternalProviderName,
} from '../../cliproxy/provider-capabilities';
import { extractProviderFromPathname } from '../../cliproxy/model-id-normalizer';
import { extractProviderFromPathname } from '../../cliproxy/ai-providers/model-id-normalizer';
import { isUnifiedMode, loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
import type { TargetType } from '../../targets/target-adapter';
import type { Settings } from '../../types/config';
@@ -7,7 +7,7 @@
import {
extractProviderFromPathname,
getDeniedModelIdReasonForProvider,
} from '../../cliproxy/model-id-normalizer';
} from '../../cliproxy/ai-providers/model-id-normalizer';
import { mapExternalProviderName } from '../../cliproxy/provider-capabilities';
import type { CLIProxyProvider } from '../../cliproxy/types';
import type { ProfileValidationIssue, ProfileValidationSummary } from './profile-types';
+1 -1
View File
@@ -17,7 +17,7 @@ import { mapExternalProviderName } from '../../cliproxy/provider-capabilities';
import {
extractProviderFromPathname,
getDeniedModelIdReasonForProvider,
} from '../../cliproxy/model-id-normalizer';
} from '../../cliproxy/ai-providers/model-id-normalizer';
import type { CLIProxyProvider } from '../../cliproxy/types';
import type {
ModelMapping,
@@ -16,7 +16,8 @@ function writeCliproxyConfig(homeDir: string, value: Record<string, unknown>): v
function readCliproxyConfig(homeDir: string): Record<string, any> {
return (
(yaml.load(fs.readFileSync(getCliproxyConfigPath(homeDir), 'utf8')) as Record<string, any>) || {}
(yaml.load(fs.readFileSync(getCliproxyConfigPath(homeDir), 'utf8')) as Record<string, any>) ||
{}
);
}
@@ -62,7 +63,9 @@ describe('ai-provider service stable ids', () => {
expect(family?.entries[1]?.id).toBeTruthy();
expect(family?.entries[0]?.id).not.toBe(family?.entries[1]?.id);
const persisted = readCliproxyConfig(tempHome)['gemini-api-key'] as Array<Record<string, unknown>>;
const persisted = readCliproxyConfig(tempHome)['gemini-api-key'] as Array<
Record<string, unknown>
>;
expect(persisted[0]?.id).toBe(family?.entries[0]?.id);
expect(persisted[1]?.id).toBe(family?.entries[1]?.id);
});
@@ -82,7 +85,9 @@ describe('ai-provider service stable ids', () => {
baseUrl: 'https://example.test/gemini',
});
let persisted = readCliproxyConfig(tempHome)['gemini-api-key'] as Array<Record<string, unknown>>;
let persisted = readCliproxyConfig(tempHome)['gemini-api-key'] as Array<
Record<string, unknown>
>;
expect(persisted[0]?.id).toBe('gemini-a');
expect(persisted[0]?.['api-key']).toBe('gamma');
expect(persisted[0]?.['base-url']).toBe('https://example.test/gemini');
@@ -105,7 +110,9 @@ describe('ai-provider service stable ids', () => {
apiKey: 'legacy-index-update',
});
const persisted = readCliproxyConfig(tempHome)['gemini-api-key'] as Array<Record<string, unknown>>;
const persisted = readCliproxyConfig(tempHome)['gemini-api-key'] as Array<
Record<string, unknown>
>;
expect(persisted[0]?.id).toBe('gemini-a');
expect(persisted[0]?.['api-key']).toBe('legacy-index-update');
});
@@ -140,9 +147,9 @@ describe('ai-provider service stable ids', () => {
>;
expect(persisted[0]?.id).toBe(connectorId);
expect(persisted[0]?.['base-url']).toBe('https://router.example/v1');
expect((persisted[0]?.['api-key-entries'] as Array<Record<string, unknown>>)[0]?.['api-key']).toBe(
'sk-openrouter'
);
expect(
(persisted[0]?.['api-key-entries'] as Array<Record<string, unknown>>)[0]?.['api-key']
).toBe('sk-openrouter');
});
it('normalizes plain openai-compatible model rules without aliases', async () => {
@@ -1,10 +1,6 @@
import { describe, it, expect } from 'bun:test';
import { findModel, supportsThinking } from '../../../src/cliproxy/model-catalog';
import {
PROVIDER_TO_CHANNEL,
SYNCABLE_PROVIDERS,
mergeCatalog,
} from '../../../src/cliproxy/catalog-cache';
import { findModel, supportsThinking } from '../model-catalog';
import { PROVIDER_TO_CHANNEL, SYNCABLE_PROVIDERS, mergeCatalog } from '../services/catalog-cache';
describe('model-catalog compatibility lookups', () => {
it('finds agy Claude models using dotted major.minor IDs', () => {
@@ -118,10 +118,7 @@ describe('Model Config', () => {
assert(parsed.env, 'Should have env object');
assert(parsed.env.ANTHROPIC_MODEL, 'Should have ANTHROPIC_MODEL');
assert.strictEqual(
parsed.env.ANTHROPIC_MODEL,
'claude-opus-4-5-thinking'
);
assert.strictEqual(parsed.env.ANTHROPIC_MODEL, 'claude-opus-4-5-thinking');
});
it('all env values should be strings (PowerShell safety)', () => {
@@ -13,11 +13,11 @@ import {
isQuotaSupportedProvider,
QUOTA_PROVIDER_OPTION_VALUES,
QUOTA_PROVIDER_HELP_TEXT,
} from '../../../src/cliproxy/provider-capabilities';
} from '../provider-capabilities';
import {
OAUTH_CALLBACK_PORTS as DIAGNOSTIC_CALLBACK_PORTS,
OAUTH_FLOW_TYPES,
} from '../../../src/management/oauth-port-diagnostics';
} from '../../management/oauth-port-diagnostics';
import {
DEFAULT_KIRO_AUTH_METHOD,
getKiroCallbackPort,
@@ -27,7 +27,7 @@ import {
normalizeKiroAuthMethod,
OAUTH_CALLBACK_PORTS as AUTH_CALLBACK_PORTS,
toKiroManagementMethod,
} from '../../../src/cliproxy/auth/auth-types';
} from '../auth/auth-types';
describe('provider-capabilities', () => {
it('keeps canonical provider IDs backward-compatible', () => {
@@ -167,9 +167,13 @@ describe('provider-capabilities', () => {
expect(getKiroCLIAuthFlag('aws-authcode')).toBe('--kiro-aws-authcode');
expect(getKiroCLIAuthFlag('google')).toBe('--kiro-google-login');
expect(getKiroCLIAuthFlag('idc')).toBe('--kiro-idc-login');
expect(getKiroCLIAuthArgs('idc', { idcStartUrl: 'https://d-123.awsapps.com/start' })).toEqual(
['--kiro-idc-login', '--kiro-idc-start-url', 'https://d-123.awsapps.com/start', '--kiro-idc-flow', 'authcode']
);
expect(getKiroCLIAuthArgs('idc', { idcStartUrl: 'https://d-123.awsapps.com/start' })).toEqual([
'--kiro-idc-login',
'--kiro-idc-start-url',
'https://d-123.awsapps.com/start',
'--kiro-idc-flow',
'authcode',
]);
expect(getKiroCallbackPort('aws')).toBeNull();
expect(getKiroCallbackPort('google')).toBe(9876);
@@ -11,7 +11,10 @@ const path = require('path');
const os = require('os');
// Set test isolation environment before importing
const testHome = path.join(os.tmpdir(), `ccs-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
const testHome = path.join(
os.tmpdir(),
`ccs-test-${Date.now()}-${Math.random().toString(36).slice(2)}`
);
const originalCcsHome = process.env.CCS_HOME;
const originalCcsDir = process.env.CCS_DIR;
process.env.CCS_HOME = testHome;
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'bun:test';
import { parseThinkingOverride } from '../../../src/cliproxy/executor/thinking-arg-parser';
import { parseThinkingOverride } from '../executor/thinking-arg-parser';
describe('parseThinkingOverride', () => {
it('parses --thinking with separate value', () => {
@@ -1,11 +1,11 @@
import { describe, expect, it } from 'bun:test';
import type { ThinkingConfig } from '../../../src/config/unified-config-types';
import type { ThinkingConfig } from '../../config/unified-config-types';
import {
buildThinkingStartupStatus,
parseEnvThinkingOverride,
resolveRuntimeThinkingOverride,
shouldDisableCodexReasoning,
} from '../../../src/cliproxy/executor/thinking-override-resolver';
} from '../executor/thinking-override-resolver';
const baseConfig: ThinkingConfig = {
mode: 'auto',
@@ -13,7 +13,7 @@ import {
THINKING_BUDGET_MIN,
THINKING_BUDGET_MAX,
THINKING_BUDGET_DEFAULT_MIN,
} from '../../../src/cliproxy/thinking-validator';
} from '../thinking-validator';
describe('Thinking Validator', () => {
describe('Constants', () => {
@@ -29,18 +29,18 @@ describe('Account Manager - discoverExistingAccounts', () => {
process.env.CCS_HOME = testDir;
// Clear cache and reload - must clear all modular submodules too
delete require.cache[require.resolve('../../../dist/cliproxy/account-manager')];
delete require.cache[require.resolve('../../../dist/cliproxy/config-generator')];
delete require.cache[require.resolve('../../../dist/utils/config-manager')];
delete require.cache[require.resolve('../../../../dist/cliproxy/account-manager')];
delete require.cache[require.resolve('../../../../dist/cliproxy/config-generator')];
delete require.cache[require.resolve('../../../../dist/utils/config-manager')];
// Clear new modular structure (accounts/ and config/ submodules)
delete require.cache[require.resolve('../../../dist/cliproxy/accounts/index')];
delete require.cache[require.resolve('../../../dist/cliproxy/accounts/registry')];
delete require.cache[require.resolve('../../../dist/cliproxy/accounts/token-file-ops')];
delete require.cache[require.resolve('../../../dist/cliproxy/accounts/query')];
delete require.cache[require.resolve('../../../dist/cliproxy/accounts/types')];
delete require.cache[require.resolve('../../../dist/cliproxy/config/index')];
delete require.cache[require.resolve('../../../dist/cliproxy/config/path-resolver')];
accountManager = require('../../../dist/cliproxy/account-manager');
delete require.cache[require.resolve('../../../../dist/cliproxy/accounts/index')];
delete require.cache[require.resolve('../../../../dist/cliproxy/accounts/registry')];
delete require.cache[require.resolve('../../../../dist/cliproxy/accounts/token-file-ops')];
delete require.cache[require.resolve('../../../../dist/cliproxy/accounts/query')];
delete require.cache[require.resolve('../../../../dist/cliproxy/accounts/types')];
delete require.cache[require.resolve('../../../../dist/cliproxy/config/index')];
delete require.cache[require.resolve('../../../../dist/cliproxy/config/path-resolver')];
accountManager = require('../../../../dist/cliproxy/account-manager');
});
afterEach(() => {
@@ -183,7 +183,11 @@ describe('Account Manager - discoverExistingAccounts', () => {
const accountIds = Object.keys(accounts.providers.kiro.accounts);
// For kiro/ghcp without email, generates unique ID like "kiro-1"
assert.strictEqual(accountIds[0], 'kiro-1', 'Should generate unique ID for kiro without email');
assert.strictEqual(
accountIds[0],
'kiro-1',
'Should generate unique ID for kiro without email'
);
});
it('handles dots in email local part', () => {
@@ -269,10 +273,10 @@ describe('Account Manager - discoverExistingAccounts', () => {
});
// Reload module to clear in-memory cache (including all submodules)
delete require.cache[require.resolve('../../../dist/cliproxy/account-manager')];
delete require.cache[require.resolve('../../../dist/cliproxy/accounts/index')];
delete require.cache[require.resolve('../../../dist/cliproxy/accounts/registry')];
accountManager = require('../../../dist/cliproxy/account-manager');
delete require.cache[require.resolve('../../../../dist/cliproxy/account-manager')];
delete require.cache[require.resolve('../../../../dist/cliproxy/accounts/index')];
delete require.cache[require.resolve('../../../../dist/cliproxy/accounts/registry')];
accountManager = require('../../../../dist/cliproxy/account-manager');
accountManager.discoverExistingAccounts();
const accounts = getAccountsFile();
@@ -353,10 +357,10 @@ describe('Account Manager - discoverExistingAccounts', () => {
});
// Reload module to pick up pre-existing accounts.json (including all submodules)
delete require.cache[require.resolve('../../../dist/cliproxy/account-manager')];
delete require.cache[require.resolve('../../../dist/cliproxy/accounts/index')];
delete require.cache[require.resolve('../../../dist/cliproxy/accounts/registry')];
accountManager = require('../../../dist/cliproxy/account-manager');
delete require.cache[require.resolve('../../../../dist/cliproxy/account-manager')];
delete require.cache[require.resolve('../../../../dist/cliproxy/accounts/index')];
delete require.cache[require.resolve('../../../../dist/cliproxy/accounts/registry')];
accountManager = require('../../../../dist/cliproxy/account-manager');
accountManager.discoverExistingAccounts();
const accounts = getAccountsFile();
@@ -2,7 +2,7 @@ import { describe, expect, it, spyOn } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { runWithScopedCcsHome } from '../../../src/utils/config-manager';
import { runWithScopedCcsHome } from '../../../utils/config-manager';
async function withIsolatedHome<T>(fn: (homeDir: string) => Promise<T> | T): Promise<T> {
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-account-registry-'));
@@ -14,14 +14,16 @@ async function withIsolatedHome<T>(fn: (homeDir: string) => Promise<T> | T): Pro
}
async function loadRegistryModule() {
return import(`../../../src/cliproxy/accounts/registry?registry-integrity=${Date.now()}`);
return import(`../registry?registry-integrity=${Date.now()}`);
}
async function loadAccountManager() {
return import(`../../../src/cliproxy/account-manager?account-registry-integrity=${Date.now()}`);
return import(`../account-manager?account-registry-integrity=${Date.now()}`);
}
async function captureConsoleError<T>(fn: () => Promise<T> | T): Promise<{ result: T; messages: string[] }> {
async function captureConsoleError<T>(
fn: () => Promise<T> | T
): Promise<{ result: T; messages: string[] }> {
const originalError = console.error;
const messages: string[] = [];
@@ -351,7 +353,9 @@ describe('account registry integrity', () => {
fs.writeFileSync(registryPath, '{"providers":', 'utf8');
const { loadAccountsRegistry } = await loadRegistryModule();
const { result: registry, messages } = await captureConsoleError(() => loadAccountsRegistry());
const { result: registry, messages } = await captureConsoleError(() =>
loadAccountsRegistry()
);
expect(registry).toEqual({
version: 1,
@@ -2,10 +2,10 @@ import { describe, expect, it } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { runWithScopedCcsHome } from '../../../src/utils/config-manager';
import { runWithScopedCcsHome } from '../../../utils/config-manager';
async function loadAccountManager() {
return import(`../../../src/cliproxy/account-manager?optional-nickname=${Date.now()}`);
return import(`../account-manager?optional-nickname=${Date.now()}`);
}
function writeTokenFile(homeDir: string, tokenFile: string): void {
@@ -121,7 +121,8 @@ describe('registerAccount optional nickname flow', () => {
const result = await withIsolatedHome(async (homeDir) => {
writeTokenFile(homeDir, 'codex-04a0f049-kaidu.kd@gmail.com-team.json');
writeTokenFile(homeDir, 'codex-kaidu.kd@gmail.com-free.json');
const { registerAccount, getProviderAccounts, findAccountByQuery } = await loadAccountManager();
const { registerAccount, getProviderAccounts, findAccountByQuery } =
await loadAccountManager();
const team = registerAccount(
'codex',
'codex-04a0f049-kaidu.kd@gmail.com-team.json',
@@ -19,8 +19,8 @@ import {
maskEmail,
pauseAccountForQuotaCooldown,
restoreExpiredQuotaPauses,
} from '../../../src/cliproxy/account-safety';
import { sanitizeEmail } from '../../../src/cliproxy/auth-utils';
} from '../../accounts/account-safety';
import { sanitizeEmail } from '../../auth/auth-utils';
// Setup test isolation
let tmpDir: string;
@@ -243,7 +243,7 @@ describe('Quota Exhaustion Handlers', () => {
},
});
const { isOnCooldown } = await import('../../../src/cliproxy/quota-manager');
const { isOnCooldown } = await import('../../quota/quota-manager');
const result = await handleQuotaExhaustion('agy', 'exhausted@gmail.com', 10);
@@ -285,7 +285,7 @@ describe('Quota Exhaustion Handlers', () => {
});
const result = await handleQuotaExhaustion('agy', 'only@gmail.com', 10);
const { getAccount } = await import('../../../src/cliproxy/account-manager');
const { getAccount } = await import('../account-manager');
// Should return gracefully with null switched
expect(result.switchedTo).toBeNull();
@@ -342,16 +342,12 @@ describe('Quota Exhaustion Handlers', () => {
}) as typeof fetch;
const result = await handleQuotaExhaustion('claude', 'exhausted@example.com', 10);
const { getAccount, getDefaultAccount } = await import(
'../../../src/cliproxy/account-manager'
);
const { getAccount, getDefaultAccount } = await import('../account-manager');
expect(result.switchedTo).toBe('fallback@example.com');
expect(getDefaultAccount('claude')?.id).toBe('fallback@example.com');
expect(getAccount('claude', 'exhausted@example.com')?.paused).not.toBe(true);
expect(fs.existsSync(path.join(tmpDir, '.ccs', 'cliproxy', 'quota-paused.json'))).toBe(
false
);
expect(fs.existsSync(path.join(tmpDir, '.ccs', 'cliproxy', 'quota-paused.json'))).toBe(false);
expect(
fs.existsSync(
path.join(
@@ -429,9 +425,7 @@ describe('Quota Exhaustion Handlers', () => {
}) as typeof fetch;
const result = await handleQuotaExhaustion('codex', 'exhausted@example.com', 10);
const { getAccount, getDefaultAccount } = await import(
'../../../src/cliproxy/account-manager'
);
const { getAccount, getDefaultAccount } = await import('../account-manager');
expect(result.switchedTo).toBe('fallback@example.com');
expect(getDefaultAccount('codex')?.id).toBe('fallback@example.com');
@@ -490,9 +484,9 @@ describe('Quota Exhaustion Handlers', () => {
)
) as typeof fetch;
const { preflightCheck } = await import('../../../src/cliproxy/quota-manager');
const { preflightCheck } = await import('../../quota/quota-manager');
const result = await preflightCheck('codex');
const { getAccount } = await import('../../../src/cliproxy/account-manager');
const { getAccount } = await import('../account-manager');
expect(result.accountId).toBe('only-codex@example.com');
expect(result.switchedFrom).toBeUndefined();
@@ -567,11 +561,9 @@ describe('Quota Exhaustion Handlers', () => {
);
}) as typeof fetch;
const { preflightCheck } = await import('../../../src/cliproxy/quota-manager');
const { preflightCheck } = await import('../../quota/quota-manager');
const result = await preflightCheck('codex');
const { getAccount, getDefaultAccount } = await import(
'../../../src/cliproxy/account-manager'
);
const { getAccount, getDefaultAccount } = await import('../account-manager');
expect(result.switchedFrom).toBe('preflight-exhausted@example.com');
expect(result.accountId).toBe('preflight-fallback@example.com');
@@ -647,18 +639,14 @@ describe('Quota Exhaustion Handlers', () => {
);
}) as typeof fetch;
const { preflightCheck } = await import('../../../src/cliproxy/quota-manager');
const { preflightCheck } = await import('../../quota/quota-manager');
const result = await preflightCheck('codex');
const { getAccount, getDefaultAccount } = await import(
'../../../src/cliproxy/account-manager'
);
const { getAccount, getDefaultAccount } = await import('../account-manager');
expect(result.switchedFrom).toBe('preflight-unknown-exhausted@example.com');
expect(result.accountId).toBe('preflight-unknown-fallback@example.com');
expect(getDefaultAccount('codex')?.id).toBe('preflight-unknown-fallback@example.com');
expect(getAccount('codex', 'preflight-unknown-exhausted@example.com')?.paused).not.toBe(
true
);
expect(getAccount('codex', 'preflight-unknown-exhausted@example.com')?.paused).not.toBe(true);
expect(
fs.existsSync(
path.join(tmpDir, '.ccs', 'cliproxy', 'auth', 'codex-preflight-unknown-exhausted.json')
@@ -675,9 +663,7 @@ describe('Quota Exhaustion Handlers', () => {
)
)
).toBe(false);
expect(fs.existsSync(path.join(tmpDir, '.ccs', 'cliproxy', 'quota-paused.json'))).toBe(
false
);
expect(fs.existsSync(path.join(tmpDir, '.ccs', 'cliproxy', 'quota-paused.json'))).toBe(false);
});
it('should self-pause exhausted Gemini accounts when a healthy fallback exists', async () => {
@@ -736,9 +722,7 @@ describe('Quota Exhaustion Handlers', () => {
}) as typeof fetch;
const result = await handleQuotaExhaustion('gemini', 'gemini-exhausted@example.com', 10);
const { getAccount, getDefaultAccount } = await import(
'../../../src/cliproxy/account-manager'
);
const { getAccount, getDefaultAccount } = await import('../account-manager');
expect(result.switchedTo).toBe('gemini-fallback@example.com');
expect(getDefaultAccount('gemini')?.id).toBe('gemini-fallback@example.com');
@@ -804,9 +788,7 @@ describe('Quota Exhaustion Handlers', () => {
}) as typeof fetch;
const result = await handleQuotaExhaustion('ghcp', 'ghcp-exhausted', 10);
const { getAccount, getDefaultAccount } = await import(
'../../../src/cliproxy/account-manager'
);
const { getAccount, getDefaultAccount } = await import('../account-manager');
expect(result.switchedTo).toBe('ghcp-fallback');
expect(getDefaultAccount('ghcp')?.id).toBe('ghcp-fallback');
@@ -868,9 +850,7 @@ describe('Quota Exhaustion Handlers', () => {
}) as typeof fetch;
const result = await handleQuotaExhaustion('ghcp', 'ghcp-partial-exhausted', 10);
const { getAccount, getDefaultAccount } = await import(
'../../../src/cliproxy/account-manager'
);
const { getAccount, getDefaultAccount } = await import('../account-manager');
expect(result.switchedTo).toBe('ghcp-partial-fallback');
expect(getDefaultAccount('ghcp')?.id).toBe('ghcp-partial-fallback');
@@ -986,7 +966,7 @@ describe('Quota Exhaustion Handlers', () => {
const now = Date.now();
expect(pauseAccountForQuotaCooldown('agy', 'cooldown@gmail.com', 5, now)).toBe(true);
const { getAccount } = await import('../../../src/cliproxy/account-manager');
const { getAccount } = await import('../account-manager');
expect(getAccount('agy', 'cooldown@gmail.com')?.paused).toBe(true);
const resumed = restoreExpiredQuotaPauses(now + 6 * 60 * 1000);
@@ -1030,7 +1010,7 @@ describe('Quota Exhaustion Handlers', () => {
fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2));
const resumed = restoreExpiredQuotaPauses(now + 6 * 60 * 1000);
const { getAccount } = await import('../../../src/cliproxy/account-manager');
const { getAccount } = await import('../account-manager');
expect(resumed).toBe(0);
expect(getAccount('agy', 'cooldown@gmail.com')?.paused).toBe(true);
@@ -1067,7 +1047,7 @@ describe('Quota Exhaustion Handlers', () => {
});
const resumed = restoreExpiredQuotaPauses(now + 6 * 60 * 1000);
const { getAccount } = await import('../../../src/cliproxy/account-manager');
const { getAccount } = await import('../account-manager');
const quotaPausedPath = path.join(tmpDir, '.ccs', 'cliproxy', 'quota-paused.json');
const quotaPaused = JSON.parse(fs.readFileSync(quotaPausedPath, 'utf8')) as {
entries?: Array<{ accountId?: string }>;
@@ -19,7 +19,7 @@ import {
checkNewAccountConflict,
handleBanDetection,
warnCrossProviderDuplicates,
} from '../../../src/cliproxy/account-safety';
} from '../../accounts/account-safety';
// --- Test isolation: use temp CCS_HOME ---
@@ -20,7 +20,7 @@ export type {
ProviderAccounts,
BulkOperationResult,
SoloOperationResult,
} from './accounts';
} from '.';
export {
PROVIDERS_WITHOUT_EMAIL,
@@ -56,4 +56,4 @@ export {
bulkPauseAccounts,
bulkResumeAccounts,
soloAccount,
} from './accounts';
} from '.';
@@ -11,10 +11,10 @@
import * as fs from 'fs';
import * as path from 'path';
import { warn, info } from '../utils/ui';
import { CLIProxyProvider } from './types';
import { loadAccountsRegistry, pauseAccount, resumeAccount } from './accounts/registry';
import { getCcsDir } from '../utils/config-manager';
import { warn, info } from '../../utils/ui';
import { CLIProxyProvider } from '../types';
import { loadAccountsRegistry, pauseAccount, resumeAccount } from './registry';
import { getCcsDir } from '../../utils/config-manager';
const ISSUE_509_URL = 'https://github.com/kaitranntt/ccs/issues/509';
@@ -688,9 +688,9 @@ export async function handleQuotaExhaustion(
cooldownMinutes: number
): Promise<{ switchedTo: string | null; reason: string }> {
// Dynamic imports to avoid circular dependencies
const { applyCooldown, findHealthyAccount } = await import('./quota-manager');
const { applyCooldown, findHealthyAccount } = await import('../quota/quota-manager');
const { setDefaultAccount, touchAccount } = await import('./account-manager');
const { loadOrCreateUnifiedConfig } = await import('../config/unified-config-loader');
const { loadOrCreateUnifiedConfig } = await import('../../config/unified-config-loader');
const config = loadOrCreateUnifiedConfig();
const threshold = config.quota_management?.auto?.exhaustion_threshold ?? 5;
+1 -1
View File
@@ -8,7 +8,7 @@ import * as path from 'path';
import * as lockfile from 'proper-lockfile';
import { CLIProxyProvider } from '../types';
import { PROVIDER_TYPE_VALUES } from '../auth/auth-types';
import { getAuthDir, getCliproxyDir } from '../config-generator';
import { getAuthDir, getCliproxyDir } from '../config/config-generator';
import { AccountsRegistry, AccountInfo, PROVIDERS_WITHOUT_EMAIL } from './types';
import {
getAccountsRegistryPath,
+1 -1
View File
@@ -4,7 +4,7 @@
import * as fs from 'fs';
import * as path from 'path';
import { getCliproxyDir, getAuthDir } from '../config-generator';
import { getCliproxyDir, getAuthDir } from '../config/config-generator';
import type { CLIProxyProvider } from '../types';
import { AccountInfo } from './types';
@@ -2,7 +2,7 @@ import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { afterEach, describe, expect, it, mock, spyOn } from 'bun:test';
import { clearQuotaCache } from '../../../src/cliproxy/quota-response-cache';
import { clearQuotaCache } from '../../quota/quota-response-cache';
afterEach(() => {
clearQuotaCache();
@@ -39,7 +39,7 @@ function createCodexSettingsFixture(haikuModel: string = 'gpt-5.4-mini'): {
}
async function importCompatibilityModule(cacheTag: string) {
return import(`../../../src/cliproxy/codex-plan-compatibility?${cacheTag}=${Date.now()}`);
return import(`../../codex-plan-compatibility?${cacheTag}=${Date.now()}`);
}
const identity = (message: string) => message;
@@ -1,11 +1,11 @@
import { describe, expect, it } from 'bun:test';
import { getProviderCatalog, getModelMaxLevel } from '../../../src/cliproxy/model-catalog';
import { getProviderCatalog, getModelMaxLevel } from '../../model-catalog';
import {
getDefaultCodexModel,
getFreePlanFallbackCodexModel,
parseCodexUnsupportedModelError,
resolveRuntimeCodexFallbackModel,
} from '../../../src/cliproxy/codex-plan-compatibility';
} from '../../ai-providers/codex-plan-compatibility';
describe('codex plan compatibility', () => {
it('uses a cross-plan safe Codex default', () => {
@@ -4,12 +4,12 @@ import {
buildCodexModelEffortMap,
CodexReasoningProxy,
getEffortForModel,
} from '../../../src/cliproxy/codex-reasoning-proxy';
} from '../../ai-providers/codex-reasoning-proxy';
import {
parseEnvThinkingOverride,
resolveRuntimeThinkingOverride,
shouldDisableCodexReasoning,
} from '../../../src/cliproxy/executor/thinking-override-resolver';
} from '../../executor/thinking-override-resolver';
type JsonRecord = Record<string, unknown>;
@@ -4,7 +4,7 @@ const {
buildCodexModelEffortMap,
getEffortForModel,
injectReasoningEffortIntoBody,
} = require('../../../dist/cliproxy/codex-reasoning-proxy');
} = require('../../../../dist/cliproxy/codex-reasoning-proxy');
describe('Codex Reasoning Proxy', () => {
describe('buildCodexModelEffortMap', () => {
@@ -153,7 +153,10 @@ describe('Codex Reasoning Proxy', () => {
});
it('preserves query strings after stripping', () => {
const result = stripPathPrefix('/api/provider/codex/v1/messages?stream=true', '/api/provider/codex');
const result = stripPathPrefix(
'/api/provider/codex/v1/messages?stream=true',
'/api/provider/codex'
);
assert.strictEqual(result, '/v1/messages?stream=true');
});
@@ -7,7 +7,7 @@ import {
applyExtendedContextSuffix,
shouldApplyExtendedContext,
applyExtendedContextConfig,
} from '../../../src/cliproxy/config/extended-context-config';
} from '../../config/extended-context-config';
describe('applyExtendedContextSuffix', () => {
it('appends [1m] to model without suffix', () => {
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, it, mock } from 'bun:test';
import { ensureManagedModelPrefixes } from '../../../src/cliproxy/managed-model-prefixes';
import { ensureManagedModelPrefixes } from '../managed-model-prefixes';
const originalFetch = global.fetch;
@@ -12,7 +12,7 @@ import {
normalizeModelIdForProvider,
normalizeModelIdForRouting,
normalizeModelEnvVarsForProvider,
} from '../../../src/cliproxy/model-id-normalizer';
} from '../model-id-normalizer';
describe('model-id-normalizer', () => {
describe('provider parsing', () => {
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'bun:test';
import {
buildCliproxyRoutingHints,
getManagedModelPrefix,
} from '../../../src/shared/cliproxy-model-routing';
} from '../../../shared/cliproxy-model-routing';
describe('cliproxy model routing hints', () => {
it('uses short managed prefixes for overlapping Gemini and Antigravity models', () => {
@@ -17,12 +17,12 @@ describe('openai-compat manager', () => {
process.env.CCS_HOME = testDir;
process.env.CCS_DIR = path.join(testDir, '.ccs');
delete require.cache[require.resolve('../../../dist/cliproxy/config-generator')];
delete require.cache[require.resolve('../../../dist/cliproxy/openai-compat-manager')];
delete require.cache[require.resolve('../../../dist/utils/config-manager')];
delete require.cache[require.resolve('../../../../dist/cliproxy/config-generator')];
delete require.cache[require.resolve('../../../../dist/cliproxy/openai-compat-manager')];
delete require.cache[require.resolve('../../../../dist/utils/config-manager')];
configGenerator = require('../../../dist/cliproxy/config-generator');
openAICompatManager = require('../../../dist/cliproxy/openai-compat-manager');
configGenerator = require('../../../../dist/cliproxy/config-generator');
openAICompatManager = require('../../../../dist/cliproxy/openai-compat-manager');
});
afterEach(() => {
@@ -56,8 +56,15 @@ describe('openai-compat manager', () => {
});
const afterWrite = fs.readFileSync(configPath, 'utf8');
assert.strictEqual(afterWrite.split('\n')[0], initialHeader, 'Should preserve the generated header');
assert(afterWrite.includes('openai-compatibility:'), 'Should write the openai-compatibility section');
assert.strictEqual(
afterWrite.split('\n')[0],
initialHeader,
'Should preserve the generated header'
);
assert(
afterWrite.includes('openai-compatibility:'),
'Should write the openai-compatibility section'
);
assert.strictEqual(
configGenerator.configNeedsRegeneration(),
false,
@@ -67,7 +74,10 @@ describe('openai-compat manager', () => {
configGenerator.regenerateConfig();
const afterRegen = fs.readFileSync(configPath, 'utf8');
assert(afterRegen.includes('openai-compatibility:'), 'Connector section should survive regeneration');
assert(
afterRegen.includes('openai-compatibility:'),
'Connector section should survive regeneration'
);
assert(afterRegen.includes('name: mimo'), 'Connector name should survive regeneration');
assert(
afterRegen.includes('base-url: https://api.xiaomimimo.com/v1'),
@@ -6,7 +6,7 @@
*/
import { describe, expect, test } from 'bun:test';
import { sanitizeInputSchema, sanitizeToolSchemas } from '../../../src/cliproxy/schema-sanitizer';
import { sanitizeInputSchema, sanitizeToolSchemas } from '../schema-sanitizer';
describe('sanitizeInputSchema', () => {
test('preserves Gemini-supported properties', () => {
@@ -4,7 +4,7 @@
const assert = require('assert');
const { ToolNameMapper } = require('../../../dist/cliproxy/tool-name-mapper');
const { ToolNameMapper } = require('../../../../dist/cliproxy/ai-providers/tool-name-mapper');
describe('Tool Name Mapper', () => {
describe('registerTools', () => {
@@ -71,11 +71,7 @@ describe('Tool Name Mapper', () => {
it('handles mixed valid and invalid names', () => {
const mapper = new ToolNameMapper();
const tools = [
{ name: 'valid_tool' },
{ name: 'foo__bar__bar' },
{ name: 'another_valid' },
];
const tools = [{ name: 'valid_tool' }, { name: 'foo__bar__bar' }, { name: 'another_valid' }];
const result = mapper.registerTools(tools);
@@ -92,9 +88,7 @@ describe('Tool Name Mapper', () => {
const mapper = new ToolNameMapper();
mapper.registerTools([{ name: 'foo__bar__bar' }]);
const content = [
{ type: 'tool_use', id: 'call_1', name: 'foo__bar', input: {} },
];
const content = [{ type: 'tool_use', id: 'call_1', name: 'foo__bar', input: {} }];
const result = mapper.restoreToolUse(content);
@@ -121,9 +115,7 @@ describe('Tool Name Mapper', () => {
const mapper = new ToolNameMapper();
mapper.registerTools([{ name: 'foo__bar__bar' }]);
const content = [
{ type: 'tool_use', id: 'call_1', name: 'unknown_tool', input: {} },
];
const content = [{ type: 'tool_use', id: 'call_1', name: 'unknown_tool', input: {} }];
const result = mapper.restoreToolUse(content);
@@ -196,10 +188,7 @@ describe('Tool Name Mapper', () => {
it('returns list of all changes', () => {
const mapper = new ToolNameMapper();
mapper.registerTools([
{ name: 'foo__bar__bar' },
{ name: 'baz__qux__qux' },
]);
mapper.registerTools([{ name: 'foo__bar__bar' }, { name: 'baz__qux__qux' }]);
const changes = mapper.getChanges();
@@ -254,4 +243,93 @@ describe('Tool Name Mapper', () => {
assert.strictEqual(mapper.restoreName('foo__bar'), 'foo__bar');
});
});
describe('collision handling', () => {
it('disambiguates when sanitized name collides with unchanged name', () => {
const mapper = new ToolNameMapper();
// 'abc__def__def' sanitizes to 'abc__def' (changed)
// 'abc__def' is unchanged but collides with the sanitized name
const tools = [{ name: 'abc__def__def' }, { name: 'abc__def' }];
const result = mapper.registerTools(tools);
assert.strictEqual(result[0].name, 'abc__def');
assert.strictEqual(result[1].name, 'abc__def_2');
assert.strictEqual(mapper.hasCollisions(), true);
});
it('disambiguates different originals that sanitize to the same name', () => {
const mapper = new ToolNameMapper();
const tools = [{ name: 'abc__def__def' }, { name: 'abc__def' }];
const result = mapper.registerTools(tools);
// 'abc__def__def' → 'abc__def' (deduped)
// 'abc__def' → 'abc__def_2' (disambiguated, unchanged but collides)
assert.strictEqual(result[0].name, 'abc__def');
assert.strictEqual(result[1].name, 'abc__def_2');
});
it('restores both colliding tools to correct originals', () => {
const mapper = new ToolNameMapper();
const tools = [{ name: 'a__b__b' }, { name: 'a__b' }];
mapper.registerTools(tools);
const content = [
{ type: 'tool_use', id: '1', name: 'a__b', input: {} },
{ type: 'tool_use', id: '2', name: 'a__b_2', input: {} },
];
const restored = mapper.restoreToolUse(content);
assert.strictEqual(restored[0].name, 'a__b__b');
assert.strictEqual(restored[1].name, 'a__b');
});
it('records collision metadata for logging', () => {
const mapper = new ToolNameMapper();
const tools = [{ name: 'x__y__y' }, { name: 'x__y' }];
mapper.registerTools(tools);
assert.strictEqual(mapper.hasCollisions(), true);
const collisions = mapper.getCollisions();
assert.strictEqual(collisions.length, 1);
assert.strictEqual(collisions[0].base, 'x__y');
assert.ok(collisions[0].originals.includes('x__y__y'));
assert.ok(collisions[0].originals.includes('x__y'));
});
it('detects collision when unchanged tool appears before sanitized one', () => {
const mapper = new ToolNameMapper();
const tools = [
{ name: 'abc__def' }, // unchanged, but registered in mapping
{ name: 'abc__def__def' }, // sanitizes to 'abc__def', collides
];
const result = mapper.registerTools(tools);
assert.strictEqual(result[0].name, 'abc__def');
assert.strictEqual(result[1].name, 'abc__def_2');
assert.strictEqual(mapper.hasCollisions(), true);
});
it('restores correctly for reverse-order collision', () => {
const mapper = new ToolNameMapper();
const tools = [{ name: 'abc__def' }, { name: 'abc__def__def' }];
mapper.registerTools(tools);
const content = [
{ type: 'tool_use', id: '1', name: 'abc__def', input: {} },
{ type: 'tool_use', id: '2', name: 'abc__def_2', input: {} },
];
const restored = mapper.restoreToolUse(content);
assert.strictEqual(restored[0].name, 'abc__def');
assert.strictEqual(restored[1].name, 'abc__def__def');
});
});
});
@@ -10,7 +10,7 @@ const {
smartTruncate,
sanitizeToolName,
GEMINI_MAX_TOOL_NAME_LENGTH,
} = require('../../../dist/cliproxy/tool-name-sanitizer');
} = require('../../../../dist/cliproxy/ai-providers/tool-name-sanitizer');
describe('Tool Name Sanitizer', () => {
describe('GEMINI_MAX_TOOL_NAME_LENGTH', () => {
@@ -170,5 +170,62 @@ describe('Tool Name Sanitizer', () => {
assert.strictEqual(result.changed, true);
assert.strictEqual(result.sanitized, 'gitmcp__plus-pro-components');
});
it('forces valid characters when name contains spaces', () => {
const result = sanitizeToolName('has space');
assert.strictEqual(result.sanitized, 'has_space');
assert.strictEqual(result.changed, true);
assert.ok(isValidToolName(result.sanitized));
});
it('forces valid characters when name starts with digit', () => {
const result = sanitizeToolName('123start');
assert.ok(result.sanitized.startsWith('_'));
assert.strictEqual(result.changed, true);
assert.ok(isValidToolName(result.sanitized));
});
it('forces valid characters when name contains symbols', () => {
const result = sanitizeToolName('tool@name#here');
assert.ok(!result.sanitized.includes('@'));
assert.ok(!result.sanitized.includes('#'));
assert.strictEqual(result.changed, true);
assert.ok(isValidToolName(result.sanitized));
});
it('produces valid name for digit-starting long name', () => {
const name = '1' + 'a'.repeat(100);
const result = sanitizeToolName(name);
assert.ok(isValidToolName(result.sanitized), `Expected valid name, got: ${result.sanitized}`);
assert.ok(result.sanitized.length <= 64);
});
it('forces valid prefix for dot-starting name', () => {
const result = sanitizeToolName('.tool');
assert.ok(result.sanitized.startsWith('_'), `Expected _ prefix, got: ${result.sanitized}`);
assert.ok(isValidToolName(result.sanitized));
assert.strictEqual(result.changed, true);
});
it('forces valid prefix for hyphen-starting name', () => {
const result = sanitizeToolName('-tool');
assert.ok(result.sanitized.startsWith('_'), `Expected _ prefix, got: ${result.sanitized}`);
assert.ok(isValidToolName(result.sanitized));
assert.strictEqual(result.changed, true);
});
it('forces valid prefix for colon-starting name', () => {
const result = sanitizeToolName(':tool');
assert.ok(result.sanitized.startsWith('_'), `Expected _ prefix, got: ${result.sanitized}`);
assert.ok(isValidToolName(result.sanitized));
assert.strictEqual(result.changed, true);
});
});
});
@@ -1,10 +1,10 @@
import { getDefaultAccount } from './account-manager';
import { getProviderCatalog } from './model-catalog';
import { getDefaultAccount } from '../accounts/account-manager';
import { getProviderCatalog } from '../model-catalog';
import { normalizeModelIdForProvider } from './model-id-normalizer';
import { fetchCodexQuota } from './quota-fetcher-codex';
import { getCachedQuota, setCachedQuota } from './quota-response-cache';
import type { CodexQuotaResult } from './quota-types';
import { info, warn } from '../utils/ui';
import { fetchCodexQuota } from '../quota/quota-fetcher-codex';
import { getCachedQuota, setCachedQuota } from '../quota/quota-response-cache';
import type { CodexQuotaResult } from '../quota/quota-types';
import { info, warn } from '../../utils/ui';
export type CodexPlanType = CodexQuotaResult['planType'];
@@ -6,7 +6,7 @@ import {
parseCodexUnsupportedModelError,
resolveRuntimeCodexFallbackModel,
} from './codex-plan-compatibility';
import { getModelMaxLevel } from './model-catalog';
import { getModelMaxLevel } from '../model-catalog';
export type CodexReasoningEffort = 'medium' | 'high' | 'xhigh';
+3 -3
View File
@@ -1,9 +1,9 @@
import { randomUUID } from 'crypto';
import * as fs from 'fs';
import * as yaml from 'js-yaml';
import { configExists, getCliproxyConfigPath, regenerateConfig } from '../config-generator';
import { getProxyTarget } from '../proxy-target-resolver';
import { createManagementClient } from '../management-api-client';
import { configExists, getCliproxyConfigPath, regenerateConfig } from '../config/config-generator';
import { getProxyTarget } from '../proxy/proxy-target-resolver';
import { createManagementClient } from '../management/management-api-client';
import { rewriteTopLevelYamlSection } from './config-yaml-sections';
import type {
AiProviderApiKeyEntry,
@@ -1,7 +1,11 @@
import { getManagedModelPrefix } from '../shared/cliproxy-model-routing';
import { buildManagementHeaders, buildProxyUrl, getProxyTarget } from './proxy-target-resolver';
import { mapExternalProviderName } from './provider-capabilities';
import type { CLIProxyProvider } from './types';
import { getManagedModelPrefix } from '../../shared/cliproxy-model-routing';
import {
buildManagementHeaders,
buildProxyUrl,
getProxyTarget,
} from '../proxy/proxy-target-resolver';
import { mapExternalProviderName } from '../provider-capabilities';
import type { CLIProxyProvider } from '../types';
const MANAGED_PREFIX_REQUEST_TIMEOUT_MS = 3000;
@@ -5,8 +5,8 @@
* model version formats (e.g., 4.6 vs 4-6).
*/
import type { CLIProxyProvider } from './types';
import { ANTHROPIC_MODEL_ENV_KEYS } from '../shared/extended-context-utils';
import type { CLIProxyProvider } from '../types';
import { ANTHROPIC_MODEL_ENV_KEYS } from '../../shared/extended-context-utils';
/** Env vars that carry model identifiers. */
export const MODEL_ENV_VAR_KEYS = ANTHROPIC_MODEL_ENV_KEYS;
@@ -7,8 +7,9 @@
import * as fs from 'fs';
import * as yaml from 'js-yaml';
import { configExists, getCliproxyConfigPath, regenerateConfig } from './config-generator';
import { rewriteTopLevelYamlSection } from './ai-providers/config-yaml-sections';
import { configExists, regenerateConfig } from '../config/generator';
import { getCliproxyConfigPath } from '../config/path-resolver';
import { rewriteTopLevelYamlSection } from './config-yaml-sections';
/** Model alias configuration */
export interface OpenAICompatModel {
@@ -41,8 +41,12 @@ export interface SanitizationChange {
/** Record of a hash collision */
export interface HashCollision {
sanitized: string;
/** The base sanitized name before disambiguation */
base: string;
/** All original names that collided */
originals: string[];
/** The sanitized name used (after disambiguation) */
sanitized: string;
}
/**
@@ -63,7 +67,8 @@ export class ToolNameMapper {
/**
* Register tools and sanitize their names.
* Stores mapping for later restoration.
* Stores mapping for later restoration. On collision (two originals
* sanitizing to the same name), disambiguates with a numeric suffix.
*
* @param tools Array of tool definitions
* @returns Array of tools with sanitized names
@@ -71,36 +76,52 @@ export class ToolNameMapper {
registerTools(tools: Tool[]): Tool[] {
return tools.map((tool) => {
const result: SanitizeResult = sanitizeToolName(tool.name);
let finalName = result.sanitized;
if (result.changed) {
// Check for collision: sanitized name already maps to different original
const existingOriginal = this.mapping.get(result.sanitized);
if (existingOriginal && existingOriginal !== tool.name) {
// Record collision
const existing = this.collisions.find((c) => c.sanitized === result.sanitized);
if (existing) {
if (!existing.originals.includes(tool.name)) {
existing.originals.push(tool.name);
}
} else {
this.collisions.push({
sanitized: result.sanitized,
originals: [existingOriginal, tool.name],
});
// Check for collision regardless of whether the name changed.
// An unchanged name can still collide with a previously sanitized name.
const existingOriginal = this.mapping.get(finalName);
if (existingOriginal && existingOriginal !== tool.name) {
// Record collision for logging
const existing = this.collisions.find((c) => c.base === finalName);
if (existing) {
if (!existing.originals.includes(tool.name)) {
existing.originals.push(tool.name);
}
} else {
this.collisions.push({
base: finalName,
originals: [existingOriginal, tool.name],
sanitized: finalName,
});
}
// Store mapping: sanitized → original
this.mapping.set(result.sanitized, tool.name);
// Disambiguate: append _N suffix to make unique
let suffix = 2;
let disambiguated: string;
do {
disambiguated = `${finalName}_${suffix}`;
suffix++;
} while (this.mapping.has(disambiguated) && this.mapping.get(disambiguated) !== tool.name);
finalName = disambiguated;
}
// Always register in mapping so later tools can detect collisions
// even when this tool's name was unchanged
this.mapping.set(finalName, tool.name);
// Record change only if name actually changed
if (finalName !== tool.name) {
this.changes.push({
original: tool.name,
sanitized: result.sanitized,
sanitized: finalName,
});
}
return {
...tool,
name: result.sanitized,
name: finalName,
};
});
}
@@ -100,13 +100,34 @@ export function smartTruncate(name: string, maxLen: number = GEMINI_MAX_TOOL_NAM
return `${prefix}_${hash}`;
}
/**
* Force a string to comply with Gemini tool name character rules.
* Replaces invalid characters with underscores and ensures the name
* starts with a letter or underscore (not a digit, dot, colon, etc.).
*/
function forceValidChars(name: string): string {
// Replace any character not in [a-zA-Z0-9_.:/-] with underscore
let fixed = name.replace(/[^a-zA-Z0-9_.:/-]/g, '_');
// Collapse multiple consecutive underscores from replacement
fixed = fixed.replace(/_+/g, '_');
// Ensure starts with letter or underscore (not digit, dot, colon, hyphen, slash)
if (fixed.length > 0 && !/^[a-zA-Z_]/.test(fixed)) {
fixed = '_' + fixed;
}
return fixed || '_';
}
/**
* Sanitize a tool name to comply with Gemini API constraints.
*
* Process:
* 1. Remove duplicate segments (always, as duplicates are likely unintentional)
* 2. Truncate with hash if >64 chars
* 3. Return result with changed flag
* 3. Force valid characters if still invalid
* 4. Truncate again if forced-valid name exceeds limit
*
* @param name The original tool name
* @returns Sanitization result with sanitized name and changed flag
@@ -121,9 +142,12 @@ export function sanitizeToolName(name: string): SanitizeResult {
sanitized = smartTruncate(sanitized);
}
// Step 3: If still invalid after sanitization, apply truncation to original
// Step 3: If still invalid, fix characters and truncate if needed
if (!isValidToolName(sanitized)) {
sanitized = smartTruncate(name);
sanitized = forceValidChars(sanitized);
if (sanitized.length > GEMINI_MAX_TOOL_NAME_LENGTH) {
sanitized = smartTruncate(sanitized);
}
}
return {
@@ -8,7 +8,7 @@ import {
hasAntigravityRiskAcceptanceFlag,
isAntigravityResponsibilityBypassEnabled,
validateAntigravityRiskAcknowledgement,
} from '../../../src/cliproxy/antigravity-responsibility';
} from '../../auth/antigravity-responsibility';
describe('antigravity-responsibility', () => {
let tempHome = '';
@@ -19,8 +19,8 @@ describe('Auth Token Manager', () => {
beforeEach(() => {
// Clear cache and reload
delete require.cache[require.resolve('../../../dist/cliproxy/auth-token-manager')];
const authTokenManager = require('../../../dist/cliproxy/auth-token-manager');
delete require.cache[require.resolve('../../../../dist/cliproxy/auth-token-manager')];
const authTokenManager = require('../../../../dist/cliproxy/auth-token-manager');
generateSecureToken = authTokenManager.generateSecureToken;
});
@@ -398,8 +398,8 @@ describe('Auth Token Manager', () => {
let CCS_CONTROL_PANEL_SECRET;
beforeEach(() => {
delete require.cache[require.resolve('../../../dist/cliproxy/config-generator')];
const configGenerator = require('../../../dist/cliproxy/config-generator');
delete require.cache[require.resolve('../../../../dist/cliproxy/config-generator')];
const configGenerator = require('../../../../dist/cliproxy/config-generator');
CCS_INTERNAL_API_KEY = configGenerator.CCS_INTERNAL_API_KEY;
CCS_CONTROL_PANEL_SECRET = configGenerator.CCS_CONTROL_PANEL_SECRET;
});
@@ -3,7 +3,7 @@ import {
getManagementAuthUrlPath,
getPasteCallbackStartPath,
getManagementOAuthCallbackPath,
} from '../../../src/cliproxy/auth/auth-types';
} from '../auth-types';
describe('auth-types paste-callback start path', () => {
it('maps providers to CLIProxyAPI management auth-url routes', () => {
@@ -5,11 +5,7 @@
*/
import { describe, it, expect } from 'bun:test';
import {
getTokenExpiryTimestamp,
isTokenExpired,
sanitizeEmail,
} from '../../../src/cliproxy/auth-utils';
import { getTokenExpiryTimestamp, isTokenExpired, sanitizeEmail } from '../../auth/auth-utils';
describe('Auth Utilities', () => {
describe('sanitizeEmail', () => {
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'bun:test';
import { parseGitLabPatAuthResponse } from '../../../src/cliproxy/auth/gitlab-pat-response';
import { parseGitLabPatAuthResponse } from '../gitlab-pat-response';
describe('parseGitLabPatAuthResponse', () => {
it('sanitizes HTML error bodies for non-ok responses', () => {
@@ -2,9 +2,9 @@ import { afterEach, describe, expect, it, mock, spyOn } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import type { ProxyTarget } from '../../../src/cliproxy/proxy-target-resolver';
import { InteractivePrompt } from '../../../src/utils/prompt';
import { getCapturedFetchRequests, mockFetch, restoreFetch } from '../../mocks';
import type { ProxyTarget } from '../../proxy/proxy-target-resolver';
import { InteractivePrompt } from '../../../utils/prompt';
import { getCapturedFetchRequests, mockFetch, restoreFetch } from '../../../../tests/mocks';
const remoteTarget: ProxyTarget = {
host: 'proxy.example.com',
@@ -28,7 +28,7 @@ describe('requestPasteCallbackStart', () => {
]);
const { requestPasteCallbackStart } = await import(
`../../../src/cliproxy/auth/oauth-handler?request-claude-start=${Date.now()}`
`../oauth-handler?request-claude-start=${Date.now()}`
);
const startData = await requestPasteCallbackStart('claude', remoteTarget);
@@ -52,7 +52,7 @@ describe('requestPasteCallbackStart', () => {
]);
const { requestPasteCallbackStart } = await import(
`../../../src/cliproxy/auth/oauth-handler?request-kiro-start=${Date.now()}`
`../oauth-handler?request-kiro-start=${Date.now()}`
);
const startData = await requestPasteCallbackStart('kiro', remoteTarget, {
kiroMethod: 'aws',
@@ -71,7 +71,7 @@ describe('requestPasteCallbackStart', () => {
it('throws for Kiro methods that require the local callback server flow', async () => {
const { requestPasteCallbackStart } = await import(
`../../../src/cliproxy/auth/oauth-handler?request-kiro-authcode-start=${Date.now()}`
`../oauth-handler?request-kiro-authcode-start=${Date.now()}`
);
await expect(
@@ -83,7 +83,7 @@ describe('requestPasteCallbackStart', () => {
describe('usesKiroLocalCallbackReplay', () => {
it('limits local callback replay to CLI auth-code flows', async () => {
const { usesKiroLocalCallbackReplay } = await import(
`../../../src/cliproxy/auth/oauth-handler?kiro-local-callback-mode=${Date.now()}`
`../oauth-handler?kiro-local-callback-mode=${Date.now()}`
);
expect(usesKiroLocalCallbackReplay('aws-authcode', 'authcode')).toBe(true);
@@ -97,7 +97,7 @@ describe('usesKiroLocalCallbackReplay', () => {
describe('resolvePasteCallbackAuthUrl', () => {
it('returns the immediate auth URL without polling', async () => {
const { resolvePasteCallbackAuthUrl } = await import(
`../../../src/cliproxy/auth/oauth-handler?resolve-immediate-auth-url=${Date.now()}`
`../oauth-handler?resolve-immediate-auth-url=${Date.now()}`
);
const authUrl = await resolvePasteCallbackAuthUrl(
remoteTarget,
@@ -119,7 +119,7 @@ describe('resolvePasteCallbackAuthUrl', () => {
]);
const { resolvePasteCallbackAuthUrl } = await import(
`../../../src/cliproxy/auth/oauth-handler?resolve-polled-auth-url=${Date.now()}`
`../oauth-handler?resolve-polled-auth-url=${Date.now()}`
);
const authUrl = await resolvePasteCallbackAuthUrl(remoteTarget, { state: 'state-123' }, 50, 0);
@@ -142,17 +142,15 @@ describe('findNewTokenSnapshotForManualAuth', () => {
const existingMtimeMs = fs.statSync(existingFile).mtimeMs;
const { findNewTokenSnapshotForManualAuth } = await import(
`../../../src/cliproxy/auth/oauth-handler?manual-auth-new-token=${Date.now()}`
`../oauth-handler?manual-auth-new-token=${Date.now()}`
);
const newFile = path.join(tokenDir, 'kiro-new.json');
fs.writeFileSync(newFile, JSON.stringify({ type: 'kiro', email: 'new@example.com' }));
const snapshot = findNewTokenSnapshotForManualAuth(
'kiro',
tokenDir,
[{ file: 'kiro-existing.json', mtimeMs: existingMtimeMs }]
);
const snapshot = findNewTokenSnapshotForManualAuth('kiro', tokenDir, [
{ file: 'kiro-existing.json', mtimeMs: existingMtimeMs },
]);
expect(snapshot?.file).toBe('kiro-new.json');
fs.rmSync(tokenDir, { recursive: true, force: true });
@@ -165,10 +163,13 @@ describe('findNewTokenSnapshotForManualAuth', () => {
const existingMtimeMs = fs.statSync(tokenFile).mtimeMs;
const { findNewTokenSnapshotForManualAuth } = await import(
`../../../src/cliproxy/auth/oauth-handler?manual-auth-updated-token=${Date.now()}`
`../oauth-handler?manual-auth-updated-token=${Date.now()}`
);
fs.writeFileSync(tokenFile, JSON.stringify({ type: 'kiro', email: 'existing@example.com', refreshed: true }));
fs.writeFileSync(
tokenFile,
JSON.stringify({ type: 'kiro', email: 'existing@example.com', refreshed: true })
);
const bumpedTime = new Date(existingMtimeMs + 10_000);
fs.utimesSync(tokenFile, bumpedTime, bumpedTime);
@@ -187,7 +188,7 @@ describe('findNewTokenSnapshotForManualAuth', () => {
describe('getCliAuthNicknameError', () => {
it('allows omitted nicknames for no-email providers', async () => {
const { getCliAuthNicknameError } = await import(
`../../../src/cliproxy/auth/oauth-handler?cli-nickname-empty=${Date.now()}`
`../oauth-handler?cli-nickname-empty=${Date.now()}`
);
expect(getCliAuthNicknameError('kiro', undefined, [])).toBeNull();
@@ -196,7 +197,7 @@ describe('getCliAuthNicknameError', () => {
it('rejects invalid supplied nicknames before OAuth starts', async () => {
const { getCliAuthNicknameError } = await import(
`../../../src/cliproxy/auth/oauth-handler?cli-nickname-invalid=${Date.now()}`
`../oauth-handler?cli-nickname-invalid=${Date.now()}`
);
expect(getCliAuthNicknameError('kiro', 'bad nickname', [])).toBe(
@@ -206,7 +207,7 @@ describe('getCliAuthNicknameError', () => {
it('rejects supplied nicknames that collide with existing ids or nicknames', async () => {
const { getCliAuthNicknameError } = await import(
`../../../src/cliproxy/auth/oauth-handler?cli-nickname-conflict=${Date.now()}`
`../oauth-handler?cli-nickname-conflict=${Date.now()}`
);
const existingAccounts = [
{ id: 'github-ABC123', nickname: 'work' },
@@ -223,7 +224,7 @@ describe('getCliAuthNicknameError', () => {
it('allows reauth when the supplied nickname already belongs to the same account', async () => {
const { getCliAuthNicknameError } = await import(
`../../../src/cliproxy/auth/oauth-handler?cli-nickname-reauth=${Date.now()}`
`../oauth-handler?cli-nickname-reauth=${Date.now()}`
);
const existingAccounts = [
{ id: 'github-ABC123', nickname: 'work' },
@@ -231,7 +232,9 @@ describe('getCliAuthNicknameError', () => {
];
expect(getCliAuthNicknameError('kiro', 'work', existingAccounts, 'github-ABC123')).toBeNull();
expect(getCliAuthNicknameError('kiro', 'github-ABC123', existingAccounts, 'github-ABC123')).toBeNull();
expect(
getCliAuthNicknameError('kiro', 'github-ABC123', existingAccounts, 'github-ABC123')
).toBeNull();
});
});
@@ -242,7 +245,7 @@ describe('promptGitLabPersonalAccessToken', () => {
);
const { promptGitLabPersonalAccessToken } = await import(
`../../../src/cliproxy/auth/oauth-handler?gitlab-pat-prompt=${Date.now()}`
`../oauth-handler?gitlab-pat-prompt=${Date.now()}`
);
await expect(promptGitLabPersonalAccessToken()).resolves.toBe('glpat-secret-token');
@@ -255,7 +258,7 @@ describe('promptGitLabPersonalAccessToken', () => {
);
const { promptGitLabPersonalAccessToken } = await import(
`../../../src/cliproxy/auth/oauth-handler?gitlab-pat-prompt-blank=${Date.now()}`
`../oauth-handler?gitlab-pat-prompt-blank=${Date.now()}`
);
await expect(promptGitLabPersonalAccessToken()).resolves.toBeNull();
@@ -266,7 +269,7 @@ describe('promptGitLabPersonalAccessToken', () => {
describe('normalizeGitLabBaseUrl', () => {
it('returns undefined for blank values', async () => {
const { normalizeGitLabBaseUrl } = await import(
`../../../src/cliproxy/auth/oauth-handler?gitlab-url-empty=${Date.now()}`
`../oauth-handler?gitlab-url-empty=${Date.now()}`
);
expect(normalizeGitLabBaseUrl(undefined)).toBeUndefined();
@@ -275,7 +278,7 @@ describe('normalizeGitLabBaseUrl', () => {
it('normalizes whitespace and trailing slashes for self-hosted URLs', async () => {
const { normalizeGitLabBaseUrl } = await import(
`../../../src/cliproxy/auth/oauth-handler?gitlab-url-normalize=${Date.now()}`
`../oauth-handler?gitlab-url-normalize=${Date.now()}`
);
expect(normalizeGitLabBaseUrl(' https://gitlab.example.com/custom/ ')).toBe(
@@ -285,7 +288,7 @@ describe('normalizeGitLabBaseUrl', () => {
it('rejects malformed or scheme-less URLs before hitting CLIProxy', async () => {
const { normalizeGitLabBaseUrl } = await import(
`../../../src/cliproxy/auth/oauth-handler?gitlab-url-invalid=${Date.now()}`
`../oauth-handler?gitlab-url-invalid=${Date.now()}`
);
expect(() => normalizeGitLabBaseUrl('gitlab.example.com')).toThrow(
@@ -7,7 +7,7 @@ import {
getExpectedLocalCallback,
getKiroBuilderIdSelectionInput,
validateManualCallbackUrl,
} from '../../../src/cliproxy/auth/oauth-process';
} from '../oauth-process';
describe('oauth-process stderr parsing', () => {
it('does not match provider-specific patterns for other providers', () => {
@@ -7,8 +7,8 @@ import * as assert from 'assert';
const fs = require('fs');
describe('Profile Mapper', () => {
const profileMapper = require('../../../dist/cliproxy/sync/profile-mapper');
const profileReader = require('../../../dist/api/services/profile-reader');
const profileMapper = require('../../../../dist/cliproxy/sync/profile-mapper');
const profileReader = require('../../../../dist/api/services/profile-reader');
describe('mapProfileToClaudeKey', () => {
it('returns null when env is missing', () => {
@@ -9,8 +9,8 @@
*/
import { createInterface, Interface } from 'readline';
import { fail, info, ok, warn } from '../utils/ui';
import { getCliproxySafetyConfig } from '../config/unified-config-loader';
import { fail, info, ok, warn } from '../../utils/ui';
import { getCliproxySafetyConfig } from '../../config/unified-config-loader';
export const ANTIGRAVITY_RISK_ISSUE_URL = 'https://github.com/kaitranntt/ccs/issues/509';
export const ANTIGRAVITY_ACK_VERSION = '2026-02-24-antigravity-oauth-v2';
@@ -20,7 +20,7 @@
*/
// Re-export types
export type { AuthStatus, ProviderOAuthConfig, OAuthOptions } from './auth';
export type { AuthStatus, ProviderOAuthConfig, OAuthOptions } from '.';
// Re-export configurations
export {
@@ -29,7 +29,7 @@ export {
PROVIDER_AUTH_PREFIXES,
PROVIDER_TYPE_VALUES,
getOAuthConfig,
} from './auth';
} from '.';
// Re-export token management functions
export {
@@ -40,21 +40,16 @@ export {
getAllAuthStatus,
clearAuth,
displayAuthStatus,
} from './auth';
} from '.';
// Re-export environment detection functions
export {
isHeadlessEnvironment,
killProcessOnPort,
getTimeoutTroubleshooting,
showStep,
} from './auth';
export { isHeadlessEnvironment, killProcessOnPort, getTimeoutTroubleshooting, showStep } from '.';
// Re-export OAuth handling functions
export { triggerOAuth, ensureAuth } from './auth';
export { triggerOAuth, ensureAuth } from '.';
// Re-export account management initialization
import { discoverExistingAccounts } from './account-manager';
import { discoverExistingAccounts } from '../accounts/account-manager';
/**
* Initialize accounts registry from existing tokens
@@ -39,14 +39,18 @@ function startCleanupInterval(): void {
authSessionEvents.emit('session:expired', sessionId);
}
}
// Stop interval if no active sessions
if (activeSessions.size === 0 && cleanupInterval) {
clearInterval(cleanupInterval);
cleanupInterval = null;
}
stopCleanupIfEmpty();
}, 60000); // Check every minute
}
/** Clear the cleanup interval when no sessions remain. */
function stopCleanupIfEmpty(): void {
if (activeSessions.size === 0 && cleanupInterval) {
clearInterval(cleanupInterval);
cleanupInterval = null;
}
}
/**
* Register an active OAuth session
*/
@@ -82,6 +86,7 @@ export function attachProcessToSession(sessionId: string, process: ChildProcess)
export function unregisterAuthSession(sessionId: string): void {
activeSessions.delete(sessionId);
authSessionEvents.emit('session:ended', sessionId);
stopCleanupIfEmpty();
}
/**
@@ -101,6 +106,7 @@ export function cancelAuthSession(sessionId: string): boolean {
activeSessions.delete(sessionId);
authSessionEvents.emit('session:cancelled', sessionId);
stopCleanupIfEmpty();
return true;
}
@@ -112,15 +118,16 @@ export function getActiveSession(sessionId: string): ActiveAuthSession | null {
}
/**
* Get active session for a provider (most recent)
* Get active session for a provider (most recent by startedAt)
*/
export function getActiveSessionForProvider(provider: string): ActiveAuthSession | null {
let latest: ActiveAuthSession | null = null;
for (const session of activeSessions.values()) {
if (session.provider === provider) {
return session;
if (session.provider === provider && (!latest || session.startedAt > latest.startedAt)) {
latest = session;
}
}
return null;
return latest;
}
/**
@@ -145,5 +152,6 @@ export function cancelAllSessionsForProvider(provider: string): number {
count++;
}
}
stopCleanupIfEmpty();
return count;
}
@@ -8,8 +8,8 @@
*/
import { randomBytes } from 'crypto';
import { loadOrCreateUnifiedConfig, mutateUnifiedConfig } from '../config/unified-config-loader';
import { CCS_INTERNAL_API_KEY, CCS_CONTROL_PANEL_SECRET } from './config-generator';
import { loadOrCreateUnifiedConfig, mutateUnifiedConfig } from '../../config/unified-config-loader';
import { CCS_INTERNAL_API_KEY, CCS_CONTROL_PANEL_SECRET } from '../config/generator';
/**
* Generate a cryptographically secure token.
+1 -1
View File
@@ -5,7 +5,7 @@
*/
import { CLIProxyProvider } from '../types';
import type { AccountInfo } from '../account-manager';
import type { AccountInfo } from '../accounts/account-manager';
import {
buildProviderMap,
CLIPROXY_PROVIDER_IDS,
+1 -1
View File
@@ -8,7 +8,7 @@
import { spawn } from 'child_process';
import { info, ok, fail } from '../../utils/ui';
import { ensureCLIProxyBinary } from '../binary-manager';
import { generateConfig } from '../config-generator';
import { generateConfig } from '../config/config-generator';
import { getProviderTokenDir } from './token-manager';
export interface KiroImportResult {
+5 -5
View File
@@ -13,7 +13,7 @@
import * as fs from 'fs';
import { fail, info, warn, color, ok } from '../../utils/ui';
import { ensureCLIProxyBinary } from '../binary-manager';
import { generateConfig } from '../config-generator';
import { generateConfig } from '../config/config-generator';
import { CLIProxyProvider } from '../types';
import {
AccountInfo,
@@ -24,7 +24,7 @@ import {
findAccountNameMatch,
PROVIDERS_WITHOUT_EMAIL,
validateNickname,
} from '../account-manager';
} from '../accounts/account-manager';
import {
enhancedPreflightOAuthCheck,
OAUTH_CALLBACK_PORTS as OAUTH_PORTS,
@@ -62,14 +62,14 @@ import {
buildProxyUrl,
buildManagementHeaders,
type ProxyTarget,
} from '../proxy-target-resolver';
} from '../proxy/proxy-target-resolver';
import {
checkNewAccountConflict,
warnNewAccountConflict,
warnOAuthBanRisk,
warnPossible403Ban,
} from '../account-safety';
import { ensureCliAntigravityResponsibility } from '../antigravity-responsibility';
} from '../accounts/account-safety';
import { ensureCliAntigravityResponsibility } from '../auth/antigravity-responsibility';
import { InteractivePrompt } from '../../utils/prompt';
interface PasteCallbackStartData {
+4 -4
View File
@@ -10,7 +10,7 @@ import { ok, fail, info, warn } from '../../utils/ui';
import { killWithEscalation } from '../../utils/process-utils';
import { tryKiroImport } from './kiro-import';
import { CLIProxyProvider } from '../types';
import { AccountInfo } from '../account-manager';
import { AccountInfo } from '../accounts/account-manager';
import {
parseProjectList,
parseDefaultProject,
@@ -21,7 +21,7 @@ import {
cancelProjectSelection,
type GCloudProject,
type ProjectSelectionPrompt,
} from '../project-selection-handler';
} from '../auth/project-selection-handler';
import { KiroAuthMethod, ProviderOAuthConfig } from './auth-types';
import { getTimeoutTroubleshooting, showStep } from './environment-detector';
import {
@@ -34,14 +34,14 @@ import {
deviceCodeEvents,
DEVICE_CODE_TIMEOUT_MS,
type DeviceCodePrompt,
} from '../device-code-handler';
} from '../auth/device-code-handler';
import { OAUTH_FLOW_TYPES } from '../../management';
import {
registerAuthSession,
attachProcessToSession,
unregisterAuthSession,
authSessionEvents,
} from '../auth-session-manager';
} from '../auth/auth-session-manager';
/** Options for OAuth process execution */
export interface OAuthProcessOptions {
@@ -1,4 +1,4 @@
import type { AccountTier } from './accounts/types';
import type { AccountTier } from '../accounts/types';
import type {
ProviderAccessState,
ProviderCapacityState,
@@ -1,4 +1,4 @@
import type { AccountTier } from './accounts/types';
import type { AccountTier } from '../accounts/types';
export type ProviderEntitlementSource =
| 'runtime_api'
@@ -10,7 +10,7 @@
*/
import { CLIProxyProvider } from '../../types';
import { getProviderAccounts } from '../../account-manager';
import { getProviderAccounts } from '../../accounts/account-manager';
import {
getTokenRefreshOwnership,
isRefreshDelegatedToCLIProxy,
+1 -1
View File
@@ -9,7 +9,7 @@ import * as fs from 'fs';
import * as path from 'path';
import { CLIProxyProvider } from '../types';
import { CLIPROXY_PROFILES } from '../../auth/profile-detector';
import { getProviderAccounts, getAccountTokenPath } from '../account-manager';
import { getProviderAccounts, getAccountTokenPath } from '../accounts/account-manager';
import { isRefreshDelegated } from './provider-refreshers';
/** Preemptive refresh time: refresh tokens 45 minutes before expiry */
+5 -5
View File
@@ -10,8 +10,8 @@ import * as path from 'path';
import { createHash } from 'crypto';
import { CLIProxyProvider } from '../types';
import { CLIPROXY_PROFILES } from '../../auth/profile-detector';
import { getProviderAuthDir } from '../config-generator';
import { getProviderAccounts, getDefaultAccount } from '../account-manager';
import { getProviderAuthDir } from '../config/config-generator';
import { getProviderAccounts, getDefaultAccount } from '../accounts/account-manager';
import { deleteTokenFile, extractAccountIdFromTokenFile } from '../accounts/token-file-ops';
import { buildEmailBackedAccountId } from '../accounts/email-account-identity';
import { getTokenRefreshOwnership } from '../provider-capabilities';
@@ -367,8 +367,8 @@ export function registerAccountFromToken(
nickname?: string,
verbose = false,
expectedAccountId?: string
): import('../account-manager').AccountInfo | null {
const { registerAccount } = require('../account-manager');
): import('../accounts/account-manager').AccountInfo | null {
const { registerAccount } = require('../accounts/account-manager');
let selectedCandidate: Omit<TokenCandidate, 'mtimeMs'> | null = null;
try {
const candidates = listTokenCandidates(provider, tokenDir);
@@ -438,7 +438,7 @@ export function registerAccountFromToken(
*/
function uploadTokenToRemoteAsync(tokenPath: string, verbose: boolean): void {
// Dynamic import to avoid circular dependencies
import('../remote-token-uploader')
import('../management/remote-token-uploader')
.then(({ uploadTokenToRemote, isRemoteUploadEnabled }) => {
if (isRemoteUploadEnabled()) {
// uploadTokenToRemote handles its own success/failure logging
+2 -2
View File
@@ -8,14 +8,14 @@
import * as fs from 'fs';
import * as path from 'path';
import { info, warn } from '../utils/ui';
import { getBinDir, CLIPROXY_DEFAULT_PORT } from './config-generator';
import { getBinDir, CLIPROXY_DEFAULT_PORT } from './config/config-generator';
import { BinaryInfo, BinaryManagerConfig } from './types';
import {
BACKEND_CONFIG,
DEFAULT_BACKEND,
CLIPROXY_MAX_STABLE_VERSION,
getExecutableName,
} from './platform-detector';
} from './binary/platform-detector';
import { stopProxy } from './services/proxy-lifecycle-service';
import { waitForPortFree } from '../utils/port-utils';
import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
@@ -5,7 +5,7 @@
*/
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
import { __testExports } from '../../../src/cliproxy/binary/downloader';
import { __testExports } from '../downloader';
const { getProxyUrl, shouldBypassProxy, getHostname, getProxyAgent } = __testExports;
@@ -9,7 +9,7 @@ import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
import { deleteBinary } from '../../../src/cliproxy/binary/installer';
import { deleteBinary } from '../installer';
describe('deleteBinary ETXTBSY guard', () => {
let tmpDir: string;
@@ -48,8 +48,7 @@ describe('deleteBinary ETXTBSY guard', () => {
// We can't reliably trigger ETXTBSY in tests (need a running Go binary),
// so we verify the code structure matches the expected behavior.
const err = Object.assign(new Error('ETXTBSY: text file busy'), { code: 'ETXTBSY' });
const code =
err instanceof Error && 'code' in err ? (err as { code: string }).code : '';
const code = err instanceof Error && 'code' in err ? (err as { code: string }).code : '';
expect(code).toBe('ETXTBSY');
// The guard only catches ETXTBSY, not EBUSY
expect(code === 'ETXTBSY').toBe(true);
@@ -59,8 +58,7 @@ describe('deleteBinary ETXTBSY guard', () => {
it('EBUSY is not treated as "binary in use"', () => {
// Verify that EBUSY (Windows mount/directory) is distinguished from ETXTBSY
const err = Object.assign(new Error('EBUSY: resource busy'), { code: 'EBUSY' });
const code =
err instanceof Error && 'code' in err ? (err as { code: string }).code : '';
const code = err instanceof Error && 'code' in err ? (err as { code: string }).code : '';
expect(code).toBe('EBUSY');
expect(code === 'ETXTBSY').toBe(false);
});
@@ -29,7 +29,7 @@ describe('installCliproxyVersion', () => {
let seenBackend: string | undefined;
const binaryManager = await import(
`../../../src/cliproxy/binary-manager?binary-manager-explicit-plus=${Date.now()}`
`../../binary-manager?binary-manager-explicit-plus=${Date.now()}`
);
await binaryManager.installCliproxyVersion('6.7.1', false, 'plus', {
@@ -52,9 +52,7 @@ describe('installCliproxyVersion', () => {
});
it('returns plus and emits an informational notice when plus backend is resolved locally', async () => {
const binaryManager = await import(
`../../../src/cliproxy/binary-manager?binary-manager-warning=${Date.now()}`
);
const binaryManager = await import(`../../binary-manager?binary-manager-warning=${Date.now()}`);
const writes: string[] = [];
const originalWrite = process.stderr.write.bind(process.stderr);
@@ -75,12 +73,12 @@ describe('installCliproxyVersion', () => {
});
it('uses plus binary and pin state when local runtime is configured for plus', async () => {
const { createEmptyUnifiedConfig } = await import('../../../src/config/unified-config-types');
const { saveUnifiedConfig } = await import('../../../src/config/unified-config-loader');
const { savePinnedVersion } = await import('../../../src/cliproxy/binary/version-cache');
const { getExecutableName } = await import('../../../src/cliproxy/platform-detector');
const { createEmptyUnifiedConfig } = await import('../../../config/unified-config-types');
const { saveUnifiedConfig } = await import('../../../config/unified-config-loader');
const { savePinnedVersion } = await import('../version-cache');
const { getExecutableName } = await import('../platform-detector');
const binaryService = await import(
`../../../src/cliproxy/services/binary-service?binary-service-plus-migration=${Date.now()}`
`../../services/binary-service?binary-service-plus-migration=${Date.now()}`
);
const config = createEmptyUnifiedConfig();
@@ -101,12 +99,12 @@ describe('installCliproxyVersion', () => {
});
it('copies plus binary and pin state when legacy deleted upstream fallback is active', async () => {
const { createEmptyUnifiedConfig } = await import('../../../src/config/unified-config-types');
const { saveUnifiedConfig } = await import('../../../src/config/unified-config-loader');
const { savePinnedVersion } = await import('../../../src/cliproxy/binary/version-cache');
const platformDetector = await import('../../../src/cliproxy/platform-detector');
const { createEmptyUnifiedConfig } = await import('../../../config/unified-config-types');
const { saveUnifiedConfig } = await import('../../../config/unified-config-loader');
const { savePinnedVersion } = await import('../version-cache');
const platformDetector = await import('../platform-detector');
const binaryService = await import(
`../../../src/cliproxy/services/binary-service?binary-service-legacy-plus-fallback=${Date.now()}`
`../../services/binary-service?binary-service-legacy-plus-fallback=${Date.now()}`
);
const plusConfig = platformDetector.BACKEND_CONFIG.plus as unknown as { repo: string };
@@ -146,9 +144,7 @@ describe('installCliproxyVersion', () => {
ensureBinary: 0,
};
const binaryManager = await import(
`../../../src/cliproxy/binary-manager?binary-manager-install=${Date.now()}`
);
const binaryManager = await import(`../../binary-manager?binary-manager-install=${Date.now()}`);
await binaryManager.installCliproxyVersion('6.7.1', false, 'plus', {
createManager: () => ({
@@ -181,9 +177,7 @@ describe('installCliproxyVersion', () => {
});
it('fails fast when runtime startup forbids installing a missing binary', async () => {
const binaryManager = await import(
`../../../src/cliproxy/binary-manager?binary-manager-runtime=${Date.now()}`
);
const binaryManager = await import(`../../binary-manager?binary-manager-runtime=${Date.now()}`);
await expect(
binaryManager.ensureCLIProxyBinary(false, {
@@ -28,12 +28,9 @@ describe('version-checker stale cache fallback', () => {
});
it('uses a stale latest-version cache when GitHub lookup fails', async () => {
const {
getVersionCachePath,
writeInstalledVersion,
} = await import('../../../src/cliproxy/binary/version-cache');
const { VERSION_CACHE_DURATION_MS } = await import('../../../src/cliproxy/binary/types');
const { checkForUpdates } = await import('../../../src/cliproxy/binary/version-checker');
const { getVersionCachePath, writeInstalledVersion } = await import('../version-cache');
const { VERSION_CACHE_DURATION_MS } = await import('../types');
const { checkForUpdates } = await import('../version-checker');
const plusBinDir = path.join(tempHome, '.ccs', 'cliproxy', 'bin', 'plus');
fs.mkdirSync(plusBinDir, { recursive: true });
@@ -60,9 +57,9 @@ describe('version-checker stale cache fallback', () => {
});
it('uses a stale release-list cache when GitHub list lookup fails', async () => {
const { getVersionListCachePath } = await import('../../../src/cliproxy/binary/version-cache');
const { VERSION_CACHE_DURATION_MS } = await import('../../../src/cliproxy/binary/types');
const { fetchAllVersions } = await import('../../../src/cliproxy/binary/version-checker');
const { getVersionListCachePath } = await import('../version-cache');
const { VERSION_CACHE_DURATION_MS } = await import('../types');
const { fetchAllVersions } = await import('../version-checker');
const plusBinDir = path.join(tempHome, '.ccs', 'cliproxy', 'bin', 'plus');
fs.mkdirSync(plusBinDir, { recursive: true });
@@ -90,8 +87,8 @@ describe('version-checker stale cache fallback', () => {
});
it('skips update lookups when runtime startup prefers the installed binary', async () => {
const { getExecutableName } = await import('../../../src/cliproxy/platform-detector');
const { ensureBinary } = await import('../../../src/cliproxy/binary/lifecycle');
const { getExecutableName } = await import('../platform-detector');
const { ensureBinary } = await import('../lifecycle');
const plusBinDir = path.join(tempHome, '.ccs', 'cliproxy', 'bin', 'plus');
fs.mkdirSync(plusBinDir, { recursive: true });
+1 -1
View File
@@ -4,7 +4,7 @@
*/
import { ArchiveExtension, CLIProxyBackend } from '../types';
import { DEFAULT_BACKEND } from '../platform-detector';
import { DEFAULT_BACKEND } from '../binary/platform-detector';
import { extractTarGz } from './tar-extractor';
import { extractZip } from './zip-extractor';
+1 -1
View File
@@ -12,7 +12,7 @@ import {
getChecksumsUrl,
getExecutableName,
DEFAULT_BACKEND,
} from '../platform-detector';
} from '../binary/platform-detector';
import { downloadWithRetry } from './downloader';
import { verifyChecksum, computeChecksum } from './verifier';
import { extractArchive } from './extractor';
+3 -3
View File
@@ -13,13 +13,13 @@ import {
} from './version-checker';
import { downloadAndInstall, deleteBinary, getBinaryPath } from './installer';
import { info, warn } from '../../utils/ui';
import { isCliproxyRunning } from '../stats-fetcher';
import { CLIPROXY_DEFAULT_PORT } from '../config-generator';
import { isCliproxyRunning } from '../services/stats-fetcher';
import { CLIPROXY_DEFAULT_PORT } from '../config/config-generator';
import {
CLIPROXY_MAX_STABLE_VERSION,
CLIPROXY_FAULTY_RANGE,
DEFAULT_BACKEND,
} from '../platform-detector';
} from '../binary/platform-detector';
/** Log helper */
function log(message: string, verbose: boolean): void {
@@ -11,7 +11,7 @@ import {
SupportedArch,
ArchiveExtension,
CLIProxyBackend,
} from './types';
} from '../types';
/** Backend configuration */
export const BACKEND_CONFIG = {
+5 -1
View File
@@ -6,7 +6,11 @@
import * as fs from 'fs';
import * as path from 'path';
import * as zlib from 'zlib';
import { getExecutableName, getArchiveBinaryName, DEFAULT_BACKEND } from '../platform-detector';
import {
getExecutableName,
getArchiveBinaryName,
DEFAULT_BACKEND,
} from '../binary/platform-detector';
import type { CLIProxyBackend } from '../types';
/**
+2 -2
View File
@@ -5,14 +5,14 @@
import * as fs from 'fs';
import * as path from 'path';
import { getBinDir } from '../config-generator';
import { getBinDir } from '../config/config-generator';
import {
VersionCache,
VERSION_CACHE_DURATION_MS,
VERSION_PIN_FILE,
VersionListCache,
} from './types';
import { DEFAULT_BACKEND } from '../platform-detector';
import { DEFAULT_BACKEND } from '../binary/platform-detector';
import type { CLIProxyBackend } from '../types';
/**
+1 -1
View File
@@ -18,7 +18,7 @@ import {
CLIPROXY_MAX_STABLE_VERSION,
CLIPROXY_FAULTY_RANGE,
DEFAULT_BACKEND,
} from '../platform-detector';
} from '../binary/platform-detector';
import type { CLIProxyBackend } from '../types';
interface FetchLatestVersionDeps {
+5 -1
View File
@@ -6,7 +6,11 @@
import * as fs from 'fs';
import * as path from 'path';
import * as zlib from 'zlib';
import { getExecutableName, getArchiveBinaryName, DEFAULT_BACKEND } from '../platform-detector';
import {
getExecutableName,
getArchiveBinaryName,
DEFAULT_BACKEND,
} from '../binary/platform-detector';
import type { CLIProxyBackend } from '../types';
/**
@@ -6,8 +6,8 @@
const assert = require('assert');
describe('Backend Selection', () => {
const platformDetector = require('../../../dist/cliproxy/platform-detector');
const types = require('../../../dist/cliproxy/types');
const platformDetector = require('../../../../dist/cliproxy/platform-detector');
const types = require('../../../../dist/cliproxy/types');
describe('BACKEND_CONFIG', () => {
it('has correct configuration for original backend', () => {
@@ -5,26 +5,26 @@
*/
import { describe, expect, test } from 'bun:test';
import { CLIPROXY_DEFAULT_PORT as BACKEND_CLIPROXY_DEFAULT_PORT } from '../../../src/cliproxy/config/port-manager';
import { DEFAULT_CURSOR_PORT as BACKEND_CURSOR_DEFAULT_PORT } from '../../../src/cursor/cursor-models';
import { CLIPROXY_DEFAULT_PORT as BACKEND_CLIPROXY_DEFAULT_PORT } from '../port-manager';
import { DEFAULT_CURSOR_PORT as BACKEND_CURSOR_DEFAULT_PORT } from '../../../cursor/cursor-models';
import {
CLIPROXY_PROVIDER_IDS as BACKEND_CLIPROXY_PROVIDER_IDS,
getProviderDescription as getBackendProviderDescription,
getProviderDisplayName as getBackendProviderDisplayName,
getProvidersByOAuthFlow,
} from '../../../src/cliproxy/provider-capabilities';
} from '../../provider-capabilities';
import {
CLIPROXY_DEFAULT_PORT as UI_CLIPROXY_DEFAULT_PORT,
DEFAULT_CURSOR_PORT as UI_CURSOR_DEFAULT_PORT,
} from '../../../ui/src/lib/default-ports';
} from '../../../../ui/src/lib/default-ports';
import {
CLIPROXY_PROVIDERS as UI_CLIPROXY_PROVIDERS,
CORE_CLIPROXY_PROVIDERS as UI_CORE_CLIPROXY_PROVIDERS,
DEVICE_CODE_PROVIDERS as UI_DEVICE_CODE_PROVIDERS,
PLUS_EXTRA_CLIPROXY_PROVIDERS as UI_PLUS_EXTRA_CLIPROXY_PROVIDERS,
PROVIDER_METADATA as UI_PROVIDER_METADATA,
} from '../../../ui/src/lib/provider-config';
import { PLUS_ONLY_PROVIDERS as BACKEND_PLUS_ONLY_PROVIDERS } from '../../../src/cliproxy/types';
} from '../../../../ui/src/lib/provider-config';
import { PLUS_ONLY_PROVIDERS as BACKEND_PLUS_ONLY_PROVIDERS } from '../../types/index';
function sorted(values: readonly string[]): string[] {
return [...values].sort((a, b) => a.localeCompare(b));
@@ -44,7 +44,9 @@ describe('Default Port Sync', () => {
});
test('Device code providers are synced between backend and UI', () => {
expect(sorted(UI_DEVICE_CODE_PROVIDERS)).toEqual(sorted(getProvidersByOAuthFlow('device_code')));
expect(sorted(UI_DEVICE_CODE_PROVIDERS)).toEqual(
sorted(getProvidersByOAuthFlow('device_code'))
);
});
test('plus-extra providers are synced between backend and UI', () => {
@@ -60,13 +62,17 @@ describe('Default Port Sync', () => {
test('Provider display names are synced between backend and UI', () => {
for (const provider of BACKEND_CLIPROXY_PROVIDER_IDS) {
expect(UI_PROVIDER_METADATA[provider].displayName).toBe(getBackendProviderDisplayName(provider));
expect(UI_PROVIDER_METADATA[provider].displayName).toBe(
getBackendProviderDisplayName(provider)
);
}
});
test('Provider descriptions are synced between backend and UI', () => {
for (const provider of BACKEND_CLIPROXY_PROVIDER_IDS) {
expect(UI_PROVIDER_METADATA[provider].description).toBe(getBackendProviderDescription(provider));
expect(UI_PROVIDER_METADATA[provider].description).toBe(
getBackendProviderDescription(provider)
);
}
});
});
@@ -6,8 +6,8 @@
*/
import { describe, expect, test } from 'bun:test';
import { CLIPROXY_PROFILES } from '../../../src/auth/profile-detector';
import { CLIPROXY_PROVIDERS } from '../../../ui/src/lib/provider-config';
import { CLIPROXY_PROFILES } from '../../../auth/profile-detector';
import { CLIPROXY_PROVIDERS } from '../../../../ui/src/lib/provider-config';
describe('Provider Sync', () => {
test('backend CLIPROXY_PROFILES matches UI CLIPROXY_PROVIDERS', () => {
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'bun:test';
import { loadBaseConfig } from '../../../src/cliproxy/base-config-loader';
import { loadBaseConfig } from '../../config/base-config-loader';
describe('base-config-loader new providers', () => {
it.each([
@@ -6,7 +6,7 @@ import { describe, it, expect } from 'bun:test';
import {
validateCompositeDefaultTier,
validateCompositeTiers,
} from '../../../src/cliproxy/composite-validator';
} from '../../config/composite-validator';
const validTier = {
provider: 'agy',
@@ -26,7 +26,7 @@ const {
deleteConfigForPort,
deleteConfig,
CLIPROXY_DEFAULT_PORT,
} = require('../../../dist/cliproxy/config-generator');
} = require('../../../../dist/cliproxy/config-generator');
describe('Config Generator Port', function () {
let cliproxyDir;
@@ -77,8 +77,15 @@ describe('Config Generator Port', function () {
it('returns config.yaml for default port (8317)', function () {
const configPath = getConfigPathForPort(CLIPROXY_DEFAULT_PORT);
const filename = path.basename(configPath);
assert.ok(configPath.endsWith('config.yaml'), `Expected path to end with config.yaml but got: ${configPath}`);
assert.strictEqual(filename, 'config.yaml', `Expected filename to be config.yaml but got: ${filename}`);
assert.ok(
configPath.endsWith('config.yaml'),
`Expected path to end with config.yaml but got: ${configPath}`
);
assert.strictEqual(
filename,
'config.yaml',
`Expected filename to be config.yaml but got: ${filename}`
);
});
it('returns config-{port}.yaml for variant ports', function () {
@@ -7,7 +7,7 @@ const assert = require('assert');
const path = require('path');
// Mock the config-manager module to provide test paths
const originalModule = require.cache[require.resolve('../../../dist/utils/config-manager')];
const originalModule = require.cache[require.resolve('../../../../dist/utils/config-manager')];
describe('Config Generator', () => {
let configGenerator;
@@ -15,7 +15,7 @@ describe('Config Generator', () => {
beforeEach(() => {
// Clear the require cache to reload module with fresh mocks
delete require.cache[require.resolve('../../../dist/cliproxy/config-generator')];
delete require.cache[require.resolve('../../../../dist/cliproxy/config-generator')];
// Set up a temporary test directory as CCS_HOME
mockCcsDir = '/test/home/.ccs';
@@ -174,8 +174,8 @@ describe('Config Generator', () => {
beforeEach(() => {
// Clear cache and reload module
delete require.cache[require.resolve('../../../dist/cliproxy/config-generator')];
const configGenerator = require('../../../dist/cliproxy/config-generator');
delete require.cache[require.resolve('../../../../dist/cliproxy/config-generator')];
const configGenerator = require('../../../../dist/cliproxy/config-generator');
parseUserApiKeys = configGenerator.parseUserApiKeys;
});
@@ -374,9 +374,9 @@ auth-dir: "/test"
process.env.CCS_HOME = testDir;
// Clear cache and reload module
delete require.cache[require.resolve('../../../dist/cliproxy/config-generator')];
delete require.cache[require.resolve('../../../dist/utils/config-manager')];
const configGenerator = require('../../../dist/cliproxy/config-generator');
delete require.cache[require.resolve('../../../../dist/cliproxy/config-generator')];
delete require.cache[require.resolve('../../../../dist/utils/config-manager')];
const configGenerator = require('../../../../dist/cliproxy/config-generator');
regenerateConfig = configGenerator.regenerateConfig;
getCliproxyConfigPath = configGenerator.getCliproxyConfigPath;
});
@@ -473,7 +473,10 @@ openai-compatibility:
const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
assert(newConfig.includes('openai-compatibility:'), 'Should preserve openai-compatibility');
assert(newConfig.includes('name: mimo'), 'Should preserve connector name');
assert(newConfig.includes('base-url: https://api.xiaomimimo.com/v1'), 'Should preserve base URL');
assert(
newConfig.includes('base-url: https://api.xiaomimimo.com/v1'),
'Should preserve base URL'
);
assert(newConfig.includes('alias: mimo-v2-flash'), 'Should preserve model aliases');
});
@@ -596,9 +599,9 @@ auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
originalCcsHome = process.env.CCS_HOME;
process.env.CCS_HOME = testDir;
delete require.cache[require.resolve('../../../dist/cliproxy/config-generator')];
delete require.cache[require.resolve('../../../dist/utils/config-manager')];
const configGenerator = require('../../../dist/cliproxy/config-generator');
delete require.cache[require.resolve('../../../../dist/cliproxy/config-generator')];
delete require.cache[require.resolve('../../../../dist/utils/config-manager')];
const configGenerator = require('../../../../dist/cliproxy/config-generator');
regenerateConfig = configGenerator.regenerateConfig;
});
@@ -2,10 +2,7 @@ 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 {
ensureProviderSettings,
getEffectiveEnvVars,
} from '../../../src/cliproxy/config/env-builder';
import { ensureProviderSettings, getEffectiveEnvVars } from '../env-builder';
interface EnvSettings {
ANTHROPIC_BASE_URL: string;
@@ -12,8 +12,8 @@
*/
import { describe, it, expect } from 'bun:test';
import { validatePort, CLIPROXY_DEFAULT_PORT } from '../../../src/cliproxy/config-generator';
import { resolveProxyConfig } from '../../../src/cliproxy/proxy-config-resolver';
import { validatePort, CLIPROXY_DEFAULT_PORT } from '../config-generator';
import { resolveProxyConfig } from '../../proxy/proxy-config-resolver';
describe('Port Validation', () => {
describe('validatePort()', () => {
@@ -9,7 +9,7 @@
import * as fs from 'fs';
import * as path from 'path';
import type { CLIProxyProvider, ProviderModelMapping } from './types';
import type { CLIProxyProvider, ProviderModelMapping } from '../types';
/** Base settings file structure */
interface BaseSettings {
@@ -33,7 +33,7 @@ const configCache: Map<CLIProxyProvider, BaseSettings> = new Map();
function getBaseConfigPath(provider: CLIProxyProvider): string {
// __dirname points to dist/cliproxy at runtime
// Config files are at package root: ../config/
const configDir = path.join(__dirname, '..', '..', 'config');
const configDir = path.join(__dirname, '..', '..', '..', 'config');
return path.join(configDir, `base-${provider}.settings.json`);
}
@@ -3,10 +3,10 @@
* Used by API routes, service layer, and config loader to avoid contract drift.
*/
import { CLIPROXY_SUPPORTED_PROVIDERS } from '../config/unified-config-types';
import type { CompositeTierConfig } from '../config/unified-config-types';
import type { CLIProxyProvider } from './types';
import { getDeniedModelIdReasonForProvider } from './model-id-normalizer';
import { CLIPROXY_SUPPORTED_PROVIDERS } from '../../config/unified-config-types';
import type { CompositeTierConfig } from '../../config/unified-config-types';
import type { CLIProxyProvider } from '../types';
import { getDeniedModelIdReasonForProvider } from '../ai-providers/model-id-normalizer';
export const VALID_COMPOSITE_TIERS = ['opus', 'sonnet', 'haiku'] as const;
export type CompositeTierName = (typeof VALID_COMPOSITE_TIERS)[number];
@@ -13,4 +13,4 @@
*/
// Re-export all modules for backwards compatibility
export * from './config';
export * from '.';
+3 -3
View File
@@ -6,9 +6,9 @@
import * as fs from 'fs';
import * as path from 'path';
import type { CLIProxyProvider, ProviderModelMapping } from '../types';
import { getModelMappingFromConfig, getEnvVarsFromConfig } from '../base-config-loader';
import { getModelMappingFromConfig, getEnvVarsFromConfig } from '../config/base-config-loader';
import { getGlobalEnvConfig } from '../../config/unified-config-loader';
import { getEffectiveApiKey } from '../auth-token-manager';
import { getEffectiveApiKey } from '../auth/auth-token-manager';
import { expandPath } from '../../utils/helpers';
import { warn } from '../../utils/ui';
import type { CompositeTierConfig } from '../../config/unified-config-types';
@@ -30,7 +30,7 @@ import {
normalizeModelEnvVarsForProvider,
normalizeIFlowLegacyModelAliases,
normalizeModelIdForProvider,
} from '../model-id-normalizer';
} from '../ai-providers/model-id-normalizer';
/** Settings file structure for user overrides */
interface ProviderSettings {

Some files were not shown because too many files have changed in this diff Show More