Apus can summarize inbound media (image/audio/video) before the reply pipeline runs, so command parsing and routing work off short text instead of raw bytes. Understanding auto-detects local tools or provider keys, or you can configure explicit models. In the Apus-managed reply path, successfully described images are represented by text instead of being attached again as native images. Failed, disabled, or unselected images remain eligible for normal native image delivery; understanding errors do not block the reply.

Vendor plugins register capability metadata (which provider supports which media type, default model, priority). Apus core owns the shared tools.media config, fallback order, and reply-pipeline integration.

How it works

Collect attachments

Collect ordered inbound media facts (`path`, `url`, `contentType`, and `kind`).

Select per capability

For each enabled capability (image/audio/video), select attachments per the `attachments` policy (default: first attachment only).

Choose a model

Pick the first eligible model entry (size + capability + auth available).

Fall back on failure

If a model errors, times out, or the media exceeds `maxBytes`, try the next entry.

Apply on success

`Body` becomes an `[Image]`, `[Audio]`, or `[Video]` block. Audio also sets `{{Transcript}}`; command parsing uses caption text when present, otherwise the transcript. Captions are preserved as `User text:` inside the block.

Config

tools.media holds one capability-tagged model list plus small per-capability controls:

{
  tools: {
    media: {
      concurrency: 2, // max concurrent capability runs (default)
      models: [
        { provider: "openai", model: "gpt-4o-mini-transcribe", capabilities: ["audio"] },
        { provider: "google", model: "gemini-3-flash-preview", capabilities: ["image", "video"] },
      ],
      image: { preferredModel: "google/gemini-3-flash-preview" },
      audio: { enabled: true },
      video: { enabled: true },
    },
  },
}

Per-capability (image/audio/video) keys:

Key Type Default Notes
enabled boolean auto (false disables) Set false to turn off auto-detect for this capability
preferredModel string first compatible entry Prefer provider/model, model id, provider:<id>, or cli:command
prompt string capability default Default prompt when an entry does not override it
maxChars number 500 image/video, unset audio Default output limit
maxBytes number 10MB image, 20MB audio, 50MB video Default input limit
timeoutSeconds number 60 image/audio, 120 video Default request timeout
language string unset Audio transcription hint
scope object unset Gate by channel/chat type/source key
attachments object { mode: "first", maxAttachments: 1 } Select which matching attachments are processed
echoTranscript boolean false Audio only: echo the transcript before agent processing
echoFormat string '๐Ÿ“ "{transcript}"' Audio only: format for the echoed transcript

Prompts, limits, language hints, request overrides, and provider options can be set as capability defaults or overridden on individual tools.media.models[] entries. Capability defaults also cover auto-detected providers when no explicit model is configured. A model list selects which entries run; it does not hide other enabled providers from provider discovery.

Model entries

Each models[] entry is a provider entry (default) or a CLI entry:

Provider entry

```json5
{
  type: "provider", // default if omitted
  provider: "openai",
  model: "gpt-6-astra",
  prompt: "Describe the image in <= 500 chars.",
  maxChars: 500,
  maxBytes: 10485760,
  timeoutSeconds: 60,
  capabilities: ["image"],
  profile: "vision-profile",
  preferredProfile: "vision-fallback",
}
```

CLI entry

```json5
{
  type: "cli",
  command: "gemini",
  args: [
    "-m",
    "gemini-3-flash",
    "--allowed-tools",
    "read_file",
    "Read the media at {{AttachmentPath}} and describe it in <= {{MaxChars}} characters.",
  ],
  maxChars: 500,
  maxBytes: 52428800,
  timeoutSeconds: 120,
  capabilities: ["video", "image"],
}
```

Placeholders are substituted literally into each argument, and an unset one becomes an **empty argument** rather than being dropped. `{{Language}}` is only populated for audio, when a language is resolved from the request, the entry's `language`, or `tools.media.audio.language`; with none set, `["--language", "{{Language}}"]` runs as `--language ""`. Transcribers that reject an empty language should omit the flag and its value together and rely on autodetection. `{{Prompt}}` falls back to the built-in default prompt, so it is normally populated. The same empty substitution applies to any other placeholder without a value, such as `{{AttachmentContentType}}` for an attachment with no MIME type.

CLI templates can also use `{{AttachmentUrl}}`, `{{AttachmentContentType}}`, `{{AttachmentDir}}`, `{{AttachmentIndex}}`, `{{OutputDir}}` (scratch dir created for this run), and `{{OutputBase}}` (scratch file base path, no extension). The `{{Attachment*}}` names replaced `{{MediaPath}}`, `{{MediaUrl}}`, `{{MediaType}}`, and `{{MediaDir}}` in 2026.8.1. The older names remain deprecated compatibility aliases under the `media-legacy-projection` record: its approved `removeAfter` date is 2026-10-01, with removal additionally gated on a clean published-plugin artifact sweep. Migrate before that date - see [Media legacy projection](/plugins/sdk-migration/compatibility-policy#media-legacy-projection).

CLI entries need a nonblank command and a nonempty args list. Arguments remain literal strings with optional template interpolation; existing literal file paths and custom wrapper arguments are supported. Pass the attachment through a template such as {{AttachmentPath}} or your command's existing input contract. Empty argument lists are not supported because Apus does not feed attachments to CLI stdin. apus doctor reports missing commands or args with the exact config path and a manual fix; it does not invent commands or rewrite these entries. At runtime, an incomplete entry records a failure without launching the binary, and the next configured model is tried. If none succeeds, the attachment gets a failure outcome and a warning is logged. Config validation remains permissive for these fields so an existing config can still start the Gateway after an update.

Remote attachments are staged as temporary files only when a CLI needs a file path. The media-understanding run owns those files and removes them after processing; a failed staging attempt leaves other staged attachments available. Existing local attachments are read in place and retained.

Provider credentials

Provider media understanding uses the same auth resolution as normal model calls: auth profiles, environment variables, then models.providers.<providerId>.apiKey. tools.media.models[] entries do not accept an inline apiKey field.

{
  models: {
    providers: {
      openai: { apiKey: "<OPENAI_API_KEY>" },
      moonshot: { apiKey: "<MOONSHOT_API_KEY>" },
    },
  },
}

See Tools and custom providers for profiles, env vars, and custom base URLs.

Rules and behavior

Auto-detect (default)

When tools.media.<capability>.enabled is not false and no models are configured, Apus tries these in order and stops at the first working option:

Configured image model (image only)

`agents.defaults.imageModel` primary/fallback refs, unless the active reply model already supports vision natively. Prefer `provider/model` refs; bare refs are qualified from configured image-capable provider model entries only when the match is unique.

Active reply model

The active reply model, when its provider supports the capability.

Provider auth (audio only, before local CLIs)

Configured `models.providers.*` entries that support audio are tried before local CLIs. Bundled provider priority order (ties break alphabetically by provider id): Groq/OpenAI &rarr; xAI &rarr; Deepgram &rarr; OpenRouter &rarr; Google/SenseAudio &rarr; Deepinfra/ElevenLabs &rarr; Mistral.

Local CLIs (audio only)

Ready local binaries become an ordered fallback list:
- `whisper-cli` first only after an earlier model invocation in the current process observed Metal or CUDA
- CPU-default `sherpa-onnx-offline` (requires `SHERPA_ONNX_MODEL_DIR` with `tokens.txt`/`encoder.onnx`/`decoder.onnx`/`joiner.onnx`)
- `whisper-cli` when acceleration is merely build-capable or unobserved
- `parakeet-mlx` on Apple Silicon (MLX-capable, device use unobserved)
- `whisper` (Python CLI; defaults to the `turbo` model, downloads automatically)

Backend capability inspection is cached and does not load a model. Build capability, requested backend flags, and backend observed from a real invocation remain separate. Auto-detected whisper.cpp leaves model-run logs enabled so the upstream selected-backend line can be recorded. Explicit CLI entries keep their configured order, backend flags, and output flags.

Provider auth (image/video)

Configured `models.providers.*` entries that support the capability are tried before the bundled fallback order. Image-only config providers with an image-capable model auto-register for media understanding even when they are not a bundled vendor plugin.

Bundled provider priority order (ties break alphabetically by provider id):
- Image: Anthropic/OpenAI &rarr; Google &rarr; MiniMax &rarr; Deepinfra &rarr; MiniMax Portal &rarr; Z.AI
- Video: Google &rarr; Qwen &rarr; Moonshot

To disable auto-detection for a capability:

{
  tools: {
    media: {
      audio: {
        enabled: false,
      },
    },
  },
}

Binary detection is best-effort across macOS/Linux/Windows; ensure the CLI is on PATH (~ is expanded), or set an explicit CLI model entry with a full command path.

Proxy support (audio/video provider calls)

Provider-based audio and video understanding honors standard outbound proxy environment variables, including NO_PROXY/no_proxy bypass rules: HTTPS_PROXY, HTTP_PROXY, ALL_PROXY, https_proxy, http_proxy, all_proxy. Lowercase vars take precedence over uppercase. If none are set, media understanding uses direct egress; if the proxy value is malformed, Apus logs a warning and falls back to direct fetch. Image understanding does not go through this proxy path.

Capabilities

Set capabilities on a models[] entry to restrict it to specific media types. For shared lists, Apus infers defaults per bundled provider:

Provider Capabilities
openai, anthropic, minimax image
minimax-portal image
moonshot image + video
openrouter image + audio
google (Gemini API) image + audio + video
qwen image + video
deepinfra image + audio
mistral audio
zai image
groq, xai, deepgram, senseaudio audio
Any models.providers.<id>.models[] catalog with an image-capable model image

CLI entries require explicit capabilities; entries without valid capability tags are skipped. Provider entries without valid explicit tags use their registered capability metadata.

Provider support matrix

Capability Providers Notes
Image Anthropic, Codex app-server, Deepinfra, Google, MiniMax, MiniMax Portal, Moonshot, OpenAI, OpenAI Codex OAuth, OpenRouter, Qwen, Z.AI, config providers Vendor plugins register image support; openai/* can use API-key or Codex OAuth routing; codex/* uses a bounded Codex app-server turn; image-capable config providers auto-register.
Audio Deepgram, Deepinfra, ElevenLabs, Google, Groq, Mistral, OpenAI, OpenRouter, SenseAudio, xAI Provider transcription (Whisper/Groq/xAI/Deepgram/OpenRouter STT/Gemini/SenseAudio/Scribe/Voxtral).
Video Google, Moonshot, Qwen Provider video understanding via vendor plugins; Qwen video understanding uses the standard DashScope endpoints.

MiniMax note: minimax, minimax-cn, minimax-portal, and minimax-portal-cn image understanding always comes from the plugin-owned MiniMax-VL-01 media provider, even if legacy MiniMax M2.x chat metadata claims image input.

Model selection guidance

Attachment policy

Per-capability attachments controls which attachments are processed:

mode "first" | "all"

Process only the first selected attachment, or all of them.

maxAttachments number

Cap the number processed.

prefer "first" | "last" | "path" | "url"

Selection preference among candidate attachments.

When mode: "all", outputs are labeled [Image 1/2], [Audio 2/2], etc.

Local attachments stay within the session's allowed media roots. Directory aliases such as macOS /tmp and /private/tmp are accepted when the opened file remains inside those roots; they do not grant access to sibling sandbox workspaces.

File-attachment extraction

Vision models and image replay cost

In the Apus-managed reply path, a vision-capable reply model normally receives native images without a separate description pass. Images retained in later requests can contribute to input cost; the amount depends on the provider and the active runtime's context handling. See Session pruning for the applicable cleanup behavior.

For identification or extraction tasks that do not need continued pixel access, an image-capable entry in tools.media.models[] opts into image understanding even when the reply model supports vision. For each selected image that is successfully described, the reply receives description text in place of that native image. This is not a guarantee that every attached image is converted or that later requests contain no images.

{
  tools: {
    media: {
      models: [{ provider: "google", model: "gemini-3-flash-preview", capabilities: ["image"] }],
    },
  },
}

Tradeoffs and boundaries:

Config examples

Shared models + overrides

```json5
{
  tools: {
    media: {
      models: [
        { provider: "openai", model: "gpt-6-astra", capabilities: ["image"] },
        {
          provider: "google",
          model: "gemini-3-flash-preview",
          capabilities: ["image", "audio", "video"],
        },
        {
          type: "cli",
          command: "gemini",
          args: [
            "-m",
            "gemini-3-flash",
            "--allowed-tools",
            "read_file",
            "Read the media at {{AttachmentPath}} and describe it in <= {{MaxChars}} characters.",
          ],
          capabilities: ["image", "video"],
        },
      ],
      audio: {
        attachments: { mode: "all", maxAttachments: 2 },
      },
      video: {
        maxChars: 500,
      },
    },
  },
}
```

Audio + video only

```json5
{
  tools: {
    media: {
      models: [
        {
          provider: "openai",
          model: "gpt-4o-mini-transcribe",
          capabilities: ["audio"],
        },
        {
          type: "cli",
          command: "whisper",
          args: ["--model", "base", "{{AttachmentPath}}"],
          capabilities: ["audio"],
        },
        {
          provider: "google",
          model: "gemini-3-flash-preview",
          capabilities: ["video"],
        },
        {
          type: "cli",
          command: "gemini",
          args: [
            "-m",
            "gemini-3-flash",
            "--allowed-tools",
            "read_file",
            "Read the media at {{AttachmentPath}} and describe it in <= {{MaxChars}} characters.",
          ],
          capabilities: ["video"],
        },
      ],
      audio: {
        enabled: true,
      },
      video: {
        enabled: true,
        maxChars: 500,
      },
    },
  },
}
```

Image only

```json5
{
  tools: {
    media: {
      models: [
        { provider: "openai", model: "gpt-6-astra", capabilities: ["image"] },
        { provider: "anthropic", model: "claude-opus-5", capabilities: ["image"] },
        {
          type: "cli",
          command: "gemini",
          args: [
            "-m",
            "gemini-3-flash",
            "--allowed-tools",
            "read_file",
            "Read the media at {{AttachmentPath}} and describe it in <= {{MaxChars}} characters.",
          ],
          capabilities: ["image"],
        },
      ],
      image: {
        enabled: true,
        maxBytes: 10485760,
        maxChars: 500,
      },
    },
  },
}
```

Multi-modal single entry

```json5
{
  tools: {
    media: {
      models: [
        {
          provider: "google",
          model: "gemini-3.1-pro-preview",
          capabilities: ["image", "video", "audio"],
        },
      ],
    },
  },
}
```

Status output

When media understanding runs, /status includes a per-capability summary line:

๐Ÿ“Ž Media: image ok (openai/gpt-6-astra) ยท audio ok (whisper-cli observed=metal)

Auto-detected local audio tools report their resolved executable path as the result's model, so status and verbose summaries can include that path alongside the tool family and backend. Explicit CLI entries retain their authored command; preflight inventory keeps the logical tool names.

For preflight inventory, run apus capability audio providers. Local rows show the local fallback winner separately from global provider selection, readiness, and separate capable/requested/observed backend fields. The same local selection is available as an informational doctor finding:

apus doctor --lint --only core/doctor/local-audio-acceleration --severity-min info

Notes

Related