Two things a session could not say, and one it was saying wrongly.
**Every provider setting is reachable.** `-np 1`, the MTP draft depth, the
tool set, the sampling parameters -- most were hardcoded to what measured
best on this machine, which is right as a default and wrong as a constant:
the next machine has a different GPU and a different core count, and
nobody running this app can edit the source. `DriverKind::params` now
declares what a provider takes -- key, label, shape, what blank means, and
whether a change waits for a restart -- and the phone renders whatever
arrives, on the spawn form and in the session settings dialog. Adding a
setting to a driver is one entry in that table and no app change.
`POST /sessions/{id}/params` takes the whole map, so an absent key is the
instruction to unset; the sampling half applies at once and the session is
told in words which of the rest are waiting for a restart.
`tools` is one of them, because it is the biggest lever on a tight
context: the seven built-in definitions are ~1,300 tokens of every prompt
(2,191 against 887 with none). `"none"` omits the flag rather than passing
it on, since `--tools none` is `unknown tool "none"` and a server that
exits.
**The context figure has a denominator.** `Event::ContextWindow` carries
it, read from `llama-server`'s `/props` once the model is up -- the
measurement rather than the request, since a session that named no context
size gets the model's own. Neither coding CLI states its window, so those
keep the bare figure: "2,042" and "2,042 / 8,192" are deliberately
different-looking, and a missing ceiling is never drawn as a proportion of
an assumed one.
**And the numerator was wrong**, by the length of the last reply: it was
the prompt alone, so a five-word answer reported 2,042 against a slot
holding 2,355. It is the turn's total now, which matches `llama-server`'s
own `n_tokens` to within a token.
Two defects the review found, both of which would have shipped: changing
settings on a *stopped* session reported "no process running, so it can't
take new settings", when a stopped session is exactly when you would set
them for the next start; and `GET /tools` answers **403** rather than an
empty list on a server started without `--tools`, so reading it as a
failure made the no-tools session one that never started.
Verified against real models: settings spawned and changed live, the
restart note, a session with two tools and one with none, and the counter
checked against the server's own slot occupancy each time.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
118 lines
5.1 KiB
Kotlin
118 lines
5.1 KiB
Kotlin
package com.example.aiapp
|
|
|
|
import androidx.compose.foundation.layout.Column
|
|
import androidx.compose.foundation.layout.Spacer
|
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
import androidx.compose.foundation.layout.height
|
|
import androidx.compose.foundation.text.KeyboardOptions
|
|
import androidx.compose.material3.MaterialTheme
|
|
import androidx.compose.material3.OutlinedTextField
|
|
import androidx.compose.material3.Text
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.text.input.KeyboardType
|
|
import androidx.compose.ui.unit.dp
|
|
|
|
/**
|
|
* The controls for whatever settings a provider says it takes.
|
|
*
|
|
* One composable for both screens that offer them — the spawn form and the session settings dialog
|
|
* — and for every provider, because the server declares the list (see `DriverKind::params`) rather
|
|
* than this file knowing it. A driver that grows a setting gets a control here with no change to
|
|
* the app, which is the whole point: the values that suit one machine ship as defaults, and every
|
|
* one of them stays reachable from a phone.
|
|
*
|
|
* [values] is the whole map and [onChange] hands back the whole map. A key absent from it means the
|
|
* setting is unset, which is what every [ParamSpec.unset] describes — so clearing a field and never
|
|
* touching it are deliberately the same state.
|
|
*/
|
|
@Composable
|
|
fun ProviderParamFields(
|
|
specs: List<ParamSpec>,
|
|
values: Map<String, String>,
|
|
onChange: (Map<String, String>) -> Unit,
|
|
/**
|
|
* Whether to say which settings wait for a restart. False on a spawn form, where nothing is
|
|
* running yet and every setting is about to be read — saying it there would be a warning about
|
|
* a state the reader cannot be in.
|
|
*/
|
|
warnAboutRestart: Boolean,
|
|
modifier: Modifier = Modifier,
|
|
) {
|
|
if (specs.isEmpty()) return
|
|
Column(modifier.fillMaxWidth()) {
|
|
specs.forEach { spec ->
|
|
val set = { value: String ->
|
|
onChange(
|
|
// Blank clears rather than storing an empty string: the server reads an absent
|
|
// key as "use the default", and an empty one would be a value it then failed
|
|
// to parse.
|
|
if (value.isBlank()) values - spec.key else values + (spec.key to value)
|
|
)
|
|
}
|
|
when (spec.kind) {
|
|
"choice" -> {
|
|
// The first option is what unset means, so selecting it clears the key — see
|
|
// `ParamKind::Choice`. Without that the picker could show a default it could
|
|
// not return to.
|
|
val default = spec.options.firstOrNull().orEmpty()
|
|
ChipGroup(
|
|
label = spec.label + restartSuffix(spec, warnAboutRestart),
|
|
options = spec.options,
|
|
selected = values[spec.key] ?: default,
|
|
onSelect = { chosen -> set(if (chosen == default) "" else chosen) },
|
|
)
|
|
}
|
|
else ->
|
|
OutlinedTextField(
|
|
value = values[spec.key].orEmpty(),
|
|
onValueChange = set,
|
|
label = { Text(spec.label + restartSuffix(spec, warnAboutRestart)) },
|
|
placeholder = { Text(spec.unset) },
|
|
singleLine = true,
|
|
keyboardOptions = KeyboardOptions(keyboardType = keyboardFor(spec.kind)),
|
|
modifier = Modifier.fillMaxWidth(),
|
|
)
|
|
}
|
|
Spacer(Modifier.height(16.dp))
|
|
}
|
|
if (warnAboutRestart && specs.any { it.restart }) {
|
|
Text(
|
|
"A setting marked “on restart” is saved now and read when this session's process " +
|
|
"next starts.",
|
|
style = MaterialTheme.typography.bodySmall,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Marks a control whose value will not take effect yet.
|
|
*
|
|
* On the label rather than beside it, because the reader decides whether to change the thing before
|
|
* they touch it — a note underneath is read after the decision.
|
|
*/
|
|
private fun restartSuffix(spec: ParamSpec, warn: Boolean): String =
|
|
if (warn && spec.restart) " (on restart)" else ""
|
|
|
|
/**
|
|
* The keyboard for a value's shape. A number field that opens the letter keyboard is one every
|
|
* entry is made harder by, and these are nearly all numbers.
|
|
*/
|
|
private fun keyboardFor(kind: String): KeyboardType =
|
|
when (kind) {
|
|
"integer" -> KeyboardType.Number
|
|
"decimal" -> KeyboardType.Decimal
|
|
else -> KeyboardType.Text
|
|
}
|
|
|
|
/**
|
|
* How long typing has to stop before edited settings are sent.
|
|
*
|
|
* Long enough that a number is one request rather than one per digit, short enough that closing the
|
|
* dialog straight after typing still saves — the save runs on the screen behind it, which outlives
|
|
* the dialog, so this delay is not a window the value can be lost in.
|
|
*/
|
|
const val PARAM_SAVE_DELAY_MS = 700L
|