@@ -67,6 +67,15 @@ interface GhostRootInfo {
6767 defaultBaseDirectionId ?: string | null ;
6868}
6969
70+ interface ModelProviderInfo {
71+ id : string ;
72+ name : string ;
73+ configured : boolean ;
74+ model : string ;
75+ utilityModel : string ;
76+ missingEnv ?: string ;
77+ }
78+
7079const layoutPresets = new Map < string , SummonLayout > ( [
7180 [
7281 'card-structured' ,
@@ -92,6 +101,7 @@ const scenarioActiveTitleEl = document.getElementById('scenario-active-title')!;
92101const scenarioActiveDescEl = document . getElementById ( 'scenario-active-desc' ) ! ;
93102const scenarioActiveFingerprintEl = document . getElementById ( 'scenario-active-fingerprint' ) ! ;
94103const scenarioActiveGrantsEl = document . getElementById ( 'scenario-active-grants' ) ! ;
104+ const modelProviderSel = document . getElementById ( 'model-provider' ) as HTMLSelectElement ;
95105const directionSel = document . getElementById ( 'direction' ) as HTMLSelectElement ;
96106const ghostTargetEl = document . getElementById ( 'ghost-target' ) as HTMLInputElement ;
97107const ghostBaseDirectionSel = document . getElementById ( 'ghost-base-direction' ) as HTMLSelectElement ;
@@ -147,6 +157,9 @@ function readMode(): Mode {
147157 const checked = document . querySelector < HTMLInputElement > ( 'input[name=mode]:checked' ) ;
148158 return ( checked ?. value as Mode ) ?? 'static' ;
149159}
160+ function readModelProviderId ( ) : string | null {
161+ return modelProviderSel . value || defaultModelProviderId ;
162+ }
150163function readLayout ( ) : SummonLayout | null {
151164 const layout = layoutPresets . get ( layoutSel . value ) ;
152165 return layout ? { id : layout . id , slots : layout . slots . map ( ( slot ) => ( { ...slot } ) ) } : null ;
@@ -169,6 +182,8 @@ function logLine(cls: string, text: string) {
169182
170183let directions : DirectionInfo [ ] = [ ] ;
171184let ghostRoots : GhostRootInfo [ ] = [ ] ;
185+ let modelProviders : ModelProviderInfo [ ] = [ ] ;
186+ let defaultModelProviderId : string | null = null ;
172187let showcaseScenarios : ShowcaseScenario [ ] = [ ...SHOWCASE_SCENARIOS ] ;
173188let currentEffectiveSurfacePlan : SurfacePlan | null = null ;
174189let currentShape : string | null = null ;
@@ -539,6 +554,7 @@ function readActiveContract(): ActiveContract {
539554 ...( readTokenOverrides ( ) ? { tokenOverrides : readTokenOverrides ( ) } : { } ) ,
540555 ...( readRepairOptions ( ) ? { repair : readRepairOptions ( ) } : { } ) ,
541556 directionId : currentDirectionId ,
557+ modelProvider : readModelProviderId ( ) ,
542558 } ;
543559}
544560
@@ -604,9 +620,11 @@ function renderContractSummary() {
604620 const validation = currentValidationSummary ?? 'pending' ;
605621 const stream = currentStreamHealth ?? 'pending' ;
606622 const effective = currentEffectiveSurfacePlan ? planText ( currentEffectiveSurfacePlan ) : 'pending' ;
623+ const provider = modelProviders . find ( ( item ) => item . id === active . modelProvider ) ;
607624 inspectorStatusEl . textContent = currentEffectiveSurfacePlan ? 'effective' : 'pending' ;
608625 contractSummaryEl . innerHTML = '' ;
609626 const rows = [
627+ [ 'provider' , 'Model provider' , provider ? `${ provider . name } · ${ provider . model } ` : 'server default' , provider ? 'neutral' : 'pending' ] ,
610628 [ 'requested' , 'Requested surface config' , planText ( requested ) , 'neutral' ] ,
611629 [ 'effective' , 'Effective safety plan' , effective , currentEffectiveSurfacePlan ? 'good' : 'pending' ] ,
612630 [ 'grants' , 'Allowed host tools' , `${ active . capabilityNames . length } : ${ hostTools } ` , active . capabilityNames . length ? 'neutral' : 'pending' ] ,
@@ -697,6 +715,38 @@ function parseAppliedTokenOverrides(value: unknown): Array<{ token: string; valu
697715}
698716
699717async function loadDirections ( ) : Promise < void > {
718+ try {
719+ const res = await fetch ( '/api/model-providers' ) ;
720+ if ( ! res . ok ) throw new Error ( `HTTP ${ res . status } ` ) ;
721+ const payload = ( await res . json ( ) ) as { defaultProvider ?: unknown ; providers ?: unknown } ;
722+ defaultModelProviderId = typeof payload . defaultProvider === 'string' ? payload . defaultProvider : null ;
723+ modelProviders = Array . isArray ( payload . providers )
724+ ? payload . providers . flatMap ( ( provider ) : ModelProviderInfo [ ] => {
725+ if ( ! provider || typeof provider !== 'object' ) return [ ] ;
726+ const item = provider as Record < string , unknown > ;
727+ if (
728+ typeof item . id !== 'string' ||
729+ typeof item . name !== 'string' ||
730+ typeof item . model !== 'string' ||
731+ typeof item . utilityModel !== 'string'
732+ ) {
733+ return [ ] ;
734+ }
735+ return [ {
736+ id : item . id ,
737+ name : item . name ,
738+ configured : item . configured === true ,
739+ model : item . model ,
740+ utilityModel : item . utilityModel ,
741+ missingEnv : typeof item . missingEnv === 'string' ? item . missingEnv : undefined ,
742+ } ] ;
743+ } )
744+ : [ ] ;
745+ } catch {
746+ modelProviders = [ ] ;
747+ defaultModelProviderId = null ;
748+ }
749+
700750 try {
701751 const res = await fetch ( '/api/directions' ) ;
702752 if ( ! res . ok ) throw new Error ( `HTTP ${ res . status } ` ) ;
@@ -711,6 +761,7 @@ async function loadDirections(): Promise<void> {
711761 } catch {
712762 ghostRoots = [ ] ;
713763 }
764+ populateModelProviderSelect ( ) ;
714765 ghostBaseDirectionSel . innerHTML = '' ;
715766 for ( const d of directions ) {
716767 const opt = document . createElement ( 'option' ) ;
@@ -754,6 +805,38 @@ async function loadDirections(): Promise<void> {
754805 updateGhostControls ( ) ;
755806}
756807
808+ function populateModelProviderSelect ( ) {
809+ modelProviderSel . innerHTML = '' ;
810+ if ( modelProviders . length === 0 ) {
811+ const opt = document . createElement ( 'option' ) ;
812+ opt . value = '' ;
813+ opt . textContent = 'Server default' ;
814+ modelProviderSel . appendChild ( opt ) ;
815+ modelProviderSel . disabled = true ;
816+ return ;
817+ }
818+
819+ modelProviderSel . disabled = false ;
820+ for ( const provider of modelProviders ) {
821+ const opt = document . createElement ( 'option' ) ;
822+ opt . value = provider . id ;
823+ opt . textContent = provider . configured
824+ ? `${ provider . name } `
825+ : `${ provider . name } (missing key)` ;
826+ opt . title = provider . configured
827+ ? `${ provider . model } for generation; ${ provider . utilityModel } for utility calls`
828+ : `Set ${ provider . missingEnv ?? 'the provider API key' } ` ;
829+ opt . disabled = ! provider . configured ;
830+ modelProviderSel . appendChild ( opt ) ;
831+ }
832+
833+ const defaultProvider = defaultModelProviderId
834+ ? modelProviders . find ( ( provider ) => provider . id === defaultModelProviderId && provider . configured )
835+ : null ;
836+ const firstConfigured = modelProviders . find ( ( provider ) => provider . configured ) ;
837+ modelProviderSel . value = defaultProvider ?. id ?? firstConfigured ?. id ?? '' ;
838+ }
839+
757840function ghostSelectionValue ( rootId : string ) : string {
758841 return `ghost:${ rootId } ` ;
759842}
@@ -856,6 +939,7 @@ function respawn(
856939
857940 if ( mode === 'interactive' ) {
858941 const registry = createScopedDemoRegistry ( {
942+ modelProvider : readModelProviderId ,
859943 onLog : ( m ) => logLine ( 'op-add' , m ) ,
860944 onError : ( m ) => logLine ( 'op-error' , m ) ,
861945 // summon needs DOM access (spawns a sibling iframe) and the streaming
@@ -940,6 +1024,11 @@ directionSel.addEventListener('change', () => {
9401024 logLine ( 'op-meta' , `direction → ${ currentDirectionId ?? 'default' } ` ) ;
9411025} ) ;
9421026
1027+ modelProviderSel . addEventListener ( 'change' , ( ) => {
1028+ clearEffectiveContractSummary ( ) ;
1029+ logLine ( 'op-meta' , `provider → ${ readModelProviderId ( ) ?? 'server default' } ` ) ;
1030+ } ) ;
1031+
9431032ghostTargetEl . addEventListener ( 'change' , ( ) => {
9441033 const root = ghostRootFromSelection ( currentDirectionId ) ;
9451034 if ( ! root ) return ;
@@ -1221,6 +1310,7 @@ function applyLineTo(target: SandboxTarget, line: ProtocolLine, context: Surface
12211310
12221311interface StreamOptions {
12231312 prompt : string ;
1313+ modelProvider ?: string | null ;
12241314 directionId : string | null ;
12251315 layout ?: SummonLayout | null ;
12261316 scriptPolicy ?: ScriptPolicy ;
@@ -1258,6 +1348,7 @@ async function streamGenerationInto(target: SandboxTarget, opts: StreamOptions):
12581348 headers : { 'Content-Type' : 'application/json' } ,
12591349 body : JSON . stringify ( {
12601350 prompt : opts . prompt ,
1351+ ...( opts . modelProvider ? { modelProvider : opts . modelProvider } : { } ) ,
12611352 ...( ghostRootId
12621353 ? {
12631354 ghost : {
@@ -1609,6 +1700,7 @@ async function generate(prompt: string) {
16091700 try {
16101701 const result = await streamGenerationInto ( target , {
16111702 prompt,
1703+ modelProvider : active . modelProvider ,
16121704 directionId : currentDirectionId ,
16131705 layout : readLayout ( ) ,
16141706 scriptPolicy : active . scriptPolicy ,
@@ -1668,6 +1760,7 @@ async function editArtifact(instruction: string) {
16681760 try {
16691761 const result = await streamGenerationInto ( createParentTarget ( active ) , {
16701762 prompt : instruction ,
1763+ modelProvider : active . modelProvider ,
16711764 directionId : currentDirectionId ,
16721765 layout : readLayout ( ) ,
16731766 scriptPolicy : active . scriptPolicy ,
@@ -1753,6 +1846,7 @@ function summonChild(childPrompt: string, title?: string) {
17531846 . map ( ( intent ) => intent . name )
17541847 . filter ( ( name ) => name !== 'summon' ) ;
17551848 const childRegistry = createScopedDemoRegistry ( {
1849+ modelProvider : readModelProviderId ,
17561850 onLog : ( ) => { } ,
17571851 onError : ( m ) => {
17581852 statusEl . textContent = `error: ${ m . slice ( 0 , 40 ) } ` ;
@@ -1834,6 +1928,7 @@ function summonChild(childPrompt: string, title?: string) {
18341928
18351929 void streamGenerationInto ( childTarget , {
18361930 prompt : childPrompt ,
1931+ modelProvider : readModelProviderId ( ) ,
18371932 directionId : currentDirectionId ,
18381933 signal : abort . signal ,
18391934 } )
0 commit comments