@howaboua/pi-codex-conversion 1.5.5-dev.25.f80a775 → 1.5.5-dev.27.aa95f55

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/README.md CHANGED
@@ -26,7 +26,7 @@ When the adapter is active, the LLM sees these tools:
26
26
  - `exec_command` — shell execution with Codex-style `cmd` parameters and resumable sessions
27
27
  - `write_stdin` — continue or poll a running exec session
28
28
  - `apply_patch` — patch tool
29
- - `web_search` — native OpenAI Codex Responses web search, enabled only on the `openai-codex` provider
29
+ - `web.run` — native OpenAI Codex Responses web search, enabled only on the `openai-codex` provider
30
30
  - `image_generation` — native OpenAI Codex Responses image generation, enabled only on image-capable `openai-codex` models
31
31
  - `view_image` — image-only wrapper around Pi's native image reading, enabled only for image-capable models
32
32
 
@@ -50,6 +50,10 @@ Use `/codex` to change adapter settings.
50
50
 
51
51
  Settings are saved globally in `~/.pi/agent/pi-codex-conversion.json`.
52
52
 
53
+ The settings UI also has an **Overrides** tab. These options intentionally do not have `/codex ...` command shortcuts:
54
+
55
+ - add only the Pi `apply_patch` tool for GPT/Codex models while keeping Pi's default toolkit, prompt, provider behavior, and compaction flow
56
+
53
57
  When `all` is on, non-Codex providers get the shell, patch, skill, and prompt-adapter behavior, but keep their normal Pi provider path. Native web search, native image generation, and priority service tier stay limited to the OpenAI Codex provider. Verbosity is applied to Responses API providers.
54
58
 
55
59
  The footer shows the active state, for example:
@@ -83,7 +87,7 @@ Raw command output is still available by expanding the tool result.
83
87
  - `exec_command` and `write_stdin` use a PTY-backed session manager for interactive commands and long-running processes.
84
88
  - `apply_patch` accepts absolute paths as-is and resolves relative paths against the current working directory.
85
89
  - Shell `apply_patch` is also available inside `exec_command`, but the dedicated `apply_patch` tool is preferred unless you are chaining edits with other shell steps.
86
- - Native `web_search` and `image_generation` are forwarded to OpenAI Codex Responses tools rather than executed as local function tools.
90
+ - Native `web.run` and `image_generation` are forwarded to OpenAI Codex Responses tools rather than executed as local function tools.
87
91
 
88
92
  ## Development checkout
89
93
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@howaboua/pi-codex-conversion",
3
- "version": "1.5.5-dev.25.f80a775",
3
+ "version": "1.5.5-dev.27.aa95f55",
4
4
  "description": "Codex-oriented tool and prompt adapter for pi coding agent",
5
5
  "type": "module",
6
6
  "repository": {
@@ -60,9 +60,9 @@
60
60
  "typebox": "*"
61
61
  },
62
62
  "devDependencies": {
63
- "@earendil-works/pi-ai": "^0.74.1",
64
- "@earendil-works/pi-coding-agent": "^0.74.1",
65
- "@earendil-works/pi-tui": "^0.74.1",
63
+ "@earendil-works/pi-ai": "^0.75.3",
64
+ "@earendil-works/pi-coding-agent": "^0.75.3",
65
+ "@earendil-works/pi-tui": "^0.75.3",
66
66
  "tsx": "^4.20.5",
67
67
  "typebox": "^1.1.24",
68
68
  "typescript": "^5.9.3"
@@ -3,21 +3,27 @@ import { isCodexLikeContext, isOpenAICodexContext, isResponsesContext } from "./
3
3
  import type { CodexConversionConfig } from "./config.ts";
4
4
  import type { AdapterState } from "./state.ts";
5
5
  import {
6
+ APPLY_PATCH_TOOL_NAME,
6
7
  CORE_ADAPTER_TOOL_NAMES,
7
8
  DEFAULT_TOOL_NAMES,
8
9
  IMAGE_GENERATION_TOOL_NAME,
10
+ APPLY_PATCH_ONLY_STATUS_TEXT,
9
11
  STATUS_KEY,
10
12
  VIEW_IMAGE_TOOL_NAME,
11
13
  WEB_SEARCH_TOOL_NAME,
14
+ SHELL_ADAPTER_TOOL_NAMES,
12
15
  buildStatusText,
13
16
  } from "./tool-set.ts";
14
17
  import { supportsNativeImageGeneration } from "../tools/image-generation-tool.ts";
15
18
  import { supportsNativeWebSearch } from "../tools/web-search-tool.ts";
16
19
 
17
20
  const ADAPTER_TOOL_NAMES = [...CORE_ADAPTER_TOOL_NAMES, WEB_SEARCH_TOOL_NAME, IMAGE_GENERATION_TOOL_NAME, VIEW_IMAGE_TOOL_NAME];
18
- const ALWAYS_OWNED_ADAPTER_TOOL_NAMES = [...CORE_ADAPTER_TOOL_NAMES, VIEW_IMAGE_TOOL_NAME];
19
21
 
20
22
  export function syncAdapter(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterState): void {
23
+ if (shouldUseApplyPatchOnly(ctx, state.config)) {
24
+ enableApplyPatchOnly(pi, ctx, state);
25
+ return;
26
+ }
21
27
  if (shouldUseCodexAdapter(ctx, state.config)) {
22
28
  enableAdapter(pi, ctx, state);
23
29
  } else {
@@ -26,9 +32,28 @@ export function syncAdapter(pi: ExtensionAPI, ctx: ExtensionContext, state: Adap
26
32
  }
27
33
 
28
34
  export function shouldUseCodexAdapter(ctx: ExtensionContext, config: CodexConversionConfig): boolean {
35
+ if (config.applyPatchOnly) return false;
29
36
  return config.useOnAllModels || isCodexLikeContext(ctx);
30
37
  }
31
38
 
39
+ export function shouldUseApplyPatchOnly(ctx: ExtensionContext, config: CodexConversionConfig): boolean {
40
+ return config.applyPatchOnly && isCodexLikeContext(ctx);
41
+ }
42
+
43
+ function enableApplyPatchOnly(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterState): void {
44
+ const adapterOwnedTools = [APPLY_PATCH_TOOL_NAME];
45
+ if (!state.enabled || state.adapterOwnedToolNames?.some((toolName) => toolName !== APPLY_PATCH_TOOL_NAME)) {
46
+ const restoredBase = state.enabled
47
+ ? restoreTools(state.previousToolNames && state.previousToolNames.length > 0 ? state.previousToolNames : DEFAULT_TOOL_NAMES, pi.getActiveTools(), state.adapterOwnedToolNames ?? ADAPTER_TOOL_NAMES)
48
+ : stripAdapterTools(pi.getActiveTools(), ADAPTER_TOOL_NAMES);
49
+ state.previousToolNames = restoredBase;
50
+ state.enabled = true;
51
+ }
52
+ state.adapterOwnedToolNames = adapterOwnedTools;
53
+ pi.setActiveTools(mergeToolNames(state.previousToolNames ?? DEFAULT_TOOL_NAMES, adapterOwnedTools));
54
+ setApplyPatchOnlyStatus(ctx, state.config);
55
+ }
56
+
32
57
  function enableAdapter(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterState): void {
33
58
  const currentAdapterOwnedTools = getAdapterOwnedToolNames(state.config);
34
59
  const adapterOwnedTools = state.enabled ? mergeToolNames(state.adapterOwnedToolNames ?? currentAdapterOwnedTools, currentAdapterOwnedTools) : currentAdapterOwnedTools;
@@ -69,14 +94,19 @@ function setStatus(ctx: ExtensionContext, enabled: boolean, config: CodexConvers
69
94
  ctx.ui.setStatus(STATUS_KEY, enabled ? buildStatusText(statusConfig) : undefined);
70
95
  }
71
96
 
97
+ function setApplyPatchOnlyStatus(ctx: ExtensionContext, config: CodexConversionConfig): void {
98
+ if (!ctx.hasUI) return;
99
+ ctx.ui.setStatus(STATUS_KEY, config.statusLine ? APPLY_PATCH_ONLY_STATUS_TEXT : undefined);
100
+ }
101
+
72
102
  function getStatusConfig(ctx: ExtensionContext, config: CodexConversionConfig): Parameters<typeof buildStatusText>[0] {
73
103
  const showOpenAICodexFlags = isOpenAICodexContext(ctx);
74
104
  const showResponsesVerbosity = isResponsesContext(ctx);
75
105
  return {
76
106
  useOnAllModels: config.useOnAllModels,
77
107
  fast: showOpenAICodexFlags && config.fast,
78
- webSearch: showOpenAICodexFlags && config.webSearch && supportsNativeWebSearch(ctx.model),
79
- imageGeneration: showOpenAICodexFlags && config.imageGeneration && supportsNativeImageGeneration(ctx.model),
108
+ webSearch: showOpenAICodexFlags && !config.applyPatchOnly && config.webSearch && supportsNativeWebSearch(ctx.model),
109
+ imageGeneration: showOpenAICodexFlags && !config.applyPatchOnly && config.imageGeneration && supportsNativeImageGeneration(ctx.model),
80
110
  compaction: { enabled: Boolean(config.responsesCompaction), model: config.compactionModel, reasoning: config.compactionReasoning },
81
111
  ...(showResponsesVerbosity ? { verbosity: config.verbosity } : {}),
82
112
  };
@@ -98,7 +128,9 @@ function getAdapterToolNames(ctx: ExtensionContext, config: CodexConversionConfi
98
128
 
99
129
  function getAdapterOwnedToolNames(config: CodexConversionConfig): string[] {
100
130
  return [
101
- ...ALWAYS_OWNED_ADAPTER_TOOL_NAMES,
131
+ ...SHELL_ADAPTER_TOOL_NAMES,
132
+ APPLY_PATCH_TOOL_NAME,
133
+ VIEW_IMAGE_TOOL_NAME,
102
134
  ...(config.webSearch ? [WEB_SEARCH_TOOL_NAME] : []),
103
135
  ...(config.imageGeneration ? [IMAGE_GENERATION_TOOL_NAME] : []),
104
136
  ];
@@ -109,7 +141,7 @@ function mergeToolNames(...toolNameGroups: string[][]): string[] {
109
141
  }
110
142
 
111
143
  export function mergeAdapterTools(activeTools: string[], adapterTools: string[], adapterOwnedTools: string[] = adapterTools): string[] {
112
- const ownedTools = new Set([...ALWAYS_OWNED_ADAPTER_TOOL_NAMES, ...adapterTools, ...adapterOwnedTools]);
144
+ const ownedTools = new Set([...CORE_ADAPTER_TOOL_NAMES, ...adapterTools, ...adapterOwnedTools]);
113
145
  const preservedTools = activeTools.filter((toolName) => !DEFAULT_TOOL_NAMES.includes(toolName) && !ownedTools.has(toolName));
114
146
  return [...adapterTools, ...preservedTools];
115
147
  }
@@ -10,6 +10,7 @@ export const COMPACTION_MODELS: readonly CompactionModel[] = ["gpt-5.5", "gpt-5.
10
10
  export const COMPACTION_REASONING_LEVELS: readonly CompactionReasoning[] = ["current", "minimal", "low", "medium", "high", "xhigh"];
11
11
 
12
12
  export interface CodexConversionConfig {
13
+ applyPatchOnly: boolean;
13
14
  fast: boolean;
14
15
  imageGeneration: boolean;
15
16
  compactionModel: CompactionModel;
@@ -23,6 +24,7 @@ export interface CodexConversionConfig {
23
24
 
24
25
  export const CODEX_CONVERSION_CONFIG_BASENAME = "pi-codex-conversion.json";
25
26
  export const DEFAULT_CODEX_CONVERSION_CONFIG: CodexConversionConfig = {
27
+ applyPatchOnly: false,
26
28
  fast: false,
27
29
  imageGeneration: true,
28
30
  compactionModel: "gpt-5.5",
@@ -68,6 +70,7 @@ export function readCodexConversionConfig(configPath: string = getCodexConversio
68
70
  const parsed = JSON.parse(readFileSync(configPath, "utf-8")) as unknown;
69
71
  if (!isObject(parsed)) return { ...DEFAULT_CODEX_CONVERSION_CONFIG };
70
72
  return {
73
+ applyPatchOnly: typeof parsed.applyPatchOnly === "boolean" ? parsed.applyPatchOnly : DEFAULT_CODEX_CONVERSION_CONFIG.applyPatchOnly,
71
74
  fast: typeof parsed.fast === "boolean" ? parsed.fast : DEFAULT_CODEX_CONVERSION_CONFIG.fast,
72
75
  imageGeneration: typeof parsed.imageGeneration === "boolean" ? parsed.imageGeneration : DEFAULT_CODEX_CONVERSION_CONFIG.imageGeneration,
73
76
  compactionModel: normalizeCompactionModel(parsed.compactionModel) ?? DEFAULT_CODEX_CONVERSION_CONFIG.compactionModel,
@@ -15,3 +15,11 @@ export function getCodexSkillPaths(cwd: string, home: string = homedir()): strin
15
15
  }
16
16
  return skillPaths.filter((path) => existsSync(path));
17
17
  }
18
+
19
+ export function hasNoSkillsFlag(argv: readonly string[] = process.argv): boolean {
20
+ for (const arg of argv) {
21
+ if (arg === "--") return false;
22
+ if (arg === "--no-skills" || arg === "-ns") return true;
23
+ }
24
+ return false;
25
+ }
@@ -1,5 +1,6 @@
1
1
  export const STATUS_KEY = "codex-adapter";
2
2
  export const STATUS_TEXT = "\u001b[38;2;0;76;255mCodex adapter\u001b[0m";
3
+ export const APPLY_PATCH_ONLY_STATUS_TEXT = `${STATUS_TEXT} • apply patch only`;
3
4
 
4
5
  export function buildStatusText(options: { verbosity?: string; webSearch: boolean; imageGeneration: boolean; fast: boolean; useOnAllModels: boolean; compaction?: { enabled: boolean; model: string; reasoning: string } }): string {
5
6
  const extras = [
@@ -17,7 +18,9 @@ export function buildStatusText(options: { verbosity?: string; webSearch: boolea
17
18
 
18
19
  export const DEFAULT_TOOL_NAMES = ["read", "bash", "edit", "write"];
19
20
 
20
- export const CORE_ADAPTER_TOOL_NAMES = ["exec_command", "write_stdin", "apply_patch"];
21
+ export const SHELL_ADAPTER_TOOL_NAMES = ["exec_command", "write_stdin"];
22
+ export const APPLY_PATCH_TOOL_NAME = "apply_patch";
23
+ export const CORE_ADAPTER_TOOL_NAMES = [...SHELL_ADAPTER_TOOL_NAMES, APPLY_PATCH_TOOL_NAME];
21
24
  export const IMAGE_GENERATION_TOOL_NAME = "image_generation";
22
25
  export const VIEW_IMAGE_TOOL_NAME = "view_image";
23
- export const WEB_SEARCH_TOOL_NAME = "web_search";
26
+ export const WEB_SEARCH_TOOL_NAME = "web.run";
@@ -17,9 +17,9 @@ export interface CodexSettingsScreenOptions {
17
17
  initialTab?: SettingsTab;
18
18
  }
19
19
 
20
- type SettingsTab = "general" | "compaction";
20
+ type SettingsTab = "general" | "compaction" | "overrides";
21
21
 
22
- const TAB_ORDER: readonly SettingsTab[] = ["general", "compaction"];
22
+ const TAB_ORDER: readonly SettingsTab[] = ["general", "compaction", "overrides"];
23
23
 
24
24
  export async function openCodexSettingsScreen(ctx: ExtensionContext, options: CodexSettingsScreenOptions): Promise<void> {
25
25
  let draft = { ...options.initialConfig };
@@ -46,6 +46,7 @@ export async function openCodexSettingsScreen(ctx: ExtensionContext, options: Co
46
46
  formatTabs(activeTab, theme),
47
47
  rule(width, theme, "borderMuted"),
48
48
  ...(activeTab === "compaction" ? formatCompactionNotes(theme) : []),
49
+ ...(activeTab === "overrides" ? formatOverridesNotes(theme) : []),
49
50
  "",
50
51
  ...settingsList.render(width),
51
52
  rule(width, theme, "borderMuted"),
@@ -74,6 +75,12 @@ function formatCompactionNotes(theme: Theme): string[] {
74
75
  ];
75
76
  }
76
77
 
78
+ function formatOverridesNotes(theme: Theme): string[] {
79
+ return [
80
+ theme.fg("dim", " Advanced tool-surface overrides."),
81
+ ];
82
+ }
83
+
77
84
  function rule(width: number, theme: Theme, color: "accent" | "borderMuted"): string {
78
85
  return theme.fg(color, "─".repeat(Math.max(0, width)));
79
86
  }
@@ -110,6 +117,12 @@ function buildItems(tab: SettingsTab, draft: CodexConversionConfig): SettingItem
110
117
  ];
111
118
  }
112
119
 
120
+ if (tab === "overrides") {
121
+ return [
122
+ { id: "applyPatchOnly", label: "Apply patch only", currentValue: draft.applyPatchOnly ? "on" : "off", values: ["off", "on"] },
123
+ ];
124
+ }
125
+
113
126
  return [
114
127
  { id: "useOnAllModels", label: "Use on all models", currentValue: draft.useOnAllModels ? "on" : "off", values: ["off", "on"] },
115
128
  { id: "statusLine", label: "Statusline", currentValue: draft.statusLine ? "on" : "off", values: ["off", "on"] },
@@ -122,6 +135,7 @@ function buildItems(tab: SettingsTab, draft: CodexConversionConfig): SettingItem
122
135
 
123
136
  function applySettingChange(id: string, value: string, draft: CodexConversionConfig): CodexConversionConfig {
124
137
  const nextDraft = { ...draft };
138
+ if (id === "applyPatchOnly") nextDraft.applyPatchOnly = value === "on";
125
139
  if (id === "useOnAllModels") nextDraft.useOnAllModels = value === "on";
126
140
  if (id === "statusLine") nextDraft.statusLine = value === "on";
127
141
  if (id === "fast") nextDraft.fast = value === "on";
@@ -136,7 +150,7 @@ function applySettingChange(id: string, value: string, draft: CodexConversionCon
136
150
 
137
151
  function formatTabs(activeTab: SettingsTab, theme: Theme): string {
138
152
  const renderTab = (tab: SettingsTab, label: string) => activeTab === tab ? theme.bold(label) : theme.fg("dim", label);
139
- return ` ${renderTab("general", "General")} ${theme.fg("dim", "/")} ${renderTab("compaction", "Compaction")}`;
153
+ return ` ${renderTab("general", "General")} ${theme.fg("dim", "/")} ${renderTab("compaction", "Compaction")} ${theme.fg("dim", "/")} ${renderTab("overrides", "Overrides")}`;
140
154
  }
141
155
 
142
156
  function formatLinks(theme: Theme): string[] {
package/src/index.ts CHANGED
@@ -18,9 +18,10 @@ import { rewriteCodexProviderRequest } from "./adapter/provider-request.ts";
18
18
  import { handleCodexSessionBeforeCompact } from "./adapter/compaction.ts";
19
19
  import { isNativeCompactionDetails, NATIVE_COMPACTION_DISPLAY_MESSAGE_TYPE, NATIVE_COMPACTION_DISPLAY_TEXT } from "./adapter/types.ts";
20
20
  import { isAdapterContextExcludedCustomMessage } from "./adapter/context-filter.ts";
21
- import { getCodexSkillPaths } from "./adapter/skills.ts";
21
+ import { getCodexSkillPaths, hasNoSkillsFlag } from "./adapter/skills.ts";
22
22
  import type { AdapterState } from "./adapter/state.ts";
23
23
  import { registerCodexCommand } from "./codex-settings/command.ts";
24
+ import { WEB_SEARCH_TOOL_NAME } from "./adapter/tool-set.ts";
24
25
 
25
26
  function getCommandArg(args: unknown): string | undefined {
26
27
  if (!args || typeof args !== "object" || !("cmd" in args) || typeof args.cmd !== "string") {
@@ -44,13 +45,16 @@ export default function codexConversion(pi: ExtensionAPI) {
44
45
  const tracker = createExecCommandTracker();
45
46
  const state: AdapterState = { enabled: false, cwd: process.cwd(), promptSkills: [], config: readCodexConversionConfig() };
46
47
  const sessions = createExecSessionManager();
47
- let nativeWebSearchRegistered = false;
48
+ const registeredNativeWebSearchTools = new Set<string>();
48
49
  let nativeImageGenerationRegistered = false;
49
50
 
50
51
  function ensureOptionalNativeToolsRegistered(config = state.config): void {
51
- if (config.webSearch && !nativeWebSearchRegistered) {
52
- registerWebSearchTool(pi);
53
- nativeWebSearchRegistered = true;
52
+ if (config.webSearch) {
53
+ const webSearchToolName = WEB_SEARCH_TOOL_NAME;
54
+ if (!registeredNativeWebSearchTools.has(webSearchToolName)) {
55
+ registerWebSearchTool(pi, webSearchToolName);
56
+ registeredNativeWebSearchTools.add(webSearchToolName);
57
+ }
54
58
  }
55
59
  if (config.imageGeneration && !nativeImageGenerationRegistered) {
56
60
  registerImageGenerationTool(pi);
@@ -93,6 +97,7 @@ export default function codexConversion(pi: ExtensionAPI) {
93
97
  });
94
98
 
95
99
  pi.on("resources_discover", async (event) => {
100
+ if (hasNoSkillsFlag()) return undefined;
96
101
  const skillPaths = getCodexSkillPaths(event.cwd);
97
102
  return skillPaths.length > 0 ? { skillPaths } : undefined;
98
103
  });
@@ -134,7 +139,7 @@ export default function codexConversion(pi: ExtensionAPI) {
134
139
  if (!shouldUseCodexAdapter(ctx, state.config)) {
135
140
  return undefined;
136
141
  }
137
- const skills = resolvePromptSkills(event.systemPromptOptions?.skills, state.promptSkills);
142
+ const skills = hasNoSkillsFlag() ? [] : resolvePromptSkills(event.systemPromptOptions?.skills, state.promptSkills);
138
143
  return {
139
144
  systemPrompt: buildCodexSystemPrompt(event.systemPrompt, {
140
145
  skills,
@@ -20,6 +20,7 @@ import {
20
20
  CODEX_TOOL_CALL_PROVIDERS,
21
21
  processResponsesStream,
22
22
  } from "./openai-responses-shared.ts";
23
+ import { WEB_SEARCH_TOOL_NAME } from "../adapter/tool-set.ts";
23
24
 
24
25
  const DEFAULT_CODEX_BASE_URL = "https://chatgpt.com/backend-api";
25
26
  const JWT_CLAIM_PATH = "https://api.openai.com/auth";
@@ -109,6 +110,19 @@ interface SessionWebSocketCacheEntry {
109
110
  continuation?: CachedWebSocketContinuationState;
110
111
  }
111
112
 
113
+ export interface OpenAICodexWebSocketDebugStats {
114
+ requests: number;
115
+ connectionsCreated: number;
116
+ connectionsReused: number;
117
+ cachedContextRequests: number;
118
+ storeTrueRequests: number;
119
+ fullContextRequests: number;
120
+ deltaRequests: number;
121
+ lastInputItems: number;
122
+ lastDeltaInputItems?: number;
123
+ lastPreviousResponseId?: string;
124
+ }
125
+
112
126
  interface AcquiredWebSocket {
113
127
  socket: WebSocketLike;
114
128
  entry?: SessionWebSocketCacheEntry;
@@ -166,6 +180,38 @@ interface ResponseEnvelope {
166
180
  type ServiceTier = ResponseCreateParamsStreaming["service_tier"];
167
181
 
168
182
  const websocketSessionCache = new Map<string, SessionWebSocketCacheEntry>();
183
+ const websocketDebugStats = new Map<string, OpenAICodexWebSocketDebugStats>();
184
+
185
+ function getOrCreateWebSocketDebugStats(sessionId: string): OpenAICodexWebSocketDebugStats {
186
+ let stats = websocketDebugStats.get(sessionId);
187
+ if (!stats) {
188
+ stats = {
189
+ requests: 0,
190
+ connectionsCreated: 0,
191
+ connectionsReused: 0,
192
+ cachedContextRequests: 0,
193
+ storeTrueRequests: 0,
194
+ fullContextRequests: 0,
195
+ deltaRequests: 0,
196
+ lastInputItems: 0,
197
+ };
198
+ websocketDebugStats.set(sessionId, stats);
199
+ }
200
+ return stats;
201
+ }
202
+
203
+ export function getOpenAICodexWebSocketDebugStats(sessionId: string): OpenAICodexWebSocketDebugStats | undefined {
204
+ const stats = websocketDebugStats.get(sessionId);
205
+ return stats ? { ...stats } : undefined;
206
+ }
207
+
208
+ export function resetOpenAICodexWebSocketDebugStats(sessionId?: string): void {
209
+ if (sessionId) {
210
+ websocketDebugStats.delete(sessionId);
211
+ return;
212
+ }
213
+ websocketDebugStats.clear();
214
+ }
169
215
 
170
216
  class NonRetryableProviderError extends Error {}
171
217
 
@@ -550,7 +596,7 @@ export function buildRequestBody<TApi extends Api>(model: Model<TApi>, context:
550
596
 
551
597
  if (context.tools && context.tools.length > 0) {
552
598
  body.tools = convertResponsesTools(context.tools, { strict: null });
553
- const hasWebSearchTool = context.tools.some((tool) => tool.name === "web_search");
599
+ const hasWebSearchTool = context.tools.some((tool) => tool.name === WEB_SEARCH_TOOL_NAME);
554
600
  if (hasWebSearchTool) {
555
601
  body.include.push("web_search_call.action.sources", "web_search_call.results");
556
602
  }
@@ -680,6 +726,7 @@ export function closeOpenAICodexWebSocketSessions(sessionId?: string): void {
680
726
  const entry = websocketSessionCache.get(sessionId);
681
727
  if (entry) closeEntry(entry);
682
728
  websocketSessionCache.delete(sessionId);
729
+ websocketDebugStats.delete(sessionId);
683
730
  return;
684
731
  }
685
732
 
@@ -687,6 +734,7 @@ export function closeOpenAICodexWebSocketSessions(sessionId?: string): void {
687
734
  closeEntry(entry);
688
735
  }
689
736
  websocketSessionCache.clear();
737
+ websocketDebugStats.clear();
690
738
  }
691
739
 
692
740
 
@@ -1230,7 +1278,7 @@ async function processWebSocketStream<TApi extends Api>(
1230
1278
  let streamStarted = false;
1231
1279
 
1232
1280
  for (let attempt = 0; attempt < 2; attempt++) {
1233
- const { socket, entry, release } = await acquireWebSocket(url, headers, options?.sessionId, options?.signal);
1281
+ const { socket, entry, reused, release } = await acquireWebSocket(url, headers, options?.sessionId, options?.signal);
1234
1282
  let keepConnection = true;
1235
1283
  let released = false;
1236
1284
  let eventCount = 0;
@@ -1240,6 +1288,24 @@ async function processWebSocketStream<TApi extends Api>(
1240
1288
  // WebSocket continuation still works via connection-scoped previous_response_id state.
1241
1289
  const fullBody = body;
1242
1290
  const requestBody = useCachedContext && entry ? buildCachedWebSocketRequestBody(entry, fullBody) : fullBody;
1291
+ const stats = options?.sessionId ? getOrCreateWebSocketDebugStats(options.sessionId) : undefined;
1292
+ if (stats) {
1293
+ stats.requests++;
1294
+ if (reused) stats.connectionsReused++;
1295
+ else stats.connectionsCreated++;
1296
+ if (useCachedContext) stats.cachedContextRequests++;
1297
+ if (requestBody.store === true) stats.storeTrueRequests++;
1298
+ stats.lastInputItems = requestBody.input?.length ?? 0;
1299
+ if (requestBody.previous_response_id) {
1300
+ stats.deltaRequests++;
1301
+ stats.lastDeltaInputItems = requestBody.input?.length ?? 0;
1302
+ stats.lastPreviousResponseId = requestBody.previous_response_id;
1303
+ } else {
1304
+ stats.fullContextRequests++;
1305
+ stats.lastDeltaInputItems = undefined;
1306
+ stats.lastPreviousResponseId = undefined;
1307
+ }
1308
+ }
1243
1309
 
1244
1310
  const releaseOnce = (releaseOptions?: { keep?: boolean }) => {
1245
1311
  if (released) return;
@@ -2,10 +2,11 @@ import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-w
2
2
  import { Type } from "typebox";
3
3
  import { Container, Text } from "@earendil-works/pi-tui";
4
4
  import { isOpenAICodexModel } from "../adapter/codex-model.ts";
5
+ import { WEB_SEARCH_TOOL_NAME } from "../adapter/tool-set.ts";
5
6
 
6
- export const WEB_SEARCH_UNSUPPORTED_MESSAGE = "web_search is only available with the openai-codex provider";
7
+ export const WEB_SEARCH_UNSUPPORTED_MESSAGE = "web.run is only available with the openai-codex provider";
7
8
  const WEB_SEARCH_LOCAL_EXECUTION_MESSAGE =
8
- "web_search is a native openai-codex provider tool and should not execute locally";
9
+ "web.run is a native openai-codex provider tool and should not execute locally";
9
10
  export const WEB_SEARCH_SESSION_NOTE_TYPE = "codex-web-search-session-note";
10
11
  const WEB_SEARCH_MULTIMODAL_CONTENT_TYPES = ["text", "image"] as const;
11
12
 
@@ -20,6 +21,7 @@ interface FunctionToolPayload {
20
21
  }
21
22
 
22
23
  interface ResponsesPayload {
24
+ include?: unknown;
23
25
  tools?: unknown[];
24
26
  [key: string]: unknown;
25
27
  }
@@ -43,7 +45,7 @@ export function supportsMultimodalNativeWebSearch(model: ExtensionContext["model
43
45
  }
44
46
 
45
47
  function isWebSearchFunctionTool(tool: unknown): tool is FunctionToolPayload {
46
- return !!tool && typeof tool === "object" && (tool as FunctionToolPayload).type === "function" && (tool as FunctionToolPayload).name === "web_search";
48
+ return !!tool && typeof tool === "object" && (tool as FunctionToolPayload).type === "function" && (tool as FunctionToolPayload).name === WEB_SEARCH_TOOL_NAME;
47
49
  }
48
50
 
49
51
  function createEmptyResultComponent(): Container {
@@ -80,17 +82,28 @@ export function rewriteNativeWebSearchTool(payload: unknown, model: ExtensionCon
80
82
  if (!rewritten) {
81
83
  return payload;
82
84
  }
85
+ const existingInclude: unknown[] = Array.isArray((payload as ResponsesPayload).include)
86
+ ? [...((payload as ResponsesPayload).include as unknown[])]
87
+ : [];
88
+ const include = [
89
+ ...existingInclude,
90
+ ...[
91
+ "web_search_call.action.sources",
92
+ "web_search_call.results",
93
+ ].filter((item) => !existingInclude.includes(item)),
94
+ ];
83
95
 
84
96
  return {
85
97
  ...(payload as ResponsesPayload),
98
+ include,
86
99
  tools: nextTools,
87
100
  };
88
101
  }
89
102
 
90
- export function createWebSearchTool(): ToolDefinition<typeof WEB_SEARCH_PARAMETERS> {
103
+ export function createWebSearchTool(name: string = WEB_SEARCH_TOOL_NAME): ToolDefinition<typeof WEB_SEARCH_PARAMETERS> {
91
104
  return {
92
- name: "web_search",
93
- label: "web_search",
105
+ name,
106
+ label: name,
94
107
  description:
95
108
  "Search the web for sources relevant to the current task. Use it when you need up-to-date information, external references, or broader context beyond the workspace.",
96
109
  promptSnippet:
@@ -104,7 +117,7 @@ export function createWebSearchTool(): ToolDefinition<typeof WEB_SEARCH_PARAMETE
104
117
  throw new Error(WEB_SEARCH_LOCAL_EXECUTION_MESSAGE);
105
118
  },
106
119
  renderCall(_args, theme) {
107
- return new Text(`${theme.fg("toolTitle", theme.bold("web_search"))}`, 0, 0);
120
+ return new Text(`${theme.fg("toolTitle", theme.bold(name))}`, 0, 0);
108
121
  },
109
122
  renderResult(result, { expanded }, theme) {
110
123
  if (!expanded) {
@@ -117,6 +130,6 @@ export function createWebSearchTool(): ToolDefinition<typeof WEB_SEARCH_PARAMETE
117
130
  };
118
131
  }
119
132
 
120
- export function registerWebSearchTool(pi: ExtensionAPI): void {
121
- pi.registerTool(createWebSearchTool());
133
+ export function registerWebSearchTool(pi: ExtensionAPI, name: string = WEB_SEARCH_TOOL_NAME): void {
134
+ pi.registerTool(createWebSearchTool(name));
122
135
  }