@@ -4842,6 +4842,7 @@ pub async fn list_providers(State(state): State<Arc<AppState>>) -> impl IntoResp
48424842 "model_count" : p. model_count,
48434843 "key_required" : p. key_required,
48444844 "api_key_env" : p. api_key_env,
4845+ "base_url" : p. base_url,
48454846 } ) ;
48464847
48474848 // For local providers, add reachability info via health probe
@@ -5899,6 +5900,122 @@ pub async fn test_provider(
58995900 }
59005901}
59015902
5903+ /// PUT /api/providers/{name}/url — Set a custom base URL for a provider.
5904+ pub async fn set_provider_url (
5905+ State ( state) : State < Arc < AppState > > ,
5906+ Path ( name) : Path < String > ,
5907+ Json ( body) : Json < serde_json:: Value > ,
5908+ ) -> impl IntoResponse {
5909+ // Validate provider exists
5910+ let provider_exists = {
5911+ let catalog = state
5912+ . kernel
5913+ . model_catalog
5914+ . read ( )
5915+ . unwrap_or_else ( |e| e. into_inner ( ) ) ;
5916+ catalog. get_provider ( & name) . is_some ( )
5917+ } ;
5918+ if !provider_exists {
5919+ return (
5920+ StatusCode :: NOT_FOUND ,
5921+ Json ( serde_json:: json!( { "error" : format!( "Unknown provider '{}'" , name) } ) ) ,
5922+ ) ;
5923+ }
5924+
5925+ let base_url = match body[ "base_url" ] . as_str ( ) {
5926+ Some ( u) if !u. trim ( ) . is_empty ( ) => u. trim ( ) . to_string ( ) ,
5927+ _ => {
5928+ return (
5929+ StatusCode :: BAD_REQUEST ,
5930+ Json ( serde_json:: json!( { "error" : "Missing or empty 'base_url' field" } ) ) ,
5931+ ) ;
5932+ }
5933+ } ;
5934+
5935+ // Validate URL scheme
5936+ if !base_url. starts_with ( "http://" ) && !base_url. starts_with ( "https://" ) {
5937+ return (
5938+ StatusCode :: BAD_REQUEST ,
5939+ Json ( serde_json:: json!( { "error" : "base_url must start with http:// or https://" } ) ) ,
5940+ ) ;
5941+ }
5942+
5943+ // Update catalog in memory
5944+ {
5945+ let mut catalog = state
5946+ . kernel
5947+ . model_catalog
5948+ . write ( )
5949+ . unwrap_or_else ( |e| e. into_inner ( ) ) ;
5950+ catalog. set_provider_url ( & name, & base_url) ;
5951+ }
5952+
5953+ // Persist to config.toml [provider_urls] section
5954+ let config_path = state. kernel . config . home_dir . join ( "config.toml" ) ;
5955+ if let Err ( e) = upsert_provider_url ( & config_path, & name, & base_url) {
5956+ return (
5957+ StatusCode :: INTERNAL_SERVER_ERROR ,
5958+ Json ( serde_json:: json!( { "error" : format!( "Failed to save config: {e}" ) } ) ) ,
5959+ ) ;
5960+ }
5961+
5962+ // Probe reachability at the new URL
5963+ let probe =
5964+ openfang_runtime:: provider_health:: probe_provider ( & name, & base_url) . await ;
5965+
5966+ (
5967+ StatusCode :: OK ,
5968+ Json ( serde_json:: json!( {
5969+ "status" : "saved" ,
5970+ "provider" : name,
5971+ "base_url" : base_url,
5972+ "reachable" : probe. reachable,
5973+ "latency_ms" : probe. latency_ms,
5974+ } ) ) ,
5975+ )
5976+ }
5977+
5978+ /// Upsert a provider URL in the `[provider_urls]` section of config.toml.
5979+ fn upsert_provider_url (
5980+ config_path : & std:: path:: Path ,
5981+ provider : & str ,
5982+ url : & str ,
5983+ ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
5984+ let content = if config_path. exists ( ) {
5985+ std:: fs:: read_to_string ( config_path) ?
5986+ } else {
5987+ String :: new ( )
5988+ } ;
5989+
5990+ let mut doc: toml:: Value = if content. trim ( ) . is_empty ( ) {
5991+ toml:: Value :: Table ( toml:: map:: Map :: new ( ) )
5992+ } else {
5993+ toml:: from_str ( & content) ?
5994+ } ;
5995+
5996+ let root = doc. as_table_mut ( ) . ok_or ( "Config is not a TOML table" ) ?;
5997+
5998+ if !root. contains_key ( "provider_urls" ) {
5999+ root. insert (
6000+ "provider_urls" . to_string ( ) ,
6001+ toml:: Value :: Table ( toml:: map:: Map :: new ( ) ) ,
6002+ ) ;
6003+ }
6004+ let urls_table = root
6005+ . get_mut ( "provider_urls" )
6006+ . and_then ( |v| v. as_table_mut ( ) )
6007+ . ok_or ( "provider_urls is not a table" ) ?;
6008+
6009+ urls_table. insert ( provider. to_string ( ) , toml:: Value :: String ( url. to_string ( ) ) ) ;
6010+
6011+ if let Some ( parent) = config_path. parent ( ) {
6012+ std:: fs:: create_dir_all ( parent) ?;
6013+ }
6014+
6015+ std:: fs:: write ( config_path, toml:: to_string_pretty ( & doc) ?) ?;
6016+ Ok ( ( ) )
6017+ }
6018+
59026019/// POST /api/skills/create — Create a local prompt-only skill.
59036020pub async fn create_skill (
59046021 State ( state) : State < Arc < AppState > > ,
0 commit comments