Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions llm-router/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@
"type": "module",
"scripts": {
"build": "tsc --noEmit && node build.mjs",
"watch": "node build.mjs --watch"
"watch": "node build.mjs --watch",
"test": "vitest run"
},
"dependencies": {
"@iii-dev/console-ui": "workspace:*"
},
"devDependencies": {
"@types/react": "^19.2.14",
"esbuild": "^0.25.0",
"typescript": "^5.9.2"
"typescript": "^5.9.2",
"vitest": "^4.1.6"
}
}
3 changes: 2 additions & 1 deletion llm-router/ui/src/configuration/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
Select,
type SelectOption,
} from '@iii-dev/console-ui'
import { providerCardIds } from './provider-cards'

type JsonObject = { [key: string]: JsonValue }

Expand Down Expand Up @@ -136,7 +137,7 @@ const SETTINGS_FIELDS = [
export function LlmRouterConfigForm(props: ConfigFormProps) {
const value = asObject(props.value)
const providers = asObject(value.providers)
const providerIds = Object.keys(providers)
const providerIds = providerCardIds(props.schema, props.value)
const settings = asObject(value.settings)
const heuristics = Array.isArray(value.routing_heuristics)
? (value.routing_heuristics as JsonValue[])
Expand Down
32 changes: 32 additions & 0 deletions llm-router/ui/src/configuration/provider-cards.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { describe, expect, it } from 'vitest'
import { providerCardIds } from './provider-cards'

const schema = {
properties: {
providers: {
properties: {
anthropic: {},
openai: {},
},
},
},
}

describe('providerCardIds', () => {
it('shows providers from the schema when fresh-install config is null', () => {
expect(providerCardIds(schema, null)).toEqual(['anthropic', 'openai'])
})

it('keeps configured providers that are no longer in the schema', () => {
expect(
providerCardIds(schema, {
providers: { openai: { api_key: 'configured' }, custom: {} },
}),
).toEqual(['anthropic', 'openai', 'custom'])
})

it('returns no cards only when neither source contains providers', () => {
expect(providerCardIds(null, null)).toEqual([])
expect(providerCardIds({ properties: {} }, {})).toEqual([])
})
})
34 changes: 34 additions & 0 deletions llm-router/ui/src/configuration/provider-cards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Build the provider card list from both the registered schema and the saved
* configuration. A fresh installation has a null configuration value, while
* its schema already contains the providers that operators can configure.
*/

type JsonObject = Record<string, unknown>

function asObject(value: unknown): JsonObject {
return value !== null && typeof value === 'object' && !Array.isArray(value)
? (value as JsonObject)
: {}
}

function schemaProviderIds(
schema: Record<string, unknown> | null,
): string[] {
const properties = asObject(schema?.properties)
const providers = asObject(properties.providers)
return Object.keys(asObject(providers.properties))
}

export function providerCardIds(
schema: Record<string, unknown> | null,
value: unknown,
): string[] {
const configuredProviders = asObject(asObject(value).providers)
return [
...new Set([
...schemaProviderIds(schema),
...Object.keys(configuredProviders),
]),
]
}
7 changes: 7 additions & 0 deletions llm-router/ui/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
environment: 'node',
},
})
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading