bunnyquery 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,682 @@
1
+ /**
2
+ * AI request builders + dispatch transport (framework-agnostic).
3
+ *
4
+ * Ported from www.skapi.com/src/code/ai_agent.ts. The only changes vs the
5
+ * original are dependency-injection seams:
6
+ * - `skapi.clientSecretRequest*` -> chatEngineConfig().clientSecretRequest*
7
+ * - MCP endpoint URL -> chatEngineConfig().mcpBaseUrl
8
+ * - `poll` on each request -> pollOpt() (set per consumer; see config.ts)
9
+ * - Vue `reactive`/`ref` removed (bgTaskQueue/agentViewMounted are app-level
10
+ * state that stays in the consumer, not here;
11
+ * only the BgTaskEntry TYPE lives here)
12
+ */
13
+ import { buildIndexingSystemPrompt, buildIndexingUserMessage } from './prompts';
14
+ import { isOfficeFile, makeExtractPlaceholder, type ExtractDirective } from './office';
15
+ import { chatEngineConfig, pollOpt } from './config';
16
+
17
+ export const ANTHROPIC_MESSAGES_API_URL = 'https://api.anthropic.com/v1/messages';
18
+ const ANTHROPIC_MODELS_API_URL = 'https://api.anthropic.com/v1/models';
19
+ const ANTHROPIC_VERSION = '2023-06-01';
20
+ const ANTHROPIC_MCP_BETA = 'mcp-client-2025-11-20';
21
+ const ANTHROPIC_WEB_FETCH_BETA = 'web-fetch-2025-09-10';
22
+ const ANTHROPIC_PROMPT_CACHING_BETA = 'prompt-caching-2024-07-31';
23
+ const ANTHROPIC_BETA_HEADER = `${ANTHROPIC_MCP_BETA},${ANTHROPIC_WEB_FETCH_BETA},${ANTHROPIC_PROMPT_CACHING_BETA}`;
24
+ const WEB_FETCH_MAX_USES = 40;
25
+ const WEB_FETCH_MAX_CONTENT_TOKENS = 200000;
26
+
27
+ export const OPENAI_RESPONSES_API_URL = 'https://api.openai.com/v1/responses';
28
+ const OPENAI_MODELS_API_URL = 'https://api.openai.com/v1/models';
29
+ const MAX_TOKENS = 25000;
30
+ const DEFAULT_OPENAI_IMAGE_DETAIL = 'auto';
31
+ const OPENAI_WEB_SEARCH_ENABLED = true;
32
+ const OPENAI_WEB_SEARCH_EXTERNAL_WEB_ACCESS = true;
33
+ export const MCP_NAME = 'BunnyQuery';
34
+
35
+ export const DEFAULT_CLAUDE_MODEL = 'claude-sonnet-4-6';
36
+ export const DEFAULT_OPENAI_MODEL = 'gpt-5.4';
37
+
38
+ const mcpUrl = () => chatEngineConfig().mcpBaseUrl;
39
+ const clientSecretRequest = (opts: any) => chatEngineConfig().clientSecretRequest(opts);
40
+
41
+ const getOpenAIImageDetail = (model?: string) => {
42
+ const normalized = (model || DEFAULT_OPENAI_MODEL).trim().toLowerCase();
43
+ const match = normalized.match(/^gpt-(\d+)(?:\.(\d+))?$/);
44
+ if (!match) {
45
+ return DEFAULT_OPENAI_IMAGE_DETAIL;
46
+ }
47
+
48
+ const major = Number(match[1]);
49
+ const minor = match[2] === undefined ? null : Number(match[2]);
50
+
51
+ if (major > 5) {
52
+ return 'original';
53
+ }
54
+
55
+ if (major === 5 && minor !== null && minor >= 4) {
56
+ return 'original';
57
+ }
58
+
59
+ return DEFAULT_OPENAI_IMAGE_DETAIL;
60
+ };
61
+
62
+ export type ClaudeRole = 'user' | 'assistant';
63
+
64
+ export type ClaudeMessage = {
65
+ role: ClaudeRole;
66
+ content: string;
67
+ };
68
+
69
+ export type OpenAIMessage = {
70
+ role: ClaudeRole;
71
+ content: string;
72
+ };
73
+
74
+ export type ClaudeMcpToolConfig = {
75
+ enabled?: boolean;
76
+ defer_loading?: boolean;
77
+ };
78
+
79
+ export type ClaudeMcpServerRequest = {
80
+ name: string;
81
+ url: string;
82
+ authorizationToken?: string;
83
+ defaultConfig?: ClaudeMcpToolConfig;
84
+ configs?: Record<string, ClaudeMcpToolConfig>;
85
+ };
86
+
87
+ const IMAGE_URL_REGEX =
88
+ /\bhttps?:\/\/[^\s<>"'()\[\]]+?\.(?:jpg|jpeg|png|gif|webp)(?:\?[^\s<>"'()\[\]]*)?/gi;
89
+
90
+ export function transformContentWithImages(
91
+ content: string,
92
+ ): string | Array<Record<string, any>> {
93
+ if (typeof content !== 'string' || !content) {
94
+ return content;
95
+ }
96
+
97
+ const matches = content.match(IMAGE_URL_REGEX);
98
+
99
+ if (!matches || !matches.length) {
100
+ return content;
101
+ }
102
+
103
+ const seen = new Set<string>();
104
+ const imageBlocks: Array<Record<string, any>> = [];
105
+
106
+ for (const url of matches) {
107
+ if (seen.has(url)) continue;
108
+ seen.add(url);
109
+ imageBlocks.push({
110
+ type: 'image',
111
+ source: { type: 'url', url },
112
+ });
113
+ }
114
+
115
+ return [...imageBlocks, { type: 'text', text: content }];
116
+ }
117
+
118
+ function prepareClaudeMessages(messages: ClaudeMessage[]) {
119
+ if (!messages.length) return messages;
120
+ // Only transform the most recent user message. Historical user messages
121
+ // may reference image URLs that are now stale (deleted, moved, expired).
122
+ const lastIndex = messages.length - 1;
123
+ const last = messages[lastIndex];
124
+ if (last.role !== 'user') return messages;
125
+ const content = transformContentWithImages(last.content);
126
+ if (content === last.content) return messages;
127
+ const next = messages.slice();
128
+ next[lastIndex] = { role: last.role, content } as unknown as ClaudeMessage;
129
+ return next;
130
+ }
131
+
132
+ export function transformContentWithOpenAIImages(
133
+ content: string,
134
+ detail = DEFAULT_OPENAI_IMAGE_DETAIL,
135
+ ): string | Array<Record<string, any>> {
136
+ if (typeof content !== 'string' || !content) {
137
+ return content;
138
+ }
139
+
140
+ const matches = content.match(IMAGE_URL_REGEX);
141
+
142
+ if (!matches || !matches.length) {
143
+ return content;
144
+ }
145
+
146
+ const seen = new Set<string>();
147
+ const imageBlocks: Array<Record<string, any>> = [];
148
+
149
+ for (const url of matches) {
150
+ if (seen.has(url)) continue;
151
+ seen.add(url);
152
+ imageBlocks.push({
153
+ type: 'input_image',
154
+ image_url: url,
155
+ detail,
156
+ });
157
+ }
158
+
159
+ return [{ type: 'input_text', text: content }, ...imageBlocks];
160
+ }
161
+
162
+ function prepareOpenAIMessages(
163
+ messages: OpenAIMessage[],
164
+ detail = DEFAULT_OPENAI_IMAGE_DETAIL,
165
+ ) {
166
+ if (!messages.length) return messages;
167
+ const lastIndex = messages.length - 1;
168
+ const last = messages[lastIndex];
169
+ if (last.role !== 'user') return messages;
170
+ const content = transformContentWithOpenAIImages(last.content, detail);
171
+ if (content === last.content) return messages;
172
+ const next = messages.slice();
173
+ next[lastIndex] = { role: last.role, content } as unknown as OpenAIMessage;
174
+ return next;
175
+ }
176
+
177
+ // Attach a cache_control breakpoint to the last message of the stable history
178
+ // prefix (everything except the final user turn) so Anthropic re-uses it at
179
+ // ~10% input-token billing.
180
+ function applyHistoryCacheBreakpoint(messages: any[]): any[] {
181
+ if (messages.length < 2) return messages;
182
+ const breakpointIndex = messages.length - 2;
183
+ return messages.map((m, i) => {
184
+ if (i !== breakpointIndex) return m;
185
+ const blocks = Array.isArray(m.content)
186
+ ? m.content.slice()
187
+ : [{ type: 'text', text: m.content }];
188
+ if (!blocks.length) return m;
189
+ const lastBlockIndex = blocks.length - 1;
190
+ blocks[lastBlockIndex] = {
191
+ ...blocks[lastBlockIndex],
192
+ cache_control: { type: 'ephemeral' },
193
+ };
194
+ return { ...m, content: blocks };
195
+ });
196
+ }
197
+
198
+ export type CallClaudeWithMcpParams = {
199
+ prompt: string;
200
+ messages?: ClaudeMessage[];
201
+ service: string;
202
+ owner: string;
203
+ userId?: string;
204
+ model?: string;
205
+ maxTokens?: number;
206
+ system?: string;
207
+ mcpServer: ClaudeMcpServerRequest;
208
+ extractContent?: ExtractDirective[];
209
+ onResponse?: (res: any) => void;
210
+ onError?: (err: any) => void;
211
+ };
212
+ export const POLL_INTERVAL = 1500;
213
+ export async function callClaudeWithMcp({
214
+ prompt,
215
+ messages,
216
+ service,
217
+ owner,
218
+ userId,
219
+ model = DEFAULT_CLAUDE_MODEL,
220
+ maxTokens = 1000,
221
+ system,
222
+ mcpServer,
223
+ extractContent,
224
+ }: CallClaudeWithMcpParams) {
225
+ const mcpServerDefinition: Record<string, any> = {
226
+ type: 'url',
227
+ name: mcpServer.name,
228
+ url: mcpServer.url,
229
+ };
230
+
231
+ if (mcpServer.authorizationToken) {
232
+ mcpServerDefinition.authorization_token = mcpServer.authorizationToken;
233
+ }
234
+
235
+ return clientSecretRequest({
236
+ clientSecretName: 'claude',
237
+ queue: userId || service,
238
+ service,
239
+ owner,
240
+ ...pollOpt(),
241
+ url: ANTHROPIC_MESSAGES_API_URL,
242
+ method: 'POST',
243
+ headers: {
244
+ 'content-type': 'application/json',
245
+ 'x-api-key': '$CLIENT_SECRET',
246
+ 'anthropic-version': ANTHROPIC_VERSION,
247
+ 'anthropic-beta': ANTHROPIC_BETA_HEADER,
248
+ },
249
+ data: {
250
+ model,
251
+ max_tokens: maxTokens,
252
+ ...(extractContent && extractContent.length
253
+ ? { _skapi_extract: extractContent }
254
+ : {}),
255
+ ...(system
256
+ ? {
257
+ system: [
258
+ {
259
+ type: 'text',
260
+ text: system,
261
+ cache_control: { type: 'ephemeral' },
262
+ },
263
+ ],
264
+ }
265
+ : {}),
266
+ messages: (() => {
267
+ const prepared =
268
+ messages && messages.length
269
+ ? prepareClaudeMessages(messages)
270
+ : [
271
+ {
272
+ role: 'user',
273
+ content: transformContentWithImages(prompt),
274
+ },
275
+ ];
276
+ return applyHistoryCacheBreakpoint(prepared as any[]);
277
+ })(),
278
+ mcp_servers: [mcpServerDefinition],
279
+ tools: [
280
+ {
281
+ type: 'mcp_toolset',
282
+ mcp_server_name: mcpServer.name,
283
+ ...(mcpServer.defaultConfig
284
+ ? { default_config: mcpServer.defaultConfig }
285
+ : {}),
286
+ ...(mcpServer.configs ? { configs: mcpServer.configs } : {}),
287
+ },
288
+ {
289
+ type: 'web_fetch_20250910',
290
+ name: 'web_fetch',
291
+ max_uses: WEB_FETCH_MAX_USES,
292
+ citations: { enabled: true },
293
+ max_content_tokens: WEB_FETCH_MAX_CONTENT_TOKENS,
294
+ },
295
+ ],
296
+ },
297
+ });
298
+ }
299
+
300
+ export async function callClaudeWithPublicMcp(
301
+ prompt: string,
302
+ service: string,
303
+ owner: string,
304
+ messages?: ClaudeMessage[],
305
+ system?: string,
306
+ model?: string,
307
+ userId?: string,
308
+ extractContent?: ExtractDirective[],
309
+ onResponse?: (res: any) => void,
310
+ onError?: (err: any) => void,
311
+ ) {
312
+ return callClaudeWithMcp({
313
+ prompt,
314
+ messages,
315
+ service,
316
+ owner,
317
+ userId,
318
+ model: model || DEFAULT_CLAUDE_MODEL,
319
+ maxTokens: MAX_TOKENS,
320
+ system,
321
+ extractContent,
322
+ mcpServer: {
323
+ name: MCP_NAME,
324
+ url: mcpUrl(),
325
+ authorizationToken: '$ACCESS_TOKEN',
326
+ },
327
+ onResponse,
328
+ onError,
329
+ });
330
+ }
331
+
332
+ export async function callOpenAIWithPublicMcp(
333
+ prompt: string,
334
+ service: string,
335
+ owner: string,
336
+ messages?: OpenAIMessage[],
337
+ system?: string,
338
+ model?: string,
339
+ userId?: string,
340
+ extractContent?: ExtractDirective[],
341
+ onResponse?: (res: any) => void,
342
+ onError?: (err: any) => void,
343
+ ) {
344
+ const resolvedModel = model || DEFAULT_OPENAI_MODEL;
345
+ const imageDetail = getOpenAIImageDetail(resolvedModel);
346
+ const messageList =
347
+ messages && messages.length
348
+ ? prepareOpenAIMessages(messages, imageDetail)
349
+ : [
350
+ {
351
+ role: 'user' as const,
352
+ content: transformContentWithOpenAIImages(prompt, imageDetail),
353
+ },
354
+ ];
355
+
356
+ const responseInput = [
357
+ ...(system
358
+ ? [
359
+ {
360
+ role: 'system',
361
+ content: system,
362
+ },
363
+ ]
364
+ : []),
365
+ ...messageList.map((m) => ({
366
+ role: m.role,
367
+ content: m.content,
368
+ })),
369
+ ];
370
+
371
+ return clientSecretRequest({
372
+ clientSecretName: 'openai',
373
+ queue: userId || service,
374
+ service,
375
+ owner,
376
+ ...pollOpt(),
377
+ url: OPENAI_RESPONSES_API_URL,
378
+ method: 'POST',
379
+ headers: {
380
+ 'content-type': 'application/json',
381
+ Authorization: 'Bearer $CLIENT_SECRET',
382
+ },
383
+ data: {
384
+ model: resolvedModel,
385
+ max_output_tokens: MAX_TOKENS,
386
+ ...(extractContent && extractContent.length
387
+ ? { _skapi_extract: extractContent }
388
+ : {}),
389
+ input: responseInput,
390
+ tools: [
391
+ {
392
+ type: 'mcp',
393
+ server_label: MCP_NAME,
394
+ server_url: mcpUrl(),
395
+ require_approval: 'never',
396
+ headers: {
397
+ Authorization: 'Bearer $ACCESS_TOKEN',
398
+ },
399
+ },
400
+ ...(OPENAI_WEB_SEARCH_ENABLED
401
+ ? [
402
+ {
403
+ type: 'web_search',
404
+ external_web_access: OPENAI_WEB_SEARCH_EXTERNAL_WEB_ACCESS,
405
+ },
406
+ ]
407
+ : []),
408
+ ],
409
+ },
410
+ });
411
+ }
412
+
413
+ export type AttachmentSaveInfo = {
414
+ platform: 'claude' | 'openai';
415
+ model?: string;
416
+ service: string;
417
+ owner: string;
418
+ userId?: string;
419
+ serviceName?: string;
420
+ serviceDescription?: string;
421
+ attachment: {
422
+ name: string;
423
+ storagePath: string;
424
+ mime?: string;
425
+ size?: number;
426
+ url: string;
427
+ };
428
+ };
429
+
430
+ // Background "save into knowledge" call (not a chat turn). Office files get the
431
+ // _skapi_extract directive + an inline-content placeholder instead of a URL.
432
+ export async function notifyAgentSaveAttachment(info: AttachmentSaveInfo) {
433
+ const { platform, service, owner, attachment } = info;
434
+
435
+ const office = isOfficeFile(attachment.name, attachment.mime);
436
+ const placeholder = office ? makeExtractPlaceholder(attachment.storagePath) : undefined;
437
+ const extractContent: ExtractDirective[] | undefined =
438
+ office && placeholder
439
+ ? [{ path: attachment.storagePath, placeholder, name: attachment.name, mime: attachment.mime }]
440
+ : undefined;
441
+ const skapiExtract =
442
+ extractContent && extractContent.length ? { _skapi_extract: extractContent } : {};
443
+
444
+ const userMessage = buildIndexingUserMessage(
445
+ attachment,
446
+ placeholder ? { inlineContentPlaceholder: placeholder } : undefined,
447
+ );
448
+
449
+ const systemPrompt = buildIndexingSystemPrompt({
450
+ service,
451
+ serviceName: info.serviceName,
452
+ serviceDescription: info.serviceDescription,
453
+ });
454
+
455
+ if (platform === 'openai') {
456
+ const resolvedModel = info.model || DEFAULT_OPENAI_MODEL;
457
+ const imageDetail = getOpenAIImageDetail(resolvedModel);
458
+ return clientSecretRequest({
459
+ clientSecretName: 'openai',
460
+ queue: (info.userId || service) + BG_INDEXING_QUEUE_SUFFIX,
461
+ service,
462
+ owner,
463
+ ...pollOpt(),
464
+ url: OPENAI_RESPONSES_API_URL,
465
+ method: 'POST',
466
+ headers: {
467
+ 'content-type': 'application/json',
468
+ Authorization: 'Bearer $CLIENT_SECRET',
469
+ },
470
+ data: {
471
+ model: resolvedModel,
472
+ max_output_tokens: MAX_TOKENS,
473
+ ...skapiExtract,
474
+ input: [
475
+ { role: 'system', content: systemPrompt },
476
+ {
477
+ role: 'user',
478
+ content: transformContentWithOpenAIImages(userMessage, imageDetail),
479
+ },
480
+ ],
481
+ tools: [
482
+ {
483
+ type: 'mcp',
484
+ server_label: MCP_NAME,
485
+ server_url: mcpUrl(),
486
+ require_approval: 'never',
487
+ headers: { Authorization: 'Bearer $ACCESS_TOKEN' },
488
+ },
489
+ ...(OPENAI_WEB_SEARCH_ENABLED
490
+ ? [
491
+ {
492
+ type: 'web_search',
493
+ external_web_access: OPENAI_WEB_SEARCH_EXTERNAL_WEB_ACCESS,
494
+ },
495
+ ]
496
+ : []),
497
+ ],
498
+ },
499
+ });
500
+ }
501
+
502
+ const resolvedModel = info.model || DEFAULT_CLAUDE_MODEL;
503
+ return clientSecretRequest({
504
+ clientSecretName: 'claude',
505
+ queue: (info.userId || service) + BG_INDEXING_QUEUE_SUFFIX,
506
+ service,
507
+ owner,
508
+ ...pollOpt(),
509
+ url: ANTHROPIC_MESSAGES_API_URL,
510
+ method: 'POST',
511
+ headers: {
512
+ 'content-type': 'application/json',
513
+ 'x-api-key': '$CLIENT_SECRET',
514
+ 'anthropic-version': ANTHROPIC_VERSION,
515
+ 'anthropic-beta': ANTHROPIC_BETA_HEADER,
516
+ },
517
+ data: {
518
+ model: resolvedModel,
519
+ max_tokens: MAX_TOKENS,
520
+ ...skapiExtract,
521
+ system: [
522
+ {
523
+ type: 'text',
524
+ text: systemPrompt,
525
+ cache_control: { type: 'ephemeral' },
526
+ },
527
+ ],
528
+ messages: [
529
+ {
530
+ role: 'user',
531
+ content: transformContentWithImages(userMessage),
532
+ },
533
+ ],
534
+ mcp_servers: [
535
+ {
536
+ type: 'url',
537
+ name: MCP_NAME,
538
+ url: mcpUrl(),
539
+ authorization_token: '$ACCESS_TOKEN',
540
+ },
541
+ ],
542
+ tools: [
543
+ {
544
+ type: 'mcp_toolset',
545
+ mcp_server_name: MCP_NAME,
546
+ },
547
+ {
548
+ type: 'web_fetch_20250910',
549
+ name: 'web_fetch',
550
+ max_uses: WEB_FETCH_MAX_USES,
551
+ citations: { enabled: true },
552
+ max_content_tokens: WEB_FETCH_MAX_CONTENT_TOKENS,
553
+ },
554
+ ],
555
+ },
556
+ });
557
+ }
558
+
559
+ export function extractClaudeText(response: any) {
560
+ if (!Array.isArray(response?.content)) {
561
+ return '';
562
+ }
563
+
564
+ return response.content
565
+ .filter((block: any) => block?.type === 'text')
566
+ .map((block: any) => block.text)
567
+ .join('\n');
568
+ }
569
+
570
+ export function extractOpenAIText(response: any) {
571
+ if (
572
+ typeof response?.output_text === 'string' &&
573
+ response.output_text.length
574
+ ) {
575
+ return response.output_text;
576
+ }
577
+
578
+ if (Array.isArray(response?.output)) {
579
+ const text = response.output
580
+ .flatMap((item: any) => item?.content || [])
581
+ .filter((part: any) => part?.type === 'output_text')
582
+ .map((part: any) => part.text || '')
583
+ .join('\n')
584
+ .trim();
585
+
586
+ if (text) {
587
+ return text;
588
+ }
589
+ }
590
+
591
+ const content = response?.choices?.[0]?.message?.content;
592
+
593
+ if (typeof content === 'string') {
594
+ return content;
595
+ }
596
+
597
+ if (Array.isArray(content)) {
598
+ return content
599
+ .map((part: any) => {
600
+ if (typeof part === 'string') {
601
+ return part;
602
+ }
603
+ if (part?.type === 'text') {
604
+ return part.text || '';
605
+ }
606
+ return '';
607
+ })
608
+ .join('\n');
609
+ }
610
+
611
+ return '';
612
+ }
613
+
614
+ export async function listClaudeModels(service: string, owner: string) {
615
+ return clientSecretRequest({
616
+ clientSecretName: 'claude',
617
+ service,
618
+ owner,
619
+ url: ANTHROPIC_MODELS_API_URL,
620
+ method: 'GET',
621
+ headers: {
622
+ 'x-api-key': '$CLIENT_SECRET',
623
+ 'anthropic-version': ANTHROPIC_VERSION,
624
+ },
625
+ });
626
+ }
627
+
628
+ export async function listOpenAIModels(service: string, owner: string) {
629
+ return clientSecretRequest({
630
+ clientSecretName: 'openai',
631
+ service,
632
+ owner,
633
+ url: OPENAI_MODELS_API_URL,
634
+ method: 'GET',
635
+ headers: {
636
+ Authorization: 'Bearer $CLIENT_SECRET',
637
+ },
638
+ });
639
+ }
640
+
641
+ // Suffix for the background-indexing queue. Must sort *before* ':' (ASCII 58)
642
+ // so the chat-history BETWEEN query never includes bg-queue items. '-' (45) works.
643
+ export const BG_INDEXING_QUEUE_SUFFIX = '-bg';
644
+
645
+ // Pending background-indexing task descriptor. NOTE: the live mutable queue
646
+ // (a Vue `reactive([])` in agent.vue, a plain array in bunnyquery) is app-level
647
+ // state owned by the consumer — only the TYPE lives in the engine.
648
+ export type BgTaskEntry = {
649
+ serviceId: string;
650
+ platform: 'claude' | 'openai';
651
+ id: string;
652
+ filename: string;
653
+ storagePath?: string;
654
+ isReindex?: boolean;
655
+ mime?: string;
656
+ size?: number;
657
+ status: 'running' | 'pending';
658
+ poll: ((opts: { latency: number }) => Promise<any>) | undefined;
659
+ };
660
+
661
+ export async function getChatHistory(
662
+ params: { service?: string; owner?: string; platform: 'claude' | 'openai'; queue?: string },
663
+ fetchOptions: Record<string, any>,
664
+ ) {
665
+ const url =
666
+ params.platform === 'claude'
667
+ ? ANTHROPIC_MESSAGES_API_URL
668
+ : OPENAI_RESPONSES_API_URL;
669
+ const p = Object.assign(
670
+ {
671
+ url,
672
+ method: 'POST',
673
+ },
674
+ { service: params.service, owner: params.owner },
675
+ params.queue ? { queue: params.queue } : {},
676
+ );
677
+
678
+ return chatEngineConfig().clientSecretRequestHistory(
679
+ p as { url: string; method: 'POST'; queue?: string },
680
+ Object.assign({ ascending: false }, fetchOptions),
681
+ );
682
+ }