@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.
Files changed (61) hide show
  1. package/README.md +1 -1
  2. package/gui/dist/assets/index-DkcRs1fL.js +102 -0
  3. package/gui/dist/index.html +1 -1
  4. package/package.json +1 -1
  5. package/src/adapters/cursor/cursor-errors.ts +65 -6
  6. package/src/adapters/cursor/discovery.ts +7 -2
  7. package/src/adapters/cursor/effort-map.ts +6 -0
  8. package/src/adapters/cursor/h2-pool.ts +123 -0
  9. package/src/adapters/cursor/live-models.ts +21 -26
  10. package/src/adapters/cursor/live-transport.ts +213 -3
  11. package/src/adapters/cursor/native-exec-common.ts +17 -0
  12. package/src/adapters/cursor/native-exec.ts +9 -4
  13. package/src/adapters/cursor/protobuf-events.ts +5 -1
  14. package/src/adapters/cursor/protobuf-request.ts +11 -4
  15. package/src/adapters/cursor/tool-definitions.ts +20 -0
  16. package/src/adapters/cursor/transport.ts +10 -0
  17. package/src/adapters/cursor.ts +23 -5
  18. package/src/adapters/google.ts +16 -3
  19. package/src/adapters/openai-responses.ts +66 -20
  20. package/src/adapters/xai-web-search.ts +185 -0
  21. package/src/cli/agent.ts +2 -1
  22. package/src/cli/dispatch.ts +2 -2
  23. package/src/cli/doctor.ts +89 -0
  24. package/src/cli/help.ts +2 -0
  25. package/src/cli/registry.ts +7 -2
  26. package/src/codex/auth-context.ts +41 -2
  27. package/src/codex/catalog/effort.ts +1 -1
  28. package/src/codex/catalog/parsing.ts +2 -0
  29. package/src/codex/catalog/provider-fetch.ts +20 -5
  30. package/src/codex/coordinator-doctor.ts +332 -0
  31. package/src/codex/inject-coordination.ts +39 -6
  32. package/src/codex/transition-state.ts +12 -12
  33. package/src/generated/compatibility-version.json +74 -50
  34. package/src/lib/errors.ts +8 -2
  35. package/src/oauth/cursor.ts +21 -0
  36. package/src/providers/cursor-pool.ts +72 -0
  37. package/src/providers/derive.ts +3 -0
  38. package/src/providers/fastwire.ts +12 -1
  39. package/src/providers/openai-sidecar.ts +1 -0
  40. package/src/providers/registry.ts +25 -0
  41. package/src/providers/service-tier.ts +22 -7
  42. package/src/responses/custom-tool-compat.ts +24 -8
  43. package/src/responses/namespace-tool-compat.ts +2 -3
  44. package/src/router.ts +3 -0
  45. package/src/server/chat-completions.ts +4 -0
  46. package/src/server/chat-native.ts +20 -0
  47. package/src/server/management/agent-settings-routes.ts +16 -5
  48. package/src/server/management/config-routes.ts +25 -5
  49. package/src/server/management/vision-sidecar-options.ts +54 -19
  50. package/src/server/responses/compact.ts +1 -2
  51. package/src/server/responses/core.ts +54 -13
  52. package/src/service.ts +122 -14
  53. package/src/types/config.ts +9 -3
  54. package/src/types/provider.ts +6 -0
  55. package/src/usage/cost.ts +52 -38
  56. package/src/usage/expected-prices.ts +79 -9
  57. package/src/vision/backends.ts +97 -0
  58. package/src/vision/eligibility.ts +43 -22
  59. package/src/vision/index.ts +73 -5
  60. package/src/vision/routed-describe.ts +175 -0
  61. 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: compactThreadId,
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: req.headers.get("x-codex-parent-thread-id"),
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
- const threadId = req.headers.get("x-codex-parent-thread-id");
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
- threadId,
2332
+ poolAffinityKey,
2299
2333
  config,
2300
2334
  Date.now(),
2301
- undefined,
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
- const visionPlan = planVisionSidecar(config, route.provider, route.modelId, parsed, openAiSidecar);
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: req.headers.get("x-codex-parent-thread-id"),
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: req.headers.get("x-codex-parent-thread-id"),
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 parsed = parseServiceArgs(args.filter((a): a is string => Boolean(a)));
3312
- const command = parsed.sub;
3313
- if (parsed.invalid.length > 0) {
3314
- console.error(`Unknown service option: ${parsed.invalid.join(" ")}`);
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/updates and starts the background service.");
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
  }
@@ -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
- /** Description backend. Unset prefers a usable stored Anthropic OAuth credential, else OpenAI. */
778
- backend?: "openai" | "anthropic";
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. */
@@ -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
- resolvePriorityMultiplier,
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 OpenAI priority-tier multiplier (undefined or 1 = standard). */
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 OpenAI priority-tier multiplier (undefined or 1 = standard). */
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, then the expected-price overlay, then the
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
- * Skipped entirely for a response-confirmed Fast request: OpenAI does not serve
409
- * long context in Fast mode, so the two are mutually exclusive regimes rather
410
- * than composable multipliers.
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 the OpenAI priority-tier multiplier to a Cost4 when applicable.
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
- * - provider is not a canonical OpenAI forward provider
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
- if (!OPENAI_TIER_PROVIDER_IDS.has(base)) return [cost4, 1];
447
- const multiplier = resolvePriorityMultiplier(modelId);
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
- // Exclusive both ways: if the long rate applied, the request was NOT served as
502
- // Fast (Fast does not support long context), so the Fast multiplier must not
503
- // also apply otherwise a downgraded request bills at both rates.
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 = isOpenRouterPriorityLowerBound(attempt.provider, attempt.tierOutcome);
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