diff --git a/.github/pr-assets/issue-1118/index.html b/.github/pr-assets/issue-1118/index.html new file mode 100644 index 00000000..eb4c4875 --- /dev/null +++ b/.github/pr-assets/issue-1118/index.html @@ -0,0 +1,191 @@ + + + + + + Issue 1118 Evidence - AI Providers model rules + + + +
+
+

Issue 1118 Evidence

+

+ AI Providers route should tolerate plain OpenAI-compatible model rules + where an alias is omitted. +

+
+ +
+
+

Before: installed dev build

+

+ The current installed CLI dashboard API crashes while reading the + local AI provider config. +

+
HTTP 500
+
before: HTTP 500
+{
+  "error": "Cannot read properties of undefined (reading 'trim')"
+}
+
+ +
+

After: patched worktree build

+

+ The same local config now loads, and omitted aliases normalize to an + empty string. +

+
HTTP 200
+
after: HTTP 200
+{
+  "familyCount": 5,
+  "openaiCompatibilityEntryCount": 1,
+  "normalizedModelAliases": [
+    {
+      "hasName": true,
+      "alias": ""
+    }
+  ]
+}
+
+
+ +
+

Validation

+ +
+
+ + diff --git a/src/cliproxy/ai-providers/service.ts b/src/cliproxy/ai-providers/service.ts index 12a5e496..d35982f4 100644 --- a/src/cliproxy/ai-providers/service.ts +++ b/src/cliproxy/ai-providers/service.ts @@ -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>)[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); } diff --git a/tests/unit/cliproxy/ai-provider-service-stable-id.test.ts b/tests/unit/cliproxy/ai-provider-service-stable-id.test.ts index a4de2cf6..f317a8e1 100644 --- a/tests/unit/cliproxy/ai-provider-service-stable-id.test.ts +++ b/tests/unit/cliproxy/ai-provider-service-stable-id.test.ts @@ -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: '' }]); + }); });