@bitkyc08/opencodex 2.10.0 → 2.10.1

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 (274) hide show
  1. package/AGENTS_INSTALL.md +77 -0
  2. package/README.md +4 -10
  3. package/bin/ocx.mjs +71 -18
  4. package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
  5. package/gui/dist/assets/index-ChZQsmBY.js +70 -0
  6. package/gui/dist/index.html +2 -2
  7. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  8. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  9. package/gui/dist/provider-icons/claude-color.svg +1 -1
  10. package/gui/dist/provider-icons/cline-color.svg +16 -0
  11. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  12. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  13. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  14. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  15. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  16. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  17. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  18. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  19. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  20. package/gui/dist/provider-icons/grok.svg +1 -1
  21. package/gui/dist/provider-icons/groq-color.svg +1 -1
  22. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  23. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  24. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  25. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  26. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  27. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  28. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  29. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  30. package/gui/dist/provider-icons/openai.svg +1 -1
  31. package/gui/dist/provider-icons/opencode.svg +2 -1
  32. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  33. package/gui/dist/provider-icons/pi.svg +2 -2
  34. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  35. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  36. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  37. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  38. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  39. package/package.json +8 -4
  40. package/src/adapters/anthropic.ts +208 -14
  41. package/src/adapters/base.ts +16 -5
  42. package/src/adapters/cursor/effort-map.ts +3 -2
  43. package/src/adapters/cursor/framing.ts +39 -0
  44. package/src/adapters/cursor/live-transport.ts +105 -95
  45. package/src/adapters/cursor/native-exec.ts +32 -6
  46. package/src/adapters/cursor/protobuf-request.ts +20 -15
  47. package/src/adapters/cursor/request-builder.ts +21 -7
  48. package/src/adapters/cursor/types.ts +7 -0
  49. package/src/adapters/google-antigravity-replay.ts +237 -21
  50. package/src/adapters/google-truncation.ts +11 -0
  51. package/src/adapters/google.ts +50 -9
  52. package/src/adapters/identity.ts +39 -6
  53. package/src/adapters/kiro-errors.ts +11 -0
  54. package/src/adapters/kiro-events.ts +19 -1
  55. package/src/adapters/kiro-thinking.ts +10 -2
  56. package/src/adapters/kiro-tools.ts +10 -1
  57. package/src/adapters/kiro.ts +37 -11
  58. package/src/adapters/openai-chat.ts +284 -83
  59. package/src/adapters/openai-responses.ts +182 -24
  60. package/src/bridge.ts +177 -7
  61. package/src/chat/outbound.ts +78 -23
  62. package/src/claude/agents-inject.ts +27 -5
  63. package/src/claude/inbound.ts +11 -1
  64. package/src/claude/model-info.ts +13 -10
  65. package/src/claude/outbound.ts +17 -0
  66. package/src/cli/account-api.ts +24 -0
  67. package/src/cli/account-auth.ts +31 -6
  68. package/src/cli/account-main.ts +317 -0
  69. package/src/cli/account.ts +5 -0
  70. package/src/cli/claude.ts +2 -1
  71. package/src/cli/doctor.ts +93 -22
  72. package/src/cli/export-command.ts +26 -12
  73. package/src/cli/help.ts +8 -6
  74. package/src/cli/index.ts +56 -22
  75. package/src/cli/integrations.ts +84 -1
  76. package/src/cli/observe.ts +54 -1
  77. package/src/cli/opencode.ts +2 -1
  78. package/src/cli/provider-runtime.ts +18 -1
  79. package/src/cli/route-policy.ts +92 -0
  80. package/src/cli/runtime-api.ts +6 -3
  81. package/src/cli/star-prompt.ts +71 -15
  82. package/src/cli/status.ts +1 -1
  83. package/src/cli/v2.ts +36 -9
  84. package/src/clients/config-export.ts +687 -10
  85. package/src/codex/account-lifecycle.ts +30 -5
  86. package/src/codex/account-usability.ts +22 -2
  87. package/src/codex/admission.ts +255 -0
  88. package/src/codex/auth-api.ts +427 -140
  89. package/src/codex/auth-context.ts +155 -30
  90. package/src/codex/autostart-health.ts +8 -1
  91. package/src/codex/catalog/account-models.ts +62 -0
  92. package/src/codex/catalog/aggregation.ts +14 -1
  93. package/src/codex/catalog/bundled.ts +282 -32
  94. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  95. package/src/codex/catalog/metadata.ts +51 -6
  96. package/src/codex/catalog/parsing.ts +6 -3
  97. package/src/codex/catalog/provider-fetch.ts +576 -41
  98. package/src/codex/catalog/sync.ts +505 -66
  99. package/src/codex/catalog-admission.ts +197 -0
  100. package/src/codex/catalog-write-serialization.ts +241 -0
  101. package/src/codex/catalog.ts +2 -1
  102. package/src/codex/codex-write-lock.ts +372 -0
  103. package/src/codex/convergence-types.ts +593 -0
  104. package/src/codex/convergence.ts +441 -0
  105. package/src/codex/desired-state.ts +177 -0
  106. package/src/codex/features.ts +52 -8
  107. package/src/codex/generation.ts +202 -0
  108. package/src/codex/history-job.ts +257 -0
  109. package/src/codex/history-lock.ts +241 -0
  110. package/src/codex/history-migration-guardian.ts +18 -5
  111. package/src/codex/history-provider.ts +9 -2
  112. package/src/codex/history-transition.ts +105 -0
  113. package/src/codex/history-worker.ts +176 -0
  114. package/src/codex/inject-coordination.ts +245 -0
  115. package/src/codex/inject.ts +605 -124
  116. package/src/codex/integration-record.ts +266 -0
  117. package/src/codex/internal/catalog-writer.ts +203 -0
  118. package/src/codex/internal/history-writer.ts +80 -0
  119. package/src/codex/journal.ts +10 -1
  120. package/src/codex/main-account-cache.ts +24 -0
  121. package/src/codex/management-convergence.ts +114 -0
  122. package/src/codex/native-main-admission.ts +47 -0
  123. package/src/codex/native-main-auth-temp.ts +187 -0
  124. package/src/codex/native-main-claim.ts +167 -0
  125. package/src/codex/native-main-lock-file.ts +158 -0
  126. package/src/codex/native-main-owner.ts +315 -0
  127. package/src/codex/native-profile-api.ts +247 -0
  128. package/src/codex/native-profile-manager.ts +1512 -0
  129. package/src/codex/native-profile-processes.ts +121 -0
  130. package/src/codex/native-profile-recovery.ts +99 -0
  131. package/src/codex/native-profile-stage-store.ts +387 -0
  132. package/src/codex/native-profile-startup.ts +340 -0
  133. package/src/codex/native-profile-store.ts +855 -0
  134. package/src/codex/native-profile-types.ts +120 -0
  135. package/src/codex/native-residue.ts +557 -0
  136. package/src/codex/project-config-warnings.ts +18 -4
  137. package/src/codex/prompt-journal.ts +311 -0
  138. package/src/codex/prompt-layers.ts +967 -0
  139. package/src/codex/prompt-lock.ts +143 -0
  140. package/src/codex/quota-rejection.ts +224 -0
  141. package/src/codex/quota.ts +86 -3
  142. package/src/codex/routing.ts +299 -62
  143. package/src/codex/runtime.ts +159 -38
  144. package/src/codex/shim.ts +39 -13
  145. package/src/codex/subagent-model-fallback.ts +73 -12
  146. package/src/codex/transition-state.ts +604 -0
  147. package/src/codex/upstream-host-health.ts +70 -0
  148. package/src/codex/user-identity.ts +266 -0
  149. package/src/codex/write-coordination.ts +114 -0
  150. package/src/config.ts +562 -26
  151. package/src/generated/jawcode-model-metadata.ts +2 -2
  152. package/src/grok/inject.ts +15 -4
  153. package/src/grok/inspect.ts +45 -0
  154. package/src/images/loop.ts +113 -20
  155. package/src/integrations/config-io.ts +151 -0
  156. package/src/integrations/journal.ts +315 -0
  157. package/src/integrations/merge.ts +135 -0
  158. package/src/integrations/native/ownership-preflight.ts +165 -0
  159. package/src/integrations/ownership.ts +111 -0
  160. package/src/integrations/registry.ts +101 -0
  161. package/src/integrations/serialize.ts +235 -0
  162. package/src/integrations/state.ts +290 -0
  163. package/src/integrations/store.ts +103 -0
  164. package/src/integrations/writer.ts +492 -0
  165. package/src/lib/bounded-body.ts +46 -8
  166. package/src/lib/bun-runtime.ts +110 -1
  167. package/src/lib/bun-stream-caps.ts +2 -1
  168. package/src/lib/redact.ts +407 -2
  169. package/src/lib/shadow-call.ts +24 -0
  170. package/src/lib/translator-budget.ts +10 -0
  171. package/src/lib/upstream-reachability.ts +91 -0
  172. package/src/lib/upstream-retry.ts +154 -2
  173. package/src/lib/windows-secret-acl.ts +212 -11
  174. package/src/lib/winsw.ts +9 -3
  175. package/src/oauth/index.ts +61 -3
  176. package/src/oauth/key-providers.ts +4 -0
  177. package/src/oauth/kiro.ts +50 -6
  178. package/src/oauth/store.ts +31 -6
  179. package/src/oauth/token-guardian.ts +9 -3
  180. package/src/providers/codex-capacity.ts +288 -0
  181. package/src/providers/derive.ts +33 -1
  182. package/src/providers/free-directory.ts +3 -1
  183. package/src/providers/key-failover.ts +71 -3
  184. package/src/providers/openai-sidecar.ts +64 -4
  185. package/src/providers/openai-virtual-models.ts +1 -0
  186. package/src/providers/quota.ts +334 -26
  187. package/src/providers/registry.ts +284 -16
  188. package/src/providers/xai-transport.ts +11 -4
  189. package/src/responses/compaction.ts +8 -1
  190. package/src/responses/hosted-tool-policy.ts +9 -0
  191. package/src/responses/parser.ts +44 -2
  192. package/src/responses/reasoning-envelope.ts +9 -1
  193. package/src/responses/reasoning-replay-cache.ts +105 -0
  194. package/src/responses/spill-store.ts +45 -8
  195. package/src/responses/state.ts +161 -17
  196. package/src/router.ts +302 -16
  197. package/src/routing/analytics.ts +377 -0
  198. package/src/routing/capability.ts +204 -0
  199. package/src/routing/cost.ts +77 -0
  200. package/src/routing/evaluator.ts +444 -0
  201. package/src/routing/health.ts +401 -0
  202. package/src/routing/history/cursor.ts +43 -0
  203. package/src/routing/history/indexer.ts +590 -0
  204. package/src/routing/history/schema.ts +72 -0
  205. package/src/routing/profile.ts +423 -0
  206. package/src/routing/quota.ts +124 -0
  207. package/src/routing/request-evidence.ts +45 -0
  208. package/src/routing/trace.ts +686 -0
  209. package/src/server/auth-cors.ts +46 -6
  210. package/src/server/chat-completions.ts +28 -13
  211. package/src/server/claude-messages.ts +23 -15
  212. package/src/server/gui-static.ts +39 -10
  213. package/src/server/images.ts +10 -1
  214. package/src/server/index.ts +238 -52
  215. package/src/server/lifecycle.ts +228 -9
  216. package/src/server/live.ts +14 -3
  217. package/src/server/management/agent-settings-routes.ts +64 -14
  218. package/src/server/management/combo-routes.ts +5 -5
  219. package/src/server/management/config-routes.ts +1 -1
  220. package/src/server/management/context.ts +42 -2
  221. package/src/server/management/integration-routes.ts +538 -0
  222. package/src/server/management/logs-usage-routes.ts +1 -1
  223. package/src/server/management/model-routes.ts +32 -113
  224. package/src/server/management/model-rows.ts +117 -0
  225. package/src/server/management/native-integration-routes.ts +587 -0
  226. package/src/server/management/oauth-account-routes.ts +1 -1
  227. package/src/server/management/provider-routes.ts +218 -117
  228. package/src/server/management/request-history-routes.ts +191 -0
  229. package/src/server/management/routing-analytics-routes.ts +74 -0
  230. package/src/server/management/routing-profile-routes.ts +177 -0
  231. package/src/server/management/shared.ts +2 -2
  232. package/src/server/management/sidebar-routes.ts +47 -31
  233. package/src/server/management/sync-response.ts +69 -0
  234. package/src/server/management/system-restart.ts +276 -24
  235. package/src/server/management/system-routes.ts +4 -0
  236. package/src/server/management-api.ts +84 -9
  237. package/src/server/management-auth.ts +43 -5
  238. package/src/server/relay-eager.ts +82 -42
  239. package/src/server/relay.ts +120 -6
  240. package/src/server/request-log.ts +26 -6
  241. package/src/server/responses/collaboration.ts +63 -8
  242. package/src/server/responses/compact.ts +272 -41
  243. package/src/server/responses/core.ts +730 -132
  244. package/src/server/responses/fetch-helpers.ts +15 -1
  245. package/src/server/responses-item-id-repair.ts +32 -3
  246. package/src/server/responses-json-events.ts +52 -0
  247. package/src/server/responses-snapshot-repair.ts +621 -0
  248. package/src/server/search.ts +51 -6
  249. package/src/server/sse-payload-rewrite.ts +89 -12
  250. package/src/server/startup-health-cache.ts +7 -1
  251. package/src/server/ws-bridge.ts +11 -17
  252. package/src/service-manager-probe.ts +297 -0
  253. package/src/service.ts +222 -32
  254. package/src/tray/windows-tray.ps1 +9 -0
  255. package/src/tray/windows.ts +15 -7
  256. package/src/types.ts +194 -14
  257. package/src/update/index.ts +13 -13
  258. package/src/update/job.ts +24 -21
  259. package/src/update/notify.ts +7 -3
  260. package/src/usage/cost.ts +0 -0
  261. package/src/usage/expected-prices.ts +129 -10
  262. package/src/usage/log.ts +50 -15
  263. package/src/usage/summary.ts +4 -4
  264. package/src/vision/index.ts +6 -1
  265. package/src/web-search/loop.ts +161 -34
  266. package/gui/dist/assets/index-OY43ubAq.css +0 -1
  267. package/gui/dist/assets/index-YwNnKZcL.js +0 -67
  268. package/gui/dist/provider-icons/antigravity.svg +0 -1
  269. package/gui/dist/provider-icons/claude.svg +0 -1
  270. package/gui/dist/provider-icons/copilot.svg +0 -1
  271. package/gui/dist/provider-icons/cursor.svg +0 -2
  272. package/gui/dist/provider-icons/gemini.svg +0 -1
  273. package/gui/dist/provider-icons/grok-color.svg +0 -1
  274. package/gui/dist/provider-icons/kiro.svg +0 -14
@@ -11,6 +11,7 @@ import {
11
11
  providerBaseUrlConfigError,
12
12
  providerHeadersConfigError,
13
13
  saveConfigPreservingClaudeCode,
14
+ withConfigMutationLockSync,
14
15
  } from "../../config";
15
16
  import {
16
17
  clearLoginState,
@@ -27,7 +28,7 @@ import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
27
28
  import { ProviderOutboundPolicyError, providerOutboundGet, providerRedirectError } from "../../lib/provider-outbound";
28
29
  import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
29
30
  import { deriveProviderPresets } from "../../providers/derive";
30
- import { providerCodexAccountMode } from "../../providers/registry";
31
+ import { providerCodexAccountMode, providerMatchesRegistryTransport } from "../../providers/registry";
31
32
  import {
32
33
  extractModelEnvelopeRows,
33
34
  extractProviderModelItems,
@@ -70,8 +71,169 @@ import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, C
70
71
  import type { ManagementContext } from "./context";
71
72
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
72
73
 
74
+ type ProviderPatchApplication =
75
+ | { error: string }
76
+ | {
77
+ next: OcxProviderConfig;
78
+ touched: boolean;
79
+ editorTouched: boolean;
80
+ enablingOpenAi: boolean;
81
+ headersTouched: boolean;
82
+ };
83
+
84
+ /**
85
+ * Apply the recognized PATCH field mask onto a provider copy. The caller runs this once
86
+ * for validation and again inside the config mutation lock against the newest provider,
87
+ * so a concurrent PATCH cannot be erased by a save of a stale snapshot. Only synchronous
88
+ * checks live here; the async destination probe stays in the route.
89
+ */
90
+ function applyProviderPatchFields(
91
+ name: string,
92
+ provider: OcxProviderConfig,
93
+ rawBody: Record<string, unknown>,
94
+ keys: string[],
95
+ config: OcxConfig,
96
+ ): ProviderPatchApplication {
97
+ const next: OcxProviderConfig = { ...provider };
98
+ let touched = false;
99
+ let headersTouched = false;
100
+
101
+ if (Object.hasOwn(rawBody, "disabled")) {
102
+ if (typeof rawBody.disabled !== "boolean") return { error: "disabled must be a boolean" };
103
+ if (rawBody.disabled && name === config.defaultProvider) {
104
+ return { error: "cannot disable the default provider; set another default first" };
105
+ }
106
+ next.disabled = rawBody.disabled;
107
+ touched = true;
108
+ }
109
+ if (Object.hasOwn(rawBody, "adapter")) {
110
+ if (typeof rawBody.adapter !== "string" || !rawBody.adapter.trim()) return { error: "adapter must be a non-empty string" };
111
+ next.adapter = rawBody.adapter.trim();
112
+ touched = true;
113
+ }
114
+ if (Object.hasOwn(rawBody, "baseUrl")) {
115
+ if (typeof rawBody.baseUrl !== "string" || !rawBody.baseUrl.trim()) return { error: "baseUrl must be a non-empty string" };
116
+ next.baseUrl = rawBody.baseUrl.trim();
117
+ touched = true;
118
+ }
119
+ if (Object.hasOwn(rawBody, "defaultModel")) {
120
+ if (typeof rawBody.defaultModel !== "string") return { error: "defaultModel must be a string" };
121
+ const dm = rawBody.defaultModel.trim();
122
+ if (dm) next.defaultModel = dm;
123
+ else delete next.defaultModel;
124
+ touched = true;
125
+ }
126
+ if (Object.hasOwn(rawBody, "authMode")) {
127
+ if (typeof rawBody.authMode !== "string") return { error: "authMode must be a string" };
128
+ const mode = rawBody.authMode.trim();
129
+ if (mode === "key" || mode === "forward" || mode === "oauth" || mode === "local") {
130
+ next.authMode = mode;
131
+ touched = true;
132
+ } else if (mode === "") {
133
+ delete next.authMode;
134
+ touched = true;
135
+ } else {
136
+ return { error: "authMode must be key, forward, oauth, or local" };
137
+ }
138
+ }
139
+ if (Object.hasOwn(rawBody, "apiKeyTransport")) {
140
+ const transport = rawBody.apiKeyTransport;
141
+ if (transport === "x-api-key" || transport === "bearer") {
142
+ next.apiKeyTransport = transport;
143
+ touched = true;
144
+ } else if (transport === "") {
145
+ delete next.apiKeyTransport;
146
+ touched = true;
147
+ } else {
148
+ return { error: "apiKeyTransport must be x-api-key, bearer, or empty to clear" };
149
+ }
150
+ }
151
+ if (Object.hasOwn(rawBody, "note")) {
152
+ if (typeof rawBody.note !== "string") return { error: "note must be a string" };
153
+ const note = rawBody.note.trim();
154
+ if (note) next.note = note;
155
+ else delete next.note;
156
+ touched = true;
157
+ }
158
+ if (Object.hasOwn(rawBody, "allowPrivateNetwork")) {
159
+ if (typeof rawBody.allowPrivateNetwork !== "boolean") return { error: "allowPrivateNetwork must be a boolean" };
160
+ next.allowPrivateNetwork = rawBody.allowPrivateNetwork;
161
+ touched = true;
162
+ }
163
+ if (Object.hasOwn(rawBody, "liveModels")) {
164
+ if (typeof rawBody.liveModels !== "boolean") return { error: "liveModels must be a boolean" };
165
+ next.liveModels = rawBody.liveModels;
166
+ touched = true;
167
+ }
168
+
169
+ // headers is the one object-valued field in the mask. PATCH semantics merge it
170
+ // shallowly into the existing block so a single fingerprint header can be added
171
+ // without wiping the rest; null or an empty object clears user-managed headers.
172
+ if (Object.hasOwn(rawBody, "headers")) {
173
+ const headersValue = rawBody.headers;
174
+ if (headersValue === null || (isPlainRecord(headersValue) && Object.keys(headersValue).length === 0)) {
175
+ // Registry-owned static metadata (e.g. opencode-free's x-opencode-client marker)
176
+ // is not user-managed: restoring it keeps the upstream transport intact after a
177
+ // clear instead of deleting the whole block.
178
+ const entry = getProviderRegistryEntry(name);
179
+ if (entry?.staticHeaders && providerMatchesRegistryTransport(name, next)) {
180
+ next.headers = { ...entry.staticHeaders };
181
+ } else {
182
+ delete next.headers;
183
+ }
184
+ } else {
185
+ if (!isPlainRecord(headersValue)) return { error: "headers must be an object" };
186
+ const headersError = providerHeadersConfigError(headersValue);
187
+ if (headersError) return { error: headersError };
188
+ // Header names are case-insensitive on the wire. Drop any existing key whose
189
+ // lowercase name collides with an incoming one, or Headers normalization would
190
+ // send a combined "x-custom: v1, v2" value upstream.
191
+ const incoming = new Map(
192
+ Object.entries(headersValue as Record<string, string>).map(([key, value]) => [key.toLowerCase(), [key, value] as const]),
193
+ );
194
+ const merged: Record<string, string> = {};
195
+ for (const [key, value] of Object.entries(next.headers ?? {})) {
196
+ if (!incoming.has(key.toLowerCase())) merged[key] = value;
197
+ }
198
+ for (const [key, value] of incoming.values()) merged[key] = value;
199
+ next.headers = merged;
200
+ }
201
+ touched = true;
202
+ headersTouched = true;
203
+ }
204
+
205
+ if (!touched) return { error: "no recognized fields to update" };
206
+
207
+ // A disabled-only toggle preserves the v2 fast lane for non-openai providers: it changes
208
+ // routing eligibility, not the provider shape. Re-enabling `openai` is different — a
209
+ // malformed disabled row must not come back online unchanged, so canonicalize/reject
210
+ // against the same built-in gate used by mode PATCH and POST.
211
+ const editorTouched = keys.some(key => key !== "disabled");
212
+ const enablingOpenAi = name === "openai"
213
+ && Object.hasOwn(rawBody, "disabled")
214
+ && rawBody.disabled === false
215
+ && provider.disabled === true;
216
+ if (!editorTouched && enablingOpenAi) {
217
+ if (!isCanonicalOpenAiForwardProvider(next)) {
218
+ return { error: "provider openai must be the canonical built-in provider" };
219
+ }
220
+ // Persist the byte-identical canonical URL so config.ts startup checks (case-sensitive)
221
+ // accept the row after we fill mode. Equivalent hosts like CHATGPT.com/:443 normalize here.
222
+ next.baseUrl = CODEX_FORWARD_BASE_URL;
223
+ // Fill missing mode so a disabled canonical row becomes a complete live openai entry.
224
+ if (next.codexAccountMode !== "pool" && next.codexAccountMode !== "direct") {
225
+ next.codexAccountMode = "pool";
226
+ }
227
+ if (next.disabled === false) delete next.disabled;
228
+ // Canonical openai never uses private-network opt-in; drop a stale flag that
229
+ // was ignored for the DNS probe so it cannot linger on the live row.
230
+ delete next.allowPrivateNetwork;
231
+ }
232
+ return { next, touched, editorTouched, enablingOpenAi, headersTouched };
233
+ }
234
+
73
235
  export async function handleProviderRoutes(ctx: ManagementContext): Promise<Response | null> {
74
- const { req, url, config, deps, refreshCodexCatalogBestEffort, syncClaudeAgentDefsBestEffort } = ctx;
236
+ const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
75
237
 
76
238
  if (url.pathname === "/api/provider-quotas" && req.method === "GET") {
77
239
  const forceRefresh = url.searchParams.get("refresh") === "1" || url.searchParams.get("refresh") === "true";
@@ -82,6 +244,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
82
244
  return jsonResponse(Object.entries(config.providers).map(([name, p]) => ({
83
245
  name, adapter: p.adapter, baseUrl: publicProviderBaseUrl(p.baseUrl), defaultModel: p.defaultModel,
84
246
  hasApiKey: !!p.apiKey,
247
+ // Presence only (#959 review): header names and values never leave the process.
248
+ hasHeaders: !!p.headers && Object.keys(p.headers).length > 0,
85
249
  allowPrivateNetwork: p.allowPrivateNetwork === true,
86
250
  liveModels: p.liveModels !== false,
87
251
  models: p.models ?? [],
@@ -144,8 +308,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
144
308
  }
145
309
  const { clearModelCache } = await import("../../codex/model-cache");
146
310
  clearModelCache(name);
147
- await refreshCodexCatalogBestEffort();
148
- return jsonResponse({ success: true, name });
311
+ const catalogRefresh = await convergeCodexCatalog();
312
+ return jsonResponse({ success: true, name, catalogRefresh });
149
313
  }
150
314
 
151
315
  if (url.pathname === "/api/providers" && req.method === "PATCH") {
@@ -213,106 +377,16 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
213
377
  if (Object.hasOwn(rawBody, "apiKey")) {
214
378
  return jsonResponse({ error: "apiKey cannot be patched here; use the provider API-key endpoints" }, 400);
215
379
  }
216
- const next: OcxProviderConfig = { ...config.providers[name]! };
217
- let touched = false;
218
-
219
- if (Object.hasOwn(rawBody, "disabled")) {
220
- if (typeof rawBody.disabled !== "boolean") return jsonResponse({ error: "disabled must be a boolean" }, 400);
221
- if (rawBody.disabled && name === config.defaultProvider) {
222
- return jsonResponse({ error: "cannot disable the default provider; set another default first" }, 400);
223
- }
224
- next.disabled = rawBody.disabled;
225
- touched = true;
226
- }
227
- if (Object.hasOwn(rawBody, "adapter")) {
228
- if (typeof rawBody.adapter !== "string" || !rawBody.adapter.trim()) return jsonResponse({ error: "adapter must be a non-empty string" }, 400);
229
- next.adapter = rawBody.adapter.trim();
230
- touched = true;
231
- }
232
- if (Object.hasOwn(rawBody, "baseUrl")) {
233
- if (typeof rawBody.baseUrl !== "string" || !rawBody.baseUrl.trim()) return jsonResponse({ error: "baseUrl must be a non-empty string" }, 400);
234
- next.baseUrl = rawBody.baseUrl.trim();
235
- touched = true;
236
- }
237
- if (Object.hasOwn(rawBody, "defaultModel")) {
238
- if (typeof rawBody.defaultModel !== "string") return jsonResponse({ error: "defaultModel must be a string" }, 400);
239
- const dm = rawBody.defaultModel.trim();
240
- if (dm) next.defaultModel = dm;
241
- else delete next.defaultModel;
242
- touched = true;
243
- }
244
- if (Object.hasOwn(rawBody, "authMode")) {
245
- if (typeof rawBody.authMode !== "string") return jsonResponse({ error: "authMode must be a string" }, 400);
246
- const mode = rawBody.authMode.trim();
247
- if (mode === "key" || mode === "forward" || mode === "oauth" || mode === "local") {
248
- next.authMode = mode;
249
- touched = true;
250
- } else if (mode === "") {
251
- delete next.authMode;
252
- touched = true;
253
- } else {
254
- return jsonResponse({ error: "authMode must be key, forward, oauth, or local" }, 400);
255
- }
256
- }
257
- if (Object.hasOwn(rawBody, "apiKeyTransport")) {
258
- const transport = rawBody.apiKeyTransport;
259
- if (transport === "x-api-key" || transport === "bearer") {
260
- next.apiKeyTransport = transport;
261
- touched = true;
262
- } else if (transport === "") {
263
- delete next.apiKeyTransport;
264
- touched = true;
265
- } else {
266
- return jsonResponse({ error: "apiKeyTransport must be x-api-key, bearer, or empty to clear" }, 400);
267
- }
268
- }
269
- if (Object.hasOwn(rawBody, "note")) {
270
- if (typeof rawBody.note !== "string") return jsonResponse({ error: "note must be a string" }, 400);
271
- const note = rawBody.note.trim();
272
- if (note) next.note = note;
273
- else delete next.note;
274
- touched = true;
275
- }
276
-
277
- if (Object.hasOwn(rawBody, "allowPrivateNetwork")) {
278
- if (typeof rawBody.allowPrivateNetwork !== "boolean") return jsonResponse({ error: "allowPrivateNetwork must be a boolean" }, 400);
279
- next.allowPrivateNetwork = rawBody.allowPrivateNetwork;
280
- touched = true;
281
- }
282
-
283
- if (Object.hasOwn(rawBody, "liveModels")) {
284
- if (typeof rawBody.liveModels !== "boolean") return jsonResponse({ error: "liveModels must be a boolean" }, 400);
285
- next.liveModels = rawBody.liveModels;
286
- touched = true;
287
- }
380
+ const applied = applyProviderPatchFields(name, config.providers[name]!, rawBody, keys, config);
381
+ if ("error" in applied) return jsonResponse({ error: applied.error }, 400);
382
+ const next = applied.next;
288
383
 
289
- if (!touched) return jsonResponse({ error: "no recognized fields to update" }, 400);
290
-
291
- // A disabled-only toggle preserves the v2 fast lane for non-openai providers: it changes
292
- // routing eligibility, not the provider shape. Re-enabling `openai` is different — a
293
- // malformed disabled row must not come back online unchanged, so canonicalize/reject
294
- // against the same built-in gate used by mode PATCH and POST.
295
- const editorTouched = keys.some(key => key !== "disabled");
296
- const enablingOpenAi = name === "openai"
297
- && Object.hasOwn(rawBody, "disabled")
298
- && rawBody.disabled === false
299
- && config.providers[name]?.disabled === true;
300
- if (editorTouched) {
384
+ if (applied.editorTouched) {
301
385
  const providerError = providerManagementConfigError(name, next);
302
386
  if (providerError) return jsonResponse({ error: providerError }, 400);
303
387
  const resolvedError = await providerDestinationResolvedError(name, next);
304
388
  if (resolvedError) return jsonResponse({ error: resolvedError }, 400);
305
- } else if (enablingOpenAi) {
306
- if (!isCanonicalOpenAiForwardProvider(next)) {
307
- return jsonResponse({ error: "provider openai must be the canonical built-in provider" }, 400);
308
- }
309
- // Persist the byte-identical canonical URL so config.ts startup checks (case-sensitive)
310
- // accept the row after we fill mode. Equivalent hosts like CHATGPT.com/:443 normalize here.
311
- next.baseUrl = CODEX_FORWARD_BASE_URL;
312
- // Fill missing mode so a disabled canonical row becomes a complete live openai entry.
313
- if (next.codexAccountMode !== "pool" && next.codexAccountMode !== "direct") {
314
- next.codexAccountMode = "pool";
315
- }
389
+ } else if (applied.enablingOpenAi) {
316
390
  // Same DNS gate as POST: Clash fake-IP only. Never honor a persisted
317
391
  // allowPrivateNetwork on this path — it must not bypass the built-in guard.
318
392
  const resolvedError = await providerDestinationResolvedError(
@@ -321,26 +395,47 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
321
395
  { allowBenchmarkAddresses: true },
322
396
  );
323
397
  if (resolvedError) return jsonResponse({ error: resolvedError }, 400);
324
- if (next.disabled === false) delete next.disabled;
325
- // Canonical openai never uses private-network opt-in; drop a stale flag that
326
- // was ignored for the DNS probe so it cannot linger on the live row.
327
- delete next.allowPrivateNetwork;
328
398
  }
329
399
 
330
- const { saveConfigPreservingClaudeCode: save } = await import("../../config");
331
- config.providers[name] = stripRegistryOnlyStaticHeaders(name, next);
332
- save(config);
400
+ // The live config is shared and the destination probe above is awaited. Re-apply the
401
+ // mask onto the newest provider under the mutation lock right before saving, so two
402
+ // concurrent PATCHes updating different fields/headers both survive instead of the
403
+ // later save clobbering the earlier snapshot.
404
+ let replayError: string | undefined;
405
+ withConfigMutationLockSync(() => {
406
+ const replay = applyProviderPatchFields(name, config.providers[name]!, rawBody, keys, config);
407
+ if ("error" in replay) {
408
+ replayError = replay.error;
409
+ return;
410
+ }
411
+ if (replay.editorTouched) {
412
+ const syncError = providerManagementConfigError(name, replay.next);
413
+ if (syncError) {
414
+ replayError = syncError;
415
+ return;
416
+ }
417
+ } else if (replay.enablingOpenAi && !isCanonicalOpenAiForwardProvider(replay.next)) {
418
+ replayError = "provider openai must be the canonical built-in provider";
419
+ return;
420
+ }
421
+ // A PATCH that managed headers owns the resulting block: the clear path restores
422
+ // registry static headers, so exact-match stripping must not erase them again.
423
+ config.providers[name] = replay.headersTouched ? replay.next : stripRegistryOnlyStaticHeaders(name, replay.next);
424
+ saveConfigPreservingClaudeCode(config);
425
+ });
426
+ if (replayError !== undefined) return jsonResponse({ error: replayError }, 409);
333
427
  reconcileLiveStateStores();
334
- if (editorTouched) {
428
+ if (applied.editorTouched) {
335
429
  const { clearModelCache } = await import("../../codex/model-cache");
336
430
  clearModelCache(name);
337
431
  }
338
- await refreshCodexCatalogBestEffort();
432
+ const catalogRefresh = await convergeCodexCatalog();
339
433
  return jsonResponse({
340
434
  success: true,
341
435
  name,
342
436
  disabled: config.providers[name]!.disabled === true,
343
437
  hasApiKey: !!config.providers[name]!.apiKey,
438
+ catalogRefresh,
344
439
  });
345
440
  }
346
441
 
@@ -484,8 +579,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
484
579
  reconcileLiveStateStores();
485
580
  const { clearModelCache: clearCache } = await import("../../codex/model-cache");
486
581
  clearCache(name);
487
- await refreshCodexCatalogBestEffort();
488
- return jsonResponse({ success: true, ...(fallbackDefault ? { defaultProvider: fallbackDefault } : {}) });
582
+ const catalogRefresh = await convergeCodexCatalog();
583
+ return jsonResponse({ success: true, ...(fallbackDefault ? { defaultProvider: fallbackDefault } : {}), catalogRefresh });
489
584
  }
490
585
 
491
586
  if (url.pathname === "/api/provider-context-caps" && req.method === "GET") {
@@ -497,7 +592,13 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
497
592
  try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
498
593
  const { saveConfigPreservingClaudeCode: save } = await import("../../config");
499
594
  const { clearModelCache } = await import("../../codex/model-cache");
500
- const respond = () => jsonResponse({ ok: true, cap: DEFAULT_PROVIDER_CONTEXT_CAP, value: globalContextCapValue(config), caps: providerContextCaps(config) });
595
+ const respond = (catalogRefresh: Awaited<ReturnType<typeof convergeCodexCatalog>>) => jsonResponse({
596
+ ok: true,
597
+ cap: DEFAULT_PROVIDER_CONTEXT_CAP,
598
+ value: globalContextCapValue(config),
599
+ caps: providerContextCaps(config),
600
+ catalogRefresh,
601
+ });
501
602
 
502
603
  // Branch 1: set the global cap value and re-point every enabled provider to it.
503
604
  if (body.value !== undefined) {
@@ -509,8 +610,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
509
610
  save(config);
510
611
  reconcileLiveStateStores();
511
612
  for (const provider of affected) clearModelCache(provider);
512
- await refreshCodexCatalogBestEffort();
513
- return respond();
613
+ const catalogRefresh = await convergeCodexCatalog();
614
+ return respond(catalogRefresh);
514
615
  }
515
616
 
516
617
  // Branch 2: enable/clear the cap for every provider at once.
@@ -524,8 +625,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
524
625
  save(config);
525
626
  reconcileLiveStateStores();
526
627
  for (const provider of new Set([...before, ...names])) clearModelCache(provider);
527
- await refreshCodexCatalogBestEffort();
528
- return respond();
628
+ const catalogRefresh = await convergeCodexCatalog();
629
+ return respond(catalogRefresh);
529
630
  }
530
631
 
531
632
  // Branch 3: existing per-provider toggle (enable writes the current global value).
@@ -543,8 +644,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
543
644
  save(config);
544
645
  reconcileLiveStateStores();
545
646
  clearModelCache(provider);
546
- await refreshCodexCatalogBestEffort();
547
- return respond();
647
+ const catalogRefresh = await convergeCodexCatalog();
648
+ return respond(catalogRefresh);
548
649
  }
549
650
 
550
651
  // Complete GUI picker presets, derived from the canonical provider registry. The GUI is a
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Cursor-paginated request-history API (RI-02).
3
+ *
4
+ * - `GET /api/request-history` - keyset-paginated rows with filters
5
+ * - `GET /api/request-history/:requestId` - one canonical row
6
+ * - `GET /api/request-history/:requestId/route-decision` - why-this-route
7
+ * explanation (RI-09): durable trace + attempts + outcome
8
+ *
9
+ * The index is a derived projection of `usage.jsonl`; every response carries
10
+ * an `index` status block so callers can see schema version, indexed rows and
11
+ * any repair the indexer performed.
12
+ */
13
+
14
+ import {
15
+ queryRequestHistory,
16
+ requestHistoryRowById,
17
+ REQUEST_HISTORY_MAX_PAGE_SIZE,
18
+ } from "../../routing/history/indexer";
19
+ import { InvalidCursorError } from "../../routing/history/cursor";
20
+ import { requestLogEntryFromPersistedUsage } from "../request-log";
21
+ import { requestLogDto } from "./shared";
22
+ import { jsonResponse } from "../auth-cors";
23
+ import type { ManagementContext } from "./context";
24
+ import type { PersistedUsageEntry } from "../../usage/log";
25
+
26
+ function parseQueryInt(raw: string | null): number | undefined | "invalid" {
27
+ if (raw === null) return undefined;
28
+ const trimmed = raw.trim();
29
+ if (trimmed.length === 0) return "invalid";
30
+ const value = Number(trimmed);
31
+ return Number.isInteger(value) ? value : "invalid";
32
+ }
33
+
34
+ function finalAttemptTarget(entry: PersistedUsageEntry): { provider: string; model: string } {
35
+ const attempts = entry.attempts;
36
+ if (Array.isArray(attempts) && attempts.length > 0) {
37
+ const last = attempts[attempts.length - 1];
38
+ if (last && typeof last.provider === "string" && typeof last.model === "string") {
39
+ return { provider: last.provider, model: last.model };
40
+ }
41
+ }
42
+ return { provider: entry.provider, model: entry.model };
43
+ }
44
+
45
+ export async function handleRequestHistoryRoutes(ctx: ManagementContext): Promise<Response | null> {
46
+ const { url, req, config } = ctx;
47
+ if (!url.pathname.startsWith("/api/request-history")) return null;
48
+
49
+ if (url.pathname === "/api/request-history" && req.method === "GET") {
50
+ const statusRaw = parseQueryInt(url.searchParams.get("status"));
51
+ if (statusRaw === "invalid") {
52
+ return jsonResponse({ error: { code: "invalid_status", message: "status must be an integer from 100 to 599" } }, 400, req, config);
53
+ }
54
+ const status = statusRaw;
55
+ if (status !== undefined && (status < 100 || status > 599)) {
56
+ return jsonResponse({ error: { code: "invalid_status", message: "status must be an integer from 100 to 599" } }, 400, req, config);
57
+ }
58
+ const fromRaw = parseQueryInt(url.searchParams.get("from"));
59
+ if (fromRaw === "invalid") {
60
+ return jsonResponse({ error: { code: "invalid_from", message: "from must be an integer timestamp" } }, 400, req, config);
61
+ }
62
+ const toRaw = parseQueryInt(url.searchParams.get("to"));
63
+ if (toRaw === "invalid") {
64
+ return jsonResponse({ error: { code: "invalid_to", message: "to must be an integer timestamp" } }, 400, req, config);
65
+ }
66
+ const from = fromRaw;
67
+ const to = toRaw;
68
+ if (from !== undefined && to !== undefined && from > to) {
69
+ return jsonResponse({ error: { code: "invalid_range", message: "from must not be after to" } }, 400, req, config);
70
+ }
71
+ const limitRaw = url.searchParams.get("limit");
72
+ const limitParsed = limitRaw === null ? undefined : parseQueryInt(limitRaw);
73
+ if (limitParsed === "invalid") {
74
+ return jsonResponse(
75
+ { error: { code: "invalid_limit", message: `limit must be an integer from 1 to ${REQUEST_HISTORY_MAX_PAGE_SIZE}` } },
76
+ 400,
77
+ req,
78
+ config,
79
+ );
80
+ }
81
+ const limit = limitParsed;
82
+ if (limit !== undefined && (limit < 1 || limit > REQUEST_HISTORY_MAX_PAGE_SIZE)) {
83
+ return jsonResponse(
84
+ { error: { code: "invalid_limit", message: `limit must be an integer from 1 to ${REQUEST_HISTORY_MAX_PAGE_SIZE}` } },
85
+ 400,
86
+ req,
87
+ config,
88
+ );
89
+ }
90
+ const cursor = url.searchParams.get("cursor");
91
+ try {
92
+ const page = await queryRequestHistory({
93
+ provider: url.searchParams.get("provider")?.trim() || undefined,
94
+ model: url.searchParams.get("model")?.trim() || undefined,
95
+ requestedModel: url.searchParams.get("requestedModel")?.trim() || undefined,
96
+ status,
97
+ conversationId: url.searchParams.get("conversationId")?.trim() || undefined,
98
+ surface: url.searchParams.get("surface")?.trim() || undefined,
99
+ inboundProtocol: url.searchParams.get("inboundProtocol")?.trim() || undefined,
100
+ apiKeyId: url.searchParams.get("apiKeyId")?.trim() || undefined,
101
+ profileId: url.searchParams.get("profileId")?.trim() || undefined,
102
+ fallback: url.searchParams.get("fallback") === "true"
103
+ ? true
104
+ : url.searchParams.get("fallback") === "false" ? false : undefined,
105
+ from,
106
+ to,
107
+ }, cursor, limit);
108
+ return jsonResponse({
109
+ entries: page.rows.map(row => requestLogDto(requestLogEntryFromPersistedUsage(row))),
110
+ ...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
111
+ hasMore: page.hasMore,
112
+ index: {
113
+ schemaVersion: page.meta.schemaVersion,
114
+ indexedRows: page.meta.indexedRows,
115
+ sourceSize: page.meta.sourceSize,
116
+ sourceMtimeMs: page.meta.sourceMtimeMs,
117
+ builtAtMs: page.meta.builtAtMs,
118
+ lastError: page.meta.lastError,
119
+ },
120
+ }, 200, req, config);
121
+ } catch (err) {
122
+ if (err instanceof InvalidCursorError) {
123
+ return jsonResponse({ error: { code: "invalid_cursor", message: "invalid cursor" } }, 400, req, config);
124
+ }
125
+ throw err;
126
+ }
127
+ }
128
+
129
+ if (url.pathname.startsWith("/api/request-history/") && req.method === "GET") {
130
+ // Why-this-route explanation (RI-09): trace + attempts + outcome.
131
+ if (url.pathname.endsWith("/route-decision")) {
132
+ let requestId: string;
133
+ try {
134
+ requestId = decodeURIComponent(
135
+ url.pathname.slice("/api/request-history/".length, -"/route-decision".length),
136
+ );
137
+ } catch {
138
+ return jsonResponse({ error: { code: "not_found", message: "unknown request" } }, 404, req, config);
139
+ }
140
+ if (!requestId || requestId.includes("/")) {
141
+ return jsonResponse({ error: { code: "not_found", message: "unknown request" } }, 404, req, config);
142
+ }
143
+ const entry = await requestHistoryRowById(requestId);
144
+ if (!entry) {
145
+ return jsonResponse({ error: { code: "not_found", message: "unknown request" } }, 404, req, config);
146
+ }
147
+ const trace = entry.routeDecision ?? null;
148
+ const final = finalAttemptTarget(entry);
149
+ return jsonResponse({
150
+ requestId,
151
+ routeDecision: trace,
152
+ attemptSequence: entry.attempts ?? [],
153
+ outcome: {
154
+ status: entry.status,
155
+ ...(entry.terminalStatus ? { terminalStatus: entry.terminalStatus } : {}),
156
+ ...(entry.closeReason ? { closeReason: entry.closeReason } : {}),
157
+ ...(entry.errorCode ? { errorCode: entry.errorCode } : {}),
158
+ ...(entry.durationMs !== undefined ? { durationMs: entry.durationMs } : {}),
159
+ ...(entry.usageStatus ? { usageStatus: entry.usageStatus } : {}),
160
+ },
161
+ summary: {
162
+ requestedModel: entry.requestedModel ?? entry.model,
163
+ routeKind: trace?.routeKind ?? null,
164
+ ...(trace?.profile ? { profileId: trace.profile.id, revision: trace.profile.revision } : {}),
165
+ selected: trace?.selected
166
+ ? { provider: trace.selected.provider, model: trace.selected.model }
167
+ : null,
168
+ finalProvider: final.provider,
169
+ finalModel: final.model,
170
+ },
171
+ }, 200, req, config);
172
+ }
173
+
174
+ let requestId: string;
175
+ try {
176
+ requestId = decodeURIComponent(url.pathname.slice("/api/request-history/".length));
177
+ } catch {
178
+ return jsonResponse({ error: { code: "not_found", message: "unknown request" } }, 404, req, config);
179
+ }
180
+ if (!requestId || requestId.includes("/")) {
181
+ return jsonResponse({ error: { code: "not_found", message: "unknown request" } }, 404, req, config);
182
+ }
183
+ const entry = await requestHistoryRowById(requestId);
184
+ if (!entry) {
185
+ return jsonResponse({ error: { code: "not_found", message: "unknown request" } }, 404, req, config);
186
+ }
187
+ return jsonResponse(requestLogDto(requestLogEntryFromPersistedUsage(entry)), 200, req, config);
188
+ }
189
+
190
+ return null;
191
+ }