F3: Kimi inference pipeline + full grok cloud-surface excision

Sampler / inference (PRD F3):
- kimi_compat.rs: single adaptation point for the Kimi chat/completions
  dialect (thinking-field mapping, model_id stripping, empty-content
  tool-call message fix, stream_options.include_usage), with kimi-cli
  source citations
- Rate-limit handling reworked for Kimi/Moonshot semantics; UA kigi/{version}
- /models replaces the xAI models-v2 endpoint everywhere; idle model
  refresh carries X-Msh-* device headers only (X-XAI-Token-Auth and
  x-grok-client-mode/CLIENT_MODE_HEADER machinery deleted)

Cloud-surface excision (PRD §5, zero-egress):
- remote/ conversations lane, cli-chat-proxy-types crate, prod/ dir,
  share command, credit bar: deleted (single local session lane;
  paginate() replaces merge_and_paginate)
- Subscription/tier gate stack deleted end-to-end: AppView
  gate/tier/team/ZDR fields, app/subscription.rs watch loop,
  dispatch/billing.rs paywall + SuperGrok upsell, free-usage-exhausted
  chain, tier-restricted commands, GateInfo, RemoteSettings gate fields,
  SettingsUpdateNotification gate fields
- /privacy + coding-data-sharing setting deleted (backed by a dead xAI
  RPC; Kigi is zero-egress — nothing to share or retain remotely)

Auth UX correctness (user-reported):
- Device-flow fixtures now mirror the live Kimi payload shape
  (https://www.kimi.com/code/authorize_device?user_code=..., verified
  against auth.kimi.com); the fabricated auth.kimi.com/device?code=...
  URLs are gone
- open_browser_detached is a no-op under cfg(test): unit tests drove
  wiremock fixture URLs into the real browser (root cause of the
  "garbage mock link" ABCD-1234 tabs)
- Welcome/pager-minimal rebrand: Grok Build -> Kigi, grok.com ->
  kimi.com, "Sign in to Grok" -> "Sign in to Kimi"
This commit is contained in:
2026-07-17 16:05:51 -04:00
parent fe1f885bb3
commit ea0ce9d15f
231 changed files with 4730 additions and 26358 deletions
+7 -80
View File
@@ -24,14 +24,10 @@
//! - `Serialization` (response parsing failure)
//! - `MaxTokensTruncation` (by design)
//!
//! **Server hint** (`x-should-retry` header from CCP):
//! - `false` → Fatal immediately, regardless of status code
//! - `true` / absent → falls through to status-code logic above
//!
//! Today CCP's header mirrors the client's `is_retryable()` logic
//! (4xx except 429 = false, 5xx + 429 = true), so no behavior changes
//! on merge. The header enables future CCP-side refinements (e.g.
//! marking content-caused 500s as non-retryable) without client updates.
//! 429 handling honors the standard `Retry-After` response header when
//! present (delta-seconds; see `client::extract_retry_after`), matching
//! the Kimi/Moonshot API. The old xAI proxy's `x-should-retry` hint
//! header was removed with the proxy.
use std::time::Duration;
@@ -170,22 +166,6 @@ pub fn classify_error(
return RetryDecision::RetryWithImageStrip;
}
// Server explicitly said don't retry (x-should-retry: false).
// Trust the server — it knows if the error is request-content-caused
// (e.g. malformed tool call in conversation history) vs transient.
//
// x-should-retry: true is intentionally NOT handled here — we only
// use the header to suppress retries (false), not to force them
// (true). Forcing retries on non-retryable status codes could
// amplify failures. true falls through to existing status-code logic.
//
// Checked AFTER image-strip guards: image stripping changes the
// request payload, so a server "don't retry" on the original
// request doesn't apply to the stripped request.
if let Some(false) = err.should_retry_header() {
return RetryDecision::Fatal(clone_error(err));
}
// Context-window / size overflow is deterministic — re-sending the same (or
// larger) payload always fails — so never retry it, whatever status the backend
// used (in-stream `ResponseError`→500, HTTP 400/500, OpenAI/Anthropic variants).
@@ -401,13 +381,11 @@ pub(crate) fn clone_error(err: &SamplingError) -> SamplingError {
message,
model_metadata,
retry_after_secs,
should_retry,
} => SamplingError::Api {
status: *status,
message: message.clone(),
model_metadata: model_metadata.clone(),
retry_after_secs: *retry_after_secs,
should_retry: *should_retry,
},
SamplingError::EventStreamError(msg) => SamplingError::EventStreamError(msg.clone()),
SamplingError::StreamError {
@@ -445,7 +423,6 @@ mod tests {
message: message.to_string(),
model_metadata: None,
retry_after_secs: None,
should_retry: None,
}
}
@@ -455,7 +432,6 @@ mod tests {
message: "x".to_string(),
model_metadata: None,
retry_after_secs: Some(retry_after),
should_retry: None,
}
}
@@ -757,31 +733,15 @@ mod tests {
assert!(s.contains("240s"));
}
#[test]
fn should_retry_false_overrides_retryable_status() {
let err = SamplingError::Api {
status: StatusCode::INTERNAL_SERVER_ERROR,
message: "boom".into(),
model_metadata: None,
retry_after_secs: None,
should_retry: Some(false),
};
assert!(matches!(
classify_error(&err, 0, 15, RATE_LIMIT_RETRY_THRESHOLD),
RetryDecision::Fatal(_)
));
}
#[test]
fn context_length_overflow_is_fatal_even_as_500() {
// The backend streams a size overflow as a ResponseError that becomes a 500 with no
// should_retry hint; without the context-length check it would retry the full budget.
// The backend streams a size overflow as a ResponseError that becomes a 500;
// without the context-length check it would retry the full budget.
let err = SamplingError::Api {
status: StatusCode::INTERNAL_SERVER_ERROR,
message: "none: The prompt is too long for this model's context window.".into(),
model_metadata: None,
retry_after_secs: None,
should_retry: None,
};
assert!(matches!(
classify_error(&err, 0, 15, RATE_LIMIT_RETRY_THRESHOLD),
@@ -790,28 +750,12 @@ mod tests {
}
#[test]
fn should_retry_true_falls_through_to_existing_logic() {
fn api_500_first_failure_retries_with_client_rebuild() {
let err = SamplingError::Api {
status: StatusCode::INTERNAL_SERVER_ERROR,
message: "boom".into(),
model_metadata: None,
retry_after_secs: None,
should_retry: Some(true),
};
assert!(matches!(
classify_error(&err, 0, 15, RATE_LIMIT_RETRY_THRESHOLD),
RetryDecision::RetryWithClientRebuild { .. }
));
}
#[test]
fn should_retry_absent_falls_through() {
let err = SamplingError::Api {
status: StatusCode::INTERNAL_SERVER_ERROR,
message: "boom".into(),
model_metadata: None,
retry_after_secs: None,
should_retry: None,
};
assert!(matches!(
classify_error(&err, 0, 15, RATE_LIMIT_RETRY_THRESHOLD),
@@ -836,21 +780,4 @@ mod tests {
}
}
}
#[test]
fn should_retry_false_on_429_is_fatal() {
// Server says don't retry, even though 429 is normally retryable.
// should_retry check runs before rate-limit check.
let err = SamplingError::Api {
status: StatusCode::TOO_MANY_REQUESTS,
message: "rate limited".into(),
model_metadata: None,
retry_after_secs: Some(10),
should_retry: Some(false),
};
assert!(matches!(
classify_error(&err, 0, 15, RATE_LIMIT_RETRY_THRESHOLD),
RetryDecision::Fatal(_)
));
}
}