Skip to content

Stop a case-differing AI connection name from silently replacing another - #185

Merged
MikeAlhayek merged 1 commit into
mainfrom
ma/ai-config-collision-diagnostics
Sep 14, 2026
Merged

MikeAlhayek merged 1 commit into
mainfrom
ma/ai-config-collision-diagnostics

Conversation

@MikeAlhayek

Copy link
Copy Markdown
Member

Why

A connection's identifier hashes its lowercased client and connection name, so these two entries resolve to the same identifier:

CrestApps:AI:Connections:0:Name                          = "Shared-Azure"
CrestApps:AI:Providers:Azure:Connections:shared-azure:*

AddConfiguredConnection guards against a duplicate name only when the identifiers differ, and then ends with a plain connections[connection.ItemId] = connection. Both conditions are met here, so the provider-section entry silently replaced the top-level one — its endpoint and its credentials gone, with no diagnostic at any log level.

Every deployment naming that connection then reached whichever resource happened to be read last. That surfaces as an unexplained HTTP 404 DeploymentNotFound when the model is deployed only on the other resource, with nothing anywhere to suggest the connection the operator configured was not the one being used.

This came out of a real staging incident: two connections named WinnerWare and winnerware pointing at different Azure resources. One model worked, the other 404'd, and the config looked correct.

What changed

  • ConfigurationAIProviderConnectionSource — first definition wins on an identifier collision, matching ConfigurationAIDeploymentSource.AddDeployment, and the dropped entry is logged as a warning naming the entry it collided with:

    Skipping AI connection 'shared-azure' from 'CrestApps:AI:Providers:Azure:Connections:shared-azure' because 'Shared-Azure' resolves to the same identifier. Connection names are case-insensitive; rename one of them so both are read.

  • AzureOpenAICompletionClient — a failed completion now logs the deployment name, the model name that forms the request URL, the connection name and the endpoint host. The provider's own exception names none of them. The streaming path enumerates by hand because a yield cannot sit inside a try; OperationCanceledException still propagates unlogged.

  • Changelog entries and a regression test.

Behavior change

A site that has such a pair today will switch to the earlier definition rather than the later one. That is the deterministic, source-order-consistent choice, and the new warning names both entries so the duplicate can be renamed. Noted in the changelog.

Testing

Full suite green — 3242 passed, 0 failed. The new test (ConfigurationAIProviderConnectionStore_WhenTwoConfiguredNamesDifferOnlyByCase_ShouldKeepTheFirstOne) fails on main: the surviving entry there is the second one, with the second endpoint.

Not included

AIDeploymentManagerBase.ResolveSlotAsync falls through to GetFirstQualifiedDeploymentAsync when the requested deployment cannot be resolved, substituting an arbitrary capable deployment with no log at all. Same class of silent surprise, but it wasn't the cause here — happy to do it separately.

🤖 Generated with Claude Code

A connection's identifier hashes its lowercased client and connection name, so
`Shared-Azure` under `CrestApps:AI:Connections` and `shared-azure` under
`CrestApps:AI:Providers:Azure:Connections` resolve to the same identifier. The
existing duplicate-name guard is suppressed when the identifiers match, and the
final assignment overwrote the dictionary entry, so the later definition
replaced the earlier one -- its endpoint and its credentials gone, with no
diagnostic at any log level. Every deployment naming that connection then
reached whichever resource happened to be read last, surfacing as an
unexplained DeploymentNotFound when the model is deployed only on the other.

The first definition now wins, matching how configured deployments are read,
and the dropped entry is logged as a warning naming the entry it collided with.

A failed Azure OpenAI completion also names the deployment, the model name that
forms the request URL, the connection and the endpoint host it used. The
provider's own exception names none of them, so a misrouted request read as a
bare HTTP error. The streaming path enumerates by hand because a yield cannot
sit inside a try block.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@MikeAlhayek
MikeAlhayek merged commit f267193 into main Sep 14, 2026
10 checks passed
@MikeAlhayek
MikeAlhayek deleted the ma/ai-config-collision-diagnostics branch September 14, 2026 17:15
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