Preserve image generation failure metadata for premium usage limits (#57275)

## Why

Image generation usage-limit errors with the `imagegen_premium` limit ID were not recognized as structured `UsageLimitExceeded` failures.

## What changed

Recognize `imagegen_premium` alongside `image_gen`, preserving the limit ID and available reset time in image generation failure metadata.

## Testing

Extend app-server coverage to verify premium limit metadata and reset-time fallback in completion events, thread reads, and resumed threads. Add unit coverage for missing exhausted-window reset times and unrelated or absent limit IDs.

GitOrigin-RevId: 8cf991776403ddce1f45d9780b8040982fd0fcfe
This commit is contained in:
jimxie-oai
2026-10-26 21:46:01 +00:00
committed by copyberry
parent 369b2eaa62
commit 166aed9854
3 changed files with 64 additions and 31 deletions
@@ -8,8 +8,11 @@ use codex_exec_server::LOCAL_FS;
use codex_extension_api::ToolOutput;
use codex_extension_api::ToolPayload;
use codex_extension_api::ToolSpec;
use codex_extension_items::image_generation::ImageGenerationFailure;
use codex_extension_items::image_generation::ImageGenerationInputImage;
use codex_protocol::ResponseItemId;
use codex_protocol::error::CodexErr;
use codex_protocol::error::UsageLimitReachedError;
use codex_protocol::models::ContentItem;
use codex_protocol::models::DEFAULT_IMAGE_DETAIL;
use codex_protocol::models::FunctionCallOutputBody;
@@ -32,6 +35,7 @@ use super::ImagegenArgs;
use super::PreparedImageRequest;
use super::imagegen_tool_spec;
use super::request_for_call_args;
use super::usage_limit_failure;
use crate::IMAGE_GEN_NAMESPACE;
use crate::IMAGEGEN_TOOL_NAME;
use crate::artifact::image_generation_artifact_path;
@@ -39,6 +43,39 @@ use crate::artifact::image_generation_output_hint;
const RESULT: &str = "cG5n";
#[test]
fn test_usage_limit_failure_without_an_exhausted_window_reset() {
for (limit_id, expected) in [
(
Some("imagegen_premium"),
Some(ImageGenerationFailure::UsageLimitExceeded {
limit_id: "imagegen_premium".to_string(),
resets_at: None,
}),
),
(Some("premium"), None),
(None, None),
] {
let error = CodexErr::UsageLimitReached(UsageLimitReachedError {
plan_type: None,
resets_at: None,
limit_window_minutes: None,
rate_limits: Some(Box::new(
serde_json::from_value(serde_json::json!({
"limit_id": limit_id,
"primary": {"used_percent": 100.0},
"secondary": {"used_percent": 50.0, "resets_at": 3000}
}))
.expect("rate limit snapshot"),
)),
promo_message: None,
rate_limit_reached_type: None,
});
assert_eq!(usage_limit_failure(&error), expected);
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn referenced_files_record_the_exact_bytes_read_for_each_request() {
let directory = tempfile::tempdir().expect("temporary directory");
+1 -1
View File
@@ -323,7 +323,7 @@ fn usage_limit_failure(error: &CodexErr) -> Option<ImageGenerationFailure> {
};
let rate_limits = usage_limit.rate_limits.as_deref()?;
let limit_id = rate_limits.limit_id.as_deref()?;
if limit_id != "image_gen" {
if !matches!(limit_id, "image_gen" | "imagegen_premium") {
return None;
}