@@ -485,11 +485,18 @@ extension OpenAIResponsesService {
485485 " max_output_tokens " : ConfigService . maxOutputTokens ( )
486486 ]
487487 if let web, web. enabled {
488- var tools : [ [ String : Any ] ] = [ [ " type " : " web_search " ] ]
489- if !web. allowedDomains. isEmpty {
490- tools = [ [ " type " : " web_search " , " filters " : [ " allowed_domains " : web. allowedDomains] ] ]
488+ var tool : [ String : Any ] = [ " type " : " web_search " ]
489+ if !web. allowedDomains. isEmpty { tool [ " filters " ] = [ " allowed_domains " : web. allowedDomains] }
490+ // Build validated user_location, following Responses tool schema
491+ var userLoc : [ String : Any ] = [ " type " : " approximate " ]
492+ if let cc = web. country? . trimmingCharacters ( in: . whitespacesAndNewlines) . uppercased ( ) , cc. range ( of: " ^[A-Z]{2}$ " , options: . regularExpression) != nil {
493+ userLoc [ " country " ] = cc
491494 }
492- body [ " tools " ] = tools
495+ if let city = web. city? . trimmingCharacters ( in: . whitespacesAndNewlines) , !city. isEmpty { userLoc [ " city " ] = city }
496+ if let region = web. region? . trimmingCharacters ( in: . whitespacesAndNewlines) , !region. isEmpty { userLoc [ " region " ] = region }
497+ if let tz = web. timezone? . trimmingCharacters ( in: . whitespacesAndNewlines) , !tz. isEmpty, TimeZone ( identifier: tz) != nil { userLoc [ " timezone " ] = tz }
498+ if userLoc. keys. count > 1 { tool [ " user_location " ] = userLoc } // more than just type
499+ body [ " tools " ] = [ tool]
493500 body [ " tool_choice " ] = " auto "
494501 var include : [ String ] = [ ]
495502 if let cur = body [ " include " ] as? [ String ] { include = cur }
@@ -541,11 +548,17 @@ extension OpenAIResponsesService {
541548 " stream " : true
542549 ]
543550 if let web, web. enabled {
544- var tools : [ [ String : Any ] ] = [ [ " type " : " web_search " ] ]
545- if !web. allowedDomains. isEmpty {
546- tools = [ [ " type " : " web_search " , " filters " : [ " allowed_domains " : web. allowedDomains] ] ]
551+ var tool : [ String : Any ] = [ " type " : " web_search " ]
552+ if !web. allowedDomains. isEmpty { tool [ " filters " ] = [ " allowed_domains " : web. allowedDomains] }
553+ var userLoc : [ String : Any ] = [ " type " : " approximate " ]
554+ if let cc = web. country? . trimmingCharacters ( in: . whitespacesAndNewlines) . uppercased ( ) , cc. range ( of: " ^[A-Z]{2}$ " , options: . regularExpression) != nil {
555+ userLoc [ " country " ] = cc
547556 }
548- body [ " tools " ] = tools
557+ if let city = web. city? . trimmingCharacters ( in: . whitespacesAndNewlines) , !city. isEmpty { userLoc [ " city " ] = city }
558+ if let region = web. region? . trimmingCharacters ( in: . whitespacesAndNewlines) , !region. isEmpty { userLoc [ " region " ] = region }
559+ if let tz = web. timezone? . trimmingCharacters ( in: . whitespacesAndNewlines) , !tz. isEmpty, TimeZone ( identifier: tz) != nil { userLoc [ " timezone " ] = tz }
560+ if userLoc. keys. count > 1 { tool [ " user_location " ] = userLoc }
561+ body [ " tools " ] = [ tool]
549562 body [ " tool_choice " ] = " auto "
550563 var include : [ String ] = [ ]
551564 if let cur = body [ " include " ] as? [ String ] { include = cur }
@@ -686,11 +699,21 @@ extension OpenAIResponsesService {
686699 ]
687700 if let web, web. enabled {
688701 var ws : [ String : Any ] = [ : ]
689- if let c = web. country, let city = web. city, let reg = web. region {
690- ws [ " user_location " ] = [ " type " : " approximate " , " approximate " : [ " country " : c, " city " : city, " region " : reg] ]
691- }
702+ // Validate inputs
703+ let cc = web. country? . trimmingCharacters ( in: . whitespacesAndNewlines) . uppercased ( )
704+ let validCC = cc? . range ( of: " ^[A-Z]{2}$ " , options: . regularExpression) != nil ? cc : nil
705+ let city = web. city? . trimmingCharacters ( in: . whitespacesAndNewlines)
706+ let region = web. region? . trimmingCharacters ( in: . whitespacesAndNewlines)
707+ let tz = web. timezone? . trimmingCharacters ( in: . whitespacesAndNewlines)
708+ let validTZ = ( tz != nil && !tz!. isEmpty && TimeZone ( identifier: tz!) != nil ) ? tz : nil
709+
710+ var userLoc : [ String : Any ] = [ " type " : " approximate " ]
711+ if let validCC { userLoc [ " country " ] = validCC }
712+ if let city, !city. isEmpty { userLoc [ " city " ] = city }
713+ if let region, !region. isEmpty { userLoc [ " region " ] = region }
714+ if let validTZ { userLoc [ " timezone " ] = validTZ }
715+ if userLoc. keys. count > 1 { ws [ " user_location " ] = userLoc }
692716 if !web. allowedDomains. isEmpty { ws [ " allowed_domains " ] = web. allowedDomains }
693- if let tz = web. timezone, !tz. isEmpty { ws [ " timezone " ] = tz }
694717 if !ws. isEmpty { payload [ " web_search_options " ] = ws }
695718 }
696719 if supportsReasoning && !chatModelA. contains ( " search-preview " ) { payload [ " reasoning_effort " ] = reasoningEffort; payload [ " verbosity " ] = verbosity }
@@ -740,11 +763,20 @@ extension OpenAIResponsesService {
740763 ]
741764 if let web, web. enabled {
742765 var ws : [ String : Any ] = [ : ]
743- if let c = web. country, let city = web. city, let reg = web. region {
744- ws [ " user_location " ] = [ " type " : " approximate " , " approximate " : [ " country " : c, " city " : city, " region " : reg] ]
745- }
766+ let cc = web. country? . trimmingCharacters ( in: . whitespacesAndNewlines) . uppercased ( )
767+ let validCC = cc? . range ( of: " ^[A-Z]{2}$ " , options: . regularExpression) != nil ? cc : nil
768+ let city = web. city? . trimmingCharacters ( in: . whitespacesAndNewlines)
769+ let region = web. region? . trimmingCharacters ( in: . whitespacesAndNewlines)
770+ let tz = web. timezone? . trimmingCharacters ( in: . whitespacesAndNewlines)
771+ let validTZ = ( tz != nil && !tz!. isEmpty && TimeZone ( identifier: tz!) != nil ) ? tz : nil
772+
773+ var userLoc : [ String : Any ] = [ " type " : " approximate " ]
774+ if let validCC { userLoc [ " country " ] = validCC }
775+ if let city, !city. isEmpty { userLoc [ " city " ] = city }
776+ if let region, !region. isEmpty { userLoc [ " region " ] = region }
777+ if let validTZ { userLoc [ " timezone " ] = validTZ }
778+ if userLoc. keys. count > 1 { ws [ " user_location " ] = userLoc }
746779 if !web. allowedDomains. isEmpty { ws [ " allowed_domains " ] = web. allowedDomains }
747- if let tz = web. timezone, !tz. isEmpty { ws [ " timezone " ] = tz }
748780 if !ws. isEmpty { payload [ " web_search_options " ] = ws }
749781 }
750782 if supportsReasoning && !chatModelB. contains ( " search-preview " ) { payload [ " reasoning_effort " ] = reasoningEffort; payload [ " verbosity " ] = verbosity }
0 commit comments