oc-codex-multi-auth 6.0.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 (248) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +162 -0
  3. package/assets/opencode-logo-ornate-dark.svg +18 -0
  4. package/assets/readme-hero.svg +31 -0
  5. package/config/README.md +96 -0
  6. package/config/minimal-opencode.json +13 -0
  7. package/config/opencode-legacy.json +871 -0
  8. package/config/opencode-modern.json +358 -0
  9. package/dist/index.d.ts +45 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +5008 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/lib/accounts/rate-limits.d.ts +22 -0
  14. package/dist/lib/accounts/rate-limits.d.ts.map +1 -0
  15. package/dist/lib/accounts/rate-limits.js +63 -0
  16. package/dist/lib/accounts/rate-limits.js.map +1 -0
  17. package/dist/lib/accounts.d.ts +134 -0
  18. package/dist/lib/accounts.d.ts.map +1 -0
  19. package/dist/lib/accounts.js +795 -0
  20. package/dist/lib/accounts.js.map +1 -0
  21. package/dist/lib/audit.d.ts +45 -0
  22. package/dist/lib/audit.d.ts.map +1 -0
  23. package/dist/lib/audit.js +154 -0
  24. package/dist/lib/audit.js.map +1 -0
  25. package/dist/lib/auth/auth.d.ts +51 -0
  26. package/dist/lib/auth/auth.d.ts.map +1 -0
  27. package/dist/lib/auth/auth.js +191 -0
  28. package/dist/lib/auth/auth.js.map +1 -0
  29. package/dist/lib/auth/browser.d.ts +17 -0
  30. package/dist/lib/auth/browser.d.ts.map +1 -0
  31. package/dist/lib/auth/browser.js +90 -0
  32. package/dist/lib/auth/browser.js.map +1 -0
  33. package/dist/lib/auth/device-code.d.ts +28 -0
  34. package/dist/lib/auth/device-code.d.ts.map +1 -0
  35. package/dist/lib/auth/device-code.js +215 -0
  36. package/dist/lib/auth/device-code.js.map +1 -0
  37. package/dist/lib/auth/login-runner.d.ts +55 -0
  38. package/dist/lib/auth/login-runner.d.ts.map +1 -0
  39. package/dist/lib/auth/login-runner.js +631 -0
  40. package/dist/lib/auth/login-runner.js.map +1 -0
  41. package/dist/lib/auth/server.d.ts +10 -0
  42. package/dist/lib/auth/server.d.ts.map +1 -0
  43. package/dist/lib/auth/server.js +98 -0
  44. package/dist/lib/auth/server.js.map +1 -0
  45. package/dist/lib/auth/token-utils.d.ts +60 -0
  46. package/dist/lib/auth/token-utils.d.ts.map +1 -0
  47. package/dist/lib/auth/token-utils.js +416 -0
  48. package/dist/lib/auth/token-utils.js.map +1 -0
  49. package/dist/lib/auth-rate-limit.d.ts +20 -0
  50. package/dist/lib/auth-rate-limit.d.ts.map +1 -0
  51. package/dist/lib/auth-rate-limit.js +91 -0
  52. package/dist/lib/auth-rate-limit.js.map +1 -0
  53. package/dist/lib/auto-update-checker.d.ts +10 -0
  54. package/dist/lib/auto-update-checker.d.ts.map +1 -0
  55. package/dist/lib/auto-update-checker.js +129 -0
  56. package/dist/lib/auto-update-checker.js.map +1 -0
  57. package/dist/lib/circuit-breaker.d.ts +34 -0
  58. package/dist/lib/circuit-breaker.d.ts.map +1 -0
  59. package/dist/lib/circuit-breaker.js +124 -0
  60. package/dist/lib/circuit-breaker.js.map +1 -0
  61. package/dist/lib/cli.d.ts +46 -0
  62. package/dist/lib/cli.d.ts.map +1 -0
  63. package/dist/lib/cli.js +192 -0
  64. package/dist/lib/cli.js.map +1 -0
  65. package/dist/lib/config.d.ts +42 -0
  66. package/dist/lib/config.d.ts.map +1 -0
  67. package/dist/lib/config.js +314 -0
  68. package/dist/lib/config.js.map +1 -0
  69. package/dist/lib/constants.d.ts +90 -0
  70. package/dist/lib/constants.d.ts.map +1 -0
  71. package/dist/lib/constants.js +90 -0
  72. package/dist/lib/constants.js.map +1 -0
  73. package/dist/lib/context-overflow.d.ts +27 -0
  74. package/dist/lib/context-overflow.d.ts.map +1 -0
  75. package/dist/lib/context-overflow.js +124 -0
  76. package/dist/lib/context-overflow.js.map +1 -0
  77. package/dist/lib/errors.d.ts +113 -0
  78. package/dist/lib/errors.d.ts.map +1 -0
  79. package/dist/lib/errors.js +103 -0
  80. package/dist/lib/errors.js.map +1 -0
  81. package/dist/lib/health.d.ts +33 -0
  82. package/dist/lib/health.d.ts.map +1 -0
  83. package/dist/lib/health.js +70 -0
  84. package/dist/lib/health.js.map +1 -0
  85. package/dist/lib/index.d.ts +20 -0
  86. package/dist/lib/index.d.ts.map +1 -0
  87. package/dist/lib/index.js +20 -0
  88. package/dist/lib/index.js.map +1 -0
  89. package/dist/lib/logger.d.ts +40 -0
  90. package/dist/lib/logger.d.ts.map +1 -0
  91. package/dist/lib/logger.js +337 -0
  92. package/dist/lib/logger.js.map +1 -0
  93. package/dist/lib/oauth-success.html +712 -0
  94. package/dist/lib/parallel-probe.d.ts +28 -0
  95. package/dist/lib/parallel-probe.d.ts.map +1 -0
  96. package/dist/lib/parallel-probe.js +97 -0
  97. package/dist/lib/parallel-probe.js.map +1 -0
  98. package/dist/lib/proactive-refresh.d.ts +67 -0
  99. package/dist/lib/proactive-refresh.d.ts.map +1 -0
  100. package/dist/lib/proactive-refresh.js +161 -0
  101. package/dist/lib/proactive-refresh.js.map +1 -0
  102. package/dist/lib/prompts/codex-opencode-bridge.d.ts +20 -0
  103. package/dist/lib/prompts/codex-opencode-bridge.d.ts.map +1 -0
  104. package/dist/lib/prompts/codex-opencode-bridge.js +126 -0
  105. package/dist/lib/prompts/codex-opencode-bridge.js.map +1 -0
  106. package/dist/lib/prompts/codex.d.ts +41 -0
  107. package/dist/lib/prompts/codex.d.ts.map +1 -0
  108. package/dist/lib/prompts/codex.js +400 -0
  109. package/dist/lib/prompts/codex.js.map +1 -0
  110. package/dist/lib/prompts/opencode-codex.d.ts +25 -0
  111. package/dist/lib/prompts/opencode-codex.d.ts.map +1 -0
  112. package/dist/lib/prompts/opencode-codex.js +223 -0
  113. package/dist/lib/prompts/opencode-codex.js.map +1 -0
  114. package/dist/lib/recovery/constants.d.ts +12 -0
  115. package/dist/lib/recovery/constants.d.ts.map +1 -0
  116. package/dist/lib/recovery/constants.js +25 -0
  117. package/dist/lib/recovery/constants.js.map +1 -0
  118. package/dist/lib/recovery/index.d.ts +12 -0
  119. package/dist/lib/recovery/index.d.ts.map +1 -0
  120. package/dist/lib/recovery/index.js +12 -0
  121. package/dist/lib/recovery/index.js.map +1 -0
  122. package/dist/lib/recovery/storage.d.ts +24 -0
  123. package/dist/lib/recovery/storage.d.ts.map +1 -0
  124. package/dist/lib/recovery/storage.js +362 -0
  125. package/dist/lib/recovery/storage.js.map +1 -0
  126. package/dist/lib/recovery/types.d.ts +116 -0
  127. package/dist/lib/recovery/types.d.ts.map +1 -0
  128. package/dist/lib/recovery/types.js +7 -0
  129. package/dist/lib/recovery/types.js.map +1 -0
  130. package/dist/lib/recovery.d.ts +31 -0
  131. package/dist/lib/recovery.d.ts.map +1 -0
  132. package/dist/lib/recovery.js +313 -0
  133. package/dist/lib/recovery.js.map +1 -0
  134. package/dist/lib/refresh-queue.d.ts +131 -0
  135. package/dist/lib/refresh-queue.d.ts.map +1 -0
  136. package/dist/lib/refresh-queue.js +297 -0
  137. package/dist/lib/refresh-queue.js.map +1 -0
  138. package/dist/lib/request/fetch-helpers.d.ts +142 -0
  139. package/dist/lib/request/fetch-helpers.d.ts.map +1 -0
  140. package/dist/lib/request/fetch-helpers.js +749 -0
  141. package/dist/lib/request/fetch-helpers.js.map +1 -0
  142. package/dist/lib/request/helpers/input-utils.d.ts +7 -0
  143. package/dist/lib/request/helpers/input-utils.d.ts.map +1 -0
  144. package/dist/lib/request/helpers/input-utils.js +214 -0
  145. package/dist/lib/request/helpers/input-utils.js.map +1 -0
  146. package/dist/lib/request/helpers/model-map.d.ts +22 -0
  147. package/dist/lib/request/helpers/model-map.d.ts.map +1 -0
  148. package/dist/lib/request/helpers/model-map.js +183 -0
  149. package/dist/lib/request/helpers/model-map.js.map +1 -0
  150. package/dist/lib/request/helpers/tool-utils.d.ts +29 -0
  151. package/dist/lib/request/helpers/tool-utils.d.ts.map +1 -0
  152. package/dist/lib/request/helpers/tool-utils.js +117 -0
  153. package/dist/lib/request/helpers/tool-utils.js.map +1 -0
  154. package/dist/lib/request/rate-limit-backoff.d.ts +17 -0
  155. package/dist/lib/request/rate-limit-backoff.d.ts.map +1 -0
  156. package/dist/lib/request/rate-limit-backoff.js +83 -0
  157. package/dist/lib/request/rate-limit-backoff.js.map +1 -0
  158. package/dist/lib/request/request-transformer.d.ts +107 -0
  159. package/dist/lib/request/request-transformer.d.ts.map +1 -0
  160. package/dist/lib/request/request-transformer.js +930 -0
  161. package/dist/lib/request/request-transformer.js.map +1 -0
  162. package/dist/lib/request/response-handler.d.ts +23 -0
  163. package/dist/lib/request/response-handler.d.ts.map +1 -0
  164. package/dist/lib/request/response-handler.js +245 -0
  165. package/dist/lib/request/response-handler.js.map +1 -0
  166. package/dist/lib/request/retry-budget.d.ts +19 -0
  167. package/dist/lib/request/retry-budget.d.ts.map +1 -0
  168. package/dist/lib/request/retry-budget.js +99 -0
  169. package/dist/lib/request/retry-budget.js.map +1 -0
  170. package/dist/lib/rotation.d.ts +130 -0
  171. package/dist/lib/rotation.d.ts.map +1 -0
  172. package/dist/lib/rotation.js +301 -0
  173. package/dist/lib/rotation.js.map +1 -0
  174. package/dist/lib/runtime-contracts.d.ts +16 -0
  175. package/dist/lib/runtime-contracts.d.ts.map +1 -0
  176. package/dist/lib/runtime-contracts.js +24 -0
  177. package/dist/lib/runtime-contracts.js.map +1 -0
  178. package/dist/lib/schemas.d.ts +457 -0
  179. package/dist/lib/schemas.d.ts.map +1 -0
  180. package/dist/lib/schemas.js +283 -0
  181. package/dist/lib/schemas.js.map +1 -0
  182. package/dist/lib/shutdown.d.ts +7 -0
  183. package/dist/lib/shutdown.d.ts.map +1 -0
  184. package/dist/lib/shutdown.js +43 -0
  185. package/dist/lib/shutdown.js.map +1 -0
  186. package/dist/lib/storage/migrations.d.ts +65 -0
  187. package/dist/lib/storage/migrations.d.ts.map +1 -0
  188. package/dist/lib/storage/migrations.js +44 -0
  189. package/dist/lib/storage/migrations.js.map +1 -0
  190. package/dist/lib/storage/paths.d.ts +16 -0
  191. package/dist/lib/storage/paths.d.ts.map +1 -0
  192. package/dist/lib/storage/paths.js +92 -0
  193. package/dist/lib/storage/paths.js.map +1 -0
  194. package/dist/lib/storage.d.ts +157 -0
  195. package/dist/lib/storage.d.ts.map +1 -0
  196. package/dist/lib/storage.js +1169 -0
  197. package/dist/lib/storage.js.map +1 -0
  198. package/dist/lib/table-formatter.d.ts +32 -0
  199. package/dist/lib/table-formatter.d.ts.map +1 -0
  200. package/dist/lib/table-formatter.js +44 -0
  201. package/dist/lib/table-formatter.js.map +1 -0
  202. package/dist/lib/types.d.ts +131 -0
  203. package/dist/lib/types.d.ts.map +1 -0
  204. package/dist/lib/types.js +2 -0
  205. package/dist/lib/types.js.map +1 -0
  206. package/dist/lib/ui/ansi.d.ts +22 -0
  207. package/dist/lib/ui/ansi.d.ts.map +1 -0
  208. package/dist/lib/ui/ansi.js +38 -0
  209. package/dist/lib/ui/ansi.js.map +1 -0
  210. package/dist/lib/ui/auth-menu.d.ts +39 -0
  211. package/dist/lib/ui/auth-menu.d.ts.map +1 -0
  212. package/dist/lib/ui/auth-menu.js +186 -0
  213. package/dist/lib/ui/auth-menu.js.map +1 -0
  214. package/dist/lib/ui/beginner.d.ts +62 -0
  215. package/dist/lib/ui/beginner.d.ts.map +1 -0
  216. package/dist/lib/ui/beginner.js +259 -0
  217. package/dist/lib/ui/beginner.js.map +1 -0
  218. package/dist/lib/ui/confirm.d.ts +2 -0
  219. package/dist/lib/ui/confirm.d.ts.map +1 -0
  220. package/dist/lib/ui/confirm.js +21 -0
  221. package/dist/lib/ui/confirm.js.map +1 -0
  222. package/dist/lib/ui/format.d.ts +9 -0
  223. package/dist/lib/ui/format.d.ts.map +1 -0
  224. package/dist/lib/ui/format.js +49 -0
  225. package/dist/lib/ui/format.js.map +1 -0
  226. package/dist/lib/ui/runtime.d.ts +11 -0
  227. package/dist/lib/ui/runtime.d.ts.map +1 -0
  228. package/dist/lib/ui/runtime.js +31 -0
  229. package/dist/lib/ui/runtime.js.map +1 -0
  230. package/dist/lib/ui/select.d.ts +20 -0
  231. package/dist/lib/ui/select.d.ts.map +1 -0
  232. package/dist/lib/ui/select.js +343 -0
  233. package/dist/lib/ui/select.js.map +1 -0
  234. package/dist/lib/ui/theme.d.ts +34 -0
  235. package/dist/lib/ui/theme.d.ts.map +1 -0
  236. package/dist/lib/ui/theme.js +84 -0
  237. package/dist/lib/ui/theme.js.map +1 -0
  238. package/dist/lib/utils.d.ts +29 -0
  239. package/dist/lib/utils.d.ts.map +1 -0
  240. package/dist/lib/utils.js +54 -0
  241. package/dist/lib/utils.js.map +1 -0
  242. package/package.json +113 -0
  243. package/scripts/audit-dev-allowlist.js +114 -0
  244. package/scripts/copy-oauth-success.js +37 -0
  245. package/scripts/install-oc-codex-multi-auth-core.js +381 -0
  246. package/scripts/install-oc-codex-multi-auth.js +15 -0
  247. package/scripts/test-all-models.sh +260 -0
  248. package/scripts/validate-model-map.sh +97 -0
@@ -0,0 +1,930 @@
1
+ import { logDebug, logWarn } from "../logger.js";
2
+ import { TOOL_REMAP_MESSAGE } from "../prompts/codex.js";
3
+ import { renderCodexOpenCodeBridge } from "../prompts/codex-opencode-bridge.js";
4
+ import { getOpenCodeCodexPrompt } from "../prompts/opencode-codex.js";
5
+ import { getNormalizedModel } from "./helpers/model-map.js";
6
+ import { filterOpenCodeSystemPromptsWithCachedPrompt, normalizeOrphanedToolOutputs, injectMissingToolOutputs, } from "./helpers/input-utils.js";
7
+ import { cleanupToolDefinitions } from "./helpers/tool-utils.js";
8
+ const PLAN_MODE_ONLY_TOOLS = new Set(["request_user_input"]);
9
+ export { isOpenCodeSystemPrompt, filterOpenCodeSystemPromptsWithCachedPrompt, } from "./helpers/input-utils.js";
10
+ /**
11
+ * Normalize model name to Codex-supported variants
12
+ *
13
+ * Uses explicit model map for known models, with fallback pattern matching
14
+ * for unknown/custom model names.
15
+ *
16
+ * @param model - Original model name (e.g., "gpt-5-codex-low", "openai/gpt-5-codex")
17
+ * @returns Normalized model name (e.g., "gpt-5-codex", "gpt-5.1-codex-max")
18
+ */
19
+ export function normalizeModel(model) {
20
+ if (!model)
21
+ return "gpt-5.4";
22
+ // Strip provider prefix if present (e.g., "openai/gpt-5-codex" → "gpt-5-codex")
23
+ const modelId = model.includes("/") ? model.split("/").pop() ?? model : model;
24
+ // Try explicit model map first (handles all known model variants)
25
+ const mappedModel = getNormalizedModel(modelId);
26
+ if (mappedModel) {
27
+ return mappedModel;
28
+ }
29
+ // Fallback: Pattern-based matching for unknown/custom model names
30
+ // This preserves backwards compatibility with old verbose names
31
+ // like "GPT 5 Codex Low (ChatGPT Subscription)"
32
+ const normalized = modelId.toLowerCase();
33
+ // Priority order for pattern matching (most specific first):
34
+ // 1. GPT-5.3 Codex Spark (legacy alias -> canonical gpt-5-codex)
35
+ if (normalized.includes("gpt-5.3-codex-spark") ||
36
+ normalized.includes("gpt 5.3 codex spark")) {
37
+ return "gpt-5-codex";
38
+ }
39
+ // 2. GPT-5.3 Codex (legacy alias -> canonical gpt-5-codex)
40
+ if (normalized.includes("gpt-5.3-codex") ||
41
+ normalized.includes("gpt 5.3 codex")) {
42
+ return "gpt-5-codex";
43
+ }
44
+ // 3. GPT-5.2 Codex (legacy alias -> canonical gpt-5-codex)
45
+ if (normalized.includes("gpt-5.2-codex") ||
46
+ normalized.includes("gpt 5.2 codex")) {
47
+ return "gpt-5-codex";
48
+ }
49
+ // 4. GPT-5.4 Pro (first-class model)
50
+ if (/\bgpt(?:-| )5\.4(?:-| )pro(?:\b|[- ])/.test(normalized)) {
51
+ return "gpt-5.4-pro";
52
+ }
53
+ // 5. GPT-5.4 Mini (first-class model)
54
+ if (/\bgpt(?:-| )5\.4(?:-| )mini(?:\b|[- ])/.test(normalized)) {
55
+ return "gpt-5.4-mini";
56
+ }
57
+ // 6. GPT-5.4 Nano (first-class model)
58
+ if (/\bgpt(?:-| )5\.4(?:-| )nano(?:\b|[- ])/.test(normalized)) {
59
+ return "gpt-5.4-nano";
60
+ }
61
+ // 7. GPT-5.4 (general purpose)
62
+ if (/\bgpt(?:-| )5\.4(?:\b|[- ])/.test(normalized)) {
63
+ return "gpt-5.4";
64
+ }
65
+ // 8. GPT-5.2 (general purpose)
66
+ if (normalized.includes("gpt-5.2") || normalized.includes("gpt 5.2")) {
67
+ return "gpt-5.2";
68
+ }
69
+ // 9. GPT-5.1 Codex Max
70
+ if (normalized.includes("gpt-5.1-codex-max") ||
71
+ normalized.includes("gpt 5.1 codex max")) {
72
+ return "gpt-5.1-codex-max";
73
+ }
74
+ // 10. GPT-5.1 Codex Mini
75
+ if (normalized.includes("gpt-5.1-codex-mini") ||
76
+ normalized.includes("gpt 5.1 codex mini")) {
77
+ return "gpt-5.1-codex-mini";
78
+ }
79
+ // 11. Legacy Codex Mini
80
+ if (normalized.includes("codex-mini-latest") ||
81
+ normalized.includes("gpt-5-codex-mini") ||
82
+ normalized.includes("gpt 5 codex mini")) {
83
+ return "gpt-5.1-codex-mini";
84
+ }
85
+ // 12. GPT-5 Codex canonical + GPT-5.1 Codex legacy alias
86
+ if (normalized.includes("gpt-5-codex") ||
87
+ normalized.includes("gpt 5 codex")) {
88
+ return "gpt-5-codex";
89
+ }
90
+ // 13. GPT-5.1 Codex (legacy alias)
91
+ if (normalized.includes("gpt-5.1-codex") ||
92
+ normalized.includes("gpt 5.1 codex")) {
93
+ return "gpt-5-codex";
94
+ }
95
+ // 14. GPT-5.1 (general-purpose)
96
+ if (normalized.includes("gpt-5.1") || normalized.includes("gpt 5.1")) {
97
+ return "gpt-5.1";
98
+ }
99
+ // 15. GPT-5 Codex family (any other variant with "codex")
100
+ if (normalized.includes("codex")) {
101
+ return "gpt-5-codex";
102
+ }
103
+ // 16. GPT-5 family (any variant) - default to 5.4 latest general model
104
+ if (normalized.includes("gpt-5") || normalized.includes("gpt 5")) {
105
+ return "gpt-5.4";
106
+ }
107
+ // Default fallback
108
+ return "gpt-5.4";
109
+ }
110
+ /**
111
+ * Extract configuration for a specific model
112
+ * Merges global options with model-specific options (model-specific takes precedence)
113
+ * @param modelName - Model name (e.g., "gpt-5-codex")
114
+ * @param userConfig - Full user configuration object
115
+ * @returns Merged configuration for this model
116
+ */
117
+ export function getModelConfig(modelName, userConfig = { global: {}, models: {} }) {
118
+ const globalOptions = userConfig.global ?? {};
119
+ const modelMap = userConfig.models ?? {};
120
+ const stripProviderPrefix = (name) => name.includes("/") ? (name.split("/").pop() ?? name) : name;
121
+ const getVariantFromModelName = (name) => {
122
+ const stripped = stripProviderPrefix(name).toLowerCase();
123
+ const match = stripped.match(/-(none|minimal|low|medium|high|xhigh)$/);
124
+ if (!match)
125
+ return undefined;
126
+ const variant = match[1];
127
+ if (variant === "none" ||
128
+ variant === "minimal" ||
129
+ variant === "low" ||
130
+ variant === "medium" ||
131
+ variant === "high" ||
132
+ variant === "xhigh") {
133
+ return variant;
134
+ }
135
+ return undefined;
136
+ };
137
+ const removeVariantSuffix = (name) => stripProviderPrefix(name).replace(/-(none|minimal|low|medium|high|xhigh)$/i, "");
138
+ const findModelEntry = (candidates) => {
139
+ for (const key of candidates) {
140
+ const entry = modelMap[key];
141
+ if (entry)
142
+ return { key, entry };
143
+ }
144
+ return undefined;
145
+ };
146
+ const strippedModelName = stripProviderPrefix(modelName);
147
+ const normalizedModelName = normalizeModel(strippedModelName);
148
+ const normalizedBaseModelName = normalizeModel(removeVariantSuffix(strippedModelName));
149
+ const baseModelName = removeVariantSuffix(strippedModelName);
150
+ const requestedVariant = getVariantFromModelName(strippedModelName);
151
+ // 1) Honor exact per-model keys first (including variant-specific keys)
152
+ const directMatch = findModelEntry([modelName, strippedModelName]);
153
+ if (directMatch?.entry?.options) {
154
+ return { ...globalOptions, ...directMatch.entry.options };
155
+ }
156
+ // 2) Resolve to base model config (supports provider-prefixed names + aliases)
157
+ const baseMatch = findModelEntry([
158
+ baseModelName,
159
+ normalizedBaseModelName,
160
+ normalizedModelName,
161
+ ]);
162
+ const baseOptions = baseMatch?.entry?.options ?? {};
163
+ // 3) If model requested a variant, merge variant options from base model config
164
+ const variantConfig = requestedVariant && baseMatch?.entry?.variants
165
+ ? baseMatch.entry.variants[requestedVariant]
166
+ : undefined;
167
+ let variantOptions = {};
168
+ if (variantConfig) {
169
+ const { disabled: _disabled, ...rest } = variantConfig;
170
+ void _disabled;
171
+ variantOptions = rest;
172
+ }
173
+ // Model-specific options override global options
174
+ return { ...globalOptions, ...baseOptions, ...variantOptions };
175
+ }
176
+ /**
177
+ * Apply fast-session defaults to reduce latency/cost for interactive sessions.
178
+ * Explicit user/model overrides still take precedence.
179
+ */
180
+ export function applyFastSessionDefaults(userConfig = { global: {}, models: {} }) {
181
+ const global = userConfig.global ?? {};
182
+ return {
183
+ ...userConfig,
184
+ global: {
185
+ ...global,
186
+ reasoningEffort: global.reasoningEffort ?? "low",
187
+ textVerbosity: global.textVerbosity ?? "low",
188
+ },
189
+ };
190
+ }
191
+ /**
192
+ * Resolves reasoning settings by layering transformed config with body/provider overrides.
193
+ */
194
+ function resolveReasoningConfig(modelName, modelConfig, body) {
195
+ const providerOpenAI = body.providerOptions?.openai;
196
+ const existingEffort = body.reasoning?.effort ?? providerOpenAI?.reasoningEffort;
197
+ const existingSummary = body.reasoning?.summary ?? providerOpenAI?.reasoningSummary;
198
+ const mergedConfig = {
199
+ ...modelConfig,
200
+ ...(existingEffort ? { reasoningEffort: existingEffort } : {}),
201
+ ...(existingSummary ? { reasoningSummary: existingSummary } : {}),
202
+ };
203
+ return getReasoningConfig(modelName, mergedConfig);
204
+ }
205
+ /**
206
+ * Picks effective text verbosity with body/provider values taking precedence.
207
+ */
208
+ function resolveTextVerbosity(modelConfig, body) {
209
+ const providerOpenAI = body.providerOptions?.openai;
210
+ return (body.text?.verbosity ??
211
+ providerOpenAI?.textVerbosity ??
212
+ modelConfig.textVerbosity ??
213
+ "medium");
214
+ }
215
+ /**
216
+ * Resolves include fields and always preserves encrypted reasoning continuity payloads.
217
+ */
218
+ function resolveInclude(modelConfig, body) {
219
+ const providerOpenAI = body.providerOptions?.openai;
220
+ const base = body.include ??
221
+ providerOpenAI?.include ??
222
+ modelConfig.include ??
223
+ ["reasoning.encrypted_content"];
224
+ const include = Array.from(new Set(base.filter(Boolean)));
225
+ if (!include.includes("reasoning.encrypted_content")) {
226
+ include.push("reasoning.encrypted_content");
227
+ }
228
+ return include;
229
+ }
230
+ /**
231
+ * Parses a collaboration mode token from env/config text.
232
+ */
233
+ function parseCollaborationMode(value) {
234
+ if (!value)
235
+ return undefined;
236
+ const normalized = value.trim().toLowerCase();
237
+ if (normalized === "plan")
238
+ return "plan";
239
+ if (normalized === "default")
240
+ return "default";
241
+ return undefined;
242
+ }
243
+ /**
244
+ * Extracts plain text from mixed message-content payloads.
245
+ */
246
+ function extractMessageText(content) {
247
+ if (typeof content === "string")
248
+ return content;
249
+ if (!Array.isArray(content))
250
+ return "";
251
+ return content
252
+ .map((item) => {
253
+ if (typeof item === "string")
254
+ return item;
255
+ if (!item || typeof item !== "object")
256
+ return "";
257
+ const typedItem = item;
258
+ return typeof typedItem.text === "string" ? typedItem.text : "";
259
+ })
260
+ .filter(Boolean)
261
+ .join("\n");
262
+ }
263
+ /**
264
+ * Detects active collaboration mode using explicit env overrides first, then prompt hints.
265
+ */
266
+ function detectCollaborationMode(body) {
267
+ const envMode = parseCollaborationMode(process.env.CODEX_COLLABORATION_MODE) ??
268
+ parseCollaborationMode(process.env.OPENCODE_COLLABORATION_MODE);
269
+ if (envMode)
270
+ return envMode;
271
+ if (!Array.isArray(body.input))
272
+ return "unknown";
273
+ let sawPlan = false;
274
+ let sawDefault = false;
275
+ for (const item of body.input) {
276
+ if (!item || typeof item !== "object")
277
+ continue;
278
+ const role = typeof item.role === "string" ? item.role.toLowerCase() : "";
279
+ if (role !== "developer" && role !== "system")
280
+ continue;
281
+ const text = extractMessageText(item.content);
282
+ if (!text)
283
+ continue;
284
+ if (/collaboration mode:\s*plan/i.test(text) || /in Plan mode/i.test(text)) {
285
+ sawPlan = true;
286
+ }
287
+ if (/collaboration mode:\s*default/i.test(text) || /in Default mode/i.test(text)) {
288
+ sawDefault = true;
289
+ }
290
+ }
291
+ if (sawPlan && !sawDefault)
292
+ return "plan";
293
+ if (sawDefault)
294
+ return "default";
295
+ return "unknown";
296
+ }
297
+ /**
298
+ * Removes tools that are only valid in plan mode when the request is not in plan mode.
299
+ */
300
+ function sanitizePlanOnlyTools(tools, mode) {
301
+ if (!Array.isArray(tools) || mode === "plan")
302
+ return tools;
303
+ let removed = 0;
304
+ const filtered = tools.filter((entry) => {
305
+ if (!entry || typeof entry !== "object")
306
+ return true;
307
+ const functionDef = entry.function;
308
+ if (!functionDef || typeof functionDef !== "object")
309
+ return true;
310
+ const name = functionDef.name;
311
+ if (typeof name !== "string")
312
+ return true;
313
+ if (!PLAN_MODE_ONLY_TOOLS.has(name))
314
+ return true;
315
+ removed++;
316
+ return false;
317
+ });
318
+ if (removed > 0) {
319
+ logWarn(`Removed ${removed} plan-mode-only tool definition(s) because collaboration mode is ${mode}`);
320
+ }
321
+ return filtered;
322
+ }
323
+ /**
324
+ * Collects runtime tool names from either direct tool entries or function-wrapped definitions.
325
+ */
326
+ function extractRuntimeToolNames(tools) {
327
+ if (!Array.isArray(tools))
328
+ return [];
329
+ const names = [];
330
+ for (const tool of tools) {
331
+ if (!tool || typeof tool !== "object")
332
+ continue;
333
+ // Pure in-memory name extraction only: no filesystem I/O, token access,
334
+ // or logging side effects are introduced by recognizing built-in tool types.
335
+ const toolType = tool.type;
336
+ if (typeof toolType === "string" && toolType.trim() && toolType !== "function") {
337
+ names.push(toolType);
338
+ continue;
339
+ }
340
+ const directName = tool.name;
341
+ if (typeof directName === "string" && directName.trim()) {
342
+ names.push(directName);
343
+ continue;
344
+ }
345
+ const functionDef = tool.function;
346
+ if (!functionDef || typeof functionDef !== "object")
347
+ continue;
348
+ const functionName = functionDef.name;
349
+ if (typeof functionName === "string" && functionName.trim()) {
350
+ names.push(functionName);
351
+ }
352
+ }
353
+ return names;
354
+ }
355
+ /**
356
+ * Configure reasoning parameters based on model variant and user config
357
+ *
358
+ * NOTE: This plugin follows Codex CLI defaults instead of opencode defaults because:
359
+ * - We're accessing the ChatGPT backend API (not OpenAI Platform API)
360
+ * - opencode explicitly excludes gpt-5-codex from automatic reasoning configuration
361
+ * - Codex CLI has been thoroughly tested against this backend
362
+ *
363
+ * @param originalModel - Original model name before normalization
364
+ * @param userConfig - User configuration object
365
+ * @returns Reasoning configuration
366
+ */
367
+ export function getReasoningConfig(modelName, userConfig = {}) {
368
+ const normalizedName = modelName?.toLowerCase() ?? "";
369
+ const canonicalModelName = normalizeModel(modelName);
370
+ // Canonical GPT-5 Codex (stable) defaults to high and does not support "none".
371
+ const isGpt5Codex = normalizedName.includes("gpt-5-codex") ||
372
+ normalizedName.includes("gpt 5 codex");
373
+ // Legacy GPT-5.3 Codex alias behavior (supports xhigh, but not "none")
374
+ const isGpt53Codex = normalizedName.includes("gpt-5.3-codex") ||
375
+ normalizedName.includes("gpt 5.3 codex");
376
+ // Legacy GPT-5.2 Codex alias behavior (supports xhigh, but not "none")
377
+ const isGpt52Codex = normalizedName.includes("gpt-5.2-codex") ||
378
+ normalizedName.includes("gpt 5.2 codex");
379
+ // GPT-5.4 Pro (optional/manual model) supports xhigh but not "none"
380
+ const isGpt54Pro = normalizedName.includes("gpt-5.4-pro") ||
381
+ normalizedName.includes("gpt 5.4 pro");
382
+ // GPT-5.4 Mini is a first-class explicit model.
383
+ const isGpt54Mini = canonicalModelName === "gpt-5.4-mini";
384
+ // GPT-5.4 Nano is a first-class explicit model.
385
+ const isGpt54Nano = canonicalModelName === "gpt-5.4-nano";
386
+ // GPT-5.4 general purpose (latest default family)
387
+ const isGpt54General = (normalizedName.includes("gpt-5.4") || normalizedName.includes("gpt 5.4")) &&
388
+ !isGpt54Pro &&
389
+ !isGpt54Mini &&
390
+ !isGpt54Nano;
391
+ // GPT-5.2 general purpose (not codex variant)
392
+ const isGpt52General = (normalizedName.includes("gpt-5.2") || normalizedName.includes("gpt 5.2")) &&
393
+ !isGpt52Codex;
394
+ const canonicalSupportsXhigh = canonicalModelName === "gpt-5.4" ||
395
+ canonicalModelName === "gpt-5.4-mini" ||
396
+ canonicalModelName === "gpt-5.4-nano" ||
397
+ canonicalModelName === "gpt-5.4-pro" ||
398
+ canonicalModelName === "gpt-5.2";
399
+ const isCodexMax = normalizedName.includes("codex-max") ||
400
+ normalizedName.includes("codex max");
401
+ const isCodexMini = canonicalModelName === "gpt-5.1-codex-mini";
402
+ const isCodex = normalizedName.includes("codex") && !isCodexMini;
403
+ const isLightweight = !isGpt54Mini &&
404
+ !isGpt54Nano &&
405
+ !isCodexMini &&
406
+ /\bgpt(?:-| )5(?:-| )(?:mini|nano)(?:\b|[- ])/.test(normalizedName);
407
+ // GPT-5.1 general purpose (not codex variants) - supports "none" per OpenAI API docs
408
+ const isGpt51General = (normalizedName.includes("gpt-5.1") ||
409
+ normalizedName.includes("gpt 5.1") ||
410
+ normalizedName === "gpt-5" ||
411
+ normalizedName.startsWith("gpt-5-")) &&
412
+ !isCodex &&
413
+ !isGpt54General &&
414
+ !isGpt54Mini &&
415
+ !isGpt54Pro &&
416
+ !isGpt52General &&
417
+ !isCodexMax &&
418
+ !isCodexMini;
419
+ // GPT-5.4/5.2 general, GPT-5.4 Mini/Pro, legacy GPT-5.2/5.3 Codex aliases, and Codex Max support xhigh reasoning
420
+ const supportsXhigh = isGpt54General ||
421
+ isGpt54Mini ||
422
+ isGpt54Nano ||
423
+ isGpt54Pro ||
424
+ isGpt52General ||
425
+ isGpt53Codex ||
426
+ isGpt52Codex ||
427
+ isCodexMax;
428
+ // GPT 5.1/5.2/5.4 general and GPT-5.4 Mini support "none" reasoning per:
429
+ // - OpenAI API docs: "gpt-5.1 defaults to none, supports: none, low, medium, high"
430
+ // - GPT-5.4 latest model docs list reasoning controls for the base model family
431
+ // - GPT-5.4 Mini should stay aligned with GPT-5.4 reasoning support as a first-class model
432
+ // - Legacy aliases like gpt-5-mini/gpt-5-nano now resolve to first-class
433
+ // GPT-5.4 Mini / GPT-5.4 Nano models, so they inherit the same "none" support
434
+ // - Codex CLI: ReasoningEffort enum includes None variant (codex-rs/protocol/src/openai_models.rs)
435
+ // - Codex CLI: docs/config.md lists "none" as valid for model_reasoning_effort
436
+ // - gpt-5.2 and gpt-5.4 general models support: none, low, medium, high, xhigh
437
+ // - Codex/Pro models (including GPT-5 Codex, GPT-5.4 Pro, and legacy GPT-5.3/5.2 Codex aliases) do NOT support "none"
438
+ const supportsNone = isGpt54General ||
439
+ isGpt54Mini ||
440
+ isGpt54Nano ||
441
+ isGpt52General ||
442
+ (isGpt51General && !isLightweight);
443
+ // Default based on model type (Codex CLI defaults + plugin opinionated tuning)
444
+ // Note: OpenAI docs say gpt-5.1 defaults to "none", but we default to "medium"
445
+ // for better coding assistance unless user explicitly requests "none".
446
+ // - Canonical GPT-5 Codex defaults to high in stable Codex.
447
+ // - Legacy GPT-5.3/5.2 Codex aliases default to xhigh for backward compatibility.
448
+ // - Legacy gpt-5-mini / gpt-5-nano aliases now resolve to GPT-5.4 Mini / Nano,
449
+ // so they inherit the same default "high" effort and direct xhigh support.
450
+ const defaultEffort = isCodexMini
451
+ ? "medium"
452
+ : isGpt5Codex
453
+ ? "high"
454
+ : isGpt53Codex || isGpt52Codex
455
+ ? "xhigh"
456
+ : supportsXhigh
457
+ ? "high"
458
+ : isLightweight
459
+ ? "minimal"
460
+ : "medium";
461
+ // Get user-requested effort
462
+ let effort = userConfig.reasoningEffort || defaultEffort;
463
+ const originalRequestedEffort = userConfig.reasoningEffort ?? defaultEffort;
464
+ if (isCodexMini) {
465
+ if (effort === "minimal" || effort === "low" || effort === "none") {
466
+ effort = "medium";
467
+ }
468
+ if (effort === "xhigh") {
469
+ effort = "high";
470
+ }
471
+ if (effort !== "high" && effort !== "medium") {
472
+ effort = "medium";
473
+ }
474
+ }
475
+ // For models that don't support xhigh, downgrade to high
476
+ // Legacy gpt-5-mini/gpt-5-nano aliases now normalize to GPT-5.4 Mini / Nano,
477
+ // both of which support xhigh directly.
478
+ const supportsRequestedXhigh = supportsXhigh || canonicalSupportsXhigh;
479
+ if (!supportsRequestedXhigh && effort === "xhigh") {
480
+ effort = "high";
481
+ }
482
+ // For models that don't support "none", upgrade to "low"
483
+ // (Codex/Pro models don't support "none" - only GPT-5.1/5.2/5.4 general purpose do)
484
+ if (!supportsNone && effort === "none") {
485
+ effort = "low";
486
+ }
487
+ // GPT-5.4 Pro only supports medium/high/xhigh reasoning.
488
+ // originalRequestedEffort is a non-sensitive model setting string, not a token.
489
+ // Logging this coercion does not introduce new redaction or filesystem-race risk.
490
+ if (isGpt54Pro && (effort === "low" || effort === "minimal")) {
491
+ logWarn(`GPT-5.4 Pro supports medium/high/xhigh only; coercing '${originalRequestedEffort}' to 'medium'`);
492
+ effort = "medium";
493
+ }
494
+ // Normalize "minimal" to "low" for Codex families
495
+ // Codex CLI presets are low/medium/high (or xhigh for Codex Max / GPT-5.3/5.2 Codex)
496
+ if (isCodex && effort === "minimal") {
497
+ effort = "low";
498
+ }
499
+ const summary = sanitizeReasoningSummary(userConfig.reasoningSummary);
500
+ return {
501
+ effort,
502
+ summary,
503
+ };
504
+ }
505
+ function sanitizeReasoningSummary(summary) {
506
+ if (!summary)
507
+ return "auto";
508
+ const normalized = summary.toLowerCase();
509
+ if (normalized === "concise" || normalized === "detailed" || normalized === "auto") {
510
+ return normalized;
511
+ }
512
+ return "auto";
513
+ }
514
+ /**
515
+ * Filter input array for stateless Codex API (store: false)
516
+ *
517
+ * Two transformations needed:
518
+ * 1. Remove AI SDK-specific items (not supported by Codex API)
519
+ * 2. Strip IDs from all remaining items (stateless mode)
520
+ *
521
+ * AI SDK constructs to REMOVE (not in OpenAI Responses API spec):
522
+ * - type: "item_reference" - AI SDK uses this for server-side state lookup
523
+ *
524
+ * Items to KEEP (strip IDs):
525
+ * - type: "message" - Conversation messages (provides context to LLM)
526
+ * - type: "function_call" - Tool calls from conversation
527
+ * - type: "function_call_output" - Tool results from conversation
528
+ *
529
+ * Context is maintained through:
530
+ * - Full message history (without IDs)
531
+ * - reasoning.encrypted_content (for reasoning continuity)
532
+ *
533
+ * @param input - Original input array from OpenCode/AI SDK
534
+ * @returns Filtered input array compatible with Codex API
535
+ */
536
+ export function filterInput(input) {
537
+ if (!Array.isArray(input))
538
+ return input;
539
+ return input
540
+ .filter((item) => {
541
+ // Remove AI SDK constructs not supported by Codex API
542
+ if (item.type === "item_reference") {
543
+ return false; // AI SDK only - references server state
544
+ }
545
+ return true; // Keep all other items
546
+ })
547
+ .map((item) => {
548
+ // Strip IDs from all items (Codex API stateless mode)
549
+ if (item.id) {
550
+ const { id: _omit, ...itemWithoutId } = item;
551
+ void _omit;
552
+ return itemWithoutId;
553
+ }
554
+ return item;
555
+ });
556
+ }
557
+ /**
558
+ * Trim long stateless histories for low-latency sessions.
559
+ * Keeps a small leading developer/system context plus the most recent items.
560
+ */
561
+ export function trimInputForFastSession(input, maxItems, options) {
562
+ if (!Array.isArray(input))
563
+ return input;
564
+ const MAX_HEAD_INSTRUCTION_CHARS = 1200;
565
+ const MAX_HEAD_INSTRUCTION_CHARS_TRIVIAL = 400;
566
+ if (options?.preferLatestUserOnly) {
567
+ const keepIndexes = new Set();
568
+ for (let i = 0; i < input.length; i++) {
569
+ const item = input[i];
570
+ if (!item || typeof item !== "object")
571
+ continue;
572
+ const role = typeof item?.role === "string" ? item.role : "";
573
+ if (role === "developer" || role === "system") {
574
+ const headText = extractMessageText(item.content);
575
+ if (headText.length <= MAX_HEAD_INSTRUCTION_CHARS_TRIVIAL) {
576
+ keepIndexes.add(i);
577
+ }
578
+ break;
579
+ }
580
+ }
581
+ for (let i = input.length - 1; i >= 0; i--) {
582
+ const item = input[i];
583
+ const role = typeof item?.role === "string" ? item.role.toLowerCase() : "";
584
+ if (role === "user") {
585
+ keepIndexes.add(i);
586
+ break;
587
+ }
588
+ }
589
+ const compacted = input.filter((_item, index) => keepIndexes.has(index));
590
+ if (compacted.length > 0)
591
+ return compacted;
592
+ }
593
+ const safeMax = Math.max(8, Math.floor(maxItems));
594
+ const keepIndexes = new Set();
595
+ const excludedHeadIndexes = new Set();
596
+ let keptHead = 0;
597
+ for (let i = 0; i < input.length && keptHead < 2; i++) {
598
+ const item = input[i];
599
+ if (!item || typeof item !== "object")
600
+ break;
601
+ const role = typeof item?.role === "string" ? item.role : "";
602
+ if (role === "developer" || role === "system") {
603
+ const headText = extractMessageText(item.content);
604
+ if (headText.length <= MAX_HEAD_INSTRUCTION_CHARS) {
605
+ keepIndexes.add(i);
606
+ keptHead++;
607
+ }
608
+ else {
609
+ excludedHeadIndexes.add(i);
610
+ }
611
+ continue;
612
+ }
613
+ break;
614
+ }
615
+ for (let i = Math.max(0, input.length - safeMax); i < input.length; i++) {
616
+ if (excludedHeadIndexes.has(i))
617
+ continue;
618
+ keepIndexes.add(i);
619
+ }
620
+ const trimmed = input.filter((_item, index) => keepIndexes.has(index));
621
+ if (trimmed.length === 0)
622
+ return input;
623
+ if (input.length <= maxItems && excludedHeadIndexes.size === 0)
624
+ return input;
625
+ if (trimmed.length <= safeMax)
626
+ return trimmed;
627
+ return trimmed.slice(trimmed.length - safeMax);
628
+ }
629
+ function isTrivialLatestPrompt(text) {
630
+ const normalized = text.trim();
631
+ if (!normalized)
632
+ return false;
633
+ if (normalized.length > 220)
634
+ return false;
635
+ if (normalized.includes("\n"))
636
+ return false;
637
+ if (normalized.includes("```"))
638
+ return false;
639
+ if (/(^|\n)\s*(?:[-*]|\d+\.)\s+\S/m.test(normalized))
640
+ return false;
641
+ if (/https?:\/\//i.test(normalized))
642
+ return false;
643
+ if (/\|.+\|/.test(normalized))
644
+ return false;
645
+ return true;
646
+ }
647
+ function isStructurallyComplexPrompt(text) {
648
+ const normalized = text.trim();
649
+ if (!normalized)
650
+ return false;
651
+ if (normalized.includes("```"))
652
+ return true;
653
+ const lineCount = normalized.split(/\r?\n/).filter(Boolean).length;
654
+ if (lineCount >= 3)
655
+ return true;
656
+ if (/(^|\n)\s*(?:[-*]|\d+\.)\s+\S/m.test(normalized))
657
+ return true;
658
+ if (/\|.+\|/.test(normalized))
659
+ return true;
660
+ return false;
661
+ }
662
+ function isComplexFastSessionRequest(body, maxItems) {
663
+ const input = Array.isArray(body.input) ? body.input : [];
664
+ const lookbackWindow = Math.max(12, Math.floor(maxItems / 2));
665
+ const recentItems = input.slice(-lookbackWindow);
666
+ const userTexts = [];
667
+ for (const item of recentItems) {
668
+ if (!item || typeof item !== "object")
669
+ continue;
670
+ if (item.type === "function_call" || item.type === "function_call_output") {
671
+ return true;
672
+ }
673
+ const role = typeof item.role === "string" ? item.role.toLowerCase() : "";
674
+ if (role !== "user")
675
+ continue;
676
+ const text = extractMessageText(item.content);
677
+ if (!text)
678
+ continue;
679
+ userTexts.push(text);
680
+ }
681
+ if (userTexts.length === 0)
682
+ return false;
683
+ const latestUserText = userTexts[userTexts.length - 1];
684
+ if (latestUserText && isTrivialLatestPrompt(latestUserText)) {
685
+ return false;
686
+ }
687
+ const recentUserTexts = userTexts.slice(-3);
688
+ if (recentUserTexts.some(isStructurallyComplexPrompt))
689
+ return true;
690
+ return false;
691
+ }
692
+ function getLatestUserText(input) {
693
+ if (!Array.isArray(input))
694
+ return undefined;
695
+ for (let i = input.length - 1; i >= 0; i--) {
696
+ const item = input[i];
697
+ if (!item || typeof item !== "object")
698
+ continue;
699
+ const role = typeof item.role === "string" ? item.role.toLowerCase() : "";
700
+ if (role !== "user")
701
+ continue;
702
+ const text = extractMessageText(item.content);
703
+ if (text)
704
+ return text;
705
+ }
706
+ return undefined;
707
+ }
708
+ function compactInstructionsForFastSession(instructions, isTrivialTurn = false) {
709
+ const normalized = instructions.trim();
710
+ if (!normalized)
711
+ return instructions;
712
+ const MAX_FAST_INSTRUCTION_CHARS = isTrivialTurn ? 320 : 900;
713
+ if (normalized.length <= MAX_FAST_INSTRUCTION_CHARS) {
714
+ return instructions;
715
+ }
716
+ const splitIndex = normalized.lastIndexOf("\n", MAX_FAST_INSTRUCTION_CHARS);
717
+ const safeCutoff = splitIndex >= 180 ? splitIndex : MAX_FAST_INSTRUCTION_CHARS;
718
+ const compacted = normalized.slice(0, safeCutoff).trimEnd();
719
+ return `${compacted}\n\n[Fast session mode: keep answers concise, direct, and action-oriented. Do not output internal planning labels such as "Thinking:".]`;
720
+ }
721
+ /**
722
+ * Filter out OpenCode system prompts from input
723
+ * Used in CODEX_MODE to replace OpenCode prompts with Codex-OpenCode bridge
724
+ * @param input - Input array
725
+ * @returns Input array without OpenCode system prompts
726
+ */
727
+ export async function filterOpenCodeSystemPrompts(input) {
728
+ if (!Array.isArray(input))
729
+ return input;
730
+ // Fetch cached OpenCode prompt for verification
731
+ let cachedPrompt = null;
732
+ try {
733
+ cachedPrompt = await getOpenCodeCodexPrompt();
734
+ }
735
+ catch {
736
+ // If fetch fails, fallback to text-based detection only
737
+ // This is safe because we still have the "starts with" check
738
+ }
739
+ return filterOpenCodeSystemPromptsWithCachedPrompt(input, cachedPrompt);
740
+ }
741
+ /**
742
+ * Add Codex-OpenCode bridge message to input if tools are present
743
+ * @param input - Input array
744
+ * @param hasTools - Whether tools are present in request
745
+ * @returns Input array with bridge message prepended if needed
746
+ */
747
+ export function addCodexBridgeMessage(input, hasTools, tools) {
748
+ if (!hasTools || !Array.isArray(input))
749
+ return input;
750
+ const bridgeText = renderCodexOpenCodeBridge(extractRuntimeToolNames(tools));
751
+ const bridgeMessage = {
752
+ type: "message",
753
+ role: "developer",
754
+ content: [
755
+ {
756
+ type: "input_text",
757
+ text: bridgeText,
758
+ },
759
+ ],
760
+ };
761
+ return [bridgeMessage, ...input];
762
+ }
763
+ /**
764
+ * Add tool remapping message to input if tools are present
765
+ * @param input - Input array
766
+ * @param hasTools - Whether tools are present in request
767
+ * @returns Input array with tool remap message prepended if needed
768
+ */
769
+ export function addToolRemapMessage(input, hasTools) {
770
+ if (!hasTools || !Array.isArray(input))
771
+ return input;
772
+ const toolRemapMessage = {
773
+ type: "message",
774
+ role: "developer",
775
+ content: [
776
+ {
777
+ type: "input_text",
778
+ text: TOOL_REMAP_MESSAGE,
779
+ },
780
+ ],
781
+ };
782
+ return [toolRemapMessage, ...input];
783
+ }
784
+ /**
785
+ * Transform request body for Codex API
786
+ *
787
+ * NOTE: Configuration follows Codex CLI patterns instead of opencode defaults:
788
+ * - opencode sets textVerbosity="low" for gpt-5, but Codex CLI uses "medium"
789
+ * - opencode excludes gpt-5-codex from reasoning configuration
790
+ * - This plugin uses store=false (stateless), requiring encrypted reasoning content
791
+ *
792
+ * @param body - Original request body
793
+ * @param codexInstructions - Codex system instructions
794
+ * @param userConfig - User configuration from loader
795
+ * @param codexMode - Enable CODEX_MODE (bridge prompt instead of tool remap) - defaults to true
796
+ * @param fastSession - Force low-latency output settings for faster responses
797
+ * @returns Transformed request body
798
+ */
799
+ export async function transformRequestBody(body, codexInstructions, userConfig = { global: {}, models: {} }, codexMode = true, fastSession = false, fastSessionStrategy = "hybrid", fastSessionMaxInputItems = 30) {
800
+ const originalModel = body.model;
801
+ const normalizedModel = normalizeModel(body.model);
802
+ // Get model-specific configuration using ORIGINAL model name (config key)
803
+ // This allows per-model options like "gpt-5-codex-low" to work correctly
804
+ const lookupModel = originalModel || normalizedModel;
805
+ const modelConfig = getModelConfig(lookupModel, userConfig);
806
+ // Debug: Log which config was resolved
807
+ logDebug(`Model config lookup: "${lookupModel}" → normalized to "${normalizedModel}" for API`, {
808
+ hasModelSpecificConfig: !!userConfig.models?.[lookupModel],
809
+ resolvedConfig: modelConfig,
810
+ });
811
+ // Normalize model name for API call
812
+ body.model = normalizedModel;
813
+ const shouldApplyFastSessionTuning = fastSession &&
814
+ (fastSessionStrategy === "always" ||
815
+ !isComplexFastSessionRequest(body, fastSessionMaxInputItems));
816
+ const latestUserText = getLatestUserText(body.input);
817
+ const isTrivialTurn = isTrivialLatestPrompt(latestUserText ?? "");
818
+ const shouldDisableToolsForTrivialTurn = shouldApplyFastSessionTuning &&
819
+ isTrivialTurn;
820
+ const shouldPreferLatestUserOnly = shouldApplyFastSessionTuning && isTrivialTurn;
821
+ // Codex required fields
822
+ // ChatGPT backend REQUIRES store=false (confirmed via testing)
823
+ body.store = false;
824
+ // Always set stream=true for API - response handling detects original intent
825
+ body.stream = true;
826
+ // Clean up tool definitions (implement strict "require" logic)
827
+ // Filters invalid required fields and ensures empty objects have placeholders
828
+ const collaborationMode = detectCollaborationMode(body);
829
+ if (body.tools) {
830
+ if (shouldDisableToolsForTrivialTurn) {
831
+ body.tools = undefined;
832
+ }
833
+ }
834
+ if (body.tools) {
835
+ body.tools = cleanupToolDefinitions(body.tools);
836
+ body.tools = sanitizePlanOnlyTools(body.tools, collaborationMode);
837
+ }
838
+ body.instructions = shouldApplyFastSessionTuning
839
+ ? compactInstructionsForFastSession(codexInstructions, isTrivialTurn)
840
+ : codexInstructions;
841
+ // Prompt caching relies on the host providing a stable prompt_cache_key
842
+ // (OpenCode passes its session identifier). We no longer synthesize one here.
843
+ // Filter and transform input
844
+ if (body.input && Array.isArray(body.input)) {
845
+ let inputItems = body.input;
846
+ if (shouldApplyFastSessionTuning) {
847
+ inputItems =
848
+ trimInputForFastSession(inputItems, fastSessionMaxInputItems, {
849
+ preferLatestUserOnly: shouldPreferLatestUserOnly,
850
+ }) ?? inputItems;
851
+ }
852
+ // Debug: Log original input message IDs before filtering
853
+ const originalIds = inputItems
854
+ .filter((item) => item.id)
855
+ .map((item) => item.id);
856
+ if (originalIds.length > 0) {
857
+ logDebug(`Filtering ${originalIds.length} message IDs from input:`, originalIds);
858
+ }
859
+ inputItems = filterInput(inputItems) ?? inputItems;
860
+ body.input = inputItems;
861
+ // istanbul ignore next -- filterInput always removes IDs; this is defensive debug code
862
+ const remainingIds = (body.input || [])
863
+ .filter((item) => item.id)
864
+ .map((item) => item.id);
865
+ // istanbul ignore if -- filterInput always removes IDs; defensive debug warning
866
+ if (remainingIds.length > 0) {
867
+ logWarn(`WARNING: ${remainingIds.length} IDs still present after filtering:`, remainingIds);
868
+ }
869
+ else if (originalIds.length > 0) {
870
+ logDebug(`Successfully removed all ${originalIds.length} message IDs`);
871
+ }
872
+ if (codexMode) {
873
+ // CODEX_MODE: Remove OpenCode system prompt, add bridge prompt
874
+ body.input = await filterOpenCodeSystemPrompts(body.input);
875
+ body.input = addCodexBridgeMessage(body.input, !!body.tools, body.tools);
876
+ }
877
+ else {
878
+ // DEFAULT MODE: Keep original behavior with tool remap message
879
+ body.input = addToolRemapMessage(body.input, !!body.tools);
880
+ }
881
+ // Handle orphaned function_call_output items (where function_call was an item_reference that got filtered)
882
+ // Instead of removing orphans (which causes infinite loops as LLM loses tool results),
883
+ // convert them to messages to preserve context while avoiding API errors
884
+ if (body.input) {
885
+ body.input = normalizeOrphanedToolOutputs(body.input);
886
+ body.input = injectMissingToolOutputs(body.input);
887
+ }
888
+ }
889
+ // Configure reasoning (prefer existing body/provider options, then config defaults)
890
+ const reasoningConfig = resolveReasoningConfig(lookupModel, modelConfig, body);
891
+ body.reasoning = {
892
+ ...body.reasoning,
893
+ ...reasoningConfig,
894
+ };
895
+ // Configure text verbosity (support user config)
896
+ // Default: "medium" (matches Codex CLI default for all GPT-5 models)
897
+ body.text = {
898
+ ...body.text,
899
+ verbosity: resolveTextVerbosity(modelConfig, body),
900
+ };
901
+ if (shouldApplyFastSessionTuning) {
902
+ // In fast-session mode, prioritize speed by clamping to minimum reasoning + verbosity.
903
+ // getReasoningConfig normalizes unsupported values per model family.
904
+ const fastReasoning = getReasoningConfig(lookupModel, {
905
+ reasoningEffort: "none",
906
+ reasoningSummary: "auto",
907
+ });
908
+ body.reasoning = {
909
+ ...body.reasoning,
910
+ ...fastReasoning,
911
+ };
912
+ body.text = {
913
+ ...body.text,
914
+ verbosity: "low",
915
+ };
916
+ }
917
+ // Add include for encrypted reasoning content
918
+ // Default: ["reasoning.encrypted_content"] (required for stateless operation with store=false)
919
+ // This allows reasoning context to persist across turns without server-side storage
920
+ body.include = resolveInclude(modelConfig, body);
921
+ // Preserve caller-supplied max_output_tokens from the in-memory request body.
922
+ // max_output_tokens is a numeric budget, not a credential, so no redaction is required.
923
+ // Windows filesystem: no file I/O occurs here; no concurrency risk is introduced.
924
+ // Regression: "should preserve max_output_tokens while removing max_completion_tokens"
925
+ // in test/request-transformer.test.ts.
926
+ // Remove unsupported parameters.
927
+ body.max_completion_tokens = undefined;
928
+ return body;
929
+ }
930
+ //# sourceMappingURL=request-transformer.js.map