Skip to content

feat!: profile-driven upstream routing and per-profile image analysis#36

Open
flobernd wants to merge 18 commits into
local-inference-lab:masterfrom
flobernd:profile-routing
Open

feat!: profile-driven upstream routing and per-profile image analysis#36
flobernd wants to merge 18 commits into
local-inference-lab:masterfrom
flobernd:profile-routing

Conversation

@flobernd

@flobernd flobernd commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Collapses all routing into one rule: a request model resolves to a model profile, and the profile names its upstream, failover chain, and optional image-analysis redirect. Upstreams become pure named endpoints; the legacy global knobs, catalog-union routing, model_routes, and the global vision offload are removed, each failing startup with a message naming its replacement (see the new README "Migrating" section).

Intention

Routing previously had three overlapping mechanisms (global upstream_* knobs, catalog-union discovery, model_routes) plus a fourth global image subsystem, and they all interacted. This replaces them with a single explicit mechanism and deletes roughly 4,600 net lines.

Testing

Full cargo test (748 unit + ~320 integration tests, 1069 total, 0 failed), cargo clippy --all-targets with zero warnings, cargo fmt --check clean. Wire-level tests cover profile routing, fallback model rewrites, cooldown shared across profiles, the 404/400 taxonomy, the image redirect and native passthrough, and a startup-error test per removed key.

Notes: breaking changes

  • Unknown models return 404 instead of collapsing to the catalog default; blank models return 400 unless a catch-all profile sets upstream_model.
  • /v1/models advertises the configured profiles (declaration order) instead of the upstream catalog union, and no longer forwards the upstream ETag.
  • Images pass through to the upstream natively unless the profile sets image_analysis; the global vision offload and unsupported_image_policy are gone.
  • All legacy keys hard-error at startup with migration hints: top-level upstream_base_url, upstream_api_key, upstream_model, fallback_upstreams, model_routes, image_agent_enabled, vision_url, vision_model, unsupported_image_policy, profile native_vision, per-entry upstream_model and nested fallback_upstreams (including exposed_model).
  • LLMCONDUIT_UPSTREAM_BASE_URL, LLMCONDUIT_UPSTREAM_API_KEY, LLMCONDUIT_UPSTREAM_MODEL, and the vision/image env overrides are removed and now ignored.
  • OPENAI_API_KEY is no longer read implicitly; a keyless upstreams entry sends no credentials. Each entry declares its own api_key, or names an environment variable via the new api_key_env (mutually exclusive with api_key; an unset or blank variable is a startup error).
  • An upstream stream that ends with only [DONE] now counts as a failed attempt (failover and cooldown apply).
  • Replay-cache hits are currently lost for profiles that set upstream_model (Key the replay cache by served model on lookup #32).
  • llmconduit configure rewrites the config to the minimal shape and collapses multi-upstream configs; --model-route is removed.

Related

Follow-ups tracked in #32, #33, #34, #35.

Summary by CodeRabbit

  • New Features
    • Added ordered, profile-based routing to named upstreams with exact-vs-glob matching, upstream_model rewriting, and per-profile fallbacks.
    • Added configurable image analysis offload via image_analysis, including image stripping and residual handling (placeholder vs reject) with routing through upstreams.
    • Updated GET /v1/models to list configured profile models locally.
    • Improved request-log configuration with per-upstream logging.
  • Bug Fixes
    • Clearer 404/400 behavior for unmatched vs blank models; stricter startup failure when legacy routing knobs are present.
    • Replay-cache bypass when image degradation results in placeholder-collapsed content.
  • Documentation
    • Refreshed configuration and migration guidance, including new upstreams/model_profiles semantics and request logging details.

flobernd added 14 commits July 17, 2026 14:50
A request model's routing target will be named by a model profile
(later tasks), so an `upstreams:` entry no longer needs its own
`upstream_model` remap or nested `fallback_upstreams` chain. Those
keys are now detected and rejected with the replacement they migrate
to, rather than silently ignored, and an entry's name is required and
validated unique so a profile can reference it unambiguously.

`UpstreamConfig`/`PersistedUpstream` fields are renamed to their short
forms (`url`, `api_key`, `chat_kwargs`, `request_log_path`), with serde
aliases for the old `upstream_*` names as the only back-compat path.
Removing the per-entry `fallback_upstreams` field breaks 3 integration
tests that exercised routing-mode fallback dispatch end-to-end
(selected-upstream failover to a nested fallback, and an exposed
fallback alias being listed/routed); that dispatch has no equivalent
in the still-supported global `fallback_upstreams` (non-routing
failover) path, so those tests are deleted rather than ported.

The GLOBAL top-level `fallback_upstreams` (non-routing failover mode)
is untouched.
Adds the persisted and runtime schema for profile-driven routing: a
profile's `upstream` (named endpoint), ordered `fallbacks` (upstream +
optional model remap, bare-string or table form), and `image_analysis`
redirect (model + residual-image policy). `native_vision` becomes
removed-key detection only, erroring at startup toward `image_analysis`
as its replacement; `Config::profile_native_vision` now always returns
None so existing G4 gating falls through to its name-based heuristic.

No referential validation of these fields' targets yet -- that lands
with resolve_route in a later task.

Retiring the profile native_vision knob breaks tests that relied on it
to force a native-vision backend. Tests that exercised the knob's own
mechanics (request override, candidate lookup, decision-table cells)
have no substitute and are deleted (listed in task-2-report.md); tests
that only used it incidentally, to observe unrelated behavior on a
native backend (multimodal content-part fidelity, tool_result image
conversion, request-log redaction on a passthrough path), are migrated
onto the surviving Kimi name-sniff fallback instead.
The single-upstream knobs (upstream_base_url/api_key/model), the ad-hoc
model_routes map, the global fallback_upstreams chain, and the vision
knobs (image_agent_enabled, vision_url/model, unsupported_image_policy)
are all replaced by named `upstreams` entries plus `model_profiles`.
Leaving these keys parsed-but-ignored would silently mislead an upgraded
config into thinking it still routed a certain way, so each is now a hard
startup error that names its replacement and points at the README
migration notes, rather than a warning that scrolls past unheeded.

`PersistedConfig::default()` becomes the minimal profile config (one
`default` upstream plus a `"*"` catch-all profile) so a missing config
file still proxies to localhost out of the box, and OPENAI_API_KEY now
seeds any `upstreams` entry that declares no key of its own. The `--model-route`
CLI flag and its plumbing die with the persisted routes; `configure` writes
the new upstreams/profiles shape. The runtime routing and vision fields are
left inert (always empty) for now; Tasks 6-9 remove that machinery.
Replace the runtime `model_profiles` BTreeMap with an ordered
`Vec<CompiledProfile>` and add `Config::resolve_route`, the single entry
point for resolving a request model to a profile and served model: an exact
key (trimmed, case-insensitive) beats any glob, globs match first in
declaration order, and `served_model` defaults to `upstream_model`, then the
exact key, then the passed-through request model.

Tighten `ModelProfile.upstream` to a required `String` and validate profile
references in `from_persisted`: the primary upstream must exist, fallbacks
must be known, distinct from the primary, and unique, and an `image_analysis`
redirect must target an existing exact-key profile that is not itself, a glob,
or a profile that also sets `image_analysis`.

Collapse the request-model shaping helpers (system prompt prefix, roles) onto
the single profile from `resolve_route`, and key the leaf finalization
policies by the effective backend model with an ordered glob fallback, so the
upstream leaf resolves per-model kwargs/effort/family the same way routing
does. Add `exact_profile_keys` and `upstream_by_name` accessors.
…m cooldown

Add ProfileRoutingClient, which resolves each request model to a model
profile (Config::resolve_route) and dispatches over that profile's failover
chain: the primary named upstream plus its declared fallbacks. Profiles
share one provider per named upstream, so cooldown is keyed by upstream name
and observed across every profile that routes to it.

Generalize the shared failover dispatch (stream/count/proxy
*_with_provider_indices) from a bare provider-index list to a Vec<ChainStep>
so the model override rides on the chain step rather than the provider. The
existing failover/routing callers map their indices to steps with the
per-provider static upstream_model rewrite, preserving prior behavior.

The existing routing/failover/single-mode clients are left intact; lib.rs
wiring is swapped in a later change.
Wire ProfileRoutingClient as the single upstream client and delete the
legacy routing machinery it replaces. build_app now always builds one
FailoverUpstreamProvider per configured upstreams entry (pure named
endpoints) and dispatches every request through the profile router, so
the routing_mode split and the standalone/bare-primary/global-failover
branches are gone.

Remove the now-unreferenced RoutingUpstreamClient and its catalog resolve
machinery, RouteUpstreamProvider, ModelRouteSpec, the exposed-alias
plumbing, and the bare-primary marker path (into_bare_primary), so a
single TTL catalog cache (ProfileRoutingClient's) remains. Delete the
inert legacy Config fields (upstream_base_url, upstream_api_key,
upstream_model, fallback_upstreams, model_routes) and their consumers;
the engine's vestigial route-match and global-upstream_model fallbacks
drop with them. Persisted legacy knobs still hard-error.

Migrate the test suite to the profile shape (named upstreams plus a
catch-all or specific profiles) and drop tests that asserted legacy
single-mode, global-fallback, or catalog-union semantics with no profile
equivalent.
The engine ran its own pre-dispatch resolution ladder that rewrote the
dispatched request model to a profile's upstream_model, a catalog canonical
id, or the catalog default, and ProfileRoutingClient then re-resolved that
rewritten id. Any non-catch-all profile whose key was not itself a served id
(for example one with upstream_model set) therefore 404d end to end; the suite
stayed green only because the migrated configs used a "*" catch-all.

Resolve the route exactly once, in the router. The backend request keeps the
original (trimmed) request model and each chain step supplies the per-provider
POSTed model. The engine still resolves the route for its own needs (served
label, price/header key, capture, non-routing budgeting fallback) through a
thin resolve_request_model wrapper that returns the route's served_model.

Unknown models now surface a clean 404 before any upstream call
("model \"x\" is not served by any configured model profile") and a blank model
with no glob to resolve it is a 400, both from the engine streaming path and the
HTTP label-resolution sites. The exact-id / canonical-key / catalog-default
ladder, should_warn_model_fallback, and the genuine flag are deleted; the engine
catalog is kept only for context-limit metadata.

BREAKING CHANGE: an unserved model returns 404 (blank returns 400) instead of
collapsing to a catalog default, and the served-model label is now the route's
served model (profile key for exact profiles, request model for globs) rather
than a catalog-normalized id.
…ault

Image handling is now driven by the resolved profile's `image_analysis`
redirect instead of a global image-agent switch and vision endpoint. When a
profile declares `image_analysis`, the latest user turn's images are stripped
to placeholders, the `analyzeImage` server tool is injected, and the analysis
call is dispatched to the analyzer model through the gateway's own upstreams:
the router resolves it to the analyzer profile's upstream and served model like
any other request, so no separate endpoint is configured. The residual sweep
runs only for such profiles, applying the profile's `residual_images` policy
(placeholder degrade with a replay-cache bypass, or a pre-dispatch 4xx reject).

BREAKING CHANGE: native passthrough replaces degrade-by-default. A profile
without `image_analysis` now receives images verbatim regardless of model name.
The name-based native-vision sniff, the runtime `image_agent_enabled`,
`vision_url`, `vision_model`, and `unsupported_image_policy` knobs, and the
standalone vision client are removed.
`get_models` proxied the first configured upstream's `/v1/models` listing
verbatim - a placeholder from before profile routing existed, and one that
leaked whichever upstream happened to be first rather than describing what
the gateway actually serves. It now synthesizes the OpenAI-shaped list
directly from the configured profiles: one entry per exact-key profile
(glob profiles carry no fixed id to advertise), in declaration order, with
`context_length` filled in from the router's unioned model catalog when it
knows the profile's served model. A catalog-load failure just omits
`context_length`; it never fails the response.

The Anthropic reshaping (`transform_models_response_for_anthropic`) is
unchanged and now operates over this synthesized list, so `capabilities`
merge, pagination cursors, and the display/created-at fallbacks all still
apply - just to profile-derived entries rather than raw upstream fields.

`ProfileRoutingClient::list_models` (the trait's now-unreachable proxy
placeholder) returns an explicit error pointing at `supported_model_catalog`
instead of silently proxying the first upstream. Also opportunistically
populates `provider_health`'s `catalog_fetched_ms`/`catalog_size` from the
shared catalog cache via a non-blocking `try_lock`, since they'd been stuck
at `None` since the routing client rewrite.

BREAKING CHANGE: `/v1/models` now advertises the configured model profiles
instead of proxying the first upstream's catalog. Ids are profile keys,
`owned_by` is always `"llmconduit"`, and upstream-only fields
(`display_name`, `created`/`created_at`, `max_output_tokens`, per-model
`capabilities`) are gone from the raw OpenAI-shaped body - the
Anthropic-flavored dressing still fills sensible defaults for those. The
response also no longer carries an `ETag` header.
Extract build_configured_persisted_config out of run_configure_flow so
the pure config-construction part of the interactive wizard can be
exercised without a TTY. Cover both the blank-default-model case
(request model passes through) and the entered-default-model case
(profile serves the entered model), routing through
Config::from_persisted + resolve_route as an integration check of the
emitted upstreams/model_profiles shape.
Rewrite the README Configuration section for named upstreams and
glob-capable model_profiles (upstream/upstream_model/fallbacks/
image_analysis), add a Migrating section covering every removed
top-level knob, and fix AGENTS.md invariants left over from
catalog-union routing, nested fallback_upstreams, and the vision
knobs. Add the now-required upstream field plus a commented
image_analysis example to the GLM-5.2 reference profile.
Scrub remaining planning-label references from comments, correct stale
doc comments that described deleted machinery, and tighten two tests:

- Reword the removed-key rejection and shorthand-sweep comments so they
  describe current behavior without task numbers or stale enumerations.
- Fix the redaction, request-log, and error-body comments that still
  narrated deleted vision-agent gating states and the removed
  VisionOutcome.text field.
- Correct the ProviderHealth.catalog_size doc: it is the client-wide
  union catalog size stamped onto every provider, not a per-provider
  snapshot.
- Delete the unused latest_user_message_has_images predicate, its
  re-export, and its tests; it had no production caller.
- Make the /v1/models declaration-order test discriminate a sort
  regression by declaring profile keys out of alphabetical order.
- Add a startup-validation case for a blank upstream name.
An upgraded config could declare upstreams but no model_profiles at all,
starting up successfully while every request 404s at routing time. Fail
at startup instead, naming the catch-all fix and pointing at the README
migration section, matching the other removed/misconfigured-key errors.
Several existing fixtures relied on a profile-less config resolving
cleanly to exercise unrelated behavior (debug_log_dirs, price_table,
env-seeded upstreams); updated them to either supply a minimal profile
or, where the test genuinely needs zero upstreams, build the resolved
Config directly instead of going through PersistedConfig.

Also, while touching this file and its neighbors:
- Reworded the DEFAULT_UPSTREAM_BASE_URL doc to describe its current
  role (seeding the default upstream entry and the wizard's URL
  prompt) instead of a removed field and a stale task reference.
- Reworded the catalog_fetched_ms doc to match its already-correct
  sibling catalog_size: the value is the routing client's client-wide
  union snapshot stamped identically onto every provider, not a
  per-provider fetch time.
- Replaced stray em-dashes introduced since a96abaf with spaced
  hyphens across engine.rs, upstream.rs, vision/mod.rs, and a few
  test files.
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 58b1f7f8-411a-48af-9af6-5666aef059e4

📥 Commits

Reviewing files that changed from the base of the PR and between 74fe14c and 2b4a6e9.

📒 Files selected for processing (4)
  • AGENTS.md
  • README.md
  • src/cli.rs
  • src/config.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/cli.rs
  • AGENTS.md
  • README.md
  • src/config.rs

📝 Walkthrough

Walkthrough

The gateway migrates to named upstreams, ordered model profiles, profile-scoped failover, local model listings, and gateway-routed image analysis. Configuration, CLI behavior, runtime wiring, documentation, and tests are updated.

Changes

Profile routing and image analysis

Layer / File(s) Summary
Profile configuration and CLI migration
AGENTS.md, README.md, profiles/glm52.yaml, src/config.rs, src/cli.rs, src/main.rs
Configuration uses named upstreams and ordered model_profiles, with exact/glob precedence, explicit fallbacks, image-analysis settings, legacy-key validation, updated documentation, and revised interactive configuration.
Profile routing, failover, and leaf policies
src/upstream.rs, src/lib.rs
Failover chains carry per-hop model rewrites, shared upstream health and cooldown state is used across profiles, and leaf policies support exact and ordered glob matches.
Gateway model resolution and image analysis
src/engine.rs, src/http.rs, src/vision/*, src/redaction.rs
Model resolution is synchronous and profile-based, /v1/models is generated locally, and image analysis uses cached images and gateway upstream routing instead of a dedicated vision client.
Routing and image-analysis integration coverage
tests/*, src/vision/cache.rs, src/vision/mod.rs
Fixtures and tests cover profile precedence, failover, synthesized model listings, analyzer routing, residual-image policies, replay-cache bypass, redaction, and compatibility updates.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: profile-driven routing with per-profile image analysis.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
putComment timed out

@flobernd

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/engine.rs (1)

2818-2830: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Replay lookup should use the served model label, not request.model.
ReplayRecord is stored with model: served_model, but find_replay_baseline still hashes &request.model, so any profile that remaps the model name will never hit the replay cache. Use the same model string on both insert and lookup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/engine.rs` around lines 2818 - 2830, Update find_replay_baseline to hash
and look up using the served model label, matching the model value stored in
ReplayRecord. Reuse the same served-model resolution used by the replay
insertion path, while preserving existing lookup behavior for non-remapped
models.
🧹 Nitpick comments (3)
tests/port_server.rs (1)

387-396: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Align the opening comment with the defer-to-provider behavior.

The test now expects HTTP 200 and one upstream POST, while Lines 371-376 still claim HTTP 400 and zero POSTs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/port_server.rs` around lines 387 - 396, Update the opening comment for
the chat completions mock and its associated assertions to consistently describe
the defer-to-provider behavior: expect HTTP 200 with one upstream POST, not HTTP
400 with zero POSTs. Use the existing test symbols and preserve the current SSE
response setup.
tests/image_agent.rs (1)

3-5: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the actual request-wide image-stripping scope.

strip_and_cache_images rewrites image_url parts in every user message, not only the latest turn.

  • tests/image_agent.rs#L3-L5: say user-message images across request history are stripped.
  • tests/image_agent.rs#L748-L750: narrow residual examples to file_id images and images in non-user roles.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/image_agent.rs` around lines 3 - 5, The documentation at
tests/image_agent.rs lines 3-5 must state that image_url parts in user messages
across the full request history are stripped and cached, not only images in the
latest user turn. Update the residual examples at tests/image_agent.rs lines
748-750 to refer specifically to file_id images and images in non-user roles; no
other behavior changes are needed.
tests/gateway.rs (1)

8237-8240: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the stale model-name-sniff rationale.

Native passthrough now follows from the profile lacking image_analysis, not from recognizing Kimi by name.

  • tests/gateway.rs#L8237-L8240: describe passthrough as profile-driven.
  • tests/gateway.rs#L8990-L8993: remove the Kimi name-sniff claim.
  • tests/gateway.rs#L9084-L9088: attribute raw image forwarding to the absent image_analysis setting.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/gateway.rs` around lines 8237 - 8240, Update the comments in
tests/gateway.rs at 8237-8240, 8990-8993, and 9084-9088 to remove the stale Kimi
name-sniff rationale and describe native/raw image passthrough as driven by the
profile’s absent image_analysis setting; no code behavior changes are needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/cli.rs`:
- Around line 168-188: Update the existing_default lookup and catch-all model
profile lookup in the surrounding configuration merge logic to normalize names
by trimming whitespace and comparing case-insensitively. Apply the same
normalization to the "*" profile key, while preserving the current fallback
behavior only when no normalized default upstream matches.

In `@src/config.rs`:
- Around line 1044-1050: Update the From<PersistedProfileFallback> for
ProfileFallback conversion to normalize persisted.model before constructing
ProfileFallback: trim surrounding whitespace and convert a whitespace-only value
to the same empty/absent representation used for primary upstream_model values.
Leave persisted.upstream handling unchanged.
- Around line 2286-2323: Remove the implicit OPENAI_API_KEY fallback from the
upstream construction in the upstream parsing flow. In the upstreams loop, set
UpstreamConfig.api_key only from the individual entry.api_key after
trim_nonempty, so local and third-party endpoints receive no ambient credential
unless explicitly configured.

---

Outside diff comments:
In `@src/engine.rs`:
- Around line 2818-2830: Update find_replay_baseline to hash and look up using
the served model label, matching the model value stored in ReplayRecord. Reuse
the same served-model resolution used by the replay insertion path, while
preserving existing lookup behavior for non-remapped models.

---

Nitpick comments:
In `@tests/gateway.rs`:
- Around line 8237-8240: Update the comments in tests/gateway.rs at 8237-8240,
8990-8993, and 9084-9088 to remove the stale Kimi name-sniff rationale and
describe native/raw image passthrough as driven by the profile’s absent
image_analysis setting; no code behavior changes are needed.

In `@tests/image_agent.rs`:
- Around line 3-5: The documentation at tests/image_agent.rs lines 3-5 must
state that image_url parts in user messages across the full request history are
stripped and cached, not only images in the latest user turn. Update the
residual examples at tests/image_agent.rs lines 748-750 to refer specifically to
file_id images and images in non-user roles; no other behavior changes are
needed.

In `@tests/port_server.rs`:
- Around line 387-396: Update the opening comment for the chat completions mock
and its associated assertions to consistently describe the defer-to-provider
behavior: expect HTTP 200 with one upstream POST, not HTTP 400 with zero POSTs.
Use the existing test symbols and preserve the current SSE response setup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cacc4ec0-2820-4b4e-b1c3-f6acf470ade0

📥 Commits

Reviewing files that changed from the base of the PR and between a96abaf and 419978b.

📒 Files selected for processing (30)
  • AGENTS.md
  • README.md
  • profiles/glm52.yaml
  • src/adapters/responses_to_anthropic/tests.rs
  • src/cli.rs
  • src/config.rs
  • src/engine.rs
  • src/http.rs
  • src/lib.rs
  • src/main.rs
  • src/redaction.rs
  • src/search.rs
  • src/upstream.rs
  • src/vision/cache.rs
  • src/vision/client.rs
  • src/vision/mod.rs
  • src/vision/strip.rs
  • tests/common/mod.rs
  • tests/dashboard_provider_health.rs
  • tests/dashboard_serving_token.rs
  • tests/gateway.rs
  • tests/http_body_limit.rs
  • tests/image_agent.rs
  • tests/port_config.rs
  • tests/port_config_routing.rs
  • tests/port_errors.rs
  • tests/port_routing.rs
  • tests/port_server.rs
  • tests/port_streaming_peek.rs
  • tests/port_translation.rs
💤 Files with no reviewable changes (2)
  • tests/dashboard_serving_token.rs
  • src/vision/client.rs

Comment thread src/cli.rs Outdated
Comment thread src/config.rs
Comment thread src/config.rs Outdated
flobernd added 4 commits July 18, 2026 13:01
The configure wizard matched the default upstream and the catch-all
profile key exactly, while runtime resolution trims names and compares
upstream names case-insensitively. A config with 'name: Default' was
valid at runtime but made the wizard pre-fill from an unrelated first
entry and overwrite the config with that entry's URL and credentials.

Extract the two lookups into helpers so they are testable without a
TTY, and match the default upstream trimmed and case-insensitively and
the '*' profile key trimmed.
Primary upstream_model values are normalized with trim_nonempty, but
fallback model overrides were passed through verbatim. A padded value
was posted with its whitespace on failover, and a whitespace-only value
was posted as-is instead of falling back to the served model.
Seeding every keyless upstreams entry from the environment leaked an
ambient OpenAI credential to local and third-party endpoints, and an
empty api_key could not opt out because it trims to absent and
inherits anyway. An entry now authenticates only with its own api_key.
Removing the implicit OPENAI_API_KEY fallback left no way to keep an
upstream key out of the config file. api_key_env names an environment
variable to read the key from, resolved once at startup. It is mutually
exclusive with api_key, and a declared-but-unset (or blank) variable is
a startup error so a missing key fails loud instead of surfacing as
upstream 401s. The configure wizard carries the field through unchanged
unless a literal key is entered.
@flobernd

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@flobernd

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@flobernd

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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.

1 participant