@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
package/src/cli/observe.ts
CHANGED
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
takeOption,
|
|
11
11
|
type RuntimeApiDeps,
|
|
12
12
|
} from "./runtime-api";
|
|
13
|
+
import { formatUsageReport } from "./usage-report";
|
|
14
|
+
import { USAGE_RANGES, USAGE_SURFACES } from "../usage/summary";
|
|
13
15
|
|
|
14
16
|
const USAGE = `Usage:
|
|
15
17
|
ocx observe logs [--provider <name>] [--model <id>] [--status <code>]
|
|
@@ -17,7 +19,8 @@ const USAGE = `Usage:
|
|
|
17
19
|
ocx logs explain <request-id> [--json]
|
|
18
20
|
ocx logs rebuild-index
|
|
19
21
|
ocx logs index-status
|
|
20
|
-
ocx observe usage [--range <7d|30d|all>] [--surface <all|codex|claude|grok>]
|
|
22
|
+
ocx observe usage [--range <today|1d|7d|30d|all>] [--surface <all|codex|claude|grok>]
|
|
23
|
+
[--provider <name>] [--model <id>] [--json]
|
|
21
24
|
ocx observe storage [codex-logs [status|protect|unprotect|repair|compact] [--mode <compat|quiet>]] [--json]
|
|
22
25
|
ocx observe memory [--json]
|
|
23
26
|
ocx observe debug [--json]
|
|
@@ -131,11 +134,23 @@ async function usage(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
131
134
|
const wantsJson = takeFlag(args, "--json");
|
|
132
135
|
const range = takeOption(args, "--range") ?? "30d";
|
|
133
136
|
const surface = takeOption(args, "--surface") ?? "all";
|
|
134
|
-
|
|
135
|
-
|
|
137
|
+
const provider = takeOption(args, "--provider");
|
|
138
|
+
const model = takeOption(args, "--model");
|
|
139
|
+
// `1d` is accepted here as well as server-side so the CLI does not reject an
|
|
140
|
+
// alias the API would have understood.
|
|
141
|
+
const ranges = [...USAGE_RANGES, "1d"];
|
|
142
|
+
if (!ranges.includes(range)) throw new CliUsageError(`--range must be one of ${USAGE_RANGES.join(", ")} (1d aliases today)`, USAGE);
|
|
143
|
+
if (!USAGE_SURFACES.includes(surface as (typeof USAGE_SURFACES)[number])) {
|
|
144
|
+
throw new CliUsageError(`--surface must be one of ${USAGE_SURFACES.join(", ")}`, USAGE);
|
|
145
|
+
}
|
|
136
146
|
rejectArgs(args, USAGE);
|
|
137
|
-
const result = await runtimeRequest(`/api/usage${query({ range, surface })}`, {}, deps);
|
|
138
|
-
|
|
147
|
+
const result = await runtimeRequest(`/api/usage${query({ range, surface, provider, model })}`, {}, deps);
|
|
148
|
+
// Built only when it will be printed: JavaScript evaluates arguments before
|
|
149
|
+
// the call, so passing formatUsageReport(...) inline would run the human
|
|
150
|
+
// renderer during --json and let its assumptions affect a path that is meant
|
|
151
|
+
// to bypass it entirely.
|
|
152
|
+
if (wantsJson) printData(result, true);
|
|
153
|
+
else printData(result, false, formatUsageReport(result as Parameters<typeof formatUsageReport>[0]));
|
|
139
154
|
}
|
|
140
155
|
|
|
141
156
|
async function simple(path: string, argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
package/src/cli/provider.ts
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* show <name> Show provider config details (secrets masked)
|
|
9
9
|
* set-default <name> Change the default provider
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
11
|
+
import { hasOwnProvider, isValidProviderName, loadConfig, sanitizeModelCostsForDisplay, saveConfig } from "../config";
|
|
12
|
+
import { apiKeyTransportConfigError } from "../config/provider-validation";
|
|
12
13
|
import { hasHelpFlag } from "./help";
|
|
13
14
|
import { getProviderRegistryEntry, PROVIDER_REGISTRY } from "../providers/registry";
|
|
14
15
|
import { providerConfigSeed } from "../providers/derive";
|
|
@@ -359,6 +360,12 @@ function handleShow(args: string[]): void {
|
|
|
359
360
|
...(prov.modelCosts !== undefined ? { modelCosts: sanitizeModelCostsForDisplay(prov.modelCosts) } : {}),
|
|
360
361
|
...(prov.apiKey ? { apiKey: maskSecret(prov.apiKey) } : {}),
|
|
361
362
|
...(prov.apiKeyPool ? { apiKeyPool: prov.apiKeyPool.map(e => ({ ...e, key: maskSecret(e.key) })) } : {}),
|
|
363
|
+
...(prov.azureCredential ? {
|
|
364
|
+
azureCredential: {
|
|
365
|
+
type: prov.azureCredential.type,
|
|
366
|
+
hasManagedIdentityClientId: Boolean(prov.azureCredential.managedIdentityClientId?.trim()),
|
|
367
|
+
},
|
|
368
|
+
} : {}),
|
|
362
369
|
};
|
|
363
370
|
|
|
364
371
|
if (wantsJson) {
|
package/src/cli/registry.ts
CHANGED
|
@@ -36,8 +36,8 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
38
|
name: "recover-history",
|
|
39
|
-
usage: "ocx recover-history --legacy-openai",
|
|
40
|
-
summary: "
|
|
39
|
+
usage: "ocx recover-history --legacy-openai --yes",
|
|
40
|
+
summary: "Force all user-message opencodex rows to OpenAI for legacy recovery.",
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
name: "uninstall",
|
|
@@ -88,20 +88,22 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
88
88
|
{ name: "ensure", usage: "ocx ensure", summary: "Ensure the proxy is running and Codex config/cache are current." },
|
|
89
89
|
{
|
|
90
90
|
name: "sync",
|
|
91
|
-
usage: "ocx sync [--restart-codex]",
|
|
91
|
+
usage: "ocx sync [--restart-codex] [--restart-desktop-app]",
|
|
92
92
|
summary: "Fetch provider models and inject them into Codex config.",
|
|
93
93
|
details: [
|
|
94
94
|
"After writing the catalog, warns if long-lived Codex app-server processes are still running.",
|
|
95
95
|
"--restart-codex sends SIGTERM only to matching app-server / code-mode-host processes (may interrupt active turns).",
|
|
96
|
+
"--restart-desktop-app (Windows only, opt-in) fully restarts the Codex desktop app so its model picker re-reads the catalog. Never implied by --restart-codex: it ends live conversations.",
|
|
96
97
|
],
|
|
97
98
|
},
|
|
98
99
|
{
|
|
99
100
|
name: "sync-cache",
|
|
100
|
-
usage: "ocx sync-cache [--restart-codex]",
|
|
101
|
+
usage: "ocx sync-cache [--restart-codex] [--restart-desktop-app]",
|
|
101
102
|
summary: "Refresh Codex's model cache from the active catalog.",
|
|
102
103
|
details: [
|
|
103
104
|
"Warns when Codex app-server processes still hold an in-memory model list.",
|
|
104
105
|
"--restart-codex sends SIGTERM only to matching app-server / code-mode-host processes (may interrupt active turns).",
|
|
106
|
+
"--restart-desktop-app (Windows only, opt-in) fully restarts the Codex desktop app so its model picker re-reads the catalog. Never implied by --restart-codex: it ends live conversations.",
|
|
105
107
|
],
|
|
106
108
|
},
|
|
107
109
|
{ name: "status", usage: "ocx status", summary: "Check proxy server status." },
|
|
@@ -209,7 +211,7 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
209
211
|
{ name: "logs", usage: "ocx logs [filters] [--follow] [--json|--jsonl]", summary: "Alias of ocx observe logs." },
|
|
210
212
|
{
|
|
211
213
|
name: "usage",
|
|
212
|
-
usage: "ocx usage [--range <7d|30d|all>] [--surface <all|codex|claude|grok>] [--json]",
|
|
214
|
+
usage: "ocx usage [--range <today|1d|7d|30d|all>] [--surface <all|codex|claude|grok>] [--provider <name>] [--model <id>] [--json]",
|
|
213
215
|
summary: "Alias of ocx observe usage.",
|
|
214
216
|
},
|
|
215
217
|
{ name: "storage", usage: "ocx storage [--json]", summary: "Alias of ocx observe storage." },
|
package/src/cli/status.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { durableBunRuntime } from "../lib/bun-runtime";
|
|
2
|
-
import { codexAutoStartEnabled, getConfigPath,
|
|
2
|
+
import { codexAutoStartEnabled, getConfigPath, readConfigDiagnostics } from "../config";
|
|
3
|
+
import { getPidPath, readPid, readRuntimePort, type RuntimePortState } from "../config/process-state";
|
|
3
4
|
import { diagnoseCodexBundledPlugins, type CodexPluginsDiagnostic } from "../codex/plugins-doctor";
|
|
4
5
|
import { findLiveProxy, isOpencodexHealthz, probeHostname } from "../server/proxy-liveness";
|
|
5
6
|
import { directLocalHttpFetch } from "../server/direct-local-http";
|
|
@@ -117,6 +118,29 @@ export function proxyHealthFailureReason(error: unknown, signal: AbortSignal): "
|
|
|
117
118
|
: "unreachable";
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
/**
|
|
122
|
+
* `ocx status` greens on process liveness alone, so a proxy that answers
|
|
123
|
+
* /healthz reads healthy even when Codex is not pointed at it and every routed
|
|
124
|
+
* request goes to OpenAI instead (#2411). The proxy line is not wrong — the
|
|
125
|
+
* listener really is up — so it keeps its check, and this supplies the signal
|
|
126
|
+
* that was missing rather than corrupting the one that was already honest.
|
|
127
|
+
*
|
|
128
|
+
* Only `native` warns. `custom-local` and `unknown` are also "this proxy is
|
|
129
|
+
* unused", but startupHealthSummary already renders both as AT RISK with a
|
|
130
|
+
* remedy command, and `custom-remote` is a deliberate operator choice. Warning
|
|
131
|
+
* on all four would teach operators to skip the line that matters.
|
|
132
|
+
*/
|
|
133
|
+
export function unusedProxyWarningLines(input: {
|
|
134
|
+
proxyUp: boolean;
|
|
135
|
+
routingKind: StartupHealth["routingKind"];
|
|
136
|
+
}): string[] {
|
|
137
|
+
if (!input.proxyUp || input.routingKind !== "native") return [];
|
|
138
|
+
return [
|
|
139
|
+
"⚠️ Codex routing is native — the running proxy is unused.",
|
|
140
|
+
" Codex requests go to OpenAI, not this proxy. Re-point with: ocx start",
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
|
|
120
144
|
async function checkProxyHealth(target: ListenTarget): Promise<HealthCheck> {
|
|
121
145
|
const url = target.healthUrl;
|
|
122
146
|
const controller = new AbortController();
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human rendering for `ocx usage`.
|
|
3
|
+
*
|
|
4
|
+
* Kept out of `observe.ts` and away from the shared `summaryLines()` helper on
|
|
5
|
+
* purpose. `summaryLines()` is a generic depth-1 flattener shared with
|
|
6
|
+
* storage/memory/debug/claude-inbound/injection; it renders any array as
|
|
7
|
+
* "N item(s)", which is why every per-model and per-provider cost the server
|
|
8
|
+
* computes used to vanish before reaching the terminal. Deepening it would
|
|
9
|
+
* change five unrelated commands.
|
|
10
|
+
*
|
|
11
|
+
* Formatting follows the existing CLI house style: dynamic `padEnd` columns
|
|
12
|
+
* (as in `formatAccountTable`), plain text, no ANSI colour.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
interface CostRow {
|
|
16
|
+
provider: string;
|
|
17
|
+
model?: string;
|
|
18
|
+
requests: number;
|
|
19
|
+
totalTokens: number;
|
|
20
|
+
estimatedCostUsd?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface UsageReportInput {
|
|
24
|
+
range?: string;
|
|
25
|
+
surface?: string;
|
|
26
|
+
since?: number | null;
|
|
27
|
+
summary?: {
|
|
28
|
+
requests?: number;
|
|
29
|
+
totalTokens?: number;
|
|
30
|
+
inputTokens?: number;
|
|
31
|
+
outputTokens?: number;
|
|
32
|
+
cachedInputTokens?: number;
|
|
33
|
+
estimatedCostUsd?: number;
|
|
34
|
+
unpricedRequests?: number;
|
|
35
|
+
unmeteredRequests?: number;
|
|
36
|
+
};
|
|
37
|
+
models?: CostRow[];
|
|
38
|
+
providers?: CostRow[];
|
|
39
|
+
days?: { date: string; requests: number; totalTokens: number; estimatedCostUsd?: number }[];
|
|
40
|
+
filter?: { provider: string | null; model: string | null; matched: boolean; comboOverlap: boolean };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const MAX_MODEL_ROWS = 10;
|
|
44
|
+
|
|
45
|
+
function count(value: number | undefined): string {
|
|
46
|
+
return (value ?? 0).toLocaleString("en-US");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Matches the dashboard's `~$` with four fraction digits. Estimates below a
|
|
51
|
+
* hundredth of a cent still read as a number rather than collapsing to $0.00,
|
|
52
|
+
* which matters when a single request is being inspected.
|
|
53
|
+
*/
|
|
54
|
+
function usd(value: number | undefined): string {
|
|
55
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return "—";
|
|
56
|
+
return `~$${value.toFixed(4)}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function table(header: string[], rows: string[][]): string[] {
|
|
60
|
+
if (rows.length === 0) return [];
|
|
61
|
+
const widths = header.map((h, i) => Math.max(h.length, ...rows.map(r => (r[i] ?? "").length)));
|
|
62
|
+
const line = (cols: string[]): string => cols.map((c, i) => (c ?? "").padEnd(widths[i]!)).join(" ").trimEnd();
|
|
63
|
+
return [line(header), ...rows.map(line)];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function describeScope(data: UsageReportInput): string {
|
|
67
|
+
const parts = [`Usage — ${data.range ?? "?"}`];
|
|
68
|
+
if (data.surface && data.surface !== "all") parts.push(`surface=${data.surface}`);
|
|
69
|
+
if (data.filter?.provider) parts.push(`provider=${data.filter.provider}`);
|
|
70
|
+
if (data.filter?.model) parts.push(`model=${data.filter.model}`);
|
|
71
|
+
return parts.join(", ");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function formatUsageReport(data: UsageReportInput): string[] {
|
|
75
|
+
const summary = data.summary ?? {};
|
|
76
|
+
const lines: string[] = [describeScope(data), ""];
|
|
77
|
+
|
|
78
|
+
if (data.filter && !data.filter.matched) {
|
|
79
|
+
const what = [data.filter.provider && `provider "${data.filter.provider}"`, data.filter.model && `model "${data.filter.model}"`]
|
|
80
|
+
.filter(Boolean).join(" and ");
|
|
81
|
+
lines.push(`No usage recorded for ${what} in this range.`);
|
|
82
|
+
lines.push("Check the spelling against `ocx usage --json`, or widen --range.");
|
|
83
|
+
return lines;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const tokenSplit = [
|
|
87
|
+
summary.inputTokens !== undefined ? `in ${count(summary.inputTokens)}` : null,
|
|
88
|
+
summary.outputTokens !== undefined ? `out ${count(summary.outputTokens)}` : null,
|
|
89
|
+
summary.cachedInputTokens ? `cached ${count(summary.cachedInputTokens)}` : null,
|
|
90
|
+
].filter(Boolean).join(" / ");
|
|
91
|
+
|
|
92
|
+
lines.push(`Requests ${count(summary.requests)}`);
|
|
93
|
+
lines.push(`Tokens ${count(summary.totalTokens)}${tokenSplit ? ` (${tokenSplit})` : ""}`);
|
|
94
|
+
lines.push(`Est. cost ${usd(summary.estimatedCostUsd)} API list-price equivalent (this range)`);
|
|
95
|
+
|
|
96
|
+
const unpriced = summary.unpricedRequests ?? 0;
|
|
97
|
+
const unmetered = summary.unmeteredRequests ?? 0;
|
|
98
|
+
if (unpriced > 0 || unmetered > 0) {
|
|
99
|
+
// Spelled out because a $0 total is ambiguous otherwise: it can mean "no
|
|
100
|
+
// spend" or "no price row matched", and those are very different answers.
|
|
101
|
+
lines.push(` ${count(unpriced)} unpriced, ${count(unmetered)} unmetered excluded from ~$`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const providers = (data.providers ?? []).filter(row => row.requests > 0);
|
|
105
|
+
if (providers.length > 0) {
|
|
106
|
+
lines.push("");
|
|
107
|
+
lines.push(...table(
|
|
108
|
+
["PROVIDER", "REQUESTS", "TOKENS", "EST. COST"],
|
|
109
|
+
providers.map(row => [row.provider, count(row.requests), count(row.totalTokens), usd(row.estimatedCostUsd)]),
|
|
110
|
+
));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const models = (data.models ?? []).filter(row => row.requests > 0);
|
|
114
|
+
if (models.length > 0) {
|
|
115
|
+
lines.push("");
|
|
116
|
+
const shown = models.slice(0, MAX_MODEL_ROWS);
|
|
117
|
+
lines.push(...table(
|
|
118
|
+
["MODEL", "PROVIDER", "REQUESTS", "TOKENS", "EST. COST"],
|
|
119
|
+
shown.map(row => [row.model ?? "-", row.provider, count(row.requests), count(row.totalTokens), usd(row.estimatedCostUsd)]),
|
|
120
|
+
));
|
|
121
|
+
if (models.length > shown.length) {
|
|
122
|
+
lines.push(`... ${models.length - shown.length} more (use --json)`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (data.filter?.comboOverlap) {
|
|
127
|
+
lines.push("");
|
|
128
|
+
lines.push("Some requests ran as combos, so per-model request counts can overlap. Cost does not.");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
lines.push("");
|
|
132
|
+
lines.push("Not a billing receipt. Subscription usage or provider credits may apply instead.");
|
|
133
|
+
return lines;
|
|
134
|
+
}
|
|
@@ -17,7 +17,8 @@ import {
|
|
|
17
17
|
import { readCodexCatalogPath } from "./catalog/parsing";
|
|
18
18
|
|
|
19
19
|
export const STALE_CODEX_APP_SERVER_HINT =
|
|
20
|
-
"If Codex still shows an older model list, restart its long-lived app-server process after sync (ocx sync --restart-codex)."
|
|
20
|
+
"If Codex still shows an older model list, restart its long-lived app-server process after sync (ocx sync --restart-codex). "
|
|
21
|
+
+ "On Windows the desktop app itself may also need a full restart (ocx sync --restart-desktop-app).";
|
|
21
22
|
|
|
22
23
|
/** Attach the shared dashboard hint only after a catalog or models_cache write. */
|
|
23
24
|
export function attachStaleAppServerHint<T extends {
|
|
@@ -505,6 +506,7 @@ export function formatStaleCodexAppServerWarning(
|
|
|
505
506
|
`WARNING: ${processes.length} Codex app-server process(es) still running (PID${processes.length === 1 ? "" : "s"}: ${pids}). `
|
|
506
507
|
+ "Disk catalog/cache were updated, but Codex may keep showing the old model list until those processes restart. "
|
|
507
508
|
+ "Re-run with `ocx sync --restart-codex` (or `ocx sync-cache --restart-codex`) to send SIGTERM only to matching app-server processes. "
|
|
509
|
+
+ "On Windows the desktop app itself may also need a full restart (`ocx sync --restart-desktop-app`). "
|
|
508
510
|
+ "Active turns may be interrupted."
|
|
509
511
|
);
|
|
510
512
|
}
|
package/src/codex/auth-api.ts
CHANGED
|
@@ -1806,7 +1806,7 @@ export async function handleCodexAuthAPI(
|
|
|
1806
1806
|
}
|
|
1807
1807
|
|
|
1808
1808
|
if (url.pathname === "/api/codex-auth/login" && req.method === "POST") {
|
|
1809
|
-
const body = (await req.json().catch(() => ({}))) as { id?: string; reauth?: boolean };
|
|
1809
|
+
const body = (await req.json().catch(() => ({}))) as { id?: string; reauth?: boolean; openBrowser?: unknown };
|
|
1810
1810
|
const requestedAccountId = body.id?.trim();
|
|
1811
1811
|
const reauth = body.reauth === true;
|
|
1812
1812
|
if (requestedAccountId && !isValidCodexAccountId(requestedAccountId)) {
|
|
@@ -1840,7 +1840,9 @@ export async function handleCodexAuthAPI(
|
|
|
1840
1840
|
|
|
1841
1841
|
// Open the browser server-side (same pattern as /api/oauth/login in management-api.ts).
|
|
1842
1842
|
// The GUI's window.open is popup-blocked because it runs after an await, not a direct click.
|
|
1843
|
-
|
|
1843
|
+
// Both login routes share one resolver so this surface cannot drift from the other.
|
|
1844
|
+
const { shouldOpenBrowserForLogin } = await import("../oauth/open-browser-choice");
|
|
1845
|
+
if (result.url && shouldOpenBrowserForLogin(body.openBrowser, runtimeConfig)) {
|
|
1844
1846
|
const { openUrl } = await import("../lib/open-url");
|
|
1845
1847
|
openUrl(result.url);
|
|
1846
1848
|
}
|
|
@@ -154,3 +154,19 @@ export function startupHealthSummary(health: StartupHealth): string {
|
|
|
154
154
|
if (health.serviceInstalled && !health.serviceViable) return `AT RISK after restart (installed service is disabled, stopped, or unhealthy; run '${command}')`;
|
|
155
155
|
return `AT RISK after restart (no viable background service; run '${command}')`;
|
|
156
156
|
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The routing/service/shim token `ocx doctor` prints under restart safety.
|
|
160
|
+
* Extracted so `ocx status` can show the same string rather than growing a
|
|
161
|
+
* second copy that drifts (#2411). Two management routes computing the same
|
|
162
|
+
* thing separately is exactly how #2457 happened.
|
|
163
|
+
*/
|
|
164
|
+
export function formatStartupRoutingDetail(health: StartupHealth): string {
|
|
165
|
+
const service = health.serviceViable
|
|
166
|
+
? "viable"
|
|
167
|
+
: health.serviceInstalled ? "installed-but-unhealthy" : "absent";
|
|
168
|
+
const shim = health.shimHealthy
|
|
169
|
+
? "healthy"
|
|
170
|
+
: health.shimInstalled ? "stale" : "absent";
|
|
171
|
+
return `routing=${health.routingKind}, service=${service}, shim=${shim}`;
|
|
172
|
+
}
|
|
@@ -13,6 +13,7 @@ import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, r
|
|
|
13
13
|
import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
|
|
14
14
|
import { getProviderRegistryEntry } from "../../providers/registry";
|
|
15
15
|
import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
|
|
16
|
+
import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget";
|
|
16
17
|
import { routedSlug, slugEquals, slugEquivalenceKey, slugsEquivalent } from "../../providers/slug-codec";
|
|
17
18
|
import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity";
|
|
18
19
|
import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
|
|
@@ -154,8 +155,16 @@ export function deriveComboCatalogModel(
|
|
|
154
155
|
// combo would have the same window even without the cap.
|
|
155
156
|
const contextCapped = limitingMembers.every(member => member.contextCapped === true);
|
|
156
157
|
const maxInputTokens = Math.min(
|
|
158
|
+
contextWindow,
|
|
157
159
|
...members.map(member => member.maxInputTokens ?? member.contextWindow!),
|
|
158
160
|
);
|
|
161
|
+
const autoCompactTokenLimit = Math.min(
|
|
162
|
+
...members.map(member => clampAutoCompactTokenLimit(
|
|
163
|
+
member.contextWindow!,
|
|
164
|
+
member.maxInputTokens,
|
|
165
|
+
member.autoCompactTokenLimit,
|
|
166
|
+
)),
|
|
167
|
+
);
|
|
159
168
|
const defaultReasoningEffort = effectiveComboDefault(
|
|
160
169
|
combo.defaultEffort,
|
|
161
170
|
reasoningEfforts,
|
|
@@ -167,6 +176,7 @@ export function deriveComboCatalogModel(
|
|
|
167
176
|
owned_by: COMBO_NAMESPACE,
|
|
168
177
|
contextWindow,
|
|
169
178
|
maxInputTokens,
|
|
179
|
+
autoCompactTokenLimit,
|
|
170
180
|
...(hasLimitingContextCapMetadata ? { contextCapped } : {}),
|
|
171
181
|
inputModalities,
|
|
172
182
|
reasoningEfforts,
|
|
@@ -210,6 +220,7 @@ export function comboCatalogWarningSignature(
|
|
|
210
220
|
key,
|
|
211
221
|
contextWindow: member?.contextWindow ?? null,
|
|
212
222
|
maxInputTokens: member?.maxInputTokens ?? null,
|
|
223
|
+
autoCompactTokenLimit: member?.autoCompactTokenLimit ?? null,
|
|
213
224
|
inputModalities: [...new Set(member?.inputModalities ?? [])].sort(),
|
|
214
225
|
reasoningEfforts: [...new Set(member?.reasoningEfforts ?? [])].sort(),
|
|
215
226
|
parallelToolCalls: member?.parallelToolCalls === true,
|
|
@@ -299,6 +310,7 @@ export function normalizedOpenAiApiSignature(model: CatalogModel): string {
|
|
|
299
310
|
id: model.id,
|
|
300
311
|
contextWindow: model.contextWindow ?? null,
|
|
301
312
|
maxInputTokens: model.maxInputTokens ?? null,
|
|
313
|
+
autoCompactTokenLimit: model.autoCompactTokenLimit ?? null,
|
|
302
314
|
inputModalities: [...new Set(model.inputModalities ?? [])].sort(),
|
|
303
315
|
reasoningEfforts: [...new Set(model.reasoningEfforts ?? [])].sort(),
|
|
304
316
|
ownedBy: model.owned_by ?? null,
|
|
@@ -13,6 +13,7 @@ import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, r
|
|
|
13
13
|
import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
|
|
14
14
|
import { getProviderRegistryEntry } from "../../providers/registry";
|
|
15
15
|
import { applyProviderContextCap, providerContextCap, resolveUnknownRoutedContextWindow } from "../../providers/context-cap";
|
|
16
|
+
import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget";
|
|
16
17
|
import { routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec";
|
|
17
18
|
import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity";
|
|
18
19
|
import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
|
|
@@ -33,6 +34,7 @@ import upstreamModelsSnapshot from "../data/upstream-models.json";
|
|
|
33
34
|
|
|
34
35
|
import { generatedModelMetadata, readCatalog, readCodexCatalogPath } from "./parsing";
|
|
35
36
|
import type { CatalogModel, RawEntry } from "./parsing";
|
|
37
|
+
import { shouldStampContextProvenance } from "./model-metadata";
|
|
36
38
|
import { UPSTREAM_NATIVE_ENTRIES } from "./metadata";
|
|
37
39
|
import { nativeOpenAiCapabilitySourceSlug } from "./native-models";
|
|
38
40
|
import { loadBundledCodexCatalog } from "./bundled";
|
|
@@ -128,9 +130,23 @@ export function applyCatalogModelMetadata(entry: RawEntry, model?: CatalogModel)
|
|
|
128
130
|
if (typeof resolvedContext === "number" && resolvedContext > 0) {
|
|
129
131
|
entry.context_window = resolvedContext;
|
|
130
132
|
entry.max_context_window = resolvedContext;
|
|
131
|
-
entry.auto_compact_token_limit =
|
|
132
|
-
|
|
133
|
-
model.maxInputTokens
|
|
133
|
+
entry.auto_compact_token_limit = clampAutoCompactTokenLimit(
|
|
134
|
+
resolvedContext,
|
|
135
|
+
model.maxInputTokens,
|
|
136
|
+
model.autoCompactTokenLimit,
|
|
137
|
+
);
|
|
138
|
+
} else if (
|
|
139
|
+
typeof entry.context_window === "number"
|
|
140
|
+
&& entry.context_window > 0
|
|
141
|
+
&& typeof model.maxInputTokens === "number"
|
|
142
|
+
&& model.maxInputTokens > 0
|
|
143
|
+
) {
|
|
144
|
+
// A conservative routed fallback is not evidence for applying the optional soft policy,
|
|
145
|
+
// but a measured/configured input ceiling is still a hard bound. Compact before that
|
|
146
|
+
// ceiling even when the provider supplied no authoritative context window.
|
|
147
|
+
entry.auto_compact_token_limit = clampAutoCompactTokenLimit(
|
|
148
|
+
entry.context_window,
|
|
149
|
+
model.maxInputTokens,
|
|
134
150
|
);
|
|
135
151
|
}
|
|
136
152
|
if (Array.isArray(model.inputModalities) && model.inputModalities.length > 0) {
|
|
@@ -171,33 +187,47 @@ export function applyCatalogModelMetadata(entry: RawEntry, model?: CatalogModel)
|
|
|
171
187
|
* live in that table.
|
|
172
188
|
*/
|
|
173
189
|
function stampCapabilityProvenance(entry: RawEntry, model: CatalogModel): void {
|
|
174
|
-
// Virtual combo rows
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
if (model.provider === COMBO_NAMESPACE) return;
|
|
190
|
+
// Virtual combo rows without derived member evidence are placeholders
|
|
191
|
+
// (generic 128k / `["text"]`). Stamping those would reintroduce the
|
|
192
|
+
// false-evidence defect this block exists to prevent. Combos that actually
|
|
193
|
+
// min() real member windows carry `metadataSource: "derived"`.
|
|
194
|
+
if (model.provider === COMBO_NAMESPACE && model.metadataSource !== "derived") return;
|
|
179
195
|
|
|
180
196
|
const meta = generatedModelMetadata(model.provider, model.id);
|
|
181
197
|
const metaContext = typeof meta?.contextWindow === "number" && meta.contextWindow > 0
|
|
182
|
-
// The generated context is capped before it reaches the entry, so provenance
|
|
183
|
-
// must apply the same cap or routing would advertise a window the cap refused.
|
|
184
198
|
? applyProviderContextCap(meta.contextWindow, model.contextCap) ?? meta.contextWindow
|
|
185
199
|
: undefined;
|
|
186
|
-
const
|
|
200
|
+
const stampModelContext = shouldStampContextProvenance(model);
|
|
201
|
+
const contextWindow = stampModelContext
|
|
187
202
|
? model.contextWindow
|
|
188
|
-
: metaContext;
|
|
203
|
+
: (model.metadataSource === undefined ? metaContext : undefined);
|
|
189
204
|
const inputModalities = Array.isArray(model.inputModalities) && model.inputModalities.length > 0
|
|
190
205
|
? model.inputModalities
|
|
191
206
|
: (Array.isArray(meta?.input) && meta.input.length > 0 ? meta.input : undefined);
|
|
207
|
+
const maxInputTokens = typeof model.maxInputTokens === "number" && model.maxInputTokens > 0
|
|
208
|
+
? model.maxInputTokens
|
|
209
|
+
: undefined;
|
|
210
|
+
const source = model.metadataSource;
|
|
211
|
+
const stampSource = source === "live" || source === "registry" || source === "snapshot" || source === "derived";
|
|
212
|
+
|
|
213
|
+
if (contextWindow === undefined && inputModalities === undefined
|
|
214
|
+
&& !(Array.isArray(model.capabilities) && model.capabilities.length > 0)) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
192
217
|
|
|
193
218
|
entry.opencodex_capability_provenance = {
|
|
194
219
|
provider: model.provider,
|
|
195
220
|
model_id: model.id,
|
|
196
221
|
...(contextWindow !== undefined ? { context_window: contextWindow } : {}),
|
|
222
|
+
...(maxInputTokens !== undefined && stampModelContext ? { max_input_tokens: maxInputTokens } : {}),
|
|
197
223
|
...(inputModalities !== undefined ? { input_modalities: [...inputModalities] } : {}),
|
|
198
224
|
...(Array.isArray(model.capabilities) && model.capabilities.length > 0
|
|
199
225
|
? { capabilities: [...model.capabilities] }
|
|
200
226
|
: {}),
|
|
227
|
+
...(stampSource ? { source } : {}),
|
|
228
|
+
...(typeof model.metadataObservedAt === "string" && stampSource
|
|
229
|
+
? { observed_at: model.metadataObservedAt }
|
|
230
|
+
: {}),
|
|
201
231
|
};
|
|
202
232
|
}
|
|
203
233
|
|
|
@@ -14,6 +14,7 @@ import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, r
|
|
|
14
14
|
import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
|
|
15
15
|
import { getProviderRegistryEntry, providerCodexAccountMode } from "../../providers/registry";
|
|
16
16
|
import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
|
|
17
|
+
import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget";
|
|
17
18
|
import { routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec";
|
|
18
19
|
import { identifyRoutedModel } from "../../adapters/identity";
|
|
19
20
|
import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
|
|
@@ -204,6 +205,8 @@ export interface NativeContextLimits {
|
|
|
204
205
|
readonly providerWindow?: number;
|
|
205
206
|
/** `providers.openai.modelContextWindows` — per-model, wins over `providerWindow`. */
|
|
206
207
|
readonly modelWindows?: Readonly<Record<string, number>>;
|
|
208
|
+
/** `providers.openai.modelAutoCompactTokenLimits` — soft, lowering-only budgets. */
|
|
209
|
+
readonly modelAutoCompactTokenLimits?: Readonly<Record<string, number>>;
|
|
207
210
|
}
|
|
208
211
|
|
|
209
212
|
export type NativeContextLimitsInput = NativeContextLimits | number | undefined;
|
|
@@ -227,12 +230,18 @@ export function nativeContextLimits(
|
|
|
227
230
|
const window = positiveInt(value);
|
|
228
231
|
if (window !== undefined) modelWindows[slug] = window;
|
|
229
232
|
}
|
|
233
|
+
const modelAutoCompactTokenLimits: Record<string, number> = {};
|
|
234
|
+
for (const [slug, value] of Object.entries(provider?.modelAutoCompactTokenLimits ?? {})) {
|
|
235
|
+
const budget = positiveInt(value);
|
|
236
|
+
if (budget !== undefined) modelAutoCompactTokenLimits[slug] = budget;
|
|
237
|
+
}
|
|
230
238
|
return {
|
|
231
239
|
...(positiveInt(providerContextCap(config, OPENAI_CODEX_PROVIDER_ID)) !== undefined
|
|
232
240
|
? { cap: providerContextCap(config, OPENAI_CODEX_PROVIDER_ID) }
|
|
233
241
|
: {}),
|
|
234
242
|
...(positiveInt(provider?.contextWindow) !== undefined ? { providerWindow: provider!.contextWindow } : {}),
|
|
235
243
|
...(Object.keys(modelWindows).length > 0 ? { modelWindows } : {}),
|
|
244
|
+
...(Object.keys(modelAutoCompactTokenLimits).length > 0 ? { modelAutoCompactTokenLimits } : {}),
|
|
236
245
|
};
|
|
237
246
|
}
|
|
238
247
|
|
|
@@ -277,6 +286,21 @@ export function nativeOpenAiMaxInputTokens(slug: string, limits?: NativeContextL
|
|
|
277
286
|
return window === undefined ? narrowed : Math.min(narrowed, window);
|
|
278
287
|
}
|
|
279
288
|
|
|
289
|
+
/** Effective native soft budget after every hard window/input limit is resolved. */
|
|
290
|
+
export function nativeOpenAiAutoCompactTokenLimit(
|
|
291
|
+
slug: string,
|
|
292
|
+
limits?: NativeContextLimitsInput,
|
|
293
|
+
): number | undefined {
|
|
294
|
+
const contextWindow = nativeOpenAiContextWindow(slug, limits);
|
|
295
|
+
if (contextWindow === undefined) return undefined;
|
|
296
|
+
const configured = positiveInt(asLimits(limits).modelAutoCompactTokenLimits?.[slug]);
|
|
297
|
+
return clampAutoCompactTokenLimit(
|
|
298
|
+
contextWindow,
|
|
299
|
+
nativeOpenAiMaxInputTokens(slug, limits),
|
|
300
|
+
configured,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
280
304
|
export function nativeInputModalities(slug: string): string[] {
|
|
281
305
|
const upstream = PINNED_NATIVE_CAPABILITY_ENTRIES.get(slug);
|
|
282
306
|
if (Array.isArray(upstream?.input_modalities) && upstream!.input_modalities!.length > 0) {
|
|
@@ -387,7 +411,7 @@ export function desktopVisibleNativeSlugs(
|
|
|
387
411
|
]);
|
|
388
412
|
}
|
|
389
413
|
|
|
390
|
-
export function nativeModelRows(config: Pick<OcxConfig, "disabledModels" | "combos" | "providerContextCaps" | "providers">): Array<{ slug: string; disabled: boolean; contextWindow?: number; maxInputTokens?: number }> {
|
|
414
|
+
export function nativeModelRows(config: Pick<OcxConfig, "disabledModels" | "combos" | "providerContextCaps" | "providers">): Array<{ slug: string; disabled: boolean; contextWindow?: number; maxInputTokens?: number; autoCompactTokenLimit?: number }> {
|
|
391
415
|
const disabled = disabledNativeSlugs(config);
|
|
392
416
|
const shadowed = configuredNativeAliasSlugs(config);
|
|
393
417
|
// Both user levers, not just the cap: a per-model window set from the dashboard has to show
|
|
@@ -403,11 +427,13 @@ export function nativeModelRows(config: Pick<OcxConfig, "disabledModels" | "comb
|
|
|
403
427
|
.filter(slug => !shadowed.has(slug)).map(slug => {
|
|
404
428
|
const contextWindow = nativeOpenAiContextWindow(slug, limits);
|
|
405
429
|
const maxInputTokens = nativeOpenAiMaxInputTokens(slug, limits);
|
|
430
|
+
const autoCompactTokenLimit = nativeOpenAiAutoCompactTokenLimit(slug, limits);
|
|
406
431
|
return {
|
|
407
432
|
slug,
|
|
408
433
|
disabled: disabled.has(slug),
|
|
409
434
|
...(contextWindow !== undefined ? { contextWindow } : {}),
|
|
410
435
|
...(maxInputTokens !== undefined ? { maxInputTokens } : {}),
|
|
436
|
+
...(autoCompactTokenLimit !== undefined ? { autoCompactTokenLimit } : {}),
|
|
411
437
|
};
|
|
412
438
|
});
|
|
413
439
|
}
|