mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-09 04:30:20 +08:00
4228 lines
141 KiB
Rust
4228 lines
141 KiB
Rust
use std::collections::BTreeMap;
|
|
use std::future::Future;
|
|
use std::io::Error as IoError;
|
|
use std::net::IpAddr;
|
|
use std::sync::OnceLock;
|
|
use std::time::{Duration, Instant};
|
|
|
|
use aether_contracts::{
|
|
ExecutionPlan, ExecutionResult, ExecutionStreamTerminalSummary, ExecutionTelemetry,
|
|
RequestBody, ResponseBody, StandardizedUsage, StreamFrame, StreamFramePayload, StreamFrameType,
|
|
EXECUTION_REQUEST_FOLLOW_REDIRECTS_HEADER,
|
|
};
|
|
use axum::body::Bytes;
|
|
use base64::Engine as _;
|
|
use futures_util::stream::{self, BoxStream};
|
|
use futures_util::StreamExt;
|
|
use http::{HeaderMap, HeaderName, HeaderValue};
|
|
use http_body_util::BodyExt;
|
|
use regex::{Captures, Regex};
|
|
use serde_json::{json, Map, Value};
|
|
use uuid::Uuid;
|
|
use wreq::ws::message::Message as WreqWsMessage;
|
|
|
|
use crate::ai_serving::api::{
|
|
convert_standard_chat_response, maybe_bridge_standard_sync_json_to_stream,
|
|
CanonicalContentPart, CanonicalStreamEvent, CanonicalStreamFrame, ClaudeClientEmitter,
|
|
OpenAIChatClientEmitter, OpenAIResponsesClientEmitter, StreamingCanonicalUsage,
|
|
};
|
|
use crate::ai_serving::openai_responses_synthetic_reasoning_item_id;
|
|
use crate::clock::current_unix_secs;
|
|
use crate::execution_runtime::ndjson::encode_stream_frame_ndjson;
|
|
use crate::execution_runtime::transport::{
|
|
build_browser_wreq_client, build_request_body, build_request_headers,
|
|
decode_response_body_bytes, format_hyper_error_chain, format_upstream_request_error,
|
|
format_wreq_upstream_request_error, resolve_stream_first_byte_timeout, send_request,
|
|
stream_first_byte_timeout_message, with_non_stream_total_timeout, DirectHttpResponse,
|
|
ExecutionRuntimeTransportError, ExecutionTransportControls,
|
|
};
|
|
|
|
const GROK_INTERNAL_HEADER: &str = "x-aether-grok-runtime";
|
|
const GROK_ASSET_BASE: &str = "https://assets.grok.com/";
|
|
const GROK_UPLOAD_PATH: &str = "/rest/app-chat/upload-file";
|
|
const GROK_MEDIA_POST_PATH: &str = "/rest/media/post/create";
|
|
const GROK_IMAGINE_WS_URL: &str = "wss://grok.com/ws/imagine/listen";
|
|
const GROK_STANDARD_PROVIDER_API_FORMAT: &str = "openai:responses";
|
|
const GROK_PROMPT_OVERHEAD_TOKENS: u64 = 4;
|
|
const GROK_MAX_ATTACHMENT_REDIRECTS: usize = 5;
|
|
const GROK_IMAGINE_STREAM_TIMEOUT_MS: u64 = 10_000;
|
|
const GROK_IMAGINE_ROUND_TIMEOUT_MS: u64 = 120_000;
|
|
|
|
static GROK_RENDER_RE: OnceLock<Regex> = OnceLock::new();
|
|
|
|
fn grok_render_regex() -> &'static Regex {
|
|
GROK_RENDER_RE.get_or_init(|| {
|
|
Regex::new(
|
|
r#"(?s)<grok:render\s+card_id="([^"]+)"\s+card_type="([^"]+)"\s+type="([^"]+)"[^>]*>.*?</grok:render>"#,
|
|
)
|
|
.expect("Grok render regex should compile")
|
|
})
|
|
}
|
|
|
|
pub(crate) struct GrokRuntimeStream {
|
|
pub(crate) frame_stream: BoxStream<'static, Result<Bytes, IoError>>,
|
|
pub(crate) report_context: Option<Value>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct GrokCollected {
|
|
status_code: u16,
|
|
headers: BTreeMap<String, String>,
|
|
text: String,
|
|
thinking: String,
|
|
images: Vec<String>,
|
|
telemetry: ExecutionTelemetry,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct GrokImagineImage {
|
|
image_id: String,
|
|
order: usize,
|
|
url: Option<String>,
|
|
blob_b64: Option<String>,
|
|
done: bool,
|
|
moderated: bool,
|
|
}
|
|
|
|
impl Default for GrokCollected {
|
|
fn default() -> Self {
|
|
Self {
|
|
status_code: 0,
|
|
headers: BTreeMap::new(),
|
|
text: String::new(),
|
|
thinking: String::new(),
|
|
images: Vec::new(),
|
|
telemetry: ExecutionTelemetry {
|
|
ttfb_ms: None,
|
|
elapsed_ms: None,
|
|
upstream_bytes: None,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
struct GrokStreamAdapter {
|
|
buffered: String,
|
|
text: String,
|
|
thinking: String,
|
|
images: Vec<String>,
|
|
cards: BTreeMap<String, GrokCard>,
|
|
citation_order: Vec<String>,
|
|
last_citation_index: Option<usize>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct GrokCard {
|
|
url: Option<String>,
|
|
title: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
struct GrokUsageEstimate {
|
|
input_tokens: u64,
|
|
output_tokens: u64,
|
|
reasoning_tokens: u64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
struct GrokAttachmentInput {
|
|
source: String,
|
|
filename: Option<String>,
|
|
mime_type: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct GrokAttachmentPayload {
|
|
filename: String,
|
|
mime_type: String,
|
|
content_b64: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct GrokUploadedAttachment {
|
|
file_id: String,
|
|
file_uri: Option<String>,
|
|
}
|
|
|
|
pub(crate) async fn maybe_execute_grok_sync(
|
|
plan: &ExecutionPlan,
|
|
report_context: Option<&Value>,
|
|
) -> Result<Option<ExecutionResult>, ExecutionRuntimeTransportError> {
|
|
if !is_grok_plan(plan, report_context) {
|
|
return Ok(None);
|
|
}
|
|
with_non_stream_total_timeout(plan, async move {
|
|
let mut collected = execute_grok_app_chat(plan, report_context).await?;
|
|
materialize_grok_image_assets(plan, &mut collected).await;
|
|
Ok(Some(grok_execution_result(plan, collected, report_context)))
|
|
})
|
|
.await
|
|
}
|
|
|
|
pub(crate) async fn maybe_execute_grok_stream(
|
|
plan: &ExecutionPlan,
|
|
report_context: Option<&Value>,
|
|
) -> Result<Option<GrokRuntimeStream>, ExecutionRuntimeTransportError> {
|
|
if !is_grok_plan(plan, report_context) {
|
|
return Ok(None);
|
|
}
|
|
if grok_should_collect_image_stream(plan, report_context)? {
|
|
let collected = execute_grok_app_chat(plan, report_context).await?;
|
|
return Ok(Some(GrokRuntimeStream {
|
|
frame_stream: grok_collected_frame_stream(plan.clone(), collected, report_context),
|
|
report_context: report_context.cloned(),
|
|
}));
|
|
}
|
|
Ok(Some(
|
|
execute_grok_app_chat_stream(plan, report_context).await?,
|
|
))
|
|
}
|
|
|
|
fn is_grok_plan(plan: &ExecutionPlan, report_context: Option<&Value>) -> bool {
|
|
let header_marker = plan
|
|
.headers
|
|
.iter()
|
|
.any(|(name, value)| name.eq_ignore_ascii_case(GROK_INTERNAL_HEADER) && value == "1");
|
|
let context_marker = report_context
|
|
.and_then(|value| value.get("provider_type"))
|
|
.and_then(Value::as_str)
|
|
.map(|value| value.eq_ignore_ascii_case("grok"))
|
|
.unwrap_or(false);
|
|
header_marker || context_marker
|
|
}
|
|
|
|
async fn execute_grok_app_chat(
|
|
plan: &ExecutionPlan,
|
|
report_context: Option<&Value>,
|
|
) -> Result<GrokCollected, ExecutionRuntimeTransportError> {
|
|
if grok_should_use_imagine_websocket(plan, report_context)? {
|
|
return execute_grok_imagine_websocket(plan, report_context).await;
|
|
}
|
|
let upstream_plan = grok_upstream_plan(plan, report_context).await?;
|
|
let request_body = build_request_body(&upstream_plan)?;
|
|
let started_at = Instant::now();
|
|
let response = send_request(&upstream_plan, request_body).await?;
|
|
let ttfb_ms = started_at.elapsed().as_millis() as u64;
|
|
let status_code = response.status_code();
|
|
let headers = response.headers();
|
|
let mut upstream_bytes = 0u64;
|
|
let mut raw_body = Vec::new();
|
|
let mut adapter = GrokStreamAdapter::default();
|
|
collect_grok_response_stream(
|
|
response,
|
|
status_code,
|
|
&mut upstream_bytes,
|
|
&mut raw_body,
|
|
&mut adapter,
|
|
)
|
|
.await?;
|
|
if (200..300).contains(&status_code) {
|
|
adapter.finish();
|
|
}
|
|
|
|
let elapsed_ms = started_at.elapsed().as_millis() as u64;
|
|
if !(200..300).contains(&status_code) {
|
|
let decoded = decode_response_body_bytes(&headers, &raw_body)?;
|
|
let text = String::from_utf8_lossy(decoded.as_ref()).to_string();
|
|
return Ok(GrokCollected {
|
|
status_code,
|
|
headers,
|
|
text,
|
|
telemetry: ExecutionTelemetry {
|
|
ttfb_ms: Some(ttfb_ms),
|
|
elapsed_ms: Some(elapsed_ms),
|
|
upstream_bytes: Some(upstream_bytes),
|
|
},
|
|
..GrokCollected::default()
|
|
});
|
|
}
|
|
|
|
Ok(GrokCollected {
|
|
status_code,
|
|
headers,
|
|
text: adapter.text,
|
|
thinking: adapter.thinking,
|
|
images: adapter.images,
|
|
telemetry: ExecutionTelemetry {
|
|
ttfb_ms: Some(ttfb_ms),
|
|
elapsed_ms: Some(elapsed_ms),
|
|
upstream_bytes: Some(upstream_bytes),
|
|
},
|
|
})
|
|
}
|
|
|
|
async fn execute_grok_app_chat_stream(
|
|
plan: &ExecutionPlan,
|
|
report_context: Option<&Value>,
|
|
) -> Result<GrokRuntimeStream, ExecutionRuntimeTransportError> {
|
|
let upstream_plan = grok_upstream_plan(plan, report_context).await?;
|
|
let request_body = build_request_body(&upstream_plan)?;
|
|
let started_at = Instant::now();
|
|
let response = send_request(&upstream_plan, request_body).await?;
|
|
let status_code = response.status_code();
|
|
let headers = response.headers();
|
|
if !(200..300).contains(&status_code) {
|
|
let mut upstream_bytes = 0u64;
|
|
let mut raw_body = Vec::new();
|
|
let mut adapter = GrokStreamAdapter::default();
|
|
collect_grok_response_stream(
|
|
response,
|
|
status_code,
|
|
&mut upstream_bytes,
|
|
&mut raw_body,
|
|
&mut adapter,
|
|
)
|
|
.await?;
|
|
let decoded = decode_response_body_bytes(&headers, &raw_body)?;
|
|
let text = String::from_utf8_lossy(decoded.as_ref()).to_string();
|
|
let elapsed_ms = started_at.elapsed().as_millis() as u64;
|
|
let collected = GrokCollected {
|
|
status_code,
|
|
headers,
|
|
text,
|
|
telemetry: ExecutionTelemetry {
|
|
ttfb_ms: Some(elapsed_ms),
|
|
elapsed_ms: Some(elapsed_ms),
|
|
upstream_bytes: Some(upstream_bytes),
|
|
},
|
|
..GrokCollected::default()
|
|
};
|
|
return Ok(GrokRuntimeStream {
|
|
frame_stream: grok_collected_frame_stream(plan.clone(), collected, report_context),
|
|
report_context: report_context.cloned(),
|
|
});
|
|
}
|
|
|
|
Ok(GrokRuntimeStream {
|
|
frame_stream: grok_success_frame_stream(
|
|
plan.clone(),
|
|
status_code,
|
|
headers,
|
|
started_at,
|
|
grok_response_body_stream(response),
|
|
),
|
|
report_context: report_context.cloned(),
|
|
})
|
|
}
|
|
|
|
fn grok_should_use_imagine_websocket(
|
|
plan: &ExecutionPlan,
|
|
report_context: Option<&Value>,
|
|
) -> Result<bool, ExecutionRuntimeTransportError> {
|
|
let client_format = normalized_client_api_format(plan);
|
|
if !matches!(
|
|
client_format.as_str(),
|
|
"openai:image" | "openai:responses" | "openai:responses:compact" | "openai:chat"
|
|
) {
|
|
return Ok(false);
|
|
}
|
|
let mapped_model = grok_upstream_model_name(report_context)?;
|
|
let model = mapped_model.to_ascii_lowercase();
|
|
Ok(model.contains("grok-imagine-image") && !model.contains("lite") && !model.contains("edit"))
|
|
}
|
|
|
|
fn grok_should_collect_image_stream(
|
|
plan: &ExecutionPlan,
|
|
report_context: Option<&Value>,
|
|
) -> Result<bool, ExecutionRuntimeTransportError> {
|
|
if normalized_client_api_format(plan) == "openai:image" {
|
|
return Ok(true);
|
|
}
|
|
if grok_plan_uses_structured_image_generation(plan, report_context) {
|
|
return Ok(true);
|
|
}
|
|
grok_should_use_imagine_websocket(plan, report_context)
|
|
}
|
|
|
|
async fn execute_grok_imagine_websocket(
|
|
plan: &ExecutionPlan,
|
|
report_context: Option<&Value>,
|
|
) -> Result<GrokCollected, ExecutionRuntimeTransportError> {
|
|
let body = plan.body.json_body.as_ref().ok_or_else(|| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(
|
|
"Grok Imagine requires JSON request body".to_string(),
|
|
)
|
|
})?;
|
|
let prompt = grok_image_prompt_from_provider_body(body).ok_or_else(|| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(
|
|
"Grok Imagine requires a non-empty prompt".to_string(),
|
|
)
|
|
})?;
|
|
let requested = grok_image_count_from_provider_body(body).clamp(1, 4);
|
|
let enable_pro = grok_upstream_model_name(report_context)?
|
|
.to_ascii_lowercase()
|
|
.contains("pro");
|
|
let aspect_ratio = grok_aspect_ratio_from_provider_body(body);
|
|
let started_at = Instant::now();
|
|
let mut images =
|
|
grok_imagine_websocket_images(plan, &prompt, requested, &aspect_ratio, enable_pro).await?;
|
|
images.sort_by_key(|image| image.order);
|
|
Ok(GrokCollected {
|
|
status_code: 200,
|
|
headers: BTreeMap::from([("content-type".to_string(), "application/json".to_string())]),
|
|
images: images
|
|
.into_iter()
|
|
.filter_map(|image| {
|
|
image
|
|
.url
|
|
.or_else(|| image.blob_b64.map(grok_data_image_url))
|
|
})
|
|
.collect(),
|
|
telemetry: ExecutionTelemetry {
|
|
ttfb_ms: None,
|
|
elapsed_ms: Some(started_at.elapsed().as_millis() as u64),
|
|
upstream_bytes: None,
|
|
},
|
|
..GrokCollected::default()
|
|
})
|
|
}
|
|
|
|
async fn grok_imagine_websocket_images(
|
|
plan: &ExecutionPlan,
|
|
prompt: &str,
|
|
requested: usize,
|
|
aspect_ratio: &str,
|
|
enable_pro: bool,
|
|
) -> Result<Vec<GrokImagineImage>, ExecutionRuntimeTransportError> {
|
|
let headers = build_request_headers(&plan.headers, None, false)?;
|
|
let profile = plan.transport_profile.as_ref().ok_or_else(|| {
|
|
ExecutionRuntimeTransportError::UnsupportedTransportProfile("browser_wreq".to_string())
|
|
})?;
|
|
let client = build_browser_wreq_client(
|
|
plan.timeouts.as_ref(),
|
|
plan.proxy.as_ref(),
|
|
profile,
|
|
ExecutionTransportControls::default(),
|
|
true,
|
|
)?;
|
|
let response = client
|
|
.websocket(GROK_IMAGINE_WS_URL)
|
|
.headers(headers)
|
|
.max_frame_size(64 << 20)
|
|
.max_message_size(64 << 20)
|
|
.send()
|
|
.await
|
|
.map_err(|err| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(format_wreq_upstream_request_error(
|
|
&err,
|
|
))
|
|
})?;
|
|
let status = response.status();
|
|
if !status.is_success() && status.as_u16() != 101 {
|
|
return Err(ExecutionRuntimeTransportError::UpstreamRequest(format!(
|
|
"Grok Imagine websocket returned {}",
|
|
status.as_u16()
|
|
)));
|
|
}
|
|
let mut websocket = response.into_websocket().await.map_err(|err| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(format_wreq_upstream_request_error(&err))
|
|
})?;
|
|
let reset = grok_imagine_reset_message();
|
|
websocket
|
|
.send(WreqWsMessage::text(reset.to_string()))
|
|
.await
|
|
.map_err(|err| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(format_wreq_upstream_request_error(
|
|
&err,
|
|
))
|
|
})?;
|
|
let request = grok_imagine_request_message(prompt, aspect_ratio, enable_pro);
|
|
websocket
|
|
.send(WreqWsMessage::text(request.to_string()))
|
|
.await
|
|
.map_err(|err| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(format_wreq_upstream_request_error(
|
|
&err,
|
|
))
|
|
})?;
|
|
|
|
let deadline = Instant::now() + Duration::from_millis(GROK_IMAGINE_ROUND_TIMEOUT_MS);
|
|
let mut slots: BTreeMap<String, GrokImagineImage> = BTreeMap::new();
|
|
while Instant::now() < deadline {
|
|
if grok_imagine_completed_count(&slots) >= requested {
|
|
break;
|
|
}
|
|
let remaining = deadline.saturating_duration_since(Instant::now());
|
|
let timeout = remaining.min(Duration::from_millis(GROK_IMAGINE_STREAM_TIMEOUT_MS));
|
|
let Some(message) = tokio::time::timeout(timeout, websocket.recv())
|
|
.await
|
|
.map_err(|_| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(
|
|
"Grok Imagine websocket timed out waiting for image frames".to_string(),
|
|
)
|
|
})?
|
|
else {
|
|
break;
|
|
};
|
|
let message = message.map_err(|err| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(format_wreq_upstream_request_error(
|
|
&err,
|
|
))
|
|
})?;
|
|
let WreqWsMessage::Text(text) = message else {
|
|
continue;
|
|
};
|
|
let Ok(value) = serde_json::from_str::<Value>(text.as_str()) else {
|
|
continue;
|
|
};
|
|
grok_handle_imagine_ws_message(&value, &mut slots)?;
|
|
}
|
|
|
|
let mut images = slots
|
|
.into_values()
|
|
.filter(|image| {
|
|
image.done && !image.moderated && (image.url.is_some() || image.blob_b64.is_some())
|
|
})
|
|
.collect::<Vec<_>>();
|
|
images.sort_by_key(|image| image.order);
|
|
images.truncate(requested);
|
|
if images.is_empty() {
|
|
return Err(ExecutionRuntimeTransportError::UpstreamRequest(
|
|
"Grok Imagine returned no images".to_string(),
|
|
));
|
|
}
|
|
Ok(images)
|
|
}
|
|
|
|
async fn collect_grok_response_stream(
|
|
response: DirectHttpResponse,
|
|
status_code: u16,
|
|
upstream_bytes: &mut u64,
|
|
raw_body: &mut Vec<u8>,
|
|
adapter: &mut GrokStreamAdapter,
|
|
) -> Result<(), ExecutionRuntimeTransportError> {
|
|
match response {
|
|
DirectHttpResponse::Reqwest(response) => {
|
|
let mut stream = response.bytes_stream();
|
|
while let Some(chunk) = stream.next().await {
|
|
let chunk = chunk.map_err(|err| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(format_upstream_request_error(
|
|
&err,
|
|
))
|
|
})?;
|
|
collect_grok_response_chunk(status_code, upstream_bytes, raw_body, adapter, &chunk);
|
|
}
|
|
}
|
|
DirectHttpResponse::HyperH2c(response) => {
|
|
let mut stream = response.into_body().into_data_stream();
|
|
while let Some(chunk) = stream.next().await {
|
|
let chunk = chunk.map_err(|err| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(format_hyper_error_chain(&err))
|
|
})?;
|
|
collect_grok_response_chunk(status_code, upstream_bytes, raw_body, adapter, &chunk);
|
|
}
|
|
}
|
|
DirectHttpResponse::BrowserWreq(response) => {
|
|
let mut stream = response.bytes_stream();
|
|
while let Some(chunk) = stream.next().await {
|
|
let chunk = chunk.map_err(|err| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(
|
|
format_wreq_upstream_request_error(&err),
|
|
)
|
|
})?;
|
|
collect_grok_response_chunk(status_code, upstream_bytes, raw_body, adapter, &chunk);
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn collect_grok_response_chunk(
|
|
status_code: u16,
|
|
upstream_bytes: &mut u64,
|
|
raw_body: &mut Vec<u8>,
|
|
adapter: &mut GrokStreamAdapter,
|
|
chunk: &[u8],
|
|
) {
|
|
*upstream_bytes += chunk.len() as u64;
|
|
raw_body.extend_from_slice(chunk);
|
|
if (200..300).contains(&status_code) {
|
|
adapter.push_chunk(chunk);
|
|
}
|
|
}
|
|
|
|
type GrokUpstreamBodyStream = BoxStream<'static, Result<Bytes, String>>;
|
|
|
|
fn grok_response_body_stream(response: DirectHttpResponse) -> GrokUpstreamBodyStream {
|
|
match response {
|
|
DirectHttpResponse::Reqwest(response) => response
|
|
.bytes_stream()
|
|
.map(|chunk| {
|
|
chunk.map_err(|err| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(format_upstream_request_error(
|
|
&err,
|
|
))
|
|
.to_string()
|
|
})
|
|
})
|
|
.boxed(),
|
|
DirectHttpResponse::HyperH2c(response) => response
|
|
.into_body()
|
|
.into_data_stream()
|
|
.map(|chunk| {
|
|
chunk.map_err(|err| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(format_hyper_error_chain(&err))
|
|
.to_string()
|
|
})
|
|
})
|
|
.boxed(),
|
|
DirectHttpResponse::BrowserWreq(response) => response
|
|
.bytes_stream()
|
|
.map(|chunk| {
|
|
chunk.map_err(|err| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(
|
|
format_wreq_upstream_request_error(&err),
|
|
)
|
|
.to_string()
|
|
})
|
|
})
|
|
.boxed(),
|
|
}
|
|
}
|
|
|
|
fn grok_success_frame_stream(
|
|
plan: ExecutionPlan,
|
|
status_code: u16,
|
|
headers: BTreeMap<String, String>,
|
|
started_at: Instant,
|
|
mut body_stream: GrokUpstreamBodyStream,
|
|
) -> BoxStream<'static, Result<Bytes, IoError>> {
|
|
let stream_first_byte_timeout = resolve_stream_first_byte_timeout(&plan);
|
|
async_stream::stream! {
|
|
match encode_grok_headers_frame(
|
|
status_code,
|
|
BTreeMap::from([("content-type".to_string(), "text/event-stream".to_string())]),
|
|
) {
|
|
Ok(frame) => yield Ok(frame),
|
|
Err(err) => {
|
|
yield Err(err);
|
|
return;
|
|
}
|
|
}
|
|
|
|
let mut adapter = GrokStreamAdapter::default();
|
|
let mut client_emitter = GrokClientStreamEmitter::new(&plan);
|
|
let mut upstream_bytes = 0u64;
|
|
let mut ttfb_ms = None;
|
|
let mut first_chunk_telemetry_emitted = false;
|
|
let mut text_len = 0usize;
|
|
let mut thinking_len = 0usize;
|
|
let mut image_len = 0usize;
|
|
let mut terminal_error_emitted = false;
|
|
|
|
loop {
|
|
let item = if ttfb_ms.is_none() {
|
|
match await_grok_stream_first_byte(
|
|
body_stream.next(),
|
|
started_at,
|
|
stream_first_byte_timeout,
|
|
)
|
|
.await
|
|
{
|
|
Ok(item) => item,
|
|
Err(timeout) => {
|
|
match encode_grok_first_byte_timeout_frame(timeout) {
|
|
Ok(frame) => yield Ok(frame),
|
|
Err(err) => {
|
|
yield Err(err);
|
|
return;
|
|
}
|
|
}
|
|
terminal_error_emitted = true;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
body_stream.next().await
|
|
};
|
|
let Some(item) = item else {
|
|
break;
|
|
};
|
|
let chunk = match item {
|
|
Ok(chunk) => chunk,
|
|
Err(message) => {
|
|
match encode_grok_error_frame(message) {
|
|
Ok(frame) => yield Ok(frame),
|
|
Err(err) => {
|
|
yield Err(err);
|
|
return;
|
|
}
|
|
}
|
|
terminal_error_emitted = true;
|
|
break;
|
|
}
|
|
};
|
|
if ttfb_ms.is_none() {
|
|
ttfb_ms = Some(started_at.elapsed().as_millis() as u64);
|
|
}
|
|
if !first_chunk_telemetry_emitted {
|
|
match encode_grok_telemetry_frame(ttfb_ms, ttfb_ms, upstream_bytes) {
|
|
Ok(frame) => yield Ok(frame),
|
|
Err(err) => {
|
|
yield Err(err);
|
|
return;
|
|
}
|
|
}
|
|
first_chunk_telemetry_emitted = true;
|
|
}
|
|
upstream_bytes += chunk.len() as u64;
|
|
adapter.push_chunk(&chunk);
|
|
match emit_grok_adapter_deltas(
|
|
&mut client_emitter,
|
|
&adapter,
|
|
&mut text_len,
|
|
&mut thinking_len,
|
|
&mut image_len,
|
|
) {
|
|
Ok(frames) => {
|
|
for frame in frames {
|
|
yield Ok(frame);
|
|
}
|
|
}
|
|
Err(err) => {
|
|
yield Err(err);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if terminal_error_emitted {
|
|
match encode_grok_telemetry_frame(
|
|
ttfb_ms,
|
|
Some(started_at.elapsed().as_millis() as u64),
|
|
upstream_bytes,
|
|
) {
|
|
Ok(frame) => yield Ok(frame),
|
|
Err(err) => {
|
|
yield Err(err);
|
|
return;
|
|
}
|
|
}
|
|
match encode_stream_frame_ndjson(&StreamFrame::eof_with_summary(None)) {
|
|
Ok(frame) => yield Ok(frame),
|
|
Err(err) => yield Err(err),
|
|
}
|
|
return;
|
|
}
|
|
|
|
adapter.finish();
|
|
match emit_grok_adapter_deltas(
|
|
&mut client_emitter,
|
|
&adapter,
|
|
&mut text_len,
|
|
&mut thinking_len,
|
|
&mut image_len,
|
|
) {
|
|
Ok(frames) => {
|
|
for frame in frames {
|
|
yield Ok(frame);
|
|
}
|
|
}
|
|
Err(err) => {
|
|
yield Err(err);
|
|
return;
|
|
}
|
|
}
|
|
|
|
let elapsed_ms = Some(started_at.elapsed().as_millis() as u64);
|
|
let collected = GrokCollected {
|
|
status_code,
|
|
headers,
|
|
text: adapter.text,
|
|
thinking: adapter.thinking,
|
|
images: adapter.images,
|
|
telemetry: ExecutionTelemetry {
|
|
ttfb_ms,
|
|
elapsed_ms,
|
|
upstream_bytes: Some(upstream_bytes),
|
|
},
|
|
};
|
|
let usage = grok_usage_estimate(&plan, &collected);
|
|
match emit_grok_client_bytes(client_emitter.finish(usage)) {
|
|
Ok(frames) => {
|
|
for frame in frames {
|
|
yield Ok(frame);
|
|
}
|
|
}
|
|
Err(err) => {
|
|
yield Err(err);
|
|
return;
|
|
}
|
|
}
|
|
match encode_grok_telemetry_frame(ttfb_ms, elapsed_ms, upstream_bytes) {
|
|
Ok(frame) => yield Ok(frame),
|
|
Err(err) => {
|
|
yield Err(err);
|
|
return;
|
|
}
|
|
}
|
|
match encode_stream_frame_ndjson(&StreamFrame::eof_with_summary(Some(
|
|
grok_stream_terminal_summary(&plan, usage),
|
|
))) {
|
|
Ok(frame) => yield Ok(frame),
|
|
Err(err) => yield Err(err),
|
|
}
|
|
}
|
|
.boxed()
|
|
}
|
|
|
|
async fn await_grok_stream_first_byte<T, F>(
|
|
future: F,
|
|
started_at: Instant,
|
|
timeout: Option<Duration>,
|
|
) -> Result<T, Duration>
|
|
where
|
|
F: Future<Output = T>,
|
|
{
|
|
let Some(timeout) = timeout else {
|
|
return Ok(future.await);
|
|
};
|
|
let Some(remaining) = timeout.checked_sub(started_at.elapsed()) else {
|
|
return Err(timeout);
|
|
};
|
|
if remaining.is_zero() {
|
|
return Err(timeout);
|
|
}
|
|
tokio::time::timeout(remaining, future)
|
|
.await
|
|
.map_err(|_| timeout)
|
|
}
|
|
|
|
fn emit_grok_adapter_deltas(
|
|
client_emitter: &mut GrokClientStreamEmitter,
|
|
adapter: &GrokStreamAdapter,
|
|
text_len: &mut usize,
|
|
thinking_len: &mut usize,
|
|
image_len: &mut usize,
|
|
) -> Result<Vec<Bytes>, IoError> {
|
|
let mut out = Vec::new();
|
|
if let Some(delta) = adapter.thinking.get(*thinking_len..) {
|
|
if !delta.is_empty() {
|
|
out.extend(emit_grok_client_bytes(
|
|
client_emitter.emit_reasoning_delta(delta.to_string()),
|
|
)?);
|
|
}
|
|
}
|
|
*thinking_len = adapter.thinking.len();
|
|
if let Some(delta) = adapter.text.get(*text_len..) {
|
|
if !delta.is_empty() {
|
|
out.extend(emit_grok_client_bytes(
|
|
client_emitter.emit_text_delta(delta.to_string()),
|
|
)?);
|
|
}
|
|
}
|
|
*text_len = adapter.text.len();
|
|
for image in adapter.images.iter().skip(*image_len) {
|
|
out.extend(emit_grok_client_bytes(
|
|
client_emitter.emit_image_url(image.clone()),
|
|
)?);
|
|
}
|
|
*image_len = adapter.images.len();
|
|
Ok(out)
|
|
}
|
|
|
|
fn emit_grok_client_bytes(
|
|
body: Result<Vec<u8>, ExecutionRuntimeTransportError>,
|
|
) -> Result<Vec<Bytes>, IoError> {
|
|
let body = body.map_err(|err| IoError::other(err.to_string()))?;
|
|
if body.is_empty() {
|
|
return Ok(Vec::new());
|
|
}
|
|
encode_grok_data_frame(&Bytes::from(body)).map(|frame| vec![frame])
|
|
}
|
|
|
|
fn encode_grok_headers_frame(
|
|
status_code: u16,
|
|
headers: BTreeMap<String, String>,
|
|
) -> Result<Bytes, IoError> {
|
|
encode_stream_frame_ndjson(&StreamFrame {
|
|
frame_type: StreamFrameType::Headers,
|
|
payload: StreamFramePayload::Headers {
|
|
status_code,
|
|
headers,
|
|
response_observation: None,
|
|
},
|
|
})
|
|
}
|
|
|
|
fn encode_grok_data_frame(chunk: &Bytes) -> Result<Bytes, IoError> {
|
|
encode_stream_frame_ndjson(&StreamFrame {
|
|
frame_type: StreamFrameType::Data,
|
|
payload: StreamFramePayload::Data {
|
|
chunk_b64: Some(base64::engine::general_purpose::STANDARD.encode(chunk)),
|
|
text: None,
|
|
},
|
|
})
|
|
}
|
|
|
|
fn encode_grok_telemetry_frame(
|
|
ttfb_ms: Option<u64>,
|
|
elapsed_ms: Option<u64>,
|
|
upstream_bytes: u64,
|
|
) -> Result<Bytes, IoError> {
|
|
encode_stream_frame_ndjson(&StreamFrame {
|
|
frame_type: StreamFrameType::Telemetry,
|
|
payload: StreamFramePayload::Telemetry {
|
|
telemetry: ExecutionTelemetry {
|
|
ttfb_ms,
|
|
elapsed_ms,
|
|
upstream_bytes: Some(upstream_bytes),
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
fn encode_grok_error_frame(message: String) -> Result<Bytes, IoError> {
|
|
encode_stream_frame_ndjson(&StreamFrame {
|
|
frame_type: StreamFrameType::Error,
|
|
payload: StreamFramePayload::Error {
|
|
error: aether_contracts::ExecutionError {
|
|
kind: aether_contracts::ExecutionErrorKind::ProtocolError,
|
|
phase: aether_contracts::ExecutionPhase::StreamRead,
|
|
message,
|
|
upstream_status: None,
|
|
retryable: true,
|
|
failover_recommended: true,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
fn encode_grok_first_byte_timeout_frame(timeout: Duration) -> Result<Bytes, IoError> {
|
|
encode_stream_frame_ndjson(&StreamFrame {
|
|
frame_type: StreamFrameType::Error,
|
|
payload: StreamFramePayload::Error {
|
|
error: aether_contracts::ExecutionError {
|
|
kind: aether_contracts::ExecutionErrorKind::FirstByteTimeout,
|
|
phase: aether_contracts::ExecutionPhase::FirstByte,
|
|
message: stream_first_byte_timeout_message(timeout),
|
|
upstream_status: None,
|
|
retryable: true,
|
|
failover_recommended: true,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
enum GrokClientStreamEmitter {
|
|
OpenAiChat {
|
|
id: String,
|
|
model: String,
|
|
emitter: OpenAIChatClientEmitter,
|
|
},
|
|
OpenAiResponses {
|
|
id: String,
|
|
model: String,
|
|
emitter: OpenAIResponsesClientEmitter,
|
|
},
|
|
ClaudeMessages {
|
|
id: String,
|
|
model: String,
|
|
emitter: ClaudeClientEmitter,
|
|
},
|
|
}
|
|
|
|
impl GrokClientStreamEmitter {
|
|
fn new(plan: &ExecutionPlan) -> Self {
|
|
let model = plan
|
|
.model_name
|
|
.clone()
|
|
.unwrap_or_else(|| "grok".to_string());
|
|
match normalized_client_api_format(plan).as_str() {
|
|
"openai:responses" | "openai:responses:compact" => Self::OpenAiResponses {
|
|
id: format!("resp_{}", Uuid::new_v4()),
|
|
model,
|
|
emitter: OpenAIResponsesClientEmitter::default(),
|
|
},
|
|
"claude:messages" => Self::ClaudeMessages {
|
|
id: format!("msg_{}", Uuid::new_v4()),
|
|
model,
|
|
emitter: ClaudeClientEmitter::default(),
|
|
},
|
|
_ => Self::OpenAiChat {
|
|
id: format!("chatcmpl-{}", Uuid::new_v4()),
|
|
model,
|
|
emitter: OpenAIChatClientEmitter::default(),
|
|
},
|
|
}
|
|
}
|
|
|
|
fn emit_text_delta(&mut self, text: String) -> Result<Vec<u8>, ExecutionRuntimeTransportError> {
|
|
self.emit(CanonicalStreamEvent::TextDelta(text))
|
|
}
|
|
|
|
fn emit_reasoning_delta(
|
|
&mut self,
|
|
text: String,
|
|
) -> Result<Vec<u8>, ExecutionRuntimeTransportError> {
|
|
self.emit(CanonicalStreamEvent::ReasoningDelta(text))
|
|
}
|
|
|
|
fn emit_image_url(&mut self, url: String) -> Result<Vec<u8>, ExecutionRuntimeTransportError> {
|
|
self.emit(CanonicalStreamEvent::ContentPart(
|
|
CanonicalContentPart::ImageUrl(url),
|
|
))
|
|
}
|
|
|
|
fn finish(
|
|
&mut self,
|
|
usage: GrokUsageEstimate,
|
|
) -> Result<Vec<u8>, ExecutionRuntimeTransportError> {
|
|
let mut out = self.emit(CanonicalStreamEvent::Finish {
|
|
finish_reason: Some("stop".to_string()),
|
|
usage: Some(grok_canonical_usage(usage)),
|
|
})?;
|
|
out.extend(self.finish_emitter()?);
|
|
Ok(out)
|
|
}
|
|
|
|
fn emit(
|
|
&mut self,
|
|
event: CanonicalStreamEvent,
|
|
) -> Result<Vec<u8>, ExecutionRuntimeTransportError> {
|
|
let frame = self.frame(event);
|
|
match self {
|
|
Self::OpenAiChat { emitter, .. } => emitter.emit(frame),
|
|
Self::OpenAiResponses { emitter, .. } => emitter.emit(frame),
|
|
Self::ClaudeMessages { emitter, .. } => emitter.emit(frame),
|
|
}
|
|
.map_err(|err| ExecutionRuntimeTransportError::UpstreamRequest(err.to_string()))
|
|
}
|
|
|
|
fn finish_emitter(&mut self) -> Result<Vec<u8>, ExecutionRuntimeTransportError> {
|
|
match self {
|
|
Self::OpenAiChat { emitter, .. } => emitter.finish(),
|
|
Self::OpenAiResponses { emitter, .. } => emitter.finish(),
|
|
Self::ClaudeMessages { emitter, .. } => emitter.finish(),
|
|
}
|
|
.map_err(|err| ExecutionRuntimeTransportError::UpstreamRequest(err.to_string()))
|
|
}
|
|
|
|
fn frame(&self, event: CanonicalStreamEvent) -> CanonicalStreamFrame {
|
|
match self {
|
|
Self::OpenAiChat { id, model, .. }
|
|
| Self::OpenAiResponses { id, model, .. }
|
|
| Self::ClaudeMessages { id, model, .. } => CanonicalStreamFrame {
|
|
id: id.clone(),
|
|
model: model.clone(),
|
|
event,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
fn grok_canonical_usage(usage: GrokUsageEstimate) -> StreamingCanonicalUsage {
|
|
StreamingCanonicalUsage {
|
|
input_tokens: usage.input_tokens,
|
|
input_tokens_include_cache: false,
|
|
output_tokens: usage.output_tokens,
|
|
total_tokens: usage.input_tokens.saturating_add(usage.output_tokens),
|
|
cache_creation_tokens: 0,
|
|
cache_creation_ephemeral_5m_tokens: 0,
|
|
cache_creation_ephemeral_1h_tokens: 0,
|
|
cache_read_tokens: 0,
|
|
reasoning_tokens: usage.reasoning_tokens,
|
|
}
|
|
}
|
|
|
|
fn grok_standardized_usage(usage: GrokUsageEstimate) -> StandardizedUsage {
|
|
let mut standardized = StandardizedUsage::new();
|
|
standardized.input_tokens = i64::try_from(usage.input_tokens).unwrap_or(i64::MAX);
|
|
standardized.output_tokens = i64::try_from(usage.output_tokens).unwrap_or(i64::MAX);
|
|
standardized.reasoning_tokens = i64::try_from(usage.reasoning_tokens).unwrap_or(i64::MAX);
|
|
standardized
|
|
}
|
|
|
|
fn grok_stream_terminal_summary(
|
|
plan: &ExecutionPlan,
|
|
usage: GrokUsageEstimate,
|
|
) -> ExecutionStreamTerminalSummary {
|
|
ExecutionStreamTerminalSummary {
|
|
standardized_usage: Some(grok_standardized_usage(usage)),
|
|
finish_reason: Some("stop".to_string()),
|
|
response_id: None,
|
|
model: plan.model_name.clone(),
|
|
provider_actual_service_tier: None,
|
|
observed_finish: true,
|
|
unknown_event_count: 0,
|
|
parser_error: None,
|
|
}
|
|
}
|
|
|
|
async fn grok_upstream_plan(
|
|
plan: &ExecutionPlan,
|
|
report_context: Option<&Value>,
|
|
) -> Result<ExecutionPlan, ExecutionRuntimeTransportError> {
|
|
let body = plan.body.json_body.as_ref().ok_or_else(|| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(
|
|
"Grok runtime requires JSON request body".to_string(),
|
|
)
|
|
})?;
|
|
let mapped_model = grok_upstream_model_name(report_context)?;
|
|
let mut upstream_body = crate::ai_serving::transport::build_grok_app_chat_body(
|
|
plan.client_api_format.as_str(),
|
|
Some(mapped_model.as_str()),
|
|
body,
|
|
);
|
|
if grok_is_image_edit_plan(plan, &upstream_body) {
|
|
attach_grok_image_edit_references(plan, body, &mut upstream_body).await?;
|
|
} else {
|
|
attach_grok_uploaded_files(plan, body, &mut upstream_body).await?;
|
|
}
|
|
let mut upstream_plan = plan.clone();
|
|
upstream_plan.body = RequestBody::from_json(upstream_body);
|
|
upstream_plan.stream = true;
|
|
upstream_plan.model_name = Some(mapped_model);
|
|
Ok(upstream_plan)
|
|
}
|
|
|
|
fn grok_is_image_edit_plan(plan: &ExecutionPlan, upstream_body: &Value) -> bool {
|
|
normalized_client_api_format(plan) == "openai:image"
|
|
&& upstream_body
|
|
.get("modelName")
|
|
.and_then(Value::as_str)
|
|
.is_some_and(|value| value == "imagine-image-edit")
|
|
}
|
|
|
|
fn grok_upstream_model_name(
|
|
report_context: Option<&Value>,
|
|
) -> Result<String, ExecutionRuntimeTransportError> {
|
|
let mapped_model = report_context
|
|
.and_then(|value| value.get("mapped_model"))
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.ok_or_else(|| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(
|
|
"Grok runtime requires mapped_model in report context".to_string(),
|
|
)
|
|
})?;
|
|
Ok(mapped_model.to_string())
|
|
}
|
|
|
|
async fn attach_grok_uploaded_files(
|
|
plan: &ExecutionPlan,
|
|
original_body: &Value,
|
|
upstream_body: &mut Value,
|
|
) -> Result<(), ExecutionRuntimeTransportError> {
|
|
let inputs = extract_grok_attachment_inputs(plan.client_api_format.as_str(), original_body);
|
|
if inputs.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
let mut attachment_ids = Vec::with_capacity(inputs.len());
|
|
for (index, input) in inputs.into_iter().enumerate() {
|
|
let payload = resolve_grok_attachment_payload(&input, index).await?;
|
|
let uploaded = upload_grok_attachment(plan, payload).await?;
|
|
if !uploaded.file_id.trim().is_empty() {
|
|
attachment_ids.push(Value::String(uploaded.file_id));
|
|
}
|
|
}
|
|
if attachment_ids.is_empty() {
|
|
return Ok(());
|
|
}
|
|
let Some(object) = upstream_body.as_object_mut() else {
|
|
return Err(ExecutionRuntimeTransportError::UpstreamRequest(
|
|
"Grok runtime generated non-object app-chat body".to_string(),
|
|
));
|
|
};
|
|
object.insert("fileAttachments".to_string(), Value::Array(attachment_ids));
|
|
Ok(())
|
|
}
|
|
|
|
async fn attach_grok_image_edit_references(
|
|
plan: &ExecutionPlan,
|
|
original_body: &Value,
|
|
upstream_body: &mut Value,
|
|
) -> Result<(), ExecutionRuntimeTransportError> {
|
|
let inputs = extract_grok_attachment_inputs(plan.client_api_format.as_str(), original_body);
|
|
if inputs.is_empty() {
|
|
return Err(ExecutionRuntimeTransportError::UpstreamRequest(
|
|
"Grok image edit requires at least one reference image".to_string(),
|
|
));
|
|
}
|
|
|
|
let mut image_references = Vec::with_capacity(inputs.len());
|
|
for (index, input) in inputs.into_iter().enumerate() {
|
|
let payload = resolve_grok_attachment_payload(&input, index).await?;
|
|
let uploaded = upload_grok_attachment(plan, payload).await?;
|
|
let reference = resolve_grok_uploaded_asset_reference(plan, &uploaded)?;
|
|
image_references.push(Value::String(reference));
|
|
}
|
|
let parent_post_id =
|
|
create_grok_media_post(plan, grok_image_edit_prompt(upstream_body)).await?;
|
|
set_grok_image_edit_config(upstream_body, image_references, parent_post_id)
|
|
}
|
|
|
|
fn extract_grok_attachment_inputs(
|
|
client_api_format: &str,
|
|
body: &Value,
|
|
) -> Vec<GrokAttachmentInput> {
|
|
match client_api_format.trim().to_ascii_lowercase().as_str() {
|
|
"openai:responses" | "openai:responses:compact" => {
|
|
extract_responses_attachment_inputs(body)
|
|
}
|
|
"claude:messages" => extract_claude_attachment_inputs(body),
|
|
_ => extract_openai_chat_attachment_inputs(body),
|
|
}
|
|
}
|
|
|
|
fn extract_openai_chat_attachment_inputs(body: &Value) -> Vec<GrokAttachmentInput> {
|
|
let mut out = Vec::new();
|
|
if let Some(messages) = body.get("messages").and_then(Value::as_array) {
|
|
for message in messages {
|
|
collect_content_attachment_inputs(message.get("content"), &mut out);
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
fn extract_responses_attachment_inputs(body: &Value) -> Vec<GrokAttachmentInput> {
|
|
let mut out = Vec::new();
|
|
collect_responses_input_attachment_inputs(body.get("input"), &mut out);
|
|
out
|
|
}
|
|
|
|
fn collect_responses_input_attachment_inputs(
|
|
value: Option<&Value>,
|
|
out: &mut Vec<GrokAttachmentInput>,
|
|
) {
|
|
let Some(value) = value else {
|
|
return;
|
|
};
|
|
match value {
|
|
Value::Array(items) => {
|
|
for item in items {
|
|
if item.get("type").and_then(Value::as_str) == Some("message") {
|
|
collect_content_attachment_inputs(item.get("content"), out);
|
|
} else {
|
|
collect_attachment_input_from_object(item, out);
|
|
}
|
|
}
|
|
}
|
|
Value::Object(_) => collect_attachment_input_from_object(value, out),
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn extract_claude_attachment_inputs(body: &Value) -> Vec<GrokAttachmentInput> {
|
|
let mut out = Vec::new();
|
|
if let Some(messages) = body.get("messages").and_then(Value::as_array) {
|
|
for message in messages {
|
|
collect_content_attachment_inputs(message.get("content"), &mut out);
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
fn collect_content_attachment_inputs(value: Option<&Value>, out: &mut Vec<GrokAttachmentInput>) {
|
|
let Some(value) = value else {
|
|
return;
|
|
};
|
|
match value {
|
|
Value::Array(items) => {
|
|
for item in items {
|
|
collect_attachment_input_from_object(item, out);
|
|
}
|
|
}
|
|
Value::Object(_) => collect_attachment_input_from_object(value, out),
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn collect_attachment_input_from_object(value: &Value, out: &mut Vec<GrokAttachmentInput>) {
|
|
let Some(object) = value.as_object() else {
|
|
return;
|
|
};
|
|
if let Some(input) = claude_source_attachment(object) {
|
|
out.push(input);
|
|
return;
|
|
}
|
|
if let Some(source) = image_url_source(object) {
|
|
out.push(GrokAttachmentInput {
|
|
source,
|
|
filename: None,
|
|
mime_type: None,
|
|
});
|
|
return;
|
|
}
|
|
if let Some(input) = file_source(object) {
|
|
out.push(input);
|
|
}
|
|
}
|
|
|
|
fn image_url_source(object: &Map<String, Value>) -> Option<String> {
|
|
if let Some(source) = object.get("image_url").and_then(string_or_url_value) {
|
|
return Some(source);
|
|
}
|
|
if object
|
|
.get("type")
|
|
.and_then(Value::as_str)
|
|
.is_some_and(|value| value.eq_ignore_ascii_case("image_url"))
|
|
{
|
|
return object
|
|
.get("url")
|
|
.and_then(Value::as_str)
|
|
.map(trimmed_string);
|
|
}
|
|
if object
|
|
.get("type")
|
|
.and_then(Value::as_str)
|
|
.is_some_and(|value| value.eq_ignore_ascii_case("input_image"))
|
|
{
|
|
return object
|
|
.get("image_url")
|
|
.or_else(|| object.get("source"))
|
|
.and_then(string_or_url_value);
|
|
}
|
|
None
|
|
}
|
|
|
|
fn file_source(object: &Map<String, Value>) -> Option<GrokAttachmentInput> {
|
|
let file_object = object.get("file").and_then(Value::as_object);
|
|
let source = file_object
|
|
.and_then(|file| {
|
|
file.get("file_data")
|
|
.or_else(|| file.get("data"))
|
|
.or_else(|| file.get("url"))
|
|
.or_else(|| file.get("file_url"))
|
|
.and_then(Value::as_str)
|
|
})
|
|
.or_else(|| object.get("file_data").and_then(Value::as_str))
|
|
.or_else(|| object.get("file_url").and_then(Value::as_str))
|
|
.or_else(|| object.get("data").and_then(Value::as_str))
|
|
.map(trimmed_string)
|
|
.filter(|value| !value.is_empty())?;
|
|
let filename = file_object
|
|
.and_then(|file| file.get("filename").or_else(|| file.get("name")))
|
|
.or_else(|| object.get("filename"))
|
|
.or_else(|| object.get("name"))
|
|
.and_then(Value::as_str)
|
|
.map(trimmed_string)
|
|
.filter(|value| !value.is_empty());
|
|
let mime_type = file_object
|
|
.and_then(|file| file.get("mime_type").or_else(|| file.get("mimeType")))
|
|
.or_else(|| object.get("mime_type"))
|
|
.or_else(|| object.get("mimeType"))
|
|
.and_then(Value::as_str)
|
|
.map(trimmed_string)
|
|
.filter(|value| !value.is_empty());
|
|
Some(GrokAttachmentInput {
|
|
source,
|
|
filename,
|
|
mime_type,
|
|
})
|
|
}
|
|
|
|
fn claude_source_attachment(object: &Map<String, Value>) -> Option<GrokAttachmentInput> {
|
|
let block_type = object
|
|
.get("type")
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.unwrap_or_default();
|
|
if !matches!(block_type, "image" | "document") {
|
|
return None;
|
|
}
|
|
let source = object.get("source").and_then(Value::as_object)?;
|
|
let source_type = source
|
|
.get("type")
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.unwrap_or_default();
|
|
let mime_type = source
|
|
.get("media_type")
|
|
.or_else(|| source.get("mediaType"))
|
|
.and_then(Value::as_str)
|
|
.map(trimmed_string)
|
|
.filter(|value| !value.is_empty());
|
|
let filename = object
|
|
.get("filename")
|
|
.or_else(|| object.get("name"))
|
|
.and_then(Value::as_str)
|
|
.map(trimmed_string)
|
|
.filter(|value| !value.is_empty());
|
|
|
|
match source_type {
|
|
"base64" => {
|
|
let data = source
|
|
.get("data")
|
|
.and_then(Value::as_str)
|
|
.map(trimmed_string)
|
|
.filter(|value| !value.is_empty())?;
|
|
let mime = mime_type
|
|
.clone()
|
|
.unwrap_or_else(|| "application/octet-stream".to_string());
|
|
Some(GrokAttachmentInput {
|
|
source: format!("data:{mime};base64,{data}"),
|
|
filename,
|
|
mime_type,
|
|
})
|
|
}
|
|
"url" => {
|
|
let url = source
|
|
.get("url")
|
|
.and_then(Value::as_str)
|
|
.map(trimmed_string)
|
|
.filter(|value| !value.is_empty())?;
|
|
Some(GrokAttachmentInput {
|
|
source: url,
|
|
filename,
|
|
mime_type,
|
|
})
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn string_or_url_value(value: &Value) -> Option<String> {
|
|
value
|
|
.as_str()
|
|
.map(trimmed_string)
|
|
.or_else(|| value.get("url").and_then(Value::as_str).map(trimmed_string))
|
|
.filter(|value| !value.is_empty())
|
|
}
|
|
|
|
fn trimmed_string(value: &str) -> String {
|
|
value.trim().to_string()
|
|
}
|
|
|
|
async fn resolve_grok_attachment_payload(
|
|
input: &GrokAttachmentInput,
|
|
index: usize,
|
|
) -> Result<GrokAttachmentPayload, ExecutionRuntimeTransportError> {
|
|
if input.source.starts_with("data:") {
|
|
return grok_attachment_payload_from_data_uri(input, index);
|
|
}
|
|
grok_attachment_payload_from_url(input, index).await
|
|
}
|
|
|
|
fn grok_attachment_payload_from_data_uri(
|
|
input: &GrokAttachmentInput,
|
|
index: usize,
|
|
) -> Result<GrokAttachmentPayload, ExecutionRuntimeTransportError> {
|
|
let (header, content_b64) = input.source.split_once(',').ok_or_else(|| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(
|
|
"Grok attachment data URI is missing comma separator".to_string(),
|
|
)
|
|
})?;
|
|
if !header.contains(";base64") {
|
|
return Err(ExecutionRuntimeTransportError::UpstreamRequest(
|
|
"Grok attachment data URI must be base64 encoded".to_string(),
|
|
));
|
|
}
|
|
let mime_type = input
|
|
.mime_type
|
|
.clone()
|
|
.or_else(|| {
|
|
header
|
|
.strip_prefix("data:")
|
|
.and_then(|value| value.split(';').next())
|
|
.map(trimmed_string)
|
|
.filter(|value| !value.is_empty())
|
|
})
|
|
.unwrap_or_else(|| "application/octet-stream".to_string());
|
|
let normalized_b64 = content_b64.split_whitespace().collect::<String>();
|
|
drop(
|
|
base64::engine::general_purpose::STANDARD
|
|
.decode(&normalized_b64)
|
|
.map_err(|err| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(format!(
|
|
"Grok attachment data URI base64 is invalid: {err}"
|
|
))
|
|
})?,
|
|
);
|
|
Ok(GrokAttachmentPayload {
|
|
filename: input
|
|
.filename
|
|
.clone()
|
|
.unwrap_or_else(|| default_attachment_filename(index, &mime_type)),
|
|
mime_type,
|
|
content_b64: normalized_b64,
|
|
})
|
|
}
|
|
|
|
async fn grok_attachment_payload_from_url(
|
|
input: &GrokAttachmentInput,
|
|
index: usize,
|
|
) -> Result<GrokAttachmentPayload, ExecutionRuntimeTransportError> {
|
|
let url = reqwest::Url::parse(input.source.as_str()).map_err(|err| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(format!(
|
|
"Grok attachment URL is invalid: {err}"
|
|
))
|
|
})?;
|
|
if !matches!(url.scheme(), "http" | "https") {
|
|
return Err(ExecutionRuntimeTransportError::UpstreamRequest(
|
|
"Grok attachment URL must use http or https".to_string(),
|
|
));
|
|
}
|
|
let response = fetch_grok_attachment_url(url.clone(), 0).await?;
|
|
let final_url = response.url().clone();
|
|
let mime_type = input
|
|
.mime_type
|
|
.clone()
|
|
.or_else(|| {
|
|
response
|
|
.headers()
|
|
.get(reqwest::header::CONTENT_TYPE)
|
|
.and_then(|value| value.to_str().ok())
|
|
.and_then(|value| value.split(';').next())
|
|
.map(trimmed_string)
|
|
.filter(|value| !value.is_empty())
|
|
})
|
|
.unwrap_or_else(|| "application/octet-stream".to_string());
|
|
let bytes = collect_grok_attachment_url_bytes(response).await?;
|
|
Ok(GrokAttachmentPayload {
|
|
filename: input
|
|
.filename
|
|
.clone()
|
|
.or_else(|| filename_from_url_path(final_url.path()))
|
|
.unwrap_or_else(|| default_attachment_filename(index, &mime_type)),
|
|
mime_type,
|
|
content_b64: base64::engine::general_purpose::STANDARD.encode(&bytes),
|
|
})
|
|
}
|
|
|
|
async fn fetch_grok_attachment_url(
|
|
mut url: reqwest::Url,
|
|
mut redirects: usize,
|
|
) -> Result<reqwest::Response, ExecutionRuntimeTransportError> {
|
|
loop {
|
|
validate_grok_attachment_public_url(&url).await?;
|
|
let response = reqwest::Client::builder()
|
|
.timeout(Duration::from_secs(30))
|
|
.redirect(reqwest::redirect::Policy::none())
|
|
.resolve_to_addrs(
|
|
url.host_str().unwrap_or_default(),
|
|
&[public_socket_addr_for_url(&url).await?],
|
|
)
|
|
.build()
|
|
.map_err(ExecutionRuntimeTransportError::ClientBuild)?
|
|
.get(url.clone())
|
|
.send()
|
|
.await
|
|
.map_err(|err| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(format_upstream_request_error(&err))
|
|
})?;
|
|
if response.status().is_redirection() {
|
|
redirects += 1;
|
|
if redirects > GROK_MAX_ATTACHMENT_REDIRECTS {
|
|
return Err(ExecutionRuntimeTransportError::UpstreamRequest(
|
|
"Grok attachment URL fetch exceeded redirect limit".to_string(),
|
|
));
|
|
}
|
|
let location = response
|
|
.headers()
|
|
.get(reqwest::header::LOCATION)
|
|
.and_then(|value| value.to_str().ok())
|
|
.ok_or_else(|| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(
|
|
"Grok attachment redirect is missing Location header".to_string(),
|
|
)
|
|
})?;
|
|
url = url.join(location).map_err(|err| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(format!(
|
|
"Grok attachment redirect URL is invalid: {err}"
|
|
))
|
|
})?;
|
|
continue;
|
|
}
|
|
if !response.status().is_success() {
|
|
return Err(ExecutionRuntimeTransportError::UpstreamRequest(format!(
|
|
"Grok attachment URL fetch returned {}",
|
|
response.status().as_u16()
|
|
)));
|
|
}
|
|
return Ok(response);
|
|
}
|
|
}
|
|
|
|
async fn validate_grok_attachment_public_url(
|
|
url: &reqwest::Url,
|
|
) -> Result<(), ExecutionRuntimeTransportError> {
|
|
if !matches!(url.scheme(), "http" | "https") {
|
|
return Err(ExecutionRuntimeTransportError::UpstreamRequest(format!(
|
|
"Grok attachment URL scheme is unsupported: {}",
|
|
url.scheme()
|
|
)));
|
|
}
|
|
public_socket_addr_for_url(url).await.map(|_| ())
|
|
}
|
|
|
|
async fn public_socket_addr_for_url(
|
|
url: &reqwest::Url,
|
|
) -> Result<std::net::SocketAddr, ExecutionRuntimeTransportError> {
|
|
let host = url.host_str().ok_or_else(|| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(
|
|
"Grok attachment URL is missing a host".to_string(),
|
|
)
|
|
})?;
|
|
let port = url.port_or_known_default().ok_or_else(|| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(
|
|
"Grok attachment URL is missing a port".to_string(),
|
|
)
|
|
})?;
|
|
if let Ok(ip) = host.parse::<IpAddr>() {
|
|
if !grok_attachment_ip_is_public(ip) {
|
|
return Err(ExecutionRuntimeTransportError::UpstreamRequest(
|
|
"Grok attachment URL resolves to a non-public address".to_string(),
|
|
));
|
|
}
|
|
return Ok(std::net::SocketAddr::new(ip, port));
|
|
}
|
|
let mut public_addr = None;
|
|
let mut resolved_any = false;
|
|
for addr in tokio::net::lookup_host((host, port)).await.map_err(|err| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(format!(
|
|
"Grok attachment URL DNS resolution failed: {err}"
|
|
))
|
|
})? {
|
|
resolved_any = true;
|
|
if !grok_attachment_ip_is_public(addr.ip()) {
|
|
return Err(ExecutionRuntimeTransportError::UpstreamRequest(
|
|
"Grok attachment URL resolves to a non-public address".to_string(),
|
|
));
|
|
}
|
|
public_addr.get_or_insert(addr);
|
|
}
|
|
if !resolved_any {
|
|
return Err(ExecutionRuntimeTransportError::UpstreamRequest(
|
|
"Grok attachment URL DNS resolution returned no addresses".to_string(),
|
|
));
|
|
}
|
|
public_addr.ok_or_else(|| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(
|
|
"Grok attachment URL has no public address".to_string(),
|
|
)
|
|
})
|
|
}
|
|
|
|
fn grok_attachment_ip_is_public(ip: IpAddr) -> bool {
|
|
match ip {
|
|
IpAddr::V4(ip) => {
|
|
!(ip.is_private()
|
|
|| ip.is_loopback()
|
|
|| ip.is_link_local()
|
|
|| ip.is_broadcast()
|
|
|| ip.is_documentation()
|
|
|| ip.is_unspecified())
|
|
}
|
|
IpAddr::V6(ip) => {
|
|
!(ip.is_loopback()
|
|
|| ip.is_unspecified()
|
|
|| ip.is_unique_local()
|
|
|| ip.is_unicast_link_local()
|
|
|| is_ipv6_documentation_addr(ip))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn is_ipv6_documentation_addr(ip: std::net::Ipv6Addr) -> bool {
|
|
let segments = ip.segments();
|
|
segments[0] == 0x2001 && segments[1] == 0x0db8
|
|
}
|
|
|
|
async fn collect_grok_attachment_url_bytes(
|
|
response: reqwest::Response,
|
|
) -> Result<Vec<u8>, ExecutionRuntimeTransportError> {
|
|
let mut bytes = Vec::new();
|
|
let mut stream = response.bytes_stream();
|
|
while let Some(chunk) = stream.next().await {
|
|
let chunk = chunk.map_err(|err| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(format_upstream_request_error(&err))
|
|
})?;
|
|
bytes.extend_from_slice(&chunk);
|
|
}
|
|
Ok(bytes)
|
|
}
|
|
|
|
async fn upload_grok_attachment(
|
|
plan: &ExecutionPlan,
|
|
payload: GrokAttachmentPayload,
|
|
) -> Result<GrokUploadedAttachment, ExecutionRuntimeTransportError> {
|
|
let body = json!({
|
|
"fileName": payload.filename,
|
|
"fileMimeType": payload.mime_type,
|
|
"content": payload.content_b64,
|
|
});
|
|
let mut upload_plan = plan.clone();
|
|
upload_plan.url = grok_upload_url(plan.url.as_str());
|
|
upload_plan.method = "POST".to_string();
|
|
upload_plan.stream = false;
|
|
upload_plan.content_type = Some("application/json".to_string());
|
|
upload_plan.body = RequestBody::from_json(body);
|
|
upload_plan.headers = grok_upload_headers(&plan.headers)?;
|
|
let request_body = build_request_body(&upload_plan)?;
|
|
let response = send_request(&upload_plan, request_body).await?;
|
|
let status_code = response.status_code();
|
|
let bytes = response.bytes().await?;
|
|
if !(200..300).contains(&status_code) {
|
|
let text = String::from_utf8_lossy(&bytes).to_string();
|
|
return Err(ExecutionRuntimeTransportError::UpstreamRequest(format!(
|
|
"Grok attachment upload returned {status_code}: {text}"
|
|
)));
|
|
}
|
|
let value = serde_json::from_slice::<Value>(&bytes)
|
|
.map_err(ExecutionRuntimeTransportError::InvalidJson)?;
|
|
let file_id = value
|
|
.get("fileMetadataId")
|
|
.or_else(|| value.get("fileId"))
|
|
.and_then(Value::as_str)
|
|
.map(trimmed_string)
|
|
.filter(|value| !value.is_empty())
|
|
.ok_or_else(|| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(
|
|
"Grok attachment upload response is missing fileMetadataId".to_string(),
|
|
)
|
|
})?;
|
|
let file_uri = value
|
|
.get("fileUri")
|
|
.and_then(Value::as_str)
|
|
.map(trimmed_string)
|
|
.filter(|value| !value.is_empty());
|
|
Ok(GrokUploadedAttachment { file_id, file_uri })
|
|
}
|
|
|
|
async fn create_grok_media_post(
|
|
plan: &ExecutionPlan,
|
|
prompt: String,
|
|
) -> Result<String, ExecutionRuntimeTransportError> {
|
|
let body = json!({
|
|
"mediaType": "MEDIA_POST_TYPE_IMAGE",
|
|
"prompt": prompt,
|
|
});
|
|
let mut media_plan = plan.clone();
|
|
media_plan.url = grok_media_post_url(plan.url.as_str());
|
|
media_plan.method = "POST".to_string();
|
|
media_plan.stream = false;
|
|
media_plan.content_type = Some("application/json".to_string());
|
|
media_plan.body = RequestBody::from_json(body);
|
|
media_plan.headers = grok_upload_headers(&plan.headers)?;
|
|
media_plan.headers.insert(
|
|
"referer".to_string(),
|
|
grok_imagine_referer(plan.url.as_str()),
|
|
);
|
|
let request_body = build_request_body(&media_plan)?;
|
|
let response = send_request(&media_plan, request_body).await?;
|
|
let status_code = response.status_code();
|
|
let bytes = response.bytes().await?;
|
|
if !(200..300).contains(&status_code) {
|
|
let text = String::from_utf8_lossy(&bytes).to_string();
|
|
return Err(ExecutionRuntimeTransportError::UpstreamRequest(format!(
|
|
"Grok media post create returned {status_code}: {text}"
|
|
)));
|
|
}
|
|
let value = serde_json::from_slice::<Value>(&bytes)
|
|
.map_err(ExecutionRuntimeTransportError::InvalidJson)?;
|
|
value
|
|
.get("post")
|
|
.and_then(|post| post.get("id"))
|
|
.or_else(|| value.get("id"))
|
|
.and_then(Value::as_str)
|
|
.map(trimmed_string)
|
|
.filter(|value| !value.is_empty())
|
|
.ok_or_else(|| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(
|
|
"Grok media post create response is missing post id".to_string(),
|
|
)
|
|
})
|
|
}
|
|
|
|
fn grok_upload_headers(
|
|
headers: &BTreeMap<String, String>,
|
|
) -> Result<BTreeMap<String, String>, ExecutionRuntimeTransportError> {
|
|
let mut out = headers.clone();
|
|
out.insert("accept".to_string(), "application/json".to_string());
|
|
out.insert("content-type".to_string(), "application/json".to_string());
|
|
out.insert("sec-fetch-dest".to_string(), "empty".to_string());
|
|
out.insert("sec-fetch-mode".to_string(), "cors".to_string());
|
|
out.insert("sec-fetch-site".to_string(), "same-origin".to_string());
|
|
out.insert("x-xai-request-id".to_string(), Uuid::new_v4().to_string());
|
|
Ok(out)
|
|
}
|
|
|
|
fn grok_upload_url(chat_url: &str) -> String {
|
|
let Ok(mut url) = reqwest::Url::parse(chat_url) else {
|
|
return format!("https://grok.com{GROK_UPLOAD_PATH}");
|
|
};
|
|
url.set_path(GROK_UPLOAD_PATH);
|
|
url.set_query(None);
|
|
url.set_fragment(None);
|
|
url.to_string()
|
|
}
|
|
|
|
fn grok_media_post_url(chat_url: &str) -> String {
|
|
let Ok(mut url) = reqwest::Url::parse(chat_url) else {
|
|
return format!("https://grok.com{GROK_MEDIA_POST_PATH}");
|
|
};
|
|
url.set_path(GROK_MEDIA_POST_PATH);
|
|
url.set_query(None);
|
|
url.set_fragment(None);
|
|
url.to_string()
|
|
}
|
|
|
|
fn grok_imagine_referer(chat_url: &str) -> String {
|
|
let Ok(mut url) = reqwest::Url::parse(chat_url) else {
|
|
return "https://grok.com/imagine".to_string();
|
|
};
|
|
url.set_path("/imagine");
|
|
url.set_query(None);
|
|
url.set_fragment(None);
|
|
url.to_string()
|
|
}
|
|
|
|
fn resolve_grok_uploaded_asset_reference(
|
|
plan: &ExecutionPlan,
|
|
uploaded: &GrokUploadedAttachment,
|
|
) -> Result<String, ExecutionRuntimeTransportError> {
|
|
if let Some(file_uri) = uploaded.file_uri.as_deref() {
|
|
if !file_uri.trim().is_empty() {
|
|
return Ok(grok_asset_url(file_uri));
|
|
}
|
|
}
|
|
let user_id = grok_user_id_from_cookie_header(&plan.headers).ok_or_else(|| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(
|
|
"Grok image edit upload response is missing fileUri and cookie x-userid is unavailable"
|
|
.to_string(),
|
|
)
|
|
})?;
|
|
Ok(format!(
|
|
"{GROK_ASSET_BASE}users/{}/{}/content",
|
|
user_id, uploaded.file_id
|
|
))
|
|
}
|
|
|
|
fn grok_asset_url(value: &str) -> String {
|
|
let value = value.trim();
|
|
if value.starts_with("http://") || value.starts_with("https://") {
|
|
value.to_string()
|
|
} else {
|
|
format!("{GROK_ASSET_BASE}{}", value.trim_start_matches('/'))
|
|
}
|
|
}
|
|
|
|
fn grok_user_id_from_cookie_header(headers: &BTreeMap<String, String>) -> Option<String> {
|
|
let cookie = headers
|
|
.iter()
|
|
.find(|(name, _)| name.eq_ignore_ascii_case("cookie"))
|
|
.map(|(_, value)| value.as_str())?;
|
|
cookie.split(';').find_map(|part| {
|
|
let (name, value) = part.trim().split_once('=')?;
|
|
(name.trim() == "x-userid")
|
|
.then(|| value.trim().to_string())
|
|
.filter(|value| !value.is_empty())
|
|
})
|
|
}
|
|
|
|
fn grok_image_edit_prompt(upstream_body: &Value) -> String {
|
|
upstream_body
|
|
.get("message")
|
|
.and_then(Value::as_str)
|
|
.map(trimmed_string)
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
fn grok_image_prompt_from_provider_body(body: &Value) -> Option<String> {
|
|
body.get("prompt")
|
|
.and_then(Value::as_str)
|
|
.map(trimmed_string)
|
|
.filter(|value| !value.is_empty())
|
|
.or_else(|| {
|
|
body.get("input")
|
|
.and_then(grok_value_text)
|
|
.map(|value| trimmed_string(&value))
|
|
})
|
|
.or_else(|| grok_last_user_message_text(body))
|
|
.filter(|value| !value.is_empty())
|
|
}
|
|
|
|
fn grok_image_count_from_provider_body(body: &Value) -> usize {
|
|
body.get("n")
|
|
.and_then(Value::as_u64)
|
|
.or_else(|| body.get("imageGenerationCount").and_then(Value::as_u64))
|
|
.or_else(|| {
|
|
body.get("image_config")
|
|
.and_then(|config| {
|
|
config
|
|
.get("n")
|
|
.or_else(|| config.get("imageGenerationCount"))
|
|
})
|
|
.and_then(Value::as_u64)
|
|
})
|
|
.or_else(|| {
|
|
body.get("tools")
|
|
.and_then(Value::as_array)
|
|
.and_then(|tools| tools.first())
|
|
.and_then(|tool| tool.get("n").or_else(|| tool.get("imageGenerationCount")))
|
|
.and_then(Value::as_u64)
|
|
})
|
|
.and_then(|value| usize::try_from(value).ok())
|
|
.unwrap_or(1)
|
|
}
|
|
|
|
fn grok_aspect_ratio_from_provider_body(body: &Value) -> String {
|
|
let size_or_ratio = grok_image_option_from_provider_body(body, "aspect_ratio")
|
|
.or_else(|| grok_image_option_from_provider_body(body, "aspectRatio"))
|
|
.or_else(|| grok_image_option_from_provider_body(body, "ratio"))
|
|
.or_else(|| grok_image_option_from_provider_body(body, "size"))
|
|
.unwrap_or("1024x1024");
|
|
match size_or_ratio {
|
|
"1280x720" | "16:9" => "16:9",
|
|
"720x1280" | "9:16" => "9:16",
|
|
"1792x1024" | "3:2" => "3:2",
|
|
"1024x1792" | "2:3" => "2:3",
|
|
"1024x1024" | "1:1" => "1:1",
|
|
_ => "2:3",
|
|
}
|
|
.to_string()
|
|
}
|
|
|
|
fn grok_image_option_from_provider_body<'a>(body: &'a Value, key: &str) -> Option<&'a str> {
|
|
body.get(key)
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.or_else(|| {
|
|
body.get("image_config")
|
|
.and_then(|config| config.get(key))
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
})
|
|
.or_else(|| {
|
|
body.get("tools")
|
|
.and_then(Value::as_array)
|
|
.and_then(|tools| tools.first())
|
|
.and_then(|tool| tool.get(key))
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
})
|
|
}
|
|
|
|
fn grok_last_user_message_text(body: &Value) -> Option<String> {
|
|
body.get("messages")
|
|
.and_then(Value::as_array)?
|
|
.iter()
|
|
.rev()
|
|
.find_map(|message| {
|
|
let role = message.get("role").and_then(Value::as_str)?;
|
|
if !role.eq_ignore_ascii_case("user") {
|
|
return None;
|
|
}
|
|
message
|
|
.get("content")
|
|
.and_then(grok_value_text)
|
|
.map(|value| trimmed_string(&value))
|
|
.filter(|value| !value.is_empty())
|
|
})
|
|
}
|
|
|
|
fn grok_value_text(value: &Value) -> Option<String> {
|
|
match value {
|
|
Value::String(text) => Some(text.clone()),
|
|
Value::Array(items) => {
|
|
let parts = items
|
|
.iter()
|
|
.filter_map(grok_value_text)
|
|
.map(|value| value.trim().to_string())
|
|
.filter(|value| !value.is_empty())
|
|
.collect::<Vec<_>>();
|
|
(!parts.is_empty()).then(|| parts.join("\n"))
|
|
}
|
|
Value::Object(object) => object
|
|
.get("text")
|
|
.or_else(|| object.get("input_text"))
|
|
.or_else(|| object.get("content"))
|
|
.and_then(grok_value_text),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn grok_imagine_reset_message() -> Value {
|
|
json!({
|
|
"type": "conversation.item.create",
|
|
"timestamp": current_unix_secs().saturating_mul(1000),
|
|
"item": {
|
|
"type": "message",
|
|
"content": [{"type": "reset"}],
|
|
},
|
|
})
|
|
}
|
|
|
|
fn grok_imagine_request_message(prompt: &str, aspect_ratio: &str, enable_pro: bool) -> Value {
|
|
json!({
|
|
"type": "conversation.item.create",
|
|
"timestamp": current_unix_secs().saturating_mul(1000),
|
|
"item": {
|
|
"type": "message",
|
|
"content": [{
|
|
"requestId": Uuid::new_v4().to_string(),
|
|
"text": prompt,
|
|
"type": "input_text",
|
|
"properties": {
|
|
"section_count": 0,
|
|
"is_kids_mode": false,
|
|
"enable_nsfw": true,
|
|
"skip_upsampler": false,
|
|
"enable_side_by_side": true,
|
|
"is_initial": false,
|
|
"aspect_ratio": aspect_ratio,
|
|
"enable_pro": enable_pro,
|
|
},
|
|
}],
|
|
},
|
|
})
|
|
}
|
|
|
|
fn grok_handle_imagine_ws_message(
|
|
value: &Value,
|
|
slots: &mut BTreeMap<String, GrokImagineImage>,
|
|
) -> Result<(), ExecutionRuntimeTransportError> {
|
|
match value.get("type").and_then(Value::as_str) {
|
|
Some("json") => grok_handle_imagine_json_frame(value, slots),
|
|
Some("image") => {
|
|
grok_handle_imagine_image_frame(value, slots);
|
|
Ok(())
|
|
}
|
|
Some("error") => Err(ExecutionRuntimeTransportError::UpstreamRequest(format!(
|
|
"Grok Imagine websocket error: {}",
|
|
value
|
|
.get("err_msg")
|
|
.or_else(|| value.get("error"))
|
|
.and_then(Value::as_str)
|
|
.unwrap_or("unknown")
|
|
))),
|
|
_ => Ok(()),
|
|
}
|
|
}
|
|
|
|
fn grok_handle_imagine_json_frame(
|
|
value: &Value,
|
|
slots: &mut BTreeMap<String, GrokImagineImage>,
|
|
) -> Result<(), ExecutionRuntimeTransportError> {
|
|
let status = value.get("current_status").and_then(Value::as_str);
|
|
let Some(image_id) = value
|
|
.get("image_id")
|
|
.or_else(|| value.get("job_id"))
|
|
.and_then(Value::as_str)
|
|
.map(trimmed_string)
|
|
.filter(|value| !value.is_empty())
|
|
else {
|
|
return Ok(());
|
|
};
|
|
let order = value
|
|
.get("order")
|
|
.and_then(Value::as_u64)
|
|
.and_then(|value| usize::try_from(value).ok())
|
|
.unwrap_or_default();
|
|
match status {
|
|
Some("start_stage") => {
|
|
slots.entry(image_id.clone()).or_insert(GrokImagineImage {
|
|
image_id,
|
|
order,
|
|
url: None,
|
|
blob_b64: None,
|
|
done: false,
|
|
moderated: false,
|
|
});
|
|
}
|
|
Some("completed") => {
|
|
let slot = slots.entry(image_id.clone()).or_insert(GrokImagineImage {
|
|
image_id,
|
|
order,
|
|
url: None,
|
|
blob_b64: None,
|
|
done: false,
|
|
moderated: false,
|
|
});
|
|
slot.order = order;
|
|
slot.done = true;
|
|
slot.moderated = value
|
|
.get("moderated")
|
|
.and_then(Value::as_bool)
|
|
.unwrap_or(false);
|
|
}
|
|
_ => {}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn grok_handle_imagine_image_frame(value: &Value, slots: &mut BTreeMap<String, GrokImagineImage>) {
|
|
let Some(url) = value.get("url").and_then(Value::as_str).map(grok_asset_url) else {
|
|
return;
|
|
};
|
|
let image_id =
|
|
grok_imagine_image_id_from_url(&url).unwrap_or_else(|| Uuid::new_v4().to_string());
|
|
let fallback_order = slots.len();
|
|
let slot = slots.entry(image_id.clone()).or_insert(GrokImagineImage {
|
|
image_id,
|
|
order: fallback_order,
|
|
url: None,
|
|
blob_b64: None,
|
|
done: false,
|
|
moderated: false,
|
|
});
|
|
slot.url = Some(url);
|
|
slot.blob_b64 = value
|
|
.get("blob")
|
|
.and_then(Value::as_str)
|
|
.map(trimmed_string)
|
|
.filter(|value| !value.is_empty());
|
|
}
|
|
|
|
fn grok_imagine_image_id_from_url(url: &str) -> Option<String> {
|
|
let path = reqwest::Url::parse(url)
|
|
.ok()
|
|
.map(|url| url.path().to_string())
|
|
.unwrap_or_else(|| url.to_string());
|
|
let file_name = path.rsplit('/').next()?;
|
|
let (stem, _) = file_name.rsplit_once('.')?;
|
|
(!stem.trim().is_empty()).then(|| stem.to_string())
|
|
}
|
|
|
|
fn grok_imagine_completed_count(slots: &BTreeMap<String, GrokImagineImage>) -> usize {
|
|
slots
|
|
.values()
|
|
.filter(|image| {
|
|
image.done && !image.moderated && (image.url.is_some() || image.blob_b64.is_some())
|
|
})
|
|
.count()
|
|
}
|
|
|
|
fn grok_data_image_url(blob_b64: String) -> String {
|
|
format!("data:image/png;base64,{blob_b64}")
|
|
}
|
|
|
|
fn set_grok_image_edit_config(
|
|
upstream_body: &mut Value,
|
|
image_references: Vec<Value>,
|
|
parent_post_id: String,
|
|
) -> Result<(), ExecutionRuntimeTransportError> {
|
|
let Some(config) = upstream_body
|
|
.get_mut("responseMetadata")
|
|
.and_then(|value| value.get_mut("modelConfigOverride"))
|
|
.and_then(|value| value.get_mut("modelMap"))
|
|
.and_then(|value| value.get_mut("imageEditModelConfig"))
|
|
.and_then(Value::as_object_mut)
|
|
else {
|
|
return Err(ExecutionRuntimeTransportError::UpstreamRequest(
|
|
"Grok image edit payload is missing imageEditModelConfig".to_string(),
|
|
));
|
|
};
|
|
config.insert(
|
|
"imageReferences".to_string(),
|
|
Value::Array(image_references),
|
|
);
|
|
config.insert("parentPostId".to_string(), Value::String(parent_post_id));
|
|
Ok(())
|
|
}
|
|
|
|
fn filename_from_url_path(path: &str) -> Option<String> {
|
|
path.rsplit('/')
|
|
.next()
|
|
.map(trimmed_string)
|
|
.filter(|value| !value.is_empty())
|
|
}
|
|
|
|
fn default_attachment_filename(index: usize, mime_type: &str) -> String {
|
|
let ext = mime_type
|
|
.rsplit('/')
|
|
.next()
|
|
.map(|value| value.split('+').next().unwrap_or(value))
|
|
.map(trimmed_string)
|
|
.filter(|value| !value.is_empty())
|
|
.unwrap_or_else(|| "bin".to_string());
|
|
format!("file-{}.{}", index + 1, ext)
|
|
}
|
|
|
|
fn grok_execution_result(
|
|
plan: &ExecutionPlan,
|
|
collected: GrokCollected,
|
|
report_context: Option<&Value>,
|
|
) -> ExecutionResult {
|
|
let status_code = collected.status_code;
|
|
let body_json = if (200..300).contains(&status_code) {
|
|
grok_client_json_body(plan, &collected, report_context)
|
|
} else {
|
|
json!({
|
|
"error": {
|
|
"message": collected.text,
|
|
"type": "grok_upstream_error",
|
|
"code": status_code,
|
|
}
|
|
})
|
|
};
|
|
ExecutionResult {
|
|
request_id: plan.request_id.clone(),
|
|
candidate_id: plan.candidate_id.clone(),
|
|
status_code,
|
|
headers: BTreeMap::from([("content-type".to_string(), "application/json".to_string())]),
|
|
response_observation: None,
|
|
body: Some(ResponseBody {
|
|
json_body: Some(body_json),
|
|
body_bytes_b64: None,
|
|
}),
|
|
telemetry: Some(collected.telemetry),
|
|
error: None,
|
|
}
|
|
}
|
|
|
|
fn grok_client_json_body(
|
|
plan: &ExecutionPlan,
|
|
collected: &GrokCollected,
|
|
report_context: Option<&Value>,
|
|
) -> Value {
|
|
let model = plan.model_name.as_deref().unwrap_or("grok");
|
|
let usage = grok_usage_estimate(plan, collected);
|
|
let client_format = normalized_client_api_format(plan);
|
|
if client_format == "openai:image" {
|
|
return openai_image_body(collected);
|
|
}
|
|
|
|
let provider_body = openai_responses_body(
|
|
model,
|
|
collected,
|
|
usage,
|
|
grok_plan_uses_structured_image_generation(plan, report_context),
|
|
);
|
|
if client_format == "openai:responses" {
|
|
return provider_body;
|
|
}
|
|
|
|
convert_standard_chat_response(
|
|
&provider_body,
|
|
GROK_STANDARD_PROVIDER_API_FORMAT,
|
|
client_format.as_str(),
|
|
&grok_conversion_report_context(plan, model),
|
|
)
|
|
.unwrap_or_else(|| {
|
|
grok_legacy_client_json_body(client_format.as_str(), model, collected, usage)
|
|
})
|
|
}
|
|
|
|
fn grok_collected_frame_stream(
|
|
plan: ExecutionPlan,
|
|
collected: GrokCollected,
|
|
report_context: Option<&Value>,
|
|
) -> BoxStream<'static, Result<Bytes, IoError>> {
|
|
let body = grok_client_stream_body(&plan, &collected, report_context);
|
|
let telemetry = collected.telemetry.clone();
|
|
let status_code = collected.status_code;
|
|
let frames = vec![
|
|
StreamFrame {
|
|
frame_type: StreamFrameType::Headers,
|
|
payload: StreamFramePayload::Headers {
|
|
status_code,
|
|
headers: BTreeMap::from([(
|
|
"content-type".to_string(),
|
|
if (200..300).contains(&status_code) {
|
|
"text/event-stream".to_string()
|
|
} else {
|
|
"application/json".to_string()
|
|
},
|
|
)]),
|
|
response_observation: None,
|
|
},
|
|
},
|
|
StreamFrame {
|
|
frame_type: StreamFrameType::Telemetry,
|
|
payload: StreamFramePayload::Telemetry {
|
|
telemetry: ExecutionTelemetry {
|
|
ttfb_ms: telemetry.ttfb_ms,
|
|
elapsed_ms: telemetry.ttfb_ms,
|
|
upstream_bytes: Some(0),
|
|
},
|
|
},
|
|
},
|
|
StreamFrame {
|
|
frame_type: StreamFrameType::Data,
|
|
payload: StreamFramePayload::Data {
|
|
chunk_b64: Some(base64::engine::general_purpose::STANDARD.encode(body.as_bytes())),
|
|
text: None,
|
|
},
|
|
},
|
|
StreamFrame {
|
|
frame_type: StreamFrameType::Telemetry,
|
|
payload: StreamFramePayload::Telemetry { telemetry },
|
|
},
|
|
StreamFrame::eof(),
|
|
];
|
|
stream::iter(
|
|
frames
|
|
.into_iter()
|
|
.map(|frame| encode_stream_frame_ndjson(&frame)),
|
|
)
|
|
.boxed()
|
|
}
|
|
|
|
fn grok_client_stream_body(
|
|
plan: &ExecutionPlan,
|
|
collected: &GrokCollected,
|
|
report_context: Option<&Value>,
|
|
) -> String {
|
|
if !(200..300).contains(&collected.status_code) {
|
|
return serde_json::to_string(&json!({
|
|
"error": {
|
|
"message": collected.text,
|
|
"type": "grok_upstream_error",
|
|
"code": collected.status_code,
|
|
}
|
|
}))
|
|
.unwrap_or_else(|_| "{}".to_string());
|
|
}
|
|
let model = plan.model_name.as_deref().unwrap_or("grok");
|
|
let usage = grok_usage_estimate(plan, collected);
|
|
let client_format = normalized_client_api_format(plan);
|
|
if client_format == "openai:image" {
|
|
return openai_image_sse(collected);
|
|
}
|
|
|
|
let provider_body = openai_responses_body(
|
|
model,
|
|
collected,
|
|
usage,
|
|
grok_plan_uses_structured_image_generation(plan, report_context),
|
|
);
|
|
let report_context = grok_conversion_report_context(plan, model);
|
|
match maybe_bridge_standard_sync_json_to_stream(
|
|
&provider_body,
|
|
GROK_STANDARD_PROVIDER_API_FORMAT,
|
|
client_format.as_str(),
|
|
Some(&report_context),
|
|
) {
|
|
Ok(Some(outcome)) => String::from_utf8(outcome.sse_body)
|
|
.unwrap_or_else(|err| String::from_utf8_lossy(&err.into_bytes()).into_owned()),
|
|
Ok(None) | Err(_) => {
|
|
grok_legacy_client_stream_body(client_format.as_str(), model, collected, usage)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn normalized_client_api_format(plan: &ExecutionPlan) -> String {
|
|
let value = plan.client_api_format.trim();
|
|
if value.is_empty() {
|
|
"openai:chat".to_string()
|
|
} else {
|
|
value.to_ascii_lowercase()
|
|
}
|
|
}
|
|
|
|
fn grok_plan_uses_structured_image_generation(
|
|
plan: &ExecutionPlan,
|
|
report_context: Option<&Value>,
|
|
) -> bool {
|
|
let client_format = normalized_client_api_format(plan);
|
|
if client_format == "openai:image" {
|
|
return true;
|
|
}
|
|
if !matches!(
|
|
client_format.as_str(),
|
|
"openai:chat" | "openai:responses" | "openai:responses:compact"
|
|
) {
|
|
return false;
|
|
}
|
|
let model_is_image_generation = plan
|
|
.model_name
|
|
.as_deref()
|
|
.is_some_and(grok_model_name_is_image_generation)
|
|
|| grok_report_context_mapped_model(report_context)
|
|
.is_some_and(grok_model_name_is_image_generation);
|
|
let body_has_image_generation_tool = plan
|
|
.body
|
|
.json_body
|
|
.as_ref()
|
|
.is_some_and(grok_body_has_image_generation_tool);
|
|
model_is_image_generation || body_has_image_generation_tool
|
|
}
|
|
|
|
fn grok_report_context_mapped_model(report_context: Option<&Value>) -> Option<&str> {
|
|
report_context
|
|
.and_then(|value| value.get("mapped_model"))
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
}
|
|
|
|
fn grok_model_name_is_image_generation(model: &str) -> bool {
|
|
let model = model.trim().to_ascii_lowercase();
|
|
model.contains("grok-imagine-image") && !model.contains("edit")
|
|
}
|
|
|
|
fn grok_body_has_image_generation_tool(body: &Value) -> bool {
|
|
body.get("tools")
|
|
.and_then(Value::as_array)
|
|
.into_iter()
|
|
.flatten()
|
|
.any(|tool| {
|
|
tool.get("type")
|
|
.and_then(Value::as_str)
|
|
.is_some_and(|value| value.eq_ignore_ascii_case("image_generation"))
|
|
&& !tool
|
|
.get("action")
|
|
.and_then(Value::as_str)
|
|
.is_some_and(|value| value.eq_ignore_ascii_case("edit"))
|
|
})
|
|
}
|
|
|
|
fn grok_conversion_report_context(plan: &ExecutionPlan, model: &str) -> Value {
|
|
json!({
|
|
"provider_type": "grok",
|
|
"provider_api_format": GROK_STANDARD_PROVIDER_API_FORMAT,
|
|
"client_api_format": normalized_client_api_format(plan),
|
|
"mapped_model": model,
|
|
"model": model,
|
|
})
|
|
}
|
|
|
|
fn grok_legacy_client_json_body(
|
|
client_format: &str,
|
|
model: &str,
|
|
collected: &GrokCollected,
|
|
usage: GrokUsageEstimate,
|
|
) -> Value {
|
|
match client_format {
|
|
"openai:responses" | "openai:responses:compact" => {
|
|
openai_responses_body(model, collected, usage, false)
|
|
}
|
|
"claude:messages" => claude_messages_body(model, collected, usage),
|
|
_ => openai_chat_body(model, collected, usage),
|
|
}
|
|
}
|
|
|
|
fn grok_legacy_client_stream_body(
|
|
client_format: &str,
|
|
model: &str,
|
|
collected: &GrokCollected,
|
|
usage: GrokUsageEstimate,
|
|
) -> String {
|
|
match client_format {
|
|
"openai:responses" | "openai:responses:compact" => {
|
|
openai_responses_sse(model, collected, usage)
|
|
}
|
|
"claude:messages" => claude_messages_sse(model, collected, usage),
|
|
_ => openai_chat_sse(model, collected, usage),
|
|
}
|
|
}
|
|
|
|
impl GrokStreamAdapter {
|
|
fn push_chunk(&mut self, chunk: &[u8]) {
|
|
self.buffered.push_str(&String::from_utf8_lossy(chunk));
|
|
while let Some(index) = self.buffered.find('\n') {
|
|
let line = self.buffered.drain(..=index).collect::<String>();
|
|
self.handle_line(line.trim());
|
|
}
|
|
}
|
|
|
|
fn finish(&mut self) {
|
|
if !self.buffered.trim().is_empty() {
|
|
let line = std::mem::take(&mut self.buffered);
|
|
self.handle_line(line.trim());
|
|
}
|
|
}
|
|
|
|
fn handle_line(&mut self, line: &str) {
|
|
let line = line.trim();
|
|
if line.is_empty() || line.starts_with("event:") {
|
|
return;
|
|
}
|
|
let data = line.strip_prefix("data:").map(str::trim).unwrap_or(line);
|
|
if data.is_empty() || data == "[DONE]" || !data.starts_with('{') {
|
|
return;
|
|
}
|
|
let Ok(value) = serde_json::from_str::<Value>(data) else {
|
|
return;
|
|
};
|
|
self.handle_event(&value);
|
|
}
|
|
|
|
fn handle_event(&mut self, value: &Value) {
|
|
let Some(response) = value
|
|
.get("result")
|
|
.and_then(|result| result.get("response"))
|
|
else {
|
|
return;
|
|
};
|
|
self.handle_streaming_image_generation_response(response);
|
|
self.handle_model_response_images(response);
|
|
if let Some(card) = response.get("cardAttachment") {
|
|
self.handle_card(card);
|
|
}
|
|
if let Some(token) = response.get("token").and_then(Value::as_str) {
|
|
if response
|
|
.get("isThinking")
|
|
.and_then(Value::as_bool)
|
|
.unwrap_or(false)
|
|
{
|
|
self.thinking.push_str(token);
|
|
} else if response.get("messageTag").and_then(Value::as_str) == Some("final") {
|
|
let cleaned = self.clean_token(token);
|
|
self.text.push_str(&cleaned);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handle_streaming_image_generation_response(&mut self, response: &Value) {
|
|
let Some(stream) = response.get("streamingImageGenerationResponse") else {
|
|
return;
|
|
};
|
|
if stream
|
|
.get("moderated")
|
|
.and_then(Value::as_bool)
|
|
.unwrap_or(false)
|
|
{
|
|
return;
|
|
}
|
|
if stream
|
|
.get("progress")
|
|
.and_then(Value::as_u64)
|
|
.unwrap_or_default()
|
|
< 100
|
|
{
|
|
return;
|
|
}
|
|
let url = stream
|
|
.get("assetId")
|
|
.and_then(Value::as_str)
|
|
.map(|asset_id| format!("{GROK_ASSET_BASE}{asset_id}/content"))
|
|
.or_else(|| {
|
|
stream
|
|
.get("imageUrl")
|
|
.and_then(Value::as_str)
|
|
.map(grok_asset_url)
|
|
});
|
|
if let Some(url) = url {
|
|
self.push_image_url(url);
|
|
}
|
|
}
|
|
|
|
fn handle_model_response_images(&mut self, response: &Value) {
|
|
let Some(model_response) = response.get("modelResponse") else {
|
|
return;
|
|
};
|
|
if let Some(urls) = model_response
|
|
.get("generatedImageUrls")
|
|
.and_then(Value::as_array)
|
|
{
|
|
for url in urls.iter().filter_map(Value::as_str) {
|
|
self.push_image_url(grok_asset_url(url));
|
|
}
|
|
}
|
|
if let Some(attachments) = model_response
|
|
.get("fileAttachments")
|
|
.and_then(Value::as_array)
|
|
{
|
|
for asset_id in attachments.iter().filter_map(Value::as_str) {
|
|
self.push_image_url(format!("{GROK_ASSET_BASE}{asset_id}/content"));
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handle_card(&mut self, card: &Value) {
|
|
let Some(json_data) = card.get("jsonData").and_then(Value::as_str) else {
|
|
return;
|
|
};
|
|
let Ok(value) = serde_json::from_str::<Value>(json_data) else {
|
|
return;
|
|
};
|
|
if let Some(card_id) = value.get("id").and_then(Value::as_str) {
|
|
self.cards.insert(
|
|
card_id.to_string(),
|
|
GrokCard {
|
|
url: value
|
|
.get("url")
|
|
.and_then(Value::as_str)
|
|
.map(ToOwned::to_owned),
|
|
title: value
|
|
.get("title")
|
|
.and_then(Value::as_str)
|
|
.map(ToOwned::to_owned),
|
|
},
|
|
);
|
|
}
|
|
let Some(chunk) = value.get("image_chunk") else {
|
|
return;
|
|
};
|
|
if chunk.get("progress").and_then(Value::as_u64) != Some(100)
|
|
|| chunk
|
|
.get("moderated")
|
|
.and_then(Value::as_bool)
|
|
.unwrap_or(false)
|
|
{
|
|
return;
|
|
}
|
|
let Some(path) = chunk.get("imageUrl").and_then(Value::as_str) else {
|
|
return;
|
|
};
|
|
let url = if path.starts_with("http://") || path.starts_with("https://") {
|
|
path.to_string()
|
|
} else {
|
|
format!("{GROK_ASSET_BASE}{path}")
|
|
};
|
|
self.push_image_url(url);
|
|
}
|
|
|
|
fn push_image_url(&mut self, url: String) {
|
|
if !url.trim().is_empty() && !self.images.iter().any(|item| item == &url) {
|
|
self.images.push(url);
|
|
}
|
|
}
|
|
|
|
fn clean_token(&mut self, token: &str) -> String {
|
|
if !token.contains("<grok:render") {
|
|
return token.to_string();
|
|
}
|
|
let replaced = grok_render_regex()
|
|
.replace_all(token, |captures: &Captures<'_>| {
|
|
self.render_replacement(
|
|
captures
|
|
.get(1)
|
|
.map(|value| value.as_str())
|
|
.unwrap_or_default(),
|
|
captures
|
|
.get(3)
|
|
.map(|value| value.as_str())
|
|
.unwrap_or_default(),
|
|
)
|
|
})
|
|
.to_string();
|
|
if replaced.starts_with('\n') && replaced.contains("[[") {
|
|
replaced.trim_start_matches('\n').to_string()
|
|
} else {
|
|
replaced
|
|
}
|
|
}
|
|
|
|
fn render_replacement(&mut self, card_id: &str, render_type: &str) -> String {
|
|
let Some(card) = self.cards.get(card_id) else {
|
|
return String::new();
|
|
};
|
|
match render_type {
|
|
"render_inline_citation" => {
|
|
let Some(url) = card.url.clone().filter(|value| !value.trim().is_empty()) else {
|
|
return String::new();
|
|
};
|
|
let index = self
|
|
.citation_order
|
|
.iter()
|
|
.position(|existing| existing == &url)
|
|
.map(|position| position + 1)
|
|
.unwrap_or_else(|| {
|
|
self.citation_order.push(url.clone());
|
|
self.citation_order.len()
|
|
});
|
|
if self.last_citation_index == Some(index) {
|
|
return String::new();
|
|
}
|
|
self.last_citation_index = Some(index);
|
|
let title = card.title.clone().unwrap_or_else(|| url.clone());
|
|
format!(" [[{index}]]({url} \"{}\")", title.replace('"', "'"))
|
|
}
|
|
"render_searched_image" => {
|
|
let Some(url) = card.url.clone().filter(|value| !value.trim().is_empty()) else {
|
|
return String::new();
|
|
};
|
|
let title = card.title.clone().unwrap_or_else(|| "image".to_string());
|
|
format!("", title.replace(['[', ']'], ""))
|
|
}
|
|
"render_generated_image" => String::new(),
|
|
_ => String::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn openai_chat_body(model: &str, collected: &GrokCollected, usage: GrokUsageEstimate) -> Value {
|
|
json!({
|
|
"id": format!("chatcmpl-{}", Uuid::new_v4()),
|
|
"object": "chat.completion",
|
|
"created": current_unix_secs(),
|
|
"model": model,
|
|
"choices": [{
|
|
"index": 0,
|
|
"message": {
|
|
"role": "assistant",
|
|
"content": chat_text_with_images(collected),
|
|
},
|
|
"finish_reason": "stop",
|
|
}],
|
|
"usage": openai_chat_usage(usage),
|
|
})
|
|
}
|
|
|
|
fn openai_chat_sse(model: &str, collected: &GrokCollected, usage: GrokUsageEstimate) -> String {
|
|
let id = format!("chatcmpl-{}", Uuid::new_v4());
|
|
let mut body = String::new();
|
|
if !collected.thinking.is_empty() {
|
|
push_sse_data(
|
|
&mut body,
|
|
&json!({
|
|
"id": id,
|
|
"object": "chat.completion.chunk",
|
|
"created": current_unix_secs(),
|
|
"model": model,
|
|
"choices": [{
|
|
"index": 0,
|
|
"delta": {"role": "assistant", "reasoning_content": collected.thinking},
|
|
}],
|
|
}),
|
|
);
|
|
}
|
|
push_sse_data(
|
|
&mut body,
|
|
&json!({
|
|
"id": id,
|
|
"object": "chat.completion.chunk",
|
|
"created": current_unix_secs(),
|
|
"model": model,
|
|
"choices": [{
|
|
"index": 0,
|
|
"delta": {"role": "assistant", "content": chat_text_with_images(collected)},
|
|
}],
|
|
}),
|
|
);
|
|
push_sse_data(
|
|
&mut body,
|
|
&json!({
|
|
"id": id,
|
|
"object": "chat.completion.chunk",
|
|
"created": current_unix_secs(),
|
|
"model": model,
|
|
"choices": [{
|
|
"index": 0,
|
|
"delta": {},
|
|
"finish_reason": "stop",
|
|
}],
|
|
"usage": openai_chat_usage(usage),
|
|
}),
|
|
);
|
|
body.push_str("data: [DONE]\n\n");
|
|
body
|
|
}
|
|
|
|
fn openai_responses_body(
|
|
model: &str,
|
|
collected: &GrokCollected,
|
|
usage: GrokUsageEstimate,
|
|
images_as_generation_calls: bool,
|
|
) -> Value {
|
|
let response_id = format!("resp_{}", Uuid::new_v4());
|
|
let mut output = Vec::new();
|
|
if !collected.thinking.trim().is_empty() {
|
|
output.push(json!({
|
|
"id": openai_responses_synthetic_reasoning_item_id(&response_id, 0),
|
|
"type": "reasoning",
|
|
"status": "completed",
|
|
"summary": [{
|
|
"type": "summary_text",
|
|
"text": collected.thinking.trim(),
|
|
}],
|
|
}));
|
|
}
|
|
let message_text = if images_as_generation_calls {
|
|
collected.text.clone()
|
|
} else {
|
|
chat_text_with_images(collected)
|
|
};
|
|
if !message_text.trim().is_empty() {
|
|
output.push(json!({
|
|
"id": format!("{response_id}_msg"),
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"content": [{"type": "output_text", "text": message_text, "annotations": []}],
|
|
"status": "completed",
|
|
}));
|
|
}
|
|
if images_as_generation_calls {
|
|
for (index, image) in collected.images.iter().enumerate() {
|
|
output.push(grok_openai_responses_image_generation_item(
|
|
response_id.as_str(),
|
|
index,
|
|
image.as_str(),
|
|
));
|
|
}
|
|
}
|
|
json!({
|
|
"id": response_id,
|
|
"object": "response",
|
|
"created_at": current_unix_secs(),
|
|
"status": "completed",
|
|
"model": model,
|
|
"output": output,
|
|
"usage": openai_responses_usage(usage),
|
|
})
|
|
}
|
|
|
|
fn grok_openai_responses_image_generation_item(
|
|
response_id: &str,
|
|
index: usize,
|
|
image: &str,
|
|
) -> Value {
|
|
let mut item = Map::new();
|
|
item.insert(
|
|
"id".to_string(),
|
|
Value::String(format!("{response_id}_ig_{index}")),
|
|
);
|
|
item.insert(
|
|
"type".to_string(),
|
|
Value::String("image_generation_call".to_string()),
|
|
);
|
|
item.insert("status".to_string(), Value::String("completed".to_string()));
|
|
item.insert("action".to_string(), Value::String("generate".to_string()));
|
|
if let Some((mime_type, b64_json)) = grok_data_image_parts(image) {
|
|
item.insert("result".to_string(), Value::String(b64_json));
|
|
item.insert(
|
|
"output_format".to_string(),
|
|
Value::String(grok_output_format_from_mime_type(mime_type.as_str())),
|
|
);
|
|
item.insert("mime_type".to_string(), Value::String(mime_type));
|
|
} else {
|
|
item.insert("url".to_string(), Value::String(image.to_string()));
|
|
item.insert(
|
|
"output_format".to_string(),
|
|
Value::String("png".to_string()),
|
|
);
|
|
}
|
|
Value::Object(item)
|
|
}
|
|
|
|
fn grok_output_format_from_mime_type(mime_type: &str) -> String {
|
|
match mime_type.trim().to_ascii_lowercase().as_str() {
|
|
"image/jpeg" | "image/jpg" => "jpeg",
|
|
"image/webp" => "webp",
|
|
"image/gif" => "gif",
|
|
_ => "png",
|
|
}
|
|
.to_string()
|
|
}
|
|
|
|
fn openai_responses_sse(
|
|
model: &str,
|
|
collected: &GrokCollected,
|
|
usage: GrokUsageEstimate,
|
|
) -> String {
|
|
let response_id = format!("resp_{}", Uuid::new_v4());
|
|
let message_id = format!("msg_{}", Uuid::new_v4());
|
|
let text = chat_text_with_images(collected);
|
|
let response = openai_responses_body(model, collected, usage, false);
|
|
let mut body = String::new();
|
|
push_sse_event(
|
|
&mut body,
|
|
"response.created",
|
|
&json!({"type": "response.created", "response": {
|
|
"id": response_id,
|
|
"object": "response",
|
|
"created_at": current_unix_secs(),
|
|
"status": "in_progress",
|
|
"model": model,
|
|
"output": [],
|
|
}}),
|
|
);
|
|
push_sse_event(
|
|
&mut body,
|
|
"response.output_item.added",
|
|
&json!({"type":"response.output_item.added","output_index":0,"item":{
|
|
"id": message_id, "type":"message", "role":"assistant", "content":[], "status":"in_progress"
|
|
}}),
|
|
);
|
|
push_sse_event(
|
|
&mut body,
|
|
"response.output_text.delta",
|
|
&json!({"type":"response.output_text.delta","item_id":message_id,"output_index":0,"content_index":0,"delta":text}),
|
|
);
|
|
push_sse_event(
|
|
&mut body,
|
|
"response.completed",
|
|
&json!({"type":"response.completed","response": response}),
|
|
);
|
|
body.push_str("data: [DONE]\n\n");
|
|
body
|
|
}
|
|
|
|
fn claude_messages_body(model: &str, collected: &GrokCollected, usage: GrokUsageEstimate) -> Value {
|
|
json!({
|
|
"id": format!("msg_{}", Uuid::new_v4()),
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"model": model,
|
|
"content": [{"type": "text", "text": chat_text_with_images(collected)}],
|
|
"stop_reason": "end_turn",
|
|
"stop_sequence": Value::Null,
|
|
"usage": {
|
|
"input_tokens": usage.input_tokens,
|
|
"output_tokens": usage.output_tokens,
|
|
},
|
|
})
|
|
}
|
|
|
|
fn claude_messages_sse(model: &str, collected: &GrokCollected, usage: GrokUsageEstimate) -> String {
|
|
let message_id = format!("msg_{}", Uuid::new_v4());
|
|
let mut body = String::new();
|
|
let mut next_block_index = 0usize;
|
|
|
|
push_sse_event(
|
|
&mut body,
|
|
"message_start",
|
|
&json!({
|
|
"type": "message_start",
|
|
"message": {
|
|
"id": message_id,
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"model": model,
|
|
"content": [],
|
|
"stop_reason": Value::Null,
|
|
"stop_sequence": Value::Null,
|
|
"usage": {
|
|
"input_tokens": 0,
|
|
"output_tokens": 0,
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
if !collected.thinking.is_empty() {
|
|
let block_index = next_block_index;
|
|
next_block_index += 1;
|
|
push_sse_event(
|
|
&mut body,
|
|
"content_block_start",
|
|
&json!({
|
|
"type": "content_block_start",
|
|
"index": block_index,
|
|
"content_block": {
|
|
"type": "thinking",
|
|
"thinking": "",
|
|
},
|
|
}),
|
|
);
|
|
push_sse_event(
|
|
&mut body,
|
|
"content_block_delta",
|
|
&json!({
|
|
"type": "content_block_delta",
|
|
"index": block_index,
|
|
"delta": {
|
|
"type": "thinking_delta",
|
|
"thinking": collected.thinking,
|
|
},
|
|
}),
|
|
);
|
|
push_sse_event(
|
|
&mut body,
|
|
"content_block_stop",
|
|
&json!({
|
|
"type": "content_block_stop",
|
|
"index": block_index,
|
|
}),
|
|
);
|
|
}
|
|
|
|
let text = chat_text_with_images(collected);
|
|
if !text.is_empty() {
|
|
let block_index = next_block_index;
|
|
push_sse_event(
|
|
&mut body,
|
|
"content_block_start",
|
|
&json!({
|
|
"type": "content_block_start",
|
|
"index": block_index,
|
|
"content_block": {
|
|
"type": "text",
|
|
"text": "",
|
|
},
|
|
}),
|
|
);
|
|
push_sse_event(
|
|
&mut body,
|
|
"content_block_delta",
|
|
&json!({
|
|
"type": "content_block_delta",
|
|
"index": block_index,
|
|
"delta": {
|
|
"type": "text_delta",
|
|
"text": text,
|
|
},
|
|
}),
|
|
);
|
|
push_sse_event(
|
|
&mut body,
|
|
"content_block_stop",
|
|
&json!({
|
|
"type": "content_block_stop",
|
|
"index": block_index,
|
|
}),
|
|
);
|
|
}
|
|
|
|
push_sse_event(
|
|
&mut body,
|
|
"message_delta",
|
|
&json!({
|
|
"type": "message_delta",
|
|
"delta": {
|
|
"stop_reason": "end_turn",
|
|
"stop_sequence": Value::Null,
|
|
},
|
|
"usage": {
|
|
"input_tokens": usage.input_tokens,
|
|
"output_tokens": usage.output_tokens,
|
|
},
|
|
}),
|
|
);
|
|
push_sse_event(
|
|
&mut body,
|
|
"message_stop",
|
|
&json!({
|
|
"type": "message_stop",
|
|
}),
|
|
);
|
|
body
|
|
}
|
|
|
|
fn openai_image_body(collected: &GrokCollected) -> Value {
|
|
json!({
|
|
"created": current_unix_secs(),
|
|
"data": collected
|
|
.images
|
|
.iter()
|
|
.map(|url| grok_openai_image_item(url.as_str()))
|
|
.collect::<Vec<_>>(),
|
|
})
|
|
}
|
|
|
|
fn grok_openai_image_item(url: &str) -> Value {
|
|
if let Some((mime_type, b64_json)) = grok_data_image_parts(url) {
|
|
return json!({
|
|
"b64_json": b64_json,
|
|
"mime_type": mime_type,
|
|
});
|
|
}
|
|
json!({ "url": url })
|
|
}
|
|
|
|
async fn materialize_grok_image_assets(plan: &ExecutionPlan, collected: &mut GrokCollected) {
|
|
if normalized_client_api_format(plan) != "openai:image" || collected.images.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let mut resolved_images = Vec::with_capacity(collected.images.len());
|
|
for image_url in &collected.images {
|
|
match grok_download_image_asset(plan, image_url.as_str()).await {
|
|
Ok(Some(data_url)) => resolved_images.push(data_url),
|
|
Ok(None) | Err(_) => resolved_images.push(image_url.clone()),
|
|
}
|
|
}
|
|
collected.images = resolved_images;
|
|
}
|
|
|
|
async fn grok_download_image_asset(
|
|
plan: &ExecutionPlan,
|
|
raw_url: &str,
|
|
) -> Result<Option<String>, ExecutionRuntimeTransportError> {
|
|
if !grok_image_asset_url_is_supported(raw_url) {
|
|
return Ok(None);
|
|
}
|
|
let url = reqwest::Url::parse(raw_url).map_err(|err| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(format!(
|
|
"Grok image asset URL is invalid: {err}"
|
|
))
|
|
})?;
|
|
let mut download_plan = plan.clone();
|
|
download_plan.method = "GET".to_string();
|
|
download_plan.url = url.to_string();
|
|
download_plan.headers.remove("content-type");
|
|
download_plan
|
|
.headers
|
|
.insert("accept".to_string(), "image/*,*/*;q=0.8".to_string());
|
|
download_plan.headers.insert(
|
|
EXECUTION_REQUEST_FOLLOW_REDIRECTS_HEADER.to_string(),
|
|
"true".to_string(),
|
|
);
|
|
download_plan.body = RequestBody {
|
|
json_body: None,
|
|
body_bytes_b64: None,
|
|
body_ref: None,
|
|
};
|
|
download_plan.stream = false;
|
|
|
|
let response = send_request(&download_plan, Vec::new()).await?;
|
|
if !(200..300).contains(&response.status_code()) {
|
|
return Ok(None);
|
|
}
|
|
let headers = response.headers();
|
|
let content_type = headers
|
|
.get("content-type")
|
|
.map(String::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| value.starts_with("image/"))
|
|
.unwrap_or("image/png")
|
|
.to_string();
|
|
let bytes = response.bytes().await.map_err(|err| {
|
|
ExecutionRuntimeTransportError::UpstreamRequest(format!(
|
|
"Grok image asset download failed: {err}"
|
|
))
|
|
})?;
|
|
if bytes.is_empty() {
|
|
return Ok(None);
|
|
}
|
|
Ok(Some(format!(
|
|
"data:{content_type};base64,{}",
|
|
base64::engine::general_purpose::STANDARD.encode(bytes)
|
|
)))
|
|
}
|
|
|
|
fn grok_image_asset_url_is_supported(raw_url: &str) -> bool {
|
|
if raw_url.starts_with("data:image/") {
|
|
return true;
|
|
}
|
|
let Ok(url) = reqwest::Url::parse(raw_url) else {
|
|
return false;
|
|
};
|
|
if !matches!(url.scheme(), "http" | "https") {
|
|
return false;
|
|
}
|
|
let Some(host) = url.host_str() else {
|
|
return false;
|
|
};
|
|
if matches!(host, "assets.grok.com" | "assets.grokusercontent.com") {
|
|
return true;
|
|
}
|
|
cfg!(test) && matches!(host, "localhost" | "127.0.0.1" | "::1")
|
|
}
|
|
|
|
fn grok_data_image_parts(raw_url: &str) -> Option<(String, String)> {
|
|
let Some((header, data)) = raw_url.trim().split_once(',') else {
|
|
return None;
|
|
};
|
|
if !header.starts_with("data:image/") || !header.contains(";base64") {
|
|
return None;
|
|
}
|
|
let mime = header
|
|
.strip_prefix("data:")
|
|
.and_then(|value| value.split(';').next())
|
|
.map(str::trim)
|
|
.filter(|value| value.starts_with("image/"))?
|
|
.to_string();
|
|
let normalized = data.split_whitespace().collect::<String>();
|
|
if normalized.is_empty() {
|
|
return None;
|
|
}
|
|
Some((mime, normalized))
|
|
}
|
|
|
|
fn openai_image_sse(collected: &GrokCollected) -> String {
|
|
let mut body = String::new();
|
|
for (index, url) in collected.images.iter().enumerate() {
|
|
push_sse_event(
|
|
&mut body,
|
|
"image_generation.completed",
|
|
&json!({
|
|
"type": "image_generation.completed",
|
|
"url": url,
|
|
"partial_image_index": index,
|
|
}),
|
|
);
|
|
}
|
|
body.push_str("data: [DONE]\n\n");
|
|
body
|
|
}
|
|
|
|
fn chat_text_with_images(collected: &GrokCollected) -> String {
|
|
let mut text = collected.text.clone();
|
|
for image in &collected.images {
|
|
if !text.is_empty() {
|
|
text.push_str("\n\n");
|
|
}
|
|
text.push_str(image);
|
|
}
|
|
text
|
|
}
|
|
|
|
fn grok_usage_estimate(plan: &ExecutionPlan, collected: &GrokCollected) -> GrokUsageEstimate {
|
|
let input_tokens = plan
|
|
.body
|
|
.json_body
|
|
.as_ref()
|
|
.map(estimated_prompt_tokens)
|
|
.unwrap_or_default();
|
|
let reasoning_tokens = estimated_text_tokens(&collected.thinking);
|
|
let output_tokens =
|
|
estimated_text_tokens(&chat_text_with_images(collected)).saturating_add(reasoning_tokens);
|
|
GrokUsageEstimate {
|
|
input_tokens,
|
|
output_tokens,
|
|
reasoning_tokens,
|
|
}
|
|
}
|
|
|
|
fn estimated_text_tokens(text: &str) -> u64 {
|
|
if text.trim().is_empty() {
|
|
return 0;
|
|
}
|
|
let chars = text.chars().count() as u64;
|
|
((chars + 3) / 4).max(1)
|
|
}
|
|
|
|
fn estimated_prompt_tokens(value: &Value) -> u64 {
|
|
let Ok(text) = serde_json::to_string(value) else {
|
|
return 0;
|
|
};
|
|
let tokens = estimated_text_tokens(&text);
|
|
if tokens == 0 {
|
|
0
|
|
} else {
|
|
tokens.saturating_add(GROK_PROMPT_OVERHEAD_TOKENS)
|
|
}
|
|
}
|
|
|
|
fn openai_chat_usage(usage: GrokUsageEstimate) -> Value {
|
|
json!({
|
|
"prompt_tokens": usage.input_tokens,
|
|
"completion_tokens": usage.output_tokens,
|
|
"total_tokens": usage.input_tokens.saturating_add(usage.output_tokens),
|
|
"prompt_tokens_details": {
|
|
"cached_tokens": 0,
|
|
"text_tokens": usage.input_tokens,
|
|
"audio_tokens": 0,
|
|
"image_tokens": 0,
|
|
},
|
|
"completion_tokens_details": {
|
|
"text_tokens": usage.output_tokens.saturating_sub(usage.reasoning_tokens),
|
|
"audio_tokens": 0,
|
|
"reasoning_tokens": usage.reasoning_tokens,
|
|
},
|
|
})
|
|
}
|
|
|
|
fn openai_responses_usage(usage: GrokUsageEstimate) -> Value {
|
|
json!({
|
|
"input_tokens": usage.input_tokens,
|
|
"output_tokens": usage.output_tokens,
|
|
"total_tokens": usage.input_tokens.saturating_add(usage.output_tokens),
|
|
"output_tokens_details": {
|
|
"reasoning_tokens": usage.reasoning_tokens,
|
|
},
|
|
})
|
|
}
|
|
|
|
fn push_sse_data(body: &mut String, data: &Value) {
|
|
body.push_str("data: ");
|
|
body.push_str(&serde_json::to_string(data).unwrap_or_else(|_| "{}".to_string()));
|
|
body.push_str("\n\n");
|
|
}
|
|
|
|
fn push_sse_event(body: &mut String, event: &str, data: &Value) {
|
|
body.push_str("event: ");
|
|
body.push_str(event);
|
|
body.push('\n');
|
|
push_sse_data(body, data);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::collections::BTreeMap;
|
|
|
|
use aether_contracts::{
|
|
ExecutionErrorKind, ExecutionPhase, ExecutionPlan, RequestBody, StreamFrame,
|
|
StreamFramePayload,
|
|
};
|
|
use axum::body::{Body, Bytes};
|
|
use axum::extract::Request;
|
|
use axum::routing::any;
|
|
use axum::Router;
|
|
use base64::Engine as _;
|
|
use futures_util::{stream, StreamExt};
|
|
use http::{Method, StatusCode};
|
|
|
|
use super::{
|
|
encode_grok_error_frame, encode_grok_first_byte_timeout_frame,
|
|
extract_grok_attachment_inputs, grok_aspect_ratio_from_provider_body, grok_asset_url,
|
|
grok_attachment_ip_is_public, grok_attachment_payload_from_data_uri, grok_client_json_body,
|
|
grok_client_stream_body, grok_handle_imagine_ws_message,
|
|
grok_image_count_from_provider_body, grok_image_prompt_from_provider_body,
|
|
grok_imagine_request_message, grok_imagine_reset_message, grok_media_post_url,
|
|
grok_plan_uses_structured_image_generation, grok_should_collect_image_stream,
|
|
grok_should_use_imagine_websocket, grok_success_frame_stream, grok_upload_url,
|
|
grok_upstream_model_name, grok_usage_estimate, grok_user_id_from_cookie_header,
|
|
materialize_grok_image_assets, openai_chat_body, openai_image_body, openai_responses_body,
|
|
set_grok_image_edit_config, GrokAttachmentInput, GrokCollected, GrokImagineImage,
|
|
GrokStreamAdapter,
|
|
};
|
|
|
|
fn sample_plan(body: serde_json::Value, client_api_format: &str) -> ExecutionPlan {
|
|
ExecutionPlan {
|
|
request_id: "req-1".to_string(),
|
|
candidate_id: Some("cand-1".to_string()),
|
|
provider_name: Some("Grok".to_string()),
|
|
provider_id: "provider-1".to_string(),
|
|
endpoint_id: "endpoint-1".to_string(),
|
|
key_id: "key-1".to_string(),
|
|
method: "POST".to_string(),
|
|
url: "https://grok.com/rest/app-chat/conversations/new".to_string(),
|
|
headers: BTreeMap::new(),
|
|
content_type: Some("application/json".to_string()),
|
|
content_encoding: None,
|
|
body: RequestBody::from_json(body),
|
|
stream: true,
|
|
client_api_format: client_api_format.to_string(),
|
|
provider_api_format: "openai:chat".to_string(),
|
|
model_name: Some("grok-test".to_string()),
|
|
proxy: None,
|
|
transport_profile: None,
|
|
timeouts: None,
|
|
}
|
|
}
|
|
|
|
fn report_context_with_mapped_model(mapped_model: &str) -> serde_json::Value {
|
|
serde_json::json!({
|
|
"mapped_model": mapped_model,
|
|
"provider_type": "grok",
|
|
})
|
|
}
|
|
|
|
fn grok_token_chunk(token: &str) -> Bytes {
|
|
Bytes::from(format!(
|
|
"data: {}\n\n",
|
|
serde_json::json!({
|
|
"result": {
|
|
"response": {
|
|
"token": token,
|
|
"messageTag": "final"
|
|
}
|
|
}
|
|
})
|
|
))
|
|
}
|
|
|
|
async fn collect_decoded_data_frames(
|
|
mut frame_stream: futures_util::stream::BoxStream<'static, Result<Bytes, std::io::Error>>,
|
|
) -> Vec<String> {
|
|
let mut out = Vec::new();
|
|
while let Some(item) = frame_stream.next().await {
|
|
let bytes = item.expect("frame should encode");
|
|
let line = String::from_utf8(bytes.to_vec()).expect("frame should be utf8");
|
|
let frame: StreamFrame =
|
|
serde_json::from_str(line.trim()).expect("frame should deserialize");
|
|
if let StreamFramePayload::Data { chunk_b64, text } = frame.payload {
|
|
let chunk = if let Some(chunk_b64) = chunk_b64 {
|
|
base64::engine::general_purpose::STANDARD
|
|
.decode(chunk_b64)
|
|
.expect("chunk should decode")
|
|
} else {
|
|
text.unwrap_or_default().into_bytes()
|
|
};
|
|
out.push(String::from_utf8(chunk).expect("chunk should be utf8"));
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
fn decode_encoded_frame(encoded: Bytes) -> StreamFrame {
|
|
let line = String::from_utf8(encoded.to_vec()).expect("frame should be utf8");
|
|
serde_json::from_str(line.trim()).expect("frame should deserialize")
|
|
}
|
|
|
|
#[test]
|
|
fn grok_stream_read_error_is_retryable_transport_without_upstream_status() {
|
|
let frame = decode_encoded_frame(
|
|
encode_grok_error_frame("connection reset while reading response body".to_string())
|
|
.expect("error frame should encode"),
|
|
);
|
|
let StreamFramePayload::Error { error } = frame.payload else {
|
|
panic!("encoded frame should contain an execution error");
|
|
};
|
|
|
|
assert_eq!(error.kind, ExecutionErrorKind::ProtocolError);
|
|
assert_eq!(error.phase, ExecutionPhase::StreamRead);
|
|
assert_eq!(error.upstream_status, None);
|
|
assert!(error.retryable);
|
|
assert!(error.failover_recommended);
|
|
}
|
|
|
|
#[test]
|
|
fn grok_first_byte_timeout_is_retryable_transport_without_upstream_status() {
|
|
let frame = decode_encoded_frame(
|
|
encode_grok_first_byte_timeout_frame(std::time::Duration::from_millis(250))
|
|
.expect("timeout frame should encode"),
|
|
);
|
|
let StreamFramePayload::Error { error } = frame.payload else {
|
|
panic!("encoded frame should contain an execution error");
|
|
};
|
|
|
|
assert_eq!(error.kind, ExecutionErrorKind::FirstByteTimeout);
|
|
assert_eq!(error.phase, ExecutionPhase::FirstByte);
|
|
assert_eq!(error.upstream_status, None);
|
|
assert!(error.retryable);
|
|
assert!(error.failover_recommended);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn grok_success_stream_forwards_token_chunks_incrementally() {
|
|
let plan = sample_plan(
|
|
serde_json::json!({
|
|
"messages": [{"role": "user", "content": "hello"}],
|
|
"stream": true,
|
|
}),
|
|
"openai:chat",
|
|
);
|
|
let upstream = stream::iter(vec![
|
|
Ok(grok_token_chunk("hel")),
|
|
Ok(grok_token_chunk("lo")),
|
|
])
|
|
.boxed();
|
|
|
|
let chunks = collect_decoded_data_frames(grok_success_frame_stream(
|
|
plan,
|
|
200,
|
|
BTreeMap::new(),
|
|
std::time::Instant::now(),
|
|
upstream,
|
|
))
|
|
.await;
|
|
|
|
assert!(
|
|
chunks
|
|
.iter()
|
|
.any(|chunk| chunk.contains("\"content\":\"hel\"")),
|
|
"first upstream token should be emitted as its own client chunk: {chunks:?}"
|
|
);
|
|
assert!(
|
|
chunks
|
|
.iter()
|
|
.any(|chunk| chunk.contains("\"content\":\"lo\"")),
|
|
"second upstream token should be emitted as its own client chunk: {chunks:?}"
|
|
);
|
|
let joined = chunks.join("");
|
|
let first = joined.find("\"content\":\"hel\"").expect("hel chunk");
|
|
let second = joined.find("\"content\":\"lo\"").expect("lo chunk");
|
|
let done = joined.find("data: [DONE]").expect("done chunk");
|
|
assert!(first < second && second < done);
|
|
}
|
|
|
|
#[test]
|
|
fn grok_upstream_model_name_prefers_report_context_mapping() {
|
|
let mut plan = sample_plan(
|
|
serde_json::json!({"messages": [{"role": "user", "content": "hello"}]}),
|
|
"openai:chat",
|
|
);
|
|
plan.model_name = Some("grok-4.20-0309-reasoning".to_string());
|
|
|
|
let mapped =
|
|
grok_upstream_model_name(Some(&report_context_with_mapped_model("grok-4.20-fast")))
|
|
.expect("mapped model should resolve");
|
|
|
|
assert_eq!(mapped, "grok-4.20-fast");
|
|
}
|
|
|
|
#[test]
|
|
fn grok_upstream_model_name_requires_report_context_mapping() {
|
|
let err = grok_upstream_model_name(None).expect_err("missing mapped model should fail");
|
|
|
|
assert!(err
|
|
.to_string()
|
|
.contains("Grok runtime requires mapped_model"));
|
|
}
|
|
|
|
#[test]
|
|
fn adapter_extracts_text_and_image_url() {
|
|
let image_json = serde_json::json!({
|
|
"image_chunk": {
|
|
"progress": 100,
|
|
"imageUrl": "generated/example.png"
|
|
}
|
|
})
|
|
.to_string();
|
|
let line = format!(
|
|
"data: {}\n",
|
|
serde_json::json!({
|
|
"result": {
|
|
"response": {
|
|
"token": "hello",
|
|
"messageTag": "final",
|
|
"cardAttachment": {"jsonData": image_json}
|
|
}
|
|
}
|
|
})
|
|
);
|
|
let mut adapter = GrokStreamAdapter::default();
|
|
adapter.push_chunk(line.as_bytes());
|
|
|
|
assert_eq!(adapter.text, "hello");
|
|
assert_eq!(
|
|
adapter.images,
|
|
vec!["https://assets.grok.com/generated/example.png"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn adapter_extracts_grok_image_edit_streaming_response() {
|
|
let line = format!(
|
|
"data: {}\n",
|
|
serde_json::json!({
|
|
"result": {
|
|
"response": {
|
|
"streamingImageGenerationResponse": {
|
|
"progress": 100,
|
|
"imageUrl": "generated/edit.png"
|
|
}
|
|
}
|
|
}
|
|
})
|
|
);
|
|
let mut adapter = GrokStreamAdapter::default();
|
|
adapter.push_chunk(line.as_bytes());
|
|
|
|
assert_eq!(
|
|
adapter.images,
|
|
vec!["https://assets.grok.com/generated/edit.png"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn adapter_extracts_grok_image_edit_model_response_fallbacks() {
|
|
let line = format!(
|
|
"data: {}\n",
|
|
serde_json::json!({
|
|
"result": {
|
|
"response": {
|
|
"modelResponse": {
|
|
"generatedImageUrls": ["/generated/a.png"],
|
|
"fileAttachments": ["asset-123"]
|
|
}
|
|
}
|
|
}
|
|
})
|
|
);
|
|
let mut adapter = GrokStreamAdapter::default();
|
|
adapter.push_chunk(line.as_bytes());
|
|
|
|
assert_eq!(
|
|
adapter.images,
|
|
vec![
|
|
"https://assets.grok.com/generated/a.png",
|
|
"https://assets.grok.com/asset-123/content"
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn grok_image_edit_config_sets_references_and_parent_post() {
|
|
let mut body = serde_json::json!({
|
|
"responseMetadata": {
|
|
"modelConfigOverride": {
|
|
"modelMap": {
|
|
"imageEditModelConfig": {
|
|
"imageReferences": [],
|
|
"parentPostId": ""
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
set_grok_image_edit_config(
|
|
&mut body,
|
|
vec![serde_json::json!("https://assets.grok.com/ref.png")],
|
|
"post-1".to_string(),
|
|
)
|
|
.expect("config should update");
|
|
|
|
assert_eq!(
|
|
body["responseMetadata"]["modelConfigOverride"]["modelMap"]["imageEditModelConfig"]
|
|
["imageReferences"][0],
|
|
serde_json::json!("https://assets.grok.com/ref.png")
|
|
);
|
|
assert_eq!(
|
|
body["responseMetadata"]["modelConfigOverride"]["modelMap"]["imageEditModelConfig"]
|
|
["parentPostId"],
|
|
serde_json::json!("post-1")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn grok_image_edit_helpers_resolve_urls_and_cookie_user_id() {
|
|
assert_eq!(
|
|
grok_upload_url("https://grok.com/rest/app-chat/conversations/new"),
|
|
"https://grok.com/rest/app-chat/upload-file"
|
|
);
|
|
assert_eq!(
|
|
grok_media_post_url("https://grok.com/rest/app-chat/conversations/new"),
|
|
"https://grok.com/rest/media/post/create"
|
|
);
|
|
assert_eq!(
|
|
grok_asset_url("/users/u/file/content"),
|
|
"https://assets.grok.com/users/u/file/content"
|
|
);
|
|
|
|
let headers = BTreeMap::from([(
|
|
"cookie".to_string(),
|
|
"sso=abc; x-userid=user-1; cf_clearance=ok".to_string(),
|
|
)]);
|
|
assert_eq!(
|
|
grok_user_id_from_cookie_header(&headers),
|
|
Some("user-1".to_string())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn grok_imagine_request_message_matches_websocket_protocol() {
|
|
let reset = grok_imagine_reset_message();
|
|
assert_eq!(reset["type"], serde_json::json!("conversation.item.create"));
|
|
assert_eq!(
|
|
reset["item"]["content"][0]["type"],
|
|
serde_json::json!("reset")
|
|
);
|
|
|
|
let request = grok_imagine_request_message("a red chair", "16:9", true);
|
|
assert_eq!(
|
|
request["item"]["content"][0]["requestId"]
|
|
.as_str()
|
|
.is_some(),
|
|
true
|
|
);
|
|
assert_eq!(
|
|
request["item"]["content"][0]["text"],
|
|
serde_json::json!("a red chair")
|
|
);
|
|
assert_eq!(
|
|
request["item"]["content"][0]["properties"]["aspect_ratio"],
|
|
serde_json::json!("16:9")
|
|
);
|
|
assert_eq!(
|
|
request["item"]["content"][0]["properties"]["enable_pro"],
|
|
serde_json::json!(true)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn grok_imagine_ws_parser_collects_completed_image() {
|
|
let mut slots = BTreeMap::<String, GrokImagineImage>::new();
|
|
grok_handle_imagine_ws_message(
|
|
&serde_json::json!({
|
|
"type": "json",
|
|
"current_status": "start_stage",
|
|
"image_id": "abc",
|
|
"order": 1
|
|
}),
|
|
&mut slots,
|
|
)
|
|
.expect("start stage should parse");
|
|
grok_handle_imagine_ws_message(
|
|
&serde_json::json!({
|
|
"type": "image",
|
|
"url": "/images/abc.png",
|
|
"blob": "aW1hZ2U="
|
|
}),
|
|
&mut slots,
|
|
)
|
|
.expect("image frame should parse");
|
|
grok_handle_imagine_ws_message(
|
|
&serde_json::json!({
|
|
"type": "json",
|
|
"current_status": "completed",
|
|
"image_id": "abc",
|
|
"order": 1,
|
|
"moderated": false
|
|
}),
|
|
&mut slots,
|
|
)
|
|
.expect("completed frame should parse");
|
|
|
|
let image = slots.get("abc").expect("slot should exist");
|
|
assert_eq!(
|
|
image.url.as_deref(),
|
|
Some("https://assets.grok.com/images/abc.png")
|
|
);
|
|
assert_eq!(image.blob_b64.as_deref(), Some("aW1hZ2U="));
|
|
assert!(image.done);
|
|
assert!(!image.moderated);
|
|
}
|
|
|
|
#[test]
|
|
fn grok_imagine_helpers_extract_prompt_count_and_route() {
|
|
let body = serde_json::json!({
|
|
"input": [{
|
|
"role": "user",
|
|
"content": [{"type": "input_text", "text": "a chair"}]
|
|
}],
|
|
"tools": [{"size": "1280x720"}],
|
|
"n": 3
|
|
});
|
|
assert_eq!(
|
|
grok_image_prompt_from_provider_body(&body),
|
|
Some("a chair".to_string())
|
|
);
|
|
assert_eq!(grok_image_count_from_provider_body(&body), 3);
|
|
assert_eq!(grok_aspect_ratio_from_provider_body(&body), "16:9");
|
|
|
|
let plan = sample_plan(body, "openai:image");
|
|
assert!(grok_should_use_imagine_websocket(
|
|
&plan,
|
|
Some(&report_context_with_mapped_model("grok-imagine-image-pro"))
|
|
)
|
|
.expect("route should resolve"));
|
|
assert!(!grok_should_use_imagine_websocket(
|
|
&plan,
|
|
Some(&report_context_with_mapped_model("grok-imagine-image-lite"))
|
|
)
|
|
.expect("route should resolve"));
|
|
}
|
|
|
|
#[test]
|
|
fn grok_attachment_public_ip_guard_rejects_private_ranges() {
|
|
for ip in [
|
|
"127.0.0.1",
|
|
"10.0.0.1",
|
|
"172.16.0.1",
|
|
"192.168.1.1",
|
|
"169.254.169.254",
|
|
"::1",
|
|
"fc00::1",
|
|
"fe80::1",
|
|
] {
|
|
assert!(
|
|
!grok_attachment_ip_is_public(ip.parse().expect("ip should parse")),
|
|
"{ip} should be rejected"
|
|
);
|
|
}
|
|
assert!(grok_attachment_ip_is_public(
|
|
"8.8.8.8".parse().expect("ip should parse")
|
|
));
|
|
assert!(grok_attachment_ip_is_public(
|
|
"2606:4700:4700::1111".parse().expect("ip should parse")
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn adapter_cleans_inline_citation_render_tags() {
|
|
let card_json = serde_json::json!({
|
|
"id": "803514",
|
|
"url": "https://example.com/source",
|
|
"title": "Example Source"
|
|
})
|
|
.to_string();
|
|
let line = format!(
|
|
"data: {}\n",
|
|
serde_json::json!({
|
|
"result": {
|
|
"response": {
|
|
"messageTag": "final",
|
|
"cardAttachment": {"jsonData": card_json},
|
|
"token": "answer<grok:render card_id=\"803514\" card_type=\"citation_card\" type=\"render_inline_citation\"><argument name=\"citation_id\">5</argument></grok:render>"
|
|
}
|
|
}
|
|
})
|
|
);
|
|
let mut adapter = GrokStreamAdapter::default();
|
|
adapter.push_chunk(line.as_bytes());
|
|
|
|
assert!(!adapter.text.contains("<grok:render"));
|
|
assert!(adapter.text.contains("[[1]](https://example.com/source"));
|
|
}
|
|
|
|
#[test]
|
|
fn openai_chat_body_includes_estimated_usage() {
|
|
let plan = sample_plan(
|
|
serde_json::json!({
|
|
"messages": [{"role": "user", "content": "hello"}]
|
|
}),
|
|
"openai:chat",
|
|
);
|
|
let collected = GrokCollected {
|
|
text: "hello back".to_string(),
|
|
thinking: "thinking".to_string(),
|
|
..GrokCollected::default()
|
|
};
|
|
let usage = grok_usage_estimate(&plan, &collected);
|
|
let body = openai_chat_body("grok-test", &collected, usage);
|
|
|
|
assert!(body["usage"]["prompt_tokens"].as_u64().unwrap_or_default() > 0);
|
|
assert!(
|
|
body["usage"]["completion_tokens"]
|
|
.as_u64()
|
|
.unwrap_or_default()
|
|
> 0
|
|
);
|
|
assert_eq!(
|
|
body["usage"]["completion_tokens_details"]["reasoning_tokens"],
|
|
serde_json::json!(usage.reasoning_tokens)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn openai_responses_body_includes_estimated_usage() {
|
|
let plan = sample_plan(serde_json::json!({"input": "hello"}), "openai:responses");
|
|
let collected = GrokCollected {
|
|
text: "hello back".to_string(),
|
|
thinking: "short reasoning".to_string(),
|
|
..GrokCollected::default()
|
|
};
|
|
let usage = grok_usage_estimate(&plan, &collected);
|
|
let body = openai_responses_body("grok-test", &collected, usage, false);
|
|
|
|
assert_eq!(
|
|
body["usage"]["input_tokens"],
|
|
serde_json::json!(usage.input_tokens)
|
|
);
|
|
assert_eq!(
|
|
body["usage"]["output_tokens"],
|
|
serde_json::json!(usage.output_tokens)
|
|
);
|
|
assert_eq!(
|
|
body["usage"]["output_tokens_details"]["reasoning_tokens"],
|
|
serde_json::json!(usage.reasoning_tokens)
|
|
);
|
|
assert_eq!(body["output"][0]["type"], serde_json::json!("reasoning"));
|
|
assert_eq!(body["output"][1]["type"], serde_json::json!("message"));
|
|
}
|
|
|
|
#[test]
|
|
fn openai_responses_body_emits_structured_image_generation_calls() {
|
|
let plan = sample_plan(serde_json::json!({"input": "draw"}), "openai:responses");
|
|
let collected = GrokCollected {
|
|
text: "done".to_string(),
|
|
images: vec!["data:image/png;base64,aW1hZ2U=".to_string()],
|
|
..GrokCollected::default()
|
|
};
|
|
let usage = grok_usage_estimate(&plan, &collected);
|
|
let body = openai_responses_body("grok-imagine-image-lite", &collected, usage, true);
|
|
|
|
assert_eq!(body["output"][0]["type"], serde_json::json!("message"));
|
|
assert_eq!(
|
|
body["output"][1]["type"],
|
|
serde_json::json!("image_generation_call")
|
|
);
|
|
assert_eq!(body["output"][1]["result"], serde_json::json!("aW1hZ2U="));
|
|
assert_eq!(body["output"][1]["output_format"], serde_json::json!("png"));
|
|
}
|
|
|
|
#[test]
|
|
fn openai_responses_body_preserves_url_images_without_result_field() {
|
|
let plan = sample_plan(serde_json::json!({"input": "draw"}), "openai:responses");
|
|
let collected = GrokCollected {
|
|
images: vec!["https://assets.grok.com/generated/example.png".to_string()],
|
|
..GrokCollected::default()
|
|
};
|
|
let usage = grok_usage_estimate(&plan, &collected);
|
|
let body = openai_responses_body("grok-imagine-image-lite", &collected, usage, true);
|
|
|
|
assert_eq!(
|
|
body["output"][0]["type"],
|
|
serde_json::json!("image_generation_call")
|
|
);
|
|
assert_eq!(
|
|
body["output"][0]["url"],
|
|
serde_json::json!("https://assets.grok.com/generated/example.png")
|
|
);
|
|
assert!(body["output"][0].get("result").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn openai_responses_body_preserves_text_whitespace() {
|
|
let plan = sample_plan(serde_json::json!({"input": "hello"}), "openai:responses");
|
|
let collected = GrokCollected {
|
|
text: "\n hello back \n".to_string(),
|
|
..GrokCollected::default()
|
|
};
|
|
let usage = grok_usage_estimate(&plan, &collected);
|
|
let body = openai_responses_body("grok-test", &collected, usage, false);
|
|
|
|
assert_eq!(
|
|
body["output"][0]["content"][0]["text"],
|
|
serde_json::json!("\n hello back \n")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn openai_responses_body_keeps_non_image_intent_images_as_text() {
|
|
let plan = sample_plan(
|
|
serde_json::json!({"input": "show source"}),
|
|
"openai:responses",
|
|
);
|
|
let collected = GrokCollected {
|
|
text: "source".to_string(),
|
|
images: vec!["https://assets.grok.com/generated/example.png".to_string()],
|
|
..GrokCollected::default()
|
|
};
|
|
let usage = grok_usage_estimate(&plan, &collected);
|
|
let body = openai_responses_body("grok-test", &collected, usage, false);
|
|
|
|
assert_eq!(body["output"][0]["type"], serde_json::json!("message"));
|
|
assert_eq!(
|
|
body["output"][0]["content"][0]["text"],
|
|
serde_json::json!("source\n\nhttps://assets.grok.com/generated/example.png")
|
|
);
|
|
assert_eq!(body["output"].as_array().map(Vec::len), Some(1));
|
|
}
|
|
|
|
#[test]
|
|
fn openai_chat_json_body_uses_standard_responses_conversion() {
|
|
let plan = sample_plan(
|
|
serde_json::json!({
|
|
"messages": [{"role": "user", "content": "hello"}]
|
|
}),
|
|
"openai:chat",
|
|
);
|
|
let collected = GrokCollected {
|
|
status_code: 200,
|
|
text: "hello back".to_string(),
|
|
thinking: "Thinking about your request".to_string(),
|
|
..GrokCollected::default()
|
|
};
|
|
let body = grok_client_json_body(&plan, &collected, None);
|
|
|
|
assert_eq!(body["object"], serde_json::json!("chat.completion"));
|
|
assert_eq!(
|
|
body["choices"][0]["message"]["content"],
|
|
serde_json::json!("hello back")
|
|
);
|
|
assert_eq!(
|
|
body["usage"]["completion_tokens_details"]["reasoning_tokens"],
|
|
serde_json::json!(grok_usage_estimate(&plan, &collected).reasoning_tokens)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn openai_chat_json_body_converts_grok_images_through_standard_matrix() {
|
|
let plan = sample_plan(
|
|
serde_json::json!({
|
|
"messages": [{"role": "user", "content": "draw"}]
|
|
}),
|
|
"openai:chat",
|
|
);
|
|
let mut plan = plan;
|
|
plan.model_name = Some("grok-imagine-image-lite".to_string());
|
|
let collected = GrokCollected {
|
|
status_code: 200,
|
|
images: vec!["data:image/png;base64,aW1hZ2U=".to_string()],
|
|
..GrokCollected::default()
|
|
};
|
|
let body = grok_client_json_body(&plan, &collected, None);
|
|
|
|
assert_eq!(body["object"], serde_json::json!("chat.completion"));
|
|
assert_eq!(
|
|
body["choices"][0]["message"]["content"][0]["type"],
|
|
serde_json::json!("image_url")
|
|
);
|
|
assert_eq!(
|
|
body["choices"][0]["message"]["content"][0]["image_url"]["url"],
|
|
serde_json::json!("data:image/png;base64,aW1hZ2U=")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn openai_chat_json_body_uses_mapped_image_model_for_alias() {
|
|
let mut plan = sample_plan(
|
|
serde_json::json!({
|
|
"messages": [{"role": "user", "content": "draw"}]
|
|
}),
|
|
"openai:chat",
|
|
);
|
|
plan.model_name = Some("custom-image-alias".to_string());
|
|
let report_context = report_context_with_mapped_model("grok-imagine-image-lite");
|
|
let collected = GrokCollected {
|
|
status_code: 200,
|
|
images: vec!["data:image/png;base64,aW1hZ2U=".to_string()],
|
|
..GrokCollected::default()
|
|
};
|
|
let body = grok_client_json_body(&plan, &collected, Some(&report_context));
|
|
|
|
assert_eq!(body["object"], serde_json::json!("chat.completion"));
|
|
assert_eq!(
|
|
body["choices"][0]["message"]["content"][0]["type"],
|
|
serde_json::json!("image_url")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn openai_responses_stream_uses_aether_standard_emitter() {
|
|
let plan = sample_plan(
|
|
serde_json::json!({"input": "hello", "stream": true}),
|
|
"openai:responses",
|
|
);
|
|
let collected = GrokCollected {
|
|
status_code: 200,
|
|
text: "hello back".to_string(),
|
|
thinking: "Thinking about your request".to_string(),
|
|
..GrokCollected::default()
|
|
};
|
|
let body = grok_client_stream_body(&plan, &collected, None);
|
|
|
|
assert!(body.contains("event: response.created"));
|
|
assert!(body.contains("event: response.in_progress"));
|
|
assert!(body.contains("event: response.reasoning_summary_part.added"));
|
|
assert!(body.contains("event: response.content_part.added"));
|
|
assert!(body.contains("event: response.output_text.done"));
|
|
assert!(body.contains("event: response.completed"));
|
|
assert!(body.contains("\"sequence_number\""));
|
|
assert!(!body.contains("chat.completion.chunk"));
|
|
}
|
|
|
|
#[test]
|
|
fn openai_responses_stream_preserves_image_generation_calls() {
|
|
let plan = sample_plan(
|
|
serde_json::json!({
|
|
"input": "draw",
|
|
"stream": true,
|
|
"tools": [{"type": "image_generation"}]
|
|
}),
|
|
"openai:responses",
|
|
);
|
|
let collected = GrokCollected {
|
|
status_code: 200,
|
|
images: vec!["data:image/png;base64,aGVsbG8=".to_string()],
|
|
..GrokCollected::default()
|
|
};
|
|
let body = grok_client_stream_body(&plan, &collected, None);
|
|
|
|
assert!(body.contains("event: response.output_item.done"));
|
|
assert!(body.contains("\"type\":\"image_generation_call\""));
|
|
assert!(body.contains("\"result\":\"aGVsbG8=\""));
|
|
assert!(body.contains("event: response.completed"));
|
|
}
|
|
|
|
#[test]
|
|
fn openai_responses_stream_uses_mapped_image_model_for_alias() {
|
|
let mut plan = sample_plan(
|
|
serde_json::json!({
|
|
"input": "draw",
|
|
"stream": true
|
|
}),
|
|
"openai:responses",
|
|
);
|
|
plan.model_name = Some("custom-image-alias".to_string());
|
|
let report_context = report_context_with_mapped_model("grok-imagine-image-lite");
|
|
let collected = GrokCollected {
|
|
status_code: 200,
|
|
images: vec!["data:image/png;base64,aGVsbG8=".to_string()],
|
|
..GrokCollected::default()
|
|
};
|
|
let body = grok_client_stream_body(&plan, &collected, Some(&report_context));
|
|
|
|
assert!(grok_plan_uses_structured_image_generation(
|
|
&plan,
|
|
Some(&report_context)
|
|
));
|
|
assert!(body.contains("event: response.output_item.done"));
|
|
assert!(body.contains("\"type\":\"image_generation_call\""));
|
|
assert!(body.contains("\"result\":\"aGVsbG8=\""));
|
|
}
|
|
|
|
#[test]
|
|
fn grok_lite_image_responses_stream_collects_before_client_bridge() {
|
|
let mut plan = sample_plan(
|
|
serde_json::json!({
|
|
"input": "draw",
|
|
"stream": true,
|
|
"tools": [{"type": "image_generation"}]
|
|
}),
|
|
"openai:responses",
|
|
);
|
|
plan.model_name = Some("grok-imagine-image-lite".to_string());
|
|
|
|
assert!(grok_plan_uses_structured_image_generation(&plan, None));
|
|
assert!(grok_should_collect_image_stream(
|
|
&plan,
|
|
Some(&report_context_with_mapped_model("grok-imagine-image-lite"))
|
|
)
|
|
.expect("collect decision should succeed"));
|
|
}
|
|
|
|
#[test]
|
|
fn grok_lite_alias_responses_stream_collects_from_mapped_model() {
|
|
let mut plan = sample_plan(
|
|
serde_json::json!({
|
|
"input": "draw",
|
|
"stream": true
|
|
}),
|
|
"openai:responses",
|
|
);
|
|
plan.model_name = Some("custom-image-alias".to_string());
|
|
|
|
assert!(!grok_plan_uses_structured_image_generation(&plan, None));
|
|
assert!(grok_should_collect_image_stream(
|
|
&plan,
|
|
Some(&report_context_with_mapped_model("grok-imagine-image-lite"))
|
|
)
|
|
.expect("collect decision should succeed"));
|
|
}
|
|
|
|
#[test]
|
|
fn claude_messages_stream_uses_claude_event_shape() {
|
|
let plan = sample_plan(
|
|
serde_json::json!({"messages": [{"role": "user", "content": "hello"}]}),
|
|
"claude:messages",
|
|
);
|
|
let collected = GrokCollected {
|
|
status_code: 200,
|
|
text: "hello back".to_string(),
|
|
thinking: "Thinking about your request".to_string(),
|
|
..GrokCollected::default()
|
|
};
|
|
let body = grok_client_stream_body(&plan, &collected, None);
|
|
|
|
assert!(body.contains("event: message_start"));
|
|
assert!(body.contains("\"type\":\"message_start\""));
|
|
assert!(body.contains("\"type\":\"thinking_delta\""));
|
|
assert!(body.contains("\"type\":\"text_delta\""));
|
|
assert!(body.contains("event: message_delta"));
|
|
assert!(body.contains("event: message_stop"));
|
|
assert!(!body.contains("chat.completion.chunk"));
|
|
}
|
|
|
|
#[test]
|
|
fn extracts_openai_chat_image_and_file_attachment_inputs() {
|
|
let inputs = extract_grok_attachment_inputs(
|
|
"openai:chat",
|
|
&serde_json::json!({
|
|
"messages": [{
|
|
"role": "user",
|
|
"content": [
|
|
{"type": "text", "text": "describe these"},
|
|
{"type": "image_url", "image_url": {"url": "data:image/png;base64,aGVsbG8="}},
|
|
{"type": "file", "file": {"filename": "notes.txt", "file_data": "data:text/plain;base64,bm90ZXM="}}
|
|
]
|
|
}]
|
|
}),
|
|
);
|
|
|
|
assert_eq!(inputs.len(), 2);
|
|
assert_eq!(inputs[0].source.as_str(), "data:image/png;base64,aGVsbG8=");
|
|
assert_eq!(inputs[1].filename.as_deref(), Some("notes.txt"));
|
|
assert_eq!(inputs[1].source.as_str(), "data:text/plain;base64,bm90ZXM=");
|
|
}
|
|
|
|
#[test]
|
|
fn grok_data_uri_attachment_accepts_content_above_previous_size_cap() {
|
|
const PREVIOUS_CAP_BYTES: usize = 25 * 1024 * 1024;
|
|
let base64_blocks = PREVIOUS_CAP_BYTES / 3 + 1;
|
|
let mut source = String::from("data:application/octet-stream;base64,");
|
|
source.extend(std::iter::repeat_n('A', base64_blocks * 4));
|
|
let input = GrokAttachmentInput {
|
|
source,
|
|
filename: Some("large.bin".to_string()),
|
|
mime_type: None,
|
|
};
|
|
|
|
let payload = grok_attachment_payload_from_data_uri(&input, 0)
|
|
.expect("attachment above the previous size cap should be accepted");
|
|
|
|
assert_eq!(payload.filename, "large.bin");
|
|
assert_eq!(payload.mime_type, "application/octet-stream");
|
|
assert_eq!(payload.content_b64.len(), base64_blocks * 4);
|
|
}
|
|
|
|
#[test]
|
|
fn extracts_responses_and_claude_attachment_inputs() {
|
|
let responses = extract_grok_attachment_inputs(
|
|
"openai:responses",
|
|
&serde_json::json!({
|
|
"input": [{
|
|
"type": "message",
|
|
"role": "user",
|
|
"content": [
|
|
{"type": "input_text", "text": "read it"},
|
|
{"type": "input_image", "image_url": "https://example.com/a.png"},
|
|
{"type": "input_file", "filename": "doc.pdf", "file_data": "data:application/pdf;base64,JVBERi0="}
|
|
]
|
|
}]
|
|
}),
|
|
);
|
|
let claude = extract_grok_attachment_inputs(
|
|
"claude:messages",
|
|
&serde_json::json!({
|
|
"messages": [{
|
|
"role": "user",
|
|
"content": [
|
|
{"type": "text", "text": "read"},
|
|
{"type": "image", "source": {"type": "url", "url": "https://example.com/b.png"}},
|
|
{"type": "document", "filename": "memo.pdf", "source": {"type": "base64", "media_type": "application/pdf", "data": "JVBERi0="}}
|
|
]
|
|
}]
|
|
}),
|
|
);
|
|
|
|
assert_eq!(responses.len(), 2);
|
|
assert_eq!(responses[0].source.as_str(), "https://example.com/a.png");
|
|
assert_eq!(responses[1].filename.as_deref(), Some("doc.pdf"));
|
|
assert_eq!(claude.len(), 2);
|
|
assert_eq!(claude[0].source.as_str(), "https://example.com/b.png");
|
|
assert_eq!(claude[1].filename.as_deref(), Some("memo.pdf"));
|
|
assert_eq!(
|
|
claude[1].source.as_str(),
|
|
"data:application/pdf;base64,JVBERi0="
|
|
);
|
|
}
|
|
|
|
fn response(
|
|
status: StatusCode,
|
|
content_type: &'static str,
|
|
body: impl Into<Body>,
|
|
) -> http::Response<Body> {
|
|
http::Response::builder()
|
|
.status(status)
|
|
.header("content-type", content_type)
|
|
.body(body.into())
|
|
.expect("response should build")
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn grok_openai_image_sync_materializes_public_asset_urls_for_preview() {
|
|
let listener = crate::test_support::bind_loopback_listener()
|
|
.await
|
|
.expect("listener should bind");
|
|
let addr = listener.local_addr().expect("local addr should resolve");
|
|
let app = Router::new().fallback(any(|request: Request| async move {
|
|
let path = request.uri().path().to_string();
|
|
let method = request.method().clone();
|
|
match (method, path.as_str()) {
|
|
(Method::GET, "/generated.png") => response(
|
|
StatusCode::OK,
|
|
"image/png",
|
|
Body::from(vec![0x89, b'P', b'N', b'G']),
|
|
),
|
|
_ => response(StatusCode::NOT_FOUND, "text/plain", "not found"),
|
|
}
|
|
}));
|
|
let server = tokio::spawn(async move {
|
|
axum::serve(listener, app)
|
|
.await
|
|
.expect("mock server should run");
|
|
});
|
|
|
|
let plan = sample_plan(
|
|
serde_json::json!({"model": "grok-imagine-image-lite"}),
|
|
"openai:image",
|
|
);
|
|
let mut collected = GrokCollected {
|
|
status_code: 200,
|
|
images: vec![format!("http://{addr}/generated.png")],
|
|
..GrokCollected::default()
|
|
};
|
|
|
|
materialize_grok_image_assets(&plan, &mut collected).await;
|
|
|
|
server.abort();
|
|
|
|
assert!(collected.images[0].starts_with("data:image/png;base64,"));
|
|
let body = openai_image_body(&collected);
|
|
assert_eq!(body["data"][0]["b64_json"].as_str().is_some(), true);
|
|
assert!(body["data"][0].get("url").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn grok_openai_image_body_preserves_plain_urls_when_asset_is_not_materialized() {
|
|
let body = openai_image_body(&GrokCollected {
|
|
status_code: 200,
|
|
images: vec!["https://assets.grok.com/generated/example.png".to_string()],
|
|
..GrokCollected::default()
|
|
});
|
|
|
|
assert_eq!(
|
|
body["data"][0]["url"].as_str(),
|
|
Some("https://assets.grok.com/generated/example.png")
|
|
);
|
|
assert!(body["data"][0].get("b64_json").is_none());
|
|
}
|
|
}
|