Ctrl K
ComunidadGuías…

src/dashboard/model-reconcile.js

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/**
 * P0-3 live model reconciliation.
 *
 * Pure reconciler: compares the deployment's `/v1/models` rows, the live
 * DEVIN_CONNECT catalog and the committed snapshot, then folds in the
 * account-scoped model sources (internal catalog, federation overlay, toggleable
 * agent versions, org config). Counts and keys only — raw upstream values for
 * the unverified shapes are never forwarded.
 *
 * `snapshotCount` is the committed snapshot size (233), never the older 181
 * number from an earlier capture.
 */

import { buildCatalogDiff } from './catalog-diff.js';
import {
  getLiveCatalog,
  getSnapshotSelectors,
  resolveConnectSelector,
} from '../devin-connect-models.js';
import { handleModels } from '../handlers/models.js';
import {
  TTL_MS,
  cacheKey,
  httpStatusForKind,
  readCached,
  readDevinJson,
  resolveAccount,
  resolveOrgId,
  sourceSummary,
  ReadError,
} from './devin-read.js';

const TOGGLEABLE_ROW_CAP = 50;
const DIFF_ROW_CAP = 500;
const LABEL_CAP = 20;
const LABEL_LENGTH_CAP = 80;

const TEAM_FIELDS = [
  'allowMcpServers',
  'maxUnclaimedSites',
  'allowAppDeployments',
  'allowSandboxAppDeployments',
  'allowTeamsAppDeployments',
  'maxNewSitesPerDay',
  'allowBrowserExperimentalFeatures',
  'allowCodemapSharing',
  'maxCascadeAutoExecutionLevel',
  'cascadeWebSearchEnabled',
  'allowArenaMode',
  'allowBundlingDevinCli',
  'devinTerminalAcpEnabled',
  'devinCloudAcpEnabled',
  'subagentDefaultModelUid',
];

function objectOrEmpty(value) {
  return value && typeof value === 'object' ? value : {};
}

function selectorOf(row) {
  if (typeof row === 'string') return row.trim();
  if (row && typeof row === 'object') {
    const value = row.selector || row._windsurf_id || row.id;
    return typeof value === 'string' ? value.trim() : '';
  }
  return '';
}

function uniqueSelectors(rows) {
  const out = new Set();
  for (const row of Array.isArray(rows) ? rows : []) {
    const selector = selectorOf(row);
    if (selector) out.add(selector);
  }
  return out;
}

/**
 * Deterministic toggleable-version → selector ladder. No 19-row hardcode: the
 * mapping follows the documented name transformations and asks the same resolver
 * the chat path uses, so a candidate is accepted ONLY when the authoritative
 * catalog actually exposes it.
 *
 * Two kinds of transformation are allowed here:
 *
 *   1. Reorderings that preserve every token. The upstream writes the effort tier
 *      LAST for these families — `swe-2-<effort>-lightning` and
 *      `<family>-<effort>-priority` — while the toggleable-version names put the
 *      qualifier first (`devin-swe-2-lightning-high`, `devin-swe-2-priority-low`).
 *      Same words, same model; only the order differs.
 *   2. The `claude-` family prefix the catalog adds to bare Anthropic families
 *      (`fable-5-1` → `claude-fable-5-1`), which the alias map already owns.
 *
 * Everything else stays UNMAPPED on purpose, and the row still renders with
 * `mapped:false` so an operator can see the version exists and that nothing
 * claims it. Names such as `devin-2-5` (an agent release, not a model), `auto`
 * (upstream routing, no selector), `lite`/`ultra` (plan tiers) and `fast-opus`
 * (needs an effort tier the name does not carry) have no token-preserving
 * selector. Guessing one would put a different model behind the operator's
 * choice — worse than admitting the gap. A future catalog that adds `swe-2-low`,
 * `auto` or `lite` as real selectors maps them automatically through the plain
 * pass-through candidate, with no code change.
 */
export function mapToggleableVersion(version) {
  const raw = typeof version === 'string' ? version.trim() : '';
  if (!raw) return { selector: null, mapped: false };

  const stripped = raw.replace(/^devin[-_]/, '').replace(/_/g, '-');
  const candidates = [];
  const push = (candidate) => {
    if (candidate && !candidates.includes(candidate)) candidates.push(candidate);
  };
  push(raw);
  if (stripped && stripped !== raw) push(stripped);

  for (const base of [...candidates]) {
    if (/^(fable|opus|sonnet|haiku)-/.test(base)) push(`claude-${base}`);
    // Effort tier reordering, preserving every token.
    const lightning = base.match(/^(swe-\d+)-lightning-(low|medium|high|max)$/);
    if (lightning) push(`${lightning[1]}-${lightning[2]}-lightning`);
    const priority = base.match(/^(swe-\d+)-priority-(low|medium|high|max)$/);
    if (priority) push(`${priority[1]}-${priority[2]}-priority`);
  }

  for (const candidate of candidates) {
    const resolved = resolveConnectSelector(candidate, { warnOnFallback: false });
    if (resolved?.mapped) return { selector: resolved.selector, mapped: true };
  }
  return { selector: null, mapped: false };
}

function buildToggleable(toggleable, localSelectors) {
  const versions = (Array.isArray(toggleable) ? toggleable : [])
    .map((row) => (typeof row === 'string' ? row : row?.version))
    .filter((version) => typeof version === 'string' && version.trim())
    .map((version) => version.trim());

  const all = versions.map((version) => ({ version, ...mapToggleableVersion(version) }));
  const mapped = all.filter((row) => row.mapped);
  const present = mapped.filter((row) => localSelectors.has(row.selector)).length;
  return {
    total: all.length,
    mappedCount: mapped.length,
    unmappedCount: all.length - mapped.length,
    localPresentCount: present,
    localMissingCount: mapped.length - present,
    rows: all.slice(0, TOGGLEABLE_ROW_CAP),
  };
}

function buildFedModels(fedModels, liveSelectors) {
  const raw = fedModels && typeof fedModels === 'object' ? fedModels : null;
  const tiers = objectOrEmpty(raw?.tiers);
  const models = objectOrEmpty(raw?.models);
  const helpers = Array.isArray(raw?.helpers) ? raw.helpers : [];
  const allowlist = Array.isArray(raw?.allowlist) ? raw.allowlist : null;
  const tierCount = Object.keys(tiers).length;
  const modelCount = Object.keys(models).length;
  const helperCount = helpers.length;
  const allowlistCount = allowlist ? allowlist.length : 0;
  const present = modelCount > 0 || helperCount > 0 || tierCount > 0 || allowlist !== null;

  let allowedLiveCount = null;
  let blockedLiveCount = null;
  if (present) {
    const overlay = new Set([
      ...Object.keys(models),
      ...(allowlist || []).map((value) => String(value)),
    ]);
    allowedLiveCount = liveSelectors.filter((selector) => overlay.has(selector)).length;
    blockedLiveCount = liveSelectors.length - allowedLiveCount;
  }

  return {
    present,
    modelCount,
    helperCount,
    tierCount,
    allowlistPresent: allowlist !== null,
    allowlistCount,
    allowedLiveCount,
    blockedLiveCount,
  };
}

function buildUpstreamCatalog(upstreamCatalog) {
  const raw = upstreamCatalog && typeof upstreamCatalog === 'object' ? upstreamCatalog : null;
  const groupCount = Array.isArray(raw?.groups) ? raw.groups.length : 0;
  const sectionCount = Array.isArray(raw?.sections) ? raw.sections.length : 0;
  return { empty: groupCount === 0 && sectionCount === 0, groupCount, sectionCount };
}

function buildOrgConfig(orgConfigs) {
  const raw = orgConfigs && typeof orgConfigs === 'object' ? orgConfigs : null;
  const team = objectOrEmpty(raw?.enterpriseTeamConfig);
  const control = objectOrEmpty(raw?.enterpriseControlConfig);
  const labelsRaw = Array.isArray(control.extensionModelLabels)
    ? control.extensionModelLabels
    : (Array.isArray(team.extensionModelLabels) ? team.extensionModelLabels : []);
  const labels = labelsRaw
    .slice(0, LABEL_CAP)
    .map((label) => String(label ?? '').slice(0, LABEL_LENGTH_CAP));

  const out = {
    present: !!raw && (Object.keys(team).length > 0 || Object.keys(control).length > 0),
    teamIdPresent: typeof team.teamId === 'string'
      || typeof control.teamId === 'string'
      || typeof raw?.teamId === 'string',
  };
  for (const field of TEAM_FIELDS) {
    const value = team[field] !== undefined ? team[field] : control[field];
    out[field] = value === undefined ? null : value;
  }
  out.extensionModelLabels = labels;
  return out;
}

export function buildModelReconciliation({
  local = [],
  live = [],
  snapshot = [],
  toggleable = [],
  fedModels = null,
  orgConfigs = null,
  upstreamCatalog = null,
  sources = {},
  accountId = null,
  accountRef = null,
  now = Date.now(),
} = {}) {
  const resolve = (name) => resolveConnectSelector(name, { warnOnFallback: false });
  const diff = buildCatalogDiff({ live, snapshot, resolve });
  const liveSelectors = [...uniqueSelectors(live)];
  const localSelectors = new Set();
  for (const row of Array.isArray(local) ? local : []) {
    const raw = typeof row === 'string' ? row : (row?._windsurf_id || row?.id);
    if (!raw) continue;
    const resolved = resolve(raw);
    if (resolved?.mapped) localSelectors.add(resolved.selector);
  }
  const localIds = new Set(
    (Array.isArray(local) ? local : [])
      .map((row) => (typeof row === 'string' ? row : (row?.id || row?._windsurf_id)))
      .filter(Boolean),
  );

  const degraded = Object.entries(sources || {})
    .filter(([, source]) => source?.ok === false)
    .map(([source, result]) => ({
      source,
      kind: result.error || 'upstream',
      status: result.status ?? null,
    }));

  return {
    ok: true,
    accountId,
    accountRef,
    fetchedAt: now,
    stale: Object.values(sources || {}).some((source) => source?.stale === true),
    partial: degraded.length > 0,
    synced: diff.synced,
    snapshotCount: diff.snapshotCount,
    liveCount: diff.liveCount,
    localCount: localIds.size,
    added: diff.added.slice(0, DIFF_ROW_CAP),
    missing: diff.missing.slice(0, DIFF_ROW_CAP),
    unmapped: diff.unmapped.slice(0, DIFF_ROW_CAP),
    aliases: diff.aliases.slice(0, DIFF_ROW_CAP),
    aliasCount: diff.aliasCount,
    toggleable: buildToggleable(toggleable, localSelectors),
    fedModels: buildFedModels(fedModels, liveSelectors),
    upstreamCatalog: buildUpstreamCatalog(upstreamCatalog),
    orgConfig: buildOrgConfig(orgConfigs),
    degraded,
    sources,
  };
}

export async function readModelReconciliation({ id, force = false, fetchImpl = null, now = Date.now() } = {}) {
  const account = resolveAccount(id);
  if (!account.ok) return { ok: false, error: account.error, httpStatus: account.status };

  let orgId;
  try {
    orgId = await resolveOrgId({ token: account.token, fetchImpl, force });
  } catch (err) {
    const kind = err instanceof ReadError ? err.kind : 'upstream';
    return {
      ok: false,
      accountId: account.accountId,
      accountRef: account.accountRef,
      error: kind,
      httpStatus: err?.status || httpStatusForKind(kind),
    };
  }

  const read = (name, path, orgScoped) => readCached(
    cacheKey({ accountId: account.accountId, token: account.token, name }),
    TTL_MS.models,
    () => readDevinJson({
      token: account.token,
      orgId: orgScoped ? orgId : '',
      path,
      fetchImpl,
    }),
    { force },
  );

  const [catalog, fed, toggleable, orgConfig] = await Promise.all([
    read('model-catalog', '/internal-model-catalog', false),
    read('fed-models', `/${orgId}/fed-models`, true),
    read('toggleable', '/toggleable-agent-versions', false),
    read('org-configs', '/windsurf/org-configs', false),
  ]);

  const sources = {
    catalog: sourceSummary(catalog),
    fed: sourceSummary(fed),
    toggleable: sourceSummary(toggleable),
    orgConfig: sourceSummary(orgConfig),
  };

  let local;
  try {
    local = handleModels().data;
  } catch {
    return {
      ok: false,
      accountId: account.accountId,
      accountRef: account.accountRef,
      error: 'upstream',
      httpStatus: 502,
      sources,
    };
  }

  const upstreamCatalog = catalog.value;
  const fedModels = fed.value;
  const toggleableValue = Array.isArray(toggleable.value?.toggleable_versions)
    ? toggleable.value.toggleable_versions
    : [];
  const orgConfigs = orgConfig.value;

  return buildModelReconciliation({
    local,
    live: getLiveCatalog(),
    snapshot: getSnapshotSelectors(),
    toggleable: toggleableValue,
    fedModels,
    orgConfigs,
    upstreamCatalog,
    sources,
    accountId: account.accountId,
    accountRef: account.accountRef,
    now,
  });
}