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/README.md +23 -11
- package/bunnyquery.js +139 -62
- package/dist/engine.cjs +78 -23
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +137 -35
- package/dist/engine.d.ts +137 -35
- package/dist/engine.mjs +78 -23
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/budget.ts +13 -3
- package/src/engine/config.ts +249 -39
- package/src/engine/history.ts +1 -1
- package/src/engine/requests.ts +25 -17
- package/src/engine/session.ts +8 -9
- package/src/engine/sse.ts +1 -1
package/package.json
CHANGED
package/src/engine/budget.ts
CHANGED
|
@@ -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
|
package/src/engine/config.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
/**
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
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 `
|
|
149
|
-
*
|
|
150
|
-
*
|
|
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.
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
* `
|
|
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
|
-
|
|
319
|
-
|
|
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.
|
|
332
|
-
* THE KEYS IN THAT SCHEMA, so an skapi-js predating the feature
|
|
333
|
-
* `stream` - it silently DROPS it. What
|
|
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
|
|
362
|
-
|
|
363
|
-
|
|
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. */
|
package/src/engine/history.ts
CHANGED
|
@@ -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
|
|
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.
|
package/src/engine/requests.ts
CHANGED
|
@@ -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*` ->
|
|
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
|
-
|
|
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
|
|
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
|
|
665
|
-
|
|
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
|
|
822
|
-
|
|
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(
|
|
1142
|
-
|
|
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(
|
|
1192
|
-
|
|
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
|
|
1312
|
-
|
|
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
|
|
1326
|
-
|
|
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
|
|
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
|
);
|
package/src/engine/session.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
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 =
|
|
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]
|
|
1577
|
+
console.warn('[chat-engine] forwardRequestFinalize failed', err);
|
|
1578
1578
|
});
|
|
1579
1579
|
} catch (e) {
|
|
1580
|
-
console.warn('[chat-engine]
|
|
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
|
|
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 =
|
|
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
|
|
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
|