@bitkyc08/opencodex 2.41.0 → 2.43.0-preview.20260906

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 (260) hide show
  1. package/AGENTS_INSTALL.md +2 -2
  2. package/README.md +31 -0
  3. package/bin/ocx.mjs +10 -1
  4. package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
  5. package/gui/dist/assets/index-VGeQEZ_v.js +112 -0
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +1 -1
  8. package/src/adapters/anthropic-image-codec.ts +304 -0
  9. package/src/adapters/anthropic-image-normalize.ts +8 -298
  10. package/src/adapters/anthropic.ts +30 -7
  11. package/src/adapters/command-code.ts +7 -5
  12. package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
  13. package/src/adapters/cursor/images.ts +36 -6
  14. package/src/adapters/cursor/live-transport.ts +7 -2
  15. package/src/adapters/cursor/native-exec-desktop.ts +2 -15
  16. package/src/adapters/cursor/protobuf-request.ts +54 -24
  17. package/src/adapters/cursor/tool-definitions.ts +5 -670
  18. package/src/adapters/cursor/tool-guidance.ts +236 -0
  19. package/src/adapters/cursor/tool-naming.ts +252 -0
  20. package/src/adapters/cursor/tool-schemas.ts +195 -0
  21. package/src/adapters/cursor/types.ts +6 -3
  22. package/src/adapters/exec-tool-result-normalize.ts +1 -1
  23. package/src/adapters/google-errors.ts +9 -1
  24. package/src/adapters/google.ts +1 -0
  25. package/src/adapters/identity.ts +8 -2
  26. package/src/adapters/kiro-calibration.ts +181 -0
  27. package/src/adapters/kiro.ts +135 -3
  28. package/src/adapters/openai-responses.ts +259 -29
  29. package/src/adapters/responses-code-mode.ts +59 -0
  30. package/src/adapters/tool-catalog-nudge.ts +1 -1
  31. package/src/adapters/xai-schema-analysis.ts +86 -0
  32. package/src/adapters/xai-tool-schema.ts +2 -87
  33. package/src/adapters/xai-web-search.ts +1 -1
  34. package/src/bridge.ts +47 -13
  35. package/src/chat/inbound.ts +11 -3
  36. package/src/claude/inbound-content-options.ts +60 -0
  37. package/src/claude/inbound-model-options.ts +142 -0
  38. package/src/claude/inbound-records.ts +7 -0
  39. package/src/claude/inbound.ts +10 -202
  40. package/src/claude/model-info.ts +45 -0
  41. package/src/cli/account-auth.ts +49 -9
  42. package/src/cli/account-extended.ts +7 -1
  43. package/src/cli/capabilities.ts +15 -4
  44. package/src/cli/claude.ts +232 -39
  45. package/src/cli/config-command.ts +9 -1
  46. package/src/cli/dispatch.ts +5 -1
  47. package/src/cli/doctor.ts +10 -0
  48. package/src/cli/effort.ts +372 -0
  49. package/src/cli/export-command.ts +3 -9
  50. package/src/cli/help.ts +1 -0
  51. package/src/cli/index.ts +13 -0
  52. package/src/cli/init.ts +4 -0
  53. package/src/cli/model-selection-guidance.ts +30 -0
  54. package/src/cli/models-runtime.ts +3 -2
  55. package/src/cli/models.ts +8 -3
  56. package/src/cli/observe.ts +3 -1
  57. package/src/cli/opencode.ts +4 -1
  58. package/src/cli/provider-runtime.ts +65 -0
  59. package/src/cli/provider.ts +8 -0
  60. package/src/cli/registry.ts +16 -2
  61. package/src/cli/runtime-api.ts +3 -1
  62. package/src/cli/star-prompt.ts +22 -6
  63. package/src/cli/status-probes.ts +168 -0
  64. package/src/cli/status.ts +5 -168
  65. package/src/clients/config-export/constants.ts +69 -0
  66. package/src/clients/config-export/contracts.ts +154 -0
  67. package/src/clients/config-export/dsh.ts +132 -0
  68. package/src/clients/config-export/fast-models.ts +29 -0
  69. package/src/clients/config-export/mcode.ts +83 -0
  70. package/src/clients/config-export/model-metadata.ts +108 -0
  71. package/src/clients/config-export/omp.ts +104 -0
  72. package/src/clients/config-export/zcode.ts +92 -0
  73. package/src/clients/config-export.ts +18 -710
  74. package/src/codex/account-lifecycle.ts +20 -3
  75. package/src/codex/account-usability.ts +2 -0
  76. package/src/codex/auth-api.ts +272 -32
  77. package/src/codex/auth-context.ts +328 -24
  78. package/src/codex/catalog/effort.ts +44 -5
  79. package/src/codex/catalog/metadata.ts +149 -14
  80. package/src/codex/catalog/native-models.ts +116 -4
  81. package/src/codex/catalog/parsing.ts +122 -8
  82. package/src/codex/catalog/provider-fetch.ts +154 -23
  83. package/src/codex/catalog/reserve.ts +52 -0
  84. package/src/codex/catalog/sync.ts +89 -16
  85. package/src/codex/catalog.ts +1 -1
  86. package/src/codex/convergence-types.ts +1 -0
  87. package/src/codex/convergence.ts +2 -0
  88. package/src/codex/data/upstream-models.json +169 -0
  89. package/src/codex/forward-transport-headers.ts +25 -0
  90. package/src/codex/inject.ts +99 -34
  91. package/src/codex/injected-marker.ts +30 -4
  92. package/src/codex/journal.ts +14 -0
  93. package/src/codex/legacy-config-keys.ts +68 -0
  94. package/src/codex/log-guard/inspect-schema.ts +137 -0
  95. package/src/codex/log-guard/inspect.ts +2 -134
  96. package/src/codex/loopback-target.ts +54 -0
  97. package/src/codex/main-account-cache.ts +63 -1
  98. package/src/codex/main-account-hard-lock.ts +52 -0
  99. package/src/codex/main-account.ts +3 -1
  100. package/src/codex/management-convergence.ts +3 -0
  101. package/src/codex/model-entitlements.ts +54 -4
  102. package/src/codex/project-config-warnings.ts +92 -2
  103. package/src/codex/prompt-layers/encoding.ts +80 -0
  104. package/src/codex/prompt-layers/paths.ts +54 -0
  105. package/src/codex/prompt-layers/revision.ts +55 -0
  106. package/src/codex/prompt-layers/toml-edit.ts +163 -0
  107. package/src/codex/prompt-layers/toml-read.ts +181 -0
  108. package/src/codex/prompt-layers.ts +14 -520
  109. package/src/codex/quota-auto-refresh-state.ts +16 -0
  110. package/src/codex/quota-auto-refresh.ts +219 -0
  111. package/src/codex/quota-types.ts +51 -0
  112. package/src/codex/quota.ts +252 -93
  113. package/src/codex/reserve-availability.ts +177 -0
  114. package/src/codex/routing.ts +28 -9
  115. package/src/codex/shim.ts +53 -11
  116. package/src/codex/subagent-model-fallback.ts +23 -3
  117. package/src/combos/failover.ts +125 -7
  118. package/src/combos/identifiers.ts +89 -0
  119. package/src/combos/index.ts +4 -0
  120. package/src/combos/resolve.ts +80 -9
  121. package/src/combos/types.ts +20 -93
  122. package/src/config/subagent-models.ts +24 -0
  123. package/src/config.ts +156 -13
  124. package/src/generated/compatibility-version.json +474 -178
  125. package/src/generated/model-metadata.ts +1 -1
  126. package/src/integrations/journal.ts +65 -4
  127. package/src/integrations/store.ts +5 -0
  128. package/src/lab/events/limits.ts +4 -0
  129. package/src/lib/destination-policy.ts +31 -2
  130. package/src/lib/errors.ts +39 -0
  131. package/src/lib/provider-outbound.ts +69 -3
  132. package/src/lib/proxy-env.ts +22 -0
  133. package/src/lib/redact-folding.ts +176 -0
  134. package/src/lib/redact.ts +2 -175
  135. package/src/lib/state-store-sweeper.ts +20 -6
  136. package/src/lib/token-estimate.ts +94 -27
  137. package/src/lib/windows-user-principal.ts +53 -5
  138. package/src/oauth/account-quota-rank.ts +40 -1
  139. package/src/oauth/anthropic-routing.ts +99 -3
  140. package/src/oauth/chatgpt-device.ts +187 -0
  141. package/src/oauth/chatgpt.ts +31 -4
  142. package/src/oauth/generic-account-failover.ts +36 -13
  143. package/src/oauth/index.ts +140 -29
  144. package/src/oauth/log.ts +3 -0
  145. package/src/oauth/login-cli.ts +5 -0
  146. package/src/oauth/meta-muse.ts +117 -15
  147. package/src/oauth/pool-settings-capability.ts +15 -4
  148. package/src/providers/api-keys.ts +8 -10
  149. package/src/providers/default-aliases.ts +39 -0
  150. package/src/providers/derive.ts +10 -2
  151. package/src/providers/fastwire.ts +36 -7
  152. package/src/providers/initial-model-selection-runtime.ts +90 -0
  153. package/src/providers/initial-model-selection.ts +120 -0
  154. package/src/providers/key-failover.ts +134 -54
  155. package/src/providers/key-store.ts +11 -1
  156. package/src/providers/label.ts +1 -1
  157. package/src/providers/model-discovery.ts +76 -0
  158. package/src/providers/model-rename-startup.ts +72 -8
  159. package/src/providers/muse-subscription-usage.ts +95 -0
  160. package/src/providers/openai-sidecar.ts +17 -5
  161. package/src/providers/openai-tiers-destination.ts +102 -0
  162. package/src/providers/openai-tiers.ts +2 -99
  163. package/src/providers/opencode-go-transport.ts +41 -0
  164. package/src/providers/quota-key-accounts.ts +141 -0
  165. package/src/providers/quota-types.ts +9 -0
  166. package/src/providers/quota.ts +625 -98
  167. package/src/providers/registry.ts +60 -17
  168. package/src/providers/xai-responses-opt-in.ts +31 -5
  169. package/src/quota/reset-activation.ts +81 -0
  170. package/src/quota/reset-detector.ts +305 -0
  171. package/src/quota/reset-notify-config.ts +162 -0
  172. package/src/quota/reset-observer.ts +125 -0
  173. package/src/quota/reset-poller.ts +160 -0
  174. package/src/quota/reset-seen-store.ts +385 -0
  175. package/src/quota/reset-sinks.ts +199 -0
  176. package/src/quota/window-mapping.ts +106 -0
  177. package/src/responses/apply-patch-envelope.ts +46 -0
  178. package/src/responses/code-mode-helper-compat.ts +39 -1
  179. package/src/responses/custom-tool-compat.ts +10 -4
  180. package/src/responses/hosted-tool-policy.ts +12 -4
  181. package/src/responses/parser-content.ts +133 -0
  182. package/src/responses/parser-text-format.ts +24 -0
  183. package/src/responses/parser-tools.ts +188 -0
  184. package/src/responses/parser.ts +3 -326
  185. package/src/responses/state.ts +124 -28
  186. package/src/router.ts +48 -13
  187. package/src/routing/analytics.ts +1 -0
  188. package/src/routing/capability.ts +17 -4
  189. package/src/server/auth-cors.ts +7 -1
  190. package/src/server/background-lifecycle.ts +23 -1
  191. package/src/server/chat-completions.ts +25 -3
  192. package/src/server/claude-messages.ts +62 -5
  193. package/src/server/effort-row.ts +1 -1
  194. package/src/server/fast-row.ts +295 -0
  195. package/src/server/gui-static.ts +30 -4
  196. package/src/server/index.ts +122 -28
  197. package/src/server/live.ts +18 -4
  198. package/src/server/management/agent-settings-routes.ts +2 -2
  199. package/src/server/management/combo-routes.ts +37 -9
  200. package/src/server/management/config-routes.ts +93 -2
  201. package/src/server/management/integration-routes.ts +108 -0
  202. package/src/server/management/model-routes.ts +13 -3
  203. package/src/server/management/model-rows.ts +20 -1
  204. package/src/server/management/native-integration-routes.ts +4 -1
  205. package/src/server/management/oauth-account-routes.ts +45 -10
  206. package/src/server/management/provider-routes.ts +34 -3
  207. package/src/server/management/quota-reset-routes.ts +57 -0
  208. package/src/server/management/route-registry.ts +7 -4
  209. package/src/server/management/shared.ts +19 -5
  210. package/src/server/management/system-routes.ts +3 -2
  211. package/src/server/management-api.ts +14 -2
  212. package/src/server/ports.ts +12 -2
  213. package/src/server/relay-eager.ts +38 -23
  214. package/src/server/relay.ts +4 -0
  215. package/src/server/request-log.ts +6 -0
  216. package/src/server/responses/codex-ws-correlation.ts +65 -0
  217. package/src/server/responses/codex-ws-exchange.ts +261 -0
  218. package/src/server/responses/codex-ws-metadata.ts +134 -0
  219. package/src/server/responses/codex-ws-pool.ts +162 -0
  220. package/src/server/responses/codex-ws-request.ts +87 -0
  221. package/src/server/responses/codex-ws-session.ts +93 -0
  222. package/src/server/responses/codex-ws-wire.ts +144 -0
  223. package/src/server/responses/collaboration.ts +41 -1
  224. package/src/server/responses/compact.ts +105 -12
  225. package/src/server/responses/core.ts +510 -57
  226. package/src/server/responses/empty-completion-guard.ts +4 -0
  227. package/src/server/responses/fetch-helpers.ts +10 -3
  228. package/src/server/responses/input-admission.ts +16 -9
  229. package/src/server/responses/responses-field-backfill.ts +1 -1
  230. package/src/server/responses/ws-upstream.ts +34 -318
  231. package/src/server/responses-custom-tool-repair.ts +20 -4
  232. package/src/server/responses-undeclared-tool-guard.ts +100 -8
  233. package/src/server/safe-response-headers.ts +23 -0
  234. package/src/server/search.ts +9 -0
  235. package/src/server/subagent-models-startup.ts +27 -0
  236. package/src/server/system-env-shell.ts +238 -0
  237. package/src/server/system-env.ts +7 -234
  238. package/src/server/ws-bridge.ts +3 -25
  239. package/src/server/xai-responses-startup.ts +21 -0
  240. package/src/service-manager-probe.ts +1 -1
  241. package/src/service.ts +55 -16
  242. package/src/types/config.ts +108 -12
  243. package/src/types/provider.ts +36 -7
  244. package/src/types/request.ts +8 -0
  245. package/src/types/tools.ts +26 -1
  246. package/src/types.ts +2 -0
  247. package/src/update/notify.ts +8 -2
  248. package/src/usage/cost.ts +38 -28
  249. package/src/usage/expected-prices.ts +34 -15
  250. package/src/usage/log.ts +2 -0
  251. package/src/usage/model-identity.ts +26 -0
  252. package/src/usage/summary.ts +15 -1
  253. package/src/vision/describe.ts +6 -0
  254. package/src/vision/image-rewrite.ts +108 -0
  255. package/src/vision/index.ts +19 -306
  256. package/src/vision/plan.ts +205 -0
  257. package/src/web-search/executor.ts +6 -0
  258. package/src/web-search/index.ts +8 -1
  259. package/gui/dist/assets/index-B2YjLA-i.css +0 -1
  260. package/gui/dist/assets/index-aPup8CKb.js +0 -112
@@ -10,130 +10,24 @@
10
10
  * - top_k is accepted and silently dropped (no Responses equivalent, CCR parity).
11
11
  */
12
12
  import type { OcxClaudeCodeConfig } from "../types";
13
- import { isAnthropicOutputSchema } from "../adapters/anthropic-output-schema";
14
- import { resolveAlias } from "./alias";
15
- import { stripOneMillionMarker } from "./context-windows";
16
- import { resolveDesktop3pAlias } from "./desktop-3p";
17
- import { isClaudeWebSearchToolName } from "./outbound";
18
13
  import { createHash } from "node:crypto";
19
14
 
20
- export class AnthropicRequestError extends Error {}
15
+ export { AnthropicRequestError } from "./inbound-records";
16
+ export { resolveInboundModel, effortForThinkingBudget, effortFromOutputConfig, extractOcxRouteDirective, extractOcxEffortDirective } from "./inbound-model-options";
17
+ import { AnthropicRequestError, isRec, type Rec } from "./inbound-records";
18
+ import { resolveInboundModel, effortForThinkingBudget, effortFromOutputConfig, formatFromOutputConfig } from "./inbound-model-options";
19
+ import { systemToInstructions, toolsToResponses, toolChoiceToResponses } from "./inbound-content-options";
21
20
 
22
- type Rec = Record<string, unknown>;
23
21
 
24
- function isRec(v: unknown): v is Rec {
25
- return !!v && typeof v === "object" && !Array.isArray(v);
26
- }
27
-
28
- function isClaudeClassifierModel(model: string): boolean {
29
- const stripped = model.replace(/-\d{8}$/, "");
30
- return /^claude-opus-[45]/.test(stripped);
31
- }
32
-
33
- /**
34
- * Explicitly configured classifier route for Claude Code Auto Mode safety checks (#1697).
35
- *
36
- * Only OPERATOR-DECLARED targets are used: `classifierModel`, then the ordered
37
- * `classifierFallbacks`. Both are qualified `provider/model` strings the operator chose, so
38
- * routing them crosses no boundary the operator did not ask for.
39
- *
40
- * Deliberately NOT here: inferring a provider from `claudeCode.model`. That value is the
41
- * injected/default config slot, not the provider the live session actually selected, so it goes
42
- * stale the moment the user changes the model picker -- and acting on it would silently move a
43
- * classifier turn onto a provider with its own privacy and billing consequences. Live session
44
- * affinity needs the request/session state this function does not have; it is tracked as
45
- * follow-up work rather than approximated from static config.
46
- */
47
- function configuredClassifierRoute(cc?: OcxClaudeCodeConfig): string | undefined {
48
- const explicit = typeof cc?.classifierModel === "string" ? cc.classifierModel.trim() : "";
49
- if (explicit.length > 0) return explicit;
50
- if (Array.isArray(cc?.classifierFallbacks)) {
51
- for (const candidate of cc.classifierFallbacks) {
52
- if (typeof candidate === "string" && candidate.trim().length > 0) return candidate.trim();
53
- }
54
- }
55
- return undefined;
56
- }
57
-
58
- /** Alias first, then modelMap: exact id, then date-suffix-stripped (`-\d{8}$`), then classifier affinity/config, else passthrough. */
59
- export function resolveInboundModel(model: string, cc?: OcxClaudeCodeConfig): string {
60
- // Defensive: Desktop/CLI strip the [1m] context-variant marker client-side, but a
61
- // leaking build must not break alias decode (devlog 138 — the 1M signal is the
62
- // anthropic-beta header, never the id). Case-insensitive: the CLI matches /\[1m\]/i.
63
- model = stripOneMillionMarker(model);
64
- const aliased = resolveAlias(model);
65
- if (aliased) return aliased;
66
- // Desktop 3P aliases: claude-opus-4-{code} → provider/model route key
67
- const desktop3p = resolveDesktop3pAlias(model);
68
- if (desktop3p) {
69
- // Native pseudo-provider returns bare slug; routed returns provider/model
70
- const sep = desktop3p.indexOf("/");
71
- if (sep > 0 && desktop3p.slice(0, sep) === "native") return desktop3p.slice(sep + 1);
72
- return desktop3p;
73
- }
74
- const map = cc?.modelMap ?? {};
75
- const exact = map[model];
76
- if (typeof exact === "string" && exact.length > 0) return exact;
77
- const stripped = model.replace(/-\d{8}$/, "");
78
- const dateless = map[stripped];
79
- if (typeof dateless === "string" && dateless.length > 0) return dateless;
80
-
81
- // Claude Code Auto Mode classifier routing (#1697). Bare classifier checks such as
82
- // `claude-opus-5` carry no provider, so without this they fall through to defaultProvider --
83
- // which may not speak Anthropic at all. Only an operator-declared target is used.
84
- if (isClaudeClassifierModel(model)) {
85
- const configured = configuredClassifierRoute(cc);
86
- if (configured) return configured;
87
- }
88
- return model;
89
- }
90
-
91
- /** budget_tokens ladder -> Responses reasoning effort (003: real API min is 1024; never forward raw). */
92
- export function effortForThinkingBudget(budget: number): string {
93
- if (budget <= 4096) return "low";
94
- if (budget <= 16384) return "medium";
95
- return "high";
96
- }
97
-
98
- /**
99
- * Adaptive-thinking wire (devlog 080): Claude Code /effort sends
100
- * `thinking:{type:"adaptive"}` + `output_config:{effort:"..."}` (verified by local
101
- * capture of claude 2.1.207 and CLIProxyAPI#1540). Forward the level verbatim when it
102
- * is a known Responses effort; unknown strings are dropped so downstream defaults win.
103
- */
104
- const OUTPUT_CONFIG_EFFORTS = new Set(["minimal", "low", "medium", "high", "xhigh", "max", "ultra"]);
105
- export function effortFromOutputConfig(outputConfig: unknown): string | undefined {
106
- if (!isRec(outputConfig)) return undefined;
107
- const effort = outputConfig.effort;
108
- return typeof effort === "string" && OUTPUT_CONFIG_EFFORTS.has(effort) ? effort : undefined;
109
- }
110
-
111
- function formatFromOutputConfig(outputConfig: unknown): Rec | undefined {
112
- if (!isRec(outputConfig) || !isRec(outputConfig.format)) return undefined;
113
- const format = outputConfig.format;
114
- if (
115
- format.type !== "json_schema"
116
- || !isRec(format.schema)
117
- || !isAnthropicOutputSchema(format.schema)
118
- ) return undefined;
119
- return { type: "json_schema", name: "response", schema: format.schema };
120
- }
121
-
122
- function systemToInstructions(system: unknown): string | undefined {
123
- if (typeof system === "string") return system.length > 0 ? system : undefined;
124
- if (Array.isArray(system)) {
125
- const parts: string[] = [];
126
- for (const block of system) {
127
- if (isRec(block) && block.type === "text" && typeof block.text === "string") parts.push(block.text);
128
- }
129
- return parts.length > 0 ? parts.join("\n\n") : undefined;
130
- }
131
- return undefined;
132
- }
133
22
 
134
23
  function imageBlockToInputImage(block: Rec): Rec | null {
135
24
  const source = block.source;
136
25
  if (!isRec(source)) return null;
26
+ if (source.type === "file") {
27
+ throw new AnthropicRequestError(
28
+ "File-backed images require native Anthropic passthrough; use base64 or URL images on translated routes.",
29
+ );
30
+ }
137
31
  if (source.type === "base64" && typeof source.data === "string") {
138
32
  const media = typeof source.media_type === "string" ? source.media_type : "image/png";
139
33
  return { type: "input_image", image_url: `data:${media};base64,${source.data}` };
@@ -194,47 +88,6 @@ export function effectiveBlockedSkillNames(cc?: Pick<OcxClaudeCodeConfig, "block
194
88
  .filter(name => name.length > 0))];
195
89
  }
196
90
 
197
- /**
198
- * ocx-route directive (devlog 072): injected agent-definition bodies carry
199
- * `<!-- ocx-route: <model> -->` because Claude Code 2.1.207 ignores custom
200
- * gateway ids in agent frontmatter (live-proven fallback to sonnet). The body
201
- * rides the subagent's system prompt, so the proxy re-routes here. Only the
202
- * FIRST directive wins; the scan is bounded to the system field.
203
- */
204
- const OCX_ROUTE_RE = /<!--\s*ocx-route:\s*([^\s]+)\s*-->/;
205
- const OCX_EFFORT_RE = /<!--\s*ocx-effort:\s*(low|medium|high|xhigh|max)\s*-->/;
206
-
207
- function systemText(body: unknown): string | null {
208
- if (!isRec(body)) return null;
209
- const system = body.system;
210
- if (typeof system === "string") return system || null;
211
- if (!Array.isArray(system)) return null;
212
- const text = system
213
- .filter((b): b is Rec => isRec(b) && b.type === "text" && typeof b.text === "string")
214
- .map(b => b.text as string)
215
- .join("\n");
216
- return text || null;
217
- }
218
-
219
- export function extractOcxRouteDirective(body: unknown): string | null {
220
- const text = systemText(body);
221
- if (!text) return null;
222
- const match = OCX_ROUTE_RE.exec(text);
223
- return match ? match[1]! : null;
224
- }
225
-
226
- /**
227
- * Claude Code 2.1.220 collapses custom-agent frontmatter `effort: max` and
228
- * `effort: xhigh` into the legacy `thinking.budget_tokens` shape. Preserve the
229
- * exact generated-agent setting through the same trusted system-body channel as
230
- * ocx-route so the inbound translator can restore `output_config.effort`.
231
- */
232
- export function extractOcxEffortDirective(body: unknown): NonNullable<OcxClaudeCodeConfig["subagentEffort"]> | null {
233
- const text = systemText(body);
234
- if (!text) return null;
235
- const match = OCX_EFFORT_RE.exec(text);
236
- return match ? match[1] as NonNullable<OcxClaudeCodeConfig["subagentEffort"]> : null;
237
- }
238
91
 
239
92
  /** Injected-skill payloads below this size are never stubbed (not worth it). */
240
93
  const SKILL_ELISION_MIN_CHARS = 10_000;
@@ -391,51 +244,6 @@ function assistantMessageToItems(content: unknown, input: Rec[]): void {
391
244
  flush();
392
245
  }
393
246
 
394
- function toolsToResponses(tools: unknown): Rec[] | undefined {
395
- if (!Array.isArray(tools) || tools.length === 0) return undefined;
396
- const out: Rec[] = [];
397
- for (const raw of tools) {
398
- if (!isRec(raw)) continue;
399
- const type = typeof raw.type === "string" ? raw.type : "";
400
- if (type.startsWith("web_search")) {
401
- out.push({ type: "web_search" }); // hosted sidecar path
402
- continue;
403
- }
404
- if (typeof raw.name === "string" && raw.name.length > 0 && isRec(raw.input_schema)) {
405
- out.push({
406
- type: "function",
407
- name: raw.name,
408
- ...(typeof raw.description === "string" ? { description: raw.description } : {}),
409
- parameters: raw.input_schema as Record<string, unknown>,
410
- });
411
- continue;
412
- }
413
- // Other server tools (bash_*, text_editor_*, ...) have no routed equivalent: drop.
414
- }
415
- return out.length > 0 ? out : undefined;
416
- }
417
-
418
- function toolChoiceToResponses(choice: unknown, body: Rec): void {
419
- if (!isRec(choice)) return;
420
- if (choice.disable_parallel_tool_use === true) body.parallel_tool_calls = false;
421
- switch (choice.type) {
422
- case "auto": body.tool_choice = "auto"; break;
423
- case "none": body.tool_choice = "none"; break;
424
- case "any": body.tool_choice = "required"; break;
425
- case "tool":
426
- if (typeof choice.name !== "string" || choice.name.length === 0) {
427
- throw new AnthropicRequestError("tool_choice.tool requires a name");
428
- }
429
- // Anthropic represents hosted WebSearch as a named tool choice, while
430
- // Responses requires the choice type to match the hosted declaration.
431
- // Preserve forced-tool intent rather than weakening it to `auto`.
432
- body.tool_choice = isClaudeWebSearchToolName(choice.name)
433
- ? { type: "web_search" }
434
- : { type: "function", name: choice.name };
435
- break;
436
- default: break;
437
- }
438
- }
439
247
 
440
248
  /** Recursive canonical JSON (keys sorted at every depth) — stable cache-cohort input. */
441
249
  function canonicalJson(value: unknown): string {
@@ -111,9 +111,32 @@ export function buildAnthropicModelInfos(
111
111
  aliasForRoute: (provider: string, modelId: string) => string = desktop3pAlias,
112
112
  nativeContextCap?: NativeContextLimitsInput,
113
113
  fastMode?: boolean,
114
+ // Presence is the feature gate: the caller passes undefined when `fastRows` is off, so a
115
+ // default install publishes nothing. The predicate answers ELIGIBILITY, not enablement.
116
+ fastRows?: (model: CatalogModel | { provider: string; id: string }) => boolean,
114
117
  ): AnthropicModelInfo[] {
115
118
  const out: AnthropicModelInfo[] = [];
116
119
  const seen = new Set<string>();
120
+ // Every id the loops below will really emit, computed BEFORE either runs. `seen` alone is
121
+ // not enough: it grows as they run, so whether a synthetic id collided with a real one
122
+ // would depend on iteration order. With both `foo` and a real `foo--fast` in the roster,
123
+ // the synthetic id for `foo` IS the real model's id, and whichever ran first would win it.
124
+ const realDiscoveryIds = new Set<string>([
125
+ ...nativeSlugs.map(slug => (
126
+ idStyle === "readable" ? claudeCodeNativeAlias(slug) : aliasForRoute("native", slug)
127
+ )),
128
+ ...routedModels.map(m => {
129
+ // The same asymmetry the routed loop applies: readable uses the LISTED id, so a
130
+ // fastMode-rewritten Cursor row is counted under the id it is really published as,
131
+ // while Desktop 3P hashes the RAW id.
132
+ const listed = fastMode === true && m.provider === "cursor" && idStyle === "readable"
133
+ ? cursorFastIdFor(m.id) ?? m.id
134
+ : m.id;
135
+ return idStyle === "readable"
136
+ ? claudeCodeAlias(m.provider, listed)
137
+ : aliasForRoute(m.provider, m.id);
138
+ }),
139
+ ]);
117
140
  // [1m] picker variant (devlog 260712 B1): Claude Code accounts exactly 1M for ids
118
141
  // carrying the marker (2.1.207 binary: /\[1m\]/i → 1e6, compaction preserved), so
119
142
  // ONLY models with an authoritative >=1M window get a second selectable row —
@@ -140,6 +163,21 @@ export function buildAnthropicModelInfos(
140
163
  : ONE_MILLION;
141
164
  out.push({ ...base, id, display_name: `${base.display_name} · 1M`, max_input_tokens: advertised });
142
165
  };
166
+ /**
167
+ * Publish a Fast sibling beside a row, following `push1mVariant` rather than the
168
+ * `fastMode` rewrite below: `fastMode` is a global switch with no per-request choice, so
169
+ * it REPLACES the listed id, while a selector has to leave the default pickable beside it.
170
+ *
171
+ * Because it only ADDS a row, it is safe for the Desktop 3P hashed style too — the
172
+ * exclusion `fastMode` needs exists because rewriting a hash strands a saved selection.
173
+ */
174
+ const pushFastVariant = (base: AnthropicModelInfo) => {
175
+ const id = `${base.id}--fast`;
176
+ // A real model always wins its own id, whatever the iteration order.
177
+ if (realDiscoveryIds.has(id) || seen.has(id)) return;
178
+ seen.add(id);
179
+ out.push({ ...base, id, display_name: `${base.display_name} · Fast` });
180
+ };
143
181
  for (const slug of nativeSlugs) {
144
182
  const id = idStyle === "readable" ? claudeCodeNativeAlias(slug) : aliasForRoute("native", slug);
145
183
  if (seen.has(id)) continue;
@@ -151,6 +189,9 @@ export function buildAnthropicModelInfos(
151
189
  const info = modelInfo(id, `${slug} (native)`, nativeEffectiveLadder(slug), true, nativeMaxInput ?? nativeWindow);
152
190
  out.push(info);
153
191
  push1mVariant(info, nativeWindow, nativeMaxInput);
192
+ // Natives too, not only routed rows: gpt-5.6-sol is the flagship Fast model, and
193
+ // omitting it would leave this surface without the model the feature exists for.
194
+ if (fastRows?.({ provider: "native", id: slug }) === true) pushFastVariant(info);
154
195
  }
155
196
  for (const m of routedModels) {
156
197
  // Global Fast has no toggle on this surface, so the fast identity is what gets listed —
@@ -180,6 +221,10 @@ export function buildAnthropicModelInfos(
180
221
  // Anthropic passthrough guard (audit 021 #3): never auto-widen canonical claude
181
222
  // routes — only a genuine >=1M window earns the variant row there.
182
223
  push1mVariant(info, m.contextWindow, routedMaxInput);
224
+ // The whole model is passed, not a (provider, id) pair: a combo row lives in its own
225
+ // namespace with no config.providers entry, so the caller classifies it from the
226
+ // aggregated supportsServiceTier the row already carries.
227
+ if (fastRows?.(m) === true) pushFastVariant(info);
183
228
  }
184
229
  return out;
185
230
  }
@@ -1,5 +1,7 @@
1
1
  import { writeSync } from "node:fs";
2
+ import { modelSelectionGuidance, modelSelectionNextSteps } from "./model-selection-guidance";
2
3
  import { warnIfCodexCatalogRefreshPending } from "./account-catalog-refresh";
4
+ import { isCodexResetCreditOperationId } from "../codex/reset-credit-recovery";
3
5
  import {
4
6
  CliUsageError,
5
7
  printData,
@@ -31,10 +33,15 @@ function writeStdoutFully(text: string): void {
31
33
  }
32
34
 
33
35
  const USAGE = `Usage:
34
- ocx account login <provider> [--id <account-id>] [--reauth] [--code -] [--no-wait] [--json]
36
+ ocx account login <provider> [--id <account-id>] [--reauth] [--device] [--code -] [--no-wait] [--json]
35
37
  ocx account code <provider> [--flow <flow-id>] [--json] (reads the code from stdin)
36
38
  ocx account cancel <provider> [--flow <flow-id>] [--json]
37
- ocx account reset-credits <account-id|main> [--consume --yes] [--json]
39
+ ocx account reset-credits <account-id|main> [--consume --yes [--operation-id <uuid>]] [--json]
40
+
41
+ --device runs the OpenAI device-code login instead of the browser callback: use
42
+ it when the proxy has no browser or nothing can reach localhost:1455, such as a
43
+ headless or remote hub. Enter the printed code at the printed URL from any other
44
+ machine.
38
45
 
39
46
  The redirect URL or authorization code is a short-lived credential. Pipe it in
40
47
  rather than passing it as an argument, where it lands in shell history and is
@@ -54,6 +61,9 @@ interface LoginStart {
54
61
  /** `-` means "read it from stdin", the documented way to pass a code silently. */
55
62
  const STDIN_SENTINEL = "-";
56
63
 
64
+ /** Providers whose ONLY login is already a device flow; --device is redundant, not wrong. */
65
+ const DEVICE_NATIVE_PROVIDERS = new Set(["kimi", "nous", "github-copilot"]);
66
+
57
67
  const ARGV_WARNING =
58
68
  "warning: the authorization code was passed as a command-line argument, so it is now in your shell history and was visible in the process list while this ran. Pipe it on stdin instead, or pass `-` to read from stdin.";
59
69
 
@@ -86,10 +96,17 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
86
96
  const wantsJson = takeFlag(args, "--json");
87
97
  const noWait = takeFlag(args, "--no-wait");
88
98
  const reauth = takeFlag(args, "--reauth");
99
+ const device = takeFlag(args, "--device");
89
100
  const id = takeOption(args, "--id");
90
101
  const suppliedCode = takeOptionWithSyntax(args, "--code");
91
102
  if (!provider) throw new CliUsageError("provider is required", USAGE);
92
103
  rejectArgs(args, USAGE);
104
+ // kimi, nous, and github-copilot are already device flows, so --device is a
105
+ // true statement about them and is accepted as a no-op rather than an error.
106
+ // Anything else has no device grant at all and must fail loudly.
107
+ if (device && !CODEX_NAMES.has(provider) && !DEVICE_NATIVE_PROVIDERS.has(provider)) {
108
+ throw new CliUsageError(`--device is not supported for provider '${provider}'`, USAGE);
109
+ }
93
110
  // Only resolve when --code was actually given: a plain `ocx account login`
94
111
  // opens the browser flow and polls, and must not block on stdin.
95
112
  const code = await resolveCode(suppliedCode, deps, false);
@@ -97,13 +114,18 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
97
114
  if (CODEX_NAMES.has(provider)) {
98
115
  const start = await runtimeRequest<LoginStart>("/api/codex-auth/login", {
99
116
  method: "POST",
100
- body: JSON.stringify({ ...(id ? { id } : {}), ...(reauth ? { reauth: true } : {}) }),
117
+ body: JSON.stringify({
118
+ ...(id ? { id } : {}),
119
+ ...(reauth ? { reauth: true } : {}),
120
+ ...(device ? { device: true } : {}),
121
+ }),
101
122
  }, deps);
102
123
  if (!wantsJson) {
103
124
  // One atomic pre-poll block, flushed synchronously so a piped parent
104
125
  // reads the URL before the polling window starts (#1007).
105
126
  const block = [
106
127
  start.url ? `Open this URL to sign in:\n${start.url}` : "",
128
+ start.deviceCode ? `Device code: ${start.deviceCode}` : "",
107
129
  start.instructions ?? "",
108
130
  start.flowId ? `Flow: ${start.flowId}` : "",
109
131
  ].filter(line => line !== "").join("\n");
@@ -116,18 +138,23 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
116
138
  }, deps);
117
139
  }
118
140
  if (noWait) {
119
- if (wantsJson) printData(start, true);
141
+ printData({ ...start, modelSelection: modelSelectionNextSteps("openai", true) }, wantsJson, modelSelectionGuidance("openai", true));
120
142
  return;
121
143
  }
122
144
  if (!start.flowId) throw new CliUsageError("login did not return a flow id");
123
- for (let attempt = 0; attempt < 150; attempt++) {
145
+ // A device login is deliberately slow: the user leaves this machine to
146
+ // enter the code elsewhere. Match the 15-minute grant instead of giving up
147
+ // at minute five while it is still valid, plus settlement margin for the
148
+ // token exchange and credential write after the final poll.
149
+ const maxAttempts = device ? 480 : 150;
150
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
124
151
  await Bun.sleep(2_000);
125
152
  const state = await runtimeRequest<Record<string, unknown>>(
126
153
  `/api/codex-auth/login-status?flowId=${encodeURIComponent(start.flowId)}${id ? `&accountId=${encodeURIComponent(id)}` : ""}${reauth ? "&reauth=1" : ""}`,
127
154
  {}, deps,
128
155
  );
129
156
  if (state.status === "done") {
130
- printData(state, wantsJson, [`Logged in${state.email ? ` as ${String(state.email)}` : ""}.`]);
157
+ printData({ ...state, modelSelection: modelSelectionNextSteps("openai") }, wantsJson, [`Logged in${state.email ? ` as ${String(state.email)}` : ""}.`, ...modelSelectionGuidance("openai")]);
131
158
  if (!wantsJson) warnIfCodexCatalogRefreshPending(state);
132
159
  return;
133
160
  }
@@ -158,7 +185,7 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
158
185
  }, deps);
159
186
  }
160
187
  if (noWait) {
161
- if (wantsJson) printData(start, true);
188
+ printData({ ...start, modelSelection: modelSelectionNextSteps(provider, true) }, wantsJson, modelSelectionGuidance(provider, true));
162
189
  return;
163
190
  }
164
191
  for (let attempt = 0; attempt < 100; attempt++) {
@@ -166,7 +193,7 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
166
193
  const state = await runtimeRequest<Record<string, unknown>>(`/api/oauth/status?provider=${encodeURIComponent(provider)}`, {}, deps);
167
194
  if (state.error) throw new CliUsageError(String(state.error));
168
195
  if (state.loggedIn === true) {
169
- printData(state, wantsJson, [`Logged in to ${provider}.`]);
196
+ printData({ ...state, modelSelection: modelSelectionNextSteps(provider) }, wantsJson, [`Logged in to ${provider}.`, ...modelSelectionGuidance(provider)]);
170
197
  return;
171
198
  }
172
199
  }
@@ -227,12 +254,25 @@ async function resetCredits(argv: string[], deps: RuntimeApiDeps): Promise<void>
227
254
  const wantsJson = takeFlag(args, "--json");
228
255
  const consume = takeFlag(args, "--consume");
229
256
  const yes = takeFlag(args, "--yes");
257
+ // Before rejectArgs: takeOption splices its two tokens out of `args`.
258
+ const operationId = takeOption(args, "--operation-id");
230
259
  if (!rawId) throw new CliUsageError("account id is required", USAGE);
231
260
  if (consume && !yes) throw new CliUsageError("consuming a reset credit requires --yes", USAGE);
261
+ if (operationId !== undefined && !consume) {
262
+ throw new CliUsageError("--operation-id requires --consume", USAGE);
263
+ }
264
+ if (operationId !== undefined && !isCodexResetCreditOperationId(operationId)) {
265
+ throw new CliUsageError("--operation-id must be a UUIDv4", USAGE);
266
+ }
232
267
  rejectArgs(args, USAGE);
233
268
  const accountId = rawId === "main" ? "__main__" : rawId;
234
269
  const result = consume
235
- ? await runtimeRequest("/api/codex-auth/reset-credits/consume", { method: "POST", body: JSON.stringify({ accountId }) }, deps)
270
+ ? await runtimeRequest("/api/codex-auth/reset-credits/consume", {
271
+ method: "POST",
272
+ // Spread, not `operationId: undefined`: the server distinguishes an absent
273
+ // key (legacy random id) from a caller who asked for a stable identity.
274
+ body: JSON.stringify({ accountId, ...(operationId === undefined ? {} : { operationId }) }),
275
+ }, deps)
236
276
  : await runtimeRequest(`/api/codex-auth/reset-credits?accountId=${encodeURIComponent(accountId)}`, {}, deps);
237
277
  printData(result, wantsJson);
238
278
  }
@@ -1,4 +1,5 @@
1
1
  import { loadConfig } from "../config";
2
+ import { hasPassiveAccountQuota } from "../providers/quota";
2
3
  import { closeSync, openSync, readSync } from "node:fs";
3
4
  import {
4
5
  MAX_ACCOUNT_PRIORITY,
@@ -330,7 +331,12 @@ export async function cmdRefresh(args: string[], deps: AccountDeps): Promise<num
330
331
  if (result.status === 0) return proxyUnreachable(result.transportError);
331
332
  if (result.status !== 200) return apiError(result.errorJson ?? {}, `failed to refresh ${name}`, result.status);
332
333
  if (wantsJson) console.log(JSON.stringify({ provider: name, report: result.report }, null, 2));
333
- else console.log(result.report ? providerQuotaLine(name, result.report) : `no quota report available for ${name}`);
334
+ else if (result.report) console.log(providerQuotaLine(name, result.report));
335
+ // A passive provider has no probe to run, so "no report available" reads as a
336
+ // failure of something that was never attempted. Say what is actually true.
337
+ else if (hasPassiveAccountQuota(name)) {
338
+ console.log(`${name} reports usage only during a streaming response; there is nothing to refresh. Run a request through this provider to update it, then see \`ocx account list ${name}\`.`);
339
+ } else console.log(`no quota report available for ${name}`);
334
340
  return 0;
335
341
  }
336
342
  const result = await fetchCodexRows(deps, baseUrl, true);
@@ -11,7 +11,7 @@
11
11
  * top-level `const USAGE`, evaluated at import time, so a cycle back into this table
12
12
  * would resolve to `undefined` under ESM rather than throwing -- silently emptying the
13
13
  * usage text that `rejectArgs` hands to `CliUsageError`, in the exact error-reporting
14
- * surface the CLI-operability issues are about. `tests/cli-capabilities.test.ts` asserts
14
+ * surface the CLI-operability issues are about. `tests/cli/cli-capabilities.test.ts` asserts
15
15
  * the absence of those imports and that every rendered usage string is non-empty, so the
16
16
  * failure mode is loud instead of degraded.
17
17
  *
@@ -19,7 +19,7 @@
19
19
  * `HEAD_CAPABILITIES`. They exit in the CLI head (`root.ts`) before dispatch and have no
20
20
  * runner key, so listing them as ordinary capabilities would break the registry parity
21
21
  * assertion that every canonical entry is a direct runner. `help` is excluded from
22
- * `CLI_COMMANDS` deliberately -- `tests/cli-registry.test.ts` documents it as a
22
+ * `CLI_COMMANDS` deliberately -- `tests/cli/cli-registry.test.ts` documents it as a
23
23
  * head-handled pseudo-case -- and that decision is preserved here rather than reversed.
24
24
  */
25
25
 
@@ -153,6 +153,17 @@ export const CAPABILITIES: readonly Capability[] = [
153
153
  json: "envelope",
154
154
  details: ["Reads local config; drives no management API route."],
155
155
  },
156
+ {
157
+ command: ["provider", "resets"],
158
+ summary: "Recently detected quota resets and whether reset notifications are enabled.",
159
+ routes: [{ method: "GET", path: "/api/quota-resets" }],
160
+ flags: [
161
+ { name: "--json", value: "boolean", summary: "Emit reset events as JSON." },
162
+ { name: "--limit", value: "number", summary: "Limit returned events; defaults to 20, capped at 100." },
163
+ ],
164
+ mutates: false,
165
+ json: "payload",
166
+ },
156
167
  {
157
168
  command: ["provider", "keychain"],
158
169
  summary: "Move a provider's API key into the OS keychain, restore it, or report where it lives.",
@@ -247,7 +258,7 @@ export const CAPABILITIES: readonly Capability[] = [
247
258
  "A bare invocation reads and never writes.",
248
259
  "The APPLIED value is echoed, not the requested one, so a server-side normalization stays visible.",
249
260
  "Values are not re-validated in the CLI: the server owns the strategy names and the 1-100 sticky bound.",
250
- "`anthropic` is the only OAuth pool with this setting; other OAuth providers are refused without a round-trip.",
261
+ "`anthropic` owns the full pool contract. Other OAuth providers reach the same endpoint with a generic subset (enabled/strategy/autoSwitchThreshold) whose settings persist but do not yet steer selection; `sticky` and `quotaWindow` are refused for them.",
251
262
  ],
252
263
  },
253
264
  {
@@ -274,7 +285,7 @@ export const CAPABILITIES: readonly Capability[] = [
274
285
  { name: "--conversation", value: "string", summary: "Restrict to one conversation id (`--conversationId` is accepted too)." },
275
286
  { name: "--status", value: "string", summary: "An exact code (429) or a class (5xx)." },
276
287
  { name: "--limit", value: "number", summary: "Row cap; defaults to 200." },
277
- { name: "--follow", value: "boolean", summary: "Stream new rows as JSONL; implies --jsonl." },
288
+ { name: "--follow", value: "boolean", summary: "Poll for new rows; add --jsonl to emit JSONL." },
278
289
  { name: "--json", value: "boolean", summary: "Emit the server payload as JSON." },
279
290
  { name: "--jsonl", value: "boolean", summary: "Emit one row per line." },
280
291
  ],