Skip to content

growth(day-27): @byok-relay/preact — Preact hooks for Astro/Vite BYOK AI#64

Open
alokit-bot wants to merge 4 commits into
mainfrom
feat/growth-2026-07-05
Open

growth(day-27): @byok-relay/preact — Preact hooks for Astro/Vite BYOK AI#64
alokit-bot wants to merge 4 commits into
mainfrom
feat/growth-2026-07-05

Conversation

@alokit-bot

@alokit-bot alokit-bot commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

Growth Day 27@byok-relay/preact package targeting Preact + Astro ecosystem.

What is included

packages/preact/src/index.js — four hooks with runtime hook resolution:

  1. Tries preact/hooks (native Preact)
  2. Falls back to react
  3. Falls back to inline shim (SSR / test env)
  • useByokRelay({ relayUrl, appId }) — token registration, key CRUD, logout, SSR-safe localStorage
  • useChat({ relayUrl, appId, provider, model, systemPrompt, extraParams }) — stateful non-streaming chat
  • useStreamingChat(...) — SSE streaming with AbortController, stopStreaming(), streamingContent live string
  • useRelayHealth({ relayUrl, intervalMs, deep }) — polls /health, manual check(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 table

llms.txt — Preact Hooks section added

README.md — Preact hooks section after Angular services, with Astro island example

metrics/daily.jsonl — 2026-07-05 snapshot

Why 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

cd packages/preact && npm publish --access public

Summary by CodeRabbit

  • New Features

    • Added official integration guides for SolidJS, Angular, Preact, and the Vercel AI SDK.
    • Introduced new framework packages and APIs for chat, streaming, token management, and relay health checks.
    • Added monorepo workspace support for the multi-package project layout.
  • Bug Fixes

    • Improved SSR-safe behavior and fallback handling for environments without browser storage.
    • Added streaming cancellation and partial-response handling for a smoother chat experience.
  • Documentation

    • Expanded README files with setup steps, usage examples, and self-hosting guidance.

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.
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds 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.

Changes

Workspace config and root documentation

Layer / File(s) Summary
Workspace config and metrics
package.json, metrics/daily.jsonl
Adds workspaces: ["packages/*"] to package.json and appends a new daily metrics record.
Root usage documentation
README.md, llms.txt
Adds SolidJS, Angular, Preact, and Vercel AI SDK integration sections with install/usage snippets.

Angular package

Layer / File(s) Summary
Manifest and README
packages/angular/package.json, packages/angular/README.md
Defines package metadata/entrypoints and documents installation, quick start, service APIs, signals support, and self-hosting.
Constants, shims, ByokRelayService
packages/angular/src/index.js
Adds relay path constants, a signal/storage shim, and ByokRelayService for token/key CRUD.
Chat and streaming services
packages/angular/src/index.js
Adds ChatService and StreamingChatService with SSE parsing, rollback, and abort/stop handling.
Health, bundling, and provider wiring
packages/angular/src/index.js
Adds RelayHealthService, createByokRelayBundle, provideByokRelay, and exports.
Tests
packages/angular/test/services.test.js
Adds a smoke-test suite covering all four services and the bundle factory.

Preact package

Layer / File(s) Summary
Manifest and README
packages/preact/package.json, packages/preact/README.md
Defines package metadata and documents hooks, Astro integration, and provider support.
Hooks implementation
packages/preact/src/index.js
Implements useByokRelay, useChat, useStreamingChat, useRelayHealth with runtime hook resolution and SSR-safe storage.
Tests
packages/preact/test/hooks.test.js
Adds a smoke-test suite including SSR-safety checks for all hooks.

Solid package

Layer / File(s) Summary
Manifest and README
packages/solid/package.json, packages/solid/README.md
Defines package metadata and documents store APIs, SSR guard usage, and self-hosting.
Reactivity shims and relay store
packages/solid/src/index.js
Adds signal/store shims, storage helpers, SSE utilities, and createByokRelayStore.
Chat, streaming, and health stores
packages/solid/src/index.js
Adds createChatStore, createStreamingChatStore, createRelayHealthStore, and exports.
Tests
packages/solid/test/stores.test.js
Adds a smoke-test suite covering all four store factories.

Vercel AI SDK package

Layer / File(s) Summary
Manifest and README
packages/vercel-ai/package.json, packages/vercel-ai/README.md
Defines package metadata and documents the provider adapter's routing flow and usage.
Storage, token, and prompt helpers
packages/vercel-ai/src/index.js
Adds storage fallback, token/key/account helpers, prompt conversion, and modelId parsing.
LanguageModelV1 adapter
packages/vercel-ai/src/index.js
Implements createLanguageModel with doGenerate/doStream request/response mapping.
Provider factories
packages/vercel-ai/src/index.js
Adds createByokRelayProvider and createByokRelayProviderSync with exports.
Tests
packages/vercel-ai/test/smoke.test.js
Adds a smoke-test suite covering generate/stream, auto-registration, and error handling.

Estimated code review effort: 4 (Complex) | ~75 minutes

Suggested reviewers: avikalpg

Poem

A rabbit hopped through packages new,
Angular, Preact, Solid too,
Vercel's SDK joined the crew,
Tokens stored and streams flow through,
Four little burrows, docs in view,
Hop hop hooray, the relay grew! 🐰

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately highlights the main visible addition: the new @byok-relay/preact hook package for Astro/Vite BYOK AI.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/growth-2026-07-05

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

🧹 Nitpick comments (13)
packages/solid/src/index.js (2)

264-272: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

opts.appId is documented but unused in createChatStore, createStreamingChatStore, and createRelayHealthStore.

JSDoc for these three factories lists opts.appId as a parameter, but it's never read in the implementation (only createByokRelayStore consumes appId). 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

createStore shim is unused dead code.

createStore/createSignal's sibling helper is defined but never called or exported from this file — only createSignal is 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 (no sendMessage() 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 win

Native Angular signal() path and non-default providers are untested.

The suite runs with no @angular/core installed, so createAngularSignal's Angular-16+ branch is never exercised, and only the openai provider is tested for Chat/Streaming — this is why the anthropic request-construction bug in src/index.js went unnoticed. Consider adding @angular/core as a devDependency (used only for npm test) plus an anthropic test 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 win

Dependent services reach into ByokRelayService's private fields instead of a public API.

ChatService (line 270, 290), StreamingChatService (line 375, 399), and RelayHealthService (line 512-513) all directly access this._relay._storage, this._relay._storageKey, and this._relay._relayUrl — underscore-prefixed fields that are meant to be private implementation details. This tightly couples every dependent service to ByokRelayService's internals; any future rename/refactor of those fields silently breaks three other classes.

Consider exposing small public accessors (e.g., getTokenValue(), get relayUrl()) on ByokRelayService for 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 on this, breaking if destructured off the provider object.

chat(modelId, overrides) { return this.languageModel(...) } only works when called as provider.chat(...). Common consumption patterns like const { chat } = provider; would break with this being undefined.

🛠️ Suggested fix
-    chat(modelId, overrides = {}) {
-      return this.languageModel(modelId, overrides);
-    },
+    chat: languageModel,

(reference the languageModel function/closure directly instead of this.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/none map explicitly; a specific tool selection ({ type: 'tool', toolName: '...' }, per LanguageModelV1CallOptions) 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 win

Streaming test coverage doesn't exercise a trailing-chunk without a final newline, or multi-result tool messages / specific toolChoice selection.

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 win

No test script despite a smoke-test suite existing.

test/smoke.test.js is added but this manifest has no scripts.test entry 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 win

Add 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.md around 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.js around 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 win

Declare react as an optional peer dependency too.

src/index.js explicitly falls back to require('react') when preact/hooks isn't resolvable (runtime hook resolution), but only preact is 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

📥 Commits

Reviewing files that changed from the base of the PR and between f0e1a43 and 283782f.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (20)
  • README.md
  • llms.txt
  • metrics/daily.jsonl
  • package.json
  • packages/angular/README.md
  • packages/angular/package.json
  • packages/angular/src/index.js
  • packages/angular/test/services.test.js
  • packages/preact/README.md
  • packages/preact/package.json
  • packages/preact/src/index.js
  • packages/preact/test/hooks.test.js
  • packages/solid/README.md
  • packages/solid/package.json
  • packages/solid/src/index.js
  • packages/solid/test/stores.test.js
  • packages/vercel-ai/README.md
  • packages/vercel-ai/package.json
  • packages/vercel-ai/src/index.js
  • packages/vercel-ai/test/smoke.test.js

Comment on lines +32 to +51
```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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Suggested change
```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.

Comment on lines +262 to +317
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);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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' -n

Repository: 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 -n

Repository: 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.

Comment on lines +366 to +467
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;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.js

Repository: 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/angular

Repository: 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 || true

Repository: 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.js

Repository: 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.js

Repository: 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.js

Repository: 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.

Comment thread packages/preact/README.md
Comment on lines +210 to +221
## 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 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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
done

Repository: 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#^\./##' | sort

Repository: 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 "")
PY

Repository: 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.

Comment on lines +19 to +52
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 });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Suggested change
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.

Comment on lines +186 to +215
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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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.

Comment on lines +294 to +360
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,
});
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.js

Repository: 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.

Comment on lines +393 to +405
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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Comment thread README.md
Comment on lines +28 to +47
```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>
</>
);
}
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
```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.

Comment thread README.md
Comment on lines +57 to +77
```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); }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.md

Repository: 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.md

Repository: 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant