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.
- package/LICENSE +21 -0
- package/README.md +162 -0
- package/assets/opencode-logo-ornate-dark.svg +18 -0
- package/assets/readme-hero.svg +31 -0
- package/config/README.md +96 -0
- package/config/minimal-opencode.json +13 -0
- package/config/opencode-legacy.json +871 -0
- package/config/opencode-modern.json +358 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5008 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/accounts/rate-limits.d.ts +22 -0
- package/dist/lib/accounts/rate-limits.d.ts.map +1 -0
- package/dist/lib/accounts/rate-limits.js +63 -0
- package/dist/lib/accounts/rate-limits.js.map +1 -0
- package/dist/lib/accounts.d.ts +134 -0
- package/dist/lib/accounts.d.ts.map +1 -0
- package/dist/lib/accounts.js +795 -0
- package/dist/lib/accounts.js.map +1 -0
- package/dist/lib/audit.d.ts +45 -0
- package/dist/lib/audit.d.ts.map +1 -0
- package/dist/lib/audit.js +154 -0
- package/dist/lib/audit.js.map +1 -0
- package/dist/lib/auth/auth.d.ts +51 -0
- package/dist/lib/auth/auth.d.ts.map +1 -0
- package/dist/lib/auth/auth.js +191 -0
- package/dist/lib/auth/auth.js.map +1 -0
- package/dist/lib/auth/browser.d.ts +17 -0
- package/dist/lib/auth/browser.d.ts.map +1 -0
- package/dist/lib/auth/browser.js +90 -0
- package/dist/lib/auth/browser.js.map +1 -0
- package/dist/lib/auth/device-code.d.ts +28 -0
- package/dist/lib/auth/device-code.d.ts.map +1 -0
- package/dist/lib/auth/device-code.js +215 -0
- package/dist/lib/auth/device-code.js.map +1 -0
- package/dist/lib/auth/login-runner.d.ts +55 -0
- package/dist/lib/auth/login-runner.d.ts.map +1 -0
- package/dist/lib/auth/login-runner.js +631 -0
- package/dist/lib/auth/login-runner.js.map +1 -0
- package/dist/lib/auth/server.d.ts +10 -0
- package/dist/lib/auth/server.d.ts.map +1 -0
- package/dist/lib/auth/server.js +98 -0
- package/dist/lib/auth/server.js.map +1 -0
- package/dist/lib/auth/token-utils.d.ts +60 -0
- package/dist/lib/auth/token-utils.d.ts.map +1 -0
- package/dist/lib/auth/token-utils.js +416 -0
- package/dist/lib/auth/token-utils.js.map +1 -0
- package/dist/lib/auth-rate-limit.d.ts +20 -0
- package/dist/lib/auth-rate-limit.d.ts.map +1 -0
- package/dist/lib/auth-rate-limit.js +91 -0
- package/dist/lib/auth-rate-limit.js.map +1 -0
- package/dist/lib/auto-update-checker.d.ts +10 -0
- package/dist/lib/auto-update-checker.d.ts.map +1 -0
- package/dist/lib/auto-update-checker.js +129 -0
- package/dist/lib/auto-update-checker.js.map +1 -0
- package/dist/lib/circuit-breaker.d.ts +34 -0
- package/dist/lib/circuit-breaker.d.ts.map +1 -0
- package/dist/lib/circuit-breaker.js +124 -0
- package/dist/lib/circuit-breaker.js.map +1 -0
- package/dist/lib/cli.d.ts +46 -0
- package/dist/lib/cli.d.ts.map +1 -0
- package/dist/lib/cli.js +192 -0
- package/dist/lib/cli.js.map +1 -0
- package/dist/lib/config.d.ts +42 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +314 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/constants.d.ts +90 -0
- package/dist/lib/constants.d.ts.map +1 -0
- package/dist/lib/constants.js +90 -0
- package/dist/lib/constants.js.map +1 -0
- package/dist/lib/context-overflow.d.ts +27 -0
- package/dist/lib/context-overflow.d.ts.map +1 -0
- package/dist/lib/context-overflow.js +124 -0
- package/dist/lib/context-overflow.js.map +1 -0
- package/dist/lib/errors.d.ts +113 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +103 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/health.d.ts +33 -0
- package/dist/lib/health.d.ts.map +1 -0
- package/dist/lib/health.js +70 -0
- package/dist/lib/health.js.map +1 -0
- package/dist/lib/index.d.ts +20 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +20 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/logger.d.ts +40 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/logger.js +337 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/oauth-success.html +712 -0
- package/dist/lib/parallel-probe.d.ts +28 -0
- package/dist/lib/parallel-probe.d.ts.map +1 -0
- package/dist/lib/parallel-probe.js +97 -0
- package/dist/lib/parallel-probe.js.map +1 -0
- package/dist/lib/proactive-refresh.d.ts +67 -0
- package/dist/lib/proactive-refresh.d.ts.map +1 -0
- package/dist/lib/proactive-refresh.js +161 -0
- package/dist/lib/proactive-refresh.js.map +1 -0
- package/dist/lib/prompts/codex-opencode-bridge.d.ts +20 -0
- package/dist/lib/prompts/codex-opencode-bridge.d.ts.map +1 -0
- package/dist/lib/prompts/codex-opencode-bridge.js +126 -0
- package/dist/lib/prompts/codex-opencode-bridge.js.map +1 -0
- package/dist/lib/prompts/codex.d.ts +41 -0
- package/dist/lib/prompts/codex.d.ts.map +1 -0
- package/dist/lib/prompts/codex.js +400 -0
- package/dist/lib/prompts/codex.js.map +1 -0
- package/dist/lib/prompts/opencode-codex.d.ts +25 -0
- package/dist/lib/prompts/opencode-codex.d.ts.map +1 -0
- package/dist/lib/prompts/opencode-codex.js +223 -0
- package/dist/lib/prompts/opencode-codex.js.map +1 -0
- package/dist/lib/recovery/constants.d.ts +12 -0
- package/dist/lib/recovery/constants.d.ts.map +1 -0
- package/dist/lib/recovery/constants.js +25 -0
- package/dist/lib/recovery/constants.js.map +1 -0
- package/dist/lib/recovery/index.d.ts +12 -0
- package/dist/lib/recovery/index.d.ts.map +1 -0
- package/dist/lib/recovery/index.js +12 -0
- package/dist/lib/recovery/index.js.map +1 -0
- package/dist/lib/recovery/storage.d.ts +24 -0
- package/dist/lib/recovery/storage.d.ts.map +1 -0
- package/dist/lib/recovery/storage.js +362 -0
- package/dist/lib/recovery/storage.js.map +1 -0
- package/dist/lib/recovery/types.d.ts +116 -0
- package/dist/lib/recovery/types.d.ts.map +1 -0
- package/dist/lib/recovery/types.js +7 -0
- package/dist/lib/recovery/types.js.map +1 -0
- package/dist/lib/recovery.d.ts +31 -0
- package/dist/lib/recovery.d.ts.map +1 -0
- package/dist/lib/recovery.js +313 -0
- package/dist/lib/recovery.js.map +1 -0
- package/dist/lib/refresh-queue.d.ts +131 -0
- package/dist/lib/refresh-queue.d.ts.map +1 -0
- package/dist/lib/refresh-queue.js +297 -0
- package/dist/lib/refresh-queue.js.map +1 -0
- package/dist/lib/request/fetch-helpers.d.ts +142 -0
- package/dist/lib/request/fetch-helpers.d.ts.map +1 -0
- package/dist/lib/request/fetch-helpers.js +749 -0
- package/dist/lib/request/fetch-helpers.js.map +1 -0
- package/dist/lib/request/helpers/input-utils.d.ts +7 -0
- package/dist/lib/request/helpers/input-utils.d.ts.map +1 -0
- package/dist/lib/request/helpers/input-utils.js +214 -0
- package/dist/lib/request/helpers/input-utils.js.map +1 -0
- package/dist/lib/request/helpers/model-map.d.ts +22 -0
- package/dist/lib/request/helpers/model-map.d.ts.map +1 -0
- package/dist/lib/request/helpers/model-map.js +183 -0
- package/dist/lib/request/helpers/model-map.js.map +1 -0
- package/dist/lib/request/helpers/tool-utils.d.ts +29 -0
- package/dist/lib/request/helpers/tool-utils.d.ts.map +1 -0
- package/dist/lib/request/helpers/tool-utils.js +117 -0
- package/dist/lib/request/helpers/tool-utils.js.map +1 -0
- package/dist/lib/request/rate-limit-backoff.d.ts +17 -0
- package/dist/lib/request/rate-limit-backoff.d.ts.map +1 -0
- package/dist/lib/request/rate-limit-backoff.js +83 -0
- package/dist/lib/request/rate-limit-backoff.js.map +1 -0
- package/dist/lib/request/request-transformer.d.ts +107 -0
- package/dist/lib/request/request-transformer.d.ts.map +1 -0
- package/dist/lib/request/request-transformer.js +930 -0
- package/dist/lib/request/request-transformer.js.map +1 -0
- package/dist/lib/request/response-handler.d.ts +23 -0
- package/dist/lib/request/response-handler.d.ts.map +1 -0
- package/dist/lib/request/response-handler.js +245 -0
- package/dist/lib/request/response-handler.js.map +1 -0
- package/dist/lib/request/retry-budget.d.ts +19 -0
- package/dist/lib/request/retry-budget.d.ts.map +1 -0
- package/dist/lib/request/retry-budget.js +99 -0
- package/dist/lib/request/retry-budget.js.map +1 -0
- package/dist/lib/rotation.d.ts +130 -0
- package/dist/lib/rotation.d.ts.map +1 -0
- package/dist/lib/rotation.js +301 -0
- package/dist/lib/rotation.js.map +1 -0
- package/dist/lib/runtime-contracts.d.ts +16 -0
- package/dist/lib/runtime-contracts.d.ts.map +1 -0
- package/dist/lib/runtime-contracts.js +24 -0
- package/dist/lib/runtime-contracts.js.map +1 -0
- package/dist/lib/schemas.d.ts +457 -0
- package/dist/lib/schemas.d.ts.map +1 -0
- package/dist/lib/schemas.js +283 -0
- package/dist/lib/schemas.js.map +1 -0
- package/dist/lib/shutdown.d.ts +7 -0
- package/dist/lib/shutdown.d.ts.map +1 -0
- package/dist/lib/shutdown.js +43 -0
- package/dist/lib/shutdown.js.map +1 -0
- package/dist/lib/storage/migrations.d.ts +65 -0
- package/dist/lib/storage/migrations.d.ts.map +1 -0
- package/dist/lib/storage/migrations.js +44 -0
- package/dist/lib/storage/migrations.js.map +1 -0
- package/dist/lib/storage/paths.d.ts +16 -0
- package/dist/lib/storage/paths.d.ts.map +1 -0
- package/dist/lib/storage/paths.js +92 -0
- package/dist/lib/storage/paths.js.map +1 -0
- package/dist/lib/storage.d.ts +157 -0
- package/dist/lib/storage.d.ts.map +1 -0
- package/dist/lib/storage.js +1169 -0
- package/dist/lib/storage.js.map +1 -0
- package/dist/lib/table-formatter.d.ts +32 -0
- package/dist/lib/table-formatter.d.ts.map +1 -0
- package/dist/lib/table-formatter.js +44 -0
- package/dist/lib/table-formatter.js.map +1 -0
- package/dist/lib/types.d.ts +131 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/lib/ui/ansi.d.ts +22 -0
- package/dist/lib/ui/ansi.d.ts.map +1 -0
- package/dist/lib/ui/ansi.js +38 -0
- package/dist/lib/ui/ansi.js.map +1 -0
- package/dist/lib/ui/auth-menu.d.ts +39 -0
- package/dist/lib/ui/auth-menu.d.ts.map +1 -0
- package/dist/lib/ui/auth-menu.js +186 -0
- package/dist/lib/ui/auth-menu.js.map +1 -0
- package/dist/lib/ui/beginner.d.ts +62 -0
- package/dist/lib/ui/beginner.d.ts.map +1 -0
- package/dist/lib/ui/beginner.js +259 -0
- package/dist/lib/ui/beginner.js.map +1 -0
- package/dist/lib/ui/confirm.d.ts +2 -0
- package/dist/lib/ui/confirm.d.ts.map +1 -0
- package/dist/lib/ui/confirm.js +21 -0
- package/dist/lib/ui/confirm.js.map +1 -0
- package/dist/lib/ui/format.d.ts +9 -0
- package/dist/lib/ui/format.d.ts.map +1 -0
- package/dist/lib/ui/format.js +49 -0
- package/dist/lib/ui/format.js.map +1 -0
- package/dist/lib/ui/runtime.d.ts +11 -0
- package/dist/lib/ui/runtime.d.ts.map +1 -0
- package/dist/lib/ui/runtime.js +31 -0
- package/dist/lib/ui/runtime.js.map +1 -0
- package/dist/lib/ui/select.d.ts +20 -0
- package/dist/lib/ui/select.d.ts.map +1 -0
- package/dist/lib/ui/select.js +343 -0
- package/dist/lib/ui/select.js.map +1 -0
- package/dist/lib/ui/theme.d.ts +34 -0
- package/dist/lib/ui/theme.d.ts.map +1 -0
- package/dist/lib/ui/theme.js +84 -0
- package/dist/lib/ui/theme.js.map +1 -0
- package/dist/lib/utils.d.ts +29 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +54 -0
- package/dist/lib/utils.js.map +1 -0
- package/package.json +113 -0
- package/scripts/audit-dev-allowlist.js +114 -0
- package/scripts/copy-oauth-success.js +37 -0
- package/scripts/install-oc-codex-multi-auth-core.js +381 -0
- package/scripts/install-oc-codex-multi-auth.js +15 -0
- package/scripts/test-all-models.sh +260 -0
- 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
|