@bitkyc08/opencodex 2.29.0 → 2.31.0-preview.20260822

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 (68) hide show
  1. package/README.md +5 -5
  2. package/gui/dist/assets/index-DyWYnr-t.js +102 -0
  3. package/gui/dist/index.html +1 -1
  4. package/package.json +3 -3
  5. package/src/adapters/cursor/cursor-errors.ts +65 -6
  6. package/src/adapters/cursor/discovery.ts +29 -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/images.ts +704 -0
  10. package/src/adapters/cursor/live-models.ts +21 -26
  11. package/src/adapters/cursor/live-transport.ts +239 -8
  12. package/src/adapters/cursor/native-exec-common.ts +17 -0
  13. package/src/adapters/cursor/native-exec.ts +9 -4
  14. package/src/adapters/cursor/protobuf-events.ts +5 -1
  15. package/src/adapters/cursor/protobuf-request.ts +46 -9
  16. package/src/adapters/cursor/request-builder.ts +29 -14
  17. package/src/adapters/cursor/tool-definitions.ts +20 -0
  18. package/src/adapters/cursor/transport.ts +10 -0
  19. package/src/adapters/cursor/types.ts +8 -1
  20. package/src/adapters/cursor.ts +23 -5
  21. package/src/adapters/google.ts +16 -3
  22. package/src/adapters/openai-responses.ts +66 -20
  23. package/src/adapters/xai-web-search.ts +185 -0
  24. package/src/cli/agent.ts +2 -1
  25. package/src/cli/dispatch.ts +2 -2
  26. package/src/cli/doctor.ts +89 -0
  27. package/src/cli/help.ts +2 -0
  28. package/src/cli/registry.ts +7 -2
  29. package/src/codex/auth-context.ts +41 -2
  30. package/src/codex/catalog/effort.ts +1 -1
  31. package/src/codex/catalog/parsing.ts +2 -0
  32. package/src/codex/catalog/provider-fetch.ts +20 -5
  33. package/src/codex/coordinator-doctor.ts +332 -0
  34. package/src/codex/features.ts +58 -0
  35. package/src/codex/inject-coordination.ts +39 -6
  36. package/src/codex/transition-state.ts +12 -12
  37. package/src/generated/compatibility-version.json +86 -58
  38. package/src/lib/bun-stream-caps.ts +7 -4
  39. package/src/lib/errors.ts +8 -2
  40. package/src/oauth/cursor.ts +21 -0
  41. package/src/providers/command-code-efforts.ts +7 -0
  42. package/src/providers/cursor-pool.ts +72 -0
  43. package/src/providers/derive.ts +3 -0
  44. package/src/providers/fastwire.ts +12 -1
  45. package/src/providers/openai-sidecar.ts +1 -0
  46. package/src/providers/quota.ts +98 -25
  47. package/src/providers/registry.ts +115 -10
  48. package/src/providers/service-tier.ts +22 -7
  49. package/src/responses/custom-tool-compat.ts +24 -8
  50. package/src/responses/namespace-tool-compat.ts +2 -3
  51. package/src/router.ts +3 -0
  52. package/src/server/chat-completions.ts +4 -0
  53. package/src/server/chat-native.ts +20 -0
  54. package/src/server/management/agent-settings-routes.ts +16 -5
  55. package/src/server/management/config-routes.ts +25 -5
  56. package/src/server/management/vision-sidecar-options.ts +54 -19
  57. package/src/server/responses/compact.ts +1 -2
  58. package/src/server/responses/core.ts +54 -13
  59. package/src/service.ts +122 -14
  60. package/src/types/config.ts +9 -3
  61. package/src/types/provider.ts +6 -0
  62. package/src/usage/cost.ts +52 -38
  63. package/src/usage/expected-prices.ts +79 -9
  64. package/src/vision/backends.ts +97 -0
  65. package/src/vision/eligibility.ts +43 -22
  66. package/src/vision/index.ts +73 -5
  67. package/src/vision/routed-describe.ts +175 -0
  68. package/gui/dist/assets/index-BNESwCzn.js +0 -102
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
 
@@ -181,6 +181,30 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
181
181
  { provider: "cursor", modelId: "auto", cost4: { input: 1.25, output: 6, cacheRead: 0.25, cacheWrite: 1.25 }, source: "https://docs.cursor.com/account/pricing + https://cursor.com/blog/aug-2025-pricing", verifiedAt: "2026-07-20", status: "verified" },
182
182
  ];
183
183
 
184
+ /**
185
+ * Exact official corrections for stale nonzero catalog rows. These are intentionally separate
186
+ * from fallback overlays: they win over the bundled row only for the declared provider/model and
187
+ * therefore cannot reprice routed resellers that reuse the same model slug.
188
+ */
189
+ export const VERIFIED_PRICE_OVERRIDES: readonly ExpectedPriceOverlay[] = [
190
+ {
191
+ provider: "xai",
192
+ modelId: "grok-4.6",
193
+ cost4: { input: 2, output: 6, cacheRead: 0.5, cacheWrite: 0 },
194
+ source: "https://docs.x.ai/developers/pricing",
195
+ verifiedAt: "2026-08-18",
196
+ status: "verified",
197
+ },
198
+ ];
199
+
200
+ export function findVerifiedPriceOverride(
201
+ provider: string,
202
+ modelId: string,
203
+ overrides: readonly ExpectedPriceOverlay[] = VERIFIED_PRICE_OVERRIDES,
204
+ ): ExpectedPriceOverlay | undefined {
205
+ return overrides.find(row => row.provider === provider && row.modelId === modelId);
206
+ }
207
+
184
208
  /**
185
209
  * Exact-key overlay lookup. Returns verified first, then verified-derived.
186
210
  * NEVER returns "unverified" rows — fail-closed is enforced in code, not just docs.
@@ -196,12 +220,7 @@ export function findExpectedPriceOverlay(
196
220
  ?? exact.find(row => row.status === "verified-derived");
197
221
  }
198
222
 
199
- /**
200
- * OpenAI Fast mode (`service_tier=priority`) price multipliers by model slug.
201
- * Source: https://openai.com/api-fast-mode/ (2026-07-31).
202
- * Fast pricing applies uniformly to all token types (input, output, cache).
203
- * Models not listed here fall back to 1× (no multiplier).
204
- */
223
+ /** OpenAI Fast price multipliers retained as a compatibility export. */
205
224
  export const PRIORITY_MULTIPLIERS: Readonly<Record<string, number>> = {
206
225
  "gpt-5.6-sol": 2,
207
226
  // Post-price-cut Fast tables (https://openai.com/api-fast-mode/, 2026-08-05):
@@ -219,6 +238,52 @@ export function resolvePriorityMultiplier(modelId: string): number {
219
238
  return PRIORITY_MULTIPLIERS[modelId] ?? 1;
220
239
  }
221
240
 
241
+ export interface PriorityPricingRule {
242
+ provider: string;
243
+ modelId: string;
244
+ multiplier: number;
245
+ /** Apply the premium only after the upstream response confirms this tier. */
246
+ requiresResponseConfirmation?: true;
247
+ source: string;
248
+ verifiedAt: string;
249
+ }
250
+
251
+ const OPENAI_FAST_PRICING = "https://openai.com/api-fast-mode/";
252
+ const XAI_PRIORITY_PRICING = "https://docs.x.ai/developers/advanced-api-usage/priority-processing";
253
+
254
+ /**
255
+ * Exact provider/model priority premiums. Routed resellers never inherit a vendor rule merely
256
+ * because they reuse its model slug. Multipliers apply uniformly after cache discounts.
257
+ */
258
+ export const PRIORITY_PRICING_RULES: readonly PriorityPricingRule[] = [
259
+ ...["openai", "openai-apikey"].flatMap(provider =>
260
+ Object.entries(PRIORITY_MULTIPLIERS).map(([modelId, multiplier]): PriorityPricingRule => ({
261
+ provider,
262
+ modelId,
263
+ multiplier,
264
+ source: OPENAI_FAST_PRICING,
265
+ verifiedAt: "2026-08-05",
266
+ })),
267
+ ),
268
+ ...["grok-4.5", "grok-4.6"].map((modelId): PriorityPricingRule => ({
269
+ provider: "xai",
270
+ modelId,
271
+ multiplier: 2,
272
+ requiresResponseConfirmation: true,
273
+ source: XAI_PRIORITY_PRICING,
274
+ verifiedAt: "2026-08-18",
275
+ })),
276
+ ];
277
+
278
+ /** Exact provider/model priority-pricing lookup. */
279
+ export function findPriorityPricingRule(
280
+ provider: string,
281
+ modelId: string,
282
+ rules: readonly PriorityPricingRule[] = PRIORITY_PRICING_RULES,
283
+ ): PriorityPricingRule | undefined {
284
+ return rules.find(rule => rule.provider === provider && rule.modelId === modelId);
285
+ }
286
+
222
287
  /**
223
288
  * Long-context pricing tiers (#908). Several vendors reprice the ENTIRE request
224
289
  * once the prompt crosses a published input-token threshold, so a flat Cost4
@@ -244,6 +309,8 @@ export interface ContextTier {
244
309
  inclusive: boolean;
245
310
  /** Per-field factor from the short rate to the published long rate. */
246
311
  multiplier: Cost4;
312
+ /** Published relationship between confirmed priority and long-context bands. */
313
+ confirmedPriorityRelation?: "exclusive" | "lower-bound";
247
314
  source: string;
248
315
  verifiedAt: string;
249
316
  }
@@ -277,6 +344,7 @@ export const CONTEXT_TIERS: readonly ContextTier[] = [
277
344
  thresholdInputTokens: 272_000,
278
345
  inclusive: false,
279
346
  multiplier: OPENAI_LONG_CONTEXT,
347
+ confirmedPriorityRelation: "exclusive",
280
348
  source: OPENAI_PRICING_DOC,
281
349
  verifiedAt: "2026-08-03",
282
350
  })),
@@ -287,19 +355,21 @@ export const CONTEXT_TIERS: readonly ContextTier[] = [
287
355
  thresholdInputTokens: 200_000,
288
356
  inclusive: true,
289
357
  multiplier: UNIFORM_DOUBLE,
358
+ confirmedPriorityRelation: "lower-bound",
290
359
  source: "https://docs.x.ai/developers/pricing",
291
360
  verifiedAt: "2026-08-03",
292
361
  },
293
362
  {
294
- // 260813: grok-4.6 long-context tier mirrored from grok-4.5; the official pricing row
295
- // was not yet published when the model page went up, so treat as provisional.
363
+ // xAI publishes the whole-request >=200k band for grok-4.6. Its combination with
364
+ // Priority Processing is not published, so confirmed priority uses this row as a lower bound.
296
365
  provider: "xai",
297
366
  modelId: "grok-4.6",
298
367
  thresholdInputTokens: 200_000,
299
368
  inclusive: true,
300
369
  multiplier: UNIFORM_DOUBLE,
370
+ confirmedPriorityRelation: "lower-bound",
301
371
  source: "https://docs.x.ai/developers/pricing",
302
- verifiedAt: "2026-08-13",
372
+ verifiedAt: "2026-08-18",
303
373
  },
304
374
  {
305
375
  // daybreak-blue-latest aliases gpt-5.6-sol, which publishes the full long-context row
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Which backends may DESCRIBE images for the vision sidecar, and which
3
+ * candidate rows each can describe through (#2188 vision rules; roadmap 170
4
+ * REVISED: the "routed" backend).
5
+ *
6
+ * A SIBLING of WEB_SEARCH_BACKENDS, not a shared table: vision has no
7
+ * per-model probe (rule 2 is "− provably text-only", enforced by
8
+ * modelAcceptsImageInput, not here), carries per-side baseline models, and
9
+ * excludes non-LLM backends like exa.
10
+ *
11
+ * Three backends, not one per provider: "openai" and "anthropic" carry auth
12
+ * semantics loopback routing cannot replicate (forwarded ChatGPT headers,
13
+ * OAuth beta fences) and their defaults must not drift. Every OTHER
14
+ * picker-visible provider row reaches the describer through "routed" — a
15
+ * loopback self-fetch of the proxy's own /v1/chat/completions, where the
16
+ * router and adapters already speak each provider's wire. That is what makes
17
+ * this table closed under provider growth: a new provider needs no new
18
+ * describe executor.
19
+ */
20
+ import type { OcxConfig } from "../types";
21
+ import type { SidecarAuthState } from "../sidecar/auth";
22
+ import { listOpenAiForwardSidecarCandidates } from "../providers/openai-sidecar";
23
+ import type { VisionCandidateModel, VisionSidecarBackend } from "./eligibility";
24
+
25
+ export interface VisionBackendDescriptor {
26
+ backend: VisionSidecarBackend;
27
+ /** Liveness signal for this backend. */
28
+ isActive(auth: SidecarAuthState, config: OcxConfig): boolean;
29
+ /** Which candidate rows this backend's describe executor can actually run. */
30
+ candidateMatch(candidate: VisionCandidateModel, auth: SidecarAuthState): boolean;
31
+ /**
32
+ * Default entry for this side: cheap, image-capable, present in every
33
+ * deployment. Only the two universal sides carry one — "routed" has no
34
+ * universal model to name.
35
+ */
36
+ baseline?: string;
37
+ /** Stable option ordering (baselines first within a side). */
38
+ rank: number;
39
+ }
40
+
41
+ export const VISION_BACKENDS: readonly VisionBackendDescriptor[] = [
42
+ {
43
+ backend: "openai",
44
+ // The OpenAI describer needs a CANONICAL ChatGPT forward provider, not
45
+ // merely a provider keyed "openai" — same predicate the runtime sidecar
46
+ // resolver uses. Deliberately NOT auth.isCodexAuth: tightening to a live
47
+ // credential here would change which options a fresh install sees, and
48
+ // the options list is a suggestion surface, not the write gate.
49
+ isActive: (_auth, config) => listOpenAiForwardSidecarCandidates(config).length > 0,
50
+ candidateMatch: candidate => candidate.native === true || candidate.provider === "openai",
51
+ baseline: "gpt-5.6-luna",
52
+ rank: 0,
53
+ },
54
+ {
55
+ backend: "anthropic",
56
+ isActive: auth => auth.isAnthropicAuth,
57
+ // The runtime dispatches through exactly ONE Anthropic provider — the
58
+ // resolved OAuth row. Same-adapter keyed rows are unreachable (see
59
+ // visionBackendForCandidate's original stance).
60
+ candidateMatch: (candidate, auth) => candidate.provider === auth.anthropicProviderName,
61
+ baseline: "claude-haiku-4-5",
62
+ rank: 1,
63
+ },
64
+ {
65
+ backend: "routed",
66
+ // Always offered: options only materialize when a matching picker row
67
+ // exists, and the row's own provider config is the liveness signal — the
68
+ // loopback request fails closed through ordinary routing errors.
69
+ isActive: () => true,
70
+ // Any row the other two executors do NOT own. Auth-slot rows are
71
+ // entitlements of the openai/anthropic sides and never route here.
72
+ candidateMatch: (candidate, auth) =>
73
+ candidate.native !== true
74
+ && candidate.provider !== "openai"
75
+ && candidate.provider !== auth.anthropicProviderName,
76
+ rank: 2,
77
+ },
78
+ ];
79
+
80
+ export function visionBackendDescriptor(backend: VisionSidecarBackend): VisionBackendDescriptor {
81
+ const descriptor = VISION_BACKENDS.find(entry => entry.backend === backend);
82
+ if (!descriptor) throw new Error(`unknown vision backend "${backend}"`);
83
+ return descriptor;
84
+ }
85
+
86
+ /**
87
+ * The active backend set for option generation. Falls back to the two
88
+ * UNIVERSAL sides when neither is active (fresh install: picker stays
89
+ * populated, permissive-unknown rule); "routed" is active by construction.
90
+ */
91
+ export function activeVisionBackends(auth: SidecarAuthState, config: OcxConfig): VisionSidecarBackend[] {
92
+ const active = VISION_BACKENDS.filter(entry => entry.isActive(auth, config)).map(entry => entry.backend);
93
+ return active.includes("openai") || active.includes("anthropic")
94
+ ? active
95
+ : ["openai", "anthropic", ...active.filter(backend => backend === "routed")];
96
+ }
97
+