Repository navigation
feat(*): let the image role run on an OpenAI-compatible provider - #866
Conversation
The image role in Settings offered OpenRouter alone, though image_generate already speaks the OpenAI Images API on any other base. tools.media.image gains a provider: a section naming one runs on that provider's address and key as one pair, resolved at call time the way the embedding pin is, and its own apiKey and apiBase stand aside. The registry marks the providers the image tool can run on (image_api: OpenRouter, OpenAI, Custom); model.options carries the flag, the roles card offers exactly those, and settings.set refuses any other. Speech and video keep to OpenRouter. Two fixes on the same path: - The OpenRouter key, configured or exported, 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. - Off OpenRouter, every image model goes to the Images API. The chat route with output modalities is OpenRouter's own, and a relay answering it with its own 404 never reached the fallback, so a model outside the name list could not generate there. raven-ppt inherits the host section through the same resolver; when it pins an image setting, the snapshot drops the provider name, keeps the provider's address and key with the bare model id, and does not carry that key to a pinned base. The settings-page specs are amended to the new rule. Co-authored-by: Claude (claude-opus-5-5[1m]) <noreply@anthropic.com>
gloryfromca
left a comment
There was a problem hiding this comment.
Blocking: custom provider headers must reach image requests.
I found one functional blocker. I covered the full diff, AGENTS.md and the Runtime/Web context rules, relevant callers and recent history, backward compatibility, test changes for weakening, and the Live preference/Wire Schema/Source architecture boundaries.
Verification: uv run --all-extras pytest tests/test_config_live.py tests/test_media_gen_tool.py tests/test_tool_capabilities.py tests/test_agents_ppt_launcher.py tests/test_raven_config_tool.py tests/test_rpc_console.py tests/test_rpc_model.py tests/test_config_update_tools.py -x (823 passed); targeted web tests (38 passed); web type-check and generated RPC check passed; Ruff and git diff --check passed. The first plain uv run pytest ... attempt could not start because raven_everos was absent, so I reran with the repository's declared extras. make check-source-language could not run because make is unavailable in this environment.
A provider's connection is its address, its key and its headers, and the image tool took the first two alone: a Custom endpoint that routes or authenticates by header showed in the image picker, saved, and then refused every picture. The media section now carries extraHeaders, taken from the same endpoint the key comes from (the section's flat headers where the endpoint names none), and every media request sends them after the key, the order the chat route gives a provider's extra_headers. A provider with no known address supplies neither key nor headers, a poll or content URL on another origin gets neither, and a raven-ppt section pinned to its own base drops the provider's headers. Co-authored-by: Claude (claude-opus-5-5[1m]) <noreply@anthropic.com>
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
The provider-header fix resolves my prior blocker without exposing the connection material to pinned or cross-origin destinations. I reviewed the new delta and rechecked the full change against AGENTS.md and the Runtime/Web context rules, relevant callers and history, backward compatibility, test changes for weakening, and the Live preference/Wire Schema/Source architecture boundaries. I found no new plain error.
Verification: the affected Python suite passed 830 tests; the targeted web role-picker suite passed 38 tests; web type-check and generated RPC checks passed; Ruff, git diff --check, and the source-language gate passed.
|
Not a blocker -- five things I reproduced at head b3aeb6a while grading. None of them holds the merge button; the separate thread on Severity of this one finding, not a verdict on the pull request. The blocking findings from this pass are review threads on the changed files; GitHub renders those collapsed, as a file name with no text. 1.
2. A case-different name in The docstring promises the extras "win where they name the same one"; for a case-different spelling they collide. 3. The live lane drops a key the boot lane resolves, when the providers subtree spells the provider a second way. The live answer wins at call time, so the tool is registered with a key and then runs without one -- which is what the docstring at schema.py:2741-2742 says must never happen ("never a valid-looking config with the borrowed credential dropped"). schema.py:660 already names 4. The agent is told to set a key it already has, at a path that would change nothing. 5. The raven-ppt snapshot treats Same intent, opposite answers, and the deck draws nothing on the first. Probes were run through the board's pytest wrapper on the merged tree; each measurement above has the control printed beside it. |
gloryfromca
left a comment
There was a problem hiding this comment.
Blocking: PPT_IMAGE_API_KEY must not be overridden by an inherited Authorization header.
The new review evidence changes my earlier clean stance. On this unchanged revision, the launcher replaces apiKey when only PPT_IMAGE_API_KEY is pinned but retains the named provider's extraHeaders; the media header builder then merges those extras last. If they contain Authorization, the host credential wins on the wire and the deck-specific key is silently inert despite being logged as the payer. I am relying on the existing blocker thread for the requested change rather than duplicating its finding or grading its severity.
Verification this turn: uv run --all-extras pytest tests/test_agents_ppt_launcher.py tests/test_media_gen_tool.py -x passed 173 tests. That green suite does not cover the key-only pin plus inherited Authorization combination identified in the open thread.
HTTP header names are case-insensitive and a dict is not, so a media section whose extraHeaders spelled authorization in lower case sent a second Authorization header beside the bearer key instead of replacing it, as the builder's docstring promised. The key's headers now give way to a section header of the same name in any case. Co-authored-by: Claude (claude-opus-5-5[1m]) <noreply@anthropic.com>
The live media reader took the first key spelling the named provider and validated it alone, where ProvidersConfig folds every spelling, former name and field alias into one section. With providers.openai holding the address and providers.OpenAI the key, or a section still saved as zhipu, the tool was registered with a key at boot and then ran without one. The reader now passes every key that may spell the provider through ProvidersConfig itself, and still validates no other provider's section. Co-authored-by: Claude (claude-opus-5-5[1m]) <noreply@anthropic.com>
provider sat on the shared MediaToolConfig, so a speech or video section naming a provider borrowed that provider's address and key on the read path, although the settings writes refuse it and both tools keep to OpenRouter's request shapes. The field moves to an ImageToolConfig subclass: written into the speech or video section it is ignored like any undeclared key by the boot reader and the live reader, which now validates with the class MediaGenConfig declares for the tool's kind, and set_media refuses it there. MediaGenConfig still accepts a plain MediaToolConfig as the image section, and the live reader's kind defaults to the image tool, so a launcher copied out of an older raven keeps its two-argument call. Co-authored-by: Claude (claude-opus-5-5[1m]) <noreply@anthropic.com>
The raven_config tool's describe looked a provider's key up at providers.<name>.apiKey alone, so an install keeping it on an endpoint, or under another spelling, was told to set a key it already had, at a path that would change nothing. It now resolves the section with the media resolver as it would stand once a model is set and asks the tool's own has_key, the rule the doctor rows use, so a key counts wherever the tool would find it, OPENROUTER_API_KEY included. With no key it names the provider rather than one slot. Co-authored-by: Claude (claude-opus-5-5[1m]) <noreply@anthropic.com>
PPT_IMAGE_API_KEY replaced the inherited key but kept the inherited extraHeaders, which the media request builder sends after the key, so an Authorization among them outranked the deck's key on the wire while the launcher logged the deck's key as the payer. The headers complete the host's key at the host's address: a pinned key now leaves them behind, as a pinned base already did, whether they came from a named provider or from the host section itself. Co-authored-by: Claude (claude-opus-5-5[1m]) <noreply@anthropic.com>
With PPT_IMAGE_API_BASE pinned and no deck key, a host image section calling OpenRouter kept the OpenRouter key the host had borrowed for OpenRouter's address, and the deck sent it to the pinned base, as every 0.2 release did. A pinned base now clears whatever key the host resolved, for any section, and 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. Co-authored-by: Claude (claude-opus-5-5[1m]) <noreply@anthropic.com>
When the host's image section named a provider that holds no key, the deck fell through to its key slot and sent the host section's own key to that provider's address: a key the section sets aside for the provider's, which the host's own tool never sends there. On the provider's address the deck now sends the provider's key or none. Co-authored-by: Claude (claude-opus-5-5[1m]) <noreply@anthropic.com>
The launcher read the provider name off the dumped section, so a section naming openrouter, which the console stores verbatim and every other reader folds back into the default, took the named-provider branch and named providers.openrouter as the payer. It now asks media_provider. The launcher can outlive its raven, since a stamped home tree outranks a checkout's own agents/, so it looks media_provider up and keeps the two-argument reader call that every 0.2 release has. Co-authored-by: Claude (claude-opus-5-5[1m]) <noreply@anthropic.com>
|
All five reproduced on b3aeb6a with your inputs before any change. Each is fixed in its own commit, with tests that were red on b3aeb6a.
Verification at the new head: |
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; suggestions only, and they are marked inline.
The PPT key-precedence blocker and the five reported follow-ups are fixed, and this revision can merge as far as I am concerned. I reviewed the complete eight-commit delta, its callers and history, AGENTS.md and Runtime context constraints, backward compatibility, test changes for weakening, and the live-config/provider/PPT boundaries.
Named nonblocking follow-up: the old-runtime compatibility path in configure_image_generation still calls a historical two-argument live_media_tool_config with the whole providers map. In release 3632e604, that second argument is the OpenRouter provider subsection itself; validating the nested map as ProviderConfig yields no apiKey, so a stamped new launcher running on that older Raven loses a borrowed OpenRouter key. The new compatibility test's lambda ignores its second argument and therefore proves only that the call does not raise. This is low-reachability and has working escapes (upgrade Raven or configure/pin the image key), so I am not holding this fourth-round revision for it.
Verification: uv run --all-extras pytest tests/test_agents_ppt_launcher.py tests/test_config_live.py tests/test_config_update_tools.py tests/test_media_gen_tool.py tests/test_raven_config_tool.py tests/test_tool_capabilities.py -x passed 472 tests. Ruff, git diff --check, and the source-language gate passed.
ZuyiZhou
left a comment
There was a problem hiding this comment.
Approved at 1a23ab3. Read the credential path in the non-test diff. A media section takes a named provider address, key and headers as one group from provider_connection, resolved the same way at boot and live. The OpenRouter key is borrowed only by a section whose address parses to openrouter.ai, and OPENROUTER_API_KEY follows the same media_provider test, so a keyless section on another endpoint no longer gets it. Section extraHeaders replace the bearer header case-insensitively, and they ride the same-origin gate as the key for video poll and download. Speech and video have no provider field on any path, and settings.set refuses a provider without image_api. The raven-ppt launcher drops inherited headers and the borrowed key when the deck pins a key or base, and still calls only what every 0.2 release has. All review threads are resolved and all CI checks are green at this head. Not rerun here: the test suites and the live relay run described in the PR. Non-blocking, not verified: a keyless Custom endpoint resolves an empty key, so image_generate may report no key for it until a key is set.
Summary
The image role in Settings offered OpenRouter alone, although
image_generatealready 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-editingtools.media.image.apiKeyandapiBase.tools.media.imagegainsprovider. 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 ofresolve_provider_credentials); the section's ownapiKey,apiBaseandextraHeadersstand aside. Every media request sends the section'sextraHeaders(a new field, secret like a provider's own) after the bearer key, the order the chat route gives a provider'sextra_headers. The registry declares which providers the image tool can run on (ProviderSpec.image_api: OpenRouter, OpenAI, Custom),model.optionscarries the flag per row, the roles card offers exactly those for the image slot, andsettings.setrefuses 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 noproviderfield, so one written there is ignored on every reader, andsettings.setandset_mediarefuse it.Two fixes on the same path:
providers.openrouter.apiKeyorOPENROUTER_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.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 ownagents/, so it calls only what every 0.2 release has: the reader's two-argument form, andmedia_providerlooked 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'sexecute, a_resolve_keyoverride on each tool, and the lines beside the removedborrows_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
Verification
uv run --frozen --python 3.12 --all-extras pytest -q: 27601 passed, 7 failed, 115 skipped. The 7 fail identically onorigin/main3632e60 on this machine (five proxy tests intest_config_update_providers.pyunder the shell's proxy variables,test_install_script.py::test_resolve_node_dir_answers_each_case_it_exists_for, andtest_subagent_node_runtime.py::test_what_cannot_be_read_names_nothingrun 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 --noEmitin ui-web and ui-tui,gen-rpc-client.mjs --check,gen-rpc-types.mjs --check, eslint over the touched ui-web directories, andruff format --check/ruff checkover 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_generatewith the section's ownapiKey/apiBaseposted to/chat/completionsand got 404. After, on an isolatedraven webin 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/generationswith the Custom provider's key.Relevant tests pass locally
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
Security impact considered
Backward compatibility considered
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.providerjoins thesettings.setwhitelist 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
extraHeadersis 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
apiBaseis not OpenRouter's now reports a missing key instead of borrowing the OpenRouter key. A relay in front of OpenRouter at another address needsapiKeyon the section.On such a base, a model outside the Images API name list (a Gemini image model, say) now goes to
/images/generationsinstead of/chat/completions.Choosing a provider other than OpenRouter in Settings sets aside a hand-written
apiKey/apiBaseon the image section; they stay in the file and apply again once an OpenRouter model is picked.A
providerhand-written intotools.media.speechortools.media.videois now ignored: that section runs on OpenRouter, with the OpenRouter key.raven-ppt: a deck pinning
PPT_IMAGE_API_KEYno longer sends the host'sextraHeaders, 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 pinningPPT_IMAGE_API_BASEwithout a key of its own no longer receives the OpenRouter key the host borrowed; it needsPPT_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