@bitkyc08/opencodex 2.6.17 → 2.6.18
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 +9 -0
- package/bin/ocx.mjs +70 -5
- package/gui/dist/assets/index-DDcEW0Cm.css +1 -0
- package/gui/dist/assets/index-DbTEyo46.js +9 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -1
- package/src/adapters/anthropic.ts +9 -2
- package/src/adapters/base.ts +6 -0
- package/src/adapters/cursor/arg-codec.ts +38 -0
- package/src/adapters/cursor/arg-normalize.ts +88 -0
- package/src/adapters/cursor/cursor-errors.ts +85 -0
- package/src/adapters/cursor/discovery.ts +144 -0
- package/src/adapters/cursor/effort-map.ts +74 -0
- package/src/adapters/cursor/exec-policy.ts +44 -0
- package/src/adapters/cursor/framing.ts +136 -0
- package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
- package/src/adapters/cursor/kv-store.ts +25 -0
- package/src/adapters/cursor/live-models.ts +93 -0
- package/src/adapters/cursor/live-smoke-gate.ts +41 -0
- package/src/adapters/cursor/live-transport.ts +758 -0
- package/src/adapters/cursor/mcp-config.ts +42 -0
- package/src/adapters/cursor/mcp-manager.ts +236 -0
- package/src/adapters/cursor/message-mapper.ts +46 -0
- package/src/adapters/cursor/native-exec-common.ts +55 -0
- package/src/adapters/cursor/native-exec-desktop.ts +177 -0
- package/src/adapters/cursor/native-exec-fs.ts +284 -0
- package/src/adapters/cursor/native-exec-mcp.ts +151 -0
- package/src/adapters/cursor/native-exec-network.ts +32 -0
- package/src/adapters/cursor/native-exec-shell.ts +191 -0
- package/src/adapters/cursor/native-exec-tools.ts +118 -0
- package/src/adapters/cursor/native-exec.ts +177 -0
- package/src/adapters/cursor/protobuf-events.ts +309 -0
- package/src/adapters/cursor/protobuf-request.ts +347 -0
- package/src/adapters/cursor/request-builder.ts +98 -0
- package/src/adapters/cursor/tool-definitions.ts +301 -0
- package/src/adapters/cursor/transport-retry.ts +116 -0
- package/src/adapters/cursor/transport.ts +47 -0
- package/src/adapters/cursor/types.ts +36 -0
- package/src/adapters/cursor.ts +99 -0
- package/src/adapters/google.ts +7 -1
- package/src/adapters/kiro.ts +15 -0
- package/src/adapters/openai-chat.ts +7 -2
- package/src/adapters/run-turn-queue.ts +58 -0
- package/src/adapters/tool-catalog-nudge.ts +71 -0
- package/src/bridge.ts +7 -1
- package/src/cli-help.ts +9 -2
- package/src/cli-status.ts +7 -5
- package/src/cli.ts +122 -79
- package/src/codex-catalog.ts +213 -71
- package/src/codex-history-provider.ts +31 -14
- package/src/codex-inject.ts +17 -9
- package/src/codex-paths.ts +2 -1
- package/src/codex-shim.ts +30 -7
- package/src/codex-sync.ts +70 -0
- package/src/config.ts +58 -2
- package/src/doctor.ts +4 -2
- package/src/index.ts +1 -0
- package/src/model-cache.ts +22 -2
- package/src/oauth/callback-server.ts +44 -16
- package/src/oauth/cursor.ts +188 -0
- package/src/oauth/index.ts +29 -3
- package/src/oauth/key-providers.ts +20 -33
- package/src/oauth/login-cli.ts +7 -4
- package/src/open-url.ts +5 -1
- package/src/ports.ts +13 -0
- package/src/process-control.ts +76 -0
- package/src/provider-label.ts +10 -5
- package/src/providers/derive.ts +30 -3
- package/src/providers/registry.ts +39 -1
- package/src/proxy-liveness.ts +122 -0
- package/src/responses/parser.ts +1 -0
- package/src/responses/state.ts +83 -0
- package/src/router.ts +38 -23
- package/src/server/adapter-resolve.ts +3 -0
- package/src/server.ts +130 -18
- package/src/service.ts +94 -32
- package/src/types.ts +24 -1
- package/src/update-job.ts +360 -0
- package/src/update.ts +73 -11
- package/src/usage-log.ts +3 -3
- package/src/usage-summary.ts +3 -2
- package/src/win-paths.ts +68 -0
- package/gui/dist/assets/index-DIBiVVC0.css +0 -1
- package/gui/dist/assets/index-DcnD944i.js +0 -9
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { OcxProviderConfig } from "../types";
|
|
2
|
-
import { deriveKeyLoginMap } from "../providers/derive";
|
|
2
|
+
import { deriveKeyLoginMap, enrichProviderFromRegistry } from "../providers/derive";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* API-key "login" providers: not OAuth — the flow opens the provider's dashboard so the user can
|
|
@@ -14,6 +14,7 @@ export interface KeyLoginProvider {
|
|
|
14
14
|
/** Where the user creates/copies the API key. */
|
|
15
15
|
dashboardUrl: string;
|
|
16
16
|
models?: string[];
|
|
17
|
+
liveModels?: boolean;
|
|
17
18
|
defaultModel?: string;
|
|
18
19
|
contextWindow?: number;
|
|
19
20
|
modelContextWindows?: Record<string, number>;
|
|
@@ -35,45 +36,19 @@ export interface KeyLoginProvider {
|
|
|
35
36
|
autoToolChoiceOnlyModels?: string[];
|
|
36
37
|
preserveReasoningContentModels?: string[];
|
|
37
38
|
escapeBuiltinToolNames?: boolean;
|
|
39
|
+
googleMode?: "ai-studio" | "vertex" | "cloud-code-assist";
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
export const KEY_LOGIN_PROVIDERS: Record<string, KeyLoginProvider> = deriveKeyLoginMap();
|
|
41
43
|
|
|
42
44
|
/**
|
|
43
|
-
* Copy a
|
|
44
|
-
* `noReasoningModels`, `defaultModel`) onto a provider config being created, for any field the
|
|
45
|
-
* didn't already supply. Lets the vision/reasoning classification actually reach the saved
|
|
46
|
-
* (the GUI/API only send adapter/baseUrl/apiKey/defaultModel). No-op for
|
|
45
|
+
* Copy a registry entry's seed/classification (`models`, `liveModels`, `noVisionModels`,
|
|
46
|
+
* `noReasoningModels`, `defaultModel`) onto a provider config being created, for any field the
|
|
47
|
+
* caller didn't already supply. Lets the vision/reasoning classification actually reach the saved
|
|
48
|
+
* config (the GUI/API only send adapter/baseUrl/apiKey/defaultModel). No-op for unknown names.
|
|
47
49
|
*/
|
|
48
50
|
export function enrichProviderFromCatalog(name: string, prov: OcxProviderConfig): void {
|
|
49
|
-
|
|
50
|
-
if (!e) return;
|
|
51
|
-
if (!prov.models && e.models) prov.models = [...e.models];
|
|
52
|
-
if (!prov.defaultModel && e.defaultModel) prov.defaultModel = e.defaultModel;
|
|
53
|
-
if (prov.contextWindow === undefined && e.contextWindow !== undefined) prov.contextWindow = e.contextWindow;
|
|
54
|
-
if (!prov.modelContextWindows && e.modelContextWindows) prov.modelContextWindows = { ...e.modelContextWindows };
|
|
55
|
-
if (!prov.modelInputModalities && e.modelInputModalities) prov.modelInputModalities = cloneRecordOfArrays(e.modelInputModalities);
|
|
56
|
-
if (!prov.reasoningEfforts && e.reasoningEfforts) prov.reasoningEfforts = [...e.reasoningEfforts];
|
|
57
|
-
if (!prov.modelReasoningEfforts && e.modelReasoningEfforts) prov.modelReasoningEfforts = cloneRecordOfArrays(e.modelReasoningEfforts);
|
|
58
|
-
if (!prov.reasoningEffortMap && e.reasoningEffortMap) prov.reasoningEffortMap = { ...e.reasoningEffortMap };
|
|
59
|
-
if (!prov.modelReasoningEffortMap && e.modelReasoningEffortMap) prov.modelReasoningEffortMap = cloneNestedRecord(e.modelReasoningEffortMap);
|
|
60
|
-
if (!prov.noVisionModels && e.noVisionModels) prov.noVisionModels = [...e.noVisionModels];
|
|
61
|
-
if (!prov.noReasoningModels && e.noReasoningModels) prov.noReasoningModels = [...e.noReasoningModels];
|
|
62
|
-
if (!prov.noTemperatureModels && e.noTemperatureModels) prov.noTemperatureModels = [...e.noTemperatureModels];
|
|
63
|
-
if (!prov.noTopPModels && e.noTopPModels) prov.noTopPModels = [...e.noTopPModels];
|
|
64
|
-
if (!prov.noPenaltyModels && e.noPenaltyModels) prov.noPenaltyModels = [...e.noPenaltyModels];
|
|
65
|
-
if (!prov.autoToolChoiceOnlyModels && e.autoToolChoiceOnlyModels) prov.autoToolChoiceOnlyModels = [...e.autoToolChoiceOnlyModels];
|
|
66
|
-
if (!prov.preserveReasoningContentModels && e.preserveReasoningContentModels) prov.preserveReasoningContentModels = [...e.preserveReasoningContentModels];
|
|
67
|
-
if (prov.escapeBuiltinToolNames === undefined && e.escapeBuiltinToolNames !== undefined) prov.escapeBuiltinToolNames = e.escapeBuiltinToolNames;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
function cloneRecordOfArrays(input: Record<string, string[]>): Record<string, string[]> {
|
|
72
|
-
return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, [...value]]));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function cloneNestedRecord(input: Record<string, Record<string, string>>): Record<string, Record<string, string>> {
|
|
76
|
-
return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, { ...value }]));
|
|
51
|
+
enrichProviderFromRegistry(name, prov);
|
|
77
52
|
}
|
|
78
53
|
|
|
79
54
|
export function isKeyLoginProvider(name: string): boolean {
|
|
@@ -108,6 +83,18 @@ export async function validateApiKey(provider: KeyLoginProvider, key: string): P
|
|
|
108
83
|
return "unknown";
|
|
109
84
|
}
|
|
110
85
|
|
|
86
|
+
if (provider.adapter === "google" && (provider.googleMode ?? "ai-studio") === "ai-studio") {
|
|
87
|
+
// Generative Language API rejects Bearer-wrapped API keys; probe models.list with the
|
|
88
|
+
// documented x-goog-api-key header instead (pageSize=1 — validation only needs a 200).
|
|
89
|
+
const res = await fetch(`${provider.baseUrl}/v1beta/models?pageSize=1`, {
|
|
90
|
+
headers: { "x-goog-api-key": key },
|
|
91
|
+
signal: AbortSignal.timeout(8000),
|
|
92
|
+
});
|
|
93
|
+
if (res.ok) return true;
|
|
94
|
+
if (res.status === 400 || res.status === 401 || res.status === 403) return false;
|
|
95
|
+
return "unknown";
|
|
96
|
+
}
|
|
97
|
+
|
|
111
98
|
const res = await fetch(`${provider.baseUrl}/models`, {
|
|
112
99
|
headers: { Authorization: `Bearer ${key}` },
|
|
113
100
|
signal: AbortSignal.timeout(8000),
|
package/src/oauth/login-cli.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as readline from "node:readline";
|
|
2
2
|
import { openUrl } from "../open-url";
|
|
3
|
-
import { loadConfig,
|
|
3
|
+
import { loadConfig, saveConfig } from "../config";
|
|
4
|
+
import { findLiveProxy, probeHostname } from "../proxy-liveness";
|
|
4
5
|
import { OAUTH_PROVIDERS, runLogin } from "./index";
|
|
5
6
|
import { KEY_LOGIN_PROVIDERS, isKeyLoginProvider, validateApiKey, type KeyLoginProvider } from "./key-providers";
|
|
6
7
|
import type { OcxProviderConfig } from "../types";
|
|
@@ -14,10 +15,12 @@ export function runningProxyUpdateHeaders(): Headers {
|
|
|
14
15
|
|
|
15
16
|
/** Push the new provider into a running proxy's live config so it routes without a restart. */
|
|
16
17
|
async function notifyRunningProxy(name: string, provider: unknown): Promise<void> {
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
// Identity-checked runtime-port lookup: reaches a fallback-port proxy and avoids
|
|
19
|
+
// posting credentials-adjacent config to whatever else answers on config.port.
|
|
20
|
+
const live = await findLiveProxy();
|
|
21
|
+
if (!live) return;
|
|
19
22
|
try {
|
|
20
|
-
await fetch(`http
|
|
23
|
+
await fetch(`http://${probeHostname(live.hostname)}:${live.port}/api/providers`, {
|
|
21
24
|
method: "POST",
|
|
22
25
|
headers: runningProxyUpdateHeaders(),
|
|
23
26
|
body: JSON.stringify({ name, provider }),
|
package/src/open-url.ts
CHANGED
|
@@ -17,5 +17,9 @@ export function openUrl(url: string): void {
|
|
|
17
17
|
const args = process.platform === "win32"
|
|
18
18
|
? ["url.dll,FileProtocolHandler", url]
|
|
19
19
|
: [url];
|
|
20
|
-
spawn(cmd, args, { detached: true, stdio: "ignore", shell: false })
|
|
20
|
+
const child = spawn(cmd, args, { detached: true, stdio: "ignore", shell: false });
|
|
21
|
+
// Headless hosts (no xdg-open) emit ENOENT as an async 'error' event; without a
|
|
22
|
+
// listener that is an uncaught exception that kills the whole proxy/login flow.
|
|
23
|
+
child.on("error", () => {});
|
|
24
|
+
child.unref();
|
|
21
25
|
}
|
package/src/ports.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import { createServer } from "node:net";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* True when an error means "this port/address is already bound" — the only bind failure
|
|
5
|
+
* that is safe to answer with a retry on another port. Bun/Node surface it as
|
|
6
|
+
* `code: "EADDRINUSE"` or an EADDRINUSE / "address in use" message depending on the API.
|
|
7
|
+
*/
|
|
8
|
+
export function isAddrInUse(err: unknown): boolean {
|
|
9
|
+
if (!err || typeof err !== "object") return false;
|
|
10
|
+
const { code, message } = err as { code?: unknown; message?: unknown };
|
|
11
|
+
if (code === "EADDRINUSE") return true;
|
|
12
|
+
const text = typeof message === "string" ? message.toLowerCase() : "";
|
|
13
|
+
return text.includes("eaddrinuse") || text.includes("in use");
|
|
14
|
+
}
|
|
15
|
+
|
|
3
16
|
export async function isPortAvailable(port: number, hostname = "127.0.0.1"): Promise<boolean> {
|
|
4
17
|
return await new Promise(resolve => {
|
|
5
18
|
const server = createServer();
|
package/src/process-control.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { loadConfig, readRuntimePort } from "./config";
|
|
2
3
|
|
|
3
4
|
export function isProcessAlive(pid: number): boolean {
|
|
4
5
|
try {
|
|
@@ -19,6 +20,81 @@ export function waitForExit(pid: number, timeoutMs: number): boolean {
|
|
|
19
20
|
return !isProcessAlive(pid);
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
/** Injectable seams so the graceful-stop flow is unit-testable without a live proxy. */
|
|
24
|
+
export interface GracefulStopIo {
|
|
25
|
+
fetchFn?: typeof fetch;
|
|
26
|
+
readRuntime?: (pid: number) => { port: number; hostname?: string } | null;
|
|
27
|
+
waitExit?: (pid: number, timeoutMs: number) => boolean;
|
|
28
|
+
env?: Record<string, string | undefined>;
|
|
29
|
+
exitTimeoutMs?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Host to POST /api/stop against: follow the recorded bind hostname when it names a
|
|
34
|
+
* concrete address (a proxy bound to ::1 or a LAN IP is unreachable on 127.0.0.1);
|
|
35
|
+
* loopback aliases and wildcard binds all answer on IPv4 loopback.
|
|
36
|
+
*/
|
|
37
|
+
export function gracefulStopHost(hostname: string | undefined): string {
|
|
38
|
+
const trimmed = (hostname ?? "").trim();
|
|
39
|
+
const lower = trimmed.toLowerCase();
|
|
40
|
+
if (!trimmed || lower === "localhost" || trimmed === "127.0.0.1" || trimmed === "0.0.0.0" || trimmed === "::" || trimmed === "[::]") {
|
|
41
|
+
return "127.0.0.1";
|
|
42
|
+
}
|
|
43
|
+
if (lower === "::1" || lower === "[::1]") return "[::1]";
|
|
44
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) return trimmed;
|
|
45
|
+
return trimmed.includes(":") ? `[${trimmed}]` : trimmed;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Ask a running proxy to stop itself via the management API (`POST /api/stop`), which
|
|
50
|
+
* drains in-flight turns, restores native Codex, and cleans its pid/runtime files.
|
|
51
|
+
* This is the only way to get a GRACEFUL stop on Windows, where the POSIX
|
|
52
|
+
* SIGTERM-then-SIGKILL ladder does not exist and `taskkill /F` gives the proxy no
|
|
53
|
+
* chance to run its shutdown handlers. Returns false when the proxy can't be reached
|
|
54
|
+
* or doesn't exit in time — callers fall back to {@link killProxy}.
|
|
55
|
+
*/
|
|
56
|
+
export async function stopProxyGracefully(pid: number, io: GracefulStopIo = {}): Promise<boolean> {
|
|
57
|
+
const readRuntime = io.readRuntime ?? readRuntimePort;
|
|
58
|
+
const runtime = readRuntime(pid);
|
|
59
|
+
if (!runtime?.port) return false;
|
|
60
|
+
const env = io.env ?? process.env;
|
|
61
|
+
const headers: Record<string, string> = {};
|
|
62
|
+
// Non-loopback binds require management auth; loopback ignores the extra header.
|
|
63
|
+
const token = env.OPENCODEX_API_AUTH_TOKEN?.trim();
|
|
64
|
+
if (token) headers["x-opencodex-api-key"] = token;
|
|
65
|
+
const fetchFn = io.fetchFn ?? fetch;
|
|
66
|
+
try {
|
|
67
|
+
const res = await fetchFn(`http://${gracefulStopHost(runtime.hostname)}:${runtime.port}/api/stop`, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers,
|
|
70
|
+
signal: AbortSignal.timeout(2000),
|
|
71
|
+
});
|
|
72
|
+
if (!res.ok) return false;
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const waitExit = io.waitExit ?? waitForExit;
|
|
77
|
+
// Honor the server's own drain window: /api/stop answers 200 first, then drains for
|
|
78
|
+
// config.shutdownTimeoutMs. Waiting less than that hard-kills mid-drain.
|
|
79
|
+
const exitTimeoutMs = io.exitTimeoutMs ?? drainDeadlineMs();
|
|
80
|
+
return waitExit(pid, exitTimeoutMs);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function drainDeadlineMs(): number {
|
|
84
|
+
try {
|
|
85
|
+
return (loadConfig().shutdownTimeoutMs ?? 5000) + 3000;
|
|
86
|
+
} catch {
|
|
87
|
+
return 8000;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Graceful-first stop: management-API drain, then the platform kill ladder. */
|
|
92
|
+
export async function stopProxy(pid: number): Promise<void> {
|
|
93
|
+
if (!isProcessAlive(pid)) return;
|
|
94
|
+
if (await stopProxyGracefully(pid)) return;
|
|
95
|
+
killProxy(pid);
|
|
96
|
+
}
|
|
97
|
+
|
|
22
98
|
export function killProxy(pid: number): void {
|
|
23
99
|
if (!isProcessAlive(pid)) return;
|
|
24
100
|
if (process.platform === "win32") {
|
package/src/provider-label.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { CODEX_ACCOUNT_LOG_LABEL_RE } from "./codex-account-label";
|
|
2
2
|
|
|
3
|
+
function canonicalUsageProviderLabel(provider: string): string {
|
|
4
|
+
return provider === "chatgpt" ? "openai" : provider;
|
|
5
|
+
}
|
|
6
|
+
|
|
3
7
|
export function baseProviderLabel(provider: string): string {
|
|
4
8
|
const cut = provider.lastIndexOf("-");
|
|
5
|
-
if (cut <= 0) return provider;
|
|
9
|
+
if (cut <= 0) return canonicalUsageProviderLabel(provider);
|
|
6
10
|
const suffix = provider.slice(cut + 1);
|
|
7
11
|
// `-main` is the legacy log label for the main Codex account (MAIN_CODEX_ACCOUNT_ID). New entries
|
|
8
|
-
// log under the base provider name, but historical `<provider>-main` entries must still collapse
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
// log under the base provider name, but historical `<provider>-main` entries must still collapse.
|
|
13
|
+
// ChatGPT auth-pool and OpenAI passthrough are the same Codex/OpenAI usage surface, so display
|
|
14
|
+
// summaries normalize them to one `openai` row after recognized main/pool suffixes are removed.
|
|
15
|
+
if (suffix === "main") return canonicalUsageProviderLabel(provider.slice(0, cut));
|
|
16
|
+
return CODEX_ACCOUNT_LOG_LABEL_RE.test(suffix) ? canonicalUsageProviderLabel(provider.slice(0, cut)) : provider;
|
|
12
17
|
}
|
package/src/providers/derive.ts
CHANGED
|
@@ -7,6 +7,7 @@ export interface DerivedKeyLoginProvider {
|
|
|
7
7
|
adapter: string;
|
|
8
8
|
dashboardUrl: string;
|
|
9
9
|
models?: string[];
|
|
10
|
+
liveModels?: boolean;
|
|
10
11
|
defaultModel?: string;
|
|
11
12
|
contextWindow?: number;
|
|
12
13
|
modelContextWindows?: Record<string, number>;
|
|
@@ -41,7 +42,7 @@ export interface DerivedProviderPreset {
|
|
|
41
42
|
adapter: string;
|
|
42
43
|
baseUrl: string;
|
|
43
44
|
defaultModel?: string;
|
|
44
|
-
auth: "oauth" | "forward" | "key";
|
|
45
|
+
auth: "oauth" | "forward" | "key" | "local";
|
|
45
46
|
oauthProvider?: string;
|
|
46
47
|
dashboardUrl?: string;
|
|
47
48
|
note?: string;
|
|
@@ -66,6 +67,7 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
|
|
|
66
67
|
authMode: entry.authKind === "local" ? undefined : entry.authKind,
|
|
67
68
|
...(entry.defaultModel ? { defaultModel: entry.defaultModel } : {}),
|
|
68
69
|
...(entry.models ? { models: [...entry.models] } : {}),
|
|
70
|
+
...(entry.liveModels !== undefined ? { liveModels: entry.liveModels } : {}),
|
|
69
71
|
...(entry.contextWindow !== undefined ? { contextWindow: entry.contextWindow } : {}),
|
|
70
72
|
...(entry.modelContextWindows ? { modelContextWindows: { ...entry.modelContextWindows } } : {}),
|
|
71
73
|
...(entry.modelInputModalities ? { modelInputModalities: cloneRecordOfArrays(entry.modelInputModalities) } : {}),
|
|
@@ -98,6 +100,7 @@ export function deriveKeyLoginMap(): Record<string, DerivedKeyLoginProvider> {
|
|
|
98
100
|
adapter: entry.adapter,
|
|
99
101
|
dashboardUrl: entry.dashboardUrl,
|
|
100
102
|
...(entry.models ? { models: [...entry.models] } : {}),
|
|
103
|
+
...(entry.liveModels !== undefined ? { liveModels: entry.liveModels } : {}),
|
|
101
104
|
...(entry.defaultModel ? { defaultModel: entry.defaultModel } : {}),
|
|
102
105
|
...(entry.contextWindow !== undefined ? { contextWindow: entry.contextWindow } : {}),
|
|
103
106
|
...(entry.modelContextWindows ? { modelContextWindows: { ...entry.modelContextWindows } } : {}),
|
|
@@ -149,11 +152,35 @@ export function deriveOAuthIds(): string[] {
|
|
|
149
152
|
|
|
150
153
|
export function deriveProviderPresets(): DerivedProviderPreset[] {
|
|
151
154
|
const presets = PROVIDER_REGISTRY
|
|
152
|
-
.filter(entry => entry.featured || entry.authKind === "key")
|
|
155
|
+
.filter(entry => entry.featured || entry.authKind === "key" || entry.dashboardPreset)
|
|
153
156
|
.map(entryToPreset);
|
|
154
157
|
return [...dedupePresets(presets), customPreset()];
|
|
155
158
|
}
|
|
156
159
|
|
|
160
|
+
export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig): void {
|
|
161
|
+
const entry = PROVIDER_REGISTRY.find(row => row.id === name);
|
|
162
|
+
if (!entry) return;
|
|
163
|
+
const seed = providerConfigSeed(entry);
|
|
164
|
+
if (!prov.defaultModel && seed.defaultModel) prov.defaultModel = seed.defaultModel;
|
|
165
|
+
if (!prov.models && seed.models) prov.models = [...seed.models];
|
|
166
|
+
if (prov.liveModels === undefined && seed.liveModels !== undefined) prov.liveModels = seed.liveModels;
|
|
167
|
+
if (prov.contextWindow === undefined && seed.contextWindow !== undefined) prov.contextWindow = seed.contextWindow;
|
|
168
|
+
if (!prov.modelContextWindows && seed.modelContextWindows) prov.modelContextWindows = { ...seed.modelContextWindows };
|
|
169
|
+
if (!prov.modelInputModalities && seed.modelInputModalities) prov.modelInputModalities = cloneRecordOfArrays(seed.modelInputModalities);
|
|
170
|
+
if (!prov.reasoningEfforts && seed.reasoningEfforts) prov.reasoningEfforts = [...seed.reasoningEfforts];
|
|
171
|
+
if (!prov.modelReasoningEfforts && seed.modelReasoningEfforts) prov.modelReasoningEfforts = cloneRecordOfArrays(seed.modelReasoningEfforts);
|
|
172
|
+
if (!prov.reasoningEffortMap && seed.reasoningEffortMap) prov.reasoningEffortMap = { ...seed.reasoningEffortMap };
|
|
173
|
+
if (!prov.modelReasoningEffortMap && seed.modelReasoningEffortMap) prov.modelReasoningEffortMap = cloneNestedRecord(seed.modelReasoningEffortMap);
|
|
174
|
+
if (!prov.noVisionModels && seed.noVisionModels) prov.noVisionModels = [...seed.noVisionModels];
|
|
175
|
+
if (!prov.noReasoningModels && seed.noReasoningModels) prov.noReasoningModels = [...seed.noReasoningModels];
|
|
176
|
+
if (!prov.noTemperatureModels && seed.noTemperatureModels) prov.noTemperatureModels = [...seed.noTemperatureModels];
|
|
177
|
+
if (!prov.noTopPModels && seed.noTopPModels) prov.noTopPModels = [...seed.noTopPModels];
|
|
178
|
+
if (!prov.noPenaltyModels && seed.noPenaltyModels) prov.noPenaltyModels = [...seed.noPenaltyModels];
|
|
179
|
+
if (!prov.autoToolChoiceOnlyModels && seed.autoToolChoiceOnlyModels) prov.autoToolChoiceOnlyModels = [...seed.autoToolChoiceOnlyModels];
|
|
180
|
+
if (!prov.preserveReasoningContentModels && seed.preserveReasoningContentModels) prov.preserveReasoningContentModels = [...seed.preserveReasoningContentModels];
|
|
181
|
+
if (prov.escapeBuiltinToolNames === undefined && seed.escapeBuiltinToolNames !== undefined) prov.escapeBuiltinToolNames = seed.escapeBuiltinToolNames;
|
|
182
|
+
}
|
|
183
|
+
|
|
157
184
|
export function deriveFeaturedProviderIds(): string[] {
|
|
158
185
|
return PROVIDER_REGISTRY.filter(entry => entry.featured).map(entry => entry.id);
|
|
159
186
|
}
|
|
@@ -181,7 +208,7 @@ function entryToPreset(entry: ProviderRegistryEntry): DerivedProviderPreset {
|
|
|
181
208
|
label: entry.label,
|
|
182
209
|
adapter: entry.adapter,
|
|
183
210
|
baseUrl: entry.baseUrl,
|
|
184
|
-
auth: entry.authKind === "forward" ? "forward" : entry.authKind === "oauth" ? "oauth" : "key",
|
|
211
|
+
auth: entry.authKind === "forward" ? "forward" : entry.authKind === "oauth" ? "oauth" : entry.authKind === "local" ? "local" : "key",
|
|
185
212
|
...(entry.defaultModel ? { defaultModel: entry.defaultModel } : {}),
|
|
186
213
|
...(entry.authKind === "oauth" ? { oauthProvider: entry.oauthId ?? entry.id } : {}),
|
|
187
214
|
...(entry.dashboardUrl ? { dashboardUrl: entry.dashboardUrl } : {}),
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import type { OcxProviderConfig } from "../types";
|
|
2
2
|
import { KIRO_MODELS, KIRO_MODEL_CONTEXT_WINDOWS, KIRO_MODEL_REASONING_EFFORTS } from "./kiro-models";
|
|
3
3
|
import { ANTIGRAVITY_MODELS, ANTIGRAVITY_MODEL_CONTEXT_WINDOWS } from "./antigravity-models";
|
|
4
|
+
import {
|
|
5
|
+
CURSOR_STATIC_MODELS,
|
|
6
|
+
cursorModelContextWindows,
|
|
7
|
+
cursorModelIds,
|
|
8
|
+
cursorModelInputModalities,
|
|
9
|
+
cursorModelReasoningEfforts,
|
|
10
|
+
} from "../adapters/cursor/discovery";
|
|
4
11
|
|
|
5
12
|
export type ProviderAuthKind = "forward" | "oauth" | "key" | "local";
|
|
6
13
|
export type MetadataModelIdNormalize = "case-insensitive";
|
|
@@ -12,10 +19,12 @@ export interface ProviderRegistryEntry {
|
|
|
12
19
|
baseUrl: string;
|
|
13
20
|
authKind: ProviderAuthKind;
|
|
14
21
|
featured?: boolean;
|
|
22
|
+
dashboardPreset?: boolean;
|
|
15
23
|
note?: string;
|
|
16
24
|
dashboardUrl?: string;
|
|
17
25
|
defaultModel?: string;
|
|
18
26
|
models?: string[];
|
|
27
|
+
liveModels?: boolean;
|
|
19
28
|
contextWindow?: number;
|
|
20
29
|
modelContextWindows?: Record<string, number>;
|
|
21
30
|
modelInputModalities?: Record<string, string[]>;
|
|
@@ -43,7 +52,7 @@ export interface ProviderRegistryEntry {
|
|
|
43
52
|
export type ProviderConfigSeed = Pick<
|
|
44
53
|
OcxProviderConfig,
|
|
45
54
|
"adapter" | "baseUrl" | "authMode" | "defaultModel" | "models"
|
|
46
|
-
| "contextWindow" | "modelContextWindows" | "modelInputModalities"
|
|
55
|
+
| "liveModels" | "contextWindow" | "modelContextWindows" | "modelInputModalities"
|
|
47
56
|
| "reasoningEfforts" | "modelReasoningEfforts" | "reasoningEffortMap" | "modelReasoningEffortMap"
|
|
48
57
|
| "noVisionModels" | "noReasoningModels" | "noTemperatureModels" | "noTopPModels" | "noPenaltyModels"
|
|
49
58
|
| "autoToolChoiceOnlyModels" | "preserveReasoningContentModels" | "escapeBuiltinToolNames"
|
|
@@ -115,6 +124,22 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
115
124
|
featured: true,
|
|
116
125
|
note: "Uses your codex login — no API key",
|
|
117
126
|
},
|
|
127
|
+
{
|
|
128
|
+
id: "cursor",
|
|
129
|
+
label: "Cursor (experimental, live native exec)",
|
|
130
|
+
adapter: "cursor",
|
|
131
|
+
baseUrl: "https://api2.cursor.sh",
|
|
132
|
+
authKind: "oauth",
|
|
133
|
+
featured: false,
|
|
134
|
+
dashboardPreset: true,
|
|
135
|
+
note: "Experimental Cursor bridge. Live transport, live model discovery, and native read/write/delete/shell execution are enabled after a standalone PKCE browser login via 'ocx login cursor'.",
|
|
136
|
+
models: cursorModelIds(CURSOR_STATIC_MODELS),
|
|
137
|
+
liveModels: true,
|
|
138
|
+
defaultModel: "auto",
|
|
139
|
+
modelContextWindows: cursorModelContextWindows(CURSOR_STATIC_MODELS),
|
|
140
|
+
modelInputModalities: cursorModelInputModalities(CURSOR_STATIC_MODELS),
|
|
141
|
+
modelReasoningEfforts: cursorModelReasoningEfforts(CURSOR_STATIC_MODELS),
|
|
142
|
+
},
|
|
118
143
|
{
|
|
119
144
|
id: "xai",
|
|
120
145
|
label: "xAI Grok",
|
|
@@ -355,3 +380,16 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
355
380
|
export function getProviderRegistryEntry(id: string): ProviderRegistryEntry | undefined {
|
|
356
381
|
return PROVIDER_REGISTRY.find(entry => entry.id === id);
|
|
357
382
|
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Effective Google wire mode for a provider: config value, else registry backfill (a saved
|
|
386
|
+
* key-login config may omit `googleMode` — mirrors the router's backfill), else "ai-studio"
|
|
387
|
+
* (the Generative Language API default). Null for non-google adapters.
|
|
388
|
+
*/
|
|
389
|
+
export function effectiveGoogleMode(
|
|
390
|
+
providerId: string,
|
|
391
|
+
prov: { adapter?: string; googleMode?: "ai-studio" | "vertex" | "cloud-code-assist" },
|
|
392
|
+
): "ai-studio" | "vertex" | "cloud-code-assist" | null {
|
|
393
|
+
if (prov.adapter !== "google") return null;
|
|
394
|
+
return prov.googleMode ?? getProviderRegistryEntry(providerId)?.googleMode ?? "ai-studio";
|
|
395
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime-state-first proxy liveness with identity checking.
|
|
3
|
+
*
|
|
4
|
+
* Historically `ensure`/`start` probed only `config.port` and accepted ANY 2xx /healthz:
|
|
5
|
+
* a proxy that started on a fallback port was invisible (duplicate starts, Codex synced
|
|
6
|
+
* back to a dead port), and a foreign app answering 200 on the configured port counted
|
|
7
|
+
* as "our proxy". Liveness now (1) prefers the pid + runtime-port record and (2) requires
|
|
8
|
+
* the /healthz body to identify as opencodex.
|
|
9
|
+
*
|
|
10
|
+
* Lives outside cli.ts (which dispatches argv at module top level) so tests can import it.
|
|
11
|
+
*/
|
|
12
|
+
import { loadConfig, readPid, readRuntimePort } from "./config";
|
|
13
|
+
|
|
14
|
+
export interface HealthzIdentity {
|
|
15
|
+
service?: unknown;
|
|
16
|
+
status?: unknown;
|
|
17
|
+
version?: unknown;
|
|
18
|
+
uptime?: unknown;
|
|
19
|
+
pid?: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface LivenessIo {
|
|
23
|
+
fetchFn?: typeof fetch;
|
|
24
|
+
readPidFn?: () => number | null;
|
|
25
|
+
readRuntimeFn?: (pid?: number) => { pid?: number; port: number; hostname?: string } | null;
|
|
26
|
+
configFn?: () => { port?: number; hostname?: string };
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface LiveProxy {
|
|
31
|
+
pid: number | null;
|
|
32
|
+
port: number;
|
|
33
|
+
/** Raw bind hostname the probe succeeded against; compose URLs via `probeHostname`. */
|
|
34
|
+
hostname?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Host to probe for a given bind hostname: wildcards answer on IPv4 loopback, and raw
|
|
39
|
+
* IPv6 addresses must be bracketed or the composed URL is invalid.
|
|
40
|
+
*/
|
|
41
|
+
export function probeHostname(hostname: string | undefined): string {
|
|
42
|
+
const trimmed = (hostname ?? "").trim();
|
|
43
|
+
if (!trimmed || trimmed === "0.0.0.0" || trimmed === "::" || trimmed === "[::]") return "127.0.0.1";
|
|
44
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) return trimmed;
|
|
45
|
+
return trimmed.includes(":") ? `[${trimmed}]` : trimmed;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* True when a /healthz body identifies an opencodex proxy. Accepts the explicit
|
|
50
|
+
* `service: "opencodex"` marker, plus the legacy `{status, version, uptime}` trio so a
|
|
51
|
+
* still-running pre-identity proxy (e.g. right after `ocx update`) is not mistaken for a
|
|
52
|
+
* foreign server and shadow-started over.
|
|
53
|
+
*/
|
|
54
|
+
export function isOpencodexHealthz(body: HealthzIdentity | null): boolean {
|
|
55
|
+
if (!body) return false;
|
|
56
|
+
if (body.service === "opencodex") return true;
|
|
57
|
+
if (body.service !== undefined) return false;
|
|
58
|
+
return body.status === "ok" && typeof body.version === "string" && typeof body.uptime === "number";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Identity-checked /healthz probe; null when unreachable, non-OK, or not our proxy. */
|
|
62
|
+
export async function proxyIdentityAt(
|
|
63
|
+
port: number,
|
|
64
|
+
opts: { hostname?: string; expectedPid?: number } = {},
|
|
65
|
+
io: LivenessIo = {},
|
|
66
|
+
): Promise<{ pid: number | null } | null> {
|
|
67
|
+
const fetchFn = io.fetchFn ?? fetch;
|
|
68
|
+
try {
|
|
69
|
+
const res = await fetchFn(`http://${probeHostname(opts.hostname)}:${port}/healthz`, {
|
|
70
|
+
signal: AbortSignal.timeout(io.timeoutMs ?? 750),
|
|
71
|
+
});
|
|
72
|
+
if (!res.ok) return null;
|
|
73
|
+
const body = (await res.json().catch(() => null)) as HealthzIdentity | null;
|
|
74
|
+
if (!isOpencodexHealthz(body)) return null;
|
|
75
|
+
const pid = typeof body?.pid === "number" ? body.pid : null;
|
|
76
|
+
if (opts.expectedPid !== undefined && pid !== null && pid !== opts.expectedPid) return null;
|
|
77
|
+
return { pid };
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Locate the live proxy: pid file → runtime-port record → identity probe. Falls back to
|
|
85
|
+
* the configured port ONLY when no runtime record answers, so a fallback-port proxy is
|
|
86
|
+
* found and a foreign listener on the configured port is rejected.
|
|
87
|
+
*/
|
|
88
|
+
export async function findLiveProxy(io: LivenessIo = {}): Promise<LiveProxy | null> {
|
|
89
|
+
const readPidFn = io.readPidFn ?? readPid;
|
|
90
|
+
const readRuntimeFn = io.readRuntimeFn ?? readRuntimePort;
|
|
91
|
+
const configFn = io.configFn ?? loadConfig;
|
|
92
|
+
|
|
93
|
+
const pid = readPidFn();
|
|
94
|
+
let probedPort: number | null = null;
|
|
95
|
+
if (pid) {
|
|
96
|
+
const runtime = readRuntimeFn(pid);
|
|
97
|
+
if (runtime?.port) {
|
|
98
|
+
probedPort = runtime.port;
|
|
99
|
+
const identity = await proxyIdentityAt(runtime.port, { hostname: runtime.hostname, expectedPid: pid }, io);
|
|
100
|
+
if (identity) return { pid, port: runtime.port, hostname: runtime.hostname };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Orphan recovery: the pid file can be lost/corrupt while the proxy is alive (crash of a
|
|
105
|
+
// sibling command, manual deletion). The runtime record still says where it listens —
|
|
106
|
+
// identity-probe it so ensure/update/stop see the live proxy instead of shadowing it.
|
|
107
|
+
const record = readRuntimeFn();
|
|
108
|
+
if (record?.port && record.port !== probedPort) {
|
|
109
|
+
const expectedPid = typeof record.pid === "number" ? record.pid : undefined;
|
|
110
|
+
const identity = await proxyIdentityAt(record.port, { hostname: record.hostname, expectedPid }, io);
|
|
111
|
+
// Only the healthz-reported pid is authoritative here. The record's pid may be stale
|
|
112
|
+
// (its process dead, the port reused by a pidless legacy proxy) — synthesizing it
|
|
113
|
+
// would hand destructive callers (stopProxy → kill fallback) a reusable pid.
|
|
114
|
+
if (identity) return { pid: identity.pid ?? null, port: record.port, hostname: record.hostname };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const config = configFn();
|
|
118
|
+
const port = config.port ?? 10100;
|
|
119
|
+
const identity = await proxyIdentityAt(port, { hostname: config.hostname }, io);
|
|
120
|
+
if (identity) return { pid: identity.pid ?? pid ?? null, port, hostname: config.hostname };
|
|
121
|
+
return null;
|
|
122
|
+
}
|
package/src/responses/parser.ts
CHANGED
|
@@ -388,6 +388,7 @@ export function parseRequest(body: unknown): OcxParsedRequest {
|
|
|
388
388
|
}
|
|
389
389
|
const tc = mapToolChoice(data.tool_choice);
|
|
390
390
|
if (tc !== undefined) options.toolChoice = tc;
|
|
391
|
+
if (data.parallel_tool_calls !== undefined) options.parallelToolCalls = data.parallel_tool_calls;
|
|
391
392
|
if (data.reasoning?.effort && REASONING_EFFORTS.has(data.reasoning.effort)) {
|
|
392
393
|
options.reasoning = data.reasoning.effort;
|
|
393
394
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const MAX_STORED_RESPONSES = 1_000;
|
|
2
|
+
const RESPONSE_TTL_MS = 60 * 60 * 1_000;
|
|
3
|
+
|
|
4
|
+
interface StoredResponseState {
|
|
5
|
+
createdAt: number;
|
|
6
|
+
items: unknown[];
|
|
7
|
+
conversationId?: string;
|
|
8
|
+
cursorCheckpointUsable?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const states = new Map<string, StoredResponseState>();
|
|
12
|
+
|
|
13
|
+
function now(): number {
|
|
14
|
+
return Date.now();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function inputItems(input: unknown): unknown[] {
|
|
18
|
+
if (input === undefined) return [];
|
|
19
|
+
if (Array.isArray(input)) return input;
|
|
20
|
+
if (typeof input === "string") return [{ role: "user", content: input }];
|
|
21
|
+
return [input];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function pruneResponses(at = now()): void {
|
|
25
|
+
for (const [id, state] of states) {
|
|
26
|
+
if (at - state.createdAt > RESPONSE_TTL_MS) states.delete(id);
|
|
27
|
+
}
|
|
28
|
+
while (states.size > MAX_STORED_RESPONSES) {
|
|
29
|
+
const oldest = states.keys().next().value;
|
|
30
|
+
if (!oldest) break;
|
|
31
|
+
states.delete(oldest);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function expandPreviousResponseInput(body: unknown): unknown {
|
|
36
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return body;
|
|
37
|
+
const request = body as Record<string, unknown>;
|
|
38
|
+
const previousId = typeof request.previous_response_id === "string" ? request.previous_response_id : undefined;
|
|
39
|
+
if (!previousId) return body;
|
|
40
|
+
pruneResponses();
|
|
41
|
+
const previous = states.get(previousId);
|
|
42
|
+
if (!previous) return body;
|
|
43
|
+
return {
|
|
44
|
+
...request,
|
|
45
|
+
input: [...previous.items, ...inputItems(request.input)],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function previousResponseConversationId(responseId: string | undefined): string | undefined {
|
|
50
|
+
if (!responseId) return undefined;
|
|
51
|
+
pruneResponses();
|
|
52
|
+
return states.get(responseId)?.conversationId;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function rememberResponseState(
|
|
56
|
+
requestBody: unknown,
|
|
57
|
+
response: { id?: unknown; output?: unknown; status?: unknown },
|
|
58
|
+
conversationId?: string,
|
|
59
|
+
): void {
|
|
60
|
+
if (!requestBody || typeof requestBody !== "object" || Array.isArray(requestBody)) return;
|
|
61
|
+
const request = requestBody as Record<string, unknown>;
|
|
62
|
+
if (request.store === false) return;
|
|
63
|
+
if (typeof response.id !== "string" || !Array.isArray(response.output)) return;
|
|
64
|
+
if (response.status !== undefined && response.status !== "completed") return;
|
|
65
|
+
states.set(response.id, {
|
|
66
|
+
createdAt: now(),
|
|
67
|
+
items: [...inputItems(request.input), ...response.output],
|
|
68
|
+
// Always preserve the Cursor conversation id so the next tool-result turn can continue the SAME
|
|
69
|
+
// Cursor conversation (multi-turn continuation). Separately track whether Cursor's own
|
|
70
|
+
// checkpoint/cache is safe to reuse: a turn that ended with a pending client tool call produced an
|
|
71
|
+
// incomplete agent turn on the Cursor side (we suspended without a real mcpResult), so its
|
|
72
|
+
// checkpoint must not be reused — but the conversation id string itself is still valid.
|
|
73
|
+
...(conversationId ? { conversationId } : {}),
|
|
74
|
+
cursorCheckpointUsable: !response.output.some(item => {
|
|
75
|
+
return !!item && typeof item === "object" && (item as { type?: unknown }).type === "function_call";
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
pruneResponses();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function clearResponseStateForTests(): void {
|
|
82
|
+
states.clear();
|
|
83
|
+
}
|