ViewImage工具
view_image 的名字容易让人误以为它会把图片“显示在终端里”。在 core 中,它实际完成的是另一件事:从选定 execution environment 的文件系统读取字节,封装为 Data URL,作为 input_image 内容项返回给模型;同一次调用还把已解析的路径写成 ImageView turn item,供历史和客户端显示“看过哪个文件”。图片字节和路径因此走两条不同的通道。
本文面向已经读过工具运行时抽象、ToolRouter解析与分派和 ToolOutput与错误模型的读者。本文只追踪内置 view_image 从注册到下一次模型请求的图片 路径,不展开用户消息自身的图片输入,也不把 TUI 的路径摘要误写成图片渲染。读完后,读者应能回答:为什么工具有时不在 schema 中,relative path 相对哪个 cwd,Unified Image Budget 如何改变 detail 契约,以及损坏文件在哪一层被拒绝。
1. 工具边界
1.1 注册条件
view_image 是 environment-backed core tool。工具计划至少检查两个条件:当前 turn 是否有可用 environment,以及 Feature::ViewImage 是否开启。环境只有一个时,模型只需要传 path;有多个环境时,规格额外暴露 environment_id,让模型明确选择读取发生在哪个环境。
源码位置:codex-rs/core/src/tools/spec_plan.rs :: add_core_tool_sources
if environment_mode.has_environment() && features.enabled(Feature::ViewImage) {
let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple);
registry.add(ViewImageHandler::new(ViewImageToolOptions {
can_request_original_image_detail: can_request_original_image_detail(
&turn_context.model_info,
),
include_environment_id,
}));
}没有 environment 时,view_image 不只是调用失败,而是没有 runtime 和 visible spec。这个区别很重要:模型看不到的工具不会产生 function call;模型已经发来调用后才会遇到的错误,则属于 handler 的运行时边界。
basic reviewer session 是一个更窄的工具来源。它只在 parent 与所有 environment 都使用 Managed permission profile 时提供 exec_command、write_stdin 和可用的 view_image;任一 profile 不是 Managed,整个 reviewer 工具集合都为空。满足权限门槛后, view_image 仍要求 environment 和 feature,不会自动继承普通 Turn 的全部工具。
源码位置:codex-rs/core/src/tools/spec_plan.rs :: add_core_tool_sources
if crate::guardian::is_basic_session_source(&context.turn_context.session_source) {
let turn_context = context.turn_context;
if !matches!(turn_context.permission_profile(), PermissionProfile::Managed { .. })
|| context.environments.turn_environments().any(|environment| {
!matches!(environment.permission_profile(), PermissionProfile::Managed { .. })
})
{
return;
}
let environment_mode = tool_environment_mode(context.environments);
if environment_mode.has_environment() {
let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple);
registry.add(ExecCommandHandler::new(/* ... */));
registry.add(WriteStdinHandler);
if turn_context.config.features.enabled(Feature::ViewImage) {
registry.add(ViewImageHandler::new(ViewImageToolOptions {
can_request_original_image_detail: can_request_original_image_detail(
&turn_context.model_info,
),
unified_image_budget: unified_image_budget_enabled(
&turn_context.config.features,
&turn_context.model_info,
),
include_environment_id,
}));
}
}
return;
}这段提前 return 划出了 reviewer 的所有权边界:它是一条单独的最小注册分支,并且先证明 host 能用 managed sandbox 强制 parent 权限。普通 session 的 environment/feature gate 与多环境 schema 仍由共享测试覆盖。
图中的“没有 view_image”是规格层结果,不是一个空字符串错误。读者在调试工具列表时,应先查 environment snapshot 和 feature,而不是直接跳到 handler。
1.2 规格形状
工具描述按模型能力和 image budget 模式动态构造。path 永远必填,environment_id 只在多环境场景出现。在传统 detail-based 模式下,模型支持 original 时才暴露 detail,输出要求 image_url + detail。启用 Unified Image Budget 后,输入 schema 隐藏 detail,输出 schema 也只要求 image_url,避免让模型选择已经由统一预算接管的细节模式。
源码位置:codex-rs/core/src/tools/handlers/view_image_spec.rs :: create_view_image_tool
pub struct ViewImageToolOptions {
pub can_request_original_image_detail: bool,
pub unified_image_budget: bool,
pub include_environment_id: bool,
}
pub fn create_view_image_tool(options: ViewImageToolOptions) -> ToolSpec {
let mut properties = BTreeMap::from([(
"path".to_string(),
JsonSchema::string(Some("Local filesystem path to an image file.".to_string())),
)]);
if options.can_request_original_image_detail && !options.unified_image_budget {
properties.insert(
"detail".to_string(),
JsonSchema::string_enum(
vec![json!("high"), json!("original")],
Some("Image detail level. Defaults to `high`; use `original` to preserve exact resolution.".to_string()),
),
);
}
if options.include_environment_id {
properties.insert(
"environment_id".to_string(),
JsonSchema::string(Some(
"Environment id from <environment_context>. Omit to use the primary environment."
.to_string(),
)),
);
}
ToolSpec::Function(ResponsesApiTool {
name: VIEW_IMAGE_TOOL_NAME.to_string(),
description: "View a local image file from the filesystem when visual inspection is needed. Use this for images already available on disk.".to_string(),
strict: false,
defer_loading: None,
parameters: JsonSchema::object(properties, Some(vec!["path".to_string()]), Some(false.into())),
output_schema: Some(view_image_output_schema(options)),
})
}Unified budget 的开关同时要求 Feature::UnifiedImageBudget,以及模型使用 Responses Lite 或支持 original detail。换模型或关闭 feature 都可能让 schema 回到 detail-based 模式。为兼容旧调用,即使 detail 从 schema 消失,handler 仍接受 high/original; 只是 unified 模式会忽略这个选择并按统一预算执行。
2. 参数契约
2.1 路径解析
handler 接受 path、可选的 environment_id 和可选的字符串 detail。它不会先把 path 当作主机路径交给标准库,而是先通过 resolve_tool_environment 选出 TurnEnvironment,再调用该环境的 cwd 解析方法。相对路径因此相对所选 environment,而不是相对运行 Codex 的进程目录。
源码位置:codex-rs/core/src/tools/handlers/view_image.rs :: ViewImageHandler::handle_call
let ViewImageArgs {
path,
environment_id,
detail,
} = parse_arguments(&arguments)?;
let Some(turn_environment) =
resolve_tool_environment(&step_context.environments, environment_id.as_deref())?
else {
return Err(FunctionCallError::RespondToModel(
"view_image is unavailable in this session".to_string(),
));
};
let path_uri = turn_environment.cwd().join(&path).map_err(|err| {
FunctionCallError::RespondToModel(format!(
"unable to resolve image path `{path}` against environment cwd `{}`: {err}",
turn_environment.cwd(),
))
})?;解析结果是 PathUri。它同时保留跨平台路径语义和环境边界,后面的 metadata、read、ImageViewItem 与错误信息都使用这个已解析对象。多环境测试分别在 local 和 remote environment 写入同名文件,再以 environment_id 调用,断言两个 function output 都来自选中的环境。
2.2 Detail值
输入 detail 仍只接受省略、high 或 original,包括 Unified Budget 隐藏该字段时的兼容调用。low 和其他值直接报错。 detail-based 模式中,original 只有模型支持时才生效;Unified Budget 模式中输出按 Original preparation 处理,不由该字段决定。
源码位置:codex-rs/core/src/tools/handlers/view_image.rs :: ViewImageHandler::handle_call
let detail = match detail.as_deref() {
None => None,
Some("high") => Some(ViewImageDetail::High),
Some("original") => Some(ViewImageDetail::Original),
Some(detail) => {
return Err(FunctionCallError::RespondToModel(format!(
"view_image.detail only supports `high` or `original`; omit `detail` for default high resized behavior, got `{detail}`"
)));
}
};这里的 ViewImageDetail::High 只是 handler 内部的输入标记;真正写进输出的是 protocol 的 ImageDetail。这种分层避免把“模型请求了什么”和“当前模型最终能收到什么”混为一谈。
2.3 模态门槛
即使工具已经出现在 registry,handler 仍先检查 turn.model_info.input_modalities 是否包含 Image。文本模型收到调用时,错误是 view_image is not allowed because you do not support image inputs,不会读取文件,也不会产生 ImageView 历史 item。
源码位置:codex-rs/core/src/tools/handlers/view_image.rs :: ViewImageHandler::handle_call
if !invocation
.turn
.model_info
.input_modalities
.contains(&InputModality::Image)
{
return Err(FunctionCallError::RespondToModel(
VIEW_IMAGE_UNSUPPORTED_MESSAGE.to_string(),
));
}这个检查位于 payload 解析和文件系统访问之前,因此它既是能力反馈,也是资源保护。模型能力测试显式构造只有 Text modality 的 ModelInfo,断言 function output 是错误文本;测试并没有证明所有上游 provider 都会准确填充 modality 元数据。
3. 读取路径
3.1 沙箱读取
环境和路径准备好后,handler 直接从选定 TurnEnvironment 取得 sandbox context,把它同时传给带 options 的 get_metadata 与 read_file。sandbox owner 因此是 environment,而不是 thread-wide TurnContext。先查 metadata 再读字节可区分目录,并保留 environment-specific read policy。
源码位置:codex-rs/core/src/tools/handlers/view_image.rs :: ViewImageHandler::handle_call
let model_visible_path = path_uri.inferred_native_path_string();
let sandbox = turn_environment.sandbox_context(/*additional_permissions*/ None);
let fs = turn_environment.environment.get_filesystem();
let metadata = fs
.get_metadata(
&path_uri,
GetMetadataOptions::default(),
Some(&sandbox),
)
.await
.map_err(|error| {
FunctionCallError::RespondToModel(format!(
"unable to locate image at `{model_visible_path}`: {error}"
))
})?;
if !metadata.is_file {
return Err(FunctionCallError::RespondToModel(format!(
"image path `{model_visible_path}` is not a file"
)));
}
let file_bytes = fs
.read_file(
&path_uri,
ReadFileOptions::default(),
Some(&sandbox),
)
.await
.map_err(|error| {
FunctionCallError::RespondToModel(format!(
"unable to read image at `{model_visible_path}`: {error}"
))
})?;
image::load_from_memory(&file_bytes).map_err(|_| {
FunctionCallError::RespondToModel(
"unable to process image: invalid or unsupported image data".to_string(),
)
})?;读取后 handler 现在先用 image::load_from_memory 验证字节确实可解码,再生成 data_url_from_bytes("application/octet-stream", ...)。它仍不根据扩展名手写 MIME,缩放与最终格式准备留在 history insertion 边界;但任意文本文件已经不能作为成功 tool output 或 Code Mode JSON 逸出。
3.2 原图能力
detail-based 模式下,handler 只有同时看到模型支持 original 且请求 detail=original,才输出 Original;否则为 High。Unified Image Budget 模式则无条件使用 Original preparation hint,后续统一限制为最大 dimension 6000、patches 10000。它不是“完全不限制原图”, 而是把旧 high/original 两套预算合并成一套更大的上限。
源码位置:codex-rs/tools/src/image_detail.rs :: can_request_original_image_detail
pub fn can_request_original_image_detail(model_info: &ModelInfo) -> bool {
model_info.supports_image_detail_original
}源码位置:codex-rs/core/src/tools/handlers/view_image.rs :: ViewImageHandler::handle_call
let can_request_original_detail = can_request_original_image_detail(&turn.model_info);
let use_original_detail = self.options.unified_image_budget
|| can_request_original_detail
&& matches!(detail, Some(ViewImageDetail::Original));
let image_detail = if use_original_detail {
ImageDetail::Original
} else {
DEFAULT_IMAGE_DETAIL
};
let image_url = data_url_from_bytes("application/octet-stream", &file_bytes);测试同时覆盖两种策略:detail-based 下无 original 能力会按 2048 上限缩放;Unified Budget 下即使 Responses Lite 不声明 original detail,也能保留 2304×864,并对 6401×1 输入缩到 6000×1。后者验证统一预算,不应解释为 provider 原图能力字段被忽略。
4. 输出回路
4.1 两条输出
读取成功后,handler 先发送 TurnItem::ImageView 的 started/completed 事件,再返回 ViewImageOutput。这两个结果面向不同消费者:turn item 面向历史、TUI 和 App Server;function call output 面向下一次模型请求。前者只存 id 与解析后的路径,后者才携带 Data URL。
源码位置:codex-rs/core/src/tools/handlers/view_image.rs :: ViewImageHandler::handle_call
let item = TurnItem::ImageView(ImageViewItem {
id: call_id,
path: path_uri,
});
session.emit_turn_item_started(turn.as_ref(), &item).await;
session.emit_turn_item_completed(turn.as_ref(), item).await;
Ok(boxed_tool_output(ViewImageOutput {
image_url,
image_detail,
}))call_id 被移动进 ImageViewItem,所以之后的 output 使用 ToolOutput::to_response_item 收到自己的 call id 参数。这个设计把历史事件与 function output 绑定到同一个工具调用,而不是依赖文件路径去猜测关联关系。
4.2 模型输入
ViewImageOutput 的响应体只有一个 InputImage content item,并将成功标志设为 true。log_output 只记录 Data URL 长度。 Code Mode 在 detail-based 模式得到 {image_url, detail};Unified Budget 模式只得到 {image_url},与隐藏 detail 的 output schema 保持一致。
源码位置:codex-rs/core/src/tools/handlers/view_image.rs :: ToolOutput for ViewImageOutput
impl ToolOutput for ViewImageOutput {
fn log_output(&self) -> String {
format!("<image data URL omitted: {} bytes>", self.image_url.len())
}
fn success_for_logging(&self) -> bool {
true
}
fn to_response_item(&self, call_id: &str, _payload: &ToolPayload) -> ResponseInputItem {
let body =
FunctionCallOutputBody::ContentItems(vec![FunctionCallOutputContentItem::InputImage {
image_url: self.image_url.clone(),
detail: Some(self.image_detail),
}]);
let output = FunctionCallOutputPayload {
body,
success: Some(true),
};
ResponseInputItem::FunctionCallOutput {
call_id: call_id.to_string(),
output,
}
}
fn code_mode_result(&self, _payload: &ToolPayload) -> serde_json::Value {
if self.unified_image_budget {
serde_json::json!({ "image_url": self.image_url })
} else {
serde_json::json!({
"image_url": self.image_url,
"detail": self.image_detail
})
}
}
}因此测试 view_image_tool_attaches_local_image 断言 function output 中只有一个 input_image,并特别断言请求里没有额外的 image message。view_image 的结果不是“工具输出文本 + 单独用户消息”,而是函数调用输出内部的多模态 content item。
4.3 历史事件
core protocol 的 ImageViewItem 只有 id 和 PathUri。它不序列化图片 Data URL,也不承担图像解码;legacy event 只是把同一个 item 投影为 ViewImageToolCallEvent。
源码位置:codex-rs/protocol/src/items.rs :: ImageViewItem
pub struct ImageViewItem {
pub id: String,
/// Path resolved within the selected execution environment.
pub path: PathUri,
}源码位置:codex-rs/protocol/src/legacy_events.rs :: TurnItem::as_legacy_events
TurnItem::ImageView(item) => {
vec![EventMsg::ViewImageToolCall(ViewImageToolCallEvent {
call_id: item.id.clone(),
path: item.path.clone(),
})]
}这个投影解释了一个常见误读:客户端收到 ViewImageToolCallEvent 只能知道路径和调用 id;真正供模型使用的图片已经在 function call output 中,两个事件不能互相替代。
5. 历史边界
5.1 延迟处理
handler 生成的 Data URL 仍带有 application/octet-stream 前缀。历史插入边界的 prepare_response_items 扫描 message 与 function output 的 InputImage,按 ImagePreparationMode 解码、限制尺寸并重新编码。handler 已经排除明显的非图片字节,但超大图片、 不支持的 detail 或准备过程错误仍可能在这里转换为占位文本。
源码位置:codex-rs/core/src/image_preparation.rs :: prepare_response_items
pub(crate) fn prepare_response_items(
items: &mut Vec<ResponseItem>,
mode: ImagePreparationMode,
resize_notice_mode: ImageResizeNoticeMode,
) -> Vec<ImagePreparationMetadata> {
let mut metadata = Vec::new();
let mut prepared_items = Vec::with_capacity(items.len());
for mut item in std::mem::take(items) {
let resize_notice = match &mut item {
ResponseItem::Message { role, content, .. } => {
let resized_images = prepare_message_content(
content,
ImageOrigin {
message_role: Some(role),
item_id: None,
},
if role == "user" {
resize_notice_mode
} else {
ImageResizeNoticeMode::Disabled
},
&mut metadata,
mode,
);
(!resized_images.is_empty()).then(|| {
ImageResizeNotice::new(ImageResizeNoticeSource::UserMessage, resized_images)
})
}
ResponseItem::FunctionCallOutput { call_id, output, .. }
| ResponseItem::CustomToolCallOutput { call_id, output, .. } => output
.content_items_mut()
.and_then(|content| {
let resized_images = prepare_tool_output_content(
content,
ImageOrigin {
message_role: None,
item_id: Some(call_id),
},
resize_notice_mode,
&mut metadata,
mode,
);
(!resized_images.is_empty()).then(|| {
ImageResizeNotice::new(ImageResizeNoticeSource::ToolOutput, resized_images)
})
}),
_ => None,
};
prepared_items.push(item);
if let Some(resize_notice) = resize_notice {
prepared_items.push(ContextualUserFragment::into(resize_notice));
}
}
*items = prepared_items;
metadata
}view_image 的工具输出属于 FunctionCallOutput,所以会走 prepare_tool_output_content;它不会被当成 user message,也不会自动产生用户侧 resize notice,除非当前配置打开了相应 notice。这个位置是“工具读取”和“模型请求准备”的生效屏障。
5.2 限制与占位
DetailBased 模式的 High 限制是单边 2048、最多 2500 patches,Original 使用统一限制:单边 6000、最多 10000 patches。 UnifiedBudget 模式无论兼容 detail hint 是什么,都直接选择统一限制。ImageDetail::Low 与 remote URL 不支持;超出尺寸或处理失败 时,函数输出的 InputImage 会替换为 InputText 占位,而不是把整个 Turn 标成 fatal。
源码位置:codex-rs/core/src/image_preparation.rs :: prepare_tool_output_content
match prepare_image(image_url, detail, origin, metadata, mode) {
Ok(Some(resize)) if resize_notice_mode == ImageResizeNoticeMode::Enabled => {
resized_images.push(ResizedImage {
image_number,
image_count,
source_width: resize.source_width,
source_height: resize.source_height,
prepared_width: resize.prepared_width,
prepared_height: resize.prepared_height,
});
}
Ok(_) => {}
Err(error) => {
warn!(%error, "failed to prepare tool output image");
*item = FunctionCallOutputContentItem::InputText {
text: error.placeholder().to_string(),
};
}
}具体的 detail 选择和限制来自 prepare_image:省略、auto、high 使用高细节限制,original 使用原图限制,low 直接产生占位错误。这个阶段重新编码 Data URL,因此 handler 中的通用 MIME 前缀不会决定最终请求中的 MIME。
源码位置:codex-rs/core/src/image_preparation.rs :: prepare_image
let (effective_detail, limits) = match mode {
ImagePreparationMode::UnifiedBudget => {
(ImageDetailSetting::Original, UNIFIED_IMAGE_LIMITS)
}
ImagePreparationMode::DetailBased => match detail {
None | Some(ImageDetail::Auto | ImageDetail::High) => {
(ImageDetailSetting::High, HIGH_DETAIL_LIMITS)
}
Some(ImageDetail::Original) => {
(ImageDetailSetting::Original, UNIFIED_IMAGE_LIMITS)
}
Some(ImageDetail::Low) => {
return Err(ImagePreparationError::UnsupportedLowDetail);
}
},
};
let image = load_data_url_for_prompt(image_url, PromptImageMode::ResizeWithLimits(limits))?;
metadata.push(ImagePreparationMetadata {
message_role: origin.message_role.map(str::to_string),
item_id: origin.item_id.map(str::to_string),
effective_detail,
source_width: image.source_width,
source_height: image.source_height,
prepared_width: image.width,
prepared_height: image.height,
});
*image_url = image.into_data_url();当前 view_image_tool_rejects_invalid_image_before_tool_output 让 handler 读取 JSON 字节,断言第二次请求中没有 input_image,而是 模型可见的 invalid-image 错误。准备阶段占位仍用于已经形成 InputImage 后发生的预算或处理错误,但“不是真图片”已前移到 handler 拒绝。
6. 客户端消费
6.1 App Server
App Server 把 core 的 TurnItem::ImageView 映射成公开 ThreadItem::ImageView,只携带 id 和 LegacyAppPathString。它不从 core event 中重新读取图片,也不把 Data URL 放入 thread item。
源码位置:codex-rs/app-server-protocol/src/protocol/v2/item.rs :: CoreTurnItem::ImageView
CoreTurnItem::ImageView(image) => ThreadItem::ImageView {
id: image.id,
path: image.path.into(),
},因此 App Server 客户端若要观察“调用发生了”,监听 item/started、item/completed 或 thread history;若要让模型继续推理,使用已经进入 Responses 请求的 input_image。这两个消费者的载荷不同,不能用 thread item 代替模型输入。
6.2 TUI与Code Mode
TUI transcript 将 ThreadItem::ImageView 渲染成 image: <path> 摘要,只说明访问了哪个文件。Code Mode 在 detail-based 模式 得到 {image_url, detail},Unified Budget 模式得到 {image_url};image(...) helper 两种都能消费,并在 custom tool output 进入 history 时继续经过同一 ImagePreparationMode。
源码位置:codex-rs/tui/src/thread_transcript.rs :: ThreadItem::ImageView
ThreadItem::ImageView { path, .. } => {
let path = path.render_for_ui();
vec![format!("image: {path}").dim().into()]
}源码位置:codex-rs/core/tests/suite/code_mode.rs :: code_mode_can_use_view_image_result_with_image_helper
let code = format!(
r#"
const out = await tools.view_image({{ path: {image_path_json}, detail: "original" }});
image(out);
"#
);detail-based Code Mode 测试断言最终 custom tool output 中存在 input_image、Data URL 使用 PNG 且 detail 为 original。Unified Budget 下 detail 对脚本隐藏,但 history preparation 会为旧 transport 保留兼容 Original hint,并在 Responses Lite 发送前移除。
6.3 Control与Hook
ViewImageHandler 现在是 builtin control tool,registry 会记录 Completed、Failed、Rejected 或 Interrupted。它使用默认 function PreToolUse/PostToolUse 投影:PreToolUse 在任何文件读取前运行,可以阻断或重写 path/environment/detail;PostToolUse 在图片已读取、 ImageView item 已发送后运行,不能撤销历史事件或读取副作用。
源码位置:codex-rs/core/src/tools/handlers/view_image.rs :: CoreToolRuntime for ViewImageHandler
impl CoreToolRuntime for ViewImageHandler {
fn is_builtin_control_tool(&self) -> bool {
true
}
}PostToolUse 的 function response 可能包含 Data URL,因此 hook 配置应谨慎处理输出体积。统一预算只隐藏 Code Mode/schema 的 detail 字段,不会让图片字节从 function output 中消失。
7. 失败分支
7.1 读取失败
下面几类失败发生在 handler,并会把错误直接作为 function call output 文本返回:没有可选 environment、路径无法相对 cwd 解析、metadata 读取失败、路径是目录、文件读取被 sandbox 拒绝或文件不存在。它们共同点是:不会发送成功的 ImageView item,也不会产生 input_image。
源码位置:codex-rs/core/tests/suite/view_image.rs :: view_image_tool_applies_local_sandbox_read_denies
assert!(
request.inputs_of_type("input_image").is_empty(),
"sandboxed local view_image should not attach denied images"
);
let output_text = request
.function_call_output_content_and_success(call_id)
.and_then(|(content, _)| content)
.context("sandboxed view_image error text present")?;
assert!(
output_text.starts_with(&expected_locate_prefix)
|| output_text.starts_with(&expected_read_prefix),
);目录测试和缺失文件测试分别断言 image path ... is not a file 与 unable to locate image at ... 前缀。测试关注错误类型和“没有图像内容”这两个稳定事实,没有把底层操作系统的完整错误文本当作跨平台契约。
7.2 准备失败
损坏或不支持的图片数据现在由 handler 的 image::load_from_memory 直接拒绝,错误文本为 unable to process image: invalid or unsupported image data,不会发送 ImageView item 或成功 InputImage。历史准备阶段的占位用于 另一类错误:合法图片在统一/细节预算中超限,或已存在的其他 InputImage 无法处理。两者都不应归为文件读取失败。
排查时先看 handler 是否返回 invalid-image 错误;只有 handler 成功后模型仍看不到图片,才继续检查 history preparation 是否将 input_image 替换成预算/处理占位。
8. 源码练习
可以用下面的路径复述一次完整调用:add_core_tool_sources 注册 → ViewImageHandler::handle_call 检查模态并选择 environment → sandbox get_metadata/read_file → ViewImageOutput::to_response_item → prepare_tool_output_content → 下一次 Responses 请求。复述时要指出哪一步拥有路径,哪一步拥有图片字节,哪一步决定最终 detail。
再做三个只读验证:
- 在
core/tests/suite/view_image.rs中找到view_image_tool_resizes_when_model_lacks_original_detail_support,解释为什么请求没有写detail: original仍然能验证原图能力缺失后的high路径; - 找到
user_turn_unified_image_budget_supports_responses_lite_without_original_detail,说明为什么 schema 不需要 original detail 能力仍可使用统一预算; - 找到
view_image_tool_rejects_invalid_image_before_tool_output,说明 invalid bytes 为什么不会进入 Code Mode 或 history preparation。
在 Codex 源码仓库根目录可以直接运行:
rg -n "view_image_tool_(attaches_local_image|rejects_invalid_image|returns_unsupported_message)" codex-rs/core/tests/suite/view_image.rs
RUST_MIN_STACK=16777216 cargo test -p codex-core --test all suite::view_image::view_image_tool_attaches_local_image -- --exact --nocapture
RUST_MIN_STACK=16777216 cargo test -p codex-core --test all suite::view_image::view_image_tool_rejects_invalid_image_before_tool_output -- --exact --nocapture第一个命令定位测试入口;后两个集成测试需要 mock network 环境。它们分别检查正常输出/resize notice 与 handler 前置图片验证, 不覆盖真实远程 environment 的部署连通性。
掌握这两条验证后,读者就能把“工具注册问题”“文件系统问题”“图片准备问题”和“模型能力问题”分成四个不同层次,而不是把所有现象都归结为图片格式错误。
