march-cli 0.1.4 → 0.1.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
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) => {
|
|
@@ -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
|
+
}
|