fix(sdk): rank standard provider keys above custom model lists (#5117) - #5601
fix(sdk): rank standard provider keys above custom model lists (#5117)#5601Souptik96 wants to merge 1 commit into
Conversation
On a slug-less default connection, a custom provider whose `models` list happened to include a model the standard key also serves would resolve instead of that standard key. The mechanism is narrower than "model matches win". A `provider_key` candidate carries no model list at all -- `_provider_key_candidate` populates neither `model_slugs` nor `model_keys` -- so `matches_model()` is structurally always False for a standard key. As soon as any custom provider advertises the requested id, `_candidate_pool`'s model-match list is non-empty and its `model.provider` fallback is never reached, so the standard key is not out-ranked but excluded from consideration. Add `_default_candidate_pool`, used only by `_choose_default`, which ranks provider-family matches on standard keys ahead of a custom provider's model-name match. Only the ranking changes: the resulting pool still goes through the existing single-candidate, `default`-slug and ambiguity rules. `_choose_named` keeps `_candidate_pool` unchanged, since naming a connection by slug should still narrow that slug by model id. Refs Agenta-AI#5117
|
@Souptik96 is attempting to deploy a commit to the agenta projects Team on Vercel. A member of the Team first needs to authorize it. |
|
✅ Thanks @Souptik96! This PR now meets the contribution requirements and has been reopened. A maintainer will review it soon. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughSummary by CodeRabbit
WalkthroughSlug-less default connection resolution now prioritizes matching standard provider keys over custom-provider model matches. Named connections and ambiguity handling remain unchanged. Regression tests cover the updated resolution cases. ChangesDefault connection resolution
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related issues
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
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. Comment |
|
@coderabbitai review Context for the skip: this PR was auto-closed by Two notes for whoever reviews:
|
|
I will treat the Vercel The Comment image supports the stated regression case and the focused test results. The pending CLA remains an external merge requirement. ✅ Action performedReview finished.
|
Summary
Fixes bug 1 of #5117: on a slug-less default connection, a custom provider whose
modelslist happens to include a model the standard key also serves resolves instead of that standard key.The root cause is narrower than "model matches are returned first." A
provider_keycandidate carries no model list at all —_provider_key_candidatepopulates neithermodel_slugsnormodel_keys— somatches_model()is structurally alwaysFalsefor a standard key. The moment any custom provider advertises the requested id,_candidate_pool'smodel_matcheslist is non-empty and theif model.provider:provider-family branch is never reached. The standard key is not out-ranked; it is excluded from consideration. (On currentmainthis isconnections.py:417-431; the issue cites ~364-378, which has since drifted.)The fix adds
_default_candidate_pool, used only by_choose_default, which ranks provider-family matches on standard keys ahead of a custom provider's model-name match. Only the ranking changes — the resulting pool still passes through the existing single-candidate,default-slug and ambiguity rules untouched._choose_nameddeliberately keeps_candidate_poolas-is: when a user names a connection by slug, narrowing that slug's records by model id is the correct behaviour, and there is a test pinning that it still does.Two judgement calls I'd like a maintainer's read on
1. A behaviour change beyond the strict letter of the issue. When a user has two standard keys for the same provider family and a custom provider advertising the requested model, the ranked pool is now genuinely ambiguous, so resolution raises
AmbiguousConnectionErrorwhere today it silently resolves to the custom provider. I think failing loud is correct there and consistent with how_choose_defaultalready treats competing defaults — and it is arguably the same class of complaint as bug 2 — but it is a change the issue does not ask for, so it is pinned by its own test (test_default_two_standard_keys_with_a_custom_model_match_is_ambiguous) rather than left implicit. Happy to gate it differently if you'd rather.2. Deliberate custom-provider routing. A user who wants their custom gateway for a model their standard key also serves now gets the standard key on a slug-less default connection. The remedy is to name the connection by slug, which is unchanged. This follows the issue's own stated preference ("the standard OpenAI key would have worked and is what the user most likely intended"), but it is a real trade-off worth naming.
Scope: bug 1 only
Bug 2 is not addressed here.
services/oss/src/agent/app.pyon currentmainis 168 lines and contains no_resolve_session_connection— that logic has moved since the issue was filed, so the path in the description is stale and the two fixes no longer live near each other. It needs re-locating before it can be fixed properly, and doing that blind in this PR would be guesswork.On process: @sumit1kr posted a correct analysis of both bugs on 7 July and asked to be assigned. That went unanswered for 24 days and the issue was never assigned, so per CONTRIBUTING ("An issue may only be assigned to one person for up to one week... If the issue remains unsolved after a week, it will be unassigned and made available to others") I've picked up bug 1 and left bug 2 to them. I credited their analysis on the issue before opening this. If they are still on it, close this and I'll step aside.
Testing
Verified locally
Environment: Windows, Python 3.13.4,
uv 0.11.14(the pinnedUV_VERSION),ruff 0.15.12(the pinned CI version).pytest oss/tests/pytest/unit/agents/platform/test_connections_http.py53aaf6f5aconnections.py, keep the new testsThe negative control fails on exactly the three tests that assert the new ranking, while both guard tests (custom provider still wins with no matching standard key; the named path still narrows by model id) pass unpatched — which is the point, since they assert preserved behaviour.
Full unit layer, the same set CI runs (
pytest oss/tests/pytest/unit,-n autoviapytest.ini):53aaf6f5a+5 passedis exactly the tests added, and the one failure is identical on both sides:agents/test_streaming.py::test_cli_stream_terminal_only_on_empty_request, which fails on pristinemaintoo withFileNotFoundError: [WinError 2]fromsubprocess— a Windows-only environment failure unrelated to this change.One flake, disclosed: the first branch run also failed
agents/test_runner_transport_timeout.py::test_subprocess_stream_survives_a_run_longer_than_the_idle_window. It passes 4/4 in isolation and did not recur on a second full run (which completed in 36s vs 73s, so the first run was under heavier machine load). It is an idle-window timing test running under-n auto, so I've treated it as load-sensitive rather than caused by this change — the change touches only candidate ranking and cannot affect subprocess streaming.Linting, at CI's pinned
ruff==0.15.12:ruff format --check→ 465 files already formatted;ruff check→ All checks passed!Added or updated tests
Five tests in
oss/tests/pytest/unit/agents/platform/test_connections_http.py:test_default_standard_key_outranks_custom_provider_model_list— the issue's exact scenario, baregpt-4o-miniwith both connections present.test_default_standard_key_outranks_custom_provider_with_explicit_prefix— same with the provider stated rather than inferred from the catalog.test_default_custom_provider_still_wins_without_a_matching_standard_key— guards against over-narrowing: an unrelated family's key must not suppress the custom provider that genuinely serves the model.test_named_connection_still_narrows_by_model_id— guards that_choose_namedis unaffected.test_default_two_standard_keys_with_a_custom_model_match_is_ambiguous— pins judgement call 1 above.QA follow-up
fake_httpunit seam, not against a real vault, so a quick end-to-end confirmation with two real overlapping connections is worth doing.uv.lock, disclosed.uv sync --lockedcannot complete on this machine:litellm==1.92.0resolves to an sdist that builds a pyo3/maturin Rust extension, and this box has neither Rust nor MSVC build tools. I synced every other locked dependency and installedlitellm==1.94.1(wheel-only) so the import chain inagenta/sdk/utils/assets.pyresolves. This does not affect what is under test:supported_llm_modelsis agenta's own hard-coded dict inassets.py, and litellm is used there only forcost_calculator.cost_per_token. The repo already tolerates litellm build drift —test_supported_llm_models.pyxfails 10 OpenRouter ids with "not yet in this litellm build's model_cost". Worth a confirming run on CI's exact lock.sdks/python/**paths; this change is Python-only and touches no TypeScript, so I have not run them locally.Demo
No UI in this change, so this is a capture of the actual resolver behaviour before and after. The script builds the two vault records the issue describes — a slug-less standard OpenAI
provider_keyand a slug-lesscustom_providerwhosemodelslist also advertisesgpt-4o-mini— and prints which credential actually resolves. Run against pristinemain53aaf6f5a, then against this branch:Verbatim output, pristine
main:and on this branch, same script, same input:
The repro script is not included in the diff (it is scratch, not a test — the equivalent assertions live in
test_connections_http.py); the image is hosted on a branch of my fork so it stays out of this PR's two files. Happy to attach it as a proper test fixture or ascripts/helper if you'd find it useful.Checklist
Note on the auto-close: the PR template says the demo is "required for UI changes... if this is not a UI change, write N/A", so I first wrote N/A.
13-check-pr-contribution.ymlis stricter than the template — it requires media for any non-exempt file, andsdks/python/agenta/**is not exempt. Worth reconciling the template wording with the workflow, since a Python-only SDK fix reads as exempt from the template alone.