Repository navigation
feat(agent): support MiniMax image references - #432
octo-patch wants to merge 1 commit into
Conversation
## 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
left a comment
There was a problem hiding this comment.
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 optionalraven_ppt,raven_design, andraven_everospackages, 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: |
There was a problem hiding this comment.
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.
## 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"] = [ |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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.
|
No blockers in the reviewed scope. Scope: I reviewed only the What I examined: the new MiniMax route end to end ( Findings filed inline:
Verification:
Checked and deliberately not reported: a non-zero Rebasing onto main: this branch no longer merges (3 files, 5 conflicting regions against
|
LivXue
left a comment
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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.
|
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 Scope: unchanged. I reviewed only the Confirmed findings:
Verification:
Notes for the rebase, updated from the earlier summary:
|
LivXue
left a comment
There was a problem hiding this comment.
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.
|
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 Thank you for the contribution. |
#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>
Reason: Raven's image tool does not send reference images to the MiniMax image-generation API.
Summary
Type
Verification
uv run pytest tests/test_media_gen_tool.py tests/test_config_provider_passthrough.py -x -q(121 passed).uv run ruff check raven/agent/tools/media_gen.py raven/config/schema.py tests/test_media_gen_tool.py.docs/minimax-images.md.Risk
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