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:
@@ -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(_)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user