feat: provider api_formats 可空继承、OpenAI 图片 edit/variation 与用量配额多项补强

- 鉴权: provider_api_keys.api_formats 改为可空,OAuth 托管 key 自动继承 provider endpoints 激活格式,相关 handler/测试同步更新
- 图片 planner: OpenAI 图片路由新增 edit/variation 操作并完善参数校验、响应合并与流式处理
- 用量: user me usage 返回区分 client_requested_stream/upstream_is_stream,前端 usage 列表筛选与展示增强
- 统计: stats_daily_model 新增 cache_creation_ephemeral_5m/1h tokens 字段与回填链路
- 配额/observability: quota repository 新增内存与 SQL 扩展,admin observability usage 字段扩充
- 其它: OAuth 导入/轮询收敛、provider 汇总与 pool admin 读写链路小修、新增 system_config 缓存与 provider template handler

Closes #318

Co-authored-by: Entropy.Xu <53283266+Entropy-Xu@users.noreply.github.com>
This commit is contained in:
fawney19
2026-04-23 14:42:51 +08:00
parent f55f22d2e8
commit fa328e18a1
128 changed files with 5583 additions and 690 deletions

View File

@@ -48,7 +48,8 @@ pub use crate::contracts::{
OPENAI_CLI_SYNC_FINALIZE_REPORT_KIND, OPENAI_CLI_SYNC_PLAN_KIND,
OPENAI_CLI_SYNC_SUCCESS_REPORT_KIND, OPENAI_COMPACT_STREAM_PLAN_KIND,
OPENAI_COMPACT_SYNC_ERROR_REPORT_KIND, OPENAI_COMPACT_SYNC_FINALIZE_REPORT_KIND,
OPENAI_COMPACT_SYNC_PLAN_KIND, OPENAI_IMAGE_SYNC_FINALIZE_REPORT_KIND,
OPENAI_COMPACT_SYNC_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND,
OPENAI_IMAGE_STREAM_SUCCESS_REPORT_KIND, OPENAI_IMAGE_SYNC_FINALIZE_REPORT_KIND,
OPENAI_IMAGE_SYNC_PLAN_KIND, OPENAI_IMAGE_SYNC_SUCCESS_REPORT_KIND,
OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND, OPENAI_VIDEO_CONTENT_PLAN_KIND,
OPENAI_VIDEO_CREATE_SYNC_FINALIZE_REPORT_KIND, OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND,
@@ -135,7 +136,10 @@ pub use crate::planner::specialized::{
resolve_stream_spec as resolve_gemini_files_stream_spec,
resolve_sync_spec as resolve_gemini_files_sync_spec, LocalGeminiFilesSpec,
},
image::{resolve_sync_spec as resolve_local_image_sync_spec, LocalOpenAiImageSpec},
image::{
resolve_stream_spec as resolve_local_image_stream_spec,
resolve_sync_spec as resolve_local_image_sync_spec, LocalOpenAiImageSpec,
},
video::{
resolve_sync_spec as resolve_local_video_sync_spec, LocalVideoCreateFamily,
LocalVideoCreateSpec,
@@ -160,4 +164,7 @@ pub use crate::planner::standard::{
resolve_sync_spec as resolve_openai_cli_sync_spec, LocalOpenAiCliSpec,
},
LocalStandardSourceFamily, LocalStandardSourceMode, LocalStandardSpec,
CODEX_OPENAI_IMAGE_DEFAULT_MODEL, CODEX_OPENAI_IMAGE_DEFAULT_OUTPUT_FORMAT,
CODEX_OPENAI_IMAGE_DEFAULT_VARIATION_MODEL, CODEX_OPENAI_IMAGE_DEFAULT_VARIATION_PROMPT,
CODEX_OPENAI_IMAGE_INTERNAL_MODEL,
};

View File

@@ -23,26 +23,28 @@ pub use plan_kinds::{
GEMINI_FILES_UPLOAD_PLAN_KIND, GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND,
GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND,
OPENAI_CLI_STREAM_PLAN_KIND, OPENAI_CLI_SYNC_PLAN_KIND, OPENAI_COMPACT_STREAM_PLAN_KIND,
OPENAI_COMPACT_SYNC_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND, OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND,
OPENAI_VIDEO_CONTENT_PLAN_KIND, OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND,
OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND, OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND,
OPENAI_COMPACT_SYNC_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND,
OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND, OPENAI_VIDEO_CONTENT_PLAN_KIND,
OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND,
OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND,
};
pub use report_kinds::{
core_error_background_report_kind, core_error_default_client_api_format,
core_success_background_report_kind, implicit_sync_finalize_report_kind,
CLAUDE_CHAT_STREAM_SUCCESS_REPORT_KIND, CLAUDE_CHAT_SYNC_ERROR_REPORT_KIND,
CLAUDE_CHAT_SYNC_FINALIZE_REPORT_KIND, CLAUDE_CHAT_SYNC_SUCCESS_REPORT_KIND,
CLAUDE_CLI_STREAM_SUCCESS_REPORT_KIND, CLAUDE_CLI_SYNC_ERROR_REPORT_KIND,
CLAUDE_CLI_SYNC_FINALIZE_REPORT_KIND, CLAUDE_CLI_SYNC_SUCCESS_REPORT_KIND,
GEMINI_CHAT_STREAM_SUCCESS_REPORT_KIND, GEMINI_CHAT_SYNC_ERROR_REPORT_KIND,
GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND, GEMINI_CHAT_SYNC_SUCCESS_REPORT_KIND,
GEMINI_CLI_STREAM_SUCCESS_REPORT_KIND, GEMINI_CLI_SYNC_ERROR_REPORT_KIND,
GEMINI_CLI_SYNC_FINALIZE_REPORT_KIND, GEMINI_CLI_SYNC_SUCCESS_REPORT_KIND,
GEMINI_VIDEO_CREATE_SYNC_FINALIZE_REPORT_KIND, OPENAI_CHAT_STREAM_SUCCESS_REPORT_KIND,
OPENAI_CHAT_SYNC_ERROR_REPORT_KIND, OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND,
OPENAI_CHAT_SYNC_SUCCESS_REPORT_KIND, OPENAI_CLI_STREAM_SUCCESS_REPORT_KIND,
OPENAI_CLI_SYNC_ERROR_REPORT_KIND, OPENAI_CLI_SYNC_FINALIZE_REPORT_KIND,
OPENAI_CLI_SYNC_SUCCESS_REPORT_KIND, OPENAI_COMPACT_SYNC_ERROR_REPORT_KIND,
OPENAI_COMPACT_SYNC_FINALIZE_REPORT_KIND, OPENAI_IMAGE_SYNC_FINALIZE_REPORT_KIND,
core_success_background_report_kind, implicit_stream_success_report_kind,
implicit_sync_finalize_report_kind, CLAUDE_CHAT_STREAM_SUCCESS_REPORT_KIND,
CLAUDE_CHAT_SYNC_ERROR_REPORT_KIND, CLAUDE_CHAT_SYNC_FINALIZE_REPORT_KIND,
CLAUDE_CHAT_SYNC_SUCCESS_REPORT_KIND, CLAUDE_CLI_STREAM_SUCCESS_REPORT_KIND,
CLAUDE_CLI_SYNC_ERROR_REPORT_KIND, CLAUDE_CLI_SYNC_FINALIZE_REPORT_KIND,
CLAUDE_CLI_SYNC_SUCCESS_REPORT_KIND, GEMINI_CHAT_STREAM_SUCCESS_REPORT_KIND,
GEMINI_CHAT_SYNC_ERROR_REPORT_KIND, GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND,
GEMINI_CHAT_SYNC_SUCCESS_REPORT_KIND, GEMINI_CLI_STREAM_SUCCESS_REPORT_KIND,
GEMINI_CLI_SYNC_ERROR_REPORT_KIND, GEMINI_CLI_SYNC_FINALIZE_REPORT_KIND,
GEMINI_CLI_SYNC_SUCCESS_REPORT_KIND, GEMINI_VIDEO_CREATE_SYNC_FINALIZE_REPORT_KIND,
OPENAI_CHAT_STREAM_SUCCESS_REPORT_KIND, OPENAI_CHAT_SYNC_ERROR_REPORT_KIND,
OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND, OPENAI_CHAT_SYNC_SUCCESS_REPORT_KIND,
OPENAI_CLI_STREAM_SUCCESS_REPORT_KIND, OPENAI_CLI_SYNC_ERROR_REPORT_KIND,
OPENAI_CLI_SYNC_FINALIZE_REPORT_KIND, OPENAI_CLI_SYNC_SUCCESS_REPORT_KIND,
OPENAI_COMPACT_SYNC_ERROR_REPORT_KIND, OPENAI_COMPACT_SYNC_FINALIZE_REPORT_KIND,
OPENAI_IMAGE_STREAM_SUCCESS_REPORT_KIND, OPENAI_IMAGE_SYNC_FINALIZE_REPORT_KIND,
OPENAI_IMAGE_SYNC_SUCCESS_REPORT_KIND, OPENAI_VIDEO_CREATE_SYNC_FINALIZE_REPORT_KIND,
};

View File

@@ -3,6 +3,7 @@ pub const GEMINI_FILES_UPLOAD_PLAN_KIND: &str = "gemini_files_upload";
pub const GEMINI_FILES_LIST_PLAN_KIND: &str = "gemini_files_list";
pub const GEMINI_FILES_DELETE_PLAN_KIND: &str = "gemini_files_delete";
pub const GEMINI_FILES_DOWNLOAD_PLAN_KIND: &str = "gemini_files_download";
pub const OPENAI_IMAGE_STREAM_PLAN_KIND: &str = "openai_image_stream";
pub const OPENAI_IMAGE_SYNC_PLAN_KIND: &str = "openai_image_sync";
pub const OPENAI_VIDEO_CONTENT_PLAN_KIND: &str = "openai_video_content";
pub const OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND: &str = "openai_video_cancel_sync";

View File

@@ -1,7 +1,7 @@
use crate::contracts::{
CLAUDE_CHAT_SYNC_PLAN_KIND, CLAUDE_CLI_SYNC_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND,
GEMINI_CLI_SYNC_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND, OPENAI_CLI_SYNC_PLAN_KIND,
OPENAI_COMPACT_SYNC_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND,
OPENAI_COMPACT_SYNC_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND,
};
pub const OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND: &str = "openai_chat_sync_finalize";
@@ -27,6 +27,7 @@ pub const OPENAI_CHAT_STREAM_SUCCESS_REPORT_KIND: &str = "openai_chat_stream_suc
pub const CLAUDE_CHAT_STREAM_SUCCESS_REPORT_KIND: &str = "claude_chat_stream_success";
pub const GEMINI_CHAT_STREAM_SUCCESS_REPORT_KIND: &str = "gemini_chat_stream_success";
pub const OPENAI_CLI_STREAM_SUCCESS_REPORT_KIND: &str = "openai_cli_stream_success";
pub const OPENAI_IMAGE_STREAM_SUCCESS_REPORT_KIND: &str = "openai_image_stream_success";
pub const CLAUDE_CLI_STREAM_SUCCESS_REPORT_KIND: &str = "claude_cli_stream_success";
pub const GEMINI_CLI_STREAM_SUCCESS_REPORT_KIND: &str = "gemini_cli_stream_success";
@@ -93,3 +94,10 @@ pub fn core_success_background_report_kind(report_kind: &str) -> Option<&'static
_ => None,
}
}
pub fn implicit_stream_success_report_kind(plan_kind: &str) -> Option<&'static str> {
match plan_kind {
OPENAI_IMAGE_STREAM_PLAN_KIND => Some(OPENAI_IMAGE_STREAM_SUCCESS_REPORT_KIND),
_ => None,
}
}

View File

@@ -7,6 +7,7 @@ use crate::adaptation::surfaces::{
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FinalizeStreamRewriteMode {
EnvelopeUnwrap,
OpenAiImage,
Standard,
KiroToClaudeCli,
}
@@ -45,6 +46,10 @@ pub fn resolve_finalize_stream_rewrite_mode(
.then_some(FinalizeStreamRewriteMode::Standard);
}
if provider_api_format == "openai:image" && client_api_format == "openai:image" {
return Some(FinalizeStreamRewriteMode::OpenAiImage);
}
if envelope_name.eq_ignore_ascii_case(KIRO_ENVELOPE_NAME) {
return (provider_api_format == "claude:cli" && client_api_format == "claude:cli")
.then_some(FinalizeStreamRewriteMode::KiroToClaudeCli);
@@ -144,4 +149,17 @@ mod tests {
});
assert_eq!(resolve_finalize_stream_rewrite_mode(&report_context), None);
}
#[test]
fn resolves_openai_image_mode_for_same_format_image_streams() {
let report_context = json!({
"provider_api_format": "openai:image",
"client_api_format": "openai:image",
"needs_conversion": false,
});
assert_eq!(
resolve_finalize_stream_rewrite_mode(&report_context),
Some(FinalizeStreamRewriteMode::OpenAiImage)
);
}
}

View File

@@ -8,9 +8,10 @@ use crate::contracts::{
GEMINI_FILES_UPLOAD_PLAN_KIND, GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND,
GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND,
OPENAI_CLI_STREAM_PLAN_KIND, OPENAI_CLI_SYNC_PLAN_KIND, OPENAI_COMPACT_STREAM_PLAN_KIND,
OPENAI_COMPACT_SYNC_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND, OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND,
OPENAI_VIDEO_CONTENT_PLAN_KIND, OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND,
OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND, OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND,
OPENAI_COMPACT_SYNC_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND,
OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND, OPENAI_VIDEO_CONTENT_PLAN_KIND,
OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND,
OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND,
};
pub fn resolve_execution_runtime_stream_plan_kind(
@@ -88,6 +89,14 @@ pub fn resolve_execution_runtime_stream_plan_kind(
return Some(OPENAI_COMPACT_STREAM_PLAN_KIND);
}
if route_family == Some("openai")
&& route_kind == Some("image")
&& *method == Method::POST
&& matches!(path, "/v1/images/generations" | "/v1/images/edits")
{
return Some(OPENAI_IMAGE_STREAM_PLAN_KIND);
}
if route_family == Some("openai")
&& route_kind == Some("video")
&& *method == Method::GET
@@ -171,7 +180,10 @@ pub fn resolve_execution_runtime_sync_plan_kind(
if route_family == Some("openai")
&& route_kind == Some("image")
&& *method == Method::POST
&& matches!(path, "/v1/images/generations" | "/v1/images/edits")
&& matches!(
path,
"/v1/images/generations" | "/v1/images/edits" | "/v1/images/variations"
)
{
return Some(OPENAI_IMAGE_SYNC_PLAN_KIND);
}
@@ -258,7 +270,8 @@ pub fn is_matching_stream_request(
| CLAUDE_CHAT_STREAM_PLAN_KIND
| OPENAI_CLI_STREAM_PLAN_KIND
| OPENAI_COMPACT_STREAM_PLAN_KIND
| CLAUDE_CLI_STREAM_PLAN_KIND => body_json
| CLAUDE_CLI_STREAM_PLAN_KIND
| OPENAI_IMAGE_STREAM_PLAN_KIND => body_json
.get("stream")
.and_then(|value| value.as_bool())
.unwrap_or(false),
@@ -300,6 +313,7 @@ pub fn supports_stream_scheduler_decision_kind(plan_kind: &str) -> bool {
| CLAUDE_CHAT_STREAM_PLAN_KIND
| GEMINI_CHAT_STREAM_PLAN_KIND
| OPENAI_CLI_STREAM_PLAN_KIND
| OPENAI_IMAGE_STREAM_PLAN_KIND
| OPENAI_COMPACT_STREAM_PLAN_KIND
| CLAUDE_CLI_STREAM_PLAN_KIND
| GEMINI_CLI_STREAM_PLAN_KIND
@@ -318,7 +332,8 @@ mod tests {
supports_sync_scheduler_decision_kind,
};
use crate::contracts::{
OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND,
OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND,
OPENAI_IMAGE_SYNC_PLAN_KIND,
};
#[test]
@@ -387,8 +402,59 @@ mod tests {
),
Some(OPENAI_IMAGE_SYNC_PLAN_KIND)
);
assert_eq!(
resolve_execution_runtime_sync_plan_kind(
Some("ai_public"),
Some("openai"),
Some("image"),
&Method::POST,
"/v1/images/variations",
),
Some(OPENAI_IMAGE_SYNC_PLAN_KIND)
);
assert!(supports_sync_scheduler_decision_kind(
OPENAI_IMAGE_SYNC_PLAN_KIND
));
}
#[test]
fn resolves_openai_image_stream_plan_kind() {
assert_eq!(
resolve_execution_runtime_stream_plan_kind(
Some("ai_public"),
Some("openai"),
Some("image"),
&Method::POST,
"/v1/images/generations",
),
Some(OPENAI_IMAGE_STREAM_PLAN_KIND)
);
assert_eq!(
resolve_execution_runtime_stream_plan_kind(
Some("ai_public"),
Some("openai"),
Some("image"),
&Method::POST,
"/v1/images/edits",
),
Some(OPENAI_IMAGE_STREAM_PLAN_KIND)
);
assert!(supports_stream_scheduler_decision_kind(
OPENAI_IMAGE_STREAM_PLAN_KIND
));
}
#[test]
fn stream_matching_requires_openai_image_stream_flag() {
assert!(!is_matching_stream_request(
OPENAI_IMAGE_STREAM_PLAN_KIND,
"/v1/images/generations",
&serde_json::json!({"stream": false}),
));
assert!(is_matching_stream_request(
OPENAI_IMAGE_STREAM_PLAN_KIND,
"/v1/images/generations",
&serde_json::json!({"stream": true}),
));
}
}

View File

@@ -1,10 +1,11 @@
use crate::contracts::OPENAI_IMAGE_SYNC_PLAN_KIND;
use crate::contracts::{OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND};
#[derive(Debug, Clone, Copy)]
pub struct LocalOpenAiImageSpec {
pub api_format: &'static str,
pub decision_kind: &'static str,
pub report_kind: &'static str,
pub require_streaming: bool,
}
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalOpenAiImageSpec> {
@@ -13,6 +14,19 @@ pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalOpenAiImageSpec> {
api_format: "openai:image",
decision_kind: OPENAI_IMAGE_SYNC_PLAN_KIND,
report_kind: "openai_image_sync_finalize",
require_streaming: false,
}),
_ => None,
}
}
pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalOpenAiImageSpec> {
match plan_kind {
OPENAI_IMAGE_STREAM_PLAN_KIND => Some(LocalOpenAiImageSpec {
api_format: "openai:image",
decision_kind: OPENAI_IMAGE_STREAM_PLAN_KIND,
report_kind: "openai_image_stream_success",
require_streaming: true,
}),
_ => None,
}
@@ -20,12 +34,21 @@ pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalOpenAiImageSpec> {
#[cfg(test)]
mod tests {
use super::resolve_sync_spec;
use super::{resolve_stream_spec, resolve_sync_spec};
#[test]
fn resolves_openai_image_sync_spec() {
let spec = resolve_sync_spec("openai_image_sync").expect("spec");
assert_eq!(spec.api_format, "openai:image");
assert_eq!(spec.report_kind, "openai_image_sync_finalize");
assert!(!spec.require_streaming);
}
#[test]
fn resolves_openai_image_stream_spec() {
let spec = resolve_stream_spec("openai_image_stream").expect("spec");
assert_eq!(spec.api_format, "openai:image");
assert_eq!(spec.report_kind, "openai_image_stream_success");
assert!(spec.require_streaming);
}
}

View File

@@ -8,10 +8,19 @@ use sha2::Sha256;
use uuid::Uuid;
const CODEX_PROMPT_CACHE_NAMESPACE_VERSION: &str = "v3";
const CODEX_DEFAULT_INSTRUCTIONS: &str = "You are ChatGPT.";
const CODEX_DEFAULT_USER_AGENT: &str =
"codex-tui/0.122.0 (Aether; x86_64) vscode/3.0.12 (codex-tui; 0.122.0)";
const CODEX_DEFAULT_VERSION: &str = "0.122.0";
const CODEX_DEFAULT_ORIGINATOR: &str = "codex_cli_rs";
"codex-tui/0.122.0 (Mac OS 15.2.0; arm64) vscode/2.6.11 (codex-tui; 0.122.0)";
const CODEX_DEFAULT_ORIGINATOR: &str = "codex-tui";
pub const CODEX_OPENAI_IMAGE_INTERNAL_MODEL: &str = "gpt-5.4-mini";
pub const CODEX_OPENAI_IMAGE_DEFAULT_MODEL: &str = "gpt-image-2";
pub const CODEX_OPENAI_IMAGE_DEFAULT_VARIATION_MODEL: &str = "dall-e-2";
pub const CODEX_OPENAI_IMAGE_DEFAULT_OUTPUT_FORMAT: &str = "png";
pub const CODEX_OPENAI_IMAGE_DEFAULT_VARIATION_PROMPT: &str =
"Create a faithful variation of the provided image.";
const CODEX_IMAGE_TOOL_DEFAULT_SIZE: &str = "1024x1024";
const CODEX_IMAGE_TOOL_DEFAULT_QUALITY: &str = "high";
const CODEX_IMAGE_TOOL_DEFAULT_BACKGROUND: &str = "auto";
const UUID_NAMESPACE_OID_BYTES: [u8; 16] = [
0x6b, 0xa7, 0xb8, 0x12, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8,
];
@@ -30,6 +39,100 @@ fn is_openai_compact_request(provider_api_format: &str) -> bool {
.eq_ignore_ascii_case("openai:compact")
}
fn is_openai_image_request(provider_api_format: &str) -> bool {
provider_api_format
.trim()
.eq_ignore_ascii_case("openai:image")
}
fn apply_codex_openai_image_tool_overrides(body_object: &mut serde_json::Map<String, Value>) {
let mut tool = body_object
.get("tools")
.and_then(Value::as_array)
.and_then(|tools| tools.first())
.and_then(Value::as_object)
.cloned()
.unwrap_or_default();
tool.insert("type".to_string(), json!("image_generation"));
tool.entry("output_format".to_string())
.or_insert_with(|| json!(CODEX_OPENAI_IMAGE_DEFAULT_OUTPUT_FORMAT));
if !tool.contains_key("action") {
tool.entry("size".to_string())
.or_insert_with(|| json!(CODEX_IMAGE_TOOL_DEFAULT_SIZE));
tool.entry("quality".to_string())
.or_insert_with(|| json!(CODEX_IMAGE_TOOL_DEFAULT_QUALITY));
tool.entry("background".to_string())
.or_insert_with(|| json!(CODEX_IMAGE_TOOL_DEFAULT_BACKGROUND));
}
body_object.insert("tools".to_string(), json!([tool]));
body_object.insert(
"tool_choice".to_string(),
json!({
"type": "image_generation"
}),
);
}
fn codex_openai_image_has_prompt(body_object: &serde_json::Map<String, Value>) -> bool {
body_object
.get("input")
.and_then(Value::as_array)
.into_iter()
.flatten()
.filter_map(Value::as_object)
.filter_map(|item| item.get("content"))
.any(|content| match content {
Value::String(text) => !text.trim().is_empty(),
Value::Array(items) => items.iter().any(|item| {
item.as_object()
.filter(|item| item.get("type").and_then(Value::as_str) == Some("input_text"))
.and_then(|item| item.get("text").and_then(Value::as_str))
.map(str::trim)
.is_some_and(|text| !text.is_empty())
}),
_ => false,
})
}
fn inject_codex_default_variation_prompt(body_object: &mut serde_json::Map<String, Value>) {
let Some(action) = body_object
.get("tools")
.and_then(Value::as_array)
.and_then(|tools| tools.first())
.and_then(Value::as_object)
.and_then(|tool| tool.get("action"))
.and_then(Value::as_str)
else {
return;
};
if action != "edit" || codex_openai_image_has_prompt(body_object) {
return;
}
let Some(input) = body_object.get_mut("input").and_then(Value::as_array_mut) else {
return;
};
let Some(first_message) = input.first_mut().and_then(Value::as_object_mut) else {
return;
};
let Some(content) = first_message
.get_mut("content")
.and_then(Value::as_array_mut)
else {
return;
};
content.insert(
0,
json!({
"type": "input_text",
"text": CODEX_OPENAI_IMAGE_DEFAULT_VARIATION_PROMPT,
}),
);
}
fn build_stable_codex_prompt_cache_key(user_api_key_id: &str) -> Option<String> {
let normalized = user_api_key_id.trim();
if normalized.is_empty() {
@@ -111,6 +214,21 @@ fn extract_codex_account_id(decrypted_auth_config_raw: Option<&str>) -> Option<S
})
}
fn maybe_insert_default_codex_header(
provider_request_headers: &mut BTreeMap<String, String>,
original_headers: &http::HeaderMap,
header_name: &str,
header_value: &str,
) {
if header_map_has_non_empty_value(original_headers, header_name)
|| btree_map_has_non_empty_value(provider_request_headers, header_name)
{
return;
}
provider_request_headers.insert(header_name.to_string(), header_value.to_string());
}
fn maybe_inject_codex_prompt_cache_key(
provider_request_body: &mut Value,
provider_type: &str,
@@ -196,7 +314,19 @@ pub fn apply_codex_openai_cli_special_body_edits(
if !body_rules_handle_path(body_rules, "instructions")
&& !body_object.contains_key("instructions")
{
body_object.insert("instructions".to_string(), json!("You are GPT-5."));
body_object.insert(
"instructions".to_string(),
json!(CODEX_DEFAULT_INSTRUCTIONS),
);
}
if is_openai_image_request(provider_api_format) {
body_object.insert(
"model".to_string(),
json!(CODEX_OPENAI_IMAGE_INTERNAL_MODEL),
);
body_object.insert("stream".to_string(), json!(true));
apply_codex_openai_image_tool_overrides(body_object);
inject_codex_default_variation_prompt(body_object);
}
maybe_inject_codex_prompt_cache_key(
@@ -243,27 +373,18 @@ pub fn apply_codex_openai_cli_special_headers(
}
}
if !header_map_has_non_empty_value(original_headers, "user-agent")
&& !btree_map_has_non_empty_value(provider_request_headers, "user-agent")
{
provider_request_headers.insert(
"user-agent".to_string(),
CODEX_DEFAULT_USER_AGENT.to_string(),
if !is_openai_image_request(provider_api_format) {
maybe_insert_default_codex_header(
provider_request_headers,
original_headers,
"user-agent",
CODEX_DEFAULT_USER_AGENT,
);
}
if !header_map_has_non_empty_value(original_headers, "version")
&& !btree_map_has_non_empty_value(provider_request_headers, "version")
{
provider_request_headers.insert("version".to_string(), CODEX_DEFAULT_VERSION.to_string());
}
if !header_map_has_non_empty_value(original_headers, "originator")
&& !btree_map_has_non_empty_value(provider_request_headers, "originator")
{
provider_request_headers.insert(
"originator".to_string(),
CODEX_DEFAULT_ORIGINATOR.to_string(),
maybe_insert_default_codex_header(
provider_request_headers,
original_headers,
"originator",
CODEX_DEFAULT_ORIGINATOR,
);
}
@@ -289,3 +410,100 @@ pub fn apply_codex_openai_cli_special_headers(
}
}
}
#[cfg(test)]
mod tests {
use super::{apply_codex_openai_cli_special_body_edits, CODEX_OPENAI_IMAGE_INTERNAL_MODEL};
use serde_json::json;
#[test]
fn codex_image_body_edits_force_tool_choice_and_default_generate_tool_fields() {
let mut provider_request_body = json!({
"input": [{
"role": "user",
"content": "generate image"
}],
"tools": [{
"type": "image_generation"
}],
"tool_choice": "auto"
});
apply_codex_openai_cli_special_body_edits(
&mut provider_request_body,
"codex",
"openai:image",
None,
None,
);
assert_eq!(
provider_request_body["tools"][0]["size"],
json!("1024x1024")
);
assert_eq!(provider_request_body["tools"][0]["quality"], json!("high"));
assert_eq!(
provider_request_body["tools"][0]["background"],
json!("auto")
);
assert_eq!(
provider_request_body["tools"][0]["output_format"],
json!("png")
);
assert_eq!(
provider_request_body["model"],
json!(CODEX_OPENAI_IMAGE_INTERNAL_MODEL)
);
assert_eq!(provider_request_body["stream"], json!(true));
assert_eq!(
provider_request_body["tool_choice"]["type"],
json!("image_generation")
);
}
#[test]
fn codex_image_body_edits_preserve_edit_action_without_generate_defaults() {
let mut provider_request_body = json!({
"tools": [{
"type": "image_generation",
"action": "edit",
"input_image_mask": { "image_url": "data:image/png;base64,mask" }
}],
"input": [{
"role": "user",
"content": [{
"type": "input_image",
"image_url": "data:image/png;base64,image"
}]
}],
"tool_choice": "auto"
});
apply_codex_openai_cli_special_body_edits(
&mut provider_request_body,
"codex",
"openai:image",
None,
None,
);
assert_eq!(provider_request_body["tools"][0]["action"], json!("edit"));
assert!(provider_request_body["tools"][0].get("size").is_none());
assert!(provider_request_body["tools"][0].get("quality").is_none());
assert!(provider_request_body["tools"][0]
.get("background")
.is_none());
assert_eq!(
provider_request_body["tools"][0]["output_format"],
json!("png")
);
assert_eq!(
provider_request_body["input"][0]["content"][0]["text"],
json!("Create a faithful variation of the provided image.")
);
assert_eq!(
provider_request_body["tool_choice"]["type"],
json!("image_generation")
);
}
}

View File

@@ -8,7 +8,9 @@ pub mod openai_cli;
pub use codex::{
apply_codex_openai_cli_special_body_edits, apply_codex_openai_cli_special_headers,
apply_openai_compact_special_body_edits,
apply_openai_compact_special_body_edits, CODEX_OPENAI_IMAGE_DEFAULT_MODEL,
CODEX_OPENAI_IMAGE_DEFAULT_OUTPUT_FORMAT, CODEX_OPENAI_IMAGE_DEFAULT_VARIATION_MODEL,
CODEX_OPENAI_IMAGE_DEFAULT_VARIATION_PROMPT, CODEX_OPENAI_IMAGE_INTERNAL_MODEL,
};
pub use family::{LocalStandardSourceFamily, LocalStandardSourceMode, LocalStandardSpec};
pub use matrix::{