@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.
- package/README.ko.md +29 -4
- package/README.md +30 -5
- package/README.zh-CN.md +6 -0
- package/gui/dist/assets/index-DB2i6w5f.js +9 -0
- package/gui/dist/assets/{index-cEIM1XWY.css → index-dCS-lwCM.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +6 -3
- package/src/adapters/azure.ts +7 -7
- package/src/adapters/google.ts +4 -3
- package/src/adapters/openai-chat.ts +2 -1
- package/src/adapters/openai-responses.ts +2 -1
- package/src/bridge.ts +125 -24
- package/src/cli.ts +166 -13
- package/src/codex-catalog.ts +1 -0
- package/src/codex-history-provider.ts +86 -0
- package/src/codex-inject.ts +9 -1
- package/src/codex-shim.ts +42 -24
- package/src/config.ts +31 -5
- package/src/init.ts +11 -0
- package/src/oauth/store.ts +10 -4
- package/src/open-url.ts +7 -3
- package/src/ports.ts +30 -0
- package/src/providers/registry.ts +1 -1
- package/src/responses/parser.ts +9 -6
- package/src/responses/schema.ts +1 -0
- package/src/server.ts +182 -13
- package/src/service.ts +29 -2
- package/src/types.ts +8 -0
- package/src/update.ts +12 -2
- package/src/web-search/loop.ts +4 -1
- package/src/ws-bridge.ts +1 -1
- package/gui/dist/assets/index-DgCnBxqJ.js +0 -9
package/src/codex-catalog.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/codex-inject.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
79
|
-
if
|
|
80
|
-
if
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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))
|
|
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
|
-
|
|
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))
|
|
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
|
}
|
package/src/oauth/store.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/** OAuth token store at ~/.opencodex/auth.json, keyed by provider name. */
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
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))
|
|
22
|
-
|
|
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 {
|
|
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" ?
|
|
7
|
+
: process.platform === "win32" ? "rundll32"
|
|
7
8
|
: "xdg-open";
|
|
8
|
-
|
|
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
|
|
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" },
|
package/src/responses/parser.ts
CHANGED
|
@@ -177,15 +177,15 @@ function outputToToolResultContent(output: string | unknown[] | undefined): stri
|
|
|
177
177
|
return parts;
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
function
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
package/src/responses/schema.ts
CHANGED
|
@@ -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({
|