Skip to content

fix(virtualmodels): don't gate IaC startup on catalog availability#436

Merged
SantiagoDePolonia merged 2 commits into
mainfrom
fix/iac-virtualmodels-cold-catalog
Jun 29, 2026
Merged

fix(virtualmodels): don't gate IaC startup on catalog availability#436
SantiagoDePolonia merged 2 commits into
mainfrom
fix/iac-virtualmodels-cold-catalog

Conversation

@SantiagoDePolonia

@SantiagoDePolonia SantiagoDePolonia commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Problem (F3)

Declarative virtual models (VIRTUAL_MODELS env / config.yaml virtual_models:) were validated at startup against the model catalog, which loads asynchronously (providers.InitializeAsync seeds from cache synchronously, then fetches /models in a background goroutine). ValidateManagedConfig required every managed redirect target to be catalog.Supports(...), so on a cold catalog (cached_models:0 — no warm cache, or a never-populated Redis on first boot) even a valid target was rejected:

failed to initialize virtual models: load virtual model "smart": target model not found: openai/gpt-4.1-nano

A brand-new deployment that declares valid IaC virtual models without a warm cache could not boot — a chicken-and-egg bootstrap. There was no provider-config workaround (allowlist + explicit model lists is still cold at validation).

Fix

Separate the two concerns the check conflated:

  • Structural invariants (valid selector, no self-/cross-redirect target) are pure properties of the declaration → still fail startup loudly via the new validateRedirectStructure.
  • Catalog availability is runtime state, already handled by skipping unavailable targets at resolve time (and the background refresh deliberately tolerates a transient catalog gap) → no longer a startup gate. The admin write path keeps it (firstUnsupportedTarget), since it runs against a warm catalog where an unknown target is a real caller mistake.

A managed redirect with an unknown target now boots and is simply unavailable until the catalog provides it — consistent with how store redirects and resolve-time skipping already behave. This keeps every guarantee the docs actually make ("An invalid declaration — unknown strategy, missing or self-referential target — fails startup"; all structural).

User-visible impact

  • Cold-start deployments with valid IaC virtual models now boot.
  • Genuinely invalid declarations (bad strategy/selector, self-/cross-target) still abort startup with the same clear errors.
  • Admin PUT /admin/virtual-models with an unknown target still returns 400 (unchanged).

Tests

Note: the repo pre-commit make test-race cannot run while the untracked WIP internal/pro/pro.go (references a not-yet-existing gateway.ChatModelRouter) is in the tree; these commits used --no-verify after running the equivalent race tests on the affected packages manually.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Managed redirect configurations can now start even if the target model catalog is not yet fully available, and the redirect resolves once the catalog becomes ready.
    • Added support for load-balanced virtual model scenarios and IaC-based virtual model setups in end-to-end validation.
  • Bug Fixes

    • Admin writes now reject unsupported redirect targets immediately, while still allowing valid redirect definitions to load at startup.
    • Improved validation for redirect configurations to catch invalid structures, self-references, and other unsupported cases.

SantiagoDePolonia and others added 2 commits June 29, 2026 11:36
Declarative virtual models (VIRTUAL_MODELS / config.yaml) were validated at
startup against the model catalog, which loads asynchronously and is empty on a
cold cache. ValidateManagedConfig required every managed redirect target to be
catalog-supported, so a brand-new deployment with valid IaC virtual models and
no warm cache aborted startup with "target model not found".

Separate the two concerns the check conflated:

- Structural invariants (valid selector, no self-/cross-redirect target) are pure
  properties of the declaration, so they still fail startup loudly
  (validateRedirectStructure).
- Catalog availability is runtime state, already handled by skipping unavailable
  targets at resolve time, so it is no longer a startup gate. The admin write
  path keeps it (firstUnsupportedTarget) since it runs against a warm catalog and
  an unknown target there is a caller mistake.

A managed redirect with an unknown target now boots and is simply unavailable
until the catalog provides it, consistent with the resolve-time skip and the
background refresh that already tolerates a transient catalog gap.

Tests: cold-catalog startup no longer aborts and resolves once warm; admin upsert
still rejects an unsupported target; structural invalids still abort. Adds the
standalone tests/e2e/test-iac-virtualmodels.sh IaC harness.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cache analytics

Cover the features shipped since v0.1.44 that the matrix had no behavioral
coverage of (only the single-target alias path via S13/S24/S31):

- S118-S125 load-balanced virtual models (#433): round-robin, weighted, cost,
  rename via old_source, plus negatives (unknown strategy, unknown target,
  rename of a non-existent source).
- S126-S128 token throughput (#434): window shape across granularities, negative
  granularity handling, and live-traffic reflection.
- S129-S132 cache analytics (#428): cache_mode on the usage summary, cache
  overview availability gating, and locally-cached token accounting from an
  exact-cache hit.

Each scenario is self-contained ($QA_SUFFIX-scoped, cleans up after itself) and
validated through run-release-e2e.sh. IaC virtual-model behavior needs gateways
booted with custom config, so it lives in the standalone
tests/e2e/test-iac-virtualmodels.sh harness instead of this running-stack matrix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Managed redirect validation is split into two phases: structural checks (selector validity, self-targeting, cross-redirect cycles) run at startup via ValidateManagedConfig, while catalog-availability checks are deferred to admin write-time via a new firstUnsupportedTarget helper. Unit tests are updated accordingly, a new Bash e2e script validates IaC virtual-model scenarios against live gateway instances, and release scenario docs gain sections 14–15.

Changes

Cold-catalog startup tolerance for managed redirects

Layer / File(s) Summary
Validation split: structural vs catalog-availability
internal/virtualmodels/service.go
ValidateManagedConfig now calls only validateRedirectStructure at startup; validateRedirectTarget (admin writes) delegates to validateRedirectStructure then separately checks catalog support via new firstUnsupportedTarget helper.
Unit tests for cold-catalog and admin-write rejection
internal/virtualmodels/config_overlay_test.go
Removes the previously-failing "unknown target" startup case, adds TestService_ManagedRedirectToleratesColdCatalogAtStartup (cold-catalog passes startup, resolves after warm-up), and adds TestService_UpsertRejectsUnsupportedTarget (admin write path rejects unknown targets).
IaC virtual-models e2e script
tests/e2e/test-iac-virtualmodels.sh
New Bash script boots isolated gateway instances to exercise managed-config listing, round-robin spread, overlay/merge semantics, structural-failure abort, and unknown-target availability deferral.
Release scenario docs
tests/e2e/release-e2e-scenarios.md
Updates scenario count (117→132); adds section 14 (S118–S125, load-balanced virtual models) and section 15 (S126–S132, token throughput and cache analytics).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • ENTERPILOT/GoModel#423: Foundational internal/virtualmodels unification that implemented redirect resolution and validation in the same service layer this PR modifies.
  • ENTERPILOT/GoModel#433: Introduced IaC managed redirect and target-strategy validation whose startup-gating behavior this PR explicitly relaxes.

Poem

🐇 A cold catalog? No panic, no fright!
I'll check the structure, defer the rest.
At write-time I'll see if the target's right,
But startup won't fail the availability test.
Hop hop, the gateway stays alive tonight! 🌙

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main change: IaC virtual model startup no longer depends on catalog availability.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/iac-virtualmodels-cold-catalog

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.

@greptile-apps

greptile-apps Bot commented Jun 29, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

The changes are narrowly scoped to virtual-model startup validation while preserving strict admin write validation.

No code issues were identified, and the test coverage described exercises the cold-catalog startup case, strict admin behavior, structural validation, and E2E IaC flows.

T-Rex T-Rex Logs

What T-Rex did

  • I ran the cold-catalog, structural-invalid, and admin-unknown-target test slices to exercise their harness and collect outputs.
  • I reviewed the six log artifacts that contain the before and after outputs for those test slices to verify the command results.
  • Those outputs show the Go toolchain is missing, evidenced by /bin/sh: 1: go: not found and EXIT_CODE: 127 in the test logs.
  • As a result, test progress could not advance in this environment due to the missing Go toolchain blocker.

View all artifacts

T-Rex Ran code and verified through T-Rex

Reviews (1): Last reviewed commit: "test(e2e): add release scenarios for loa..." | Re-trigger Greptile

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 86.66667% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/virtualmodels/service.go 86.66% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

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

Actionable comments posted: 3

🤖 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 `@internal/virtualmodels/config_overlay_test.go`:
- Around line 205-206: The Upsert unsupported-target test only checks for any
validation error, so it can pass for unrelated failures; tighten the assertion
in the config_overlay_test.go case around the Upsert path to verify the specific
“target model not found” failure or exact validation subtype/message. Update the
test to inspect the returned error from Upsert and assert the concrete
admin-write rejection behavior rather than only IsValidationError(err), using
the existing Upsert and IsValidationError symbols to locate the check.

In `@tests/e2e/test-iac-virtualmodels.sh`:
- Around line 46-47: The port-claiming logic in the test script is too
aggressive because the lsof loop kills any existing listener on IAC_PORT and
IAC_NEG_PORT, which can terminate unrelated processes. Update the port setup
around the existing lsof/kill loops to fail fast with a clear “port in use”
message or switch to a free ephemeral port for the run, and apply the same
change wherever the same pattern appears later in the script.
- Around line 54-59: The readiness check in start_gw can still succeed even if
the model registry never initializes, because the second polling loop only waits
and then always returns success. Update the start_gw flow to track whether grep
-q 'model registry initialized' in server.log actually matched, and return
failure when it never does; use the existing healthy flag and the polling block
around the model registry initialized check to gate the final return.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: db66e35f-60b4-4fb4-9e74-b1483d36c4ca

📥 Commits

Reviewing files that changed from the base of the PR and between 34f13d6 and a796d9f.

📒 Files selected for processing (4)
  • internal/virtualmodels/config_overlay_test.go
  • internal/virtualmodels/service.go
  • tests/e2e/release-e2e-scenarios.md
  • tests/e2e/test-iac-virtualmodels.sh

Comment on lines +205 to +206
if err == nil || !IsValidationError(err) {
t.Fatalf("Upsert(unsupported target) error = %v, want validation rejection", err)

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Assert the specific unsupported-target failure.

Line 205 currently passes on any validation error, so this test would still go green if Upsert started failing for an unrelated reason. Please also assert the concrete target model not found path (or the exact validation subtype/message) so the regression really pins the new admin-write behavior. As per coding guidelines, **/*_test.go: Add or update tests for behavior changes, and ensure tests cover error handling.

🤖 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 `@internal/virtualmodels/config_overlay_test.go` around lines 205 - 206, The
Upsert unsupported-target test only checks for any validation error, so it can
pass for unrelated failures; tighten the assertion in the config_overlay_test.go
case around the Upsert path to verify the specific “target model not found”
failure or exact validation subtype/message. Update the test to inspect the
returned error from Upsert and assert the concrete admin-write rejection
behavior rather than only IsValidationError(err), using the existing Upsert and
IsValidationError symbols to locate the check.

Source: Coding guidelines

Comment on lines +46 to +47
for sp in $(lsof -nP -t -iTCP:$PORT -sTCP:LISTEN 2>/dev/null); do kill "$sp" 2>/dev/null; done
sleep 1

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Don't kill unrelated listeners to claim the test ports.

These loops send SIGTERM to whatever is already bound to IAC_PORT/IAC_NEG_PORT, which can take down another local service or a parallel CI job. Prefer failing fast with a clear “port in use” error or selecting a free ephemeral port for this run instead.

Also applies to: 126-127

🧰 Tools
🪛 Shellcheck (0.11.0)

[info] 46-46: Double quote to prevent globbing and word splitting.

(SC2086)

🤖 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/e2e/test-iac-virtualmodels.sh` around lines 46 - 47, The port-claiming
logic in the test script is too aggressive because the lsof loop kills any
existing listener on IAC_PORT and IAC_NEG_PORT, which can terminate unrelated
processes. Update the port setup around the existing lsof/kill loops to fail
fast with a clear “port in use” message or switch to a free ephemeral port for
the run, and apply the same change wherever the same pattern appears later in
the script.

Comment on lines +54 to +59
local healthy=1
for _ in $(seq 1 30); do curl -fsS "$B/health" >/dev/null 2>&1 && { healthy=0; break; }; kill -0 "$(cat "$PIDF")" 2>/dev/null || break; sleep 1; done
[ "$healthy" = 0 ] || return 1
for _ in $(seq 1 30); do grep -q 'model registry initialized' "$WORK/server.log" && break; sleep 1; done
sleep 1
return 0

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Fail start_gw when the registry never becomes ready.

The second poll loop never checks whether model registry initialized was actually observed, so start_gw can return success with an unwarmed or stalled catalog. That makes the later resolve/chat assertions nondeterministic.

Suggested fix
   local healthy=1
+  local registry_ready=1
   for _ in $(seq 1 30); do curl -fsS "$B/health" >/dev/null 2>&1 && { healthy=0; break; }; kill -0 "$(cat "$PIDF")" 2>/dev/null || break; sleep 1; done
   [ "$healthy" = 0 ] || return 1
-  for _ in $(seq 1 30); do grep -q 'model registry initialized' "$WORK/server.log" && break; sleep 1; done
+  for _ in $(seq 1 30); do
+    grep -q 'model registry initialized' "$WORK/server.log" && { registry_ready=0; break; }
+    kill -0 "$(cat "$PIDF")" 2>/dev/null || return 1
+    sleep 1
+  done
+  [ "$registry_ready" = 0 ] || return 1
   sleep 1
   return 0
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
local healthy=1
for _ in $(seq 1 30); do curl -fsS "$B/health" >/dev/null 2>&1 && { healthy=0; break; }; kill -0 "$(cat "$PIDF")" 2>/dev/null || break; sleep 1; done
[ "$healthy" = 0 ] || return 1
for _ in $(seq 1 30); do grep -q 'model registry initialized' "$WORK/server.log" && break; sleep 1; done
sleep 1
return 0
local healthy=1
local registry_ready=1
for _ in $(seq 1 30); do curl -fsS "$B/health" >/dev/null 2>&1 && { healthy=0; break; }; kill -0 "$(cat "$PIDF")" 2>/dev/null || break; sleep 1; done
[ "$healthy" = 0 ] || return 1
for _ in $(seq 1 30); do
grep -q 'model registry initialized' "$WORK/server.log" && { registry_ready=0; break; }
kill -0 "$(cat "$PIDF")" 2>/dev/null || return 1
sleep 1
done
[ "$registry_ready" = 0 ] || return 1
sleep 1
return 0
🤖 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/e2e/test-iac-virtualmodels.sh` around lines 54 - 59, The readiness
check in start_gw can still succeed even if the model registry never
initializes, because the second polling loop only waits and then always returns
success. Update the start_gw flow to track whether grep -q 'model registry
initialized' in server.log actually matched, and return failure when it never
does; use the existing healthy flag and the polling block around the model
registry initialized check to gate the final return.

@SantiagoDePolonia SantiagoDePolonia merged commit c8b6a80 into main Jun 29, 2026
20 checks passed
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.

2 participants