march-cli 0.1.4 → 0.1.6
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/agent/runner.mjs +6 -6
- package/src/agent/runtime/runner-runtime-host.mjs +3 -0
- package/src/agent/turn/turn-runner.mjs +5 -158
- package/src/auth/storage.mjs +3 -2
- package/src/cli/commands/model-command.mjs +40 -9
- package/src/cli/commands/provider-command.mjs +4 -0
- package/src/cli/tui/tui-handlers.mjs +7 -6
- package/src/provider/custom-provider.mjs +112 -0
- package/src/provider/presets.mjs +2 -2
- package/src/supergrok/provider.mjs +2 -2
package/package.json
CHANGED
package/src/agent/runner.mjs
CHANGED
|
@@ -18,16 +18,17 @@ import { resolveRunnerSessionOptions } from "./session/session-options.mjs";
|
|
|
18
18
|
import { createSessionBinding } from "./session/session-binding.mjs";
|
|
19
19
|
import { maybeAutoNameSession } from "./session/session-auto-name.mjs";
|
|
20
20
|
import { MARCH_BASE_TOOL_NAMES } from "./tool-names.mjs";
|
|
21
|
-
import {
|
|
21
|
+
import { runRunnerTurn } from "./turn/turn-runner.mjs";
|
|
22
22
|
import { appendFastVariants, createFastModelEntry, fromFastEntryModel, isFastProvider } from "./runner/fast-model.mjs";
|
|
23
23
|
import { registerSuperGrokProvider } from "../supergrok/provider.mjs";
|
|
24
|
+
import { registerCustomProviders } from "../provider/custom-provider.mjs";
|
|
24
25
|
|
|
25
26
|
export { MARCH_BASE_TOOL_NAMES };
|
|
26
27
|
export { installModelPayloadDumper } from "./model-payload-dumper.mjs";
|
|
27
28
|
export { createDefaultSessionManager, resolveRunnerSessionManager } from "./runner/runner-init.mjs";
|
|
28
29
|
export { getRunnerSessionStats, syncEngineSessionState } from "./runner/runner-session-state.mjs";
|
|
29
30
|
|
|
30
|
-
export async function createRunner({ cwd, modelId = null, provider = null, providers = {}, stateRoot, ui, memoryRoot = null, centerMemoryPath = null, memoryStore = null, memoryTools = [], shellRuntime = null, mcpTools = [], mcpInjections = [], mcpClientManager = null, webTools = [], namespace = "", sessionManager = null, useRuntimeHost = false, projectMarchDir = null, syncPiSidecar = false, extensionPaths = [], lifecycleHooks = [], lifecycleDiagnostics = [], authStorage = null, permissionController = null, modelContextDumper = null, turnNotifier = null, onModelPayload = null, createAgentSessionImpl = createAgentSession, createAgentSessionRuntimeImpl, createRuntimeServices, createRuntimeSessionFromServices, maxTurns, trimBatch, serviceTier = null
|
|
31
|
+
export async function createRunner({ cwd, modelId = null, provider = null, providers = {}, stateRoot, ui, memoryRoot = null, centerMemoryPath = null, memoryStore = null, memoryTools = [], shellRuntime = null, mcpTools = [], mcpInjections = [], mcpClientManager = null, webTools = [], namespace = "", sessionManager = null, useRuntimeHost = false, projectMarchDir = null, syncPiSidecar = false, extensionPaths = [], lifecycleHooks = [], lifecycleDiagnostics = [], authStorage = null, permissionController = null, modelContextDumper = null, turnNotifier = null, onModelPayload = null, createAgentSessionImpl = createAgentSession, createAgentSessionRuntimeImpl, createRuntimeServices, createRuntimeSessionFromServices, maxTurns, trimBatch, serviceTier = null }) {
|
|
31
32
|
if (!useRuntimeHost && extensionPaths.length > 0) {
|
|
32
33
|
throw new Error("--extension requires the default pi runtime host path");
|
|
33
34
|
}
|
|
@@ -38,6 +39,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
38
39
|
const resolvedAuth = authConfig.authStorage;
|
|
39
40
|
const modelRegistry = ModelRegistry.create(resolvedAuth);
|
|
40
41
|
registerSuperGrokProvider(modelRegistry);
|
|
42
|
+
registerCustomProviders(modelRegistry, providers);
|
|
41
43
|
const selectedModel = resolveInitialModel({ modelRegistry, provider, modelId });
|
|
42
44
|
if (!selectedModel) throw new Error("No authenticated models available. Run: march provider --config");
|
|
43
45
|
provider = selectedModel.provider;
|
|
@@ -63,6 +65,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
63
65
|
runtimeHost = await createRunnerRuntimeHost({
|
|
64
66
|
cwd, stateRoot, provider, modelId,
|
|
65
67
|
authStorage: resolvedAuth, settingsManager, modelRegistry,
|
|
68
|
+
providers,
|
|
66
69
|
sessionManager: resolvedSessionManager, sessionBinding, engine, ui,
|
|
67
70
|
projectMarchDir,
|
|
68
71
|
memoryTools, memoryStore, shellRuntime, lspService, mcpTools, webTools,
|
|
@@ -113,7 +116,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
113
116
|
const turnStartedAt = Date.now();
|
|
114
117
|
try {
|
|
115
118
|
const result = await runRunnerTurn({
|
|
116
|
-
prompt, userMessage, options: { userRecallHints, currentProject
|
|
119
|
+
prompt, userMessage, options: { userRecallHints, currentProject },
|
|
117
120
|
sessionBinding, engine, ui, projectMarchDir, memoryStore,
|
|
118
121
|
setModelCallKind: (kind) => { currentModelCallKind = kind; },
|
|
119
122
|
onMidTurnRecallHints: (hints) => { pendingMidTurnRecallHints.push(...hints); },
|
|
@@ -129,9 +132,6 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
129
132
|
});
|
|
130
133
|
return result;
|
|
131
134
|
} catch (err) {
|
|
132
|
-
if (isModelStreamIdleTimeoutError(err)) {
|
|
133
|
-
nextTurnContextMode = "continueExistingPiTranscript";
|
|
134
|
-
}
|
|
135
135
|
lastNotificationResult = await notifyTurnEndBestEffort(turnNotifier, {
|
|
136
136
|
status: "error",
|
|
137
137
|
sessionName: engine.sessionName,
|
|
@@ -3,6 +3,7 @@ import { createMarchRuntimeFactory } from "./runtime-factory.mjs";
|
|
|
3
3
|
import { createRuntimeHost } from "./runtime-host.mjs";
|
|
4
4
|
import { resolveRunnerSessionOptions } from "../session/session-options.mjs";
|
|
5
5
|
import { registerSuperGrokProvider } from "../../supergrok/provider.mjs";
|
|
6
|
+
import { registerCustomProviders } from "../../provider/custom-provider.mjs";
|
|
6
7
|
|
|
7
8
|
export async function createRunnerRuntimeHost({
|
|
8
9
|
cwd,
|
|
@@ -12,6 +13,7 @@ export async function createRunnerRuntimeHost({
|
|
|
12
13
|
authStorage,
|
|
13
14
|
settingsManager,
|
|
14
15
|
modelRegistry,
|
|
16
|
+
providers = {},
|
|
15
17
|
sessionManager,
|
|
16
18
|
sessionBinding,
|
|
17
19
|
engine,
|
|
@@ -42,6 +44,7 @@ export async function createRunnerRuntimeHost({
|
|
|
42
44
|
resolveSessionOptions: ({ cwd: sessionCwd, services }) => {
|
|
43
45
|
const activeModelRegistry = services.modelRegistry ?? modelRegistry;
|
|
44
46
|
registerSuperGrokProvider(activeModelRegistry);
|
|
47
|
+
registerCustomProviders(activeModelRegistry, providers);
|
|
45
48
|
return resolveRunnerSessionOptions({
|
|
46
49
|
cwd: sessionCwd,
|
|
47
50
|
provider,
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { resolveImageAttachmentReferences } from "../../session/attachment-references.mjs";
|
|
2
2
|
import { closeAssistantReply, createTurnEventState, handleRunnerSessionEvent } from "./turn-events.mjs";
|
|
3
3
|
|
|
4
|
-
export function isModelStreamIdleTimeoutError(err) {
|
|
5
|
-
return err?.code === "MODEL_STREAM_IDLE_TIMEOUT";
|
|
6
|
-
}
|
|
7
|
-
|
|
8
4
|
export async function runRunnerTurn({
|
|
9
5
|
prompt,
|
|
10
6
|
userMessage,
|
|
@@ -23,21 +19,13 @@ export async function runRunnerTurn({
|
|
|
23
19
|
const {
|
|
24
20
|
userRecallHints = [],
|
|
25
21
|
currentProject = "",
|
|
26
|
-
modelStreamIdleTimeoutMs = 7000,
|
|
27
|
-
modelStreamIdleMaxRetries = 5,
|
|
28
22
|
} = options;
|
|
29
23
|
const activeSession = sessionBinding.get();
|
|
30
24
|
const turnState = createTurnEventState();
|
|
31
25
|
const midTurnRecallHints = [];
|
|
32
|
-
const modelIdleWatchdog = createModelStreamIdleWatchdog({
|
|
33
|
-
session: activeSession,
|
|
34
|
-
ui,
|
|
35
|
-
timeoutMs: modelStreamIdleTimeoutMs,
|
|
36
|
-
});
|
|
37
26
|
ui.turnStart();
|
|
38
27
|
|
|
39
28
|
const unsubscribe = activeSession.subscribe((event) => {
|
|
40
|
-
modelIdleWatchdog.handleEvent(event);
|
|
41
29
|
handleRunnerSessionEvent(event, { ui, engine, state: turnState });
|
|
42
30
|
if (event.type === "tool_execution_start") {
|
|
43
31
|
const hints = flushAssistantRecall({ memoryStore, engine, turnState, currentProject });
|
|
@@ -56,32 +44,12 @@ export async function runRunnerTurn({
|
|
|
56
44
|
});
|
|
57
45
|
setModelCallKind("user");
|
|
58
46
|
try {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
maxRetries: modelStreamIdleMaxRetries,
|
|
65
|
-
watchdog: modelIdleWatchdog,
|
|
66
|
-
});
|
|
67
|
-
} catch (err) {
|
|
68
|
-
if (!isModelStreamIdleTimeoutError(err)) throw err;
|
|
69
|
-
finalizeTurn({
|
|
70
|
-
prompt,
|
|
71
|
-
userMessage,
|
|
72
|
-
userRecallHints,
|
|
73
|
-
currentProject,
|
|
74
|
-
memoryStore,
|
|
75
|
-
engine,
|
|
76
|
-
ui,
|
|
77
|
-
turnState,
|
|
78
|
-
midTurnRecallHints,
|
|
79
|
-
syncCurrentPiSidecar,
|
|
80
|
-
autoNameSession,
|
|
81
|
-
});
|
|
82
|
-
throw err;
|
|
47
|
+
if (contextMode === "rebuild") resetPiMessageHistory(activeSession);
|
|
48
|
+
await activeSession.prompt(
|
|
49
|
+
contextMode === "continueExistingPiTranscript" ? (userMessage ?? prompt) : prompt,
|
|
50
|
+
attachmentReferences.images.length > 0 ? { images: attachmentReferences.images } : undefined,
|
|
51
|
+
);
|
|
83
52
|
} finally {
|
|
84
|
-
modelIdleWatchdog.stop();
|
|
85
53
|
setModelCallKind("model");
|
|
86
54
|
}
|
|
87
55
|
|
|
@@ -105,38 +73,6 @@ export async function runRunnerTurn({
|
|
|
105
73
|
}
|
|
106
74
|
}
|
|
107
75
|
|
|
108
|
-
async function promptWithModelIdleRetry({ session, prompt, promptOptions, resetBeforeAttempt, maxRetries, watchdog }) {
|
|
109
|
-
let attempt = 0;
|
|
110
|
-
while (true) {
|
|
111
|
-
attempt += 1;
|
|
112
|
-
if (resetBeforeAttempt) resetPiMessageHistory(session);
|
|
113
|
-
const messageHistorySnapshot = clonePiMessageHistory(session);
|
|
114
|
-
watchdog.startAttempt({ attempt, maxAttempts: maxRetries + 1 });
|
|
115
|
-
let idleTimedOut = false;
|
|
116
|
-
try {
|
|
117
|
-
await session.prompt(prompt, promptOptions);
|
|
118
|
-
idleTimedOut = watchdog.consumeTimedOut();
|
|
119
|
-
if (!idleTimedOut) {
|
|
120
|
-
if (attempt > 1) watchdog.reportRecovered({ attempt: attempt - 1 });
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
} catch (err) {
|
|
124
|
-
idleTimedOut = watchdog.consumeTimedOut();
|
|
125
|
-
if (!idleTimedOut) throw err;
|
|
126
|
-
} finally {
|
|
127
|
-
watchdog.stop();
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (attempt > maxRetries) {
|
|
131
|
-
const err = createModelStreamIdleTimeoutError(watchdog.timeoutMs);
|
|
132
|
-
watchdog.reportExhausted({ attempt, error: err });
|
|
133
|
-
throw err;
|
|
134
|
-
}
|
|
135
|
-
restorePiMessageHistory(session, messageHistorySnapshot);
|
|
136
|
-
watchdog.reportRetry({ attempt, maxAttempts: maxRetries + 1 });
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
76
|
function finalizeTurn({ prompt, userMessage, userRecallHints, currentProject, memoryStore, engine, ui, turnState, midTurnRecallHints, syncCurrentPiSidecar, autoNameSession }) {
|
|
141
77
|
closeAssistantReply({ ui, state: turnState });
|
|
142
78
|
const assistantRecallHints = flushAssistantRecall({ memoryStore, engine, turnState, currentProject });
|
|
@@ -154,95 +90,6 @@ function finalizeTurn({ prompt, userMessage, userRecallHints, currentProject, me
|
|
|
154
90
|
syncCurrentPiSidecar();
|
|
155
91
|
}
|
|
156
92
|
|
|
157
|
-
function createModelStreamIdleTimeoutError(timeoutMs) {
|
|
158
|
-
const err = new Error(`Model stream idle for ${timeoutMs}ms`);
|
|
159
|
-
err.code = "MODEL_STREAM_IDLE_TIMEOUT";
|
|
160
|
-
return err;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function createModelStreamIdleWatchdog({ session, ui, timeoutMs }) {
|
|
164
|
-
let timer = null;
|
|
165
|
-
let active = false;
|
|
166
|
-
let inTool = false;
|
|
167
|
-
let timedOut = false;
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
timeoutMs,
|
|
171
|
-
startAttempt() {
|
|
172
|
-
timedOut = false;
|
|
173
|
-
inTool = false;
|
|
174
|
-
active = true;
|
|
175
|
-
arm();
|
|
176
|
-
},
|
|
177
|
-
handleEvent(event) {
|
|
178
|
-
if (!active || timedOut) return;
|
|
179
|
-
if (event.type === "tool_execution_start") {
|
|
180
|
-
inTool = true;
|
|
181
|
-
clear();
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
if (event.type === "tool_execution_end") {
|
|
185
|
-
inTool = false;
|
|
186
|
-
arm();
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
if (event.type === "message_update") arm();
|
|
190
|
-
},
|
|
191
|
-
stop() {
|
|
192
|
-
active = false;
|
|
193
|
-
clear();
|
|
194
|
-
},
|
|
195
|
-
consumeTimedOut() {
|
|
196
|
-
const value = timedOut;
|
|
197
|
-
timedOut = false;
|
|
198
|
-
return value;
|
|
199
|
-
},
|
|
200
|
-
reportRetry({ attempt, maxAttempts }) {
|
|
201
|
-
ui.retryStart?.({
|
|
202
|
-
attempt,
|
|
203
|
-
maxAttempts,
|
|
204
|
-
delayMs: 0,
|
|
205
|
-
errorMessage: `Model stream idle for ${timeoutMs}ms; retrying request`,
|
|
206
|
-
});
|
|
207
|
-
},
|
|
208
|
-
reportRecovered({ attempt }) {
|
|
209
|
-
ui.retryEnd?.({ success: true, attempt });
|
|
210
|
-
},
|
|
211
|
-
reportExhausted({ attempt, error }) {
|
|
212
|
-
ui.retryEnd?.({ success: false, attempt, finalError: error.message });
|
|
213
|
-
},
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
function arm() {
|
|
217
|
-
clear();
|
|
218
|
-
if (!active || inTool || timeoutMs <= 0) return;
|
|
219
|
-
timer = setTimeout(() => {
|
|
220
|
-
timedOut = true;
|
|
221
|
-
active = false;
|
|
222
|
-
clear();
|
|
223
|
-
session.abortRetry?.();
|
|
224
|
-
session.abort();
|
|
225
|
-
}, timeoutMs);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function clear() {
|
|
229
|
-
if (!timer) return;
|
|
230
|
-
clearTimeout(timer);
|
|
231
|
-
timer = null;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function clonePiMessageHistory(session) {
|
|
236
|
-
const messages = session?.agent?.state?.messages;
|
|
237
|
-
if (!Array.isArray(messages)) return null;
|
|
238
|
-
return JSON.parse(JSON.stringify(messages));
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function restorePiMessageHistory(session, messages) {
|
|
242
|
-
if (!Array.isArray(session?.agent?.state?.messages) || !Array.isArray(messages)) return;
|
|
243
|
-
session.agent.state.messages = JSON.parse(JSON.stringify(messages));
|
|
244
|
-
}
|
|
245
|
-
|
|
246
93
|
function flushAssistantRecall({ memoryStore, engine, turnState, currentProject }) {
|
|
247
94
|
if (!memoryStore) return [];
|
|
248
95
|
const text = assistantRecallDeltaText(turnState);
|
package/src/auth/storage.mjs
CHANGED
|
@@ -13,11 +13,12 @@ export function createMarchAuthStorage({
|
|
|
13
13
|
} = {}) {
|
|
14
14
|
const resolvedAuthStorage = authStorage ?? AuthStorage.create(getMarchAuthPath(homeDir));
|
|
15
15
|
|
|
16
|
-
for (const profile of Object.
|
|
16
|
+
for (const [id, profile] of Object.entries(providers ?? {})) {
|
|
17
17
|
if (!profile || typeof profile !== "object") continue;
|
|
18
18
|
const type = profile.type ?? profile.provider;
|
|
19
|
+
const providerKey = type === "openai-compatible" ? id : type;
|
|
19
20
|
const profileKey = profile.auth?.method === "apiKey" ? profile.auth?.apiKey : null;
|
|
20
|
-
if (
|
|
21
|
+
if (providerKey && profileKey) resolvedAuthStorage.setRuntimeApiKey(providerKey, profileKey);
|
|
21
22
|
}
|
|
22
23
|
const hasStoredAuth = Boolean(resolvedAuthStorage.list?.().length);
|
|
23
24
|
const hasConfiguredProvider = Object.values(providers ?? {}).some((profile) => {
|
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
import { getProviderLabel } from "../../provider/presets.mjs";
|
|
2
2
|
import { globalConfigJsonPath, upsertModelSelection } from "../../config/config-json.mjs";
|
|
3
3
|
|
|
4
|
+
// Deduplicate models by id, preferring canonical providers.
|
|
5
|
+
// When the same model id appears under multiple providers (e.g. supergrok-oauth
|
|
6
|
+
// and xai-oauth), keep only the entry with the preferred provider.
|
|
7
|
+
const PREFERRED_PROVIDERS = ["supergrok-oauth"];
|
|
8
|
+
|
|
9
|
+
function dedupByModelId(scopedModels) {
|
|
10
|
+
const seen = new Map();
|
|
11
|
+
const result = [];
|
|
12
|
+
for (const entry of scopedModels) {
|
|
13
|
+
const { model } = entry;
|
|
14
|
+
const existing = seen.get(model.id);
|
|
15
|
+
if (!existing) {
|
|
16
|
+
seen.set(model.id, entry);
|
|
17
|
+
result.push(entry);
|
|
18
|
+
} else {
|
|
19
|
+
// Prefer canonical provider when duplicates exist
|
|
20
|
+
const existingPref = PREFERRED_PROVIDERS.indexOf(existing.model.provider);
|
|
21
|
+
const currentPref = PREFERRED_PROVIDERS.indexOf(model.provider);
|
|
22
|
+
if (currentPref !== -1 && (existingPref === -1 || currentPref < existingPref)) {
|
|
23
|
+
const idx = result.indexOf(existing);
|
|
24
|
+
result[idx] = entry;
|
|
25
|
+
seen.set(model.id, entry);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
|
|
4
32
|
export function parseModelCommand(input) {
|
|
5
33
|
if (input !== "/model" && !input.startsWith("/model ")) {
|
|
6
34
|
return { type: "none" };
|
|
@@ -21,9 +49,10 @@ export async function selectModelByIndex(index, { runner }) {
|
|
|
21
49
|
}
|
|
22
50
|
|
|
23
51
|
export function buildModelSelectItems({ current, scopedModels = [] }) {
|
|
24
|
-
|
|
52
|
+
const deduped = dedupByModelId(scopedModels);
|
|
53
|
+
return deduped.map(({ model }, index) => ({
|
|
25
54
|
value: String(index),
|
|
26
|
-
label: `${
|
|
55
|
+
label: `${model.name || model.id} (${getProviderLabel(model.provider)})`,
|
|
27
56
|
description: current && model.id === current.id && model.provider === current.provider ? "current" : model.provider,
|
|
28
57
|
model,
|
|
29
58
|
}));
|
|
@@ -34,19 +63,20 @@ export async function handleModelCommand(parsed, { runner, ui = null, configHome
|
|
|
34
63
|
const scopedModels = runner.getScopedModels?.() || [];
|
|
35
64
|
if (!ui?.selectList || scopedModels.length === 0) return "Use Ctrl+L to choose a model.";
|
|
36
65
|
const current = runner.getCurrentModel?.();
|
|
37
|
-
const
|
|
38
|
-
|
|
66
|
+
const items = buildModelSelectItems({ current, scopedModels });
|
|
67
|
+
const selectedIndex = Math.max(0, items.findIndex((item) =>
|
|
68
|
+
current && item.model.id === current.id && item.model.provider === current.provider
|
|
39
69
|
));
|
|
40
|
-
const
|
|
41
|
-
items
|
|
70
|
+
const selectedItem = await ui.selectList({
|
|
71
|
+
items,
|
|
42
72
|
selectedIndex,
|
|
43
73
|
width: 72,
|
|
44
74
|
suppressInitialConfirm: true,
|
|
45
75
|
searchable: true,
|
|
46
76
|
getSearchText: modelSelectSearchText,
|
|
47
77
|
});
|
|
48
|
-
if (!
|
|
49
|
-
const model = await runner.setModel(
|
|
78
|
+
if (!selectedItem) return "Model unchanged.";
|
|
79
|
+
const model = await runner.setModel(selectedItem.model);
|
|
50
80
|
persistModelSelection(model, { configHomeDir });
|
|
51
81
|
return `Model: ${model.name || model.id} (${model.provider})`;
|
|
52
82
|
}
|
|
@@ -85,8 +115,9 @@ export function formatModelsList({ current, scopedModels = [] }) {
|
|
|
85
115
|
}
|
|
86
116
|
|
|
87
117
|
function formatGroupedModels({ current, scopedModels }) {
|
|
118
|
+
const deduped = dedupByModelId(scopedModels);
|
|
88
119
|
const groups = new Map();
|
|
89
|
-
for (const item of
|
|
120
|
+
for (const item of deduped) {
|
|
90
121
|
const provider = item.model.provider;
|
|
91
122
|
if (!groups.has(provider)) groups.set(provider, []);
|
|
92
123
|
groups.get(provider).push(item);
|
|
@@ -11,8 +11,12 @@ export function parseProviderCommand(input) {
|
|
|
11
11
|
|
|
12
12
|
export function listProviders({ runner }) {
|
|
13
13
|
const scopedModels = runner.getScopedModels?.() || [];
|
|
14
|
+
const seen = new Set();
|
|
14
15
|
const providers = new Map();
|
|
15
16
|
for (const { model } of scopedModels) {
|
|
17
|
+
// Deduplicate: skip models that differ only by provider (e.g. supergrok-oauth vs xai-oauth)
|
|
18
|
+
if (seen.has(model.id)) continue;
|
|
19
|
+
seen.add(model.id);
|
|
16
20
|
if (!providers.has(model.provider)) {
|
|
17
21
|
providers.set(model.provider, {
|
|
18
22
|
provider: model.provider,
|
|
@@ -76,19 +76,20 @@ export function wireTuiHandlers({
|
|
|
76
76
|
const scopedModels = runner.getScopedModels?.() || [];
|
|
77
77
|
if (ui.selectList && scopedModels.length > 0) {
|
|
78
78
|
const current = runner.getCurrentModel?.();
|
|
79
|
-
const
|
|
80
|
-
|
|
79
|
+
const items = buildModelSelectItems({ current, scopedModels });
|
|
80
|
+
const selectedIndex = Math.max(0, items.findIndex((item) =>
|
|
81
|
+
current && item.model.id === current.id && item.model.provider === current.provider
|
|
81
82
|
));
|
|
82
|
-
const
|
|
83
|
-
items
|
|
83
|
+
const selectedItem = await ui.selectList({
|
|
84
|
+
items,
|
|
84
85
|
selectedIndex,
|
|
85
86
|
width: 72,
|
|
86
87
|
});
|
|
87
|
-
if (!
|
|
88
|
+
if (!selectedItem) {
|
|
88
89
|
ui.writeln(brightBlack(`● model: unchanged`));
|
|
89
90
|
return;
|
|
90
91
|
}
|
|
91
|
-
const model = await runner.setModel(
|
|
92
|
+
const model = await runner.setModel(selectedItem.model);
|
|
92
93
|
persistModelSelection(model, { configHomeDir });
|
|
93
94
|
const name = model.name || model.id;
|
|
94
95
|
ui.writeln(brightBlack(`● model: ${name} (${model.provider})`));
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const CUSTOM_PROVIDER_TYPE = "openai-compatible";
|
|
2
|
+
const DEFAULT_API = "openai-completions";
|
|
3
|
+
const SUPPORTED_APIS = new Set(["openai-completions", "openai-responses"]);
|
|
4
|
+
const DEFAULT_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
5
|
+
|
|
6
|
+
export function registerCustomProviders(modelRegistry, providers = {}) {
|
|
7
|
+
if (!modelRegistry?.registerProvider) return [];
|
|
8
|
+
const registered = [];
|
|
9
|
+
|
|
10
|
+
for (const [providerId, profile] of Object.entries(providers ?? {})) {
|
|
11
|
+
if (!isCustomProviderProfile(profile)) continue;
|
|
12
|
+
modelRegistry.registerProvider(providerId, toProviderConfig(providerId, profile));
|
|
13
|
+
registered.push(providerId);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return registered;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isCustomProviderProfile(profile) {
|
|
20
|
+
return Boolean(profile && typeof profile === "object" && profile.type === CUSTOM_PROVIDER_TYPE);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function toProviderConfig(providerId, profile) {
|
|
24
|
+
const api = normalizeApi(providerId, profile.api ?? DEFAULT_API);
|
|
25
|
+
const baseUrl = requireString(providerId, "baseUrl", profile.baseUrl);
|
|
26
|
+
const models = normalizeModels(providerId, profile.models, { api, baseUrl });
|
|
27
|
+
return omitUndefined({
|
|
28
|
+
name: typeof profile.name === "string" && profile.name.trim() ? profile.name : providerId,
|
|
29
|
+
baseUrl,
|
|
30
|
+
apiKey: profile.auth?.method === "apiKey" ? profile.auth.apiKey : undefined,
|
|
31
|
+
api,
|
|
32
|
+
headers: normalizeHeaders(providerId, profile.headers),
|
|
33
|
+
authHeader: typeof profile.authHeader === "boolean" ? profile.authHeader : undefined,
|
|
34
|
+
models,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeModels(providerId, models, { api, baseUrl }) {
|
|
39
|
+
if (!Array.isArray(models) || models.length === 0) {
|
|
40
|
+
throw new Error(`Custom provider "${providerId}" requires a non-empty models array`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return models.map((model, index) => {
|
|
44
|
+
if (!model || typeof model !== "object" || Array.isArray(model)) {
|
|
45
|
+
throw new Error(`Custom provider "${providerId}" model #${index + 1} must be an object`);
|
|
46
|
+
}
|
|
47
|
+
const id = requireString(providerId, `models[${index}].id`, model.id);
|
|
48
|
+
return omitUndefined({
|
|
49
|
+
...model,
|
|
50
|
+
id,
|
|
51
|
+
name: typeof model.name === "string" && model.name.trim() ? model.name : id,
|
|
52
|
+
api: normalizeApi(providerId, model.api ?? api),
|
|
53
|
+
baseUrl: typeof model.baseUrl === "string" && model.baseUrl.trim() ? model.baseUrl : baseUrl,
|
|
54
|
+
reasoning: typeof model.reasoning === "boolean" ? model.reasoning : false,
|
|
55
|
+
input: normalizeInput(model.input),
|
|
56
|
+
cost: normalizeCost(model.cost),
|
|
57
|
+
contextWindow: normalizePositiveNumber(model.contextWindow, 128000),
|
|
58
|
+
maxTokens: normalizePositiveNumber(model.maxTokens, 4096),
|
|
59
|
+
headers: normalizeHeaders(providerId, model.headers),
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeApi(providerId, api) {
|
|
65
|
+
if (typeof api !== "string" || !SUPPORTED_APIS.has(api)) {
|
|
66
|
+
throw new Error(`Custom provider "${providerId}" api must be "openai-completions" or "openai-responses"`);
|
|
67
|
+
}
|
|
68
|
+
return api;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function requireString(providerId, field, value) {
|
|
72
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
73
|
+
throw new Error(`Custom provider "${providerId}" requires ${field}`);
|
|
74
|
+
}
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeInput(input) {
|
|
79
|
+
if (!Array.isArray(input) || input.length === 0) return ["text"];
|
|
80
|
+
const normalized = input.filter((item) => item === "text" || item === "image");
|
|
81
|
+
return normalized.length > 0 ? normalized : ["text"];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function normalizeCost(cost) {
|
|
85
|
+
if (!cost || typeof cost !== "object" || Array.isArray(cost)) return DEFAULT_COST;
|
|
86
|
+
return {
|
|
87
|
+
input: normalizeNumber(cost.input, 0),
|
|
88
|
+
output: normalizeNumber(cost.output, 0),
|
|
89
|
+
cacheRead: normalizeNumber(cost.cacheRead, 0),
|
|
90
|
+
cacheWrite: normalizeNumber(cost.cacheWrite, 0),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizeHeaders(providerId, headers) {
|
|
95
|
+
if (headers == null) return undefined;
|
|
96
|
+
if (typeof headers !== "object" || Array.isArray(headers)) {
|
|
97
|
+
throw new Error(`Custom provider "${providerId}" headers must be an object`);
|
|
98
|
+
}
|
|
99
|
+
return Object.fromEntries(Object.entries(headers).filter(([, value]) => typeof value === "string"));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function normalizePositiveNumber(value, fallback) {
|
|
103
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function normalizeNumber(value, fallback) {
|
|
107
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function omitUndefined(value) {
|
|
111
|
+
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
|
|
112
|
+
}
|
package/src/provider/presets.mjs
CHANGED
|
@@ -28,10 +28,10 @@ const PROVIDER_LABELS = {
|
|
|
28
28
|
openai: "OpenAI",
|
|
29
29
|
"openai-codex": "OpenAI Codex",
|
|
30
30
|
openrouter: "OpenRouter",
|
|
31
|
-
"supergrok-oauth": "SuperGrok
|
|
31
|
+
"supergrok-oauth": "SuperGrok",
|
|
32
32
|
"vercel-ai-gateway": "Vercel AI Gateway",
|
|
33
33
|
xai: "xAI",
|
|
34
|
-
"xai-oauth": "xAI OAuth
|
|
34
|
+
"xai-oauth": "xAI OAuth",
|
|
35
35
|
zai: "ZAI",
|
|
36
36
|
xiaomi: "Xiaomi MiMo",
|
|
37
37
|
"xiaomi-token-plan-cn": "Xiaomi MiMo Token Plan (China)",
|
|
@@ -14,7 +14,7 @@ export function registerSuperGrokProvider(modelRegistry) {
|
|
|
14
14
|
if (!modelRegistry?.registerProvider) return;
|
|
15
15
|
for (const providerId of [SUPERGROK_OAUTH_PROVIDER_ID, XAI_OAUTH_COMPAT_PROVIDER_ID]) {
|
|
16
16
|
modelRegistry.registerProvider(providerId, {
|
|
17
|
-
name: providerId === SUPERGROK_OAUTH_PROVIDER_ID ? "SuperGrok
|
|
17
|
+
name: providerId === SUPERGROK_OAUTH_PROVIDER_ID ? "SuperGrok" : "xAI OAuth",
|
|
18
18
|
baseUrl: XAI_BASE_URL,
|
|
19
19
|
api: "openai-responses",
|
|
20
20
|
oauth: { ...superGrokOAuthProvider, id: providerId },
|
|
@@ -33,4 +33,4 @@ export function registerSuperGrokProvider(modelRegistry) {
|
|
|
33
33
|
|
|
34
34
|
export function getDefaultSuperGrokModelId() {
|
|
35
35
|
return DEFAULT_SUPERGROK_MODEL;
|
|
36
|
-
}
|
|
36
|
+
}
|