mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 03:13:12 +00:00
fix(cliproxy): normalize stale iflow model aliases
This commit is contained in:
1 parent
34971c8be4
commit
546c4ee4ee
11 files changed
+368
-61
No files matched your search
@@ -25,6 +25,7 @@ import {
|
|||||||
MODEL_ENV_VAR_KEYS,
|
MODEL_ENV_VAR_KEYS,
|
||||||
migrateDeniedAntigravityModelAliases,
|
migrateDeniedAntigravityModelAliases,
|
||||||
normalizeModelEnvVarsForProvider,
|
normalizeModelEnvVarsForProvider,
|
||||||
|
normalizeIFlowLegacyModelAliases,
|
||||||
normalizeModelIdForProvider,
|
normalizeModelIdForProvider,
|
||||||
} from '../model-id-normalizer';
|
} from '../model-id-normalizer';
|
||||||
|
|
||||||
@@ -39,8 +40,6 @@ const DEPRECATED_MODEL_PREFIX = 'gemini-claude-';
|
|||||||
/** Replacement prefix matching actual upstream model names */
|
/** Replacement prefix matching actual upstream model names */
|
||||||
const UPSTREAM_MODEL_PREFIX = 'claude-';
|
const UPSTREAM_MODEL_PREFIX = 'claude-';
|
||||||
const CODEX_EFFORT_SUFFIX_REGEX = /-(xhigh|high|medium)$/i;
|
const CODEX_EFFORT_SUFFIX_REGEX = /-(xhigh|high|medium)$/i;
|
||||||
const IFLOW_PLACEHOLDER_MODEL = 'iflow-default';
|
|
||||||
const IFLOW_DEFAULT_MODEL = 'qwen3-coder-plus';
|
|
||||||
const PRESET_MODEL_KEYS = ['default', 'opus', 'sonnet', 'haiku'] as const;
|
const PRESET_MODEL_KEYS = ['default', 'opus', 'sonnet', 'haiku'] as const;
|
||||||
const REQUIRED_PROVIDER_ENV_KEYS = [
|
const REQUIRED_PROVIDER_ENV_KEYS = [
|
||||||
'ANTHROPIC_BASE_URL',
|
'ANTHROPIC_BASE_URL',
|
||||||
@@ -172,8 +171,8 @@ function migrateCodexEffortSuffixes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate legacy iFlow placeholder model IDs to a real default model.
|
* Migrate legacy iFlow model IDs to current upstream model IDs.
|
||||||
* Example: iflow-default -> qwen3-coder-plus
|
* Example: iflow-default -> qwen3-coder-plus, kimi-k2.5 -> kimi-k2
|
||||||
*/
|
*/
|
||||||
function migrateIFlowPlaceholderModel(
|
function migrateIFlowPlaceholderModel(
|
||||||
settingsPath: string,
|
settingsPath: string,
|
||||||
@@ -184,14 +183,13 @@ function migrateIFlowPlaceholderModel(
|
|||||||
if (!settings.env || typeof settings.env !== 'object') return false;
|
if (!settings.env || typeof settings.env !== 'object') return false;
|
||||||
|
|
||||||
let migrated = false;
|
let migrated = false;
|
||||||
const normalize = (value: string): string => value.trim().toLowerCase();
|
const replaceLegacyIFlowModel = (value: string): string =>
|
||||||
const replaceIfPlaceholder = (value: string): string =>
|
normalizeIFlowLegacyModelAliases(value);
|
||||||
normalize(value) === IFLOW_PLACEHOLDER_MODEL ? IFLOW_DEFAULT_MODEL : value;
|
|
||||||
|
|
||||||
for (const key of MODEL_ENV_VAR_KEYS) {
|
for (const key of MODEL_ENV_VAR_KEYS) {
|
||||||
const value = settings.env[key];
|
const value = settings.env[key];
|
||||||
if (typeof value !== 'string') continue;
|
if (typeof value !== 'string') continue;
|
||||||
const replaced = replaceIfPlaceholder(value);
|
const replaced = replaceLegacyIFlowModel(value);
|
||||||
if (replaced !== value) {
|
if (replaced !== value) {
|
||||||
settings.env[key] = replaced;
|
settings.env[key] = replaced;
|
||||||
migrated = true;
|
migrated = true;
|
||||||
@@ -206,7 +204,7 @@ function migrateIFlowPlaceholderModel(
|
|||||||
for (const key of PRESET_MODEL_KEYS) {
|
for (const key of PRESET_MODEL_KEYS) {
|
||||||
const value = presetRecord[key];
|
const value = presetRecord[key];
|
||||||
if (typeof value !== 'string') continue;
|
if (typeof value !== 'string') continue;
|
||||||
const replaced = replaceIfPlaceholder(value);
|
const replaced = replaceLegacyIFlowModel(value);
|
||||||
if (replaced !== value) {
|
if (replaced !== value) {
|
||||||
presetRecord[key] = replaced;
|
presetRecord[key] = replaced;
|
||||||
migrated = true;
|
migrated = true;
|
||||||
|
|||||||
@@ -31,11 +31,34 @@ const DENIED_ANTIGRAVITY_SONNET_45_REGEX =
|
|||||||
/claude-sonnet-4(?:[.-])5(?:-thinking)?(?=(?:$|[^a-z0-9]))/gi;
|
/claude-sonnet-4(?:[.-])5(?:-thinking)?(?=(?:$|[^a-z0-9]))/gi;
|
||||||
const CANONICAL_ANTIGRAVITY_OPUS_46_MODEL = 'claude-opus-4-6-thinking';
|
const CANONICAL_ANTIGRAVITY_OPUS_46_MODEL = 'claude-opus-4-6-thinking';
|
||||||
const CODEX_EFFORT_SUFFIX_REGEX = /-(xhigh|high|medium)$/i;
|
const CODEX_EFFORT_SUFFIX_REGEX = /-(xhigh|high|medium)$/i;
|
||||||
|
const IFLOW_LEGACY_MODEL_ALIASES: Readonly<Record<string, string>> = Object.freeze({
|
||||||
|
'iflow-default': 'qwen3-coder-plus',
|
||||||
|
'kimi-k2.5': 'kimi-k2',
|
||||||
|
'deepseek-v3.2-chat': 'deepseek-v3.2',
|
||||||
|
'glm-4.7': 'glm-4.6',
|
||||||
|
'minimax-m2.5': 'qwen3-coder-plus',
|
||||||
|
});
|
||||||
|
|
||||||
function trimModelId(model: string): string {
|
function trimModelId(model: string): string {
|
||||||
return model.trim();
|
return model.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function splitBaseModelAndSuffix(model: string): { baseModel: string; suffix: string } {
|
||||||
|
const openParenIndex = model.indexOf('(');
|
||||||
|
const openBracketIndex = model.indexOf('[');
|
||||||
|
const suffixIndexCandidates = [openParenIndex, openBracketIndex].filter((index) => index >= 0);
|
||||||
|
|
||||||
|
if (suffixIndexCandidates.length === 0) {
|
||||||
|
return { baseModel: model, suffix: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const suffixStart = Math.min(...suffixIndexCandidates);
|
||||||
|
return {
|
||||||
|
baseModel: model.slice(0, suffixStart),
|
||||||
|
suffix: model.slice(suffixStart),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract provider segment from `/api/provider/{provider}` request paths.
|
* Extract provider segment from `/api/provider/{provider}` request paths.
|
||||||
*
|
*
|
||||||
@@ -62,11 +85,31 @@ export function isCodexProvider(provider: ProviderLike): boolean {
|
|||||||
return provider.trim().toLowerCase() === 'codex';
|
return provider.trim().toLowerCase() === 'codex';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether provider maps to iFlow model alias canonicalization rules. */
|
||||||
|
export function isIFlowProvider(provider: ProviderLike): boolean {
|
||||||
|
if (typeof provider !== 'string') return false;
|
||||||
|
return provider.trim().toLowerCase() === 'iflow';
|
||||||
|
}
|
||||||
|
|
||||||
/** Normalize Codex effort-suffixed IDs to canonical IDs. */
|
/** Normalize Codex effort-suffixed IDs to canonical IDs. */
|
||||||
export function stripCodexEffortSuffix(model: string): string {
|
export function stripCodexEffortSuffix(model: string): string {
|
||||||
return model.replace(CODEX_EFFORT_SUFFIX_REGEX, '');
|
return model.replace(CODEX_EFFORT_SUFFIX_REGEX, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize known legacy iFlow model aliases to current upstream model IDs.
|
||||||
|
* Preserves suffixes such as (budget) and [1m].
|
||||||
|
*/
|
||||||
|
export function normalizeIFlowLegacyModelAliases(model: string): string {
|
||||||
|
const trimmed = trimModelId(model);
|
||||||
|
const { baseModel, suffix } = splitBaseModelAndSuffix(trimmed);
|
||||||
|
const replacement = IFLOW_LEGACY_MODEL_ALIASES[baseModel.trim().toLowerCase()];
|
||||||
|
if (!replacement) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
return `${replacement}${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
/** Normalize Claude dotted major.minor IDs to hyphenated format. */
|
/** Normalize Claude dotted major.minor IDs to hyphenated format. */
|
||||||
export function normalizeClaudeDottedMajorMinor(model: string): string {
|
export function normalizeClaudeDottedMajorMinor(model: string): string {
|
||||||
return model.replace(
|
return model.replace(
|
||||||
@@ -144,6 +187,9 @@ export function getDeniedModelIdReasonForProvider(
|
|||||||
*/
|
*/
|
||||||
export function normalizeModelIdForProvider(model: string, provider: ProviderLike): string {
|
export function normalizeModelIdForProvider(model: string, provider: ProviderLike): string {
|
||||||
const trimmedModel = trimModelId(model);
|
const trimmedModel = trimModelId(model);
|
||||||
|
if (isIFlowProvider(provider)) {
|
||||||
|
return normalizeIFlowLegacyModelAliases(trimmedModel);
|
||||||
|
}
|
||||||
if (!isAntigravityProvider(provider)) return trimmedModel;
|
if (!isAntigravityProvider(provider)) return trimmedModel;
|
||||||
const normalizedDottedVersion = normalizeClaudeDottedMajorMinor(trimmedModel);
|
const normalizedDottedVersion = normalizeClaudeDottedMajorMinor(trimmedModel);
|
||||||
return normalizeDeprecatedAntigravityModelAliases(normalizedDottedVersion);
|
return normalizeDeprecatedAntigravityModelAliases(normalizedDottedVersion);
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ import { hasActiveSessions, deleteSessionLockForPort } from '../session-tracker'
|
|||||||
import { warn } from '../../utils/ui';
|
import { warn } from '../../utils/ui';
|
||||||
import { getCcsDir } from '../../utils/config-manager';
|
import { getCcsDir } from '../../utils/config-manager';
|
||||||
import { validateCompositeTiers } from '../composite-validator';
|
import { validateCompositeTiers } from '../composite-validator';
|
||||||
import { getDeniedModelIdReasonForProvider } from '../model-id-normalizer';
|
import {
|
||||||
|
canonicalizeModelIdForProvider,
|
||||||
|
getDeniedModelIdReasonForProvider,
|
||||||
|
} from '../model-id-normalizer';
|
||||||
import {
|
import {
|
||||||
createSettingsFile,
|
createSettingsFile,
|
||||||
createSettingsFileUnified,
|
createSettingsFileUnified,
|
||||||
@@ -116,7 +119,8 @@ export function createVariant(
|
|||||||
target: TargetType = 'claude'
|
target: TargetType = 'claude'
|
||||||
): VariantOperationResult {
|
): VariantOperationResult {
|
||||||
try {
|
try {
|
||||||
const deniedModelReason = getDeniedModelIdReasonForProvider(model, provider);
|
const canonicalModel = canonicalizeModelIdForProvider(model, provider);
|
||||||
|
const deniedModelReason = getDeniedModelIdReasonForProvider(canonicalModel, provider);
|
||||||
if (deniedModelReason) {
|
if (deniedModelReason) {
|
||||||
return { success: false, error: deniedModelReason };
|
return { success: false, error: deniedModelReason };
|
||||||
}
|
}
|
||||||
@@ -133,7 +137,7 @@ export function createVariant(
|
|||||||
let settingsPath: string;
|
let settingsPath: string;
|
||||||
|
|
||||||
if (isUnifiedMode()) {
|
if (isUnifiedMode()) {
|
||||||
settingsPath = createSettingsFileUnified(name, provider, model, port);
|
settingsPath = createSettingsFileUnified(name, provider, canonicalModel, port);
|
||||||
saveVariantUnified(
|
saveVariantUnified(
|
||||||
name,
|
name,
|
||||||
provider as CLIProxyProvider,
|
provider as CLIProxyProvider,
|
||||||
@@ -143,7 +147,7 @@ export function createVariant(
|
|||||||
target
|
target
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
settingsPath = createSettingsFile(name, provider, model, port);
|
settingsPath = createSettingsFile(name, provider, canonicalModel, port);
|
||||||
saveVariantLegacy(
|
saveVariantLegacy(
|
||||||
name,
|
name,
|
||||||
provider,
|
provider,
|
||||||
@@ -157,7 +161,7 @@ export function createVariant(
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
settingsPath,
|
settingsPath,
|
||||||
variant: { provider, model, account, port, target },
|
variant: { provider, model: canonicalModel, account, port, target },
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
@@ -253,6 +257,11 @@ export function updateVariant(name: string, updates: UpdateVariantOptions): Vari
|
|||||||
const existingTarget = existing.target || 'claude';
|
const existingTarget = existing.target || 'claude';
|
||||||
const targetChanged = updates.target !== undefined && updates.target !== existingTarget;
|
const targetChanged = updates.target !== undefined && updates.target !== existingTarget;
|
||||||
const hasModelUpdate = updates.model !== undefined && updates.model.trim().length > 0;
|
const hasModelUpdate = updates.model !== undefined && updates.model.trim().length > 0;
|
||||||
|
const providerForModelUpdate = (updates.provider ?? existing.provider) as CLIProxyProfileName;
|
||||||
|
const canonicalModelUpdate =
|
||||||
|
updates.model !== undefined
|
||||||
|
? canonicalizeModelIdForProvider(updates.model.trim(), providerForModelUpdate)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
if (providerChanged && !hasModelUpdate) {
|
if (providerChanged && !hasModelUpdate) {
|
||||||
return {
|
return {
|
||||||
@@ -262,9 +271,8 @@ export function updateVariant(name: string, updates: UpdateVariantOptions): Vari
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasModelUpdate) {
|
if (hasModelUpdate) {
|
||||||
const providerForModelUpdate = updates.provider ?? (existing.provider as CLIProxyProfileName);
|
|
||||||
const deniedModelReason = getDeniedModelIdReasonForProvider(
|
const deniedModelReason = getDeniedModelIdReasonForProvider(
|
||||||
updates.model?.trim() || '',
|
canonicalModelUpdate || '',
|
||||||
providerForModelUpdate
|
providerForModelUpdate
|
||||||
);
|
);
|
||||||
if (deniedModelReason) {
|
if (deniedModelReason) {
|
||||||
@@ -279,11 +287,15 @@ export function updateVariant(name: string, updates: UpdateVariantOptions): Vari
|
|||||||
updateSettingsProviderAndModel(
|
updateSettingsProviderAndModel(
|
||||||
settingsPath,
|
settingsPath,
|
||||||
updates.provider as CLIProxyProfileName,
|
updates.provider as CLIProxyProfileName,
|
||||||
updates.model?.trim() || '',
|
canonicalModelUpdate || '',
|
||||||
existing.port
|
existing.port
|
||||||
);
|
);
|
||||||
} else if (updates.model !== undefined) {
|
} else if (updates.model !== undefined) {
|
||||||
updateSettingsModel(settingsPath, updates.model, existing.provider as CLIProxyProfileName);
|
updateSettingsModel(
|
||||||
|
settingsPath,
|
||||||
|
canonicalModelUpdate || '',
|
||||||
|
existing.provider as CLIProxyProfileName
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,7 +338,7 @@ export function updateVariant(name: string, updates: UpdateVariantOptions): Vari
|
|||||||
success: true,
|
success: true,
|
||||||
variant: {
|
variant: {
|
||||||
provider: updates.provider ?? existing.provider,
|
provider: updates.provider ?? existing.provider,
|
||||||
model: updates.model?.trim() || existing.model,
|
model: canonicalModelUpdate || existing.model,
|
||||||
account: updates.account !== undefined ? updates.account : existing.account,
|
account: updates.account !== undefined ? updates.account : existing.account,
|
||||||
port: existing.port,
|
port: existing.port,
|
||||||
settings: existing.settings,
|
settings: existing.settings,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { getClaudeSettingsPath } from '../../utils/claude-config-path';
|
|||||||
import { resolveDroidProvider } from '../../targets/droid-provider';
|
import { resolveDroidProvider } from '../../targets/droid-provider';
|
||||||
import { mapExternalProviderName } from '../../cliproxy/provider-capabilities';
|
import { mapExternalProviderName } from '../../cliproxy/provider-capabilities';
|
||||||
import {
|
import {
|
||||||
|
canonicalizeModelIdForProvider,
|
||||||
extractProviderFromPathname,
|
extractProviderFromPathname,
|
||||||
getDeniedModelIdReasonForProvider,
|
getDeniedModelIdReasonForProvider,
|
||||||
} from '../../cliproxy/model-id-normalizer';
|
} from '../../cliproxy/model-id-normalizer';
|
||||||
@@ -52,6 +53,15 @@ function getDeniedModelReasonForProvider(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function canonicalizeModelForProvider(
|
||||||
|
provider: CLIProxyProvider | null,
|
||||||
|
value: string | undefined
|
||||||
|
): string | undefined {
|
||||||
|
if (typeof value !== 'string') return value;
|
||||||
|
if (!provider || value.trim().length === 0) return value;
|
||||||
|
return canonicalizeModelIdForProvider(value, provider);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read config safely with fallback.
|
* Read config safely with fallback.
|
||||||
* Uses loadConfigSafe which supports both unified (config.yaml) and legacy (config.json).
|
* Uses loadConfigSafe which supports both unified (config.yaml) and legacy (config.json).
|
||||||
@@ -105,11 +115,15 @@ export function createSettingsFile(
|
|||||||
const settingsPath = path.join(getCcsDir(), `${name}.settings.json`);
|
const settingsPath = path.join(getCcsDir(), `${name}.settings.json`);
|
||||||
const { model, opusModel, sonnetModel, haikuModel } = models;
|
const { model, opusModel, sonnetModel, haikuModel } = models;
|
||||||
const providerFromBaseUrl = resolveProviderFromBaseUrl(baseUrl);
|
const providerFromBaseUrl = resolveProviderFromBaseUrl(baseUrl);
|
||||||
|
const canonicalModel = canonicalizeModelForProvider(providerFromBaseUrl, model);
|
||||||
|
const canonicalOpusModel = canonicalizeModelForProvider(providerFromBaseUrl, opusModel);
|
||||||
|
const canonicalSonnetModel = canonicalizeModelForProvider(providerFromBaseUrl, sonnetModel);
|
||||||
|
const canonicalHaikuModel = canonicalizeModelForProvider(providerFromBaseUrl, haikuModel);
|
||||||
const deniedReason = getDeniedModelReasonForProvider(providerFromBaseUrl, [
|
const deniedReason = getDeniedModelReasonForProvider(providerFromBaseUrl, [
|
||||||
model,
|
canonicalModel,
|
||||||
opusModel,
|
canonicalOpusModel,
|
||||||
sonnetModel,
|
canonicalSonnetModel,
|
||||||
haikuModel,
|
canonicalHaikuModel,
|
||||||
]);
|
]);
|
||||||
if (deniedReason) {
|
if (deniedReason) {
|
||||||
throw new Error(deniedReason);
|
throw new Error(deniedReason);
|
||||||
@@ -117,21 +131,22 @@ export function createSettingsFile(
|
|||||||
const droidProvider = resolveDroidProvider({
|
const droidProvider = resolveDroidProvider({
|
||||||
provider,
|
provider,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
model,
|
model: canonicalModel,
|
||||||
});
|
});
|
||||||
|
|
||||||
const settings: Settings = {
|
const settings: Settings = {
|
||||||
env: {
|
env: {
|
||||||
ANTHROPIC_BASE_URL: baseUrl,
|
ANTHROPIC_BASE_URL: baseUrl,
|
||||||
ANTHROPIC_AUTH_TOKEN: apiKey,
|
ANTHROPIC_AUTH_TOKEN: apiKey,
|
||||||
...(model && { ANTHROPIC_MODEL: model }),
|
...(canonicalModel && { ANTHROPIC_MODEL: canonicalModel }),
|
||||||
...(opusModel && { ANTHROPIC_DEFAULT_OPUS_MODEL: opusModel }),
|
...(canonicalOpusModel && { ANTHROPIC_DEFAULT_OPUS_MODEL: canonicalOpusModel }),
|
||||||
...(sonnetModel && { ANTHROPIC_DEFAULT_SONNET_MODEL: sonnetModel }),
|
...(canonicalSonnetModel && { ANTHROPIC_DEFAULT_SONNET_MODEL: canonicalSonnetModel }),
|
||||||
...(haikuModel && { ANTHROPIC_DEFAULT_HAIKU_MODEL: haikuModel }),
|
...(canonicalHaikuModel && { ANTHROPIC_DEFAULT_HAIKU_MODEL: canonicalHaikuModel }),
|
||||||
CCS_DROID_PROVIDER: droidProvider,
|
CCS_DROID_PROVIDER: droidProvider,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
||||||
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
||||||
return `~/.ccs/${name}.settings.json`;
|
return `~/.ccs/${name}.settings.json`;
|
||||||
}
|
}
|
||||||
@@ -161,16 +176,32 @@ export function updateSettingsFile(
|
|||||||
const providerForValidation =
|
const providerForValidation =
|
||||||
resolveProviderFromBaseUrl(updates.baseUrl) ??
|
resolveProviderFromBaseUrl(updates.baseUrl) ??
|
||||||
resolveProviderFromBaseUrl(settings.env?.ANTHROPIC_BASE_URL);
|
resolveProviderFromBaseUrl(settings.env?.ANTHROPIC_BASE_URL);
|
||||||
const deniedReason = getDeniedModelReasonForProvider(providerForValidation, [
|
const canonicalModel =
|
||||||
updates.model !== undefined ? updates.model : settings.env?.ANTHROPIC_MODEL,
|
updates.model !== undefined
|
||||||
|
? canonicalizeModelForProvider(providerForValidation, updates.model)
|
||||||
|
: undefined;
|
||||||
|
const canonicalOpusModel =
|
||||||
updates.opusModel !== undefined
|
updates.opusModel !== undefined
|
||||||
? updates.opusModel
|
? canonicalizeModelForProvider(providerForValidation, updates.opusModel)
|
||||||
: settings.env?.ANTHROPIC_DEFAULT_OPUS_MODEL,
|
: undefined;
|
||||||
|
const canonicalSonnetModel =
|
||||||
updates.sonnetModel !== undefined
|
updates.sonnetModel !== undefined
|
||||||
? updates.sonnetModel
|
? canonicalizeModelForProvider(providerForValidation, updates.sonnetModel)
|
||||||
: settings.env?.ANTHROPIC_DEFAULT_SONNET_MODEL,
|
: undefined;
|
||||||
|
const canonicalHaikuModel =
|
||||||
updates.haikuModel !== undefined
|
updates.haikuModel !== undefined
|
||||||
? updates.haikuModel
|
? canonicalizeModelForProvider(providerForValidation, updates.haikuModel)
|
||||||
|
: undefined;
|
||||||
|
const deniedReason = getDeniedModelReasonForProvider(providerForValidation, [
|
||||||
|
canonicalModel !== undefined ? canonicalModel : settings.env?.ANTHROPIC_MODEL,
|
||||||
|
canonicalOpusModel !== undefined
|
||||||
|
? canonicalOpusModel
|
||||||
|
: settings.env?.ANTHROPIC_DEFAULT_OPUS_MODEL,
|
||||||
|
canonicalSonnetModel !== undefined
|
||||||
|
? canonicalSonnetModel
|
||||||
|
: settings.env?.ANTHROPIC_DEFAULT_SONNET_MODEL,
|
||||||
|
canonicalHaikuModel !== undefined
|
||||||
|
? canonicalHaikuModel
|
||||||
: settings.env?.ANTHROPIC_DEFAULT_HAIKU_MODEL,
|
: settings.env?.ANTHROPIC_DEFAULT_HAIKU_MODEL,
|
||||||
]);
|
]);
|
||||||
if (deniedReason) {
|
if (deniedReason) {
|
||||||
@@ -189,8 +220,8 @@ export function updateSettingsFile(
|
|||||||
|
|
||||||
if (updates.model !== undefined) {
|
if (updates.model !== undefined) {
|
||||||
settings.env = settings.env || {};
|
settings.env = settings.env || {};
|
||||||
if (updates.model) {
|
if (canonicalModel) {
|
||||||
settings.env.ANTHROPIC_MODEL = updates.model;
|
settings.env.ANTHROPIC_MODEL = canonicalModel;
|
||||||
} else {
|
} else {
|
||||||
delete settings.env.ANTHROPIC_MODEL;
|
delete settings.env.ANTHROPIC_MODEL;
|
||||||
}
|
}
|
||||||
@@ -199,8 +230,8 @@ export function updateSettingsFile(
|
|||||||
// Handle model mapping fields
|
// Handle model mapping fields
|
||||||
if (updates.opusModel !== undefined) {
|
if (updates.opusModel !== undefined) {
|
||||||
settings.env = settings.env || {};
|
settings.env = settings.env || {};
|
||||||
if (updates.opusModel) {
|
if (canonicalOpusModel) {
|
||||||
settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL = updates.opusModel;
|
settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL = canonicalOpusModel;
|
||||||
} else {
|
} else {
|
||||||
delete settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL;
|
delete settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL;
|
||||||
}
|
}
|
||||||
@@ -208,8 +239,8 @@ export function updateSettingsFile(
|
|||||||
|
|
||||||
if (updates.sonnetModel !== undefined) {
|
if (updates.sonnetModel !== undefined) {
|
||||||
settings.env = settings.env || {};
|
settings.env = settings.env || {};
|
||||||
if (updates.sonnetModel) {
|
if (canonicalSonnetModel) {
|
||||||
settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL = updates.sonnetModel;
|
settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL = canonicalSonnetModel;
|
||||||
} else {
|
} else {
|
||||||
delete settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL;
|
delete settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL;
|
||||||
}
|
}
|
||||||
@@ -217,8 +248,8 @@ export function updateSettingsFile(
|
|||||||
|
|
||||||
if (updates.haikuModel !== undefined) {
|
if (updates.haikuModel !== undefined) {
|
||||||
settings.env = settings.env || {};
|
settings.env = settings.env || {};
|
||||||
if (updates.haikuModel) {
|
if (canonicalHaikuModel) {
|
||||||
settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = updates.haikuModel;
|
settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = canonicalHaikuModel;
|
||||||
} else {
|
} else {
|
||||||
delete settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
|
delete settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
|
||||||
}
|
}
|
||||||
@@ -234,7 +265,7 @@ export function updateSettingsFile(
|
|||||||
const resolvedProvider = resolveDroidProvider({
|
const resolvedProvider = resolveDroidProvider({
|
||||||
provider: updates.provider ?? settings.env.CCS_DROID_PROVIDER,
|
provider: updates.provider ?? settings.env.CCS_DROID_PROVIDER,
|
||||||
baseUrl: updates.baseUrl ?? settings.env.ANTHROPIC_BASE_URL,
|
baseUrl: updates.baseUrl ?? settings.env.ANTHROPIC_BASE_URL,
|
||||||
model: updates.model ?? settings.env.ANTHROPIC_MODEL,
|
model: canonicalModel ?? settings.env.ANTHROPIC_MODEL,
|
||||||
});
|
});
|
||||||
settings.env.CCS_DROID_PROVIDER = resolvedProvider;
|
settings.env.CCS_DROID_PROVIDER = resolvedProvider;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -242,6 +242,51 @@ describe('getEffectiveEnvVars local provider URL normalization', () => {
|
|||||||
expect(persisted.presets[0]?.haiku).toBe('qwen3-coder-plus');
|
expect(persisted.presets[0]?.haiku).toBe('qwen3-coder-plus');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('migrates stale iflow model aliases (including kimi-k2.5) to supported IDs', () => {
|
||||||
|
const iflowSettingsPath = path.join(tempHome, 'iflow.settings.json');
|
||||||
|
writeSettings(
|
||||||
|
iflowSettingsPath,
|
||||||
|
{
|
||||||
|
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/iflow',
|
||||||
|
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
|
||||||
|
ANTHROPIC_MODEL: 'kimi-k2.5',
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL: 'iflow-default',
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL: 'deepseek-v3.2-chat',
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'glm-4.7',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
presets: [
|
||||||
|
{
|
||||||
|
name: 'legacy-iflow',
|
||||||
|
default: 'kimi-k2.5',
|
||||||
|
opus: 'iflow-default',
|
||||||
|
sonnet: 'deepseek-v3.2-chat',
|
||||||
|
haiku: 'minimax-m2.5',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const env = getEffectiveEnvVars('iflow', 8317, iflowSettingsPath);
|
||||||
|
expect(env.ANTHROPIC_MODEL).toBe('kimi-k2');
|
||||||
|
expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('qwen3-coder-plus');
|
||||||
|
expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('deepseek-v3.2');
|
||||||
|
expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('glm-4.6');
|
||||||
|
|
||||||
|
const persisted = JSON.parse(fs.readFileSync(iflowSettingsPath, 'utf-8')) as {
|
||||||
|
env: Record<string, string>;
|
||||||
|
presets: Array<Record<string, string>>;
|
||||||
|
};
|
||||||
|
expect(persisted.env.ANTHROPIC_MODEL).toBe('kimi-k2');
|
||||||
|
expect(persisted.env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('qwen3-coder-plus');
|
||||||
|
expect(persisted.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('deepseek-v3.2');
|
||||||
|
expect(persisted.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('glm-4.6');
|
||||||
|
expect(persisted.presets[0]?.default).toBe('kimi-k2');
|
||||||
|
expect(persisted.presets[0]?.opus).toBe('qwen3-coder-plus');
|
||||||
|
expect(persisted.presets[0]?.sonnet).toBe('deepseek-v3.2');
|
||||||
|
expect(persisted.presets[0]?.haiku).toBe('qwen3-coder-plus');
|
||||||
|
});
|
||||||
|
|
||||||
it('repairs existing provider settings files that are missing env keys', () => {
|
it('repairs existing provider settings files that are missing env keys', () => {
|
||||||
process.env.CCS_HOME = tempHome;
|
process.env.CCS_HOME = tempHome;
|
||||||
const agySettingsPath = path.join(tempHome, '.ccs', 'agy.settings.json');
|
const agySettingsPath = path.join(tempHome, '.ccs', 'agy.settings.json');
|
||||||
|
|||||||
@@ -103,6 +103,16 @@ describe('model-id-normalizer', () => {
|
|||||||
'claude-sonnet-4-6'
|
'claude-sonnet-4-6'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('normalizes known legacy iflow model aliases to supported upstream IDs', () => {
|
||||||
|
expect(normalizeModelIdForProvider('iflow-default', 'iflow')).toBe('qwen3-coder-plus');
|
||||||
|
expect(normalizeModelIdForProvider('kimi-k2.5', 'iflow')).toBe('kimi-k2');
|
||||||
|
expect(normalizeModelIdForProvider('kimi-k2.5(8192)', 'iflow')).toBe('kimi-k2(8192)');
|
||||||
|
expect(canonicalizeModelIdForProvider('deepseek-v3.2-chat', 'iflow')).toBe('deepseek-v3.2');
|
||||||
|
expect(canonicalizeModelIdForProvider('glm-4.7', 'iflow')).toBe('glm-4.6');
|
||||||
|
expect(canonicalizeModelIdForProvider('minimax-m2.5', 'iflow')).toBe('qwen3-coder-plus');
|
||||||
|
expect(canonicalizeModelIdForProvider('kimi-k2.5', 'gemini')).toBe('kimi-k2.5');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('env normalization', () => {
|
describe('env normalization', () => {
|
||||||
@@ -127,6 +137,21 @@ describe('model-id-normalizer', () => {
|
|||||||
expect(unchanged.UNRELATED).toBe('keep-me');
|
expect(unchanged.UNRELATED).toBe('keep-me');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('normalizes iflow legacy model aliases in env vars', () => {
|
||||||
|
const input: NodeJS.ProcessEnv = {
|
||||||
|
ANTHROPIC_MODEL: 'kimi-k2.5',
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL: 'iflow-default',
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL: 'deepseek-v3.2-chat',
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'glm-4.7',
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalized = normalizeModelEnvVarsForProvider(input, 'iflow');
|
||||||
|
expect(normalized.ANTHROPIC_MODEL).toBe('kimi-k2');
|
||||||
|
expect(normalized.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('qwen3-coder-plus');
|
||||||
|
expect(normalized.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('deepseek-v3.2');
|
||||||
|
expect(normalized.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('glm-4.6');
|
||||||
|
});
|
||||||
|
|
||||||
it('flags denylisted antigravity models', () => {
|
it('flags denylisted antigravity models', () => {
|
||||||
expect(isDeniedAntigravityModelId('claude-opus-4-5-thinking')).toBe(true);
|
expect(isDeniedAntigravityModelId('claude-opus-4-5-thinking')).toBe(true);
|
||||||
expect(isDeniedAntigravityModelId('claude-sonnet-4.5')).toBe(true);
|
expect(isDeniedAntigravityModelId('claude-sonnet-4.5')).toBe(true);
|
||||||
|
|||||||
@@ -147,4 +147,34 @@ describe('cliproxy-stats-routes model update canonicalization', () => {
|
|||||||
expect(persisted.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.3-codex');
|
expect(persisted.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.3-codex');
|
||||||
expect(persisted.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5-mini');
|
expect(persisted.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5-mini');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('canonicalizes legacy iflow model IDs to supported upstream IDs', async () => {
|
||||||
|
const settingsPath = path.join(tempHome, '.ccs', 'cliproxy', 'iflow.settings.json');
|
||||||
|
writeSettings(settingsPath, {
|
||||||
|
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/iflow',
|
||||||
|
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
|
||||||
|
ANTHROPIC_MODEL: 'qwen3-coder-plus',
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL: 'qwen3-coder-plus',
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL: 'qwen3-coder-plus',
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'qwen3-coder-plus',
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch(`${baseUrl}/api/cliproxy/models/iflow`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ model: 'kimi-k2.5' }),
|
||||||
|
});
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
|
||||||
|
const body = (await response.json()) as { model: string };
|
||||||
|
expect(body.model).toBe('kimi-k2');
|
||||||
|
|
||||||
|
const persisted = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as {
|
||||||
|
env: Record<string, string>;
|
||||||
|
};
|
||||||
|
expect(persisted.env.ANTHROPIC_MODEL).toBe('kimi-k2');
|
||||||
|
expect(persisted.env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('kimi-k2');
|
||||||
|
expect(persisted.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('kimi-k2');
|
||||||
|
expect(persisted.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('kimi-k2');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -71,4 +71,64 @@ describe('route-helpers AGY denylist', () => {
|
|||||||
/denylist/i
|
/denylist/i
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('canonicalizes legacy iflow model IDs on settings create', () => {
|
||||||
|
createSettingsFile(
|
||||||
|
'iflow-profile',
|
||||||
|
'http://127.0.0.1:8317/api/provider/iflow',
|
||||||
|
'test-token',
|
||||||
|
{
|
||||||
|
model: 'kimi-k2.5',
|
||||||
|
opusModel: 'iflow-default',
|
||||||
|
sonnetModel: 'deepseek-v3.2-chat',
|
||||||
|
haikuModel: 'glm-4.7',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const settingsPath = path.join(tempHome, '.ccs', 'iflow-profile.settings.json');
|
||||||
|
const persisted = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as {
|
||||||
|
env: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(persisted.env.ANTHROPIC_MODEL).toBe('kimi-k2');
|
||||||
|
expect(persisted.env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('qwen3-coder-plus');
|
||||||
|
expect(persisted.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('deepseek-v3.2');
|
||||||
|
expect(persisted.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('glm-4.6');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('canonicalizes legacy iflow model IDs on settings update', () => {
|
||||||
|
const settingsDir = path.join(tempHome, '.ccs');
|
||||||
|
fs.mkdirSync(settingsDir, { recursive: true });
|
||||||
|
const settingsPath = path.join(settingsDir, 'iflow-profile.settings.json');
|
||||||
|
fs.writeFileSync(
|
||||||
|
settingsPath,
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
env: {
|
||||||
|
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/iflow',
|
||||||
|
ANTHROPIC_AUTH_TOKEN: 'test-token',
|
||||||
|
ANTHROPIC_MODEL: 'qwen3-coder-plus',
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL: 'qwen3-coder-plus',
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL: 'qwen3-coder-plus',
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'qwen3-coder-plus',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
) + '\n'
|
||||||
|
);
|
||||||
|
|
||||||
|
updateSettingsFile('iflow-profile', {
|
||||||
|
model: 'kimi-k2.5',
|
||||||
|
sonnetModel: 'deepseek-v3.2-chat',
|
||||||
|
haikuModel: 'minimax-m2.5',
|
||||||
|
});
|
||||||
|
|
||||||
|
const persisted = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as {
|
||||||
|
env: Record<string, string>;
|
||||||
|
};
|
||||||
|
expect(persisted.env.ANTHROPIC_MODEL).toBe('kimi-k2');
|
||||||
|
expect(persisted.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('deepseek-v3.2');
|
||||||
|
expect(persisted.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('qwen3-coder-plus');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -108,6 +108,52 @@ describe('settings-routes model canonicalization', () => {
|
|||||||
expect(persisted.presets[0]?.haiku).toBe('claude-haiku-4-5');
|
expect(persisted.presets[0]?.haiku).toBe('claude-haiku-4-5');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('canonicalizes legacy iflow model IDs on PUT /:profile', async () => {
|
||||||
|
const settingsPath = path.join(tempHome, '.ccs', 'iflow.settings.json');
|
||||||
|
writeSettings(settingsPath, { env: {} });
|
||||||
|
|
||||||
|
const response = await fetch(`${baseUrl}/api/settings/iflow`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
settings: {
|
||||||
|
env: {
|
||||||
|
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/iflow',
|
||||||
|
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
|
||||||
|
ANTHROPIC_MODEL: 'kimi-k2.5',
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL: 'iflow-default',
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL: 'deepseek-v3.2-chat',
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'glm-4.7',
|
||||||
|
},
|
||||||
|
presets: [
|
||||||
|
{
|
||||||
|
name: 'legacy-iflow',
|
||||||
|
default: 'kimi-k2.5',
|
||||||
|
opus: 'iflow-default',
|
||||||
|
sonnet: 'deepseek-v3.2-chat',
|
||||||
|
haiku: 'minimax-m2.5',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
|
||||||
|
const persisted = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as {
|
||||||
|
env: Record<string, string>;
|
||||||
|
presets: Array<Record<string, string>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(persisted.env.ANTHROPIC_MODEL).toBe('kimi-k2');
|
||||||
|
expect(persisted.env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('qwen3-coder-plus');
|
||||||
|
expect(persisted.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('deepseek-v3.2');
|
||||||
|
expect(persisted.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('glm-4.6');
|
||||||
|
expect(persisted.presets[0]?.default).toBe('kimi-k2');
|
||||||
|
expect(persisted.presets[0]?.opus).toBe('qwen3-coder-plus');
|
||||||
|
expect(persisted.presets[0]?.sonnet).toBe('deepseek-v3.2');
|
||||||
|
expect(persisted.presets[0]?.haiku).toBe('qwen3-coder-plus');
|
||||||
|
});
|
||||||
|
|
||||||
it('canonicalizes AGY preset values on POST /:profile/presets', async () => {
|
it('canonicalizes AGY preset values on POST /:profile/presets', async () => {
|
||||||
const settingsPath = path.join(tempHome, '.ccs', 'agy.settings.json');
|
const settingsPath = path.join(tempHome, '.ccs', 'agy.settings.json');
|
||||||
writeSettings(settingsPath, {
|
writeSettings(settingsPath, {
|
||||||
|
|||||||
@@ -262,20 +262,15 @@ export const MODEL_CATALOGS: Record<string, ProviderCatalog> = {
|
|||||||
name: 'Qwen3 Max',
|
name: 'Qwen3 Max',
|
||||||
description: 'Flagship Qwen model via iFlow',
|
description: 'Flagship Qwen model via iFlow',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'kimi-k2.5',
|
|
||||||
name: 'Kimi K2.5',
|
|
||||||
description: 'Latest Kimi model via iFlow',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'kimi-k2',
|
id: 'kimi-k2',
|
||||||
name: 'Kimi K2',
|
name: 'Kimi K2',
|
||||||
description: 'Kimi general model',
|
description: 'Kimi model currently available via iFlow',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'deepseek-v3.2-chat',
|
id: 'deepseek-v3.2',
|
||||||
name: 'DeepSeek V3.2 Chat',
|
name: 'DeepSeek V3.2',
|
||||||
description: 'Stable DeepSeek chat model',
|
description: 'Current DeepSeek V3.2 model via iFlow',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'deepseek-r1',
|
id: 'deepseek-r1',
|
||||||
@@ -283,14 +278,9 @@ export const MODEL_CATALOGS: Record<string, ProviderCatalog> = {
|
|||||||
description: 'Reasoning-focused DeepSeek model',
|
description: 'Reasoning-focused DeepSeek model',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'glm-4.7',
|
id: 'glm-4.6',
|
||||||
name: 'GLM 4.7',
|
name: 'GLM 4.6',
|
||||||
description: 'Zhipu GLM 4.7 via iFlow',
|
description: 'Zhipu GLM 4.6 via iFlow',
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'minimax-m2.5',
|
|
||||||
name: 'MiniMax M2.5',
|
|
||||||
description: 'MiniMax M2.5 via iFlow',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'qwen3-vl-plus',
|
id: 'qwen3-vl-plus',
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { MODEL_CATALOGS } from '@/lib/model-catalogs';
|
||||||
|
|
||||||
|
describe('iflow model catalog compatibility', () => {
|
||||||
|
it('does not advertise stale iflow model aliases that fail provider routing', () => {
|
||||||
|
const iflowCatalog = MODEL_CATALOGS.iflow;
|
||||||
|
expect(iflowCatalog).toBeDefined();
|
||||||
|
|
||||||
|
const ids = iflowCatalog.models.map((model) => model.id);
|
||||||
|
expect(ids).not.toContain('kimi-k2.5');
|
||||||
|
expect(ids).not.toContain('deepseek-v3.2-chat');
|
||||||
|
expect(ids).not.toContain('glm-4.7');
|
||||||
|
expect(ids).not.toContain('minimax-m2.5');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps supported iflow model IDs for Kimi and DeepSeek entries', () => {
|
||||||
|
const iflowCatalog = MODEL_CATALOGS.iflow;
|
||||||
|
const ids = iflowCatalog.models.map((model) => model.id);
|
||||||
|
|
||||||
|
expect(ids).toContain('kimi-k2');
|
||||||
|
expect(ids).toContain('deepseek-v3.2');
|
||||||
|
expect(ids).toContain('glm-4.6');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user