@bitkyc08/opencodex 2.6.26-preview.20260705 → 2.6.28-preview.20260707
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 +1 -0
- package/bin/ocx.mjs +4 -4
- package/gui/dist/assets/index-ByGC8-Bm.css +1 -0
- package/gui/dist/assets/index-CkV5xFA8.js +15 -0
- package/gui/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/adapters/anthropic-image-guard.ts +195 -0
- package/src/adapters/anthropic.ts +85 -14
- package/src/adapters/cursor/cursor-errors.ts +2 -2
- package/src/adapters/cursor/live-transport.ts +1 -1
- package/src/adapters/cursor/transport-retry.ts +2 -2
- package/src/adapters/google-errors.ts +1 -1
- package/src/adapters/google-http.ts +1 -1
- package/src/adapters/google-truncation.ts +1 -1
- package/src/adapters/google.ts +1 -1
- package/src/adapters/kiro-errors.ts +1 -1
- package/src/adapters/kiro-retry.ts +1 -1
- package/src/adapters/kiro-truncation.ts +1 -1
- package/src/adapters/kiro.ts +1 -1
- package/src/adapters/openai-chat.ts +12 -2
- package/src/adapters/openai-responses.ts +126 -3
- package/src/bridge.ts +164 -7
- package/src/{doctor.ts → cli/doctor.ts} +21 -4
- package/src/{cli-help.ts → cli/help.ts} +1 -1
- package/src/cli/index.ts +584 -0
- package/src/{init.ts → cli/init.ts} +6 -6
- package/src/{cli-models.ts → cli/models.ts} +2 -2
- package/src/{cli-provider.ts → cli/provider.ts} +12 -8
- package/src/{star-prompt.ts → cli/star-prompt.ts} +1 -1
- package/src/{cli-status.ts → cli/status.ts} +7 -7
- package/src/cli.ts +9 -575
- package/src/{codex-account-label.ts → codex/account-label.ts} +1 -1
- package/src/{codex-account-lifecycle.ts → codex/account-lifecycle.ts} +6 -6
- package/src/{codex-account-store.ts → codex/account-store.ts} +2 -2
- package/src/{codex-account-usability.ts → codex/account-usability.ts} +4 -4
- package/src/{codex-auth-api.ts → codex/auth-api.ts} +18 -18
- package/src/{codex-auth-collision.ts → codex/auth-collision.ts} +4 -4
- package/src/{codex-auth-context.ts → codex/auth-context.ts} +9 -9
- package/src/{codex-catalog.ts → codex/catalog.ts} +49 -20
- package/src/codex/history-migration-guardian.ts +102 -0
- package/src/{codex-history-provider.ts → codex/history-provider.ts} +111 -7
- package/src/{codex-home.ts → codex/home.ts} +1 -1
- package/src/{codex-inject.ts → codex/inject.ts} +204 -26
- package/src/{codex-journal.ts → codex/journal.ts} +2 -2
- package/src/{codex-main-account.ts → codex/main-account.ts} +2 -2
- package/src/{model-cache.ts → codex/model-cache.ts} +1 -1
- package/src/{codex-paths.ts → codex/paths.ts} +2 -2
- package/src/{codex-plugins-doctor.ts → codex/plugins-doctor.ts} +2 -2
- package/src/{codex-refresh.ts → codex/refresh.ts} +4 -4
- package/src/{codex-routing.ts → codex/routing.ts} +8 -8
- package/src/{codex-shim.ts → codex/shim.ts} +6 -5
- package/src/{codex-sync.ts → codex/sync.ts} +4 -4
- package/src/{codex-websocket-registry.ts → codex/websocket-registry.ts} +1 -1
- package/src/config.ts +2 -0
- package/src/generated/jawcode-model-metadata.ts +2 -0
- package/src/{bun-runtime.ts → lib/bun-runtime.ts} +1 -1
- package/src/{crash-guard.ts → lib/crash-guard.ts} +1 -1
- package/src/{process-control.ts → lib/process-control.ts} +1 -1
- package/src/{service-secrets.ts → lib/service-secrets.ts} +1 -1
- package/src/oauth/callback-server.ts +1 -1
- package/src/oauth/google-antigravity.ts +7 -4
- package/src/oauth/index.ts +67 -16
- package/src/oauth/login-cli.ts +2 -2
- package/src/oauth/store.ts +236 -20
- package/src/oauth/token-guardian.ts +24 -20
- package/src/oauth/types.ts +16 -0
- package/src/providers/api-keys.ts +121 -0
- package/src/{provider-context-cap.ts → providers/context-cap.ts} +1 -1
- package/src/providers/derive.ts +2 -0
- package/src/providers/key-failover.ts +145 -0
- package/src/{provider-label.ts → providers/label.ts} +1 -1
- package/src/{provider-quota.ts → providers/quota.ts} +12 -7
- package/src/providers/registry.ts +66 -4
- package/src/responses/compaction.ts +117 -0
- package/src/responses/parser.ts +89 -13
- package/src/responses/reasoning-envelope.ts +52 -0
- package/src/responses/schema.ts +15 -3
- package/src/responses/state.ts +117 -2
- package/src/router.ts +2 -0
- package/src/server/auth-cors.ts +231 -0
- package/src/server/index.ts +523 -0
- package/src/server/lifecycle.ts +73 -0
- package/src/server/management-api.ts +628 -0
- package/src/{proxy-liveness.ts → server/proxy-liveness.ts} +1 -1
- package/src/server/relay.ts +534 -0
- package/src/server/request-decompress.ts +46 -0
- package/src/server/request-log.ts +310 -0
- package/src/server/responses.ts +775 -0
- package/src/{ws-bridge.ts → server/ws-bridge.ts} +44 -13
- package/src/service.ts +19 -11
- package/src/types.ts +27 -0
- package/src/{update.ts → update/index.ts} +5 -5
- package/src/{update-job.ts → update/job.ts} +7 -6
- package/src/{update-notify.ts → update/notify.ts} +3 -3
- package/src/{usage-debug.ts → usage/debug.ts} +3 -3
- package/src/{usage-log.ts → usage/log.ts} +3 -3
- package/src/{usage-summary.ts → usage/summary.ts} +3 -3
- package/src/{usage-totals.ts → usage/totals.ts} +1 -1
- package/src/vision/describe.ts +3 -3
- package/src/vision/index.ts +21 -1
- package/src/web-search/executor.ts +4 -4
- package/src/web-search/index.ts +1 -1
- package/src/web-search/loop.ts +84 -24
- package/gui/dist/assets/index-BcHhxo1I.css +0 -1
- package/gui/dist/assets/index-DCC1q_Jx.js +0 -15
- package/src/server.ts +0 -2501
- /package/src/{codex-account-runtime-state.ts → codex/account-runtime-state.ts} +0 -0
- /package/src/{codex-quota.ts → codex/quota.ts} +0 -0
- /package/src/{abort.ts → lib/abort.ts} +0 -0
- /package/src/{debug.ts → lib/debug.ts} +0 -0
- /package/src/{errors.ts → lib/errors.ts} +0 -0
- /package/src/{open-url.ts → lib/open-url.ts} +0 -0
- /package/src/{privacy.ts → lib/privacy.ts} +0 -0
- /package/src/{redact.ts → lib/redact.ts} +0 -0
- /package/src/{sidecar-tracker.ts → lib/sidecar-tracker.ts} +0 -0
- /package/src/{upstream-retry.ts → lib/upstream-retry.ts} +0 -0
- /package/src/{win-paths.ts → lib/win-paths.ts} +0 -0
- /package/src/{ports.ts → server/ports.ts} +0 -0
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import type { CatalogModel } from "../codex/catalog";
|
|
3
|
+
import { invalidateCodexModelsCache } from "../codex/catalog";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_SUBAGENT_MODELS,
|
|
6
|
+
codexAutoStartEnabled,
|
|
7
|
+
hasOwnProvider,
|
|
8
|
+
isValidProviderName,
|
|
9
|
+
providerBaseUrlConfigError,
|
|
10
|
+
providerHeadersConfigError,
|
|
11
|
+
saveConfig,
|
|
12
|
+
} from "../config";
|
|
13
|
+
import {
|
|
14
|
+
clearLoginState,
|
|
15
|
+
getLoginStatus,
|
|
16
|
+
isOAuthProvider,
|
|
17
|
+
listOAuthProviders,
|
|
18
|
+
startLoginFlow,
|
|
19
|
+
upsertOAuthProvider,
|
|
20
|
+
} from "../oauth";
|
|
21
|
+
import { removeCredential } from "../oauth/store";
|
|
22
|
+
import { enrichProviderFromCatalog, listKeyLoginProviders } from "../oauth/key-providers";
|
|
23
|
+
import { deriveProviderPresets } from "../providers/derive";
|
|
24
|
+
import { fetchProviderQuotaReports } from "../providers/quota";
|
|
25
|
+
import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../providers/context-cap";
|
|
26
|
+
import { readUsageEntries } from "../usage/log";
|
|
27
|
+
import { parseRange, summarizeUsage } from "../usage/summary";
|
|
28
|
+
import { stripCodexRuntimeProviderFields } from "../codex/auth-context";
|
|
29
|
+
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
30
|
+
import { drainAndShutdown } from "./lifecycle";
|
|
31
|
+
import { filterRequestLogs, getRequestLogEntries } from "./request-log";
|
|
32
|
+
import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "./auth-cors";
|
|
33
|
+
|
|
34
|
+
// Single source of truth = package.json (../ from src/), so /healthz + the GUI badge match the
|
|
35
|
+
// installed npm version instead of a stale hardcode.
|
|
36
|
+
export const VERSION = (() => {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version as string;
|
|
39
|
+
} catch {
|
|
40
|
+
return "0.0.0";
|
|
41
|
+
}
|
|
42
|
+
})();
|
|
43
|
+
|
|
44
|
+
export async function handleManagementAPI(req: Request, url: URL, config: OcxConfig): Promise<Response | null> {
|
|
45
|
+
if (!isAllowedRequestOrigin(req, config)) {
|
|
46
|
+
return jsonResponse({ error: "cross-origin request blocked" }, 403, req, config);
|
|
47
|
+
}
|
|
48
|
+
// Management bodies are small JSON (provider names, key ids, settings). Reject oversized
|
|
49
|
+
// payloads before any handler buffers them — the data plane has its own decompression cap.
|
|
50
|
+
if (req.method === "POST" || req.method === "PUT") {
|
|
51
|
+
const contentLength = Number(req.headers.get("content-length") ?? "0");
|
|
52
|
+
if (Number.isFinite(contentLength) && contentLength > 2 * 1024 * 1024) {
|
|
53
|
+
return jsonResponse({ error: "request body too large" }, 413, req, config);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function refreshCodexCatalogBestEffort(): Promise<void> {
|
|
57
|
+
try {
|
|
58
|
+
const { refreshCodexModelCatalog } = await import("../codex/refresh");
|
|
59
|
+
await refreshCodexModelCatalog(config);
|
|
60
|
+
} catch {
|
|
61
|
+
/* catalog absent */
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (url.pathname === "/api/config" && req.method === "GET") {
|
|
66
|
+
return jsonResponse(safeConfigDTO(config));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (url.pathname === "/api/config" && req.method === "PUT") {
|
|
70
|
+
return jsonResponse({ error: "Full config PUT is disabled. Use /api/providers POST for provider changes." }, 405);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (url.pathname === "/api/settings" && req.method === "GET") {
|
|
74
|
+
return jsonResponse({
|
|
75
|
+
codexAutoStart: codexAutoStartEnabled(config),
|
|
76
|
+
port: config.port,
|
|
77
|
+
hostname: config.hostname ?? "127.0.0.1",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (url.pathname === "/api/settings" && req.method === "PUT") {
|
|
82
|
+
let body: { codexAutoStart?: unknown };
|
|
83
|
+
try { body = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
84
|
+
if (typeof body.codexAutoStart !== "boolean") {
|
|
85
|
+
return jsonResponse({ error: "codexAutoStart boolean is required" }, 400);
|
|
86
|
+
}
|
|
87
|
+
config.codexAutoStart = body.codexAutoStart;
|
|
88
|
+
saveConfig(config);
|
|
89
|
+
return jsonResponse({ ok: true, codexAutoStart: codexAutoStartEnabled(config) });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (url.pathname === "/api/sync" && req.method === "POST") {
|
|
93
|
+
const { syncModelsToCodex } = await import("../codex/sync");
|
|
94
|
+
const result = await syncModelsToCodex(undefined, config, null);
|
|
95
|
+
return jsonResponse({
|
|
96
|
+
...result,
|
|
97
|
+
staleAppServerHint: "If Codex App still shows an older model list, restart its long-lived app-server process after sync.",
|
|
98
|
+
}, result.ok ? 200 : 500);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (url.pathname === "/api/update/check" && req.method === "GET") {
|
|
102
|
+
const { checkForUpdate, normalizeUpdateChannel } = await import("../update/job");
|
|
103
|
+
const rawTag = url.searchParams.get("tag");
|
|
104
|
+
if (rawTag && rawTag !== "latest" && rawTag !== "preview") {
|
|
105
|
+
return jsonResponse({ error: "tag must be latest or preview" }, 400);
|
|
106
|
+
}
|
|
107
|
+
return jsonResponse(checkForUpdate(normalizeUpdateChannel(rawTag)));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (url.pathname === "/api/update/run" && req.method === "POST") {
|
|
111
|
+
const { normalizeUpdateChannel, startUpdateJob, UpdateJobError } = await import("../update/job");
|
|
112
|
+
let body: { tag?: unknown; restart?: unknown };
|
|
113
|
+
try { body = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
114
|
+
if (body.tag !== undefined && body.tag !== "latest" && body.tag !== "preview") {
|
|
115
|
+
return jsonResponse({ error: "tag must be latest or preview" }, 400);
|
|
116
|
+
}
|
|
117
|
+
if (body.restart !== undefined && typeof body.restart !== "boolean") {
|
|
118
|
+
return jsonResponse({ error: "restart boolean is required" }, 400);
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
return jsonResponse({ ok: true, job: startUpdateJob(normalizeUpdateChannel(body.tag as string | undefined), body.restart !== false) });
|
|
122
|
+
} catch (err) {
|
|
123
|
+
if (err instanceof UpdateJobError) {
|
|
124
|
+
return jsonResponse({ error: err.message, code: err.code }, err.status);
|
|
125
|
+
}
|
|
126
|
+
return jsonResponse({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (url.pathname === "/api/update/status" && req.method === "GET") {
|
|
131
|
+
const { readUpdateJob } = await import("../update/job");
|
|
132
|
+
const job = readUpdateJob(url.searchParams.get("jobId"));
|
|
133
|
+
if (!job) return jsonResponse({ error: "update job not found" }, 404);
|
|
134
|
+
return jsonResponse({ ok: true, job });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (url.pathname === "/api/sidecar-settings" && req.method === "GET") {
|
|
138
|
+
const ws = config.webSearchSidecar ?? {};
|
|
139
|
+
const vs = config.visionSidecar ?? {};
|
|
140
|
+
return jsonResponse({
|
|
141
|
+
webSearch: { model: ws.model ?? "gpt-5.4-mini", reasoning: ws.reasoning ?? "low" },
|
|
142
|
+
vision: { model: vs.model ?? "gpt-5.4-mini" },
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (url.pathname === "/api/sidecar-settings" && req.method === "PUT") {
|
|
147
|
+
let body: { webSearch?: { model?: string; reasoning?: string }; vision?: { model?: string } };
|
|
148
|
+
try { body = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
149
|
+
if (body.webSearch) {
|
|
150
|
+
config.webSearchSidecar = { ...config.webSearchSidecar };
|
|
151
|
+
if (typeof body.webSearch.model === "string") config.webSearchSidecar.model = body.webSearch.model;
|
|
152
|
+
if (typeof body.webSearch.reasoning === "string") config.webSearchSidecar.reasoning = body.webSearch.reasoning;
|
|
153
|
+
}
|
|
154
|
+
if (body.vision) {
|
|
155
|
+
config.visionSidecar = { ...config.visionSidecar };
|
|
156
|
+
if (typeof body.vision.model === "string") config.visionSidecar.model = body.vision.model;
|
|
157
|
+
}
|
|
158
|
+
saveConfig(config);
|
|
159
|
+
const ws = config.webSearchSidecar ?? {};
|
|
160
|
+
const vs = config.visionSidecar ?? {};
|
|
161
|
+
return jsonResponse({
|
|
162
|
+
ok: true,
|
|
163
|
+
webSearch: { model: ws.model ?? "gpt-5.4-mini", reasoning: ws.reasoning ?? "low" },
|
|
164
|
+
vision: { model: vs.model ?? "gpt-5.4-mini" },
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (url.pathname === "/api/logs" && req.method === "GET") {
|
|
169
|
+
return jsonResponse(filterRequestLogs(getRequestLogEntries(), url.searchParams));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (url.pathname === "/api/usage" && req.method === "GET") {
|
|
173
|
+
const range = parseRange(url.searchParams.get("range"));
|
|
174
|
+
const now = Date.now();
|
|
175
|
+
try {
|
|
176
|
+
return jsonResponse(summarizeUsage(readUsageEntries(), range, now));
|
|
177
|
+
} catch {
|
|
178
|
+
return jsonResponse({
|
|
179
|
+
range,
|
|
180
|
+
since: null,
|
|
181
|
+
generatedAt: now,
|
|
182
|
+
summary: {
|
|
183
|
+
requests: 0,
|
|
184
|
+
measuredRequests: 0,
|
|
185
|
+
reportedRequests: 0,
|
|
186
|
+
unreportedRequests: 0,
|
|
187
|
+
unsupportedRequests: 0,
|
|
188
|
+
estimatedRequests: 0,
|
|
189
|
+
inputTokens: 0,
|
|
190
|
+
outputTokens: 0,
|
|
191
|
+
cachedInputTokens: 0,
|
|
192
|
+
reasoningOutputTokens: 0,
|
|
193
|
+
totalTokens: 0,
|
|
194
|
+
coverageRatio: 0,
|
|
195
|
+
},
|
|
196
|
+
days: [],
|
|
197
|
+
models: [],
|
|
198
|
+
providers: [],
|
|
199
|
+
error: "read_failed",
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (url.pathname === "/api/provider-quotas" && req.method === "GET") {
|
|
205
|
+
const forceRefresh = url.searchParams.get("refresh") === "1" || url.searchParams.get("refresh") === "true";
|
|
206
|
+
return jsonResponse(await fetchProviderQuotaReports(config, forceRefresh));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (url.pathname === "/api/providers" && req.method === "GET") {
|
|
210
|
+
return jsonResponse(Object.entries(config.providers).map(([name, p]) => ({
|
|
211
|
+
name, adapter: p.adapter, baseUrl: publicProviderBaseUrl(p.baseUrl), defaultModel: p.defaultModel,
|
|
212
|
+
hasApiKey: !!p.apiKey,
|
|
213
|
+
disabled: p.disabled === true,
|
|
214
|
+
})));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Add (or overwrite) a single provider. Merges into the live in-memory config and
|
|
218
|
+
// persists — existing providers' real keys are never round-tripped (unlike PUT /api/config,
|
|
219
|
+
// which would re-save the masked keys from GET). Live routing picks it up immediately.
|
|
220
|
+
if (url.pathname === "/api/providers" && req.method === "POST") {
|
|
221
|
+
let body: { name?: string; provider?: OcxProviderConfig; setDefault?: boolean };
|
|
222
|
+
try { body = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
223
|
+
const name = body.name?.trim();
|
|
224
|
+
const prov = body.provider ? stripCodexRuntimeProviderFields(body.provider) : undefined;
|
|
225
|
+
if (!name || !prov?.adapter || !prov?.baseUrl) {
|
|
226
|
+
return jsonResponse({ error: "name, provider.adapter and provider.baseUrl are required" }, 400);
|
|
227
|
+
}
|
|
228
|
+
if (!isValidProviderName(name)) {
|
|
229
|
+
return jsonResponse({ error: "provider name must use letters, numbers, dot, underscore, or hyphen and cannot be a reserved object key" }, 400);
|
|
230
|
+
}
|
|
231
|
+
const providerError = providerManagementConfigError(name, prov);
|
|
232
|
+
if (providerError) return jsonResponse({ error: providerError }, 400);
|
|
233
|
+
// Catalog providers (e.g. ollama-cloud) carry a models + vision/reasoning classification the GUI
|
|
234
|
+
// doesn't send — merge it in so the sidecars are gated correctly.
|
|
235
|
+
enrichProviderFromCatalog(name, prov);
|
|
236
|
+
const { saveConfig: save } = await import("../config");
|
|
237
|
+
// Overwriting an existing provider must not drop its multi-key pool: carry it over, then
|
|
238
|
+
// let the (possibly new) apiKey join the pool as the active entry.
|
|
239
|
+
const existingPool = config.providers[name]?.apiKeyPool;
|
|
240
|
+
if (existingPool && !prov.apiKeyPool) prov.apiKeyPool = existingPool;
|
|
241
|
+
config.providers[name] = prov;
|
|
242
|
+
if (body.setDefault) config.defaultProvider = name;
|
|
243
|
+
save(config);
|
|
244
|
+
if (prov.apiKey && prov.apiKeyPool) {
|
|
245
|
+
const { addProviderApiKey } = await import("../providers/api-keys");
|
|
246
|
+
addProviderApiKey(config, name, prov.apiKey);
|
|
247
|
+
}
|
|
248
|
+
const { clearModelCache } = await import("../codex/model-cache");
|
|
249
|
+
clearModelCache(name);
|
|
250
|
+
await refreshCodexCatalogBestEffort();
|
|
251
|
+
return jsonResponse({ success: true, name });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (url.pathname === "/api/providers" && req.method === "PATCH") {
|
|
255
|
+
const name = url.searchParams.get("name")?.trim();
|
|
256
|
+
if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
|
|
257
|
+
let body: { disabled?: unknown };
|
|
258
|
+
try { body = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
259
|
+
if (typeof body.disabled !== "boolean") return jsonResponse({ error: "disabled boolean is required" }, 400);
|
|
260
|
+
if (body.disabled && name === config.defaultProvider) {
|
|
261
|
+
return jsonResponse({ error: "cannot disable the default provider; set another default first" }, 400);
|
|
262
|
+
}
|
|
263
|
+
const { saveConfig: save } = await import("../config");
|
|
264
|
+
config.providers[name] = { ...config.providers[name], disabled: body.disabled };
|
|
265
|
+
save(config);
|
|
266
|
+
await refreshCodexCatalogBestEffort();
|
|
267
|
+
return jsonResponse({ success: true, name, disabled: body.disabled });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (url.pathname === "/api/providers" && req.method === "DELETE") {
|
|
271
|
+
const name = url.searchParams.get("name")?.trim();
|
|
272
|
+
if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
|
|
273
|
+
if (name === config.defaultProvider) return jsonResponse({ error: "cannot delete the default provider; set another default first" }, 400);
|
|
274
|
+
const { saveConfig: save } = await import("../config");
|
|
275
|
+
delete config.providers[name];
|
|
276
|
+
setProviderContextCap(config, name, false);
|
|
277
|
+
save(config);
|
|
278
|
+
const { clearModelCache: clearCache } = await import("../codex/model-cache");
|
|
279
|
+
clearCache(name);
|
|
280
|
+
await refreshCodexCatalogBestEffort();
|
|
281
|
+
return jsonResponse({ success: true });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (url.pathname === "/api/models" && req.method === "GET") {
|
|
285
|
+
const models = await fetchAllModels(config);
|
|
286
|
+
const disabled = new Set(config.disabledModels ?? []);
|
|
287
|
+
return jsonResponse(models.map(m => {
|
|
288
|
+
const namespaced = `${m.provider}/${m.id}`;
|
|
289
|
+
const contextCap = providerContextCap(config, m.provider);
|
|
290
|
+
return {
|
|
291
|
+
...m,
|
|
292
|
+
namespaced,
|
|
293
|
+
disabled: disabled.has(namespaced),
|
|
294
|
+
...(contextCap !== undefined ? { contextCap, contextCapped: m.contextCapped === true } : {}),
|
|
295
|
+
};
|
|
296
|
+
}));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (url.pathname === "/api/provider-context-caps" && req.method === "GET") {
|
|
300
|
+
return jsonResponse({ cap: DEFAULT_PROVIDER_CONTEXT_CAP, value: globalContextCapValue(config), caps: providerContextCaps(config) });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (url.pathname === "/api/provider-context-caps" && req.method === "PUT") {
|
|
304
|
+
let body: { provider?: unknown; enabled?: unknown; value?: unknown; setAll?: unknown };
|
|
305
|
+
try { body = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
306
|
+
const { saveConfig: save } = await import("../config");
|
|
307
|
+
const { clearModelCache } = await import("../codex/model-cache");
|
|
308
|
+
const respond = () => jsonResponse({ ok: true, cap: DEFAULT_PROVIDER_CONTEXT_CAP, value: globalContextCapValue(config), caps: providerContextCaps(config) });
|
|
309
|
+
|
|
310
|
+
// Branch 1: set the global cap value and re-point every enabled provider to it.
|
|
311
|
+
if (body.value !== undefined) {
|
|
312
|
+
if (typeof body.value !== "number" || !Number.isFinite(body.value) || body.value <= 0) {
|
|
313
|
+
return jsonResponse({ error: "value must be a positive number" }, 400);
|
|
314
|
+
}
|
|
315
|
+
const affected = Object.keys(providerContextCaps(config));
|
|
316
|
+
setGlobalContextCapValue(config, body.value);
|
|
317
|
+
save(config);
|
|
318
|
+
for (const provider of affected) clearModelCache(provider);
|
|
319
|
+
await refreshCodexCatalogBestEffort();
|
|
320
|
+
return respond();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Branch 2: enable/clear the cap for every provider at once.
|
|
324
|
+
if (body.setAll !== undefined) {
|
|
325
|
+
if (typeof body.setAll !== "boolean") {
|
|
326
|
+
return jsonResponse({ error: "setAll must be a boolean" }, 400);
|
|
327
|
+
}
|
|
328
|
+
const before = Object.keys(providerContextCaps(config));
|
|
329
|
+
const names = Object.keys(config.providers);
|
|
330
|
+
setAllProviderContextCaps(config, names, body.setAll);
|
|
331
|
+
save(config);
|
|
332
|
+
for (const provider of new Set([...before, ...names])) clearModelCache(provider);
|
|
333
|
+
await refreshCodexCatalogBestEffort();
|
|
334
|
+
return respond();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Branch 3: existing per-provider toggle (enable writes the current global value).
|
|
338
|
+
if (typeof body.provider !== "string" || typeof body.enabled !== "boolean") {
|
|
339
|
+
return jsonResponse({ error: "provider string and enabled boolean are required" }, 400);
|
|
340
|
+
}
|
|
341
|
+
const provider = body.provider.trim();
|
|
342
|
+
if (!isValidProviderName(provider)) {
|
|
343
|
+
return jsonResponse({ error: "provider name must use letters, numbers, dot, underscore, or hyphen and cannot be a reserved object key" }, 400);
|
|
344
|
+
}
|
|
345
|
+
if (!hasOwnProvider(config.providers, provider)) {
|
|
346
|
+
return jsonResponse({ error: "unknown provider" }, 404);
|
|
347
|
+
}
|
|
348
|
+
setProviderContextCap(config, provider, body.enabled);
|
|
349
|
+
save(config);
|
|
350
|
+
clearModelCache(provider);
|
|
351
|
+
await refreshCodexCatalogBestEffort();
|
|
352
|
+
return respond();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Enable/disable models: which routed models Codex sees. PUT hides them from the catalog +
|
|
356
|
+
// /v1/models and invalidates Codex's 5-min models cache so it applies on the next turn.
|
|
357
|
+
if (url.pathname === "/api/disabled-models" && req.method === "PUT") {
|
|
358
|
+
let body: { models?: unknown };
|
|
359
|
+
try { body = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
360
|
+
const disabled = Array.isArray(body.models) ? body.models.filter((m): m is string => typeof m === "string") : [];
|
|
361
|
+
config.disabledModels = disabled;
|
|
362
|
+
const { saveConfig: save } = await import("../config");
|
|
363
|
+
save(config);
|
|
364
|
+
await refreshCodexCatalogBestEffort();
|
|
365
|
+
return jsonResponse({ ok: true, disabled });
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Which providers support real OAuth login (drives the GUI's "Log in with …" buttons).
|
|
369
|
+
if (url.pathname === "/api/oauth/providers" && req.method === "GET") {
|
|
370
|
+
return jsonResponse({ providers: listOAuthProviders() });
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// API-key "login" providers (open dashboard → paste key). Drives the GUI's key-provider picker.
|
|
374
|
+
if (url.pathname === "/api/key-providers" && req.method === "GET") {
|
|
375
|
+
return jsonResponse({ providers: listKeyLoginProviders() });
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Complete GUI picker presets, derived from the canonical provider registry. The GUI is a
|
|
379
|
+
// standalone Vite package, so it consumes this runtime view instead of importing repo-root src.
|
|
380
|
+
if (url.pathname === "/api/provider-presets" && req.method === "GET") {
|
|
381
|
+
return jsonResponse({ providers: deriveProviderPresets() });
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Subagent model picker: which ≤5 routed models Codex's spawn_agent advertises (it shows the
|
|
385
|
+
// first 5 routed catalog entries). PUT reorders the injected catalog so the chosen ones lead.
|
|
386
|
+
if (url.pathname === "/api/subagent-models" && req.method === "GET") {
|
|
387
|
+
const models = await fetchAllModels(config);
|
|
388
|
+
const disabled = new Set(config.disabledModels ?? []);
|
|
389
|
+
// Native gpt (passthrough) are also valid subagent picks — they're picker-visible models in the
|
|
390
|
+
// catalog, just buried by priority. List them first so the user can feature them over routed.
|
|
391
|
+
const { listCatalogNativeSlugs } = await import("../codex/catalog");
|
|
392
|
+
const available = [
|
|
393
|
+
...listCatalogNativeSlugs(),
|
|
394
|
+
...models.map(m => `${m.provider}/${m.id}`),
|
|
395
|
+
].filter(ns => !disabled.has(ns));
|
|
396
|
+
return jsonResponse({ chosen: config.subagentModels ?? [], available });
|
|
397
|
+
}
|
|
398
|
+
if (url.pathname === "/api/subagent-models" && req.method === "PUT") {
|
|
399
|
+
let body: { models?: unknown };
|
|
400
|
+
try { body = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
401
|
+
const chosen = Array.isArray(body.models) ? body.models.filter((m): m is string => typeof m === "string").slice(0, 5) : [];
|
|
402
|
+
config.subagentModels = chosen;
|
|
403
|
+
const { saveConfig: save } = await import("../config");
|
|
404
|
+
save(config);
|
|
405
|
+
await refreshCodexCatalogBestEffort();
|
|
406
|
+
return jsonResponse({ ok: true, applied: chosen });
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Per-provider catalog allowlist (issue #52): when a provider has a non-empty selectedModels list,
|
|
410
|
+
// only those ids ship to Codex's catalog / /v1/models. GET returns the CURRENT selection plus the
|
|
411
|
+
// FULL available set per provider (unfiltered — the picker needs everything to choose from).
|
|
412
|
+
if (url.pathname === "/api/selected-models" && req.method === "GET") {
|
|
413
|
+
const models = await fetchAllModels(config);
|
|
414
|
+
const available: Record<string, string[]> = {};
|
|
415
|
+
for (const m of models) (available[m.provider] ??= []).push(m.id);
|
|
416
|
+
const selected: Record<string, string[]> = {};
|
|
417
|
+
for (const [name, prov] of Object.entries(config.providers)) {
|
|
418
|
+
if (Array.isArray(prov.selectedModels) && prov.selectedModels.length > 0) selected[name] = [...prov.selectedModels];
|
|
419
|
+
}
|
|
420
|
+
return jsonResponse({ selected, available });
|
|
421
|
+
}
|
|
422
|
+
if (url.pathname === "/api/selected-models" && req.method === "PUT") {
|
|
423
|
+
let body: { provider?: unknown; models?: unknown };
|
|
424
|
+
try { body = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
425
|
+
const provider = typeof body.provider === "string" ? body.provider : "";
|
|
426
|
+
if (!provider || !hasOwnProvider(config.providers, provider)) {
|
|
427
|
+
return jsonResponse({ error: "unknown provider" }, provider ? 404 : 400);
|
|
428
|
+
}
|
|
429
|
+
const models = Array.isArray(body.models)
|
|
430
|
+
? [...new Set(body.models.filter((m): m is string => typeof m === "string"))]
|
|
431
|
+
: [];
|
|
432
|
+
// Empty list clears the allowlist (provider reverts to exposing all models).
|
|
433
|
+
if (models.length > 0) config.providers[provider].selectedModels = models;
|
|
434
|
+
else delete config.providers[provider].selectedModels;
|
|
435
|
+
const { saveConfig: save } = await import("../config");
|
|
436
|
+
save(config);
|
|
437
|
+
await refreshCodexCatalogBestEffort();
|
|
438
|
+
return jsonResponse({ ok: true, provider, selected: models });
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// OAuth login (xai now; anthropic/kimi in cycle 2). Starts the flow and returns the auth URL;
|
|
442
|
+
// the provider's loopback callback server (inside this process) captures the redirect in the
|
|
443
|
+
// background, then the credential is persisted. The GUI opens the URL and polls /api/oauth/status.
|
|
444
|
+
if (url.pathname === "/api/oauth/login" && req.method === "POST") {
|
|
445
|
+
const body = await req.json().catch(() => ({})) as { provider?: string; addAccount?: boolean };
|
|
446
|
+
const provider = (body.provider ?? "").trim().toLowerCase();
|
|
447
|
+
if (!isOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
|
|
448
|
+
try {
|
|
449
|
+
// addAccount forces a fresh browser identity (skips local-CLI token import) so a
|
|
450
|
+
// SECOND account can be added instead of re-importing the first one.
|
|
451
|
+
const { url: authUrl, instructions } = await startLoginFlow(provider, body.addAccount ? { forceLogin: true } : undefined);
|
|
452
|
+
upsertOAuthProvider(config, provider); // mutate LIVE config — routing sees it without restart
|
|
453
|
+
if (authUrl) {
|
|
454
|
+
// Open the browser server-side (the proxy runs on the user's machine) — the GUI's
|
|
455
|
+
// window.open is popup-blocked because it runs after an await, not a direct click.
|
|
456
|
+
const { openUrl } = await import("../lib/open-url");
|
|
457
|
+
openUrl(authUrl);
|
|
458
|
+
}
|
|
459
|
+
return jsonResponse({ url: authUrl, instructions });
|
|
460
|
+
} catch (err) {
|
|
461
|
+
return jsonResponse({ error: err instanceof Error ? err.message : String(err) }, 409);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (url.pathname === "/api/oauth/status" && req.method === "GET") {
|
|
466
|
+
const provider = (url.searchParams.get("provider") ?? "").trim().toLowerCase();
|
|
467
|
+
return jsonResponse(getLoginStatus(provider));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (url.pathname === "/api/oauth/logout" && req.method === "POST") {
|
|
471
|
+
const provider = (url.searchParams.get("provider") ?? "").trim().toLowerCase();
|
|
472
|
+
if (!isOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
|
|
473
|
+
removeCredential(provider);
|
|
474
|
+
clearLoginState(provider);
|
|
475
|
+
return jsonResponse({ success: true });
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Multiauth account management: list a provider's logged-in accounts, switch the active
|
|
479
|
+
// one, or remove one. Emails are masked; tokens never leave the store.
|
|
480
|
+
if (url.pathname === "/api/oauth/accounts" && req.method === "GET") {
|
|
481
|
+
const provider = (url.searchParams.get("provider") ?? "").trim().toLowerCase();
|
|
482
|
+
if (!isOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
|
|
483
|
+
const status = getLoginStatus(provider);
|
|
484
|
+
return jsonResponse({ activeAccountId: status.activeAccountId ?? null, accounts: status.accounts ?? [] });
|
|
485
|
+
}
|
|
486
|
+
if (url.pathname === "/api/oauth/accounts/active" && req.method === "PUT") {
|
|
487
|
+
const body = await req.json().catch(() => ({})) as { provider?: string; accountId?: string };
|
|
488
|
+
const provider = (body.provider ?? "").trim().toLowerCase();
|
|
489
|
+
if (!isOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
|
|
490
|
+
if (!body.accountId) return jsonResponse({ error: "missing accountId" }, 400);
|
|
491
|
+
const { setActiveAccount } = await import("../oauth/store");
|
|
492
|
+
if (!setActiveAccount(provider, body.accountId)) return jsonResponse({ error: "account not found" }, 404);
|
|
493
|
+
const { clearProviderQuotaCache } = await import("../providers/quota");
|
|
494
|
+
clearProviderQuotaCache();
|
|
495
|
+
return jsonResponse({ ok: true, provider, activeAccountId: body.accountId });
|
|
496
|
+
}
|
|
497
|
+
if (url.pathname === "/api/oauth/accounts" && req.method === "DELETE") {
|
|
498
|
+
const provider = (url.searchParams.get("provider") ?? "").trim().toLowerCase();
|
|
499
|
+
const id = url.searchParams.get("id") ?? "";
|
|
500
|
+
if (!isOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
|
|
501
|
+
if (!id) return jsonResponse({ error: "missing id" }, 400);
|
|
502
|
+
const { removeAccount, getAccountSet } = await import("../oauth/store");
|
|
503
|
+
if (!removeAccount(provider, id)) return jsonResponse({ error: "account not found" }, 404);
|
|
504
|
+
if (!getAccountSet(provider)) clearLoginState(provider);
|
|
505
|
+
const { clearProviderQuotaCache } = await import("../providers/quota");
|
|
506
|
+
clearProviderQuotaCache();
|
|
507
|
+
return jsonResponse({ ok: true });
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Multi-key pool for API-key providers (same GUI dropdown as OAuth multiauth): list masked
|
|
511
|
+
// keys, add one (upserts + activates), switch the active key, or remove one. `apiKey` always
|
|
512
|
+
// mirrors the active entry so routing is untouched.
|
|
513
|
+
if (url.pathname === "/api/providers/keys" && req.method === "GET") {
|
|
514
|
+
const name = (url.searchParams.get("name") ?? "").trim();
|
|
515
|
+
if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
|
|
516
|
+
const { listProviderApiKeys } = await import("../providers/api-keys");
|
|
517
|
+
return jsonResponse(listProviderApiKeys(config, name));
|
|
518
|
+
}
|
|
519
|
+
if (url.pathname === "/api/providers/keys" && req.method === "POST") {
|
|
520
|
+
const body = await req.json().catch(() => ({})) as { name?: string; key?: string; label?: string };
|
|
521
|
+
const name = (body.name ?? "").trim();
|
|
522
|
+
if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
|
|
523
|
+
if (typeof body.key !== "string" || !body.key.trim()) return jsonResponse({ error: "key is required" }, 400);
|
|
524
|
+
const { addProviderApiKey } = await import("../providers/api-keys");
|
|
525
|
+
const result = addProviderApiKey(config, name, body.key, body.label);
|
|
526
|
+
if ("error" in result) return jsonResponse({ error: result.error }, 400);
|
|
527
|
+
const { clearModelCache } = await import("../codex/model-cache");
|
|
528
|
+
clearModelCache(name);
|
|
529
|
+
const { clearProviderQuotaCache } = await import("../providers/quota");
|
|
530
|
+
clearProviderQuotaCache();
|
|
531
|
+
const { clearKeyCooldowns } = await import("../providers/key-failover");
|
|
532
|
+
clearKeyCooldowns(name); // manual key management resets 429 cooldown state
|
|
533
|
+
return jsonResponse({ ok: true, id: result.id }, 201);
|
|
534
|
+
}
|
|
535
|
+
if (url.pathname === "/api/providers/keys/active" && req.method === "PUT") {
|
|
536
|
+
const body = await req.json().catch(() => ({})) as { name?: string; id?: string };
|
|
537
|
+
const name = (body.name ?? "").trim();
|
|
538
|
+
if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
|
|
539
|
+
if (!body.id) return jsonResponse({ error: "missing id" }, 400);
|
|
540
|
+
const { setActiveProviderApiKey } = await import("../providers/api-keys");
|
|
541
|
+
if (!setActiveProviderApiKey(config, name, body.id)) return jsonResponse({ error: "key not found" }, 404);
|
|
542
|
+
const { clearModelCache } = await import("../codex/model-cache");
|
|
543
|
+
clearModelCache(name);
|
|
544
|
+
const { clearProviderQuotaCache } = await import("../providers/quota");
|
|
545
|
+
clearProviderQuotaCache();
|
|
546
|
+
const { clearKeyCooldowns } = await import("../providers/key-failover");
|
|
547
|
+
clearKeyCooldowns(name); // manual key management resets 429 cooldown state
|
|
548
|
+
return jsonResponse({ ok: true, name, activeId: body.id });
|
|
549
|
+
}
|
|
550
|
+
if (url.pathname === "/api/providers/keys" && req.method === "DELETE") {
|
|
551
|
+
const name = (url.searchParams.get("name") ?? "").trim();
|
|
552
|
+
const id = url.searchParams.get("id") ?? "";
|
|
553
|
+
if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
|
|
554
|
+
if (!id) return jsonResponse({ error: "missing id" }, 400);
|
|
555
|
+
const { removeProviderApiKey } = await import("../providers/api-keys");
|
|
556
|
+
if (!removeProviderApiKey(config, name, id)) return jsonResponse({ error: "key not found" }, 404);
|
|
557
|
+
const { clearModelCache } = await import("../codex/model-cache");
|
|
558
|
+
clearModelCache(name);
|
|
559
|
+
const { clearProviderQuotaCache } = await import("../providers/quota");
|
|
560
|
+
clearProviderQuotaCache();
|
|
561
|
+
const { clearKeyCooldowns } = await import("../providers/key-failover");
|
|
562
|
+
clearKeyCooldowns(name); // manual key management resets 429 cooldown state
|
|
563
|
+
return jsonResponse({ ok: true });
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// ---------------------------------------------------------------------------
|
|
567
|
+
// API Keys management
|
|
568
|
+
// ---------------------------------------------------------------------------
|
|
569
|
+
if (url.pathname === "/api/keys" && req.method === "GET") {
|
|
570
|
+
const keys = config.apiKeys ?? [];
|
|
571
|
+
return jsonResponse({ keys: keys.map(k => ({ id: k.id, name: k.name, prefix: k.key.slice(0, 8) + "...", createdAt: k.createdAt })), endpoint: `http://${config.hostname ?? "127.0.0.1"}:${config.port ?? 10100}/v1/responses` }, 200, req, config);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (url.pathname === "/api/keys" && req.method === "POST") {
|
|
575
|
+
const body = await req.json() as { name?: string };
|
|
576
|
+
const name = (body.name ?? "").trim() || "default";
|
|
577
|
+
// Generate key from provider keys hash + random salt
|
|
578
|
+
const providerKeys = Object.values(config.providers).map(p => p.apiKey ?? "").filter(Boolean).join("|");
|
|
579
|
+
const salt = crypto.randomUUID();
|
|
580
|
+
const hashInput = `${providerKeys}|${salt}|${Date.now()}`;
|
|
581
|
+
const hashBuf = new Bun.CryptoHasher("sha256").update(hashInput).digest();
|
|
582
|
+
const key = "ocx_" + Buffer.from(hashBuf).toString("hex").slice(0, 40);
|
|
583
|
+
const entry = { id: crypto.randomUUID(), name, key, createdAt: new Date().toISOString() };
|
|
584
|
+
config.apiKeys = [...(config.apiKeys ?? []), entry];
|
|
585
|
+
saveConfig(config);
|
|
586
|
+
return jsonResponse({ id: entry.id, name: entry.name, key: entry.key, createdAt: entry.createdAt }, 201, req, config);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (url.pathname === "/api/keys" && req.method === "DELETE") {
|
|
590
|
+
const body = await req.json() as { id?: string };
|
|
591
|
+
if (!body.id) return jsonResponse({ error: "id required" }, 400, req, config);
|
|
592
|
+
config.apiKeys = (config.apiKeys ?? []).filter(k => k.id !== body.id);
|
|
593
|
+
saveConfig(config);
|
|
594
|
+
return jsonResponse({ success: true }, 200, req, config);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (url.pathname === "/api/stop" && req.method === "POST") {
|
|
598
|
+
const { restoreNativeCodex } = await import("../codex/inject");
|
|
599
|
+
const { stopServiceIfInstalled } = await import("../service");
|
|
600
|
+
stopServiceIfInstalled();
|
|
601
|
+
const restore = restoreNativeCodex();
|
|
602
|
+
setTimeout(async () => {
|
|
603
|
+
await drainAndShutdown(undefined, config.shutdownTimeoutMs ?? 5000);
|
|
604
|
+
process.exit(0);
|
|
605
|
+
}, 200);
|
|
606
|
+
return jsonResponse(restore.success
|
|
607
|
+
? { success: true, message: "Proxy stopping, native Codex restored." }
|
|
608
|
+
: { success: false, message: `Proxy stopping, but native Codex restore failed: ${restore.message}. Run \`ocx restore\`.` });
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (url.pathname.startsWith("/api/codex-auth/")) {
|
|
612
|
+
const { handleCodexAuthAPI } = await import("../codex/auth-api");
|
|
613
|
+
return handleCodexAuthAPI(req, url, config);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Live routed-provider models for the proxy's /api/* and /v1/models endpoints. Delegates to the
|
|
621
|
+
* canonical, TTL-cached `gatherRoutedModels` (single source of truth) — so the GUI/codex endpoints
|
|
622
|
+
* share the same fetch, the same per-provider cache (dedups Codex's frequent /v1/models polling),
|
|
623
|
+
* and the same stale fallback when a provider blips, instead of a parallel uncached copy.
|
|
624
|
+
*/
|
|
625
|
+
export async function fetchAllModels(config: OcxConfig): Promise<CatalogModel[]> {
|
|
626
|
+
const { gatherRoutedModels } = await import("../codex/catalog");
|
|
627
|
+
return gatherRoutedModels(config);
|
|
628
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Lives outside cli.ts (which dispatches argv at module top level) so tests can import it.
|
|
11
11
|
*/
|
|
12
|
-
import { loadConfig, readPid, readRuntimePort } from "
|
|
12
|
+
import { loadConfig, readPid, readRuntimePort } from "../config";
|
|
13
13
|
|
|
14
14
|
export interface HealthzIdentity {
|
|
15
15
|
service?: unknown;
|