Skip to content

feat(*): let the image role run on an OpenAI-compatible provider - #866

Merged
LivXue merged 10 commits into
mainfrom
feat/image_role_provider_choice
Oct 8, 2026
Merged

LivXue merged 10 commits into
mainfrom
feat/image_role_provider_choice

Conversation

@LivXue

@LivXue LivXue commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

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
  • 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.

  • 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.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

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>
@LivXue
LivXue requested review from 0xKT and gloryfromca October 6, 2026 16:38

@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: 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.

Comment thread raven/config/schema.py Outdated
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 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.

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.

Comment thread agents/raven-ppt/run.py
@0xKT

0xKT commented Oct 7, 2026

Copy link
Copy Markdown
Member

Not a blocker -- five things I reproduced at head b3aeb6a while grading. None of them holds the merge button; the separate thread on agents/raven-ppt/run.py does.

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. tools.media.speech.provider and .video.provider are honoured, against this PR's own sentence.
provider lands on the shared MediaToolConfig (schema.py:1165), and effective_media_config (schema.py:2371-2373) runs borrow_media_credentials over all three tools without consulting image_api or the tool kind. Measured:

providers.openai = {apiKey: sk-oa, apiBase: https://relay.test/v1}
tools.media.speech = {model: m, provider: openai}
-> speech resolved: api_base='https://relay.test/v1' api_key='sk-oa'   (video identical)

SpeechGenerateTool then posts modalities:["audio","text"] + stream:true there, and VideoGenerateTool posts {base}/videos. schema.py:1176-1177 says "speech and video keep to OpenRouter's request shapes", and the amended spec row says "speech and video still run only against OpenRouter". Both write paths refuse it (_chk_media_selection, the roles card); the read path does not, so a hand-written section gets through. Gating the borrow on the tool kind would make the three sentences true.

2. A case-different name in extraHeaders duplicates the Authorization header instead of replacing it.
_headers (media_gen.py:188-196) is a plain dict merge, and HTTP header names are case-insensitive while a Python dict is not:

extra_headers = {"authorization": "Bearer sk-LOWER"}, api_key = "sk-real"
dict      -> {'Authorization': 'Bearer sk-real', 'Content-Type': ..., 'authorization': 'Bearer sk-LOWER'}
on the wire -> authorization: ['Bearer sk-real', 'Bearer sk-LOWER']     (two headers)
CONTROL, {"Authorization": ...} -> ['Bearer sk-SAME']                   (replaces, as documented)

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.
_provider_section (schema.py:2656) resolves a name by exact key, then one names_same_provider scan, and returns the first match without merging -- where ProvidersConfig.get and update_providers._raw_section both fold a provider's spellings together. Measured through the two production readers (Config.effective_media_config() and config.live.media_tool_config):

providers {"openai": {apiBase: https://relay.test/v1}, "OpenAI": {apiKey: sk-oa}}, image.provider=openai
  boot ('https://relay.test/v1', 'sk-oa')   live ('https://relay.test/v1', '')
providers {"zhipu": {apiKey: sk-z, apiBase: https://z.test/v1}}, image.provider=zai
  boot ('https://z.test/v1', 'sk-z')        live ('https://api.z.ai/api/paas/v4', '')
CONTROL, single canonical spelling: boot == live

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 ProvidersConfig.get as "the only place a provider name may be resolved to its config".

4. _media_needs names a slot that would not be read, on an install configured through endpoints[].
raven_config.py:341 asks providers.<slug>.apiKey flat, while the resolver goes through provider_connection -> provider_endpoints:

providers.openai.endpoints[0] = {label: main, apiBase: https://relay.test/v1, apiKey: sk-ep}
resolver -> key='sk-ep'
describe -> "it also needs a key: providers.openai.apiKey"
CONTROL, flat providers.openai.apiKey=sk-flat -> "a key it can use is already set"

The agent is told to set a key it already has, at a path that would change nothing.

5. The raven-ppt snapshot treats provider: "openrouter" as a named provider, so spelling the default out loud loses the key.
run.py:247 reads named = image.get("provider", "") off the dumped section, where every other reader canonicalises and folds openrouter back into the default (media_provider, schema.py:2602). With PPT_IMAGE_API_BASE pinned:

host image {model: m, provider: "openrouter"} -> {'apiKey': '', 'model': 'm', 'apiBase': 'https://deck.test/v1'}
host image {model: m}                         -> {'apiKey': 'sk-or', 'model': 'm', 'apiBase': 'https://deck.test/v1'}

Same intent, opposite answers, and the deck draws nothing on the first. _chk_image_provider (console.py:948-963) stores "openrouter" verbatim when a client asks for it, so the spelling is reachable without hand-editing; the web roles card happens to write "" instead.

Probes were run through the board's pytest wrapper on the merged tree; each measurement above has the control printed beside it.

@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: 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.

LivXue and others added 8 commits October 7, 2026 07:39
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>
@LivXue

LivXue commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

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.

  1. Speech and video honoured provider (7b73d1c). provider moves from MediaToolConfig to an ImageToolConfig subclass, so a provider written into the speech or video section is ignored like any undeclared key. That holds on every path: effective_media_config, the live reader, which now validates with the class MediaGenConfig declares for the tool's kind, and set_media, which refuses the field for those two kinds. Gating the borrow by kind would still have left media_provider and the doctor rows (capabilities.configured_from) reading the field, so the field itself goes. On your input, speech and video now resolve to ('', '') on both paths, OpenRouter's default address with no OpenRouter key to borrow, while image still resolves to ('https://relay.test/v1', 'sk-oa'). With providers.openrouter.apiKey added, speech and video take that key. Two compatibility edges are kept: MediaGenConfig(image=MediaToolConfig(...)) still validates, for an embedder handing AgentLoop one, and the live reader's kind defaults to the image tool (see 5).

  2. A case-different header name (e8520e6). The key's headers now give way to a section header of the same name in any case. Your authorization input sends one header, Bearer sk-LOWER.

  3. The live path dropping a key the boot path resolves (fdc4efb). The live reader passes every key that may spell the provider (another case, a hyphen, a former name, a field alias) through ProvidersConfig itself and resolves the name with ProvidersConfig.get, so it folds exactly as at boot. It still validates only those keys, so an unrelated provider's bad edit still cannot hold the answer back, and the existing test for that still passes. Your two inputs and a field alias (fireworks for fireworks_ai) now agree between boot and live. One mutant survives here: matching on the provider's name instead of its route_names, because every former name in the registry is also a field alias today. The route names stay so that a former name without a field alias still folds as boot folds it.

  4. _media_needs naming a slot that would not be read (2708d84). 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 capabilities already uses. A key on an endpoint, under another spelling, or in OPENROUTER_API_KEY now counts. When no key resolves, it names the provider (one under providers.openai, the provider it runs on) rather than a field that may not be read. The existing test's expected wording changed with it.

  5. provider: "openrouter" in the raven-ppt snapshot (1a23ab3). The launcher now asks media_provider. The key half of your measurement turned out to be a separate gap: the control's sk-or was the OpenRouter key the host had borrowed for OpenRouter's address, sent to the deck's pinned base. be8dfaa stops that for every section (see the thread on agents/raven-ppt/run.py). After that, the two spellings rendered the same section and differed only in the payer the log named, which the test now compares as well. agents/raven-ppt/run.py can outlive its raven, since a stamped home tree outranks a checkout's own agents/. So it looks media_provider up with getattr and keeps the two-argument reader call that every 0.2 release has. A test runs the launcher against a reader with the 0.2 signature and with media_provider deleted.

Verification at the new head: uv run --frozen --python 3.12 --all-extras pytest -q gives 27601 passed, 7 failed, 115 skipped. The 7 are the machine-local failures listed in the description, which fail the same way on origin/main. A mutation pass removed each construct this round adds, one at a time: 18 mutants, 17 killed. The one survivor is the route_names match named in 3.

@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.

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 ZuyiZhou left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@LivXue
LivXue merged commit 2f0a09d into main Oct 8, 2026
27 checks passed
@LivXue
LivXue deleted the feat/image_role_provider_choice branch October 8, 2026 09:33
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.

Why is image generation limited to OpenRouter when my OpenAI provider has GPT Image models?

4 participants