Ctrl K
ComunidadGuías…

utils/modelAccess.ts

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

Archivo original ↗Preguntar a KiBot
/**
 * Model-access gate for explicitly-requested per-run models (`--opus`,
 * `--model=<slug>`). An explicit model the run can't serve hard-fails *before*
 * the agent starts with one consistent, reason-tailored error.
 *
 * The provenance split lives upstream: `payload.modelExplicit` is true only for
 * a flag in the triggerer's own @pullfrog prompt. A standing default (repo
 * setting, a model flag in org/repo `baseInstructions`, or a trigger's fallback
 * instructions) keeps `modelExplicit = false` and never reaches the error
 * branches here — a missing key for a standing default surfaces through
 * `validateAgentApiKey`'s missing-key error instead (#938).
 *
 * `decideModelAccess` is pure (no env / IO) so the branching is unit-testable;
 * `buildModelAccessError` renders the user-facing markdown that `main.ts`
 * throws and `runErrorRenderer.ts` re-surfaces on both run surfaces.
 */

import {
  AZURE_PROVIDER,
  isOssAllowedModel,
  OPENAI_COMPATIBLE_PROVIDER,
  ossFundedModelNames,
  resolveOpenRouterModel,
} from "../models.ts";
import { pickModelLink, providerLink } from "./apiKeys.ts";
import { renderFailure } from "./failureBody.ts";

export type ModelAccessReason = "oss" | "byok_no_key" | "router";

export type ModelAccessDecision =
  | { kind: "ok" }
  /** route through the Router/OSS proxy, re-targeted to the requested model. */
  | { kind: "proxy"; target: string }
  /** run BYOK with the requested model — clear the minted proxy target. */
  | { kind: "byok" }
  | { kind: "error"; reason: ModelAccessReason };

/**
 * decide whether an (already-resolved) requested model can run, and how.
 *
 * - non-explicit / no model → `ok` (caller's missing-key validation applies).
 * - proxy active + OSS → the funded subsidy target and anything else on
 *   `OSS_MODEL_ALLOWLIST` route; a model off the list needs the repo's own key
 *   (`byok`) else `error("oss")`. the allowlist arm is what keeps
 *   `--model=<allowlisted>` from hard-failing on a pick the console offers.
 * - proxy active + Router → honor any Router-servable model (`proxy`); fall to
 *   BYOK when locally authorized, else `error("router")`.
 * - no proxy → the resolved model must be locally authorized, else
 *   `error("byok_no_key")`.
 */
export function decideModelAccess(input: {
  modelExplicit: boolean;
  model: string | undefined;
  oss: boolean;
  proxyActive: boolean;
  subsidyTarget: string | undefined;
  resolvedModel: string | undefined;
  authorized: Set<string>;
}): ModelAccessDecision {
  if (!input.modelExplicit || !input.model) return { kind: "ok" };

  // raw routing slugs (bedrock/vertex — no provider/model slash) and the
  // provider-prefixed backends that are absent from the opencode `authorized`
  // snapshot (openai-compatible always; azure whenever the deployment name isn't
  // also a catalog model id) are validated by their own setup checks.
  // discriminate on the RESOLVED specifier, matching `validateAgentApiKey`: both
  // the catalog slug and a raw `<provider>/<id>` specifier resolve to the same
  // prefixed form, so one check covers both and the two gates can't disagree
  // about what they'll admit.
  const byokAuthorized =
    !!input.resolvedModel &&
    (input.authorized.has(input.resolvedModel) ||
      !input.resolvedModel.includes("/") ||
      input.resolvedModel.startsWith(`${OPENAI_COMPATIBLE_PROVIDER}/`) ||
      input.resolvedModel.startsWith(`${AZURE_PROVIDER}/`));

  if (input.proxyActive) {
    const target = resolveOpenRouterModel(input.model);
    if (input.oss) {
      if (target && (target === input.subsidyTarget || isOssAllowedModel(input.model))) {
        return { kind: "proxy", target };
      }
      if (byokAuthorized) return { kind: "byok" };
      return { kind: "error", reason: "oss" };
    }
    if (target) return { kind: "proxy", target };
    if (byokAuthorized) return { kind: "byok" };
    return { kind: "error", reason: "router" };
  }

  if (byokAuthorized) return { kind: "ok" };
  return { kind: "error", reason: "byok_no_key" };
}

/** marker on the throw message so `runErrorRenderer` can reclassify it. */
export const MODEL_ACCESS_MARKER = "requested model is not available";

/** display names of the OSS-funded set, for the `oss` failure copy. */
function ossAllowedLabels(): string {
  return ossFundedModelNames()
    .map((name) => `\`${name}\``)
    .join(", ");
}

/**
 * render the model-access failure body (used for both the thrown error and the
 * PR comment / job summary). one shape, reason-branched why → CTA.
 */
export function buildModelAccessError(input: {
  reason: ModelAccessReason;
  model: string;
  owner: string;
  name: string;
}): string {
  const why: Record<ModelAccessReason, string> = {
    oss: `This repo runs on Pullfrog's OSS subsidy, which funds a set of pre-approved models: ${ossAllowedLabels()}. Pick one of those, or connect your own provider to run a different one.`,
    byok_no_key: `This run has no API key for \`${input.model}\`. Connect one, or pick a model you already have a key for.`,
    router:
      "Pullfrog Router can't serve this model. Pick a Router-supported model, or connect your own provider to run it directly.",
  };
  return renderFailure({
    headline: `The ${MODEL_ACCESS_MARKER}: \`${input.model}\`.`,
    detail: why[input.reason],
    links: [pickModelLink(input), providerLink(input.owner)],
  });
}