Uh oh!
There was an error while loading. Please reload this page.
feat: add headless BYOK via defineByok - #906
Conversation
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR adds ChangesCore BYOK package
ts-react-chat example integration
End-to-end BYOK flow
Estimated code review effort: 4 (Complex) | ~75 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 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 |
🚀 Changeset Version Preview21 package(s) bumped directly, 25 bumped as dependents. 🟥 Major bumps
🟨 Minor bumps
🟩 Patch bumps
|
View your CI Pipeline Execution ↗ for commit d88b2e4
☁️ Nx Cloud last updated this comment at |
View your CI Pipeline Execution ↗ for commit c9d0226
☁️ Nx Cloud last updated this comment at |
@tanstack/ai@tanstack/ai-acp@tanstack/ai-angular@tanstack/ai-anthropic@tanstack/ai-bedrock@tanstack/ai-byteplus@tanstack/ai-claude-code@tanstack/ai-client@tanstack/ai-code-mode@tanstack/ai-code-mode-snippets@tanstack/ai-codex@tanstack/ai-cohere@tanstack/ai-devtools-core@tanstack/ai-durable-stream@tanstack/ai-elevenlabs@tanstack/ai-event-client@tanstack/ai-fal@tanstack/ai-gemini@tanstack/ai-grok@tanstack/ai-grok-build@tanstack/ai-groq@tanstack/ai-isolate-cloudflare@tanstack/ai-isolate-daytona@tanstack/ai-isolate-node@tanstack/ai-isolate-quickjs@tanstack/ai-isolate-quickjs-bun@tanstack/ai-mcp@tanstack/ai-memory@tanstack/ai-mistral@tanstack/ai-ollama@tanstack/ai-openai@tanstack/ai-opencode@tanstack/ai-openrouter@tanstack/ai-perplexity@tanstack/ai-persistence@tanstack/ai-preact@tanstack/ai-react@tanstack/ai-react-ui@tanstack/ai-sandbox@tanstack/ai-sandbox-cloudflare@tanstack/ai-sandbox-daytona@tanstack/ai-sandbox-docker@tanstack/ai-sandbox-local-process@tanstack/ai-sandbox-sprites@tanstack/ai-sandbox-vercel@tanstack/ai-solid@tanstack/ai-solid-ui@tanstack/ai-svelte@tanstack/ai-utils@tanstack/ai-vercel-gateway@tanstack/ai-vue@tanstack/ai-vue-ui@tanstack/openai-base@tanstack/preact-ai-devtools@tanstack/react-ai-devtools@tanstack/solid-ai-devtoolscommit: |
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (8)
packages/ai-byok/tests/react.test.tsx (1)
6-7: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMerge duplicate
../src/indextype imports.Static analysis flags
import/no-duplicatesfor the two separate type-only imports from../src/index.🧹 Proposed fix
-import type { Keyring } from '../src/index'-import type { KeyringStorage } from '../src/index'+import type { Keyring, KeyringStorage } from '../src/index'🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-byok/tests/react.test.tsx` around lines 6 - 7, The React test file has duplicate type-only imports from the same module, triggering import/no-duplicates. Update the import section in react.test.tsx to merge the Keyring and KeyringStorage type imports into a single import statement from ../src/index, keeping the existing type-only form and preserving the referenced symbols.Source: Linters/SAST tools
packages/ai-byok/tests/byok.test.ts (1)
1-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winFix import order/sort lint errors.
Static analysis flags:
byokHeaderNameout of alphabetical order in the named import on Line 2, and the type import on Line 3 should be ordered after the../src/client/passkeyimport.🧹 Proposed fix for import ordering
-import { byokFetch, byokHeaders, byokHeaderName, withByok } from '../src/index'-import type { Keyring } from '../src/index'-import {- byokMissing,- getByokKey,- isByokMissingBody,- maskKey,- scrubSecrets,-} from '../src/server'-import { memoryStorage } from '../src/client/storage'-import {- decryptKeyring,- deriveAesKey,- encryptKeyring,-} from '../src/client/passkey'+import { byokFetch, byokHeaderName, byokHeaders, withByok } from '../src/index'+import {+ byokMissing,+ getByokKey,+ isByokMissingBody,+ maskKey,+ scrubSecrets,+} from '../src/server'+import { memoryStorage } from '../src/client/storage'+import {+ decryptKeyring,+ deriveAesKey,+ encryptKeyring,+} from '../src/client/passkey'+import type { Keyring } from '../src/index'🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-byok/tests/byok.test.ts` around lines 1 - 16, The test file import ordering is violating lint rules: the named import from byok helpers has an out-of-order symbol, and the type-only import is placed before the client/passkey imports. Reorder the imports in byok.test.ts so the named specifiers in the ../src/index import are alphabetized and the type import for Keyring is moved to the correct position after the ../src/client/passkey import, keeping the existing symbols and groupings intact.Source: Linters/SAST tools
packages/ai-byok/src/client/passkey.ts (1)
299-322: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winGuard
ensureKeyagainst concurrent ceremonies.The doc comment promises "exactly one WebAuthn ceremony," but because the
cachedKey/cachedMetaassignment isawait-gated, two overlapping calls (e.g. asaveracing aload, or rapid double-save) both observe an empty cache, both callidbGet, and both run a full ceremony. In the register branch this can mint two passkeys with only the last record persisted, orphaning the other. Memoize the in-flight promise so concurrent callers share one ceremony.♻️ Sketch
let cachedKey: CryptoKey | null = null let cachedMeta: { credentialId: ArrayBuffer salt: Uint8Array<ArrayBuffer> } | null = null + let pending: Promise<{+ key: CryptoKey+ credentialId: ArrayBuffer+ salt: Uint8Array<ArrayBuffer>+ }> | null = null async function ensureKey(): Promise<{ key: CryptoKey credentialId: ArrayBuffer salt: Uint8Array<ArrayBuffer> }> { if (cachedKey && cachedMeta) { return { key: cachedKey, ...cachedMeta } } - const existing = await idbGet(dbName)- ...- return { key: cachedKey, ...cachedMeta }+ if (!pending) {+ pending = (async () => {+ // ...existing register/unlock logic, sets cachedKey/cachedMeta...+ return { key: cachedKey!, ...cachedMeta! }+ })().finally(() => { pending = null })+ }+ return pending }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-byok/src/client/passkey.ts` around lines 299 - 322, The ensureKey helper currently allows overlapping calls to start separate WebAuthn ceremonies because cachedKey/cachedMeta are only populated after awaited work completes. Update ensureKey to memoize and share a single in-flight promise so concurrent callers (for example load and save, or rapid double-save) all await the same ceremony; reuse the existing cachedKey/cachedMeta fast path, but ensure the first unresolved call to ensureKey owns the idbGet/registerPasskey flow and later calls return that same promise until it settles.packages/ai-byok/src/client/validate.ts (1)
22-40: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winNo timeout on the validation fetch.
fetchhas noAbortSignal/timeout, so a stalled network or slow-to-respond provider endpoint leaves the caller'sawait(and, perbyok-context.tsx, the UI'svalidatingstate) hanging indefinitely with no way for the user to recover short of a page reload.⏱️ Proposed fix
export async function validateKey( provider: ProviderId, key: string, ): Promise<ValidationStatus> { const config = providerValidateConfig(provider) if (!config) return 'unsupported' - const response = await fetch(config.url, {- method: 'GET',- headers: config.headers(key),- })+ const response = await fetch(config.url, {+ method: 'GET',+ headers: config.headers(key),+ signal: AbortSignal.timeout(10_000),+ })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-byok/src/client/validate.ts` around lines 22 - 40, The validateKey() fetch call currently has no timeout, so a slow or stalled provider can leave the request and validating state hanging forever. Update validateKey() in validate.ts to use an AbortSignal with a timeout (for example via AbortController) around the fetch call, and ensure timeout aborts are handled as a validation failure instead of hanging. Keep the existing providerValidateConfig(), response handling, and ValidationStatus behavior intact.packages/ai-byok/src/server/byok-missing.ts (1)
39-44: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winType guard doesn't fully validate its asserted shape.
isByokMissingBodyonly checkserror.type, noterror.provider/error.message, yet it asserts the fullByokMissingBodyinterface (Line 4-10). Downstream,with-byok.tscallsonMissingKey(body.error.provider)(Line 23) trusting this is a validProviderId— an untrusted/malformed 401 body could pass this guard and hand a bad value to the caller's callback.♻️ Proposed fix to validate provider/message
import type { ProviderId } from '../shared/providers' +import { isProviderId } from '../shared/providers' ... export function isByokMissingBody(value: unknown): value is ByokMissingBody { if (typeof value !== 'object' || value === null) return false const { error } = value as { error?: unknown } if (typeof error !== 'object' || error === null) return false - return (error as { type?: unknown }).type === 'byok_missing'+ const { type, provider, message } = error as {+ type?: unknown+ provider?: unknown+ message?: unknown+ }+ return (+ type === 'byok_missing' &&+ typeof provider === 'string' &&+ isProviderId(provider) &&+ typeof message === 'string'+ ) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-byok/src/server/byok-missing.ts` around lines 39 - 44, `isByokMissingBody` currently only verifies `error.type`, so it can accept malformed bodies while claiming to be `ByokMissingBody`. Update the guard in `byok-missing.ts` to fully validate the asserted shape by checking `error.provider` and `error.message` alongside `error.type`, ensuring `provider` matches a valid `ProviderId`-like string and `message` is a string before returning true. Keep the checks inside `isByokMissingBody` so `with-byok.ts` can safely call `onMissingKey(body.error.provider)` without trusting unvalidated data.packages/ai-byok/src/react/byok-key-manager.tsx (2)
101-107: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueNo confirmation before clearing a saved key.
A single click on "Clear" permanently removes the stored key with no undo/confirm step. Worth a lightweight confirm (or a two-step "Clear?" toggle) to prevent accidental deletion, though the user can always re-enter the key.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-byok/src/react/byok-key-manager.tsx` around lines 101 - 107, The Clear action in the byok-key-manager component triggers immediate key deletion with no safeguard, so add a lightweight confirmation step before calling clearKey(provider). Update the button behavior in ByokKeyManager to require a second explicit confirmation (or a confirm dialog/toggle) before clearing the saved key, while keeping the existing clearKey provider flow unchanged once confirmed.
88-88: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRedundant
'masked' in statuscheck.
hasKeyalready excludes only the'empty'state, and every non-'empty'KeyStatusvariant carriesmasked(perbyok-context.tsx'sKeyStatusunion). The extra narrowing is harmless but adds noise.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-byok/src/react/byok-key-manager.tsx` at line 88, Remove the redundant masked property check in byok-key-manager.tsx’s render condition: the hasKey guard already covers all non-empty KeyStatus values, so update the conditional around the masked UI branch to rely on hasKey alone and keep the logic aligned with the KeyStatus union defined in byok-context.tsx.packages/ai-byok/src/react.ts (1)
4-4: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueExporting the raw
ByokContextbypasses theuseByoksafety guard.Consumers can call
useContext(ByokContext)directly and getundefinedsilently instead of the descriptive error thrown byuseByok. Consider keepingByokContextinternal (only exported for testing) and steering public consumers exclusively touseByok.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-byok/src/react.ts` at line 4, The public re-export in react.ts is exposing ByokContext directly, which lets consumers bypass the safety checks in useByok and receive undefined silently. Remove ByokContext from the public export surface in react.ts, keep it internal or test-only in react/byok-context, and ensure consumers are directed to use the useByok hook (and ByokProvider) for all supported access paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@examples/ts-react-chat/src/routes/api.tanchat.ts`:
- Around line 22-24: The import block in api.tanchat.ts violates the
import/order lint rule because the value import from BYOK config is placed
before the type-only import. Reorder the imports so the type import from
`@tanstack/ai-byok/server` stays grouped correctly and the value import
BYOK_PROVIDER_MAP/byokIdForProvider from `@/lib/byok-config` comes after it; keep
byokMissing and getByokKey with the other server imports and let the import
grouping in the module follow the linter’s expected order.
- Line 260: The generic type parameter name in byokAdapter violates the
TypeScript naming convention rule; rename M to a valid type-parameter identifier
that matches the required pattern, and update any references to that type
parameter within byokAdapter accordingly. Keep the change scoped to the
byokAdapter declaration and its uses so the lint error is resolved without
altering behavior.
In `@examples/ts-react-chat/src/routes/index.tsx`:
- Around line 37-39: The import order in the route module is violating the
import/order lint rule, so adjust the grouped and sorted imports near the top of
the file. Reorder the type-only and value imports involving ProviderId,
ByokKeyDialog, and byokIdForProvider/getEnvKeyStatus to match the project’s
import grouping conventions, then verify the file passes lint (or use the
auto-fix from the linter).
- Around line 877-882: Remove the unnecessary optional chaining in the
platformAuth calculation inside the index route component:
isPasskeyStorageSupported() already guards globalThis.PublicKeyCredential, so
update the PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
call to use it directly instead of ?. to satisfy no-unnecessary-condition while
keeping the existing runtime behavior unchanged.
In `@packages/ai-byok/src/client/passkey.ts`:
- Around line 57-64: The support check in isPasskeyStorageSupported() is unsafe
because it dereferences navigator.credentials.create directly, so it can throw
when navigator exists but credentials is undefined. Update the guard in this
function to safely probe navigator.credentials with optional chaining or an
explicit credentials existence check before accessing create, matching the safer
PublicKeyCredential probe so the fallback path can run.
In `@packages/ai-byok/src/react/byok-key-manager.tsx`:
- Around line 121-129: The password input placeholder in byok-key-manager.tsx is
using the raw provider id instead of the user-facing provider label, causing
inconsistent copy with the row header. Update the placeholder logic in the BYOK
key input to use the same label source as the header, namely
BYOK_PROVIDERS[provider].label, while preserving the existing Replace key…
behavior when hasKey is true.
- Around line 93-119: The `ByokKeyManager` action handlers are swallowing async
failures from `setKey` and `clearKey`, so rejected promises never reach the
user. Update the `Validate`, `Clear`, and form submit flows in `ByokKeyManager`
to handle rejections explicitly, similar to how `unlock()` surfaces
`unlockError`, and avoid clearing `draft` until `setKey` succeeds. Use the
existing `validateKey`, `clearKey`, and `setKey` entry points to attach error
handling and show the failure state instead of silently ignoring it.
In `@packages/ai-byok/src/shared/providers.ts`:
- Around line 62-70: The Gemini validation note in providers.ts is inaccurate:
the validate block for the gemini provider uses the x-goog-api-key header, not a
query parameter. Update the inline comment near the gemini validate
configuration to describe the actual header-based authentication used by the
validate URL, so the comment matches the behavior of the gemini provider
definition.
In `@testing/e2e/src/routes/byok.tsx`:
- Around line 4-12: The import order in byok.tsx violates the import/order rule
because the type-only `@tanstack/ai-byok/react` import is placed after the local
`@/components/ChatUI` import. Reorder the imports in the byok module so all
`@tanstack/ai-byok/react` imports (including the Keyring/KeyringStorage type
import) come before the local ChatUI import, keeping the existing grouped
structure intact.
---
Nitpick comments:
In `@packages/ai-byok/src/client/passkey.ts`:
- Around line 299-322: The ensureKey helper currently allows overlapping calls
to start separate WebAuthn ceremonies because cachedKey/cachedMeta are only
populated after awaited work completes. Update ensureKey to memoize and share a
single in-flight promise so concurrent callers (for example load and save, or
rapid double-save) all await the same ceremony; reuse the existing
cachedKey/cachedMeta fast path, but ensure the first unresolved call to
ensureKey owns the idbGet/registerPasskey flow and later calls return that same
promise until it settles.
In `@packages/ai-byok/src/client/validate.ts`:
- Around line 22-40: The validateKey() fetch call currently has no timeout, so a
slow or stalled provider can leave the request and validating state hanging
forever. Update validateKey() in validate.ts to use an AbortSignal with a
timeout (for example via AbortController) around the fetch call, and ensure
timeout aborts are handled as a validation failure instead of hanging. Keep the
existing providerValidateConfig(), response handling, and ValidationStatus
behavior intact.
In `@packages/ai-byok/src/react.ts`:
- Line 4: The public re-export in react.ts is exposing ByokContext directly,
which lets consumers bypass the safety checks in useByok and receive undefined
silently. Remove ByokContext from the public export surface in react.ts, keep it
internal or test-only in react/byok-context, and ensure consumers are directed
to use the useByok hook (and ByokProvider) for all supported access paths.
In `@packages/ai-byok/src/react/byok-key-manager.tsx`:
- Around line 101-107: The Clear action in the byok-key-manager component
triggers immediate key deletion with no safeguard, so add a lightweight
confirmation step before calling clearKey(provider). Update the button behavior
in ByokKeyManager to require a second explicit confirmation (or a confirm
dialog/toggle) before clearing the saved key, while keeping the existing
clearKey provider flow unchanged once confirmed.
- Line 88: Remove the redundant masked property check in byok-key-manager.tsx’s
render condition: the hasKey guard already covers all non-empty KeyStatus
values, so update the conditional around the masked UI branch to rely on hasKey
alone and keep the logic aligned with the KeyStatus union defined in
byok-context.tsx.
In `@packages/ai-byok/src/server/byok-missing.ts`:
- Around line 39-44: `isByokMissingBody` currently only verifies `error.type`,
so it can accept malformed bodies while claiming to be `ByokMissingBody`. Update
the guard in `byok-missing.ts` to fully validate the asserted shape by checking
`error.provider` and `error.message` alongside `error.type`, ensuring `provider`
matches a valid `ProviderId`-like string and `message` is a string before
returning true. Keep the checks inside `isByokMissingBody` so `with-byok.ts` can
safely call `onMissingKey(body.error.provider)` without trusting unvalidated
data.
In `@packages/ai-byok/tests/byok.test.ts`:
- Around line 1-16: The test file import ordering is violating lint rules: the
named import from byok helpers has an out-of-order symbol, and the type-only
import is placed before the client/passkey imports. Reorder the imports in
byok.test.ts so the named specifiers in the ../src/index import are alphabetized
and the type import for Keyring is moved to the correct position after the
../src/client/passkey import, keeping the existing symbols and groupings intact.
In `@packages/ai-byok/tests/react.test.tsx`:
- Around line 6-7: The React test file has duplicate type-only imports from the
same module, triggering import/no-duplicates. Update the import section in
react.test.tsx to merge the Keyring and KeyringStorage type imports into a
single import statement from ../src/index, keeping the existing type-only form
and preserving the referenced symbols.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b9c868f3-0974-4289-8a38-e41c277bf284
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (35)
.changeset/byok-package.mdexamples/ts-react-chat/package.jsonexamples/ts-react-chat/src/components/ByokKeyDialog.tsxexamples/ts-react-chat/src/lib/byok-config.tsexamples/ts-react-chat/src/routes/api.tanchat.tsexamples/ts-react-chat/src/routes/index.tsxknip.jsonpackages/ai-byok/README.mdpackages/ai-byok/package.jsonpackages/ai-byok/src/client/keyring.tspackages/ai-byok/src/client/passkey.tspackages/ai-byok/src/client/storage.tspackages/ai-byok/src/client/validate.tspackages/ai-byok/src/client/with-byok.tspackages/ai-byok/src/index.tspackages/ai-byok/src/react.tspackages/ai-byok/src/react/byok-context.tsxpackages/ai-byok/src/react/byok-key-manager.tsxpackages/ai-byok/src/react/use-byok.tspackages/ai-byok/src/server.tspackages/ai-byok/src/server/byok-missing.tspackages/ai-byok/src/server/get-byok-key.tspackages/ai-byok/src/server/scrub.tspackages/ai-byok/src/shared/providers.tspackages/ai-byok/tests/byok.test.tspackages/ai-byok/tests/react.test.tsxpackages/ai-byok/tsconfig.jsonpackages/ai-byok/vite.config.tstesting/e2e/README.mdtesting/e2e/package.jsontesting/e2e/src/lib/providers.tstesting/e2e/src/routeTree.gen.tstesting/e2e/src/routes/api.byok-chat.tstesting/e2e/src/routes/byok.tsxtesting/e2e/tests/byok.spec.ts
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
examples/ts-react-chat/src/routes/index.tsx (2)
389-392: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winAvoid mutating refs during render.
Writing to
keysRef.currentandstatusRef.currentduring the render phase violates React's pure function rules and can cause state inconsistencies in React 19's Concurrent Mode or Strict Mode. Move these mutations into auseEffecthook to ensure they only execute after a render commits.🔒️ Proposed fix to synchronize refs safely
- const keysRef = useRef(keys)- keysRef.current = keys- const statusRef = useRef(status)- statusRef.current = status+ const keysRef = useRef(keys)+ const statusRef = useRef(status)++ useEffect(() => {+ keysRef.current = keys+ statusRef.current = status+ }, [keys, status])🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@examples/ts-react-chat/src/routes/index.tsx` around lines 389 - 392, Update the ref synchronization near keysRef and statusRef so render no longer assigns to either .current; synchronize both refs inside a useEffect that depends on keys and status, while preserving their latest committed values for the existing consumers.
393-424: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winPrevent UI flash during initial environment key status fetch.
Initializing
envKeyStatusto an empty object{}causes!envKeyStatus[activeByokId]to evaluate totrueon the first render before the server response arrives, flashing the "No key..." warning prematurely. Introduce a loaded state to prevent this visual glitch.✨ Proposed fix to delay `notUsable` evaluation
- const [envKeyStatus, setEnvKeyStatus] = useState<- Partial<Record<ProviderId, boolean>>- >({})- useEffect(() => {- void getEnvKeyStatus().then(setEnvKeyStatus)- }, [])+ const [envKeyStatus, setEnvKeyStatus] = useState<+ Partial<Record<ProviderId, boolean>>+ >({})+ const [isEnvKeyLoaded, setIsEnvKeyLoaded] = useState(false)+ useEffect(() => {+ void getEnvKeyStatus().then((res) => {+ setEnvKeyStatus(res)+ setIsEnvKeyLoaded(true)+ })+ }, []) // Key dialog, opened either by the toolbar icon or reactively when the relay // reports a missing key. const [keyDialog, setKeyDialog] = useState<{ open: boolean provider: ProviderId | null }>({ open: false, provider: null }) // The relay returned a byokMissing 401 (no server key, no BYOK key). If we // already hold that key but it's locked, unlock it; otherwise prompt to add. const handleMissingKey = useCallback( (provider: ProviderId) => { if (statusRef.current[provider]?.state === 'locked') { void unlock() } else { setKeyDialog({ open: true, provider }) } }, [unlock], ) const activeByokId = byokIdForProvider(selectedModel.provider) // The selected model can't run right now if its provider has no server key // and no decrypted key in the browser. const notUsable = - activeByokId != null && !envKeyStatus[activeByokId] && !keys[activeByokId]+ isEnvKeyLoaded &&+ activeByokId != null &&+ !envKeyStatus[activeByokId] &&+ !keys[activeByokId]🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@examples/ts-react-chat/src/routes/index.tsx` around lines 393 - 424, Track whether the environment key status request has completed alongside envKeyStatus, updating that flag in the getEnvKeyStatus flow. Update notUsable to remain false until the status is loaded, then apply the existing provider-key checks so the “No key” warning does not flash on the initial render.
🧹 Nitpick comments (1)
docs/api/ai-byok.md (1)
275-277: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winSpecify a language for the fenced code block.
This fenced code block lacks a language identifier, which triggers a markdownlint warning (MD040).
♻️ Proposed fix
-```+```http x-byok-<provider>: <api-key></details> <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.In
@docs/api/ai-byok.mdaround lines 275 - 277, Specify the HTTP language
identifier on the fenced code block containing the x-byok header example by
changing its opening fence to use http.</details> <!-- cr-comment:v1:706760bc5f68c00716f954e6 --> _Source: Linters/SAST tools_ </blockquote></details> </blockquote></details> <details> <summary>🤖 Prompt for all review comments with AI agents</summary>Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.Inline comments:
In@docs/advanced/byok.md:
- Around line 377-379: Add a small client-side code snippet in the “Lower-level
client API” section showing how to consume the server-provided boolean
environment-key flags and warn the user before selecting an unsupported model.
Keep the example limited to flag consumption and UI warning behavior, without
exposing env-key values.- Around line 120-129: Update the handleMissingKey callback in Chat to accept
the existing ProviderId type instead of string, then access status with that
typed provider directly and remove theasassertion.- Line 167: Update the model identifier passed to createOpenaiChat in the BYOK
documentation example from gpt-5.2 to gpt-5.5, leaving the adapter and apiKey
usage unchanged.In
@docs/getting-started/overview.md:
- Around line 104-111: Update the getting-started/overview entry in
docs/config.json to set updatedAt to 2026-07-16, reflecting the documentation
change in the overview page.- Around line 104-111: Expand the
@tanstack/ai-byoksection in the overview
documentation with minimal server and client examples: add a relay endpoint
showing server-side extraction via getByokKey or byokMissing, and a
corresponding client snippet demonstrating key transport and consumption through
the BYOK React bindings. Keep the examples concise and consistent with the
documented API.In
@packages/ai-byok/src/client/openrouter-pkce.ts:
- Around line 23-27: Align the PKCE pending state and token exchange parameters
with the authorization behavior in the OpenRouter PKCE flow. When useS256 is
false, do not retain or submit a plain codeVerifier unless the authorization URL
also sends the matching plain challenge; otherwise omit the verifier fields for
that path. Update the OpenRouterPkcePending handling and related
authorization/exchange logic while preserving the S256 flow.In
@packages/ai-byok/src/react/use-openrouter-pkce.ts:
- Around line 35-60: Update the useEffect in the PKCE hook to deduplicate
concurrent completeOpenRouterPkceFromUrl exchanges across React Strict Mode
re-runs, using a shared in-flight promise or completion ref. Ensure both effect
instances reuse the same one-time-code exchange, while cancellation still
prevents stale instances from updating setKey, setError, or setCompleting.In
@packages/ai-byok/tests/byok.test.ts:
- Line 1: Relocate the unit tests from the dedicated tests directory into the
corresponding src directories, placing each test alongside the source it covers;
split the combined coverage into focused files such as passkey.test.ts and
openrouter-pkce.test.ts where appropriate, while preserving the existing test
behavior.Outside diff comments:
In@examples/ts-react-chat/src/routes/index.tsx:
- Around line 389-392: Update the ref synchronization near keysRef and statusRef
so render no longer assigns to either .current; synchronize both refs inside a
useEffect that depends on keys and status, while preserving their latest
committed values for the existing consumers.- Around line 393-424: Track whether the environment key status request has
completed alongside envKeyStatus, updating that flag in the getEnvKeyStatus
flow. Update notUsable to remain false until the status is loaded, then apply
the existing provider-key checks so the “No key” warning does not flash on the
initial render.Nitpick comments:
In@docs/api/ai-byok.md:
- Around line 275-277: Specify the HTTP language identifier on the fenced code
block containing the x-byok header example by changing its opening fence to use
http.</details> <details> <summary>🪄 Autofix (Beta)</summary> Fix all unresolved CodeRabbit comments on this PR: - [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended) - [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes </details> --- <details> <summary>ℹ️ Review info</summary> <details> <summary>⚙️ Run configuration</summary> **Configuration used**: defaults **Review profile**: CHILL **Plan**: Pro **Run ID**: `50886328-979f-4edb-b72a-6927d656960a` </details> <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between 31a8d7242de37f7129fe902c454923fb2c5291ad and 2eafb8f4f3849f2391c1639afa5b5cc7dab5f1ae. </details> <details> <summary>📒 Files selected for processing (16)</summary> * `docs/advanced/byok.md` * `docs/api/ai-byok.md` * `docs/chat/connection-adapters.md` * `docs/config.json` * `docs/getting-started/overview.md` * `examples/ts-react-chat/src/components/ByokKeyDialog.tsx` * `examples/ts-react-chat/src/routes/index.tsx` * `packages/ai-byok/README.md` * `packages/ai-byok/src/client/openrouter-pkce.ts` * `packages/ai-byok/src/client/passkey.ts` * `packages/ai-byok/src/index.ts` * `packages/ai-byok/src/react.ts` * `packages/ai-byok/src/react/byok-context.tsx` * `packages/ai-byok/src/react/byok-key-manager.tsx` * `packages/ai-byok/src/react/use-openrouter-pkce.ts` * `packages/ai-byok/tests/byok.test.ts` </details> <details> <summary>🚧 Files skipped from review as they are similar to previous changes (6)</summary> * packages/ai-byok/src/react/byok-key-manager.tsx * packages/ai-byok/src/react.ts * examples/ts-react-chat/src/components/ByokKeyDialog.tsx * packages/ai-byok/src/react/byok-context.tsx * packages/ai-byok/src/client/passkey.ts * packages/ai-byok/README.md </details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/api/ai-byok.md`:
- Line 275: Update the header example fenced code block in the AI BYOK
documentation to specify the text language identifier, changing the unlabeled
fence to a text-labeled fence so it satisfies markdownlint MD040.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 27c17c30-1230-4c63-a91d-a8ffec076451
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (40)
.changeset/byok-package.mddocs/advanced/byok.mddocs/api/ai-byok.mddocs/chat/connection-adapters.mddocs/config.jsondocs/getting-started/overview.mdexamples/ts-react-chat/package.jsonexamples/ts-react-chat/src/components/ByokKeyDialog.tsxexamples/ts-react-chat/src/lib/byok-config.tsexamples/ts-react-chat/src/routes/api.tanchat.tsexamples/ts-react-chat/src/routes/index.tsxknip.jsonpackages/ai-byok/README.mdpackages/ai-byok/package.jsonpackages/ai-byok/src/client/keyring.tspackages/ai-byok/src/client/openrouter-pkce.tspackages/ai-byok/src/client/passkey.tspackages/ai-byok/src/client/storage.tspackages/ai-byok/src/client/validate.tspackages/ai-byok/src/client/with-byok.tspackages/ai-byok/src/index.tspackages/ai-byok/src/react.tspackages/ai-byok/src/react/byok-context.tsxpackages/ai-byok/src/react/byok-key-manager.tsxpackages/ai-byok/src/react/use-byok.tspackages/ai-byok/src/react/use-openrouter-pkce.tspackages/ai-byok/src/server.tspackages/ai-byok/src/server/byok-missing.tspackages/ai-byok/src/server/get-byok-key.tspackages/ai-byok/src/server/scrub.tspackages/ai-byok/src/shared/providers.tspackages/ai-byok/tests/byok.test.tspackages/ai-byok/tests/react.test.tsxpackages/ai-byok/tsconfig.jsonpackages/ai-byok/vite.config.tstesting/e2e/README.mdtesting/e2e/package.jsontesting/e2e/src/lib/providers.tstesting/e2e/src/routeTree.gen.tstesting/e2e/src/routes/api.byok-chat.ts
🚧 Files skipped from review as they are similar to previous changes (36)
- packages/ai-byok/src/server/get-byok-key.ts
- packages/ai-byok/src/react/use-byok.ts
- packages/ai-byok/package.json
- packages/ai-byok/tsconfig.json
- examples/ts-react-chat/src/lib/byok-config.ts
- docs/config.json
- docs/chat/connection-adapters.md
- packages/ai-byok/vite.config.ts
- knip.json
- packages/ai-byok/src/server.ts
- packages/ai-byok/src/index.ts
- testing/e2e/src/lib/providers.ts
- packages/ai-byok/src/client/validate.ts
- packages/ai-byok/src/server/byok-missing.ts
- packages/ai-byok/src/server/scrub.ts
- packages/ai-byok/src/client/storage.ts
- .changeset/byok-package.md
- testing/e2e/README.md
- packages/ai-byok/src/client/keyring.ts
- testing/e2e/package.json
- testing/e2e/src/routes/api.byok-chat.ts
- packages/ai-byok/tests/react.test.tsx
- packages/ai-byok/src/shared/providers.ts
- packages/ai-byok/src/react.ts
- packages/ai-byok/README.md
- examples/ts-react-chat/src/routes/index.tsx
- testing/e2e/src/routeTree.gen.ts
- packages/ai-byok/src/react/use-openrouter-pkce.ts
- packages/ai-byok/src/react/byok-key-manager.tsx
- examples/ts-react-chat/src/components/ByokKeyDialog.tsx
- packages/ai-byok/src/react/byok-context.tsx
- packages/ai-byok/src/client/openrouter-pkce.ts
- examples/ts-react-chat/src/routes/api.tanchat.ts
- packages/ai-byok/src/client/passkey.ts
- packages/ai-byok/src/client/with-byok.ts
- packages/ai-byok/tests/byok.test.ts
Uh oh!
There was an error while loading. Please reload this page.
Users define a keyring, pass it into useChat or useGeneration, and store keys in their own UI. Keys travel in x-byok-* headers only.
| * Optional BYOK keyring. On each send the client prepares the resolved | ||
| * provider and stamps `x-byok-*` request headers. Keys never go in the body. | ||
| */ | ||
| byok?: ByokClient |
There was a problem hiding this comment.
I'm thinking byok and byokProvider should be on the one field. i.e. byok should be {client, provider}
| Authorization: `Bearer ${key}`, | ||
| }) | ||
| export const BYOK_PROVIDERS = { |
There was a problem hiding this comment.
Not sure about listing providers in the ai package. Did I do that?
Drop the hardcoded BYOK_PROVIDERS catalog. Provider ids are slugs. Each adapter exports a defineByokProvider object whose id is required and cannot be optional.
OpenRouter OAuth mints a user key. completeOpenRouterPkceIntoByok writes it with the required adapter slug so the relay reads x-byok-openrouter.
Adapters export env var names on defineByokProvider. Relays call getByokKey from @tanstack/ai/byok/server so process.env stays out of the client graph. Remove getByokOrEnvKey.
Changes
This PR rewrites BYOK as a headless client. There is no
@tanstack/ai-byokpackage. There is no packaged key dialog.Users define a keyring, pass it into
useChatoruseGeneration, and store keys in their own UI. Keys travel inx-byok-*headers only. The key never goes in the JSON body.defineByokin@tanstack/ai-client/byok— create oneByokClientfor the appbyokon hooks — pass it intouseChat,useGeneration, and the other generation hooks (React, Preact, Solid, Vue, Svelte, Angular)useByok(client)— live snapshot for your own form (last four characters, lock state, missing-key prompt)x-byok-<provider>on each POST.runContext.headerscarries them. The key is not in the body or the message history@tanstack/ai/byok(getByokOrEnvKey,byokMissing,scrubSecrets,maskKey). The header wins. If it is empty, the relay can fall back to envdefaultByokStorage()uses a passkey when the browser supports it (WebAuthn PRF → HKDF → AES-GCM). If it does not, keys stay in session memory. After refresh, saved keys arelockeduntilunlock()byok.update(provider, value)from your own UI. This library does not shipByokKeyDialogorByokProviderai,ai-client, and each frameworkts-react-chathas an app-owned key form and a relay that reads the headertesting/e2e/tests/byok.spec.tscovers header transport andbyokMissingUsage
Checklist
pnpm run test:pr.Targeted tests that did run:
@tanstack/aiBYOK unit tests,@tanstack/ai-clientBYOK unit tests,@tanstack/ai-reactBYOK hook tests, andtesting/e2e/tests/byok.spec.ts.Release Impact