66 * model-registry.ts getter functions.
77 */
88
9- import { ICON_KEY , ICON_EYE , ICON_EYE_OFF , ICON_CHECK } from "./icons.ts" ;
9+ import { ICON_KEY , ICON_EYE , ICON_EYE_OFF , ICON_CHECK , ICON_LOADER , ICON_CLOSE } from "./icons.ts" ;
1010import {
1111 getInferenceApiKey ,
1212 getIntegrateApiKey ,
@@ -24,6 +24,7 @@ interface KeyFieldDef {
2424 label : string ;
2525 description : string ;
2626 placeholder : string ;
27+ serverCredentialKey : string ;
2728 get : ( ) => string ;
2829 set : ( v : string ) => void ;
2930}
@@ -34,6 +35,7 @@ const KEY_FIELDS: KeyFieldDef[] = [
3435 label : "Inference API Key" ,
3536 description : "For inference-api.nvidia.com — powers NVIDIA Claude Opus 4.6" ,
3637 placeholder : "nvapi-..." ,
38+ serverCredentialKey : "OPENAI_API_KEY" ,
3739 get : getInferenceApiKey ,
3840 set : setInferenceApiKey ,
3941 } ,
@@ -42,11 +44,67 @@ const KEY_FIELDS: KeyFieldDef[] = [
4244 label : "Integrate API Key" ,
4345 description : "For integrate.api.nvidia.com — powers Kimi K2.5, Nemotron Ultra, DeepSeek V3.2" ,
4446 placeholder : "nvapi-..." ,
47+ serverCredentialKey : "NVIDIA_API_KEY" ,
4548 get : getIntegrateApiKey ,
4649 set : setIntegrateApiKey ,
4750 } ,
4851] ;
4952
53+ // ---------------------------------------------------------------------------
54+ // Sync localStorage keys to server-side provider credentials
55+ // ---------------------------------------------------------------------------
56+
57+ interface ProviderSummary {
58+ name : string ;
59+ type : string ;
60+ credentialKeys : string [ ] ;
61+ }
62+
63+ /**
64+ * Push localStorage API keys to every server-side provider whose credential
65+ * key matches. This bridges the gap between the browser-only API Keys tab
66+ * and the NemoClaw proxy which reads credentials from the server-side store.
67+ */
68+ export async function syncKeysToProviders ( ) : Promise < void > {
69+ const res = await fetch ( "/api/providers" ) ;
70+ if ( ! res . ok ) throw new Error ( `Failed to fetch providers: ${ res . status } ` ) ;
71+ const body = await res . json ( ) ;
72+ if ( ! body . ok ) throw new Error ( body . error || "Failed to fetch providers" ) ;
73+
74+ const providers : ProviderSummary [ ] = body . providers || [ ] ;
75+ const errors : string [ ] = [ ] ;
76+
77+ for ( const provider of providers ) {
78+ for ( const field of KEY_FIELDS ) {
79+ const key = field . get ( ) ;
80+ if ( ! isKeyConfigured ( key ) ) continue ;
81+ if ( ! provider . credentialKeys ?. includes ( field . serverCredentialKey ) ) continue ;
82+
83+ try {
84+ const updateRes = await fetch ( `/api/providers/${ encodeURIComponent ( provider . name ) } ` , {
85+ method : "PUT" ,
86+ headers : { "Content-Type" : "application/json" } ,
87+ body : JSON . stringify ( {
88+ type : provider . type ,
89+ credentials : { [ field . serverCredentialKey ] : key } ,
90+ config : { } ,
91+ } ) ,
92+ } ) ;
93+ const updateBody = await updateRes . json ( ) ;
94+ if ( ! updateBody . ok ) {
95+ errors . push ( `${ provider . name } : ${ updateBody . error || "update failed" } ` ) ;
96+ }
97+ } catch ( err ) {
98+ errors . push ( `${ provider . name } : ${ err } ` ) ;
99+ }
100+ }
101+ }
102+
103+ if ( errors . length > 0 ) {
104+ throw new Error ( errors . join ( "; " ) ) ;
105+ }
106+ }
107+
50108// ---------------------------------------------------------------------------
51109// Render the API Keys page into a container element
52110// ---------------------------------------------------------------------------
@@ -71,7 +129,7 @@ export function renderApiKeysPage(container: HTMLElement): void {
71129 Enter your NVIDIA API keys to enable model switching and DGX deployment.
72130 Keys are stored locally in your browser and never sent to third parties.
73131 </p>
74- <a class="nemoclaw-key-intro__link" href="https://build.nvidia.com/models " target="_blank" rel="noopener noreferrer">
132+ <a class="nemoclaw-key-intro__link" href="https://build.nvidia.com/settings/api-keys " target="_blank" rel="noopener noreferrer">
75133 Get your keys at build.nvidia.com →
76134 </a>` ;
77135 page . appendChild ( intro ) ;
@@ -100,20 +158,33 @@ export function renderApiKeysPage(container: HTMLElement): void {
100158 form . appendChild ( actions ) ;
101159 page . appendChild ( form ) ;
102160
103- saveBtn . addEventListener ( "click" , ( ) => {
161+ saveBtn . addEventListener ( "click" , async ( ) => {
104162 for ( const field of KEY_FIELDS ) {
105163 const input = form . querySelector < HTMLInputElement > ( `[data-key-id="${ field . id } "]` ) ;
106164 if ( input ) field . set ( input . value . trim ( ) ) ;
107165 }
108166
109167 updateStatusDots ( ) ;
110168
111- feedback . className = "nemoclaw-key-feedback nemoclaw-key-feedback--success" ;
112- feedback . innerHTML = `${ ICON_CHECK } <span>Keys saved</span>` ;
113- setTimeout ( ( ) => {
114- feedback . className = "nemoclaw-key-feedback" ;
115- feedback . textContent = "" ;
116- } , 3000 ) ;
169+ feedback . className = "nemoclaw-key-feedback nemoclaw-key-feedback--saving" ;
170+ feedback . innerHTML = `${ ICON_LOADER } <span>Syncing keys to providers\u2026</span>` ;
171+ saveBtn . disabled = true ;
172+
173+ try {
174+ await syncKeysToProviders ( ) ;
175+ feedback . className = "nemoclaw-key-feedback nemoclaw-key-feedback--success" ;
176+ feedback . innerHTML = `${ ICON_CHECK } <span>Keys saved & synced to providers</span>` ;
177+ } catch ( err ) {
178+ console . warn ( "[NeMoClaw] Provider key sync failed:" , err ) ;
179+ feedback . className = "nemoclaw-key-feedback nemoclaw-key-feedback--error" ;
180+ feedback . innerHTML = `${ ICON_CLOSE } <span>Keys saved locally but sync failed</span>` ;
181+ } finally {
182+ saveBtn . disabled = false ;
183+ setTimeout ( ( ) => {
184+ feedback . className = "nemoclaw-key-feedback" ;
185+ feedback . textContent = "" ;
186+ } , 4000 ) ;
187+ }
117188 } ) ;
118189}
119190
0 commit comments