mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 08:17:11 +00:00
fix(cliproxy): tolerate plain AI provider model rules
This commit is contained in:
@@ -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>
|
||||
@@ -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: '' }]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user