@golba98/codexa 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +320 -0
- package/bin/codexa.js +445 -0
- package/package.json +45 -0
- package/scripts/audit-codexa-capabilities.mjs +466 -0
- package/scripts/smoke-terminal-bench.mjs +35 -0
- package/src/app.tsx +4561 -0
- package/src/appRenderStability.test.ts +131 -0
- package/src/commands/handler.test.ts +643 -0
- package/src/commands/handler.ts +875 -0
- package/src/config/launchArgs.test.ts +158 -0
- package/src/config/launchArgs.ts +186 -0
- package/src/config/layeredConfig.test.ts +143 -0
- package/src/config/layeredConfig.ts +836 -0
- package/src/config/persistence.test.ts +110 -0
- package/src/config/persistence.ts +311 -0
- package/src/config/runtimeConfig.test.ts +218 -0
- package/src/config/runtimeConfig.ts +554 -0
- package/src/config/settings.test.ts +155 -0
- package/src/config/settings.ts +401 -0
- package/src/config/toml-serialize.ts +98 -0
- package/src/config/trustStore.test.ts +29 -0
- package/src/config/trustStore.ts +68 -0
- package/src/core/attachments.test.ts +155 -0
- package/src/core/attachments.ts +71 -0
- package/src/core/auth/codexAuth.test.ts +68 -0
- package/src/core/auth/codexAuth.ts +359 -0
- package/src/core/cleanupFastFail.test.ts +76 -0
- package/src/core/cleanupFastFail.ts +67 -0
- package/src/core/clipboard.ts +24 -0
- package/src/core/codex.ts +124 -0
- package/src/core/codexExecArgs.test.ts +195 -0
- package/src/core/codexExecArgs.ts +152 -0
- package/src/core/codexLaunch.test.ts +205 -0
- package/src/core/codexLaunch.ts +162 -0
- package/src/core/codexPrompt.test.ts +252 -0
- package/src/core/codexPrompt.ts +428 -0
- package/src/core/executables/claudeExecutable.ts +63 -0
- package/src/core/executables/codexExecutable.test.ts +212 -0
- package/src/core/executables/codexExecutable.ts +159 -0
- package/src/core/executables/executableResolver.test.ts +129 -0
- package/src/core/executables/executableResolver.ts +138 -0
- package/src/core/executables/geminiExecutable.test.ts +116 -0
- package/src/core/executables/geminiExecutable.ts +78 -0
- package/src/core/executables/pathSanityScan.test.ts +47 -0
- package/src/core/githubDiagnostics.test.ts +92 -0
- package/src/core/githubDiagnostics.ts +222 -0
- package/src/core/hollowResponseFormat.test.ts +58 -0
- package/src/core/hollowResponseFormat.ts +39 -0
- package/src/core/inputDebug.ts +51 -0
- package/src/core/launchContext.test.ts +157 -0
- package/src/core/launchContext.ts +266 -0
- package/src/core/models/codexCapabilities.test.ts +45 -0
- package/src/core/models/codexCapabilities.ts +95 -0
- package/src/core/models/codexModelCapabilities.test.ts +246 -0
- package/src/core/models/codexModelCapabilities.ts +571 -0
- package/src/core/models/modelSpecs.test.ts +283 -0
- package/src/core/models/modelSpecs.ts +300 -0
- package/src/core/perf/profiler.ts +125 -0
- package/src/core/perf/renderDebug.test.ts +230 -0
- package/src/core/perf/renderDebug.ts +373 -0
- package/src/core/planStorage.test.ts +143 -0
- package/src/core/planStorage.ts +141 -0
- package/src/core/process/CommandRunner.test.ts +105 -0
- package/src/core/process/CommandRunner.ts +269 -0
- package/src/core/process/processValidation.ts +101 -0
- package/src/core/projectInstructions.test.ts +50 -0
- package/src/core/projectInstructions.ts +54 -0
- package/src/core/providerLauncher/launcher.test.ts +238 -0
- package/src/core/providerLauncher/launcher.ts +203 -0
- package/src/core/providerLauncher/registry.test.ts +324 -0
- package/src/core/providerLauncher/registry.ts +253 -0
- package/src/core/providerLauncher/types.ts +84 -0
- package/src/core/providerLauncher/workspaceConfig.test.ts +638 -0
- package/src/core/providerLauncher/workspaceConfig.ts +407 -0
- package/src/core/providerRuntime/anthropic.test.ts +1120 -0
- package/src/core/providerRuntime/anthropic.ts +576 -0
- package/src/core/providerRuntime/capabilityProfile.test.ts +311 -0
- package/src/core/providerRuntime/capabilityProfile.ts +288 -0
- package/src/core/providerRuntime/claudeCodeDiscovery.ts +446 -0
- package/src/core/providerRuntime/contextMetadata.test.ts +468 -0
- package/src/core/providerRuntime/contextMetadata.ts +409 -0
- package/src/core/providerRuntime/gemini.test.ts +437 -0
- package/src/core/providerRuntime/gemini.ts +784 -0
- package/src/core/providerRuntime/lmstudio.test.ts +168 -0
- package/src/core/providerRuntime/lmstudio.ts +118 -0
- package/src/core/providerRuntime/local.test.ts +787 -0
- package/src/core/providerRuntime/local.ts +754 -0
- package/src/core/providerRuntime/models.ts +150 -0
- package/src/core/providerRuntime/reasoning.ts +17 -0
- package/src/core/providerRuntime/registry.test.ts +233 -0
- package/src/core/providerRuntime/registry.ts +203 -0
- package/src/core/providerRuntime/types.ts +103 -0
- package/src/core/providers/codexJsonStream.test.ts +148 -0
- package/src/core/providers/codexJsonStream.ts +305 -0
- package/src/core/providers/codexSubprocess.test.ts +68 -0
- package/src/core/providers/codexSubprocess.ts +372 -0
- package/src/core/providers/codexTranscript.test.ts +284 -0
- package/src/core/providers/codexTranscript.ts +695 -0
- package/src/core/providers/openaiNative.ts +13 -0
- package/src/core/providers/registry.ts +21 -0
- package/src/core/providers/types.ts +59 -0
- package/src/core/terminal/terminalCapabilities.test.ts +93 -0
- package/src/core/terminal/terminalCapabilities.ts +100 -0
- package/src/core/terminal/terminalControl.test.ts +75 -0
- package/src/core/terminal/terminalControl.ts +147 -0
- package/src/core/terminal/terminalSanitize.test.ts +22 -0
- package/src/core/terminal/terminalSanitize.ts +147 -0
- package/src/core/terminal/terminalSelection.test.ts +42 -0
- package/src/core/terminal/terminalSelection.ts +66 -0
- package/src/core/terminal/terminalTitle.test.ts +328 -0
- package/src/core/terminal/terminalTitle.ts +483 -0
- package/src/core/workspaceActivity.test.ts +163 -0
- package/src/core/workspaceActivity.ts +380 -0
- package/src/core/workspaceGuard.test.ts +151 -0
- package/src/core/workspaceGuard.ts +288 -0
- package/src/core/workspaceRoot.test.ts +23 -0
- package/src/core/workspaceRoot.ts +47 -0
- package/src/exec.test.ts +13 -0
- package/src/exec.ts +72 -0
- package/src/headless/execArgs.test.ts +147 -0
- package/src/headless/execArgs.ts +294 -0
- package/src/headless/execRunner.test.ts +434 -0
- package/src/headless/execRunner.ts +304 -0
- package/src/index.test.tsx +618 -0
- package/src/index.tsx +296 -0
- package/src/session/appSession.test.ts +897 -0
- package/src/session/appSession.ts +761 -0
- package/src/session/chatLifecycle.test.ts +64 -0
- package/src/session/chatLifecycle.ts +951 -0
- package/src/session/liveRenderScheduler.test.ts +201 -0
- package/src/session/liveRenderScheduler.ts +214 -0
- package/src/session/planFlow.test.ts +103 -0
- package/src/session/planFlow.ts +149 -0
- package/src/session/planTranscript.test.ts +65 -0
- package/src/session/planTranscript.ts +15 -0
- package/src/session/promptRunSchedule.test.ts +36 -0
- package/src/session/promptRunSchedule.ts +26 -0
- package/src/session/types.ts +228 -0
- package/src/test/runtimeTestUtils.ts +14 -0
- package/src/types/react-dom.d.ts +3 -0
- package/src/ui/ActionRequiredBlock.tsx +38 -0
- package/src/ui/ActivityBars.tsx +68 -0
- package/src/ui/ActivityIndicator.test.tsx +58 -0
- package/src/ui/ActivityIndicator.tsx +58 -0
- package/src/ui/AgentBlock.test.ts +6 -0
- package/src/ui/AgentBlock.tsx +130 -0
- package/src/ui/AnimatedStatusText.test.ts +16 -0
- package/src/ui/AnimatedStatusText.tsx +69 -0
- package/src/ui/AppShell.test.tsx +1739 -0
- package/src/ui/AppShell.tsx +698 -0
- package/src/ui/AttachmentImportPanel.test.tsx +204 -0
- package/src/ui/AttachmentImportPanel.tsx +98 -0
- package/src/ui/AuthPanel.tsx +113 -0
- package/src/ui/BackendPicker.tsx +28 -0
- package/src/ui/BottomComposer.test.ts +674 -0
- package/src/ui/BottomComposer.tsx +1028 -0
- package/src/ui/CodexLogo.tsx +55 -0
- package/src/ui/DashCard.tsx +82 -0
- package/src/ui/Markdown.test.ts +157 -0
- package/src/ui/Markdown.tsx +310 -0
- package/src/ui/ModePicker.tsx +27 -0
- package/src/ui/ModelPicker.tsx +31 -0
- package/src/ui/ModelPickerProviderScope.test.tsx +411 -0
- package/src/ui/ModelPickerScreen.test.tsx +99 -0
- package/src/ui/ModelPickerScreen.tsx +416 -0
- package/src/ui/ModelPickerState.test.tsx +151 -0
- package/src/ui/ModelReasoningPicker.test.tsx +447 -0
- package/src/ui/ModelReasoningPicker.tsx +458 -0
- package/src/ui/Panel.tsx +51 -0
- package/src/ui/PermissionsPanel.tsx +78 -0
- package/src/ui/PlanActionPicker.tsx +119 -0
- package/src/ui/PlanReviewPanel.test.tsx +267 -0
- package/src/ui/PlanReviewPanel.tsx +212 -0
- package/src/ui/PromptCardBorder.test.tsx +161 -0
- package/src/ui/ProviderPicker.test.tsx +289 -0
- package/src/ui/ProviderPicker.tsx +321 -0
- package/src/ui/ProviderShortcut.test.tsx +143 -0
- package/src/ui/ReasoningPicker.tsx +46 -0
- package/src/ui/RunFooter.tsx +65 -0
- package/src/ui/SelectionPanel.tsx +67 -0
- package/src/ui/SettingsPanel.test.tsx +233 -0
- package/src/ui/SettingsPanel.tsx +156 -0
- package/src/ui/Spinner.tsx +25 -0
- package/src/ui/StaticIntroItem.tsx +54 -0
- package/src/ui/StaticTranscriptItem.tsx +56 -0
- package/src/ui/TextEntryPanel.tsx +139 -0
- package/src/ui/ThemePicker.tsx +31 -0
- package/src/ui/ThinkingBlock.tsx +100 -0
- package/src/ui/Timeline.test.ts +2067 -0
- package/src/ui/Timeline.tsx +1472 -0
- package/src/ui/TimelineNavigation.test.tsx +201 -0
- package/src/ui/TopHeader.test.tsx +239 -0
- package/src/ui/TopHeader.tsx +257 -0
- package/src/ui/TurnGroup.test.tsx +365 -0
- package/src/ui/TurnGroup.tsx +657 -0
- package/src/ui/busyStatusAnimation.test.ts +30 -0
- package/src/ui/busyStatusAnimation.ts +11 -0
- package/src/ui/commandNormalize.test.ts +142 -0
- package/src/ui/commandNormalize.ts +66 -0
- package/src/ui/diffRenderer.test.ts +102 -0
- package/src/ui/diffRenderer.ts +116 -0
- package/src/ui/focus.ts +61 -0
- package/src/ui/focusFlow.test.tsx +1098 -0
- package/src/ui/inputBuffer.test.ts +151 -0
- package/src/ui/inputBuffer.ts +203 -0
- package/src/ui/layout.test.ts +145 -0
- package/src/ui/layout.ts +287 -0
- package/src/ui/modeDisplay.test.ts +42 -0
- package/src/ui/modeDisplay.ts +52 -0
- package/src/ui/outputPipeline.ts +64 -0
- package/src/ui/progressEntries.ts +156 -0
- package/src/ui/runActivityView.test.ts +89 -0
- package/src/ui/runActivityView.ts +37 -0
- package/src/ui/runLifecycleView.test.tsx +237 -0
- package/src/ui/slashCommands.ts +41 -0
- package/src/ui/statusRenderIsolation.test.tsx +654 -0
- package/src/ui/terminalAnswerFormat.test.ts +19 -0
- package/src/ui/terminalAnswerFormat.ts +128 -0
- package/src/ui/textLayout.test.ts +18 -0
- package/src/ui/textLayout.ts +338 -0
- package/src/ui/theme.tsx +395 -0
- package/src/ui/themeFlow.test.ts +53 -0
- package/src/ui/themeFlow.ts +41 -0
- package/src/ui/timelineMeasure.ts +3088 -0
- package/src/ui/timelineMeasureCache.test.ts +986 -0
- package/src/ui/useThrottledValue.ts +31 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { CodexModelCapability, CodexModelCapabilities } from "../models/codexModelCapabilities.js";
|
|
2
|
+
import type { ProviderModel } from "./types.js";
|
|
3
|
+
import { getClaudeCodeEffortLevels } from "./reasoning.js";
|
|
4
|
+
|
|
5
|
+
export const GEMINI_DEFAULT_MODEL_ID = "gemini-3-flash-preview";
|
|
6
|
+
export const GEMINI_VERIFIED_MODEL_IDS = [
|
|
7
|
+
"gemini-3.1-pro-preview",
|
|
8
|
+
"gemini-3-flash-preview",
|
|
9
|
+
"gemini-3.1-flash-lite-preview",
|
|
10
|
+
"gemini-2.5-pro",
|
|
11
|
+
"gemini-2.5-flash",
|
|
12
|
+
"gemini-2.5-flash-lite",
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
export function isVerifiedGeminiModelId(modelId: string | null | undefined): modelId is typeof GEMINI_VERIFIED_MODEL_IDS[number] {
|
|
16
|
+
return typeof modelId === "string" && GEMINI_VERIFIED_MODEL_IDS.includes(modelId as typeof GEMINI_VERIFIED_MODEL_IDS[number]);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function normalizeGeminiModelId(modelId: string | null | undefined): string {
|
|
20
|
+
// "gemini-3-flash" is an older shorthand; remap it to the canonical preview ID.
|
|
21
|
+
if (modelId === "gemini-3-flash") return "gemini-3-flash-preview";
|
|
22
|
+
return isVerifiedGeminiModelId(modelId) ? modelId : GEMINI_DEFAULT_MODEL_ID;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const GEMINI_FALLBACK_MODELS: readonly ProviderModel[] = [
|
|
26
|
+
{
|
|
27
|
+
id: "gemini-3-flash-preview",
|
|
28
|
+
modelId: "gemini-3-flash-preview",
|
|
29
|
+
label: "Gemini 3 Flash Preview",
|
|
30
|
+
description: "Verified Gemini CLI Flash Preview route.",
|
|
31
|
+
defaultReasoningLevel: "medium",
|
|
32
|
+
supportedReasoningLevels: null,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "gemini-3.1-pro-preview",
|
|
36
|
+
modelId: "gemini-3.1-pro-preview",
|
|
37
|
+
label: "Gemini 3.1 Pro Preview",
|
|
38
|
+
description: "Verified Gemini CLI Pro Preview route.",
|
|
39
|
+
defaultReasoningLevel: "high",
|
|
40
|
+
supportedReasoningLevels: null,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "gemini-3.1-flash-lite-preview",
|
|
44
|
+
modelId: "gemini-3.1-flash-lite-preview",
|
|
45
|
+
label: "Gemini 3.1 Flash Lite Preview",
|
|
46
|
+
description: "Verified Gemini CLI Flash Lite Preview route.",
|
|
47
|
+
defaultReasoningLevel: "medium",
|
|
48
|
+
supportedReasoningLevels: null,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "gemini-2.5-pro",
|
|
52
|
+
modelId: "gemini-2.5-pro",
|
|
53
|
+
label: "Gemini 2.5 Pro",
|
|
54
|
+
description: "Verified Gemini CLI Pro route.",
|
|
55
|
+
defaultReasoningLevel: "high",
|
|
56
|
+
supportedReasoningLevels: null,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "gemini-2.5-flash",
|
|
60
|
+
modelId: "gemini-2.5-flash",
|
|
61
|
+
label: "Gemini 2.5 Flash",
|
|
62
|
+
description: "Verified Gemini CLI Flash route.",
|
|
63
|
+
defaultReasoningLevel: "medium",
|
|
64
|
+
supportedReasoningLevels: null,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "gemini-2.5-flash-lite",
|
|
68
|
+
modelId: "gemini-2.5-flash-lite",
|
|
69
|
+
label: "Gemini 2.5 Flash Lite",
|
|
70
|
+
description: "Verified Gemini CLI Flash Lite route.",
|
|
71
|
+
defaultReasoningLevel: "medium",
|
|
72
|
+
supportedReasoningLevels: null,
|
|
73
|
+
},
|
|
74
|
+
] as const;
|
|
75
|
+
|
|
76
|
+
export const ANTHROPIC_FALLBACK_MODELS: readonly ProviderModel[] = [
|
|
77
|
+
{
|
|
78
|
+
id: "opus",
|
|
79
|
+
modelId: "opus",
|
|
80
|
+
label: "Opus 4.7",
|
|
81
|
+
description: "Claude Opus 4.7 - Fallback defaults",
|
|
82
|
+
defaultReasoningLevel: "xhigh",
|
|
83
|
+
supportedReasoningLevels: getClaudeCodeEffortLevels(["low", "medium", "high", "xhigh", "max"]),
|
|
84
|
+
source: "fallback",
|
|
85
|
+
canonicalId: "claude-opus-4-7",
|
|
86
|
+
family: "opus",
|
|
87
|
+
effortSource: "fallback",
|
|
88
|
+
effortVerified: false,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: "sonnet",
|
|
92
|
+
modelId: "sonnet",
|
|
93
|
+
label: "Sonnet 4.6",
|
|
94
|
+
description: "Claude Sonnet 4.6 - Fallback defaults",
|
|
95
|
+
defaultReasoningLevel: "high",
|
|
96
|
+
supportedReasoningLevels: getClaudeCodeEffortLevels(["low", "medium", "high", "max"]),
|
|
97
|
+
source: "fallback",
|
|
98
|
+
canonicalId: "claude-sonnet-4-6",
|
|
99
|
+
family: "sonnet",
|
|
100
|
+
effortSource: "fallback",
|
|
101
|
+
effortVerified: false,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "haiku",
|
|
105
|
+
modelId: "haiku",
|
|
106
|
+
label: "Haiku 4.5",
|
|
107
|
+
description: "Claude Haiku 4.5 - Fallback defaults; effort metadata unverified",
|
|
108
|
+
defaultReasoningLevel: "medium",
|
|
109
|
+
supportedReasoningLevels: getClaudeCodeEffortLevels(["low", "medium", "high"]),
|
|
110
|
+
source: "fallback",
|
|
111
|
+
canonicalId: "claude-haiku-4-5",
|
|
112
|
+
family: "haiku",
|
|
113
|
+
effortSource: "fallback",
|
|
114
|
+
effortVerified: false,
|
|
115
|
+
},
|
|
116
|
+
] as const;
|
|
117
|
+
|
|
118
|
+
function isRuntimeSource(source: ProviderModel["source"]): boolean {
|
|
119
|
+
return source === "discovered" || source === "claude-code" || source === "settings" || source === "config";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function providerModelsToCodexCapabilities(
|
|
123
|
+
models: readonly ProviderModel[],
|
|
124
|
+
currentModel: string,
|
|
125
|
+
): CodexModelCapabilities {
|
|
126
|
+
const capabilities: readonly CodexModelCapability[] = models.map((model, index) => ({
|
|
127
|
+
id: model.id,
|
|
128
|
+
model: model.modelId,
|
|
129
|
+
label: model.label,
|
|
130
|
+
description: model.description,
|
|
131
|
+
available: true,
|
|
132
|
+
hidden: false,
|
|
133
|
+
isDefault: model.modelId === currentModel || (!currentModel && index === 0),
|
|
134
|
+
defaultReasoningLevel: model.defaultReasoningLevel,
|
|
135
|
+
supportedReasoningLevels: model.supportedReasoningLevels,
|
|
136
|
+
reasoningLevelCount: model.supportedReasoningLevels ? model.supportedReasoningLevels.length : null,
|
|
137
|
+
source: isRuntimeSource(model.source) ? "runtime" : "fallback",
|
|
138
|
+
raw: model,
|
|
139
|
+
}));
|
|
140
|
+
|
|
141
|
+
const anyDiscovered = models.some((m) => isRuntimeSource(m.source));
|
|
142
|
+
return {
|
|
143
|
+
status: "ready",
|
|
144
|
+
source: anyDiscovered ? "runtime" : "fallback",
|
|
145
|
+
models: capabilities,
|
|
146
|
+
discoveredAt: Date.now(),
|
|
147
|
+
executable: null,
|
|
148
|
+
error: null,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReasoningEffortCapability } from "../models/codexModelCapabilities.js";
|
|
2
|
+
|
|
3
|
+
export const CLAUDE_CODE_EFFORT_LEVELS: readonly ReasoningEffortCapability[] = [
|
|
4
|
+
{ id: "low", label: "Low", description: "Claude Code low effort." },
|
|
5
|
+
{ id: "medium", label: "Medium", description: "Claude Code medium effort." },
|
|
6
|
+
{ id: "high", label: "High", description: "Claude Code high effort." },
|
|
7
|
+
{ id: "xhigh", label: "XHigh", description: "Claude Code extra-high effort." },
|
|
8
|
+
{ id: "max", label: "Max", description: "Claude Code maximum effort." },
|
|
9
|
+
] as const;
|
|
10
|
+
|
|
11
|
+
export const CLAUDE_CODE_EFFORT_IDS = new Set(CLAUDE_CODE_EFFORT_LEVELS.map((level) => level.id));
|
|
12
|
+
|
|
13
|
+
export function getClaudeCodeEffortLevels(ids: readonly string[]): readonly ReasoningEffortCapability[] {
|
|
14
|
+
return ids
|
|
15
|
+
.map((id) => CLAUDE_CODE_EFFORT_LEVELS.find((level) => level.id === id))
|
|
16
|
+
.filter((level): level is ReasoningEffortCapability => Boolean(level));
|
|
17
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
discoverProviderModels,
|
|
5
|
+
getProviderRouteSetupMessage,
|
|
6
|
+
getProviderRuntime,
|
|
7
|
+
isProviderRouteConfigured,
|
|
8
|
+
resolveActiveProviderRoute,
|
|
9
|
+
} from "./registry.js";
|
|
10
|
+
import { resetGeminiRouteValidationCacheForTests } from "./gemini.js";
|
|
11
|
+
import { resetAnthropicRouteValidationCacheForTests } from "./anthropic.js";
|
|
12
|
+
import { checkLocalProvider, resetLocalProviderStateForTests } from "./local.js";
|
|
13
|
+
|
|
14
|
+
test("google runtime exposes configured Gemini models for in-Codexa routing", () => {
|
|
15
|
+
const runtime = getProviderRuntime("google");
|
|
16
|
+
const discovery = discoverProviderModels("google");
|
|
17
|
+
|
|
18
|
+
assert.equal(runtime.routeAvailable, true);
|
|
19
|
+
assert.equal(runtime.backendKind, "gemini-cli-auth");
|
|
20
|
+
assert.equal(discovery.status, "ready");
|
|
21
|
+
assert.ok(discovery.models.length > 0);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("anthropic runtime exposes configured Claude models for in-Codexa routing", () => {
|
|
25
|
+
const runtime = getProviderRuntime("anthropic");
|
|
26
|
+
const discovery = discoverProviderModels("anthropic");
|
|
27
|
+
|
|
28
|
+
assert.equal(runtime.routeAvailable, true);
|
|
29
|
+
assert.equal(runtime.backendKind, "claude-code-auth");
|
|
30
|
+
assert.equal(discovery.status, "ready");
|
|
31
|
+
assert.ok(discovery.models.length > 0);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("active route resolution preserves routable google routes", () => {
|
|
35
|
+
const route = resolveActiveProviderRoute({
|
|
36
|
+
workspaceConfigActiveRoute: {
|
|
37
|
+
providerId: "google",
|
|
38
|
+
modelId: "gemini-2.5-pro",
|
|
39
|
+
backendKind: "gemini-cli-auth",
|
|
40
|
+
reasoning: "medium",
|
|
41
|
+
},
|
|
42
|
+
currentModel: "gpt-5.4",
|
|
43
|
+
currentReasoning: "high",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
assert.deepEqual(route, {
|
|
47
|
+
providerId: "google",
|
|
48
|
+
modelId: "gemini-2.5-pro",
|
|
49
|
+
backendKind: "gemini-cli-auth",
|
|
50
|
+
reasoning: "medium",
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("active route resolution normalizes legacy Gemini 3 Flash routes to preview", () => {
|
|
55
|
+
const route = resolveActiveProviderRoute({
|
|
56
|
+
workspaceConfigActiveRoute: {
|
|
57
|
+
providerId: "google",
|
|
58
|
+
modelId: "gemini-3-flash",
|
|
59
|
+
backendKind: "gemini-cli-auth",
|
|
60
|
+
reasoning: "high",
|
|
61
|
+
},
|
|
62
|
+
currentModel: "gpt-5.4",
|
|
63
|
+
currentReasoning: "medium",
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
assert.deepEqual(route, {
|
|
67
|
+
providerId: "google",
|
|
68
|
+
modelId: "gemini-3-flash-preview",
|
|
69
|
+
backendKind: "gemini-cli-auth",
|
|
70
|
+
reasoning: "high",
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("active route resolution preserves routable anthropic routes", () => {
|
|
75
|
+
const route = resolveActiveProviderRoute({
|
|
76
|
+
workspaceConfigActiveRoute: {
|
|
77
|
+
providerId: "anthropic",
|
|
78
|
+
modelId: "claude-sonnet-4-20250514",
|
|
79
|
+
backendKind: "anthropic-api-key",
|
|
80
|
+
reasoning: "high",
|
|
81
|
+
},
|
|
82
|
+
currentModel: "gpt-5.4",
|
|
83
|
+
currentReasoning: "medium",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
assert.deepEqual(route, {
|
|
87
|
+
providerId: "anthropic",
|
|
88
|
+
modelId: "claude-sonnet-4-20250514",
|
|
89
|
+
backendKind: "anthropic-api-key",
|
|
90
|
+
reasoning: "high",
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("active route resolution preserves routable local routes", async () => {
|
|
95
|
+
resetLocalProviderStateForTests();
|
|
96
|
+
await checkLocalProvider({
|
|
97
|
+
fetchImpl: (async () => new Response(JSON.stringify({
|
|
98
|
+
data: [{ id: "llama-local" }],
|
|
99
|
+
}), { status: 200 })) as typeof fetch,
|
|
100
|
+
});
|
|
101
|
+
const route = resolveActiveProviderRoute({
|
|
102
|
+
workspaceConfigActiveRoute: {
|
|
103
|
+
providerId: "local",
|
|
104
|
+
modelId: "llama-local",
|
|
105
|
+
backendKind: "local-openai-compatible",
|
|
106
|
+
},
|
|
107
|
+
currentModel: "gpt-5.4",
|
|
108
|
+
currentReasoning: "high",
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
assert.deepEqual(route, {
|
|
112
|
+
providerId: "local",
|
|
113
|
+
modelId: "llama-local",
|
|
114
|
+
backendKind: "local-openai-compatible",
|
|
115
|
+
});
|
|
116
|
+
resetLocalProviderStateForTests();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ─── CLI --model override precedence ─────────────────────────────────────────
|
|
120
|
+
// These tests mirror the app-level logic: when --model is given on the CLI,
|
|
121
|
+
// the caller constructs effectiveRoute = { ...savedRoute, modelId: cliModel }
|
|
122
|
+
// before passing it to resolveActiveProviderRoute. The tests verify the resolver
|
|
123
|
+
// honours that override correctly.
|
|
124
|
+
|
|
125
|
+
test("CLI model override wins over persisted OpenAI activeRoute model", () => {
|
|
126
|
+
// Simulate: providers.json has gpt-5.4-mini, but user ran --model gpt-5.5
|
|
127
|
+
const cliModel = "gpt-5.5";
|
|
128
|
+
const savedRoute = {
|
|
129
|
+
providerId: "openai" as const,
|
|
130
|
+
modelId: "gpt-5.4-mini",
|
|
131
|
+
backendKind: "codex-cli-auth" as const,
|
|
132
|
+
reasoning: "low",
|
|
133
|
+
};
|
|
134
|
+
const effectiveRoute = { ...savedRoute, modelId: cliModel };
|
|
135
|
+
|
|
136
|
+
const route = resolveActiveProviderRoute({
|
|
137
|
+
workspaceConfigActiveRoute: effectiveRoute,
|
|
138
|
+
currentModel: "gpt-5.5",
|
|
139
|
+
currentReasoning: "low",
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
assert.equal(route.modelId, "gpt-5.5", "CLI model must be used for the run, not providers.json model");
|
|
143
|
+
assert.equal(route.providerId, "openai");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("without CLI model override the persisted providers.json activeRoute model is used", () => {
|
|
147
|
+
const savedRoute = {
|
|
148
|
+
providerId: "openai" as const,
|
|
149
|
+
modelId: "gpt-5.4-mini",
|
|
150
|
+
backendKind: "codex-cli-auth" as const,
|
|
151
|
+
reasoning: "low",
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const route = resolveActiveProviderRoute({
|
|
155
|
+
workspaceConfigActiveRoute: savedRoute,
|
|
156
|
+
currentModel: "gpt-5.4",
|
|
157
|
+
currentReasoning: "high",
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
assert.equal(route.modelId, "gpt-5.4-mini", "Without CLI override, persisted model must win over layered-config default");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("CLI model override preserves provider from providers.json activeRoute", () => {
|
|
164
|
+
// --model on the CLI should not change the provider, only the modelId
|
|
165
|
+
const cliModel = "gpt-5.5";
|
|
166
|
+
const savedRoute = {
|
|
167
|
+
providerId: "openai" as const,
|
|
168
|
+
modelId: "gpt-5.4-mini",
|
|
169
|
+
backendKind: "codex-cli-auth" as const,
|
|
170
|
+
reasoning: "low",
|
|
171
|
+
};
|
|
172
|
+
const effectiveRoute = { ...savedRoute, modelId: cliModel };
|
|
173
|
+
|
|
174
|
+
const route = resolveActiveProviderRoute({
|
|
175
|
+
workspaceConfigActiveRoute: effectiveRoute,
|
|
176
|
+
currentModel: cliModel,
|
|
177
|
+
currentReasoning: "low",
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
assert.equal(route.providerId, "openai", "Provider from providers.json must be preserved");
|
|
181
|
+
assert.equal(route.modelId, cliModel);
|
|
182
|
+
assert.equal(route.reasoning, "low", "Reasoning from providers.json must be preserved");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ─── Authentication and setup gates ──────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
test("anthropic route configuration is gated by ANTHROPIC_API_KEY or Claude Code", () => {
|
|
188
|
+
const original = process.env.ANTHROPIC_API_KEY;
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
delete process.env.ANTHROPIC_API_KEY;
|
|
192
|
+
resetAnthropicRouteValidationCacheForTests();
|
|
193
|
+
assert.equal(isProviderRouteConfigured("anthropic"), false);
|
|
194
|
+
assert.match(getProviderRouteSetupMessage("anthropic"), /set ANTHROPIC_API_KEY/);
|
|
195
|
+
} finally {
|
|
196
|
+
if (original) process.env.ANTHROPIC_API_KEY = original;
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("google route configuration is gated by Gemini API key or validated headless CLI", () => {
|
|
201
|
+
const originalGemini = process.env.GEMINI_API_KEY;
|
|
202
|
+
const originalGoogle = process.env.GOOGLE_API_KEY;
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
delete process.env.GEMINI_API_KEY;
|
|
206
|
+
delete process.env.GOOGLE_API_KEY;
|
|
207
|
+
resetGeminiRouteValidationCacheForTests();
|
|
208
|
+
assert.equal(isProviderRouteConfigured("google"), false);
|
|
209
|
+
assert.match(getProviderRouteSetupMessage("google"), /GEMINI_API_KEY \/ GOOGLE_API_KEY/);
|
|
210
|
+
} finally {
|
|
211
|
+
if (originalGemini) process.env.GEMINI_API_KEY = originalGemini;
|
|
212
|
+
if (originalGoogle) process.env.GOOGLE_API_KEY = originalGoogle;
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("local route configuration is gated by endpoint model discovery", async () => {
|
|
217
|
+
resetLocalProviderStateForTests();
|
|
218
|
+
assert.equal(isProviderRouteConfigured("local"), false);
|
|
219
|
+
|
|
220
|
+
await checkLocalProvider({
|
|
221
|
+
fetchImpl: (async (input) => {
|
|
222
|
+
if (String(input).includes("/api/v0/")) {
|
|
223
|
+
return new Response(null, { status: 404 });
|
|
224
|
+
}
|
|
225
|
+
return new Response(JSON.stringify({
|
|
226
|
+
data: [{ id: "google/gemma-4-26b-a4b" }],
|
|
227
|
+
}), { status: 200 });
|
|
228
|
+
}) as typeof fetch,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
assert.equal(isProviderRouteConfigured("local"), true);
|
|
232
|
+
resetLocalProviderStateForTests();
|
|
233
|
+
});
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { codexSubprocessProvider } from "../providers/codexSubprocess.js";
|
|
2
|
+
import type { BackendRunHandlers } from "../providers/types.js";
|
|
3
|
+
import type { ProviderId, ProviderActiveRoute, ProviderWorkspaceOverride } from "../providerLauncher/types.js";
|
|
4
|
+
import { anthropicRuntime } from "./anthropic.js";
|
|
5
|
+
import { geminiRuntime } from "./gemini.js";
|
|
6
|
+
import { localRuntime } from "./local.js";
|
|
7
|
+
import {
|
|
8
|
+
ANTHROPIC_FALLBACK_MODELS,
|
|
9
|
+
GEMINI_DEFAULT_MODEL_ID,
|
|
10
|
+
GEMINI_FALLBACK_MODELS,
|
|
11
|
+
normalizeGeminiModelId,
|
|
12
|
+
} from "./models.js";
|
|
13
|
+
import type {
|
|
14
|
+
ActiveProviderRoute,
|
|
15
|
+
GeminiModelSelection,
|
|
16
|
+
ProviderChatRequest,
|
|
17
|
+
ProviderModelDiscoveryResult,
|
|
18
|
+
ProviderRoute,
|
|
19
|
+
ProviderRouteValidationResult,
|
|
20
|
+
ProviderRuntime,
|
|
21
|
+
} from "./types.js";
|
|
22
|
+
|
|
23
|
+
const openAiRuntime: ProviderRuntime = {
|
|
24
|
+
providerId: "openai",
|
|
25
|
+
label: "OpenAI/Codex",
|
|
26
|
+
backendKind: "codex-cli-auth",
|
|
27
|
+
routeAvailable: true,
|
|
28
|
+
routeStatus: "Uses the configured Codex/OpenAI backend inside Codexa.",
|
|
29
|
+
launchAvailable: true,
|
|
30
|
+
discoverModels: () => ({
|
|
31
|
+
status: "ready",
|
|
32
|
+
providerId: "openai",
|
|
33
|
+
backendKind: "codex-cli-auth",
|
|
34
|
+
models: [],
|
|
35
|
+
}),
|
|
36
|
+
run: (request: ProviderChatRequest, handlers: BackendRunHandlers) => {
|
|
37
|
+
handlers.onProgress?.({
|
|
38
|
+
id: "openai-route",
|
|
39
|
+
source: "stdout",
|
|
40
|
+
text: "Starting Codex CLI",
|
|
41
|
+
});
|
|
42
|
+
return codexSubprocessProvider.run!(
|
|
43
|
+
request.prompt,
|
|
44
|
+
{
|
|
45
|
+
runtime: request.runtime,
|
|
46
|
+
workspaceRoot: request.workspaceRoot,
|
|
47
|
+
projectInstructions: request.projectInstructions,
|
|
48
|
+
},
|
|
49
|
+
handlers,
|
|
50
|
+
);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function unavailableRuntime(providerId: ProviderId, label: string): ProviderRuntime {
|
|
55
|
+
return {
|
|
56
|
+
providerId,
|
|
57
|
+
label,
|
|
58
|
+
backendKind: "unavailable",
|
|
59
|
+
routeAvailable: false,
|
|
60
|
+
routeStatus: `${label} is available as a launcher, but in-Codexa routing is not configured yet.`,
|
|
61
|
+
launchAvailable: providerId !== "local",
|
|
62
|
+
discoverModels: (): ProviderModelDiscoveryResult => ({
|
|
63
|
+
status: "not-configured",
|
|
64
|
+
providerId,
|
|
65
|
+
backendKind: "unavailable",
|
|
66
|
+
models: [],
|
|
67
|
+
message: `${label} is available as a launcher, but in-Codexa routing is not configured yet.`,
|
|
68
|
+
}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const PROVIDER_RUNTIMES: Record<ProviderId, ProviderRuntime> = {
|
|
73
|
+
openai: openAiRuntime,
|
|
74
|
+
anthropic: anthropicRuntime,
|
|
75
|
+
google: geminiRuntime,
|
|
76
|
+
local: localRuntime,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export function getProviderRuntime(providerId: ProviderId): ProviderRuntime {
|
|
80
|
+
return PROVIDER_RUNTIMES[providerId];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function isProviderRoutableInCodexa(providerId: ProviderId): boolean {
|
|
84
|
+
return getProviderRuntime(providerId).routeAvailable;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function isProviderRouteConfigured(providerId: ProviderId): boolean {
|
|
88
|
+
const runtime = getProviderRuntime(providerId);
|
|
89
|
+
return runtime.routeAvailable && (runtime.isRouteConfigured?.() ?? true);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function getProviderRouteSetupMessage(providerId: ProviderId): string {
|
|
93
|
+
const runtime = getProviderRuntime(providerId);
|
|
94
|
+
return runtime.routeSetupMessage ?? runtime.routeStatus;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function discoverProviderModels(providerId: ProviderId): ProviderModelDiscoveryResult {
|
|
98
|
+
return getProviderRuntime(providerId).discoverModels();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function validateProviderRouteActivation(options: {
|
|
102
|
+
route: ProviderRoute;
|
|
103
|
+
workspaceRoot: string;
|
|
104
|
+
geminiCommandPath?: string | null;
|
|
105
|
+
claudeCommandPath?: string | null;
|
|
106
|
+
localConfig?: ProviderWorkspaceOverride | null;
|
|
107
|
+
}): Promise<ProviderRouteValidationResult> {
|
|
108
|
+
const runtime = getProviderRuntime(options.route.providerId);
|
|
109
|
+
if (!runtime.routeAvailable) {
|
|
110
|
+
return {
|
|
111
|
+
status: "not-configured",
|
|
112
|
+
providerId: options.route.providerId,
|
|
113
|
+
backendKind: "unavailable",
|
|
114
|
+
message: getProviderRouteSetupMessage(options.route.providerId),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (runtime.validateRoute) {
|
|
119
|
+
return runtime.validateRoute(options);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!isProviderRouteConfigured(options.route.providerId)) {
|
|
123
|
+
return {
|
|
124
|
+
status: "not-configured",
|
|
125
|
+
providerId: options.route.providerId,
|
|
126
|
+
backendKind: "unavailable",
|
|
127
|
+
message: getProviderRouteSetupMessage(options.route.providerId),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
status: "ready",
|
|
133
|
+
providerId: options.route.providerId,
|
|
134
|
+
backendKind: runtime.backendKind,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function resolveGeminiModelId(selection: GeminiModelSelection): string {
|
|
139
|
+
if (selection.kind === "manual") {
|
|
140
|
+
return normalizeGeminiModelId(selection.modelId);
|
|
141
|
+
}
|
|
142
|
+
if (selection.family === "gemini-3") {
|
|
143
|
+
return "gemini-3-flash-preview";
|
|
144
|
+
}
|
|
145
|
+
if (selection.family === "gemini-2.5") {
|
|
146
|
+
return "gemini-2.5-pro";
|
|
147
|
+
}
|
|
148
|
+
return GEMINI_DEFAULT_MODEL_ID;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function resolveActiveProviderRoute(options: {
|
|
152
|
+
workspaceConfigActiveRoute?: ProviderActiveRoute;
|
|
153
|
+
currentModel: string;
|
|
154
|
+
currentReasoning: string;
|
|
155
|
+
}): ActiveProviderRoute {
|
|
156
|
+
const configuredRoute = options.workspaceConfigActiveRoute;
|
|
157
|
+
if (configuredRoute && isProviderRoutableInCodexa(configuredRoute.providerId)) {
|
|
158
|
+
const route: ActiveProviderRoute = {
|
|
159
|
+
providerId: configuredRoute.providerId,
|
|
160
|
+
modelId: configuredRoute.modelId,
|
|
161
|
+
backendKind: configuredRoute.backendKind ?? getProviderRuntime(configuredRoute.providerId).backendKind,
|
|
162
|
+
...(configuredRoute.reasoning ? { reasoning: configuredRoute.reasoning } : {}),
|
|
163
|
+
...(configuredRoute.modelSelection ? { modelSelection: configuredRoute.modelSelection } : {}),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
if (route.providerId === "google" && route.modelSelection) {
|
|
167
|
+
route.modelId = resolveGeminiModelId(route.modelSelection);
|
|
168
|
+
} else if (route.providerId === "google") {
|
|
169
|
+
route.modelId = normalizeGeminiModelId(route.modelId);
|
|
170
|
+
} else if (route.providerId === "local") {
|
|
171
|
+
const discovery = discoverProviderModels("local");
|
|
172
|
+
const selectedModel = typeof discovery.diagnostics?.selectedModel === "string"
|
|
173
|
+
? discovery.diagnostics.selectedModel.trim()
|
|
174
|
+
: "";
|
|
175
|
+
if (discovery.status === "ready" && selectedModel) {
|
|
176
|
+
route.modelId = selectedModel;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return route;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
providerId: "openai",
|
|
185
|
+
modelId: options.currentModel,
|
|
186
|
+
backendKind: "codex-cli-auth",
|
|
187
|
+
reasoning: options.currentReasoning,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function getDefaultRouteModel(providerId: ProviderId, currentOpenAiModel: string): string {
|
|
192
|
+
if (providerId === "anthropic") {
|
|
193
|
+
return ANTHROPIC_FALLBACK_MODELS[0]?.modelId ?? "claude-sonnet-4-20250514";
|
|
194
|
+
}
|
|
195
|
+
if (providerId === "google") {
|
|
196
|
+
return GEMINI_FALLBACK_MODELS[0]?.modelId ?? GEMINI_DEFAULT_MODEL_ID;
|
|
197
|
+
}
|
|
198
|
+
if (providerId === "local") {
|
|
199
|
+
const discovery = discoverProviderModels("local");
|
|
200
|
+
return discovery.models[0]?.modelId ?? "Local default";
|
|
201
|
+
}
|
|
202
|
+
return currentOpenAiModel;
|
|
203
|
+
}
|