@sellable/install
Version:
One-command installer for Sellable MCP in Claude Code, Codex, and Hermes
252 lines (242 loc) • 8.79 kB
JavaScript
// Shared by the control plane and the runtime. A custom Runs API is only a
// requested destination here; the runtime must also match a trusted connection.
export const ENDPOINT_MODEL_SELECTION_ALLOWLIST = Object.freeze({
openrouter: Object.freeze([
// Retained for existing saved selections; the UI shortlist excludes Opus.
"anthropic/claude-opus-5.5",
"deepseek/deepseek-v4-flash-0731",
"openai/gpt-6-luna",
"anthropic/claude-sonnet-5.5",
]),
// The Agent's own Codex model (model-auth-reconciler.mjs), served by Hermes
// per run from the machine's Codex subscription pool.
"openai-codex": Object.freeze(["gpt-6.1-sol"]),
});
export const ENDPOINT_MODEL_REASONING_EFFORTS = Object.freeze([
"none",
"low",
"medium",
"high",
"xhigh",
]);
export const ENDPOINT_MODEL_MAX_FALLBACKS = 2;
export function normalizeRunsApiBaseUrl(value) {
if (
typeof value !== "string" ||
value.length > 1_024 ||
value !== value.trim()
)
return null;
try {
const url = new URL(value);
if (
url.username ||
url.password ||
url.search ||
url.hash ||
/[\\\s]/.test(value) ||
(url.protocol !== "https:" &&
!(
url.protocol === "http:" &&
["127.0.0.1", "[::1]"].includes(url.hostname)
))
)
return null;
return `${url.origin}${url.pathname.replace(/\/$/, "")}`;
} catch {
return null;
}
}
// One model: provider, model, effort and, for a custom Runs API, its URL.
// A custom step may also name the workspace's own model connection
// (modelConnectionId), which is always https. Skew: an older runtime refuses
// that unknown key as "invalid", so Verify & save refuses a connection step
// on a machine that cannot fetch its key per attempt.
function compileEndpointModelStep(value, extraKeys = []) {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
const custom = Object.hasOwn(value, "runsApiBaseUrl");
const connection = Object.hasOwn(value, "modelConnectionId");
if (
Object.keys(value)
.filter((key) => !extraKeys.includes(key))
.sort()
.join(",") !==
(connection
? "model,modelConnectionId,provider,reasoningEffort,runsApiBaseUrl"
: custom
? "model,provider,reasoningEffort,runsApiBaseUrl"
: "model,provider,reasoningEffort")
)
return null;
const { provider, model, reasoningEffort } = value;
if (
typeof provider !== "string" ||
typeof model !== "string" ||
!ENDPOINT_MODEL_REASONING_EFFORTS.includes(reasoningEffort)
)
return null;
if (
!custom &&
(!Object.hasOwn(ENDPOINT_MODEL_SELECTION_ALLOWLIST, provider) ||
!ENDPOINT_MODEL_SELECTION_ALLOWLIST[provider].includes(model))
)
return null;
const runsApiBaseUrl = custom
? normalizeRunsApiBaseUrl(value.runsApiBaseUrl)
: null;
if (
custom &&
(!runsApiBaseUrl ||
!/^[a-z][a-z0-9_-]{0,63}$/.test(provider) ||
!/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$/.test(model))
)
return null;
const { modelConnectionId } = value;
if (
connection &&
(typeof modelConnectionId !== "string" ||
!/^[A-Za-z0-9_-]{1,128}$/.test(modelConnectionId) ||
!runsApiBaseUrl.startsWith("https://"))
)
return null;
return Object.freeze({
provider,
model,
reasoningEffort,
...(custom ? { runsApiBaseUrl } : {}),
...(connection ? { modelConnectionId } : {}),
});
}
const sameStep = (left, right) =>
left.provider === right.provider &&
left.model === right.model &&
left.reasoningEffort === right.reasoningEffort &&
left.runsApiBaseUrl === right.runsApiBaseUrl &&
left.modelConnectionId === right.modelConnectionId;
/**
* The top-level fields are the endpoint's first model. An optional ordered
* `fallbacks` list holds up to two more, each a full model choice of its own;
* `fallbackPolicy` applies only after every model in the chain has failed.
*/
export function compileEndpointModelSelection(value) {
if (value === undefined || value === null) return null;
if (typeof value !== "object" || Array.isArray(value)) return "invalid";
const primary = compileEndpointModelStep(value, [
"fallbackPolicy",
"fallbacks",
]);
if (!primary) return "invalid";
const hasFallbackPolicy = Object.hasOwn(value, "fallbackPolicy");
if (
hasFallbackPolicy &&
!["STOP", "PRODUCT_DEFAULT"].includes(value.fallbackPolicy)
)
return "invalid";
let fallbacks = null;
if (Object.hasOwn(value, "fallbacks")) {
if (
!Array.isArray(value.fallbacks) ||
value.fallbacks.length < 1 ||
value.fallbacks.length > ENDPOINT_MODEL_MAX_FALLBACKS
)
return "invalid";
fallbacks = value.fallbacks.map((step) => compileEndpointModelStep(step));
if (
fallbacks.some(
(step, index) =>
!step ||
[primary, ...fallbacks.slice(0, index)].some((earlier) =>
sameStep(earlier, step)
)
)
)
return "invalid";
}
return Object.freeze({
...primary,
...(hasFallbackPolicy ? { fallbackPolicy: value.fallbackPolicy } : {}),
...(fallbacks ? { fallbacks: Object.freeze(fallbacks) } : {}),
});
}
/** The models a run tries, in order; empty means the Agent's own default. */
export function endpointModelSelectionChain(selection) {
const compiled = compileEndpointModelSelection(selection);
if (!compiled || compiled === "invalid") return [];
const { fallbacks = [], fallbackPolicy: _policy, ...primary } = compiled;
return [Object.freeze(primary), ...fallbacks];
}
/** The settings source a model belongs to: Agent default, OpenRouter, Codex or a custom Runs API. */
export function endpointModelSelectionSource(selection) {
if (!selection) return "default";
if (selection.runsApiBaseUrl) return "custom";
return selection.provider === "openai-codex" ? "codex" : "openrouter";
}
// A step that cannot be admitted waits this long at most before the next
// model takes the run. Claude Code runs take 9-15 s, so a busy slot normally
// frees well inside it.
export const ENDPOINT_CHAIN_STEP_ADMISSION_MS = 30_000;
// Starting another model with less time than this left cannot finish it.
export const ENDPOINT_CHAIN_MIN_STEP_MS = 5_000;
// A step that failed in one of these ways produced no output, so another
// model can write without risking a second draft or send.
export const ENDPOINT_CHAIN_ADVANCE_FAILURES = Object.freeze([
"NATIVE_FAILED", // the run failed with no output (incl. Claude Code usage limit or sign-in refusals)
"OUTPUT_CONTRACT_FAILED", // its output failed the product contract and was not used
"NOT_ADMITTED", // the service stayed busy (429/503) for the step's admission budget
"STEP_UNAVAILABLE", // this machine could not open the step's connection; nothing was sent
]);
/**
* Decide whether a run moves to the next model of its chain. `history` holds
* the failure classes of the steps already left behind; `failure` is how the
* current step ended. Anything not listed in ENDPOINT_CHAIN_ADVANCE_FAILURES
* (an ambiguous submit, a lost stream, a stop or cancel, a late completion,
* success including a SUPPRESS decision, a timeout) ends the run.
*/
export function nextEndpointChainStep(
chain,
history,
failure,
remainingMs = Number.POSITIVE_INFINITY
) {
if (
!Array.isArray(chain) ||
chain.length < 1 ||
chain.length > ENDPOINT_MODEL_MAX_FALLBACKS + 1 ||
!Array.isArray(history) ||
history.length >= chain.length ||
history.some((item) => !ENDPOINT_CHAIN_ADVANCE_FAILURES.includes(item))
)
return Object.freeze({ advance: false, reason: "INVALID" });
if (!ENDPOINT_CHAIN_ADVANCE_FAILURES.includes(failure))
return Object.freeze({ advance: false, reason: "NOT_ADVANCEABLE" });
const stepIndex = history.length + 1;
if (stepIndex >= chain.length)
return Object.freeze({ advance: false, reason: "CHAIN_EXHAUSTED" });
if (!(remainingMs >= ENDPOINT_CHAIN_MIN_STEP_MS))
return Object.freeze({ advance: false, reason: "NO_TIME_LEFT" });
return Object.freeze({
advance: true,
stepIndex,
selection: chain[stepIndex],
});
}
/**
* When a busy step stops waiting for admission. Every step but the last gets
* an equal share of the time left, at most ENDPOINT_CHAIN_STEP_ADMISSION_MS;
* the last step, like a single model, waits until the attempt deadline.
*/
export function endpointChainStepAdmissionDeadline({
startedAtMs,
deadlineMs,
stepIndex,
stepCount,
}) {
if (stepIndex >= stepCount - 1) return deadlineMs;
const share = Math.floor(
(deadlineMs - startedAtMs) / (stepCount - stepIndex)
);
return Math.min(
deadlineMs,
startedAtMs + Math.max(0, Math.min(ENDPOINT_CHAIN_STEP_ADMISSION_MS, share))
);
}