Merge pull request #1677 from kaitranntt/kai/feat/1651-context-window

feat(cliproxy): expose model context windows
This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-07-29 13:57:05 -04:00
commit 6d754eff36
10 files changed
+315 -26

No files matched your search

+6 -6
View File
@@ -1,8 +1,8 @@
{
"scope": "src/**/*.{ts,tsx,js,jsx,mjs,cjs}",
"syncFs": {
"totalOccurrences": 2379,
"filesAffected": 255,
"totalOccurrences": 2382,
"filesAffected": 256,
"hotpathOccurrences": 1103,
"hotpathFilesAffected": 151,
"topHotpathFiles": [
@@ -741,6 +741,10 @@
"file": "src/cliproxy/auth/oauth-process.ts",
"loc": 1048
},
{
"file": "src/cliproxy/config/env-builder.ts",
"loc": 1045
},
{
"file": "src/web-server/routes/settings-routes.ts",
"loc": 1042
@@ -753,10 +757,6 @@
"file": "src/cliproxy/proxy/tool-sanitization-proxy.ts",
"loc": 1020
},
{
"file": "src/cliproxy/config/env-builder.ts",
"loc": 1017
},
{
"file": "src/commands/cliproxy/variant-subcommand.ts",
"loc": 997
+3 -3
View File
@@ -6,8 +6,8 @@ Scope: `src/**/*.{ts,tsx,js,jsx,mjs,cjs}`
| Metric | Value |
|---|---:|
| Sync fs occurrences (all) | 2379 |
| Sync fs files affected (all) | 255 |
| Sync fs occurrences (all) | 2382 |
| Sync fs files affected (all) | 256 |
| Sync fs occurrences (runtime hotpaths) | 1103 |
| Sync fs files affected (runtime hotpaths) | 151 |
| Legacy shim markers | 456 |
@@ -95,10 +95,10 @@ Scope: `src/**/*.{ts,tsx,js,jsx,mjs,cjs}`
| `src/cursor/cursor-executor.ts` | 1234 |
| `src/web-server/model-pricing.ts` | 1127 |
| `src/cliproxy/auth/oauth-process.ts` | 1048 |
| `src/cliproxy/config/env-builder.ts` | 1045 |
| `src/web-server/routes/settings-routes.ts` | 1042 |
| `src/cliproxy/config/generator.ts` | 1034 |
| `src/cliproxy/proxy/tool-sanitization-proxy.ts` | 1020 |
| `src/cliproxy/config/env-builder.ts` | 1017 |
| `src/commands/cliproxy/variant-subcommand.ts` | 997 |
| `src/cliproxy/quota/quota-manager.ts` | 954 |
| `src/web-server/services/codex-dashboard-service.ts` | 940 |
@@ -75,6 +75,18 @@ describe('model-catalog compatibility lookups', () => {
expect(catalog?.models[0]?.extendedContext).toBeUndefined();
});
it('maps positive integer live context length into contextWindow metadata', () => {
const catalog = mergeCatalog('xai', [
{
id: 'grok-4.3',
display_name: 'Grok 4.3 Live',
context_length: 1_000_000,
},
]);
expect(catalog?.models[0]?.contextWindow).toBe(1_000_000);
});
it('does not re-add stale static-only models when live catalog data is present', () => {
const catalog = mergeCatalog('gemini', [
{
@@ -0,0 +1,180 @@
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 { invalidateConfigCache } from '../../../config/config-loader-facade';
import { clearConfigCache } from '../base-config-loader';
import { applyClaudeAutoCompactWindow } from '../env-builder';
import { buildClaudeEnvironment } from '../../executor/env-resolver';
describe('applyClaudeAutoCompactWindow', () => {
it('injects the catalog window for the effective model after suffix normalization', () => {
const env = applyClaudeAutoCompactWindow(
{
ANTHROPIC_MODEL: 'gpt-5.6-sol-xhigh[1m]',
},
'codex'
);
expect(env.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBe('372000');
});
it('uses the selected provider when the same model family exists in multiple catalogs', () => {
const env = applyClaudeAutoCompactWindow(
{
ANTHROPIC_MODEL: 'claude-opus-4-6-thinking',
},
'agy'
);
expect(env.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBe('200000');
});
it('does not inject a value for unknown or custom models', () => {
const env = applyClaudeAutoCompactWindow(
{
ANTHROPIC_MODEL: 'custom-model-with-unknown-window',
},
'codex'
);
expect('CLAUDE_CODE_AUTO_COMPACT_WINDOW' in env).toBe(false);
});
it('preserves an explicit user value', () => {
const env = applyClaudeAutoCompactWindow(
{
ANTHROPIC_MODEL: 'gpt-5.6-sol',
CLAUDE_CODE_AUTO_COMPACT_WINDOW: '123456',
},
'codex'
);
expect(env.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBe('123456');
});
it('treats an explicit empty value as authoritative', () => {
const env = applyClaudeAutoCompactWindow(
{
ANTHROPIC_MODEL: 'gpt-5.6-sol',
CLAUDE_CODE_AUTO_COMPACT_WINDOW: '',
},
'codex'
);
expect(env.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBe('');
});
it('does not mistake a differently cased variable for the Claude Code key', () => {
const env = applyClaudeAutoCompactWindow(
{
ANTHROPIC_MODEL: 'gpt-5.6-sol',
claude_code_auto_compact_window: '123456',
},
'codex'
);
expect(env.claude_code_auto_compact_window).toBe('123456');
expect(env.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBe('372000');
});
});
describe('Claude launch auto-compact integration', () => {
let tempHome: string;
let originalCcsHome: string | undefined;
let originalAutoCompactWindow: string | undefined;
beforeEach(() => {
originalCcsHome = process.env.CCS_HOME;
originalAutoCompactWindow = process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW;
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-auto-compact-'));
process.env.CCS_HOME = tempHome;
delete process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW;
invalidateConfigCache();
clearConfigCache();
});
afterEach(() => {
if (originalCcsHome === undefined) delete process.env.CCS_HOME;
else process.env.CCS_HOME = originalCcsHome;
if (originalAutoCompactWindow === undefined) {
delete process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW;
} else {
process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW = originalAutoCompactWindow;
}
invalidateConfigCache();
clearConfigCache();
fs.rmSync(tempHome, { recursive: true, force: true });
});
it('injects the catalog window for the provider default model on Claude launches', () => {
const env = buildClaudeEnvironment({
provider: 'codex',
useRemoteProxy: false,
localPort: 8317,
verbose: false,
});
expect(env.ANTHROPIC_MODEL).toBe('gpt-5.4(high)');
expect(env.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBe('1050000');
});
it('preserves an inherited user value on Claude launches', () => {
process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW = '';
const env = buildClaudeEnvironment({
provider: 'codex',
useRemoteProxy: false,
localPort: 8317,
verbose: false,
});
expect(env.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBe('');
});
it('uses the composite default tier provider for model lookup', () => {
const env = buildClaudeEnvironment({
provider: 'codex',
useRemoteProxy: false,
localPort: 8317,
verbose: false,
isComposite: true,
compositeTiers: {
opus: { provider: 'agy', model: 'claude-opus-4-6-thinking' },
sonnet: { provider: 'codex', model: 'gpt-5.6-sol' },
haiku: { provider: 'kimi', model: 'kimi-k2' },
},
compositeDefaultTier: 'opus',
});
expect(env.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBe('200000');
});
it('preserves an explicit empty value from a custom settings file', () => {
const settingsPath = path.join(tempHome, 'codex-custom.settings.json');
fs.writeFileSync(
settingsPath,
JSON.stringify({
env: {
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317',
ANTHROPIC_AUTH_TOKEN: 'test-token',
ANTHROPIC_MODEL: 'gpt-5.6-sol',
CLAUDE_CODE_AUTO_COMPACT_WINDOW: '',
},
})
);
const env = buildClaudeEnvironment({
provider: 'codex',
useRemoteProxy: false,
localPort: 8317,
customSettingsPath: settingsPath,
verbose: false,
});
expect(env.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBe('');
});
});
+28
View File
@@ -38,6 +38,7 @@ import {
} from '../../config/config-loader-facade';
import { buildCliproxyProviderPath, buildLocalProviderBaseUrl } from './provider-route';
import { ConfigError } from '../../errors/error-types';
import { findModel } from '../model-catalog';
export { buildCliproxyProviderPath, buildLocalProviderBaseUrl } from './provider-route';
@@ -68,11 +69,38 @@ const CURSOR_LEGACY_ENV_OVERRIDE_KEYS = new Set([
'ANTHROPIC_AUTH_TOKEN',
'ANTHROPIC_API_KEY',
]);
const CLAUDE_AUTO_COMPACT_WINDOW_ENV = 'CLAUDE_CODE_AUTO_COMPACT_WINDOW';
function isObjectRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
/**
* Add Claude Code's auto-compact window for a known effective model.
* Existing env keys win by presence, including an explicit empty value.
*/
export function applyClaudeAutoCompactWindow(
envVars: NodeJS.ProcessEnv,
provider: CLIProxyProvider
): NodeJS.ProcessEnv {
if (Object.prototype.hasOwnProperty.call(envVars, CLAUDE_AUTO_COMPACT_WINDOW_ENV)) {
return envVars;
}
const modelId = envVars.ANTHROPIC_MODEL;
if (typeof modelId !== 'string' || modelId.trim().length === 0) return envVars;
const contextWindow = findModel(provider, modelId)?.contextWindow;
if (typeof contextWindow !== 'number' || !Number.isInteger(contextWindow) || contextWindow <= 0) {
return envVars;
}
return {
...envVars,
[CLAUDE_AUTO_COMPACT_WINDOW_ENV]: String(contextWindow),
};
}
/**
* Migrate deprecated gemini-claude-* model names to upstream claude-* names in a settings file.
* CLIProxyAPI registry no longer recognizes the gemini-claude-* prefix convention.
+11 -1
View File
@@ -15,6 +15,7 @@ import {
getRemoteEnvVars,
getCompositeEnvVars,
applyThinkingConfig,
applyClaudeAutoCompactWindow,
} from '../config/config-generator';
import { applyExtendedContextConfig } from '../config/extended-context-config';
import { CLIProxyProvider } from '../types';
@@ -414,8 +415,17 @@ export function buildClaudeEnvironment(config: ProxyChainConfig): Record<string,
CCS_PROFILE_TYPE: 'cliproxy', // Signal to WebSearch hook this is a third-party provider
};
const effectiveProvider =
isComposite && compositeTiers && compositeDefaultTier
? compositeTiers[compositeDefaultTier].provider
: provider;
const autoCompactEnv = applyClaudeAutoCompactWindow(
stripClaudeCodeEnv(mergedEnv),
effectiveProvider
);
return Object.fromEntries(
Object.entries(stripClaudeCodeEnv(mergedEnv)).filter(([, v]) => v !== undefined)
Object.entries(autoCompactEnv).filter(([, v]) => v !== undefined)
) as Record<string, string>;
}
+54
View File
@@ -52,6 +52,8 @@ export interface ModelEntry {
tier?: 'free' | 'pro' | 'ultra';
/** Optional description for the model */
description?: string;
/** Total context window in tokens when authoritatively known */
contextWindow?: number;
/** Model has known issues - show warning when selected */
broken?: boolean;
/** Issue URL for broken models */
@@ -95,6 +97,7 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
id: 'claude-opus-4-6-thinking',
name: 'Claude Opus 4.6 Thinking',
description: 'Latest flagship, extended thinking',
contextWindow: 200000,
nativeImageInput: true,
thinking: {
type: 'budget',
@@ -111,6 +114,7 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
id: 'claude-sonnet-4-6',
name: 'Claude Sonnet 4.6',
description: 'Latest Sonnet with thinking budget support',
contextWindow: 200000,
nativeImageInput: true,
thinking: {
type: 'budget',
@@ -194,6 +198,7 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
id: 'gpt-5.6-sol',
name: 'GPT-5.6 Sol',
description: 'Latest frontier agentic coding model.',
contextWindow: 372000,
thinking: {
type: 'levels',
levels: ['low', 'medium', 'high', 'xhigh'],
@@ -206,6 +211,7 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
id: 'gpt-5.6-terra',
name: 'GPT-5.6 Terra',
description: 'Balanced agentic coding model for everyday work.',
contextWindow: 372000,
thinking: {
type: 'levels',
levels: ['low', 'medium', 'high', 'xhigh'],
@@ -218,6 +224,7 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
id: 'gpt-5.6-luna',
name: 'GPT-5.6 Luna',
description: 'Fast and affordable agentic coding model.',
contextWindow: 372000,
thinking: {
type: 'levels',
levels: ['low', 'medium', 'high', 'xhigh'],
@@ -232,6 +239,7 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
tier: 'pro',
description:
'Newest Codex-released GPT-5 family model; falls back to GPT-5.4 on free plans',
contextWindow: 272000,
thinking: {
type: 'levels',
levels: ['low', 'medium', 'high', 'xhigh'],
@@ -244,6 +252,7 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
id: 'gpt-5.4',
name: 'GPT-5.4',
description: 'Recommended Codex default for most coding and agentic tasks',
contextWindow: 1050000,
thinking: {
type: 'levels',
levels: ['low', 'medium', 'high', 'xhigh'],
@@ -256,6 +265,7 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
id: 'gpt-5.4-mini',
name: 'GPT-5.4 Mini',
description: 'Fast, lower-cost Codex option for lighter tasks and haiku-tier routing',
contextWindow: 400000,
thinking: {
type: 'levels',
levels: ['low', 'medium', 'high'],
@@ -281,6 +291,7 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
tier: 'pro',
description:
'Research preview model for ChatGPT Pro subscribers, optimized for near-instant coding iteration',
contextWindow: 128000,
thinking: {
type: 'levels',
levels: ['low', 'medium', 'high', 'xhigh'],
@@ -310,11 +321,13 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
id: 'grok-build-0.1',
name: 'Grok Build 0.1',
description: 'Fast coding model for agentic software engineering workflows',
contextWindow: 256000,
},
{
id: 'grok-4.5',
name: 'Grok 4.5',
description: 'Frontier model for coding, engineering, and agentic workflows',
contextWindow: 500000,
thinking: {
type: 'levels',
levels: ['low', 'medium', 'high'],
@@ -325,6 +338,7 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
id: 'grok-4.3',
name: 'Grok 4.3',
description: 'General-purpose Grok model with a one-million-token context window',
contextWindow: 1000000,
thinking: {
type: 'levels',
levels: ['none', 'low', 'medium', 'high'],
@@ -335,34 +349,40 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
id: 'grok-4.20-0309-reasoning',
name: 'Grok 4.20 0309 Reasoning',
description: 'Reasoning model with a two-million-token context window',
contextWindow: 2000000,
},
{
id: 'grok-4.20-0309-non-reasoning',
name: 'Grok 4.20 0309 Non Reasoning',
description: 'Non-reasoning model with a two-million-token context window',
contextWindow: 2000000,
},
{
id: 'grok-4.20-multi-agent-0309',
name: 'Grok 4.20 Multi Agent 0309',
description: 'Multi-agent model with a two-million-token context window',
contextWindow: 2000000,
thinking: { type: 'levels', levels: ['low', 'medium', 'high'] },
},
{
id: 'grok-3-mini',
name: 'Grok 3 Mini',
description: 'Compact reasoning model',
contextWindow: 131072,
thinking: { type: 'levels', levels: ['low', 'medium', 'high'] },
},
{
id: 'grok-3-mini-fast',
name: 'Grok 3 Mini Fast',
description: 'Faster compact reasoning model',
contextWindow: 131072,
thinking: { type: 'levels', levels: ['low', 'medium', 'high'] },
},
{
id: 'grok-composer-2.5-fast',
name: 'Grok Composer 2.5 Fast',
description: 'Fast Composer model for lightweight coding tasks',
contextWindow: 200000,
},
],
},
@@ -375,56 +395,67 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
id: 'qoder/auto',
name: 'Qoder Auto',
description: 'Auto selects the best Qoder model for your prompt',
contextWindow: 180000,
},
{
id: 'qoder/ultimate',
name: 'Qoder Ultimate',
description: 'Highest quality Qoder tier',
contextWindow: 180000,
},
{
id: 'qoder/performance',
name: 'Qoder Performance',
description: 'Balanced quality and speed',
contextWindow: 272000,
},
{
id: 'qoder/efficient',
name: 'Qoder Efficient',
description: 'Cost-efficient Qoder tier',
contextWindow: 180000,
},
{
id: 'qoder/lite',
name: 'Qoder Lite',
description: 'Fastest and most affordable Qoder tier',
contextWindow: 180000,
},
{
id: 'qoder/qmodel',
name: 'Qwen 3.6 Plus (via Qoder)',
description: 'Qwen 3.6 Plus frontier model',
contextWindow: 180000,
},
{
id: 'qoder/dmodel',
name: 'DeepSeek V4 Pro (via Qoder)',
description: 'DeepSeek V4 Pro frontier model',
contextWindow: 180000,
},
{
id: 'qoder/dfmodel',
name: 'DeepSeek V4 Flash (via Qoder)',
description: 'DeepSeek V4 Flash frontier model',
contextWindow: 180000,
},
{
id: 'qoder/gm51model',
name: 'GLM 5.1 (via Qoder)',
description: 'GLM 5.1 frontier model',
contextWindow: 180000,
},
{
id: 'qoder/kmodel',
name: 'Kimi K2.6 (via Qoder)',
description: 'Kimi K2.6 frontier model',
contextWindow: 256000,
},
{
id: 'qoder/mmodel',
name: 'MiniMax M2.7 (via Qoder)',
description: 'MiniMax M2.7 frontier model',
contextWindow: 180000,
},
],
},
@@ -437,6 +468,7 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
id: 'kimi-k2.5',
name: 'Kimi K2.5',
description: 'Latest multimodal model (262K context)',
contextWindow: 262144,
nativeImageInput: true,
thinking: {
type: 'budget',
@@ -450,6 +482,7 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
id: 'kimi-k2-thinking',
name: 'Kimi K2 Thinking',
description: 'Extended reasoning model',
contextWindow: 131072,
thinking: {
type: 'budget',
min: 1024,
@@ -462,6 +495,7 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
id: 'kimi-k2',
name: 'Kimi K2',
description: 'Flagship coding model',
contextWindow: 131072,
},
],
},
@@ -474,6 +508,7 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
id: 'claude-sonnet-5',
name: 'Claude Sonnet 5',
description: 'Latest Sonnet model',
contextWindow: 1000000,
nativeImageInput: true,
// Sonnet 5 uses adaptive thinking; manual budget_tokens is rejected with 400.
thinking: {
@@ -488,6 +523,7 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
id: 'claude-fable-5',
name: 'Claude Fable 5',
description: 'Most powerful model',
contextWindow: 1000000,
nativeImageInput: true,
// New tier above Opus. Same adaptive-thinking surface as Opus 4.8:
// Anthropic accepts only effort levels; manual budget_tokens is rejected with 400.
@@ -520,6 +556,7 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
id: 'claude-opus-4-8',
name: 'Claude Opus 4.8',
description: 'Latest flagship model',
contextWindow: 1000000,
nativeImageInput: true,
// Mirrors 4.7: Anthropic accepts only adaptive thinking levels on the
// current Opus generation; manual budget_tokens is rejected with 400.
@@ -535,6 +572,7 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
id: 'claude-opus-4-7',
name: 'Claude Opus 4.7',
description: 'Previous flagship model',
contextWindow: 1000000,
nativeImageInput: true,
// Opus 4.7 only supports adaptive thinking on the Anthropic API; manual
// thinking.type: "enabled" with budget_tokens is rejected with 400.
@@ -552,6 +590,7 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
id: 'claude-opus-4-6',
name: 'Claude Opus 4.6',
description: 'Older flagship model',
contextWindow: 1000000,
nativeImageInput: true,
thinking: {
type: 'budget',
@@ -566,6 +605,7 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
id: 'claude-sonnet-4-6',
name: 'Claude Sonnet 4.6',
description: 'Balanced performance and speed',
contextWindow: 200000,
nativeImageInput: true,
thinking: {
type: 'budget',
@@ -580,6 +620,7 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
id: 'claude-opus-4-5-20251101',
name: 'Claude Opus 4.5',
description: 'Most capable Claude model',
contextWindow: 200000,
nativeImageInput: true,
thinking: {
type: 'budget',
@@ -594,6 +635,7 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
id: 'claude-sonnet-4-5-20250929',
name: 'Claude Sonnet 4.5',
description: 'Balanced performance and speed',
contextWindow: 200000,
nativeImageInput: true,
thinking: {
type: 'budget',
@@ -608,6 +650,7 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
id: 'claude-sonnet-4-20250514',
name: 'Claude Sonnet 4',
description: 'Previous generation Sonnet',
contextWindow: 200000,
nativeImageInput: true,
thinking: {
type: 'budget',
@@ -622,6 +665,7 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
id: 'claude-haiku-4-5-20251001',
name: 'Claude Haiku 4.5',
description: 'Fast and efficient',
contextWindow: 200000,
nativeImageInput: true,
thinking: { type: 'none' },
},
@@ -675,6 +719,16 @@ export function findModel(provider: CLIProxyProvider, modelId: string): ModelEnt
.trim()
.toLowerCase();
const lookupCandidates = new Set([normalizedId, providerNormalizedId]);
if (provider === 'codex') {
for (const candidate of [...lookupCandidates]) {
const tuningMatch = candidate.match(
/^(.*?)(?:-(?:minimal|low|medium|high|xhigh)(?:-fast)?|-fast(?:-(?:minimal|low|medium|high|xhigh))?)$/i
);
if (tuningMatch?.[1]) {
lookupCandidates.add(tuningMatch[1].trim());
}
}
}
if (isAntigravityProvider(provider)) {
const migratedRaw = migrateDeniedAntigravityModelAliases(normalizedId).trim().toLowerCase();
const migratedProvider = migrateDeniedAntigravityModelAliases(providerNormalizedId)
+8
View File
@@ -221,6 +221,13 @@ function mapRemoteToModelEntry(provider: CLIProxyProvider, remote: RemoteModelIn
name: remote.display_name || remote.id,
};
if (remote.description) entry.description = remote.description;
if (
typeof remote.context_length === 'number' &&
Number.isInteger(remote.context_length) &&
remote.context_length > 0
) {
entry.contextWindow = remote.context_length;
}
// xAI context length is inherent to the model ID; its API does not accept
// Claude's [1m] model suffix.
if (provider !== 'xai' && remote.context_length && remote.context_length >= 1_000_000) {
@@ -286,6 +293,7 @@ export function mergeCatalog(
issueUrl: staticEntry.issueUrl,
deprecated: staticEntry.deprecated,
deprecationReason: staticEntry.deprecationReason,
contextWindow: remoteEntry.contextWindow ?? staticEntry.contextWindow,
});
} else {
mergedModels.push(remoteEntry);
@@ -181,6 +181,7 @@ interface CatalogJsonModel {
name: string;
tier?: 'free' | 'pro' | 'ultra';
description?: string;
contextWindow?: number;
deprecated?: boolean;
deprecationReason?: string;
broken?: boolean;
@@ -206,6 +207,7 @@ export function handleCatalogJson(): void {
const entry: CatalogJsonModel = { id: m.id, name: m.name };
if (m.tier !== undefined) entry.tier = m.tier;
if (m.description !== undefined) entry.description = m.description;
if (m.contextWindow !== undefined) entry.contextWindow = m.contextWindow;
if (m.deprecated !== undefined) entry.deprecated = m.deprecated;
if (m.deprecationReason !== undefined) entry.deprecationReason = m.deprecationReason;
if (m.broken !== undefined) entry.broken = m.broken;
@@ -42,10 +42,7 @@ describe('cliproxy catalog --json output', () => {
it('includes metadata fields when present on model entries', () => {
handleCatalogJson();
const parsed = JSON.parse(capturedOutput[0]) as Record<
string,
Array<Record<string, unknown>>
>;
const parsed = JSON.parse(capturedOutput[0]) as Record<string, Array<Record<string, unknown>>>;
const allModels = Object.values(parsed).flat();
// At least some models in the static catalog have tier set
@@ -56,15 +53,19 @@ describe('cliproxy catalog --json output', () => {
for (const model of withTier) {
expect(['free', 'pro', 'ultra']).toContain(model.tier);
}
const withContextWindow = allModels.filter((m) => m.contextWindow !== undefined);
expect(withContextWindow.length).toBeGreaterThan(0);
for (const model of withContextWindow) {
expect(Number.isInteger(model.contextWindow)).toBe(true);
expect(model.contextWindow as number).toBeGreaterThan(0);
}
});
it('omits undefined optional fields instead of including nulls', () => {
handleCatalogJson();
const parsed = JSON.parse(capturedOutput[0]) as Record<
string,
Array<Record<string, unknown>>
>;
const parsed = JSON.parse(capturedOutput[0]) as Record<string, Array<Record<string, unknown>>>;
const allModels = Object.values(parsed).flat();
for (const model of allModels) {
@@ -78,10 +79,7 @@ describe('cliproxy catalog --json output', () => {
it('includes explicit false boolean values in output', () => {
handleCatalogJson();
const parsed = JSON.parse(capturedOutput[0]) as Record<
string,
Array<Record<string, unknown>>
>;
const parsed = JSON.parse(capturedOutput[0]) as Record<string, Array<Record<string, unknown>>>;
const allModels = Object.values(parsed).flat();
// Static catalog has models with extendedContext: false
@@ -92,10 +90,7 @@ describe('cliproxy catalog --json output', () => {
it('includes thinking configuration when present on models', () => {
handleCatalogJson();
const parsed = JSON.parse(capturedOutput[0]) as Record<
string,
Array<Record<string, unknown>>
>;
const parsed = JSON.parse(capturedOutput[0]) as Record<string, Array<Record<string, unknown>>>;
const allModels = Object.values(parsed).flat();
// Static catalog has thinking models (e.g. Claude Opus 4.6 Thinking)