@oai404iao/pi-codex-core 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/LICENSE +28 -0
  2. package/LICENSES/Apache-2.0.txt +201 -0
  3. package/LICENSES/OpenAI-Codex-NOTICE.txt +6 -0
  4. package/README.md +26 -0
  5. package/THIRD_PARTY_NOTICES.md +18 -0
  6. package/package.json +84 -0
  7. package/provenance/openai-codex-eb9dceba-reserved-tools.json +140 -0
  8. package/src/adapter/compaction/checkpoint.ts +159 -0
  9. package/src/adapter/compaction/collect.ts +51 -0
  10. package/src/adapter/compaction/http.ts +101 -0
  11. package/src/adapter/compaction/request.ts +159 -0
  12. package/src/adapter/compaction/transport.ts +125 -0
  13. package/src/adapter/compaction/websocket.ts +119 -0
  14. package/src/extension/prewarm-snapshot.ts +27 -0
  15. package/src/extension/provider-runtime.ts +101 -0
  16. package/src/extension/startup-prewarm.ts +264 -0
  17. package/src/fast-mode.ts +124 -0
  18. package/src/index.ts +257 -0
  19. package/src/native-compaction.ts +392 -0
  20. package/src/patch/apply.ts +338 -0
  21. package/src/patch/parser.ts +224 -0
  22. package/src/patch/render.ts +201 -0
  23. package/src/provider-native-tools.ts +75 -0
  24. package/src/providers/codex-apply-patch-tool.ts +23 -0
  25. package/src/providers/codex-apply-patch.lark +19 -0
  26. package/src/providers/openai-codex/cache-key.ts +52 -0
  27. package/src/providers/openai-codex/captured-stream.ts +50 -0
  28. package/src/providers/openai-codex/constants.ts +61 -0
  29. package/src/providers/openai-codex/continuation.ts +110 -0
  30. package/src/providers/openai-codex/errors.ts +130 -0
  31. package/src/providers/openai-codex/events.ts +123 -0
  32. package/src/providers/openai-codex/headers.ts +224 -0
  33. package/src/providers/openai-codex/lite.ts +24 -0
  34. package/src/providers/openai-codex/message.ts +33 -0
  35. package/src/providers/openai-codex/prewarm.ts +76 -0
  36. package/src/providers/openai-codex/proxy.ts +55 -0
  37. package/src/providers/openai-codex/reasoning.ts +54 -0
  38. package/src/providers/openai-codex/request-body.ts +149 -0
  39. package/src/providers/openai-codex/request-context.ts +20 -0
  40. package/src/providers/openai-codex/request-metadata.ts +137 -0
  41. package/src/providers/openai-codex/retry.ts +154 -0
  42. package/src/providers/openai-codex/runtime.ts +1 -0
  43. package/src/providers/openai-codex/sse.ts +93 -0
  44. package/src/providers/openai-codex/stream.ts +367 -0
  45. package/src/providers/openai-codex/urls.ts +24 -0
  46. package/src/providers/openai-codex/usage.ts +60 -0
  47. package/src/providers/openai-codex/websocket-connection.ts +216 -0
  48. package/src/providers/openai-codex/websocket-events.ts +210 -0
  49. package/src/providers/openai-codex/websocket-session.ts +192 -0
  50. package/src/providers/openai-codex/websocket-socket.ts +18 -0
  51. package/src/providers/openai-codex/websocket-stream.ts +151 -0
  52. package/src/tools/apply-patch.ts +84 -0
  53. package/src/tools/view-image.ts +98 -0
@@ -0,0 +1,264 @@
1
+ import type { Api, Model } from "@earendil-works/pi-ai/compat";
2
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import { hasCodexRequestAuth, resolveCodexRequestAccountId } from "@oai404iao/pi-codex-runtime/internal/codex-http";
4
+ import { resolveCodexRequestProfile } from "@oai404iao/pi-codex-runtime/internal/codex-request-profile";
5
+ import { resolveCodexRequestIdentity } from "@oai404iao/pi-codex-runtime/internal/codex-wire-identity";
6
+ import { applyFastModeServiceTier } from "../fast-mode.js";
7
+ import { loadModelSettings } from "@oai404iao/pi-codex-runtime/internal/model-catalog/runtime";
8
+ import { rewriteNativeOpenAiTools } from "../provider-native-tools.js";
9
+ import { webSocketCacheKey, webSocketFallbackKey } from "../providers/openai-codex/cache-key.js";
10
+ import { WEBSOCKET_PREWARM_TIMEOUT_MS } from "../providers/openai-codex/constants.js";
11
+ import { applyConfiguredResponsesFeatureHeaders, buildWebSocketHeaders } from "../providers/openai-codex/headers.js";
12
+ import { withResponsesLiteWebSocketMetadata } from "../providers/openai-codex/lite.js";
13
+ import { prewarmWebSocket } from "../providers/openai-codex/prewarm.js";
14
+ import { buildRequestBody, ensureWebSearchDetailsIncluded } from "../providers/openai-codex/request-body.js";
15
+ import { isWebSocketUpgradeRejectedError } from "../providers/openai-codex/retry.js";
16
+ import { resolveResponsesWebSocketUrl } from "../providers/openai-codex/urls.js";
17
+ import { websocketHttpFallbackSessions, websocketSessionCache } from "../providers/openai-codex/websocket-session.js";
18
+ import { startupPrewarmSnapshot, type StartupPrewarmSnapshot } from "./prewarm-snapshot.js";
19
+ import type { NativeToolOwnership } from "@oai404iao/pi-codex-runtime/internal/providers/openai-codex/types";
20
+
21
+ interface StartupPrewarmState {
22
+ status: "pending" | "ready" | "failed";
23
+ promise: Promise<void>;
24
+ abortController: AbortController;
25
+ }
26
+
27
+ interface SessionStartupPrewarmTask {
28
+ generation: number;
29
+ modelIdentity: string;
30
+ promise: Promise<void>;
31
+ abortController: AbortController;
32
+ }
33
+
34
+ function settleSpeculativeTask(task: Promise<void>, signal: AbortSignal): Promise<void> {
35
+ return new Promise(resolve => {
36
+ const finish = () => {
37
+ signal.removeEventListener("abort", finish);
38
+ resolve();
39
+ };
40
+ signal.addEventListener("abort", finish, { once: true });
41
+ // The auth API may not cooperate with cancellation. Release callers now
42
+ // but keep both handlers attached to consume a late resolution/rejection.
43
+ void task.then(finish, finish);
44
+ if (signal.aborted) finish();
45
+ });
46
+ }
47
+
48
+ export function createStartupPrewarmLifecycle(pi: ExtensionAPI, ownsNativeTool?: NativeToolOwnership) {
49
+ const startupPrewarms = new Map<string, StartupPrewarmState>();
50
+ const sessionStartupPrewarmTasks = new Map<string, SessionStartupPrewarmTask>();
51
+ let sessionGeneration = 0;
52
+
53
+ const reset = () => {
54
+ sessionGeneration++;
55
+ for (const state of startupPrewarms.values()) state.abortController.abort();
56
+ for (const task of sessionStartupPrewarmTasks.values()) task.abortController.abort();
57
+ startupPrewarms.clear();
58
+ sessionStartupPrewarmTasks.clear();
59
+ };
60
+
61
+ const modelIdentity = (model: Model<Api>): string =>
62
+ `${model.provider}\n${model.api}\n${model.id}\n${model.baseUrl}`;
63
+
64
+ const scheduleStartupPrewarm = (
65
+ ctx: ExtensionContext,
66
+ generation: number,
67
+ startupSignal: AbortSignal,
68
+ snapshot: StartupPrewarmSnapshot,
69
+ ): Promise<void> => (async () => {
70
+ if (startupSignal.aborted) return;
71
+ const model = ctx.model as Model<Api> | undefined;
72
+ const sessionId = ctx?.sessionManager?.getSessionId?.();
73
+ if (!model || !sessionId || generation !== sessionGeneration) return;
74
+ const settings = loadModelSettings(model, ctx.cwd);
75
+ if (
76
+ !settings.enabled
77
+ || !settings.modelProfile?.effective.enabled
78
+ || !settings.providerShimActive
79
+ || !settings.openaiWebSocketPrewarm
80
+ || settings.openaiTransport === "sse"
81
+ ) {
82
+ return;
83
+ }
84
+
85
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
86
+ if (
87
+ startupSignal.aborted
88
+ || generation !== sessionGeneration
89
+ || !auth.ok
90
+ || !hasCodexRequestAuth({
91
+ modelHeaders: model.headers,
92
+ auth: { apiKey: auth.apiKey, headers: auth.headers },
93
+ })
94
+ ) {
95
+ return;
96
+ }
97
+
98
+ const profile = resolveCodexRequestProfile(settings.requestProfile);
99
+ const requestIdentity = resolveCodexRequestIdentity(
100
+ sessionId,
101
+ undefined,
102
+ "prewarm",
103
+ );
104
+ // Match Codex startup prewarm: snapshot only stable request context.
105
+ // Conversation history and the first user message are sent by the first
106
+ // real request as an incremental continuation from this response.
107
+ let body = applyFastModeServiceTier(
108
+ buildRequestBody(model, {
109
+ systemPrompt: snapshot.systemPrompt,
110
+ messages: [],
111
+ tools: snapshot.tools,
112
+ }, profile, {
113
+ ownsNativeTool,
114
+ apiKey: auth.apiKey,
115
+ headers: auth.headers,
116
+ sessionId,
117
+ reasoning: snapshot.reasoning ?? (model.reasoning ? "medium" : undefined),
118
+ }),
119
+ settings,
120
+ model,
121
+ );
122
+ if (settings.nativeProviderTools) {
123
+ const webSearch = settings.modelProfile.effective.tools.webSearch;
124
+ body = rewriteNativeOpenAiTools(body, {
125
+ ownsNativeTool,
126
+ imageModel: settings.imageModel,
127
+ imageGeneration: settings.imageGenerationImplementation ?? false,
128
+ webSearch: settings.webSearchEnabled && webSearch
129
+ ? {
130
+ implementation: webSearch.implementation,
131
+ contentTypes: webSearch.contentTypes,
132
+ }
133
+ : false,
134
+ }).payload;
135
+ }
136
+ ensureWebSearchDetailsIncluded(body);
137
+ body = withResponsesLiteWebSocketMetadata(body, profile.responsesMode);
138
+
139
+ const websocketUrl = resolveResponsesWebSocketUrl(model.baseUrl, { apiKeyMode: settings.apiKeyMode });
140
+ const fallbackKey = webSocketFallbackKey(
141
+ sessionId,
142
+ model,
143
+ websocketUrl,
144
+ settings.modelProfileHash,
145
+ );
146
+ if (settings.openaiTransport === "auto" && fallbackKey && websocketHttpFallbackSessions.has(fallbackKey)) {
147
+ return;
148
+ }
149
+ if (startupSignal.aborted || generation !== sessionGeneration) return;
150
+ const headers = applyConfiguredResponsesFeatureHeaders(buildWebSocketHeaders(
151
+ model.headers,
152
+ auth.headers,
153
+ resolveCodexRequestAccountId({
154
+ modelHeaders: model.headers,
155
+ auth: { apiKey: auth.apiKey, headers: auth.headers },
156
+ apiKeyMode: settings.apiKeyMode,
157
+ }),
158
+ auth.apiKey ?? "",
159
+ sessionId,
160
+ requestIdentity?.threadId ?? sessionId,
161
+ requestIdentity,
162
+ ), settings, model);
163
+ const cacheKey = webSocketCacheKey(
164
+ sessionId,
165
+ model,
166
+ websocketUrl,
167
+ headers,
168
+ settings.modelProfileHash,
169
+ );
170
+ if (
171
+ !cacheKey
172
+ || startupSignal.aborted
173
+ || generation !== sessionGeneration
174
+ || startupPrewarms.has(cacheKey)
175
+ ) {
176
+ return;
177
+ }
178
+ if (websocketSessionCache.get(cacheKey)?.continuation) return;
179
+
180
+ const abortController = new AbortController();
181
+ const abortFromSession = () => abortController.abort();
182
+ startupSignal.addEventListener("abort", abortFromSession, { once: true });
183
+ const state: StartupPrewarmState = {
184
+ status: "pending",
185
+ abortController,
186
+ promise: Promise.resolve(),
187
+ };
188
+ startupPrewarms.set(cacheKey, state);
189
+ const timeout = setTimeout(() => abortController.abort(), WEBSOCKET_PREWARM_TIMEOUT_MS);
190
+ state.promise = prewarmWebSocket({
191
+ url: websocketUrl,
192
+ headers,
193
+ cacheKey,
194
+ body,
195
+ requestMetadata: {
196
+ sessionId,
197
+ threadId: requestIdentity?.threadId ?? sessionId,
198
+ turnId: requestIdentity?.turnId ?? "",
199
+ requestKind: "prewarm",
200
+ ...(requestIdentity ? { identity: requestIdentity } : {}),
201
+ },
202
+ signal: abortController.signal,
203
+ connectTimeoutMs: WEBSOCKET_PREWARM_TIMEOUT_MS,
204
+ })
205
+ .then(() => {
206
+ state.status = "ready";
207
+ })
208
+ .catch((error) => {
209
+ state.status = "failed";
210
+ if (
211
+ !startupSignal.aborted
212
+ && generation === sessionGeneration
213
+ && settings.openaiTransport === "auto"
214
+ && fallbackKey
215
+ && isWebSocketUpgradeRejectedError(error)
216
+ ) {
217
+ websocketHttpFallbackSessions.add(fallbackKey);
218
+ }
219
+ })
220
+ .finally(() => {
221
+ clearTimeout(timeout);
222
+ startupSignal.removeEventListener("abort", abortFromSession);
223
+ });
224
+ await state.promise;
225
+ })();
226
+
227
+ const start = (ctx: ExtensionContext) => {
228
+ const generation = sessionGeneration;
229
+ // Do not block session startup. The first provider request naturally
230
+ // serializes behind this socket operation if it is still pending.
231
+ const model = ctx?.model as Model<Api> | undefined;
232
+ const sessionId = ctx?.sessionManager?.getSessionId?.();
233
+ if (model && sessionId) {
234
+ const abortController = new AbortController();
235
+ const snapshot = startupPrewarmSnapshot(pi, ctx);
236
+ // Prewarm is speculative. Auth/request preparation failures must not
237
+ // escape as unhandled promises or prevent the real request from running.
238
+ const promise = settleSpeculativeTask(
239
+ scheduleStartupPrewarm(ctx, generation, abortController.signal, snapshot),
240
+ abortController.signal,
241
+ );
242
+ sessionStartupPrewarmTasks.set(sessionId, {
243
+ generation,
244
+ modelIdentity: modelIdentity(model),
245
+ promise,
246
+ abortController,
247
+ });
248
+ void promise;
249
+ }
250
+ };
251
+
252
+ return {
253
+ reset,
254
+ start,
255
+ get(sessionId: string, requestModel: Model<Api>): Promise<void> | undefined {
256
+ const task = sessionStartupPrewarmTasks.get(sessionId);
257
+ return task
258
+ && task.generation === sessionGeneration
259
+ && task.modelIdentity === modelIdentity(requestModel)
260
+ ? task.promise
261
+ : undefined;
262
+ },
263
+ };
264
+ }
@@ -0,0 +1,124 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionCommandContext,
4
+ ExtensionContext,
5
+ } from "@earendil-works/pi-coding-agent";
6
+ import {
7
+ modelKey,
8
+ type ModelLike,
9
+ } from "@oai404iao/pi-codex-runtime/internal/capabilities";
10
+ import { loadModelSettings } from "@oai404iao/pi-codex-runtime/internal/model-catalog/runtime";
11
+ import type { ResolvedCodexModelSettings } from "@oai404iao/pi-codex-runtime/internal/model-catalog/runtime";
12
+ import {
13
+ configPath,
14
+ loadSettings,
15
+ updateConfig,
16
+ type CodexMinimalToolsSettings,
17
+ } from "@oai404iao/pi-codex-runtime/internal/settings";
18
+
19
+ export const FAST_MODE_STATUS_KEY = "codex-fast-mode";
20
+ export const FAST_MODE_SERVICE_TIER = "priority" as const;
21
+
22
+ export function isFastModeModel(model: ModelLike | undefined): boolean {
23
+ return Boolean(loadModelSettings(model).fastServiceTier);
24
+ }
25
+
26
+ export function resolveFastModeServiceTier(
27
+ settings: Pick<CodexMinimalToolsSettings, "enabled" | "fastMode"> & Partial<ResolvedCodexModelSettings>,
28
+ model: ModelLike | undefined,
29
+ ): string | undefined {
30
+ if (!settings.enabled || !settings.fastMode) return undefined;
31
+ if (settings.modelProfile) return settings.fastServiceTier;
32
+ return loadModelSettings(model, undefined, settings as CodexMinimalToolsSettings).fastServiceTier;
33
+ }
34
+
35
+ export function applyFastModeServiceTier<T extends Record<string, unknown>>(
36
+ body: T,
37
+ settings: Pick<CodexMinimalToolsSettings, "enabled" | "fastMode"> & Partial<ResolvedCodexModelSettings>,
38
+ model: ModelLike | undefined,
39
+ ): T {
40
+ const serviceTier = resolveFastModeServiceTier(settings, model);
41
+ if (!serviceTier || body.service_tier !== undefined) return body;
42
+ return { ...body, service_tier: serviceTier };
43
+ }
44
+
45
+ function fastModeLines(ctx: ExtensionContext): string[] {
46
+ const settings = loadSettings(ctx.cwd);
47
+ const model = ctx.model as ModelLike | undefined;
48
+ const modelSettings = loadModelSettings(model, ctx.cwd, settings);
49
+ const activeTier = resolveFastModeServiceTier(modelSettings, model);
50
+ return [
51
+ "Codex Fast mode",
52
+ `enabled: ${settings.fastMode}`,
53
+ `service tier: ${modelSettings.fastServiceTier ?? "(unsupported)"}`,
54
+ `model: ${modelKey(model)}`,
55
+ `active for current model: ${Boolean(activeTier)}`,
56
+ `config: ${configPath()}`,
57
+ ];
58
+ }
59
+
60
+ export function syncFastModeStatus(ctx: ExtensionContext): void {
61
+ const settings = loadSettings(ctx.cwd);
62
+ const model = ctx.model as ModelLike | undefined;
63
+ const tier = resolveFastModeServiceTier(loadModelSettings(model, ctx.cwd, settings), model);
64
+ const ui = ctx.ui as ExtensionContext["ui"] | undefined;
65
+ ui?.setStatus?.(
66
+ FAST_MODE_STATUS_KEY,
67
+ tier,
68
+ );
69
+ }
70
+
71
+ function showFastModeStatus(ctx: ExtensionCommandContext): void {
72
+ syncFastModeStatus(ctx as ExtensionContext);
73
+ ctx.ui.notify(fastModeLines(ctx as ExtensionContext).join("\n"), "info");
74
+ }
75
+
76
+ export function registerFastMode(pi: ExtensionAPI): void {
77
+ pi.registerCommand("fast", {
78
+ description: "Toggle model-profile Fast mode. Usage: /fast [on|off|status]",
79
+ handler: async (args: string, ctx) => {
80
+ const command = args.trim().toLowerCase().split(/\s+/, 1)[0] ?? "";
81
+ const settings = loadSettings(ctx.cwd);
82
+ let patch: Partial<CodexMinimalToolsSettings> | undefined;
83
+
84
+ switch (command) {
85
+ case "":
86
+ patch = { fastMode: !settings.fastMode };
87
+ break;
88
+ case "on":
89
+ patch = { fastMode: true };
90
+ break;
91
+ case "off":
92
+ patch = { fastMode: false };
93
+ break;
94
+ case "status":
95
+ showFastModeStatus(ctx);
96
+ return;
97
+ default:
98
+ ctx.ui.notify("Usage: /fast [on|off|status]", "warning");
99
+ return;
100
+ }
101
+
102
+ try {
103
+ updateConfig(patch);
104
+ showFastModeStatus(ctx);
105
+ } catch (error) {
106
+ ctx.ui.notify(
107
+ `Failed to update Fast mode: ${error instanceof Error ? error.message : String(error)}`,
108
+ "error",
109
+ );
110
+ }
111
+ },
112
+ });
113
+
114
+ pi.on("session_start", async (_event, ctx) => {
115
+ syncFastModeStatus(ctx);
116
+ });
117
+ pi.on("model_select", async (_event, ctx) => {
118
+ syncFastModeStatus(ctx);
119
+ });
120
+ pi.on("session_shutdown", async (_event, ctx) => {
121
+ const ui = ctx.ui as ExtensionContext["ui"] | undefined;
122
+ ui?.setStatus?.(FAST_MODE_STATUS_KEY, undefined);
123
+ });
124
+ }
package/src/index.ts ADDED
@@ -0,0 +1,257 @@
1
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
3
+ import { getCapabilities, Image, Text, type Component } from "@earendil-works/pi-tui";
4
+
5
+ import { hasConfiguredModelsLoaded } from "@oai404iao/pi-codex-runtime/internal/activation";
6
+
7
+ import {
8
+ computeToolCapabilities,
9
+ modelKey,
10
+ type ModelLike,
11
+ } from "@oai404iao/pi-codex-runtime/internal/capabilities";
12
+
13
+ import { resolveCodexRequestProfile } from "@oai404iao/pi-codex-runtime/internal/codex-request-profile";
14
+
15
+ import { registerFastMode, resolveFastModeServiceTier } from "./fast-mode.js";
16
+
17
+ import { glyphs } from "@oai404iao/pi-codex-runtime/internal/glyphs";
18
+
19
+ import {
20
+ modelCatalogDiagnostics,
21
+ modelsPath,
22
+ resolveModelProfile,
23
+ } from "@oai404iao/pi-codex-runtime/internal/model-catalog/catalog";
24
+
25
+ import { loadModelSettings } from "@oai404iao/pi-codex-runtime/internal/model-catalog/runtime";
26
+
27
+ import { registerNativeCompaction } from "./native-compaction.js";
28
+
29
+ import { rewriteNativeOpenAiTools } from "./provider-native-tools.js";
30
+
31
+ import { configPath, loadSettings, settingsDiagnostics } from "@oai404iao/pi-codex-runtime/internal/settings";
32
+
33
+ import { createApplyPatchToolDefinition } from "./tools/apply-patch.js";
34
+
35
+ import { viewImage, viewImageToolSchema, type ValidatedImage, type ViewImageInput } from "./tools/view-image.js";
36
+
37
+ import { addPackageTool, ensureCodexServices } from "@oai404iao/pi-codex-runtime";
38
+
39
+ import { registerResponsesProviderRuntime } from "./extension/provider-runtime.js";
40
+
41
+
42
+
43
+ function terminalImageProtocol(): "kitty" | "iterm2" | null {
44
+ return getCapabilities().images ?? null;
45
+ }
46
+
47
+ function formatBytes(bytes: number): string {
48
+ if (!Number.isFinite(bytes) || bytes < 0) return "unknown size";
49
+ if (bytes < 1024) return `${bytes} B`;
50
+ if (bytes < 1024 * 1024) return `${Math.round(bytes / 102.4) / 10}K`;
51
+ return `${Math.round(bytes / (1024 * 102.4)) / 10}M`;
52
+ }
53
+
54
+ function viewImageCallText(args: ViewImageInput | undefined, theme: any): string {
55
+ const path = typeof args?.path === "string" ? args.path : "image";
56
+ const detail = args?.detail && args.detail !== "auto" ? ` ${theme.fg("dim", `${glyphs().dot.trim()} ${args.detail}`)}` : "";
57
+ return `${theme.fg("accent", glyphs().bullet)}${theme.fg("text", theme.bold("View Image "))}${theme.fg("accent", path)}${detail}`;
58
+ }
59
+
60
+ function viewImageResultText(details: ValidatedImage | undefined, theme: any): string {
61
+ if (!details) return `${theme.fg("accent", glyphs().bullet)}${theme.fg("text", theme.bold("View Image"))}${theme.fg("dim", `${glyphs().dot}image loaded`)}`;
62
+ const type = details.mimeType.replace(/^image\//, "").toUpperCase();
63
+ const protocol = terminalImageProtocol();
64
+ const preview = protocol ? theme.fg("success", `inline ${protocol}`) : theme.fg("warning", "fallback");
65
+ return `${theme.fg("accent", glyphs().bullet)}${theme.fg("text", theme.bold("View Image "))}${theme.fg("accent", details.displayPath)}${theme.fg("dim", glyphs().dot)}${theme.fg("success", type)}${theme.fg("dim", `${glyphs().dot}${formatBytes(details.sizeBytes)}${glyphs().dot}`)}${preview}`;
66
+ }
67
+
68
+ function emptyComponent(): Component {
69
+ return { invalidate() {}, render: () => [] };
70
+ }
71
+
72
+ function textComponent(text: string): Component {
73
+ return new Text(text, 0, 0);
74
+ }
75
+
76
+ function viewImageResultComponent(result: any, options: any, theme: any, context: any): Component {
77
+ if (options?.isPartial) return emptyComponent();
78
+ const details = result?.details as ValidatedImage | undefined;
79
+ const imagePart = result?.content?.find?.((part: any) => part?.type === "image" && typeof part.data === "string" && typeof part.mimeType === "string");
80
+ const header = textComponent(viewImageResultText(details, theme));
81
+ if (!imagePart) return header;
82
+ const imageTheme = { fallbackColor: (text: string) => theme.fg("dim", text) };
83
+ const maxHeightCells = options?.expanded ? 28 : 18;
84
+ const image = new Image(imagePart.data, imagePart.mimeType, imageTheme, { maxWidthCells: 80, maxHeightCells, filename: details?.displayPath });
85
+ return {
86
+ invalidate() {
87
+ header.invalidate();
88
+ image.invalidate();
89
+ },
90
+ render(width: number): string[] {
91
+ return [...header.render(width), ...image.render(width)];
92
+ },
93
+ };
94
+ }
95
+
96
+ function contextModel(ctx: ExtensionContext): ModelLike | undefined {
97
+ return ctx.model as ModelLike | undefined;
98
+ }
99
+
100
+ function statusLines(pi: ExtensionAPI, ctx: ExtensionContext): string[] {
101
+ const settings = loadSettings(ctx.cwd);
102
+ const model = contextModel(ctx);
103
+ const modelSettings = loadModelSettings(model, ctx.cwd, settings);
104
+ const capabilities = computeToolCapabilities(model, settings);
105
+ const requestProfile = resolveCodexRequestProfile(modelSettings.requestProfile);
106
+ const modelProfile = modelSettings.modelProfile;
107
+ const active = new Set(pi.getActiveTools?.() ?? []);
108
+ const fastModeTier = resolveFastModeServiceTier(modelSettings, model);
109
+ return [
110
+ "Codex Minimal Tools",
111
+ `model: ${modelKey(model)}`,
112
+ `config: ${configPath()}`,
113
+ `models: ${modelsPath()}`,
114
+ `model profile: ${modelProfile ? `${modelProfile.sources.join("+")} #${modelProfile.profileHash}` : "(none)"}`,
115
+ `configured models loaded: ${hasConfiguredModelsLoaded(ctx, settings)}`,
116
+ `enabled: ${settings.enabled}`,
117
+ `autoEnable: ${settings.autoEnable}`,
118
+ `provider shim: ${modelSettings.providerShimActive ? "active" : "inactive"}`,
119
+ `responses endpoint: ${modelSettings.apiKeyMode ? "openai" : "codex"}`,
120
+ `responses transport: ${modelSettings.openaiTransport}`,
121
+ `Responses WebSocket prewarm: ${modelSettings.openaiWebSocketPrewarm}`,
122
+ `fast mode: ${settings.fastMode ? modelSettings.fastServiceTier ?? "on (unsupported)" : "off"}${fastModeTier ? ", active" : ""}`,
123
+ `compaction: ${modelSettings.compactionMode}`,
124
+ `request profile: ${requestProfile.responsesMode}/${requestProfile.patchTransport}, summary=${requestProfile.reasoningSummary}, system=${requestProfile.systemPromptPlacement}, hosted=${requestProfile.supportsHostedTools}, parallel=${requestProfile.supportsParallelTools}`,
125
+ `web search: ${modelSettings.webSearchImplementation ?? "off"}`,
126
+ `image generation: ${modelSettings.imageGenerationImplementation ?? "off"}`,
127
+ `legacy additionalModelIds: ${settings.additionalModelIds.length > 0 ? settings.additionalModelIds.join(", ") : "(none)"}`,
128
+ `apiKeyMode: ${modelSettings.apiKeyMode}`,
129
+ `native provider shim: ${settings.enabled ? "registered" : "disabled"}`,
130
+ "tools:",
131
+ ...Object.entries(capabilities).map(([name, capability]) => `- ${name}: ${capability.enabled ? "supported" : "disabled"}${active.has(name) ? ", active" : ""} — ${capability.reason}`),
132
+ ];
133
+ }
134
+
135
+ function registerDiagnosticCommand(pi: ExtensionAPI): void {
136
+ const showDoctor = (ctx: ExtensionCommandContext) => {
137
+ const settings = loadSettings(ctx.cwd);
138
+ const lines = statusLines(pi, ctx as ExtensionContext);
139
+ lines.push(`image output dir: ${settings.imageOutputDir}`);
140
+ lines.push(`OPENAI_API_KEY: ${process.env.OPENAI_API_KEY ? "present" : "not set"}`);
141
+ const diagnostics = settingsDiagnostics();
142
+ if (diagnostics.length > 0) lines.push("settings diagnostics:", ...diagnostics.map((line) => `- ${line}`));
143
+ const catalogDiagnostics = modelCatalogDiagnostics();
144
+ if (catalogDiagnostics.length > 0) lines.push("model catalog diagnostics:", ...catalogDiagnostics.map((line) => `- ${line}`));
145
+ const profileDiagnostics = loadModelSettings(contextModel(ctx), ctx.cwd, settings)
146
+ .modelProfile?.diagnostics
147
+ .filter((line) => !catalogDiagnostics.includes(line)) ?? [];
148
+ if (profileDiagnostics.length > 0) {
149
+ lines.push("active model profile diagnostics:", ...profileDiagnostics.map((line) => `- ${line}`));
150
+ }
151
+ ctx.ui.notify(lines.join("\n"), "info");
152
+ };
153
+ pi.registerCommand("codex-minimal-tools", {
154
+ description: "Show Codex Minimal Tools status. Usage: /codex-minimal-tools | /codex-minimal-tools:doctor",
155
+ handler: async (args: string, ctx) => {
156
+ const subcommand = args.trim().split(/\s+/, 1)[0]?.toLowerCase();
157
+ if (subcommand === "doctor") {
158
+ showDoctor(ctx);
159
+ return;
160
+ }
161
+ if (!subcommand) {
162
+ ctx.ui.notify(statusLines(pi, ctx as ExtensionContext).join("\n"), "info");
163
+ return;
164
+ }
165
+ ctx.ui.notify(statusLines(pi, ctx as ExtensionContext).join("\n"), "info");
166
+ },
167
+ });
168
+ pi.registerCommand("codex-minimal-tools:doctor", {
169
+ description: "Run lightweight self-checks",
170
+ handler: async (_args: string, ctx) => showDoctor(ctx),
171
+ });
172
+ }
173
+
174
+ function registerCoreTools(pi: ExtensionAPI): void {
175
+ pi.registerTool({
176
+ renderShell: "self",
177
+ name: "view_image",
178
+ label: "View Image",
179
+ description: "Inspect a local image file by returning image content to the model. Relative paths resolve against ctx.cwd; a leading @ is accepted.",
180
+ promptSnippet: "Inspect local image files by path.",
181
+ promptGuidelines: ["Use view_image when you need to inspect a local image file; pass the path in the path argument."],
182
+ parameters: viewImageToolSchema,
183
+ async execute(_toolCallId: string, params: ViewImageInput, _signal: AbortSignal | undefined, _onUpdate: unknown, ctx: ExtensionContext) {
184
+ const settings = loadSettings(ctx.cwd);
185
+ return viewImage(params, ctx.cwd, { workspaceOnly: settings.viewImageWorkspaceOnly }) as never;
186
+ },
187
+ renderCall(args: ViewImageInput, theme: any, context: any) {
188
+ if (context?.executionStarted && !context?.isPartial) return emptyComponent();
189
+ return textComponent(viewImageCallText(args, theme));
190
+ },
191
+ renderResult(result: any, options: any, theme: any, context: any) {
192
+ return viewImageResultComponent(result, options, theme, context);
193
+ },
194
+ } as never);
195
+ pi.registerTool(createApplyPatchToolDefinition({
196
+ deferRendering: loadSettings().deferApplyPatchRendering,
197
+ }) as never);
198
+ }
199
+
200
+
201
+ export default function codexCore(pi: ExtensionAPI): void {
202
+ const broker = ensureCodexServices(pi);
203
+ if (!broker.claim("package:core")) return;
204
+ const ownsNativeTool = (name: "web_search" | "image_generation") => broker.tools.has(name);
205
+ let currentCwd = process.cwd();
206
+ pi.on("session_start", (_event, ctx) => { currentCwd = ctx.cwd; });
207
+ pi.on("model_select", (_event, ctx) => { currentCwd = ctx.cwd; });
208
+ if (loadSettings(currentCwd).enabled) {
209
+ const controller = registerResponsesProviderRuntime(pi, { getCurrentCwd: () => currentCwd, ownsNativeTool }, {
210
+ // Shared services own presentation hooks even when core is absent.
211
+ clear() {}, flush() {}, scheduleFlush() {}, registerRenderers() {},
212
+ streamEffects: () => broker.presentation.streamEffects(),
213
+ });
214
+ broker.coreEnabled = true;
215
+ registerNativeCompaction(pi, controller);
216
+ }
217
+ let registered = false;
218
+ const register = () => {
219
+ if (registered) return;
220
+ registerCoreTools(pi);
221
+ registered = true;
222
+ };
223
+ addPackageTool(broker, "view_image", register);
224
+ addPackageTool(broker, "apply_patch", register);
225
+ registerDiagnosticCommand(pi);
226
+ registerFastMode(pi);
227
+
228
+
229
+ pi.on("before_provider_request", (event, ctx) => {
230
+ currentCwd = ctx.cwd;
231
+ const settings = loadSettings(ctx.cwd);
232
+ const model = contextModel(ctx);
233
+ const modelSettings = loadModelSettings(model, ctx.cwd, settings);
234
+ const profile = resolveModelProfile(model, { settings });
235
+ if (
236
+ !settings.enabled
237
+ || !profile?.effective.enabled
238
+ || !modelSettings.providerShimActive
239
+ || !modelSettings.nativeProviderTools
240
+ ) return undefined;
241
+ const capabilities = computeToolCapabilities(contextModel(ctx), settings);
242
+ const webSearch = profile.effective.tools.webSearch;
243
+ const result = rewriteNativeOpenAiTools(event.payload, {
244
+ ownsNativeTool,
245
+ imageModel: settings.imageModel,
246
+ imageGeneration: modelSettings.imageGenerationImplementation ?? false,
247
+ webSearch: capabilities.web_search.enabled
248
+ && webSearch
249
+ ? {
250
+ implementation: webSearch.implementation,
251
+ contentTypes: webSearch.contentTypes,
252
+ }
253
+ : false,
254
+ });
255
+ return result.rewritten.length > 0 ? result.payload : undefined;
256
+ });
257
+ }