fix(cliproxy): tolerate plain AI provider model rules

This commit is contained in:
Tam Nhu Tran
2026-04-28 14:19:21 -04:00
parent 28a22a1b8a
commit 9a28ca55d8
3 changed files with 224 additions and 4 deletions
+191
View File
@@ -0,0 +1,191 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Issue 1118 Evidence - AI Providers model rules</title>
<style>
:root {
color-scheme: light;
--bg: #f8fafc;
--panel: #ffffff;
--ink: #0f172a;
--muted: #475569;
--line: #cbd5e1;
--bad: #b91c1c;
--good: #047857;
--code: #111827;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font:
15px/1.55 Inter,
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
}
main {
width: min(1040px, calc(100vw - 48px));
margin: 0 auto;
padding: 48px 0;
}
header {
margin-bottom: 24px;
}
h1,
h2 {
margin: 0;
letter-spacing: 0;
}
h1 {
font-size: 30px;
line-height: 1.15;
}
h2 {
font-size: 18px;
}
p {
margin: 8px 0 0;
color: var(--muted);
}
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
margin-top: 24px;
}
.panel {
border: 1px solid var(--line);
border-radius: 8px;
background: var(--panel);
padding: 20px;
}
.status {
display: inline-flex;
align-items: center;
gap: 8px;
margin-top: 14px;
border-radius: 999px;
padding: 4px 10px;
font-size: 13px;
font-weight: 700;
}
.status.bad {
background: #fee2e2;
color: var(--bad);
}
.status.good {
background: #dcfce7;
color: var(--good);
}
pre {
overflow-x: auto;
margin: 16px 0 0;
border-radius: 8px;
background: var(--code);
color: #f8fafc;
padding: 16px;
font-size: 13px;
line-height: 1.45;
}
ul {
margin: 12px 0 0;
padding-left: 20px;
color: var(--muted);
}
li + li {
margin-top: 6px;
}
@media (max-width: 760px) {
main {
width: min(100vw - 32px, 1040px);
padding: 32px 0;
}
.grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<main>
<header>
<h1>Issue 1118 Evidence</h1>
<p>
AI Providers route should tolerate plain OpenAI-compatible model rules
where an alias is omitted.
</p>
</header>
<section class="grid" aria-label="before and after evidence">
<article class="panel">
<h2>Before: installed dev build</h2>
<p>
The current installed CLI dashboard API crashes while reading the
local AI provider config.
</p>
<div class="status bad">HTTP 500</div>
<pre><code>before: HTTP 500
{
"error": "Cannot read properties of undefined (reading 'trim')"
}</code></pre>
</article>
<article class="panel">
<h2>After: patched worktree build</h2>
<p>
The same local config now loads, and omitted aliases normalize to an
empty string.
</p>
<div class="status good">HTTP 200</div>
<pre><code>after: HTTP 200
{
"familyCount": 5,
"openaiCompatibilityEntryCount": 1,
"normalizedModelAliases": [
{
"hasName": true,
"alias": ""
}
]
}</code></pre>
</article>
</section>
<section class="panel" style="margin-top: 16px">
<h2>Validation</h2>
<ul>
<li>Regression covers model entries with missing aliases.</li>
<li>Malformed model entries are ignored instead of crashing the API.</li>
<li>The AI Providers dashboard route loads against the patched server.</li>
</ul>
</section>
</main>
</body>
</html>
+13 -4
View File
@@ -36,11 +36,20 @@ function toHeaderPairs(
return Object.entries(headers || {}).map(([key, value]) => ({ key, value }));
}
function normalizeModelAliases(models: AiProviderModelAlias[] | undefined): AiProviderModelAlias[] {
return (models || [])
function readModelRulePart(model: unknown, key: keyof AiProviderModelAlias) {
if (!model || typeof model !== 'object') {
return '';
}
const value = (model as Partial<Record<keyof AiProviderModelAlias, unknown>>)[key];
return typeof value === 'string' ? value.trim() : '';
}
function normalizeModelAliases(models: unknown): AiProviderModelAlias[] {
return (Array.isArray(models) ? models : [])
.map((model) => ({
name: model.name.trim(),
alias: model.alias.trim(),
name: readModelRulePart(model, 'name'),
alias: readModelRulePart(model, 'alias'),
}))
.filter((model) => model.name.length > 0 || model.alias.length > 0);
}
@@ -144,4 +144,24 @@ describe('ai-provider service stable ids', () => {
'sk-openrouter'
);
});
it('normalizes plain openai-compatible model rules without aliases', async () => {
const { listAiProviders } = await loadAiProviderService();
writeCliproxyConfig(tempHome, {
'openai-compatibility': [
{
name: 'openrouter',
'base-url': 'https://openrouter.ai/api/v1',
'api-key-entries': [{ 'api-key': 'sk-openrouter' }],
models: [{ name: 'gpt-4o-mini' }, null, { name: 42 }],
},
],
});
const listed = await listAiProviders();
const family = listed.families.find((entry) => entry.id === 'openai-compatibility');
expect(family?.entries[0]?.models).toEqual([{ name: 'gpt-4o-mini', alias: '' }]);
});
});