git.iko.soy/opencode-android
on d518ef7 help sign in
84 lines 3.1 kB history raw
1package soy.iko.opencode.data.model
3import androidx.compose.runtime.Immutable
4import kotlinx.serialization.Serializable
6/** Response of `GET /config/providers`. */
7@Immutable
8@Serializable
9data class ProvidersResponse(
10 val providers: List<Provider> = emptyList(),
11 val default: Map<String, String> = emptyMap(),
14@Immutable
15@Serializable
16data class Provider(
17 // Defaulted so one id-less element in GET /config/providers doesn't fail the whole list decode
18 // (emptying the model picker) — matches MessageInfo's defaulted id and the resilient-decoding
19 // philosophy. A blank id is degraded but recoverable; a thrown decode is not.
20 val id: String = "",
21 val name: String? = null,
22 val models: Map<String, ModelInfo> = emptyMap(),
23) {
24 val displayName: String get() = name?.takeIf { it.isNotBlank() } ?: id
27@Immutable
28@Serializable
29data class ModelInfo(
30 val id: String? = null,
31 val name: String? = null,
32) {
33 fun displayName(modelKey: String): String =
34 name?.takeIf { it.isNotBlank() } ?: id ?: modelKey
37/** A flattened provider/model selection used by the prompt body and the model picker. */
38@Immutable
39@Serializable
40data class ModelRef(
41 val providerID: String,
42 val modelID: String,
45/** A single selectable entry in the model picker, flattened from [ProvidersResponse]. */
46@Immutable
47data class ModelOption(
48 val providerID: String,
49 val modelID: String,
50 val providerLabel: String,
51 val modelLabel: String,
52) {
53 val ref: ModelRef get() = ModelRef(providerID, modelID)
56/** Flatten providers into a sorted list of pickable options. */
57fun ProvidersResponse.toOptions(): List<ModelOption> =
58 providers.flatMap { provider ->
59 provider.models.map { (modelKey, info) ->
60 ModelOption(
61 providerID = provider.id,
62 modelID = info.id ?: modelKey,
63 providerLabel = provider.displayName,
64 modelLabel = info.displayName(modelKey),
65 )
66 }
67 }.sortedWith(
68 // CASE_INSENSITIVE_ORDER compares in place; compareBy { it.x.lowercase() } would
69 // allocate a fresh lowercased string on every comparison (O(N log N) allocations).
70 compareBy<ModelOption, String>(String.CASE_INSENSITIVE_ORDER) { it.providerLabel }
71 .thenBy(String.CASE_INSENSITIVE_ORDER) { it.modelLabel },
72 )
74/** The default option to preselect: the server's default for some provider, else the first model. */
75fun ProvidersResponse.defaultOption(options: List<ModelOption> = toOptions()): ModelOption? {
76 // Try each configured provider default in turn and take the first whose model actually
77 // survives in `options` (the list may be filtered). Checking only the first `default` entry
78 // would mispreselect an arbitrary first option whenever that one provider's default model
79 // isn't present, ignoring the other configured defaults.
80 for ((providerID, modelID) in default) {
81 options.firstOrNull { it.providerID == providerID && it.modelID == modelID }?.let { return it }
82 }
83 return options.firstOrNull()