pi-openai-toolkit 0.14.6 → 0.14.8

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.
@@ -1,7 +1,10 @@
1
1
  import type { WebSearchConfig } from "../types";
2
2
  import {
3
+ LOCAL_WEB_SEARCH_TOOL_NAME,
4
+ resolveWebSearchRoute,
5
+ WEB_RUN_TOOL_NAME,
3
6
  WEB_SEARCH_SOURCE_INCLUDE,
4
- isWebSearchEnabledForModel,
7
+ isWebSearchRoute,
5
8
  type WebSearchModel,
6
9
  type WebSearchPayloadTransform,
7
10
  } from "./types";
@@ -13,47 +16,236 @@ function isRecord(value: unknown): value is JsonObject {
13
16
  }
14
17
 
15
18
  function isLocalWebSearchFunction(tool: unknown): boolean {
16
- return isRecord(tool) && tool.type === "function" && tool.name === "web_search";
19
+ return isRecord(tool) && tool.type === "function" && tool.name === LOCAL_WEB_SEARCH_TOOL_NAME;
20
+ }
21
+
22
+ function isStandaloneWebRunFunction(tool: unknown): boolean {
23
+ return isRecord(tool) && tool.type === "function" && tool.name === WEB_RUN_TOOL_NAME;
17
24
  }
18
25
 
19
26
  function isNativeWebSearchTool(tool: unknown): boolean {
20
27
  return isRecord(tool) && (tool.type === "web_search" || tool.type === "web_search_preview");
21
28
  }
22
29
 
30
+ function explicitRouteFailure(resolution: ReturnType<typeof resolveWebSearchRoute>): WebSearchPayloadTransform {
31
+ const reason = resolution.route === "none" ? resolution.reason : resolution.reason;
32
+ return {
33
+ payload: undefined,
34
+ outcome: "unavailable-route",
35
+ changed: false,
36
+ fatal: true,
37
+ errorMessage: `Web Search route is unavailable${reason ? ` (${reason})` : ""}; request aborted without fallback.`,
38
+ };
39
+ }
40
+
41
+ function unavailableRouteTransform(
42
+ payload: unknown,
43
+ resolution: ReturnType<typeof resolveWebSearchRoute>,
44
+ ): WebSearchPayloadTransform {
45
+ const base = explicitRouteFailure(resolution);
46
+ return { ...base, payload };
47
+ }
48
+
49
+ function transformStandalonePayload(args: {
50
+ payload: JsonObject;
51
+ resolution: ReturnType<typeof resolveWebSearchRoute>;
52
+ }): WebSearchPayloadTransform {
53
+ const { payload, resolution } = args;
54
+ const toolsValue = payload.tools;
55
+ if (toolsValue !== undefined && !Array.isArray(toolsValue)) {
56
+ return {
57
+ payload,
58
+ outcome: "invalid-tools",
59
+ changed: false,
60
+ fatal: resolution.source !== "legacy",
61
+ errorMessage: "Standalone Web Search requires a provider payload tools array.",
62
+ };
63
+ }
64
+ const includeValue = payload.include;
65
+ if (includeValue !== undefined && !Array.isArray(includeValue)) {
66
+ return {
67
+ payload,
68
+ outcome: "invalid-include",
69
+ changed: false,
70
+ fatal: resolution.source !== "legacy",
71
+ errorMessage: "Standalone Web Search requires a provider payload include array.",
72
+ };
73
+ }
74
+
75
+ const tools = toolsValue ?? [];
76
+ let standaloneToolCount = 0;
77
+ const normalizedTools = tools.filter((tool) => {
78
+ if (isLocalWebSearchFunction(tool) || isNativeWebSearchTool(tool)) return false;
79
+ if (!isStandaloneWebRunFunction(tool)) return true;
80
+ standaloneToolCount += 1;
81
+ return standaloneToolCount === 1;
82
+ });
83
+ const include = includeValue ?? [];
84
+ const normalizedInclude = include.filter((item) => item !== WEB_SEARCH_SOURCE_INCLUDE);
85
+ const toolsChanged = normalizedTools.length !== tools.length;
86
+ const includeChanged = normalizedInclude.length !== include.length;
87
+ if (toolsValue !== undefined && !normalizedTools.some(isStandaloneWebRunFunction)) {
88
+ return {
89
+ payload,
90
+ outcome: "unavailable-route",
91
+ changed: false,
92
+ fatal: resolution.source !== "legacy",
93
+ errorMessage: "Standalone Web Search requires the registered web.run tool in the provider payload.",
94
+ };
95
+ }
96
+ if (!toolsChanged && !includeChanged) {
97
+ return { payload, outcome: "standalone-route", changed: false };
98
+ }
99
+
100
+ return {
101
+ payload: {
102
+ ...payload,
103
+ ...(toolsChanged ? { tools: normalizedTools } : {}),
104
+ ...(includeChanged ? { include: normalizedInclude } : {}),
105
+ },
106
+ outcome: "removed-conflicting-tools",
107
+ changed: true,
108
+ };
109
+ }
110
+
111
+ function transformLocalPayload(args: {
112
+ payload: JsonObject;
113
+ resolution: ReturnType<typeof resolveWebSearchRoute>;
114
+ }): WebSearchPayloadTransform {
115
+ const { payload, resolution } = args;
116
+ const toolsValue = payload.tools;
117
+ if (toolsValue !== undefined && !Array.isArray(toolsValue)) {
118
+ return {
119
+ payload,
120
+ outcome: "invalid-tools",
121
+ changed: false,
122
+ fatal: resolution.source !== "legacy",
123
+ errorMessage: "Local Web Search requires a provider payload tools array.",
124
+ };
125
+ }
126
+ const includeValue = payload.include;
127
+ if (includeValue !== undefined && !Array.isArray(includeValue)) {
128
+ return {
129
+ payload,
130
+ outcome: "invalid-include",
131
+ changed: false,
132
+ fatal: resolution.source !== "legacy",
133
+ errorMessage: "Local Web Search requires a provider payload include array.",
134
+ };
135
+ }
136
+
137
+ const tools = toolsValue ?? [];
138
+ const normalizedTools = tools.filter(
139
+ (tool) => !isStandaloneWebRunFunction(tool) && !isNativeWebSearchTool(tool),
140
+ );
141
+ const include = includeValue ?? [];
142
+ const normalizedInclude = include.filter((item) => item !== WEB_SEARCH_SOURCE_INCLUDE);
143
+ const toolsChanged = normalizedTools.length !== tools.length;
144
+ const includeChanged = normalizedInclude.length !== include.length;
145
+ if (!toolsChanged && !includeChanged) return { payload, outcome: "local-route", changed: false };
146
+
147
+ return {
148
+ payload: {
149
+ ...payload,
150
+ ...(toolsChanged ? { tools: normalizedTools } : {}),
151
+ ...(includeChanged ? { include: normalizedInclude } : {}),
152
+ },
153
+ outcome: "removed-conflicting-tools",
154
+ changed: true,
155
+ };
156
+ }
157
+
23
158
  export function transformWebSearchPayload(args: {
24
159
  model: WebSearchModel | undefined;
25
160
  config: WebSearchConfig;
26
161
  payload: unknown;
27
162
  }): WebSearchPayloadTransform {
28
163
  const { model, config, payload } = args;
29
- if (!config.enabled) {
30
- return { payload, outcome: "disabled", changed: false };
164
+ const resolution = resolveWebSearchRoute({ model, config });
165
+
166
+ if (resolution.route === "none") {
167
+ return {
168
+ payload,
169
+ outcome: resolution.reason === "disabled" ? "disabled" : "unsupported-model",
170
+ changed: false,
171
+ };
31
172
  }
32
- if (!isWebSearchEnabledForModel(model, config)) {
33
- return { payload, outcome: "unsupported-model", changed: false };
173
+ if (!resolution.available) {
174
+ return unavailableRouteTransform(payload, resolution);
34
175
  }
176
+ if (resolution.route === "local") {
177
+ if (!isRecord(payload)) {
178
+ return {
179
+ payload,
180
+ outcome: "non-object-payload",
181
+ changed: false,
182
+ fatal: resolution.source !== "legacy",
183
+ errorMessage: "Local Web Search requires an object provider payload.",
184
+ };
185
+ }
186
+ return transformLocalPayload({ payload, resolution });
187
+ }
188
+ if (resolution.route === "standalone-alpha") {
189
+ if (!isRecord(payload)) {
190
+ return {
191
+ payload,
192
+ outcome: "non-object-payload",
193
+ changed: false,
194
+ fatal: resolution.source !== "legacy",
195
+ errorMessage: "Standalone Web Search requires an object provider payload.",
196
+ };
197
+ }
198
+ return transformStandalonePayload({ payload, resolution });
199
+ }
200
+
35
201
  if (!isRecord(payload)) {
36
- return { payload, outcome: "non-object-payload", changed: false };
202
+ return {
203
+ payload,
204
+ outcome: "non-object-payload",
205
+ changed: false,
206
+ ...(resolution.source !== "legacy"
207
+ ? { fatal: true, errorMessage: "Hosted Web Search requires an object provider payload." }
208
+ : {}),
209
+ };
37
210
  }
38
211
 
39
212
  const toolsValue = payload.tools;
40
213
  if (toolsValue !== undefined && !Array.isArray(toolsValue)) {
41
- return { payload, outcome: "invalid-tools", changed: false };
214
+ return {
215
+ payload,
216
+ outcome: "invalid-tools",
217
+ changed: false,
218
+ ...(resolution.source !== "legacy"
219
+ ? { fatal: true, errorMessage: "Hosted Web Search requires a provider payload tools array." }
220
+ : {}),
221
+ };
42
222
  }
43
223
  const tools = toolsValue ?? [];
44
224
  const includeValue = payload.include;
45
225
  if (includeValue !== undefined && !Array.isArray(includeValue)) {
46
- return { payload, outcome: "invalid-include", changed: false };
226
+ return {
227
+ payload,
228
+ outcome: "invalid-include",
229
+ changed: false,
230
+ ...(resolution.source !== "legacy"
231
+ ? { fatal: true, errorMessage: "Hosted Web Search requires a provider payload include array." }
232
+ : {}),
233
+ };
47
234
  }
48
235
  const include = includeValue ?? [];
49
236
  const normalizedTools: unknown[] = [];
50
237
  let nativeToolCount = 0;
51
238
  let localToolRemoved = false;
239
+ let standaloneToolRemoved = false;
52
240
  for (const tool of tools) {
53
241
  if (isLocalWebSearchFunction(tool)) {
54
242
  localToolRemoved = true;
55
243
  continue;
56
244
  }
245
+ if (isStandaloneWebRunFunction(tool)) {
246
+ standaloneToolRemoved = true;
247
+ continue;
248
+ }
57
249
  if (isNativeWebSearchTool(tool)) {
58
250
  nativeToolCount += 1;
59
251
  if (nativeToolCount > 1) continue;
@@ -78,7 +270,7 @@ export function transformWebSearchPayload(args: {
78
270
  normalizedInclude.push(WEB_SEARCH_SOURCE_INCLUDE);
79
271
  }
80
272
 
81
- const toolsChanged = localToolRemoved || !hasNativeTool || nativeToolCount > 1;
273
+ const toolsChanged = localToolRemoved || standaloneToolRemoved || !hasNativeTool || nativeToolCount > 1;
82
274
  const includeChanged = sourceIncludeCount !== 1;
83
275
  if (!toolsChanged && !includeChanged) {
84
276
  return { payload, outcome: "existing-native-tool", changed: false };
@@ -94,3 +286,10 @@ export function transformWebSearchPayload(args: {
94
286
  changed: true,
95
287
  };
96
288
  }
289
+
290
+ export const _payloadTest = {
291
+ isLocalWebSearchFunction,
292
+ isNativeWebSearchTool,
293
+ isStandaloneWebRunFunction,
294
+ isWebSearchRoute,
295
+ };
@@ -1,20 +1,59 @@
1
1
  import type { WebSearchConfig } from "../types";
2
- import { isWebSearchEnabledForModel, WEB_SEARCH_PROMPT_MARKER, type WebSearchModel } from "./types";
2
+ import {
3
+ resolveWebSearchRoute,
4
+ WEB_SEARCH_PROMPT_MARKER,
5
+ type WebSearchModel,
6
+ } from "./types";
3
7
 
4
8
  export const WEB_SEARCH_PROMPT_SECTION = `${WEB_SEARCH_PROMPT_MARKER}
5
9
  ## Web Search
6
10
 
7
11
  The hosted \`web_search\` tool is part of your tool list for this model. The API executes it server-side: it is a real callable tool even though no client-side function schema or description accompanies it. Call it whenever current or online information matters instead of guessing, cite the returned sources in the answer, and never claim that web search or internet access is unavailable while this section is present.`;
8
12
 
13
+ export const LOCAL_WEB_SEARCH_PROMPT_SECTION = `${WEB_SEARCH_PROMPT_MARKER}
14
+ ## Web Search
15
+
16
+ The local \`web_search\` tool is available for this model. Use it when current or online information matters, and cite the sources returned by that tool. Do not claim that the provider API executes this local tool server-side.`;
17
+
18
+ export const STANDALONE_ALPHA_WEB_SEARCH_PROMPT_SECTION = `${WEB_SEARCH_PROMPT_MARKER}
19
+ ## Web Search
20
+
21
+ The \`web.run\` tool is available for this model. It is a sequential local wrapper backed by CPA/Codex standalone search at the provider's \`/alpha/search\` endpoint. It supports \`search_query\`, \`image_query\`, \`open\`, \`click\`, \`find\`, \`screenshot\`, \`finance\`, \`weather\`, \`sports\`, and \`time\`. Use it for current or online information, preserve reference ids when following results, and cite the returned sources.`;
22
+
23
+ function removeToolkitWebSearchPrompt(systemPrompt: string): string {
24
+ const markerIndex = systemPrompt.indexOf(WEB_SEARCH_PROMPT_MARKER);
25
+ if (markerIndex < 0) return systemPrompt;
26
+ return systemPrompt.slice(0, markerIndex).trimEnd();
27
+ }
28
+
29
+ function promptSectionForRoute(route: "local" | "hosted" | "standalone-alpha"): string {
30
+ switch (route) {
31
+ case "local":
32
+ return LOCAL_WEB_SEARCH_PROMPT_SECTION;
33
+ case "hosted":
34
+ return WEB_SEARCH_PROMPT_SECTION;
35
+ case "standalone-alpha":
36
+ return STANDALONE_ALPHA_WEB_SEARCH_PROMPT_SECTION;
37
+ }
38
+ }
39
+
9
40
  export function appendWebSearchPrompt(args: {
10
41
  model: WebSearchModel | undefined;
11
42
  config: WebSearchConfig;
12
43
  systemPrompt: string;
44
+ /** Lifecycle registration can override this when the selected local wrapper is unavailable. */
45
+ routeAvailable?: boolean;
13
46
  }): string {
14
47
  const { model, config, systemPrompt } = args;
15
- if (!isWebSearchEnabledForModel(model, config) || systemPrompt.includes(WEB_SEARCH_PROMPT_MARKER)) {
16
- return systemPrompt;
17
- }
48
+ const basePrompt = removeToolkitWebSearchPrompt(systemPrompt);
49
+ const resolution = resolveWebSearchRoute({ model, config });
50
+ if (args.routeAvailable === false || resolution.route === "none" || !resolution.available) return basePrompt;
18
51
 
19
- return `${systemPrompt.trimEnd()}\n\n${WEB_SEARCH_PROMPT_SECTION}`;
52
+ const section = promptSectionForRoute(resolution.route);
53
+ return `${basePrompt.trimEnd()}\n\n${section}`;
20
54
  }
55
+
56
+ export const _promptTest = {
57
+ removeToolkitWebSearchPrompt,
58
+ promptSectionForRoute,
59
+ };
@@ -1,11 +1,15 @@
1
1
  import { getExactModelKey, isExactModelAllowed } from "../model-scope";
2
- import type { WebSearchConfig } from "../types";
2
+ import type { WebSearchConfig, WebSearchRoute } from "../types";
3
3
 
4
4
  export const WEB_SEARCH_CAPABLE_APIS = ["openai-responses", "openai-codex-responses"] as const;
5
5
  export const WEB_SEARCH_SOURCE_INCLUDE = "web_search_call.action.sources";
6
6
  export const WEB_SEARCH_PROMPT_MARKER = "<!-- pi-openai-toolkit:web-search -->";
7
+ export const WEB_RUN_TOOL_NAME = "web.run";
8
+ export const LOCAL_WEB_SEARCH_TOOL_NAME = "web_search";
7
9
 
8
10
  export type WebSearchCapableApi = (typeof WEB_SEARCH_CAPABLE_APIS)[number];
11
+ export type WebSearchRouteSource = "exact" | "default" | "legacy" | "none";
12
+ export type WebSearchRouteUnavailableReason = "missing-model" | "unsupported-api" | "invalid-route";
9
13
 
10
14
  export type WebSearchModel = {
11
15
  api?: string;
@@ -13,6 +17,21 @@ export type WebSearchModel = {
13
17
  id?: string;
14
18
  };
15
19
 
20
+ export type WebSearchRouteResolution =
21
+ | {
22
+ route: "none";
23
+ source: "none";
24
+ modelKey?: string;
25
+ reason: "disabled" | "unconfigured";
26
+ }
27
+ | {
28
+ route: WebSearchRoute;
29
+ source: Exclude<WebSearchRouteSource, "none">;
30
+ modelKey?: string;
31
+ available: boolean;
32
+ reason?: WebSearchRouteUnavailableReason;
33
+ };
34
+
16
35
  export type WebSearchPayloadOutcome =
17
36
  | "disabled"
18
37
  | "unsupported-model"
@@ -20,26 +39,106 @@ export type WebSearchPayloadOutcome =
20
39
  | "invalid-tools"
21
40
  | "invalid-include"
22
41
  | "existing-native-tool"
23
- | "injected-native-tool";
42
+ | "injected-native-tool"
43
+ | "local-route"
44
+ | "standalone-route"
45
+ | "removed-conflicting-tools"
46
+ | "unavailable-route";
24
47
 
25
48
  export type WebSearchPayloadTransform = {
26
49
  payload: unknown;
27
50
  outcome: WebSearchPayloadOutcome;
28
51
  changed: boolean;
52
+ /** Explicit route failures must abort rather than silently select another search path. */
53
+ fatal?: boolean;
54
+ errorMessage?: string;
29
55
  };
30
56
 
31
57
  export function getWebSearchModelKey(model: WebSearchModel | undefined): string | undefined {
32
58
  return getExactModelKey(model);
33
59
  }
34
60
 
61
+ export function isWebSearchRoute(value: unknown): value is WebSearchRoute {
62
+ return value === "local" || value === "hosted" || value === "standalone-alpha";
63
+ }
64
+
65
+ function selectedRoute(args: {
66
+ route: WebSearchRoute;
67
+ source: Exclude<WebSearchRouteSource, "none">;
68
+ model: WebSearchModel | undefined;
69
+ modelKey: string | undefined;
70
+ }): WebSearchRouteResolution {
71
+ if (!args.modelKey) {
72
+ return {
73
+ route: args.route,
74
+ source: args.source,
75
+ available: false,
76
+ reason: "missing-model",
77
+ };
78
+ }
79
+ if (args.route !== "local" && !isWebSearchCapableApi(args.model?.api)) {
80
+ return {
81
+ route: args.route,
82
+ source: args.source,
83
+ modelKey: args.modelKey,
84
+ available: false,
85
+ reason: "unsupported-api",
86
+ };
87
+ }
88
+ return {
89
+ route: args.route,
90
+ source: args.source,
91
+ modelKey: args.modelKey,
92
+ available: true,
93
+ };
94
+ }
95
+
96
+ export function isWebSearchCapableApi(api: unknown): api is WebSearchCapableApi {
97
+ return typeof api === "string" && (WEB_SEARCH_CAPABLE_APIS as readonly string[]).includes(api);
98
+ }
99
+
100
+ /**
101
+ * Resolve exactly one configured Web Search route. The legacy model allowlist is
102
+ * deliberately consulted only after explicit route fields so old configs retain
103
+ * hosted behavior without becoming an implicit alpha-search opt-in.
104
+ */
105
+ export function resolveWebSearchRoute(args: {
106
+ model: WebSearchModel | undefined;
107
+ config: WebSearchConfig;
108
+ }): WebSearchRouteResolution {
109
+ const { model, config } = args;
110
+ const modelKey = getWebSearchModelKey(model);
111
+ if (!config.enabled) {
112
+ return { route: "none", source: "none", modelKey, reason: "disabled" };
113
+ }
114
+
115
+ const exactRoute = modelKey ? config.routes?.[modelKey] : undefined;
116
+ if (exactRoute !== undefined) {
117
+ if (!isWebSearchRoute(exactRoute)) {
118
+ return { route: "none", source: "none", modelKey, reason: "unconfigured" };
119
+ }
120
+ return selectedRoute({ route: exactRoute, source: "exact", model, modelKey });
121
+ }
122
+
123
+ if (config.defaultRoute !== undefined) {
124
+ if (!isWebSearchRoute(config.defaultRoute)) {
125
+ return { route: "none", source: "none", modelKey, reason: "unconfigured" };
126
+ }
127
+ return selectedRoute({ route: config.defaultRoute, source: "default", model, modelKey });
128
+ }
129
+
130
+ if (modelKey && isExactModelAllowed(model, config.models) && isWebSearchCapableApi(model?.api)) {
131
+ return selectedRoute({ route: "hosted", source: "legacy", model, modelKey });
132
+ }
133
+
134
+ return { route: "none", source: "none", modelKey, reason: "unconfigured" };
135
+ }
136
+
137
+ /** Compatibility predicate for callers that specifically need the hosted path. */
35
138
  export function isWebSearchEnabledForModel(
36
139
  model: WebSearchModel | undefined,
37
140
  config: WebSearchConfig,
38
141
  ): boolean {
39
- return (
40
- config.enabled &&
41
- typeof model?.api === "string" &&
42
- (WEB_SEARCH_CAPABLE_APIS as readonly string[]).includes(model.api) &&
43
- isExactModelAllowed(model, config.models)
44
- );
142
+ const resolution = resolveWebSearchRoute({ model, config });
143
+ return resolution.route === "hosted" && resolution.available;
45
144
  }
@@ -17,6 +17,8 @@
17
17
  "extensions/image-generation.ts",
18
18
  "extensions/auto-mode.ts",
19
19
  "src/compact-client.ts",
20
+ "src/compaction-projection.ts",
21
+ "src/pi-context-hook.ts",
20
22
  "src/config.ts",
21
23
  "src/debug.ts",
22
24
  "src/details-store.ts",