mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 08:19:59 +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 }));
|
return Object.entries(headers || {}).map(([key, value]) => ({ key, value }));
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeModelAliases(models: AiProviderModelAlias[] | undefined): AiProviderModelAlias[] {
|
function readModelRulePart(model: unknown, key: keyof AiProviderModelAlias) {
|
||||||
return (models || [])
|
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) => ({
|
.map((model) => ({
|
||||||
name: model.name.trim(),
|
name: readModelRulePart(model, 'name'),
|
||||||
alias: model.alias.trim(),
|
alias: readModelRulePart(model, 'alias'),
|
||||||
}))
|
}))
|
||||||
.filter((model) => model.name.length > 0 || model.alias.length > 0);
|
.filter((model) => model.name.length > 0 || model.alias.length > 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,4 +144,24 @@ describe('ai-provider service stable ids', () => {
|
|||||||
'sk-openrouter'
|
'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: '' }]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import { describe, expect, it } from 'bun:test';
|
import { describe, expect, it } from 'bun:test';
|
||||||
import { getCliproxyRoutingManagementUrl } from '../../../src/cliproxy/routing-strategy-http';
|
|
||||||
import type { ProxyTarget } from '../../../src/cliproxy/proxy-target-resolver';
|
import type { ProxyTarget } from '../../../src/cliproxy/proxy-target-resolver';
|
||||||
|
|
||||||
|
async function loadRoutingHttpModule() {
|
||||||
|
return import(
|
||||||
|
`../../../src/cliproxy/routing-strategy-http?test=${Date.now()}-${Math.random()}`
|
||||||
|
) as Promise<typeof import('../../../src/cliproxy/routing-strategy-http')>;
|
||||||
|
}
|
||||||
|
|
||||||
describe('routing-strategy-http', () => {
|
describe('routing-strategy-http', () => {
|
||||||
it('builds the local management URL for routing strategy reads', () => {
|
it('builds the local management URL for routing strategy reads', async () => {
|
||||||
|
const { getCliproxyRoutingManagementUrl } = await loadRoutingHttpModule();
|
||||||
const target: ProxyTarget = {
|
const target: ProxyTarget = {
|
||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port: 8317,
|
port: 8317,
|
||||||
@@ -16,7 +22,8 @@ describe('routing-strategy-http', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('builds the remote management URL for routing strategy writes', () => {
|
it('builds the remote management URL for routing strategy writes', async () => {
|
||||||
|
const { getCliproxyRoutingManagementUrl } = await loadRoutingHttpModule();
|
||||||
const target: ProxyTarget = {
|
const target: ProxyTarget = {
|
||||||
host: 'proxy.example.com',
|
host: 'proxy.example.com',
|
||||||
port: 443,
|
port: 443,
|
||||||
|
|||||||
Reference in New Issue
Block a user