@golba98/codexa 1.0.4 → 1.0.5
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/package.json +1 -1
- package/src/app.tsx +128 -47
- package/src/commands/handler.ts +5 -16
- package/src/config/buildInfo.ts +2 -2
- package/src/core/models/codexModelCapabilities.ts +57 -4
- package/src/core/models/codexModelsCacheSeed.ts +153 -0
- package/src/core/models/providerModelCache.ts +106 -0
- package/src/core/process/CommandRunner.ts +12 -1
- package/src/core/providerLauncher/registry.ts +37 -7
- package/src/core/providerLauncher/types.ts +2 -1
- package/src/core/providerRuntime/anthropic.ts +3 -5
- package/src/core/providerRuntime/antigravity.ts +246 -90
- package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -692
- package/src/core/providerRuntime/mistralVibe.ts +663 -0
- package/src/core/providerRuntime/models.ts +18 -4
- package/src/core/providerRuntime/reasoning.ts +9 -6
- package/src/core/providerRuntime/registry.ts +94 -49
- package/src/core/providerRuntime/types.ts +1 -0
- package/src/index.tsx +23 -9
- package/src/test/runtimeTestUtils.ts +90 -0
- package/src/ui/{ActivityBars.tsx → chrome/ActivityBars.tsx} +1 -1
- package/src/ui/{ActivityIndicator.tsx → chrome/ActivityIndicator.tsx} +3 -3
- package/src/ui/{AnimatedStatusText.tsx → chrome/AnimatedStatusText.tsx} +3 -3
- package/src/ui/{AppShell.tsx → chrome/AppShell.tsx} +11 -11
- package/src/ui/{BottomComposer.tsx → chrome/BottomComposer.tsx} +15 -15
- package/src/ui/{DashCard.tsx → chrome/DashCard.tsx} +2 -2
- package/src/ui/{RunFooter.tsx → chrome/RunFooter.tsx} +3 -3
- package/src/ui/{RuntimeStatusBar.tsx → chrome/RuntimeStatusBar.tsx} +3 -3
- package/src/ui/{Spinner.tsx → chrome/Spinner.tsx} +1 -1
- package/src/ui/{TopHeader.tsx → chrome/TopHeader.tsx} +12 -12
- package/src/ui/{UpdateAvailableCard.tsx → chrome/UpdateAvailableCard.tsx} +3 -3
- package/src/ui/{commandNormalize.ts → input/commandNormalize.ts} +1 -1
- package/src/ui/{focus.ts → input/focus.ts} +1 -1
- package/src/ui/{inputBuffer.ts → input/inputBuffer.ts} +3 -3
- package/src/ui/{AttachmentImportPanel.tsx → panels/AttachmentImportPanel.tsx} +1 -1
- package/src/ui/{AuthPanel.tsx → panels/AuthPanel.tsx} +5 -5
- package/src/ui/{BackendPicker.tsx → panels/BackendPicker.tsx} +2 -2
- package/src/ui/{ModePicker.tsx → panels/ModePicker.tsx} +2 -2
- package/src/ui/{ModelPicker.tsx → panels/ModelPicker.tsx} +2 -2
- package/src/ui/{ModelPickerScreen.tsx → panels/ModelPickerScreen.tsx} +11 -9
- package/src/ui/{ModelReasoningPicker.tsx → panels/ModelReasoningPicker.tsx} +5 -5
- package/src/ui/{Panel.tsx → panels/Panel.tsx} +1 -1
- package/src/ui/{PermissionsPanel.tsx → panels/PermissionsPanel.tsx} +3 -3
- package/src/ui/{PlanActionPicker.tsx → panels/PlanActionPicker.tsx} +2 -2
- package/src/ui/{PlanReviewPanel.tsx → panels/PlanReviewPanel.tsx} +5 -5
- package/src/ui/{ProviderPicker.tsx → panels/ProviderPicker.tsx} +9 -7
- package/src/ui/{ReasoningPicker.tsx → panels/ReasoningPicker.tsx} +3 -3
- package/src/ui/{SelectionPanel.tsx → panels/SelectionPanel.tsx} +1 -1
- package/src/ui/{SettingsPanel.tsx → panels/SettingsPanel.tsx} +3 -3
- package/src/ui/{TextEntryPanel.tsx → panels/TextEntryPanel.tsx} +3 -3
- package/src/ui/{ThemePicker.tsx → panels/ThemePicker.tsx} +2 -2
- package/src/ui/{UpdatePromptPanel.tsx → panels/UpdatePromptPanel.tsx} +2 -2
- package/src/ui/{Markdown.tsx → render/Markdown.tsx} +2 -2
- package/src/ui/{diffRenderer.ts → render/diffRenderer.ts} +1 -1
- package/src/ui/{modeDisplay.ts → render/modeDisplay.ts} +2 -2
- package/src/ui/{outputPipeline.ts → render/outputPipeline.ts} +2 -2
- package/src/ui/{runtimeDisplay.ts → render/runtimeDisplay.ts} +9 -8
- package/src/ui/{ActionRequiredBlock.tsx → timeline/ActionRequiredBlock.tsx} +3 -3
- package/src/ui/{AgentBlock.tsx → timeline/AgentBlock.tsx} +10 -10
- package/src/ui/{StaticIntroItem.tsx → timeline/StaticIntroItem.tsx} +2 -2
- package/src/ui/{ThinkingBlock.tsx → timeline/ThinkingBlock.tsx} +4 -4
- package/src/ui/{Timeline.tsx → timeline/Timeline.tsx} +8 -8
- package/src/ui/{TranscriptShell.tsx → timeline/TranscriptShell.tsx} +6 -6
- package/src/ui/{TurnGroup.tsx → timeline/TurnGroup.tsx} +15 -15
- package/src/ui/{progressEntries.ts → timeline/progressEntries.ts} +3 -3
- package/src/ui/{runActivityView.ts → timeline/runActivityView.ts} +1 -1
- package/src/ui/{timelineMeasure.ts → timeline/timelineMeasure.ts} +14 -14
- /package/src/ui/{busyStatusAnimation.ts → chrome/busyStatusAnimation.ts} +0 -0
- /package/src/ui/{slashCommands.ts → input/slashCommands.ts} +0 -0
- /package/src/ui/{logoVariants.ts → render/logoVariants.ts} +0 -0
- /package/src/ui/{terminalAnswerFormat.ts → render/terminalAnswerFormat.ts} +0 -0
- /package/src/ui/{textLayout.ts → render/textLayout.ts} +0 -0
- /package/src/ui/{layoutListWindow.ts → timeline/layoutListWindow.ts} +0 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { formatReasoningLabel } from "../../config/settings.js";
|
|
5
|
+
import type { ProviderModel } from "../providerRuntime/types.js";
|
|
6
|
+
import type {
|
|
7
|
+
CodexModelCapabilities,
|
|
8
|
+
CodexModelCapability,
|
|
9
|
+
ReasoningEffortCapability,
|
|
10
|
+
} from "./codexModelCapabilities.js";
|
|
11
|
+
import { loadCachedProviderModels, type CachedProviderModels } from "./providerModelCache.js";
|
|
12
|
+
|
|
13
|
+
// The codex CLI maintains its own model catalog cache with slugs, labels and
|
|
14
|
+
// reasoning levels. Reading it seeds Codexa's OpenAI model list instantly —
|
|
15
|
+
// no subprocess — and stays current because the codex CLI refreshes the file
|
|
16
|
+
// on its own runs.
|
|
17
|
+
// Resolved per call from env (matching claudeCodeDiscovery) so HOME
|
|
18
|
+
// redirection in tests holds — Bun's homedir() ignores runtime HOME changes.
|
|
19
|
+
export function getCodexModelsCacheFile(): string {
|
|
20
|
+
const home = process.env.USERPROFILE ?? process.env.HOME ?? homedir();
|
|
21
|
+
return join(home, ".codex", "models_cache.json");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface CodexSeed {
|
|
25
|
+
fetchedAt: number;
|
|
26
|
+
models: readonly ProviderModel[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
30
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function asString(value: unknown): string | null {
|
|
34
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseReasoningLevels(raw: unknown): readonly ReasoningEffortCapability[] | null {
|
|
38
|
+
if (!Array.isArray(raw)) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const levels = raw
|
|
42
|
+
.map((entry): ReasoningEffortCapability | null => {
|
|
43
|
+
if (!isRecord(entry)) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const id = asString(entry.effort);
|
|
47
|
+
if (!id) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return { id, label: formatReasoningLabel(id), description: asString(entry.description) };
|
|
51
|
+
})
|
|
52
|
+
.filter((entry): entry is ReasoningEffortCapability => entry !== null);
|
|
53
|
+
return levels.length > 0 ? levels : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseSeedModel(raw: unknown): ProviderModel | null {
|
|
57
|
+
if (!isRecord(raw)) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
// Models with visibility "hide" are internal codex routes (e.g. auto-review).
|
|
61
|
+
if (asString(raw.visibility) === "hide") {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const modelId = asString(raw.slug);
|
|
65
|
+
if (!modelId) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
id: modelId,
|
|
70
|
+
modelId,
|
|
71
|
+
label: asString(raw.display_name) ?? modelId,
|
|
72
|
+
description: asString(raw.description),
|
|
73
|
+
defaultReasoningLevel: asString(raw.default_reasoning_level),
|
|
74
|
+
supportedReasoningLevels: parseReasoningLevels(raw.supported_reasoning_levels),
|
|
75
|
+
source: "discovered",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function loadCodexSeedModels(cacheFile = getCodexModelsCacheFile()): CodexSeed | null {
|
|
80
|
+
try {
|
|
81
|
+
if (!existsSync(cacheFile)) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const parsed: unknown = JSON.parse(readFileSync(cacheFile, "utf8"));
|
|
85
|
+
if (!isRecord(parsed) || !Array.isArray(parsed.models)) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const models = parsed.models
|
|
89
|
+
.map(parseSeedModel)
|
|
90
|
+
.filter((model): model is ProviderModel => model !== null);
|
|
91
|
+
if (models.length === 0) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const fetchedAtRaw = asString(parsed.fetched_at);
|
|
95
|
+
const fetchedAt = fetchedAtRaw ? Date.parse(fetchedAtRaw) : Number.NaN;
|
|
96
|
+
return { fetchedAt: Number.isFinite(fetchedAt) ? fetchedAt : 0, models };
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Freshest locally known OpenAI models without spawning a subprocess:
|
|
103
|
+
// codex's own cache file vs Codexa's persisted last-good discovery.
|
|
104
|
+
export function loadSeededOpenAiModels(options: {
|
|
105
|
+
codexCacheFile?: string;
|
|
106
|
+
providerCacheFile?: string;
|
|
107
|
+
} = {}): CachedProviderModels | null {
|
|
108
|
+
const seed = loadCodexSeedModels(options.codexCacheFile);
|
|
109
|
+
const persisted = options.providerCacheFile === undefined
|
|
110
|
+
? loadCachedProviderModels("openai")
|
|
111
|
+
: loadCachedProviderModels("openai", options.providerCacheFile);
|
|
112
|
+
if (seed && (!persisted || seed.fetchedAt >= persisted.discoveredAt)) {
|
|
113
|
+
return { discoveredAt: seed.fetchedAt, models: seed.models };
|
|
114
|
+
}
|
|
115
|
+
return persisted;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function toCapability(model: ProviderModel, index: number): CodexModelCapability {
|
|
119
|
+
return {
|
|
120
|
+
id: model.id,
|
|
121
|
+
model: model.modelId,
|
|
122
|
+
label: model.label,
|
|
123
|
+
description: model.description,
|
|
124
|
+
available: true,
|
|
125
|
+
hidden: false,
|
|
126
|
+
isDefault: index === 0,
|
|
127
|
+
defaultReasoningLevel: model.defaultReasoningLevel,
|
|
128
|
+
supportedReasoningLevels: model.supportedReasoningLevels,
|
|
129
|
+
reasoningLevelCount: model.supportedReasoningLevels ? model.supportedReasoningLevels.length : null,
|
|
130
|
+
source: "runtime",
|
|
131
|
+
raw: model,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Capabilities for the OpenAI picker sourced purely from local caches.
|
|
136
|
+
// Returns null when nothing is cached yet (first ever launch).
|
|
137
|
+
export function loadSeededCodexCapabilities(options: {
|
|
138
|
+
codexCacheFile?: string;
|
|
139
|
+
providerCacheFile?: string;
|
|
140
|
+
} = {}): CodexModelCapabilities | null {
|
|
141
|
+
const seeded = loadSeededOpenAiModels(options);
|
|
142
|
+
if (!seeded) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
status: "ready",
|
|
147
|
+
source: "runtime",
|
|
148
|
+
models: seeded.models.map(toCapability),
|
|
149
|
+
discoveredAt: seeded.discoveredAt,
|
|
150
|
+
executable: null,
|
|
151
|
+
error: null,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { ProviderId } from "../providerLauncher/types.js";
|
|
5
|
+
import type { ProviderModel } from "../providerRuntime/types.js";
|
|
6
|
+
|
|
7
|
+
// Persistent last-good model discovery results, one entry per provider.
|
|
8
|
+
// Lets pickers open instantly with the previous session's discovered models
|
|
9
|
+
// while a background refresh runs. Corrupt or missing cache degrades to null.
|
|
10
|
+
// Resolved per call from env (matching claudeCodeDiscovery) so HOME
|
|
11
|
+
// redirection in tests holds — Bun's homedir() ignores runtime HOME changes.
|
|
12
|
+
export function getProviderModelCacheFile(): string {
|
|
13
|
+
const home = process.env.USERPROFILE ?? process.env.HOME ?? homedir();
|
|
14
|
+
return join(home, ".codexa-model-cache.json");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const CACHE_VERSION = 1;
|
|
18
|
+
|
|
19
|
+
export interface CachedProviderModels {
|
|
20
|
+
discoveredAt: number;
|
|
21
|
+
models: readonly ProviderModel[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ProviderModelCacheFile {
|
|
25
|
+
version: number;
|
|
26
|
+
providers: Partial<Record<ProviderId, CachedProviderModels>>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readCacheFile(cacheFile: string): ProviderModelCacheFile | null {
|
|
30
|
+
try {
|
|
31
|
+
if (!existsSync(cacheFile)) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const parsed: unknown = JSON.parse(readFileSync(cacheFile, "utf8"));
|
|
35
|
+
if (
|
|
36
|
+
typeof parsed !== "object" || parsed === null || Array.isArray(parsed)
|
|
37
|
+
|| (parsed as ProviderModelCacheFile).version !== CACHE_VERSION
|
|
38
|
+
|| typeof (parsed as ProviderModelCacheFile).providers !== "object"
|
|
39
|
+
|| (parsed as ProviderModelCacheFile).providers === null
|
|
40
|
+
) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return parsed as ProviderModelCacheFile;
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// A model id is passed straight to a provider CLI, so it must be a bare token.
|
|
50
|
+
// Discovery scrapes CLI output, and a buggy build can bake an ANSI escape into
|
|
51
|
+
// an id (e.g. "claude-fable-5\x1b[1m"). Reject rather than repair: a cached id
|
|
52
|
+
// we cannot vouch for is dropped so discovery re-probes for a clean one.
|
|
53
|
+
const VALID_MODEL_ID = /^[A-Za-z0-9._:\/-]+$/;
|
|
54
|
+
|
|
55
|
+
function isUsableModel(model: unknown): model is ProviderModel {
|
|
56
|
+
if (typeof model !== "object" || model === null) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
const candidate = model as ProviderModel;
|
|
60
|
+
return typeof candidate.id === "string"
|
|
61
|
+
&& typeof candidate.modelId === "string"
|
|
62
|
+
&& typeof candidate.label === "string"
|
|
63
|
+
&& VALID_MODEL_ID.test(candidate.id)
|
|
64
|
+
&& VALID_MODEL_ID.test(candidate.modelId);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isValidEntry(entry: unknown): entry is CachedProviderModels {
|
|
68
|
+
if (typeof entry !== "object" || entry === null) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const candidate = entry as CachedProviderModels;
|
|
72
|
+
return typeof candidate.discoveredAt === "number" && Array.isArray(candidate.models);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function loadCachedProviderModels(
|
|
76
|
+
providerId: ProviderId,
|
|
77
|
+
cacheFile = getProviderModelCacheFile(),
|
|
78
|
+
): CachedProviderModels | null {
|
|
79
|
+
const cache = readCacheFile(cacheFile);
|
|
80
|
+
const entry = cache?.providers?.[providerId];
|
|
81
|
+
if (!entry || !isValidEntry(entry)) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const models = entry.models.filter(isUsableModel);
|
|
85
|
+
if (models.length === 0) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
return { discoveredAt: entry.discoveredAt, models };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function saveCachedProviderModels(
|
|
92
|
+
providerId: ProviderId,
|
|
93
|
+
entry: CachedProviderModels,
|
|
94
|
+
cacheFile = getProviderModelCacheFile(),
|
|
95
|
+
): void {
|
|
96
|
+
if (entry.models.length === 0) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const cache = readCacheFile(cacheFile) ?? { version: CACHE_VERSION, providers: {} };
|
|
101
|
+
cache.providers[providerId] = entry;
|
|
102
|
+
writeFileSync(cacheFile, `${JSON.stringify(cache, null, 2)}\n`, "utf8");
|
|
103
|
+
} catch {
|
|
104
|
+
// Persistence is best-effort; discovery still works without it.
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -9,6 +9,7 @@ export interface CommandSpec {
|
|
|
9
9
|
cwd: string;
|
|
10
10
|
env?: NodeJS.ProcessEnv;
|
|
11
11
|
timeoutMs?: number;
|
|
12
|
+
stdinData?: string;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export interface CommandResult {
|
|
@@ -177,10 +178,20 @@ function runProcess(
|
|
|
177
178
|
cwd: spec.cwd,
|
|
178
179
|
env: spec.env,
|
|
179
180
|
shell: false,
|
|
180
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
181
|
+
stdio: [spec.stdinData !== undefined ? "pipe" : "ignore", "pipe", "pipe"],
|
|
181
182
|
});
|
|
182
183
|
handlers.onProcessLifecycle?.("spawned");
|
|
183
184
|
|
|
185
|
+
if (spec.stdinData !== undefined) {
|
|
186
|
+
try {
|
|
187
|
+
child.stdin?.on("error", () => { /* EPIPE when the process exits before reading stdin */ });
|
|
188
|
+
child.stdin?.write(spec.stdinData);
|
|
189
|
+
child.stdin?.end();
|
|
190
|
+
} catch {
|
|
191
|
+
// stdin already closed; the close/error handlers report the outcome
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
184
195
|
const result = new Promise<CommandResult>((resolve) => {
|
|
185
196
|
const finish = (partial: Omit<CommandResult, "stdout" | "stderr" | "startedAt" | "endedAt" | "durationMs" | "userMessage"> & { endedAt?: number }) => {
|
|
186
197
|
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
@@ -16,14 +16,15 @@ import {
|
|
|
16
16
|
} from "../providerRuntime/registry.js";
|
|
17
17
|
import { normalizeGeminiModelId } from "../providerRuntime/models.js";
|
|
18
18
|
import { ANTIGRAVITY_DEFAULT_MODEL_ID } from "../providerRuntime/antigravity.js";
|
|
19
|
+
import { discoverMistralVibeModels } from "../providerRuntime/mistralVibe.js";
|
|
19
20
|
import { setLocalProviderConfig } from "../providerRuntime/local.js";
|
|
20
21
|
import { formatContextLength, resolveModelContextLengthCached } from "../providerRuntime/contextMetadata.js";
|
|
21
22
|
import { resolveModelCapabilityProfileCached } from "../providerRuntime/capabilityProfile.js";
|
|
22
23
|
|
|
23
24
|
// Google/Gemini remains a recognized legacy config value so existing workspace
|
|
24
25
|
// files can be migrated, but it is no longer a selectable Codexa provider.
|
|
25
|
-
const PROVIDER_ORDER: readonly ProviderId[] = ["openai", "anthropic", "local", "antigravity"];
|
|
26
|
-
const KNOWN_PROVIDER_IDS: readonly ProviderId[] = ["openai", "anthropic", "google", "local", "antigravity"];
|
|
26
|
+
const PROVIDER_ORDER: readonly ProviderId[] = ["openai", "anthropic", "mistral", "local", "antigravity"];
|
|
27
|
+
const KNOWN_PROVIDER_IDS: readonly ProviderId[] = ["openai", "anthropic", "google", "mistral", "local", "antigravity"];
|
|
27
28
|
|
|
28
29
|
const DEFAULT_PROVIDER_ID: ProviderId = "openai";
|
|
29
30
|
|
|
@@ -78,6 +79,17 @@ const DEFAULT_PROVIDERS: Record<ProviderId, ProviderDefault> = {
|
|
|
78
79
|
isActiveRoute: false,
|
|
79
80
|
routeUnavailableReason: "Local provider unavailable. Start LM Studio, load a model, and enable the local server.",
|
|
80
81
|
},
|
|
82
|
+
mistral: {
|
|
83
|
+
id: "mistral",
|
|
84
|
+
displayName: "Mistral Vibe CLI",
|
|
85
|
+
currentModel: () => "Vibe default",
|
|
86
|
+
backendType: "mistral-vibe-cli-auth",
|
|
87
|
+
routeMode: "in-codexa",
|
|
88
|
+
enabled: true,
|
|
89
|
+
launchCommand: { executable: "vibe", args: [] },
|
|
90
|
+
isActiveRoute: false,
|
|
91
|
+
routeUnavailableReason: null,
|
|
92
|
+
},
|
|
81
93
|
antigravity: {
|
|
82
94
|
id: "antigravity",
|
|
83
95
|
displayName: "Antigravity",
|
|
@@ -137,9 +149,11 @@ function applyOverride(
|
|
|
137
149
|
: provider.currentModel,
|
|
138
150
|
enabled: nextEnabled,
|
|
139
151
|
launchCommand: nextCommand,
|
|
140
|
-
statusLabel: nextEnabled
|
|
141
|
-
?
|
|
142
|
-
: "
|
|
152
|
+
statusLabel: !nextEnabled
|
|
153
|
+
? "Disabled"
|
|
154
|
+
: provider.routeMode === "launch-only"
|
|
155
|
+
? provider.statusLabel
|
|
156
|
+
: (provider.routeUnavailableReason ? "Needs config" : "Enabled"),
|
|
143
157
|
};
|
|
144
158
|
}
|
|
145
159
|
|
|
@@ -157,6 +171,7 @@ export function getActiveRouteProviderId(config: ProviderWorkspaceConfig | null
|
|
|
157
171
|
|
|
158
172
|
export function buildProviderRegistry(options: {
|
|
159
173
|
activeModel: string;
|
|
174
|
+
workspaceRoot?: string;
|
|
160
175
|
workspaceConfig?: ProviderWorkspaceConfig | null;
|
|
161
176
|
diagnostics?: Record<string, Record<string, string | number | boolean | null>>;
|
|
162
177
|
routeErrors?: Record<string, string>;
|
|
@@ -170,7 +185,9 @@ export function buildProviderRegistry(options: {
|
|
|
170
185
|
}
|
|
171
186
|
const defaults = DEFAULT_PROVIDERS[id];
|
|
172
187
|
const runtime = getProviderRuntime(id);
|
|
173
|
-
const discovery =
|
|
188
|
+
const discovery = id === "mistral"
|
|
189
|
+
? discoverMistralVibeModels(options.workspaceRoot ?? process.cwd())
|
|
190
|
+
: runtime.discoverModels();
|
|
174
191
|
|
|
175
192
|
const activeRoute = options.workspaceConfig?.activeRoute;
|
|
176
193
|
const isThisActive = activeRoute?.providerId === id;
|
|
@@ -205,6 +222,10 @@ export function buildProviderRegistry(options: {
|
|
|
205
222
|
}
|
|
206
223
|
}
|
|
207
224
|
|
|
225
|
+
if (id === "mistral") {
|
|
226
|
+
currentModelLabel = discovery.models[0]?.modelId ?? "Vibe default";
|
|
227
|
+
}
|
|
228
|
+
|
|
208
229
|
const rawMetadataForModel = discovery.models.find((model) => model.modelId === currentModelLabel)?.raw;
|
|
209
230
|
const contextMetadata = resolveModelContextLengthCached({
|
|
210
231
|
providerId: id,
|
|
@@ -230,7 +251,16 @@ export function buildProviderRegistry(options: {
|
|
|
230
251
|
|
|
231
252
|
const enabled = id === "local" ? discovery.status === "ready" : defaults.enabled;
|
|
232
253
|
|
|
233
|
-
const
|
|
254
|
+
const availabilityStatus = options.diagnostics?.[id]?.availabilityStatus;
|
|
255
|
+
const statusLabel = id === "mistral"
|
|
256
|
+
? availabilityStatus === "checking"
|
|
257
|
+
? "Checking"
|
|
258
|
+
: availabilityStatus === "unavailable"
|
|
259
|
+
? "Missing"
|
|
260
|
+
: routeUnavailableReason
|
|
261
|
+
? "Needs config"
|
|
262
|
+
: "Enabled"
|
|
263
|
+
: id === "local"
|
|
234
264
|
? (discovery.status === "ready" ? "Enabled" : "Disabled")
|
|
235
265
|
: !defaults.enabled
|
|
236
266
|
? "Disabled"
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
export type ProviderId = "openai" | "anthropic" | "google" | "local" | "antigravity";
|
|
1
|
+
export type ProviderId = "openai" | "anthropic" | "google" | "mistral" | "local" | "antigravity";
|
|
2
2
|
|
|
3
3
|
export type ProviderBackendType =
|
|
4
4
|
| "codex-cli-auth"
|
|
5
5
|
| "gemini-cli-auth"
|
|
6
6
|
| "claude-code-auth"
|
|
7
|
+
| "mistral-vibe-cli-auth"
|
|
7
8
|
| "antigravity-cli-auth"
|
|
8
9
|
| "openai-api-key"
|
|
9
10
|
| "gemini-api-key"
|
|
@@ -4,7 +4,6 @@ import type { BackendRunHandlers } from "../providers/types.js";
|
|
|
4
4
|
import { ANTHROPIC_FALLBACK_MODELS } from "./models.js";
|
|
5
5
|
import type { ProviderBackendKind, ProviderChatRequest, ProviderModel, ProviderModelDiscoveryResult, ProviderRouteValidationResult, ProviderRuntime } from "./types.js";
|
|
6
6
|
import { buildClaudeSpawnSpec, resetClaudeExecutableCacheForTests } from "../executables/claudeExecutable.js";
|
|
7
|
-
import { CLAUDE_CODE_EFFORT_IDS } from "./reasoning.js";
|
|
8
7
|
import {
|
|
9
8
|
claudeCodeModelsToProviderModels,
|
|
10
9
|
discoverClaudeCodeCapabilities,
|
|
@@ -56,10 +55,9 @@ export function mapModelIdToClaudeArg(modelId: string): string {
|
|
|
56
55
|
return modelId;
|
|
57
56
|
}
|
|
58
57
|
|
|
59
|
-
export function mapReasoningToEffort(reasoning: string | null | undefined): string | null {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
58
|
+
export function mapReasoningToEffort(reasoning: string | null | undefined): string | null {
|
|
59
|
+
return reasoning?.trim() || null;
|
|
60
|
+
}
|
|
63
61
|
|
|
64
62
|
function buildClaudeCodeBaseArgs(request: ProviderChatRequest): string[] {
|
|
65
63
|
const effort = mapReasoningToEffort(request.route.reasoning ?? null);
|