@chipmobilesdk/rn-ai 0.4.0 → 0.5.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.
Files changed (88) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/README.md +5 -2
  3. package/lib/commonjs/config/behaviorConfig.js +283 -0
  4. package/lib/commonjs/config/behaviorConfig.js.map +1 -0
  5. package/lib/commonjs/contract/gateway.js +60 -0
  6. package/lib/commonjs/contract/gateway.js.map +1 -0
  7. package/lib/commonjs/core/aiClient.js +330 -0
  8. package/lib/commonjs/core/aiClient.js.map +1 -0
  9. package/lib/commonjs/core/quotaRequest.js +105 -0
  10. package/lib/commonjs/core/quotaRequest.js.map +1 -0
  11. package/lib/commonjs/diagnostics/errors.js +108 -0
  12. package/lib/commonjs/diagnostics/errors.js.map +1 -0
  13. package/lib/commonjs/diagnostics/messageKeys.js +68 -0
  14. package/lib/commonjs/diagnostics/messageKeys.js.map +1 -0
  15. package/lib/commonjs/index.js +102 -0
  16. package/lib/commonjs/index.js.map +1 -0
  17. package/lib/commonjs/package.json +1 -0
  18. package/lib/commonjs/react/hooks.js +170 -0
  19. package/lib/commonjs/react/hooks.js.map +1 -0
  20. package/lib/commonjs/transport/sseClient.js +151 -0
  21. package/lib/commonjs/transport/sseClient.js.map +1 -0
  22. package/lib/commonjs/transport/timeouts.js +46 -0
  23. package/lib/commonjs/transport/timeouts.js.map +1 -0
  24. package/lib/module/config/behaviorConfig.js +275 -0
  25. package/lib/module/config/behaviorConfig.js.map +1 -0
  26. package/lib/module/contract/gateway.js +56 -0
  27. package/lib/module/contract/gateway.js.map +1 -0
  28. package/lib/module/core/aiClient.js +327 -0
  29. package/lib/module/core/aiClient.js.map +1 -0
  30. package/lib/module/core/quotaRequest.js +102 -0
  31. package/lib/module/core/quotaRequest.js.map +1 -0
  32. package/lib/module/diagnostics/errors.js +99 -0
  33. package/lib/module/diagnostics/errors.js.map +1 -0
  34. package/lib/module/diagnostics/messageKeys.js +61 -0
  35. package/lib/module/diagnostics/messageKeys.js.map +1 -0
  36. package/lib/module/index.js +19 -0
  37. package/lib/module/index.js.map +1 -0
  38. package/lib/module/package.json +1 -0
  39. package/lib/module/react/hooks.js +164 -0
  40. package/lib/module/react/hooks.js.map +1 -0
  41. package/lib/module/transport/sseClient.js +146 -0
  42. package/lib/module/transport/sseClient.js.map +1 -0
  43. package/lib/module/transport/timeouts.js +41 -0
  44. package/lib/module/transport/timeouts.js.map +1 -0
  45. package/lib/typescript/commonjs/config/behaviorConfig.d.ts +119 -0
  46. package/lib/typescript/commonjs/config/behaviorConfig.d.ts.map +1 -0
  47. package/lib/typescript/commonjs/contract/gateway.d.ts +64 -0
  48. package/lib/typescript/commonjs/contract/gateway.d.ts.map +1 -0
  49. package/lib/typescript/commonjs/core/aiClient.d.ts +104 -0
  50. package/lib/typescript/commonjs/core/aiClient.d.ts.map +1 -0
  51. package/lib/typescript/commonjs/core/quotaRequest.d.ts +45 -0
  52. package/lib/typescript/commonjs/core/quotaRequest.d.ts.map +1 -0
  53. package/lib/typescript/commonjs/diagnostics/errors.d.ts +38 -0
  54. package/lib/typescript/commonjs/diagnostics/errors.d.ts.map +1 -0
  55. package/lib/typescript/commonjs/diagnostics/messageKeys.d.ts +46 -0
  56. package/lib/typescript/commonjs/diagnostics/messageKeys.d.ts.map +1 -0
  57. package/lib/typescript/commonjs/index.d.ts +21 -0
  58. package/lib/typescript/commonjs/index.d.ts.map +1 -0
  59. package/lib/typescript/commonjs/package.json +1 -0
  60. package/lib/typescript/commonjs/react/hooks.d.ts +37 -0
  61. package/lib/typescript/commonjs/react/hooks.d.ts.map +1 -0
  62. package/lib/typescript/commonjs/transport/sseClient.d.ts +57 -0
  63. package/lib/typescript/commonjs/transport/sseClient.d.ts.map +1 -0
  64. package/lib/typescript/commonjs/transport/timeouts.d.ts +28 -0
  65. package/lib/typescript/commonjs/transport/timeouts.d.ts.map +1 -0
  66. package/lib/typescript/module/config/behaviorConfig.d.ts +119 -0
  67. package/lib/typescript/module/config/behaviorConfig.d.ts.map +1 -0
  68. package/lib/typescript/module/contract/gateway.d.ts +64 -0
  69. package/lib/typescript/module/contract/gateway.d.ts.map +1 -0
  70. package/lib/typescript/module/core/aiClient.d.ts +104 -0
  71. package/lib/typescript/module/core/aiClient.d.ts.map +1 -0
  72. package/lib/typescript/module/core/quotaRequest.d.ts +45 -0
  73. package/lib/typescript/module/core/quotaRequest.d.ts.map +1 -0
  74. package/lib/typescript/module/diagnostics/errors.d.ts +38 -0
  75. package/lib/typescript/module/diagnostics/errors.d.ts.map +1 -0
  76. package/lib/typescript/module/diagnostics/messageKeys.d.ts +46 -0
  77. package/lib/typescript/module/diagnostics/messageKeys.d.ts.map +1 -0
  78. package/lib/typescript/module/index.d.ts +21 -0
  79. package/lib/typescript/module/index.d.ts.map +1 -0
  80. package/lib/typescript/module/package.json +1 -0
  81. package/lib/typescript/module/react/hooks.d.ts +37 -0
  82. package/lib/typescript/module/react/hooks.d.ts.map +1 -0
  83. package/lib/typescript/module/transport/sseClient.d.ts +57 -0
  84. package/lib/typescript/module/transport/sseClient.d.ts.map +1 -0
  85. package/lib/typescript/module/transport/timeouts.d.ts +28 -0
  86. package/lib/typescript/module/transport/timeouts.d.ts.map +1 -0
  87. package/package.json +40 -12
  88. package/skills/sdk-rn-ai/SKILL.md +8 -0
@@ -0,0 +1,275 @@
1
+ "use strict";
2
+
3
+ import { createListenerSet, createSingleFlight } from '@chipmobilesdk/rn-core';
4
+ import { AiError } from "../diagnostics/errors.js";
5
+ import { DEFAULT_AI_TIMEOUTS, withTimeout } from "../transport/timeouts.js";
6
+
7
+ /** App behavior configuration (contracts/app-behavior-config.md). */
8
+
9
+ /** Persistence port; default is in-memory (inject a durable store for offline launch). */
10
+
11
+ export class MemoryConfigCache {
12
+ map = new Map();
13
+ async get(key) {
14
+ return this.map.get(key) ?? null;
15
+ }
16
+ async set(key, value) {
17
+ this.map.set(key, value);
18
+ }
19
+ }
20
+
21
+ /**
22
+ * An `AiConfigCache` over rn-core's synchronous `KeyValuePort` (AI-10).
23
+ *
24
+ * Without a durable cache every launch waits for the network before the first
25
+ * chat, which is exactly what the cache-first design exists to avoid. Open the
26
+ * port the way the app opens its other durable state, for example
27
+ * `openDurable(() => createMMKV({ id: 'app' }))`.
28
+ */
29
+ export function configCacheFromKeyValue(port) {
30
+ return {
31
+ async get(key) {
32
+ return port.getString(key) ?? null;
33
+ },
34
+ async set(key, value) {
35
+ port.set(key, value);
36
+ }
37
+ };
38
+ }
39
+ /** The newest document layout this release understands. */
40
+ export const SUPPORTED_SCHEMA_VERSION = 1;
41
+
42
+ /** Bounds on the remote-configured recovery, so a bad document cannot stall a chat. */
43
+ const MAX_SILENT_RETRIES = 5;
44
+ const MIN_REFRESH_TTL_SECONDS = 30;
45
+ const DEFAULT_REFRESH_TTL_SECONDS = 21_600;
46
+ const numbers = value => Array.isArray(value) ? value.filter(n => Number.isFinite(n)) : undefined;
47
+ const positiveInt = (value, fallback) => typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
48
+ function invalid(reason) {
49
+ throw new AiError('config-unavailable', `behavior config ${reason}`);
50
+ }
51
+
52
+ /** https, or http on a development machine: a bearer token travels to this host. */
53
+ export function isTrustedUrl(url) {
54
+ if (/^https:\/\//i.test(url)) {
55
+ return true;
56
+ }
57
+ return /^http:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?(\/|$)/i.test(url);
58
+ }
59
+ function hostOf(url) {
60
+ return url.replace(/^[a-z]+:\/\//i, '').split(/[/:?#]/)[0].toLowerCase();
61
+ }
62
+ /**
63
+ * Parse + validate; unknown fields ignored (forward compatibility).
64
+ *
65
+ * Every field this package acts on is checked here, because the document is
66
+ * remote and the alternative is a TypeError inside an XHR callback, a revalidate
67
+ * on every single request, or a bearer token sent to a host nobody vetted
68
+ * (AI-06, AI-07).
69
+ */
70
+ export function parseBehaviorConfig(json, options = {}) {
71
+ let raw;
72
+ try {
73
+ raw = JSON.parse(json);
74
+ } catch {
75
+ invalid('is not valid JSON');
76
+ }
77
+ const api = raw.api;
78
+ if (typeof raw.appId !== 'string' || typeof api?.baseUrl !== 'string' || !Array.isArray(api.scopes)) {
79
+ invalid('is missing required fields');
80
+ }
81
+ const schemaVersion = positiveInt(raw.schemaVersion, 1);
82
+ if (schemaVersion > SUPPORTED_SCHEMA_VERSION) {
83
+ // A newer layout may mean something different by the same field names.
84
+ invalid(`declares schema version ${schemaVersion}, newer than ${SUPPORTED_SCHEMA_VERSION}`);
85
+ }
86
+ if (options.expectedAppId !== undefined && raw.appId !== options.expectedAppId) {
87
+ // The cache key and the request path carry the app id; a document for
88
+ // another app would send this app's traffic somewhere else entirely.
89
+ invalid(`is for another app (${raw.appId})`);
90
+ }
91
+ const baseUrl = api.baseUrl.replace(/\/$/, '');
92
+ if (!isTrustedUrl(baseUrl)) {
93
+ invalid('names a baseUrl that is not https');
94
+ }
95
+ if (options.allowedHosts && !options.allowedHosts.includes(hostOf(baseUrl))) {
96
+ invalid(`names a baseUrl outside the allowed hosts (${hostOf(baseUrl)})`);
97
+ }
98
+ const recovery = raw.recovery ?? {};
99
+ const features = raw.features ?? {};
100
+ return {
101
+ schemaVersion,
102
+ appId: raw.appId,
103
+ configVersion: positiveInt(raw.configVersion, 0),
104
+ // A TTL that was NaN made `isFresh` false forever, so every request
105
+ // revalidated over the network (AI-06).
106
+ refreshTtlSeconds: Math.max(MIN_REFRESH_TTL_SECONDS, positiveInt(raw.refreshTtlSeconds, DEFAULT_REFRESH_TTL_SECONDS)),
107
+ api: {
108
+ baseUrl,
109
+ scopes: api.scopes.filter(s => typeof s === 'string'),
110
+ supportsStreaming: api.supportsStreaming !== false
111
+ },
112
+ limitsDisplay: raw.limitsDisplay,
113
+ // Typed here, once: a remote document with `reloadOnHttpStatus: "404"` used to
114
+ // throw a TypeError inside the XHR error callback, where nothing caught it.
115
+ recovery: {
116
+ reloadOnHttpStatus: numbers(recovery.reloadOnHttpStatus) ?? [404, 410],
117
+ maxSilentRetries: Math.min(MAX_SILENT_RETRIES, Number.isFinite(recovery.maxSilentRetries) ? Math.max(0, Number(recovery.maxSilentRetries)) : 1),
118
+ retryBackoffMs: numbers(recovery.retryBackoffMs) ?? [500, 2000]
119
+ },
120
+ features: {
121
+ aiEnabled: features.aiEnabled !== false
122
+ }
123
+ };
124
+ }
125
+ /**
126
+ * Cache-first behavior config (FR-018): launch never blocks on the network;
127
+ * TTL revalidation uses ETag; designated errors force a reload.
128
+ */
129
+ export class BehaviorConfigManager {
130
+ /**
131
+ * One cache read and one network fetch at a time (AI-08): a cold start makes a
132
+ * chat and a quota call together, and each used to fetch the config itself.
133
+ */
134
+ cacheRead = createSingleFlight();
135
+ networkFetch = createSingleFlight();
136
+ currentState = 'unloaded';
137
+ stateListeners = createListenerSet();
138
+
139
+ /** Where the config came from, or why there is none. */
140
+ get state() {
141
+ return this.currentState;
142
+ }
143
+
144
+ /** Called whenever `state` changes (AI-09). */
145
+ subscribe(listener) {
146
+ return this.stateListeners.add(listener);
147
+ }
148
+ setState(next) {
149
+ if (next === this.currentState) return;
150
+ this.currentState = next;
151
+ this.stateListeners.notify();
152
+ }
153
+ constructor(deps) {
154
+ this.deps = deps;
155
+ if (!isTrustedUrl(deps.configUrl)) {
156
+ // The document decides where the tokens go, so where it comes from
157
+ // matters as much (AI-07).
158
+ throw new AiError('config-unavailable', 'configUrl must be https');
159
+ }
160
+ this.cacheKey = `rn-ai:config:${deps.appId}`;
161
+ this.cache = deps.cache ?? new MemoryConfigCache();
162
+ this.fetchFn = deps.fetchFn ?? fetch;
163
+ this.now = deps.now ?? Date.now;
164
+ }
165
+ parse(json) {
166
+ return parseBehaviorConfig(json, {
167
+ expectedAppId: this.deps.appId,
168
+ ...(this.deps.allowedHosts ? {
169
+ allowedHosts: this.deps.allowedHosts
170
+ } : {})
171
+ });
172
+ }
173
+ isFresh(envelope, config) {
174
+ return this.now() - envelope.fetchedAt < config.refreshTtlSeconds * 1000;
175
+ }
176
+ loadFromCache() {
177
+ return this.current ? Promise.resolve() : this.cacheRead.run(() => this.readCache());
178
+ }
179
+ async readCache() {
180
+ if (this.current) return;
181
+ const stored = await this.cache.get(this.cacheKey).catch(() => null);
182
+ if (!stored) return;
183
+ try {
184
+ const envelope = JSON.parse(stored);
185
+ this.current = {
186
+ config: this.parse(envelope.configJson),
187
+ envelope
188
+ };
189
+ this.setState('ready-cache');
190
+ } catch {
191
+ // corrupted cache entries are ignored, not fatal
192
+ }
193
+ }
194
+ fetchNetwork() {
195
+ return this.networkFetch.run(() => this.fetchFromNetwork());
196
+ }
197
+
198
+ /** One attempt; `conditional` sends the stored ETag when there is one. */
199
+ async fetchOnce(conditional) {
200
+ const headers = {};
201
+ const etag = this.current?.envelope.etag;
202
+ if (conditional && etag) {
203
+ headers['If-None-Match'] = etag;
204
+ }
205
+ // Bounded: with no cache, a hung fetch used to block every chat and quota call.
206
+ return withTimeout(this.deps.timeoutMs ?? DEFAULT_AI_TIMEOUTS.configMs, async signal => {
207
+ const response = await this.fetchFn(this.deps.configUrl, {
208
+ headers,
209
+ signal
210
+ });
211
+ return {
212
+ status: response.status,
213
+ text: response.ok && response.status !== 304 ? await response.text() : '',
214
+ etag: response.headers.get('etag') ?? undefined
215
+ };
216
+ }, () => new AiError('config-unavailable', 'config fetch timed out'));
217
+ }
218
+ async fetchFromNetwork() {
219
+ let result = await this.fetchOnce(true);
220
+ if (result.status === 304) {
221
+ if (this.current) {
222
+ this.current.envelope.fetchedAt = this.now();
223
+ await this.persist(this.current.envelope);
224
+ this.setState('ready-network');
225
+ return this.current.config;
226
+ }
227
+ // Not-modified against a cache that is no longer there (it was cleared,
228
+ // or it failed to parse). Asking again without the ETag is the answer;
229
+ // reporting `config fetch failed: 304` was not (AI-17).
230
+ result = await this.fetchOnce(false);
231
+ }
232
+ if (result.status < 200 || result.status >= 300) {
233
+ throw new AiError('config-unavailable', `config fetch failed: ${result.status}`);
234
+ }
235
+ const config = this.parse(result.text);
236
+ const envelope = {
237
+ configJson: result.text,
238
+ etag: result.etag,
239
+ fetchedAt: this.now()
240
+ };
241
+ this.current = {
242
+ config,
243
+ envelope
244
+ };
245
+ await this.persist(envelope);
246
+ this.setState('ready-network');
247
+ return config;
248
+ }
249
+ async persist(envelope) {
250
+ await this.cache.set(this.cacheKey, JSON.stringify(envelope)).catch(() => undefined);
251
+ }
252
+
253
+ /**
254
+ * Cache-first read. Fresh cache returns immediately; stale cache returns
255
+ * immediately too while a background revalidation runs; no cache at all
256
+ * falls through to the network (or `config-unavailable`).
257
+ */
258
+ async getConfig(options = {}) {
259
+ await this.loadFromCache();
260
+ if (!options.forceReload && this.current) {
261
+ if (!this.isFresh(this.current.envelope, this.current.config)) {
262
+ this.fetchNetwork().catch(() => undefined); // background revalidate
263
+ }
264
+ return this.current.config;
265
+ }
266
+ try {
267
+ return await this.fetchNetwork();
268
+ } catch (e) {
269
+ if (this.current) return this.current.config; // stale beats broken
270
+ this.setState('unavailable');
271
+ throw e instanceof AiError ? e : new AiError('config-unavailable', String(e));
272
+ }
273
+ }
274
+ }
275
+ //# sourceMappingURL=behaviorConfig.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["createListenerSet","createSingleFlight","AiError","DEFAULT_AI_TIMEOUTS","withTimeout","MemoryConfigCache","map","Map","get","key","set","value","configCacheFromKeyValue","port","getString","SUPPORTED_SCHEMA_VERSION","MAX_SILENT_RETRIES","MIN_REFRESH_TTL_SECONDS","DEFAULT_REFRESH_TTL_SECONDS","numbers","Array","isArray","filter","n","Number","isFinite","undefined","positiveInt","fallback","Math","floor","invalid","reason","isTrustedUrl","url","test","hostOf","replace","split","toLowerCase","parseBehaviorConfig","json","options","raw","JSON","parse","api","appId","baseUrl","scopes","schemaVersion","expectedAppId","allowedHosts","includes","recovery","features","configVersion","refreshTtlSeconds","max","s","supportsStreaming","limitsDisplay","reloadOnHttpStatus","maxSilentRetries","min","retryBackoffMs","aiEnabled","BehaviorConfigManager","cacheRead","networkFetch","currentState","stateListeners","state","subscribe","listener","add","setState","next","notify","constructor","deps","configUrl","cacheKey","cache","fetchFn","fetch","now","Date","isFresh","envelope","config","fetchedAt","loadFromCache","current","Promise","resolve","run","readCache","stored","catch","configJson","fetchNetwork","fetchFromNetwork","fetchOnce","conditional","headers","etag","timeoutMs","configMs","signal","response","status","text","ok","result","persist","stringify","getConfig","forceReload","e","String"],"sourceRoot":"../../../src","sources":["config/behaviorConfig.ts"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * The gateway↔client contract: error codes, quota headers, stream events.
5
+ *
6
+ * One definition, imported by both sides (GW-13). The gateway's own codes and
7
+ * the client's map of them were two hand-maintained lists, and they had already
8
+ * drifted: `BAD_REQUEST`, `ENDPOINT_RETIRED` and `INTERNAL` existed on the
9
+ * server and were unknown to the client, which fell back to a guess from the
10
+ * status. Adding a code now fails to compile on whichever side has not handled
11
+ * it.
12
+ *
13
+ * Types and plain constants only, and no import of anything: the gateway runs
14
+ * on Node and imports this file for its types alone.
15
+ */
16
+
17
+ /** Every error the gateway answers with (`error.code` in the body). */
18
+
19
+ /** The HTTP status each code is sent with. */
20
+ export const GATEWAY_ERROR_STATUS = {
21
+ BAD_REQUEST: 400,
22
+ AUTH_INVALID: 401,
23
+ NOT_ENTITLED: 403,
24
+ USER_BLOCKED: 403,
25
+ APP_UNKNOWN: 404,
26
+ ENDPOINT_RETIRED: 410,
27
+ PAYLOAD_TOO_LARGE: 413,
28
+ MODALITY_NOT_ALLOWED: 415,
29
+ RATE_LIMITED: 429,
30
+ UPSTREAM_BUSY: 429,
31
+ UPSTREAM_ERROR: 500,
32
+ INTERNAL: 500
33
+ };
34
+
35
+ /** The error body. `message` is safe to log; it never carries upstream detail. */
36
+
37
+ /** The quota headers, by name. A rename here breaks both sides' compile. */
38
+ export const QUOTA_HEADERS = {
39
+ minuteRemaining: 'X-Quota-Minute-Remaining',
40
+ dayRemaining: 'X-Quota-Day-Remaining',
41
+ dayTokensRemaining: 'X-Quota-Day-Tokens-Remaining'
42
+ };
43
+
44
+ /** The names field by field, so a side declaring its own literals is checked exactly. */
45
+
46
+ /** What is left of an app's allowance, as the gateway reports it. */
47
+
48
+ /** What one answer cost. */
49
+
50
+ /** The `data:` payloads of a chat stream, before the `[DONE]` sentinel. */
51
+
52
+ /** The last `data:` line of a stream that completed. */
53
+ export const SSE_DONE_SENTINEL = '[DONE]';
54
+ /** The `Retry-After` header, which a 429 always carries. */
55
+ export const RETRY_AFTER_HEADER = 'Retry-After';
56
+ //# sourceMappingURL=gateway.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["GATEWAY_ERROR_STATUS","BAD_REQUEST","AUTH_INVALID","NOT_ENTITLED","USER_BLOCKED","APP_UNKNOWN","ENDPOINT_RETIRED","PAYLOAD_TOO_LARGE","MODALITY_NOT_ALLOWED","RATE_LIMITED","UPSTREAM_BUSY","UPSTREAM_ERROR","INTERNAL","QUOTA_HEADERS","minuteRemaining","dayRemaining","dayTokensRemaining","SSE_DONE_SENTINEL","RETRY_AFTER_HEADER"],"sourceRoot":"../../../src","sources":["contract/gateway.ts"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAeA;AACA,OAAO,MAAMA,oBAAgE,GAAG;EAC/EC,WAAW,EAAE,GAAG;EAChBC,YAAY,EAAE,GAAG;EACjBC,YAAY,EAAE,GAAG;EACjBC,YAAY,EAAE,GAAG;EACjBC,WAAW,EAAE,GAAG;EAChBC,gBAAgB,EAAE,GAAG;EACrBC,iBAAiB,EAAE,GAAG;EACtBC,oBAAoB,EAAE,GAAG;EACzBC,YAAY,EAAE,GAAG;EACjBC,aAAa,EAAE,GAAG;EAClBC,cAAc,EAAE,GAAG;EACnBC,QAAQ,EAAE;AACX,CAAC;;AAED;;AASA;AACA,OAAO,MAAMC,aAAa,GAAG;EAC5BC,eAAe,EAAE,0BAA0B;EAC3CC,YAAY,EAAE,uBAAuB;EACrCC,kBAAkB,EAAE;AACrB,CAAU;;AAEV;;AAIA;;AAOA;;AAOA;;AAMA;AACA,OAAO,MAAMC,iBAAiB,GAAG,QAAQ;AAGzC;AACA,OAAO,MAAMC,kBAAkB,GAAG,aAAa","ignoreList":[]}
@@ -0,0 +1,327 @@
1
+ "use strict";
2
+
3
+ import { createDiagnosticChannel, createListenerSet, realScheduler } from '@chipmobilesdk/rn-core';
4
+ import { BehaviorConfigManager } from "../config/behaviorConfig.js";
5
+ import { SSE_DONE_SENTINEL } from "../contract/gateway.js";
6
+ import { AiError, errorFromHttp } from "../diagnostics/errors.js";
7
+ import { requestSse } from "../transport/sseClient.js";
8
+ import { createQuotaRequest } from "./quotaRequest.js";
9
+ import { DEFAULT_AI_TIMEOUTS } from "../transport/timeouts.js";
10
+
11
+ /** Every failure a caller receives also goes to the app's SDK sinks (CORE-08). */
12
+ const channel = createDiagnosticChannel('rn-ai');
13
+ function report(operation, error) {
14
+ channel.emit(error.code, error.retryable ? 'warn' : 'error', {
15
+ operation,
16
+ retryable: error.retryable
17
+ });
18
+ }
19
+
20
+ /** Minimal slice of the rn-auth `Auth` controller the client needs. */
21
+
22
+ /** What one answer cost, as the gateway reports it. */
23
+
24
+ /** What is left of the app's allowance. */
25
+
26
+ /** Ceiling on one remote-configured backoff step, so a bad document cannot stall a chat. */
27
+ const MAX_RETRY_BACKOFF_MS = 30_000;
28
+
29
+ /** The wait before silent retry `index` (0-based); the last configured step repeats. */
30
+ function backoffBefore(steps, index) {
31
+ const step = steps[Math.min(index, steps.length - 1)] ?? 0;
32
+ return Math.min(Math.max(0, step), MAX_RETRY_BACKOFF_MS);
33
+ }
34
+ /** A quota from the wire, or nothing: a malformed one is not displayed (AI-13). */
35
+ function asQuota(value) {
36
+ if (typeof value !== 'object' || value === null) {
37
+ return undefined;
38
+ }
39
+ const candidate = value;
40
+ const fields = [candidate.minuteRemaining, candidate.dayRemaining, candidate.dayTokensRemaining];
41
+ if (!fields.every(field => typeof field === 'number' && Number.isFinite(field))) {
42
+ return undefined;
43
+ }
44
+ return {
45
+ minuteRemaining: candidate.minuteRemaining,
46
+ dayRemaining: candidate.dayRemaining,
47
+ dayTokensRemaining: candidate.dayTokensRemaining
48
+ };
49
+ }
50
+ export function createAiClient(clientConfig) {
51
+ const resourceKey = clientConfig.resourceKey ?? 'ai-gateway';
52
+ const transport = clientConfig.transport ?? requestSse;
53
+ const fetchFn = clientConfig.fetchFn ?? fetch;
54
+ const timeouts = {
55
+ ...DEFAULT_AI_TIMEOUTS,
56
+ ...clientConfig.timeouts
57
+ };
58
+ const scheduler = clientConfig.scheduler ?? realScheduler;
59
+ const configManager = new BehaviorConfigManager({
60
+ configUrl: clientConfig.configUrl,
61
+ appId: clientConfig.appId,
62
+ cache: clientConfig.storage,
63
+ fetchFn: clientConfig.fetchFn,
64
+ timeoutMs: timeouts.configMs,
65
+ ...(clientConfig.allowedHosts ? {
66
+ allowedHosts: clientConfig.allowedHosts
67
+ } : {})
68
+ });
69
+ let lastConfig;
70
+ const behaviorListeners = createListenerSet();
71
+ let snapshot;
72
+ let quota;
73
+
74
+ /** Rebuilt only when a field changes, so useSyncExternalStore sees a stable object. */
75
+ function behaviorState() {
76
+ const config = configManager.state;
77
+ const aiEnabled = lastConfig?.features.aiEnabled ?? true;
78
+ if (!snapshot || snapshot.config !== config || snapshot.aiEnabled !== aiEnabled) {
79
+ snapshot = {
80
+ config,
81
+ aiEnabled
82
+ };
83
+ }
84
+ return snapshot;
85
+ }
86
+ function setLastConfig(next) {
87
+ const wasEnabled = lastConfig?.features.aiEnabled ?? true;
88
+ lastConfig = next;
89
+ if (next.features.aiEnabled !== wasEnabled) behaviorListeners.notify();
90
+ }
91
+
92
+ /** One place records the quota, whichever call learned it (AI-13). */
93
+ function setQuota(next) {
94
+ if (!next || quota && shallowSameQuota(quota, next)) {
95
+ return;
96
+ }
97
+ quota = next;
98
+ behaviorListeners.notify();
99
+ }
100
+ async function acquire(options = {}) {
101
+ const config = await configManager.getConfig();
102
+ setLastConfig(config);
103
+ if (!config.features.aiEnabled) {
104
+ throw new AiError('config-unavailable', 'AI is disabled for this app', {
105
+ recoverable: false
106
+ });
107
+ }
108
+ const tokenResult = await clientConfig.auth.requestAccessToken(resourceKey, {
109
+ forceRefresh: options.forceRefresh === true
110
+ });
111
+ if (tokenResult.status !== 'success' || !tokenResult.token) {
112
+ // What the auth layer called it, kept as the cause: a screen has to tell
113
+ // "sign in again" from "you are offline" (AI-11).
114
+ throw tokenError(tokenResult.status, tokenResult.errorCode);
115
+ }
116
+ return {
117
+ config,
118
+ token: tokenResult.token
119
+ };
120
+ }
121
+ const endpoint = (config, path) => `${config.api.baseUrl}/api/v1/apps/${clientConfig.appId}/${path}`;
122
+ function chat(request, handlers) {
123
+ let cancelled = false;
124
+ let settled = false;
125
+ let inner;
126
+
127
+ // Every callback to the app goes through here. Nothing is delivered after
128
+ // cancel() — an acquire() or config reload that failed after the user
129
+ // cancelled (or sent the next message) used to flip that screen to
130
+ // `error` — and exactly one terminal callback is delivered.
131
+ const emit = {
132
+ delta(text) {
133
+ if (!cancelled && !settled) handlers.onDelta(text);
134
+ },
135
+ done(result) {
136
+ if (cancelled || settled) return;
137
+ settled = true;
138
+ setQuota(result.quota);
139
+ handlers.onDone(result);
140
+ },
141
+ error(error) {
142
+ if (cancelled || settled) return;
143
+ settled = true;
144
+ inner?.cancel();
145
+ report('chat', error);
146
+ handlers.onError(error);
147
+ }
148
+ };
149
+
150
+ // Any exception on the way becomes a reported error. Each path used to end
151
+ // in `.catch(() => undefined)`, so a transport that threw (a bad URL from
152
+ // remote config, say) left the UI in `loading` with nothing to show.
153
+ const fail = e => emit.error(e instanceof AiError ? e : new AiError('internal', e instanceof Error ? e.message : String(e)));
154
+ const guarded = fn => (...args) => {
155
+ try {
156
+ fn(...args);
157
+ } catch (e) {
158
+ fail(e);
159
+ }
160
+ };
161
+ const attempt = async state => {
162
+ const {
163
+ config,
164
+ token
165
+ } = await acquire({
166
+ forceRefresh: state.authRetries > 0
167
+ });
168
+ if (cancelled) return;
169
+ let done;
170
+ let streamErrorCode;
171
+ inner = transport({
172
+ url: endpoint(config, 'chat'),
173
+ method: 'POST',
174
+ headers: {
175
+ Authorization: `Bearer ${token}`,
176
+ 'Content-Type': 'application/json'
177
+ },
178
+ body: JSON.stringify(request),
179
+ connectTimeoutMs: timeouts.connectMs,
180
+ idleTimeoutMs: timeouts.idleMs
181
+ }, {
182
+ onData: guarded(payload => {
183
+ if (payload === SSE_DONE_SENTINEL) return;
184
+ let event;
185
+ try {
186
+ event = JSON.parse(payload);
187
+ } catch {
188
+ return;
189
+ }
190
+ if (event.type === 'delta' && typeof event.text === 'string') {
191
+ emit.delta(event.text);
192
+ } else if (event.type === 'done' && event.usage) {
193
+ done = {
194
+ usage: event.usage,
195
+ quota: asQuota(event.quota)
196
+ };
197
+ } else if (event.type === 'error') {
198
+ streamErrorCode = event.code ?? 'UPSTREAM_ERROR';
199
+ }
200
+ }),
201
+ onEnd: guarded(() => {
202
+ if (streamErrorCode) {
203
+ emit.error(new AiError('upstream-error', 'the AI service failed mid-response', {
204
+ cause: streamErrorCode
205
+ }));
206
+ } else if (done) {
207
+ emit.done(done);
208
+ } else {
209
+ // Headers said 200 and the body stopped short: the connection
210
+ // dropped mid-answer, which is a network failure, not a fault
211
+ // of the service.
212
+ emit.error(new AiError('network', 'the stream ended before it completed'));
213
+ }
214
+ }),
215
+ onError: guarded((status, bodyText, response) => {
216
+ if (cancelled) return;
217
+ recover(status, bodyText, state, {
218
+ ...(response ? {
219
+ getHeader: name => response.getHeader(name)
220
+ } : {})
221
+ }, next => attempt(next).catch(fail), fail, emit.error);
222
+ })
223
+ });
224
+ };
225
+ attempt({
226
+ configReloads: 0,
227
+ authRetries: 0
228
+ }).catch(fail);
229
+ return {
230
+ cancel() {
231
+ cancelled = true;
232
+ inner?.cancel();
233
+ }
234
+ };
235
+ }
236
+
237
+ /**
238
+ * The two recoveries the contract promises, in one place so chat and quota
239
+ * cannot drift (AI-13): a designated status reloads the configuration and
240
+ * retries after the configured backoff, and a 401 retries once with a token
241
+ * the provider had to fetch again.
242
+ */
243
+ function recover(status, bodyText, state, context, retry, onFailure, onGiveUp) {
244
+ const config = lastConfig;
245
+ if (config && config.recovery.reloadOnHttpStatus.includes(status) && state.configReloads < config.recovery.maxSilentRetries) {
246
+ // Waits `recovery.retryBackoffMs` first (AI-05): a gateway mid-rollout
247
+ // that answered 410 a moment ago may well answer it again at once.
248
+ const retryAfter = backoffBefore(config.recovery.retryBackoffMs, state.configReloads);
249
+ configManager.getConfig({
250
+ forceReload: true
251
+ }).then(reloaded => {
252
+ setLastConfig(reloaded);
253
+ scheduler.setTimeout(() => retry({
254
+ ...state,
255
+ configReloads: state.configReloads + 1
256
+ }), retryAfter);
257
+ }, e => onFailure(e instanceof AiError ? e : new AiError('config-unavailable', String(e))));
258
+ return;
259
+ }
260
+ // Expired server-side session → one retry with a token the provider
261
+ // actually re-fetched.
262
+ if (status === 401 && state.authRetries < 1) {
263
+ retry({
264
+ ...state,
265
+ authRetries: state.authRetries + 1
266
+ });
267
+ return;
268
+ }
269
+ onGiveUp(errorFromHttp(status, bodyText, context));
270
+ }
271
+ const {
272
+ getQuota
273
+ } = createQuotaRequest({
274
+ fetchFn,
275
+ timeouts,
276
+ acquire,
277
+ endpoint,
278
+ asQuota,
279
+ recover,
280
+ report: error => report('quota', error),
281
+ setQuota
282
+ });
283
+ return {
284
+ chat,
285
+ getQuota,
286
+ lastQuota: () => quota,
287
+ getBehaviorState: () => behaviorState(),
288
+ subscribe: listener => {
289
+ const fromManager = configManager.subscribe(listener);
290
+ const fromClient = behaviorListeners.add(listener);
291
+ return () => {
292
+ fromManager();
293
+ fromClient();
294
+ };
295
+ },
296
+ reloadConfig: async () => {
297
+ setLastConfig(await configManager.getConfig({
298
+ forceReload: true
299
+ }));
300
+ }
301
+ };
302
+ }
303
+ function shallowSameQuota(a, b) {
304
+ return a.minuteRemaining === b.minuteRemaining && a.dayRemaining === b.dayRemaining && a.dayTokensRemaining === b.dayTokensRemaining;
305
+ }
306
+
307
+ /**
308
+ * What the auth layer's refusal means for a chat. The code rn-auth gave is kept
309
+ * as the cause, and it decides the copy: "sign in again" and "you are offline"
310
+ * are different answers, and both used to arrive as `auth-invalid` (AI-11).
311
+ */
312
+ function tokenError(status, errorCode) {
313
+ const cause = errorCode ? {
314
+ cause: errorCode
315
+ } : {};
316
+ switch (errorCode) {
317
+ case 'session-expired':
318
+ return new AiError('session-expired', 'the session ended; sign in again', cause);
319
+ case 'network-unavailable':
320
+ return new AiError('network', 'no connection to the identity provider', cause);
321
+ case 'consent-denied':
322
+ return new AiError('not-entitled', 'access to the AI resource was denied', cause);
323
+ default:
324
+ return new AiError('auth-invalid', status === 'denied' ? 'access to the AI resource was refused' : 'could not acquire an access token', cause);
325
+ }
326
+ }
327
+ //# sourceMappingURL=aiClient.js.map