Skip to content

feat(agent): support MiniMax image references - #432

Closed
octo-patch wants to merge 1 commit into
EverMind-AI:mainfrom
octo-patch:octo/20260915-image-to-image-tool-recvvcgC9mdbbA
Closed

octo-patch wants to merge 1 commit into
EverMind-AI:mainfrom
octo-patch:octo/20260915-image-to-image-tool-recvvcgC9mdbbA

Conversation

@octo-patch

Copy link
Copy Markdown
Contributor

Reason: Raven's image tool does not send reference images to the MiniMax image-generation API.

Summary

  • Route the configured MiniMax global and China API bases to image generation.
  • Map local paths, data URLs and public reference URLs to character subjects while retaining batching, output naming and workspace restrictions.
  • Resolve MiniMax credentials explicitly and guard generated image downloads.
  • Document configuration for image-01 and image-01-live.

Type

  • Fix
  • Feature
  • Docs
  • CI / tooling
  • Refactor
  • Other

Verification

  • Relevant tests pass locally: uv run pytest tests/test_media_gen_tool.py tests/test_config_provider_passthrough.py -x -q (121 passed).
  • Relevant lint / type checks pass locally: uv run ruff check raven/agent/tools/media_gen.py raven/config/schema.py tests/test_media_gen_tool.py.
  • User-facing docs or screenshots are updated when needed: docs/minimax-images.md.

Risk

  • Security impact considered
  • Backward compatibility considered
  • Rollback path is clear for risky changes

The new route activates only for the two configured MiniMax regional API bases. Reference images are character subjects, not mask edits. API credentials are excluded from image downloads, which retain the public-target checks. Verification uses mocked HTTP responses; no paid generation request was made. Revert this commit to remove the route.

Related Issues

#307

@LivXue
LivXue self-requested a review September 18, 2026 09:14
LivXue added a commit that referenced this pull request Oct 8, 2026
## Summary

The image role in Settings offered OpenRouter alone, although
`image_generate` already speaks the OpenAI Images API
(`/images/generations`, `/images/edits`) on any other base. A user with
an OpenAI or OpenAI-compatible provider could reach it only by
hand-editing `tools.media.image.apiKey` and `apiBase`.

`tools.media.image` gains `provider`. A section naming one runs on that
provider's address, key and headers as one group, taken from the
endpoint the key comes from and resolved at call time the way the
embedding pin is (`provider_connection`, factored out of
`resolve_provider_credentials`); the section's own `apiKey`, `apiBase`
and `extraHeaders` stand aside. Every media request sends the section's
`extraHeaders` (a new field, secret like a provider's own) after the
bearer key, the order the chat route gives a provider's `extra_headers`.
The registry declares which providers the image tool can run on
(`ProviderSpec.image_api`: OpenRouter, OpenAI, Custom), `model.options`
carries the flag per row, the roles card offers exactly those for the
image slot, and `settings.set` refuses any other. An OpenRouter pick
writes an empty provider, so existing sections keep their current
meaning. Speech and video keep to OpenRouter: their sections have no
`provider` field, so one written there is ignored on every reader, and
`settings.set` and `set_media` refuse it.

Two fixes on the same path:

- The OpenRouter key, `providers.openrouter.apiKey` or
`OPENROUTER_API_KEY`, is borrowed only by a section that calls
OpenRouter's address. A keyless section pointed at another endpoint used
to send that endpoint the OpenRouter key as its bearer token.
- Off OpenRouter, every image model goes to the Images API. The chat
route with output modalities is OpenRouter's own; an OpenAI-compatible
relay answered it with its own 404 (`model is not found`), which the
OpenRouter-only fallback did not recognise, so a model outside the
Images API name list could not generate there.

The live config reader resolves the named provider's section exactly as
boot does: every spelling, former name and field alias of it goes
through `ProvidersConfig`, which folds them into one section, and no
other provider's section is validated. A section header replaces the
key's header of the same name in any case.

raven-ppt and raven-design inherit the host's image section through the
same resolver. When raven-ppt pins an image setting, the section becomes
a snapshot read as written: it drops the provider name and takes the
Wire Model that provider uses. A pinned key or base leaves the inherited
headers behind, since they complete the host's key at the host's
address. A pinned base also clears whatever key the host resolved, so
the deck's key slot decides afresh: `PPT_IMAGE_API_KEY`, the host
section's own key, or, toward OpenRouter only, the key that pays for the
words. That closes a leak every 0.2 release has, the OpenRouter key the
host borrowed for OpenRouter's address going to a pinned deck base. On a
named provider's own address the deck sends that provider's key or none.
The launcher can outlive its raven, since a stamped home tree outranks a
checkout's own `agents/`, so it calls only what every 0.2 release has:
the reader's two-argument form, and `media_provider` looked up where it
exists.

The settings dialog's design and acceptance specs (C14, A22, A24 and
their decisions) are amended to the new rule.

Overlap: open #432 and #433 (MiniMax images and video) edit
`_generate_one`, the video tool's `execute`, a `_resolve_key` override
on each tool, and the lines beside the removed `borrows_openrouter_key`.
Their MiniMax routes return before the routing this change touches and
their overrides delegate to the base resolver, so the conflicts are
textual.

## Type

- [ ] Fix
- [x] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

- `uv run --frozen --python 3.12 --all-extras pytest -q`: 27601 passed,
7 failed, 115 skipped. The 7 fail identically on `origin/main` 3632e60
on this machine (five proxy tests in `test_config_update_providers.py`
under the shell's proxy variables,
`test_install_script.py::test_resolve_node_dir_answers_each_case_it_exists_for`,
and
`test_subagent_node_runtime.py::test_what_cannot_be_read_names_nothing`
run as root); 0 introduced.
- ui-web, `node node_modules/vitest/vitest.mjs run
--no-file-parallelism` (both test roots, 214 files): 3188 passed, 0
failed, 0 unhandled errors. Measured before the review commits, which
touch no TypeScript, generated file or RPC contract.
- ui-tui, the same command (144 files): 2089 passed, 9 failed, 0
unhandled errors. The 9 are rendering tests in four files that fail with
identical ids on `origin/main`'s ui-tui on this machine; 0 introduced.
- `tsc --noEmit` in ui-web and ui-tui, `gen-rpc-client.mjs --check`,
`gen-rpc-types.mjs --check`, eslint over the touched ui-web directories,
and `ruff format --check` / `ruff check` over the changed Python files:
all clean.
- `scripts/check_commit_messages.py origin/main..HEAD`, commitlint over
the same range, `scripts/check_source_language.py origin/main...HEAD`,
`scripts/check_large_files.py origin/main...HEAD`: all exit 0.
- The new tests were written first and watched fail for the missing
behaviour, the review rounds' tests on the head before each fix. A
mutation pass then removed each construct this change adds, one at a
time: 40 Python and 3 TypeScript mutants, all killed but one, controls
green. The survivor matches a provider on its name instead of its
`route_names`, which is equivalent today because every former name in
the registry is also a field alias.
- Each commit added in review passes the touched test files on its own.
- Live, against an OpenAI-compatible relay serving
`sensenova-u1.5-lite`: before, `image_generate` with the section's own
`apiKey`/`apiBase` posted to `/chat/completions` and got 404. After, on
an isolated `raven web` in Chromium, the image row offered Custom,
OpenAI and OpenRouter (speech and video OpenRouter only), the typed id
was added to Custom as an image model and saved as `{model, quality,
provider: "custom"}`, and one call through the live config reader
returned a 2048x1536 PNG from `/images/generations` with the Custom
provider's key.

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [ ] User-facing docs or screenshots are updated when needed

No user-facing documentation covers the media tools; the settings
dialog's specs carry the rule.

## Risk

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

- Security: credentials travel to fewer places. A section borrows only
from the provider it names, address and key together, and a provider
with no known address supplies no key. `tools.media.image.provider`
joins the `settings.set` whitelist with the reach the subsystem pins
already have: it moves the tool to a provider the operator configured,
and names no address and carries no key.
- Provider headers can carry credentials: they travel with the key to
the provider's own address only, a video poll or content URL on another
origin gets neither, and a media section's `extraHeaders` is masked like
a provider's. The OpenRouter default lane still borrows only the
OpenRouter key, not that provider's headers.
- A keyless image, speech or video section whose `apiBase` is not
OpenRouter's now reports a missing key instead of borrowing the
OpenRouter key. A relay in front of OpenRouter at another address needs
`apiKey` on the section.
- On such a base, a model outside the Images API name list (a Gemini
image model, say) now goes to `/images/generations` instead of
`/chat/completions`.
- Choosing a provider other than OpenRouter in Settings sets aside a
hand-written `apiKey`/`apiBase` on the image section; they stay in the
file and apply again once an OpenRouter model is picked.
- A `provider` hand-written into `tools.media.speech` or
`tools.media.video` is now ignored: that section runs on OpenRouter,
with the OpenRouter key.
- raven-ppt: a deck pinning `PPT_IMAGE_API_KEY` no longer sends the
host's `extraHeaders`, so an endpoint that also routes by header cannot
take the deck's own key; before this change no header travelled at all.
A deck pinning `PPT_IMAGE_API_BASE` without a key of its own no longer
receives the OpenRouter key the host borrowed; it needs
`PPT_IMAGE_API_KEY`, the host section's own key, or a base on
OpenRouter's address.
- The raven-ppt launcher still renders under every 0.2 release, which
lacks `media_provider`; a test runs it against a reader with the 0.2
signature.
- Rollback: revert the squash commit. The previous config models ignore
a saved `provider`, so the image tool returns to OpenRouter with the
earlier key rules.

## Related Issues

Fixes #828

---------

Co-authored-by: Claude (claude-opus-5-5[1m]) <noreply@anthropic.com>

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: MiniMax credential resolution must reach every image-capability reader.

I found one concrete blocker inline. I covered the repository rules and context map, the full diff, the image tool's callers and relevant history, configuration/live-resolution behavior, backward compatibility, test integrity, and the capability architecture. I also checked the current official MiniMax image-to-image request and response contract; the new endpoint, subject-reference, model, aspect-ratio, and base64 shapes agree with it. No tests were weakened, and the new coverage stays in the canonical media-tool test file.

Verification:

  • uv run pytest tests/test_media_gen_tool.py tests/test_tool_capabilities.py -x: 140 passed.
  • uv run pytest: 17,187 passed, 62 failed, 184 errors, 64 skipped. The full environment lacks optional raven_ppt, raven_design, and raven_everos packages, producing most collection/setup errors; the focused affected suites are green.
  • scripts/check_large_files.py github/main...HEAD: passed.
  • scripts/check_source_language.py github/main...HEAD: passed.

return is_minimax_image_base(getattr(config, "api_base", ""))

@staticmethod
def _resolve_key(config: "MediaToolConfig | None") -> str:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: MiniMax credentials must be visible to every image-capability reader.

This override is used by ImageGenerateTool, but the centralized credential readers in raven/agent/tools/capabilities.py and the routed-product readiness check in raven/agent/loop/wiring.py explicitly call _OpenRouterMediaTool.has_key(...), bypassing it. I reproduced a config using this MiniMax base, model image-01, and only MINIMAX_API_KEY: ImageGenerateTool.has_key(...) is true, while has_credential(...) is false, configured_from(...) is empty, and _lane_generates_images(...) is false. Consequently raven doctor reports a working image setup as credentialless and a product route requiring image generation is refused. Please make the capability/source and route-readiness readers use the image provider's credential chain, with a regression case for the documented environment-only setup.

LivXue added a commit that referenced this pull request Oct 12, 2026
## Summary

The image role in Settings offered OpenRouter alone, although
`image_generate` already speaks the OpenAI Images API
(`/images/generations`, `/images/edits`) on any other base. A user with
an OpenAI or OpenAI-compatible provider could reach it only by
hand-editing `tools.media.image.apiKey` and `apiBase`.

`tools.media.image` gains `provider`. A section naming one runs on that
provider's address, key and headers as one group, taken from the
endpoint the key comes from and resolved at call time the way the
embedding pin is (`provider_connection`, factored out of
`resolve_provider_credentials`); the section's own `apiKey`, `apiBase`
and `extraHeaders` stand aside. Every media request sends the section's
`extraHeaders` (a new field, secret like a provider's own) after the
bearer key, the order the chat route gives a provider's `extra_headers`.
The registry declares which providers the image tool can run on
(`ProviderSpec.image_api`: OpenRouter, OpenAI, Custom), `model.options`
carries the flag per row, the roles card offers exactly those for the
image slot, and `settings.set` refuses any other. An OpenRouter pick
writes an empty provider, so existing sections keep their current
meaning. Speech and video keep to OpenRouter: their sections have no
`provider` field, so one written there is ignored on every reader, and
`settings.set` and `set_media` refuse it.

Two fixes on the same path:

- The OpenRouter key, `providers.openrouter.apiKey` or
`OPENROUTER_API_KEY`, is borrowed only by a section that calls
OpenRouter's address. A keyless section pointed at another endpoint used
to send that endpoint the OpenRouter key as its bearer token.
- Off OpenRouter, every image model goes to the Images API. The chat
route with output modalities is OpenRouter's own; an OpenAI-compatible
relay answered it with its own 404 (`model is not found`), which the
OpenRouter-only fallback did not recognise, so a model outside the
Images API name list could not generate there.

The live config reader resolves the named provider's section exactly as
boot does: every spelling, former name and field alias of it goes
through `ProvidersConfig`, which folds them into one section, and no
other provider's section is validated. A section header replaces the
key's header of the same name in any case.

raven-ppt and raven-design inherit the host's image section through the
same resolver. When raven-ppt pins an image setting, the section becomes
a snapshot read as written: it drops the provider name and takes the
Wire Model that provider uses. A pinned key or base leaves the inherited
headers behind, since they complete the host's key at the host's
address. A pinned base also clears whatever key the host resolved, so
the deck's key slot decides afresh: `PPT_IMAGE_API_KEY`, the host
section's own key, or, toward OpenRouter only, the key that pays for the
words. That closes a leak every 0.2 release has, the OpenRouter key the
host borrowed for OpenRouter's address going to a pinned deck base. On a
named provider's own address the deck sends that provider's key or none.
The launcher can outlive its raven, since a stamped home tree outranks a
checkout's own `agents/`, so it calls only what every 0.2 release has:
the reader's two-argument form, and `media_provider` looked up where it
exists.

The settings dialog's design and acceptance specs (C14, A22, A24 and
their decisions) are amended to the new rule.

Overlap: open #432 and #433 (MiniMax images and video) edit
`_generate_one`, the video tool's `execute`, a `_resolve_key` override
on each tool, and the lines beside the removed `borrows_openrouter_key`.
Their MiniMax routes return before the routing this change touches and
their overrides delegate to the base resolver, so the conflicts are
textual.

## Type

- [ ] Fix
- [x] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

- `uv run --frozen --python 3.12 --all-extras pytest -q`: 27601 passed,
7 failed, 115 skipped. The 7 fail identically on `origin/main` 3632e60
on this machine (five proxy tests in `test_config_update_providers.py`
under the shell's proxy variables,
`test_install_script.py::test_resolve_node_dir_answers_each_case_it_exists_for`,
and
`test_subagent_node_runtime.py::test_what_cannot_be_read_names_nothing`
run as root); 0 introduced.
- ui-web, `node node_modules/vitest/vitest.mjs run
--no-file-parallelism` (both test roots, 214 files): 3188 passed, 0
failed, 0 unhandled errors. Measured before the review commits, which
touch no TypeScript, generated file or RPC contract.
- ui-tui, the same command (144 files): 2089 passed, 9 failed, 0
unhandled errors. The 9 are rendering tests in four files that fail with
identical ids on `origin/main`'s ui-tui on this machine; 0 introduced.
- `tsc --noEmit` in ui-web and ui-tui, `gen-rpc-client.mjs --check`,
`gen-rpc-types.mjs --check`, eslint over the touched ui-web directories,
and `ruff format --check` / `ruff check` over the changed Python files:
all clean.
- `scripts/check_commit_messages.py origin/main..HEAD`, commitlint over
the same range, `scripts/check_source_language.py origin/main...HEAD`,
`scripts/check_large_files.py origin/main...HEAD`: all exit 0.
- The new tests were written first and watched fail for the missing
behaviour, the review rounds' tests on the head before each fix. A
mutation pass then removed each construct this change adds, one at a
time: 40 Python and 3 TypeScript mutants, all killed but one, controls
green. The survivor matches a provider on its name instead of its
`route_names`, which is equivalent today because every former name in
the registry is also a field alias.
- Each commit added in review passes the touched test files on its own.
- Live, against an OpenAI-compatible relay serving
`sensenova-u1.5-lite`: before, `image_generate` with the section's own
`apiKey`/`apiBase` posted to `/chat/completions` and got 404. After, on
an isolated `raven web` in Chromium, the image row offered Custom,
OpenAI and OpenRouter (speech and video OpenRouter only), the typed id
was added to Custom as an image model and saved as `{model, quality,
provider: "custom"}`, and one call through the live config reader
returned a 2048x1536 PNG from `/images/generations` with the Custom
provider's key.

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [ ] User-facing docs or screenshots are updated when needed

No user-facing documentation covers the media tools; the settings
dialog's specs carry the rule.

## Risk

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

- Security: credentials travel to fewer places. A section borrows only
from the provider it names, address and key together, and a provider
with no known address supplies no key. `tools.media.image.provider`
joins the `settings.set` whitelist with the reach the subsystem pins
already have: it moves the tool to a provider the operator configured,
and names no address and carries no key.
- Provider headers can carry credentials: they travel with the key to
the provider's own address only, a video poll or content URL on another
origin gets neither, and a media section's `extraHeaders` is masked like
a provider's. The OpenRouter default lane still borrows only the
OpenRouter key, not that provider's headers.
- A keyless image, speech or video section whose `apiBase` is not
OpenRouter's now reports a missing key instead of borrowing the
OpenRouter key. A relay in front of OpenRouter at another address needs
`apiKey` on the section.
- On such a base, a model outside the Images API name list (a Gemini
image model, say) now goes to `/images/generations` instead of
`/chat/completions`.
- Choosing a provider other than OpenRouter in Settings sets aside a
hand-written `apiKey`/`apiBase` on the image section; they stay in the
file and apply again once an OpenRouter model is picked.
- A `provider` hand-written into `tools.media.speech` or
`tools.media.video` is now ignored: that section runs on OpenRouter,
with the OpenRouter key.
- raven-ppt: a deck pinning `PPT_IMAGE_API_KEY` no longer sends the
host's `extraHeaders`, so an endpoint that also routes by header cannot
take the deck's own key; before this change no header travelled at all.
A deck pinning `PPT_IMAGE_API_BASE` without a key of its own no longer
receives the OpenRouter key the host borrowed; it needs
`PPT_IMAGE_API_KEY`, the host section's own key, or a base on
OpenRouter's address.
- The raven-ppt launcher still renders under every 0.2 release, which
lacks `media_provider`; a test runs it against a reader with the 0.2
signature.
- Rollback: revert the squash commit. The previous config models ignore
a saved `provider`, so the image tool returns to OpenRouter with the
earlier key rules.

## Related Issues

Fixes #828

---------

Co-authored-by: Claude (claude-opus-5-5[1m]) <noreply@anthropic.com>
if aspect_ratio:
body["aspect_ratio"] = aspect_ratio
if images:
body["subject_reference"] = [

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nonblocking - cap the references this route sends. _via_minimax turns every entry of images into a subject_reference item with no limit: one call with 20 references at this head posted all 20 to /image_generation (with httpx stubbed). The two routes beside it cap theirs, the chat route at 6 (images[:6], line 661) and the Images API at 16 ((images or [])[:16], line 807). MiniMax's image generation guide says only a single reference image is supported per request, while its API reference sets no maximum on the array, so what the service does with the other entries is undocumented. A cap here at the number MiniMax documents, stated in the parameter description (see the note at line 648), keeps that outcome defined.

json=body,
)
response.raise_for_status()
data = response.json()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nonblocking - pass the MiniMax response to _record_usage, as the other routes do. The response read here never reaches the usage recorder: with a recorder attached to the tool, one successful MiniMax call recorded 0 snapshots at this head. Both sibling routes record every response (_chat at line 234, _via_images_api at line 855). The recorder the agent loop attaches (_record_image_usage in loop/wiring.py) hands each snapshot to the usage_tracker strategy, which counts it as a call and appends a row to that day's usage-YYYY-MM-DD.jsonl, so a MiniMax image call is missing from the tracker's totals and from the telemetry file. MiniMax's response carries no usage block, but image_usage already leaves absent token counts and cost empty instead of inventing them, so recording this response would count the call and nothing more. test_image_usage_reaches_runtime_tracker_and_telemetry covers the existing routes (chat, openrouter, generation, edit, fallback) and has no MiniMax case.

model_id = (
getattr(config, "model", "") or model or ("image-01" if self._uses_minimax(config) else self.default_model)
)
if self._uses_minimax(config):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nonblocking - say what images does when this branch is taken. This branch sends images to MiniMax as subject references, whose only documented type is character (a portrait), but every description the model reads still presents them as images to edit: "optionally editing/varying input images" (line 404), "Optional input images to edit/vary ... (max 6 for chat-routed models, 16 for gpt-image models)" (lines 425-426) and "Input images to edit/vary for this picture only" (line 482), none of which this diff touches. On a MiniMax section the model therefore has neither this route's behaviour nor its limit to plan with. The descriptions are class attributes shared by every route, so either wording that holds on all of them or a description that follows the configured route would do.

@LivXue

LivXue commented Oct 12, 2026

Copy link
Copy Markdown
Member

No blockers in the reviewed scope.

Scope: I reviewed only the raven/agent/** portion of this PR -- 1 of 4 changed files (raven/agent/tools/media_gen.py). The remaining 3 (raven/config/schema.py, docs/minimax-images.md and tests/test_media_gen_tool.py) were read as context but are not covered by this verdict. The verdict covers the three notes below only: the Blocking note at media_gen.py:500 stands on its own and is not repeated here. I reproduced its mechanism at this head: for an image section on the MiniMax base with model image-01 and only MINIMAX_API_KEY set, ImageGenerateTool.has_key is true while _OpenRouterMediaTool.has_key and the capability readers has_credential and configured_from find no key.

What I examined: the new MiniMax route end to end (_uses_minimax, the key override, _via_minimax and its reply handling) beside the chat and Images API routes; the descriptions the model reads; the usage recorder the agent loop attaches to the image tool; and this branch against current main.

Findings filed inline:

  1. References are forwarded without a cap (media_gen.py:740).
  2. The MiniMax response never reaches the usage recorder (media_gen.py:750).
  3. The descriptions the model reads still present images as edit inputs with the other routes' limits (media_gen.py:648).

Verification:

  • python -m pytest tests/test_media_gen_tool.py tests/test_tool_capabilities.py at this head: 140 passed.
  • MiniMax route at this head, with httpx stubbed: one call with 20 references posted to /image_generation with 20 subject_reference entries and returned success; a usage recorder attached to the tool received 0 snapshots.
  • Credential readers at this head: the result quoted in the scope paragraph.
  • No live MiniMax call on my side.

Checked and deliberately not reported: a non-zero base_resp.status_code and an empty data both come back to the model as errors rather than success, and URL outputs are fetched through guarded_fetch.

Rebasing onto main: this branch no longer merges (3 files, 5 conflicting regions against 562c8457c), and main has moved in ways the conflicts alone do not show:

  • borrows_openrouter_key no longer exists on main, so the schema.py change to it has nothing to attach to.
  • _generate_one on main sends every address other than OpenRouter's through the Images API (media_gen.py:684 on main), so the MiniMax branch has to be taken before that line.
  • Every API request in media_gen.py on main takes its headers from _headers(), which adds the section's extraHeaders; _via_minimax builds its own Authorization header, so a MiniMax section's extra headers would not be sent.
  • The image section now has a provider field (feat(*): let the image role run on an OpenAI-compatible provider #866). MiniMax can stay detected by its base URL, or be named there (the registry already has minimax and minimax_cn_api). On either path the environment-only setup in docs/minimax-images.md needs its own fallback: a section naming minimax with only MINIMAX_API_KEY exported gets MiniMax's address and an empty key on main, since provider_connection reads keys from the provider's config section only.
  • feat(*): let the image role run on an OpenAI-compatible provider #866 added a third caller of the base check, _media_needs at raven/agent/tools/raven_config.py:342, beside the two in the Blocking note; it needs the same answer as the tool.

@LivXue LivXue left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes on the raven/agent/** slice of this PR: the branch needs a rebase onto main, and the Blocking note at media_gen.py:500 stands. 3 nonblocking notes are filed inline on raven/agent/tools/media_gen.py; the scope statement, the verification log, and notes for the rebase are in the PR-level summary comment.

}

@staticmethod
def _uses_minimax(config: "MediaToolConfig | None") -> bool:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker - route MiniMax through the image section's provider instead of recognising its base URL. This revision decides that a section is MiniMax by matching api_base against two fixed addresses (_uses_minimax here, is_minimax_image_base in raven/config/schema.py) and gives it a key chain of its own (the _resolve_key and _no_key_error overrides below, with MINIMAX_API_KEY as the fallback). Since #866 the image section names the provider it runs on (tools.media.image.provider), and borrow_media_credentials fills in that provider's address, key and headers from providers.<name> as one group; main's _no_key_error already words its message by that provider, and every credential reader reads the borrowed section. Measured on main 146bba8d8a8a: a section with provider: "minimax" and its key under providers.minimax resolves to https://api.minimax.io/v1 with that key ("minimax_cn_api" to https://api.minimaxi.com/v1), and has_credential, configured_from (borrowed: providers.minimax.apiKey), the section the lane check reads (live_media_tool_config) and raven_config's _media_needs all report the key, with no change to any of them -- the disagreement the Blocking note at line 500 describes does not arise on this route. Please select the MiniMax branch by the section's provider (media_provider), take the key, address and headers from the borrowed section (_headers()), and drop the base-URL test and the MiniMax-only key chain.

Three things on main are part of this change. The image section accepts only providers the registry marks image_api, and image_api is defined as serving OpenAI's Images API, which MiniMax does not: the settings handler refuses provider: "minimax" today ("the image tool runs on Custom, OpenRouter, OpenAI, not 'minimax'"), and raven_config offers the same three. Whether image_api widens to cover MiniMax with the request shape chosen per provider, or MiniMax gets a field of its own, is open. _generate_one on main sends every address other than OpenRouter's to the Images API (media_gen.py:684 on main) -- a call through either route posted to /images/generations in the same measurement -- so the MiniMax branch has to come before that line. And on this route the environment-only setup in docs/minimax-images.md yields no key, because provider_connection reads a provider's key from its config section only, so the page needs to describe the key under providers.minimax.

@LivXue

LivXue commented Oct 12, 2026

Copy link
Copy Markdown
Member

Blocking: 1 finding needs correction before this revision can merge; see the inline notes.

This supersedes my earlier summary on this PR, which opened "No blockers in the reviewed scope.": I have since settled how MiniMax is configured here, and that is filed as a Blocking note at raven/agent/tools/media_gen.py:494. The three nonblocking notes from that round (lines 740, 750 and 648) stand as they were.

Scope: unchanged. I reviewed only the raven/agent/** portion of this PR -- 1 of 4 changed files (raven/agent/tools/media_gen.py). The remaining 3 were read as context but are not covered by this verdict. The Blocking note at media_gen.py:500 stands on its own and is not repeated here.

Confirmed findings:

Verification:

  • On main 146bba8d8a8a, with httpx stubbed: an image section with provider: "minimax" and the key under providers.minimax resolved to https://api.minimax.io/v1 with that key, and has_credential, configured_from, live_media_tool_config and _media_needs all reported it; "minimax_cn_api" resolved to https://api.minimaxi.com/v1. With only MINIMAX_API_KEY set, no key resolved. A call posted to /images/generations on both routes. The settings handler refused provider: "minimax".
  • No live MiniMax call on my side.

Notes for the rebase, updated from the earlier summary:

  • Its bullet on the image section's provider named two paths and said the environment-only setup needs its own fallback on either. It is now this one path, on which the three credential readers, _media_needs included, need no change, and docs/minimax-images.md describes the key under providers.minimax instead (see the inline note).
  • Still to carry over: the MiniMax branch comes before _generate_one's Images API dispatch (line 684 on main); _via_minimax takes its headers from _headers(), which on this route carries the provider's extraHeaders; and is_minimax_image_base goes with the base-URL test, as does the schema.py change to borrows_openrouter_key, which no longer exists on main.

@LivXue LivXue left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes: 1 blocking note is filed inline on raven/agent/tools/media_gen.py -- route MiniMax through the image section's provider instead of recognising its base URL. The PR-level summary posted with it supersedes my earlier one and carries the verification and the notes for the rebase.

@LivXue

LivXue commented Oct 19, 2026

Copy link
Copy Markdown
Member

Closing in favour of #1013, which carries this PR and #433 together.

Your commit is its first commit, authored by you and replayed onto main: one test now calls borrow_media_credentials, the helper #866 put in place of borrow_openrouter_key, with your assertions unchanged. On top of it the PR moves the image path onto the provider route this review asked for: the image section names minimax or minimax_cn_api and borrows that provider's key. It also sends one reference per request, records the call as usage, and describes what MiniMax takes only to a model that runs on it. Its description maps every note filed here to where it went.

Thank you for the contribution.

@LivXue LivXue closed this Oct 19, 2026
0xKT pushed a commit that referenced this pull request Oct 21, 2026
#1013)

## Summary

Runs the image and video roles on MiniMax. This carries octo-patch's
#432 (MiniMax image references) and #433 (MiniMax image-to-video) and
reworks both onto the provider route the review settled on #432: MiniMax
is a provider a media section names, not an address or a model name the
tool recognises.

- The registry marks `minimax` and `minimax_cn_api` as `image_api` and,
new, `video_api`. A section naming either borrows that provider's
address, key and headers through `borrow_media_credentials`, and the
tool dispatches on `media_provider()` to MiniMax's own APIs.
`MINIMAX_API_KEY` is not read for these tools, so the tool, the
capability report and doctor resolve one key, which settles the blocking
note on #432 about the readers.
- Images: `POST /v1/image_generation`. An input image is the character
to keep (`subject_reference`), one per request as MiniMax documents. A
`base_resp` status is the error class with MiniMax's sentence as the
detail, a picture MiniMax's content check withheld is a refusal, the
call is recorded as usage, a picture link goes through the guarded
download, and the file takes the format its bytes declare.
- Video: the video section gains `provider` (`VideoToolConfig`), the
settings save and `raven_config` accept a provider marked `video_api`,
`model.options` carries the flag (both generated clients regenerated),
and the roles card offers the video role those providers. A video pick
writes the provider it runs on, empty for OpenRouter.
- On MiniMax, MiniMax-H3 and MiniMax-H3-Max take `/v2/video_generation`
and the other models `/v1/video_generation` with its query and file
handoff. A call is text-to-video unless it gives `first_frame_image` (a
local file under the image tool's workspace rule, a URL or a data URL).
A params key the model family does not take, a ratio beside a first
frame, and `adaptive` on a text-only call are refused before anything is
sent, since MiniMax would ignore the first two silently and takes
`adaptive` only beside a frame; a concrete ratio is passed through. The
finished video is downloaded through the guarded fetch, without the key.
- Off MiniMax, and read off the class, both tools' schemas are
byte-identical to main's. On MiniMax they say what MiniMax takes: the
tools author `to_schema`, so the registry serves them per turn, and the
search and index readers of `description` and `parameters` get the same
per-provider answer.
- A capability row on a named provider names `providers.<name>.apiKey`,
with no OpenRouter variable or credit note, and doctor prints no empty
"or env:" line. `docs/minimax-media.md` replaces the two pages the PRs
added, the settings design records carry a dated amendment, and the
changelog has the entry.
- Raven-PPT draws with the host's image tool, so an unpinned deck takes
MiniMax's route from the host. A deck pinned by `PPT_IMAGE_MODEL`,
`PPT_IMAGE_API_BASE` or `PPT_IMAGE_API_KEY` used to drop the provider's
name, since the deck would have resolved it against the providers its
chat runs on, and with the name it lost MiniMax's request shape: the
OpenAI Images path on MiniMax's address, which MiniMax answers with 404.
A pinned section on a named provider now keeps the name in a file of its
own beside the render (`product_render.write_rendered_part`), with that
provider's endpoint: owner-only, named by the render's pid so the same
sweep removes it, and resolved there, so the deck's chat providers are
untouched. New `PPT_IMAGE_PROVIDER` puts a deck's pictures on a provider
other than the host image role's; it needs `PPT_IMAGE_MODEL`, carries
none of the host's endpoint across, and a name the image tool does not
run on draws nothing, with the launch log saying what to set. A
`PPT_IMAGE_API_BASE` without it is an OpenAI-compatible endpoint, as
before.
- The routing gate that decides whether a lane can draw reads the same
pin. With `<PREFIX>_IMAGE_PROVIDER` set it answers for that provider, by
the launcher's rule: no for a provider the image tool cannot run on and
for a switch without a model, yes for the lane's own image key, no for a
base of its own without one, and otherwise the host's live key for that
provider. It never counts the host image role's key for another
provider, and the key that pays for the lane's words is lent only
towards OpenRouter. An unpinned lane is answered as before. The gate
stays read-only. The rule is stated twice because a launcher can run on
an older raven, so one table drives both sides: the gate's test, and a
launcher test that renders each row and asserts the gate opens exactly
where a picture request goes out.
- A launch refused after the deck's image section was written to its own
file (at the ACP home, or a failed final write) now removes that file;
it is named by the launcher's pid, so no other run's is touched. On a
raven from before render parts, a `PPT_IMAGE_PROVIDER` switch renders
inline instead of raising.

The two carried commits keep octo-patch's authorship. Replaying them
onto main needed one adaptation each, named in its commit body, because
#866 replaced `borrow_openrouter_key` with `borrow_media_credentials`
and removed `borrows_openrouter_key`. What the reviews filed, and where
it went:

- #432 provider-route blocker, and the readers blocker: provider route,
one key resolution.
- #432 references uncapped / no usage / description: one per request /
recorded / per-provider schema.
- #433 resolution held to 2K and V2 picked by equality: the client-side
value checks are gone (per-model tables, and MiniMax answers a value
outside them with an error); V2 is a membership test over MiniMax-H3 and
MiniMax-H3-Max.
- #433 readers, static capability row, duplicated MiniMax test: provider
route and provider-aware rows.
- #433 unknown params dropped silently, ratio beside a frame, inline
origin check, params text written for Kling: refused, refused, guarded
download, per-provider schema.
- Review follow-up: a text-only V2 call that names `adaptive` is refused
before a request is sent, since `options` spread over the default;
covered for both providers and both V2 models.
- #433 conflict with #432 in `borrows_openrouter_key`: gone with that
function.
- Review follow-up, four non-blocking notes: a provider switch on a
raven without render parts no longer raises; the routing gate reads the
provider pin as the launcher does (a deck pinned to MiniMax on the
host's MiniMax key was refused, and four misconfigured pins were
admitted); a refused launch leaves no image file behind; a test ties the
providers offered for video to the ones the video tool has a path for.

Known gaps, checked and not changed here:

- The deck's `ppt_generate_image` describes `references` as up to six
pictures to match or vary, and on MiniMax the image tool sends the first
alone, as the character to keep. Its schema does not vary by provider
the way `image_generate`'s now does; that belongs with the deck engine,
not here.
- A hand-written video `provider` the video tool has no request path for
is refused in a sentence on every call, while doctor still reports the
key it borrows. The settings save and `raven_config` refuse such a
provider.
- The video download goes through the guarded fetch, which refuses an
address a fake-IP resolver hands out, so behind one a MiniMax video
arrives as "generated, but the video URL was refused or could not be
fetched". The image route asks for base64 and is unaffected.
- The gate mirrors the launcher in two places only approximately, both
rare and both closing rather than opening a route: a lane pinned to the
host's own image provider with a base of its own and no key, on a host
whose image section holds a key of its own, is refused though the
launcher would send that key; and a switch to OpenRouter on the host's
chat reads the host's `providers.openrouter` key, where the launcher
takes any host provider block that addresses OpenRouter.
- A launcher killed outright (SIGKILL) after writing the deck's image
file still leaves it until the next successful launch's sweep, as
before.
- No authenticated MiniMax call was made. Requests and answers follow
MiniMax's API reference and are mocked; the routes were checked against
MiniMax's live API with no key, on `api.minimax.io` and
`api.minimaxi.com`: `POST /v1/image_generation` answers its
authentication error, `POST /v1/images/generations` the same 404 as a
made-up path.

## Type

- [ ] Fix
- [x] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

The full-suite counts below were measured on origin/main ba3a3f8 with
this branch applied, on Linux with Python 3.12, and were not re-measured
since. The branch was rebased onto 55c145c and then onto 05cf9c3,
without conflicts and with every earlier commit patch-identical; the
review follow-up was verified at 05cf9c3:

- `pytest -n 0 tests/test_agents_ppt_launcher.py
tests/test_subagent_routed_lane_readiness.py
tests/test_subagent_vendored_agents.py tests/test_media_gen_minimax.py
tests/test_media_gen_tool.py tests/test_rpc_model.py`: 703 passed, 28 of
them new, in a venv built by `uv sync --frozen --python 3.12
--all-extras`; `tests/test_config_product_render.py
tests/test_subagent_routing_backend.py`: 213 passed, 3 skipped.
- Each new regression was watched failing before its fix: the provider
switch raised `KeyError`, the refused and failed launches left the image
file, and five of the gate table's rows disagreed with the launcher.
Five mutants of the fixes (the switch's default, the cleanup, the gate
falling back to the host image key, the OpenRouter loan on the lane's
own key, the chat key lent to another provider) each turned their tests
red; the video-set test turned red when one side of the set was changed.
The gate's table run with file writes, sockets and key output trapped
stayed clean, beside a control mutant for each trap.
- At 05cf9c3: `ruff check`, `ruff format --check`, `ty check`,
`lint-imports` (10 contracts kept), `scripts/check_commit_messages.py`,
`commitlint`, `scripts/check_large_files.py` and
`scripts/check_source_language.py` over the 21 commits: pass, the last
beside a CJK control commit it refuses, and commitlint beside a header
it refuses.

Earlier rounds:

- `uv run --frozen --python 3.12 --all-extras pytest -q -p no:randomly`:
33584 passed, 178 skipped, 2 xfailed, 8 failed. The same 8 fail on clean
ba3a3f8 on this machine: five proxy tests in
`tests/test_config_update_providers.py`,
`test_install_script.py::test_resolve_node_dir_answers_each_case_it_exists_for`,
`test_knowledge_parser_excel.py::test_a_legacy_workbook_is_converted_and_read`
and
`test_subagent_node_runtime.py::test_what_cannot_be_read_names_nothing`.
- `ruff check` and `ruff format --check` over `raven evolver agents
plugins-dist tests scripts docs-site`, `lint-imports` (10 contracts
kept), `deptry raven` and `ty check raven evolver agents plugins-dist
scripts`: all pass.
- `scripts/check_large_files.py` and `scripts/check_source_language.py`
over `origin/main...` this branch: exit 0, each beside a control commit
it refuses (a 2.1 MiB file; a CJK comment in `media_gen.py`).
- `scripts/check_commit_messages.py` and `commitlint --config
commitlint.config.cjs` over the fifteen commits then on the branch:
pass.
- ui-web: `npm run gen:check` (generated.ts matches the contract, 245
methods), `npm run lint` (0 errors; 4 warnings, all in files this PR
does not touch), `npm run type-check`, and `npx vitest run
--maxWorkers=8`: 251 files, 4159 tests passed. On this 32-core machine
vitest's default worker count failed a varying set of tests on clean
main a8455ba too, mostly 5 s timeouts, each passing when its file ran
alone.
- ui-tui: `npm run lint`, `lint:rpc`, `lint:i18n` and `type-check` pass.
`npx vitest run --maxWorkers=8`: 2233 passed, 13 skipped, 9 failed, the
same 9 (in `paintDump`, `intensityTransition`, `textInputTypingBurst`
and `userSlabRule`) that fail on clean ba3a3f8 here.
- Off MiniMax, and read off the class, both tools' schemas were compared
with main's module for a section on no provider, `openrouter`, `openai`
and `volcengine`: byte-identical.
- Mutation check: each behaviour this PR adds was removed one at a time
(27 in the tools and settings, 17 in the deck launcher and its render
parts, 2 in `Roles.tsx`), and every mutant turned at least one test red,
beside an unmutated run that is green. The three on the text-only ratio
refusal are the guard, the default it fills and the direction of the
comparison.

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [x] User-facing docs or screenshots are updated when needed

## Risk

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

A section naming no provider, or one on another provider, behaves as on
main, and both tools show it main's schema. A config that wrote
`provider` into the video section by hand was ignored on main and is
honoured now: on a provider without a video path every call is refused.
MiniMax keys travel only to the address of the provider section that
holds them. A deck pinned on a named provider now reads its image
section from an owner-only file beside its render, which holds the image
key as the render does; an unpinned deck, and one pinned to a base
without a provider, renders as before. A route into a lane that pins
`<PREFIX>_IMAGE_PROVIDER` can now open or close differently from before:
it opens on the host's key for the pinned provider, and closes where the
launcher would draw nothing. Revert the squash commit to remove the
routes; a config naming MiniMax for a media role then fails the settings
save again.

## Related Issues

Supersedes #432 and #433; both are carried here.

---------

Co-authored-by: octo-patch <266937838+octo-patch@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants