@bitkyc08/opencodex 2.30.0-preview.20260821 → 2.31.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 +1 -1
- package/gui/dist/assets/index-DkcRs1fL.js +102 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/cursor/cursor-errors.ts +65 -6
- package/src/adapters/cursor/discovery.ts +7 -2
- package/src/adapters/cursor/effort-map.ts +6 -0
- package/src/adapters/cursor/h2-pool.ts +123 -0
- package/src/adapters/cursor/live-models.ts +21 -26
- package/src/adapters/cursor/live-transport.ts +213 -3
- package/src/adapters/cursor/native-exec-common.ts +17 -0
- package/src/adapters/cursor/native-exec.ts +9 -4
- package/src/adapters/cursor/protobuf-events.ts +5 -1
- package/src/adapters/cursor/protobuf-request.ts +11 -4
- package/src/adapters/cursor/tool-definitions.ts +20 -0
- package/src/adapters/cursor/transport.ts +10 -0
- package/src/adapters/cursor.ts +23 -5
- package/src/adapters/google.ts +16 -3
- package/src/adapters/openai-responses.ts +66 -20
- package/src/adapters/xai-web-search.ts +185 -0
- package/src/cli/agent.ts +2 -1
- package/src/cli/dispatch.ts +2 -2
- package/src/cli/doctor.ts +89 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/registry.ts +7 -2
- package/src/codex/auth-context.ts +41 -2
- package/src/codex/catalog/effort.ts +1 -1
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-fetch.ts +20 -5
- package/src/codex/coordinator-doctor.ts +332 -0
- package/src/codex/inject-coordination.ts +39 -6
- package/src/codex/transition-state.ts +12 -12
- package/src/generated/compatibility-version.json +74 -50
- package/src/lib/errors.ts +8 -2
- package/src/oauth/cursor.ts +21 -0
- package/src/providers/cursor-pool.ts +72 -0
- package/src/providers/derive.ts +3 -0
- package/src/providers/fastwire.ts +12 -1
- package/src/providers/openai-sidecar.ts +1 -0
- package/src/providers/registry.ts +25 -0
- package/src/providers/service-tier.ts +22 -7
- package/src/responses/custom-tool-compat.ts +24 -8
- package/src/responses/namespace-tool-compat.ts +2 -3
- package/src/router.ts +3 -0
- package/src/server/chat-completions.ts +4 -0
- package/src/server/chat-native.ts +20 -0
- package/src/server/management/agent-settings-routes.ts +16 -5
- package/src/server/management/config-routes.ts +25 -5
- package/src/server/management/vision-sidecar-options.ts +54 -19
- package/src/server/responses/compact.ts +1 -2
- package/src/server/responses/core.ts +54 -13
- package/src/service.ts +122 -14
- package/src/types/config.ts +9 -3
- package/src/types/provider.ts +6 -0
- package/src/usage/cost.ts +52 -38
- package/src/usage/expected-prices.ts +79 -9
- package/src/vision/backends.ts +97 -0
- package/src/vision/eligibility.ts +43 -22
- package/src/vision/index.ts +73 -5
- package/src/vision/routed-describe.ts +175 -0
- package/gui/dist/assets/index-eBA05kYB.js +0 -102
|
@@ -461,7 +461,6 @@ export async function handleResponsesCompact(
|
|
|
461
461
|
}
|
|
462
462
|
compactHostAdmissionLease = null;
|
|
463
463
|
};
|
|
464
|
-
const compactThreadId = req.headers.get("x-codex-parent-thread-id");
|
|
465
464
|
const connectMs = config.connectTimeoutMs ?? 200_000;
|
|
466
465
|
// Takes its context explicitly: the alternate-account flow below records a rejection
|
|
467
466
|
// against A while promoting B, then records B's own outcome. A closure over a single
|
|
@@ -478,7 +477,7 @@ export async function handleResponsesCompact(
|
|
|
478
477
|
if (!usesCodexForwardPoolAuth(ctx, route.provider)) return;
|
|
479
478
|
recordCodexUpstreamOutcome(config, ctx.accountId, outcome, {
|
|
480
479
|
...meta,
|
|
481
|
-
threadId:
|
|
480
|
+
threadId: ctx.kind === "pool" || ctx.kind === "main-pool" ? ctx.affinityKey : undefined,
|
|
482
481
|
fixedAccount: ctx.fixedAccount,
|
|
483
482
|
modelId: selectedModelId,
|
|
484
483
|
probeLeaseId: codexProbeLeaseId(ctx),
|
|
@@ -111,6 +111,7 @@ import { describeImagesInPlace, isModelTextOnly, planVisionSidecar, resolveOpenA
|
|
|
111
111
|
import { createAdapterEventQueue, preflightAdapterEvents, type AdapterEventQueue } from "../../adapters/run-turn-queue";
|
|
112
112
|
import {
|
|
113
113
|
applyCodexAuthContextToProvider,
|
|
114
|
+
codexPoolAffinityKey,
|
|
114
115
|
CodexAccountCooldownError,
|
|
115
116
|
codexMainProfileDrainingResponse,
|
|
116
117
|
cooldownErrorResponse,
|
|
@@ -139,6 +140,7 @@ import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "../../codex/catalog/native-m
|
|
|
139
140
|
import { captureCodexAffinityDiagnostic } from "../../codex/affinity-debug";
|
|
140
141
|
import {
|
|
141
142
|
computeQuotaCooldown,
|
|
143
|
+
codexQuotaScopeForModel,
|
|
142
144
|
formatCodexProviderForLog,
|
|
143
145
|
previewCodexAccountForRequest,
|
|
144
146
|
recordCodexUpstreamOutcome,
|
|
@@ -328,11 +330,10 @@ export function adapterNeedsForcedContinuation(name: string): boolean {
|
|
|
328
330
|
export function sidecarOutcomeRecorder(
|
|
329
331
|
config: OcxConfig,
|
|
330
332
|
authCtx: CodexAuthContext,
|
|
331
|
-
threadId?: string | null,
|
|
332
333
|
): ((outcome: CodexUpstreamOutcome) => void) | undefined {
|
|
333
334
|
return authCtx.kind === "pool" || authCtx.kind === "main-pool"
|
|
334
335
|
? outcome => recordCodexUpstreamOutcome(config, authCtx.accountId, outcome, {
|
|
335
|
-
threadId,
|
|
336
|
+
threadId: authCtx.affinityKey,
|
|
336
337
|
fixedAccount: authCtx.fixedAccount,
|
|
337
338
|
probeLeaseId: authCtx.probeLeaseId,
|
|
338
339
|
probeQuotaScope: authCtx.probeQuotaScope,
|
|
@@ -946,7 +947,7 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
946
947
|
const recordFirstOutcome = (): void => {
|
|
947
948
|
recordCodexUpstreamOutcome(config, firstAuthCtx.accountId, outcomeStatus, {
|
|
948
949
|
...quotaMeta,
|
|
949
|
-
threadId:
|
|
950
|
+
threadId: firstAuthCtx.affinityKey,
|
|
950
951
|
modelId: route.modelId,
|
|
951
952
|
probeLeaseId: codexProbeLeaseId(firstAuthCtx),
|
|
952
953
|
probeQuotaScope: codexProbeQuotaScope(firstAuthCtx),
|
|
@@ -1081,7 +1082,6 @@ export function codexForwardTerminalOutcomeRecorder(
|
|
|
1081
1082
|
provider: OcxProviderConfig,
|
|
1082
1083
|
modelId?: string,
|
|
1083
1084
|
logCtx?: RequestLogContext,
|
|
1084
|
-
threadId?: string | null,
|
|
1085
1085
|
): ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined {
|
|
1086
1086
|
if (!usesCodexForwardPoolAuth(authCtx, provider)) return undefined;
|
|
1087
1087
|
return (status, httpStatusOverride) => {
|
|
@@ -1090,7 +1090,7 @@ export function codexForwardTerminalOutcomeRecorder(
|
|
|
1090
1090
|
// request. Don't penalize account health; record success to clear any
|
|
1091
1091
|
// prior soft-avoid so a healthy account isn't stuck avoided.
|
|
1092
1092
|
recordCodexUpstreamOutcome(config, authCtx.accountId, 200, {
|
|
1093
|
-
threadId,
|
|
1093
|
+
threadId: authCtx.affinityKey,
|
|
1094
1094
|
fixedAccount: authCtx.fixedAccount,
|
|
1095
1095
|
modelId,
|
|
1096
1096
|
probeLeaseId: codexProbeLeaseId(authCtx),
|
|
@@ -1112,7 +1112,7 @@ export function codexForwardTerminalOutcomeRecorder(
|
|
|
1112
1112
|
? 200
|
|
1113
1113
|
: (httpStatusOverride ?? logCtx?.terminalHttpStatus ?? 502);
|
|
1114
1114
|
recordCodexUpstreamOutcome(config, authCtx.accountId, outcome, {
|
|
1115
|
-
threadId,
|
|
1115
|
+
threadId: authCtx.affinityKey,
|
|
1116
1116
|
fixedAccount: authCtx.fixedAccount,
|
|
1117
1117
|
modelId,
|
|
1118
1118
|
probeLeaseId: codexProbeLeaseId(authCtx),
|
|
@@ -1227,6 +1227,15 @@ export interface HandleResponsesOptions {
|
|
|
1227
1227
|
onConsumedComboFailure?: (failure: ConsumedComboFailure) => void;
|
|
1228
1228
|
/** Caller-owned for Chat/Claude replay; omitted only at genuine Responses ingress. */
|
|
1229
1229
|
translatorBudget?: TranslatorBudget;
|
|
1230
|
+
/**
|
|
1231
|
+
* Terminal vision-describe marker (roadmap 180): true when the inbound
|
|
1232
|
+
* request IS the vision sidecar's own loopback describe call. The plan site
|
|
1233
|
+
* then STRIPS images instead of planning another describe — a depth cap of 1
|
|
1234
|
+
* that holds under predicate drift and combo re-resolution. The Chat surface
|
|
1235
|
+
* detects the raw `x-opencodex-vision-describe` header before its bridge
|
|
1236
|
+
* rebuilds headers and carries the fact through this flag.
|
|
1237
|
+
*/
|
|
1238
|
+
visionDescribeTerminal?: boolean;
|
|
1230
1239
|
}
|
|
1231
1240
|
|
|
1232
1241
|
|
|
@@ -2177,6 +2186,27 @@ async function handleResponsesInner(
|
|
|
2177
2186
|
if (inboundClientThreadId) {
|
|
2178
2187
|
parsed._clientThreadId = inboundClientThreadId;
|
|
2179
2188
|
parsed._reasoningReplayScope = { clientThreadId: inboundClientThreadId };
|
|
2189
|
+
} else if (
|
|
2190
|
+
options.inboundWire === "anthropic"
|
|
2191
|
+
&& options.promptCacheKeyIsSharedCohort !== true
|
|
2192
|
+
&& typeof parsed.options.promptCacheKey === "string"
|
|
2193
|
+
&& parsed.options.promptCacheKey.trim().length > 0
|
|
2194
|
+
) {
|
|
2195
|
+
// Claude Code has no Codex parent-thread header, but its metadata.user_id is
|
|
2196
|
+
// translated into a stable per-session prompt_cache_key. Use it as the replay
|
|
2197
|
+
// thread identity so Gemini thought signatures are remembered by call_id for
|
|
2198
|
+
// Anthropic Messages clients too (#1735/#1926). Keep `_clientThreadId` unset so
|
|
2199
|
+
// existing provider session-id derivation (first-user-text fallback) is unchanged.
|
|
2200
|
+
// Normalize through anthropicSessionKeyFromParts so overlong keys are hashed and
|
|
2201
|
+
// trimming matches the affinity/session-key path exactly (no raw >128-char ids).
|
|
2202
|
+
const normalizedCacheKey = anthropicSessionKeyFromParts({
|
|
2203
|
+
promptCacheKey: parsed.options.promptCacheKey,
|
|
2204
|
+
// The enclosing branch already proves this is not the shared cohort.
|
|
2205
|
+
promptCacheKeyIsSharedCohort: false,
|
|
2206
|
+
});
|
|
2207
|
+
if (normalizedCacheKey) {
|
|
2208
|
+
parsed._reasoningReplayScope = { clientThreadId: normalizedCacheKey };
|
|
2209
|
+
}
|
|
2180
2210
|
}
|
|
2181
2211
|
} catch (err) {
|
|
2182
2212
|
if (isTranslatorBudgetExceededError(err)) {
|
|
@@ -2278,6 +2308,7 @@ async function handleResponsesInner(
|
|
|
2278
2308
|
let subagentFallbackPreviewAccountId: string | null | undefined;
|
|
2279
2309
|
let subagentQuotaFailureModel = parsed.modelId;
|
|
2280
2310
|
const parentThreadId = req.headers.get("x-codex-parent-thread-id")?.trim() ?? null;
|
|
2311
|
+
const poolAffinityKey = codexPoolAffinityKey(req.headers) ?? null;
|
|
2281
2312
|
|
|
2282
2313
|
try {
|
|
2283
2314
|
if (
|
|
@@ -2293,12 +2324,15 @@ async function handleResponsesInner(
|
|
|
2293
2324
|
// Preview the preferred Codex account without acquiring a probe lease or refreshing
|
|
2294
2325
|
// tokens — auth is resolved only after the final route is selected.
|
|
2295
2326
|
if (threadSpawn && !options.comboAttempt && route.codexAccountId === undefined) {
|
|
2296
|
-
|
|
2327
|
+
// The final resolveCodexAuthContext binds under codexQuotaScopeForModel(route.modelId),
|
|
2328
|
+
// so the preview must read the same scope slot — an undefined scope would map to the
|
|
2329
|
+
// "legacy" affinity bucket and never find a binding made under "shared" or a native
|
|
2330
|
+
// model scope, making the preview diverge from the account that actually authenticates.
|
|
2297
2331
|
const previewAccountId = previewCodexAccountForRequest(
|
|
2298
|
-
|
|
2332
|
+
poolAffinityKey,
|
|
2299
2333
|
config,
|
|
2300
2334
|
Date.now(),
|
|
2301
|
-
|
|
2335
|
+
codexQuotaScopeForModel(route.modelId),
|
|
2302
2336
|
previewSelectionOptions,
|
|
2303
2337
|
);
|
|
2304
2338
|
subagentFallbackPreviewAccountId = previewAccountId;
|
|
@@ -2725,7 +2759,15 @@ async function handleResponsesInner(
|
|
|
2725
2759
|
// Vision sidecar: the routed model can't see images (provider.noVisionModels). Describe each
|
|
2726
2760
|
// attached image through the selected sidecar backend and replace it with text BEFORE the main
|
|
2727
2761
|
// call, so the text-only model can reason about it.
|
|
2728
|
-
|
|
2762
|
+
// Terminal describe fence (roadmap 180): the sidecar's OWN loopback describe
|
|
2763
|
+
// call must never plan another describe. The flag arrives from the Chat
|
|
2764
|
+
// surface (whose bridge rebuilds headers) or as the raw header for native
|
|
2765
|
+
// Responses callers. Marked + text-only routed model → strip, depth cap 1.
|
|
2766
|
+
const visionDescribeTerminal = options.visionDescribeTerminal === true
|
|
2767
|
+
|| req.headers.get("x-opencodex-vision-describe") === "1";
|
|
2768
|
+
const visionPlan = visionDescribeTerminal
|
|
2769
|
+
? undefined
|
|
2770
|
+
: planVisionSidecar(config, route.provider, route.modelId, parsed, openAiSidecar);
|
|
2729
2771
|
const recordSidecarOutcome = openAiSidecar?.recordOutcome;
|
|
2730
2772
|
if (visionPlan) {
|
|
2731
2773
|
await describeImagesInPlace(
|
|
@@ -3005,7 +3047,7 @@ async function handleResponsesInner(
|
|
|
3005
3047
|
}
|
|
3006
3048
|
if (usesCodexForwardPoolAuth(authCtx, route.provider)) {
|
|
3007
3049
|
recordCodexUpstreamOutcome(config, authCtx.accountId, outcome, {
|
|
3008
|
-
threadId:
|
|
3050
|
+
threadId: authCtx.affinityKey,
|
|
3009
3051
|
fixedAccount: authCtx.fixedAccount,
|
|
3010
3052
|
modelId: route.modelId,
|
|
3011
3053
|
probeLeaseId: codexProbeLeaseId(authCtx),
|
|
@@ -3388,7 +3430,6 @@ async function handleResponsesInner(
|
|
|
3388
3430
|
route.provider,
|
|
3389
3431
|
route.modelId,
|
|
3390
3432
|
logCtx,
|
|
3391
|
-
req.headers.get("x-codex-parent-thread-id"),
|
|
3392
3433
|
);
|
|
3393
3434
|
const terminalBodyWillRecord = !!terminalRecorder && upstreamResponse.ok && isEventStream;
|
|
3394
3435
|
// Capture quota from upstream response for multi-account tracking
|
|
@@ -3429,7 +3470,7 @@ async function handleResponsesInner(
|
|
|
3429
3470
|
)) {
|
|
3430
3471
|
recordCodexUpstreamOutcome(config, authCtx.accountId, upstreamResponse.status, {
|
|
3431
3472
|
...quotaMeta,
|
|
3432
|
-
threadId:
|
|
3473
|
+
threadId: authCtx.affinityKey,
|
|
3433
3474
|
fixedAccount: authCtx.fixedAccount,
|
|
3434
3475
|
modelId: route.modelId,
|
|
3435
3476
|
probeLeaseId: codexProbeLeaseId(authCtx),
|
package/src/service.ts
CHANGED
|
@@ -3274,6 +3274,7 @@ export async function serviceStatusReport(
|
|
|
3274
3274
|
}
|
|
3275
3275
|
|
|
3276
3276
|
export function normalizeServiceSubcommand(sub?: string): string {
|
|
3277
|
+
if (sub === "restart") return "repair";
|
|
3277
3278
|
return sub ?? "install";
|
|
3278
3279
|
}
|
|
3279
3280
|
|
|
@@ -3283,6 +3284,119 @@ export interface ParsedServiceArgs {
|
|
|
3283
3284
|
invalid: string[];
|
|
3284
3285
|
}
|
|
3285
3286
|
|
|
3287
|
+
export type ServiceInstallationState = "installed" | "absent" | "unknown";
|
|
3288
|
+
|
|
3289
|
+
export interface ServiceInstallationProbe {
|
|
3290
|
+
state: ServiceInstallationState;
|
|
3291
|
+
detail?: string;
|
|
3292
|
+
}
|
|
3293
|
+
|
|
3294
|
+
export interface ServiceInstallationProbeHooks {
|
|
3295
|
+
platform?: NodeJS.Platform;
|
|
3296
|
+
exists?: (path: string) => boolean;
|
|
3297
|
+
probeWindowsTask?: () => WindowsSchedulerTaskProbe;
|
|
3298
|
+
nativeStatus?: () => WinswStatus;
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
/**
|
|
3302
|
+
* Read only enough registration state to choose between install and repair.
|
|
3303
|
+
* Windows must keep query failure distinct from proven absence: treating an
|
|
3304
|
+
* unreadable scheduler/SCM as absent would send a bare command into the
|
|
3305
|
+
* elevated registration path and recreate the original #2287 failure.
|
|
3306
|
+
*/
|
|
3307
|
+
export function probeServiceInstallation(
|
|
3308
|
+
hooks: ServiceInstallationProbeHooks = {},
|
|
3309
|
+
): ServiceInstallationProbe {
|
|
3310
|
+
const platform = hooks.platform ?? process.platform;
|
|
3311
|
+
const exists = hooks.exists ?? existsSync;
|
|
3312
|
+
if (platform === "darwin") {
|
|
3313
|
+
return { state: exists(plistPath()) ? "installed" : "absent" };
|
|
3314
|
+
}
|
|
3315
|
+
if (platform === "linux") {
|
|
3316
|
+
return { state: exists(unitPath()) ? "installed" : "absent" };
|
|
3317
|
+
}
|
|
3318
|
+
if (platform !== "win32") return { state: "absent" };
|
|
3319
|
+
|
|
3320
|
+
let scheduler: WindowsSchedulerTaskProbe;
|
|
3321
|
+
try {
|
|
3322
|
+
scheduler = (hooks.probeWindowsTask ?? probeWindowsSchedulerTask)();
|
|
3323
|
+
} catch (cause) {
|
|
3324
|
+
scheduler = { status: "unknown", detail: schtasksErrorDetail(cause) };
|
|
3325
|
+
}
|
|
3326
|
+
let native: WinswStatus;
|
|
3327
|
+
try {
|
|
3328
|
+
native = (hooks.nativeStatus ?? statusWinswRaw)();
|
|
3329
|
+
} catch {
|
|
3330
|
+
native = "unknown";
|
|
3331
|
+
}
|
|
3332
|
+
|
|
3333
|
+
if (scheduler.status === "present" || native === "started" || native === "stopped") {
|
|
3334
|
+
return { state: "installed" };
|
|
3335
|
+
}
|
|
3336
|
+
if (scheduler.status === "unknown" || native === "unknown") {
|
|
3337
|
+
const parts = [
|
|
3338
|
+
scheduler.status === "unknown" ? `Task Scheduler: ${scheduler.detail}` : null,
|
|
3339
|
+
native === "unknown" ? "WinSW status could not be determined" : null,
|
|
3340
|
+
].filter((part): part is string => Boolean(part));
|
|
3341
|
+
return { state: "unknown", detail: parts.join("; ") };
|
|
3342
|
+
}
|
|
3343
|
+
return { state: "absent" };
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
/**
|
|
3347
|
+
* A bare invocation is an idempotent "make the installed service current"
|
|
3348
|
+
* operation. First-time setup still installs, but an existing registration must
|
|
3349
|
+
* use the repair path so Windows does not re-run the elevated `schtasks /create`.
|
|
3350
|
+
* Backend flags remain an explicit install request because they select which
|
|
3351
|
+
* registration mechanism to create.
|
|
3352
|
+
*/
|
|
3353
|
+
export function selectServiceSubcommand(
|
|
3354
|
+
parsed: ParsedServiceArgs,
|
|
3355
|
+
options: { hasExplicitSubcommand: boolean; installed: boolean },
|
|
3356
|
+
): string {
|
|
3357
|
+
if (!options.hasExplicitSubcommand && parsed.backend === null && options.installed) return "repair";
|
|
3358
|
+
return parsed.sub;
|
|
3359
|
+
}
|
|
3360
|
+
|
|
3361
|
+
export type ServiceCommandPlan =
|
|
3362
|
+
| { ok: true; parsed: ParsedServiceArgs; command: string }
|
|
3363
|
+
| { ok: false; message: string };
|
|
3364
|
+
|
|
3365
|
+
export function planServiceCommand(
|
|
3366
|
+
args: string[],
|
|
3367
|
+
options: { platform?: NodeJS.Platform; probeInstallation?: () => ServiceInstallationProbe } = {},
|
|
3368
|
+
): ServiceCommandPlan {
|
|
3369
|
+
const parsed = parseServiceArgs(args);
|
|
3370
|
+
if (parsed.invalid.length > 0) {
|
|
3371
|
+
return { ok: false, message: `Unknown service option: ${parsed.invalid.join(" ")}` };
|
|
3372
|
+
}
|
|
3373
|
+
if (parsed.backend && parsed.sub !== "install") {
|
|
3374
|
+
return { ok: false, message: "--native/--scheduler apply to `ocx service install` only; other subcommands use the installed backend." };
|
|
3375
|
+
}
|
|
3376
|
+
if (parsed.backend === "native" && (options.platform ?? process.platform) !== "win32") {
|
|
3377
|
+
return { ok: false, message: "--native (WinSW) is Windows-only." };
|
|
3378
|
+
}
|
|
3379
|
+
|
|
3380
|
+
const hasExplicitSubcommand = args.some(arg => !arg.startsWith("--"));
|
|
3381
|
+
let installed = false;
|
|
3382
|
+
if (!hasExplicitSubcommand && parsed.backend === null) {
|
|
3383
|
+
const probe = (options.probeInstallation ?? probeServiceInstallation)();
|
|
3384
|
+
if (probe.state === "unknown") {
|
|
3385
|
+
const suffix = probe.detail ? ` (${probe.detail})` : "";
|
|
3386
|
+
return {
|
|
3387
|
+
ok: false,
|
|
3388
|
+
message: `Could not safely determine whether the service is installed${suffix}. Run 'ocx service status' and retry; use explicit 'ocx service install' only after confirming it is absent.`,
|
|
3389
|
+
};
|
|
3390
|
+
}
|
|
3391
|
+
installed = probe.state === "installed";
|
|
3392
|
+
}
|
|
3393
|
+
return {
|
|
3394
|
+
ok: true,
|
|
3395
|
+
parsed,
|
|
3396
|
+
command: selectServiceSubcommand(parsed, { hasExplicitSubcommand, installed }),
|
|
3397
|
+
};
|
|
3398
|
+
}
|
|
3399
|
+
|
|
3286
3400
|
/**
|
|
3287
3401
|
* `ocx service [sub] [--native|--scheduler]`. The first non-flag token is the
|
|
3288
3402
|
* subcommand; backend flags are only meaningful for `install` (validated by the caller).
|
|
@@ -3308,20 +3422,13 @@ export function parseServiceArgs(args: string[]): ParsedServiceArgs {
|
|
|
3308
3422
|
}
|
|
3309
3423
|
|
|
3310
3424
|
export async function serviceCommand(...args: (string | undefined)[]): Promise<void> {
|
|
3311
|
-
const
|
|
3312
|
-
const
|
|
3313
|
-
if (
|
|
3314
|
-
console.error(
|
|
3315
|
-
process.exit(1);
|
|
3316
|
-
}
|
|
3317
|
-
if (parsed.backend && command !== "install") {
|
|
3318
|
-
console.error("--native/--scheduler apply to `ocx service install` only; other subcommands use the installed backend.");
|
|
3319
|
-
process.exit(1);
|
|
3320
|
-
}
|
|
3321
|
-
if (parsed.backend === "native" && process.platform !== "win32") {
|
|
3322
|
-
console.error("--native (WinSW) is Windows-only.");
|
|
3425
|
+
const filteredArgs = args.filter((a): a is string => Boolean(a));
|
|
3426
|
+
const plan = planServiceCommand(filteredArgs);
|
|
3427
|
+
if (!plan.ok) {
|
|
3428
|
+
console.error(plan.message);
|
|
3323
3429
|
process.exit(1);
|
|
3324
3430
|
}
|
|
3431
|
+
const { parsed, command } = plan;
|
|
3325
3432
|
if (command === "repair") {
|
|
3326
3433
|
assertServiceEnvironmentMatchesInstall();
|
|
3327
3434
|
assertServiceAuthEnvironment();
|
|
@@ -3458,9 +3565,10 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
3458
3565
|
console.log("✅ service uninstalled.");
|
|
3459
3566
|
break;
|
|
3460
3567
|
default:
|
|
3461
|
-
console.error("Usage: ocx service [install|repair|start|stop|status|uninstall|remove] [--native|--scheduler]");
|
|
3462
|
-
console.error(" With no subcommand, installs
|
|
3568
|
+
console.error("Usage: ocx service [install|repair|restart|start|stop|status|uninstall|remove] [--native|--scheduler]");
|
|
3569
|
+
console.error(" With no subcommand, installs when absent or repairs/restarts an existing service.");
|
|
3463
3570
|
console.error(" repair: refresh assets and restart an already-installed service (no admin re-prompt).");
|
|
3571
|
+
console.error(" restart: alias of repair.");
|
|
3464
3572
|
console.error(" --native (Windows only): register a real SCM service via WinSW instead of Task Scheduler.");
|
|
3465
3573
|
process.exit(1);
|
|
3466
3574
|
}
|
package/src/types/config.ts
CHANGED
|
@@ -130,7 +130,7 @@ export interface OcxClaudeCodeConfig {
|
|
|
130
130
|
/** Claude-originated web-search override. Unset fields inherit the global sidecar settings. */
|
|
131
131
|
webSearchSidecar?: { backend?: "openai" | "anthropic" | "xai" | "gemini" | "exa"; model?: string };
|
|
132
132
|
/** Claude-originated vision override. Unset fields inherit the global sidecar settings. */
|
|
133
|
-
visionSidecar?: { backend?: "openai" | "anthropic"; model?: string };
|
|
133
|
+
visionSidecar?: { backend?: "openai" | "anthropic" | "routed"; model?: string };
|
|
134
134
|
/** Persisted Claude Desktop four-family routing profile. */
|
|
135
135
|
desktopProfile?: OcxClaudeDesktopProfile;
|
|
136
136
|
/** Auto-reconcile Desktop 3P config when provider catalog changes. Default: enabled. */
|
|
@@ -774,8 +774,14 @@ export interface OcxSearchConfig {
|
|
|
774
774
|
export interface OcxVisionSidecarConfig {
|
|
775
775
|
/** Master switch. Default: enabled when the selected backend has a usable credential. */
|
|
776
776
|
enabled?: boolean;
|
|
777
|
-
/**
|
|
778
|
-
|
|
777
|
+
/**
|
|
778
|
+
* Description backend. Unset prefers a usable stored Anthropic OAuth credential, else OpenAI —
|
|
779
|
+
* the historical default order, deliberately unchanged by the union widening (#2188 roadmap
|
|
780
|
+
* 170/180 revised): "routed" describes through the proxy's OWN routing (loopback
|
|
781
|
+
* /v1/chat/completions) with a NAMESPACED "provider/model" describer, is explicit-only, and is
|
|
782
|
+
* never auto-selected from credential availability.
|
|
783
|
+
*/
|
|
784
|
+
backend?: "openai" | "anthropic" | "routed";
|
|
779
785
|
/** Vision model that describes images. */
|
|
780
786
|
model?: string;
|
|
781
787
|
/** Max description cache misses admitted in one main-model turn. Zero disables description calls. */
|
package/src/types/provider.ts
CHANGED
|
@@ -352,6 +352,12 @@ export interface OcxProviderConfig {
|
|
|
352
352
|
* passthrough compatibility for OpenAI and unclassified gateways.
|
|
353
353
|
*/
|
|
354
354
|
supportsOpenAiWebSearchToolFields?: boolean;
|
|
355
|
+
/**
|
|
356
|
+
* Whether the Responses upstream accepts native custom tools and custom_tool_call items.
|
|
357
|
+
* Set false only for a provider whose native contract rejects them; absence preserves
|
|
358
|
+
* apply_patch passthrough compatibility for OpenAI and unclassified gateways.
|
|
359
|
+
*/
|
|
360
|
+
supportsResponsesCustomTools?: boolean;
|
|
355
361
|
/**
|
|
356
362
|
* Provider-local repair for Responses gateways whose lifecycle snapshots omit canonical
|
|
357
363
|
* fields or closing events (#893). Disabled by default and applied only to client-facing
|
package/src/usage/cost.ts
CHANGED
|
@@ -22,7 +22,8 @@ import { activeConfiguredProviders, activeUserCostOverlays, userCostOverlayVersi
|
|
|
22
22
|
import {
|
|
23
23
|
EXPECTED_PRICE_OVERLAYS,
|
|
24
24
|
findExpectedPriceOverlay,
|
|
25
|
-
|
|
25
|
+
findVerifiedPriceOverride,
|
|
26
|
+
findPriorityPricingRule,
|
|
26
27
|
findContextTier,
|
|
27
28
|
isLongContext,
|
|
28
29
|
type Cost4,
|
|
@@ -81,12 +82,12 @@ export interface AttemptCostEstimate {
|
|
|
81
82
|
price: MatchedPrice;
|
|
82
83
|
cost: CostBreakdown;
|
|
83
84
|
estimated: boolean;
|
|
84
|
-
/** Applied
|
|
85
|
+
/** Applied provider priority-tier multiplier (undefined or 1 = standard). */
|
|
85
86
|
priorityMultiplier?: number;
|
|
86
|
-
/** Standard-price estimate is a known floor for a confirmed, unpriced priority endpoint. */
|
|
87
|
-
priorityLowerBound?: boolean;
|
|
88
87
|
/** Set when the published long-context rate was applied (#908). */
|
|
89
88
|
contextTier?: ContextTierName;
|
|
89
|
+
/** The numeric estimate is a known floor because the exact Priority price is unavailable. */
|
|
90
|
+
priorityLowerBound?: boolean;
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
export interface CostEstimate {
|
|
@@ -95,12 +96,12 @@ export interface CostEstimate {
|
|
|
95
96
|
estimated: boolean;
|
|
96
97
|
attempts?: AttemptCostEstimate[];
|
|
97
98
|
price?: MatchedPrice;
|
|
98
|
-
/** Applied
|
|
99
|
+
/** Applied provider priority-tier multiplier (undefined or 1 = standard). */
|
|
99
100
|
priorityMultiplier?: number;
|
|
100
|
-
/** Standard-price estimate is a known floor for a confirmed, unpriced priority endpoint. */
|
|
101
|
-
priorityLowerBound?: boolean;
|
|
102
101
|
/** Set when any priced attempt used the published long-context rate (#908). */
|
|
103
102
|
contextTier?: ContextTierName;
|
|
103
|
+
/** The aggregate is a known floor because every priced attempt is a lower bound. */
|
|
104
|
+
priorityLowerBound?: boolean;
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
function finiteNonNegative(value: number): boolean {
|
|
@@ -236,7 +237,7 @@ function resolveMatchedPriceInner(
|
|
|
236
237
|
|
|
237
238
|
/**
|
|
238
239
|
* Exact provider/model price lookup: user-configured `modelCosts` first, then
|
|
239
|
-
* the jawcode provider bundle,
|
|
240
|
+
* an exact official correction, the jawcode provider bundle, the expected-price overlay, then the
|
|
240
241
|
* model-level vendor fallback. All-zero rows fall through ("not billable").
|
|
241
242
|
*/
|
|
242
243
|
function resolveMatchedPriceExact(
|
|
@@ -249,6 +250,20 @@ function resolveMatchedPriceExact(
|
|
|
249
250
|
// operator's explicit price is authoritative for the ~$ estimate.
|
|
250
251
|
const userOverlay = userOverlayMatch(provider, modelId, userOverlays);
|
|
251
252
|
if (userOverlay) return userOverlay;
|
|
253
|
+
const verifiedOverride = overlays === EXPECTED_PRICE_OVERLAYS
|
|
254
|
+
? findVerifiedPriceOverride(provider, modelId)
|
|
255
|
+
: undefined;
|
|
256
|
+
if (verifiedOverride && validCost4(verifiedOverride.cost4) && hasNonZeroCost(verifiedOverride.cost4)) {
|
|
257
|
+
return {
|
|
258
|
+
provider,
|
|
259
|
+
modelId,
|
|
260
|
+
cost4: verifiedOverride.cost4,
|
|
261
|
+
source: "expected",
|
|
262
|
+
sourceRef: verifiedOverride.source,
|
|
263
|
+
verifiedAt: verifiedOverride.verifiedAt,
|
|
264
|
+
status: "verified",
|
|
265
|
+
};
|
|
266
|
+
}
|
|
252
267
|
const metadataProvider = resolveMetadataProvider(provider);
|
|
253
268
|
const bundled = metadataProvider
|
|
254
269
|
? getModelMetadata(metadataProvider, modelId)
|
|
@@ -320,13 +335,6 @@ function isEstimated(usage: OcxUsage, usageStatus: UsageStatus, priceStatus: Exp
|
|
|
320
335
|
return usage.estimated === true || usageStatus === "estimated" || priceStatus === "verified-derived";
|
|
321
336
|
}
|
|
322
337
|
|
|
323
|
-
/**
|
|
324
|
-
* OpenAI provider ids eligible for service_tier "priority" price multipliers.
|
|
325
|
-
* Only canonical OpenAI forward providers use the priority tier; routed providers
|
|
326
|
-
* (OpenRouter, Cursor, etc.) may share model slugs but have independent pricing.
|
|
327
|
-
*/
|
|
328
|
-
const OPENAI_TIER_PROVIDER_IDS = new Set(["openai", "openai-apikey"]);
|
|
329
|
-
|
|
330
338
|
/**
|
|
331
339
|
* Resolve the effective service tier from persisted log fields.
|
|
332
340
|
* Priority: responseServiceTier (server-confirmed) > requestedServiceTier
|
|
@@ -405,9 +413,9 @@ function isConfirmedFast(tier?: ServiceTierInput): boolean {
|
|
|
405
413
|
* normalized billable input — normalization subtracts cache read/write, so a
|
|
406
414
|
* cache-heavy long prompt would fall below the boundary and under-bill.
|
|
407
415
|
*
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
416
|
+
* A provider's declaration decides how a response-confirmed priority tier relates to this band.
|
|
417
|
+
* OpenAI declares the bands exclusive. xAI publishes neither a combined rate nor an exclusion,
|
|
418
|
+
* so its long-context rate remains the known lower bound instead of inventing a stacked multiplier.
|
|
411
419
|
*/
|
|
412
420
|
function applyContextTier(
|
|
413
421
|
cost4: Cost4,
|
|
@@ -415,25 +423,27 @@ function applyContextTier(
|
|
|
415
423
|
modelId: string,
|
|
416
424
|
rawInputTokens: number | undefined,
|
|
417
425
|
tier?: ServiceTierInput,
|
|
418
|
-
): [Cost4, ContextTierName | undefined] {
|
|
419
|
-
if (rawInputTokens === undefined) return [cost4, undefined];
|
|
420
|
-
if (isConfirmedFast(tier)) return [cost4, undefined];
|
|
426
|
+
): [Cost4, ContextTierName | undefined, boolean] {
|
|
427
|
+
if (rawInputTokens === undefined) return [cost4, undefined, false];
|
|
421
428
|
const rule = findContextTier(baseProviderLabel(provider), modelId);
|
|
422
|
-
if (!rule || !isLongContext(rule, rawInputTokens)) return [cost4, undefined];
|
|
429
|
+
if (!rule || !isLongContext(rule, rawInputTokens)) return [cost4, undefined, false];
|
|
430
|
+
const confirmedFast = isConfirmedFast(tier);
|
|
431
|
+
if (confirmedFast && rule.confirmedPriorityRelation === "exclusive") {
|
|
432
|
+
return [cost4, undefined, false];
|
|
433
|
+
}
|
|
423
434
|
return [{
|
|
424
435
|
input: cost4.input * rule.multiplier.input,
|
|
425
436
|
output: cost4.output * rule.multiplier.output,
|
|
426
437
|
cacheRead: cost4.cacheRead * rule.multiplier.cacheRead,
|
|
427
438
|
cacheWrite: cost4.cacheWrite * rule.multiplier.cacheWrite,
|
|
428
|
-
}, "long"];
|
|
439
|
+
}, "long", confirmedFast && rule.confirmedPriorityRelation === "lower-bound"];
|
|
429
440
|
}
|
|
430
441
|
|
|
431
442
|
/**
|
|
432
|
-
* Apply
|
|
443
|
+
* Apply a declared provider/model priority-tier multiplier to a Cost4 when applicable.
|
|
433
444
|
* Returns [effectiveCost4, multiplier]. Multiplier is 1 (no-op) when:
|
|
434
445
|
* - serviceTier is not "priority"
|
|
435
|
-
* -
|
|
436
|
-
* - model is not in PRIORITY_MULTIPLIERS
|
|
446
|
+
* - no exact provider/model rule exists
|
|
437
447
|
*/
|
|
438
448
|
function applyPriorityMultiplier(
|
|
439
449
|
cost4: Cost4,
|
|
@@ -443,8 +453,9 @@ function applyPriorityMultiplier(
|
|
|
443
453
|
): [Cost4, number] {
|
|
444
454
|
if (tierScalar(serviceTier) !== "priority") return [cost4, 1];
|
|
445
455
|
const base = baseProviderLabel(provider);
|
|
446
|
-
|
|
447
|
-
|
|
456
|
+
const rule = findPriorityPricingRule(base, modelId);
|
|
457
|
+
if (rule?.requiresResponseConfirmation && !isConfirmedFast(serviceTier)) return [cost4, 1];
|
|
458
|
+
const multiplier = rule?.multiplier ?? 1;
|
|
448
459
|
if (multiplier === 1) return [cost4, 1];
|
|
449
460
|
return [{
|
|
450
461
|
input: cost4.input * multiplier,
|
|
@@ -495,16 +506,17 @@ export function estimateAttemptCost(
|
|
|
495
506
|
const attemptServiceTier = attempt.tierOutcome
|
|
496
507
|
? serviceTierContextFromOutcome(attempt.tierOutcome)
|
|
497
508
|
: serviceTier;
|
|
498
|
-
const [tieredCost4, contextTier] = applyContextTier(
|
|
509
|
+
const [tieredCost4, contextTier, contextPriorityLowerBound] = applyContextTier(
|
|
499
510
|
price.cost4, attempt.provider, attempt.model, attempt.usage.inputTokens, attemptServiceTier,
|
|
500
511
|
);
|
|
501
|
-
//
|
|
502
|
-
//
|
|
503
|
-
//
|
|
512
|
+
// A published long-context row owns the numeric estimate. OpenAI declares that band
|
|
513
|
+
// exclusive with Fast; xAI's confirmed combination is deliberately left unmultiplied
|
|
514
|
+
// and marked as a lower bound because no combined price has been published.
|
|
504
515
|
const [effectiveCost4, multiplier] = contextTier
|
|
505
516
|
? [tieredCost4, 1] as const
|
|
506
517
|
: applyPriorityMultiplier(tieredCost4, attempt.provider, attempt.model, attemptServiceTier);
|
|
507
|
-
const priorityLowerBound =
|
|
518
|
+
const priorityLowerBound = contextPriorityLowerBound
|
|
519
|
+
|| isOpenRouterPriorityLowerBound(attempt.provider, attempt.tierOutcome);
|
|
508
520
|
return {
|
|
509
521
|
ordinal: attempt.ordinal,
|
|
510
522
|
provider: attempt.provider,
|
|
@@ -514,8 +526,8 @@ export function estimateAttemptCost(
|
|
|
514
526
|
cost: calculateCost(tokens, effectiveCost4),
|
|
515
527
|
estimated: isEstimated(attempt.usage, attempt.usageStatus, price.status),
|
|
516
528
|
...(multiplier !== 1 ? { priorityMultiplier: multiplier } : {}),
|
|
517
|
-
...(priorityLowerBound ? { priorityLowerBound: true } : {}),
|
|
518
529
|
...(contextTier ? { contextTier } : {}),
|
|
530
|
+
...(priorityLowerBound ? { priorityLowerBound: true } : {}),
|
|
519
531
|
};
|
|
520
532
|
}
|
|
521
533
|
|
|
@@ -557,8 +569,10 @@ export function estimateComboCost(
|
|
|
557
569
|
...(estimates.some(est => est.priorityMultiplier && est.priorityMultiplier !== 1)
|
|
558
570
|
? { priorityMultiplier: estimates.find(est => est.priorityMultiplier)?.priorityMultiplier }
|
|
559
571
|
: {}),
|
|
560
|
-
...(estimates.some(est => est.priorityLowerBound) ? { priorityLowerBound: true } : {}),
|
|
561
572
|
...(estimates.some(est => est.contextTier) ? { contextTier: "long" as const } : {}),
|
|
573
|
+
...(estimates.every(est => est.priorityLowerBound === true)
|
|
574
|
+
? { priorityLowerBound: true as const }
|
|
575
|
+
: {}),
|
|
562
576
|
};
|
|
563
577
|
}
|
|
564
578
|
|
|
@@ -579,13 +593,13 @@ export function estimateRequestCost(
|
|
|
579
593
|
if (!tokens) return null;
|
|
580
594
|
const price = resolveMatchedPrice(input.provider, input.model, overlays, userOverlays);
|
|
581
595
|
if (!price) return null;
|
|
582
|
-
const [tieredCost4, contextTier] = applyContextTier(
|
|
596
|
+
const [tieredCost4, contextTier, contextPriorityLowerBound] = applyContextTier(
|
|
583
597
|
price.cost4, input.provider, input.model, input.usage.inputTokens, input.serviceTier,
|
|
584
598
|
);
|
|
585
599
|
const [effectiveCost4, multiplier] = contextTier
|
|
586
600
|
? [tieredCost4, 1] as const
|
|
587
601
|
: applyPriorityMultiplier(tieredCost4, input.provider, input.model, input.serviceTier);
|
|
588
|
-
const priorityLowerBound = isOpenRouterPriorityLowerBound(
|
|
602
|
+
const priorityLowerBound = contextPriorityLowerBound || isOpenRouterPriorityLowerBound(
|
|
589
603
|
input.provider,
|
|
590
604
|
typeof input.serviceTier === "object" ? input.serviceTier.tierOutcome : undefined,
|
|
591
605
|
);
|
|
@@ -595,8 +609,8 @@ export function estimateRequestCost(
|
|
|
595
609
|
cost: calculateCost(tokens, effectiveCost4),
|
|
596
610
|
estimated: isEstimated(input.usage, input.usageStatus, price.status),
|
|
597
611
|
...(multiplier !== 1 ? { priorityMultiplier: multiplier } : {}),
|
|
598
|
-
...(priorityLowerBound ? { priorityLowerBound: true } : {}),
|
|
599
612
|
...(contextTier ? { contextTier } : {}),
|
|
613
|
+
...(priorityLowerBound ? { priorityLowerBound: true } : {}),
|
|
600
614
|
};
|
|
601
615
|
}
|
|
602
616
|
|