bunnyquery 1.10.12 → 1.11.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunnyquery",
3
- "version": "1.10.12",
3
+ "version": "1.11.1",
4
4
  "description": "Embeddable BunnyQuery AI chat widget + its framework-agnostic chat engine",
5
5
  "main": "bunnyquery.js",
6
6
  "exports": {
@@ -19,6 +19,8 @@ export var CONTEXT_WINDOW_DEFAULT: Record<string, number> = { claude: 200000, op
19
19
  // 128,000 output cap, i.e. the ~922,000 of usable input quoted for it elsewhere.
20
20
  export var CONTEXT_WINDOW_BY_MODEL: Record<string, number> = {
21
21
  // claude, exact ids
22
+ 'claude-fable-5-1': 1000000, 'claude-opus-5-5': 1000000,
23
+ 'claude-sonnet-5-5': 1000000,
22
24
  'claude-fable-5': 1000000, 'claude-opus-5': 1000000,
23
25
  'claude-opus-4-8': 1000000, 'claude-opus-4-7': 1000000,
24
26
  'claude-opus-4-6': 1000000, 'claude-opus-4-5': 200000,
@@ -26,13 +28,14 @@ export var CONTEXT_WINDOW_BY_MODEL: Record<string, number> = {
26
28
  'claude-sonnet-4-5': 1000000, 'claude-sonnet-4': 200000,
27
29
  'claude-haiku-4-5': 200000, 'claude-3-5-sonnet': 200000,
28
30
  // openai, exact ids
31
+ 'gpt-6-astra': 1050000, 'gpt-6-sol': 1050000, 'gpt-6-luna': 1050000,
29
32
  'gpt-5.6-sol': 1050000, 'gpt-5.6-terra': 1050000, 'gpt-5.6-luna': 1050000,
30
33
  'gpt-5.5': 1000000, 'gpt-5.4': 1050000,
31
34
  'gpt-5.4-mini': 400000, 'gpt-5.4-nano': 400000,
32
35
  'gpt-4.1': 1040000, 'gpt-4o': 128000, 'o1': 200000, 'o1-pro': 200000,
33
36
  // family keys
34
37
  'claude-fable': 1000000, 'claude-opus': 1000000, 'claude-sonnet': 1000000,
35
- 'claude-haiku': 200000, 'gpt-5.6': 1050000, 'gpt-5': 128000,
38
+ 'claude-haiku': 200000, 'gpt-6': 1050000, 'gpt-5.6': 1050000, 'gpt-5': 128000,
36
39
  };
37
40
  // Two rows above are load-bearing rather than redundant, both because the family
38
41
  // walk drops trailing segments:
@@ -44,6 +47,10 @@ export var CONTEXT_WINDOW_BY_MODEL: Record<string, number> = {
44
47
  // matters most in practice: the indexing path selects it by name.
45
48
  // The bare 'gpt-5' family stays deliberately low: it is the catch-all for gpt-5
46
49
  // variants not listed here, and a mini-class variant is the likelier unknown.
50
+ // 'gpt-6' is a family key at the full window for the opposite reason: every
51
+ // gpt-6 tier is 1050000, luna (the smallest) included, and without the key an
52
+ // id this table has not met yet drops to the 128000 platform default, which is
53
+ // what astra, sol and luna did on the day they were added to the model list.
47
54
  //
48
55
  // One asymmetry these rows expose: a total minus our output reserve can exceed a
49
56
  // model's separately-published INPUT ceiling (gpt-5.4-nano is 400000 total but
@@ -58,23 +65,26 @@ export var CONTEXT_WINDOW_BY_MODEL: Record<string, number> = {
58
65
  // flat 25000 the request builder used to send unconditionally.
59
66
  export var MAX_OUTPUT_BY_MODEL: Record<string, number> = {
60
67
  // claude
68
+ 'claude-fable-5-1': 128000, 'claude-opus-5-5': 128000,
69
+ 'claude-sonnet-5-5': 128000,
61
70
  'claude-fable-5': 128000, 'claude-opus-5': 128000,
62
71
  'claude-opus-4-8': 128000, 'claude-sonnet-5': 128000,
63
72
  'claude-sonnet-4-6': 64000, 'claude-haiku-4-5': 64000,
64
73
  'claude-3-5-sonnet': 8000,
65
74
  // openai
75
+ 'gpt-6-astra': 128000, 'gpt-6-sol': 128000, 'gpt-6-luna': 128000,
66
76
  'gpt-5.6-sol': 128000, 'gpt-5.6-terra': 128000, 'gpt-5.6-luna': 128000,
67
77
  'gpt-5.5': 128000, 'gpt-5.4': 128000,
68
78
  'gpt-5.4-mini': 128000, 'gpt-5.4-nano': 128000,
69
79
  'gpt-4.1': 16000, 'gpt-4o': 4000, 'o1': 100000, 'o1-pro': 100000,
70
80
  // family keys
71
81
  'claude-fable': 128000, 'claude-opus': 128000, 'claude-sonnet': 64000,
72
- 'claude-haiku': 64000, 'gpt-5.6': 128000, 'gpt-5': 128000,
82
+ 'claude-haiku': 64000, 'gpt-6': 128000, 'gpt-5.6': 128000, 'gpt-5': 128000,
73
83
  };
74
84
 
75
85
  // The window a project runs at when nobody has touched the setting, clamped per
76
86
  // model by getContextWindow. Deliberately BELOW every frontier ceiling it can
77
- // resolve against (1,000,000 on the Claude 5 line, 1,050,000 on gpt-5.6) because
87
+ // resolve against (1,000,000 on the Claude 5 line, 1,050,000 on gpt-5.6 and gpt-6) because
78
88
  // the client-side budget covers only the FIRST request: after that the model
79
89
  // runs a server-side tool loop whose web_fetch and MCP results accumulate in the
80
90
  // same conversation and are not counted here. The gap (120k and 170k
@@ -4,13 +4,22 @@
4
4
  * The engine is framework- and transport-agnostic: it never imports a skapi
5
5
  * instance or `import.meta.env`. Each consumer calls `configureChatEngine()`
6
6
  * once at startup to inject the skapi transport functions, the MCP base URL,
7
- * and (optionally) the `poll` value to attach to clientSecretRequest.
7
+ * and (optionally) the `poll` value to attach to every forwarded request.
8
8
  *
9
9
  * Why `poll` is configurable: agent.vue uses the npm-bundled skapi-js and OMITS
10
- * `poll` (its clientSecretRequest auto-resolves with the final body), whereas
10
+ * `poll` (its request auto-resolves with the final body), whereas
11
11
  * the BunnyQuery widget uses the deployed skapi-js@latest and must pass
12
12
  * `poll: 0` to get the early ack + a manual `.poll()` handle (needed for queued-
13
13
  * send cancel). So the request builders include `poll` only when it is set.
14
+ *
15
+ * THE TRANSPORT HAS TWO SPELLINGS, and a host may inject either. skapi-js renamed
16
+ * clientSecretRequest and its companions to the forwardRequest family, and kept
17
+ * every old name working. So this config accepts both families of keys, prefers
18
+ * the new one, and every call goes through the resolveForwardRequest* functions
19
+ * below, which are the only code in the engine that knows there are two. The old
20
+ * keys are not a courtesy: this package is pinned with a caret range, so a host
21
+ * written against an earlier release takes this one on its next install with
22
+ * nothing but the old keys injected, and it has to keep working unchanged.
14
23
  */
15
24
 
16
25
  import { type AttachmentParser, registerAttachmentParser } from './attachment_parsers';
@@ -61,15 +70,78 @@ export interface LiveStreamUpdate {
61
70
  transport: { socket: number; poll: number };
62
71
  }
63
72
 
73
+ /** The finalize call, identical under its new and its deprecated key. */
74
+ type ForwardRequestFinalizeFn = (
75
+ requestId: string,
76
+ data: any,
77
+ options: { url: string; method: string; service?: string; owner?: string },
78
+ ) => Promise<any>;
79
+
80
+ /** The chunk read-back call, identical under its new and its deprecated key. */
81
+ type ForwardRequestStreamFn = (
82
+ requestId: string,
83
+ options: {
84
+ url: string;
85
+ method: string;
86
+ onStream?: (chunk: string, seq: number) => void;
87
+ since?: number;
88
+ poll?: number;
89
+ service?: string;
90
+ owner?: string;
91
+ },
92
+ ) => Promise<any>;
93
+
64
94
  export interface ChatEngineConfig {
65
- /** skapi.clientSecretRequest, bound to the consumer's skapi instance. */
66
- clientSecretRequest: (opts: any) => Promise<any>;
67
- /** skapi.clientSecretRequestHistory, bound to the consumer's skapi instance. */
68
- clientSecretRequestHistory: (params: any, fetchOptions: any) => Promise<any>;
95
+ /**
96
+ * skapi.forwardRequest, bound to the consumer's skapi instance.
97
+ *
98
+ * The engine always calls it as `forwardRequest(null, opts)`. Null because it
99
+ * never has a form: every request it builds carries its whole body in
100
+ * `opts.data`, the `_skapi_*` directives included, and the stored secret is
101
+ * named with `secretName`.
102
+ *
103
+ * INJECT IT ONLY FROM AN SKAPI-JS THAT HAS THE WHOLE FORWARDREQUEST FAMILY. An
104
+ * older skapi-js also has a method called forwardRequest, taking the same two
105
+ * arguments, and it is a different thing entirely: the retired streaming
106
+ * forwarder, which posts to another endpoint and never queues, so a chat turn
107
+ * handed to it goes somewhere else and comes back as nothing the engine can
108
+ * read. A host that cannot be sure which skapi-js it holds (the widget, which
109
+ * is given the embedder's) decides by a companion only the new SDK has,
110
+ * `forwardRequestHistory`, and never by this method's presence.
111
+ *
112
+ * This or the deprecated `clientSecretRequest` is required; with both, this
113
+ * one is used. See resolveForwardRequest.
114
+ */
115
+ forwardRequest?: (form: any, opts: any) => Promise<any>;
116
+ /**
117
+ * skapi.forwardRequestHistory, bound to the consumer's skapi instance. This or
118
+ * the deprecated `clientSecretRequestHistory` is required; with both, this one
119
+ * is used. Same arguments either way.
120
+ */
121
+ forwardRequestHistory?: (params: any, fetchOptions: any) => Promise<any>;
122
+ /**
123
+ * @deprecated Inject `forwardRequest` instead.
124
+ *
125
+ * skapi.clientSecretRequest, bound to the consumer's skapi instance. Still
126
+ * accepted as a COMPATIBILITY CONTRACT, not a leftover: a host that injects
127
+ * only the old keys must keep working on this release. Used only when
128
+ * `forwardRequest` is absent, and handed the engine's request with `secretName`
129
+ * moved back to `clientSecretName`, which is exactly the request it was handed
130
+ * before the rename (see resolveForwardRequest).
131
+ */
132
+ clientSecretRequest?: (opts: any) => Promise<any>;
133
+ /**
134
+ * @deprecated Inject `forwardRequestHistory` instead.
135
+ *
136
+ * skapi.clientSecretRequestHistory, bound to the consumer's skapi instance.
137
+ * Kept for the same compatibility contract as `clientSecretRequest`, and used
138
+ * only when `forwardRequestHistory` is absent.
139
+ */
140
+ clientSecretRequestHistory?: (params: any, fetchOptions: any) => Promise<any>;
69
141
  /** MCP server base URL (prod vs dev resolved by the consumer). */
70
142
  mcpBaseUrl: string;
71
143
  /**
72
- * Value to attach as `poll` on every clientSecretRequest. When `undefined`
144
+ * Value to attach as `poll` on every forwarded request. When `undefined`
73
145
  * the `poll` key is omitted entirely (agent.vue). BunnyQuery sets `0`.
74
146
  */
75
147
  poll?: number;
@@ -145,9 +217,10 @@ export interface ChatEngineConfig {
145
217
  * answer. So it stays off until the worker is deployed, then flips per
146
218
  * environment.
147
219
  *
148
- * It also needs `clientSecretRequestFinalize` below: without it a streamed
149
- * turn is never finalized, so its row keeps a status and no body forever and
150
- * a later history load shows the question with an empty answer.
220
+ * It also needs `forwardRequestFinalize` below (or its deprecated name,
221
+ * `clientSecretRequestFinalize`): without it a streamed turn is never
222
+ * finalized, so its row keeps a status and no body forever and a later
223
+ * history load shows the question with an empty answer.
151
224
  */
152
225
  liveStreaming?: boolean;
153
226
  /**
@@ -165,24 +238,30 @@ export interface ChatEngineConfig {
165
238
  */
166
239
  liveStreamingRealtime?: boolean;
167
240
  /**
168
- * skapi.clientSecretRequestFinalize, bound to the consumer's skapi instance.
241
+ * skapi.forwardRequestFinalize, bound to the consumer's skapi instance.
169
242
  * Stores the version of a streamed turn that history should keep (the engine
170
243
  * sends the ASSEMBLED provider body, so history reads it exactly as it reads
171
244
  * a buffered turn) and releases that request's chunks. Optional: a host
172
245
  * without it can still stream, it just leaves the chunks and an empty row.
246
+ * Takes precedence over the deprecated `clientSecretRequestFinalize`.
173
247
  */
174
- clientSecretRequestFinalize?: (
175
- requestId: string,
176
- data: any,
177
- options: { url: string; method: string; service?: string; owner?: string },
178
- ) => Promise<any>;
248
+ forwardRequestFinalize?: ForwardRequestFinalizeFn;
179
249
  /**
180
- * skapi.clientSecretRequestStream, bound to the consumer's skapi instance.
250
+ * @deprecated Inject `forwardRequestFinalize` instead.
251
+ *
252
+ * skapi.clientSecretRequestFinalize, the same call under its old name. Kept
253
+ * for the compatibility contract `clientSecretRequest` describes, and used
254
+ * only when `forwardRequestFinalize` is absent.
255
+ */
256
+ clientSecretRequestFinalize?: ForwardRequestFinalizeFn;
257
+ /**
258
+ * skapi.forwardRequestStream, bound to the consumer's skapi instance. Takes
259
+ * precedence over the deprecated `clientSecretRequestStream`.
181
260
  *
182
261
  * THE SECOND HALF OF THE DURABILITY GUARANTEE, and without it a streamed turn
183
262
  * is only as durable as the tab that started it. A streamed row settles with a
184
263
  * status and NO body; the answer is stored as chunks until
185
- * clientSecretRequestFinalize says what to keep. A row that settles while no
264
+ * forwardRequestFinalize says what to keep. A row that settles while no
186
265
  * poll is attached (the user closed the tab, a mobile browser discarded it,
187
266
  * the device slept and the interval stopped) is therefore never finalized, and
188
267
  * a later history load sees a terminal row with no body and used to emit no
@@ -204,18 +283,16 @@ export interface ChatEngineConfig {
204
283
  * NOTE THAT THIS HOOK, NOT `liveStreaming`, IS WHAT ARMS RECOVERY. See
205
284
  * streamRecoveryEnabled() below for why the two decisions are separate.
206
285
  */
207
- clientSecretRequestStream?: (
208
- requestId: string,
209
- options: {
210
- url: string;
211
- method: string;
212
- onStream?: (chunk: string, seq: number) => void;
213
- since?: number;
214
- poll?: number;
215
- service?: string;
216
- owner?: string;
217
- },
218
- ) => Promise<any>;
286
+ forwardRequestStream?: ForwardRequestStreamFn;
287
+ /**
288
+ * @deprecated Inject `forwardRequestStream` instead.
289
+ *
290
+ * skapi.clientSecretRequestStream, the same call under its old name. Kept for
291
+ * the compatibility contract `clientSecretRequest` describes, and used only
292
+ * when `forwardRequestStream` is absent. It arms recovery exactly as the new
293
+ * key does.
294
+ */
295
+ clientSecretRequestStream?: ForwardRequestStreamFn;
219
296
  /**
220
297
  * Observation hook for a live-streaming turn, called at most once per paint
221
298
  * (about once a second) plus once when the turn settles.
@@ -235,7 +312,8 @@ export interface ChatEngineConfig {
235
312
  * reader is injected.
236
313
  *
237
314
  * There is no need to set it to turn recovery ON: injecting
238
- * `clientSecretRequestStream` is what arms it (see streamRecoveryEnabled).
315
+ * `forwardRequestStream` (or its deprecated name, `clientSecretRequestStream`)
316
+ * is what arms it (see streamRecoveryEnabled).
239
317
  * This exists only as the way back out for a host that wants byte-for-byte the
240
318
  * pre-recovery rendering of a terminal-but-empty row - no bubble, no marker, no
241
319
  * chunk read - while keeping the reader available for its own use. Omit it and
@@ -271,6 +349,108 @@ export function chatEngineConfig(): ChatEngineConfig {
271
349
  return _config;
272
350
  }
273
351
 
352
+ /* ---- the transport, resolved from whichever family the host injected ------------
353
+ *
354
+ * One resolver per operation, and nothing else in the engine reads the transport
355
+ * keys. Each prefers the forwardRequest key and falls back to the deprecated
356
+ * clientSecretRequest one, and each reads the config at CALL time rather than
357
+ * caching: a host may inject through a getter (agent.vue's finalize and stream hooks
358
+ * are getters, so its capability probe runs on first use) or re-configure, and the
359
+ * request has to go through whatever is there when it leaves.
360
+ *
361
+ * A key counts only when it holds a FUNCTION. `forwardRequestFinalize: undefined`
362
+ * beside a working old key is a host saying "not this one", and the old key answers.
363
+ */
364
+
365
+ /**
366
+ * Sends one request the engine built, through whichever family the host injected.
367
+ *
368
+ * Every builder in requests.ts produces the NEW spelling, `secretName`, and hands the
369
+ * options here. A host that injected `forwardRequest` gets `forwardRequest(null,
370
+ * opts)`: null because the engine never has a form, the whole body is already in
371
+ * `opts.data`. A host that injected only the deprecated `clientSecretRequest` gets it
372
+ * with `secretName` moved back to `clientSecretName` and placed first, which is key
373
+ * for key, in the same order, the request it was handed before the rename. Nothing
374
+ * else about the options is touched on either path: `service`, `owner`, `queue`,
375
+ * `poll`, the stream flags, `headers` and `data` pass through as the builder made them.
376
+ *
377
+ * THE RENAME LIVES HERE AND NOWHERE ELSE. Done at each call site it is six copies of
378
+ * one translation, and the copy that drifts sends a request naming no secret at all.
379
+ * The old SDK refuses that; the new one, where the secret is optional, forwards it
380
+ * WITHOUT resolving anything, so the provider receives a literal "$CLIENT_SECRET" as
381
+ * its api key and the turn fails at the far end with nothing here to say why.
382
+ *
383
+ * Called as a method of the config, as the engine always called the transport, so a
384
+ * host that relied on `this` sees what it saw before.
385
+ *
386
+ * Throws when the host injected neither, with a message that names the fix. The
387
+ * builders that call this are async, so it reaches the caller as a rejected request,
388
+ * which is the shape the old "is not a function" TypeError had.
389
+ */
390
+ export function resolveForwardRequest(): (opts: any) => Promise<any> {
391
+ const cfg = chatEngineConfig();
392
+ const fwd = cfg.forwardRequest;
393
+ if (typeof fwd === 'function') {
394
+ return (opts: any) => fwd.call(cfg, null, opts);
395
+ }
396
+ const legacy = cfg.clientSecretRequest;
397
+ if (typeof legacy === 'function') {
398
+ return (opts: any) => {
399
+ if (!opts || typeof opts !== 'object' || !('secretName' in opts)) return legacy.call(cfg, opts);
400
+ const { secretName, ...rest } = opts;
401
+ return legacy.call(cfg, Object.assign({ clientSecretName: secretName }, rest));
402
+ };
403
+ }
404
+ throw new Error(
405
+ '[chat-engine] No request transport: configureChatEngine() needs forwardRequest '
406
+ + '(or the deprecated clientSecretRequest), bound to a skapi instance.',
407
+ );
408
+ }
409
+
410
+ /**
411
+ * Lists chat history through whichever family the host injected. The two names take
412
+ * the same arguments and return the same thing, so this is a pure preference with no
413
+ * translation. Throws, like resolveForwardRequest, when the host injected neither.
414
+ */
415
+ export function resolveForwardRequestHistory(): (params: any, fetchOptions: any) => Promise<any> {
416
+ const cfg = chatEngineConfig();
417
+ const hist = typeof cfg.forwardRequestHistory === 'function'
418
+ ? cfg.forwardRequestHistory
419
+ : cfg.clientSecretRequestHistory;
420
+ if (typeof hist === 'function') {
421
+ return (params: any, fetchOptions: any) => hist.call(cfg, params, fetchOptions);
422
+ }
423
+ throw new Error(
424
+ '[chat-engine] No history transport: configureChatEngine() needs forwardRequestHistory '
425
+ + '(or the deprecated clientSecretRequestHistory), bound to a skapi instance.',
426
+ );
427
+ }
428
+
429
+ /**
430
+ * The finalize hook, from whichever family the host injected, or undefined when it
431
+ * injected neither. Optional by design (a host without it still streams, it just
432
+ * leaves the chunks), so absence is an answer here and not an error. Returned
433
+ * unbound, because the engine has always called it as a plain function.
434
+ */
435
+ export function resolveForwardRequestFinalize(): ForwardRequestFinalizeFn | undefined {
436
+ const cfg = chatEngineConfig();
437
+ if (typeof cfg.forwardRequestFinalize === 'function') return cfg.forwardRequestFinalize;
438
+ if (typeof cfg.clientSecretRequestFinalize === 'function') return cfg.clientSecretRequestFinalize;
439
+ return undefined;
440
+ }
441
+
442
+ /**
443
+ * The chunk reader, from whichever family the host injected, or undefined when it
444
+ * injected neither. Optional, and its presence is what arms recovery (see
445
+ * streamRecoveryEnabled), so both spellings arm it alike. Returned unbound, as above.
446
+ */
447
+ export function resolveForwardRequestStream(): ForwardRequestStreamFn | undefined {
448
+ const cfg = chatEngineConfig();
449
+ if (typeof cfg.forwardRequestStream === 'function') return cfg.forwardRequestStream;
450
+ if (typeof cfg.clientSecretRequestStream === 'function') return cfg.clientSecretRequestStream;
451
+ return undefined;
452
+ }
453
+
274
454
  /** True when the consumer has opted in to server-driven windowed indexing. */
275
455
  export function windowedIndexingEnabled(): boolean {
276
456
  return _config?.windowedIndexing === true;
@@ -313,10 +493,18 @@ export function liveStreamingEnabled(): boolean {
313
493
  * whose body the worker never managed to spill). That read finds no chunks, the
314
494
  * bubble is dropped, and the list looks exactly as it did before. Set
315
495
  * `streamRecovery: false` to opt out of even that.
496
+ *
497
+ * The reader is asked for through resolveForwardRequestStream, so either spelling of
498
+ * it arms recovery: `forwardRequestStream`, or the deprecated
499
+ * `clientSecretRequestStream` a host written before the rename still injects. Asked
500
+ * for the new key alone, this would quietly disarm recovery for every such host on
501
+ * its next install, which is the stranding described above arriving by a rename.
316
502
  */
317
503
  export function streamRecoveryEnabled(): boolean {
318
- if (_config?.streamRecovery === false) return false;
319
- return typeof _config?.clientSecretRequestStream === 'function';
504
+ // Unconfigured is "no reader", never a throw: this is asked on render paths.
505
+ if (!_config) return false;
506
+ if (_config.streamRecovery === false) return false;
507
+ return typeof resolveForwardRequestStream() === 'function';
320
508
  }
321
509
 
322
510
  /**
@@ -328,9 +516,10 @@ export function streamRecoveryEnabled(): boolean {
328
516
  * streamed turn carries TWO `stream` flags: skapi's (relay the destination's bytes
329
517
  * into the chunk table) and the DESTINATION's own field inside `data`, which
330
518
  * BunnyQuery is the party that sets, because skapi relays bytes and knows no
331
- * vendor. clientSecretRequest validates its params against a schema and KEEPS ONLY
332
- * THE KEYS IN THAT SCHEMA, so an skapi-js predating the feature does not reject
333
- * `stream` - it silently DROPS it. What ships is then the exact split
519
+ * vendor. skapi validates the request's params against a schema and KEEPS ONLY
520
+ * THE KEYS IN THAT SCHEMA, so an skapi-js predating the feature (whose only door
521
+ * was clientSecretRequest) does not reject `stream` - it silently DROPS it. What
522
+ * ships is then the exact split
334
523
  * chatStreamWiring exists to make impossible: the destination is asked to answer in
335
524
  * SSE frames, skapi waits and stores the whole transcript on the row, and
336
525
  * extractClaudeText / extractOpenAIText read a wall of `data: {...}` lines where a
@@ -356,11 +545,32 @@ export function streamRecoveryEnabled(): boolean {
356
545
  * able to fetch it. There is no cheap DIRECT probe of the schema: the only way to
357
546
  * learn that `stream` was dropped is to send a real request and read an empty
358
547
  * answer, which is the bug itself.
548
+ *
549
+ * EITHER SPELLING OF THE PAIR ANSWERS YES, and it has to be a whole pair. skapi-js
550
+ * renamed the two to forwardRequestStream and forwardRequestFinalize and kept the old
551
+ * names as aliases, so three SDKs are out there: the renamed one carries both pairs,
552
+ * one from the streaming release up to the rename carries only the old pair, and one
553
+ * that predates streaming carries neither. Accepting the old pair is what keeps an
554
+ * embed page pinned between the two releases streaming; accepting the new pair is
555
+ * what keeps this answering yes once the old aliases are finally removed. The
556
+ * reasoning above holds for both: each pair shipped in the same change as, or after,
557
+ * the `stream` key, and each IS the capability rather than a proxy for it. A mixed
558
+ * half of each is not a pair and is refused, because no real SDK has that shape and
559
+ * the engine would still be missing one half of the protocol under the name it asks
560
+ * for.
561
+ *
562
+ * A presence probe is safe for these two names in a way it is NOT for forwardRequest
563
+ * itself: no skapi-js ever shipped a forwardRequestStream or forwardRequestFinalize
564
+ * that meant something else, while an older forwardRequest exists and is the retired
565
+ * streaming forwarder. That is why the widget's family probe asks for
566
+ * forwardRequestHistory instead.
359
567
  */
360
568
  export function skapiSupportsStreaming(sk: any): boolean {
361
- return !!sk
362
- && typeof sk.clientSecretRequestStream === 'function'
363
- && typeof sk.clientSecretRequestFinalize === 'function';
569
+ if (!sk) return false;
570
+ return (typeof sk.forwardRequestStream === 'function'
571
+ && typeof sk.forwardRequestFinalize === 'function')
572
+ || (typeof sk.clientSecretRequestStream === 'function'
573
+ && typeof sk.clientSecretRequestFinalize === 'function');
364
574
  }
365
575
 
366
576
  /** Spread helper: `{ ...pollOpt() }` adds `poll` only when configured. */
@@ -745,7 +745,7 @@ export function mapHistoryListToMessages(list: any[], platform: 'claude' | 'open
745
745
  // Read as "empty" (which is what the `assistantText` guard further down did),
746
746
  // such a row produces NO assistant bubble at all and the answer is simply gone
747
747
  // from the conversation, with every byte of it still sitting in the chunk
748
- // table, reachable through clientSecretRequestStream. So it is marked instead,
748
+ // table, reachable through forwardRequestStream. So it is marked instead,
749
749
  // and the two things that act on the mark are the merge (an unknown answer
750
750
  // never overwrites a known one) and the recovery (an unknown answer is
751
751
  // resolved by reading the chunks back). See ChatMessage._streamPending.
@@ -3,7 +3,11 @@
3
3
  *
4
4
  * Ported from www.skapi.com/src/code/ai_agent.ts. The only changes vs the
5
5
  * original are dependency-injection seams:
6
- * - `skapi.clientSecretRequest*` -> chatEngineConfig().clientSecretRequest*
6
+ * - `skapi.clientSecretRequest*` -> resolveForwardRequest*() in config.ts,
7
+ * which dispatches through the host's
8
+ * `forwardRequest*` keys, or the deprecated
9
+ * `clientSecretRequest*` ones a host written
10
+ * before the rename still injects
7
11
  * - MCP endpoint URL -> chatEngineConfig().mcpBaseUrl
8
12
  * - `poll` on each request -> pollOpt() (set per consumer; see config.ts)
9
13
  * - Vue `reactive`/`ref` removed (bgTaskQueue/agentViewMounted are app-level
@@ -12,7 +16,7 @@
12
16
  */
13
17
  import { buildIndexingSystemPrompt, buildIndexingUserMessage, buildIndexingContinueMessage, buildIndexingRenderMessage, buildIndexingRenderContinueTemplate, buildIndexingWindowMessage } from './prompts';
14
18
  import { isServerExtractable, isPagedReadFile, isImageVisionFile, isWindowedReadFile, makeExtractPlaceholder, makeRenderPlaceholder, makeWindowPlaceholder, RENDER_PAGES_PER_WINDOW, type ExtractDirective, type FileUrlDirective } from './office';
15
- import { chatEngineConfig, pollOpt, windowedIndexingEnabled, liveStreamingEnabled, liveStreamingRealtimeEnabled } from './config';
19
+ import { chatEngineConfig, resolveForwardRequest, resolveForwardRequestHistory, pollOpt, windowedIndexingEnabled, liveStreamingEnabled, liveStreamingRealtimeEnabled } from './config';
16
20
  // Output sizing lives in budget.ts so the request cap and the reserve the input
17
21
  // budget subtracts cannot drift; getMaxOutputTokens also clamps per model.
18
22
  import { getMaxOutputTokens, getModelContextWindow } from './budget';
@@ -134,7 +138,11 @@ function mcpEndpointFor(
134
138
  const project = publicProjectId || service;
135
139
  return { url: String(mcpUrl()).replace(/\/+$/, '') + '/p/' + project };
136
140
  }
137
- const clientSecretRequest = (opts: any) => chatEngineConfig().clientSecretRequest(opts);
141
+ // Every request below is built with `secretName` and sent through here. Which of
142
+ // the host's two transport families it reaches, and the rename back to
143
+ // `clientSecretName` for a host on the old one, is decided in resolveForwardRequest
144
+ // and nowhere else, so no builder ever spells the old name.
145
+ const forwardRequest = (opts: any) => resolveForwardRequest()(opts);
138
146
 
139
147
  /**
140
148
  * THE two `stream` flags of a streamed chat turn, produced together or not at all.
@@ -164,7 +172,7 @@ const clientSecretRequest = (opts: any) => chatEngineConfig().clientSecretReques
164
172
  * that drifts when someone edits one arm.
165
173
  */
166
174
  export type ChatStreamWiring = {
167
- /** Spread into the clientSecretRequest OPTIONS (skapi's relay switch). `realtime`
175
+ /** Spread into the forwardRequest OPTIONS (skapi's relay switch). `realtime`
168
176
  * belongs here and never in `body`: it is skapi's, not the destination's. */
169
177
  transport: { stream?: true; realtime?: true };
170
178
  /** Spread into `data` (the destination's own switch). */
@@ -661,8 +669,8 @@ export async function callClaudeWithMcp({
661
669
  // on the bg queue) is recognised and left buffered.
662
670
  const stream = chatStreamWiring(userId || service);
663
671
 
664
- return clientSecretRequest({
665
- clientSecretName: 'claude',
672
+ return forwardRequest({
673
+ secretName: 'claude',
666
674
  queue: userId || service,
667
675
  service,
668
676
  owner,
@@ -818,8 +826,8 @@ export async function callOpenAIWithPublicMcp(
818
826
  // ONE decision, spread in TWO places - see chatStreamWiring.
819
827
  const stream = chatStreamWiring(userId || service);
820
828
 
821
- return clientSecretRequest({
822
- clientSecretName: 'openai',
829
+ return forwardRequest({
830
+ secretName: 'openai',
823
831
  queue: userId || service,
824
832
  service,
825
833
  owner,
@@ -1138,8 +1146,8 @@ export async function notifyAgentSaveAttachment(info: AttachmentSaveInfo) {
1138
1146
  if (platform === 'openai') {
1139
1147
  const resolvedModel = info.model || DEFAULT_OPENAI_MODEL;
1140
1148
  const imageDetail = getOpenAIImageDetail(resolvedModel);
1141
- return tapDispatchFailure(clientSecretRequest({
1142
- clientSecretName: 'openai',
1149
+ return tapDispatchFailure(forwardRequest({
1150
+ secretName: 'openai',
1143
1151
  queue: bgIndexingQueueName(info.userId, service),
1144
1152
  service,
1145
1153
  owner,
@@ -1188,8 +1196,8 @@ export async function notifyAgentSaveAttachment(info: AttachmentSaveInfo) {
1188
1196
  }
1189
1197
 
1190
1198
  const resolvedModel = info.model || DEFAULT_CLAUDE_MODEL;
1191
- return tapDispatchFailure(clientSecretRequest({
1192
- clientSecretName: 'claude',
1199
+ return tapDispatchFailure(forwardRequest({
1200
+ secretName: 'claude',
1193
1201
  queue: bgIndexingQueueName(info.userId, service),
1194
1202
  service,
1195
1203
  owner,
@@ -1308,8 +1316,8 @@ export function extractOpenAIText(response: any) {
1308
1316
  // not even a reader to hand chunks to. chatStreamWiring is deliberately not called
1309
1317
  // here - the pair is chat-turn-only.
1310
1318
  export async function listClaudeModels(service: string, owner: string) {
1311
- return clientSecretRequest({
1312
- clientSecretName: 'claude',
1319
+ return forwardRequest({
1320
+ secretName: 'claude',
1313
1321
  service,
1314
1322
  owner,
1315
1323
  url: ANTHROPIC_MODELS_API_URL,
@@ -1322,8 +1330,8 @@ export async function listClaudeModels(service: string, owner: string) {
1322
1330
  }
1323
1331
 
1324
1332
  export async function listOpenAIModels(service: string, owner: string) {
1325
- return clientSecretRequest({
1326
- clientSecretName: 'openai',
1333
+ return forwardRequest({
1334
+ secretName: 'openai',
1327
1335
  service,
1328
1336
  owner,
1329
1337
  url: OPENAI_MODELS_API_URL,
@@ -1544,7 +1552,7 @@ export async function getChatHistory(
1544
1552
  params.queue_exclude ? { queue_exclude: params.queue_exclude } : {},
1545
1553
  );
1546
1554
 
1547
- return chatEngineConfig().clientSecretRequestHistory(
1555
+ return resolveForwardRequestHistory()(
1548
1556
  p as { url: string; method: 'POST'; queue?: string; status?: string },
1549
1557
  Object.assign({ ascending: false, limit: CHAT_HISTORY_PAGE_LIMIT }, fetchOptions),
1550
1558
  );
@@ -42,7 +42,7 @@ import {
42
42
  type BgTaskEntry,
43
43
  } from './requests';
44
44
  import { isPagedReadFile, isImageVisionFile, isWindowedReadFile } from './office';
45
- import { windowedIndexingEnabled, liveStreamingEnabled, streamRecoveryEnabled, chatEngineConfig } from './config';
45
+ import { windowedIndexingEnabled, liveStreamingEnabled, streamRecoveryEnabled, chatEngineConfig, resolveForwardRequestFinalize, resolveForwardRequestStream } from './config';
46
46
  // The wire-format knowledge. skapi relays the provider's bytes without reading them,
47
47
  // so the session hands them straight to this parser and never inspects a frame itself.
48
48
  import { createSseParser, type SseParser } from './sse';
@@ -398,7 +398,7 @@ export function typewriterResumeIndex(
398
398
  * because the row's status describes the destination's request and not our read
399
399
  * of it. Anything short of a finished answer leaves the chunks exactly where they
400
400
  * are, which is what they are for: the turn stays re-readable through
401
- * clientSecretRequestStream and a later load recovers it in full.
401
+ * forwardRequestStream and a later load recovers it in full.
402
402
  *
403
403
  * `unframed` is the one exception to needing a terminal event, and it is not a
404
404
  * loophole: bytes that were never SSE carry no events at all and none is ever
@@ -1049,7 +1049,7 @@ export class ChatSession {
1049
1049
  // Both wrappers are needed and neither is redundant: a poll attached to a
1050
1050
  // HISTORY ITEM honours the onResponse passed here (that is how the re-attach
1051
1051
  // path resolves a turn), while a poll attached to a fresh DISPATCH ack takes
1052
- // its callbacks from the original clientSecretRequest and reports only
1052
+ // its callbacks from the original forwardRequest and reports only
1053
1053
  // through the promise. Substituting in one place would leave the other
1054
1054
  // reading the envelope.
1055
1055
  var streamOpts = Object.assign({}, inner, {
@@ -1566,7 +1566,7 @@ export class ChatSession {
1566
1566
  private _finalizeStreamedTurn(st: LiveStreamState): void {
1567
1567
  if (st.finalized) return;
1568
1568
  if (!this._mayFinalize(st)) return;
1569
- var fin = chatEngineConfig().clientSecretRequestFinalize;
1569
+ var fin = resolveForwardRequestFinalize();
1570
1570
  if (!fin || st.finalBody == null) return;
1571
1571
  st.finalized = true;
1572
1572
  var url = st.platform === 'openai' ? OPENAI_RESPONSES_API_URL : ANTHROPIC_MESSAGES_API_URL;
@@ -1574,10 +1574,10 @@ export class ChatSession {
1574
1574
  Promise.resolve(fin(st.id, st.finalBody, {
1575
1575
  url: url, method: 'POST', service: st.projectId, owner: st.owner,
1576
1576
  })).catch(function (err: any) {
1577
- console.warn('[chat-engine] clientSecretRequestFinalize failed', err);
1577
+ console.warn('[chat-engine] forwardRequestFinalize failed', err);
1578
1578
  });
1579
1579
  } catch (e) {
1580
- console.warn('[chat-engine] clientSecretRequestFinalize threw', e);
1580
+ console.warn('[chat-engine] forwardRequestFinalize threw', e);
1581
1581
  }
1582
1582
  }
1583
1583
 
@@ -1888,8 +1888,7 @@ export class ChatSession {
1888
1888
  }
1889
1889
 
1890
1890
  private _readBackStreamedTurn(itemId: string, ownerKey: string, platform: 'claude' | 'openai', projectId: string, owner: string, manual?: boolean): Promise<void> {
1891
- var cfg = chatEngineConfig();
1892
- var read = cfg.clientSecretRequestStream;
1891
+ var read = resolveForwardRequestStream();
1893
1892
  if (!read || !itemId) return Promise.resolve();
1894
1893
  // IN FLIGHT is the only refusal a user-driven retry accepts, and it is about
1895
1894
  // this request rather than about the history of the row: two reads of the same
@@ -2097,7 +2096,7 @@ export class ChatSession {
2097
2096
  // to go back for the missing part.
2098
2097
  if (!degraded) delete this._rec().incomplete[itemId];
2099
2098
  if (!store || body == null || isErr) return;
2100
- var fin = chatEngineConfig().clientSecretRequestFinalize;
2099
+ var fin = resolveForwardRequestFinalize();
2101
2100
  if (!fin) return;
2102
2101
  var url = platform === 'openai' ? OPENAI_RESPONSES_API_URL : ANTHROPIC_MESSAGES_API_URL;
2103
2102
  try {
package/src/engine/sse.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * buffered call would have returned out.
4
4
  *
5
5
  * WHY THIS FILE EXISTS, AND WHY IT IS HERE AND NOT IN SKAPI.
6
- * skapi's clientSecretRequest is a byte relay. On a streamed turn the worker reads
6
+ * skapi's forwardRequest is a byte relay. On a streamed turn the worker reads
7
7
  * the destination's response incrementally and appends the raw bytes to a chunk
8
8
  * table; it settles the polling row with STATUS ONLY, no body, because the content
9
9
  * lives in the chunks. skapi therefore does not know that Anthropic or OpenAI