diff --git a/src/cliproxy/routing/__tests__/routing-strategy.test.ts b/src/cliproxy/routing/__tests__/routing-strategy.test.ts index 3005620f..271496e0 100644 --- a/src/cliproxy/routing/__tests__/routing-strategy.test.ts +++ b/src/cliproxy/routing/__tests__/routing-strategy.test.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; - +import type { CLIProxyBackend } from '../../types'; describe('cliproxy routing strategy service', () => { let tempHome = ''; let scopedConfigDir = ''; @@ -15,6 +15,10 @@ describe('cliproxy routing strategy service', () => { protocol: 'http' as const, isRemote: false, }; + let binaryBackend: CLIProxyBackend = 'original'; + let binaryVersion = '7.2.127-7'; + let installedVersionQueriedBackend: CLIProxyBackend | undefined = undefined; + let loggedWarnings: Array<{ event: string; message: string; context?: Record }> = []; let responseFactory: (() => Promise) | null = null; beforeEach(async () => { @@ -28,6 +32,10 @@ describe('cliproxy routing strategy service', () => { }; responseFactory = null; originalCcsDir = process.env.CCS_DIR; + binaryBackend = 'original'; + binaryVersion = '7.2.127-7'; + installedVersionQueriedBackend = undefined; + loggedWarnings = []; originalCcsHome = process.env.CCS_HOME; process.env.CCS_DIR = scopedConfigDir; process.env.CCS_HOME = tempHome; @@ -74,6 +82,26 @@ describe('cliproxy routing strategy service', () => { }, })); + mock.module('../../binary-manager', () => ({ + getConfiguredBackend: () => binaryBackend, + getInstalledCliproxyVersion: (backend?: CLIProxyBackend) => { + installedVersionQueriedBackend = backend; + return binaryVersion; + }, + })); + + mock.module('../../../services/logging', () => ({ + createLogger: () => ({ + debug: () => {}, + info: () => {}, + warn: (event: string, message: string, context?: Record) => { + loggedWarnings.push({ event, message, context }); + }, + error: () => {}, + child: () => ({ warn: () => {} }), + }), + })); + return import(`../routing-strategy?test=${Date.now()}-${Math.random()}`); } @@ -365,4 +393,65 @@ describe('cliproxy routing strategy service', () => { expect(result.message).not.toContain('Pool routing is active'); }); }); + + describe('pool routing version compatibility (#1726)', () => { + it('defines backend-specific minimum versions for pool routing', async () => { + const mod = await loadRoutingModule(); + expect(mod.POOL_ROUTING_MIN_VERSION).toEqual({ + original: '6.8.34', + plus: '6.8.34-0', + }); + }); + + it('evaluates isPoolRoutingSupported correctly across original and plus thresholds', async () => { + const mod = await loadRoutingModule(); + expect(mod.isPoolRoutingSupported('original', '6.8.33')).toBe(false); + expect(mod.isPoolRoutingSupported('original', '6.8.34')).toBe(true); + expect(mod.isPoolRoutingSupported('original', '6.9.0')).toBe(true); + + expect(mod.isPoolRoutingSupported('plus', '6.8.33-9')).toBe(false); + expect(mod.isPoolRoutingSupported('plus', '6.8.34-0')).toBe(true); + expect(mod.isPoolRoutingSupported('plus', '7.2.127-7')).toBe(true); + + expect(mod.isPoolRoutingSupported('original', '')).toBe(false); + expect(mod.isPoolRoutingSupported('plus', 'not-a-version')).toBe(false); + }); + + it('warns with backend-specific metadata when plus binary is below minimum', async () => { + await withScopedConfig(async () => { + binaryBackend = 'plus'; + binaryVersion = '6.8.33-9'; + loggedWarnings = []; + + const mod = await loadRoutingModule(); + const result = mod.enablePoolRouting(8317); + + expect(result.changed).toBe(true); + expect(installedVersionQueriedBackend).toBe('plus'); + expect(loggedWarnings).toHaveLength(1); + expect(loggedWarnings[0].event).toBe('pool_routing.binary_below_minimum'); + expect(loggedWarnings[0].message).toContain('CLIProxy Plus'); + expect(loggedWarnings[0].context).toEqual({ + backend: 'plus', + installedVersion: '6.8.33-9', + minimumVersion: '6.8.34-0', + }); + }); + }); + + it('does not warn when plus binary meets the minimum version', async () => { + await withScopedConfig(async () => { + binaryBackend = 'plus'; + binaryVersion = '6.8.34-0'; + loggedWarnings = []; + + const mod = await loadRoutingModule(); + const result = mod.enablePoolRouting(8317); + + expect(result.changed).toBe(true); + expect(installedVersionQueriedBackend).toBe('plus'); + expect(loggedWarnings).toHaveLength(0); + }); + }); + }); }); diff --git a/src/cliproxy/routing/routing-strategy.ts b/src/cliproxy/routing/routing-strategy.ts index d025e858..3533cef0 100644 --- a/src/cliproxy/routing/routing-strategy.ts +++ b/src/cliproxy/routing/routing-strategy.ts @@ -7,10 +7,13 @@ import { getCliproxyRoutingTarget, getRoutingErrorMessage, } from './routing-strategy-http'; -import type { CliproxyRoutingStrategy } from '../types'; +import type { CLIProxyBackend, CliproxyRoutingStrategy } from '../types'; import { loadOrCreateUnifiedConfig, mutateConfig } from '../../config/config-loader-facade'; -import { getInstalledCliproxyVersion } from '../binary-manager'; -import { compareVersions } from '../../utils/update-checker'; +import { getConfiguredBackend, getInstalledCliproxyVersion } from '../binary-manager'; +import { + type CLIProxyBackendMinVersions, + meetsBackendMinimumVersion, +} from '../binary/version-checker'; import { getConfigYamlPath } from '../../config/loader/io-locks'; import { createLogger } from '../../services/logging'; @@ -46,12 +49,18 @@ export const POOL_ROUTING_VERIFIED_PROVIDERS = new Set(['claude', 'agy']); * max-retry-credentials and the cooling flip. * Older binaries silently ignore unknown keys — pool rails would appear active * but have no effect. Warn the user at enable time if below this version. - * - * NOTE: Update this constant when upstream first ships these keys. - * Current best estimate based on spec; adjust after spike Test D confirms. */ -export const POOL_ROUTING_MIN_VERSION = '6.9.45'; +export const POOL_ROUTING_MIN_VERSION: CLIProxyBackendMinVersions = { + original: '6.8.34', + plus: '6.8.34-0', +}; +export function isPoolRoutingSupported( + backend: CLIProxyBackend, + installedVersion: string +): boolean { + return meetsBackendMinimumVersion(installedVersion, backend, POOL_ROUTING_MIN_VERSION); +} /** * Pool-active override warning text. When pool routing is enabled the generator * forces fill-first/affinity/cooling and ignores the stored strategy/affinity, so @@ -205,20 +214,21 @@ export function enablePoolRouting( const preservedExplicitSetting = hasExplicitRoutingStrategy() || hasExplicitSessionAffinity(); - // Spec step 3 / architecture: assert minimum CLIProxy version at enable time. - // Stale binaries silently ignore max-retry-credentials and the cooling flip, - // so pool rails would appear active but have no effect. Warn and proceed. try { - const installedVersion = getInstalledCliproxyVersion(); - if (compareVersions(installedVersion, POOL_ROUTING_MIN_VERSION) < 0) { + const backend = getConfiguredBackend(); + const installedVersion = getInstalledCliproxyVersion(backend); + if (!isPoolRoutingSupported(backend, installedVersion)) { + const minimumVersion = POOL_ROUTING_MIN_VERSION[backend]; + const backendLabel = backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy'; logger.warn( 'pool_routing.binary_below_minimum', - `CLIProxy v${installedVersion} is older than the pool routing minimum (v${POOL_ROUTING_MIN_VERSION}). ` + + `${backendLabel} v${installedVersion} is older than the pool routing minimum (v${minimumVersion}). ` + `The max-retry-credentials and cooling keys may be silently ignored by the running binary. ` + `Run 'ccs cliproxy --latest' to update CLIProxy, then restart with 'ccs cliproxy restart'.`, { + backend, installedVersion, - minimumVersion: POOL_ROUTING_MIN_VERSION, + minimumVersion, } ); }