11import type { ProviderAuthAuthorization , ProviderAuthMethod } from "@deepagent-code/sdk/v2/client"
22import { Button } from "@deepagent-code/ui/button"
3+ import { Collapsible } from "@deepagent-code/ui/collapsible"
34import { useDialog } from "@deepagent-code/ui/context/dialog"
45import { Dialog } from "@deepagent-code/ui/dialog"
56import { Icon } from "@deepagent-code/ui/icon"
@@ -16,19 +17,30 @@ import { useServerSDK } from "@/context/server-sdk"
1617import { useServerSync } from "@/context/server-sync"
1718import { useLanguage } from "@/context/language"
1819import { useProviders } from "@/hooks/use-providers"
20+ import { isOfficialProvider } from "@deepagent-code/core/provider-official"
1921
2022// Recommended/official providers ship a fixed protocol (npm) tied to a canonical endpoint. Letting
2123// users point one of these at a relay/local server silently breaks because the protocol no longer
2224// matches the endpoint — those users must add a custom provider instead. So the connect dialog hides
23- // the baseURL field for these and only exposes it for ad-hoc config/custom providers.
25+ // the baseURL field for these and only exposes it for ad-hoc config/custom providers. Google and xAI
26+ // resolve their real endpoint from the bundled SDK; the URL here only satisfies form validation and
27+ // the placeholder. Keys must exist for every official id (see OFFICIAL_PROVIDER_IDS in core).
2428const providerBaseURL : Record < string , string > = {
2529 openai : "https://api.openai.com/v1" ,
2630 deepseek : "https://api.deepseek.com" ,
2731 anthropic : "https://api.anthropic.com/v1" ,
2832 zhipuai : "https://open.bigmodel.cn/api/paas/v4" ,
33+ "zhipuai-coding-plan" : "https://open.bigmodel.cn/api/coding/paas/v4" ,
34+ zai : "https://api.z.ai/api/paas/v4" ,
35+ "zai-coding-plan" : "https://api.z.ai/api/coding/paas/v4" ,
36+ xai : "https://api.x.ai/v1" ,
37+ google : "https://generativelanguage.googleapis.com/v1beta" ,
2938}
3039
31- const isBuiltinProvider = ( providerID : string ) => providerID in providerBaseURL
40+ // Official = first-party in the backend (key store credentials, fixed protocol). This must match the
41+ // backend's OFFICIAL_PROVIDER_IDS exactly so the connect flow routes credentials the same way the
42+ // loader reads them (auth for official, config options.apiKey for third-party).
43+ const isBuiltinProvider = ( providerID : string ) => isOfficialProvider ( providerID )
3244
3345const providerKind = ( providerID : string ) => ( providerID === "anthropic" ? "anthropic" : "openai-compatible" )
3446
@@ -419,6 +431,13 @@ export function DialogConnectProvider(props: { provider: string }) {
419431 id : model . id ,
420432 name : model . name || model . id ,
421433 } ) )
434+ // Official-provider transport settings are surfaced by the config overlay under
435+ // provider.<id>.options (backed by SettingsStore, not the config file). Seed the fields from there.
436+ const transportOption = ( key : string ) : string => {
437+ const value = serverSync . data . config . provider ?. [ props . provider ] ?. options ?. [ key ]
438+ if ( value === false ) return "0"
439+ return typeof value === "number" ? String ( value ) : ""
440+ }
422441 const [ formStore , setFormStore ] = createStore ( {
423442 value : "" ,
424443 baseURL :
@@ -430,8 +449,42 @@ export function DialogConnectProvider(props: { provider: string }) {
430449 discovered : [ ] as DiscoveredProviderModel [ ] ,
431450 selectedModel : undefined as string | undefined ,
432451 discovering : false ,
452+ headerTimeout : transportOption ( "headerTimeout" ) ,
453+ chunkTimeout : transportOption ( "chunkTimeout" ) ,
454+ timeout : transportOption ( "timeout" ) ,
455+ maxRetries : transportOption ( "maxRetries" ) ,
456+ transportError : undefined as string | undefined ,
433457 } )
434458
459+ // Parse a transport field: empty → undefined (use default); "0" for header/request timeout →
460+ // false (disabled); otherwise a positive integer. Returns { ok, value } so callers can reject
461+ // malformed input.
462+ function parseTransport ( raw : string , allowFalse : boolean ) : { ok : boolean ; value : number | false | undefined } {
463+ const trimmed = raw . trim ( )
464+ if ( trimmed === "" ) return { ok : true , value : undefined }
465+ const n = Number ( trimmed )
466+ if ( ! Number . isInteger ( n ) || n < 0 ) return { ok : false , value : undefined }
467+ if ( n === 0 ) return allowFalse ? { ok : true , value : false } : { ok : false , value : undefined }
468+ return { ok : true , value : n }
469+ }
470+
471+ // Build the SettingsStore-bound options patch for an official provider (routed via the config
472+ // overlay's intercept). Returns undefined when a field is malformed.
473+ function transportOptions ( ) : Record < string , number | false > | undefined | "invalid" {
474+ const header = parseTransport ( formStore . headerTimeout , true )
475+ const chunk = parseTransport ( formStore . chunkTimeout , false )
476+ const request = parseTransport ( formStore . timeout , true )
477+ const retries = parseTransport ( formStore . maxRetries , false )
478+ if ( ! header . ok || ! chunk . ok || ! request . ok || ! retries . ok ) return "invalid"
479+ const options : Record < string , number | false > = { }
480+ if ( header . value !== undefined ) options . headerTimeout = header . value
481+ if ( chunk . value !== undefined && chunk . value !== false ) options . chunkTimeout = chunk . value
482+ if ( request . value !== undefined ) options . timeout = request . value
483+ if ( retries . value !== undefined && retries . value !== false ) options . maxRetries = retries . value
484+ return options
485+ }
486+
487+
435488 async function handleSubmit ( e : SubmitEvent ) {
436489 e . preventDefault ( )
437490
@@ -456,6 +509,43 @@ export function DialogConnectProvider(props: { provider: string }) {
456509
457510 setFormStore ( "error" , undefined )
458511 setFormStore ( "baseURLError" , undefined )
512+
513+ if ( isBuiltinProvider ( props . provider ) ) {
514+ const transport = transportOptions ( )
515+ if ( transport === "invalid" ) {
516+ setFormStore ( "transportError" , language . t ( "provider.connect.transport.error.number" ) )
517+ return
518+ }
519+ setFormStore ( "transportError" , undefined )
520+ if ( key ) {
521+ await serverSDK . client . auth . set ( {
522+ providerID : props . provider ,
523+ auth : {
524+ type : "api" ,
525+ key,
526+ } ,
527+ } )
528+ }
529+ // Route transport tuning through the config overlay's intercept → SettingsStore. The
530+ // remaining official-provider fields are ignored by the backend loader; only options.<transport>
531+ // is picked up (and stripped from the config file). Always send the key so cleared fields reset.
532+ const disabled = ( serverSync . data . config . disabled_providers ?? [ ] ) . filter ( ( id ) => id !== props . provider )
533+ await serverSync . updateConfig ( {
534+ disabled_providers : disabled ,
535+ provider : {
536+ [ props . provider ] : { options : transport } ,
537+ } ,
538+ } )
539+ await serverSDK . client . global . dispose ( )
540+ showToast ( {
541+ variant : "success" ,
542+ icon : "circle-check" ,
543+ title : language . t ( "provider.connect.toast.connected.title" , { provider : provider ( ) . name } ) ,
544+ description : language . t ( "provider.connect.toast.connected.description" , { provider : provider ( ) . name } ) ,
545+ } )
546+ return
547+ }
548+
459549 setFormStore ( "discovering" , true )
460550 try {
461551 const discovered = await serverSDK . client . provider . models
@@ -484,15 +574,6 @@ export function DialogConnectProvider(props: { provider: string }) {
484574 setFormStore ( "discovered" , nextModels )
485575 setFormStore ( "selectedModel" , nextSelected . id )
486576
487- if ( key ) {
488- await serverSDK . client . auth . set ( {
489- providerID : props . provider ,
490- auth : {
491- type : "api" ,
492- key,
493- } ,
494- } )
495- }
496577 const current = serverSync . data . config . provider ?. [ props . provider ] ?? { }
497578 await serverSync . updateConfig ( {
498579 provider : {
@@ -502,6 +583,7 @@ export function DialogConnectProvider(props: { provider: string }) {
502583 options : {
503584 ...( current . options ?? { } ) ,
504585 baseURL,
586+ ...( key ? { apiKey : key } : { } ) ,
505587 } ,
506588 models : {
507589 ...( current . models ?? { } ) ,
@@ -558,6 +640,55 @@ export function DialogConnectProvider(props: { provider: string }) {
558640 validationState = { formStore . error ? "invalid" : undefined }
559641 error = { formStore . error }
560642 />
643+ < Show when = { isBuiltinProvider ( props . provider ) } >
644+ < Collapsible class = "w-full" >
645+ < Collapsible . Trigger class = "flex w-full items-center justify-between gap-2 text-13-medium text-text-base" >
646+ < span > { language . t ( "provider.connect.transport.title" ) } </ span >
647+ < Collapsible . Arrow />
648+ </ Collapsible . Trigger >
649+ < Collapsible . Content >
650+ < div class = "flex flex-col gap-4 pt-4" >
651+ < div class = "text-12-regular text-text-weak" >
652+ { language . t ( "provider.connect.transport.description" ) }
653+ </ div >
654+ < TextField
655+ type = "text"
656+ inputmode = "numeric"
657+ label = { language . t ( "provider.connect.transport.headerTimeout.label" ) }
658+ placeholder = { language . t ( "provider.connect.transport.headerTimeout.placeholder" ) }
659+ value = { formStore . headerTimeout }
660+ onChange = { ( v ) => setFormStore ( "headerTimeout" , v ) }
661+ />
662+ < TextField
663+ type = "text"
664+ inputmode = "numeric"
665+ label = { language . t ( "provider.connect.transport.chunkTimeout.label" ) }
666+ placeholder = { language . t ( "provider.connect.transport.chunkTimeout.placeholder" ) }
667+ value = { formStore . chunkTimeout }
668+ onChange = { ( v ) => setFormStore ( "chunkTimeout" , v ) }
669+ />
670+ < TextField
671+ type = "text"
672+ inputmode = "numeric"
673+ label = { language . t ( "provider.connect.transport.timeout.label" ) }
674+ placeholder = { language . t ( "provider.connect.transport.timeout.placeholder" ) }
675+ value = { formStore . timeout }
676+ onChange = { ( v ) => setFormStore ( "timeout" , v ) }
677+ />
678+ < TextField
679+ type = "text"
680+ inputmode = "numeric"
681+ label = { language . t ( "provider.connect.transport.maxRetries.label" ) }
682+ placeholder = { language . t ( "provider.connect.transport.maxRetries.placeholder" ) }
683+ value = { formStore . maxRetries }
684+ onChange = { ( v ) => setFormStore ( "maxRetries" , v ) }
685+ validationState = { formStore . transportError ? "invalid" : undefined }
686+ error = { formStore . transportError }
687+ />
688+ </ div >
689+ </ Collapsible . Content >
690+ </ Collapsible >
691+ </ Show >
561692 < Button class = "w-auto" type = "submit" size = "large" variant = "primary" disabled = { formStore . discovering } >
562693 { language . t ( "common.continue" ) }
563694 </ Button >
0 commit comments