mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-09 04:30:20 +08:00
Merge pull request #635 from zhefox/main
fix(gateway): 支持 OpenAI 图片编辑端点请求
This commit is contained in:
@@ -213,6 +213,11 @@ pub(super) async fn resolve_local_openai_image_candidate_payload_parts(
|
||||
headers: effective_headers,
|
||||
auth_header: &auth_header,
|
||||
auth_value: &auth_value,
|
||||
accept: if is_codex || is_chatgpt_web {
|
||||
"text/event-stream"
|
||||
} else {
|
||||
"application/json"
|
||||
},
|
||||
header_rules: transport.endpoint.header_rules.as_ref(),
|
||||
provider_request_body: &provider_request_body,
|
||||
original_request_body: body_json,
|
||||
|
||||
@@ -1230,6 +1230,7 @@ async fn resolve_local_gemini_image_to_openai_image_candidate_payload_parts(
|
||||
headers: effective_headers,
|
||||
auth_header: &prepared_candidate.auth_header,
|
||||
auth_value: &prepared_candidate.auth_value,
|
||||
accept: "text/event-stream",
|
||||
header_rules: transport.endpoint.header_rules.as_ref(),
|
||||
provider_request_body: &converted.body_json,
|
||||
original_request_body: body_json,
|
||||
|
||||
@@ -1056,6 +1056,7 @@ async fn resolve_openai_chat_to_openai_image_payload_parts(
|
||||
headers: &parts.headers,
|
||||
auth_header: &prepared_candidate.auth_header,
|
||||
auth_value: &prepared_candidate.auth_value,
|
||||
accept: "text/event-stream",
|
||||
header_rules: transport.endpoint.header_rules.as_ref(),
|
||||
provider_request_body: &provider_request_body,
|
||||
original_request_body: body_json,
|
||||
|
||||
+1
@@ -1123,6 +1123,7 @@ async fn resolve_openai_responses_to_openai_image_payload_parts(
|
||||
headers: &parts.headers,
|
||||
auth_header: &prepared_candidate.auth_header,
|
||||
auth_value: &prepared_candidate.auth_value,
|
||||
accept: "text/event-stream",
|
||||
header_rules: transport.endpoint.header_rules.as_ref(),
|
||||
provider_request_body: &provider_request_body,
|
||||
original_request_body: body_json,
|
||||
|
||||
@@ -187,6 +187,7 @@ async fn execute_chatgpt_web_image(
|
||||
request_id = %plan.request_id,
|
||||
candidate_id = ?plan.candidate_id,
|
||||
base_url = %base_url,
|
||||
operation = %request.operation,
|
||||
image_count = request.images.len(),
|
||||
size = %request.size,
|
||||
ratio = %request.ratio,
|
||||
@@ -311,6 +312,7 @@ async fn execute_chatgpt_web_image(
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ChatGptWebImageRequest {
|
||||
operation: String,
|
||||
model: String,
|
||||
web_model: String,
|
||||
prompt: String,
|
||||
@@ -342,6 +344,7 @@ impl ChatGptWebImageRequest {
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<Vec<_>>();
|
||||
Ok(Self {
|
||||
operation: chatgpt_web_image_operation(body.get("operation")),
|
||||
model: text("model").unwrap_or_else(|| "gpt-image-2".to_string()),
|
||||
web_model: text("web_model").unwrap_or_else(|| "gpt-5-5-thinking".to_string()),
|
||||
prompt: text("prompt").unwrap_or_else(|| "Generate a high quality image.".to_string()),
|
||||
@@ -2434,10 +2437,23 @@ fn json_u64(value: Option<&Value>) -> Option<u64> {
|
||||
})
|
||||
}
|
||||
|
||||
fn chatgpt_web_image_operation(value: Option<&Value>) -> String {
|
||||
value
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.map(str::to_ascii_lowercase)
|
||||
.filter(|value| matches!(value.as_str(), "generate" | "edit"))
|
||||
.unwrap_or_else(|| "generate".to_string())
|
||||
}
|
||||
|
||||
fn build_failed_sse(request: &ChatGptWebImageRequest, failure: &Value) -> String {
|
||||
let failed = if failure.get("type").and_then(Value::as_str) == Some("response.failed") {
|
||||
failure.clone()
|
||||
} else {
|
||||
let operation = match request.operation.as_str() {
|
||||
"edit" => "edit",
|
||||
_ => "generation",
|
||||
};
|
||||
json!({
|
||||
"type": "response.failed",
|
||||
"response": {
|
||||
@@ -2445,7 +2461,7 @@ fn build_failed_sse(request: &ChatGptWebImageRequest, failure: &Value) -> String
|
||||
"model": request.model,
|
||||
"error": failure.get("error").cloned().unwrap_or_else(|| json!({
|
||||
"code": "chatgpt_web_image_failed",
|
||||
"message": "ChatGPT-Web image generation failed"
|
||||
"message": format!("ChatGPT-Web image {operation} failed")
|
||||
}))
|
||||
}
|
||||
})
|
||||
@@ -2641,7 +2657,7 @@ fn chatgpt_web_image_request_context(plan: &ExecutionPlan) -> Option<Value> {
|
||||
let mut image_request = Map::new();
|
||||
image_request.insert(
|
||||
"operation".to_string(),
|
||||
Value::String("generate".to_string()),
|
||||
Value::String(chatgpt_web_image_operation(body.get("operation"))),
|
||||
);
|
||||
for key in [
|
||||
"model",
|
||||
@@ -3223,6 +3239,7 @@ mod tests {
|
||||
#[test]
|
||||
fn chatgpt_web_success_sse_includes_estimated_image_usage() {
|
||||
let request = ChatGptWebImageRequest {
|
||||
operation: "generate".to_string(),
|
||||
model: "gpt-image-2".to_string(),
|
||||
web_model: "gpt-5-5-thinking".to_string(),
|
||||
prompt: "draw a test image".to_string(),
|
||||
@@ -3276,6 +3293,7 @@ mod tests {
|
||||
#[test]
|
||||
fn chatgpt_web_success_sse_uses_image_dimensions_not_output_text() {
|
||||
let request = ChatGptWebImageRequest {
|
||||
operation: "generate".to_string(),
|
||||
model: "gpt-image-2".to_string(),
|
||||
web_model: "gpt-5-5-thinking".to_string(),
|
||||
prompt: "draw a test image".to_string(),
|
||||
@@ -3334,6 +3352,32 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chatgpt_web_image_request_context_preserves_edit_operation() {
|
||||
let plan = sample_plan(
|
||||
CHATGPT_WEB_DEFAULT_BASE_URL,
|
||||
json!({
|
||||
"operation": "edit",
|
||||
"model": "gpt-image-2",
|
||||
"web_model": "gpt-5-5-thinking",
|
||||
"prompt": "adjust this image",
|
||||
"size": "512x512",
|
||||
"ratio": "1:1",
|
||||
"images": ["data:image/png;base64,aW1hZ2U="],
|
||||
"count": 1,
|
||||
"output_format": "png"
|
||||
}),
|
||||
true,
|
||||
);
|
||||
|
||||
let context = chatgpt_web_stream_observer_context(&plan, None);
|
||||
|
||||
assert_eq!(context["image_request"]["operation"], json!("edit"));
|
||||
assert_eq!(context["image_request"]["model"], json!("gpt-image-2"));
|
||||
assert_eq!(context["image_request"]["size"], json!("512x512"));
|
||||
assert_eq!(context["provider_api_format"], json!("openai:image"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chatgpt_web_image_quota_refresh_plan_uses_conversation_init() {
|
||||
let plan = sample_plan(
|
||||
|
||||
@@ -2183,6 +2183,11 @@ async fn provider_query_execute_openai_image_test_candidate(
|
||||
headers: &parts.headers,
|
||||
auth_header: &auth_header,
|
||||
auth_value: &auth_value,
|
||||
accept: if is_codex || is_chatgpt_web {
|
||||
"text/event-stream"
|
||||
} else {
|
||||
"application/json"
|
||||
},
|
||||
header_rules: transport.endpoint.header_rules.as_ref(),
|
||||
provider_request_body: &provider_request_body,
|
||||
original_request_body: &request_body,
|
||||
|
||||
@@ -1556,6 +1556,7 @@ impl OpenAIResponsesProviderState {
|
||||
});
|
||||
self.finished = true;
|
||||
}
|
||||
"keepalive" => {}
|
||||
event_type if openai_responses_stream_event_is_known_noop(event_type) => {
|
||||
self.ensure_started(report_context, &mut out);
|
||||
}
|
||||
|
||||
@@ -1577,6 +1577,47 @@ mod tests {
|
||||
assert!(provider_request_body.get("tools").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_image_api_provider_edit_request_body_keeps_images_edit_shape() {
|
||||
let parts = request_parts("/v1/images/edits", Some("application/json"));
|
||||
let request = normalize_openai_image_request(
|
||||
&parts,
|
||||
&json!({
|
||||
"model": "gpt-image-2",
|
||||
"prompt": "replace the background",
|
||||
"image": "data:image/png;base64,aW1hZ2U=",
|
||||
"mask": "data:image/png;base64,bWFzaw==",
|
||||
"input_fidelity": "high",
|
||||
"output_format": "png",
|
||||
"response_format": "url",
|
||||
"user": "user-123"
|
||||
}),
|
||||
None,
|
||||
)
|
||||
.expect("edit request should normalize");
|
||||
|
||||
let provider_request_body =
|
||||
build_openai_image_api_provider_request_body(&request, Some("mapped-edit-model"));
|
||||
|
||||
assert_eq!(provider_request_body["model"], "mapped-edit-model");
|
||||
assert_eq!(provider_request_body["prompt"], "replace the background");
|
||||
assert_eq!(provider_request_body["input_fidelity"], "high");
|
||||
assert_eq!(provider_request_body["output_format"], "png");
|
||||
assert_eq!(provider_request_body["response_format"], "url");
|
||||
assert_eq!(provider_request_body["user"], "user-123");
|
||||
assert_eq!(
|
||||
provider_request_body["image"]["image_url"],
|
||||
"data:image/png;base64,aW1hZ2U="
|
||||
);
|
||||
assert_eq!(
|
||||
provider_request_body["mask"]["image_url"],
|
||||
"data:image/png;base64,bWFzaw=="
|
||||
);
|
||||
assert!(provider_request_body.get("input").is_none());
|
||||
assert!(provider_request_body.get("tools").is_none());
|
||||
assert!(provider_request_body.get("action").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chatgpt_web_accepts_1k_tier_and_1024_size() {
|
||||
let parts = request_parts("/v1/images/generations", Some("application/json"));
|
||||
@@ -1630,6 +1671,40 @@ mod tests {
|
||||
assert_eq!(body["output_format"], "png");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chatgpt_web_accepts_openai_image_edit_requests() {
|
||||
let parts = request_parts("/v1/images/edits", Some("application/json"));
|
||||
let body = build_chatgpt_web_image_request_body(
|
||||
&parts,
|
||||
&json!({
|
||||
"model": "gpt-image-2",
|
||||
"prompt": "make the sky brighter",
|
||||
"image": {
|
||||
"b64_json": "aW1hZ2U=",
|
||||
"mime_type": "image/png"
|
||||
},
|
||||
"size": "1024x1024",
|
||||
"quality": "high",
|
||||
"response_format": "b64_json",
|
||||
"output_format": "png",
|
||||
"user": "user-123"
|
||||
}),
|
||||
None,
|
||||
)
|
||||
.expect("ChatGPT-Web edit request should pass");
|
||||
|
||||
assert_eq!(body["operation"], "edit");
|
||||
assert_eq!(body["model"], "gpt-image-2");
|
||||
assert_eq!(body["prompt"], "make the sky brighter");
|
||||
assert_eq!(body["size"], "1024x1024");
|
||||
assert_eq!(body["quality"], "high");
|
||||
assert_eq!(body["response_format"], "b64_json");
|
||||
assert_eq!(body["output_format"], "png");
|
||||
assert_eq!(body["user"], "user-123");
|
||||
assert_eq!(body["count"], 1);
|
||||
assert_eq!(body["images"], json!(["data:image/png;base64,aW1hZ2U="]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chatgpt_web_rejects_oversized_resolution_or_size() {
|
||||
let parts = request_parts("/v1/images/generations", Some("application/json"));
|
||||
|
||||
@@ -497,7 +497,7 @@ fn ensure_json_object_response_input_mentions_json(
|
||||
0,
|
||||
json!({
|
||||
"type": "message",
|
||||
"role": "system",
|
||||
"role": "developer",
|
||||
"content": [{
|
||||
"type": "input_text",
|
||||
"text": "Respond with JSON.",
|
||||
@@ -1056,7 +1056,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_object_response_injects_json_hint_into_input_when_only_instructions_have_it() {
|
||||
fn json_object_response_injects_json_hint_as_developer_input_when_only_instructions_have_it() {
|
||||
let request = CanonicalRequest {
|
||||
model: "gpt-5.5".to_string(),
|
||||
system: Some("Please answer in JSON.".to_string()),
|
||||
@@ -1082,7 +1082,7 @@ mod tests {
|
||||
assert_eq!(body["instructions"], json!("Please answer in JSON."));
|
||||
let input = body["input"].as_array().expect("input");
|
||||
assert_eq!(input.len(), 2);
|
||||
assert_eq!(input[0]["role"], json!("system"));
|
||||
assert_eq!(input[0]["role"], json!("developer"));
|
||||
assert!(input[0]["content"][0]["text"]
|
||||
.as_str()
|
||||
.expect("hint text")
|
||||
|
||||
@@ -715,6 +715,107 @@ mod tests {
|
||||
assert!(!sse.contains("HelloHello"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_openai_responses_keepalive_events_for_chat_clients() {
|
||||
let report_context = report_context("openai:responses", "openai:chat");
|
||||
let mut matrix = StreamingStandardFormatMatrix::default();
|
||||
let mut output = Vec::new();
|
||||
|
||||
let keepalive = matrix
|
||||
.transform_line(
|
||||
&report_context,
|
||||
data_line(json!({
|
||||
"type": "keepalive",
|
||||
"sequence_number": 1,
|
||||
})),
|
||||
)
|
||||
.expect("keepalive should be ignored");
|
||||
assert!(keepalive.is_empty());
|
||||
|
||||
for line in [
|
||||
data_line(json!({
|
||||
"type": "response.output_text.delta",
|
||||
"response_id": "resp_keepalive_123",
|
||||
"output_index": 0,
|
||||
"content_index": 0,
|
||||
"delta": "pong",
|
||||
})),
|
||||
data_line(json!({
|
||||
"type": "response.completed",
|
||||
"response": {
|
||||
"id": "resp_keepalive_123",
|
||||
"object": "response",
|
||||
"model": "gpt-5.4",
|
||||
"status": "completed",
|
||||
"output": [],
|
||||
},
|
||||
})),
|
||||
] {
|
||||
output.extend(
|
||||
matrix
|
||||
.transform_line(&report_context, line)
|
||||
.expect("keepalive and text should convert"),
|
||||
);
|
||||
}
|
||||
|
||||
let sse = String::from_utf8(output).expect("sse should be utf8");
|
||||
assert!(!sse.contains("Unsupported provider stream event"), "{sse}");
|
||||
assert!(!sse.contains("unsupported_stream_event"), "{sse}");
|
||||
assert!(sse.contains("pong"), "{sse}");
|
||||
assert!(sse.contains("chat.completion.chunk"), "{sse}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_openai_responses_keepalive_events_for_responses_clients() {
|
||||
let mut report_context = report_context("openai:chat", "openai:responses");
|
||||
report_context["provider_stream_event_api_format"] = json!("openai:responses");
|
||||
let mut matrix = StreamingStandardFormatMatrix::default();
|
||||
let mut output = Vec::new();
|
||||
|
||||
let keepalive = matrix
|
||||
.transform_line(
|
||||
&report_context,
|
||||
data_line(json!({
|
||||
"type": "keepalive",
|
||||
"sequence_number": 1,
|
||||
})),
|
||||
)
|
||||
.expect("keepalive should be ignored");
|
||||
assert!(keepalive.is_empty());
|
||||
|
||||
for line in [
|
||||
data_line(json!({
|
||||
"type": "response.output_text.delta",
|
||||
"response_id": "resp_keepalive_456",
|
||||
"output_index": 0,
|
||||
"content_index": 0,
|
||||
"delta": "pong",
|
||||
})),
|
||||
data_line(json!({
|
||||
"type": "response.completed",
|
||||
"response": {
|
||||
"id": "resp_keepalive_456",
|
||||
"object": "response",
|
||||
"model": "gpt-5.4",
|
||||
"status": "completed",
|
||||
"output": [],
|
||||
},
|
||||
})),
|
||||
] {
|
||||
output.extend(
|
||||
matrix
|
||||
.transform_line(&report_context, line)
|
||||
.expect("keepalive and text should convert"),
|
||||
);
|
||||
}
|
||||
|
||||
let sse = String::from_utf8(output).expect("sse should be utf8");
|
||||
assert!(!sse.contains("Unsupported provider stream event"), "{sse}");
|
||||
assert!(!sse.contains("unsupported_stream_event"), "{sse}");
|
||||
assert!(sse.contains("pong"), "{sse}");
|
||||
assert!(sse.contains("event: response.output_text.delta"), "{sse}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transforms_provider_errors_to_claude_error_events() {
|
||||
let cases = [
|
||||
|
||||
@@ -14,6 +14,7 @@ pub struct ProviderOpenAiImageHeadersInput<'a> {
|
||||
pub headers: &'a http::HeaderMap,
|
||||
pub auth_header: &'a str,
|
||||
pub auth_value: &'a str,
|
||||
pub accept: &'a str,
|
||||
pub header_rules: Option<&'a Value>,
|
||||
pub provider_request_body: &'a Value,
|
||||
pub original_request_body: &'a Value,
|
||||
@@ -80,7 +81,7 @@ pub fn build_openai_image_headers(
|
||||
&BTreeMap::new(),
|
||||
);
|
||||
provider_request_headers.insert("content-type".to_string(), "application/json".to_string());
|
||||
provider_request_headers.insert("accept".to_string(), "text/event-stream".to_string());
|
||||
provider_request_headers.insert("accept".to_string(), input.accept.to_string());
|
||||
if !apply_local_header_rules_with_request_headers(
|
||||
&mut provider_request_headers,
|
||||
input.header_rules,
|
||||
@@ -189,6 +190,17 @@ mod tests {
|
||||
assert_eq!(url, "https://api.openai.com/v1/images/generations?trace=1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_openai_image_url_preserves_edit_surface() {
|
||||
let mut transport = sample_transport();
|
||||
transport.provider.provider_type = "openai".to_string();
|
||||
|
||||
let url =
|
||||
build_openai_image_upstream_url(&transport, Some("/v1/images/edits"), Some("trace=1"));
|
||||
|
||||
assert_eq!(url, "https://api.openai.com/v1/images/edits?trace=1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chatgpt_web_is_supported_by_dedicated_openai_image_transport_policy() {
|
||||
let mut transport = sample_transport();
|
||||
@@ -245,6 +257,7 @@ mod tests {
|
||||
headers: &HeaderMap::new(),
|
||||
auth_header: "authorization",
|
||||
auth_value: "Bearer secret",
|
||||
accept: "text/event-stream",
|
||||
header_rules: Some(&json!([
|
||||
{"action":"set","key":"x-image-route","value":"codex"}
|
||||
])),
|
||||
@@ -267,4 +280,45 @@ mod tests {
|
||||
);
|
||||
assert_eq!(headers.get("x-image-route"), Some(&"codex".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_openai_compatible_image_headers_can_request_json() {
|
||||
let headers = build_openai_image_headers(ProviderOpenAiImageHeadersInput {
|
||||
headers: &HeaderMap::new(),
|
||||
auth_header: "authorization",
|
||||
auth_value: "Bearer secret",
|
||||
accept: "application/json",
|
||||
header_rules: None,
|
||||
provider_request_body: &json!({
|
||||
"model": "upstream-image-model",
|
||||
"prompt": "draw a city",
|
||||
}),
|
||||
original_request_body: &json!({"prompt":"draw a city"}),
|
||||
})
|
||||
.expect("headers should build");
|
||||
|
||||
assert_eq!(headers.get("accept"), Some(&"application/json".to_string()));
|
||||
assert_eq!(
|
||||
headers.get("content-type"),
|
||||
Some(&"application/json".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_openai_compatible_image_url_supports_aether_api_root() {
|
||||
let mut transport = sample_transport();
|
||||
transport.provider.provider_type = "custom".to_string();
|
||||
transport.endpoint.base_url = "https://upstream-aether.example/v1".to_string();
|
||||
|
||||
let url = build_openai_image_upstream_url(
|
||||
&transport,
|
||||
Some("/v1/images/generations"),
|
||||
Some("trace=1"),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
url,
|
||||
"https://upstream-aether.example/v1/images/generations?trace=1"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5847,6 +5847,111 @@ mod tests {
|
||||
assert!(body_size.get("provider_over_client").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_image_sync_terminal_usage_captures_request_and_response_bodies() {
|
||||
let plan = ExecutionPlan {
|
||||
request_id: "req-openai-image-sync-body-1".to_string(),
|
||||
candidate_id: Some("cand-openai-image-sync-body-1".to_string()),
|
||||
provider_name: Some("Upstream Aether".to_string()),
|
||||
provider_id: "provider-aether-1".to_string(),
|
||||
endpoint_id: "endpoint-aether-1".to_string(),
|
||||
key_id: "key-aether-1".to_string(),
|
||||
method: "POST".to_string(),
|
||||
url: "https://upstream-aether.example/v1/images/generations".to_string(),
|
||||
headers: BTreeMap::from([("content-type".to_string(), "application/json".to_string())]),
|
||||
content_type: Some("application/json".to_string()),
|
||||
content_encoding: None,
|
||||
body: RequestBody::from_json(json!({
|
||||
"model": "gpt-image-2-upstream",
|
||||
"prompt": "Draw a red kite",
|
||||
"size": "1024x1024",
|
||||
"n": 1,
|
||||
"response_format": "b64_json"
|
||||
})),
|
||||
stream: false,
|
||||
client_api_format: "openai:image".to_string(),
|
||||
provider_api_format: "openai:image".to_string(),
|
||||
model_name: Some("gpt-image-2".to_string()),
|
||||
proxy: None,
|
||||
transport_profile: None,
|
||||
timeouts: None,
|
||||
};
|
||||
let payload = GatewaySyncReportRequest {
|
||||
trace_id: "trace-openai-image-sync-body-1".to_string(),
|
||||
report_kind: "openai_image_sync_finalize".to_string(),
|
||||
report_context: Some(json!({
|
||||
"client_api_format": "openai:image",
|
||||
"provider_api_format": "openai:image",
|
||||
"needs_conversion": true,
|
||||
"original_request_body": {
|
||||
"model": "gpt-image-2",
|
||||
"prompt": "Draw a red kite",
|
||||
"size": "1024x1024",
|
||||
"response_format": "b64_json"
|
||||
},
|
||||
"provider_request_body": {
|
||||
"model": "gpt-image-2-upstream",
|
||||
"prompt": "Draw a red kite",
|
||||
"size": "1024x1024",
|
||||
"n": 1,
|
||||
"response_format": "b64_json"
|
||||
}
|
||||
})),
|
||||
status_code: 200,
|
||||
headers: BTreeMap::from([("content-type".to_string(), "application/json".to_string())]),
|
||||
body_json: Some(json!({
|
||||
"created": 1776839946,
|
||||
"data": [{
|
||||
"b64_json": "aGVsbG8=",
|
||||
"revised_prompt": "red kite"
|
||||
}],
|
||||
"usage": {
|
||||
"input_tokens": 11,
|
||||
"output_tokens": 22,
|
||||
"total_tokens": 33
|
||||
}
|
||||
})),
|
||||
client_body_json: None,
|
||||
body_base64: None,
|
||||
telemetry: None,
|
||||
};
|
||||
|
||||
let event =
|
||||
build_sync_terminal_usage_event(&plan, payload.report_context.as_ref(), &payload)
|
||||
.expect("usage event should build");
|
||||
|
||||
assert_eq!(
|
||||
event.data.request_body,
|
||||
payload
|
||||
.report_context
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("original_request_body"))
|
||||
.cloned()
|
||||
);
|
||||
assert_eq!(
|
||||
event.data.provider_request_body,
|
||||
payload
|
||||
.report_context
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("provider_request_body"))
|
||||
.cloned()
|
||||
);
|
||||
assert_eq!(event.data.response_body, payload.body_json);
|
||||
assert!(event.data.client_response_body.is_none());
|
||||
assert_eq!(
|
||||
event.data.request_body_state,
|
||||
Some(UsageBodyCaptureState::Inline)
|
||||
);
|
||||
assert_eq!(
|
||||
event.data.provider_request_body_state,
|
||||
Some(UsageBodyCaptureState::Inline)
|
||||
);
|
||||
assert_eq!(
|
||||
event.data.response_body_state,
|
||||
Some(UsageBodyCaptureState::Inline)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_terminal_usage_applies_kiro_simulated_cache_context() {
|
||||
let plan = ExecutionPlan {
|
||||
|
||||
Reference in New Issue
Block a user