growth(day-27): @byok-relay/preact — Preact hooks for Astro/Vite BYOK AI#64
growth(day-27): @byok-relay/preact — Preact hooks for Astro/Vite BYOK AI#64alokit-bot wants to merge 4 commits into
Conversation
packages/solid/src/index.js: four stores with SolidJS signal contract ([getter,setter]): - createByokRelayStore: token registration + key CRUD + logout (localStorage persistence, SolidStart SSR-safe via typeof window guard) - createChatStore: stateful message list, non-streaming, openai/anthropic/groq/mistral/openrouter, systemPrompt + extraParams - createStreamingChatStore: SSE streaming with AbortController, stopStreaming(), streamingContent() live signal, partial-commit on abort - createRelayHealthStore: polls /health on configurable interval, refetch(), destroy() for onCleanup() lifecycle management Signal shim: all stores work without solid-js installed (signals degrade to plain getter/setter pairs for testing and non-Solid environments). 20 smoke tests passing (node test/stores.test.js). packages/solid/README.md: full API docs, SolidJS quick-start, SolidStart SSR guide, all four stores documented, provider table, self-hosting section, related packages table. llms.txt: SolidJS Signals section + npm link. README.md: SolidJS stores section before 'For AI coding agents' with install + streaming example + links to other framework packages. package.json: workspaces added. metrics/daily.jsonl: 2026-07-03 snapshot (stars=52 forks=0 watchers=1 clones=110 views=20). Next for Avi: cd packages/solid && npm publish --access public
… BYOK AI packages/angular/src/index.js: four Angular injectable services: - ByokRelayService (token registration + key CRUD + logout, localStorage persistence, Analog SSR-safe in-memory fallback) - ChatService (stateful message list, non-streaming, openai/anthropic/groq/mistral/openrouter, systemPrompt + extraParams, rollback on error) - StreamingChatService (SSE streaming with AbortController, stopStreaming(), streamingContent() live signal, partial-commit on abort) - RelayHealthService (polls /health, check(deep=true) for upstream ping, destroy() for ngOnDestroy cleanup) Angular signal shim: services use Angular 16+ signal() when @angular/core is detected; plain getter/setter shim otherwise — same reactive API in both cases. provideByokRelay(config): makeEnvironmentProviders() with all four services wired together via deps when @angular/core is installed; plain createByokRelayBundle() otherwise (for testing and standalone use). createByokRelayBundle(config): factory function for standalone / non-Angular usage (Analog islands, unit tests, vanilla JS). 29 smoke tests passing (node test/services.test.js, no DOM, no @angular/core required): - ByokRelayService: 12 tests (register, getOrRegister, storeKey, listKeys, deleteKey, rotateKey, logout, error signals) - ChatService: 5 tests (send, rollback, clear) - StreamingChatService: 4 tests (stream, stopStreaming partial-commit) - RelayHealthService: 5 tests (check, deep check, error, polling lifecycle) - createByokRelayBundle: 2 tests (service wiring) packages/angular/README.md: full API docs, Angular 14+ quick-start, Angular 16+ signals note, Analog SSR guide, provider table, self-hosting section, related packages table. llms.txt: Angular Injectable Services section + npm link. README.md: Angular services section after SolidJS stores. package.json: workspaces already includes packages/*. metrics/daily.jsonl: 2026-07-04 snapshot (stars=52 forks=0 watchers=1 clones=112 views=22). Next for Avi: cd packages/angular && npm publish --access public
… adapter Implements LanguageModelV1 spec so byok-relay can be used as a first-class provider with generateText, streamText, generateObject, and all AI SDK tools. packages/vercel-ai/src/index.js: - createByokRelayProviderSync(config): synchronous factory, lazy token registration; module-scope safe for Next.js / SvelteKit / Nuxt initialisation - createByokRelayProvider(config): async factory, eager token registration - createLanguageModel(): LanguageModelV1 implementation — specificationVersion=v1, doGenerate() (non-streaming, full OpenAI response parsing, tool_calls, finish_reason), doStream() (SSE ReadableStream, text-delta / tool-call-delta / finish / error parts) - parseModelId(): 'provider/model' or bare model (default: openai) - promptToMessages(): AI SDK LanguageModelV1Prompt → OpenAI messages array, handles system/user/assistant/tool roles, multi-part content, image_url conversion - provider.storeKey(provider, apiKey): store user's API key in relay - provider.health(deep?): relay liveness/readiness check - provider.stats(appId?): per-user usage stats - provider.deleteAccount(): GDPR erasure packages/vercel-ai/test/smoke.test.js: 48 smoke tests, all passing packages/vercel-ai/README.md: full API docs, Next.js/SvelteKit/Nuxt examples, all supported providers, AI SDK feature matrix, related packages table llms.txt: Vercel AI SDK Custom Provider section + npm link README.md: Vercel AI SDK section after Angular (before 'For AI coding agents') metrics/daily.jsonl: 2026-07-04 snapshot (stars=52 forks=0 watchers=1 clones=112 views=22) Next for Avi: cd packages/vercel-ai && npm publish --access public
packages/preact/src/index.js: four hooks with runtime hook resolution (preact/hooks → react → inline shim): useByokRelay (token registration + key CRUD + logout, SSR-safe localStorage), useChat (stateful non-streaming, openai/anthropic/groq/ mistral/openrouter, systemPrompt + extraParams), useStreamingChat (SSE streaming with AbortController, stopStreaming(), streamingContent live string, partial-commit on abort), useRelayHealth (polls /health, check(deep?), intervalMs control). Hook shim ensures the package works in test/SSR environments with no preact installed. 24 smoke tests passing. packages/preact/README.md: full API docs, Astro component island quick-start, all four hooks documented, provider table, self-hosting section, related packages table. llms.txt: Preact Hooks section + npm link. README.md: Preact hooks section after Angular (before 'Vercel AI SDK') with Astro island code example. metrics/daily.jsonl: 2026-07-05 snapshot (stars=52 forks=0 watchers=1 clones=112 views=22). **Next for Avi:** cd packages/preact && npm publish --access public.
📝 WalkthroughWalkthroughAdds four new framework-specific client packages (Angular, Preact, Solid, Vercel AI SDK) under a new npm workspace, each providing token/key management, chat, streaming chat, and health-check functionality against a BYOK relay, along with package manifests, READMEs, source implementations, and smoke-test suites. Updates root package.json, README.md, llms.txt, and metrics. ChangesWorkspace config and root documentation
Angular package
Preact package
Solid package
Vercel AI SDK package
Estimated code review effort: 4 (Complex) | ~75 minutes Suggested reviewers: PoemA rabbit hopped through packages new, 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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 |
There was a problem hiding this comment.
Actionable comments posted: 20
🧹 Nitpick comments (13)
packages/solid/src/index.js (2)
264-272: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
opts.appIdis documented but unused increateChatStore,createStreamingChatStore, andcreateRelayHealthStore.JSDoc for these three factories lists
opts.appIdas a parameter, but it's never read in the implementation (onlycreateByokRelayStoreconsumesappId). Either remove the stale doc entry or clarify its intended purpose.Also applies to: 360-367, 503-508
🤖 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/solid/src/index.js` around lines 264 - 272, The JSDoc for createChatStore, createStreamingChatStore, and createRelayHealthStore still documents opts.appId even though those factories do not read it. Update the docs in the relevant factory comments in packages/solid/src/index.js to either remove the stale appId parameter or clearly state that only createByokRelayStore uses it, and keep the parameter lists aligned with the actual implementation.
61-88: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
createStoreshim is unused dead code.
createStore/createSignal's sibling helper is defined but never called or exported from this file — onlycreateSignalis used throughout. Consider removing it to reduce maintenance surface, or wire it in if it was intended for a future store-based feature.🤖 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/solid/src/index.js` around lines 61 - 88, The createStore shim is dead code in this module: it is defined in the index.js file but never referenced or exported, while createSignal is the only helper used elsewhere. Remove the unused createStore function and its related proxy/subscriber logic from the module, or alternatively connect it to an actual export/call site if this was meant to support a store-based API.packages/solid/test/stores.test.js (1)
342-348: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
stopStreaming()test doesn't exercise an in-flight abort.The test only calls
stopStreaming()on an idle store (nosendMessage()in flight), so it verifies the no-op path, not actual abort-during-stream behavior implied by the test name.🤖 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/solid/test/stores.test.js` around lines 342 - 348, The stopStreaming() test only covers the idle no-op path, so it doesn’t verify aborting an active stream. Update the test in stores.test.js to start a streaming request through createStreamingChatStore and sendMessage() so there is an in-flight stream, then call stopStreaming() and assert the stream is aborted (for example, loading() becomes false and streamingContent() stops changing). Use the existing createStreamingChatStore and stopStreaming() symbols to keep the test aligned with the behavior name.packages/angular/test/services.test.js (1)
1-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winNative Angular
signal()path and non-default providers are untested.The suite runs with no
@angular/coreinstalled, socreateAngularSignal's Angular-16+ branch is never exercised, and only theopenaiprovider is tested for Chat/Streaming — this is why theanthropicrequest-construction bug insrc/index.jswent unnoticed. Consider adding@angular/coreas adevDependency(used only fornpm test) plus ananthropictest case that asserts on the request body shape and URL.🤖 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/angular/test/services.test.js` around lines 1 - 18, The smoke suite currently only covers the non-Angular fallback and the openai provider, so the Angular `signal()` branch in `createAngularSignal` and the non-default provider path are not exercised. Update the test setup in `services.test.js` to run with `@angular/core` available during `npm test`, then add coverage for `createAngularSignal`’s Angular-native path plus an `anthropic` Chat/Streaming case that asserts the constructed request body shape and URL using `ChatService` and `StreamingChatService`.packages/angular/src/index.js (1)
99-222: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDependent services reach into
ByokRelayService's private fields instead of a public API.
ChatService(line 270, 290),StreamingChatService(line 375, 399), andRelayHealthService(line 512-513) all directly accessthis._relay._storage,this._relay._storageKey, andthis._relay._relayUrl— underscore-prefixed fields that are meant to be private implementation details. This tightly couples every dependent service toByokRelayService's internals; any future rename/refactor of those fields silently breaks three other classes.Consider exposing small public accessors (e.g.,
getTokenValue(),get relayUrl()) onByokRelayServicefor dependents to use instead.get token() { return this._token.value; } get loading() { return this._loading.value; } get error() { return this._error.value; } + + /** Public accessor for the raw stored token (distinct from the reactive signal). */ + getTokenValue() { return this._storage.getItem(this._storageKey); } + + get relayUrl() { return this._relayUrl; }🤖 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/angular/src/index.js` around lines 99 - 222, Dependent services are reaching into ByokRelayService private fields (_relayUrl, _storage, _storageKey), which tightly couples ChatService, StreamingChatService, and RelayHealthService to internal implementation details. Add small public accessors on ByokRelayService for the values they need, such as a relayUrl getter and token/storage helpers, and update those dependent classes to use the public API instead of direct underscore-prefixed field access. Keep the existing behavior unchanged while removing all direct references to _relay, _storage, and _storageKey outside ByokRelayService.packages/vercel-ai/src/index.js (2)
454-456: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win
chat()relies onthis, breaking if destructured off the provider object.
chat(modelId, overrides) { return this.languageModel(...) }only works when called asprovider.chat(...). Common consumption patterns likeconst { chat } = provider;would break withthisbeingundefined.🛠️ Suggested fix
- chat(modelId, overrides = {}) { - return this.languageModel(modelId, overrides); - }, + chat: languageModel,(reference the
languageModelfunction/closure directly instead ofthis.languageModel, applied to both factories)Also applies to: 519-521
🤖 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/vercel-ai/src/index.js` around lines 454 - 456, `chat()` is bound to `this`, so destructuring the method off the provider breaks when `this` is undefined. Update the `chat` implementation in both factory paths to call the captured `languageModel` closure directly instead of `this.languageModel`, keeping the same `modelId` and `overrides` behavior while making `const { chat } = provider` safe.
205-210: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win
toolChoice: { type: 'tool', toolName }is not forwarded.Only
required/nonemap explicitly; a specific tool selection ({ type: 'tool', toolName: '...' }, perLanguageModelV1CallOptions) silently falls back to'auto', losing the caller's intent.🛠️ Suggested fix
body.tool_choice = options.toolChoice?.type === 'required' ? 'required' : options.toolChoice?.type === 'none' ? 'none' + : options.toolChoice?.type === 'tool' + ? { type: 'function', function: { name: options.toolChoice.toolName } } : 'auto';🤖 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/vercel-ai/src/index.js` around lines 205 - 210, The tool choice mapping in the Vercel AI request builder drops specific tool selections, since the logic in the body.tool_choice assignment only handles required and none and falls back to auto for options.toolChoice.type === 'tool'. Update the mapping in the relevant request-building code in index.js so LanguageModelV1CallOptions with { type: 'tool', toolName } is forwarded to the provider instead of being converted to auto, while keeping the existing required/none behavior intact.packages/vercel-ai/test/smoke.test.js (1)
194-234: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winStreaming test coverage doesn't exercise a trailing-chunk without a final newline, or multi-result
toolmessages / specifictoolChoiceselection.These gaps line up with the correctness issues flagged in
src/index.js(buffer flush on stream end, multi tool-result truncation,toolChoice: { type: 'tool' }); adding cases here would have caught them.🤖 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/vercel-ai/test/smoke.test.js` around lines 194 - 234, Extend the smoke tests around doStream in smoke.test.js to cover the missing edge cases: add a streaming fixture that ends without a trailing newline so the parser’s final buffer flush is exercised, and add tool-related streaming cases that verify multiple tool-result messages are preserved instead of truncated. Also add a case for toolChoice selection using the doStream path to confirm toolChoice: { type: 'tool' } is handled correctly. Use the existing model.doStream, ndjsonStream, and finish/text-delta assertions as the entry points for locating and validating the behavior.packages/vercel-ai/package.json (1)
1-44: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winNo
testscript despite a smoke-test suite existing.
test/smoke.test.jsis added but this manifest has noscripts.testentry to run it.🛠️ Suggested addition
"main": "src/index.js", + "scripts": { + "test": "node test/smoke.test.js" + }, "files": [🤖 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/vercel-ai/package.json` around lines 1 - 44, The package manifest for `@byok-relay/vercel-ai` is missing a test runner entry even though a smoke test suite exists. Add a scripts.test command in package.json so the new test/smoke.test.js can be executed consistently, and make sure it points to the correct test runner used by this package. Use the package.json scripts section as the place to wire this up.packages/preact/README.md (1)
7-9: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a language to the fenced install block.
markdownlint flags MD040 here — the fence has no language identifier.
📝 Fix
-``` +```bash npm install `@byok-relay/preact`</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
@packages/preact/README.mdaround lines 7 - 9, The install command fence in
the README is missing a language identifier, triggering markdownlint MD040.
Update the fenced block around the package install command to include an
explicit shell language tag (for example, in the README install snippet) so the
markdown fence is correctly annotated.</details> <!-- cr-comment:v1:cf50e117b5c9ab644a49eed3 --> _Source: Linters/SAST tools_ </blockquote></details> <details> <summary>packages/preact/test/hooks.test.js (2)</summary><blockquote> `280-308`: _📐 Maintainability & Code Quality_ | _🔵 Trivial_ | _⚖️ Poor tradeoff_ **No test exercises the actual streaming/SSE path.** `useStreamingChat` tests only check the returned API shape, a no-op `stopStreaming` call (see separate comment), and `clearMessages`. The core `_streamSSE` accumulation/commit logic (the most complex part of `src/index.js`) has zero coverage — a mocked `fetch` returning a `ReadableStream`-like body with `data:` chunks would validate `streamingContent` accumulation, final `messages` commit, and abort/rollback behavior. <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
@packages/preact/test/hooks.test.jsaround lines 280 - 308, The
useStreamingChat test suite currently misses coverage for the real SSE streaming
path in useStreamingChat/_streamSSE, so add a test that mocks fetch with a
ReadableStream-like body emitting data: chunks and verifies streamingContent
accumulation, final messages commit, and abort/rollback behavior. Place the new
coverage alongside the existing exports expected API / clearMessages tests so
the core streaming logic in src/index.js is exercised.</details> <!-- cr-comment:v1:3b2ef43797e5af96638f7ec4 --> --- `17-27`: _📐 Maintainability & Code Quality_ | _🔵 Trivial_ | _⚡ Quick win_ **Cleanup is skipped when an assertion fails mid-test.** `afterEach()` is called manually at the end of each test body rather than in a `finally` inside the `test()` harness. If an `assert*` call throws, the remaining lines (including `afterEach()`) never run, leaking `_fetchImpl`/`_store` state into subsequent tests. <details> <summary>♻️ Suggested fix</summary> ```diff async function test (name, fn) { try { await fn(); console.log(` ✓ ${name}`); passed++; } catch (err) { console.error(` ✗ ${name}`); console.error(` ${err.message}`); failed++; + } finally { + afterEach(); } }Then the repeated
afterEach();calls inside each test body become unnecessary and can be removed.Also applies to: 85-85
🤖 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/preact/test/hooks.test.js` around lines 17 - 27, The custom test harness in test() is letting cleanup be skipped when an assertion throws, so move the shared afterEach() cleanup into the test() function itself using a finally-style path around await fn(). Update the hooks tests that currently call afterEach() manually so they rely on the harness cleanup instead, ensuring _fetchImpl and _store are reset even when a test fails mid-body.packages/preact/package.json (1)
30-37: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDeclare
reactas an optional peer dependency too.
src/index.jsexplicitly falls back torequire('react')whenpreact/hooksisn't resolvable (runtime hook resolution), but onlypreactis declared here. Consumers relying on the React fallback path get no manifest signal (npm won't warn about a missing/incompatible React version, and tooling that inspects peer deps won't surface this package as React-compatible).📝 Suggested addition
"peerDependencies": { - "preact": ">=10.0.0" + "preact": ">=10.0.0", + "react": ">=16.8.0" }, "peerDependenciesMeta": { "preact": { "optional": true + }, + "react": { + "optional": true } },🤖 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/preact/package.json` around lines 30 - 37, Declare react as an optional peer dependency alongside preact in the package manifest, since src/index.js can fall back to require('react') when preact/hooks is unavailable. Update the peerDependencies and peerDependenciesMeta entries near the existing preact declaration so tooling and consumers see the React compatibility signal, and keep react marked optional like preact.
🤖 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 `@packages/angular/README.md`:
- Around line 32-51: The standalone ChatComponent example uses `*ngFor` and
`[(ngModel)]` but does not import the modules that provide them. Update the
component’s `imports` metadata in the `ChatComponent` example to include
`CommonModule` and `FormsModule` alongside the existing Angular/ByokRelay
imports so the snippet compiles when copied as-is.
In `@packages/angular/src/index.js`:
- Around line 262-317: sendMessage() currently builds an OpenAI-style request
body even when provider is anthropic, which breaks Anthropic relay calls; update
sendMessage() and streamMessage() to branch on the provider, sending Anthropic
requests with a top-level system field instead of systemPrompt in messages and
including max_tokens, while keeping the current OpenAI-compatible payload for
other providers. Use the existing sendMessage() / streamMessage() flow and
provider selection logic to locate the fix.
- Around line 366-467: streamMessage() is still constructing an OpenAI-shaped
payload, so Anthropic/Claude requests won’t get the provider-specific request
body. Update the request assembly in streamMessage() to use the same shared
builder as sendMessage(), ensuring Anthropic gets the correct system and
max_tokens fields while keeping both chat paths aligned. Use the existing
provider handling in streamMessage() and the sendMessage() request builder as
the reference points.
In `@packages/preact/README.md`:
- Around line 210-221: The Related packages table in the Preact README contains
broken sibling links to packages that are not present in this repo. Update the
table in the README content to remove the non-existent package references,
keeping only links that point to actual package directories, or add the missing
packages before restoring those entries.
In `@packages/preact/src/index.js`:
- Around line 19-52: The fallback in _resolveHooks is silently masking a missing
hooks dependency by installing a non-reactive inline shim, which can make
components appear functional while never updating. Update the _resolveHooks
logic in packages/preact/src/index.js so that when both preact/hooks and react
fail to resolve, it emits a clear warning or error before falling back, using
the _resolveHooks function and the inline shim branch as the key location. Keep
the shim only for test/non-DOM support, but make the missing peer dependency
visible to developers so they can install the proper hooks implementation.
- Around line 93-130: The `_streamSSE` flow is treating mid-stream read failures
as a normal completion and can also throw unhandled when `res.body` is
unavailable. Update `_streamSSE` to distinguish an intentional abort from a real
`reader.read()` error in the streaming loop, and call `onError` instead of
falling through to `onDone()` on genuine failures. Also guard the
`res.body.getReader()` path so missing/unsupported bodies surface through the
same error path, and make sure the `sendMessage` caller can clear `isStreaming`
when `_streamSSE` fails.
- Around line 313-385: `useStreamingChat` does not clean up an in-flight stream
when the component unmounts, so add a `useEffect` cleanup that aborts
`abortRef.current` if present. Keep the fix localized to `useStreamingChat` and
make sure the cleanup resets the streaming state safely so `_streamSSE`,
`sendMessage`, and `stopStreaming` do not leave pending async work that can call
`setMessages`, `setStreamingContent`, or `setIsStreaming` after unmount.
- Around line 156-232: `useByokRelay` has a race in `getToken` where concurrent
callers can each POST to `/users` and create duplicate relay users. Add an
in-flight dedupe guard inside `useByokRelay` (for example a ref/promise shared
by `getToken`) so `storeKey`, `deleteKey`, `listKeys`, and direct `getToken`
calls all await the same registration request instead of starting a new one.
Keep the existing `_safeGet(tokenKey)` fast path, but ensure only one
token-registration fetch runs at a time and all callers receive the same
resolved token.
- Around line 189-223: The storeKey, deleteKey, and listKeys callbacks in the
relay API wrapper do not catch fetch failures, so network errors can reject
instead of returning the expected API shape. Update these useCallback helpers in
the Preact index to wrap the fetch/JSON logic in try/catch, returning { ok:
false, error: ... } for storeKey and deleteKey and [] for listKeys on failure,
matching the existing getToken error-handling pattern.
- Around line 262-297: The sendMessage flow in useChat still builds an
OpenAI-shaped body for Anthropic, so special-case provider handling in this
callback and the matching useStreamingChat path. When provider is anthropic,
move systemPrompt to a top-level system field, include max_tokens in the request
body, and avoid sending the current messages shape that Anthropic will reject;
keep the existing behavior for other providers via _PROVIDERS and relayPath.
In `@packages/preact/test/hooks.test.js`:
- Around line 295-299: The `stopStreaming` test in `useStreamingChat` is not
actually executing the method because the function is being passed directly to
`assert` as a truthy value. Update the test so the closure is invoked and
`h.stopStreaming()` really runs, while keeping the “no throw” expectation and
using the existing `useStreamingChat` and `stopStreaming` symbols to locate the
case.
In `@packages/solid/package.json`:
- Line 54: The package license metadata in the solid workspace does not match
the repository’s licensing, so update the `license` field in
`packages/solid/package.json` to align with the root `package.json` and
`LICENSE`, unless this package is intentionally separately licensed. Use the
`license` entry in that package manifest as the fix point, and if a separate
license is intended, add clear package-level documentation of that intent.
In `@packages/solid/src/index.js`:
- Around line 24-31: `createChatStore` and `createStreamingChatStore` currently
only special-case `anthropic`, so `google` requests still use the OpenAI-shaped
payload and response handling. Update the provider branching in these Solid
store helpers to detect `google`/Gemini and use the Google-specific request
body, endpoint shape from `PROVIDER_PATHS.google`, and streamed content parsing
so Gemini messages are sent and accumulated correctly.
In `@packages/vercel-ai/package.json`:
- Around line 27-34: The ai peer dependency range is too broad for the adapter’s
current v1-only implementation, so tighten the constraint in the package
metadata to exclude ai v5+ or add a proper V2 implementation path. Update the
peer range in the vercel-ai package manifest so installs cannot resolve an
incompatible SDK for the existing generateText and streamText adapter behavior,
and keep the optional peer metadata aligned with that change.
In `@packages/vercel-ai/src/index.js`:
- Around line 393-405: The async BYOK relay provider’s getToken path in
createByokRelayProvider can register the token twice when called concurrently
because it only stores _token after await registerToken resolves. Add an
in-flight promise guard like the one used in createByokRelayProviderSync (for
example, a _pending cache) so concurrent calls share the same registration
request. Update getToken and the related token initialization flow to return the
same pending promise until it resolves, then cache the resulting token and clear
the pending state.
- Around line 143-149: The `msg.role === 'tool'` conversion in the message
mapping only forwards `msg.content[0]`, so additional `tool-result` parts are
dropped. Update this branch to emit one OpenAI tool message per item in
`msg.content` (for example by mapping over all results and flattening the
output), using each part’s `toolCallId` and `result` instead of only the first
entry. Make sure the caller in `promptToMessages` flattens the returned array so
the final message list stays flat.
- Around line 294-360: The SSE parsing loop in the stream reader drops any
trailing buffered data when `reader.read()` returns `done`, so the last
`text-delta`, `tool-call-delta`, or `finish`/usage event can be lost if the
final chunk has no newline. Update the streaming logic around the `while (true)`
loop to drain and process the remaining `buf` after the loop ends, using the
same line parsing/JSON handling as the in-loop path so the final SSE event is
emitted correctly.
- Around line 186-215: The `buildBody` helper is spreading `settings` directly
into the OpenAI request body, so camelCase overrides like `maxTokens` and other
documented settings are not being normalized and may be ignored by the API.
Update `buildBody` in `packages/vercel-ai/src/index.js` so `settings` is passed
through the same camelCase-to-snake_case mapping used for `options.*`,
especially for `maxTokens`, while keeping `messages`, `model`, and other
existing fields intact. Use the existing `buildBody`/`options` conversion logic
as the reference point and ensure user-provided settings end up on the correct
OpenAI-shaped keys.
In `@README.md`:
- Around line 28-47: The README example uses Solid control-flow components that
are missing from the import list, so update the snippet’s imports to include the
required helpers from solid-js. Make sure the example at the top of the App
component clearly imports For and Show alongside the existing
createByokRelayStore and createStreamingChatStore references so the copied code
runs without unresolved symbols.
- Around line 57-77: The Angular example in ChatComponent is missing the setup
needed for *ngFor, so update the snippet to be standalone-ready by adding the
required import to the component’s imports or explicitly stating it belongs to
an NgModule. Make sure the `@Component` definition for ChatComponent includes
standalone usage with NgFor/CommonModule support, or revise the example text so
readers know the directive is provided by an NgModule.
---
Nitpick comments:
In `@packages/angular/src/index.js`:
- Around line 99-222: Dependent services are reaching into ByokRelayService
private fields (_relayUrl, _storage, _storageKey), which tightly couples
ChatService, StreamingChatService, and RelayHealthService to internal
implementation details. Add small public accessors on ByokRelayService for the
values they need, such as a relayUrl getter and token/storage helpers, and
update those dependent classes to use the public API instead of direct
underscore-prefixed field access. Keep the existing behavior unchanged while
removing all direct references to _relay, _storage, and _storageKey outside
ByokRelayService.
In `@packages/angular/test/services.test.js`:
- Around line 1-18: The smoke suite currently only covers the non-Angular
fallback and the openai provider, so the Angular `signal()` branch in
`createAngularSignal` and the non-default provider path are not exercised.
Update the test setup in `services.test.js` to run with `@angular/core`
available during `npm test`, then add coverage for `createAngularSignal`’s
Angular-native path plus an `anthropic` Chat/Streaming case that asserts the
constructed request body shape and URL using `ChatService` and
`StreamingChatService`.
In `@packages/preact/package.json`:
- Around line 30-37: Declare react as an optional peer dependency alongside
preact in the package manifest, since src/index.js can fall back to
require('react') when preact/hooks is unavailable. Update the peerDependencies
and peerDependenciesMeta entries near the existing preact declaration so tooling
and consumers see the React compatibility signal, and keep react marked optional
like preact.
In `@packages/preact/README.md`:
- Around line 7-9: The install command fence in the README is missing a language
identifier, triggering markdownlint MD040. Update the fenced block around the
package install command to include an explicit shell language tag (for example,
in the README install snippet) so the markdown fence is correctly annotated.
In `@packages/preact/test/hooks.test.js`:
- Around line 280-308: The useStreamingChat test suite currently misses coverage
for the real SSE streaming path in useStreamingChat/_streamSSE, so add a test
that mocks fetch with a ReadableStream-like body emitting data: chunks and
verifies streamingContent accumulation, final messages commit, and
abort/rollback behavior. Place the new coverage alongside the existing exports
expected API / clearMessages tests so the core streaming logic in src/index.js
is exercised.
- Around line 17-27: The custom test harness in test() is letting cleanup be
skipped when an assertion throws, so move the shared afterEach() cleanup into
the test() function itself using a finally-style path around await fn(). Update
the hooks tests that currently call afterEach() manually so they rely on the
harness cleanup instead, ensuring _fetchImpl and _store are reset even when a
test fails mid-body.
In `@packages/solid/src/index.js`:
- Around line 264-272: The JSDoc for createChatStore, createStreamingChatStore,
and createRelayHealthStore still documents opts.appId even though those
factories do not read it. Update the docs in the relevant factory comments in
packages/solid/src/index.js to either remove the stale appId parameter or
clearly state that only createByokRelayStore uses it, and keep the parameter
lists aligned with the actual implementation.
- Around line 61-88: The createStore shim is dead code in this module: it is
defined in the index.js file but never referenced or exported, while
createSignal is the only helper used elsewhere. Remove the unused createStore
function and its related proxy/subscriber logic from the module, or
alternatively connect it to an actual export/call site if this was meant to
support a store-based API.
In `@packages/solid/test/stores.test.js`:
- Around line 342-348: The stopStreaming() test only covers the idle no-op path,
so it doesn’t verify aborting an active stream. Update the test in
stores.test.js to start a streaming request through createStreamingChatStore and
sendMessage() so there is an in-flight stream, then call stopStreaming() and
assert the stream is aborted (for example, loading() becomes false and
streamingContent() stops changing). Use the existing createStreamingChatStore
and stopStreaming() symbols to keep the test aligned with the behavior name.
In `@packages/vercel-ai/package.json`:
- Around line 1-44: The package manifest for `@byok-relay/vercel-ai` is missing a
test runner entry even though a smoke test suite exists. Add a scripts.test
command in package.json so the new test/smoke.test.js can be executed
consistently, and make sure it points to the correct test runner used by this
package. Use the package.json scripts section as the place to wire this up.
In `@packages/vercel-ai/src/index.js`:
- Around line 454-456: `chat()` is bound to `this`, so destructuring the method
off the provider breaks when `this` is undefined. Update the `chat`
implementation in both factory paths to call the captured `languageModel`
closure directly instead of `this.languageModel`, keeping the same `modelId` and
`overrides` behavior while making `const { chat } = provider` safe.
- Around line 205-210: The tool choice mapping in the Vercel AI request builder
drops specific tool selections, since the logic in the body.tool_choice
assignment only handles required and none and falls back to auto for
options.toolChoice.type === 'tool'. Update the mapping in the relevant
request-building code in index.js so LanguageModelV1CallOptions with { type:
'tool', toolName } is forwarded to the provider instead of being converted to
auto, while keeping the existing required/none behavior intact.
In `@packages/vercel-ai/test/smoke.test.js`:
- Around line 194-234: Extend the smoke tests around doStream in smoke.test.js
to cover the missing edge cases: add a streaming fixture that ends without a
trailing newline so the parser’s final buffer flush is exercised, and add
tool-related streaming cases that verify multiple tool-result messages are
preserved instead of truncated. Also add a case for toolChoice selection using
the doStream path to confirm toolChoice: { type: 'tool' } is handled correctly.
Use the existing model.doStream, ndjsonStream, and finish/text-delta assertions
as the entry points for locating and validating the behavior.
🪄 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: 1f9c87bd-05d3-49ec-ae87-93d1ac3b7dbf
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (20)
README.mdllms.txtmetrics/daily.jsonlpackage.jsonpackages/angular/README.mdpackages/angular/package.jsonpackages/angular/src/index.jspackages/angular/test/services.test.jspackages/preact/README.mdpackages/preact/package.jsonpackages/preact/src/index.jspackages/preact/test/hooks.test.jspackages/solid/README.mdpackages/solid/package.jsonpackages/solid/src/index.jspackages/solid/test/stores.test.jspackages/vercel-ai/README.mdpackages/vercel-ai/package.jsonpackages/vercel-ai/src/index.jspackages/vercel-ai/test/smoke.test.js
| ```typescript | ||
| import { Component, inject, OnInit } from '@angular/core'; | ||
| import { ByokRelayService, ChatService } from '@byok-relay/angular'; | ||
|
|
||
| @Component({ | ||
| selector: 'app-chat', | ||
| standalone: true, | ||
| template: ` | ||
| <input [(ngModel)]="apiKey" placeholder="Your OpenAI API key" type="password" /> | ||
| <button (click)="saveKey()">Save key</button> | ||
|
|
||
| <input [(ngModel)]="message" placeholder="Ask anything…" /> | ||
| <button (click)="send()" [disabled]="chat.loading()">Send</button> | ||
|
|
||
| <div *ngFor="let m of chat.messages()"> | ||
| <strong>{{ m.role }}</strong>: {{ m.content }} | ||
| </div> | ||
| `, | ||
| }) | ||
| export class ChatComponent implements OnInit { |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Standalone component example is missing required imports.
*ngFor and [(ngModel)] are used in the template but CommonModule/FormsModule aren't imported anywhere in this standalone component snippet — copy-pasting this example as-is will fail to compile.
📝 Proposed fix
import { Component, inject, OnInit } from '`@angular/core`';
+import { CommonModule } from '`@angular/common`';
+import { FormsModule } from '`@angular/forms`';
import { ByokRelayService, ChatService } from '`@byok-relay/angular`';
`@Component`({
selector: 'app-chat',
standalone: true,
+ imports: [CommonModule, FormsModule],
template: `📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ```typescript | |
| import { Component, inject, OnInit } from '@angular/core'; | |
| import { ByokRelayService, ChatService } from '@byok-relay/angular'; | |
| @Component({ | |
| selector: 'app-chat', | |
| standalone: true, | |
| template: ` | |
| <input [(ngModel)]="apiKey" placeholder="Your OpenAI API key" type="password" /> | |
| <button (click)="saveKey()">Save key</button> | |
| <input [(ngModel)]="message" placeholder="Ask anything…" /> | |
| <button (click)="send()" [disabled]="chat.loading()">Send</button> | |
| <div *ngFor="let m of chat.messages()"> | |
| <strong>{{ m.role }}</strong>: {{ m.content }} | |
| </div> | |
| `, | |
| }) | |
| export class ChatComponent implements OnInit { |
🤖 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/angular/README.md` around lines 32 - 51, The standalone
ChatComponent example uses `*ngFor` and `[(ngModel)]` but does not import the
modules that provide them. Update the component’s `imports` metadata in the
`ChatComponent` example to include `CommonModule` and `FormsModule` alongside
the existing Angular/ByokRelay imports so the snippet compiles when copied
as-is.
| async sendMessage(content, options = {}) { | ||
| const { | ||
| provider = 'openai', | ||
| model = 'gpt-4o-mini', | ||
| systemPrompt, | ||
| ...extra | ||
| } = options; | ||
|
|
||
| const tok = this._relay._storage.getItem(this._relay._storageKey); | ||
| if (!tok) throw new Error('Not registered. Call ByokRelayService.register() first.'); | ||
|
|
||
| const newUserMsg = { role: 'user', content }; | ||
| this._messages.update((prev) => [...prev, newUserMsg]); | ||
| this._loading.set(true); | ||
| this._error.set(null); | ||
|
|
||
| const history = this._messages.value(); | ||
| const body = { | ||
| model, | ||
| messages: systemPrompt | ||
| ? [{ role: 'system', content: systemPrompt }, ...history] | ||
| : [...history], | ||
| ...extra, | ||
| }; | ||
|
|
||
| try { | ||
| const path = PROVIDER_PATHS[provider] || 'chat/completions'; | ||
| const resp = await fetch( | ||
| `${this._relay._relayUrl}/relay/${provider}/${path}`, | ||
| { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json', 'x-relay-token': tok }, | ||
| body: JSON.stringify(body), | ||
| }, | ||
| ); | ||
| if (!resp.ok) { | ||
| const msg = await resp.text().catch(() => String(resp.status)); | ||
| throw new Error(`Chat request failed (${resp.status}): ${msg}`); | ||
| } | ||
| const data = await resp.json(); | ||
| // OpenAI-compatible + Anthropic response shapes | ||
| const reply = | ||
| data.choices?.[0]?.message?.content ?? | ||
| data.content?.[0]?.text ?? | ||
| ''; | ||
| this._messages.update((prev) => [...prev, { role: 'assistant', content: reply }]); | ||
| return reply; | ||
| } catch (err) { | ||
| // Roll back user message on failure | ||
| this._messages.update((prev) => prev.slice(0, -1)); | ||
| this._error.set(err.message); | ||
| throw err; | ||
| } finally { | ||
| this._loading.set(false); | ||
| } | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map the Angular and Solid package entry files first.
ast-grep outline packages/angular/src/index.js --view expanded
ast-grep outline packages/solid/src/index.js --view expanded
# Read the relevant sections with line numbers.
sed -n '1,120p' packages/angular/src/index.js | cat -n
sed -n '240,340p' packages/angular/src/index.js | cat -n
sed -n '340,520p' packages/angular/src/index.js | cat -n
sed -n '260,380p' packages/solid/src/index.js | cat -n
# Inspect any relay protocol docs in the repo.
git ls-files | rg 'README|readme|docs|protocol|relay' -nRepository: avikalpg/byok-relay
Length of output: 23425
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Read the package docs and the main repo protocol docs.
sed -n '1,220p' README.md | cat -n
sed -n '1,260p' packages/angular/README.md | cat -n
sed -n '1,260p' packages/solid/README.md | cat -n
sed -n '1,260p' skills/byok-relay/SKILL.md | cat -n
# Look for tests and mocks that exercise relay URLs or provider-specific bodies.
rg -n "relay/|anthropic|max_tokens|systemPrompt|PROVIDER_PATHS|messages:\s*\[" packages/angular packages/solid examples test tests __tests__ -g '!**/dist/**' -g '!**/build/**'
# Find any references to the byok-relay wire protocol in docs/examples.
rg -n "POST /relay|x-relay-token|relay/<provider>|/relay/" . -g '!**/dist/**' -g '!**/build/**'Repository: avikalpg/byok-relay
Length of output: 46515
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the Angular tests around Anthropic requests and body expectations.
sed -n '1,260p' packages/angular/test/services.test.js | cat -n
sed -n '260,420p' packages/angular/test/services.test.js | cat -n
# Inspect the example client that talks to the relay directly.
sed -n '1,220p' examples/react-vite/src/relay.js | cat -n
# Read the exact Angular sendMessage / streamMessage implementations again.
sed -n '262,430p' packages/angular/src/index.js | cat -nRepository: avikalpg/byok-relay
Length of output: 31684
Handle Anthropic payloads separately in sendMessage() and streamMessage()
provider: 'anthropic' still sends an OpenAI-style body here. Move systemPrompt to a top-level system field and include max_tokens for Anthropic requests; otherwise those calls will fail against the relay’s Anthropic endpoint.
🤖 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/angular/src/index.js` around lines 262 - 317, sendMessage()
currently builds an OpenAI-style request body even when provider is anthropic,
which breaks Anthropic relay calls; update sendMessage() and streamMessage() to
branch on the provider, sending Anthropic requests with a top-level system field
instead of systemPrompt in messages and including max_tokens, while keeping the
current OpenAI-compatible payload for other providers. Use the existing
sendMessage() / streamMessage() flow and provider selection logic to locate the
fix.
| async streamMessage(content, options = {}) { | ||
| const { | ||
| provider = 'openai', | ||
| model = 'gpt-4o-mini', | ||
| systemPrompt, | ||
| onChunk, | ||
| ...extra | ||
| } = options; | ||
|
|
||
| const tok = this._relay._storage.getItem(this._relay._storageKey); | ||
| if (!tok) throw new Error('Not registered. Call ByokRelayService.register() first.'); | ||
|
|
||
| this._messages.update((prev) => [...prev, { role: 'user', content }]); | ||
| this._streaming.set(true); | ||
| this._streamingContent.set(''); | ||
| this._error.set(null); | ||
| this._abortController = new AbortController(); | ||
|
|
||
| const history = this._messages.value(); | ||
| const body = { | ||
| model, | ||
| messages: systemPrompt | ||
| ? [{ role: 'system', content: systemPrompt }, ...history] | ||
| : [...history], | ||
| stream: true, | ||
| ...extra, | ||
| }; | ||
|
|
||
| let accumulated = ''; | ||
|
|
||
| try { | ||
| const path = PROVIDER_PATHS[provider] || 'chat/completions'; | ||
| const resp = await fetch( | ||
| `${this._relay._relayUrl}/relay/${provider}/${path}`, | ||
| { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json', 'x-relay-token': tok }, | ||
| body: JSON.stringify(body), | ||
| signal: this._abortController.signal, | ||
| }, | ||
| ); | ||
| if (!resp.ok) { | ||
| const msg = await resp.text().catch(() => String(resp.status)); | ||
| throw new Error(`Stream request failed (${resp.status}): ${msg}`); | ||
| } | ||
|
|
||
| const reader = resp.body.getReader(); | ||
| const decoder = new TextDecoder(); | ||
| let leftover = ''; | ||
|
|
||
| while (true) { | ||
| const { done, value } = await reader.read(); | ||
| if (done) break; | ||
| const text = leftover + decoder.decode(value, { stream: true }); | ||
| const lines = text.split('\n'); | ||
| leftover = lines.pop() ?? ''; | ||
|
|
||
| for (const line of lines) { | ||
| if (!line.startsWith('data: ')) continue; | ||
| const payload = line.slice(6).trim(); | ||
| if (payload === '[DONE]') break; | ||
| try { | ||
| const parsed = JSON.parse(payload); | ||
| const delta = parsed.choices?.[0]?.delta?.content ?? ''; | ||
| if (delta) { | ||
| accumulated += delta; | ||
| this._streamingContent.set(accumulated); | ||
| if (onChunk) onChunk(delta, accumulated); | ||
| } | ||
| } catch { /* skip malformed SSE lines */ } | ||
| } | ||
| } | ||
|
|
||
| this._messages.update((prev) => [ | ||
| ...prev, | ||
| { role: 'assistant', content: accumulated }, | ||
| ]); | ||
| this._streamingContent.set(''); | ||
| return accumulated; | ||
| } catch (err) { | ||
| if (err.name === 'AbortError') { | ||
| // Partial commit: save whatever was streamed | ||
| if (accumulated) { | ||
| this._messages.update((prev) => [ | ||
| ...prev, | ||
| { role: 'assistant', content: accumulated + ' [stopped]' }, | ||
| ]); | ||
| } else { | ||
| // Nothing streamed — roll back user message | ||
| this._messages.update((prev) => prev.slice(0, -1)); | ||
| } | ||
| this._streamingContent.set(''); | ||
| } else { | ||
| this._messages.update((prev) => prev.slice(0, -1)); | ||
| this._error.set(err.message); | ||
| throw err; | ||
| } | ||
| } finally { | ||
| this._streaming.set(false); | ||
| this._abortController = null; | ||
| } | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the relevant service implementations and request construction.
rg -n "sendMessage\\(|streamMessage\\(|PROVIDER_PATHS|relayUrl|anthropic|chat/completions|max_tokens|messages:" packages/angular/src/index.js
echo
echo "---- context around sendMessage ----"
sed -n '240,350p' packages/angular/src/index.js
echo
echo "---- context around streamMessage ----"
sed -n '350,490p' packages/angular/src/index.jsRepository: avikalpg/byok-relay
Length of output: 10017
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect any helper or shared builder that the comment suggests might already exist.
rg -n "buildRelayRequest|build.*Request|request body|provider === 'anthropic'|relay/${provider}" packages/angular/src packages/angularRepository: avikalpg/byok-relay
Length of output: 198
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for provider-specific request normalization, Anthropic body examples, and any shared helper.
rg -n "max_tokens|system:|provider === 'anthropic'|anthropic.*messages|messages.*system|buildRelayRequest|relay/anthropic|PROVIDER_PATHS" .
echo
echo "---- files mentioning anthropic ----"
fd -a -e js -e ts -e md -e json -e yml -e yaml . . | rg 'anthropic|relay'Repository: avikalpg/byok-relay
Length of output: 3472
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for provider-specific request normalization and Anthropic body examples.
rg -n "max_tokens|provider === 'anthropic'|relay/anthropic|buildRelayRequest|system:\"|system:|messages:.*system|anthropic" packages .github README.md docs . 2>/dev/null || true
echo
echo "---- tracked files with anthropic/relay in the name ----"
git ls-files | rg 'anthropic|relay'Repository: avikalpg/byok-relay
Length of output: 16929
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for provider-specific request normalization and Anthropic body examples.
rg -n "max_tokens|provider === 'anthropic'|relay/anthropic|buildRelayRequest|system:|messages:.*system|anthropic" packages README.md docs .github 2>/dev/null || true
echo
echo "---- tracked files with anthropic/relay in the name ----"
git ls-files | rg 'anthropic|relay'Repository: avikalpg/byok-relay
Length of output: 7297
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Look for server-side handling of incoming relay requests and Anthropic translation.
rg -n "anthropic|max_tokens|system_prompt|messages\\.filter|chat/completions|relay/" packages server src . 2>/dev/null || true
echo
echo "---- package and source files ----"
git ls-files packages server src | sed -n '1,250p'Repository: avikalpg/byok-relay
Length of output: 37297
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect documentation/examples for Anthropic request shape and relay endpoints.
rg -n "anthropic|max_tokens|systemPrompt|relay.*anthropic|chat/completions|messages" README.md packages/angular/src/index.js packages README.md docs .github 2>/dev/null || trueRepository: avikalpg/byok-relay
Length of output: 23807
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "---- relay server route ----"
sed -n '185,240p' src/index.js
echo
echo "---- provider base URL handling ----"
sed -n '190,240p' src/providers.js
echo
echo "---- angular service tests around fetch URLs ----"
sed -n '220,380p' packages/angular/test/services.test.jsRepository: avikalpg/byok-relay
Length of output: 10411
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "---- angular service tests around fetch URLs ----"
rg -n "/relay/|anthropic|streamMessage|sendMessage" packages/angular/test/services.test.js
sed -n '220,380p' packages/angular/test/services.test.jsRepository: avikalpg/byok-relay
Length of output: 7387
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "---- solid comparison for anthropic body + URL ----"
sed -n '300,455p' packages/solid/src/index.jsRepository: avikalpg/byok-relay
Length of output: 5215
Normalize Anthropic request bodies in both chat paths.
streamMessage() still builds the OpenAI-style payload for Anthropic, so Claude requests miss the provider-specific system/max_tokens shape. Share the request builder with sendMessage() to keep both paths aligned.
🤖 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/angular/src/index.js` around lines 366 - 467, streamMessage() is
still constructing an OpenAI-shaped payload, so Anthropic/Claude requests won’t
get the provider-specific request body. Update the request assembly in
streamMessage() to use the same shared builder as sendMessage(), ensuring
Anthropic gets the correct system and max_tokens fields while keeping both chat
paths aligned. Use the existing provider handling in streamMessage() and the
sendMessage() request builder as the reference points.
| ## Related packages | ||
|
|
||
| | Package | Description | | ||
| |---------|-------------| | ||
| | [`@byok-relay/react`](../react) | React hooks | | ||
| | [`@byok-relay/vue`](../vue) | Vue 3 composables | | ||
| | [`@byok-relay/svelte`](../svelte) | Svelte stores | | ||
| | [`@byok-relay/solid`](../solid) | SolidJS stores | | ||
| | [`@byok-relay/angular`](../angular) | Angular injectable services | | ||
| | [`@byok-relay/vercel-ai`](../vercel-ai) | Vercel AI SDK custom provider | | ||
| | [`@byok-relay/mcp`](../mcp) | MCP server for Claude Desktop | | ||
| | [`@byok-relay/client`](../client) | Vanilla JS client | |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
for pkg in react vue svelte mcp client; do
echo "== packages/$pkg =="
fd -t d "^$pkg$" packages
doneRepository: avikalpg/byok-relay
Length of output: 260
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Top-level package directories:"
fd -t d '^packages$|^.*$' . | sed 's#^\./##' | sort | head -200
echo
echo "Package directories under packages/:"
fd -t d . packages | sed 's#^\./##' | sort
echo
echo "README references to `@byok-relay/react`|vue|svelte|mcp|client:"
rg -n '`@byok-relay/`(react|vue|svelte|mcp|client)' packages -g 'README.md'
echo
echo "Package manifests:"
fd -H -t f 'package.json$' packages | sed 's#^\./##' | sortRepository: avikalpg/byok-relay
Length of output: 3359
🏁 Script executed:
#!/bin/bash
set -euo pipefail
for p in packages/react packages/vue packages/svelte packages/mcp packages/client; do
echo "== $p =="
if [ -e "$p" ]; then
find "$p" -maxdepth 2 -mindepth 1 -print | sed 's#^\./##' | sort
else
echo "<missing>"
fi
done
echo
echo "Relevant README snippet with line numbers:"
cat -n packages/preact/README.md | sed -n '206,224p'Repository: avikalpg/byok-relay
Length of output: 1114
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
for name in ["react","vue","svelte","mcp","client"]:
p = Path("packages")/name
print(name, "exists" if p.exists() else "missing", "dir" if p.is_dir() else "file/non-dir" if p.exists() else "")
PYRepository: avikalpg/byok-relay
Length of output: 230
Remove the broken sibling package links
packages/preact/README.md links to ../react, ../vue, ../svelte, ../mcp, and ../client, but those package directories don’t exist here. Trim the table to the packages that actually ship in this repo, or add the missing packages first.
🤖 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/preact/README.md` around lines 210 - 221, The Related packages table
in the Preact README contains broken sibling links to packages that are not
present in this repo. Update the table in the README content to remove the
non-existent package references, keeping only links that point to actual package
directories, or add the missing packages before restoring those entries.
| function _resolveHooks () { | ||
| if (_useState) return; | ||
| // Try preact/hooks first | ||
| try { | ||
| const ph = require('preact/hooks'); | ||
| _useState = ph.useState; | ||
| _useEffect = ph.useEffect; | ||
| _useCallback = ph.useCallback; | ||
| _useRef = ph.useRef; | ||
| return; | ||
| } catch (_) { /* not installed */ } | ||
| // Fall back to react | ||
| try { | ||
| const r = require('react'); | ||
| _useState = r.useState; | ||
| _useEffect = r.useEffect; | ||
| _useCallback = r.useCallback; | ||
| _useRef = r.useRef; | ||
| return; | ||
| } catch (_) { /* not installed */ } | ||
| // Inline shim for test / non-DOM environments | ||
| _useState = (init) => { | ||
| const val = typeof init === 'function' ? init() : init; | ||
| const setter = (next) => { | ||
| // no-op shim; state management happens externally in tests | ||
| setter._current = typeof next === 'function' ? next(val) : next; | ||
| }; | ||
| setter._current = val; | ||
| return [val, setter]; | ||
| }; | ||
| _useEffect = (fn) => { fn(); }; | ||
| _useCallback = (fn) => fn; | ||
| _useRef = (init) => ({ current: init }); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Silent, non-reactive fallback shim with no diagnostic signal.
If neither preact/hooks nor react resolve, _resolveHooks silently swaps in an inline shim (lines 40-51) that is explicitly non-reactive ("state management happens externally in tests"). In a real component this shim never triggers re-renders and state setters are effectively no-ops from the caller's perspective — the whole package would appear to "work" (no thrown errors) while being completely non-functional (token/messages/streamingContent never update in the UI). There's no console.warn/error to alert a developer who forgot to install the peer dependency.
🔧 Suggested fix
// Inline shim for test / non-DOM environments
+ if (typeof window !== 'undefined') {
+ // eslint-disable-next-line no-console
+ console.warn('[`@byok-relay/preact`] Neither `preact` nor `react` could be resolved; falling back to a non-reactive stub. State updates will not trigger re-renders.');
+ }
_useState = (init) => {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function _resolveHooks () { | |
| if (_useState) return; | |
| // Try preact/hooks first | |
| try { | |
| const ph = require('preact/hooks'); | |
| _useState = ph.useState; | |
| _useEffect = ph.useEffect; | |
| _useCallback = ph.useCallback; | |
| _useRef = ph.useRef; | |
| return; | |
| } catch (_) { /* not installed */ } | |
| // Fall back to react | |
| try { | |
| const r = require('react'); | |
| _useState = r.useState; | |
| _useEffect = r.useEffect; | |
| _useCallback = r.useCallback; | |
| _useRef = r.useRef; | |
| return; | |
| } catch (_) { /* not installed */ } | |
| // Inline shim for test / non-DOM environments | |
| _useState = (init) => { | |
| const val = typeof init === 'function' ? init() : init; | |
| const setter = (next) => { | |
| // no-op shim; state management happens externally in tests | |
| setter._current = typeof next === 'function' ? next(val) : next; | |
| }; | |
| setter._current = val; | |
| return [val, setter]; | |
| }; | |
| _useEffect = (fn) => { fn(); }; | |
| _useCallback = (fn) => fn; | |
| _useRef = (init) => ({ current: init }); | |
| } | |
| function _resolveHooks () { | |
| if (_useState) return; | |
| // Try preact/hooks first | |
| try { | |
| const ph = require('preact/hooks'); | |
| _useState = ph.useState; | |
| _useEffect = ph.useEffect; | |
| _useCallback = ph.useCallback; | |
| _useRef = ph.useRef; | |
| return; | |
| } catch (_) { /* not installed */ } | |
| // Fall back to react | |
| try { | |
| const r = require('react'); | |
| _useState = r.useState; | |
| _useEffect = r.useEffect; | |
| _useCallback = r.useCallback; | |
| _useRef = r.useRef; | |
| return; | |
| } catch (_) { /* not installed */ } | |
| // Inline shim for test / non-DOM environments | |
| if (typeof window !== 'undefined') { | |
| // eslint-disable-next-line no-console | |
| console.warn('[`@byok-relay/preact`] Neither `preact` nor `react` could be resolved; falling back to a non-reactive stub. State updates will not trigger re-renders.'); | |
| } | |
| _useState = (init) => { | |
| const val = typeof init === 'function' ? init() : init; | |
| const setter = (next) => { | |
| // no-op shim; state management happens externally in tests | |
| setter._current = typeof next === 'function' ? next(val) : next; | |
| }; | |
| setter._current = val; | |
| return [val, setter]; | |
| }; | |
| _useEffect = (fn) => { fn(); }; | |
| _useCallback = (fn) => fn; | |
| _useRef = (init) => ({ current: init }); | |
| } |
🤖 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/preact/src/index.js` around lines 19 - 52, The fallback in
_resolveHooks is silently masking a missing hooks dependency by installing a
non-reactive inline shim, which can make components appear functional while
never updating. Update the _resolveHooks logic in packages/preact/src/index.js
so that when both preact/hooks and react fail to resolve, it emits a clear
warning or error before falling back, using the _resolveHooks function and the
inline shim branch as the key location. Keep the shim only for test/non-DOM
support, but make the missing peer dependency visible to developers so they can
install the proper hooks implementation.
| async function buildBody(options) { | ||
| const messages = promptToMessages(options.prompt); | ||
| const body = { | ||
| model, | ||
| messages, | ||
| ...settings, | ||
| }; | ||
| if (options.maxTokens !== undefined) body.max_tokens = options.maxTokens; | ||
| if (options.temperature !== undefined) body.temperature = options.temperature; | ||
| if (options.topP !== undefined) body.top_p = options.topP; | ||
| if (options.frequencyPenalty !== undefined) body.frequency_penalty = options.frequencyPenalty; | ||
| if (options.presencePenalty !== undefined) body.presence_penalty = options.presencePenalty; | ||
| if (options.stopSequences?.length) body.stop = options.stopSequences; | ||
| if (options.seed !== undefined) body.seed = options.seed; | ||
| if (options.tools?.length) { | ||
| body.tools = options.tools.map((t) => ({ | ||
| type: 'function', | ||
| function: { name: t.name, description: t.description, parameters: t.parameters }, | ||
| })); | ||
| body.tool_choice = options.toolChoice?.type === 'required' | ||
| ? 'required' | ||
| : options.toolChoice?.type === 'none' | ||
| ? 'none' | ||
| : 'auto'; | ||
| } | ||
| if (options.responseFormat?.type === 'json') { | ||
| body.response_format = { type: 'json_object' }; | ||
| } | ||
| return body; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Camel-case settings overrides are spread raw into the OpenAI-shaped body, so documented overrides like maxTokens never take effect.
The README (lines 131, 103) documents languageModel(modelId, { maxTokens: 500 }) / settings: { temperature: 0.7 } as the override mechanism, and ...settings is spread directly into body at line 191. Only options.* (the AI SDK call-time options) get the camelCase→snake_case conversion below. A user-supplied maxTokens setting ends up as body.maxTokens (unrecognized by OpenAI-compatible APIs) instead of body.max_tokens, so the override is silently ignored.
🛠️ Suggested fix — normalize settings through the same option pipeline
async function buildBody(options) {
- const messages = promptToMessages(options.prompt);
- const body = {
- model,
- messages,
- ...settings,
- };
- if (options.maxTokens !== undefined) body.max_tokens = options.maxTokens;
- if (options.temperature !== undefined) body.temperature = options.temperature;
- if (options.topP !== undefined) body.top_p = options.topP;
- if (options.frequencyPenalty !== undefined) body.frequency_penalty = options.frequencyPenalty;
- if (options.presencePenalty !== undefined) body.presence_penalty = options.presencePenalty;
- if (options.stopSequences?.length) body.stop = options.stopSequences;
- if (options.seed !== undefined) body.seed = options.seed;
+ const merged = { ...settings, ...options };
+ const messages = promptToMessages(merged.prompt);
+ const body = { model, messages };
+ if (merged.maxTokens !== undefined) body.max_tokens = merged.maxTokens;
+ if (merged.temperature !== undefined) body.temperature = merged.temperature;
+ if (merged.topP !== undefined) body.top_p = merged.topP;
+ if (merged.frequencyPenalty !== undefined) body.frequency_penalty = merged.frequencyPenalty;
+ if (merged.presencePenalty !== undefined) body.presence_penalty = merged.presencePenalty;
+ if (merged.stopSequences?.length) body.stop = merged.stopSequences;
+ if (merged.seed !== undefined) body.seed = merged.seed;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async function buildBody(options) { | |
| const messages = promptToMessages(options.prompt); | |
| const body = { | |
| model, | |
| messages, | |
| ...settings, | |
| }; | |
| if (options.maxTokens !== undefined) body.max_tokens = options.maxTokens; | |
| if (options.temperature !== undefined) body.temperature = options.temperature; | |
| if (options.topP !== undefined) body.top_p = options.topP; | |
| if (options.frequencyPenalty !== undefined) body.frequency_penalty = options.frequencyPenalty; | |
| if (options.presencePenalty !== undefined) body.presence_penalty = options.presencePenalty; | |
| if (options.stopSequences?.length) body.stop = options.stopSequences; | |
| if (options.seed !== undefined) body.seed = options.seed; | |
| if (options.tools?.length) { | |
| body.tools = options.tools.map((t) => ({ | |
| type: 'function', | |
| function: { name: t.name, description: t.description, parameters: t.parameters }, | |
| })); | |
| body.tool_choice = options.toolChoice?.type === 'required' | |
| ? 'required' | |
| : options.toolChoice?.type === 'none' | |
| ? 'none' | |
| : 'auto'; | |
| } | |
| if (options.responseFormat?.type === 'json') { | |
| body.response_format = { type: 'json_object' }; | |
| } | |
| return body; | |
| } | |
| async function buildBody(options) { | |
| const merged = { ...settings, ...options }; | |
| const messages = promptToMessages(merged.prompt); | |
| const body = { | |
| model, | |
| messages, | |
| }; | |
| if (merged.maxTokens !== undefined) body.max_tokens = merged.maxTokens; | |
| if (merged.temperature !== undefined) body.temperature = merged.temperature; | |
| if (merged.topP !== undefined) body.top_p = merged.topP; | |
| if (merged.frequencyPenalty !== undefined) body.frequency_penalty = merged.frequencyPenalty; | |
| if (merged.presencePenalty !== undefined) body.presence_penalty = merged.presencePenalty; | |
| if (merged.stopSequences?.length) body.stop = merged.stopSequences; | |
| if (merged.seed !== undefined) body.seed = merged.seed; | |
| if (options.tools?.length) { | |
| body.tools = options.tools.map((t) => ({ | |
| type: 'function', | |
| function: { name: t.name, description: t.description, parameters: t.parameters }, | |
| })); | |
| body.tool_choice = options.toolChoice?.type === 'required' | |
| ? 'required' | |
| : options.toolChoice?.type === 'none' | |
| ? 'none' | |
| : 'auto'; | |
| } | |
| if (options.responseFormat?.type === 'json') { | |
| body.response_format = { type: 'json_object' }; | |
| } | |
| return body; | |
| } |
🤖 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/vercel-ai/src/index.js` around lines 186 - 215, The `buildBody`
helper is spreading `settings` directly into the OpenAI request body, so
camelCase overrides like `maxTokens` and other documented settings are not being
normalized and may be ignored by the API. Update `buildBody` in
`packages/vercel-ai/src/index.js` so `settings` is passed through the same
camelCase-to-snake_case mapping used for `options.*`, especially for
`maxTokens`, while keeping `messages`, `model`, and other existing fields
intact. Use the existing `buildBody`/`options` conversion logic as the reference
point and ensure user-provided settings end up on the correct OpenAI-shaped
keys.
| while (true) { | ||
| const { done, value } = await reader.read(); | ||
| if (done) break; | ||
| buf += decoder.decode(value, { stream: true }); | ||
|
|
||
| const lines = buf.split('\n'); | ||
| buf = lines.pop() ?? ''; | ||
|
|
||
| for (const line of lines) { | ||
| const trimmed = line.trim(); | ||
| if (!trimmed || trimmed === 'data: [DONE]') continue; | ||
| if (!trimmed.startsWith('data: ')) continue; | ||
|
|
||
| let chunk; | ||
| try { | ||
| chunk = JSON.parse(trimmed.slice(6)); | ||
| } catch { | ||
| continue; | ||
| } | ||
|
|
||
| const choice = chunk.choices?.[0]; | ||
| if (!choice) continue; | ||
|
|
||
| const delta = choice.delta ?? {}; | ||
|
|
||
| if (delta.content) { | ||
| controller.enqueue({ type: 'text-delta', textDelta: delta.content }); | ||
| } | ||
|
|
||
| if (delta.tool_calls) { | ||
| for (const tc of delta.tool_calls) { | ||
| if (tc.function?.name) { | ||
| controller.enqueue({ | ||
| type: 'tool-call-delta', | ||
| toolCallType: 'function', | ||
| toolCallId: tc.id ?? `tool_${tc.index ?? 0}`, | ||
| toolName: tc.function.name, | ||
| argsTextDelta: tc.function.arguments ?? '', | ||
| }); | ||
| } else if (tc.function?.arguments) { | ||
| controller.enqueue({ | ||
| type: 'tool-call-delta', | ||
| toolCallType: 'function', | ||
| toolCallId: tc.id ?? `tool_${tc.index ?? 0}`, | ||
| toolName: '', | ||
| argsTextDelta: tc.function.arguments, | ||
| }); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (chunk.usage) { | ||
| usage = { | ||
| promptTokens: chunk.usage.prompt_tokens ?? 0, | ||
| completionTokens: chunk.usage.completion_tokens ?? 0, | ||
| }; | ||
| } | ||
|
|
||
| if (choice.finish_reason) { | ||
| controller.enqueue({ | ||
| type: 'finish', | ||
| finishReason: mapFinishReason(choice.finish_reason), | ||
| usage, | ||
| }); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the file and inspect the relevant section with line numbers.
wc -l packages/vercel-ai/src/index.js
sed -n '250,390p' packages/vercel-ai/src/index.js
# Search for related stream parsing helpers or duplicated logic.
rg -n "reader\.read\(\)|decoder\.decode|data: \[DONE\]|tool-call-delta|finishReason|usage" packages/vercel-ai/src/index.jsRepository: avikalpg/byok-relay
Length of output: 6214
Flush the remaining SSE buffer when the stream ends. The loop exits on done before draining buf, so a final chunk without a trailing newline can drop the last text-delta or finish/usage event.
🤖 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/vercel-ai/src/index.js` around lines 294 - 360, The SSE parsing loop
in the stream reader drops any trailing buffered data when `reader.read()`
returns `done`, so the last `text-delta`, `tool-call-delta`, or `finish`/usage
event can be lost if the final chunk has no newline. Update the streaming logic
around the `while (true)` loop to drain and process the remaining `buf` after
the loop ends, using the same line parsing/JSON handling as the in-loop path so
the final SSE event is emitted correctly.
| async function createByokRelayProvider({ relayUrl, appId = 'vercel-ai', storage, settings = {} } = {}) { | ||
| if (!relayUrl) throw new Error('byok-relay: relayUrl is required'); | ||
| const store = storage ?? defaultStorage(); | ||
| const storageKey = `byok_relay_token_${appId}`; | ||
|
|
||
| let _token = store.getItem(storageKey) ?? null; | ||
|
|
||
| async function getToken() { | ||
| if (!_token) { | ||
| _token = await registerToken({ relayUrl, appId, storage: store }); | ||
| } | ||
| return _token; | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Async provider's getToken() lacks the in-flight de-dup guard the sync factory has, allowing duplicate concurrent registrations.
In createByokRelayProvider, _token = await registerToken(...) only assigns after the await resolves, so two concurrent calls to getToken() before the first completes both invoke registerToken, potentially registering two tokens and racing on which one "wins." createByokRelayProviderSync already guards against this with a _pending promise cache — the async factory should do the same for consistency and correctness.
🛠️ Suggested fix
let _token = store.getItem(storageKey) ?? null;
+ let _pending = null;
async function getToken() {
- if (!_token) {
- _token = await registerToken({ relayUrl, appId, storage: store });
- }
- return _token;
+ if (_token) return _token;
+ if (!_pending) {
+ _pending = registerToken({ relayUrl, appId, storage: store }).then((t) => {
+ _token = t;
+ _pending = null;
+ return t;
+ });
+ }
+ return _pending;
}Also applies to: 488-504
🤖 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/vercel-ai/src/index.js` around lines 393 - 405, The async BYOK relay
provider’s getToken path in createByokRelayProvider can register the token twice
when called concurrently because it only stores _token after await registerToken
resolves. Add an in-flight promise guard like the one used in
createByokRelayProviderSync (for example, a _pending cache) so concurrent calls
share the same registration request. Update getToken and the related token
initialization flow to return the same pending promise until it resolves, then
cache the resulting token and clear the pending state.
| ```jsx | ||
| import { createByokRelayStore, createStreamingChatStore } from '@byok-relay/solid'; | ||
|
|
||
| function App() { | ||
| const relay = createByokRelayStore({ appId: 'my-app' }); | ||
| const chat = createStreamingChatStore({ provider: 'openai', model: 'gpt-4o-mini' }); | ||
|
|
||
| async function send(text) { | ||
| if (!relay.token()) await relay.register(); | ||
| await chat.sendMessage(text, relay.token()); | ||
| } | ||
|
|
||
| return ( | ||
| <> | ||
| <For each={chat.messages()}>{msg => <p>{msg.role}: {msg.content}</p>}</For> | ||
| <Show when={chat.streamingContent()}><p>assistant: {chat.streamingContent()}▋</p></Show> | ||
| </> | ||
| ); | ||
| } | ||
| ``` |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Import the Solid control-flow helpers.
For and Show are referenced in the example but never imported from solid-js, so the snippet will fail as copy/pasted.
Fix
+import { For, Show } from 'solid-js';
import { createByokRelayStore, createStreamingChatStore } from '`@byok-relay/solid`';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ```jsx | |
| import { createByokRelayStore, createStreamingChatStore } from '@byok-relay/solid'; | |
| function App() { | |
| const relay = createByokRelayStore({ appId: 'my-app' }); | |
| const chat = createStreamingChatStore({ provider: 'openai', model: 'gpt-4o-mini' }); | |
| async function send(text) { | |
| if (!relay.token()) await relay.register(); | |
| await chat.sendMessage(text, relay.token()); | |
| } | |
| return ( | |
| <> | |
| <For each={chat.messages()}>{msg => <p>{msg.role}: {msg.content}</p>}</For> | |
| <Show when={chat.streamingContent()}><p>assistant: {chat.streamingContent()}▋</p></Show> | |
| </> | |
| ); | |
| } | |
| ``` | |
| import { For, Show } from 'solid-js'; | |
| import { createByokRelayStore, createStreamingChatStore } from '`@byok-relay/solid`'; | |
| function App() { | |
| const relay = createByokRelayStore({ appId: 'my-app' }); | |
| const chat = createStreamingChatStore({ provider: 'openai', model: 'gpt-4o-mini' }); | |
| async function send(text) { | |
| if (!relay.token()) await relay.register(); | |
| await chat.sendMessage(text, relay.token()); | |
| } | |
| return ( | |
| <> | |
| <For each={chat.messages()}>{msg => <p>{msg.role}: {msg.content}</p>}</For> | |
| <Show when={chat.streamingContent()}><p>assistant: {chat.streamingContent()}▋</p></Show> | |
| </> | |
| ); | |
| } |
🤖 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 `@README.md` around lines 28 - 47, The README example uses Solid control-flow
components that are missing from the import list, so update the snippet’s
imports to include the required helpers from solid-js. Make sure the example at
the top of the App component clearly imports For and Show alongside the existing
createByokRelayStore and createStreamingChatStore references so the copied code
runs without unresolved symbols.
| ```typescript | ||
| import { Component, inject } from '@angular/core'; | ||
| import { ByokRelayService, ChatService, provideByokRelay } from '@byok-relay/angular'; | ||
|
|
||
| // app.config.ts | ||
| export const appConfig = { | ||
| providers: [provideByokRelay({ relayUrl: 'https://relay.byokrelay.com' })], | ||
| }; | ||
|
|
||
| // chat.component.ts | ||
| @Component({ template: ` | ||
| <div *ngFor="let m of chat.messages()">{{ m.role }}: {{ m.content }}</div> | ||
| <button (click)="send('Hello!')">Send</button> | ||
| ` }) | ||
| export class ChatComponent { | ||
| relay = inject(ByokRelayService); | ||
| chat = inject(ChatService); | ||
|
|
||
| async ngOnInit() { await this.relay.getOrRegister('my-app'); } | ||
| async send(text: string) { await this.chat.sendMessage(text); } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find the README and inspect the relevant section with line numbers.
git ls-files README.md
wc -l README.md
sed -n '1,140p' README.md | nl -ba | sed -n '45,90p'
# Look for any nearby mention of standalone components, NgModule, NgFor, or CommonModule.
rg -n "standalone|NgModule|CommonModule|NgFor|`@Component`\\(" README.mdRepository: avikalpg/byok-relay
Length of output: 221
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the target README section with line numbers using awk.
awk 'NR>=50 && NR<=90 { printf "%4d %s\n", NR, $0 }' README.md
# Search for any surrounding documentation about standalone components or NgModule usage.
rg -n "standalone|NgModule|CommonModule|NgFor|`@Component`\\(|provideByokRelay|ChatComponent" README.mdRepository: avikalpg/byok-relay
Length of output: 1751
Make the Angular example standalone-ready. *ngFor needs NgFor/CommonModule, but the snippet only shows @Component({ template: ... }). Either add standalone: true with imports: [NgFor] or note that the component lives in an NgModule.
🤖 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 `@README.md` around lines 57 - 77, The Angular example in ChatComponent is
missing the setup needed for *ngFor, so update the snippet to be
standalone-ready by adding the required import to the component’s imports or
explicitly stating it belongs to an NgModule. Make sure the `@Component`
definition for ChatComponent includes standalone usage with NgFor/CommonModule
support, or revise the example text so readers know the directive is provided by
an NgModule.
Summary
Growth Day 27 —
@byok-relay/preactpackage targeting Preact + Astro ecosystem.What is included
packages/preact/src/index.js— four hooks with runtime hook resolution:preact/hooks(native Preact)reactuseByokRelay({ relayUrl, appId })— token registration, key CRUD, logout, SSR-safe localStorageuseChat({ relayUrl, appId, provider, model, systemPrompt, extraParams })— stateful non-streaming chatuseStreamingChat(...)— SSE streaming with AbortController,stopStreaming(),streamingContentlive stringuseRelayHealth({ relayUrl, intervalMs, deep })— polls /health, manualcheck(deep?)24 smoke tests passing (
node test/hooks.test.js)packages/preact/README.md— full API docs, Astro component island quick-start, provider table, related packages tablellms.txt— Preact Hooks section addedREADME.md— Preact hooks section after Angular services, with Astro island examplemetrics/daily.jsonl— 2026-07-05 snapshotWhy Preact / Astro
Astro has become the dominant framework for content-heavy sites. Astro defaults to Preact for lightweight component hydration. This opens a distinct discovery channel for byok-relay.
Next for Avi
Summary by CodeRabbit
New Features
Bug Fixes
Documentation