Skip to content

Commit 77d9806

Browse files
yethikrishnaclaude
andcommitted
feat: universal multi-provider system + fix all type errors
Multi-Provider System (13 new files): - common/src/providers/: Types (Zod), registry (35 providers, 9 categories), filesystem ops, connection testing, auto-detect (Ollama/LM Studio), models.dev catalog with caching - sdk/src/impl/multi-provider.ts: LanguageModel factory for any provider - sdk/src/impl/model-provider.ts: Integrated multi-provider routing - cli/src/state/provider-store.ts: Zustand store with Immer - cli/src/components/: Provider wizard, model picker, settings panel TUI - cli/src/commands/command-registry.ts: 9 new slash commands (/provider:add, /provider:list, /provider:remove, /provider:test, /model:list, /model:set, /model:info, /settings) - cli/src/chat.tsx: Wired new screens into CommandResult system Type Error Fixes (22 modified files): - TeamTask priority: Added to Zod schema + all test data - agent-runtime: Fixed JSONValue/JSONObject return types - team-presets.test.ts: Replaced it.each() with for...of loops - notification-badge: "round" -> "rounded" border style - chat.tsx: borderTop/Bottom -> border - fingerprint.ts: Added systeminformation.d.ts type declaration - team-api.test.ts: Fixed createTeam signature + TeamStatus properties Bump to 0.2.6 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 82a64f3 commit 77d9806

35 files changed

Lines changed: 2913 additions & 125 deletions

cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@levelcode/cli",
3-
"version": "0.2.5",
3+
"version": "0.2.6",
44
"description": "LevelCode CLI - Terminal-based AI coding agent that outperforms Claude Code",
55
"author": {
66
"name": "Yethikrishna R",

cli/src/chat.tsx

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { AnalyticsEvent } from '@levelcode/common/constants/analytics-events'
22
import { isStandaloneMode } from '@levelcode/sdk'
33
import open from 'open'
4+
import { ProviderWizard } from './components/provider-wizard'
5+
import { ModelPicker } from './components/model-picker'
6+
import { SettingsPanel } from './components/settings-panel'
47
import {
58
useCallback,
69
useEffect,
@@ -124,6 +127,9 @@ export const Chat = ({
124127
onSwitchToGitRoot?: () => void
125128
}) => {
126129
const [forceFileOnlyMentions, setForceFileOnlyMentions] = useState(false)
130+
const [providerWizardMode, setProviderWizardMode] = useState(false)
131+
const [modelPickerMode, setModelPickerMode] = useState(false)
132+
const [providerSettingsMode, setProviderSettingsMode] = useState(false)
127133

128134
const { validate: validateAgents } = useAgentValidation()
129135

@@ -693,6 +699,18 @@ export const Chat = ({
693699
if (result.openTeamSettings) {
694700
useTeamSettingsStore.getState().openSettingsScreen()
695701
}
702+
703+
if (result.openProviderWizard) {
704+
setProviderWizardMode(true)
705+
}
706+
707+
if (result.openModelPicker) {
708+
setModelPickerMode(true)
709+
}
710+
711+
if (result.openSettings) {
712+
setProviderSettingsMode(true)
713+
}
696714
},
697715
[
698716
saveCurrentInput,
@@ -1415,8 +1433,7 @@ export const Chat = ({
14151433
<box
14161434
style={{
14171435
flexShrink: 0,
1418-
borderTop: true,
1419-
borderBottom: true,
1436+
border: ['top', 'bottom'],
14201437
borderColor: theme.border,
14211438
}}
14221439
>
@@ -1445,6 +1462,18 @@ export const Chat = ({
14451462
<TeamSettingsScreen onClose={closeTeamSettings} />
14461463
)}
14471464

1465+
{providerWizardMode && (
1466+
<ProviderWizard onClose={() => setProviderWizardMode(false)} />
1467+
)}
1468+
1469+
{modelPickerMode && (
1470+
<ModelPicker onClose={() => setModelPickerMode(false)} />
1471+
)}
1472+
1473+
{providerSettingsMode && (
1474+
<SettingsPanel onClose={() => setProviderSettingsMode(false)} />
1475+
)}
1476+
14481477
{reviewMode ? (
14491478
<ReviewScreen
14501479
onSelectOption={handleReviewOptionSelect}

cli/src/commands/command-registry.ts

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,17 @@ import {
3434
import { AnalyticsEvent } from '@levelcode/common/constants/analytics-events'
3535
import { dispatchTeamHookEvent } from '@levelcode/common/utils/team-hook-emitter'
3636
import { trackEvent } from '../utils/analytics'
37+
import { useProviderStore } from '../state/provider-store'
38+
import {
39+
loadProviderConfig,
40+
removeProvider as removeProviderFromConfig,
41+
setActiveModel as setActiveModelInConfig,
42+
} from '@levelcode/common/providers/provider-fs'
43+
import { testProvider } from '@levelcode/common/providers/provider-test'
44+
import {
45+
getProviderDefinition,
46+
PROVIDER_DEFINITIONS,
47+
} from '@levelcode/common/providers/provider-registry'
3748

3849
import type { PhaseTransitionHookEvent } from '@levelcode/common/types/team-hook-events'
3950
import type { DevPhase, TeamConfig } from '@levelcode/common/types/team-config'
@@ -79,6 +90,9 @@ export type CommandResult = {
7990
openChatHistory?: boolean
8091
openReviewScreen?: boolean
8192
openTeamSettings?: boolean
93+
openProviderWizard?: boolean
94+
openModelPicker?: boolean
95+
openSettings?: boolean
8296
preSelectAgents?: string[]
8397
} | void
8498

@@ -840,6 +854,224 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [
840854
return { openTeamSettings: true }
841855
},
842856
}),
857+
// ── Provider & model commands ──────────────────────────────────────────
858+
defineCommand({
859+
name: 'provider:add',
860+
aliases: ['connect'],
861+
handler: (params) => {
862+
params.saveToHistory(params.inputValue.trim())
863+
clearInput(params)
864+
return { openProviderWizard: true }
865+
},
866+
}),
867+
defineCommand({
868+
name: 'provider:list',
869+
handler: async (params) => {
870+
const config = await loadProviderConfig()
871+
const providerIds = Object.keys(config.providers)
872+
873+
if (providerIds.length === 0) {
874+
params.setMessages((prev) => [
875+
...prev,
876+
getUserMessage(params.inputValue.trim()),
877+
getSystemMessage('No providers configured. Use /provider:add to add one.'),
878+
])
879+
} else {
880+
const lines = providerIds.map((id) => {
881+
const entry = config.providers[id]!
882+
const def = getProviderDefinition(id)
883+
const name = def?.name ?? entry.displayName ?? id
884+
const status = entry.enabled ? '●' : '○'
885+
const models = [...entry.models, ...entry.customModelIds]
886+
const modelCount = models.length > 0 ? ` (${models.length} models)` : ''
887+
const auto = entry.autoDetected ? ' [auto]' : ''
888+
return ` ${status} ${name}${modelCount}${auto}`
889+
})
890+
891+
const active = config.activeProvider
892+
? `Active: ${config.activeProvider}/${config.activeModel ?? 'none'}`
893+
: 'No active model set'
894+
895+
params.setMessages((prev) => [
896+
...prev,
897+
getUserMessage(params.inputValue.trim()),
898+
getSystemMessage(`Providers:\n${lines.join('\n')}\n\n${active}`),
899+
])
900+
}
901+
params.saveToHistory(params.inputValue.trim())
902+
clearInput(params)
903+
},
904+
}),
905+
defineCommandWithArgs({
906+
name: 'provider:remove',
907+
handler: async (params, args) => {
908+
const providerId = args.trim()
909+
if (!providerId) {
910+
params.setMessages((prev) => [
911+
...prev,
912+
getUserMessage(params.inputValue.trim()),
913+
getSystemMessage('Usage: /provider:remove <provider-id>'),
914+
])
915+
params.saveToHistory(params.inputValue.trim())
916+
clearInput(params)
917+
return
918+
}
919+
920+
try {
921+
await removeProviderFromConfig(providerId)
922+
params.setMessages((prev) => [
923+
...prev,
924+
getUserMessage(params.inputValue.trim()),
925+
getSystemMessage(`Provider "${providerId}" removed.`),
926+
])
927+
} catch (error) {
928+
params.setMessages((prev) => [
929+
...prev,
930+
getUserMessage(params.inputValue.trim()),
931+
getSystemMessage(`Failed to remove provider: ${error instanceof Error ? error.message : String(error)}`),
932+
])
933+
}
934+
params.saveToHistory(params.inputValue.trim())
935+
clearInput(params)
936+
},
937+
}),
938+
defineCommandWithArgs({
939+
name: 'provider:test',
940+
handler: async (params, args) => {
941+
const providerId = args.trim()
942+
if (!providerId) {
943+
params.setMessages((prev) => [
944+
...prev,
945+
getUserMessage(params.inputValue.trim()),
946+
getSystemMessage('Usage: /provider:test <provider-id>'),
947+
])
948+
params.saveToHistory(params.inputValue.trim())
949+
clearInput(params)
950+
return
951+
}
952+
953+
params.setMessages((prev) => [
954+
...prev,
955+
getUserMessage(params.inputValue.trim()),
956+
getSystemMessage(`Testing ${providerId}...`),
957+
])
958+
959+
const config = await loadProviderConfig()
960+
const entry = config.providers[providerId]
961+
const result = await testProvider(providerId, entry?.apiKey, entry?.baseUrl)
962+
963+
const statusIcon = result.success ? '●' : '○'
964+
const latency = result.latencyMs.toFixed(0)
965+
const modelInfo = result.models ? ` (${result.models.length} models)` : ''
966+
const errorInfo = result.error ? `\nError: ${result.error}` : ''
967+
968+
params.setMessages((prev) => [
969+
...prev,
970+
getSystemMessage(`${statusIcon} ${result.providerName}: ${latency}ms${modelInfo}${errorInfo}`),
971+
])
972+
params.saveToHistory(params.inputValue.trim())
973+
clearInput(params)
974+
},
975+
}),
976+
defineCommand({
977+
name: 'model:list',
978+
aliases: ['models'],
979+
handler: (params) => {
980+
params.saveToHistory(params.inputValue.trim())
981+
clearInput(params)
982+
return { openModelPicker: true }
983+
},
984+
}),
985+
defineCommandWithArgs({
986+
name: 'model:set',
987+
handler: async (params, args) => {
988+
const modelSpec = args.trim()
989+
if (!modelSpec) {
990+
params.setMessages((prev) => [
991+
...prev,
992+
getUserMessage(params.inputValue.trim()),
993+
getSystemMessage('Usage: /model:set <provider/model> (e.g., /model:set anthropic/claude-sonnet-4.5)'),
994+
])
995+
params.saveToHistory(params.inputValue.trim())
996+
clearInput(params)
997+
return
998+
}
999+
1000+
const parts = modelSpec.split('/')
1001+
let providerId: string
1002+
let modelId: string
1003+
1004+
if (parts.length >= 2) {
1005+
providerId = parts[0]!
1006+
modelId = parts.slice(1).join('/')
1007+
} else {
1008+
// Try to find provider from active config
1009+
const config = await loadProviderConfig()
1010+
providerId = config.activeProvider ?? 'openrouter'
1011+
modelId = modelSpec
1012+
}
1013+
1014+
try {
1015+
await setActiveModelInConfig(providerId, modelId)
1016+
params.setMessages((prev) => [
1017+
...prev,
1018+
getUserMessage(params.inputValue.trim()),
1019+
getSystemMessage(`Active model set to ${providerId}/${modelId}`),
1020+
])
1021+
} catch (error) {
1022+
params.setMessages((prev) => [
1023+
...prev,
1024+
getUserMessage(params.inputValue.trim()),
1025+
getSystemMessage(`Failed to set model: ${error instanceof Error ? error.message : String(error)}`),
1026+
])
1027+
}
1028+
params.saveToHistory(params.inputValue.trim())
1029+
clearInput(params)
1030+
},
1031+
}),
1032+
defineCommand({
1033+
name: 'model:info',
1034+
handler: async (params) => {
1035+
const config = await loadProviderConfig()
1036+
1037+
if (!config.activeModel || !config.activeProvider) {
1038+
params.setMessages((prev) => [
1039+
...prev,
1040+
getUserMessage(params.inputValue.trim()),
1041+
getSystemMessage('No active model set. Use /model:set <provider/model> to set one.'),
1042+
])
1043+
params.saveToHistory(params.inputValue.trim())
1044+
clearInput(params)
1045+
return
1046+
}
1047+
1048+
const def = getProviderDefinition(config.activeProvider)
1049+
const providerName = def?.name ?? config.activeProvider
1050+
1051+
const lines = [
1052+
`Model: ${config.activeModel}`,
1053+
`Provider: ${providerName}`,
1054+
`API Format: ${def?.apiFormat ?? 'unknown'}`,
1055+
`Base URL: ${def?.baseUrl ?? 'custom'}`,
1056+
]
1057+
1058+
params.setMessages((prev) => [
1059+
...prev,
1060+
getUserMessage(params.inputValue.trim()),
1061+
getSystemMessage(lines.join('\n')),
1062+
])
1063+
params.saveToHistory(params.inputValue.trim())
1064+
clearInput(params)
1065+
},
1066+
}),
1067+
defineCommand({
1068+
name: 'settings',
1069+
handler: (params) => {
1070+
params.saveToHistory(params.inputValue.trim())
1071+
clearInput(params)
1072+
return { openSettings: true }
1073+
},
1074+
}),
8431075
// Mode commands generated from AGENT_MODES
8441076
...AGENT_MODES.map((mode) =>
8451077
defineCommandWithArgs({

0 commit comments

Comments
 (0)