Provider credential fixes and shared custom-provider registration - #123
Conversation
A refresh now pins the generation it read and commits only while the stored record still holds it, so the slower of two concurrent refreshes writes nothing. This keeps a rotated refresh token from being replaced by the one it superseded, and stops the losing exchange's rejection from tombstoning credentials the winner just renewed. Backings therefore no longer need cross-writer exclusion for OAuth, only for AWS preserve mutations, which merge fields into the current record. Documents that split on StoreBackendStorage and in the credential store memory bank.
The poll loop had no visibility into how a device-code attempt ended: a denied or expired attempt failed silently with nothing in the logs to distinguish it from a hung poll. Log the terminal error code whenever a current, non-aborted attempt reaches its terminal state.
…nvironment registry
wch
left a comment
There was a problem hiding this comment.
Looks good overall! Here's some stuff turned up by /assistant-review:
-
critical (correctness) —
packages/ai-lib/packages/ai-credentials/src/store-backend/StoreBackend.ts:605-609: Two windows can still overwrite each other's refreshed OAuth tokens. The new generation check reads the record and then callsstore.setseparately;withLockis now explicitly allowed to lock only within one window. Both windows can read generationg, receive different refresh outcomes, and both pass the check before either writes. A late write can replace a valid rotated token with an obsolete token or arefresh_failedtombstone. The same read-then-write race applies to interactivecompareAndWriteat lines 582–588. The updatedmemory-bank/aiCredentialStore.md:89-94claims the weaker backing is safe, but a generation field alone is not atomic compare-and-set. Require exclusion across every writer of these keys, or supply a backing with a genuinely atomic conditional write before advertising multi-window safety; add a test using two independently locked backings over the same record. -
critical (correctness) —
packages/ai-lib/packages/ai-credentials/src/store-backend/envCredentialResolver.ts:38-43: Workbench's admin-provisioned Databricks profile does not outrank a shell token on hosts that capture secrets at startup. The new guard checksDATABRICKS_CONFIG_FILE, butproviderEnvRegistry.ts:88-95declares only the token and M2M variables.captureProviderEnvironmenttherefore retainsDATABRICKS_TOKENwithout the config-file marker, andNodeAuthServiceresolves against that captured snapshot (packages/node/src/platform/services/NodeAuthService.ts:284-296); the shell PAT is selected. Capture the config-file marker without scrubbing it, and test capture → backend resolution with both values present. This is a credential-precedence regression for the exact managed-host case the change targets. -
critical (correctness) —
packages/ai-lib/packages/ai-provider-bridge/src/providers/google-vertex-provider.ts:480-488: Inline Vertex service-account credentials can list models but cannot authenticate chat. Discovery mints a token from the capturedGOOGLE_CLIENT_EMAILandGOOGLE_PRIVATE_KEYat lines 87–113. The client factory forwards only a brokered access token or ADC filename;GoogleVertexClient.googleAuthOptions()uses only those two inputs (model-clients/GoogleVertexClient.ts:71-80). On a host that scrubs the private key, a user with only the inline service account sees models but the SDK has no credential for either Gemini or Anthropic requests. Feed the same captured service-account material into the SDK's renewable auth path, and test discovery followed by chat without ADC. -
important (correctness) —
packages/ai-lib/packages/ai-provider-bridge/src/providers/google-vertex-provider.ts:105-110: A temporary token-service failure is reported as rejected service-account credentials. Every error fromgetAccessToken()is converted toInlineServiceAccountError, and lines 401–418 turn that name intoinline_service_account_rejectedwith instructions to replace or unset the variables. A timeout or connection reset therefore blames valid keys and clears cached models as an auth failure. Distinguish actual credential rejection from network/service errors and retain the existing network-error/stale-cache handling for the latter; cover a transient exchange failure. -
important (simplification) —
packages/ai-lib/packages/ai-provider-bridge/src/register-all-providers.ts:155-182: The new complete custom-kind registrar table duplicates the existing table inpackages/node/src/platform/providers/register-all-providers.ts:65-95. Node still calls its ownregisterCustomProviders, and Positron's catalog adapter calls that Node helper too (packages/positron/src/positron-ai/catalog-adapter.ts:468-479). Adding a kind or changing its callback/credential environment now requires synchronized edits to both tables, contrary to the stated goal of shared registration. Have the Node adapter pass its catalog-derivedcustomProvidersand prebuilt callbacks to the bridge'sregisterAllProviders, and have the Positron adapter use that same bridge entry; delete the Node registrar table/helper when consumers are updated. The bridge then owns one provider-kind mapping, while the adapters retain only host-specific selection and callback construction. -
minor (correctness) —
packages/ai-lib/packages/ai-config/src/base-url.ts:160-171: The Foundry URL normalizer strips queries but not fragments. Forhttps://r.openai.azure.com/openai/v1#section, the#prevents the existing/openai/v1recognition and the result becomeshttps://r.openai.azure.com/openai/v1#section/openai/v1, not a usable API root. Strip URL fragments along with queries, and add a pasted-URL case. This helper is exported for a future host caller, not yet called by production code in this checkout. -
minor (convention) —
packages/ai-lib/packages/ai-provider-bridge/src/register-all-providers.ts:95:customProviders[].clientKindaccepts arbitrarystringdespite the exhaustiveSupportedCustomClientKindtable, forcing aPartial<Record<string, ...>>cast at lines 204–207. Narrow the public field toSupportedCustomClientKindso typed callers cannot accidentally request an unsupported kind; keep a runtime guard if untyped inputs can reach this API. -
minor (clarity) —
packages/ai-lib/packages/ai-config/src/node/load-catalog.ts:35-39: The new host-default source and inline Vertex credential path are absent from their architectural guidance.packages/ai-lib/memory-bank/aiConfig.md:158-168still enumerates the old loader sources, whilepackages/ai-lib/memory-bank/providerGuide.md:261-290describes Vertex as ADC-only. Update those sections with the host-vs-env default ordering and inline service-account behavior so subsequent integrations do not rebuild the old assumptions.
37a9ecc to
4cea242
Compare
|
Thanks @wch I addressed your feedback, ran
|
… and auth guidance
A pasted https://<resource>.openai.azure.com/openai normalized to .../openai/openai/v1. Replace the test that pinned /openai/v10 -> /openai/v10/openai/v1 with one asserting /openai/v1 matches only as a whole segment.
Hold the token-endpoint response, write a newer record the way a writer outside the lock would, then release rotated tokens or invalid_grant. The stored record must survive; removing either generation check fails a case.
…wing One stale kind from an untyped caller (e.g. read over IPC) threw after the built-ins and earlier entries had registered, skipping later valid entries and failing the whole call. Log a warning and continue instead.
…d the tokens withRefreshTransaction noted the generation from one read, and the refresh then read the tokens again. Under a per-window lock another window could write between the two reads, so the refresh spent the newer record's refresh token and then dropped the rotated result as stale. The transaction now reads the record once and hands the operation a RefreshTransaction: the tokens from that read, plus commitTokens and commitError bound to its generation. This replaces persistRefreshedTokens, persistRefreshError and the refreshGenerations side map, so the commits can no longer be called outside a transaction. Sign-in and refresh commits share one compare-and-write path. Commits report whether they landed, so a terminal rejection that loses to another writer no longer logs "stored tokens removed".
|
I ran another review on it, and the first and third ones definitely seem worth fixing. Since I had the review and checkout right here in front of me, I went ahead and implemented and pushed fixes, and after fixing those two issues, I kept going and addressed the rest of them as well -- I hope that's OK! Details
|
|
More thank ok, thank you Winston! |
This is the first PR split out of #16102 Relies on: - posit-dev/ai-lib#123 - posit-dev/ai-lib#126 **We must pin ai-lib to a commit from main after merging the relevant PRs and before merging this PR** It adds support for custom and local providers to the headless service (Git Suggestion and Notebooks) Positron's AI features outside chat (Git Suggestions, notebook AI) go through the headless language model service, This PR also: - Bumps ai-lib to posit-dev/ai-lib#123 for the shared custom-provider registration the service now uses. The bump also brings in ai-lib's changes since #94, including the OpenCode provider (#113), removal of the obsolete OAuth fallback (#107) and coalesced model discovery (#115). The pin points at the ai-lib PR's head for now and moves to its merge commit before this merges. - Lets an extension that `product.json` trusts for a provider create a session without a consent prompt, not only reuse one. Today that affects Copilot Chat signing in to GitHub Enterprise. - Publishes the Posit AI Pass login config (`positaiLogin`) on the catalog, and adds a test that fails if a connection field doesn't reach the renderer. Fixes #16029. Part of #14477; OpenRouter still isn't served. ### Release Notes #### New Features - Git Suggestions and notebook AI can use custom and local providers (#16029, #14477) #### Bug Fixes - N/A ### Validation Steps @:assistant The easiest way to test is by configuring models and then using the command "Git: Select Git Suggestions Model" to configure the model for git suggestions. --------- Co-authored-by: Brice Stacey <bricestacey@gmail.com> Co-authored-by: sharon wang <25834218+sharon-wang@users.noreply.github.com>
This PR fills some gaps in the ai-lib's authentication process to bring it to parity with Positron's Authentication extension and adds shared custom-provider registration for Positron's headless service.
This carries the ai-lib changes from #114 that don't depend on the auth id rename.
GEMINI_API_KEYis unset, the Gemini provider readsGOOGLE_API_KEY.GOOGLE_CLIENT_EMAILandGOOGLE_PRIVATE_KEY, and tries it before Application Default Credentials. If Google rejects the credentials, the provider reports an auth error that names the two variables.DATABRICKS_TOKENin the user's shell.providers.customentries by kind, with shared helpers for a custom entry's auth mapping and for writing Google Cloud and AWS session tokens, for Positron's headless language model service and Assistant to share.AWS_WEB_IDENTITY_TOKEN_FILEis set, the AWS credential provider points the STS token exchange at the configured region.Cherry-picked from #114. Where they conflicted, I kept main's OpenCode entries and #119's "Reload model list" wording.