Skip to content

ViewImage工具

从工具注册、环境读取到模型图像输入,追踪 view_image 的真实调用链、detail能力和失败边界。

基于rust-v0.150.0
CodexRustToolsImage

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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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 源码仓库根目录可以直接运行:

bash
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 的部署连通性。

掌握这两条验证后,读者就能把“工具注册问题”“文件系统问题”“图片准备问题”和“模型能力问题”分成四个不同层次,而不是把所有现象都归结为图片格式错误。