@yansigit/opencodex 2.31.3 → 2.33.0
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 +2 -2
- package/bin/ocx.mjs +99 -70
- package/gui/dist/assets/index-DKLr4LTE.js +102 -0
- package/gui/dist/assets/index-DrSQdTRd.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +6 -5
- package/src/adapters/anthropic.ts +25 -13
- package/src/adapters/azure.ts +20 -4
- package/src/adapters/base.ts +5 -1
- package/src/adapters/command-code.ts +42 -10
- package/src/adapters/cursor/live-models.ts +8 -0
- package/src/adapters/cursor/live-transport.ts +1 -1
- package/src/adapters/cursor/native-exec-desktop.ts +16 -0
- package/src/adapters/cursor/protobuf-events.ts +158 -7
- package/src/adapters/cursor/protobuf-request.ts +33 -15
- package/src/adapters/cursor/request-builder.ts +4 -3
- package/src/adapters/cursor/tool-definitions.ts +27 -1
- package/src/adapters/cursor/types.ts +4 -3
- package/src/adapters/cursor.ts +9 -0
- package/src/adapters/google-antigravity-replay.ts +2 -2
- package/src/adapters/google-antigravity-wire.ts +7 -0
- package/src/adapters/google-errors.ts +6 -2
- package/src/adapters/google-http.ts +30 -7
- package/src/adapters/google-truncation.ts +5 -0
- package/src/adapters/google-wire-compiler.ts +38 -6
- package/src/adapters/google.ts +154 -33
- package/src/adapters/kiro-tools.ts +20 -9
- package/src/adapters/kiro.ts +0 -3
- package/src/adapters/openai-chat.ts +9 -0
- package/src/adapters/openai-responses.ts +4 -1
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-web-search.ts +7 -2
- package/src/bridge.ts +133 -24
- package/src/claude/context-windows.ts +16 -9
- package/src/cli/dispatch.ts +50 -2
- package/src/cli/doctor.ts +26 -13
- package/src/cli/help.ts +4 -3
- package/src/cli/index.ts +20 -6
- package/src/cli/models.ts +13 -3
- package/src/cli/observe.ts +20 -5
- package/src/cli/provider.ts +8 -1
- package/src/cli/registry.ts +7 -5
- package/src/cli/status.ts +25 -1
- package/src/cli/system-restart-client.ts +1 -1
- package/src/cli/usage-report.ts +134 -0
- package/src/codex/app-server-processes.ts +3 -1
- package/src/codex/auth-api.ts +4 -2
- package/src/codex/autostart-health.ts +16 -0
- package/src/codex/catalog/aggregation.ts +12 -0
- package/src/codex/catalog/effort.ts +42 -12
- package/src/codex/catalog/metadata.ts +27 -1
- package/src/codex/catalog/model-metadata.ts +593 -0
- package/src/codex/catalog/parsing.ts +71 -27
- package/src/codex/catalog/provider-fetch.ts +189 -33
- package/src/codex/catalog/sync.ts +6 -5
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desktop-app-restart.ts +342 -0
- package/src/codex/history-job.ts +32 -3
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +5 -5
- package/src/codex/history-provider.ts +825 -247
- package/src/codex/history-worker.ts +8 -5
- package/src/codex/inject.ts +49 -21
- package/src/codex/injected-marker.ts +1 -1
- package/src/codex/internal/history-writer.ts +4 -3
- package/src/codex/native-profile-startup.ts +157 -27
- package/src/codex/native-residue.ts +26 -33
- package/src/codex/shim.ts +56 -3
- package/src/combos/failover.ts +27 -0
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +81 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +308 -0
- package/src/config/provider-validation.ts +214 -0
- package/src/config.ts +153 -814
- package/src/generated/compatibility-version.json +232 -148
- package/src/images/loop.ts +37 -6
- package/src/images/plan.ts +5 -4
- package/src/integrations/ownership-policy.ts +141 -0
- package/src/integrations/ownership.ts +10 -0
- package/src/integrations/state.ts +44 -5
- package/src/integrations/writer.ts +6 -0
- package/src/lib/azure-identity.ts +154 -0
- package/src/lib/bounded-body.ts +14 -2
- package/src/lib/debug.ts +42 -0
- package/src/lib/errors.ts +14 -0
- package/src/lib/process-control.ts +2 -1
- package/src/lib/provider-outbound.ts +45 -33
- package/src/lib/provider-tls-profile.ts +309 -0
- package/src/lib/proxy-env.ts +49 -0
- package/src/lib/redact.ts +10 -1
- package/src/lib/state-store-registrations.ts +2 -0
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/antigravity-routing.ts +282 -236
- package/src/oauth/callback-server.ts +22 -2
- package/src/oauth/command-code.ts +5 -16
- package/src/oauth/google-antigravity.ts +42 -5
- package/src/oauth/health.ts +1 -1
- package/src/oauth/index.ts +15 -3
- package/src/oauth/kimi.ts +9 -1
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +6 -0
- package/src/providers/antigravity-quota.ts +3 -1
- package/src/providers/api-keys.ts +2 -1
- package/src/providers/auto-compact-budget.ts +65 -0
- package/src/providers/derive.ts +4 -0
- package/src/providers/key-failover.ts +5 -1
- package/src/providers/openai-tiers.ts +5 -0
- package/src/providers/provider-id-rewrite.ts +1 -0
- package/src/providers/quota.ts +59 -13
- package/src/providers/registry.ts +4 -2
- package/src/providers/request-pacing.ts +33 -6
- package/src/providers/xai-transport.ts +21 -0
- package/src/reasoning-effort.ts +19 -2
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/custom-tool-compat.ts +132 -38
- package/src/responses/google-provider-options.ts +36 -0
- package/src/responses/namespace-tool-compat.ts +84 -4
- package/src/responses/parser.ts +14 -2
- package/src/responses/provider-opaque-metadata.ts +3 -3
- package/src/responses/reasoning-replay-cache.ts +81 -3
- package/src/responses/schema.ts +37 -0
- package/src/responses/state.ts +94 -4
- package/src/router.ts +8 -2
- package/src/server/auth-cors.ts +37 -7
- package/src/server/images.ts +19 -35
- package/src/server/index.ts +102 -21
- package/src/server/local-management-read-client.ts +1 -1
- package/src/server/local-provider-reload-client.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +206 -16
- package/src/server/management/combo-routes.ts +6 -0
- package/src/server/management/config-routes.ts +35 -6
- package/src/server/management/context.ts +1 -1
- package/src/server/management/logs-usage-routes.ts +27 -6
- package/src/server/management/model-routes.ts +8 -4
- package/src/server/management/model-rows.ts +4 -0
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/oauth-account-routes.ts +25 -4
- package/src/server/management/provider-capability-config.ts +1 -1
- package/src/server/management/provider-routes.ts +113 -15
- package/src/server/management/routing-profile-routes.ts +3 -0
- package/src/server/management/system-restart.ts +1 -1
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/proxy-liveness.ts +2 -1
- package/src/server/request-log-conversation.ts +30 -0
- package/src/server/request-log.ts +21 -0
- package/src/server/responses/agent-task-recovery.ts +1 -1
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/combo-stream-preflight.ts +171 -0
- package/src/server/responses/compact.ts +36 -22
- package/src/server/responses/core.ts +584 -247
- package/src/server/responses/empty-completion-guard.ts +35 -6
- package/src/server/responses/fetch-helpers.ts +20 -102
- package/src/server/responses/v2-native-parent-override.ts +59 -0
- package/src/server/responses/ws-upstream.ts +75 -2
- package/src/server/responses-custom-tool-repair.ts +41 -5
- package/src/server/responses-undeclared-tool-guard.ts +241 -18
- package/src/service.ts +9 -5
- package/src/types/config.ts +16 -1
- package/src/types/provider.ts +16 -0
- package/src/types/request.ts +34 -1
- package/src/types/tools.ts +114 -11
- package/src/types.ts +7 -1
- package/src/update/index.ts +5 -4
- package/src/update/job.ts +3 -1
- package/src/update/transactional-install.mjs +8 -1
- package/src/usage/log.ts +16 -8
- package/src/usage/summary.ts +201 -8
- package/src/vision/describe.ts +18 -13
- package/src/web-search/executor.ts +10 -3
- package/src/web-search/gemini-executor.ts +6 -4
- package/src/web-search/loop.ts +42 -6
- package/gui/dist/assets/index-CGoDO3uO.css +0 -1
- package/gui/dist/assets/index-Cxt5fZMP.js +0 -102
|
@@ -55,7 +55,7 @@ import {
|
|
|
55
55
|
type PersistedUsageEntry,
|
|
56
56
|
} from "../../usage/log";
|
|
57
57
|
import { getUsageDebugLogEntries } from "../../usage/debug";
|
|
58
|
-
import { parseRange, parseUsageSurface, rangeWindow, summarizeUsage, type UsageRange, type UsageSummary, type UsageSurface } from "../../usage/summary";
|
|
58
|
+
import { USAGE_RANGES, USAGE_SURFACES, parseRange, parseUsageSurface, projectUsageSummary, rangeWindow, summarizeUsage, type UsageRange, type UsageSummary, type UsageSurface } from "../../usage/summary";
|
|
59
59
|
import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
|
|
60
60
|
import { getProviderRegistryEntry } from "../../providers/registry";
|
|
61
61
|
import { getDebugLogEntries } from "../../lib/debug-log-buffer";
|
|
@@ -197,6 +197,17 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
197
197
|
if (url.pathname === "/api/usage" && req.method === "GET") {
|
|
198
198
|
const range = parseRange(url.searchParams.get("range"));
|
|
199
199
|
const surface = parseUsageSurface(url.searchParams.get("surface"));
|
|
200
|
+
// Applied to the OUTGOING payload only. A filtered summary must never reach
|
|
201
|
+
// the cache or the warm loop below: the key is `range:surface`, so a
|
|
202
|
+
// filtered entry stored under it would be served to the next unfiltered
|
|
203
|
+
// caller, dashboard included.
|
|
204
|
+
const filter = {
|
|
205
|
+
provider: url.searchParams.get("provider"),
|
|
206
|
+
model: url.searchParams.get("model"),
|
|
207
|
+
};
|
|
208
|
+
const project = <T extends UsageSummary>(summary: T, entries?: PersistedUsageEntry[]) =>
|
|
209
|
+
projectUsageSummary(summary, filter, entries);
|
|
210
|
+
const filterRequested = Boolean(filter.provider ?? filter.model);
|
|
200
211
|
const now = Date.now();
|
|
201
212
|
try {
|
|
202
213
|
const cacheKey = `${range}:${surface}`;
|
|
@@ -206,6 +217,12 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
206
217
|
const observedSize = observed?.size ?? 0;
|
|
207
218
|
const cached = getUsageSummaryCacheEntry(cacheKey);
|
|
208
219
|
if (cached
|
|
220
|
+
// A filtered response is re-summarised from entries, which a cached
|
|
221
|
+
// summary does not carry. Serving it from cache would mean projecting
|
|
222
|
+
// over collapsed breakdown rows again — the exact defect the
|
|
223
|
+
// re-summarisation replaced. The unfiltered cache stays warm either
|
|
224
|
+
// way; only the filtered caller pays for the read.
|
|
225
|
+
&& !filterRequested
|
|
209
226
|
&& cached.identityKey === identityKey
|
|
210
227
|
&& cached.maxReadBytes === effectiveReadLimit
|
|
211
228
|
&& cached.overlayVersion === userCostOverlayVersion()
|
|
@@ -214,7 +231,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
214
231
|
&& observedSize >= cached.lastSeenSize) {
|
|
215
232
|
return jsonResponse(refreshedUsageSummary(cached.summary, range, now));
|
|
216
233
|
}
|
|
217
|
-
if (cached) discardUsageSummaryCacheEntry(cacheKey);
|
|
234
|
+
if (cached && !filterRequested) discardUsageSummaryCacheEntry(cacheKey);
|
|
218
235
|
// Capture the overlay version BEFORE reading/computing: the cache entry
|
|
219
236
|
// must be stamped with the version the summary was priced under. Reading
|
|
220
237
|
// it again at stamp time could cache an old-price summary as current,
|
|
@@ -238,14 +255,18 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
238
255
|
// summary may mix old and new prices. Serve it uncached: the next
|
|
239
256
|
// request recomputes against the settled overlay instead of caching a
|
|
240
257
|
// mixed-price entry under either version.
|
|
241
|
-
return jsonResponse(summary);
|
|
258
|
+
return jsonResponse(project(summary, snapshot.entries));
|
|
242
259
|
}
|
|
243
260
|
const freshUntil = now + 60_000;
|
|
244
261
|
const snapshotIdentity = `${usageLogIdentityKey(snapshot.revision)}\0${effectiveReadLimit}`;
|
|
245
262
|
const revisionKey = `${usageLogRevisionKey(snapshot.revision)}\0${effectiveReadLimit}`;
|
|
246
263
|
const lastSeenSize = snapshot.revision?.size ?? 0;
|
|
247
|
-
|
|
248
|
-
|
|
264
|
+
// Derived from the canonical constants rather than re-listed: a subset
|
|
265
|
+
// literal type-checks perfectly happily, so a range added to the union
|
|
266
|
+
// and forgotten here would never be warmed and never invalidated
|
|
267
|
+
// alongside its siblings.
|
|
268
|
+
const ranges: readonly UsageRange[] = USAGE_RANGES;
|
|
269
|
+
const surfaces: readonly UsageSurface[] = USAGE_SURFACES;
|
|
249
270
|
for (const nextRange of ranges) {
|
|
250
271
|
for (const nextSurface of surfaces) {
|
|
251
272
|
const nextSummary = nextRange === range && nextSurface === surface ? summary : {
|
|
@@ -279,7 +300,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
279
300
|
effectiveReadLimit,
|
|
280
301
|
now,
|
|
281
302
|
);
|
|
282
|
-
return jsonResponse(summary);
|
|
303
|
+
return jsonResponse(project(summary, snapshot.entries));
|
|
283
304
|
} catch {
|
|
284
305
|
return jsonResponse({
|
|
285
306
|
range,
|
|
@@ -394,8 +394,10 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
394
394
|
}
|
|
395
395
|
|
|
396
396
|
if (url.pathname === "/api/custom-models" && req.method === "POST") {
|
|
397
|
-
let
|
|
398
|
-
try {
|
|
397
|
+
let parsedBody: unknown;
|
|
398
|
+
try { parsedBody = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
399
|
+
if (!isPlainRecord(parsedBody)) return jsonResponse({ error: "invalid JSON body" }, 400);
|
|
400
|
+
const body = parsedBody;
|
|
399
401
|
const provider = typeof body.provider === "string" ? body.provider.trim() : "";
|
|
400
402
|
const modelId = typeof body.modelId === "string" ? body.modelId.trim() : "";
|
|
401
403
|
if (!provider || !modelId) return jsonResponse({ error: "provider and modelId are required" }, 400);
|
|
@@ -441,8 +443,10 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
441
443
|
if (customPutMatch && req.method === "PUT") {
|
|
442
444
|
let id: string;
|
|
443
445
|
try { id = decodeURIComponent(customPutMatch[1]); } catch { return jsonResponse({ error: "invalid id encoding" }, 400); }
|
|
444
|
-
let
|
|
445
|
-
try {
|
|
446
|
+
let parsedBody: unknown;
|
|
447
|
+
try { parsedBody = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
448
|
+
if (!isPlainRecord(parsedBody)) return jsonResponse({ error: "invalid JSON body" }, 400);
|
|
449
|
+
const body = parsedBody;
|
|
446
450
|
const list = config.customModels ?? [];
|
|
447
451
|
const idx = list.findIndex(cm => cm.id === id);
|
|
448
452
|
if (idx === -1) return jsonResponse({ error: "not found" }, 404);
|
|
@@ -63,6 +63,7 @@ export async function listManagementModelRows(config: OcxConfig): Promise<Manage
|
|
|
63
63
|
disabled: disabled.has(`${selector}/${slug}`) || disabled.has(slug),
|
|
64
64
|
contextWindow: undefined,
|
|
65
65
|
maxInputTokens: undefined,
|
|
66
|
+
autoCompactTokenLimit: undefined,
|
|
66
67
|
})))
|
|
67
68
|
: [];
|
|
68
69
|
const native: ManagementModelRow[] = [...nativeRows, ...accountNativeRows].map(row => {
|
|
@@ -82,6 +83,9 @@ export async function listManagementModelRows(config: OcxConfig): Promise<Manage
|
|
|
82
83
|
// 1.05M). Dropping it here made /api/models describe a native row as if the whole
|
|
83
84
|
// window were usable as input, which is the claim the measurement disproved.
|
|
84
85
|
...(row.maxInputTokens !== undefined ? { maxInputTokens: row.maxInputTokens } : {}),
|
|
86
|
+
...(row.autoCompactTokenLimit !== undefined
|
|
87
|
+
? { autoCompactTokenLimit: row.autoCompactTokenLimit }
|
|
88
|
+
: {}),
|
|
85
89
|
};
|
|
86
90
|
});
|
|
87
91
|
const customModels: ManagementModelRow[] = (config.customModels ?? []).map(cm => {
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
* Design of record: devlog/_fin/260803_integrations_toggle_all/030 (routes),
|
|
18
18
|
* 011 (Claude Code), 012 (Grok).
|
|
19
19
|
*/
|
|
20
|
-
import { loadConfig,
|
|
20
|
+
import { loadConfig, saveConfigPreservingClaudeCode } from "../../config";
|
|
21
|
+
import { readRuntimePort } from "../../config/process-state";
|
|
21
22
|
import { desktopVisibleNativeSlugs, filterCatalogVisibleModels, nativeContextLimits, nativeOpenAiContextWindow, visibleNativeSlugs } from "../../codex/catalog";
|
|
22
23
|
import { providerContextCap } from "../../providers/context-cap";
|
|
23
24
|
import { OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
@@ -64,6 +64,7 @@ import type { PersistedUsageAttempt } from "../../usage/log";
|
|
|
64
64
|
import { AUTH_MATRIX, isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
|
|
65
65
|
import { applySystemEnvToggle } from "../system-env";
|
|
66
66
|
import { buildApiAccessEndpoints } from "./api-access";
|
|
67
|
+
import { isAzureIdentityProvider } from "../../config/provider-validation";
|
|
67
68
|
|
|
68
69
|
import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
|
|
69
70
|
import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
|
|
@@ -136,7 +137,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
136
137
|
// the provider's loopback callback server (inside this process) captures the redirect in the
|
|
137
138
|
// background, then the credential is persisted. The GUI opens the URL and polls /api/oauth/status.
|
|
138
139
|
if (url.pathname === "/api/oauth/login" && req.method === "POST") {
|
|
139
|
-
const body = await readManagementJsonBodyOr(req, {}) as { provider?: string; addAccount?: boolean; accountId?: string; reauth?: boolean };
|
|
140
|
+
const body = await readManagementJsonBodyOr(req, {}) as { provider?: string; addAccount?: boolean; accountId?: string; reauth?: boolean; openBrowser?: unknown };
|
|
140
141
|
const provider = (body.provider ?? "").trim().toLowerCase();
|
|
141
142
|
if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
|
|
142
143
|
const namespaceCollision = codexAccountNamespaceProviderCollisionError(config.codexAccountNamespaces, provider);
|
|
@@ -167,9 +168,15 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
167
168
|
reconcileLiveStateStores();
|
|
168
169
|
},
|
|
169
170
|
});
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
// Open the browser server-side (the proxy runs on the user's machine) — the GUI's
|
|
172
|
+
// window.open is popup-blocked because it runs after an await, not a direct click.
|
|
173
|
+
//
|
|
174
|
+
// The operator can decline, which is the only way to finish a login in a
|
|
175
|
+
// browser profile other than the OS default, or on a different machine
|
|
176
|
+
// than the proxy. Declining changes nothing else: the URL is still
|
|
177
|
+
// returned below and every login surface renders it with a copy button.
|
|
178
|
+
const { shouldOpenBrowserForLogin } = await import("../../oauth/open-browser-choice");
|
|
179
|
+
if (authUrl && !deviceCode && shouldOpenBrowserForLogin(body.openBrowser, config)) {
|
|
173
180
|
const { openUrl } = await import("../../lib/open-url");
|
|
174
181
|
openUrl(authUrl);
|
|
175
182
|
}
|
|
@@ -300,6 +307,10 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
300
307
|
const { resetAnthropicRoutingForManualSelection } = await import("../../oauth/anthropic-routing");
|
|
301
308
|
resetAnthropicRoutingForManualSelection(body.accountId);
|
|
302
309
|
}
|
|
310
|
+
if (provider === "google-antigravity") {
|
|
311
|
+
const { resetAntigravityRoutingForManualSelection } = await import("../../oauth/antigravity-routing");
|
|
312
|
+
resetAntigravityRoutingForManualSelection(body.accountId);
|
|
313
|
+
}
|
|
303
314
|
const { clearModelCache } = await import("../../codex/model-cache");
|
|
304
315
|
const { clearGatherRoutedModelsInflight } = await import("../../codex/catalog");
|
|
305
316
|
clearModelCache(provider);
|
|
@@ -510,6 +521,11 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
510
521
|
clearAnthropicAccountCooldown(id);
|
|
511
522
|
clearAnthropicSessionAffinityForAccount(id);
|
|
512
523
|
}
|
|
524
|
+
if (provider === "google-antigravity") {
|
|
525
|
+
const { clearAntigravityAccountCooldown, clearAntigravitySessionAffinityForAccount } = await import("../../oauth/antigravity-routing");
|
|
526
|
+
clearAntigravityAccountCooldown(id);
|
|
527
|
+
clearAntigravitySessionAffinityForAccount(id);
|
|
528
|
+
}
|
|
513
529
|
if (!getAccountSet(provider)) clearLoginState(provider);
|
|
514
530
|
const { clearModelCache } = await import("../../codex/model-cache");
|
|
515
531
|
const { clearGatherRoutedModelsInflight } = await import("../../codex/catalog");
|
|
@@ -527,6 +543,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
527
543
|
if (url.pathname === "/api/providers/keys" && req.method === "GET") {
|
|
528
544
|
const name = (url.searchParams.get("name") ?? "").trim();
|
|
529
545
|
if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
|
|
546
|
+
if (isAzureIdentityProvider(config.providers[name]!)) return jsonResponse({ error: "provider does not use API-key auth" }, 400);
|
|
530
547
|
const { listProviderApiKeys } = await import("../../providers/api-keys");
|
|
531
548
|
return jsonResponse(listProviderApiKeys(config, name));
|
|
532
549
|
}
|
|
@@ -534,6 +551,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
534
551
|
const body = await readManagementJsonBodyOr(req, {}) as { name?: string; key?: string; label?: string };
|
|
535
552
|
const name = (body.name ?? "").trim();
|
|
536
553
|
if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
|
|
554
|
+
if (isAzureIdentityProvider(config.providers[name]!)) return jsonResponse({ error: "provider does not use API-key auth" }, 400);
|
|
537
555
|
if (typeof body.key !== "string" || !body.key.trim()) return jsonResponse({ error: "key is required" }, 400);
|
|
538
556
|
const { addProviderApiKey } = await import("../../providers/api-keys");
|
|
539
557
|
const result = addProviderApiKey(config, name, body.key, body.label);
|
|
@@ -550,6 +568,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
550
568
|
const body = await readManagementJsonBodyOr(req, {}) as { name?: string; id?: string };
|
|
551
569
|
const name = (body.name ?? "").trim();
|
|
552
570
|
if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
|
|
571
|
+
if (isAzureIdentityProvider(config.providers[name]!)) return jsonResponse({ error: "provider does not use API-key auth" }, 400);
|
|
553
572
|
if (!body.id) return jsonResponse({ error: "missing id" }, 400);
|
|
554
573
|
const { setActiveProviderApiKey } = await import("../../providers/api-keys");
|
|
555
574
|
if (!setActiveProviderApiKey(config, name, body.id)) return jsonResponse({ error: "key not found" }, 404);
|
|
@@ -567,6 +586,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
567
586
|
const id = typeof body.id === "string" ? body.id.trim() : "";
|
|
568
587
|
const alias = typeof body.alias === "string" ? body.alias.trim() : "";
|
|
569
588
|
if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
|
|
589
|
+
if (isAzureIdentityProvider(config.providers[name]!)) return jsonResponse({ error: "provider does not use API-key auth" }, 400);
|
|
570
590
|
if (!id) return jsonResponse({ error: "missing id" }, 400);
|
|
571
591
|
if (typeof body.alias !== "string" || alias.length > 80 || /[\x00-\x1f\x7f]/.test(alias)) {
|
|
572
592
|
return jsonResponse({ error: "alias must be at most 80 printable characters" }, 400);
|
|
@@ -579,6 +599,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
579
599
|
const name = (url.searchParams.get("name") ?? "").trim();
|
|
580
600
|
const id = url.searchParams.get("id") ?? "";
|
|
581
601
|
if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
|
|
602
|
+
if (isAzureIdentityProvider(config.providers[name]!)) return jsonResponse({ error: "provider does not use API-key auth" }, 400);
|
|
582
603
|
if (!id) return jsonResponse({ error: "missing id" }, 400);
|
|
583
604
|
const { removeProviderApiKey } = await import("../../providers/api-keys");
|
|
584
605
|
if (!removeProviderApiKey(config, name, id)) return jsonResponse({ error: "key not found" }, 404);
|
|
@@ -31,12 +31,17 @@ import {
|
|
|
31
31
|
} from "../../oauth";
|
|
32
32
|
import { replaceProviderAccountSet } from "../../oauth/store";
|
|
33
33
|
import { providerDestinationResolvedError } from "../../lib/destination-policy";
|
|
34
|
+
import {
|
|
35
|
+
antigravityOAuthDestinationConfigError,
|
|
36
|
+
getProviderTlsProfileStatus,
|
|
37
|
+
isAntigravityOAuthProvider,
|
|
38
|
+
} from "../../lib/provider-tls-profile";
|
|
34
39
|
import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
|
|
35
40
|
import { ProviderOutboundPolicyError, providerOutboundGet, providerOutboundPost, providerRedirectError } from "../../lib/provider-outbound";
|
|
36
41
|
import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
|
|
37
42
|
import { parseAntigravityAvailableModels } from "../../providers/antigravity-models";
|
|
38
43
|
import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
|
|
39
|
-
import { deriveProviderPresets } from "../../providers/derive";
|
|
44
|
+
import { deriveProviderPresets, providerConfigSeed } from "../../providers/derive";
|
|
40
45
|
import { effectiveGoogleMode, providerCodexAccountMode, providerMatchesRegistryTransport } from "../../providers/registry";
|
|
41
46
|
import {
|
|
42
47
|
extractModelEnvelopeRows,
|
|
@@ -54,6 +59,7 @@ import { clearThreadAccountMap } from "../../codex/routing";
|
|
|
54
59
|
import { primeCodexPoolQuotas } from "../../codex/auth-api";
|
|
55
60
|
import { clearModelCache, getProviderDiscoveryStatus } from "../../codex/model-cache";
|
|
56
61
|
import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
|
|
62
|
+
import { modelAutoCompactTokenLimitsConfigError } from "../../providers/auto-compact-budget";
|
|
57
63
|
import { resolveCodexHomeDir } from "../../codex/home";
|
|
58
64
|
import { readUsageEntries } from "../../usage/log";
|
|
59
65
|
import { getUsageDebugLogEntries } from "../../usage/debug";
|
|
@@ -157,6 +163,20 @@ function applyProviderPatchFields(
|
|
|
157
163
|
return { error: "authMode must be key, forward, oauth, or local" };
|
|
158
164
|
}
|
|
159
165
|
}
|
|
166
|
+
if (Object.hasOwn(rawBody, "azureCredential")) {
|
|
167
|
+
const value = rawBody.azureCredential;
|
|
168
|
+
if (value === null) {
|
|
169
|
+
delete next.azureCredential;
|
|
170
|
+
} else {
|
|
171
|
+
if (!isPlainRecord(value)) return { error: "azureCredential must be an object or null" };
|
|
172
|
+
const credential = structuredClone(value) as Record<string, unknown>;
|
|
173
|
+
if (typeof credential.managedIdentityClientId === "string") {
|
|
174
|
+
credential.managedIdentityClientId = credential.managedIdentityClientId.trim();
|
|
175
|
+
}
|
|
176
|
+
next.azureCredential = credential as OcxProviderConfig["azureCredential"];
|
|
177
|
+
}
|
|
178
|
+
touched = true;
|
|
179
|
+
}
|
|
160
180
|
if (Object.hasOwn(rawBody, "apiKeyTransport")) {
|
|
161
181
|
const transport = rawBody.apiKeyTransport;
|
|
162
182
|
if (transport === "x-api-key" || transport === "bearer") {
|
|
@@ -214,6 +234,17 @@ function applyProviderPatchFields(
|
|
|
214
234
|
}
|
|
215
235
|
touched = true;
|
|
216
236
|
}
|
|
237
|
+
if (Object.hasOwn(rawBody, "tlsProfile")) {
|
|
238
|
+
const value = rawBody.tlsProfile;
|
|
239
|
+
if (value === null || value === "") {
|
|
240
|
+
delete next.tlsProfile;
|
|
241
|
+
} else if (value === "antigravity-browser") {
|
|
242
|
+
next.tlsProfile = value;
|
|
243
|
+
} else {
|
|
244
|
+
return { error: "tlsProfile must be antigravity-browser or null" };
|
|
245
|
+
}
|
|
246
|
+
touched = true;
|
|
247
|
+
}
|
|
217
248
|
if (Object.hasOwn(rawBody, "upstreamHttpVersion")) {
|
|
218
249
|
const value = rawBody.upstreamHttpVersion;
|
|
219
250
|
if (value === null || value === "") {
|
|
@@ -266,6 +297,29 @@ function applyProviderPatchFields(
|
|
|
266
297
|
}
|
|
267
298
|
touched = true;
|
|
268
299
|
}
|
|
300
|
+
if (Object.hasOwn(rawBody, "modelAutoCompactTokenLimits")) {
|
|
301
|
+
const value = rawBody.modelAutoCompactTokenLimits;
|
|
302
|
+
const error = modelAutoCompactTokenLimitsConfigError(value, {
|
|
303
|
+
allowTombstones: true,
|
|
304
|
+
requireNativeIds: name === "openai",
|
|
305
|
+
});
|
|
306
|
+
if (error) return { error };
|
|
307
|
+
if (value === null) {
|
|
308
|
+
delete next.modelAutoCompactTokenLimits;
|
|
309
|
+
} else {
|
|
310
|
+
const budgets: Record<string, number> = Object.assign(
|
|
311
|
+
Object.create(null) as Record<string, number>,
|
|
312
|
+
next.modelAutoCompactTokenLimits ?? {},
|
|
313
|
+
);
|
|
314
|
+
for (const [model, budget] of Object.entries(value as Record<string, number | null>)) {
|
|
315
|
+
if (budget === null) delete budgets[model];
|
|
316
|
+
else budgets[model] = budget;
|
|
317
|
+
}
|
|
318
|
+
if (Object.keys(budgets).length > 0) next.modelAutoCompactTokenLimits = budgets;
|
|
319
|
+
else delete next.modelAutoCompactTokenLimits;
|
|
320
|
+
}
|
|
321
|
+
touched = true;
|
|
322
|
+
}
|
|
269
323
|
if (Object.hasOwn(rawBody, "modelSupportsServiceTier")) {
|
|
270
324
|
const value = rawBody.modelSupportsServiceTier;
|
|
271
325
|
if (value === null) {
|
|
@@ -369,6 +423,28 @@ function applyProviderPatchFields(
|
|
|
369
423
|
return { next, touched, editorTouched, enablingOpenAi, headersTouched };
|
|
370
424
|
}
|
|
371
425
|
|
|
426
|
+
/** Validate the canonical OpenAI soft-budget overlay against a fresh registry seed. */
|
|
427
|
+
function canonicalOpenAiBudgetPatchError(
|
|
428
|
+
provider: OcxProviderConfig,
|
|
429
|
+
rawBody: Record<string, unknown>,
|
|
430
|
+
keys: string[],
|
|
431
|
+
config: OcxConfig,
|
|
432
|
+
): string | null {
|
|
433
|
+
if (!isCanonicalOpenAiForwardProvider(provider)) {
|
|
434
|
+
return "provider openai must be the canonical built-in provider";
|
|
435
|
+
}
|
|
436
|
+
const entry = getProviderRegistryEntry("openai");
|
|
437
|
+
if (!entry) return "provider openai registry seed is unavailable";
|
|
438
|
+
const seed = providerConfigSeed(entry);
|
|
439
|
+
if (provider.codexAccountMode !== undefined) seed.codexAccountMode = provider.codexAccountMode;
|
|
440
|
+
if (provider.modelAutoCompactTokenLimits !== undefined) {
|
|
441
|
+
seed.modelAutoCompactTokenLimits = { ...provider.modelAutoCompactTokenLimits };
|
|
442
|
+
}
|
|
443
|
+
const applied = applyProviderPatchFields("openai", seed, rawBody, keys, config);
|
|
444
|
+
if ("error" in applied) return applied.error;
|
|
445
|
+
return providerManagementConfigError("openai", applied.next);
|
|
446
|
+
}
|
|
447
|
+
|
|
372
448
|
export async function handleProviderRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
373
449
|
const { req, url, config, deps, principal, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
|
|
374
450
|
|
|
@@ -405,11 +481,14 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
405
481
|
models: p.models ?? [],
|
|
406
482
|
contextWindow: p.contextWindow,
|
|
407
483
|
modelContextWindows: p.modelContextWindows,
|
|
484
|
+
modelAutoCompactTokenLimits: p.modelAutoCompactTokenLimits,
|
|
408
485
|
modelSupportsServiceTier: p.modelSupportsServiceTier,
|
|
409
486
|
noStructuredOutputModels: p.noStructuredOutputModels,
|
|
410
487
|
upstreamHttpVersion: p.upstreamHttpVersion,
|
|
411
488
|
authMode: p.authMode,
|
|
412
489
|
apiKeyTransport: p.apiKeyTransport,
|
|
490
|
+
tlsProfile: p.tlsProfile,
|
|
491
|
+
tlsProfileStatus: p.tlsProfile === undefined ? "disabled" : getProviderTlsProfileStatus(name, p),
|
|
413
492
|
disabled: p.disabled === true,
|
|
414
493
|
codexAccountMode: providerCodexAccountMode(name, p),
|
|
415
494
|
...(name === "xai" ? { xaiResponsesOptInState: xaiResponsesOptInState(p) } : {}),
|
|
@@ -536,13 +615,14 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
536
615
|
// call can never fire.
|
|
537
616
|
const submittedContextWindow = Object.hasOwn(prov, "contextWindow");
|
|
538
617
|
const submittedModelContextWindows = Object.hasOwn(prov, "modelContextWindows");
|
|
618
|
+
const submittedModelAutoCompactTokenLimits = Object.hasOwn(prov, "modelAutoCompactTokenLimits");
|
|
539
619
|
const submittedRequestPacing = Object.hasOwn(prov, "requestPacing");
|
|
540
620
|
enrichProviderFromCatalog(name, prov);
|
|
541
621
|
const { saveConfigPreservingClaudeCode: save } = await import("../../config");
|
|
542
622
|
// Overwriting an existing provider must not drop its multi-key pool: carry it over, then
|
|
543
623
|
// let the (possibly new) apiKey join the pool as the active entry.
|
|
544
624
|
const existingPool = config.providers[name]?.apiKeyPool;
|
|
545
|
-
if (existingPool && !prov.apiKeyPool) prov.apiKeyPool = existingPool;
|
|
625
|
+
if (existingPool && !prov.apiKeyPool && !prov.azureCredential) prov.apiKeyPool = existingPool;
|
|
546
626
|
// The same rule applies to user-configured price overlays: the dashboard's
|
|
547
627
|
// add/edit form does not send modelCosts, so an overwrite must not silently
|
|
548
628
|
// erase hand-edited per-model prices from Logs/Usage estimates.
|
|
@@ -568,6 +648,11 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
568
648
|
? { ...existing.modelContextWindows, ...(prov.modelContextWindows ?? {}) }
|
|
569
649
|
: { ...existing.modelContextWindows };
|
|
570
650
|
}
|
|
651
|
+
if (existing?.modelAutoCompactTokenLimits) {
|
|
652
|
+
prov.modelAutoCompactTokenLimits = submittedModelAutoCompactTokenLimits
|
|
653
|
+
? { ...existing.modelAutoCompactTokenLimits, ...(prov.modelAutoCompactTokenLimits ?? {}) }
|
|
654
|
+
: { ...existing.modelAutoCompactTokenLimits };
|
|
655
|
+
}
|
|
571
656
|
config.providers[name] = stripRegistryOnlyStaticHeaders(name, prov);
|
|
572
657
|
if (body.setDefault === true) config.defaultProvider = name;
|
|
573
658
|
save(config);
|
|
@@ -591,6 +676,9 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
591
676
|
const keys = Object.keys(rawBody);
|
|
592
677
|
const hasMode = Object.hasOwn(rawBody, "codexAccountMode");
|
|
593
678
|
const hasSetDefault = Object.hasOwn(rawBody, "setDefault");
|
|
679
|
+
const canonicalBudgetOnly = name === "openai"
|
|
680
|
+
&& keys.length === 1
|
|
681
|
+
&& keys[0] === "modelAutoCompactTokenLimits";
|
|
594
682
|
|
|
595
683
|
// codexAccountMode keeps its dedicated side-effect path (quota cache clear, thread map
|
|
596
684
|
// clear, pool prime) and is mutually exclusive with every other patch field.
|
|
@@ -653,12 +741,16 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
653
741
|
|
|
654
742
|
const pacingOnly = keys.every(key => key === "requestPacing");
|
|
655
743
|
if (applied.editorTouched && !pacingOnly) {
|
|
656
|
-
const providerError =
|
|
744
|
+
const providerError = canonicalBudgetOnly
|
|
745
|
+
? canonicalOpenAiBudgetPatchError(next, rawBody, keys, config)
|
|
746
|
+
: providerManagementConfigError(name, next);
|
|
657
747
|
if (providerError) return jsonResponse({ error: providerError }, 400);
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
748
|
+
if (!canonicalBudgetOnly) {
|
|
749
|
+
const serviceTierError = providerServiceTierConfigError(name, next);
|
|
750
|
+
if (serviceTierError) return jsonResponse({ error: serviceTierError }, 400);
|
|
751
|
+
const resolvedError = await providerDestinationResolvedError(name, next);
|
|
752
|
+
if (resolvedError) return jsonResponse({ error: resolvedError }, 400);
|
|
753
|
+
}
|
|
662
754
|
} else if (applied.enablingOpenAi) {
|
|
663
755
|
// Same DNS gate as POST: Clash fake-IP only. Never honor a persisted
|
|
664
756
|
// allowPrivateNetwork on this path — it must not bypass the built-in guard.
|
|
@@ -682,15 +774,19 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
682
774
|
return;
|
|
683
775
|
}
|
|
684
776
|
if (replay.editorTouched && !pacingOnly) {
|
|
685
|
-
const syncError =
|
|
777
|
+
const syncError = canonicalBudgetOnly
|
|
778
|
+
? canonicalOpenAiBudgetPatchError(replay.next, rawBody, keys, config)
|
|
779
|
+
: providerManagementConfigError(name, replay.next);
|
|
686
780
|
if (syncError) {
|
|
687
781
|
replayError = syncError;
|
|
688
782
|
return;
|
|
689
783
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
784
|
+
if (!canonicalBudgetOnly) {
|
|
785
|
+
const serviceTierError = providerServiceTierConfigError(name, replay.next);
|
|
786
|
+
if (serviceTierError) {
|
|
787
|
+
replayError = serviceTierError;
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
694
790
|
}
|
|
695
791
|
} else if (replay.enablingOpenAi && !isCanonicalOpenAiForwardProvider(replay.next)) {
|
|
696
792
|
replayError = "provider openai must be the canonical built-in provider";
|
|
@@ -733,6 +829,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
733
829
|
if (prov.disabled) {
|
|
734
830
|
return jsonResponse({ ok: false, error: "Provider is disabled", latencyMs: 0 });
|
|
735
831
|
}
|
|
832
|
+
const antigravityError = antigravityOAuthDestinationConfigError(name, prov);
|
|
833
|
+
if (antigravityError) return jsonResponse({ ok: false, error: antigravityError, latencyMs: 0 }, 400);
|
|
736
834
|
if (prov.authMode === "forward") {
|
|
737
835
|
return jsonResponse({
|
|
738
836
|
ok: true,
|
|
@@ -747,12 +845,12 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
747
845
|
return jsonResponse({ applicable: false, reason: "static_catalog", latencyMs: 0 });
|
|
748
846
|
}
|
|
749
847
|
const { buildModelsRequest, getValidAccessTokenSnapshot, resolveModelsAuthToken } = await import("../../oauth");
|
|
750
|
-
const antigravity =
|
|
848
|
+
const antigravity = isAntigravityOAuthProvider(name, prov);
|
|
751
849
|
const snapshot = antigravity
|
|
752
850
|
? await getValidAccessTokenSnapshot(name).catch(() => undefined)
|
|
753
851
|
: undefined;
|
|
754
852
|
const apiKey = snapshot?.accessToken ?? await resolveModelsAuthToken(name, prov);
|
|
755
|
-
if (prov.authMode === "oauth" && !apiKey) {
|
|
853
|
+
if ((prov.authMode === "oauth" || antigravity) && !apiKey) {
|
|
756
854
|
return jsonResponse({ ok: false, latencyMs: 0, error: "static catalog only — upstream not verified (not logged in)" });
|
|
757
855
|
}
|
|
758
856
|
if (prov.adapter === "cursor") {
|
|
@@ -776,7 +874,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
776
874
|
message: `Connected. ${live.models.length} models.`,
|
|
777
875
|
});
|
|
778
876
|
}
|
|
779
|
-
const project =
|
|
877
|
+
const project = antigravity ? snapshot?.projectId : prov.project;
|
|
780
878
|
if (antigravity && !project) {
|
|
781
879
|
return jsonResponse({ ok: false, latencyMs: 0, error: "Antigravity project unavailable — re-run `ocx login google-antigravity`" });
|
|
782
880
|
}
|
|
@@ -188,6 +188,9 @@ function migrateProfileModelReferences(
|
|
|
188
188
|
if (config.injectionModel && config.injectionModel === oldPublicModel) {
|
|
189
189
|
config.injectionModel = newPublicModel;
|
|
190
190
|
}
|
|
191
|
+
if (config.v2NativeParentOverride?.model && config.v2NativeParentOverride.model === oldPublicModel) {
|
|
192
|
+
config.v2NativeParentOverride = { ...config.v2NativeParentOverride, model: newPublicModel };
|
|
193
|
+
}
|
|
191
194
|
if (config.shadowCallIntercept?.model && config.shadowCallIntercept.model === oldPublicModel) {
|
|
192
195
|
config.shadowCallIntercept = { ...config.shadowCallIntercept, model: newPublicModel };
|
|
193
196
|
}
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
stopServerListener,
|
|
34
34
|
} from "../lifecycle";
|
|
35
35
|
import { isServiceViable } from "../../service";
|
|
36
|
-
import { readRuntimePort } from "../../config";
|
|
36
|
+
import { readRuntimePort } from "../../config/process-state";
|
|
37
37
|
import { withProcessRuntimeProvenance } from "../../lib/bun-runtime";
|
|
38
38
|
import { selfLaunchArgv } from "../../lib/self-launch-argv";
|
|
39
39
|
import {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* (non-ocx, non-allowlisted) processes are never killed.
|
|
10
10
|
*/
|
|
11
11
|
import { execFileSync } from "node:child_process";
|
|
12
|
-
import { verifyPidIdentity } from "../config";
|
|
12
|
+
import { verifyPidIdentity } from "../config/process-state";
|
|
13
13
|
import { isProcessAlive, killProxy } from "../lib/process-control";
|
|
14
14
|
import { isPortAvailable, type WaitForPortOptions } from "./ports";
|
|
15
15
|
import { dropWindowsTcpRowsForLocalPort } from "./windows-tcp-drop";
|
|
@@ -9,7 +9,8 @@
|
|
|
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
|
|
12
|
+
import { loadConfig } from "../config";
|
|
13
|
+
import { readAlivePid, readRuntimePort, verifyPidIdentity } from "../config/process-state";
|
|
13
14
|
import { directLocalHttpFetch } from "./direct-local-http";
|
|
14
15
|
|
|
15
16
|
export interface HealthzIdentity {
|
|
@@ -61,6 +61,36 @@ export function sessionIdHeaderFromRequest(headers: Headers): string | null {
|
|
|
61
61
|
return headers.get("session_id") ?? headers.get("session-id");
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
function firstSanitizedConversationId(
|
|
65
|
+
...values: Array<string | null | undefined>
|
|
66
|
+
): string | undefined {
|
|
67
|
+
for (const value of values) {
|
|
68
|
+
const sanitized = sanitizeConversationIdInput(value);
|
|
69
|
+
if (sanitized) return sanitized;
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Conversation namespace for reasoning replay. Unlike the persisted log id, this stays the raw
|
|
76
|
+
* sanitized identity so mixed headers that carry the same conversation still hit one serving
|
|
77
|
+
* record. Do not hash, and do not prefer session_id over a true per-conversation thread/Cursor
|
|
78
|
+
* identity: session_id can be synthesized from a shared prompt_cache_key.
|
|
79
|
+
*/
|
|
80
|
+
export function reasoningReplayConversationIdFromResponsesRequest(input: {
|
|
81
|
+
clientThreadId?: string;
|
|
82
|
+
threadIdHeader?: string | null;
|
|
83
|
+
cursorConversationId?: string;
|
|
84
|
+
sessionIdHeader?: string | null;
|
|
85
|
+
}): string | undefined {
|
|
86
|
+
return firstSanitizedConversationId(
|
|
87
|
+
input.clientThreadId,
|
|
88
|
+
input.threadIdHeader,
|
|
89
|
+
input.cursorConversationId,
|
|
90
|
+
input.sessionIdHeader,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
64
94
|
export function conversationIdFromResponsesRequest(input: {
|
|
65
95
|
clientThreadId?: string;
|
|
66
96
|
sessionIdHeader?: string | null;
|
|
@@ -549,6 +549,7 @@ export function requestLogErrorCode(
|
|
|
549
549
|
// Keep the high-confidence message fallback for runtimes/providers that stripped the
|
|
550
550
|
// structured code before emitting response.failed.
|
|
551
551
|
if (classifiedCode === CYBER_POLICY_ERROR_CODE) return CYBER_POLICY_ERROR_CODE;
|
|
552
|
+
if (classifiedCode === "insufficient_quota") return classifiedCode;
|
|
552
553
|
if (status === 400 || status === 409) return "invalid_request_error";
|
|
553
554
|
if (status === 401) return "invalid_api_key";
|
|
554
555
|
if (status === 403) {
|
|
@@ -1003,6 +1004,26 @@ export function filterRequestLogs(logs: RequestLogEntry[], params: URLSearchPara
|
|
|
1003
1004
|
filtered = filtered.filter(entry => entry.provider === provider
|
|
1004
1005
|
|| entry.attempts?.some(attempt => attempt.provider === provider));
|
|
1005
1006
|
}
|
|
1007
|
+
const model = params.get("model")?.trim().toLowerCase();
|
|
1008
|
+
if (model) {
|
|
1009
|
+
filtered = filtered.filter(entry => entry.model.toLowerCase() === model
|
|
1010
|
+
|| entry.resolvedModel?.toLowerCase() === model
|
|
1011
|
+
|| entry.attempts?.some(attempt => attempt.model.toLowerCase() === model));
|
|
1012
|
+
}
|
|
1013
|
+
const sinceRaw = params.get("since")?.trim();
|
|
1014
|
+
if (sinceRaw) {
|
|
1015
|
+
const since = Number(sinceRaw);
|
|
1016
|
+
if (Number.isFinite(since)) {
|
|
1017
|
+
filtered = filtered.filter(entry => entry.timestamp >= since);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
const untilRaw = params.get("until")?.trim();
|
|
1021
|
+
if (untilRaw) {
|
|
1022
|
+
const until = Number(untilRaw);
|
|
1023
|
+
if (Number.isFinite(until)) {
|
|
1024
|
+
filtered = filtered.filter(entry => entry.timestamp <= until);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1006
1027
|
const conversationId = params.get("conversationId")?.trim() || params.get("conversation")?.trim();
|
|
1007
1028
|
if (conversationId) {
|
|
1008
1029
|
filtered = filtered.filter(entry => matchesLogConversationId(entry.conversationId, conversationId));
|
|
@@ -45,7 +45,7 @@ export function agentTaskRecoveryConfig(config: OcxConfig): AgentTaskRecoveryOpt
|
|
|
45
45
|
enabled: true,
|
|
46
46
|
model: typeof raw.model === "string" && raw.model.trim().length > 0
|
|
47
47
|
? raw.model.trim()
|
|
48
|
-
: "gpt-5.6-
|
|
48
|
+
: "gpt-5.6-luna",
|
|
49
49
|
timeoutMs: Number.isFinite(raw.timeoutMs) && (raw.timeoutMs ?? 0) >= 1_000
|
|
50
50
|
? Math.min(120_000, Math.floor(raw.timeoutMs!))
|
|
51
51
|
: 45_000,
|