Enable conversation-scoped speed selection for Cloud turns (#61178)

## What changed

- Stage Cloud speed choices for the next new turn in the conversation, alongside pending model and effort selections. Update the displayed conversation speed after admission without changing the local default.
- Offer speed commands supported by the pending model. Clear an incompatible pending speed change with a warning when switching models.
- Carry `service_tier` through Cloud message requests, preserving omission to inherit the thread tier and explicit null to clear it. Translate `default` to null and reject tier overrides on queued requests.

## Testing

Add tests for Cloud speed toggling, model-dependent speed choices, admission-time updates, and preserving the conversation tier when selecting a model. Extend request and wire tests to cover service-tier values, explicit clearing, and queue rejection.

GitOrigin-RevId: b4daa701f1fe29593d90a938e60cc20cb980298a
This commit is contained in:
Benjamin Carlsson
2026-11-12 20:56:33 +00:00
committed by copyberry
parent 72532ecae5
commit b279cfb170
20 changed files with 583 additions and 73 deletions
+1
View File
@@ -32,6 +32,7 @@ async fn main() -> anyhow::Result<()> {
turn_trigger: None,
model: None,
effort: None,
service_tier: None,
mode: SubmissionMode::StartIfIdle,
shared_access: false,
}),
+14 -1
View File
@@ -30,6 +30,8 @@ pub struct AddUserMessageRequest {
/// Omit both to inherit the thread settings. A model alone uses its default effort.
pub model: Option<String>,
pub effort: Option<String>,
/// Omit to inherit the thread tier, or send an explicit null to clear it.
pub service_tier: Option<Option<String>>,
pub mode: SubmissionMode,
pub shared_access: bool,
}
@@ -128,7 +130,9 @@ impl Client {
) -> Result<Admission, MutationError> {
if request.thread_id.trim().is_empty()
|| request.client_user_message_id.trim().is_empty()
|| ((request.model.is_some() || request.effort.is_some())
|| ((request.model.is_some()
|| request.effort.is_some()
|| request.service_tier.is_some())
&& matches!(request.mode, SubmissionMode::Queue))
|| !request.input.iter().any(|input| match input {
UserMessageInput::Text { text, .. } => !text.trim().is_empty(),
@@ -213,6 +217,15 @@ impl Client {
}),
model: request.model.clone(),
effort: request.effort.clone(),
service_tier: request.service_tier.as_ref().map(|tier| {
use crate::activation_wire::nullable_string::Value;
crate::activation_wire::NullableString {
value: Some(match tier {
Some(tier) => Value::String(tier.clone()),
None => Value::Null(crate::activation_wire::Empty {}),
}),
}
}),
thread_id: request.thread_id.clone(),
turn_trigger: request.turn_trigger.clone(),
access: request.shared_access.then_some(1),
+16
View File
@@ -13,6 +13,7 @@ fn request() -> AddUserMessageRequest {
turn_trigger: None,
model: None,
effort: None,
service_tier: None,
mode: SubmissionMode::StartIfIdle,
shared_access: true,
}
@@ -45,12 +46,21 @@ async fn submission_preserves_admission_variants_and_wire_identity() {
let mut fixture = fixture(response(vec![frame(&payload)], tonic::Status::ok(""))).await;
let has_selection = !matches!(mode, SubmissionMode::Queue);
let turn_trigger = (mode_number == 3).then(|| "user".to_owned());
let (service_tier, wire_tier) = match (mode_number, outcome_number) {
(3, 1) => (
Some(Some("ultrafast".into())),
message(/*tag*/ 20, &message(/*tag*/ 1, b"ultrafast")),
),
(1, 2) => (Some(None), message(/*tag*/ 20, &message(/*tag*/ 2, b""))),
_ => (None, Vec::new()),
};
let result = fixture
.client
.add_user_message(&AddUserMessageRequest {
turn_trigger,
model: has_selection.then(|| "selected-model".into()),
effort: has_selection.then(|| "high".into()),
service_tier,
mode,
..request()
})
@@ -106,6 +116,7 @@ async fn submission_preserves_admission_variants_and_wire_identity() {
),
),
if has_selection { model } else { vec![] },
wire_tier,
message(/*tag*/ 23, b"t"),
if mode_number == 3 {
message(/*tag*/ 25, b"user")
@@ -295,6 +306,11 @@ async fn invalid_input_never_sends_and_diagnostics_redact() {
model: Some("selected-model".into()),
..request()
},
AddUserMessageRequest {
mode: SubmissionMode::Queue,
service_tier: Some(Some("ultrafast".into())),
..request()
},
AddUserMessageRequest {
input: vec![],
..request()
+2
View File
@@ -11,6 +11,8 @@ pub(super) struct AddRequest {
pub input: Option<Input>,
#[prost(string, optional, tag = "12")]
pub model: Option<String>,
#[prost(message, optional, tag = "20")]
pub service_tier: Option<crate::activation_wire::NullableString>,
#[prost(string, tag = "23")]
pub thread_id: String,
#[prost(string, optional, tag = "25")]