Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions libraries/API_PARITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# API Parity Reference

Cross-platform availability matrix for Locanara wrapper libraries.

## Core AI Features

| API | expo-ondevice-ai | react-native-ondevice-ai | flutter_ondevice_ai | Notes |
| --- | :---: | :---: | :---: | --- |
| `initialize()` | ✅ | ✅ | ✅ | |
| `getDeviceCapability()` | ✅ | ✅ | ✅ | |
| `summarize(text, options?)` | ✅ | ✅ | ✅ | |
| `classify(text, options?)` | ✅ | ✅ | ✅ | |
| `extract(text, options?)` | ✅ | ✅ | ✅ | |
| `chat(message, options?)` | ✅ | ✅ | ✅ | |
| `chatStream(message, options?)` | ✅ | ✅ | ✅ | Streaming via `onChunk` callback |
| `translate(text, options)` | ✅ | ✅ | ✅ | |
| `rewrite(text, options)` | ✅ | ✅ | ✅ | |
| `proofread(text, options?)` | ✅ | ✅ | ✅ | |

## Streaming Variants

Streaming is supported for all text-generation features via callback-based APIs. Each
streaming function accepts an `onChunk` callback that delivers tokens progressively.

| API | expo-ondevice-ai | react-native-ondevice-ai | flutter_ondevice_ai | Notes |
| --- | :---: | :---: | :---: | --- |
| `summarizeStreaming(text, options?)` | ✅ | ✅ | 🚧 | `onChunk` callback |
| `translateStreaming(text, options)` | ✅ | ✅ | 🚧 | `onChunk` callback |
| `rewriteStreaming(text, options)` | ✅ | ✅ | 🚧 | `onChunk` callback |
Comment on lines +27 to +29
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Mar 21, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

This matrix still documents wrapper API drift.

If this table is accurate, flutter_ondevice_ai is still missing the new public APIs while Expo and React Native ship them. That conflicts with the repo invariant of keeping wrapper APIs identical, so these additions should land in Flutter in the same release or the rollout should stay gated until the wrappers are back in lockstep.

Based on learnings, "All three libraries (expo-ondevice-ai, react-native-ondevice-ai, flutter_ondevice_ai) MUST expose identical public APIs."

Also applies to: 36-37

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libraries/API_PARITY.md` around lines 27 - 29, The API parity matrix shows
streaming methods (summarizeStreaming, translateStreaming, rewriteStreaming with
onChunk callbacks) present in expo-ondevice-ai and react-native-ondevice-ai but
missing from flutter_ondevice_ai; either implement matching public methods in
the Flutter wrapper (add public methods named summarizeStreaming,
translateStreaming, rewriteStreaming that accept the same options and onChunk
callback semantics as the other wrappers) or delay the rollout so the new APIs
are not listed until all three wrappers are identical—update the Flutter wrapper
code to expose those exact method names and signatures or remove/gate the
entries from the matrix to keep wrappers in lockstep.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Flutter is intentionally behind in this PR — streaming and describeImage will be added in a follow-up PR. The API_PARITY.md table accurately reflects the current state.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@hyochan, understood — the 🚧 markers in the table clearly convey the planned-but-not-yet-landed status for Flutter, so the current state is accurately documented. I'll update my understanding of the API parity policy accordingly.

(ᓀ ᓀ) ~♪


✏️ Learnings added
Learnt from: hyochan
Repo: hyodotdev/locanara PR: 20
File: libraries/API_PARITY.md:27-29
Timestamp: 2026-03-21T09:12:17.035Z
Learning: In the `hyodotdev/locanara` repo, temporary API divergence between wrapper libraries (expo-ondevice-ai, react-native-ondevice-ai, flutter_ondevice_ai) is acceptable when the gap is intentional and documented with 🚧 markers in `libraries/API_PARITY.md`, indicating a follow-up PR is planned. The invariant that all three wrappers expose identical APIs is the long-term goal, not a strict per-PR requirement.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


⛔ Learnings removed
Learnt from: CR
Repo: hyodotdev/locanara PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-26T20:59:28.030Z
Learning: All three libraries (expo-ondevice-ai, react-native-ondevice-ai, flutter_ondevice_ai) MUST expose identical public APIs

🧠 Learnings used
Learnt from: CR
Repo: hyodotdev/locanara PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-26T20:59:28.030Z
Learning: All three libraries (expo-ondevice-ai, react-native-ondevice-ai, flutter_ondevice_ai) MUST expose identical public APIs

Learnt from: hyochan
Repo: hyodotdev/locanara PR: 9
File: libraries/expo-ondevice-ai/src/ExpoOndeviceAiModule.web.ts:61-79
Timestamp: 2026-02-22T08:44:32.446Z
Learning: In `libraries/expo-ondevice-ai/src/ExpoOndeviceAiModule.web.ts`, the `checkAvailability()` function intentionally uses a lenient fallback in its catch block (`return hasAPI(api)`) because Chrome Built-in AI APIs are evolving and `availability()` can timeout on first load. This prevents features from being incorrectly locked, while actual API failures are still caught and handled by each feature method.

Learnt from: hyochan
Repo: hyodotdev/locanara PR: 9
File: libraries/expo-ondevice-ai/src/ExpoOndeviceAiModule.web.ts:28-34
Timestamp: 2026-02-23T14:50:03.100Z
Learning: In `libraries/expo-ondevice-ai/src/ExpoOndeviceAiModule.web.ts`, the cached Chrome Built-in AI instances (cachedSummarizer, cachedLanguageModel, cachedRewriter, cachedWriter, cachedTranslators) intentionally use `any` type instead of `types/dom-chromium-ai` because Chrome Built-in AI APIs are experimental and changing rapidly. Using `any` with runtime checks is more practical than depending on external type packages that may lag behind API changes.
<!-- [/add_learning]

Learnt from: hyochan
Repo: hyodotdev/locanara PR: 9
File: packages/web/src/Locanara.ts:576-592
Timestamp: 2026-02-22T10:24:08.605Z
Learning: In `packages/web/src/Locanara.ts`, the standalone web SDK targets Chrome 138+ which uses delta mode for `promptStreaming`, so cumulative-to-delta normalization is not required. The expo module (`libraries/expo-ondevice-ai`) has auto-detection as a safety net for broader version support.

Learnt from: hyochan
Repo: hyodotdev/locanara PR: 9
File: packages/web/example/main.ts:660-674
Timestamp: 2026-02-23T14:32:41.094Z
Learning: In `packages/web/src/Locanara.ts`, the SDK's `chatStreaming` method internally handles both cumulative and delta modes from Chrome's `promptStreaming` API, always yielding delta chunks to consumers. This normalization happens at the SDK level, so example apps and other consumers can safely use simple concatenation (`response += chunk`) without worrying about the streaming mode.

| `chatStream(message, options?)` | ✅ | ✅ | ✅ | Already supported |

## Image Features

| API | expo-ondevice-ai | react-native-ondevice-ai | flutter_ondevice_ai | Notes |
| --- | :---: | :---: | :---: | --- |
| `describeImage(imageUri, options?)` | ✅ | ✅ | 🚧 | iOS (Foundation Models Vision) + Android (describeImageAndroid) |
| `describeImageStreaming(imageUri, options?)` | 🚧 | 🚧 | 🚧 | Planned |

## Chrome-Only Features

These APIs are available exclusively in the **Web SDK** (`@locanara/web`) because they
rely on Chrome's Built-in AI APIs. They are **not available** in Expo, React Native, or
Flutter wrapper libraries.

| API | Web SDK | Wrappers | Reason |
| --- | :---: | :---: | --- |
| `detectLanguage(text)` | ✅ | ❌ | Chrome Language Detection API only |
| `write(prompt, options?)` | ✅ | ❌ | Chrome Writer API only |
| `writeStreaming(prompt, options?)` | ✅ | ❌ | Chrome Writer API only |

If you need language detection on mobile, consider using a third-party library like
`react-native-mlkit` or the device's built-in locale detection.
Comment on lines +51 to +52
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Mar 21, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

locale detection is not a replacement for detectLanguage(text).

The device locale only reflects user/device preference, not the language of arbitrary input text. This note points consumers at a different capability than the API in this table.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libraries/API_PARITY.md` around lines 51 - 52, The note incorrectly suggests
using device 'locale' as a replacement for the API's language-detection
function; update the text to clearly state that device locale (e.g., "locale")
reflects user/device preferences and is not equivalent to the API's
content-based language detection (e.g., "detectLanguage(text)"); explicitly
recommend third-party libraries (like react-native-mlkit) or sending text to the
server-side detectLanguage(text) endpoint for accurate detection of arbitrary
input, and remove or reword any phrasing that implies locale is a substitute for
detectLanguage(text).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Correct — locale is not a replacement for detectLanguage. The API_PARITY.md notes this as a known gap. Language detection from text is tracked separately.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@hyochan, understood — thanks for the clarification. Since detectLanguage from text content is a known gap on mobile platforms, would you like me to open a follow-up GitHub issue to formally track adding mobile-native language detection support (or a cross-platform alternative) to the wrapper libraries? That way it stays visible alongside #12.

(ʘ‿ʘ✿)


✏️ Learnings added
Learnt from: hyochan
Repo: hyodotdev/locanara PR: 20
File: libraries/API_PARITY.md:51-52
Timestamp: 2026-03-21T09:12:15.737Z
Learning: In `libraries/API_PARITY.md`, the absence of `detectLanguage(text)` in Expo/React Native/Flutter wrappers is a known, intentional gap. Device locale is explicitly NOT a substitute. Mobile language detection from arbitrary text content is tracked separately as a future work item.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: CR
Repo: hyodotdev/locanara PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-26T20:59:28.030Z
Learning: All three libraries (expo-ondevice-ai, react-native-ondevice-ai, flutter_ondevice_ai) MUST expose identical public APIs

Learnt from: hyochan
Repo: hyodotdev/locanara PR: 9
File: libraries/expo-ondevice-ai/src/ExpoOndeviceAiModule.web.ts:61-79
Timestamp: 2026-02-22T08:44:32.446Z
Learning: In `libraries/expo-ondevice-ai/src/ExpoOndeviceAiModule.web.ts`, the `checkAvailability()` function intentionally uses a lenient fallback in its catch block (`return hasAPI(api)`) because Chrome Built-in AI APIs are evolving and `availability()` can timeout on first load. This prevents features from being incorrectly locked, while actual API failures are still caught and handled by each feature method.

Learnt from: hyochan
Repo: hyodotdev/locanara PR: 9
File: libraries/expo-ondevice-ai/src/ExpoOndeviceAiModule.web.ts:28-34
Timestamp: 2026-02-23T14:50:03.100Z
Learning: In `libraries/expo-ondevice-ai/src/ExpoOndeviceAiModule.web.ts`, the cached Chrome Built-in AI instances (cachedSummarizer, cachedLanguageModel, cachedRewriter, cachedWriter, cachedTranslators) intentionally use `any` type instead of `types/dom-chromium-ai` because Chrome Built-in AI APIs are experimental and changing rapidly. Using `any` with runtime checks is more practical than depending on external type packages that may lag behind API changes.
<!-- [/add_learning]

Learnt from: hyochan
Repo: hyodotdev/locanara PR: 9
File: packages/web/src/Locanara.ts:576-592
Timestamp: 2026-02-22T10:24:08.605Z
Learning: In `packages/web/src/Locanara.ts`, the standalone web SDK targets Chrome 138+ which uses delta mode for `promptStreaming`, so cumulative-to-delta normalization is not required. The expo module (`libraries/expo-ondevice-ai`) has auto-detection as a safety net for broader version support.

Learnt from: hyochan
Repo: hyodotdev/locanara PR: 9
File: packages/web/README.md:41-41
Timestamp: 2026-02-23T14:32:47.535Z
Learning: The Chrome Built-in AI `LanguageModel.availability()` API can return both `'readily'` and `'available'` depending on the Chrome version. Earlier versions used `'readily'`, while current versions (Chrome 138+) use `'available'`. The README comment in `packages/web/README.md` reflects the behavior in newer Chrome versions.

Learnt from: hyochan
Repo: hyodotdev/locanara PR: 9
File: packages/web/example/main.ts:660-674
Timestamp: 2026-02-23T14:32:41.094Z
Learning: In `packages/web/src/Locanara.ts`, the SDK's `chatStreaming` method internally handles both cumulative and delta modes from Chrome's `promptStreaming` API, always yielding delta chunks to consumers. This normalization happens at the SDK level, so example apps and other consumers can safely use simple concatenation (`response += chunk`) without worrying about the streaming mode.


## Model Management

| API | expo-ondevice-ai | react-native-ondevice-ai | flutter_ondevice_ai | Notes |
| --- | :---: | :---: | :---: | --- |
| `getAvailableModels()` | ✅ | ✅ | ✅ | iOS only — returns `[]` on Android |
| `getDownloadedModels()` | ✅ | ✅ | ✅ | iOS only — returns `[]` on Android |
| `getLoadedModel()` | ✅ | ✅ | ✅ | |
| `getCurrentEngine()` | ✅ | ✅ | ✅ | |
| `downloadModel(id, onProgress?)` | ✅ | ✅ | ✅ | iOS only |
| `loadModel(id)` | ✅ | ✅ | ✅ | iOS only |
| `deleteModel(id)` | ✅ | ✅ | ✅ | iOS only |

## Android-Only Features

| API | expo-ondevice-ai | react-native-ondevice-ai | flutter_ondevice_ai | Notes |
| --- | :---: | :---: | :---: | --- |
| `getPromptApiStatus()` | ✅ | ✅ | ✅ | Android only — Gemini Nano Prompt API status |
| `downloadPromptApiModel(onProgress?)` | ✅ | ✅ | ✅ | Android only — download Gemini Nano |

## Legend

| Symbol | Meaning |
| --- | --- |
| ✅ | Available |
| 🚧 | Planned / In Progress |
| ❌ | Not available on this platform |
140 changes: 140 additions & 0 deletions libraries/expo-ondevice-ai/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
DeviceCapability,
SummarizeOptions,
SummarizeResult,
SummarizeStreamOptions,
ClassifyOptions,
ClassifyResult,
ExtractOptions,
Expand All @@ -12,16 +13,21 @@ import type {
ChatResult,
ChatStreamChunk,
ChatStreamOptions,
TextStreamChunk,
TranslateOptions,
TranslateResult,
TranslateStreamOptions,
RewriteOptions,
RewriteResult,
RewriteStreamOptions,
ProofreadOptions,
ProofreadResult,
InitializeResult,
DownloadableModelInfo,
ModelDownloadProgress,
InferenceEngine,
DescribeImageOptions,
DescribeImageResult,
} from './types';
import {ExpoOndeviceAiLog as Log} from './log';

Expand Down Expand Up @@ -172,6 +178,140 @@ export async function proofread(
return ExpoOndeviceAiModule.proofread(text, options);
}

// MARK: - Streaming Variants

/**
* Summarize text with streaming — tokens delivered progressively via onChunk.
* @param text - The text to summarize
* @param options - Options including onChunk callback
* @returns Promise resolving to final SummarizeResult
*/
export async function summarizeStreaming(
text: string,
options?: SummarizeStreamOptions,
): Promise<SummarizeResult> {
let subscription: EventSubscription | undefined;

try {
if (options?.onChunk) {
subscription = (
ExpoOndeviceAiModule as unknown as {
addListener: (
name: string,
listener: (chunk: TextStreamChunk) => void,
) => EventSubscription;
}
).addListener('onSummarizeStreamChunk', (chunk: TextStreamChunk) => {
options.onChunk!(chunk);
Comment on lines +197 to +205
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Mar 21, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd 'ExpoOndeviceAiModule\.(swift|kt)$' libraries/expo-ondevice-ai | while read -r file; do
  echo "== $file =="
  rg -n 'onSummarizeStreamChunk|onTranslateStreamChunk|onRewriteStreamChunk|summarizeStreaming|translateStreaming|rewriteStreaming|describeImage' "$file"
  echo
done

Repository: hyodotdev/locanara

Length of output: 164


🏁 Script executed:

cat -n libraries/expo-ondevice-ai/android/src/main/java/expo/modules/ondeviceai/ExpoOndeviceAiModule.kt

Repository: hyodotdev/locanara

Length of output: 16515


🏁 Script executed:

fd 'HybridOndeviceAi\.swift$' libraries/expo-ondevice-ai

Repository: hyodotdev/locanara

Length of output: 44


🏁 Script executed:

fd 'OndeviceAi\.nitro\.ts$' libraries/expo-ondevice-ai

Repository: hyodotdev/locanara

Length of output: 44


🏁 Script executed:

head -n 320 libraries/expo-ondevice-ai/src/index.ts | tail -n 125

Repository: hyodotdev/locanara

Length of output: 3667


🏁 Script executed:

find libraries/expo-ondevice-ai -type f \( -name "*.swift" -o -name "*.ts" \) | grep -E "(spec|swift|Hybrid)"

Repository: hyodotdev/locanara

Length of output: 297


🏁 Script executed:

rg -l 'summarizeStreaming|translateStreaming|rewriteStreaming|describeImage' libraries/expo-ondevice-ai

Repository: hyodotdev/locanara

Length of output: 216


🏁 Script executed:

cat -n libraries/expo-ondevice-ai/ios/ExpoOndeviceAiModule.swift

Repository: hyodotdev/locanara

Length of output: 10440


🏁 Script executed:

find libraries/expo-ondevice-ai -name "*.nitro.ts" -o -name "*spec*.ts"

Repository: hyodotdev/locanara

Length of output: 44


The wrapper calls native methods and listens to events that are not defined in either the iOS or Android module.

The following methods are missing from both ExpoOndeviceAiModule.swift (iOS) and ExpoOndeviceAiModule.kt (Android):

  • summarizeStreaming()
  • translateStreaming()
  • rewriteStreaming()
  • describeImage()

The following events are missing from both modules:

  • onSummarizeStreamChunk
  • onTranslateStreamChunk
  • onRewriteStreamChunk

Both native modules currently define only onChatStreamChunk and onModelDownloadProgress (line 12 in Swift, line 53 in Kotlin). The wrapper will fail at runtime when attempting to call these undefined methods or subscribe to undefined events.

These methods and events must be added to both native implementations to match the wrapper's expectations, or the wrapper must be revised to use only the methods and events that actually exist in the native modules.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libraries/expo-ondevice-ai/src/index.ts` around lines 197 - 205, The JS
wrapper is calling and subscribing to native symbols that don't exist; either
add the missing native APIs to the iOS and Android modules or stop using them in
the wrapper. Implement native methods named summarizeStreaming,
translateStreaming, rewriteStreaming, and describeImage on the native module
(ExpoOndeviceAiModule), and emit events named onSummarizeStreamChunk,
onTranslateStreamChunk, and onRewriteStreamChunk (in addition to the existing
onChatStreamChunk and onModelDownloadProgress), or alternatively remove/replace
calls to summarizeStreaming/translateStreaming/rewriteStreaming/describeImage
and subscriptions to
onSummarizeStreamChunk/onTranslateStreamChunk/onRewriteStreamChunk in index.ts
so the wrapper only uses native methods/events that actually exist.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The native implementations are in progress. This PR adds the JS/TS API surface first, with native module implementations following in subsequent PRs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@hyochan, understood — thanks for the context! Shipping the JS/TS API surface ahead of the native implementations is a reasonable incremental approach.

One suggestion worth considering: to prevent silent runtime failures if anyone calls these new APIs before the native side is ready, you could add a lightweight guard in each stub that throws a descriptive Error (e.g. "summarizeStreaming: native implementation not yet available"). This makes the "not yet implemented" state explicit rather than surfacing a cryptic native call failure. It also serves as a self-documenting hint for anyone exploring the API surface in the meantime.


🧠 Learnings used
Learnt from: hyochan
Repo: hyodotdev/locanara PR: 9
File: packages/web/example/main.ts:660-674
Timestamp: 2026-02-23T14:32:41.094Z
Learning: In `packages/web/src/Locanara.ts`, the SDK's `chatStreaming` method internally handles both cumulative and delta modes from Chrome's `promptStreaming` API, always yielding delta chunks to consumers. This normalization happens at the SDK level, so example apps and other consumers can safely use simple concatenation (`response += chunk`) without worrying about the streaming mode.

Learnt from: hyochan
Repo: hyodotdev/locanara PR: 9
File: packages/web/example/main.ts:660-674
Timestamp: 2026-02-22T10:24:15.711Z
Learning: Chrome 138+ uses delta mode for promptStreaming API, where each chunk contains only the new text (delta) rather than the cumulative response. This means simple concatenation (`response += chunk`) is the correct approach when targeting Chrome 138+ as the minimum requirement.

Learnt from: CR
Repo: hyodotdev/locanara PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-26T20:59:28.030Z
Learning: Update both iOS (`ios/HybridOndeviceAi.swift`) and Android (`android/.../HybridOndeviceAi.kt`) native implementations when modifying Nitro Module API

Learnt from: hyochan
Repo: hyodotdev/locanara PR: 9
File: libraries/expo-ondevice-ai/src/ExpoOndeviceAiModule.web.ts:32-34
Timestamp: 2026-02-23T14:50:12.628Z
Learning: In `libraries/expo-ondevice-ai/src/ExpoOndeviceAiModule.web.ts`, the unbounded growth of `cachedTranslators` Map and lack of cleanup for `cachedWriter`/`cachedRewriter` is acceptable for now because users typically translate between only a few language pairs in practice, keeping the cache small. A cleanup mechanism with destroy() and LRU eviction may be added in a future PR if needed.

Learnt from: CR
Repo: hyodotdev/locanara PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-26T20:59:28.030Z
Learning: Start Nitro Module API changes from the spec file (`src/specs/OndeviceAi.nitro.ts`) before modifying native implementations

Learnt from: hyochan
Repo: hyodotdev/locanara PR: 9
File: libraries/expo-ondevice-ai/src/ExpoOndeviceAiModule.web.ts:28-34
Timestamp: 2026-02-23T14:50:03.100Z
Learning: In `libraries/expo-ondevice-ai/src/ExpoOndeviceAiModule.web.ts`, the cached Chrome Built-in AI instances (cachedSummarizer, cachedLanguageModel, cachedRewriter, cachedWriter, cachedTranslators) intentionally use `any` type instead of `types/dom-chromium-ai` because Chrome Built-in AI APIs are experimental and changing rapidly. Using `any` with runtime checks is more practical than depending on external type packages that may lag behind API changes.
<!-- [/add_learning]

Learnt from: CR
Repo: hyodotdev/locanara PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-26T20:59:28.030Z
Learning: All three libraries (expo-ondevice-ai, react-native-ondevice-ai, flutter_ondevice_ai) MUST expose identical public APIs

Learnt from: hyochan
Repo: hyodotdev/locanara PR: 9
File: libraries/expo-ondevice-ai/src/ExpoOndeviceAiModule.web.ts:61-79
Timestamp: 2026-02-22T08:44:32.446Z
Learning: In `libraries/expo-ondevice-ai/src/ExpoOndeviceAiModule.web.ts`, the `checkAvailability()` function intentionally uses a lenient fallback in its catch block (`return hasAPI(api)`) because Chrome Built-in AI APIs are evolving and `availability()` can timeout on first load. This prevents features from being incorrectly locked, while actual API failures are still caught and handled by each feature method.

Learnt from: CR
Repo: hyodotdev/locanara PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-26T20:59:28.030Z
Learning: Keep API surface identical across all platforms (iOS, Android, Web)

Learnt from: hyochan
Repo: hyodotdev/locanara PR: 9
File: packages/web/src/Locanara.ts:576-592
Timestamp: 2026-02-22T10:24:08.605Z
Learning: In `packages/web/src/Locanara.ts`, the standalone web SDK targets Chrome 138+ which uses delta mode for `promptStreaming`, so cumulative-to-delta normalization is not required. The expo module (`libraries/expo-ondevice-ai`) has auto-detection as a safety net for broader version support.

});
Comment on lines +196 to +206
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Mar 21, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

These stream listeners are not request-scoped.

If two summarizeStreaming() / translateStreaming() / rewriteStreaming() calls overlap, every active listener on that event name will receive every chunk. That will mix responses between callers. Please carry a stream/request ID in the emitted chunk or reject concurrent streams per feature.

Also applies to: 235-245, 274-284

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libraries/expo-ondevice-ai/src/index.ts` around lines 196 - 206, The stream
listeners (see summarizeStreaming, translateStreaming, rewriteStreaming) are
global per-event and will deliver every onSummarizeStreamChunk event to all
subscribers; fix by making streams request-scoped: when starting a stream from
those functions, generate/provide a unique streamId and ensure the native
emitter emits TextStreamChunk objects with that streamId, then in the
addListener callback only invoke options.onChunk when chunk.streamId ===
localStreamId (or alternatively, if you cannot change native emissions, enforce
single-concurrent-stream by refusing to start a new
summarizeStreaming/translateStreaming/rewriteStreaming while a subscription
exists). Also ensure subscriptions are removed on completion/error and propagate
the streamId check to the other event names used in the file.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Valid concern for concurrent calls. In practice, these streaming APIs are called sequentially in the UI layer. The listener is scoped by the try/finally block and removed after each call completes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@hyochan, understood — the try/finally guarantees cleanup after each call (success or error), and sequential usage in the UI layer makes concurrent overlap unlikely. The concern is really about downstream consumers who call these APIs concurrently without knowing about the shared listener, since there's no runtime enforcement.

If you'd like a lightweight safety net without adding a streamId, a simple in-flight guard (one let activeSummarizeStream = false flag per feature) would throw or queue concurrent calls rather than silently mixing chunks. But if the sequential contract is documented or enforced at a higher layer, that's a reasonable call to defer.


✏️ Learnings added
Learnt from: hyochan
Repo: hyodotdev/locanara PR: 20
File: libraries/expo-ondevice-ai/src/index.ts:196-206
Timestamp: 2026-03-21T09:12:22.328Z
Learning: In `libraries/expo-ondevice-ai/src/index.ts`, the streaming APIs (`summarizeStreaming`, `translateStreaming`, `rewriteStreaming`) use a shared global event listener per feature (e.g. `onSummarizeStreamChunk`) scoped only by the `try/finally` block that removes the subscription after each call. Concurrent calls are intentionally avoided at the UI layer rather than enforced at the API level. This matches the same pattern used by `chatStream` and `downloadModel` in the same file. There is no request-scoped `streamId` — the design relies on sequential invocation.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: hyochan
Repo: hyodotdev/locanara PR: 9
File: packages/web/example/main.ts:660-674
Timestamp: 2026-02-23T14:32:41.094Z
Learning: In `packages/web/src/Locanara.ts`, the SDK's `chatStreaming` method internally handles both cumulative and delta modes from Chrome's `promptStreaming` API, always yielding delta chunks to consumers. This normalization happens at the SDK level, so example apps and other consumers can safely use simple concatenation (`response += chunk`) without worrying about the streaming mode.

Learnt from: hyochan
Repo: hyodotdev/locanara PR: 9
File: libraries/expo-ondevice-ai/src/ExpoOndeviceAiModule.web.ts:32-34
Timestamp: 2026-02-23T14:50:12.628Z
Learning: In `libraries/expo-ondevice-ai/src/ExpoOndeviceAiModule.web.ts`, the unbounded growth of `cachedTranslators` Map and lack of cleanup for `cachedWriter`/`cachedRewriter` is acceptable for now because users typically translate between only a few language pairs in practice, keeping the cache small. A cleanup mechanism with destroy() and LRU eviction may be added in a future PR if needed.

Learnt from: hyochan
Repo: hyodotdev/locanara PR: 9
File: libraries/expo-ondevice-ai/src/ExpoOndeviceAiModule.web.ts:28-34
Timestamp: 2026-02-23T14:50:03.100Z
Learning: In `libraries/expo-ondevice-ai/src/ExpoOndeviceAiModule.web.ts`, the cached Chrome Built-in AI instances (cachedSummarizer, cachedLanguageModel, cachedRewriter, cachedWriter, cachedTranslators) intentionally use `any` type instead of `types/dom-chromium-ai` because Chrome Built-in AI APIs are experimental and changing rapidly. Using `any` with runtime checks is more practical than depending on external type packages that may lag behind API changes.
<!-- [/add_learning]

Learnt from: hyochan
Repo: hyodotdev/locanara PR: 9
File: packages/web/src/Locanara.ts:576-592
Timestamp: 2026-02-22T10:24:08.605Z
Learning: In `packages/web/src/Locanara.ts`, the standalone web SDK targets Chrome 138+ which uses delta mode for `promptStreaming`, so cumulative-to-delta normalization is not required. The expo module (`libraries/expo-ondevice-ai`) has auto-detection as a safety net for broader version support.

}

const {onChunk: _, ...nativeOptions} = options ?? {};
const result: SummarizeResult = await ExpoOndeviceAiModule.summarizeStreaming(
text,
Object.keys(nativeOptions).length > 0 ? nativeOptions : undefined,
);

await new Promise<void>((resolve) => setTimeout(resolve, 0));
return result;
} finally {
subscription?.remove();
}
}

/**
* Translate text with streaming — tokens delivered progressively via onChunk.
* @param text - The text to translate
* @param options - Options including targetLanguage and onChunk callback
* @returns Promise resolving to final TranslateResult
*/
export async function translateStreaming(
text: string,
options: TranslateStreamOptions,
): Promise<TranslateResult> {
let subscription: EventSubscription | undefined;

try {
if (options.onChunk) {
subscription = (
ExpoOndeviceAiModule as unknown as {
addListener: (
name: string,
listener: (chunk: TextStreamChunk) => void,
) => EventSubscription;
}
).addListener('onTranslateStreamChunk', (chunk: TextStreamChunk) => {
options.onChunk!(chunk);
});
}

const {onChunk: _, ...nativeOptions} = options;
const result: TranslateResult = await ExpoOndeviceAiModule.translateStreaming(
text,
nativeOptions,
);

await new Promise<void>((resolve) => setTimeout(resolve, 0));
return result;
} finally {
subscription?.remove();
}
}

/**
* Rewrite text with streaming — tokens delivered progressively via onChunk.
* @param text - The text to rewrite
* @param options - Options including outputType and onChunk callback
* @returns Promise resolving to final RewriteResult
*/
export async function rewriteStreaming(
text: string,
options: RewriteStreamOptions,
): Promise<RewriteResult> {
let subscription: EventSubscription | undefined;

try {
if (options.onChunk) {
subscription = (
ExpoOndeviceAiModule as unknown as {
addListener: (
name: string,
listener: (chunk: TextStreamChunk) => void,
) => EventSubscription;
}
).addListener('onRewriteStreamChunk', (chunk: TextStreamChunk) => {
options.onChunk!(chunk);
});
}

const {onChunk: _, ...nativeOptions} = options;
const result: RewriteResult = await ExpoOndeviceAiModule.rewriteStreaming(
text,
nativeOptions,
);

await new Promise<void>((resolve) => setTimeout(resolve, 0));
return result;
} finally {
subscription?.remove();
}
}
Comment thread
hyochan marked this conversation as resolved.

// MARK: - Image Description

/**
* Describe the contents of an image using on-device AI.
* Supported on iOS (Foundation Models Vision API) and Android.
* @param imageUri - URI or file path to the image
* @param options - Optional prompt and other options
*/
export async function describeImage(
imageUri: string,
options?: DescribeImageOptions,
): Promise<DescribeImageResult> {
return ExpoOndeviceAiModule.describeImage(imageUri, options);
}

// MARK: - Model Management

/**
Expand Down
56 changes: 56 additions & 0 deletions libraries/expo-ondevice-ai/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,62 @@ export interface ChatStreamOptions extends ChatOptions {
onChunk?: (chunk: ChatStreamChunk) => void;
}

/**
* A single streamed chunk for summarize / translate / rewrite streaming
*/
export interface TextStreamChunk {
/** New text delta in this chunk */
delta: string;
/** Full accumulated text so far */
accumulated: string;
/** Whether this is the final chunk */
isFinal: boolean;
}

/**
* Options for streaming summarization
*/
export interface SummarizeStreamOptions extends SummarizeOptions {
/** Callback invoked for each streamed token */
onChunk?: (chunk: TextStreamChunk) => void;
}

/**
* Options for streaming translation
*/
export interface TranslateStreamOptions extends TranslateOptions {
/** Callback invoked for each streamed token */
onChunk?: (chunk: TextStreamChunk) => void;
}

/**
* Options for streaming rewrite
*/
export interface RewriteStreamOptions extends RewriteOptions {
/** Callback invoked for each streamed token */
onChunk?: (chunk: TextStreamChunk) => void;
}

// MARK: - Image Description

/**
* Options for image description
*/
export interface DescribeImageOptions {
/** Optional prompt to guide the description */
prompt?: string;
}

/**
* Result of image description
*/
export interface DescribeImageResult {
/** The generated description */
description: string;
/** Confidence score (0-1) */
confidence?: number;
}

/**
Comment thread
hyochan marked this conversation as resolved.
* Options for translation
*/
Expand Down
Loading
Loading