pi-openai-toolkit 0.14.5 → 0.14.7
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/docs/internals.md +42 -21
- package/package.json +1 -1
- package/src/auto-mode/classifier.ts +165 -165
- package/src/auto-mode/policy.ts +146 -146
- package/src/auto-mode/prompt.ts +279 -279
- package/src/auto-mode/reviewer.ts +325 -325
- package/src/auto-mode/transcript.ts +133 -133
- package/src/auto-mode/types.ts +137 -137
- package/src/codex-astra/effort-planner.ts +175 -175
- package/src/codex-astra/extension.ts +122 -122
- package/src/context-management/codex-provider.ts +20 -20
- package/src/context-management/history-notes.ts +32 -0
- package/src/context-management/messages.ts +112 -106
- package/src/context-management/namespace-tools.ts +426 -426
- package/src/context-management/tool-contract.ts +23 -23
- package/src/context-management/tools.ts +279 -311
- package/src/context-management/types.ts +7 -0
- package/src/context-management/window-budget.ts +83 -83
- package/src/context-management/window-manager.ts +60 -51
- package/src/context-management/window-request.ts +125 -125
- package/src/debug.ts +1 -1
- package/src/extension-runtime.ts +34 -9
- package/src/image-generation/artifacts.ts +237 -237
- package/src/image-generation/client.ts +248 -248
- package/src/image-generation/eligibility.ts +18 -18
- package/src/image-generation/references.ts +194 -194
- package/src/image-generation/render.ts +252 -252
- package/src/model-scope.ts +17 -17
- package/src/provider-headers.ts +23 -23
- package/src/responses-headers.ts +76 -11
- package/src/runtime.ts +37 -1
- package/src/web-search/extension.ts +80 -80
- package/src/web-search/payload.ts +96 -96
- package/src/web-search/prompt.ts +20 -20
- package/src/web-search/types.ts +45 -45
package/src/responses-headers.ts
CHANGED
|
@@ -1,11 +1,55 @@
|
|
|
1
1
|
import type { ProviderHeaders } from "@earendil-works/pi-ai";
|
|
2
2
|
import { mergeProviderHeaders } from "./provider-headers";
|
|
3
3
|
|
|
4
|
+
export const CODEX_AFFINITY_SCOPE = "codex-session-v1" as const;
|
|
5
|
+
export const CODEX_GATEWAY_ORIGINATOR = "codex_cli_rs" as const;
|
|
6
|
+
|
|
7
|
+
/** Headers permitted from Pi/provider configuration onto the gateway hop. */
|
|
8
|
+
export const CODEX_GATEWAY_FORWARD_HEADERS = [
|
|
9
|
+
"accept",
|
|
10
|
+
"content-type",
|
|
11
|
+
"session-id",
|
|
12
|
+
"thread-id",
|
|
13
|
+
"x-parent-session-id",
|
|
14
|
+
"x-codex-parent-thread-id",
|
|
15
|
+
"x-client-request-id",
|
|
16
|
+
"x-codex-model",
|
|
17
|
+
"x-codex-affinity-scope",
|
|
18
|
+
"x-codex-window-id",
|
|
19
|
+
"x-codex-turn-metadata",
|
|
20
|
+
"x-codex-beta-features",
|
|
21
|
+
"x-openai-encrypted-tool-arguments",
|
|
22
|
+
"x-openai-tool-output-truncation-policy",
|
|
23
|
+
"x-openai-internal-codex-responses-lite",
|
|
24
|
+
"version",
|
|
25
|
+
"originator",
|
|
26
|
+
"user-agent",
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
29
|
+
const CODEX_GATEWAY_FORWARD_HEADER_SET = new Set<string>(CODEX_GATEWAY_FORWARD_HEADERS);
|
|
30
|
+
|
|
31
|
+
export function filterCodexGatewayHeaders(headers: ProviderHeaders | undefined): Record<string, string> {
|
|
32
|
+
const result: Record<string, string> = {};
|
|
33
|
+
for (const [name, value] of Object.entries(headers ?? {})) {
|
|
34
|
+
if (typeof value !== "string" || !CODEX_GATEWAY_FORWARD_HEADER_SET.has(name.toLowerCase())) continue;
|
|
35
|
+
result[name] = value;
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type CodexAffinity = {
|
|
41
|
+
/** Bare model id used by the gateway route selector, never provider/model. */
|
|
42
|
+
model: string;
|
|
43
|
+
scope: typeof CODEX_AFFINITY_SCOPE;
|
|
44
|
+
};
|
|
45
|
+
|
|
4
46
|
export type ResponsesHeaderRuntime = {
|
|
5
47
|
api: string;
|
|
6
48
|
apiKey: string;
|
|
7
49
|
headers?: ProviderHeaders;
|
|
8
50
|
sessionId?: string;
|
|
51
|
+
/** Opt-in gateway Codex routing metadata for synthetic requests. */
|
|
52
|
+
codexAffinity?: CodexAffinity;
|
|
9
53
|
currentModel: {
|
|
10
54
|
headers?: ProviderHeaders;
|
|
11
55
|
};
|
|
@@ -39,6 +83,12 @@ function buildCodexUserAgent(): string {
|
|
|
39
83
|
return `pi (${platform}; ${arch})`;
|
|
40
84
|
}
|
|
41
85
|
|
|
86
|
+
export function buildCodexCliUserAgent(version: string): string {
|
|
87
|
+
const platform = typeof process !== "undefined" ? process.platform : "browser";
|
|
88
|
+
const arch = typeof process !== "undefined" ? process.arch : "unknown";
|
|
89
|
+
return `codex_cli_rs/${version} (${platform}; ${arch})`;
|
|
90
|
+
}
|
|
91
|
+
|
|
42
92
|
/**
|
|
43
93
|
* The Codex backend version-gates model availability on the `version` header
|
|
44
94
|
* (the pinned @openai/codex client version). `gpt-6-astra` requires >= 0.153.0;
|
|
@@ -68,8 +118,9 @@ export function buildResponsesRequestHeaders(
|
|
|
68
118
|
runtime: ResponsesHeaderRuntime,
|
|
69
119
|
options: ResponsesHeaderOptions,
|
|
70
120
|
): Headers {
|
|
121
|
+
const mergedHeaders = mergeProviderHeaders(runtime.currentModel.headers, runtime.headers);
|
|
71
122
|
const headers = new Headers(
|
|
72
|
-
|
|
123
|
+
runtime.codexAffinity ? filterCodexGatewayHeaders(mergedHeaders) : mergedHeaders,
|
|
73
124
|
);
|
|
74
125
|
headers.set("accept", options.accept);
|
|
75
126
|
headers.set("content-type", options.contentType ?? "application/json");
|
|
@@ -77,19 +128,33 @@ export function buildResponsesRequestHeaders(
|
|
|
77
128
|
headers.set("authorization", `Bearer ${runtime.apiKey}`);
|
|
78
129
|
}
|
|
79
130
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
131
|
+
const isNativeCodex = runtime.api === "openai-codex-responses";
|
|
132
|
+
const gatewayAffinity = runtime.codexAffinity;
|
|
133
|
+
if (isNativeCodex || gatewayAffinity) {
|
|
134
|
+
if (gatewayAffinity) {
|
|
135
|
+
// A gateway request must use the configured data-plane key, never an
|
|
136
|
+
// inherited OAuth/account header from the model registry.
|
|
137
|
+
headers.set("authorization", `Bearer ${runtime.apiKey}`);
|
|
138
|
+
headers.delete("chatgpt-account-id");
|
|
139
|
+
headers.delete("cookie");
|
|
140
|
+
headers.delete("x-api-key");
|
|
141
|
+
headers.set("originator", CODEX_GATEWAY_ORIGINATOR);
|
|
142
|
+
headers.set("user-agent", buildCodexCliUserAgent(CODEX_CLIENT_VERSION));
|
|
143
|
+
headers.set("x-codex-affinity-scope", gatewayAffinity.scope);
|
|
144
|
+
headers.set("x-codex-model", gatewayAffinity.model);
|
|
145
|
+
} else {
|
|
146
|
+
const accountId = extractCodexAccountId(runtime.apiKey);
|
|
147
|
+
if (accountId) headers.set("chatgpt-account-id", accountId);
|
|
148
|
+
headers.set("originator", "pi");
|
|
149
|
+
headers.set("user-agent", buildCodexUserAgent());
|
|
150
|
+
headers.set("openai-beta", "responses=experimental");
|
|
151
|
+
}
|
|
86
152
|
// Codex backend gating: a request without a new-enough `version` never
|
|
87
153
|
// reaches model SKUs that arrived behind the gate (gpt-6-astra).
|
|
88
154
|
headers.set("version", CODEX_CLIENT_VERSION);
|
|
89
|
-
// Prompt-cache affinity rides on the conversation identity
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
// transcript requests used, or every compact pays a full cache miss.
|
|
155
|
+
// Prompt-cache/session affinity rides on the conversation identity. The
|
|
156
|
+
// gateway path also uses the bare model hint above so NEWapi can route
|
|
157
|
+
// context/compact requests without inspecting their body.
|
|
93
158
|
const sessionId = clampCodexSessionId(options.sessionId ?? runtime.sessionId);
|
|
94
159
|
if (sessionId) {
|
|
95
160
|
// Header names match the installed host's live-request builder
|
package/src/runtime.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { Api, Model, ProviderHeaders } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { isExactModelAllowed } from "./model-scope";
|
|
4
|
+
import {
|
|
5
|
+
CODEX_AFFINITY_SCOPE,
|
|
6
|
+
type CodexAffinity,
|
|
7
|
+
} from "./responses-headers";
|
|
3
8
|
import { RESPONSES_COMPACT_CAPABLE_APIS } from "./types";
|
|
4
9
|
|
|
5
10
|
const OPENAI_RESPONSES_PATH = "responses";
|
|
@@ -24,6 +29,7 @@ export type NativeCompactionFailureReason =
|
|
|
24
29
|
| "model-not-found"
|
|
25
30
|
| "unsupported-api"
|
|
26
31
|
| "missing-base-url"
|
|
32
|
+
| "missing-session-id"
|
|
27
33
|
| "missing-api-key"
|
|
28
34
|
| "auth-resolution-failed"
|
|
29
35
|
| "unsupported-payload"
|
|
@@ -34,6 +40,8 @@ export type NativeCompactionSupportOptions = {
|
|
|
34
40
|
enabled?: boolean;
|
|
35
41
|
/** Which Responses APIs should use the compact endpoint; defaults to all capable APIs. */
|
|
36
42
|
responsesApis?: readonly string[];
|
|
43
|
+
/** Exact provider/model keys whose Responses traffic carries gateway Codex affinity metadata. */
|
|
44
|
+
codexGatewayModels?: readonly string[];
|
|
37
45
|
};
|
|
38
46
|
|
|
39
47
|
export type ResponsesSupportOptions = NativeCompactionSupportOptions;
|
|
@@ -60,6 +68,8 @@ export type ResponsesRuntime = {
|
|
|
60
68
|
* requests used or the backend treats them as a new conversation.
|
|
61
69
|
*/
|
|
62
70
|
sessionId?: string;
|
|
71
|
+
/** Opt-in gateway Codex routing metadata for synthetic Responses/compact requests. */
|
|
72
|
+
codexAffinity?: CodexAffinity;
|
|
63
73
|
currentModel: RuntimeModel;
|
|
64
74
|
};
|
|
65
75
|
|
|
@@ -130,6 +140,19 @@ function normalizeConfiguredApis(values: readonly string[] | undefined): Set<str
|
|
|
130
140
|
return new Set(values.map((value) => value.trim()).filter((value) => value.length > 0));
|
|
131
141
|
}
|
|
132
142
|
|
|
143
|
+
function resolveCodexGatewayAffinity(
|
|
144
|
+
model: RuntimeModel,
|
|
145
|
+
gatewayModels: readonly string[] | undefined,
|
|
146
|
+
): CodexAffinity | undefined {
|
|
147
|
+
if (model.api !== "openai-responses" || !isExactModelAllowed(model, gatewayModels ?? [])) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
model: model.id,
|
|
152
|
+
scope: CODEX_AFFINITY_SCOPE,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
133
156
|
/** Parse "provider/model-id" (model ids may themselves contain slashes). */
|
|
134
157
|
export function parseModelSpec(spec: string): ParsedModelSpec | undefined {
|
|
135
158
|
const trimmed = spec.trim();
|
|
@@ -329,6 +352,17 @@ async function resolveNativeCompactionEnvironmentForModel(
|
|
|
329
352
|
};
|
|
330
353
|
}
|
|
331
354
|
|
|
355
|
+
const codexAffinity = resolveCodexGatewayAffinity(currentModel, options.codexGatewayModels);
|
|
356
|
+
const sessionId = resolveRuntimeSessionId(ctx);
|
|
357
|
+
if (codexAffinity && !sessionId) {
|
|
358
|
+
return {
|
|
359
|
+
ok: false,
|
|
360
|
+
reason: "missing-session-id",
|
|
361
|
+
...descriptor,
|
|
362
|
+
baseUrl,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
332
366
|
if (!auth.apiKey) {
|
|
333
367
|
return {
|
|
334
368
|
ok: false,
|
|
@@ -351,7 +385,8 @@ async function resolveNativeCompactionEnvironmentForModel(
|
|
|
351
385
|
responsesUrl: buildResponsesUrl(baseUrl, descriptor.api),
|
|
352
386
|
compactPath: buildCompactPath(descriptor.api),
|
|
353
387
|
compactUrl: buildCompactUrl(baseUrl, descriptor.api),
|
|
354
|
-
sessionId
|
|
388
|
+
sessionId,
|
|
389
|
+
codexAffinity,
|
|
355
390
|
payload: requestPayload,
|
|
356
391
|
currentModel,
|
|
357
392
|
},
|
|
@@ -378,6 +413,7 @@ export async function resolveResponsesEnvironment(
|
|
|
378
413
|
responsesPath: runtime.responsesPath,
|
|
379
414
|
responsesUrl: runtime.responsesUrl,
|
|
380
415
|
sessionId: runtime.sessionId,
|
|
416
|
+
codexAffinity: runtime.codexAffinity,
|
|
381
417
|
currentModel: runtime.currentModel,
|
|
382
418
|
},
|
|
383
419
|
};
|
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { loadToolkitConfig } from "../config";
|
|
3
|
-
import { transformWebSearchPayload } from "./payload";
|
|
4
|
-
import { appendWebSearchPrompt } from "./prompt";
|
|
5
|
-
import { isWebSearchEnabledForModel, type WebSearchModel } from "./types";
|
|
6
|
-
|
|
7
|
-
function syncLocalWebSearchTool(
|
|
8
|
-
pi: ExtensionAPI,
|
|
9
|
-
model: WebSearchModel | undefined,
|
|
10
|
-
config: Parameters<typeof isWebSearchEnabledForModel>[1],
|
|
11
|
-
state: { wasActiveBeforeOwnership?: boolean; toolkitOwnsTool: boolean },
|
|
12
|
-
): void {
|
|
13
|
-
const eligible = isWebSearchEnabledForModel(model, config);
|
|
14
|
-
const activeTools = pi.getActiveTools();
|
|
15
|
-
|
|
16
|
-
if (eligible) {
|
|
17
|
-
if (!state.toolkitOwnsTool) {
|
|
18
|
-
state.wasActiveBeforeOwnership = activeTools.includes("web_search");
|
|
19
|
-
state.toolkitOwnsTool = true;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (activeTools.includes("web_search")) {
|
|
23
|
-
pi.setActiveTools(activeTools.filter((name) => name !== "web_search"));
|
|
24
|
-
}
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (!state.toolkitOwnsTool) return;
|
|
29
|
-
|
|
30
|
-
if (state.wasActiveBeforeOwnership && !activeTools.includes("web_search")) {
|
|
31
|
-
pi.setActiveTools([...activeTools, "web_search"]);
|
|
32
|
-
}
|
|
33
|
-
state.wasActiveBeforeOwnership = undefined;
|
|
34
|
-
state.toolkitOwnsTool = false;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function registerWebSearchExtension(
|
|
38
|
-
pi: ExtensionAPI,
|
|
39
|
-
loadConfig: typeof loadToolkitConfig = loadToolkitConfig,
|
|
40
|
-
): void {
|
|
41
|
-
const toolState = { wasActiveBeforeOwnership: undefined as boolean | undefined, toolkitOwnsTool: false };
|
|
42
|
-
|
|
43
|
-
pi.on("session_start", (_event, ctx) => {
|
|
44
|
-
const { config } = loadConfig();
|
|
45
|
-
syncLocalWebSearchTool(pi, ctx.model, config.webSearch, toolState);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
pi.on("model_select", (event) => {
|
|
49
|
-
const { config } = loadConfig();
|
|
50
|
-
syncLocalWebSearchTool(pi, event.model, config.webSearch, toolState);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
pi.on("before_agent_start", (event, ctx) => {
|
|
54
|
-
const { config } = loadConfig();
|
|
55
|
-
syncLocalWebSearchTool(pi, ctx.model, config.webSearch, toolState);
|
|
56
|
-
const systemPrompt = appendWebSearchPrompt({
|
|
57
|
-
model: ctx.model,
|
|
58
|
-
config: config.webSearch,
|
|
59
|
-
systemPrompt: event.systemPrompt,
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
return systemPrompt === event.systemPrompt ? undefined : { systemPrompt };
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
pi.on("before_provider_request", (event, ctx) => {
|
|
66
|
-
const { config } = loadConfig();
|
|
67
|
-
syncLocalWebSearchTool(pi, ctx.model, config.webSearch, toolState);
|
|
68
|
-
const transformed = transformWebSearchPayload({
|
|
69
|
-
model: ctx.model,
|
|
70
|
-
config: config.webSearch,
|
|
71
|
-
payload: event.payload,
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
return transformed.changed ? transformed.payload : undefined;
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export default function webSearchExtension(pi: ExtensionAPI): void {
|
|
79
|
-
registerWebSearchExtension(pi);
|
|
80
|
-
}
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { loadToolkitConfig } from "../config";
|
|
3
|
+
import { transformWebSearchPayload } from "./payload";
|
|
4
|
+
import { appendWebSearchPrompt } from "./prompt";
|
|
5
|
+
import { isWebSearchEnabledForModel, type WebSearchModel } from "./types";
|
|
6
|
+
|
|
7
|
+
function syncLocalWebSearchTool(
|
|
8
|
+
pi: ExtensionAPI,
|
|
9
|
+
model: WebSearchModel | undefined,
|
|
10
|
+
config: Parameters<typeof isWebSearchEnabledForModel>[1],
|
|
11
|
+
state: { wasActiveBeforeOwnership?: boolean; toolkitOwnsTool: boolean },
|
|
12
|
+
): void {
|
|
13
|
+
const eligible = isWebSearchEnabledForModel(model, config);
|
|
14
|
+
const activeTools = pi.getActiveTools();
|
|
15
|
+
|
|
16
|
+
if (eligible) {
|
|
17
|
+
if (!state.toolkitOwnsTool) {
|
|
18
|
+
state.wasActiveBeforeOwnership = activeTools.includes("web_search");
|
|
19
|
+
state.toolkitOwnsTool = true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (activeTools.includes("web_search")) {
|
|
23
|
+
pi.setActiveTools(activeTools.filter((name) => name !== "web_search"));
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!state.toolkitOwnsTool) return;
|
|
29
|
+
|
|
30
|
+
if (state.wasActiveBeforeOwnership && !activeTools.includes("web_search")) {
|
|
31
|
+
pi.setActiveTools([...activeTools, "web_search"]);
|
|
32
|
+
}
|
|
33
|
+
state.wasActiveBeforeOwnership = undefined;
|
|
34
|
+
state.toolkitOwnsTool = false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function registerWebSearchExtension(
|
|
38
|
+
pi: ExtensionAPI,
|
|
39
|
+
loadConfig: typeof loadToolkitConfig = loadToolkitConfig,
|
|
40
|
+
): void {
|
|
41
|
+
const toolState = { wasActiveBeforeOwnership: undefined as boolean | undefined, toolkitOwnsTool: false };
|
|
42
|
+
|
|
43
|
+
pi.on("session_start", (_event, ctx) => {
|
|
44
|
+
const { config } = loadConfig();
|
|
45
|
+
syncLocalWebSearchTool(pi, ctx.model, config.webSearch, toolState);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
pi.on("model_select", (event) => {
|
|
49
|
+
const { config } = loadConfig();
|
|
50
|
+
syncLocalWebSearchTool(pi, event.model, config.webSearch, toolState);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
54
|
+
const { config } = loadConfig();
|
|
55
|
+
syncLocalWebSearchTool(pi, ctx.model, config.webSearch, toolState);
|
|
56
|
+
const systemPrompt = appendWebSearchPrompt({
|
|
57
|
+
model: ctx.model,
|
|
58
|
+
config: config.webSearch,
|
|
59
|
+
systemPrompt: event.systemPrompt,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return systemPrompt === event.systemPrompt ? undefined : { systemPrompt };
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
pi.on("before_provider_request", (event, ctx) => {
|
|
66
|
+
const { config } = loadConfig();
|
|
67
|
+
syncLocalWebSearchTool(pi, ctx.model, config.webSearch, toolState);
|
|
68
|
+
const transformed = transformWebSearchPayload({
|
|
69
|
+
model: ctx.model,
|
|
70
|
+
config: config.webSearch,
|
|
71
|
+
payload: event.payload,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return transformed.changed ? transformed.payload : undefined;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default function webSearchExtension(pi: ExtensionAPI): void {
|
|
79
|
+
registerWebSearchExtension(pi);
|
|
80
|
+
}
|
|
@@ -1,96 +1,96 @@
|
|
|
1
|
-
import type { WebSearchConfig } from "../types";
|
|
2
|
-
import {
|
|
3
|
-
WEB_SEARCH_SOURCE_INCLUDE,
|
|
4
|
-
isWebSearchEnabledForModel,
|
|
5
|
-
type WebSearchModel,
|
|
6
|
-
type WebSearchPayloadTransform,
|
|
7
|
-
} from "./types";
|
|
8
|
-
|
|
9
|
-
type JsonObject = Record<string, unknown>;
|
|
10
|
-
|
|
11
|
-
function isRecord(value: unknown): value is JsonObject {
|
|
12
|
-
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function isLocalWebSearchFunction(tool: unknown): boolean {
|
|
16
|
-
return isRecord(tool) && tool.type === "function" && tool.name === "web_search";
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function isNativeWebSearchTool(tool: unknown): boolean {
|
|
20
|
-
return isRecord(tool) && (tool.type === "web_search" || tool.type === "web_search_preview");
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function transformWebSearchPayload(args: {
|
|
24
|
-
model: WebSearchModel | undefined;
|
|
25
|
-
config: WebSearchConfig;
|
|
26
|
-
payload: unknown;
|
|
27
|
-
}): WebSearchPayloadTransform {
|
|
28
|
-
const { model, config, payload } = args;
|
|
29
|
-
if (!config.enabled) {
|
|
30
|
-
return { payload, outcome: "disabled", changed: false };
|
|
31
|
-
}
|
|
32
|
-
if (!isWebSearchEnabledForModel(model, config)) {
|
|
33
|
-
return { payload, outcome: "unsupported-model", changed: false };
|
|
34
|
-
}
|
|
35
|
-
if (!isRecord(payload)) {
|
|
36
|
-
return { payload, outcome: "non-object-payload", changed: false };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const toolsValue = payload.tools;
|
|
40
|
-
if (toolsValue !== undefined && !Array.isArray(toolsValue)) {
|
|
41
|
-
return { payload, outcome: "invalid-tools", changed: false };
|
|
42
|
-
}
|
|
43
|
-
const tools = toolsValue ?? [];
|
|
44
|
-
const includeValue = payload.include;
|
|
45
|
-
if (includeValue !== undefined && !Array.isArray(includeValue)) {
|
|
46
|
-
return { payload, outcome: "invalid-include", changed: false };
|
|
47
|
-
}
|
|
48
|
-
const include = includeValue ?? [];
|
|
49
|
-
const normalizedTools: unknown[] = [];
|
|
50
|
-
let nativeToolCount = 0;
|
|
51
|
-
let localToolRemoved = false;
|
|
52
|
-
for (const tool of tools) {
|
|
53
|
-
if (isLocalWebSearchFunction(tool)) {
|
|
54
|
-
localToolRemoved = true;
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
if (isNativeWebSearchTool(tool)) {
|
|
58
|
-
nativeToolCount += 1;
|
|
59
|
-
if (nativeToolCount > 1) continue;
|
|
60
|
-
}
|
|
61
|
-
normalizedTools.push(tool);
|
|
62
|
-
}
|
|
63
|
-
const hasNativeTool = nativeToolCount > 0;
|
|
64
|
-
if (!hasNativeTool) {
|
|
65
|
-
normalizedTools.push({ type: "web_search" });
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const normalizedInclude: unknown[] = [];
|
|
69
|
-
let sourceIncludeCount = 0;
|
|
70
|
-
for (const item of include) {
|
|
71
|
-
if (item === WEB_SEARCH_SOURCE_INCLUDE) {
|
|
72
|
-
sourceIncludeCount += 1;
|
|
73
|
-
if (sourceIncludeCount > 1) continue;
|
|
74
|
-
}
|
|
75
|
-
normalizedInclude.push(item);
|
|
76
|
-
}
|
|
77
|
-
if (sourceIncludeCount === 0) {
|
|
78
|
-
normalizedInclude.push(WEB_SEARCH_SOURCE_INCLUDE);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const toolsChanged = localToolRemoved || !hasNativeTool || nativeToolCount > 1;
|
|
82
|
-
const includeChanged = sourceIncludeCount !== 1;
|
|
83
|
-
if (!toolsChanged && !includeChanged) {
|
|
84
|
-
return { payload, outcome: "existing-native-tool", changed: false };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
payload: {
|
|
89
|
-
...payload,
|
|
90
|
-
...(toolsChanged ? { tools: normalizedTools } : {}),
|
|
91
|
-
...(includeChanged ? { include: normalizedInclude } : {}),
|
|
92
|
-
},
|
|
93
|
-
outcome: hasNativeTool ? "existing-native-tool" : "injected-native-tool",
|
|
94
|
-
changed: true,
|
|
95
|
-
};
|
|
96
|
-
}
|
|
1
|
+
import type { WebSearchConfig } from "../types";
|
|
2
|
+
import {
|
|
3
|
+
WEB_SEARCH_SOURCE_INCLUDE,
|
|
4
|
+
isWebSearchEnabledForModel,
|
|
5
|
+
type WebSearchModel,
|
|
6
|
+
type WebSearchPayloadTransform,
|
|
7
|
+
} from "./types";
|
|
8
|
+
|
|
9
|
+
type JsonObject = Record<string, unknown>;
|
|
10
|
+
|
|
11
|
+
function isRecord(value: unknown): value is JsonObject {
|
|
12
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isLocalWebSearchFunction(tool: unknown): boolean {
|
|
16
|
+
return isRecord(tool) && tool.type === "function" && tool.name === "web_search";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isNativeWebSearchTool(tool: unknown): boolean {
|
|
20
|
+
return isRecord(tool) && (tool.type === "web_search" || tool.type === "web_search_preview");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function transformWebSearchPayload(args: {
|
|
24
|
+
model: WebSearchModel | undefined;
|
|
25
|
+
config: WebSearchConfig;
|
|
26
|
+
payload: unknown;
|
|
27
|
+
}): WebSearchPayloadTransform {
|
|
28
|
+
const { model, config, payload } = args;
|
|
29
|
+
if (!config.enabled) {
|
|
30
|
+
return { payload, outcome: "disabled", changed: false };
|
|
31
|
+
}
|
|
32
|
+
if (!isWebSearchEnabledForModel(model, config)) {
|
|
33
|
+
return { payload, outcome: "unsupported-model", changed: false };
|
|
34
|
+
}
|
|
35
|
+
if (!isRecord(payload)) {
|
|
36
|
+
return { payload, outcome: "non-object-payload", changed: false };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const toolsValue = payload.tools;
|
|
40
|
+
if (toolsValue !== undefined && !Array.isArray(toolsValue)) {
|
|
41
|
+
return { payload, outcome: "invalid-tools", changed: false };
|
|
42
|
+
}
|
|
43
|
+
const tools = toolsValue ?? [];
|
|
44
|
+
const includeValue = payload.include;
|
|
45
|
+
if (includeValue !== undefined && !Array.isArray(includeValue)) {
|
|
46
|
+
return { payload, outcome: "invalid-include", changed: false };
|
|
47
|
+
}
|
|
48
|
+
const include = includeValue ?? [];
|
|
49
|
+
const normalizedTools: unknown[] = [];
|
|
50
|
+
let nativeToolCount = 0;
|
|
51
|
+
let localToolRemoved = false;
|
|
52
|
+
for (const tool of tools) {
|
|
53
|
+
if (isLocalWebSearchFunction(tool)) {
|
|
54
|
+
localToolRemoved = true;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (isNativeWebSearchTool(tool)) {
|
|
58
|
+
nativeToolCount += 1;
|
|
59
|
+
if (nativeToolCount > 1) continue;
|
|
60
|
+
}
|
|
61
|
+
normalizedTools.push(tool);
|
|
62
|
+
}
|
|
63
|
+
const hasNativeTool = nativeToolCount > 0;
|
|
64
|
+
if (!hasNativeTool) {
|
|
65
|
+
normalizedTools.push({ type: "web_search" });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const normalizedInclude: unknown[] = [];
|
|
69
|
+
let sourceIncludeCount = 0;
|
|
70
|
+
for (const item of include) {
|
|
71
|
+
if (item === WEB_SEARCH_SOURCE_INCLUDE) {
|
|
72
|
+
sourceIncludeCount += 1;
|
|
73
|
+
if (sourceIncludeCount > 1) continue;
|
|
74
|
+
}
|
|
75
|
+
normalizedInclude.push(item);
|
|
76
|
+
}
|
|
77
|
+
if (sourceIncludeCount === 0) {
|
|
78
|
+
normalizedInclude.push(WEB_SEARCH_SOURCE_INCLUDE);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const toolsChanged = localToolRemoved || !hasNativeTool || nativeToolCount > 1;
|
|
82
|
+
const includeChanged = sourceIncludeCount !== 1;
|
|
83
|
+
if (!toolsChanged && !includeChanged) {
|
|
84
|
+
return { payload, outcome: "existing-native-tool", changed: false };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
payload: {
|
|
89
|
+
...payload,
|
|
90
|
+
...(toolsChanged ? { tools: normalizedTools } : {}),
|
|
91
|
+
...(includeChanged ? { include: normalizedInclude } : {}),
|
|
92
|
+
},
|
|
93
|
+
outcome: hasNativeTool ? "existing-native-tool" : "injected-native-tool",
|
|
94
|
+
changed: true,
|
|
95
|
+
};
|
|
96
|
+
}
|
package/src/web-search/prompt.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import type { WebSearchConfig } from "../types";
|
|
2
|
-
import { isWebSearchEnabledForModel, WEB_SEARCH_PROMPT_MARKER, type WebSearchModel } from "./types";
|
|
3
|
-
|
|
4
|
-
export const WEB_SEARCH_PROMPT_SECTION = `${WEB_SEARCH_PROMPT_MARKER}
|
|
5
|
-
## Web Search
|
|
6
|
-
|
|
7
|
-
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
|
-
|
|
9
|
-
export function appendWebSearchPrompt(args: {
|
|
10
|
-
model: WebSearchModel | undefined;
|
|
11
|
-
config: WebSearchConfig;
|
|
12
|
-
systemPrompt: string;
|
|
13
|
-
}): string {
|
|
14
|
-
const { model, config, systemPrompt } = args;
|
|
15
|
-
if (!isWebSearchEnabledForModel(model, config) || systemPrompt.includes(WEB_SEARCH_PROMPT_MARKER)) {
|
|
16
|
-
return systemPrompt;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
return `${systemPrompt.trimEnd()}\n\n${WEB_SEARCH_PROMPT_SECTION}`;
|
|
20
|
-
}
|
|
1
|
+
import type { WebSearchConfig } from "../types";
|
|
2
|
+
import { isWebSearchEnabledForModel, WEB_SEARCH_PROMPT_MARKER, type WebSearchModel } from "./types";
|
|
3
|
+
|
|
4
|
+
export const WEB_SEARCH_PROMPT_SECTION = `${WEB_SEARCH_PROMPT_MARKER}
|
|
5
|
+
## Web Search
|
|
6
|
+
|
|
7
|
+
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
|
+
|
|
9
|
+
export function appendWebSearchPrompt(args: {
|
|
10
|
+
model: WebSearchModel | undefined;
|
|
11
|
+
config: WebSearchConfig;
|
|
12
|
+
systemPrompt: string;
|
|
13
|
+
}): string {
|
|
14
|
+
const { model, config, systemPrompt } = args;
|
|
15
|
+
if (!isWebSearchEnabledForModel(model, config) || systemPrompt.includes(WEB_SEARCH_PROMPT_MARKER)) {
|
|
16
|
+
return systemPrompt;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return `${systemPrompt.trimEnd()}\n\n${WEB_SEARCH_PROMPT_SECTION}`;
|
|
20
|
+
}
|