@oai404iao/pi-codex-minimal-tools 1.4.0 → 2.0.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.
Files changed (99) hide show
  1. package/README.md +44 -17
  2. package/THIRD_PARTY_NOTICES.md +20 -0
  3. package/config.schema.json +1 -2
  4. package/index.ts +2 -0
  5. package/package.json +14 -13
  6. package/provenance/openai-codex-ddea03ad-astra.json +54 -0
  7. package/src/activation.ts +1 -56
  8. package/src/adapter/compaction/checkpoint.ts +1 -0
  9. package/src/adapter/compaction/collect.ts +1 -0
  10. package/src/adapter/compaction/http.ts +1 -0
  11. package/src/adapter/compaction/request.ts +1 -0
  12. package/src/adapter/compaction/transport.ts +1 -0
  13. package/src/adapter/compaction/websocket.ts +1 -0
  14. package/src/background-image-generation.ts +1 -574
  15. package/src/capabilities.ts +1 -146
  16. package/src/codex-http.ts +1 -133
  17. package/src/codex-identity-extension.ts +1 -339
  18. package/src/codex-request-profile.ts +1 -45
  19. package/src/codex-reserved-tools.ts +1 -323
  20. package/src/codex-wire-identity.ts +1 -596
  21. package/src/extension/prewarm-snapshot.ts +1 -0
  22. package/src/extension/provider-presentation.ts +1 -0
  23. package/src/extension/provider-runtime.ts +1 -0
  24. package/src/extension/register.ts +36 -0
  25. package/src/extension/startup-prewarm.ts +1 -0
  26. package/src/fast-mode.ts +1 -124
  27. package/src/glyphs.ts +1 -70
  28. package/src/index.ts +8 -331
  29. package/src/model-catalog/catalog.ts +1 -636
  30. package/src/model-catalog/default-models.json +8 -0
  31. package/src/model-catalog/runtime.ts +1 -113
  32. package/src/model-catalog/types.ts +1 -95
  33. package/src/native-compaction.ts +1 -393
  34. package/src/patch/apply.ts +1 -338
  35. package/src/patch/parser.ts +1 -224
  36. package/src/patch/render.ts +1 -201
  37. package/src/provider-headers.ts +1 -54
  38. package/src/provider-native-tools.ts +1 -71
  39. package/src/provider-shim.ts +20 -4511
  40. package/src/providers/codex-apply-patch-tool.ts +1 -23
  41. package/src/providers/openai-codex/cache-key.ts +1 -0
  42. package/src/providers/openai-codex/captured-stream.ts +1 -0
  43. package/src/providers/openai-codex/constants.ts +1 -0
  44. package/src/providers/openai-codex/continuation.ts +1 -0
  45. package/src/providers/openai-codex/errors.ts +1 -0
  46. package/src/providers/openai-codex/events.ts +1 -0
  47. package/src/providers/openai-codex/headers.ts +1 -0
  48. package/src/providers/openai-codex/lite.ts +1 -0
  49. package/src/providers/openai-codex/message.ts +1 -0
  50. package/src/providers/openai-codex/prewarm.ts +1 -0
  51. package/src/providers/openai-codex/proxy.ts +1 -0
  52. package/src/providers/openai-codex/reasoning.ts +1 -0
  53. package/src/providers/openai-codex/request-body.ts +1 -0
  54. package/src/providers/openai-codex/request-context.ts +1 -0
  55. package/src/providers/openai-codex/request-metadata.ts +1 -0
  56. package/src/providers/openai-codex/retry.ts +1 -0
  57. package/src/providers/openai-codex/runtime.ts +1 -0
  58. package/src/providers/openai-codex/sse.ts +1 -0
  59. package/src/providers/openai-codex/stream-effects.ts +1 -0
  60. package/src/providers/openai-codex/stream.ts +1 -0
  61. package/src/providers/openai-codex/types.ts +1 -0
  62. package/src/providers/openai-codex/urls.ts +1 -0
  63. package/src/providers/openai-codex/usage.ts +1 -0
  64. package/src/providers/openai-codex/websocket-connection.ts +1 -0
  65. package/src/providers/openai-codex/websocket-events.ts +1 -0
  66. package/src/providers/openai-codex/websocket-session.ts +1 -0
  67. package/src/providers/openai-codex/websocket-socket.ts +1 -0
  68. package/src/providers/openai-codex/websocket-stream.ts +1 -0
  69. package/src/providers/openai-responses-shared.ts +7 -1463
  70. package/src/providers/responses/citations.ts +1 -0
  71. package/src/providers/responses/history.ts +1 -0
  72. package/src/providers/responses/items.ts +1 -0
  73. package/src/providers/responses/markdown.ts +1 -0
  74. package/src/providers/responses/messages.ts +1 -0
  75. package/src/providers/responses/signatures.ts +1 -0
  76. package/src/providers/responses/stream-state.ts +1 -0
  77. package/src/providers/responses/stream.ts +1 -0
  78. package/src/providers/responses/text-renderer.ts +1 -0
  79. package/src/providers/responses/text.ts +1 -0
  80. package/src/providers/responses/tool-identity.ts +1 -0
  81. package/src/providers/responses/tools.ts +1 -0
  82. package/src/providers/responses/types.ts +1 -0
  83. package/src/providers/responses/usage.ts +1 -0
  84. package/src/settings.ts +1 -247
  85. package/src/subagent-inline.ts +1 -8
  86. package/src/tools/apply-patch.ts +1 -84
  87. package/src/tools/image-generation/capture.ts +1 -0
  88. package/src/tools/image-generation/display.ts +1 -0
  89. package/src/tools/image-generation/preview.ts +1 -0
  90. package/src/tools/image-generation/storage.ts +1 -0
  91. package/src/tools/image-generation/types.ts +1 -0
  92. package/src/tools/image-generation.ts +1 -274
  93. package/src/tools/view-image.ts +1 -98
  94. package/src/tools/web-search/activity.ts +1 -0
  95. package/src/tools/web-search/capture.ts +1 -0
  96. package/src/tools/web-search/render.ts +1 -0
  97. package/src/tools/web-search.ts +1 -554
  98. package/src/utils/images.ts +1 -73
  99. package/src/utils/theme.ts +1 -0
@@ -1,554 +1 @@
1
- import { buildSessionContext, type SessionEntry } from "@earendil-works/pi-coding-agent";
2
- import type { Api, Model, ProviderHeaders } from "@earendil-works/pi-ai";
3
- import { Text } from "@earendil-works/pi-tui";
4
- import {
5
- buildCodexJsonHeaders,
6
- hasCodexRequestAuth,
7
- resolveCodexApiEndpoint,
8
- } from "../codex-http.js";
9
- import { glyphs, truncateText } from "../glyphs.js";
10
- import { loadModelSettings } from "../model-catalog/runtime.js";
11
- import {
12
- resolveCodexRequestIdentity,
13
- type CodexRequestIdentity,
14
- } from "../codex-wire-identity.js";
15
-
16
- export interface SearchQuery {
17
- q: string;
18
- recency?: number;
19
- domains?: string[];
20
- }
21
-
22
- export interface WebSearchInput {
23
- search_query?: SearchQuery[];
24
- image_query?: SearchQuery[];
25
- open?: Array<{ ref_id: string; lineno?: number }>;
26
- click?: Array<{ ref_id: string; id: number }>;
27
- find?: Array<{ ref_id: string; pattern: string }>;
28
- screenshot?: Array<{ ref_id: string; pageno: number }>;
29
- finance?: Array<{
30
- ticker: string;
31
- type: "equity" | "fund" | "crypto" | "index";
32
- market?: string;
33
- }>;
34
- weather?: Array<{ location: string; start?: string; duration?: number }>;
35
- sports?: Array<{
36
- tool?: "sports";
37
- fn: "schedule" | "standings";
38
- league: "nba" | "wnba" | "nfl" | "nhl" | "mlb" | "epl" | "ncaamb" | "ncaawb" | "ipl";
39
- team?: string;
40
- opponent?: string;
41
- date_from?: string;
42
- date_to?: string;
43
- num_games?: number;
44
- locale?: string;
45
- }>;
46
- time?: Array<{ utc_offset: string }>;
47
- response_length?: "short" | "medium" | "long";
48
- }
49
-
50
- interface WebSearchToolContext {
51
- cwd: string;
52
- model?: Model<Api>;
53
- modelRegistry?: {
54
- getApiKeyAndHeaders(model: Model<Api>): Promise<
55
- | { ok: true; apiKey?: string; headers?: ProviderHeaders }
56
- | { ok: false; error: string }
57
- >;
58
- };
59
- sessionManager?: {
60
- getSessionId(): string;
61
- getBranch?(): SessionEntry[];
62
- };
63
- }
64
-
65
- interface StandaloneSearchResponse {
66
- encrypted_output?: string | null;
67
- output?: string;
68
- results?: unknown[];
69
- }
70
-
71
- export interface StandaloneWebSearchResult {
72
- type?: string;
73
- domain?: string;
74
- ref_id?: string;
75
- snippet?: string;
76
- title?: string;
77
- url?: string;
78
- }
79
-
80
- export interface StandaloneWebSearchDetails {
81
- mode: "standalone";
82
- results: StandaloneWebSearchResult[];
83
- }
84
-
85
- export interface StandaloneWebSearchInvocation {
86
- turnId?: string;
87
- identity?: CodexRequestIdentity;
88
- }
89
-
90
- const CODEX_STANDALONE_SEARCH_OUTPUT_TOKEN_LIMIT = 10_000;
91
- const SEARCH_OPERATION_KEYS = [
92
- "search_query",
93
- "image_query",
94
- "open",
95
- "click",
96
- "find",
97
- "screenshot",
98
- "finance",
99
- "weather",
100
- "sports",
101
- "time",
102
- ] as const;
103
-
104
- const searchQuerySchema = {
105
- type: "object",
106
- additionalProperties: false,
107
- required: ["q"],
108
- properties: {
109
- q: { type: "string", minLength: 1, description: "Search query." },
110
- recency: { type: "integer", minimum: 0, description: "Restrict results to this many recent days." },
111
- domains: { type: "array", items: { type: "string", minLength: 1 }, description: "Restrict results to these domains." },
112
- },
113
- };
114
-
115
- export const webSearchToolSchema = {
116
- type: "object",
117
- additionalProperties: false,
118
- properties: {
119
- search_query: {
120
- type: "array",
121
- maxItems: 4,
122
- items: searchQuerySchema,
123
- description: "Query the internet search engine.",
124
- },
125
- image_query: {
126
- type: "array",
127
- maxItems: 2,
128
- items: searchQuerySchema,
129
- description: "Query the image search engine.",
130
- },
131
- open: {
132
- type: "array",
133
- items: {
134
- type: "object",
135
- additionalProperties: false,
136
- required: ["ref_id"],
137
- properties: {
138
- ref_id: { type: "string" },
139
- lineno: { type: "integer", minimum: 0 },
140
- },
141
- },
142
- },
143
- click: {
144
- type: "array",
145
- items: {
146
- type: "object",
147
- additionalProperties: false,
148
- required: ["ref_id", "id"],
149
- properties: {
150
- ref_id: { type: "string" },
151
- id: { type: "integer", minimum: 0 },
152
- },
153
- },
154
- },
155
- find: {
156
- type: "array",
157
- items: {
158
- type: "object",
159
- additionalProperties: false,
160
- required: ["ref_id", "pattern"],
161
- properties: {
162
- ref_id: { type: "string" },
163
- pattern: { type: "string" },
164
- },
165
- },
166
- },
167
- screenshot: {
168
- type: "array",
169
- items: {
170
- type: "object",
171
- additionalProperties: false,
172
- required: ["ref_id", "pageno"],
173
- properties: {
174
- ref_id: { type: "string" },
175
- pageno: { type: "integer", minimum: 0 },
176
- },
177
- },
178
- },
179
- finance: {
180
- type: "array",
181
- items: {
182
- type: "object",
183
- additionalProperties: false,
184
- required: ["ticker", "type"],
185
- properties: {
186
- ticker: { type: "string" },
187
- type: { type: "string", enum: ["equity", "fund", "crypto", "index"] },
188
- market: { type: "string" },
189
- },
190
- },
191
- },
192
- weather: {
193
- type: "array",
194
- items: {
195
- type: "object",
196
- additionalProperties: false,
197
- required: ["location"],
198
- properties: {
199
- location: { type: "string" },
200
- start: { type: "string" },
201
- duration: { type: "integer", minimum: 1 },
202
- },
203
- },
204
- },
205
- sports: {
206
- type: "array",
207
- items: {
208
- type: "object",
209
- additionalProperties: false,
210
- required: ["fn", "league"],
211
- properties: {
212
- tool: { type: "string", enum: ["sports"] },
213
- fn: { type: "string", enum: ["schedule", "standings"] },
214
- league: { type: "string", enum: ["nba", "wnba", "nfl", "nhl", "mlb", "epl", "ncaamb", "ncaawb", "ipl"] },
215
- team: { type: "string" },
216
- opponent: { type: "string" },
217
- date_from: { type: "string" },
218
- date_to: { type: "string" },
219
- num_games: { type: "integer", minimum: 1 },
220
- locale: { type: "string" },
221
- },
222
- },
223
- },
224
- time: {
225
- type: "array",
226
- items: {
227
- type: "object",
228
- additionalProperties: false,
229
- required: ["utc_offset"],
230
- properties: {
231
- utc_offset: { type: "string" },
232
- },
233
- },
234
- },
235
- response_length: {
236
- type: "string",
237
- enum: ["short", "medium", "long"],
238
- },
239
- },
240
- };
241
-
242
- function visibleMessageText(content: unknown): string {
243
- if (typeof content === "string") return content;
244
- if (!Array.isArray(content)) return "";
245
- return content
246
- .filter((item): item is { type: "text"; text: string } =>
247
- Boolean(item)
248
- && typeof item === "object"
249
- && (item as { type?: unknown }).type === "text"
250
- && typeof (item as { text?: unknown }).text === "string")
251
- .map((item) => item.text)
252
- .join("\n")
253
- .trim();
254
- }
255
-
256
- function recentSearchInput(ctx: WebSearchToolContext, turnId?: string): unknown[] | undefined {
257
- if (!ctx.sessionManager?.getBranch) return undefined;
258
- const visible: Array<{ role: "user" | "assistant"; text: string }> = [];
259
- for (const message of buildSessionContext(ctx.sessionManager.getBranch()).messages) {
260
- if (message.role !== "user" && message.role !== "assistant") continue;
261
- const text = visibleMessageText(message.content);
262
- if (!text || (message.role === "user" && /^<environment_context>[\s\S]*<\/environment_context>$/i.test(text))) {
263
- continue;
264
- }
265
- visible.push({ role: message.role, text });
266
- }
267
- const userIndexes = visible
268
- .map((message, index) => message.role === "user" ? index : -1)
269
- .filter((index) => index >= 0);
270
- const start = userIndexes.length > 1 ? userIndexes[userIndexes.length - 2]! : userIndexes[0] ?? 0;
271
- const tail = visible.slice(start);
272
- let currentUserIndex = -1;
273
- for (let index = 0; index < tail.length; index++) {
274
- if (tail[index]?.role === "user") currentUserIndex = index;
275
- }
276
- let assistantBudget = 4_000;
277
- return tail.map((message, index) => {
278
- let text = message.text;
279
- if (message.role === "assistant") {
280
- text = text.slice(0, Math.max(0, assistantBudget));
281
- assistantBudget -= text.length;
282
- }
283
- return {
284
- type: "message",
285
- role: message.role,
286
- content: [{
287
- type: message.role === "assistant" ? "output_text" : "input_text",
288
- text,
289
- }],
290
- ...(turnId && index === currentUserIndex
291
- ? { internal_chat_message_metadata_passthrough: { turn_id: turnId } }
292
- : {}),
293
- };
294
- }).filter((message) => message.content[0]!.text.length > 0);
295
- }
296
-
297
- function searchOperationLabel(input: WebSearchInput): string {
298
- const operations = SEARCH_OPERATION_KEYS.filter((key) => (input[key]?.length ?? 0) > 0);
299
- return operations.length > 0 ? operations.join(", ") : "commands";
300
- }
301
-
302
- function assertStandaloneSearchOutput(output: string, input: WebSearchInput): void {
303
- const normalized = output.trim();
304
- if (/^Found no tool response\b[\s\S]*arguments you provided were not valid\.?$/i.test(normalized)) {
305
- throw new Error(
306
- `Standalone web search backend returned no tool response for ${searchOperationLabel(input)}. `
307
- + "The endpoint accepted the request but could not execute it; retry with search_query or another supported operation.",
308
- );
309
- }
310
- if (/^Error parsing function call\b/i.test(normalized)) {
311
- throw new Error(
312
- `Standalone web search backend rejected ${searchOperationLabel(input)}: ${normalized}`,
313
- );
314
- }
315
- }
316
-
317
- function searchInputSummary(input: WebSearchInput): string {
318
- const queries = [
319
- ...(input.search_query ?? []).map((query) => query.q),
320
- ...(input.image_query ?? []).map((query) => query.q),
321
- ].map((query) => query.trim()).filter(Boolean);
322
- if (queries.length > 0) {
323
- return queries.length > 1 ? `${queries[0]} +${queries.length - 1}` : queries[0]!;
324
- }
325
- const open = input.open?.[0]?.ref_id?.trim();
326
- if (open) return open;
327
- const find = input.find?.[0];
328
- if (find?.pattern?.trim()) return find.pattern.trim();
329
- const weather = input.weather?.[0]?.location?.trim();
330
- if (weather) return weather;
331
- const finance = input.finance?.[0]?.ticker?.trim();
332
- if (finance) return finance;
333
- const sports = input.sports?.[0];
334
- if (sports) return [sports.league, sports.team, sports.fn].filter(Boolean).join(" ");
335
- const time = input.time?.[0]?.utc_offset?.trim();
336
- if (time) return time;
337
- return searchOperationLabel(input);
338
- }
339
-
340
- function resultHost(result: StandaloneWebSearchResult): string | undefined {
341
- const domain = result.domain?.trim().replace(/^www\./i, "");
342
- if (domain) return domain;
343
- if (!result.url) return undefined;
344
- try {
345
- return new URL(result.url).hostname.replace(/^www\./i, "") || undefined;
346
- } catch {
347
- return undefined;
348
- }
349
- }
350
-
351
- export function standaloneWebSearchHosts(results: readonly StandaloneWebSearchResult[]): string[] {
352
- const seen = new Set<string>();
353
- const hosts: string[] = [];
354
- for (const result of results) {
355
- const host = resultHost(result);
356
- const key = host?.toLowerCase();
357
- if (!host || !key || seen.has(key)) continue;
358
- seen.add(key);
359
- hosts.push(host);
360
- }
361
- return hosts;
362
- }
363
-
364
- function renderHostTags(
365
- results: readonly StandaloneWebSearchResult[],
366
- theme: any,
367
- cwd?: string,
368
- ): string {
369
- const hosts = standaloneWebSearchHosts(results);
370
- if (hosts.length === 0) return "";
371
- const shown = hosts.slice(0, 8);
372
- const separator = theme.fg("dim", glyphs(cwd).dot);
373
- const tags = shown.map((host) => theme.fg("accent", host));
374
- if (hosts.length > shown.length) tags.push(theme.fg("dim", `+${hosts.length - shown.length}`));
375
- return tags.join(separator);
376
- }
377
-
378
- function renderStandaloneWebSearchCall(input: WebSearchInput, theme: any, cwd?: string): Text {
379
- const summary = truncateText(searchInputSummary(input), 96, cwd);
380
- const text = `${theme.fg("accent", glyphs(cwd).bullet)}`
381
- + theme.fg("text", theme.bold("Web Search"))
382
- + (summary ? theme.fg("dim", ` ${summary}`) : "");
383
- return new Text(text, 0, 0);
384
- }
385
-
386
- function renderStandaloneWebSearchResult(
387
- result: { content?: Array<{ type?: string; text?: string }>; details?: StandaloneWebSearchDetails },
388
- options: { expanded?: boolean; isPartial?: boolean },
389
- theme: any,
390
- context: { cwd?: string; isError?: boolean },
391
- ): Text {
392
- if (options.isPartial) return new Text("", 0, 0);
393
- const text = result.content
394
- ?.filter((part) => part.type === "text" && typeof part.text === "string")
395
- .map((part) => part.text)
396
- .join("\n") ?? "";
397
- if (context.isError) return new Text(theme.fg("error", text || "Web search failed"), 0, 0);
398
-
399
- const results = result.details?.mode === "standalone" ? result.details.results : [];
400
- const hosts = renderHostTags(results, theme, context.cwd);
401
- const count = results.length;
402
- let rendered = count > 0 ? `${hosts ? `${hosts} ` : ""}${theme.fg("dim", `(${count})`)}` : theme.fg("muted", "Search complete");
403
- if (options.expanded && text) rendered += `\n\n${theme.fg("toolOutput", text)}`;
404
- return new Text(rendered, 0, 0);
405
- }
406
-
407
- export async function standaloneWebSearch(
408
- input: WebSearchInput,
409
- ctx: WebSearchToolContext,
410
- signal?: AbortSignal,
411
- invocation: StandaloneWebSearchInvocation = {},
412
- ) {
413
- const model = ctx.model;
414
- if (!model || !ctx.modelRegistry) throw new Error("No active model is available for standalone web search.");
415
- const settings = loadModelSettings(model, ctx.cwd);
416
- if (!settings.enabled) throw new Error("pi-codex-minimal-tools is disabled.");
417
- if (settings.webSearchImplementation !== "standalone") {
418
- throw new Error(`Standalone web search is not enabled for ${model.provider}/${model.id}.`);
419
- }
420
- const contentTypes = settings.modelProfile?.effective.tools.webSearch
421
- ? settings.modelProfile.effective.tools.webSearch.contentTypes ?? ["text"]
422
- : [];
423
- if (input.search_query?.length && !contentTypes.includes("text")) {
424
- throw new Error("Text search is disabled by the current model profile.");
425
- }
426
- if (input.image_query?.length && !contentTypes.includes("image")) {
427
- throw new Error("Image search is disabled by the current model profile.");
428
- }
429
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
430
- if (!auth.ok) throw new Error(auth.error);
431
- if (!hasCodexRequestAuth({
432
- modelHeaders: model.headers,
433
- auth: { apiKey: auth.apiKey, headers: auth.headers },
434
- })) {
435
- throw new Error(`No request authentication for provider: ${model.provider}`);
436
- }
437
-
438
- const url = resolveCodexApiEndpoint(model.baseUrl, settings.apiKeyMode, "alpha/search");
439
- const piSessionId = ctx.sessionManager?.getSessionId();
440
- const identity = invocation.identity
441
- ?? resolveCodexRequestIdentity(
442
- piSessionId,
443
- invocation.turnId ? { turn_id: invocation.turnId } : undefined,
444
- "turn",
445
- );
446
- const turnId = identity?.turnId || invocation.turnId;
447
- const searchInput = recentSearchInput(ctx, turnId);
448
- const turnMetadata = identity && turnId
449
- ? JSON.stringify({
450
- session_id: identity.sessionId,
451
- thread_id: identity.threadId,
452
- turn_id: turnId,
453
- ...(identity.forkedFromThreadId
454
- ? {
455
- forked_from_thread_id:
456
- identity.forkedFromThreadId,
457
- }
458
- : {}),
459
- ...(identity.parentThreadId
460
- ? { parent_thread_id: identity.parentThreadId }
461
- : {}),
462
- model: model.id,
463
- })
464
- : undefined;
465
- const response = await fetch(url, {
466
- method: "POST",
467
- headers: buildCodexJsonHeaders({
468
- modelHeaders: model.headers,
469
- auth: { apiKey: auth.apiKey, headers: auth.headers },
470
- apiKeyMode: settings.apiKeyMode,
471
- ...(turnMetadata
472
- ? { extraHeaders: { "x-codex-turn-metadata": turnMetadata } }
473
- : {}),
474
- }),
475
- body: JSON.stringify({
476
- id: identity?.sessionId
477
- ?? piSessionId
478
- ?? `pi-search-${Date.now()}`,
479
- model: model.id,
480
- ...(searchInput ? { input: searchInput } : {}),
481
- commands: input,
482
- settings: {
483
- allowed_callers: ["direct"],
484
- external_web_access: true,
485
- },
486
- max_output_tokens: CODEX_STANDALONE_SEARCH_OUTPUT_TOKEN_LIMIT,
487
- }),
488
- signal,
489
- });
490
- if (!response.ok) {
491
- throw new Error(`Standalone web search failed: HTTP ${response.status}: ${await response.text()}`);
492
- }
493
- const result = await response.json() as StandaloneSearchResponse;
494
- if (typeof result.output !== "string" || !result.output.trim()) {
495
- throw new Error("Standalone web search returned no output.");
496
- }
497
- assertStandaloneSearchOutput(result.output, input);
498
- return {
499
- content: [{ type: "text", text: result.output }],
500
- details: {
501
- mode: "standalone",
502
- results: (result.results ?? []) as StandaloneWebSearchResult[],
503
- } satisfies StandaloneWebSearchDetails,
504
- };
505
- }
506
-
507
- export function createWebSearchToolDefinition(options: {
508
- getCurrentTurnId?: (sessionId: string | undefined) => string | undefined;
509
- getRequestIdentity?: (
510
- sessionId: string | undefined,
511
- ) => CodexRequestIdentity | undefined;
512
- } = {}) {
513
- return {
514
- name: "web_search",
515
- label: "Web Search",
516
- description: "Search the web using the implementation selected by the current model profile. Hosted profiles are rewritten into the OpenAI Responses web_search tool; standalone profiles call the Codex alpha/search endpoint.",
517
- promptSnippet: "Search the web when current information or citations are needed.",
518
- promptGuidelines: ["Use web_search when current web information or cited sources are needed."],
519
- parameters: webSearchToolSchema,
520
- renderCall(input: WebSearchInput, theme: any, context: { cwd?: string }) {
521
- return renderStandaloneWebSearchCall(input ?? {}, theme, context?.cwd);
522
- },
523
- renderResult(
524
- result: { content?: Array<{ type?: string; text?: string }>; details?: StandaloneWebSearchDetails },
525
- renderOptions: { expanded?: boolean; isPartial?: boolean },
526
- theme: any,
527
- context: { cwd?: string; isError?: boolean },
528
- ) {
529
- return renderStandaloneWebSearchResult(result, renderOptions, theme, context);
530
- },
531
- async execute(
532
- _toolCallId: string,
533
- input: WebSearchInput,
534
- signal: AbortSignal | undefined,
535
- _onUpdate: unknown,
536
- ctx: WebSearchToolContext,
537
- ) {
538
- const settings = loadModelSettings(ctx.model, ctx.cwd);
539
- if (settings.webSearchImplementation === "standalone") {
540
- const sessionId = ctx.sessionManager?.getSessionId();
541
- const identity = options.getRequestIdentity?.(sessionId);
542
- return standaloneWebSearch(input, ctx, signal, {
543
- turnId: identity?.turnId
544
- ?? options.getCurrentTurnId?.(sessionId),
545
- identity,
546
- });
547
- }
548
- return {
549
- content: [{ type: "text", text: "web_search is hosted-provider-first for this model profile and should be rewritten before execution." }],
550
- details: { phase: "native-provider", nativeTool: "web_search" },
551
- };
552
- },
553
- };
554
- }
1
+ export * from "@oai404iao/pi-codex-web-search/internal/tools/web-search";
@@ -1,73 +1 @@
1
- import { copyFile, mkdir, writeFile } from "node:fs/promises";
2
- import { existsSync } from "node:fs";
3
- import { randomUUID } from "node:crypto";
4
- import { dirname, extname, isAbsolute, join, resolve } from "node:path";
5
- import type { CodexMinimalToolsSettings } from "../settings.js";
6
-
7
- export interface SavedImageInfo {
8
- path: string;
9
- latestPath?: string;
10
- mimeType: string;
11
- format: string;
12
- bytes: number;
13
- }
14
-
15
- export function projectRoot(cwd: string): string {
16
- let current = resolve(cwd);
17
- while (true) {
18
- if (existsSync(join(current, ".git")) || existsSync(join(current, ".pi"))) return current;
19
- const parent = dirname(current);
20
- if (parent === current) return resolve(cwd);
21
- current = parent;
22
- }
23
- }
24
-
25
- export function imageOutputDir(cwd: string, settings: Pick<CodexMinimalToolsSettings, "imageOutputDir">): string {
26
- const configured = settings.imageOutputDir || ".pi/openai-codex-images";
27
- return isAbsolute(configured) ? resolve(configured) : resolve(projectRoot(cwd), configured);
28
- }
29
-
30
- export function imageFormatToMime(format: string | undefined): string {
31
- const normalized = (format || "png").toLowerCase().replace(/^\./, "");
32
- if (normalized === "jpg" || normalized === "jpeg") return "image/jpeg";
33
- if (normalized === "webp") return "image/webp";
34
- return "image/png";
35
- }
36
-
37
- export function extensionForMime(mimeType: string): string {
38
- if (mimeType === "image/jpeg") return "jpeg";
39
- if (mimeType === "image/webp") return "webp";
40
- return "png";
41
- }
42
-
43
- export function inferImageFormat(value: unknown, fallback = "png"): string {
44
- if (typeof value !== "string") return fallback;
45
- const lower = value.toLowerCase();
46
- if (lower.includes("jpeg") || lower.includes("jpg")) return "jpeg";
47
- if (lower.includes("webp")) return "webp";
48
- if (lower.includes("png")) return "png";
49
- const ext = extname(lower).replace(/^\./, "");
50
- return ext || fallback;
51
- }
52
-
53
- export async function saveBase64Image(options: {
54
- base64: string;
55
- callId?: string;
56
- cwd: string;
57
- format?: string;
58
- responseId?: string;
59
- settings: Pick<CodexMinimalToolsSettings, "imageOutputDir">;
60
- }): Promise<SavedImageInfo> {
61
- const format = inferImageFormat(options.format, "png");
62
- const mimeType = imageFormatToMime(format);
63
- const ext = extensionForMime(mimeType);
64
- const dir = imageOutputDir(options.cwd, options.settings);
65
- await mkdir(dir, { recursive: true });
66
- const unique = randomUUID().slice(0, 8);
67
- const filePath = join(dir, `${new Date().toISOString().replace(/[:.]/g, "-")}-${unique}.${ext}`);
68
- const data = Buffer.from(options.base64, "base64");
69
- await writeFile(filePath, data, { mode: 0o600 });
70
- const latestPath = join(dir, `latest.${ext}`);
71
- await copyFile(filePath, latestPath);
72
- return { bytes: data.byteLength, format: ext, latestPath, mimeType, path: filePath };
73
- }
1
+ export * from "@oai404iao/pi-codex-imagegen/internal/utils/images";
@@ -0,0 +1 @@
1
+ export * from "@oai404iao/pi-codex-runtime/internal/utils/theme";