@chipmobilesdk/rn-ai 0.2.0 → 0.3.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 +158 -55
- package/README.md +159 -151
- package/package.json +39 -43
- package/skills/sdk-rn-ai/SKILL.md +56 -57
- package/src/config/behaviorConfig.ts +233 -200
- package/src/core/aiClient.ts +354 -281
- package/src/diagnostics/errors.ts +89 -75
- package/src/diagnostics/messageKeys.ts +63 -27
- package/src/index.ts +31 -35
- package/src/react/hooks.ts +141 -134
- package/src/transport/sseClient.ts +143 -143
- package/src/transport/timeouts.ts +53 -53
package/CHANGELOG.md
CHANGED
|
@@ -1,55 +1,158 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
##
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
`
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
1
|
+
# @chipmobilesdk/rn-ai
|
|
2
|
+
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- One diagnostic stream: install a sink with `installSdkSink` from
|
|
8
|
+
`@chipmobilesdk/rn-core` and it receives what every package observes (events,
|
|
9
|
+
failures, warnings) as `SdkDiagnostic`s, redacted. Listeners are notified through
|
|
10
|
+
rn-core, so a listener that throws no longer stops the others nor fails the call that
|
|
11
|
+
notified, and its error is reported rather than swallowed. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
|
|
12
|
+
|
|
13
|
+
- rn-ads: a banner's auto-refresh is a new cycle, so its `loaded` and `impression`
|
|
14
|
+
events are no longer deduplicated away; the dedupe memory is bounded. A throwing
|
|
15
|
+
status, consent or placement listener is reported as an `internal_error` event.
|
|
16
|
+
- rn-i18n: new `onDiagnostic` config receives every diagnostic (`I18nDiagnostic`,
|
|
17
|
+
with a stable `code`); without it, warnings go to `console.warn` in development
|
|
18
|
+
builds only. The warn-once memory is bounded.
|
|
19
|
+
- rn-chart: now depends on rn-core for its diagnostics.
|
|
20
|
+
- rn-payment: status, entitlement and pending listeners report a throw to the logger
|
|
21
|
+
instead of swallowing it.
|
|
22
|
+
|
|
23
|
+
- The package's errors extend `SdkError` from the new `@chipmobilesdk/rn-core`
|
|
24
|
+
dependency: `isSdkError` recognises them alongside every other package's, and each
|
|
25
|
+
carries `pkg`, `code` and `retryable`. Names and codes are unchanged. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
|
|
26
|
+
|
|
27
|
+
- rn-ai and rn-camera: `retryable` is the new name of `recoverable`, which stays as a
|
|
28
|
+
deprecated alias.
|
|
29
|
+
- rn-auth: `retryable` is true for `network-unavailable`. `redact` also removes URLs,
|
|
30
|
+
e-mail addresses and UUIDs.
|
|
31
|
+
- rn-ads: `redact` also removes JWTs and e-mail addresses, and reduces an `AdsError`
|
|
32
|
+
to its code.
|
|
33
|
+
- rn-ai: an error message quoted from the gateway is pattern-redacted.
|
|
34
|
+
- rn-camera: `redact` also removes JWTs, e-mail addresses and UUIDs.
|
|
35
|
+
- rn-theme: `ThemeConfigError` has `code: 'invalid_config'` and `ThemeContrastError`
|
|
36
|
+
`code: 'contrast'` (new `ThemeErrorCode` type).
|
|
37
|
+
- rn-i18n: `I18nConfigError` has `code: 'invalid_config'` and `UnitConversionError`
|
|
38
|
+
`code: 'unit_conversion'` (new `I18nErrorCode` type).
|
|
39
|
+
|
|
40
|
+
- Timers, retries and bounds now come from `@chipmobilesdk/rn-core`, and the drift
|
|
41
|
+
between the copies is fixed. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
|
|
42
|
+
|
|
43
|
+
- rn-ads: a retry policy out of bounds throws `retry_out_of_bounds`, as documented,
|
|
44
|
+
instead of `timeout_out_of_bounds`; a placement's `minRequestIntervalMs` out of
|
|
45
|
+
bounds throws `invalid_config`.
|
|
46
|
+
- rn-payment: backoff validates `initialDelayMs` (100 ms to 60 s, not above
|
|
47
|
+
`maxDelayMs`) and `factor` (1 to 10); `factor: 0` used to retry at once, forever.
|
|
48
|
+
Every timeout, backoff or interval out of bounds now throws the new
|
|
49
|
+
`PAYMENT_INVALID_CONFIG` instead of `PAYMENT_INVALID_CATALOG`, which is about
|
|
50
|
+
products.
|
|
51
|
+
- rn-ai: concurrent calls share one behavior-config request, so a cold start makes
|
|
52
|
+
one instead of one per call. A silent retry after a config reload waits
|
|
53
|
+
`recovery.retryBackoffMs`, which was parsed but never applied.
|
|
54
|
+
|
|
55
|
+
- Hooks read through `useSyncExternalStore` (rn-core's `useExternalStore`) and
|
|
56
|
+
re-render only when what they return changes. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
|
|
57
|
+
|
|
58
|
+
- rn-auth: `useProfile` and `useAccessTier` no longer re-render for a status change,
|
|
59
|
+
and the provider's context no longer changes on every transition. New
|
|
60
|
+
`useAuthSelector`. `auth.getState()` returns the same frozen object until the state
|
|
61
|
+
changes, instead of a copy per call.
|
|
62
|
+
- rn-payment: `useEntitlement` re-renders only when its own entitlement changes; the
|
|
63
|
+
engine keeps unchanged entitlement objects across syncs and does not notify for a
|
|
64
|
+
sync that changed nothing.
|
|
65
|
+
- rn-ads: `usePlacementState` and `useAdsStatus` read tear-free; `useAdsEvents` keeps
|
|
66
|
+
one subscription when given an inline listener.
|
|
67
|
+
- rn-i18n: `useTranslation` returns the same `t` until the locale changes, and
|
|
68
|
+
`useLocale`'s `setLocale` is stable.
|
|
69
|
+
- rn-ai: new `useAiBehaviorState`, with `client.subscribe` and a stable
|
|
70
|
+
`client.getBehaviorState()`.
|
|
71
|
+
|
|
72
|
+
- 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))
|
|
73
|
+
|
|
74
|
+
- rn-i18n: new `runtimeEntriesFromCatalog(catalog)` registers any SDK catalog in one
|
|
75
|
+
line, converting `{name}` variables to `{{name}}`.
|
|
76
|
+
- rn-ai: new `messageKeyForError(error)` gives the copy for every error code, and
|
|
77
|
+
`aiMessageSet`. The input placeholder default is neutral ("Type a message…"), and
|
|
78
|
+
two keys were added for errors that had none (`ai.error.signIn`, `ai.error.generic`).
|
|
79
|
+
- rn-auth: new `authMessages`, the catalog split into user and developer copy.
|
|
80
|
+
- rn-payment: new `paymentMessages`.
|
|
81
|
+
- rn-chart: summaries, announcements, adjustable-action labels, built-in state text
|
|
82
|
+
and legend hints come from `chartStrings`; pass `strings` to translate them. The
|
|
83
|
+
`accessibility.summaryFormatter` prop, declared before, now replaces the summary.
|
|
84
|
+
|
|
85
|
+
### Patch Changes
|
|
86
|
+
|
|
87
|
+
- 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)):
|
|
88
|
+
- @chipmobilesdk/rn-core@0.1.0
|
|
89
|
+
|
|
90
|
+
## 0.2.1
|
|
91
|
+
|
|
92
|
+
### Patch Changes
|
|
93
|
+
|
|
94
|
+
- Packaging only; no code change. The package no longer declares `engines.node`: a
|
|
95
|
+
React Native package does not run on Node, so the field only produced install
|
|
96
|
+
warnings in apps on another Node version. rn-i18n's command-line tooling does run
|
|
97
|
+
on Node; its README's compatibility table states the version it needs. ([#14](https://github.com/operationd8979/chip-mobile-sdk/pull/14))
|
|
98
|
+
|
|
99
|
+
## [0.2.0] — 2026-11-01
|
|
100
|
+
|
|
101
|
+
Fixes a chat or quota call that could hang forever, and errors that never
|
|
102
|
+
reached the app. **No code change is required.**
|
|
103
|
+
|
|
104
|
+
### Fixed
|
|
105
|
+
|
|
106
|
+
- **Every network call has a time limit.** The chat stream had none, and
|
|
107
|
+
neither did the config or quota fetch, so on a stalled mobile connection
|
|
108
|
+
`useAiChat` stayed in `loading`/`streaming` and `useAiQuota` in `loading`
|
|
109
|
+
until the user gave up — and with no cached config, one hung fetch blocked
|
|
110
|
+
every call. New `timeouts: { connectMs, idleMs, configMs, quotaMs }` option
|
|
111
|
+
(defaults 30 s, 30 s, 10 s, 10 s). The idle limit restarts on every chunk.
|
|
112
|
+
- **No error is swallowed.** Every chat path ended in `.catch(() => undefined)`,
|
|
113
|
+
so a transport that threw (an invalid URL from remote config, for example)
|
|
114
|
+
left the UI in `loading` with nothing to show. Such failures now arrive as
|
|
115
|
+
`AiError('internal')`. The remote `recovery` block is type-checked when
|
|
116
|
+
parsed, so a mistyped value can no longer throw inside the XHR callback.
|
|
117
|
+
- **A cancelled or replaced request stays silent.** A token or config failure
|
|
118
|
+
that landed after `cancel()` (or after the next `send()`) still called
|
|
119
|
+
`onError` and flipped that screen to `error`. The client now routes every
|
|
120
|
+
callback through one guard (nothing after cancel, exactly one of
|
|
121
|
+
`onDone`/`onError`), and `useAiChat` and `useAiQuota` drop callbacks from
|
|
122
|
+
older requests.
|
|
123
|
+
|
|
124
|
+
### Changed
|
|
125
|
+
|
|
126
|
+
- A stalled stream, a timed-out quota fetch and a dropped connection (status 0)
|
|
127
|
+
report `upstream-error` with `recoverable: true`; status 0 was
|
|
128
|
+
`recoverable: false`.
|
|
129
|
+
|
|
130
|
+
### Added
|
|
131
|
+
|
|
132
|
+
- `AiClientConfig.timeouts` and the `AiTimeouts` type.
|
|
133
|
+
|
|
134
|
+
## 0.1.1 — 2026-08-01
|
|
135
|
+
|
|
136
|
+
### Changed
|
|
137
|
+
|
|
138
|
+
- The package now ships its agent skill (`skills/`), which consuming apps
|
|
139
|
+
copy in with `scripts/sync-sdk-skills.mjs`. No code change.
|
|
140
|
+
|
|
141
|
+
## 0.1.0 — 2026-07-05
|
|
142
|
+
|
|
143
|
+
Initial release (feature `005-azure-ai-gateway`).
|
|
144
|
+
|
|
145
|
+
- `createAiClient` — config-driven gateway client: cache-first app behavior
|
|
146
|
+
config (ETag/TTL), token acquisition via `@chipmobilesdk/rn-auth`
|
|
147
|
+
(`resourceKey`), SSE streaming over zero-dependency XHR transport.
|
|
148
|
+
- Multimodal chat requests: `text`, `image` (base64), `audio` (wav/mp3).
|
|
149
|
+
- Silent endpoint re-route on designated `404`/`410` signals (max 1 retry);
|
|
150
|
+
one automatic fresh-token retry on `401`; no automatic retry on `429`.
|
|
151
|
+
- `useAiChat` (idle/loading/streaming/done/error/rate-limited + accumulated
|
|
152
|
+
text + `retryAt`) and `useAiQuota` hooks.
|
|
153
|
+
- Typed `AiError` codes and `ai.*` message keys with English defaults.
|
|
154
|
+
- Per-app kill switch honored (`features.aiEnabled`).
|
|
155
|
+
|
|
156
|
+
Adoption: additive — no changes required in existing apps. Requires an
|
|
157
|
+
`ai-gateway` resource entry in the rn-auth config and a registered app on the
|
|
158
|
+
gateway platform.
|
package/README.md
CHANGED
|
@@ -1,151 +1,159 @@
|
|
|
1
|
-
# @chipmobilesdk/rn-ai
|
|
2
|
-
|
|
3
|
-
AI gateway client for ChipMobileSdk-family React Native apps. Streams
|
|
4
|
-
multimodal (text + image + audio in, text out) AI responses through the
|
|
5
|
-
platform gateway with per-user rate limiting — apps never talk to Azure
|
|
6
|
-
OpenAI directly and hold no AI credentials.
|
|
7
|
-
|
|
8
|
-
> **License:** `UNLICENSED`. Published publicly for use by the owner's
|
|
9
|
-
> applications; no open-source license is granted.
|
|
10
|
-
|
|
11
|
-
- **Version**:
|
|
12
|
-
- **Entrypoint**: `@chipmobilesdk/rn-ai`
|
|
13
|
-
- **Native code**: none
|
|
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)
|
|
18
|
-
|
|
19
|
-
## Install
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
npm install @chipmobilesdk/rn-ai @chipmobilesdk/rn-auth
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
Peers: `react >=19`, `react-native >=0.85`, `@chipmobilesdk/rn-auth >=0.2.0`.
|
|
26
|
-
**Zero runtime dependencies**; streaming uses React Native's standard
|
|
27
|
-
`XMLHttpRequest` (no native code, Hermes-compatible, iOS + Android).
|
|
28
|
-
|
|
29
|
-
## Setup
|
|
30
|
-
|
|
31
|
-
1. Add the gateway resource to your rn-auth config:
|
|
32
|
-
|
|
33
|
-
```ts
|
|
34
|
-
resources: [
|
|
35
|
-
{ key: 'ai-gateway', scopes: ['api://aiapp-gateway/AI.Access'] },
|
|
36
|
-
]
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
2. Create the client (one per app):
|
|
40
|
-
|
|
41
|
-
```ts
|
|
42
|
-
import { createAiClient } from '@chipmobilesdk/rn-ai';
|
|
43
|
-
import { auth } from './auth/setup';
|
|
44
|
-
|
|
45
|
-
export const aiClient = createAiClient({
|
|
46
|
-
appId: 'scan-food',
|
|
47
|
-
configUrl: 'https://<config-host>/appconfig/config/scan-food.json',
|
|
48
|
-
resourceKey: 'ai-gateway',
|
|
49
|
-
auth,
|
|
50
|
-
// storage: inject a persistent AiConfigCache for offline launches
|
|
51
|
-
});
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
3. Use the hooks:
|
|
55
|
-
|
|
56
|
-
```tsx
|
|
57
|
-
import { useAiChat, useAiQuota } from '@chipmobilesdk/rn-ai';
|
|
58
|
-
import { aiClient } from './ai/setup';
|
|
59
|
-
|
|
60
|
-
const { send, cancel, status, text, error, retryAt } = useAiChat(aiClient);
|
|
61
|
-
const { quota } = useAiQuota(aiClient);
|
|
62
|
-
|
|
63
|
-
send({
|
|
64
|
-
messages: [{
|
|
65
|
-
role: 'user',
|
|
66
|
-
content: [
|
|
67
|
-
{ type: 'text', text: 'What dish is this? Estimate calories.' },
|
|
68
|
-
{ type: 'image', mimeType: 'image/jpeg', dataBase64: photoBase64 },
|
|
69
|
-
],
|
|
70
|
-
}],
|
|
71
|
-
});
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## Runtime behavior
|
|
75
|
-
|
|
76
|
-
- **Config-driven endpoint**: resolved lazily on the first quota or chat
|
|
77
|
-
request. A fresh cache entry is used immediately; a stale entry is used
|
|
78
|
-
immediately and revalidated in the background with its ETag. With no cache,
|
|
79
|
-
the first operation waits for the config request, for at most `configMs`.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
`upstream-error
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
- **
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
-
|
|
119
|
-
`
|
|
120
|
-
-
|
|
121
|
-
`
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
1
|
+
# @chipmobilesdk/rn-ai
|
|
2
|
+
|
|
3
|
+
AI gateway client for ChipMobileSdk-family React Native apps. Streams
|
|
4
|
+
multimodal (text + image + audio in, text out) AI responses through the
|
|
5
|
+
platform gateway with per-user rate limiting — apps never talk to Azure
|
|
6
|
+
OpenAI directly and hold no AI credentials.
|
|
7
|
+
|
|
8
|
+
> **License:** `UNLICENSED`. Published publicly for use by the owner's
|
|
9
|
+
> applications; no open-source license is granted.
|
|
10
|
+
|
|
11
|
+
- **Version**: see [CHANGELOG](CHANGELOG.md)
|
|
12
|
+
- **Entrypoint**: `@chipmobilesdk/rn-ai`
|
|
13
|
+
- **Native code**: none
|
|
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)
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @chipmobilesdk/rn-ai @chipmobilesdk/rn-auth
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Peers: `react >=19`, `react-native >=0.85`, `@chipmobilesdk/rn-auth >=0.2.0`.
|
|
26
|
+
**Zero runtime dependencies**; streaming uses React Native's standard
|
|
27
|
+
`XMLHttpRequest` (no native code, Hermes-compatible, iOS + Android).
|
|
28
|
+
|
|
29
|
+
## Setup
|
|
30
|
+
|
|
31
|
+
1. Add the gateway resource to your rn-auth config:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
resources: [
|
|
35
|
+
{ key: 'ai-gateway', scopes: ['api://aiapp-gateway/AI.Access'] },
|
|
36
|
+
]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
2. Create the client (one per app):
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { createAiClient } from '@chipmobilesdk/rn-ai';
|
|
43
|
+
import { auth } from './auth/setup';
|
|
44
|
+
|
|
45
|
+
export const aiClient = createAiClient({
|
|
46
|
+
appId: 'scan-food',
|
|
47
|
+
configUrl: 'https://<config-host>/appconfig/config/scan-food.json',
|
|
48
|
+
resourceKey: 'ai-gateway',
|
|
49
|
+
auth,
|
|
50
|
+
// storage: inject a persistent AiConfigCache for offline launches
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
3. Use the hooks:
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
import { useAiChat, useAiQuota } from '@chipmobilesdk/rn-ai';
|
|
58
|
+
import { aiClient } from './ai/setup';
|
|
59
|
+
|
|
60
|
+
const { send, cancel, status, text, error, retryAt } = useAiChat(aiClient);
|
|
61
|
+
const { quota } = useAiQuota(aiClient);
|
|
62
|
+
|
|
63
|
+
send({
|
|
64
|
+
messages: [{
|
|
65
|
+
role: 'user',
|
|
66
|
+
content: [
|
|
67
|
+
{ type: 'text', text: 'What dish is this? Estimate calories.' },
|
|
68
|
+
{ type: 'image', mimeType: 'image/jpeg', dataBase64: photoBase64 },
|
|
69
|
+
],
|
|
70
|
+
}],
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Runtime behavior
|
|
75
|
+
|
|
76
|
+
- **Config-driven endpoint**: resolved lazily on the first quota or chat
|
|
77
|
+
request. A fresh cache entry is used immediately; a stale entry is used
|
|
78
|
+
immediately and revalidated in the background with its ETag. With no cache,
|
|
79
|
+
the first operation waits for the config request, for at most `configMs`.
|
|
80
|
+
Concurrent calls share one config request, so a cold start that asks for a
|
|
81
|
+
chat and the quota together fetches the config once. Designated `404`/`410`
|
|
82
|
+
responses invalidate config and retry against the refreshed endpoint, after
|
|
83
|
+
waiting `recovery.retryBackoffMs[n]` (the last step repeats, each capped at
|
|
84
|
+
30 s), at most `recovery.maxSilentRetries` times. A chat cancelled during the
|
|
85
|
+
wait is not retried.
|
|
86
|
+
- **Auth**: acquires tokens via rn-auth (`requestAccessToken('ai-gateway')`);
|
|
87
|
+
on `401`, reacquires a token and retries once.
|
|
88
|
+
- **Timeouts**: every network call is bounded. Override any of them with
|
|
89
|
+
`createAiClient({ timeouts: { ... } })`:
|
|
90
|
+
|
|
91
|
+
| | Default | Fails with |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| `connectMs` — chat request sent → headers | 30 s | `upstream-error`, `retryable: true` |
|
|
94
|
+
| `idleMs` — silence between two stream chunks | 30 s | `upstream-error`, `retryable: true` |
|
|
95
|
+
| `configMs` — behavior-config fetch | 10 s | stale cache if any, else `config-unavailable` |
|
|
96
|
+
| `quotaMs` — quota fetch | 10 s | `upstream-error`, `retryable: true` |
|
|
97
|
+
|
|
98
|
+
The gateway sends headers only after the model answers, so `connectMs` also
|
|
99
|
+
covers time to first token. A dropped connection (HTTP status 0) is likewise
|
|
100
|
+
`upstream-error` with `retryable: true`.
|
|
101
|
+
- **Callbacks**: each `chat()` delivers exactly one of `onDone` / `onError`,
|
|
102
|
+
and nothing after `cancel()`. An unexpected exception anywhere on the way
|
|
103
|
+
(including in your own `onDelta`) is reported as `internal`, never dropped.
|
|
104
|
+
- **Rate limits**: `429` surfaces as `status: 'rate-limited'` with `retryAt`;
|
|
105
|
+
the SDK never hammers and never enforces limits locally (display only).
|
|
106
|
+
- **Typed errors** (`AiError.code`): `auth-invalid`, `not-entitled`,
|
|
107
|
+
`user-blocked`, `app-unknown`, `rate-limited`, `upstream-busy`,
|
|
108
|
+
`payload-too-large`, `modality-not-allowed`, `config-unavailable`,
|
|
109
|
+
`upstream-error`, `internal`.
|
|
110
|
+
- **Copy keys**: `ai.*` defaults in `aiMessages` — map through your i18n.
|
|
111
|
+
|
|
112
|
+
## Public API
|
|
113
|
+
|
|
114
|
+
Everything is exported from `@chipmobilesdk/rn-ai`; internal `config/`,
|
|
115
|
+
`transport/`, `core/`, `react/`, and `diagnostics/` paths are not public.
|
|
116
|
+
|
|
117
|
+
- Client: `createAiClient`, `AiClient`, `AiClientConfig`, `TokenProvider`
|
|
118
|
+
- Chat: `ChatRequest`, `ChatMessage`, `ContentPart`, `ChatStreamHandlers`,
|
|
119
|
+
`ChatHandle`, `Usage`
|
|
120
|
+
- Behavior and cache: `AppBehaviorConfig`, `AiBehaviorState`, `BehaviorState`,
|
|
121
|
+
`AiConfigCache`, `QuotaSnapshot`
|
|
122
|
+
- React: `useAiChat`, `useAiQuota`, `useAiBehaviorState`, `AiChatStatus`,
|
|
123
|
+
`UseAiChatResult`, `UseAiQuotaResult`. `useAiBehaviorState(client)` re-renders when
|
|
124
|
+
the config loads or the `aiEnabled` kill switch flips; `client.subscribe` and
|
|
125
|
+
`client.getBehaviorState()` give the same outside React.
|
|
126
|
+
- Diagnostics and copy: `AiError`, `AiErrorCode`, `isAiError`, `aiMessages`,
|
|
127
|
+
`aiMessage`, `AiMessageKey`, `aiMessageSet` (the same catalog as an rn-core message
|
|
128
|
+
set), `messageKeyForError(error)` (the copy to show for any error code). Register the
|
|
129
|
+
copy with rn-i18n in one line: `runtimeEntriesFromCatalog(aiMessageSet.user)`.
|
|
130
|
+
|
|
131
|
+
## App responsibilities
|
|
132
|
+
|
|
133
|
+
- **AI disclosure (store policy)**: user text/images/audio are processed by
|
|
134
|
+
the platform's AI service. Update your privacy policy, Apple App Privacy,
|
|
135
|
+
and Google Play Data Safety before shipping, and disclose AI processing
|
|
136
|
+
in-app (POL-005/006 in the feature spec).
|
|
137
|
+
- Camera/microphone permissions for capture UIs are yours; the SDK only
|
|
138
|
+
accepts already-captured data and adds no permissions.
|
|
139
|
+
- Respect `payload-too-large` (default cap 10 MB/request) by resizing images
|
|
140
|
+
and bounding audio length.
|
|
141
|
+
|
|
142
|
+
## Compatibility
|
|
143
|
+
|
|
144
|
+
React ≥19 · React Native ≥0.85 (Hermes) · iOS + Android · Node ≥22.11 for
|
|
145
|
+
tooling.
|
|
146
|
+
|
|
147
|
+
The repository contains `src/screens/AiDemoScreen.tsx` and its component test.
|
|
148
|
+
The current demo sidebar does not expose that screen yet, so it must be wired
|
|
149
|
+
into `ScreenKey`, `Sidebar`, and `ActiveScreen` before it is reachable in-app.
|
|
150
|
+
|
|
151
|
+
## Validation
|
|
152
|
+
|
|
153
|
+
Run from the repository root:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
npm run typecheck --workspace @chipmobilesdk/rn-ai
|
|
157
|
+
npm run pack:check --workspace @chipmobilesdk/rn-ai
|
|
158
|
+
npm run validate
|
|
159
|
+
```
|
package/package.json
CHANGED
|
@@ -1,43 +1,39 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@chipmobilesdk/rn-ai",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"private": false,
|
|
5
|
-
"license": "UNLICENSED",
|
|
6
|
-
"main": "src/index.ts",
|
|
7
|
-
"types": "src/index.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"types": "./src/index.ts",
|
|
11
|
-
"react-native": "./src/index.ts",
|
|
12
|
-
"default": "./src/index.ts"
|
|
13
|
-
}
|
|
14
|
-
},
|
|
15
|
-
"files": [
|
|
16
|
-
"src
|
|
17
|
-
"src
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
"react": ">=
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
"access": "public",
|
|
41
|
-
"registry": "https://registry.npmjs.org/"
|
|
42
|
-
}
|
|
43
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@chipmobilesdk/rn-ai",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.ts",
|
|
11
|
+
"react-native": "./src/index.ts",
|
|
12
|
+
"default": "./src/index.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"!src/**/__tests__",
|
|
18
|
+
"skills",
|
|
19
|
+
"README.md",
|
|
20
|
+
"CHANGELOG.md"
|
|
21
|
+
],
|
|
22
|
+
"sideEffects": false,
|
|
23
|
+
"scripts": {
|
|
24
|
+
"typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.test.json",
|
|
25
|
+
"pack:check": "npm pack --dry-run"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@chipmobilesdk/rn-core": "^0.1.0"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"@chipmobilesdk/rn-auth": ">=0.2.0",
|
|
32
|
+
"react": ">=19",
|
|
33
|
+
"react-native": ">=0.85"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public",
|
|
37
|
+
"registry": "https://registry.npmjs.org/"
|
|
38
|
+
}
|
|
39
|
+
}
|