1 parent
e7f96e12d7
commit
be7284f0f1
12 files changed
+277
-32
No files matched your search
@@ -498,7 +498,7 @@ pub(crate) async fn get_model_alias_edit_source(
|
||||
alias: String,
|
||||
) -> Result<ModelAliasEditContext, String> {
|
||||
let config = gui_config_state.snapshot()?;
|
||||
let alias = validate_thinking_alias_model_id(&alias, "别名模型")?;
|
||||
let alias = existing_thinking_alias_model_id(&alias, "别名模型")?;
|
||||
let content = fetch_management_config_yaml(&config).await?;
|
||||
let definitions = fetch_oauth_model_definitions(&config).await;
|
||||
model_alias_edit_context(&content, &alias, &definitions)
|
||||
@@ -519,7 +519,16 @@ pub(crate) async fn create_thinking_alias(
|
||||
if source_id.is_empty() {
|
||||
return Err("请先选择原模型".to_string());
|
||||
}
|
||||
let alias = validate_thinking_alias_model_id(&alias, "别名模型")?;
|
||||
let original_alias = original_alias
|
||||
.as_deref()
|
||||
.map(|original| existing_thinking_alias_model_id(original, "别名模型"))
|
||||
.transpose()?;
|
||||
let alias = match original_alias.as_deref() {
|
||||
Some(original) if original.eq_ignore_ascii_case(alias.trim()) => {
|
||||
existing_thinking_alias_model_id(&alias, "别名模型")?
|
||||
}
|
||||
_ => validate_thinking_alias_model_id(&alias, "别名模型")?,
|
||||
};
|
||||
let effort = if effort.trim().is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
@@ -594,7 +603,7 @@ pub(crate) async fn delete_thinking_alias(
|
||||
oauth_channel: Option<String>,
|
||||
) -> Result<Vec<ThinkingAliasEntry>, String> {
|
||||
let config = gui_config_state.snapshot()?;
|
||||
let alias = validate_thinking_alias_model_id(&alias, "别名模型")?;
|
||||
let alias = existing_thinking_alias_model_id(&alias, "别名模型")?;
|
||||
let content = fetch_management_config_yaml(&config).await?;
|
||||
let updated =
|
||||
remove_thinking_alias_from_yaml_for_channel(&content, &alias, oauth_channel.as_deref())?;
|
||||
@@ -690,7 +699,7 @@ pub(crate) async fn delete_speed_alias(
|
||||
oauth_channel: Option<String>,
|
||||
) -> Result<Vec<SpeedAliasEntry>, String> {
|
||||
let config = gui_config_state.snapshot()?;
|
||||
let alias = validate_thinking_alias_model_id(&alias, "别名模型")?;
|
||||
let alias = existing_thinking_alias_model_id(&alias, "别名模型")?;
|
||||
let content = fetch_management_config_yaml(&config).await?;
|
||||
let updated =
|
||||
remove_speed_alias_from_yaml_for_channel(&content, &alias, oauth_channel.as_deref())?;
|
||||
|
||||
@@ -185,19 +185,26 @@ pub(crate) async fn fetch_active_oauth_alias_channels(
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub(crate) fn validate_thinking_alias_model_id(value: &str, label: &str) -> Result<String, String> {
|
||||
pub(crate) fn existing_thinking_alias_model_id(value: &str, label: &str) -> Result<String, String> {
|
||||
let value = value.trim();
|
||||
if value.is_empty() {
|
||||
return Err(format!("{label}不能为空"));
|
||||
}
|
||||
if value.len() > 240
|
||||
|| value
|
||||
.chars()
|
||||
.any(|character| character.is_whitespace() || character.is_control())
|
||||
if value.len() > 240 || value.chars().any(char::is_control) {
|
||||
return Err(format!("{label}格式无效"));
|
||||
}
|
||||
Ok(value.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn validate_thinking_alias_model_id(value: &str, label: &str) -> Result<String, String> {
|
||||
let value = existing_thinking_alias_model_id(value, label)?;
|
||||
if value
|
||||
.chars()
|
||||
.any(|character| character.is_whitespace() || character.is_control())
|
||||
{
|
||||
return Err(format!("{label}格式无效,不能包含空白字符"));
|
||||
}
|
||||
Ok(value.to_string())
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub(crate) fn validate_thinking_alias_effort(value: &str) -> Result<String, String> {
|
||||
|
||||
@@ -562,6 +562,39 @@ fn thinking_alias_effort_accepts_provider_defined_levels() {
|
||||
assert!(validate_thinking_alias_effort("32768").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn existing_aliases_with_spaces_can_be_loaded_and_deleted() {
|
||||
assert_eq!(
|
||||
existing_thinking_alias_model_id(" Codex Auto Review ", "别名模型").unwrap(),
|
||||
"Codex Auto Review"
|
||||
);
|
||||
assert!(validate_thinking_alias_model_id("Codex Auto Review", "别名模型").is_err());
|
||||
let content = "codex-api-key:\n - name: provider\n base-url: https://www.loomex.cc\n models:\n - name: codex-auto-review\n alias: Codex Auto Review\n - name: keep-me\n";
|
||||
let entries = thinking_aliases_from_yaml(content).unwrap();
|
||||
assert_eq!(entries.len(), 1);
|
||||
assert_eq!(entries[0].alias, "Codex Auto Review");
|
||||
assert_eq!(entries[0].source_model, "codex-auto-review");
|
||||
let context = model_alias_edit_context(content, "Codex Auto Review", &[]).unwrap();
|
||||
assert_eq!(context.source.model, "codex-auto-review");
|
||||
let deleted = remove_thinking_alias_from_yaml(content, "Codex Auto Review").unwrap();
|
||||
assert!(thinking_aliases_from_yaml(&deleted).unwrap().is_empty());
|
||||
assert!(deleted.contains("name: keep-me"));
|
||||
assert!(!deleted.contains("Codex Auto Review"));
|
||||
let source = resolve_model_alias_edit_source(content, "Codex Auto Review", &[]).unwrap();
|
||||
let renamed = edit_model_alias_in_yaml(
|
||||
content,
|
||||
"Codex Auto Review",
|
||||
&source,
|
||||
"codex-auto-review-alias",
|
||||
"",
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
let renamed_entries = thinking_aliases_from_yaml(&renamed).unwrap();
|
||||
assert_eq!(renamed_entries.len(), 1);
|
||||
assert_eq!(renamed_entries[0].alias, "codex-auto-review-alias");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thinking_alias_removal_keeps_other_models_in_grouped_rule() {
|
||||
let input = "oauth-model-alias:\n codex:\n - name: gpt-5.5\n alias: gpt-5.5-xhigh\n fork: true\n - name: gpt-5.4\n alias: gpt-5.4-xhigh\n fork: true\npayload:\n override:\n - models:\n - name: gpt-5.5-xhigh\n protocol: codex\n - name: gpt-5.4-xhigh\n protocol: codex\n params:\n reasoning.effort: xhigh\n";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useId, useMemo, useRef, useState } from 'react';
|
||||
import { ArrowRight, Search, X } from 'lucide-react';
|
||||
import { useI18n } from '../i18n';
|
||||
import type { ModelOption } from '../services/modelService';
|
||||
import { modelSearchText, type ModelOption } from '../services/modelService';
|
||||
|
||||
type ModelSelectionPanelProps = {
|
||||
models: ModelOption[];
|
||||
@@ -17,7 +17,7 @@ export function ModelSelectionPanel({ models, selected, loading, onMove }: Model
|
||||
const [search, setSearch] = useState('');
|
||||
const query = search.trim().toLowerCase();
|
||||
const visibleModels = useMemo(() => models.filter((model) =>
|
||||
`${model.name} ${model.alias ?? ''}`.toLowerCase().includes(query),
|
||||
modelSearchText(model).includes(query),
|
||||
), [models, query]);
|
||||
const searchLabel = t(selected ? 'apiAccess.modelDialog.searchSelected' : 'apiAccess.modelDialog.searchUnselected');
|
||||
|
||||
@@ -63,7 +63,7 @@ export function ModelSelectionPanel({ models, selected, loading, onMove }: Model
|
||||
onMove([model], !selected);
|
||||
}}
|
||||
>
|
||||
<span><strong title={model.name}>{model.name}</strong>{model.alias ? <small title={model.alias}>{model.alias}</small> : null}</span>
|
||||
<span><strong title={model.name}>{model.name}</strong>{model.alias || model.displayName ? <small title={model.alias || model.displayName}>{model.alias || model.displayName}</small> : null}</span>
|
||||
{selected ? <X size={16} aria-hidden="true" /> : <ArrowRight size={16} aria-hidden="true" />}
|
||||
</button>
|
||||
)) : (
|
||||
|
||||
@@ -525,6 +525,7 @@ export const jaOverrides = {
|
||||
'aliases.source.available': '現在コアで利用可能なモデル',
|
||||
'aliases.error.selectModel': '先に元のモデルを選択してください',
|
||||
'aliases.error.emptyAlias': 'エイリアスモデル名を入力してください',
|
||||
'aliases.error.invalidAlias': 'エイリアスモデル名に空白文字は使えません',
|
||||
'aliases.error.emptyEffort': '推論レベルを入力してください',
|
||||
'aliases.error.unsupportedEffort': 'このモデルソースには利用可能な推論レベルがありません',
|
||||
'aliases.created': '{alias} を作成し、推論レベルを {effort} に固定しました',
|
||||
|
||||
@@ -513,6 +513,7 @@ export const en: Record<MessageKey, string> = {
|
||||
'aliases.source.available': 'Models currently available from the core',
|
||||
'aliases.error.selectModel': 'Select an original model first',
|
||||
'aliases.error.emptyAlias': 'The model alias cannot be empty',
|
||||
'aliases.error.invalidAlias': 'The model alias is invalid and cannot contain whitespace',
|
||||
'aliases.error.emptyEffort': 'The reasoning effort cannot be empty',
|
||||
'aliases.error.unsupportedEffort': 'This model source has no available reasoning-effort levels',
|
||||
'aliases.error.unsupportedFast': 'Fast is only available for Codex API, OpenAI-compatible, and Codex OAuth model sources',
|
||||
|
||||
@@ -512,6 +512,7 @@ export const zhCN = {
|
||||
'aliases.source.available': '内核当前可用模型',
|
||||
'aliases.error.selectModel': '请先选择原模型',
|
||||
'aliases.error.emptyAlias': '别名模型不能为空',
|
||||
'aliases.error.invalidAlias': '别名模型格式无效,不能包含空白字符',
|
||||
'aliases.error.emptyEffort': '思考强度不能为空',
|
||||
'aliases.error.unsupportedEffort': '该模型来源没有可用的思考强度等级',
|
||||
'aliases.error.unsupportedFast': 'Fast 仅支持 Codex API、OpenAI 兼容和 Codex OAuth 模型源',
|
||||
|
||||
@@ -56,9 +56,11 @@ import {
|
||||
DEEPSEEK_BASE_URL,
|
||||
fetchModels,
|
||||
mergeModelOptions,
|
||||
modelSearchText,
|
||||
modelsFromRecord,
|
||||
normalizeBaseUrl,
|
||||
reconcileModelSelection,
|
||||
usableModelAlias,
|
||||
type ModelOption,
|
||||
type ModelProvider,
|
||||
} from '../services/modelService';
|
||||
@@ -381,9 +383,23 @@ const mergeModelRecords = (current: unknown, selected: ModelOption[]) => {
|
||||
);
|
||||
const next: Record<string, unknown> = isRecord(matched) ? { ...matched } : {};
|
||||
next.name = name;
|
||||
const alias = model.alias?.trim();
|
||||
if (alias && alias !== name) next.alias = alias;
|
||||
else delete next.alias;
|
||||
const requested = (model.alias ?? '').trim();
|
||||
const storedAlias = readString(next, 'alias');
|
||||
const alias = usableModelAlias(requested);
|
||||
if (alias && alias !== name) {
|
||||
next.alias = alias;
|
||||
} else if (
|
||||
requested
|
||||
&& storedAlias
|
||||
&& requested.toLowerCase() === storedAlias.toLowerCase()
|
||||
&& requested.toLowerCase() !== name.toLowerCase()
|
||||
) {
|
||||
next.alias = storedAlias;
|
||||
} else if (requested && requested !== name) {
|
||||
throw new Error(translate(getCurrentLocale(), 'aliases.error.invalidAlias'));
|
||||
} else {
|
||||
delete next.alias;
|
||||
}
|
||||
if (model.thinking) next.thinking = { ...model.thinking };
|
||||
models.push(next);
|
||||
return models;
|
||||
@@ -1418,9 +1434,7 @@ function ProviderHealthDialog({ row, onClose }: ProviderHealthDialogProps) {
|
||||
const visibleModels = useMemo(() => {
|
||||
const query = search.trim().toLowerCase();
|
||||
if (!query) return models;
|
||||
return models.filter((model) =>
|
||||
`${model.name} ${model.alias ?? ''}`.toLowerCase().includes(query),
|
||||
);
|
||||
return models.filter((model) => modelSearchText(model).includes(query));
|
||||
}, [models, search]);
|
||||
|
||||
const resultValues = Object.values(results);
|
||||
@@ -1532,8 +1546,8 @@ function ProviderHealthDialog({ row, onClose }: ProviderHealthDialogProps) {
|
||||
<strong title={model.name}>{model.name}</strong>
|
||||
{error
|
||||
? <small className="error" title={error}>{error}</small>
|
||||
: model.alias
|
||||
? <small title={model.alias}>{model.alias}</small>
|
||||
: model.alias || model.displayName
|
||||
? <small title={model.alias || model.displayName}>{model.alias || model.displayName}</small>
|
||||
: null}
|
||||
</div>
|
||||
<span
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { usableModelAlias } from '../services/modelService';
|
||||
import {
|
||||
ArrowRight,
|
||||
BrainCircuit,
|
||||
@@ -398,6 +399,13 @@ export function ThinkingAliasesPage() {
|
||||
setError(t('aliases.error.emptyAlias'));
|
||||
return;
|
||||
}
|
||||
const keepingExistingAlias = Boolean(
|
||||
editingEntry && editingEntry.alias.trim().toLowerCase() === normalizedAlias.toLowerCase(),
|
||||
);
|
||||
if (!keepingExistingAlias && !usableModelAlias(normalizedAlias)) {
|
||||
setError(t('aliases.error.invalidAlias'));
|
||||
return;
|
||||
}
|
||||
if (fastEnabled && !selectedSource.supportsFast) {
|
||||
setError(t('aliases.error.unsupportedFast'));
|
||||
return;
|
||||
|
||||
@@ -6,6 +6,7 @@ const modelText = (key: Parameters<typeof translate>[1]) => translate(getCurrent
|
||||
export type ModelOption = {
|
||||
name: string;
|
||||
alias?: string;
|
||||
displayName?: string;
|
||||
isAlias?: boolean;
|
||||
contextWindow?: number;
|
||||
inputModalities?: Array<'text' | 'image'>;
|
||||
@@ -21,12 +22,36 @@ export const DEEPSEEK_BASE_URL = 'https://api.deepseek.com';
|
||||
|
||||
const modelKey = (name: string) => name.trim().toLowerCase();
|
||||
|
||||
export function usableModelAlias(value: string | undefined | null): string {
|
||||
const alias = value?.trim() ?? '';
|
||||
if (!alias || alias.length > 240) return '';
|
||||
for (const character of alias) {
|
||||
const code = character.charCodeAt(0);
|
||||
if (character.trim() === '' || code < 32 || code === 127) return '';
|
||||
}
|
||||
return alias;
|
||||
}
|
||||
|
||||
export function modelSearchText(
|
||||
model: Pick<ModelOption, 'name' | 'alias' | 'displayName'>,
|
||||
): string {
|
||||
return [model.name, model.alias, model.displayName].filter(Boolean).join(' ').toLowerCase();
|
||||
}
|
||||
|
||||
export function mergeModelOptions(...groups: ModelOption[][]): ModelOption[] {
|
||||
const merged = new Map<string, ModelOption>();
|
||||
groups.flat().forEach((model) => {
|
||||
const name = model.name.trim();
|
||||
if (!name) return;
|
||||
merged.set(modelKey(name), { ...model, name });
|
||||
const previous = merged.get(modelKey(name));
|
||||
const next: ModelOption = { ...previous, ...model, name };
|
||||
const alias = (model.alias ?? '').trim() || previous?.alias;
|
||||
const displayName = (model.displayName ?? '').trim() || previous?.displayName;
|
||||
if (alias && alias !== name) next.alias = alias;
|
||||
else delete next.alias;
|
||||
if (displayName && displayName !== name) next.displayName = displayName;
|
||||
else delete next.displayName;
|
||||
merged.set(modelKey(name), next);
|
||||
});
|
||||
return Array.from(merged.values());
|
||||
}
|
||||
@@ -102,7 +127,7 @@ export const modelEndpointCandidates = (provider: ModelProvider, baseUrl: string
|
||||
return [/\/v1$/i.test(base) ? `${base}/models` : `${base}/v1/models`];
|
||||
};
|
||||
|
||||
const normalizeModelList = (payload: unknown): ModelOption[] => {
|
||||
const normalizeModelList = (payload: unknown, preserveExistingAlias = false): ModelOption[] => {
|
||||
const parsed = typeof payload === 'string' ? (() => {
|
||||
try { return JSON.parse(payload) as unknown; } catch { return payload; }
|
||||
})() : payload;
|
||||
@@ -114,13 +139,17 @@ const normalizeModelList = (payload: unknown): ModelOption[] => {
|
||||
const name = typeof item === 'string' ? item : isRecord(item) ? readString(item, 'id', 'name', 'model', 'value') : '';
|
||||
if (!name || seen.has(name.toLowerCase())) return null;
|
||||
seen.add(name.toLowerCase());
|
||||
const alias = typeof item === 'object' && isRecord(item) ? readString(item, 'alias', 'display_name', 'displayName') : '';
|
||||
const thinking = typeof item === 'object' && isRecord(item) && isRecord(item.thinking)
|
||||
? { ...item.thinking }
|
||||
const record = typeof item === 'object' && isRecord(item) ? item : null;
|
||||
const rawAlias = record ? readString(record, 'alias') : '';
|
||||
const alias = preserveExistingAlias ? rawAlias : usableModelAlias(rawAlias);
|
||||
const displayName = record ? readString(record, 'display-name', 'display_name', 'displayName') : '';
|
||||
const thinking = record && isRecord(record.thinking)
|
||||
? { ...record.thinking }
|
||||
: undefined;
|
||||
return {
|
||||
name,
|
||||
...(alias && alias !== name ? { alias } : {}),
|
||||
...(displayName && displayName !== name ? { displayName } : {}),
|
||||
...(thinking ? { thinking } : {}),
|
||||
};
|
||||
}).filter((item): item is ModelOption => item !== null);
|
||||
@@ -128,7 +157,11 @@ const normalizeModelList = (payload: unknown): ModelOption[] => {
|
||||
|
||||
export function modelsFromRecord(value: unknown): ModelOption[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return normalizeModelList(value);
|
||||
return normalizeModelList(value, true);
|
||||
}
|
||||
|
||||
export function modelsFromDiscoveredPayload(payload: unknown): ModelOption[] {
|
||||
return normalizeModelList(payload);
|
||||
}
|
||||
|
||||
export async function fetchModels(
|
||||
@@ -186,7 +219,7 @@ export async function fetchModels(
|
||||
}
|
||||
|
||||
const payload = response.body ?? response.bodyText;
|
||||
normalizeModelList(payload).forEach((model) => {
|
||||
modelsFromDiscoveredPayload(payload).forEach((model) => {
|
||||
const name = provider === 'gemini' ? model.name.replace(/^models\//i, '') : model.name;
|
||||
const dedupeKey = name.toLowerCase();
|
||||
if (!name || seen.has(dedupeKey)) return;
|
||||
@@ -216,7 +249,7 @@ export async function fetchModels(
|
||||
}, { timeoutMs });
|
||||
const status = Number(response.status_code ?? response.statusCode ?? 0);
|
||||
if (status >= 200 && status < 300) {
|
||||
const models = normalizeModelList(response.body ?? response.bodyText);
|
||||
const models = modelsFromDiscoveredPayload(response.body ?? response.bodyText);
|
||||
if (models.length) return models;
|
||||
}
|
||||
}
|
||||
@@ -230,7 +263,7 @@ export async function fetchModels(
|
||||
}, { timeoutMs });
|
||||
const status = Number(response.status_code ?? response.statusCode ?? 0);
|
||||
if (status >= 200 && status < 300) {
|
||||
const models = normalizeModelList(response.body ?? response.bodyText);
|
||||
const models = modelsFromDiscoveredPayload(response.body ?? response.bodyText);
|
||||
if (models.length) return models;
|
||||
}
|
||||
} catch {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import {
|
||||
mergeModelOptions,
|
||||
modelSearchText,
|
||||
modelsFromDiscoveredPayload,
|
||||
modelsFromRecord,
|
||||
reconcileModelSelection,
|
||||
usableModelAlias,
|
||||
} from '../src/services/modelService';
|
||||
|
||||
describe('model discovery selection', () => {
|
||||
@@ -43,4 +47,68 @@ describe('model discovery selection', () => {
|
||||
[{ name: ' DEEPSEEK-CHAT ', alias: 'Chat' }],
|
||||
)).toEqual([{ name: 'DEEPSEEK-CHAT', alias: 'Chat' }]);
|
||||
});
|
||||
|
||||
it('keeps display names for search without treating them as aliases', () => {
|
||||
expect(mergeModelOptions(
|
||||
[{ name: 'codex-auto-review', displayName: 'Codex Auto Review' }],
|
||||
[{ name: 'codex-auto-review', alias: 'review-alias' }],
|
||||
)).toEqual([{
|
||||
name: 'codex-auto-review',
|
||||
alias: 'review-alias',
|
||||
displayName: 'Codex Auto Review',
|
||||
}]);
|
||||
expect(modelSearchText({
|
||||
name: 'codex-auto-review',
|
||||
displayName: 'Codex Auto Review',
|
||||
})).toContain('codex auto review');
|
||||
});
|
||||
});
|
||||
|
||||
describe('model catalog parsing', () => {
|
||||
it('does not treat display names as aliases', () => {
|
||||
expect(modelsFromRecord([
|
||||
{
|
||||
name: 'codex-auto-review',
|
||||
display_name: 'Codex Auto Review',
|
||||
displayName: 'Ignored Fallback',
|
||||
},
|
||||
{
|
||||
id: 'gpt-5.6-terra',
|
||||
'display-name': 'GPT-5.6 Terra',
|
||||
},
|
||||
])).toEqual([
|
||||
{ name: 'codex-auto-review', displayName: 'Codex Auto Review' },
|
||||
{ name: 'gpt-5.6-terra', displayName: 'GPT-5.6 Terra' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('preserves existing aliases from saved configuration, including spaced ones', () => {
|
||||
expect(modelsFromRecord([
|
||||
{ name: 'codex-auto-review', alias: 'Codex Auto Review' },
|
||||
{ name: 'gpt-test', alias: 'review-alias' },
|
||||
])).toEqual([
|
||||
{ name: 'codex-auto-review', alias: 'Codex Auto Review' },
|
||||
{ name: 'gpt-test', alias: 'review-alias' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('rejects whitespace in newly entered aliases', () => {
|
||||
expect(usableModelAlias('Codex Auto Review')).toBe('');
|
||||
expect(usableModelAlias(' review-alias ')).toBe('review-alias');
|
||||
expect(usableModelAlias('')).toBe('');
|
||||
});
|
||||
|
||||
it('does not import spaced display names as aliases when discovering models', () => {
|
||||
expect(modelsFromDiscoveredPayload({
|
||||
data: [
|
||||
{ id: 'codex-auto-review', display_name: 'Codex Auto Review' },
|
||||
{ id: 'gpt-test', alias: 'Codex Auto Review' },
|
||||
{ id: 'gpt-alias', alias: 'review-alias' },
|
||||
],
|
||||
})).toEqual([
|
||||
{ name: 'codex-auto-review', displayName: 'Codex Auto Review' },
|
||||
{ name: 'gpt-test' },
|
||||
{ name: 'gpt-alias', alias: 'review-alias' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -25,13 +25,83 @@ it('saves non-empty custom model names and removes duplicate or blank entries',
|
||||
baseUrl: 'https://api.example.com',
|
||||
priority: '',
|
||||
models: [
|
||||
{ name: ' custom-model ', alias: ' Custom Alias ' },
|
||||
{ name: ' custom-model ', alias: ' custom-alias ' },
|
||||
{ name: ' ' },
|
||||
{ name: 'CUSTOM-MODEL', alias: 'duplicate' },
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.models).toEqual([{ name: 'custom-model', alias: 'Custom Alias' }]);
|
||||
expect(result.models).toEqual([{ name: 'custom-model', alias: 'custom-alias' }]);
|
||||
});
|
||||
|
||||
it('does not persist display names as model aliases', () => {
|
||||
const result = buildProviderRecord('codex-api-key', {
|
||||
name: '',
|
||||
apiKey: 'codex-key',
|
||||
baseUrl: 'https://www.loomex.cc',
|
||||
priority: '',
|
||||
models: [
|
||||
{ name: 'codex-auto-review', displayName: 'Codex Auto Review' },
|
||||
{ name: 'gpt-test', alias: 'review-alias', displayName: 'GPT Test' },
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.models).toEqual([
|
||||
{ name: 'codex-auto-review' },
|
||||
{ name: 'gpt-test', alias: 'review-alias' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('rejects newly entered aliases that contain whitespace', () => {
|
||||
expect(() => buildProviderRecord('codex-api-key', {
|
||||
name: '',
|
||||
apiKey: 'codex-key',
|
||||
baseUrl: 'https://www.loomex.cc',
|
||||
priority: '',
|
||||
models: [{ name: 'codex-auto-review', alias: 'Codex Auto Review' }],
|
||||
})).toThrow(/空白|whitespace|空白文字/);
|
||||
});
|
||||
|
||||
it('preserves an existing spaced alias until the user changes it', () => {
|
||||
const result = buildProviderRecord(
|
||||
'codex-api-key',
|
||||
{
|
||||
name: '',
|
||||
apiKey: 'codex-key',
|
||||
baseUrl: 'https://www.loomex.cc',
|
||||
priority: '',
|
||||
models: [{ name: 'codex-auto-review', alias: 'Codex Auto Review' }],
|
||||
},
|
||||
{
|
||||
'api-key': 'codex-key',
|
||||
models: [{ name: 'codex-auto-review', alias: 'Codex Auto Review' }],
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.models).toEqual([
|
||||
{ name: 'codex-auto-review', alias: 'Codex Auto Review' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps an existing spaced alias when only the letter case changes', () => {
|
||||
const result = buildProviderRecord(
|
||||
'codex-api-key',
|
||||
{
|
||||
name: '',
|
||||
apiKey: 'codex-key',
|
||||
baseUrl: 'https://www.loomex.cc',
|
||||
priority: '',
|
||||
models: [{ name: 'codex-auto-review', alias: 'codex auto review' }],
|
||||
},
|
||||
{
|
||||
'api-key': 'codex-key',
|
||||
models: [{ name: 'codex-auto-review', alias: 'Codex Auto Review' }],
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.models).toEqual([
|
||||
{ name: 'codex-auto-review', alias: 'Codex Auto Review' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('parses multiline API keys into unique trimmed entries', () => {
|
||||
|
||||
Reference in new issue
Block a user