@coseung2/opencodex 2.8.0-cs.13 → 2.8.0-cs.15

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 (60) hide show
  1. package/gui/dist/assets/index-BhXIu7c0.js +67 -0
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +3 -3
  4. package/packages/ocx-notch/README.md +2 -1
  5. package/src/adapters/cursor/discovery.ts +6 -2
  6. package/src/adapters/cursor/effort-map.ts +3 -0
  7. package/src/adapters/google-antigravity-replay.ts +24 -0
  8. package/src/adapters/google.ts +16 -11
  9. package/src/chat/inbound.ts +5 -11
  10. package/src/cli/account-api.ts +9 -1
  11. package/src/cli/account-extended.ts +4 -1
  12. package/src/codex/account-label.ts +14 -1
  13. package/src/codex/account-lifecycle.ts +12 -1
  14. package/src/codex/account-namespaces.ts +21 -0
  15. package/src/codex/account-priority.ts +49 -0
  16. package/src/codex/account-store.ts +2 -1
  17. package/src/codex/auth-api.ts +108 -17
  18. package/src/codex/auth-context.ts +61 -16
  19. package/src/codex/catalog/metadata.ts +34 -12
  20. package/src/codex/catalog/parsing.ts +8 -1
  21. package/src/codex/catalog/provider-fetch.ts +24 -6
  22. package/src/codex/catalog.ts +1 -1
  23. package/src/codex/pool-rotation.ts +51 -4
  24. package/src/codex/quota.ts +154 -35
  25. package/src/codex/routing.ts +139 -33
  26. package/src/codex/warmup.ts +193 -85
  27. package/src/config.ts +84 -1
  28. package/src/lib/bounded-body.ts +13 -6
  29. package/src/lib/bun-stream-caps.ts +5 -6
  30. package/src/lib/redact.ts +13 -0
  31. package/src/oauth/index.ts +79 -12
  32. package/src/oauth/log.ts +3 -1
  33. package/src/oauth/store.ts +31 -8
  34. package/src/providers/antigravity-models.ts +53 -24
  35. package/src/providers/codex-capacity.ts +303 -0
  36. package/src/providers/model-rename-migration.ts +147 -0
  37. package/src/providers/model-rename-startup.ts +29 -0
  38. package/src/providers/quota.ts +126 -16
  39. package/src/providers/registry.ts +258 -38
  40. package/src/responses/parser.ts +19 -12
  41. package/src/responses/spill-store.ts +14 -1
  42. package/src/responses/state.ts +108 -14
  43. package/src/server/index.ts +9 -1
  44. package/src/server/management/logs-usage-routes.ts +1 -0
  45. package/src/server/management/oauth-account-routes.ts +8 -1
  46. package/src/server/relay.ts +10 -42
  47. package/src/server/request-log.ts +42 -1
  48. package/src/server/responses/compact.ts +16 -4
  49. package/src/server/responses/core.ts +217 -59
  50. package/src/server/responses/empty-completion-guard.ts +275 -0
  51. package/src/server/responses/encrypted-payload.ts +54 -39
  52. package/src/server/responses/fetch-helpers.ts +24 -3
  53. package/src/server/responses/ws-upstream.ts +318 -0
  54. package/src/server/sse-frame-buffer.ts +292 -0
  55. package/src/server/ws-bridge.ts +17 -11
  56. package/src/types.ts +8 -0
  57. package/src/usage/log.ts +24 -0
  58. package/src/usage/summary.ts +152 -2
  59. package/vendor/ocx-notch/win32-x64/ocx-notch.exe +0 -0
  60. package/gui/dist/assets/index-BucjyD4I.js +0 -67
@@ -2,6 +2,11 @@ import { randomUUID } from "node:crypto";
2
2
  import { saveConfigPreservingClaudeCode } from "../config";
3
3
  import { isCodexAccountGenerationLive, readCodexAccountRecord } from "./account-store";
4
4
  import { codexAccountLogLabel } from "./account-label";
5
+ import {
6
+ clearCodexAccountPin,
7
+ codexAccountPriorityLookup,
8
+ pinnedCodexAccountId,
9
+ } from "./account-priority";
5
10
  import { isCodexAccountPaused } from "./account-pause";
6
11
  import { isCodexAccountUsable } from "./account-usability";
7
12
  import { isAccountNeedsReauth, markAccountNeedsReauth } from "./account-runtime-state";
@@ -13,9 +18,10 @@ import {
13
18
  notePoolRotationSuccess,
14
19
  peekRoundRobinAccount,
15
20
  pickRoundRobinAccount,
21
+ selectPriorityTier,
16
22
  seedPoolRotationAccount,
17
23
  } from "./pool-rotation";
18
- import { CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota } from "./quota";
24
+ import { CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota, isCodexFiveHourQuotaPlan } from "./quota";
19
25
  import { MAIN_CODEX_ACCOUNT_ID, getMainAccountPlan } from "./main-account";
20
26
  import { isSelectableCodexPoolAccount } from "./account-id";
21
27
  import type { OcxConfig } from "../types";
@@ -177,6 +183,8 @@ export type CodexUpstreamOutcomeMeta = {
177
183
  modelId?: string;
178
184
  /** When set, clears affinity for this thread immediately on transient failure. */
179
185
  threadId?: string | null;
186
+ /** Suppress Pool rotation and affinity failover for an exact account selector. */
187
+ fixedAccount?: boolean;
180
188
  /**
181
189
  * Probe lease held by this request, when it was admitted through an active
182
190
  * quota cooldown. Only the outcome carrying the current lease may clear the
@@ -279,6 +287,7 @@ function deleteScopedHealth(accountId: string, scope: CodexQuotaScope): void {
279
287
  }
280
288
 
281
289
  export function computeCodexUsageScore(quota: {
290
+ fiveHourPercent?: number;
282
291
  weeklyPercent?: number;
283
292
  monthlyPercent?: number;
284
293
  } | null, plan?: string | null): number {
@@ -289,7 +298,17 @@ export function computeCodexUsageScore(quota: {
289
298
  ? quota.monthlyPercent
290
299
  : CODEX_UNKNOWN_USAGE_SCORE;
291
300
  }
292
- const values = [quota.weeklyPercent, quota.monthlyPercent]
301
+ if (isCodexFiveHourQuotaPlan(plan)
302
+ && typeof quota.fiveHourPercent === "number"
303
+ && Number.isFinite(quota.fiveHourPercent)
304
+ && quota.fiveHourPercent >= 0
305
+ && quota.fiveHourPercent <= 100) {
306
+ return quota.fiveHourPercent;
307
+ }
308
+ const values = [
309
+ quota.weeklyPercent,
310
+ quota.monthlyPercent,
311
+ ]
293
312
  .filter((value): value is number => typeof value === "number" && Number.isFinite(value));
294
313
  return values.length > 0 ? Math.max(...values) : CODEX_UNKNOWN_USAGE_SCORE;
295
314
  }
@@ -736,7 +755,7 @@ function getEligiblePoolAccounts(
736
755
  excludeId?: string,
737
756
  now = Date.now(),
738
757
  quotaScope?: CodexQuotaScope,
739
- ): string[] {
758
+ ): readonly string[] {
740
759
  const ids = (config.codexAccounts ?? [])
741
760
  .filter(account => isSelectableCodexPoolAccount(account)
742
761
  && account.id !== excludeId
@@ -758,14 +777,20 @@ function getEligiblePoolAccounts(
758
777
  ) {
759
778
  ids.unshift(MAIN_CODEX_ACCOUNT_ID);
760
779
  }
761
- return ids;
780
+ if (!config.codexAccountPriorities) return ids;
781
+ return selectPriorityTier(
782
+ ids,
783
+ codexAccountPriorityLookup(config),
784
+ id => hasCodexQuotaHeadroom(config, id),
785
+ pinnedCodexAccountId(config),
786
+ );
762
787
  }
763
788
 
764
789
  function listEligibleCodexAccountIds(
765
790
  config: OcxConfig,
766
791
  now: number,
767
792
  quotaScope?: CodexQuotaScope,
768
- ): string[] {
793
+ ): readonly string[] {
769
794
  return getEligiblePoolAccounts(config, undefined, now, quotaScope);
770
795
  }
771
796
 
@@ -773,7 +798,7 @@ function stickyLimitForConfig(config: OcxConfig): number {
773
798
  return normalizeAccountPoolStickyLimit(config.accountPoolStickyLimit);
774
799
  }
775
800
 
776
- function isActiveUnderFillFirstThreshold(config: OcxConfig, accountId: string): boolean {
801
+ function hasCodexQuotaHeadroom(config: OcxConfig, accountId: string): boolean {
777
802
  const threshold = config.autoSwitchThreshold ?? 80;
778
803
  if (threshold <= 0) return true;
779
804
  const usage = computeCodexUsageScore(getAccountQuota(accountId), getPoolAccountPlan(config, accountId));
@@ -795,7 +820,7 @@ function pickFillFirstCodexAccount(
795
820
  if (eligible.length === 0) return null;
796
821
 
797
822
  const active = getEffectiveActiveCodexAccountId(config);
798
- if (active && eligible.includes(active) && isActiveUnderFillFirstThreshold(config, active)) {
823
+ if (active && eligible.includes(active) && hasCodexQuotaHeadroom(config, active)) {
799
824
  return active;
800
825
  }
801
826
 
@@ -806,7 +831,7 @@ function pickFillFirstCodexAccount(
806
831
  function pickNextFillFirstCodexAccount(
807
832
  config: OcxConfig,
808
833
  afterId: string | null,
809
- eligible = listEligibleCodexAccountIds(config, Date.now()),
834
+ eligible: readonly string[] = listEligibleCodexAccountIds(config, Date.now()),
810
835
  _now = Date.now(),
811
836
  ): string | null {
812
837
  if (eligible.length === 0) return null;
@@ -814,7 +839,7 @@ function pickNextFillFirstCodexAccount(
814
839
  if (!afterId) {
815
840
  // Prefer an under-threshold account when starting with no active cursor.
816
841
  for (const id of ordered) {
817
- if (isActiveUnderFillFirstThreshold(config, id)) return id;
842
+ if (hasCodexQuotaHeadroom(config, id)) return id;
818
843
  }
819
844
  return ordered[0] ?? null;
820
845
  }
@@ -829,7 +854,7 @@ function pickNextFillFirstCodexAccount(
829
854
  const startIdx = stableAll.indexOf(afterId);
830
855
  if (startIdx < 0) {
831
856
  for (const id of ordered) {
832
- if (isActiveUnderFillFirstThreshold(config, id)) return id;
857
+ if (hasCodexQuotaHeadroom(config, id)) return id;
833
858
  }
834
859
  return ordered[0] ?? null;
835
860
  }
@@ -840,7 +865,7 @@ function pickNextFillFirstCodexAccount(
840
865
  const candidate = stableAll[(startIdx + step) % stableAll.length]!;
841
866
  if (!eligible.includes(candidate)) continue;
842
867
  if (!fallback) fallback = candidate;
843
- if (isActiveUnderFillFirstThreshold(config, candidate)) return candidate;
868
+ if (hasCodexQuotaHeadroom(config, candidate)) return candidate;
844
869
  }
845
870
  return fallback ?? ordered[0] ?? null;
846
871
  }
@@ -955,11 +980,11 @@ export function pickAlternateCodexAccount(
955
980
  ): string | null {
956
981
  const strategy = normalizeAccountPoolStrategy(config.accountPoolStrategy);
957
982
  if (strategy === "round-robin") {
958
- const eligible = listEligibleCodexAccountIds(config, now, quotaScope).filter(id => id !== excludeId);
983
+ const eligible = getEligiblePoolAccounts(config, excludeId, now, quotaScope);
959
984
  return pickRoundRobinAccount(codexPoolKeyForScope(quotaScope), eligible, stickyLimitForConfig(config));
960
985
  }
961
986
  if (strategy === "fill-first") {
962
- const eligible = listEligibleCodexAccountIds(config, now, quotaScope).filter(id => id !== excludeId);
987
+ const eligible = getEligiblePoolAccounts(config, excludeId, now, quotaScope);
963
988
  return pickNextFillFirstCodexAccount(config, excludeId, eligible, now);
964
989
  }
965
990
  return pickLowestUsageCodexAccount(config, excludeId, now, quotaScope);
@@ -970,6 +995,11 @@ export function getEffectiveActiveCodexAccountId(config: OcxConfig): string | un
970
995
  return runtimeActiveCodexAccountId ?? config.activeCodexAccountId;
971
996
  }
972
997
 
998
+ export function isEffectiveCodexAccountPinned(config: OcxConfig): boolean {
999
+ const pinned = pinnedCodexAccountId(config);
1000
+ return pinned !== undefined && pinned === getEffectiveActiveCodexAccountId(config);
1001
+ }
1002
+
973
1003
  /**
974
1004
  * Automatic strategy / failover cursor only — never mutates `config.activeCodexAccountId`
975
1005
  * so an unrelated `saveConfig` cannot persist transient rotation as operator selection.
@@ -978,10 +1008,18 @@ function rememberActiveCodexAccount(_config: OcxConfig, accountId: string): void
978
1008
  runtimeActiveCodexAccountId = accountId;
979
1009
  }
980
1010
 
1011
+ function releaseCodexAccountPinFor(config: OcxConfig, accountId: string): boolean {
1012
+ const pinned = pinnedCodexAccountId(config);
1013
+ if (pinned === undefined || pinned === accountId) return false;
1014
+ clearCodexAccountPin(config);
1015
+ return true;
1016
+ }
1017
+
981
1018
  /** Persist operator (or quota-strategy) active selection to config + disk. */
982
1019
  function setActiveCodexAccount(config: OcxConfig, accountId: string): void {
983
1020
  runtimeActiveCodexAccountId = undefined;
984
- if (config.activeCodexAccountId === accountId) return;
1021
+ const releasedPin = releaseCodexAccountPinFor(config, accountId);
1022
+ if (config.activeCodexAccountId === accountId && !releasedPin) return;
985
1023
  config.activeCodexAccountId = accountId;
986
1024
  saveConfigPreservingClaudeCode(config);
987
1025
  }
@@ -992,6 +1030,7 @@ function promoteActiveCodexAccount(config: OcxConfig, accountId: string): void {
992
1030
  setActiveCodexAccount(config, accountId);
993
1031
  return;
994
1032
  }
1033
+ releaseCodexAccountPinFor(config, accountId);
995
1034
  rememberActiveCodexAccount(config, accountId);
996
1035
  }
997
1036
 
@@ -1010,6 +1049,7 @@ export function reconcileCodexActiveAfterExclusion(
1010
1049
  if (config.activeCodexAccountId === excludedAccountId) {
1011
1050
  config.activeCodexAccountId = undefined;
1012
1051
  }
1052
+ clearCodexAccountPin(config, excludedAccountId);
1013
1053
  if (!wasEffective) return getEffectiveActiveCodexAccountId(config) ?? null;
1014
1054
 
1015
1055
  runtimeActiveCodexAccountId = undefined;
@@ -1022,6 +1062,49 @@ function isUnknownUsage(usage: number): boolean {
1022
1062
  return usage >= CODEX_UNKNOWN_USAGE_SCORE;
1023
1063
  }
1024
1064
 
1065
+ function pickLowestUsageAmong(config: OcxConfig, ids: readonly string[]): string | null {
1066
+ let best: string | null = null;
1067
+ let bestUsage = Number.POSITIVE_INFINITY;
1068
+ for (const id of ids) {
1069
+ const usage = computeCodexUsageScore(getAccountQuota(id), getPoolAccountPlan(config, id));
1070
+ if (usage < bestUsage) {
1071
+ best = id;
1072
+ bestUsage = usage;
1073
+ }
1074
+ }
1075
+ return best;
1076
+ }
1077
+
1078
+ /** Move an unbound request up when a higher-priority tier regains headroom. */
1079
+ function pickPriorityPreemption(
1080
+ config: OcxConfig,
1081
+ active: string,
1082
+ now: number,
1083
+ quotaScope?: CodexQuotaScope,
1084
+ ): string | null {
1085
+ if (!config.codexAccountPriorities) return null;
1086
+ const eligible = getEligiblePoolAccounts(config, undefined, now, quotaScope);
1087
+ if (eligible.length === 0 || eligible.includes(active)) return null;
1088
+ const pinned = pinnedCodexAccountId(config);
1089
+ if (pinned !== undefined && eligible.includes(pinned) && hasCodexQuotaHeadroom(config, pinned)) return null;
1090
+ const priorityOf = codexAccountPriorityLookup(config);
1091
+ if (priorityOf(eligible[0]!) <= priorityOf(active)) return null;
1092
+ return pickLowestUsageAmong(config, eligible.filter(id => hasCodexQuotaHeadroom(config, id)));
1093
+ }
1094
+
1095
+ /** Retire a persisted manual pin only after its account is durably unavailable or drained. */
1096
+ function releaseDrainedCodexAccountPin(config: OcxConfig): void {
1097
+ const pinned = pinnedCodexAccountId(config);
1098
+ if (pinned === undefined) return;
1099
+ const drained = !isCodexAccountUsable(config, pinned)
1100
+ || isAccountNeedsReauth(pinned)
1101
+ || isCodexAccountPaused(config, pinned)
1102
+ || !hasCodexQuotaHeadroom(config, pinned);
1103
+ if (!drained) return;
1104
+ clearCodexAccountPin(config);
1105
+ saveConfigPreservingClaudeCode(config);
1106
+ }
1107
+
1025
1108
  function applyQuotaAutoSwitch(
1026
1109
  config: OcxConfig,
1027
1110
  active: string,
@@ -1053,11 +1136,16 @@ function shouldFailover(config: OcxConfig, accountId: string, now: number): bool
1053
1136
  return !!health && health.consecutiveFailures >= threshold;
1054
1137
  }
1055
1138
 
1056
- function applyFailureFailover(config: OcxConfig, active: string, now: number): string {
1139
+ function applyFailureFailover(
1140
+ config: OcxConfig,
1141
+ active: string,
1142
+ now: number,
1143
+ quotaScope?: CodexQuotaScope,
1144
+ ): string {
1057
1145
  if (!shouldFailover(config, active, now)) return active;
1058
- const best = pickAlternateCodexAccount(config, active, now);
1146
+ const best = pickAlternateCodexAccount(config, active, now, quotaScope);
1059
1147
  if (best) {
1060
- promoteActiveCodexAccount(config, best);
1148
+ if (!isIndependentCodexQuotaScope(quotaScope)) promoteActiveCodexAccount(config, best);
1061
1149
  return best;
1062
1150
  }
1063
1151
  return active;
@@ -1130,6 +1218,8 @@ export function previewCodexAccountForRequest(
1130
1218
  else return null;
1131
1219
  }
1132
1220
 
1221
+ active = pickPriorityPreemption(config, active, now, quotaScope) ?? active;
1222
+
1133
1223
  const threshold = config.autoSwitchThreshold ?? 80;
1134
1224
  if (threshold > 0) {
1135
1225
  const usage = computeCodexUsageScore(getAccountQuota(active), getPoolAccountPlan(config, active));
@@ -1157,6 +1247,7 @@ export function resolveCodexAccountForThreadDetailed(
1157
1247
  now = Date.now(),
1158
1248
  quotaScope?: CodexQuotaScope,
1159
1249
  ): CodexThreadResolution {
1250
+ if (!isIndependentCodexQuotaScope(quotaScope)) releaseDrainedCodexAccountPin(config);
1160
1251
  const entry = threadId ? getThreadAffinity(threadId, quotaScope) : undefined;
1161
1252
  if (threadId && entry) {
1162
1253
  if (isThreadAffinityExpired(entry, now)) {
@@ -1227,8 +1318,13 @@ export function resolveCodexAccountForThreadDetailed(
1227
1318
  return { status: "none" };
1228
1319
  }
1229
1320
  }
1321
+ const preempted = pickPriorityPreemption(config, active, now, quotaScope);
1322
+ if (preempted) {
1323
+ if (!isIndependentCodexQuotaScope(quotaScope)) rememberActiveCodexAccount(config, preempted);
1324
+ active = preempted;
1325
+ }
1230
1326
  active = applyQuotaAutoSwitch(config, active, now, quotaScope);
1231
- active = applyFailureFailover(config, active, now);
1327
+ active = applyFailureFailover(config, active, now, quotaScope);
1232
1328
  if (!isCodexAccountUsable(config, active)) {
1233
1329
  return hasConfiguredPoolAccount(config, active) ? { status: "selected", accountId: active } : { status: "none" };
1234
1330
  }
@@ -1354,7 +1450,7 @@ export function recordCodexUpstreamOutcome(
1354
1450
  // The shared native scope is the existing account-wide native behavior:
1355
1451
  // threads must leave it and new requests should prefer an eligible account.
1356
1452
  // Spark remains isolated so a same-account Terra/Luna combo fallback can run.
1357
- if (quotaScope === "shared") {
1453
+ if (quotaScope === "shared" && !meta.fixedAccount) {
1358
1454
  clearThreadAccountMapForAccount(accountId);
1359
1455
  notePoolRotationFailure(POOL_KEY_CODEX, accountId);
1360
1456
  if (getEffectiveActiveCodexAccountId(config) === accountId) {
@@ -1399,17 +1495,21 @@ export function recordCodexUpstreamOutcome(
1399
1495
  ...(prior?.lastProbeAt !== undefined ? { lastProbeAt: prior.lastProbeAt } : {}),
1400
1496
  }),
1401
1497
  });
1402
- clearThreadAccountMapForAccount(accountId);
1403
- notePoolRotationFailure(POOL_KEY_CODEX, accountId);
1404
- const effectiveActive = getEffectiveActiveCodexAccountId(config);
1405
- if (effectiveActive === accountId) {
1406
- // Same-request 429 retry already picked via excludeAccountId — reuse it so
1407
- // round-robin does not advance the ring a second time.
1408
- const reused = meta.promoteAccountId && meta.promoteAccountId !== accountId
1409
- ? meta.promoteAccountId
1410
- : null;
1411
- const fallback = reused ?? pickAlternateCodexAccount(config, accountId, now);
1412
- if (fallback) promoteActiveCodexAccount(config, fallback);
1498
+ if (!meta.fixedAccount) {
1499
+ clearThreadAccountMapForAccount(accountId);
1500
+ if (!isIndependentCodexQuotaScope(quotaScope)) {
1501
+ notePoolRotationFailure(POOL_KEY_CODEX, accountId);
1502
+ const effectiveActive = getEffectiveActiveCodexAccountId(config);
1503
+ if (effectiveActive === accountId) {
1504
+ // Same-request 429 retry already picked via excludeAccountId; reuse it so
1505
+ // round-robin does not advance the ring a second time.
1506
+ const reused = meta.promoteAccountId && meta.promoteAccountId !== accountId
1507
+ ? meta.promoteAccountId
1508
+ : null;
1509
+ const fallback = reused ?? pickAlternateCodexAccount(config, accountId, now, quotaScope);
1510
+ if (fallback) promoteActiveCodexAccount(config, fallback);
1511
+ }
1512
+ }
1413
1513
  }
1414
1514
  return;
1415
1515
  }
@@ -1454,15 +1554,21 @@ export function recordCodexUpstreamOutcome(
1454
1554
  // thread is still pinned to the FAILING account — a late failure from account A
1455
1555
  // must not delete a newer healthy binding to account B (race: T→A, A fails,
1456
1556
  // T→B, late A failure must not delete B's mapping).
1457
- if (failoverReady && meta.threadId) {
1557
+ if (!meta.fixedAccount && failoverReady && meta.threadId) {
1458
1558
  deleteThreadAffinitiesForAccount(meta.threadId, accountId);
1459
1559
  }
1460
1560
  // Once the account is past the failover streak, clear every thread still pinned
1461
1561
  // to it — matching 429 affinity behavior so "continue" cannot stay on a bad peer.
1462
- if (shouldFailover(config, accountId, now)) {
1562
+ if (!meta.fixedAccount && shouldFailover(config, accountId, now)) {
1463
1563
  clearThreadAccountMapForAccount(accountId);
1464
1564
  }
1465
- if (getEffectiveActiveCodexAccountId(config) === accountId) applyFailureFailover(config, accountId, now);
1565
+ if (
1566
+ !meta.fixedAccount
1567
+ && !isIndependentCodexQuotaScope(quotaScope)
1568
+ && getEffectiveActiveCodexAccountId(config) === accountId
1569
+ ) {
1570
+ applyFailureFailover(config, accountId, now, quotaScope);
1571
+ }
1466
1572
  }
1467
1573
 
1468
1574
  export function formatCodexProviderForLog(providerName: string, accountId: string | null, config: OcxConfig): string {