fix(versa_azure): each model posts to its own deployment; an unmapped model is refused before it is sent (F1) - #230
Open
Broccolito wants to merge 1 commit into
Open
Conversation
… one is refused before it is sent F1 of the 2026-09-10 QA run on 7c96d79. `versa_azure` posted every request to one fixed deployment (`VERSA_AZURE_DEPLOYMENT = gpt-5.5-2026-04-24`) and never consulted the model, so choosing any of the other eight advertised models changed the label, the context gauge and the cost basis while gpt-5.5 kept answering — and a model that does not exist completed a turn normally. `token_events.model_id` told the truth because it is the gateway's own answer. Measured before writing the map (UCSF gateway, 2026-09-11): the gateway has no listing endpoint (GET openai/deployments and openai/models answer 405), so each advertised deployment was sent a one-shot completion. All nine answer 200 with their own name as `model`; the QA's bogus name and the short aliases are DeploymentNotFound. A prompt just over each registry window was refused with exactly that window (the gpt-5 family as an input limit, window less the 128k output reservation), so MODEL_CONTEXT_WINDOWS needs no change. - `VERSA_AZURE_DEPLOYMENTS` is the measured model->deployment map and the one source of the advertised catalog. A request derives its deployment from the model IT names, which also fixes the fast model (`complete_fast`) landing on the main deployment. - A model the map does not know is refused before the payload is built or logged: RequestFailed "no Versa deployment for model `X` (Azure deployment not found, so nothing was sent); available: ...", classified ModelUnavailable, so the turn stops on its first attempt. Construction never fails, because a failed restore is a chat that will not open. - `VERSA_AZURE_DEPLOYMENT_NAME` (then the legacy AZURE_OPENAI_ key) still wins when set, but a value naming a CATALOG deployment is not an override: the onboarding card upserts the shipped default on every connect and the old setup form wrote the legacy key the same way, so honouring it would leave F1 in place for every onboarded install. A deployment the catalog does not know remains the escape hatch and serves every request. - The restore binding's `deployment` is derived on every `restore_binding()` and read back through the same rule, so a rebind refreshes it, rows the old code wrote (gpt-5.5 beside another model) self-heal, and a subagent's model rewrite follows the new model. The binding shape is unchanged — format v1 has shipped, and older builds must still parse it — and a model no deployment serves stores a self-describing marker that an older build posts to and gets DeploymentNotFound for (measured), never another model's answer. - `VERSA_AZURE_DEPLOYMENT` becomes `VERSA_AZURE_DEFAULT_MODEL` (it names a model now), `VERSA_AZURE_KNOWN_MODELS` folds into the map, and `with_unlisted_models()` is gone: an unlisted name can only be refused. No config key is added or renamed; every read stays a literal `get_param`, so the privacy config-key scan still counts 26. versa_bedrock needs no change: both Converse paths send `.model_id(model_config.model_name)`. The new routing tests drive real `from_env`/`from_resolved` providers against a local stand-in gateway; run against the pre-fix implementation, six of seven fail with F1's own symptom (gpt-5.5 answered), and the seventh (an explicit override still wins) passes on both.
This was referenced Sep 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes F1 (MEDIUM-HIGH, pre-existing) of the 2026-09-10 QA run on merged
main7c96d796: onversa_azure, choosing any model other thangpt-5.5-2026-04-24changed only the label.What was wrong
versa_azureposted every request to one deployment,VERSA_AZURE_DEPLOYMENT = "gpt-5.5-2026-04-24".deployment_namecame from config overrides only;model.model_namewas never read. So the row, the composer chip, the context gauge and the cost basis all named the selected model while gpt-5.5 answered. A deployment that does not exist (gpt-4.1-bogus-qa-probe) completed a turn normally.token_events.model_idstayed honest only because it is the gateway's ownusage.model.Two more instances of the same bug:
complete_fastsends the fast model throughcomplete_with_model, and it also landed on the main deployment.Measured before writing the map
All measurements on 2026-09-11, against the UCSF gateway with the operator's own key. The key was never printed.
No listing endpoint.
GET openai/deploymentsandGET openai/modelsreturn405 Method is not allowed(4 variants tried).One-shot completion per advertised deployment. All nine return
200, each with its own name asmodel. The QA'sgpt-4.1-bogus-qa-probeand the short aliases (gpt-5.5,gpt-4.1,gpt-4o) all return404 DeploymentNotFound.Context windows. Each deployment got a prompt just over its
MODEL_CONTEXT_WINDOWSentry. An over-limit request is refused before inference, so none was billed. Every refusal named the registry's window, soMODEL_CONTEXT_WINDOWSis unchanged:Reachable but not advertised; left out on purpose.
gpt-5-mini-2025-08-07andgpt-5-nano-2025-08-07answer.o1-2024-12-17ando3-mini-2025-01-31still answer; both were removed from the catalog deliberately.gpt-4o-mini-2024-07-18returns400 BadRequestForDependentService.The fix (
crates/biorouter/src/providers/versa_azure.rs)A measured model→deployment map.
VERSA_AZURE_DEPLOYMENTSis the only list:metadata()advertises exactly these models, and each request posts to the deployment of the model it names. That also fixes the fast-model path.An unmapped model fails the turn before anything is sent. The refusal comes before the payload is built or logged:
no Versa deployment for modelX(Azure deployment not found, so nothing was sent); available: …. Switch this chat to one of those models.RequestFailed, classifiedModelUnavailable, which is not recoverable, so the turn stops on its first attempt.The override still wins when set, but a catalog deployment is not an override.
VERSA_AZURE_DEPLOYMENT_NAMEis read first, then the legacyAZURE_OPENAI_DEPLOYMENT_NAME.InstitutionalSetupCardupsertsVERSA_AZURE_DEPLOYMENT_NAME: gpt-5.5-2026-04-24on every connect, and the setup form before 2026-09-03 wrote the legacy key the same way. The QA sandbox simply didn't carry the key.The restore binding's
deploymentis derived, not remembered.restore_binding()decides it fresh on each call, andfrom_resolvedreads it back through the same rule. As a result:gpt-5.5-2026-04-24.The binding shape is unchanged. Format v1 shipped in 1.89.x–1.90.x; a new shape would be a row those builds can't parse, and a 500 on resume. For a model no deployment serves, the binding stores a self-describing marker:
no-versa-deployment-serves-this-model.biorouter-refuses-to-send-the-turn. An older build posts to it and getsDeploymentNotFound(measured), never another model's answer.Catalog and names.
VERSA_AZURE_KNOWN_MODELSfolds into the map.VERSA_AZURE_DEPLOYMENTbecomesVERSA_AZURE_DEFAULT_MODEL, since it now names a model.default_model_nameis unchanged, and a test asserts it is on the map.with_unlisted_models()is removed. "Enter a model not listed..." could only offer a choice that is refused.biorouter models setand the workspace model check now refuse unlisted Versa models up front.Privacy config-key scan: untouched. No key was added or renamed, and every read is still a literal
get_param(...).privacy::config_keysstill counts 26 and still finds no computed keys.versa_bedrock(item 5): no change needed. Bothconverseandconverse_streamsend.model_id(model_config.model_name), so the selected model is the one invoked, and Bedrock itself rejects an unknown id.Tests
Fail-before. The new
routing_testsmodule drives realfrom_env/from_resolvedproviders against a wiremock stand-in gateway. I ran it against the pre-fixversa_azure.rs, with the module appended unchanged:The seventh test (an explicit override still wins) passes before and after; it guards behaviour that must be preserved.
New tests in
tests:create_from_persisted.Results on the final code (
BIOROUTER_DISABLE_KEYRING=trueon every run):cargo test -p biorouter --lib -- providers::versa_azure providers::factory privacy::config_keys→ 43 passed, 0 failedcargo test -p biorouter --lib→ 3805 passed, 0 failed, 2 ignoredcargo test -p biorouter --test context_windows→ 4 passedcargo test -p biorouter-server --lib -- a_new_private_provider_chat_requires_user_action_before_first_bind→ 1 passed (the test that uses the renamed constant)cargo fmt --all -- --check→ clean./scripts/clippy-lint.sh→ clean:-D warningson all targets,too_many_linesbaseline ok, no banned TLS cratesRuntime
Own sandboxed instance, launched from this worktree. The daemon was this worktree's
target/debug/biorouterd, built withbiorouter/privacy-test-auth. The sandbox also carried the onboarding card'sVERSA_AZURE_DEPLOYMENT_NAME: gpt-5.5-2026-04-24on purpose.Method, per model:
biorouter session --resume --session-id 20260911_1 --provider versa_azure --model <m> </dev/null, which exited 0 every time.select model_id, provider from token_events order by id desc limit 1.token_events(row id)binding.deploymentgpt-5.5-2026-04-24|versa_azure(3629)gpt-5.4-mini-2026-03-17|versa_azure(3630)gpt-5.4-nano-2026-03-17|versa_azure(3631)gpt-5.2-2025-12-11|versa_azure(3632)gpt-5-2025-08-07|versa_azure(3633)gpt-4.1-2025-04-14|versa_azure(3634)gpt-4.1-mini-2025-04-14|versa_azure(3635)gpt-4o-2024-11-20|versa_azure(3636)o4-mini-2025-04-16|versa_azure(3637)no-versa-deployment-serves-this-model.biorouter-refuses-to-send-the-turngpt-5.5-2026-04-24|versa_azure(3638)The bogus probe fails the turn readably. The chat shows, verbatim:
Provider call failed error_type="request", with no retries.llm_requestlogs mentions the model, so nothing was sent.Housekeeping.
secrets.yaml.versa_azure / gpt-5.5-2026-04-24before deletion.~/.config/biorouterwas never touched.Not changed here (follow-ups)
InstitutionalSetupCard.tsxstill upsertsVERSA_AZURE_DEPLOYMENT_NAME: gpt-5.5-2026-04-24. That value is now inert. Its Advanced "deployment" field, if edited to a non-catalog name, pins every model, which is the documented override semantics.DefaultProviderSetupForm'sPROVIDER_KEY_DEFAULTS.versa_azurestill listsAZURE_OPENAI_*defaults that no declared key reads.AZURE_OPENAI_DEPLOYMENT_NAMEfallback (pre-existing). A user who configured the publicazure_openaiprovider with their own deployment name has that name read as a Versa override, so every Versa request would 404. Unchanged by this PR: the value was already the deployment before.gpt-5-mini-2025-08-07andgpt-5-nano-2025-08-07are served but not offered.🤖 Generated with Claude Code