@@ -773,26 +773,28 @@ private constructor(
773773 class Options
774774 @JsonCreator(mode = JsonCreator .Mode .DISABLED )
775775 private constructor (
776- private val model: JsonField <ModelConfig >,
776+ private val model: JsonField <Model >,
777777 private val timeout: JsonField <Double >,
778778 private val variables: JsonField <Variables >,
779779 private val additionalProperties: MutableMap <String , JsonValue >,
780780 ) {
781781
782782 @JsonCreator
783783 private constructor (
784- @JsonProperty(" model" ) @ExcludeMissing model: JsonField <ModelConfig > = JsonMissing .of(),
784+ @JsonProperty(" model" ) @ExcludeMissing model: JsonField <Model > = JsonMissing .of(),
785785 @JsonProperty(" timeout" ) @ExcludeMissing timeout: JsonField <Double > = JsonMissing .of(),
786786 @JsonProperty(" variables" )
787787 @ExcludeMissing
788788 variables: JsonField <Variables > = JsonMissing .of(),
789789 ) : this (model, timeout, variables, mutableMapOf ())
790790
791791 /* *
792+ * Model configuration object or model name string (e.g., 'openai/gpt-5-nano')
793+ *
792794 * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if
793795 * the server responded with an unexpected value).
794796 */
795- fun model (): Optional <ModelConfig > = model.getOptional(" model" )
797+ fun model (): Optional <Model > = model.getOptional(" model" )
796798
797799 /* *
798800 * Timeout in ms for the action
@@ -815,7 +817,7 @@ private constructor(
815817 *
816818 * Unlike [model], this method doesn't throw if the JSON field has an unexpected type.
817819 */
818- @JsonProperty(" model" ) @ExcludeMissing fun _model (): JsonField <ModelConfig > = model
820+ @JsonProperty(" model" ) @ExcludeMissing fun _model (): JsonField <Model > = model
819821
820822 /* *
821823 * Returns the raw JSON value of [timeout].
@@ -854,7 +856,7 @@ private constructor(
854856 /* * A builder for [Options]. */
855857 class Builder internal constructor() {
856858
857- private var model: JsonField <ModelConfig > = JsonMissing .of()
859+ private var model: JsonField <Model > = JsonMissing .of()
858860 private var timeout: JsonField <Double > = JsonMissing .of()
859861 private var variables: JsonField <Variables > = JsonMissing .of()
860862 private var additionalProperties: MutableMap <String , JsonValue > = mutableMapOf ()
@@ -867,16 +869,23 @@ private constructor(
867869 additionalProperties = options.additionalProperties.toMutableMap()
868870 }
869871
870- fun model (model : ModelConfig ) = model(JsonField .of(model))
872+ /* * Model configuration object or model name string (e.g., 'openai/gpt-5-nano') */
873+ fun model (model : Model ) = model(JsonField .of(model))
871874
872875 /* *
873876 * Sets [Builder.model] to an arbitrary JSON value.
874877 *
875- * You should usually call [Builder.model] with a well-typed [ModelConfig ] value
876- * instead. This method is primarily for setting the field to an undocumented or not yet
877- * supported value.
878+ * You should usually call [Builder.model] with a well-typed [Model ] value instead. This
879+ * method is primarily for setting the field to an undocumented or not yet supported
880+ * value.
878881 */
879- fun model (model : JsonField <ModelConfig >) = apply { this .model = model }
882+ fun model (model : JsonField <Model >) = apply { this .model = model }
883+
884+ /* * Alias for calling [model] with `Model.ofConfig(config)`. */
885+ fun model (config : ModelConfig ) = model(Model .ofConfig(config))
886+
887+ /* * Alias for calling [model] with `Model.ofString(string)`. */
888+ fun model (string : String ) = model(Model .ofString(string))
880889
881890 /* * Timeout in ms for the action */
882891 fun timeout (timeout : Double ) = timeout(JsonField .of(timeout))
@@ -963,6 +972,178 @@ private constructor(
963972 (if (timeout.asKnown().isPresent) 1 else 0 ) +
964973 (variables.asKnown().getOrNull()?.validity() ? : 0 )
965974
975+ /* * Model configuration object or model name string (e.g., 'openai/gpt-5-nano') */
976+ @JsonDeserialize(using = Model .Deserializer ::class )
977+ @JsonSerialize(using = Model .Serializer ::class )
978+ class Model
979+ private constructor (
980+ private val config: ModelConfig ? = null ,
981+ private val string: String? = null ,
982+ private val _json : JsonValue ? = null ,
983+ ) {
984+
985+ fun config (): Optional <ModelConfig > = Optional .ofNullable(config)
986+
987+ fun string (): Optional <String > = Optional .ofNullable(string)
988+
989+ fun isConfig (): Boolean = config != null
990+
991+ fun isString (): Boolean = string != null
992+
993+ fun asConfig (): ModelConfig = config.getOrThrow(" config" )
994+
995+ fun asString (): String = string.getOrThrow(" string" )
996+
997+ fun _json (): Optional <JsonValue > = Optional .ofNullable(_json )
998+
999+ fun <T > accept (visitor : Visitor <T >): T =
1000+ when {
1001+ config != null -> visitor.visitConfig(config)
1002+ string != null -> visitor.visitString(string)
1003+ else -> visitor.unknown(_json )
1004+ }
1005+
1006+ private var validated: Boolean = false
1007+
1008+ fun validate (): Model = apply {
1009+ if (validated) {
1010+ return @apply
1011+ }
1012+
1013+ accept(
1014+ object : Visitor <Unit > {
1015+ override fun visitConfig (config : ModelConfig ) {
1016+ config.validate()
1017+ }
1018+
1019+ override fun visitString (string : String ) {}
1020+ }
1021+ )
1022+ validated = true
1023+ }
1024+
1025+ fun isValid (): Boolean =
1026+ try {
1027+ validate()
1028+ true
1029+ } catch (e: StagehandInvalidDataException ) {
1030+ false
1031+ }
1032+
1033+ /* *
1034+ * Returns a score indicating how many valid values are contained in this object
1035+ * recursively.
1036+ *
1037+ * Used for best match union deserialization.
1038+ */
1039+ @JvmSynthetic
1040+ internal fun validity (): Int =
1041+ accept(
1042+ object : Visitor <Int > {
1043+ override fun visitConfig (config : ModelConfig ) = config.validity()
1044+
1045+ override fun visitString (string : String ) = 1
1046+
1047+ override fun unknown (json : JsonValue ? ) = 0
1048+ }
1049+ )
1050+
1051+ override fun equals (other : Any? ): Boolean {
1052+ if (this == = other) {
1053+ return true
1054+ }
1055+
1056+ return other is Model && config == other.config && string == other.string
1057+ }
1058+
1059+ override fun hashCode (): Int = Objects .hash(config, string)
1060+
1061+ override fun toString (): String =
1062+ when {
1063+ config != null -> " Model{config=$config }"
1064+ string != null -> " Model{string=$string }"
1065+ _json != null -> " Model{_unknown=$_json }"
1066+ else -> throw IllegalStateException (" Invalid Model" )
1067+ }
1068+
1069+ companion object {
1070+
1071+ @JvmStatic fun ofConfig (config : ModelConfig ) = Model (config = config)
1072+
1073+ @JvmStatic fun ofString (string : String ) = Model (string = string)
1074+ }
1075+
1076+ /* *
1077+ * An interface that defines how to map each variant of [Model] to a value of type [T].
1078+ */
1079+ interface Visitor <out T > {
1080+
1081+ fun visitConfig (config : ModelConfig ): T
1082+
1083+ fun visitString (string : String ): T
1084+
1085+ /* *
1086+ * Maps an unknown variant of [Model] to a value of type [T].
1087+ *
1088+ * An instance of [Model] can contain an unknown variant if it was deserialized from
1089+ * data that doesn't match any known variant. For example, if the SDK is on an older
1090+ * version than the API, then the API may respond with new variants that the SDK is
1091+ * unaware of.
1092+ *
1093+ * @throws StagehandInvalidDataException in the default implementation.
1094+ */
1095+ fun unknown (json : JsonValue ? ): T {
1096+ throw StagehandInvalidDataException (" Unknown Model: $json " )
1097+ }
1098+ }
1099+
1100+ internal class Deserializer : BaseDeserializer <Model >(Model : :class) {
1101+
1102+ override fun ObjectCodec.deserialize (node : JsonNode ): Model {
1103+ val json = JsonValue .fromJsonNode(node)
1104+
1105+ val bestMatches =
1106+ sequenceOf(
1107+ tryDeserialize(node, jacksonTypeRef<ModelConfig >())?.let {
1108+ Model (config = it, _json = json)
1109+ },
1110+ tryDeserialize(node, jacksonTypeRef<String >())?.let {
1111+ Model (string = it, _json = json)
1112+ },
1113+ )
1114+ .filterNotNull()
1115+ .allMaxBy { it.validity() }
1116+ .toList()
1117+ return when (bestMatches.size) {
1118+ // This can happen if what we're deserializing is completely incompatible
1119+ // with all the possible variants (e.g. deserializing from boolean).
1120+ 0 -> Model (_json = json)
1121+ 1 -> bestMatches.single()
1122+ // If there's more than one match with the highest validity, then use the
1123+ // first completely valid match, or simply the first match if none are
1124+ // completely valid.
1125+ else -> bestMatches.firstOrNull { it.isValid() } ? : bestMatches.first()
1126+ }
1127+ }
1128+ }
1129+
1130+ internal class Serializer : BaseSerializer <Model >(Model : :class) {
1131+
1132+ override fun serialize (
1133+ value : Model ,
1134+ generator : JsonGenerator ,
1135+ provider : SerializerProvider ,
1136+ ) {
1137+ when {
1138+ value.config != null -> generator.writeObject(value.config)
1139+ value.string != null -> generator.writeObject(value.string)
1140+ value._json != null -> generator.writeObject(value._json )
1141+ else -> throw IllegalStateException (" Invalid Model" )
1142+ }
1143+ }
1144+ }
1145+ }
1146+
9661147 /* * Variables to substitute in the action instruction */
9671148 class Variables
9681149 @JsonCreator
0 commit comments