@@ -47,11 +47,13 @@ import { SearchProviderPicker } from "@/components/search-provider-picker";
4747import { McpConfigDialog } from "@/components/mcp-config-dialog" ;
4848import { CustomApiDialog } from "@/components/custom-api-dialog" ;
4949import {
50- PROVIDERS ,
50+ getBuiltinProviders ,
5151 parseModelString ,
5252 createCustomProviderInfo ,
5353 isCustomProvider ,
5454 CUSTOM_PROVIDER_PREFIX ,
55+ OPENROUTER_PROVIDER_ID ,
56+ OPENROUTER_STARTER_MODELS ,
5557} from "@/lib/model-registry" ;
5658import { Switch } from "@/components/ui/switch" ;
5759
@@ -108,6 +110,7 @@ function KittLoader() {
108110const abortControllers = new Map < string , AbortController > ( ) ;
109111const MAX_RETRIES = 2 ;
110112const RETRY_DELAY_MS = 1500 ;
113+ const OPENROUTER_STARTER_MODEL_IDS = new Set ( OPENROUTER_STARTER_MODELS . map ( ( model ) => model . id ) ) ;
111114
112115function ReasoningBlock ( {
113116 text,
@@ -461,7 +464,7 @@ async function streamToWidget(
461464 }
462465}
463466
464- function useModelSelector ( ) {
467+ function useModelSelector ( { hasOpenRouterStarter } : { hasOpenRouterStarter : boolean } ) {
465468 const selectedModel = useSettingsStore ( ( s ) => s . selectedModel ) ;
466469 const setModel = useSettingsStore ( ( s ) => s . setModel ) ;
467470 const apiKeys = useSettingsStore ( ( s ) => s . apiKeys ) ;
@@ -475,27 +478,43 @@ function useModelSelector() {
475478 const [ customApiOpen , setCustomApiOpen ] = useState ( false ) ;
476479
477480 const { providerId, modelId } = parseModelString ( selectedModel ) ;
481+ const hasOpenRouterByokKey = ! ! apiKeys [ OPENROUTER_PROVIDER_ID ] ;
478482
479483 const allProviders = useMemo ( ( ) => {
484+ const builtInProviders = getBuiltinProviders ( {
485+ includeOpenRouterByokModels : hasOpenRouterByokKey ,
486+ } ) ;
480487 const customProviders = customApis
481488 . filter ( ( c ) => c . enabled )
482489 . map ( ( c ) => createCustomProviderInfo ( c ) ) ;
483- return [ ...PROVIDERS , ...customProviders ] ;
484- } , [ customApis ] ) ;
490+ return [ ...builtInProviders , ...customProviders ] ;
491+ } , [ customApis , hasOpenRouterByokKey ] ) ;
485492
486493 const provider = allProviders . find ( ( p ) => p . id === providerId ) ;
487494 const model = provider ?. models . find ( ( m ) => m . id === modelId ) ;
495+ const isOpenRouterStarterModel = providerId === OPENROUTER_PROVIDER_ID
496+ && OPENROUTER_STARTER_MODEL_IDS . has ( modelId ) ;
488497
489- const hasKey = isCustomProvider ( providerId )
498+ const hasSavedKey = isCustomProvider ( providerId )
490499 ? ! ! customApis . find ( ( c ) => `${ CUSTOM_PROVIDER_PREFIX } ${ c . id } ` === providerId ) ?. apiKey
491500 : ! ! apiKeys [ providerId ] ;
501+ const hasStarterAccess = isOpenRouterStarterModel && hasOpenRouterStarter ;
502+ const hasAccess = hasSavedKey || hasStarterAccess ;
503+ const showOpenRouterStarterNotice = isOpenRouterStarterModel
504+ && hasOpenRouterStarter
505+ && ! hasSavedKey
506+ && ! showKeyInput ;
492507
493508 const handleSelect = ( newModel : string ) => {
494509 setModel ( newModel ) ;
495510 setOpen ( false ) ;
496- const { providerId : pid } = parseModelString ( newModel ) ;
511+ const { providerId : pid , modelId : nextModelId } = parseModelString ( newModel ) ;
512+ const isStarterOpenRouterModel = pid === OPENROUTER_PROVIDER_ID
513+ && OPENROUTER_STARTER_MODEL_IDS . has ( nextModelId ) ;
497514 if ( isCustomProvider ( pid ) ) {
498515 setShowKeyInput ( false ) ;
516+ } else if ( isStarterOpenRouterModel && hasOpenRouterStarter && ! apiKeys [ pid ] ) {
517+ setShowKeyInput ( false ) ;
499518 } else if ( ! apiKeys [ pid ] ) {
500519 setShowKeyInput ( true ) ;
501520 } else {
@@ -526,7 +545,7 @@ function useModelSelector() {
526545 < ModelSelectorLogo provider = { providerId as "anthropic" } className = "size-3.5" />
527546 ) }
528547 < span > { model ?. name ?? modelId } </ span >
529- { ! hasKey && ! isCustomProvider ( providerId ) && (
548+ { ! hasAccess && ! isCustomProvider ( providerId ) && (
530549 < span className = "size-1.5 rounded-full bg-yellow-500/70 shrink-0" />
531550 ) }
532551 </ ModelSelectorTrigger >
@@ -549,6 +568,11 @@ function useModelSelector() {
549568 < ModelSelectorLogo provider = { p . id as "anthropic" } />
550569 ) }
551570 < ModelSelectorName > { m . name } </ ModelSelectorName >
571+ { p . id === OPENROUTER_PROVIDER_ID && OPENROUTER_STARTER_MODEL_IDS . has ( m . id ) && (
572+ < span className = "shrink-0 border border-emerald-500/30 bg-emerald-500/10 px-1.5 py-0.5 text-[9px] uppercase tracking-wider text-emerald-300" >
573+ Free
574+ </ span >
575+ ) }
552576 </ ModelSelectorItem >
553577 ) ) }
554578 </ ModelSelectorGroup >
@@ -595,30 +619,51 @@ function useModelSelector() {
595619 </ >
596620 ) ;
597621
598- const keyInputEl = ! isCustomProvider ( providerId ) && ( showKeyInput || ! hasKey ) ? (
599- < div className = "flex items-center gap-1.5" >
600- < input
601- type = "password"
602- value = { keyInput }
603- onChange = { ( e ) => setKeyInput ( e . target . value ) }
604- onKeyDown = { ( e ) => e . key === "Enter" && handleSaveKey ( ) }
605- placeholder = { `${ provider ?. name ?? providerId } API key...` }
606- className = "flex-1 bg-zinc-900 border border-zinc-800 text-xs px-2.5 py-1.5 text-zinc-300 placeholder:text-zinc-600 focus:outline-none focus:border-zinc-600"
607- />
608- < button
609- onClick = { handleSaveKey }
610- className = "px-2.5 py-1.5 text-xs uppercase tracking-wider bg-zinc-800 hover:bg-zinc-700 text-zinc-300 transition-colors"
611- >
612- Save
613- </ button >
614- </ div >
615- ) : null ;
622+ const shouldShowKeyInput = ! isCustomProvider ( providerId ) && ( showKeyInput || ! hasAccess ) ;
623+
624+ const keyInputEl = ! isCustomProvider ( providerId )
625+ ? shouldShowKeyInput
626+ ? (
627+ < div className = "flex items-center gap-1.5" >
628+ < input
629+ type = "password"
630+ value = { keyInput }
631+ onChange = { ( e ) => setKeyInput ( e . target . value ) }
632+ onKeyDown = { ( e ) => e . key === "Enter" && handleSaveKey ( ) }
633+ placeholder = { `${ provider ?. name ?? providerId } API key...` }
634+ className = "flex-1 bg-zinc-900 border border-zinc-800 text-xs px-2.5 py-1.5 text-zinc-300 placeholder:text-zinc-600 focus:outline-none focus:border-zinc-600"
635+ />
636+ < button
637+ onClick = { handleSaveKey }
638+ className = "px-2.5 py-1.5 text-xs uppercase tracking-wider bg-zinc-800 hover:bg-zinc-700 text-zinc-300 transition-colors"
639+ >
640+ Save
641+ </ button >
642+ </ div >
643+ )
644+ : showOpenRouterStarterNotice
645+ ? (
646+ < div className = "flex items-center justify-between gap-3 border border-zinc-800 bg-zinc-950 px-2.5 py-2 text-[11px] text-zinc-400" >
647+ < span > Starter OpenRouter models work out of the box. Add your own key to unlock frontier models.</ span >
648+ < button
649+ type = "button"
650+ onClick = { ( ) => setShowKeyInput ( true ) }
651+ className = "shrink-0 px-2 py-1 text-[10px] uppercase tracking-wider bg-zinc-800 hover:bg-zinc-700 text-zinc-200 transition-colors"
652+ >
653+ Add key
654+ </ button >
655+ </ div >
656+ )
657+ : null
658+ : null ;
616659
617660 return { trigger, keyInputEl } ;
618661}
619662
620- export function ChatSidebar ( ) {
621- const { trigger : modelTrigger , keyInputEl : modelKeyInput } = useModelSelector ( ) ;
663+ export function ChatSidebar ( { hasOpenRouterStarter = false } : { hasOpenRouterStarter ?: boolean } ) {
664+ const { trigger : modelTrigger , keyInputEl : modelKeyInput } = useModelSelector ( {
665+ hasOpenRouterStarter,
666+ } ) ;
622667 const [ mcpOpen , setMcpOpen ] = useState ( false ) ;
623668 const mcpServers = useSettingsStore ( ( s ) => s . mcpServers ) ;
624669 const enabledMcpCount = mcpServers . filter ( ( s ) => s . enabled ) . length ;
@@ -643,10 +688,10 @@ export function ChatSidebar() {
643688 ? streamingWidgetIds . includes ( activeWidgetId )
644689 : false ;
645690
646- const handleInterrupt = ( ) => {
691+ const handleInterrupt = useCallback ( ( ) => {
647692 if ( ! activeWidgetId ) return ;
648693 abortControllers . get ( activeWidgetId ) ?. abort ( ) ;
649- } ;
694+ } , [ activeWidgetId ] ) ;
650695
651696 useEffect ( ( ) => {
652697 if ( ! isActiveStreaming ) return ;
@@ -655,7 +700,7 @@ export function ChatSidebar() {
655700 } ;
656701 document . addEventListener ( "keydown" , onKey ) ;
657702 return ( ) => document . removeEventListener ( "keydown" , onKey ) ;
658- } , [ isActiveStreaming , activeWidgetId ] ) ;
703+ } , [ handleInterrupt , isActiveStreaming ] ) ;
659704
660705 const [ input , setInput ] = useState ( "" ) ;
661706 const [ pendingFiles , setPendingFiles ] = useState < PendingFile [ ] > ( [ ] ) ;
0 commit comments