@chipmobilesdk/rn-ai 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,118 @@
1
1
  # @chipmobilesdk/rn-ai
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - A 401 retry now gets a token the provider had to fetch again. `TokenProvider.requestAccessToken` takes `{ forceRefresh }`, which rn-auth implements by dropping the cached token for that resource, so the retry no longer re-sends the token the gateway had just refused — a cached token is still unexpired by the device's clock when the server rejects it. ([#19](https://github.com/operationd8979/chip-mobile-sdk/pull/19))
8
+
9
+ Failures an app can tell apart. `network` is a request that never arrived (no connection, a timeout, a dropped stream) and is retryable; `session-expired` is a session the user must renew. `AiError.cause` carries the code from the layer underneath — the gateway's own code, or rn-auth's — so a screen can explain *why*, and `retryAfterSeconds` is read from the `Retry-After` header when the body omits it. Both new codes have user copy, and the gateway's full code list is mapped rather than guessed from the status.
10
+
11
+ The remote behavior config is checked before it is trusted. `configUrl` and the document's `api.baseUrl` must be https (localhost excepted), the document must name the app it is for, its `schemaVersion` must be one this release understands, and `allowedHosts` pins where the client may send its bearer token. A broken TTL no longer means a revalidation on every request, and a retry count is bounded. A not-modified answer against a cache that is no longer there asks again without the ETag instead of reporting the 304 as a failure.
12
+
13
+ `configCacheFromKeyValue(port)` makes a durable config cache out of rn-core's `KeyValuePort`, so a second launch uses the last-good document instead of waiting for the network.
14
+
15
+ One quota figure: a chat's `done` event and `getQuota()` both record it on the client (`lastQuota()`), so `useAiQuota` and `useAiChat` cannot disagree, and a quota response that does not match the contract is refused rather than displayed. `getQuota()` recovers like a chat: a designated 404/410 reloads the configuration and retries, and a 401 retries once.
16
+
17
+ Streamed text reaches the screen on an interval (50 ms by default, `useAiChat(client, { flushIntervalMs })`) rather than once per token, and whatever arrived is flushed with the terminal state, so a half-answer stays on screen when a stream fails. A stream framed with CRLF is parsed incrementally, as the SSE specification allows. The transport seam is a published `StreamTransport` port instead of the internal reader's signature.
18
+
19
+ - The gateway↔client contract has one definition. `GatewayErrorCode`, `GATEWAY_ERROR_STATUS`, `QUOTA_HEADERS`, `GatewayErrorBody`, `GatewayStreamEvent`, `GatewayQuota`, `GatewayUsage` and `SSE_DONE_SENTINEL` are exported from this package, and the gateway service imports the same types, so an error code, a header name or a stream event added on one side does not compile until the other handles it. The three codes the gateway could already answer with and the client had never heard of — `BAD_REQUEST`, `ENDPOINT_RETIRED`, `INTERNAL` — now map to a typed error instead of a guess from the HTTP status. ([#19](https://github.com/operationd8979/chip-mobile-sdk/pull/19))
20
+
21
+ ### Patch Changes
22
+
23
+ - The gateway service (not published to npm) now shares one credential across the process and refuses unauthenticated requests before reading storage. No client-facing change; the entry is here so the release notes record the service work that went with this version. ([#19](https://github.com/operationd8979/chip-mobile-sdk/pull/19))
24
+
25
+ - Internal: no module in these packages is over 500 lines any more. The camera controller's session I/O moved to `sessionRuntime` and its contract to `contract.ts`; the Play adapter's shape translation to `iapShapes`; the purchase flow's failure mapping and its receipt-validation hook to modules of their own; the AI client's quota request to `quotaRequest`; the in-memory adapter's attachments and the fake ads engine's clock likewise. Behaviour is unchanged — the splits are by responsibility, and every package's suite passes as it stood. ([#19](https://github.com/operationd8979/chip-mobile-sdk/pull/19))
26
+
27
+ The repository also measures what it promised: `npm run perf` prints the median of nine runs of the deterministic hot paths against written budgets (recurrence expansion, theme construction, translation lookup, chart geometry, filter compilation), the demo app has a Performance screen reporting startup phases, time to interactive and the same benchmarks on Hermes, and `npm run check:file-size` keeps the limit from drifting. The budgets and the measured figures are recorded in `docs/standards.md` §VI.
28
+
29
+ ## 0.3.0
30
+
31
+ ### Minor Changes
32
+
33
+ - One diagnostic stream: install a sink with `installSdkSink` from
34
+ `@chipmobilesdk/rn-core` and it receives what every package observes (events,
35
+ failures, warnings) as `SdkDiagnostic`s, redacted. Listeners are notified through
36
+ rn-core, so a listener that throws no longer stops the others nor fails the call that
37
+ notified, and its error is reported rather than swallowed. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
38
+
39
+ - rn-ads: a banner's auto-refresh is a new cycle, so its `loaded` and `impression`
40
+ events are no longer deduplicated away; the dedupe memory is bounded. A throwing
41
+ status, consent or placement listener is reported as an `internal_error` event.
42
+ - rn-i18n: new `onDiagnostic` config receives every diagnostic (`I18nDiagnostic`,
43
+ with a stable `code`); without it, warnings go to `console.warn` in development
44
+ builds only. The warn-once memory is bounded.
45
+ - rn-chart: now depends on rn-core for its diagnostics.
46
+ - rn-payment: status, entitlement and pending listeners report a throw to the logger
47
+ instead of swallowing it.
48
+
49
+ - The package's errors extend `SdkError` from the new `@chipmobilesdk/rn-core`
50
+ dependency: `isSdkError` recognises them alongside every other package's, and each
51
+ carries `pkg`, `code` and `retryable`. Names and codes are unchanged. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
52
+
53
+ - rn-ai and rn-camera: `retryable` is the new name of `recoverable`, which stays as a
54
+ deprecated alias.
55
+ - rn-auth: `retryable` is true for `network-unavailable`. `redact` also removes URLs,
56
+ e-mail addresses and UUIDs.
57
+ - rn-ads: `redact` also removes JWTs and e-mail addresses, and reduces an `AdsError`
58
+ to its code.
59
+ - rn-ai: an error message quoted from the gateway is pattern-redacted.
60
+ - rn-camera: `redact` also removes JWTs, e-mail addresses and UUIDs.
61
+ - rn-theme: `ThemeConfigError` has `code: 'invalid_config'` and `ThemeContrastError`
62
+ `code: 'contrast'` (new `ThemeErrorCode` type).
63
+ - rn-i18n: `I18nConfigError` has `code: 'invalid_config'` and `UnitConversionError`
64
+ `code: 'unit_conversion'` (new `I18nErrorCode` type).
65
+
66
+ - Timers, retries and bounds now come from `@chipmobilesdk/rn-core`, and the drift
67
+ between the copies is fixed. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
68
+
69
+ - rn-ads: a retry policy out of bounds throws `retry_out_of_bounds`, as documented,
70
+ instead of `timeout_out_of_bounds`; a placement's `minRequestIntervalMs` out of
71
+ bounds throws `invalid_config`.
72
+ - rn-payment: backoff validates `initialDelayMs` (100 ms to 60 s, not above
73
+ `maxDelayMs`) and `factor` (1 to 10); `factor: 0` used to retry at once, forever.
74
+ Every timeout, backoff or interval out of bounds now throws the new
75
+ `PAYMENT_INVALID_CONFIG` instead of `PAYMENT_INVALID_CATALOG`, which is about
76
+ products.
77
+ - rn-ai: concurrent calls share one behavior-config request, so a cold start makes
78
+ one instead of one per call. A silent retry after a config reload waits
79
+ `recovery.retryBackoffMs`, which was parsed but never applied.
80
+
81
+ - Hooks read through `useSyncExternalStore` (rn-core's `useExternalStore`) and
82
+ re-render only when what they return changes. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
83
+
84
+ - rn-auth: `useProfile` and `useAccessTier` no longer re-render for a status change,
85
+ and the provider's context no longer changes on every transition. New
86
+ `useAuthSelector`. `auth.getState()` returns the same frozen object until the state
87
+ changes, instead of a copy per call.
88
+ - rn-payment: `useEntitlement` re-renders only when its own entitlement changes; the
89
+ engine keeps unchanged entitlement objects across syncs and does not notify for a
90
+ sync that changed nothing.
91
+ - rn-ads: `usePlacementState` and `useAdsStatus` read tear-free; `useAdsEvents` keeps
92
+ one subscription when given an inline listener.
93
+ - rn-i18n: `useTranslation` returns the same `t` until the locale changes, and
94
+ `useLocale`'s `setLocale` is stable.
95
+ - rn-ai: new `useAiBehaviorState`, with `client.subscribe` and a stable
96
+ `client.getBehaviorState()`.
97
+
98
+ - Every package's own copy is an rn-core message catalog that an app can translate. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
99
+
100
+ - rn-i18n: new `runtimeEntriesFromCatalog(catalog)` registers any SDK catalog in one
101
+ line, converting `{name}` variables to `{{name}}`.
102
+ - rn-ai: new `messageKeyForError(error)` gives the copy for every error code, and
103
+ `aiMessageSet`. The input placeholder default is neutral ("Type a message…"), and
104
+ two keys were added for errors that had none (`ai.error.signIn`, `ai.error.generic`).
105
+ - rn-auth: new `authMessages`, the catalog split into user and developer copy.
106
+ - rn-payment: new `paymentMessages`.
107
+ - rn-chart: summaries, announcements, adjustable-action labels, built-in state text
108
+ and legend hints come from `chartStrings`; pass `strings` to translate them. The
109
+ `accessibility.summaryFormatter` prop, declared before, now replaces the summary.
110
+
111
+ ### Patch Changes
112
+
113
+ - Updated dependencies ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17), [#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17), [#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17)):
114
+ - @chipmobilesdk/rn-core@0.1.0
115
+
3
116
  ## 0.2.1
4
117
 
5
118
  ### Patch Changes
package/README.md CHANGED
@@ -12,9 +12,12 @@ OpenAI directly and hold no AI credentials.
12
12
  - **Entrypoint**: `@chipmobilesdk/rn-ai`
13
13
  - **Native code**: none
14
14
 
15
- - Contract: [rn-ai-public-api](../../specs/005-azure-ai-gateway/contracts/rn-ai-public-api.md)
16
- - Gateway API: [gateway-api](../../specs/005-azure-ai-gateway/contracts/gateway-api.md)
17
- - Behavior config: [app-behavior-config](../../specs/005-azure-ai-gateway/contracts/app-behavior-config.md)
15
+ - **Gateway contract**: the error codes, quota headers and stream events this
16
+ client and the gateway share are exported from this package
17
+ (`GatewayErrorCode`, `QUOTA_HEADERS`, `GatewayStreamEvent`), and the gateway
18
+ imports the same definitions — see `## Public API`. The feature's original
19
+ contract documents live in the repository under
20
+ `specs/005-azure-ai-gateway/legacy/contracts/` and are not shipped here.
18
21
 
19
22
  ## Install
20
23
 
@@ -39,7 +42,10 @@ resources: [
39
42
  2. Create the client (one per app):
40
43
 
41
44
  ```ts
42
- import { createAiClient } from '@chipmobilesdk/rn-ai';
45
+ import { configCacheFromKeyValue, createAiClient } from '@chipmobilesdk/rn-ai';
46
+ import { openDurable } from '@chipmobilesdk/rn-core';
47
+ import { createMMKV } from 'react-native-mmkv';
48
+
43
49
  import { auth } from './auth/setup';
44
50
 
45
51
  export const aiClient = createAiClient({
@@ -47,10 +53,21 @@ export const aiClient = createAiClient({
47
53
  configUrl: 'https://<config-host>/appconfig/config/scan-food.json',
48
54
  resourceKey: 'ai-gateway',
49
55
  auth,
50
- // storage: inject a persistent AiConfigCache for offline launches
56
+ // Cache the behavior config where it survives a restart, so the next launch
57
+ // does not wait for the network. Any `AiConfigCache` will do; this one is
58
+ // rn-core's key-value port over MMKV.
59
+ storage: configCacheFromKeyValue(openDurable(() => createMMKV({ id: 'app.ai' }))),
60
+ // Optional: pin the hosts the remote config may point the client at.
61
+ allowedHosts: ['gw.example.com'],
51
62
  });
52
63
  ```
53
64
 
65
+ The config document decides where this client sends its bearer token, so both
66
+ `configUrl` and the document's `api.baseUrl` must be `https` (plain `http` is
67
+ accepted only for `localhost`), the document must name this `appId`, and its
68
+ `schemaVersion` must be one this release understands. Anything else is a
69
+ `config-unavailable` error rather than a request to an unvetted host.
70
+
54
71
  3. Use the hooks:
55
72
 
56
73
  ```tsx
@@ -77,32 +94,52 @@ send({
77
94
  request. A fresh cache entry is used immediately; a stale entry is used
78
95
  immediately and revalidated in the background with its ETag. With no cache,
79
96
  the first operation waits for the config request, for at most `configMs`.
80
- Designated `404`/`410`
81
- responses invalidate config and retry against the refreshed endpoint.
82
- - **Auth**: acquires tokens via rn-auth (`requestAccessToken('ai-gateway')`);
83
- on `401`, reacquires a token and retries once.
97
+ Concurrent calls share one config request, so a cold start that asks for a
98
+ chat and the quota together fetches the config once. Designated `404`/`410`
99
+ responses invalidate config and retry against the refreshed endpoint, after
100
+ waiting `recovery.retryBackoffMs[n]` (the last step repeats, each capped at
101
+ 30 s), at most `recovery.maxSilentRetries` times. A chat cancelled during the
102
+ wait is not retried.
103
+ - **Auth**: acquires tokens via rn-auth
104
+ (`requestAccessToken('ai-gateway')`); on `401` it asks for a token the
105
+ provider had to fetch again (`{ forceRefresh: true }`) and retries once. The
106
+ cached token is unexpired by the device's clock after a 401, so without that
107
+ the retry would re-send the token the gateway just refused.
108
+ - **Quota**: one figure, wherever it came from. A chat's `done` event and
109
+ `getQuota()` both record it on the client, so `useAiQuota` and `useAiChat`
110
+ never disagree, and a quota display does not go stale when a chat finishes.
111
+ `getQuota()` recovers like a chat does: a designated `404`/`410` reloads the
112
+ configuration and retries, and a `401` retries once with a fresh token.
84
113
  - **Timeouts**: every network call is bounded. Override any of them with
85
114
  `createAiClient({ timeouts: { ... } })`:
86
115
 
87
116
  | | Default | Fails with |
88
117
  |---|---|---|
89
- | `connectMs` — chat request sent → headers | 30 s | `upstream-error`, `recoverable: true` |
90
- | `idleMs` — silence between two stream chunks | 30 s | `upstream-error`, `recoverable: true` |
118
+ | `connectMs` — chat request sent → headers | 30 s | `network`, `retryable: true` |
119
+ | `idleMs` — silence between two stream chunks | 30 s | `network`, `retryable: true` |
91
120
  | `configMs` — behavior-config fetch | 10 s | stale cache if any, else `config-unavailable` |
92
- | `quotaMs` — quota fetch | 10 s | `upstream-error`, `recoverable: true` |
121
+ | `quotaMs` — quota fetch | 10 s | `network`, `retryable: true` |
93
122
 
94
123
  The gateway sends headers only after the model answers, so `connectMs` also
95
124
  covers time to first token. A dropped connection (HTTP status 0) is likewise
96
- `upstream-error` with `recoverable: true`.
125
+ `network` with `retryable: true`: the request never reached the service, which
126
+ is a different thing to tell the user than a service that failed.
97
127
  - **Callbacks**: each `chat()` delivers exactly one of `onDone` / `onError`,
98
128
  and nothing after `cancel()`. An unexpected exception anywhere on the way
99
129
  (including in your own `onDelta`) is reported as `internal`, never dropped.
100
130
  - **Rate limits**: `429` surfaces as `status: 'rate-limited'` with `retryAt`;
101
131
  the SDK never hammers and never enforces limits locally (display only).
102
- - **Typed errors** (`AiError.code`): `auth-invalid`, `not-entitled`,
103
- `user-blocked`, `app-unknown`, `rate-limited`, `upstream-busy`,
104
- `payload-too-large`, `modality-not-allowed`, `config-unavailable`,
105
- `upstream-error`, `internal`.
132
+ - **Streamed text** reaches the screen on an interval (50 ms by default), not
133
+ once per token: `useAiChat(client, { flushIntervalMs })` tunes it, and `0`
134
+ renders per token. Whatever had arrived is always flushed with the terminal
135
+ state, so a half-answer stays on screen when a stream fails.
136
+ - **Typed errors** (`AiError.code`): `auth-invalid`, `session-expired`,
137
+ `not-entitled`, `user-blocked`, `app-unknown`, `rate-limited`,
138
+ `upstream-busy`, `payload-too-large`, `modality-not-allowed`,
139
+ `config-unavailable`, `network`, `upstream-error`, `internal`.
140
+ `AiError.cause` carries the code the layer underneath used — the gateway's own
141
+ code, or rn-auth's — so a screen can say *why*. `retryAfterSeconds` is read
142
+ from the body, or from the `Retry-After` header when the body omits it.
106
143
  - **Copy keys**: `ai.*` defaults in `aiMessages` — map through your i18n.
107
144
 
108
145
  ## Public API
@@ -110,15 +147,28 @@ send({
110
147
  Everything is exported from `@chipmobilesdk/rn-ai`; internal `config/`,
111
148
  `transport/`, `core/`, `react/`, and `diagnostics/` paths are not public.
112
149
 
113
- - Client: `createAiClient`, `AiClient`, `AiClientConfig`, `TokenProvider`
150
+ - Client: `createAiClient`, `AiClient`, `AiClientConfig`, `TokenProvider`,
151
+ `AccessTokenRequestOptions`
114
152
  - Chat: `ChatRequest`, `ChatMessage`, `ContentPart`, `ChatStreamHandlers`,
115
153
  `ChatHandle`, `Usage`
116
154
  - Behavior and cache: `AppBehaviorConfig`, `AiBehaviorState`, `BehaviorState`,
117
- `AiConfigCache`, `QuotaSnapshot`
118
- - React: `useAiChat`, `useAiQuota`, `AiChatStatus`, `UseAiChatResult`,
119
- `UseAiQuotaResult`
155
+ `AiConfigCache`, `configCacheFromKeyValue`, `SUPPORTED_SCHEMA_VERSION`,
156
+ `QuotaSnapshot`
157
+ - Transport: `StreamTransport` and its `SseRequest` / `SseHandlers` /
158
+ `SseHandle` / `SseResponseInfo` types. `createAiClient({ transport })` takes
159
+ any implementation; the default is an XHR reader.
160
+ - The gateway contract, shared with the service so both sides cannot drift:
161
+ `GatewayErrorCode`, `GatewayErrorBody`, `GatewayQuota`, `GatewayUsage`,
162
+ `GatewayStreamEvent`, `GATEWAY_ERROR_STATUS`, `QUOTA_HEADERS`,
163
+ `SSE_DONE_SENTINEL`
164
+ - React: `useAiChat`, `useAiQuota`, `useAiBehaviorState`, `AiChatStatus`,
165
+ `UseAiChatOptions`, `UseAiChatResult`, `UseAiQuotaResult`. `useAiBehaviorState(client)` re-renders when
166
+ the config loads or the `aiEnabled` kill switch flips; `client.subscribe` and
167
+ `client.getBehaviorState()` give the same outside React.
120
168
  - Diagnostics and copy: `AiError`, `AiErrorCode`, `isAiError`, `aiMessages`,
121
- `aiMessage`, `AiMessageKey`
169
+ `aiMessage`, `AiMessageKey`, `aiMessageSet` (the same catalog as an rn-core message
170
+ set), `messageKeyForError(error)` (the copy to show for any error code). Register the
171
+ copy with rn-i18n in one line: `runtimeEntriesFromCatalog(aiMessageSet.user)`.
122
172
 
123
173
  ## App responsibilities
124
174
 
@@ -136,10 +186,6 @@ Everything is exported from `@chipmobilesdk/rn-ai`; internal `config/`,
136
186
  React ≥19 · React Native ≥0.85 (Hermes) · iOS + Android · Node ≥22.11 for
137
187
  tooling.
138
188
 
139
- The repository contains `src/screens/AiDemoScreen.tsx` and its component test.
140
- The current demo sidebar does not expose that screen yet, so it must be wired
141
- into `ScreenKey`, `Sidebar`, and `ActiveScreen` before it is reachable in-app.
142
-
143
189
  ## Validation
144
190
 
145
191
  Run from the repository root:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chipmobilesdk/rn-ai",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "main": "src/index.ts",
@@ -21,9 +21,12 @@
21
21
  ],
22
22
  "sideEffects": false,
23
23
  "scripts": {
24
- "typecheck": "tsc -p tsconfig.json --noEmit",
24
+ "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.test.json",
25
25
  "pack:check": "npm pack --dry-run"
26
26
  },
27
+ "dependencies": {
28
+ "@chipmobilesdk/rn-core": "^0.1.0"
29
+ },
27
30
  "peerDependencies": {
28
31
  "@chipmobilesdk/rn-auth": ">=0.2.0",
29
32
  "react": ">=19",
@@ -31,18 +31,49 @@ read it before assuming a behaviour is built in.
31
31
  screen unmounts, otherwise the response resolves against a dead component. Verify
32
32
  teardown on every screen that uses it.
33
33
 
34
+ ## Cache the config, or every launch waits for the network
35
+
36
+ `createAiClient({ storage })` takes an `AiConfigCache`. Without one the behavior config
37
+ lives in memory only, so each cold start blocks the first chat on a network fetch.
38
+ `configCacheFromKeyValue(port)` adapts rn-core's `KeyValuePort` (MMKV through
39
+ `openDurable`), which is how the demo app wires it.
40
+
41
+ ## What the config document is allowed to say
42
+
43
+ `configUrl` and the document's `api.baseUrl` must be https (localhost excepted), the
44
+ document must name the same `appId`, and its `schemaVersion` must be one the installed
45
+ release supports. `allowedHosts` pins the hosts the document may point at. A document
46
+ that breaks any of these is a `config-unavailable` error, not a request sent anyway:
47
+ this is where the app's bearer token would go.
48
+
49
+ ## Errors carry a reason
50
+
51
+ `AiError.code` is what happened; `AiError.cause` is the code underneath (the gateway's,
52
+ or rn-auth's). `network` means the request never arrived and a retry may work;
53
+ `session-expired` means sign-in is needed; `not-entitled` means premium. Use
54
+ `messageKeyForError(error)` for the copy — one key per code, nothing to map by hand.
55
+
34
56
  ## Public API
35
57
 
36
58
  Runtime:
37
59
 
38
- - `createAiClient(...)` — transport-level client
39
- - `useAiChat(...)` → `UseAiChatResult`, with `AiChatStatus` for state
40
- - `useAiQuota(...)` → `UseAiQuotaResult`
60
+ - `createAiClient(...)` — the client: `chat`, `getQuota`, `lastQuota`, `reloadConfig`,
61
+ `getBehaviorState`, `subscribe`
62
+ - `useAiChat(client, { flushIntervalMs })` → `UseAiChatResult` (`send`, `cancel`,
63
+ `status`, `text`, `error`, `retryAt`, `quota`), with `AiChatStatus` for state
64
+ - `useAiQuota(...)` → `UseAiQuotaResult`; `useAiBehaviorState(...)` for the kill switch
65
+ - `configCacheFromKeyValue(port)`, `SUPPORTED_SCHEMA_VERSION`
41
66
  - `AiError`, `isAiError` — narrow before reading `AiErrorCode`
42
- - `aiMessages`, `aiMessage` — user-facing strings keyed by `AiMessageKey`
67
+ - `aiMessages`, `aiMessage`, `aiMessageSet`, `messageKeyForError`
68
+ - `GATEWAY_ERROR_STATUS`, `QUOTA_HEADERS`, `SSE_DONE_SENTINEL` — the gateway contract
43
69
 
44
70
  Use `aiMessage` for anything shown to the user. Never render `AiError.message` directly.
45
71
 
72
+ ## Streamed text is batched
73
+
74
+ `useAiChat` flushes accumulated deltas every 50 ms rather than rendering per token; pass
75
+ `flushIntervalMs: 0` for per-token rendering. Do not add your own buffering on top.
76
+
46
77
  ## Where the detail lives
47
78
 
48
79
  Read one section of `node_modules/@chipmobilesdk/rn-ai/README.md`: