bunnyquery 1.9.10 → 1.10.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.
@@ -15,7 +15,7 @@ import { isServerExtractable, isPagedReadFile, isImageVisionFile, isWindowedRead
15
15
  import { chatEngineConfig, pollOpt, windowedIndexingEnabled, liveStreamingEnabled, liveStreamingRealtimeEnabled } from './config';
16
16
  // Output sizing lives in budget.ts so the request cap and the reserve the input
17
17
  // budget subtracts cannot drift; getMaxOutputTokens also clamps per model.
18
- import { getMaxOutputTokens } from './budget';
18
+ import { getMaxOutputTokens, getModelContextWindow } from './budget';
19
19
 
20
20
  export const ANTHROPIC_MESSAGES_API_URL = 'https://api.anthropic.com/v1/messages';
21
21
  const ANTHROPIC_MODELS_API_URL = 'https://api.anthropic.com/v1/models';
@@ -39,6 +39,70 @@ export const DEFAULT_OPENAI_MODEL = 'gpt-5.6-luna';
39
39
 
40
40
  const mcpUrl = () => chatEngineConfig().mcpBaseUrl;
41
41
 
42
+ /**
43
+ * The MCP endpoint an INDEXING pass points at.
44
+ *
45
+ * Same server, same auth, same tools it can actually call: `?profile=index` only narrows
46
+ * the tools/list the model is SHOWN. An indexing pass reads one window of one file and
47
+ * writes records for it, so being told about deleteRecords, exportRecordsToFile,
48
+ * writeReport, getTemporaryUrl, getProfile and getProjectInfos costs about 2,400 tokens
49
+ * on every pass of every file and buys nothing: there is no user attached to hand a
50
+ * download or a report to, and re-index clears old records server-side before the pass
51
+ * runs. Measured: 30,577 B -> 20,755 B.
52
+ *
53
+ * A CHAT turn keeps the full list and always will. It is open ended, a person can ask
54
+ * for anything, and a tool that is missing from the list is a dead end the model cannot
55
+ * reason its way out of.
56
+ *
57
+ * Degrades in both directions, which is why it is a query param: an MCP server that
58
+ * predates it ignores the parameter and serves the full list, and a client that predates
59
+ * it just sends no parameter. Neither needs the other deployed first.
60
+ */
61
+ /**
62
+ * Append query parameters to an MCP endpoint, preserving any the base already carries.
63
+ *
64
+ * An explicit path before the query. The configured base has no trailing slash
65
+ * ("https://mcp-dev.broadwayinc.computer"), and appending "?profile=index" straight onto an
66
+ * authority with no path yields a URL that is legal but that intermediaries and clients
67
+ * normalise inconsistently. "/?profile=index" is unambiguous everywhere.
68
+ *
69
+ * Every parameter here is a HINT the server may ignore, which is what lets the two sides
70
+ * deploy in either order: a server that predates one ignores it, and a client that predates
71
+ * it sends nothing. The worker strips the whole query before appending its own /internal
72
+ * paths (_mcp_endpoint_and_token), so nothing added here reaches those routes.
73
+ */
74
+ function withMcpParams(base: string, params: Record<string, string | number | undefined>): string {
75
+ if (!base) return base;
76
+ const pairs = Object.keys(params)
77
+ .filter(k => params[k] !== undefined && params[k] !== null && params[k] !== '')
78
+ .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(String(params[k])));
79
+ if (!pairs.length) return base;
80
+ const [addr, existing] = base.split('?');
81
+ // Only supply the missing root path. Appending a slash to a base that ALREADY has a
82
+ // path would change where the request lands on a server mounted under one.
83
+ const hasPath = /^[a-z][a-z0-9+.-]*:\/\/[^/]+\/./i.test(addr);
84
+ const path = hasPath ? addr : addr.replace(/\/+$/, '') + '/';
85
+ return path + '?' + (existing ? existing + '&' : '') + pairs.join('&');
86
+ }
87
+
88
+ /**
89
+ * How large a page of a paginated tool result this model can afford.
90
+ *
91
+ * The server slices big results into pages and the model spends one round trip per page, so
92
+ * the page size sets how long reading a large record set takes: 1000 spreadsheet records is
93
+ * 74 round trips at the server's floor of 10,000 chars and 13 at 60,000. The server cannot
94
+ * know which model is on the other end of an MCP connection, so it is told.
95
+ *
96
+ * The MODEL's own ceiling, not getContextWindow(): the project's context-window setting
97
+ * budgets this client's FIRST request, while these pages accumulate in the provider's
98
+ * server-side tool loop, which runs against what the model can actually hold.
99
+ */
100
+ const mcpContextParam = (platform: 'claude' | 'openai', model?: string) =>
101
+ getModelContextWindow(platform, model);
102
+
103
+ const mcpIndexingUrl = (platform: 'claude' | 'openai' = 'openai', model?: string) =>
104
+ withMcpParams(mcpUrl(), { profile: 'index', ctx: mcpContextParam(platform, model) });
105
+
42
106
  /**
43
107
  * Where a chat turn's MCP tools point, and what they authenticate with.
44
108
  *
@@ -697,7 +761,9 @@ export async function callClaudeWithPublicMcp(
697
761
  fileUrls,
698
762
  mcpServer: {
699
763
  name: MCP_NAME,
700
- url: endpoint.url,
764
+ url: withMcpParams(endpoint.url, {
765
+ ctx: mcpContextParam('claude', model || DEFAULT_CLAUDE_MODEL),
766
+ }),
701
767
  // Omitted entirely for an anonymous turn; the `if (mcpServer.authorizationToken)`
702
768
  // guard below drops the key rather than sending an empty one.
703
769
  authorizationToken: endpoint.token,
@@ -782,7 +848,7 @@ export async function callOpenAIWithPublicMcp(
782
848
  {
783
849
  type: 'mcp',
784
850
  server_label: MCP_NAME,
785
- server_url: endpoint.url,
851
+ server_url: withMcpParams(endpoint.url, { ctx: mcpContextParam('openai', resolvedModel) }),
786
852
  require_approval: 'never',
787
853
  // No `headers` at all for an anonymous turn: `Bearer ` with an
788
854
  // empty token is a credential the MCP server rejects, and the
@@ -1086,7 +1152,7 @@ export async function notifyAgentSaveAttachment(info: AttachmentSaveInfo) {
1086
1152
  },
1087
1153
  data: {
1088
1154
  model: resolvedModel,
1089
- max_output_tokens: getMaxOutputTokens('openai', resolvedModel),
1155
+ max_output_tokens: getMaxOutputTokens('openai', resolvedModel, 'indexing'),
1090
1156
  // Nano-only transcription knobs. Indexing only; see variantIndexingOptions.
1091
1157
  ...variantIndexingOptions(resolvedModel),
1092
1158
  ...skapiExtract,
@@ -1104,7 +1170,7 @@ export async function notifyAgentSaveAttachment(info: AttachmentSaveInfo) {
1104
1170
  {
1105
1171
  type: 'mcp',
1106
1172
  server_label: MCP_NAME,
1107
- server_url: mcpUrl(),
1173
+ server_url: mcpIndexingUrl('openai', resolvedModel),
1108
1174
  require_approval: 'never',
1109
1175
  headers: { Authorization: 'Bearer $ACCESS_TOKEN' },
1110
1176
  },
@@ -1138,7 +1204,7 @@ export async function notifyAgentSaveAttachment(info: AttachmentSaveInfo) {
1138
1204
  },
1139
1205
  data: {
1140
1206
  model: resolvedModel,
1141
- max_tokens: getMaxOutputTokens('claude', resolvedModel),
1207
+ max_tokens: getMaxOutputTokens('claude', resolvedModel, 'indexing'),
1142
1208
  ...skapiExtract,
1143
1209
  ...skapiRender,
1144
1210
  ...skapiWindow,
@@ -1160,7 +1226,7 @@ export async function notifyAgentSaveAttachment(info: AttachmentSaveInfo) {
1160
1226
  {
1161
1227
  type: 'url',
1162
1228
  name: MCP_NAME,
1163
- url: mcpUrl(),
1229
+ url: mcpIndexingUrl('claude', resolvedModel),
1164
1230
  authorization_token: '$ACCESS_TOKEN',
1165
1231
  },
1166
1232
  ],