fix(aux): keep callable key_cmd credentials intact in all custom-provider resolution branches - #107344
fix(aux): keep callable key_cmd credentials intact in all custom-provider resolution branches#107344SiaoZeng wants to merge 2 commits into
Conversation
… resolution resolve_provider_client's custom branches assumed the api_key is a str: explicit_api_key.strip() raised AttributeError on a CommandTokenSource (key_cmd), and str(main_runtime api_key) would send the object repr as the bearer. vision_analyze and title generation are the observable victims on any key_cmd custom provider; every aux call routed through _resolve_custom_branch crashed identically. Pass callables through uncalled (the OpenAI SDK and the Anthropic bearer-hook path both accept them; the main runtime already builds its credential dict with this guard). Covers bare custom, named custom, api_key-branch overrides, and the main_runtime reuse path. Invariant tests pin that an explicit callable reaches the client object unchanged (not stripped, not stringified); RED on base for all three branches. Refs NousResearch#88667; NousResearch#105595 covers the api_key-branch line alone — this also covers the named-custom and main_runtime sites.
|
Thanks for the triage note — checked against all three diffs, and the coverage picture is:
What none of them cover is Since all four branches raise the same crash class, whichever subset PR lands first leaves the remaining ones crashing. Suggesting consolidation into this PR (it has per-branch RED tests for all four sites) — but happy to defer to whichever shape maintainers prefer: I can rebase onto any of these landing first, and would gladly fold #102244's async-credential helpers in here if the maintainers want the async layer in the same pass. |
…t wrap The OpenAI SDK stores a callable api_key as _api_key_provider and blanks client.api_key until the first request refreshes it. _to_async_client rebuilt AsyncOpenAI from that empty attribute, so every async auxiliary call (vision_analyze) against a key_cmd provider sent no Authorization header and 401d. Forward the provider as an awaitable bridge (blocking mint via asyncio.to_thread) so the async SDK mints per request like sync.
|
Independent confirmation of this fix's premises, reproduced live on
Suggest these three sites + a test be in this PR's scope; happy to see it land as-is. |
|
Heads-up so work isn't duplicated: #109851 fixes the same |
|
Blocking: named custom providers still lose their configured extra_headers. The exact-head code builds the named custom OpenAI client with only query parameters and global user headers in _named_custom_openai_wire_client. It never applies custom_entry.extra_headers. _to_async_client then rebuilds the client and also does not preserve those configured headers. I verified a named provider with key_cmd and a required route header: the callable bearer survives, but the required header is absent from both the sync and async client requests. Providers that require proxy authentication or routing headers still reject these auxiliary calls. Please normalize and apply the named entry extra_headers when building the sync client, preserve the effective configured headers in the async conversion, and add sync plus async regression coverage. Also remove the extra blank line at the end of tests/agent/test_command_token_source.py so git diff --check passes. |
|
Landed as #114333 with |
…r resolution branch (#88667) A key_cmd (CommandTokenSource) or Entra credential is a callable, not a string. _route_via_main_provider and the main_runtime passthrough hand it to resolve_provider_client as explicit_api_key / main_runtime["api_key"], where five branch sites still assumed a string: four call .strip() on it (AttributeError: 'CommandTokenSource' object has no attribute 'strip') and the main_runtime reuse arm wraps it in str(), so the object's repr becomes the bearer token and the request fails with a silent 401. One module-level helper, _explicit_key(), replaces the five inline expressions so the callable/str/None contract lives in one place instead of five ternaries that drift independently: a non-str callable passes through uncalled (the client calls it per request), strings are stripped, anything else collapses to "" so the existing `or <fallback>` chains keep working. The async seam was already fixed the same way in #113969. Co-authored-by: SiaoZeng <188540289+SiaoZeng@users.noreply.github.com> Credit: #107344 @SiaoZeng (four of the five sites), #88668 @LordMelkor (first submitter, bare-custom branch), #105595 @haydster7 (api_key branch)
What does this PR do?
Fixes the
key_cmdcrash class inagent/auxiliary_client.py(issue #88667) at all four call sites where a custom-provider credential is assumed to be a string:_resolve_custom_branch(barecustom, explicit key):.strip()on aCommandTokenSourceraisedAttributeError— this is the crash reported in [Bug]: callable api_key from key_cmd crashes custom-provider resolution with AttributeError #88667 (MoA) and observed viavision_analyze+agent.title_generator._resolve_named_custom_branch(namedcustom_providersentry): same naive.strip()— not covered by fix(auth): keep callable api_key intact when resolving a custom provider #88668 / fix: preserve callable custom credentials across derived clients (vision et al.) #102244 / fix(auth): keep a callable key_cmd credential in the api_key branch #105595; it is the path that breaks vision/title/compression for every aux call on akey_cmdnamed provider._resolve_api_key_branchexplicit override: same.strip()(line also touched by fix(auth): keep a callable key_cmd credential in the api_key branch #105595)._resolve_custom_branchmain-runtime reuse:str(main_runtime.get("api_key"))stringifies the token source, sending the object repr as the bearer — the Ollama capability probe sends the CommandTokenSource object repr as the bearer for key_cmd providers #104460 bug class, silent 401s instead of a crash.The fix passes callables through uncalled and normalises only strings, mirroring the guard the main runtime already uses when it builds its credential dict (
api_key.strip() if isinstance(api_key, str) else api_key if callable(api_key) else ""). The OpenAI SDK accepts a callableapi_keyper-request, and the Anthropic bearer-hook path routes callables toAuthorization: Bearer(pinned byTestCallableKeyGetsBearerAuth), so nothing downstream needs to change.Related Issue
Fixes #88667
Companion to the open PRs on the same issue: #88668 (bare custom only), #102244 (derived clients), #105595 (api_key branch only). Whichever lands first, the remaining branches still crash — this PR covers the full class in one pass; overlap is additive and mergeable.
Type of Change
Changes Made
agent/auxiliary_client.py— fourcallable()-guard passthroughs in the custom-provider resolution branches (no behaviour change for string credentials; blank-string handling preserved,no-key-requiredfallback unchanged).tests/agent/test_command_token_source.py— newTestExplicitCallableSurvivesCustomResolutionclass: three invariant tests asserting an explicitCommandTokenSourcereaches_create_openai_clientunchanged (not stripped, not stringified) on the bare-custom, named-custom, and main-runtime branches. RED on base per branch, all three green with the fix. ExistingTestAuxiliaryResolverHonoursKeyCmd._resolvegained an optionalexplicit_api_keypass-through (no existing assertions touched).How to Test
key_cmd(e.g.key_cmd: printf minted-token), run an aux task (vision/title generation) → before:AttributeError: 'CommandTokenSource' object has no attribute 'strip'inerrors.log; after: aux call succeeds with a per-request bearer.scripts/run_tests.sh tests/agent/test_command_token_source.py tests/agent/test_auxiliary_client.py→ 236/236 pass.git stashthe fix, run the file): one failure per covered branch.Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests pass (targeted: the two files covering the touched code, 236/236 viascripts/run_tests.sh)Documentation & Housekeeping
docs/, docstrings) — or N/Acli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/A