UNPKG

@sellable/install

Version:

One-command installer for Sellable MCP in Claude Code, Codex, and Hermes

252 lines (242 loc) • 8.79 kB
// 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)) ); }