@bitkyc08/opencodex 2.40.0 → 2.42.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 (97) hide show
  1. package/README.md +4 -0
  2. package/gui/dist/assets/index-BU1tE0sr.js +112 -0
  3. package/gui/dist/assets/index-DL9-iS6J.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/meta.svg +1 -0
  6. package/package.json +4 -3
  7. package/src/adapters/cursor/catalog.ts +71 -29
  8. package/src/adapters/cursor/claude-id.ts +76 -0
  9. package/src/adapters/cursor/discovery.ts +16 -3
  10. package/src/adapters/cursor/effort-map.ts +27 -12
  11. package/src/adapters/cursor/protobuf-request.ts +41 -21
  12. package/src/adapters/google.ts +39 -2
  13. package/src/adapters/identity.ts +8 -2
  14. package/src/adapters/openai-responses.ts +57 -4
  15. package/src/bridge.ts +25 -3
  16. package/src/cli/account-auth.ts +28 -3
  17. package/src/cli/account-extended.ts +7 -1
  18. package/src/cli/capabilities.ts +2 -2
  19. package/src/cli/claude.ts +11 -2
  20. package/src/cli/connect.ts +7 -1
  21. package/src/cli/observe.ts +3 -1
  22. package/src/cli/registry.ts +1 -1
  23. package/src/cli/status.ts +19 -4
  24. package/src/client/connect.ts +5 -1
  25. package/src/client/hub-client.ts +29 -5
  26. package/src/clients/config-export.ts +12 -2
  27. package/src/codex/auth-api.ts +102 -9
  28. package/src/codex/catalog/aggregation.ts +8 -0
  29. package/src/codex/catalog/effort.ts +15 -2
  30. package/src/codex/catalog/metadata.ts +119 -9
  31. package/src/codex/catalog/native-models.ts +71 -0
  32. package/src/codex/catalog/parsing.ts +5 -3
  33. package/src/codex/catalog/provider-fetch.ts +166 -28
  34. package/src/codex/catalog.ts +1 -1
  35. package/src/codex/convergence-types.ts +1 -0
  36. package/src/codex/data/upstream-models.json +169 -0
  37. package/src/codex/desired-state.ts +18 -11
  38. package/src/codex/inject.ts +96 -6
  39. package/src/codex/injected-marker.ts +30 -4
  40. package/src/codex/journal.ts +14 -0
  41. package/src/combos/failover.ts +185 -6
  42. package/src/combos/index.ts +6 -0
  43. package/src/combos/resolve.ts +43 -6
  44. package/src/config.ts +5 -1
  45. package/src/generated/compatibility-version.json +115 -83
  46. package/src/generated/model-metadata.ts +1 -1
  47. package/src/grok/sync.ts +10 -2
  48. package/src/integrations/cursor-effort-table.ts +143 -0
  49. package/src/integrations/state.ts +1 -1
  50. package/src/integrations/writer.ts +2 -2
  51. package/src/lib/app-owned-memory-stores.ts +27 -8
  52. package/src/lib/bounded-body.ts +16 -1
  53. package/src/oauth/account-quota-rank.ts +40 -1
  54. package/src/oauth/chatgpt-device.ts +187 -0
  55. package/src/oauth/chatgpt.ts +31 -4
  56. package/src/oauth/generic-account-failover.ts +2 -2
  57. package/src/oauth/index.ts +24 -3
  58. package/src/oauth/log.ts +3 -0
  59. package/src/oauth/meta-muse.ts +235 -0
  60. package/src/providers/antigravity-models.ts +71 -13
  61. package/src/providers/command-code-efforts.ts +15 -0
  62. package/src/providers/free-directory.ts +4 -1
  63. package/src/providers/muse-subscription-usage.ts +95 -0
  64. package/src/providers/quota.ts +96 -0
  65. package/src/providers/registry.ts +116 -8
  66. package/src/responses/code-mode-helper-compat.ts +4 -1
  67. package/src/responses/state.ts +5 -4
  68. package/src/server/auth-cors.ts +241 -56
  69. package/src/server/chat-completions.ts +11 -2
  70. package/src/server/chat-native.ts +30 -4
  71. package/src/server/claude-messages.ts +17 -3
  72. package/src/server/effort-row.ts +131 -0
  73. package/src/server/index.ts +82 -45
  74. package/src/server/live.ts +18 -4
  75. package/src/server/management/api-key-rotation.ts +2 -1
  76. package/src/server/management/api-key-usage.ts +97 -43
  77. package/src/server/management/context.ts +3 -0
  78. package/src/server/management/cursor-integration-routes.ts +36 -7
  79. package/src/server/management/logs-usage-routes.ts +64 -87
  80. package/src/server/management/oauth-account-routes.ts +10 -3
  81. package/src/server/management/provider-routes.ts +218 -1
  82. package/src/server/management/route-registry.ts +1 -0
  83. package/src/server/management/usage-aggregate-cache.ts +464 -0
  84. package/src/server/management/usage-summary-cache.ts +4 -0
  85. package/src/server/models-capabilities.ts +60 -5
  86. package/src/server/responses/core.ts +95 -7
  87. package/src/server/responses/empty-completion-guard.ts +4 -0
  88. package/src/types/config.ts +10 -1
  89. package/src/types/request.ts +8 -0
  90. package/src/types/tools.ts +12 -9
  91. package/src/usage/expected-prices.ts +43 -7
  92. package/src/usage/ledger-scanner.ts +448 -0
  93. package/src/usage/log.ts +1 -1
  94. package/src/usage/summary.ts +915 -655
  95. package/src/web-search/index.ts +1 -1
  96. package/gui/dist/assets/index-BHe2rl_C.js +0 -112
  97. package/gui/dist/assets/index-CJSb3HPe.css +0 -1
@@ -749,73 +749,258 @@ export function copyIfDefined<K extends keyof OcxProviderConfig>(
749
749
  if (value !== undefined) out[key as string] = value as unknown;
750
750
  }
751
751
 
752
+ /**
753
+ * Exhaustive provider-field policy shared by dashboard redaction and editor
754
+ * admission. `satisfies Record<keyof OcxProviderConfig, ...>` makes a newly added
755
+ * provider field fail typecheck until it is deliberately classified.
756
+ *
757
+ * `editor` fields are user-authored, `redacted` fields may contain credentials,
758
+ * and `runtime` fields are observations/limits that must never become editor write
759
+ * authority. MCP and desktop executor blocks are redacted as a whole because both
760
+ * contain arbitrary environment variables and/or headers.
761
+ */
762
+ type ProviderConfigFieldPolicy = "editor" | "redacted" | "runtime";
763
+
764
+ const PROVIDER_CONFIG_FIELD_POLICY = {
765
+ alias: "editor",
766
+ modelAliases: "editor",
767
+ modelDisplayNames: "editor",
768
+ defaultAliases: "editor",
769
+ adapter: "editor",
770
+ codexToolMode: "editor",
771
+ requestPacing: "editor",
772
+ mcpMaxTools: "editor",
773
+ mcpMaxSchemaBytes: "editor",
774
+ mcpMaxResultBytes: "editor",
775
+ modelAdapters: "editor",
776
+ fastWire: "editor",
777
+ baseUrl: "editor",
778
+ responsesPath: "editor",
779
+ commandCodeVersion: "editor",
780
+ statelessResponses: "editor",
781
+ requiresAdjacentResponsesToolResults: "editor",
782
+ annotateEmptyToolOutputs: "editor",
783
+ supportsServiceTier: "editor",
784
+ modelSupportsServiceTier: "editor",
785
+ preserveResponsesReasoningContent: "editor",
786
+ decodesNativeCompactionBlobs: "editor",
787
+ allowPrivateNetwork: "editor",
788
+ upstreamHttpVersion: "editor",
789
+ upstreamWebsocket: "editor",
790
+ directGeminiWireRenames: "editor",
791
+ disabled: "editor",
792
+ codexAccountMode: "editor",
793
+ apiKey: "redacted",
794
+ apiKeyTransport: "editor",
795
+ apiKeyPool: "redacted",
796
+ defaultModel: "editor",
797
+ models: "editor",
798
+ liveModels: "editor",
799
+ selectedModels: "editor",
800
+ retainModels: "editor",
801
+ newModelPolicy: "editor",
802
+ modelPreset: "editor",
803
+ contextWindow: "editor",
804
+ modelContextWindows: "editor",
805
+ modelInputModalities: "editor",
806
+ modelMaxInputTokens: "runtime",
807
+ modelAutoCompactTokenLimits: "editor",
808
+ defaultMaxOutputTokens: "editor",
809
+ modelMaxOutputTokens: "editor",
810
+ modelCosts: "editor",
811
+ headers: "redacted",
812
+ openRouterRouting: "editor",
813
+ modelOpenRouterRouting: "editor",
814
+ vercelGatewayRouting: "editor",
815
+ modelVercelGatewayRouting: "editor",
816
+ authMode: "editor",
817
+ oauthAccountFailover: "editor",
818
+ keyOptional: "editor",
819
+ freeTier: "editor",
820
+ note: "editor",
821
+ modelSuffixBracketStrip: "editor",
822
+ refreshPolicy: "editor",
823
+ reasoningEfforts: "editor",
824
+ modelReasoningEfforts: "editor",
825
+ modelDefaultReasoningEfforts: "editor",
826
+ modelSupportsReasoningSummaries: "editor",
827
+ modelSupportsVerbosity: "editor",
828
+ supportsVerbosity: "editor",
829
+ modelReasoningSummaryDelivery: "editor",
830
+ modelPreferHostedTools: "editor",
831
+ supportsOpenAiWebSearchToolFields: "editor",
832
+ xaiResponsesXSearch: "editor",
833
+ supportsResponsesCustomTools: "editor",
834
+ responsesSnapshotRepair: "editor",
835
+ reasoningEffortMap: "editor",
836
+ modelReasoningEffortMap: "editor",
837
+ reasoningWireFormat: "editor",
838
+ noReasoningModels: "editor",
839
+ noTemperatureModels: "editor",
840
+ noTopPModels: "editor",
841
+ noPenaltyModels: "editor",
842
+ noStructuredOutputModels: "editor",
843
+ omitReasoningEffortWithToolsModels: "editor",
844
+ parallelToolCalls: "editor",
845
+ pinParallelToolCallsFalse: "editor",
846
+ terminalContinuationGuard: "editor",
847
+ openaiChatEofTolerance: "editor",
848
+ promptCacheKey: "editor",
849
+ chatServiceTier: "editor",
850
+ responsesItemIdRepair: "editor",
851
+ autoToolChoiceOnlyModels: "editor",
852
+ preserveReasoningContentModels: "editor",
853
+ requiresReasoningPlaceholderModels: "editor",
854
+ retryOn429: "editor",
855
+ transientRetryOn5xx: "editor",
856
+ reasoningSplitModels: "editor",
857
+ reasoningDetailsModels: "editor",
858
+ thinkingToggleModels: "editor",
859
+ thinkingBudgetModels: "editor",
860
+ escapeBuiltinToolNames: "editor",
861
+ anthropicEofTolerance: "editor",
862
+ noVisionModels: "editor",
863
+ googleMode: "editor",
864
+ project: "editor",
865
+ location: "editor",
866
+ mcpServers: "redacted",
867
+ desktopExecutor: "redacted",
868
+ unsafeAllowNativeLocalExec: "editor",
869
+ nativeLocalExec: "editor",
870
+ } as const satisfies Record<keyof OcxProviderConfig, ProviderConfigFieldPolicy>;
871
+
872
+ type ProviderFieldWithPolicy<Policy extends ProviderConfigFieldPolicy> = {
873
+ [Field in keyof typeof PROVIDER_CONFIG_FIELD_POLICY]:
874
+ typeof PROVIDER_CONFIG_FIELD_POLICY[Field] extends Policy ? Field : never;
875
+ }[keyof typeof PROVIDER_CONFIG_FIELD_POLICY];
876
+
877
+ type RedactedProviderField = ProviderFieldWithPolicy<"redacted">;
878
+ type RuntimeProviderField = ProviderFieldWithPolicy<"runtime">;
879
+ export const REDACTED_PROVIDER_FIELDS = Object.freeze(Object.entries(PROVIDER_CONFIG_FIELD_POLICY)
880
+ .filter(([, policy]) => policy === "redacted")
881
+ .map(([field]) => field as RedactedProviderField));
882
+ const RUNTIME_PROVIDER_FIELDS = Object.freeze(Object.entries(PROVIDER_CONFIG_FIELD_POLICY)
883
+ .filter(([, policy]) => policy === "runtime")
884
+ .map(([field]) => field as RuntimeProviderField));
885
+
886
+ const PROVIDER_EDITOR_DERIVED_FIELDS = [
887
+ ...RUNTIME_PROVIDER_FIELDS,
888
+ ...FORBIDDEN_PROVIDER_RUNTIME_FIELDS,
889
+ "fetch",
890
+ "hasApiKey",
891
+ "hasHeaders",
892
+ "xaiResponsesOptInState",
893
+ ] as const;
894
+
895
+ export const PROVIDER_EDITOR_DENIED_FIELDS = [
896
+ ...REDACTED_PROVIDER_FIELDS,
897
+ ...PROVIDER_EDITOR_DERIVED_FIELDS,
898
+ ] as const;
899
+
900
+ export type ProviderEditorProviderDTO = Omit<OcxProviderConfig, RedactedProviderField | RuntimeProviderField>
901
+ & Record<string, unknown>;
902
+
903
+ export interface ProviderEditorConfigDTO {
904
+ defaultProvider: string;
905
+ providers: Record<string, ProviderEditorProviderDTO>;
906
+ }
907
+
908
+ export type ProviderEditorConfigParseResult =
909
+ | { ok: true; value: ProviderEditorConfigDTO }
910
+ | { ok: false; error: string; code: "invalid_provider_editor_body" | "invalid_provider_editor_field" };
911
+
912
+ const PROVIDER_EDITOR_DENIED_FIELD_SET = new Set<string>(PROVIDER_EDITOR_DENIED_FIELDS);
913
+ const PROVIDER_CONFIG_FIELD_SET = new Set<string>(Object.keys(PROVIDER_CONFIG_FIELD_POLICY));
914
+
915
+ function isPlainDataRecord(value: unknown): value is Record<string, unknown> {
916
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
917
+ const prototype = Object.getPrototypeOf(value);
918
+ return prototype === Object.prototype || prototype === null;
919
+ }
920
+
921
+ /**
922
+ * Project one provider through the same redaction path used by both the public
923
+ * config DTO and the raw editor. Persisted unknown fields remain on disk but are
924
+ * not exposed until OcxProviderConfig classifies them as editor-safe.
925
+ */
926
+ function providerEditorProviderDTO(name: string, provider: OcxProviderConfig): ProviderEditorProviderDTO {
927
+ const dto = Object.fromEntries(Object.entries(provider)
928
+ .filter(([field]) => PROVIDER_CONFIG_FIELD_SET.has(field) && !PROVIDER_EDITOR_DENIED_FIELD_SET.has(field))
929
+ .map(([field, value]) => [field, structuredClone(value)])) as Record<string, unknown>;
930
+ dto.baseUrl = publicProviderBaseUrl(provider.baseUrl);
931
+ const modelCosts = sanitizeModelCostsForDisplay(provider.modelCosts);
932
+ if (modelCosts) dto.modelCosts = modelCosts;
933
+ else delete dto.modelCosts;
934
+
935
+ const registryNote = (providerMatchesRegistryTransport(name, provider)
936
+ ? getProviderRegistryEntry(name)
937
+ : registryEntryForProviderDestination(provider))?.note;
938
+ if (typeof registryNote === "string" && registryNote.trim()) dto.note = registryNote;
939
+ const codexAccountMode = providerCodexAccountMode(name, provider);
940
+ if (codexAccountMode) dto.codexAccountMode = codexAccountMode;
941
+ return dto as ProviderEditorProviderDTO;
942
+ }
943
+
944
+ /** The complete non-secret provider shape the raw GUI editor may round-trip. */
945
+ export function providerEditorConfigDTO(config: OcxConfig): ProviderEditorConfigDTO {
946
+ const providers: Record<string, ProviderEditorProviderDTO> = Object.create(null);
947
+ for (const [name, provider] of Object.entries(config.providers)) {
948
+ providers[name] = providerEditorProviderDTO(name, provider);
949
+ }
950
+ return { defaultProvider: config.defaultProvider, providers };
951
+ }
952
+
953
+ /** Parse an editor snapshot; unknown, redacted, and derived fields fail closed. */
954
+ export function parseProviderEditorConfigDTO(value: unknown): ProviderEditorConfigParseResult {
955
+ if (!isPlainDataRecord(value)) {
956
+ return { ok: false, error: "provider editor config must be a plain object", code: "invalid_provider_editor_body" };
957
+ }
958
+ const rootKeys = Object.keys(value);
959
+ if (rootKeys.length !== 2 || !Object.hasOwn(value, "defaultProvider") || !Object.hasOwn(value, "providers")) {
960
+ return { ok: false, error: "provider editor config must contain only defaultProvider and providers", code: "invalid_provider_editor_body" };
961
+ }
962
+ if (typeof value.defaultProvider !== "string" || value.defaultProvider.trim() === "") {
963
+ return { ok: false, error: "defaultProvider must be a non-empty string", code: "invalid_provider_editor_body" };
964
+ }
965
+ if (!isPlainDataRecord(value.providers)) {
966
+ return { ok: false, error: "providers must be a plain object", code: "invalid_provider_editor_body" };
967
+ }
968
+
969
+ const providers: Record<string, ProviderEditorProviderDTO> = Object.create(null);
970
+ for (const [name, provider] of Object.entries(value.providers)) {
971
+ if (!isPlainDataRecord(provider)) {
972
+ return { ok: false, error: `provider ${JSON.stringify(redactSecretString(name))} must be a plain object`, code: "invalid_provider_editor_body" };
973
+ }
974
+ const deniedField = Object.keys(provider).find(field =>
975
+ !PROVIDER_CONFIG_FIELD_SET.has(field) || PROVIDER_EDITOR_DENIED_FIELD_SET.has(field));
976
+ if (deniedField) {
977
+ return {
978
+ ok: false,
979
+ error: `provider ${JSON.stringify(redactSecretString(name))} contains non-editable field ${JSON.stringify(redactSecretString(deniedField))}`,
980
+ code: "invalid_provider_editor_field",
981
+ };
982
+ }
983
+ providers[name] = structuredClone(provider) as ProviderEditorProviderDTO;
984
+ }
985
+ return {
986
+ ok: true,
987
+ value: { defaultProvider: value.defaultProvider, providers },
988
+ };
989
+ }
990
+
752
991
  /** Public dashboard DTO for config.json: provider entries with secrets stripped and documented fields exposed (including `modelCosts`). */
753
992
  export function safeConfigDTO(config: OcxConfig): unknown {
993
+ const editor = providerEditorConfigDTO(config);
754
994
  const providers: Record<string, Record<string, unknown>> = {};
755
995
  for (const [name, provider] of Object.entries(config.providers)) {
756
996
  const dto: Record<string, unknown> = {
757
- adapter: provider.adapter,
758
- baseUrl: publicProviderBaseUrl(provider.baseUrl),
997
+ ...editor.providers[name],
759
998
  hasApiKey: !!provider.apiKey,
760
999
  hasHeaders: !!provider.headers && Object.keys(provider.headers).length > 0,
761
1000
  };
762
1001
  if (name === "xai") {
763
1002
  dto.xaiResponsesOptInState = xaiResponsesOptInState(provider);
764
1003
  }
765
- for (const key of [
766
- "defaultModel",
767
- "alias",
768
- "modelAliases",
769
- "defaultAliases",
770
- "disabled",
771
- "allowPrivateNetwork",
772
- "authMode",
773
- "apiKeyTransport",
774
- "keyOptional",
775
- "freeTier",
776
- "liveModels",
777
- "requestPacing",
778
- "models",
779
- "contextWindow",
780
- "modelContextWindows",
781
- "modelAutoCompactTokenLimits",
782
- "defaultMaxOutputTokens",
783
- "modelMaxOutputTokens",
784
- "openRouterRouting",
785
- "modelOpenRouterRouting",
786
- "vercelGatewayRouting",
787
- "modelVercelGatewayRouting",
788
- "reasoningEfforts",
789
- "modelReasoningEfforts",
790
- "reasoningWireFormat",
791
- "noVisionModels",
792
- "noReasoningModels",
793
- "noTemperatureModels",
794
- "noTopPModels",
795
- "noPenaltyModels",
796
- "noStructuredOutputModels",
797
- "retainModels",
798
- "omitReasoningEffortWithToolsModels",
799
- "upstreamHttpVersion",
800
- "autoToolChoiceOnlyModels",
801
- "preserveReasoningContentModels",
802
- "requiresReasoningPlaceholderModels",
803
- "escapeBuiltinToolNames",
804
- ] as const) {
805
- copyIfDefined(dto, provider, key);
806
- }
807
- const modelCosts = sanitizeModelCostsForDisplay(provider.modelCosts);
808
- if (modelCosts) dto.modelCosts = modelCosts;
809
- // Resolve the note by DESTINATION, not by name. A preset saved under a custom name is
810
- // still pointed at the same vendor route, and a usage restriction the user needs to see
811
- // must not disappear because the row was renamed. Prefer the same-name entry so an
812
- // unrenamed provider keeps its exact registry note.
813
- const registryNote = (providerMatchesRegistryTransport(name, provider)
814
- ? getProviderRegistryEntry(name)
815
- : registryEntryForProviderDestination(provider))?.note;
816
- if (typeof registryNote === "string" && registryNote.trim()) dto.note = registryNote;
817
- const codexAccountMode = providerCodexAccountMode(name, provider);
818
- if (codexAccountMode) dto.codexAccountMode = codexAccountMode;
819
1004
  providers[name] = dto;
820
1005
  }
821
1006
  return {
@@ -46,6 +46,7 @@ import {
46
46
  type TranslatorBudget,
47
47
  } from "../lib/translator-budget";
48
48
  import { handleNativeChatCompletions, isNativeChatRouteEligible } from "./chat-native";
49
+ import { parseRequestEffortRowId } from "./effort-row";
49
50
 
50
51
  type Rec = Record<string, unknown>;
51
52
 
@@ -104,6 +105,8 @@ async function handleChatCompletionsWithBudget(
104
105
  }
105
106
 
106
107
  const requestedModel = chatBody.model as string;
108
+ const effortRow = parseRequestEffortRowId(requestedModel, config);
109
+ if (effortRow) chatBody.model = effortRow.baseId;
107
110
  const stream = chatBody.stream === true;
108
111
  // Best-effort Grok attribution: the managed fence stamps this header on every model
109
112
  // it registers (extra_headers, sent verbatim by upstream Grok). Dashboard usage
@@ -113,7 +116,7 @@ async function handleChatCompletionsWithBudget(
113
116
  let settledRoute: ReturnType<typeof routeModel> | null = null;
114
117
  let chatNativeRoute: ReturnType<typeof routeModel> | null = null;
115
118
  try {
116
- const route = routeModel(config, requestedModel, evidenceFromBody(chatBody));
119
+ const route = routeModel(config, chatBody.model as string, evidenceFromBody(chatBody));
117
120
  // Settle the wire once so every branch below reads the adapter this model will
118
121
  // actually use, not the provider-wide default (#404).
119
122
  route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, "chat");
@@ -133,7 +136,7 @@ async function handleChatCompletionsWithBudget(
133
136
  if (chatBody.tools !== undefined) parts.push(JSON.stringify(chatBody.tools));
134
137
  logCtx.usageLogInputTokens = Math.max(1, estimateTokens(parts.join("\n"), requestedModel));
135
138
  }
136
- if (isNativeChatRouteEligible(route, chatBody)) chatNativeRoute = route;
139
+ if (!effortRow && isNativeChatRouteEligible(route, chatBody)) chatNativeRoute = route;
137
140
  } catch (err) {
138
141
  if (err instanceof NoEligiblePolicyCandidateError) {
139
142
  logCtx.routeDecision = err.trace;
@@ -162,6 +165,12 @@ async function handleChatCompletionsWithBudget(
162
165
  // Validate the full Chat boundary after routing. Native Chat keeps `chatBody` as
163
166
  // its wire source; this Responses projection is used only by the fallback path.
164
167
  internalBody = chatCompletionsToResponsesBody(chatBody);
168
+ if (effortRow) {
169
+ internalBody.reasoning = {
170
+ ...(isRec(internalBody.reasoning) ? internalBody.reasoning : {}),
171
+ effort: effortRow.effort,
172
+ };
173
+ }
165
174
  } catch (err) {
166
175
  const overflow = isTranslatorBudgetExceededError(err);
167
176
  const status = overflow ? 413 : err instanceof ChatCompletionsRequestError ? 400 : 500;
@@ -204,12 +204,24 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
204
204
  return fail(400, error instanceof Error ? error.message : String(error), "invalid_request_error");
205
205
  }
206
206
 
207
+ // One inbound request owns one transient send allowance. Capture the policy before any
208
+ // key rotation so recovery cannot replace the ceiling along with the active credential.
209
+ const requestTransientPolicy = transientRetryPolicyFor(activeProvider);
210
+ let transientSendsUsed = 0;
211
+ const remainingTransientSends = (): number => requestTransientPolicy
212
+ ? Math.max(0, requestTransientPolicy.attempts - transientSendsUsed)
213
+ : Number.POSITIVE_INFINITY;
214
+ const transientSendAvailable = (): boolean => remainingTransientSends() > 0;
215
+
207
216
  const send = async (request: AdapterRequest, recovery?: "rate-limit-429" | "key-429"): Promise<Response> => {
208
217
  try {
209
218
  // #2643: opted-in key-auth openai-chat providers retry pre-stream transient statuses on
210
219
  // the native chat lane too; everyone else keeps reset-only semantics.
211
- const transientPolicy = transientRetryPolicyFor(activeProvider);
212
- const fetchWithPolicy = transientPolicy ? fetchWithTransientRetry : fetchWithResetRetry;
220
+ const remaining = remainingTransientSends();
221
+ if (requestTransientPolicy && remaining <= 0) {
222
+ throw new Error("native Chat transient send budget exhausted before recovery dispatch");
223
+ }
224
+ const fetchWithPolicy = requestTransientPolicy ? fetchWithTransientRetry : fetchWithResetRetry;
213
225
  return await fetchWithPolicy(
214
226
  (transportRecovery?: UpstreamSendRecovery) => {
215
227
  noteAttemptSend(attempt, logCtx.usageLogInputTokens, transportRecovery ?? recovery);
@@ -232,7 +244,12 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
232
244
  {
233
245
  abortSignal: upstream.signal,
234
246
  label: safeHostLabel(request.url),
235
- ...(transientPolicy ? { attempts: transientPolicy.attempts } : {}),
247
+ ...(requestTransientPolicy
248
+ ? {
249
+ attempts: remaining,
250
+ onSendsConsumed: (sends: number) => { transientSendsUsed += Math.max(0, sends); },
251
+ }
252
+ : {}),
236
253
  },
237
254
  );
238
255
  } finally {
@@ -245,7 +262,12 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
245
262
  response = await send(activeRequest);
246
263
  const retryPolicy = rateLimitRetryPolicyFor(activeProvider);
247
264
  let retries = 0;
248
- while (response.status === 429 && retryPolicy && retries < retryPolicy.attempts) {
265
+ while (
266
+ response.status === 429
267
+ && retryPolicy
268
+ && retries < retryPolicy.attempts
269
+ && transientSendAvailable()
270
+ ) {
249
271
  retries += 1;
250
272
  for await (const _ of prepareSameTarget429Wait({
251
273
  body: response.body,
@@ -263,6 +285,10 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
263
285
  promptCacheKey: typeof options.chatBody.prompt_cache_key === "string" ? options.chatBody.prompt_cache_key : undefined,
264
286
  });
265
287
  if (!rotated) break;
288
+ // Rotation also records the failed key's cooldown and persists the next healthy key.
289
+ // Keep that bookkeeping when this request has spent its final send, but preserve the
290
+ // terminal 429 body and do not dispatch with the replacement credential.
291
+ if (!transientSendAvailable()) break;
266
292
  try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
267
293
  activeProvider = rotated;
268
294
  activeAdapter = createOpenAIChatAdapter(activeProvider);
@@ -50,6 +50,10 @@ import {
50
50
  isTranslatorBudgetExceededError,
51
51
  type TranslatorBudget,
52
52
  } from "../lib/translator-budget";
53
+ import {
54
+ parseRequestEffortRowId,
55
+ type ParsedEffortRowId,
56
+ } from "./effort-row";
53
57
 
54
58
  type Rec = Record<string, unknown>;
55
59
 
@@ -600,7 +604,9 @@ async function handleClaudeMessagesWithBudget(
600
604
  let anthropicBody: unknown;
601
605
  let internalBody: Rec;
602
606
  let cacheKeySource: ClaudeCacheKeySource = null;
603
- let effortOverride: ReturnType<typeof extractOcxEffortDirective> = null;
607
+ let effortOverride: string | null = null;
608
+ let effortRow: ParsedEffortRowId | null = null;
609
+ let requestedModel = "";
604
610
  try {
605
611
  anthropicBody = await readAnthropicBody(req, translatorBudget);
606
612
  // Defensive [1m] strip (devlog 138): clients normally remove the context-variant
@@ -620,6 +626,14 @@ async function handleClaudeMessagesWithBudget(
620
626
  effortOverride = extractOcxEffortDirective(anthropicBody);
621
627
  }
622
628
  }
629
+ if (isRec(anthropicBody) && typeof anthropicBody.model === "string") {
630
+ requestedModel = anthropicBody.model;
631
+ effortRow = parseRequestEffortRowId(requestedModel, config);
632
+ if (effortRow) {
633
+ anthropicBody.model = effortRow.baseId;
634
+ effortOverride = effortRow.effort;
635
+ }
636
+ }
623
637
  // Debug capture (opt-in allowlist scalars) BEFORE the passthrough branch so
624
638
  // native, routed, and disabled-alias paths are all observable (devlog 130 B1).
625
639
  captureClaudeInbound(
@@ -643,7 +657,7 @@ async function handleClaudeMessagesWithBudget(
643
657
  );
644
658
  if (claudeConversationId) logCtx.conversationId = claudeConversationId;
645
659
  }
646
- if (isRec(anthropicBody) && wantsNativePassthrough(req, config, requestPolicy, anthropicBody.model)) {
660
+ if (!effortRow && isRec(anthropicBody) && wantsNativePassthrough(req, config, requestPolicy, anthropicBody.model)) {
647
661
  return await anthropicNativePassthrough(req, config, logCtx, logIds, anthropicBody, "/v1/messages");
648
662
  }
649
663
  if (isRec(anthropicBody) && effortOverride) {
@@ -669,7 +683,7 @@ async function handleClaudeMessagesWithBudget(
669
683
  );
670
684
  }
671
685
 
672
- const requestedModel = (anthropicBody as Rec).model as string;
686
+ if (!requestedModel) requestedModel = (anthropicBody as Rec).model as string;
673
687
  const stream = internalBody.stream === true;
674
688
  // Routed adapters only support streamed turns; always stream internally and fold
675
689
  // the translated Anthropic SSE into a message JSON for non-streaming clients.
@@ -0,0 +1,131 @@
1
+ import { comboModelId, comboPublicModelId } from "../combos/types";
2
+ import { detectCursorInstalls } from "../integrations/cursor-detect";
3
+ import {
4
+ loadCursorEffortTable,
5
+ type CursorEffortTable,
6
+ } from "../integrations/cursor-effort-table";
7
+ import {
8
+ canonicalizeReasoningEfforts,
9
+ isDeclaredReasoningEffort,
10
+ } from "../reasoning-effort";
11
+ import { knownModelIdsForProvider } from "../router";
12
+ import { policyModelId, policyPublicModelId } from "../routing/profile";
13
+ import type { OcxConfig } from "../types";
14
+ import { routedSlug } from "../providers/slug-codec";
15
+ import { predictCursorEffort } from "./models-capabilities";
16
+
17
+ const EFFORT_ROW_SEPARATOR = "--";
18
+
19
+ export interface ParsedEffortRowId {
20
+ baseId: string;
21
+ effort: string;
22
+ }
23
+
24
+ export type EffortRowKnownIds = ReadonlySet<string> | ((id: string) => boolean);
25
+
26
+ export interface EffortRowOptions {
27
+ knownIds?: EffortRowKnownIds;
28
+ table?: CursorEffortTable | null;
29
+ supportsReasoning?: boolean;
30
+ }
31
+
32
+ function isKnownId(knownIds: EffortRowKnownIds | undefined, id: string): boolean {
33
+ return typeof knownIds === "function" ? knownIds(id) : knownIds?.has(id) === true;
34
+ }
35
+
36
+ export function effortRowId(baseId: string, effort: string): string {
37
+ return `${baseId}${EFFORT_ROW_SEPARATOR}${effort}`;
38
+ }
39
+
40
+ /**
41
+ * Exact configured/public ids that must beat the synthetic terminal-suffix grammar.
42
+ * This is request-local because live-model cache contents can change while the server runs.
43
+ */
44
+ export function knownEffortRowIds(config: OcxConfig): Set<string> {
45
+ const ids = new Set<string>();
46
+ for (const [providerName, provider] of Object.entries(config.providers)) {
47
+ const known = knownModelIdsForProvider(providerName, provider, config);
48
+ const namespaces = [providerName, provider.alias].filter((value): value is string => (
49
+ typeof value === "string" && value.length > 0
50
+ ));
51
+ for (const id of known) {
52
+ ids.add(id);
53
+ ids.add(routedSlug(providerName, id));
54
+ for (const namespace of namespaces) ids.add(`${namespace}/${id}`);
55
+ }
56
+ for (const alias of Object.values(provider.modelAliases ?? {})) {
57
+ ids.add(alias);
58
+ for (const namespace of namespaces) ids.add(`${namespace}/${alias}`);
59
+ }
60
+ }
61
+ for (const [id, combo] of Object.entries(config.combos ?? {})) {
62
+ ids.add(comboModelId(id));
63
+ ids.add(comboPublicModelId(id, combo));
64
+ }
65
+ for (const [id, profile] of Object.entries(config.routingProfiles ?? {})) {
66
+ ids.add(policyModelId(id));
67
+ ids.add(policyPublicModelId(id, profile));
68
+ }
69
+ return ids;
70
+ }
71
+
72
+ /** Resolve the installed Private Inference effort table once for the current request. */
73
+ export function loadDetectedCursorEffortTable(): CursorEffortTable | null {
74
+ const privateInference = detectCursorInstalls().find(install => install.build === "private-inference");
75
+ return loadCursorEffortTable(privateInference);
76
+ }
77
+
78
+ export function parseEffortRowId(
79
+ id: string,
80
+ config: Pick<OcxConfig, "cursorEffortRows">,
81
+ options: EffortRowOptions = {},
82
+ ): ParsedEffortRowId | null {
83
+ if (config.cursorEffortRows !== true || isKnownId(options.knownIds, id)) return null;
84
+
85
+ const separator = id.lastIndexOf(EFFORT_ROW_SEPARATOR);
86
+ if (separator <= 0) return null;
87
+ const baseId = id.slice(0, separator);
88
+ const effort = id.slice(separator + EFFORT_ROW_SEPARATOR.length);
89
+ // "none" is never published as a row (discovery filters it), so it is never accepted either.
90
+ if (effort === "none" || !isDeclaredReasoningEffort(effort)) return null;
91
+ if (predictCursorEffort(baseId, options.table ?? null, options.supportsReasoning).ladder !== null) {
92
+ return null;
93
+ }
94
+ return { baseId, effort };
95
+ }
96
+
97
+ /** Parse one ingress selector against the current config and installed Cursor table. */
98
+ export function parseRequestEffortRowId(id: string, config: OcxConfig): ParsedEffortRowId | null {
99
+ if (config.cursorEffortRows !== true) return null;
100
+ // Ordinary ids carry no separator; bail before the known-id scan and install detection so
101
+ // the flag costs nothing on the request path for models that are not effort rows.
102
+ if (id.lastIndexOf(EFFORT_ROW_SEPARATOR) <= 0) return null;
103
+ return parseEffortRowId(id, config, {
104
+ knownIds: knownEffortRowIds(config),
105
+ table: loadDetectedCursorEffortTable(),
106
+ });
107
+ }
108
+
109
+ export function expandCursorEffortRow<T extends { id: string }>(
110
+ row: T,
111
+ efforts: readonly string[] | undefined,
112
+ config: Pick<OcxConfig, "cursorEffortRows">,
113
+ options: EffortRowOptions = {},
114
+ ): T[] {
115
+ if (config.cursorEffortRows !== true) return [row];
116
+
117
+ const supported = canonicalizeReasoningEfforts(
118
+ (efforts ?? []).filter(effort => effort !== "none" && isDeclaredReasoningEffort(effort)),
119
+ );
120
+ const supportsReasoning = options.supportsReasoning ?? supported.length > 0;
121
+ if (predictCursorEffort(row.id, options.table ?? null, supportsReasoning).ladder !== null) {
122
+ return [row];
123
+ }
124
+ return [
125
+ row,
126
+ ...supported
127
+ .map(effort => effortRowId(row.id, effort))
128
+ .filter(id => !isKnownId(options.knownIds, id))
129
+ .map(id => ({ ...row, id })),
130
+ ];
131
+ }