Skip to content

growth(day-23): @byok-relay/svelte — Svelte stores for BYOK AI - #61

Open
alokit-bot wants to merge 1 commit into
mainfrom
feat/growth-2026-07-02
Open

growth(day-23): @byok-relay/svelte — Svelte stores for BYOK AI#61
alokit-bot wants to merge 1 commit into
mainfrom
feat/growth-2026-07-02

Conversation

@alokit-bot

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

Copy link
Copy Markdown
Collaborator

@byok-relay/svelte — Svelte stores for BYOK AI (Growth Day 23)

Four stores covering the full BYOK lifecycle in Svelte/SvelteKit apps.

What’s in this PR

packages/svelte/src/index.js

  • createByokRelayStore — relay token registration + key CRUD + logout; localStorage persisted; SvelteKit SSR-safe (typeof window guard)
  • createChatStore — stateful message list, non-streaming; supports openai / anthropic / groq / mistral / openrouter; systemPrompt + extraParams
  • createStreamingChatStore — SSE streaming with AbortController; stopStreaming() commits partial reply; streamingContent live string
  • createRelayHealthStore — polls /health; refetch() on demand; destroy() for onDestroy cleanup; deep=true readiness probe

All stores expose the Svelte store contract ({ subscribe, ...methods }): works with Svelte’s $store auto-subscription syntax, Svelte 5 runes, and plain JS .subscribe(). Svelte peer dep is optional — stores run without a build step in any JS environment.

packages/svelte/test/stores.test.js

63 smoke tests passing (plain Node, no Svelte build step required):

  • Store shape + initial state
  • logout clears token
  • subscribe fires immediately and returns unsubscribe function
  • storeKey rejects without token
  • send without token sets error + loading=false
  • stopStreaming is a no-op when idle
  • clear resets all state
  • destroy does not throw
  • Svelte store contract validated for all four stores

packages/svelte/README.md

Full API docs: store options tables, state field tables, method tables, full <script> + template examples for all four stores. SvelteKit onMount/onDestroy pattern. Svelte 5 rune compatibility note. Provider table. Self-hosting section. Related packages table.

README.md

Svelte stores section (install + streaming example) added before “For AI coding agents”.

llms.txt

Svelte Stores section + React Hooks + Vue Composables + MCP Server + API Reference sections — complete picture for AI agents discovering the ecosystem.

package.json

workspaces: ["packages/*"] added.

Metrics (2026-07-02)

stars=52 ⬆️ (was 51) forks=0 watchers=1 clones=110 views=20

Next for Avi

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

Summary by CodeRabbit

  • New Features

    • Added Svelte store support for registering a relay, saving API keys, chatting, streaming responses, and checking relay health.
    • Added workspace-aware package setup to support the new Svelte package.
  • Documentation

    • Expanded setup and usage docs for Svelte, React, Vue, and MCP integrations.
    • Added a dedicated Svelte package guide with installation, examples, SSR-safe usage, and deployment notes.
  • Tests

    • Added smoke tests covering the new Svelte store APIs and basic runtime behavior.

packages/svelte/src/index.js: four stores: createByokRelayStore (token
registration + key CRUD + logout, localStorage persistence,
SvelteKit SSR-safe), createChatStore (stateful message list,
non-streaming, openai/anthropic/groq/mistral/openrouter, systemPrompt +
extraParams), createStreamingChatStore (SSE streaming with
AbortController, stopStreaming(), streamingContent live string,
partial-commit on abort), createRelayHealthStore (polls /health,
refetch(), destroy() for onDestroy cleanup, deep readiness probe).

Svelte-compatible store contract: each factory returns
{ subscribe, ...methods } — works with Svelte  auto-subscription
syntax, Svelte 5 runes, and plain JS .subscribe(). Svelte peer dep
optional (marked optional in peerDependenciesMeta) — stores run in any
JS environment without a build step.

63 smoke tests passing: store shape, initial state, logout, subscribe
fires immediately, storeKey rejects without token, clear, send error
handling, stopStreaming no-op, destroy, Svelte store contract for all
four stores.

packages/svelte/README.md: full store API docs, SvelteKit usage guide,
Svelte 5 runes compatibility note, provider table, self-hosting note,
related packages table.

llms.txt: Svelte Stores section (npm install + usage snippet +
composable list + npm URL) + React Hooks, Vue Composables, MCP Server,
and API Reference sections consolidated.

README.md: Svelte stores section before 'For AI coding agents'.

package.json: workspaces: ['packages/*'] added.

Opens @byok-relay/svelte npm discovery channel targeting Svelte/SvelteKit
ecosystem.

metrics/daily.jsonl: 2026-07-02 snapshot (stars=52 forks=0 views=20
clones=110).

Next for Avi: cd packages/svelte && npm publish --access public
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a new @byok-relay/svelte package providing Svelte-compatible store factories for BYOK relay registration, chat, streaming chat, and health polling, with accompanying tests, package metadata, root workspace configuration, and documentation updates in README.md, llms.txt, and a new package README.

Changes

Svelte stores package

Layer / File(s) Summary
Package and workspace configuration
package.json, packages/svelte/package.json
Root package.json gets a workspaces array pointing to packages/* and an updated description; new packages/svelte/package.json declares package identity, entry points/exports, files, test script, and an optional svelte peer dependency.
Store factories implementation
packages/svelte/src/index.js
Implements a writable() fallback, localStorage helpers, SSE parsing utilities, and four exported store factories: createByokRelayStore (token/key management), createChatStore (non-streaming chat), createStreamingChatStore (SSE streaming chat), and createRelayHealthStore (health polling).
Smoke test suite
packages/svelte/test/stores.test.js
Node-based tests validate method presence, initial state, error paths, and Svelte store-contract behavior for all four factories, logging pass/fail results and exiting non-zero on failure.
Documentation updates
README.md, llms.txt, packages/svelte/README.md
Adds a Svelte stores section to the root README, new React/Vue/MCP/API-reference sections to llms.txt, and a full package README covering install, quick start, API docs, providers, SvelteKit/runes guidance, and self-hosting.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Component
  participant StreamingChatStore
  participant RelayServer

  Component->>StreamingChatStore: send(content, opts)
  StreamingChatStore->>RelayServer: POST /relay/{provider}/{path}
  RelayServer-->>StreamingChatStore: SSE data: chunks
  StreamingChatStore->>StreamingChatStore: extractDelta, update streamingContent
  StreamingChatStore-->>Component: subscribe updates
  Component->>StreamingChatStore: stopStreaming()
  StreamingChatStore->>StreamingChatStore: abort(), commit partial content to messages
Loading

Possibly related PRs

  • avikalpg/byok-relay#13: Adds equivalent BYOK relay client logic (token/key registration and SSE streaming chat) for a different framework, sharing the same relay API contract.

Suggested reviewers: avikalpg

🚥 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 clearly summarizes the main change by naming the new @byok-relay/svelte package and its Svelte store focus.
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-02

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: 5

🧹 Nitpick comments (7)
packages/svelte/test/stores.test.js (4)

35-44: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unused helper assertThrows.

Defined but never invoked in the file — dead code.

🤖 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/svelte/test/stores.test.js` around lines 35 - 44, The helper
assertThrows is dead code because it is defined in stores.test.js but never
used. Remove the unused assertThrows function, and if the surrounding test file
no longer needs the passed/failed tracking tied to it, clean up any related
unused variables or logging in the test harness.

126-133: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Misleading "clear resets state" tests never populate prior state.

The comment at Line 129 says a message is manually injected to simulate a prior conversation, but no injection actually happens — messages is already empty before clear() runs, so the assertion is trivially true and doesn't verify clear()'s reset behavior. The same pattern repeats for the streaming-chat clear() test at Lines 171-179.

♻️ Proposed fix: actually populate state before clearing (chat store example)
   const chat = createChatStore({ appId: 'test-clear-svelte', provider: 'openai' });
-  // Manually inject a message by reaching into store update (simulate prior conversation)
-  chat.clear();
+  // Populate some state via a failed send (adds a user message even without a token)
+  await chat.send('hello');
+  chat.clear();
   const state = get(chat);
   assert(state.messages.length === 0, 'clear empties messages');

Also applies to: 171-179

🤖 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/svelte/test/stores.test.js` around lines 126 - 133, The clear()
tests in createChatStore and the streaming-chat store are currently asserting on
an already-empty state, so they do not verify reset behavior. Update the tests
to first populate the store with non-empty prior state using the store’s public
update mechanisms or helper setup, then call clear() and assert that messages
are emptied afterward. Make the same fix in both clear-reset test blocks so the
assertions exercise real state transitions.

194-216: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Timing-fragile wait for unreachable-host failure.

Waiting a fixed 200ms for the automatic initial refetch() call (triggered inside createRelayHealthStore) to fail is timing-dependent. Since refetch() is exposed on the returned store and returns a promise, awaiting it directly would be deterministic.

🐛 Proposed fix using the exposed refetch()
-  // Wait for the initial fetch to fail (unreachable host)
-  await new Promise(r => setTimeout(r, 200));
+  // Explicitly await a fetch to the unreachable host
+  await health.refetch();
   const after = get(health);
   assert(after.ok === false, 'ok=false on unreachable host');
   assert(after.status === 'unreachable', 'status=unreachable on network error');
🤖 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/svelte/test/stores.test.js` around lines 194 - 216, The test in
createRelayHealthStore is timing-fragile because it waits a fixed delay for the
initial fetch to fail; replace the sleep with an explicit await of the store’s
exposed refetch() promise so the unreachable-host assertion in stores.test.js is
deterministic. Keep the existing checks for subscribe, destroy, and the
resulting ok/status state, but drive the failure through refetch() instead of
relying on the automatic background fetch timing.

97-106: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Flaky timing-based async assertion.

storeKey returns a rejected promise directly (per packages/svelte/src/index.js, it throws inside an async function when no token exists), so relying on a .catch() flag plus a fixed 10ms setTimeout to observe the rejection is unnecessary and can be flaky under CI load.

🐛 Proposed fix using direct await
   relay.logout(); // ensure no token
-  let threw = false;
-  relay.storeKey('openai', 'sk-test').catch(() => { threw = true; });
-  // give microtask a tick
-  await new Promise(r => setTimeout(r, 10));
-  assert(threw, 'storeKey throws if not registered');
+  let threw = false;
+  try {
+    await relay.storeKey('openai', 'sk-test');
+  } catch {
+    threw = true;
+  }
+  assert(threw, 'storeKey throws if not registered');
🤖 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/svelte/test/stores.test.js` around lines 97 - 106, The `storeKey`
rejection test is using a flaky timer-based `.catch()` flag check instead of
awaiting the promise directly. Update the `storeKey` assertion in
`stores.test.js` to use a direct async rejection check against
`createByokRelayStore(...).storeKey(...)`, matching the behavior in
`packages/svelte/src/index.js` where `storeKey` throws from an async function
when no token exists, and remove the fixed `setTimeout`/manual flag pattern.
packages/svelte/src/index.js (3)

147-155: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

No validation that appId is provided.

tokenKey is built as `byok_relay_token_${appId}` with no guard against appId being undefined. Forgetting to pass appId silently produces the key byok_relay_token_undefined, which any other consumer on the same origin that also forgets appId would collide with. The same pattern is repeated in createChatStore (Line 291) and createStreamingChatStore (Line 405).

💡 Suggested fix
 function createByokRelayStore({ relayUrl = DEFAULT_RELAY_URL, appId } = {}) {
+  if (!appId) throw new Error('createByokRelayStore: `appId` is required');
   const tokenKey = `byok_relay_token_${appId}`;
🤖 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/svelte/src/index.js` around lines 147 - 155, Add a required
validation for appId before building any storage key in createByokRelayStore,
createChatStore, and createStreamingChatStore; if appId is missing, fail fast
with a clear error instead of creating a key like byok_relay_token_undefined.
Update the store factory entry points to enforce the guard consistently and make
sure the tokenKey/chat key generation only runs after appId has been verified.

93-101: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

parseSSE() is defined but never used, and diverges from the inline parser.

The streaming loop in createStreamingChatStore.send() (Lines 479-495) re-implements SSE line parsing inline instead of using this generator, and the two differ subtly: parseSSE continues past a [DONE] line (Line 98), while the inline loop breaks out of the for entirely on [DONE] (Line 485), discarding any remaining buffered lines in that chunk. Either wire the streaming loop to use parseSSE/extractDelta together, or drop the unused generator to avoid two sources of truth for SSE parsing.

🤖 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/svelte/src/index.js` around lines 93 - 101, The SSE parsing logic is
duplicated between parseSSE and createStreamingChatStore.send(), and they handle
[DONE] differently. Update send() to use the existing parseSSE generator (and
extractDelta if applicable) so there is a single source of truth, or remove
parseSSE entirely if it is no longer needed. Make sure the chosen path preserves
buffered chunk handling correctly and keeps the [DONE] behavior consistent
across parseSSE and createStreamingChatStore.send().

39-68: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

.get() API is inconsistent with the real Svelte store contract this file tries to delegate to.

The fallback store exposes a custom .get() method (Line 63-65) that the rest of the file relies on heavily (_getToken(), send(), stopStreaming(), etc.). But Svelte's real writable() has no .get() method — reading a store's value requires the separate get(store) helper from svelte/store. So if globalThis.__svelteStoreWritable is ever wired up to delegate to real Svelte (Lines 41-43), every store.get() call elsewhere in this file would throw store.get is not a function.

This is currently unreachable dead code (nothing in the provided context ever sets globalThis.__svelteStoreWritable), but it's a landmine if that hook is ever activated. Consider either removing the delegation branch entirely (simplify to always use the internal implementation, which already satisfies the Svelte store contract for $store auto-subscription) or wrapping .get() consistently regardless of branch.

💡 Suggested simplification
 function writable(initial) {
-  // Try to use Svelte's writable if available (tree-shaken out when not bundled with Svelte)
-  if (typeof globalThis !== 'undefined' && globalThis.__svelteStoreWritable) {
-    return globalThis.__svelteStoreWritable(initial);
-  }
-
   let value = initial;
🤖 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/svelte/src/index.js` around lines 39 - 68, The writable store shim
in writable() exposes a custom .get() that is used by _getToken(), send(), and
stopStreaming(), but the delegation path to globalThis.__svelteStoreWritable
returns a real Svelte store without that method. Make the store API consistent
by either removing the delegation branch and always using the local
implementation, or by ensuring the returned store always includes a compatible
get() wrapper regardless of whether __svelteStoreWritable is used. Reference
writable(), subscribe(), set(), update(), and get() when updating the
implementation.
🤖 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/svelte/package.json`:
- Around line 22-26: The package metadata is advertising the wrong module format
because the `module` field in `packages/svelte/package.json` points to
`src/index.js`, which is CommonJS in `src/index.js` rather than an ESM build.
Update the `module` entry used alongside `main` and `exports` so it either
points to a real ESM bundle or is removed entirely if no ESM build exists,
keeping the package entry points consistent for bundlers.

In `@packages/svelte/README.md`:
- Around line 305-317: The Svelte README example is missing the import for
createRelayHealthStore, so the snippet will fail as written. Update the import
statement in the example to include createRelayHealthStore alongside
createByokRelayStore and createStreamingChatStore, and ensure the snippet
remains consistent with the createRelayHealthStore usage in the health variable
initialization.

In `@packages/svelte/src/index.js`:
- Around line 24-31: The `google` provider is exposed in `PROVIDER_PATHS` but
the chat flow still only handles `anthropic` and the OpenAI-style fallback, so
Gemini requests/responses are silently mis-shaped. Update
`createChatStore.send()`, `createStreamingChatStore.send()`, and
`extractDelta()` to either add a `google`/Gemini-specific branch that sends and
parses the correct payload shape, or remove `PROVIDER_PATHS.google` and the
related provider support until it is implemented. Ensure the provider list and
the JSDoc/type surface stay aligned with the actual supported symbols.
- Around line 420-521: The send() flow can let an aborted prior request
overwrite the state of a newer active stream. Add a per-request generation/token
inside send() and compare it before applying any async state changes in the
catch/finally paths and after the fetch/read loop. In particular, guard the
AbortError branch and the non-abort error branch so stale callbacks do not reset
isStreaming or streamingContent for a newer request. Use the send(),
stopStreaming(), _controller, and store.update/_patch paths to locate the fix.

In `@README.md`:
- Around line 33-36: The Svelte README examples call relay.register() directly
from onMount, but register() is async and can reject, causing an unhandled
rejection. Update the example in README.md and the repeated snippet in
packages/svelte/README.md to handle the returned promise explicitly with a
.catch(...) on relay.register(), using the existing createByokRelayStore and
onMount example so failures are safely handled.

---

Nitpick comments:
In `@packages/svelte/src/index.js`:
- Around line 147-155: Add a required validation for appId before building any
storage key in createByokRelayStore, createChatStore, and
createStreamingChatStore; if appId is missing, fail fast with a clear error
instead of creating a key like byok_relay_token_undefined. Update the store
factory entry points to enforce the guard consistently and make sure the
tokenKey/chat key generation only runs after appId has been verified.
- Around line 93-101: The SSE parsing logic is duplicated between parseSSE and
createStreamingChatStore.send(), and they handle [DONE] differently. Update
send() to use the existing parseSSE generator (and extractDelta if applicable)
so there is a single source of truth, or remove parseSSE entirely if it is no
longer needed. Make sure the chosen path preserves buffered chunk handling
correctly and keeps the [DONE] behavior consistent across parseSSE and
createStreamingChatStore.send().
- Around line 39-68: The writable store shim in writable() exposes a custom
.get() that is used by _getToken(), send(), and stopStreaming(), but the
delegation path to globalThis.__svelteStoreWritable returns a real Svelte store
without that method. Make the store API consistent by either removing the
delegation branch and always using the local implementation, or by ensuring the
returned store always includes a compatible get() wrapper regardless of whether
__svelteStoreWritable is used. Reference writable(), subscribe(), set(),
update(), and get() when updating the implementation.

In `@packages/svelte/test/stores.test.js`:
- Around line 35-44: The helper assertThrows is dead code because it is defined
in stores.test.js but never used. Remove the unused assertThrows function, and
if the surrounding test file no longer needs the passed/failed tracking tied to
it, clean up any related unused variables or logging in the test harness.
- Around line 126-133: The clear() tests in createChatStore and the
streaming-chat store are currently asserting on an already-empty state, so they
do not verify reset behavior. Update the tests to first populate the store with
non-empty prior state using the store’s public update mechanisms or helper
setup, then call clear() and assert that messages are emptied afterward. Make
the same fix in both clear-reset test blocks so the assertions exercise real
state transitions.
- Around line 194-216: The test in createRelayHealthStore is timing-fragile
because it waits a fixed delay for the initial fetch to fail; replace the sleep
with an explicit await of the store’s exposed refetch() promise so the
unreachable-host assertion in stores.test.js is deterministic. Keep the existing
checks for subscribe, destroy, and the resulting ok/status state, but drive the
failure through refetch() instead of relying on the automatic background fetch
timing.
- Around line 97-106: The `storeKey` rejection test is using a flaky timer-based
`.catch()` flag check instead of awaiting the promise directly. Update the
`storeKey` assertion in `stores.test.js` to use a direct async rejection check
against `createByokRelayStore(...).storeKey(...)`, matching the behavior in
`packages/svelte/src/index.js` where `storeKey` throws from an async function
when no token exists, and remove the fixed `setTimeout`/manual flag pattern.
🪄 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: 0c872cc1-c526-49bc-b556-8dd661eabd95

📥 Commits

Reviewing files that changed from the base of the PR and between f0e1a43 and 6911ffb.

📒 Files selected for processing (7)
  • README.md
  • llms.txt
  • package.json
  • packages/svelte/README.md
  • packages/svelte/package.json
  • packages/svelte/src/index.js
  • packages/svelte/test/stores.test.js

Comment on lines +22 to +26
"main": "src/index.js",
"module": "src/index.js",
"exports": {
".": "./src/index.js"
},

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

git ls-files packages/svelte/package.json packages/svelte/src/index.js packages/svelte/src/index.mjs packages/svelte/src/index.ts 2>/dev/null || true

echo
echo "---- package.json ----"
cat -n packages/svelte/package.json

echo
echo "---- entry file ----"
if [ -f packages/svelte/src/index.js ]; then
  cat -n packages/svelte/src/index.js
fi

echo
echo "---- sibling entry files ----"
find packages/svelte/src -maxdepth 1 -type f \( -name 'index.*' -o -name '*.mjs' -o -name '*.cjs' \) -print | sort

Repository: avikalpg/byok-relay

Length of output: 25020


module points to the CommonJS entry

packages/svelte/src/index.js is CommonJS ('use strict' + module.exports), so "module": "src/index.js" advertises the wrong format to bundlers. Drop it, or point it at a real ESM build if tree-shaking/static analysis matters.

💡 Suggested fix
   "main": "src/index.js",
-  "module": "src/index.js",
   "exports": {
     ".": "./src/index.js"
   },
📝 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
"main": "src/index.js",
"module": "src/index.js",
"exports": {
".": "./src/index.js"
},
"main": "src/index.js",
"exports": {
".": "./src/index.js"
},
🤖 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/svelte/package.json` around lines 22 - 26, The package metadata is
advertising the wrong module format because the `module` field in
`packages/svelte/package.json` points to `src/index.js`, which is CommonJS in
`src/index.js` rather than an ESM build. Update the `module` entry used
alongside `main` and `exports` so it either points to a real ESM bundle or is
removed entirely if no ESM build exists, keeping the package entry points
consistent for bundlers.

Comment thread packages/svelte/README.md
Comment on lines +305 to +317
```svelte
<script>
import { createByokRelayStore, createStreamingChatStore } from '@byok-relay/svelte';
import { onMount, onDestroy } from 'svelte';

const relay = createByokRelayStore({ appId: 'myapp' });
const chat = createStreamingChatStore({ appId: 'myapp', provider: 'openai' });
const health = createRelayHealthStore({ pollIntervalMs: 60_000 });

onMount(() => relay.register());
onDestroy(health.destroy);
</script>
```

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 createRelayHealthStore in this snippet.

Line 312 calls createRelayHealthStore(), but the import list only includes createByokRelayStore and createStreamingChatStore, so the example won’t run as written.

♻️ Proposed fix
-  import { createByokRelayStore, createStreamingChatStore } from '`@byok-relay/svelte`';
+  import { createByokRelayStore, createStreamingChatStore, createRelayHealthStore } from '`@byok-relay/svelte`';
📝 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
```svelte
<script>
import { createByokRelayStore, createStreamingChatStore } from '@byok-relay/svelte';
import { onMount, onDestroy } from 'svelte';
const relay = createByokRelayStore({ appId: 'myapp' });
const chat = createStreamingChatStore({ appId: 'myapp', provider: 'openai' });
const health = createRelayHealthStore({ pollIntervalMs: 60_000 });
onMount(() => relay.register());
onDestroy(health.destroy);
</script>
```
🤖 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/svelte/README.md` around lines 305 - 317, The Svelte README example
is missing the import for createRelayHealthStore, so the snippet will fail as
written. Update the import statement in the example to include
createRelayHealthStore alongside createByokRelayStore and
createStreamingChatStore, and ensure the snippet remains consistent with the
createRelayHealthStore usage in the health variable initialization.

Comment on lines +24 to +31
const PROVIDER_PATHS = {
openai: 'chat/completions',
anthropic: 'messages',
google: 'models/{model}:generateContent',
groq: 'chat/completions',
mistral: 'chat/completions',
openrouter: 'chat/completions',
};

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

google provider is registered but never actually implemented.

PROVIDER_PATHS.google and its {model} templating exist, but neither createChatStore.send() (Lines 314-330) nor createStreamingChatStore.send() (Lines 438-455) nor extractDelta() (Lines 103-111) has a Gemini-specific branch — they only special-case anthropic vs. a generic OpenAI-style fallback. A caller passing provider: 'google' would silently send an OpenAI-shaped body (messages array) to a Gemini endpoint and try to read data.choices[0].message.content from a response shaped as candidates[...].content.parts[...], producing empty/broken replies instead of an error. This also contradicts the JSDoc ('openai' | 'anthropic' | 'groq' | 'mistral' | 'openrouter', Line 259) and the PR's stated provider list (openai, anthropic, groq, mistral, openrouter).

Either implement a Gemini-specific request/response branch or remove the google entry until it's supported, to avoid silently broken behavior.

🤖 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/svelte/src/index.js` around lines 24 - 31, The `google` provider is
exposed in `PROVIDER_PATHS` but the chat flow still only handles `anthropic` and
the OpenAI-style fallback, so Gemini requests/responses are silently mis-shaped.
Update `createChatStore.send()`, `createStreamingChatStore.send()`, and
`extractDelta()` to either add a `google`/Gemini-specific branch that sends and
parses the correct payload shape, or remove `PROVIDER_PATHS.google` and the
related provider support until it is implemented. Ensure the provider list and
the JSDoc/type surface stay aligned with the actual supported symbols.

Comment on lines +420 to +521
async function send(content, opts = {}) {
const provider = opts.provider || defaultProvider;
const model = opts.model || defaultModel;
const extra = { ...extraParams, ...(opts.extraParams || {}) };
const token = storageGet(tokenKey);

if (!token) { _patch({ error: 'Not registered — call relay.register() first' }); return; }
if (store.get().isStreaming) stopStreaming();

_patch({ error: null, isStreaming: true, streamingContent: '' });
store.update(s => ({
...s,
messages: [...s.messages, { role: 'user', content }],
}));

_controller = new AbortController();

try {
const path = (PROVIDER_PATHS[provider] || 'chat/completions').replace('{model}', model || '');
let body;

if (provider === 'anthropic') {
body = {
model: model || 'claude-3-haiku-20240307',
max_tokens: 1024,
stream: true,
messages: store.get().messages.map(m => ({ role: m.role === 'assistant' ? 'assistant' : 'user', content: m.content })),
...(systemPrompt ? { system: systemPrompt } : {}),
...extra,
};
} else {
const msgs = [];
if (systemPrompt) msgs.push({ role: 'system', content: systemPrompt });
msgs.push(...store.get().messages.map(m => ({ role: m.role, content: m.content })));
body = { model: model || 'gpt-4o-mini', messages: msgs, stream: true, ...extra };
}

const res = await fetch(`${relayUrl}/relay/${provider}/${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify(body),
signal: _controller.signal,
});

if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || `Request failed (${res.status})`);
}

const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let full = '';

while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });

const lines = buffer.split('\n');
buffer = lines.pop(); // keep incomplete line

for (const line of lines) {
if (!line.startsWith('data:')) continue;
const raw = line.slice(5).trim();
if (raw === '[DONE]') break;
try {
const event = JSON.parse(raw);
const delta = extractDelta(event, provider);
if (delta) {
full += delta;
_patch({ streamingContent: full });
}
} catch { /* skip malformed */ }
}
}

store.update(s => ({
...s,
isStreaming: false,
streamingContent: '',
messages: [...s.messages, { role: 'assistant', content: full }],
}));
} catch (err) {
if (err.name === 'AbortError') {
// User stopped — commit whatever was streamed
const partial = store.get().streamingContent;
store.update(s => ({
...s,
isStreaming: false,
streamingContent: '',
messages: partial
? [...s.messages, { role: 'assistant', content: partial }]
: s.messages,
}));
} else {
_patch({ isStreaming: false, streamingContent: '', error: err.message });
}
} finally {
_controller = 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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Race between an aborted send and a newly-started send can clobber isStreaming/streamingContent.

When send() is called while already streaming, it calls stopStreaming() (Line 427) then immediately resets state and starts a new controller/fetch (Lines 429-462). The aborted request's catch block (Lines 503-517) still runs asynchronously afterward and unconditionally does store.update(s => ({ ...s, isStreaming: false, streamingContent: '', ... })). If that stale handler resolves after the new send() has already set isStreaming: true and started receiving new deltas, it will flip isStreaming back to false mid-stream (hiding the "streaming" UI indicator) even though the new request is still actively writing to streamingContent.

Guard against stale completions with a generation/request token compared before applying state from an async callback.

💡 Suggested fix
   let _controller = null;
+  let _generation = 0;

   async function send(content, opts = {}) {
     const provider = opts.provider || defaultProvider;
     const model    = opts.model    || defaultModel;
     const extra    = { ...extraParams, ...(opts.extraParams || {}) };
     const token    = storageGet(tokenKey);

     if (!token) { _patch({ error: 'Not registered — call relay.register() first' }); return; }
     if (store.get().isStreaming) stopStreaming();

+    const myGen = ++_generation;
     _patch({ error: null, isStreaming: true, streamingContent: '' });
     ...
       while (true) {
         const { done, value } = await reader.read();
+        if (myGen !== _generation) return; // superseded by a newer send()
         if (done) break;
         ...
       }

+      if (myGen !== _generation) return;
       store.update(s => ({
         ...s,
         isStreaming:      false,
         streamingContent: '',
         messages:         [...s.messages, { role: 'assistant', content: full }],
       }));
     } catch (err) {
+      if (myGen !== _generation) return;
       if (err.name === 'AbortError') {
         ...
📝 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 send(content, opts = {}) {
const provider = opts.provider || defaultProvider;
const model = opts.model || defaultModel;
const extra = { ...extraParams, ...(opts.extraParams || {}) };
const token = storageGet(tokenKey);
if (!token) { _patch({ error: 'Not registered — call relay.register() first' }); return; }
if (store.get().isStreaming) stopStreaming();
_patch({ error: null, isStreaming: true, streamingContent: '' });
store.update(s => ({
...s,
messages: [...s.messages, { role: 'user', content }],
}));
_controller = new AbortController();
try {
const path = (PROVIDER_PATHS[provider] || 'chat/completions').replace('{model}', model || '');
let body;
if (provider === 'anthropic') {
body = {
model: model || 'claude-3-haiku-20240307',
max_tokens: 1024,
stream: true,
messages: store.get().messages.map(m => ({ role: m.role === 'assistant' ? 'assistant' : 'user', content: m.content })),
...(systemPrompt ? { system: systemPrompt } : {}),
...extra,
};
} else {
const msgs = [];
if (systemPrompt) msgs.push({ role: 'system', content: systemPrompt });
msgs.push(...store.get().messages.map(m => ({ role: m.role, content: m.content })));
body = { model: model || 'gpt-4o-mini', messages: msgs, stream: true, ...extra };
}
const res = await fetch(`${relayUrl}/relay/${provider}/${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify(body),
signal: _controller.signal,
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || `Request failed (${res.status})`);
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let full = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop(); // keep incomplete line
for (const line of lines) {
if (!line.startsWith('data:')) continue;
const raw = line.slice(5).trim();
if (raw === '[DONE]') break;
try {
const event = JSON.parse(raw);
const delta = extractDelta(event, provider);
if (delta) {
full += delta;
_patch({ streamingContent: full });
}
} catch { /* skip malformed */ }
}
}
store.update(s => ({
...s,
isStreaming: false,
streamingContent: '',
messages: [...s.messages, { role: 'assistant', content: full }],
}));
} catch (err) {
if (err.name === 'AbortError') {
// User stopped — commit whatever was streamed
const partial = store.get().streamingContent;
store.update(s => ({
...s,
isStreaming: false,
streamingContent: '',
messages: partial
? [...s.messages, { role: 'assistant', content: partial }]
: s.messages,
}));
} else {
_patch({ isStreaming: false, streamingContent: '', error: err.message });
}
} finally {
_controller = null;
}
}
let _generation = 0;
async function send(content, opts = {}) {
const provider = opts.provider || defaultProvider;
const model = opts.model || defaultModel;
const extra = { ...extraParams, ...(opts.extraParams || {}) };
const token = storageGet(tokenKey);
if (!token) { _patch({ error: 'Not registered — call relay.register() first' }); return; }
if (store.get().isStreaming) stopStreaming();
const myGen = ++_generation;
_patch({ error: null, isStreaming: true, streamingContent: '' });
store.update(s => ({
...s,
messages: [...s.messages, { role: 'user', content }],
}));
_controller = new AbortController();
try {
const path = (PROVIDER_PATHS[provider] || 'chat/completions').replace('{model}', model || '');
let body;
if (provider === 'anthropic') {
body = {
model: model || 'claude-3-haiku-20240307',
max_tokens: 1024,
stream: true,
messages: store.get().messages.map(m => ({ role: m.role === 'assistant' ? 'assistant' : 'user', content: m.content })),
...(systemPrompt ? { system: systemPrompt } : {}),
...extra,
};
} else {
const msgs = [];
if (systemPrompt) msgs.push({ role: 'system', content: systemPrompt });
msgs.push(...store.get().messages.map(m => ({ role: m.role, content: m.content })));
body = { model: model || 'gpt-4o-mini', messages: msgs, stream: true, ...extra };
}
const res = await fetch(`${relayUrl}/relay/${provider}/${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify(body),
signal: _controller.signal,
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || `Request failed (${res.status})`);
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let full = '';
while (true) {
const { done, value } = await reader.read();
if (myGen !== _generation) return; // superseded by a newer send()
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop(); // keep incomplete line
for (const line of lines) {
if (!line.startsWith('data:')) continue;
const raw = line.slice(5).trim();
if (raw === '[DONE]') break;
try {
const event = JSON.parse(raw);
const delta = extractDelta(event, provider);
if (delta) {
full += delta;
_patch({ streamingContent: full });
}
} catch { /* skip malformed */ }
}
}
if (myGen !== _generation) return;
store.update(s => ({
...s,
isStreaming: false,
streamingContent: '',
messages: [...s.messages, { role: 'assistant', content: full }],
}));
} catch (err) {
if (myGen !== _generation) return;
if (err.name === 'AbortError') {
// User stopped — commit whatever was streamed
const partial = store.get().streamingContent;
store.update(s => ({
...s,
isStreaming: false,
streamingContent: '',
messages: partial
? [...s.messages, { role: 'assistant', content: partial }]
: s.messages,
}));
} else {
_patch({ isStreaming: false, streamingContent: '', error: err.message });
}
} finally {
_controller = null;
}
}
🤖 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/svelte/src/index.js` around lines 420 - 521, The send() flow can let
an aborted prior request overwrite the state of a newer active stream. Add a
per-request generation/token inside send() and compare it before applying any
async state changes in the catch/finally paths and after the fetch/read loop. In
particular, guard the AbortError branch and the non-abort error branch so stale
callbacks do not reset isStreaming or streamingContent for a newer request. Use
the send(), stopStreaming(), _controller, and store.update/_patch paths to
locate the fix.

Comment thread README.md
Comment on lines +33 to +36
const relay = createByokRelayStore({ appId: 'myapp' });
const chat = createStreamingChatStore({ appId: 'myapp', provider: 'openai' });

onMount(() => relay.register());

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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== File list ==\n'
git ls-files README.md packages/svelte/README.md packages/svelte src 2>/dev/null || true

printf '\n== Search for register() definitions/usages ==\n'
rg -n "register\(\)" README.md packages/svelte/README.md packages/svelte src . --glob '!**/node_modules/**' || true

printf '\n== Search for createByokRelayStore ==\n'
rg -n "createByokRelayStore|createStreamingChatStore|onMount" README.md packages/svelte/README.md packages/svelte src . --glob '!**/node_modules/**' || true

Repository: avikalpg/byok-relay

Length of output: 17793


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant README snippets with line numbers.
for f in README.md packages/svelte/README.md; do
  if [ -f "$f" ]; then
    printf '\n== %s ==\n' "$f"
    nl -ba "$f" | sed -n '1,120p'
  fi
done

Repository: avikalpg/byok-relay

Length of output: 214


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the implementation of register() and any error handling around it.
files=$(git ls-files | tr '\n' '\0' | xargs -0 rg -n --no-heading "register\\s*\\(" --glob '!**/node_modules/**' 2>/dev/null | cut -d: -f1 | sort -u)

printf 'Candidate files:\n%s\n' "$files"

# If we can find a likely implementation file, print the relevant slice.
for f in $(printf '%s\n' "$files" | head -n 20); do
  [ -f "$f" ] || continue
  printf '\n== %s ==\n' "$f"
  nl -ba "$f" | sed -n '1,220p'
done

Repository: avikalpg/byok-relay

Length of output: 342


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== README snippets ==\n'
for f in README.md packages/svelte/README.md; do
  if [ -f "$f" ]; then
    printf '\n-- %s --\n' "$f"
    nl -ba "$f" | sed -n '25,55p'
  fi
done

printf '\n== register() implementation candidates ==\n'
rg -n --glob '!**/node_modules/**' "register\\s*[:=]\\s*(async\\s*)?\\(|async\\s+register\\s*\\(|class .*register\\s*\\(" .

Repository: avikalpg/byok-relay

Length of output: 237


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== packages/svelte/src/index.js around createByokRelayStore/register ==\n'
sed -n '147,220p' packages/svelte/src/index.js

printf '\n== README.md snippet around onMount ==\n'
sed -n '25,40p' README.md

printf '\n== packages/svelte/README.md snippets around onMount ==\n'
sed -n '30,45p;132,148p;198,214p;306,318p' packages/svelte/README.md

Repository: avikalpg/byok-relay

Length of output: 4798


Handle the register() rejection in the Svelte examples.

relay.register() is async and rethrows on failure, so calling it directly from onMount can surface an unhandled rejection. Use an explicit .catch(...) in README.md and the repeated snippets in packages/svelte/README.md.

🤖 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 33 - 36, The Svelte README examples call
relay.register() directly from onMount, but register() is async and can reject,
causing an unhandled rejection. Update the example in README.md and the repeated
snippet in packages/svelte/README.md to handle the returned promise explicitly
with a .catch(...) on relay.register(), using the existing createByokRelayStore
and onMount example so failures are safely handled.

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