@bitkyc08/opencodex 2.1.1 → 2.1.5

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.
@@ -150,6 +150,7 @@ export function normalizeRoutedCatalogEntry(entry: RawEntry): RawEntry {
150
150
  // runs through native gpt-5.4-mini, so image search is available and verbalized for text-only models.
151
151
  entry.web_search_tool_type = "text_and_image";
152
152
  entry.supports_search_tool = true;
153
+ entry.supports_parallel_tool_calls = false;
153
154
  return ensureStrictCatalogFields(entry);
154
155
  }
155
156
 
@@ -0,0 +1,86 @@
1
+ import { existsSync, readFileSync, statSync, utimesSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { Database } from "bun:sqlite";
4
+ import { CODEX_HOME } from "./codex-paths";
5
+
6
+ const STATE_DB_PATH = join(CODEX_HOME, "state_5.sqlite");
7
+ const RESUMABLE_SOURCES = ["cli", "vscode"] as const;
8
+
9
+ type CodexHistoryProvider = "openai" | "opencodex";
10
+
11
+ interface ThreadRow {
12
+ id: string;
13
+ rollout_path: string;
14
+ }
15
+
16
+ function updateSessionMetaProvider(path: string, provider: CodexHistoryProvider): boolean {
17
+ if (!path || !existsSync(path)) return false;
18
+ const stat = statSync(path);
19
+ const raw = readFileSync(path, "utf8");
20
+ const newline = raw.indexOf("\n");
21
+ const firstLine = newline === -1 ? raw : raw.slice(0, newline);
22
+ const rest = newline === -1 ? "" : raw.slice(newline);
23
+
24
+ let parsed: unknown;
25
+ try {
26
+ parsed = JSON.parse(firstLine);
27
+ } catch {
28
+ return false;
29
+ }
30
+
31
+ if (!parsed || typeof parsed !== "object") return false;
32
+ const record = parsed as { type?: unknown; payload?: { model_provider?: unknown } };
33
+ if (record.type !== "session_meta" || !record.payload || typeof record.payload !== "object") return false;
34
+ if (record.payload.model_provider === provider) return false;
35
+
36
+ record.payload.model_provider = provider;
37
+ writeFileSync(path, `${JSON.stringify(record)}${rest}`, "utf8");
38
+ utimesSync(path, stat.atime, stat.mtime);
39
+ return true;
40
+ }
41
+
42
+ export function syncCodexHistoryProvider(provider: CodexHistoryProvider, stateDbPath = STATE_DB_PATH): { rows: number; files: number } {
43
+ if (!existsSync(stateDbPath)) return { rows: 0, files: 0 };
44
+ const from = provider === "opencodex" ? "openai" : "opencodex";
45
+ const db = new Database(stateDbPath);
46
+ try {
47
+ const placeholders = RESUMABLE_SOURCES.map(() => "?").join(",");
48
+ const rows = db
49
+ .query<ThreadRow, string[]>(`
50
+ SELECT id, rollout_path
51
+ FROM threads
52
+ WHERE model_provider = ?
53
+ AND source IN (${placeholders})
54
+ `)
55
+ .all(from, ...RESUMABLE_SOURCES);
56
+
57
+ let files = 0;
58
+ for (const row of rows) {
59
+ try {
60
+ if (updateSessionMetaProvider(row.rollout_path, provider)) files++;
61
+ } catch {
62
+ /* best-effort; keep DB migration moving even if one old rollout is malformed */
63
+ }
64
+ }
65
+
66
+ const update = db.transaction(() => {
67
+ db.query(`
68
+ UPDATE threads
69
+ SET has_user_event = 1
70
+ WHERE source IN (${placeholders})
71
+ AND trim(coalesce(first_user_message, '')) != ''
72
+ `).run(...RESUMABLE_SOURCES);
73
+ db.query(`
74
+ UPDATE threads
75
+ SET model_provider = ?
76
+ WHERE model_provider = ?
77
+ AND source IN (${placeholders})
78
+ `).run(provider, from, ...RESUMABLE_SOURCES);
79
+ });
80
+ update();
81
+
82
+ return { rows: rows.length, files };
83
+ } finally {
84
+ db.close();
85
+ }
86
+ }
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { atomicWriteFile, websocketsEnabled } from "./config";
3
3
  import { restoreCodexCatalog } from "./codex-catalog";
4
+ import { syncCodexHistoryProvider } from "./codex-history-provider";
4
5
  import { CODEX_CONFIG_PATH, CODEX_PROFILE_PATH, DEFAULT_CATALOG_PATH, parseTomlString, readRootTomlString, resolveCodexConfigPath, tomlString } from "./codex-paths";
5
6
  import type { OcxConfig } from "./types";
6
7
 
@@ -228,14 +229,19 @@ export async function injectCodexConfig(port: number, config?: OcxConfig, option
228
229
 
229
230
  writeFileSync(CODEX_CONFIG_PATH, content, "utf-8");
230
231
  writeFileSync(CODEX_PROFILE_PATH, buildProfileFile(port, catalogPath), "utf-8");
232
+ const history = syncCodexHistoryProvider("opencodex");
231
233
 
232
234
  const catalogMessage = catalogPath
233
235
  ? ` Codex model catalog: ${catalogPath}\n`
234
236
  : ` Codex model catalog not injected because no opencodex catalog file exists yet.\n`;
237
+ const historyMessage = history.rows > 0
238
+ ? ` Codex resume history: ${history.rows} thread(s) mapped to opencodex.\n`
239
+ : "";
235
240
  return {
236
241
  success: true,
237
242
  message: `Injected opencodex as default provider into Codex config.\n` +
238
243
  catalogMessage +
244
+ historyMessage +
239
245
  ` All models now route through opencodex proxy (like OpenRouter).\n` +
240
246
  ` OpenAI models (gpt-5.5, etc.) are passed through to OpenAI.\n` +
241
247
  ` Custom models route to their configured providers.\n` +
@@ -311,10 +317,12 @@ export function removeCodexConfig(): { success: boolean; message: string } {
311
317
  export function restoreNativeCodex(): { success: boolean; message: string } {
312
318
  const cfg = removeCodexConfig();
313
319
  const cat = restoreCodexCatalog();
320
+ const history = syncCodexHistoryProvider("openai");
314
321
  const msg = cat.removed > 0
315
322
  ? `${cfg.message} Catalog restored to ${cat.kept} native model(s) (dropped ${cat.removed} proxy-routed).`
316
323
  : cfg.message;
317
- return { success: cfg.success, message: msg };
324
+ const historyMsg = history.rows > 0 ? ` Resume history restored to openai (${history.rows} thread(s)).` : "";
325
+ return { success: cfg.success, message: `${msg}${historyMsg}` };
318
326
  }
319
327
 
320
328
  export function getCodexConfigPath(): string {
package/src/codex-shim.ts CHANGED
@@ -4,6 +4,30 @@ import { getConfigDir } from "./config";
4
4
 
5
5
  const SHIM_MARKER = "opencodex codex autostart shim";
6
6
  const STATE_PATH = join(getConfigDir(), "codex-shim.json");
7
+ const CODEX_INTERNAL_COMMANDS = [
8
+ "app-server",
9
+ "archive",
10
+ "apply",
11
+ "cloud",
12
+ "completion",
13
+ "debug",
14
+ "delete",
15
+ "doctor",
16
+ "exec",
17
+ "exec-server",
18
+ "features",
19
+ "fork",
20
+ "help",
21
+ "login",
22
+ "logout",
23
+ "mcp",
24
+ "plugin",
25
+ "resume",
26
+ "review",
27
+ "sandbox",
28
+ "unarchive",
29
+ "update",
30
+ ];
7
31
 
8
32
  interface ShimState {
9
33
  platform: NodeJS.Platform;
@@ -51,39 +75,33 @@ function backupPathFor(path: string): string {
51
75
  }
52
76
 
53
77
  export function buildUnixCodexShim(realCodexPath: string, bunPath: string, cliPath: string): string {
78
+ const internalCommands = CODEX_INTERNAL_COMMANDS.join("|");
54
79
  return `#!/usr/bin/env sh
55
80
  # ${SHIM_MARKER}
56
- if [ -z "$OCX_SHIM_BYPASS" ]; then
57
- if ! "${bunPath}" "${cliPath}" status 2>/dev/null | grep -q "Proxy running"; then
58
- mkdir -p "$HOME/.opencodex"
59
- nohup env OCX_SERVICE=1 "${bunPath}" "${cliPath}" start >> "$HOME/.opencodex/shim.log" 2>&1 &
60
- i=0
61
- while [ "$i" -lt 50 ]; do
62
- if "${bunPath}" "${cliPath}" status 2>/dev/null | grep -q "Proxy running"; then
63
- break
64
- fi
65
- sleep 0.1
66
- i=$((i + 1))
67
- done
68
- fi
69
- fi
81
+ case "$1" in
82
+ ${internalCommands}|--help|-h|--version|-V)
83
+ ;;
84
+ *)
85
+ if [ -z "$OCX_SHIM_BYPASS" ]; then
86
+ "${bunPath}" "${cliPath}" ensure >/dev/null 2>&1 || true
87
+ fi
88
+ ;;
89
+ esac
70
90
  exec "${realCodexPath}" "$@"
71
91
  `;
72
92
  }
73
93
 
74
94
  export function buildWindowsCodexShim(realCodexPath: string, bunPath: string, cliPath: string): string {
95
+ const internalCommandChecks = CODEX_INTERNAL_COMMANDS.map(command => `if /I "%~1"=="${command}" goto run_codex`).join("\r\n");
75
96
  return `@echo off\r
76
97
  rem ${SHIM_MARKER}\r
77
98
  if not "%OCX_SHIM_BYPASS%"=="" goto run_codex\r
78
- "${bunPath}" "${cliPath}" status 2>nul | findstr /C:"Proxy running" >nul\r
79
- if %ERRORLEVEL% EQU 0 goto run_codex\r
80
- if not exist "%USERPROFILE%\\.opencodex" mkdir "%USERPROFILE%\\.opencodex"\r
81
- start "" /b cmd /c "set OCX_SERVICE=1 && ""${bunPath}"" ""${cliPath}"" start >> ""%USERPROFILE%\\.opencodex\\shim.log"" 2>&1"\r
82
- for /l %%i in (1,1,50) do (\r
83
- "${bunPath}" "${cliPath}" status 2>nul | findstr /C:"Proxy running" >nul\r
84
- if not errorlevel 1 goto run_codex\r
85
- powershell -NoProfile -Command "Start-Sleep -Milliseconds 100" >nul 2>nul\r
86
- )\r
99
+ ${internalCommandChecks}\r
100
+ if /I "%~1"=="--help" goto run_codex\r
101
+ if /I "%~1"=="-h" goto run_codex\r
102
+ if /I "%~1"=="--version" goto run_codex\r
103
+ if /I "%~1"=="-V" goto run_codex\r
104
+ "${bunPath}" "${cliPath}" ensure >nul 2>nul\r
87
105
  :run_codex\r
88
106
  "${realCodexPath}" %*\r
89
107
  `;
@@ -127,7 +145,7 @@ export function installCodexShim(): { installed: boolean; message: string } {
127
145
  return { installed: false, message: `Codex autostart shim already installed at ${existing.wrapperPath}.` };
128
146
  }
129
147
  if (existing && existsSync(existing.backupPath) && (!existsSync(existing.wrapperPath) || !isShim(existing.wrapperPath))) {
130
- if (existsSync(existing.wrapperPath)) renameSync(existing.wrapperPath, existing.backupPath);
148
+ if (existsSync(existing.wrapperPath)) unlinkSync(existing.wrapperPath);
131
149
  writeShim(existing.wrapperPath, existing.backupPath);
132
150
  writeState({ ...existing, platform: process.platform });
133
151
  return {
package/src/config.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, chmodSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import type { OcxConfig } from "./types";
@@ -10,7 +10,7 @@ let _atomicSeq = 0;
10
10
  */
11
11
  export function atomicWriteFile(path: string, content: string): void {
12
12
  const tmp = `${path}.ocx.${process.pid}.${++_atomicSeq}.tmp`;
13
- writeFileSync(tmp, content, "utf-8");
13
+ writeFileSync(tmp, content, { encoding: "utf-8", mode: 0o600 });
14
14
  renameSync(tmp, path);
15
15
  }
16
16
 
@@ -39,7 +39,22 @@ export function getPidPath(): string {
39
39
  return PID_PATH;
40
40
  }
41
41
 
42
+ export function hardenConfigDir(): void {
43
+ if (existsSync(OCX_DIR)) {
44
+ try { chmodSync(OCX_DIR, 0o700); } catch { /* best-effort */ }
45
+ }
46
+ }
47
+
48
+ export function hardenExistingSecret(path: string): void {
49
+ if (existsSync(path)) {
50
+ try { chmodSync(path, 0o600); } catch { /* best-effort */ }
51
+ }
52
+ }
53
+
42
54
  export function loadConfig(): OcxConfig {
55
+ hardenConfigDir();
56
+ hardenExistingSecret(CONFIG_PATH);
57
+ hardenExistingSecret(join(OCX_DIR, "auth.json"));
43
58
  if (!existsSync(CONFIG_PATH)) {
44
59
  return getDefaultConfig();
45
60
  }
@@ -53,15 +68,21 @@ export function loadConfig(): OcxConfig {
53
68
 
54
69
  export function saveConfig(config: OcxConfig): void {
55
70
  if (!existsSync(OCX_DIR)) {
56
- mkdirSync(OCX_DIR, { recursive: true });
71
+ mkdirSync(OCX_DIR, { recursive: true, mode: 0o700 });
72
+ } else {
73
+ try { chmodSync(OCX_DIR, 0o700); } catch { /* best-effort on existing dir */ }
57
74
  }
58
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf-8");
75
+ atomicWriteFile(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
59
76
  }
60
77
 
61
78
  export function websocketsEnabled(config: Pick<OcxConfig, "websockets">): boolean {
62
79
  return config.websockets === true;
63
80
  }
64
81
 
82
+ export function codexAutoStartEnabled(config: Pick<OcxConfig, "codexAutoStart">): boolean {
83
+ return config.codexAutoStart !== false;
84
+ }
85
+
65
86
  export function getDefaultConfig(): OcxConfig {
66
87
  // Fresh-install default: works out of the box with Codex's ChatGPT OAuth (no API key).
67
88
  // gpt-* requests forward the caller's incoming OAuth headers to the ChatGPT backend.
@@ -78,6 +99,7 @@ export function getDefaultConfig(): OcxConfig {
78
99
  defaultProvider: "openai",
79
100
  subagentModels: [...DEFAULT_SUBAGENT_MODELS],
80
101
  websockets: false,
102
+ codexAutoStart: true,
81
103
  };
82
104
  }
83
105
 
@@ -90,7 +112,11 @@ export function resolveEnvValue(value: string | undefined): string | undefined {
90
112
  }
91
113
 
92
114
  export function writePid(pid: number): void {
93
- if (!existsSync(OCX_DIR)) mkdirSync(OCX_DIR, { recursive: true });
115
+ if (!existsSync(OCX_DIR)) {
116
+ mkdirSync(OCX_DIR, { recursive: true, mode: 0o700 });
117
+ } else {
118
+ hardenConfigDir();
119
+ }
94
120
  writeFileSync(PID_PATH, String(pid), "utf-8");
95
121
  }
96
122
 
package/src/init.ts CHANGED
@@ -131,6 +131,17 @@ export async function runInit(): Promise<void> {
131
131
  console.log(result.success ? `✅ ${result.message}` : `⚠️ ${result.message}`);
132
132
  }
133
133
 
134
+ const shimAnswer = await prompt.ask("Install Codex autostart shim? [Y/n]: ");
135
+ if (shimAnswer.trim().toLowerCase() !== "n") {
136
+ try {
137
+ const { installCodexShim } = await import("./codex-shim");
138
+ const result = installCodexShim();
139
+ console.log(result.installed ? `✅ ${result.message}` : `⚠️ ${result.message}`);
140
+ } catch (err) {
141
+ console.log(`⚠️ Codex autostart shim skipped: ${err instanceof Error ? err.message : String(err)}`);
142
+ }
143
+ }
144
+
134
145
  console.log(`\n🚀 Setup complete! Run 'ocx start' to start the proxy.`);
135
146
  prompt.close();
136
147
  }
@@ -1,13 +1,15 @@
1
1
  /** OAuth token store at ~/.opencodex/auth.json, keyed by provider name. */
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, chmodSync } from "node:fs";
3
3
  import { join } from "node:path";
4
- import { getConfigDir } from "../config";
4
+ import { getConfigDir, atomicWriteFile, hardenConfigDir, hardenExistingSecret } from "../config";
5
5
  import type { OAuthCredentials } from "./types";
6
6
 
7
7
  const AUTH_PATH = join(getConfigDir(), "auth.json");
8
8
  type AuthStore = Record<string, OAuthCredentials>;
9
9
 
10
10
  export function loadAuthStore(): AuthStore {
11
+ hardenConfigDir();
12
+ hardenExistingSecret(AUTH_PATH);
11
13
  if (!existsSync(AUTH_PATH)) return {};
12
14
  try {
13
15
  return JSON.parse(readFileSync(AUTH_PATH, "utf-8")) as AuthStore;
@@ -18,8 +20,12 @@ export function loadAuthStore(): AuthStore {
18
20
 
19
21
  function persist(store: AuthStore): void {
20
22
  const dir = getConfigDir();
21
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
22
- writeFileSync(AUTH_PATH, JSON.stringify(store, null, 2) + "\n", "utf-8");
23
+ if (!existsSync(dir)) {
24
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
25
+ } else {
26
+ try { chmodSync(dir, 0o700); } catch { /* best-effort on existing dir */ }
27
+ }
28
+ atomicWriteFile(AUTH_PATH, JSON.stringify(store, null, 2) + "\n");
23
29
  }
24
30
 
25
31
  export function getCredential(provider: string): OAuthCredentials | null {
package/src/open-url.ts CHANGED
@@ -1,9 +1,13 @@
1
- import { exec } from "node:child_process";
1
+ import { spawn } from "node:child_process";
2
2
 
3
3
  export function openUrl(url: string): void {
4
+ if (!/^https?:\/\//i.test(url)) return;
4
5
  const cmd =
5
6
  process.platform === "darwin" ? "open"
6
- : process.platform === "win32" ? 'start ""'
7
+ : process.platform === "win32" ? "rundll32"
7
8
  : "xdg-open";
8
- exec(`${cmd} ${JSON.stringify(url)}`);
9
+ const args = process.platform === "win32"
10
+ ? ["url.dll,FileProtocolHandler", url]
11
+ : [url];
12
+ spawn(cmd, args, { detached: true, stdio: "ignore", shell: false }).unref();
9
13
  }
package/src/ports.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { createServer } from "node:net";
2
+
3
+ export async function isPortAvailable(port: number, hostname = "127.0.0.1"): Promise<boolean> {
4
+ return await new Promise(resolve => {
5
+ const server = createServer();
6
+ server.once("error", () => resolve(false));
7
+ server.once("listening", () => {
8
+ server.close(() => resolve(true));
9
+ });
10
+ server.listen({ port, host: hostname });
11
+ });
12
+ }
13
+
14
+ export async function findAvailablePort(preferredPort: number, hostname = "127.0.0.1"): Promise<number> {
15
+ if (await isPortAvailable(preferredPort, hostname)) return preferredPort;
16
+ return await new Promise((resolve, reject) => {
17
+ const server = createServer();
18
+ server.once("error", reject);
19
+ server.once("listening", () => {
20
+ const address = server.address();
21
+ const port = typeof address === "object" && address ? address.port : 0;
22
+ server.close(() => {
23
+ if (port > 0) resolve(port);
24
+ else reject(new Error("failed to allocate an available port"));
25
+ });
26
+ });
27
+ server.listen({ port: 0, host: hostname });
28
+ });
29
+ }
30
+
@@ -176,7 +176,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
176
176
  { id: "openrouter", label: "OpenRouter", adapter: "openai-chat", baseUrl: "https://openrouter.ai/api/v1", authKind: "key", featured: true, dashboardUrl: "https://openrouter.ai/keys", jawcodeBundle: "openrouter" },
177
177
  { id: "groq", label: "Groq", adapter: "openai-chat", baseUrl: "https://api.groq.com/openai/v1", authKind: "key", featured: true, dashboardUrl: "https://console.groq.com/keys" },
178
178
  { id: "google", label: "Google Gemini", adapter: "google", baseUrl: "https://generativelanguage.googleapis.com", authKind: "key", featured: true, dashboardUrl: "https://aistudio.google.com/apikey", defaultModel: "gemini-3-pro", jawcodeBundle: "google", extraMetadataAliases: ["gemini"] },
179
- { id: "azure-openai", label: "Azure OpenAI", adapter: "azure-openai", baseUrl: "https://{resource}.openai.azure.com/openai/deployments/{deployment}", authKind: "key", featured: true, dashboardUrl: "https://portal.azure.com" },
179
+ { id: "azure-openai", label: "Azure OpenAI", adapter: "azure-openai", baseUrl: "https://{resource}.openai.azure.com/openai", authKind: "key", featured: true, dashboardUrl: "https://portal.azure.com" },
180
180
  { id: "ollama", label: "Ollama (local)", adapter: "openai-chat", baseUrl: "http://localhost:11434/v1", authKind: "local", featured: true, note: "Local — key usually blank" },
181
181
  { id: "vllm", label: "vLLM (local)", adapter: "openai-chat", baseUrl: "http://localhost:8000/v1", authKind: "local", featured: true, note: "Local — key usually blank" },
182
182
  { id: "lm-studio", label: "LM Studio (local)", adapter: "openai-chat", baseUrl: "http://localhost:1234/v1", authKind: "local", featured: true, note: "Local — no key needed" },
@@ -177,15 +177,15 @@ function outputToToolResultContent(output: string | unknown[] | undefined): stri
177
177
  return parts;
178
178
  }
179
179
 
180
- function findToolNameById(messages: OcxMessage[], callId: string): string {
180
+ function findToolById(messages: OcxMessage[], callId: string): { name: string; namespace?: string } {
181
181
  for (let i = messages.length - 1; i >= 0; i--) {
182
182
  const m = messages[i];
183
183
  if (m.role !== "assistant") continue;
184
184
  for (const part of m.content) {
185
- if (part.type === "toolCall" && part.id === callId) return part.name;
185
+ if (part.type === "toolCall" && part.id === callId) return { name: part.name, namespace: part.namespace };
186
186
  }
187
187
  }
188
- return "";
188
+ return { name: "" };
189
189
  }
190
190
 
191
191
  const REASONING_EFFORTS = new Set(["none", "minimal", "low", "medium", "high", "xhigh", "max"]);
@@ -327,9 +327,10 @@ export function parseRequest(body: unknown): OcxParsedRequest {
327
327
 
328
328
  if (effectiveType === "function_call_output") {
329
329
  const output = item as { call_id: string; output?: string | unknown[] };
330
+ const toolInfo = findToolById(messages, output.call_id);
330
331
  messages.push({
331
332
  role: "toolResult", toolCallId: output.call_id,
332
- toolName: findToolNameById(messages, output.call_id),
333
+ toolName: toolInfo.name, toolNamespace: toolInfo.namespace,
333
334
  content: outputToToolResultContent(output.output), isError: false, timestamp: now,
334
335
  });
335
336
  continue;
@@ -337,9 +338,10 @@ export function parseRequest(body: unknown): OcxParsedRequest {
337
338
 
338
339
  if (effectiveType === "custom_tool_call_output") {
339
340
  const output = item as { call_id: string; output: string };
341
+ const toolInfo = findToolById(messages, output.call_id);
340
342
  messages.push({
341
343
  role: "toolResult", toolCallId: output.call_id,
342
- toolName: findToolNameById(messages, output.call_id),
344
+ toolName: toolInfo.name, toolNamespace: toolInfo.namespace,
343
345
  content: output.output ?? "", isError: false, timestamp: now,
344
346
  });
345
347
  }
@@ -373,7 +375,8 @@ export function parseRequest(body: unknown): OcxParsedRequest {
373
375
  if (data.reasoning?.effort && REASONING_EFFORTS.has(data.reasoning.effort)) {
374
376
  options.reasoning = data.reasoning.effort;
375
377
  }
376
- if (data.reasoning?.summary === "none") options.hideThinkingSummary = true;
378
+ const summaryMode = data.reasoning?.summary;
379
+ if (!summaryMode || summaryMode === "none") options.hideThinkingSummary = true;
377
380
  if (data.presence_penalty !== undefined) options.presencePenalty = data.presence_penalty;
378
381
  if (data.frequency_penalty !== undefined) options.frequencyPenalty = data.frequency_penalty;
379
382
 
@@ -50,6 +50,7 @@ const functionCallItemSchema = z.object({
50
50
  id: z.string().optional(),
51
51
  call_id: z.string().min(1),
52
52
  name: z.string().min(1),
53
+ namespace: z.string().optional(),
53
54
  arguments: z.string().optional(),
54
55
  });
55
56
  const functionCallOutputItemSchema = z.object({