Ctrl K
ComunidadGuías…

utils/agent.ts

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

Archivo original ↗Preguntar a KiBot
import type { Agent } from "../agents/index.ts";
import { agents } from "../agents/index.ts";
import {
  AZURE_DEPLOYMENT_ENV,
  AZURE_PROVIDER,
  BEDROCK_MODEL_ID_ENV,
  CLAUDE_CODE_ONLY_CREDENTIALS,
  getModelProvider,
  isBedrockAnthropicId,
  isVertexAnthropicId,
  OPENAI_COMPATIBLE_FIELDS,
  OPENAI_COMPATIBLE_PROVIDER,
  openAICompatibleEnv,
  openAICompatibleSetName,
  resolveCliModel,
  resolveDisplayAlias,
  VERTEX_MODEL_ID_ENV,
} from "../models.ts";
import { BYOK_SLUG_MARKER } from "./apiKeys.ts";
import { log } from "./cli.ts";
import { VERTEX_SERVICE_ACCOUNT_JSON_ENV } from "./vertex.ts";

function hasEnvVar(name: string): boolean {
  const val = process.env[name];
  return typeof val === "string" && val.length > 0;
}

/** every credential claude-code can run on, including `ANTHROPIC_AUTH_TOKEN` —
 * the gateway variable it sends as `Authorization: Bearer`. opencode cannot use
 * that one, so leaving it out routed a gateway-only run to the harness that had
 * no way to authenticate it. */
function hasClaudeCodeAuth(): boolean {
  return (
    hasEnvVar("CLAUDE_CODE_OAUTH_TOKEN") ||
    hasEnvVar("ANTHROPIC_API_KEY") ||
    hasEnvVar("ANTHROPIC_AUTH_TOKEN")
  );
}

/** either credential the codex CLI can run on: the ChatGPT-subscription blob
 * `pullfrog auth codex` stores, or a plain OpenAI API key. */
function hasCodexAuth(): boolean {
  return hasEnvVar("CODEX_AUTH_JSON") || hasEnvVar("OPENAI_API_KEY");
}

function hasBedrockAuth(): boolean {
  return (
    hasEnvVar("AWS_BEARER_TOKEN_BEDROCK") ||
    (hasEnvVar("AWS_ACCESS_KEY_ID") && hasEnvVar("AWS_SECRET_ACCESS_KEY"))
  );
}

function hasVertexAuth(): boolean {
  return hasEnvVar(VERTEX_SERVICE_ACCOUNT_JSON_ENV);
}

/**
 * resolve a single slug to its CLI-ready model string. routing aliases
 * (e.g. `bedrock/byok`) defer to their backing env var instead of the
 * sentinel stored in `resolve`. shared between PULLFROG_MODEL override
 * and repo-config slug resolution so both paths get the same routing
 * semantics — without this helper, `PULLFROG_MODEL=bedrock/byok` would
 * leak the literal sentinel string `"bedrock"` downstream.
 */
function resolveSlug(slug: string): string | undefined {
  const alias = resolveDisplayAlias(slug);
  if (alias?.routing === "bedrock") {
    const bedrockId = process.env[BEDROCK_MODEL_ID_ENV]?.trim();
    if (!bedrockId) {
      throw new Error(
        `\`${BEDROCK_MODEL_ID_ENV}\` ${BYOK_SLUG_MARKER} \`${slug}\`. ` +
          "Set it in the workflow's `env:` block to a Bedrock model ID from the AWS console. " +
          "[Amazon Bedrock setup guide](https://docs.pullfrog.com/bedrock)"
      );
    }
    return bedrockId;
  }
  if (alias?.routing === "vertex") {
    const vertexId = process.env[VERTEX_MODEL_ID_ENV]?.trim();
    if (!vertexId) {
      throw new Error(
        `\`${VERTEX_MODEL_ID_ENV}\` ${BYOK_SLUG_MARKER} \`${slug}\`. ` +
          "Set it in the workflow's `env:` block to a Vertex AI model ID from Model Garden. " +
          "[Google Vertex AI setup guide](https://docs.pullfrog.com/vertex)"
      );
    }
    return vertexId;
  }
  if (alias?.routing === "azure") {
    const deployment = process.env[AZURE_DEPLOYMENT_ENV]?.trim();
    if (!deployment) {
      throw new Error(
        `\`${AZURE_DEPLOYMENT_ENV}\` ${BYOK_SLUG_MARKER} \`${slug}\`. ` +
          "Set it to your deployment's name, not the model it serves, on the Azure OpenAI provider in Pullfrog or in the workflow's `env:` block. " +
          "[Azure OpenAI setup guide](https://docs.pullfrog.com/azure)"
      );
    }
    return `${AZURE_PROVIDER}/${deployment}`;
  }
  const set = heldOpenAICompatibleSet(slug);
  if (alias?.routing === "openai-compatible" || set) {
    const modelEnv = openAICompatibleEnv(set).model;
    const modelId = process.env[modelEnv]?.trim();
    if (!modelId) {
      throw new Error(
        `\`${modelEnv}\` ${BYOK_SLUG_MARKER} \`${slug}\`. ` +
          "Set it to the model ID your endpoint serves, on the OpenAI-compatible provider in Pullfrog or in the workflow's `env:` block. " +
          "[OpenAI-compatible setup guide](https://docs.pullfrog.com/openai-compatible)"
      );
    }
    return `${OPENAI_COMPATIBLE_PROVIDER}/${modelId}`;
  }
  return resolveCliModel(slug);
}

/**
 * the named set `slug` picks, when this runner holds any of its values. holding none, the slug
 * is a raw model id on the default endpoint, exactly as it was before sets existed.
 */
function heldOpenAICompatibleSet(slug: string): string | undefined {
  const set = openAICompatibleSetName(slug);
  return set && Object.values(openAICompatibleEnv(set)).some(hasEnvVar) ? set : undefined;
}

/**
 * make the run's named set (`openai-compatible/<name>`) its endpoint: the set's values
 * replace the bare `OPENAI_COMPATIBLE_*` that validation and the opencode provider read. a value
 * the set lacks is cleared, never inherited, so the default endpoint's key cannot reach the set's
 * URL. returns the set, so setup errors can name its own variables.
 */
export function activateOpenAICompatibleSet(ctx: { slug: string | undefined }): string | undefined {
  const slug = process.env.PULLFROG_MODEL?.trim() || ctx.slug?.trim();
  const set = slug ? heldOpenAICompatibleSet(slug) : undefined;
  if (!set) return undefined;
  const from = openAICompatibleEnv(set);
  const to = openAICompatibleEnv();
  for (const field of OPENAI_COMPATIBLE_FIELDS) {
    const value = process.env[from[field]];
    if (value === undefined) delete process.env[to[field]];
    else process.env[to[field]] = value;
  }
  log.info(`» using the OpenAI-compatible endpoint "${set}"`);
  return set;
}

/**
 * resolve the effective model for this run.
 *
 * priority:
 *   1. PULLFROG_MODEL env var — resolved through the alias registry first,
 *      so values like "anthropic/claude-opus" become "anthropic/claude-opus-4-7".
 *      raw specifiers (e.g. "anthropic/claude-opus-4-6") pass through unchanged.
 *      always wins — bypasses Bedrock routing entirely. to test a different
 *      Bedrock model, change `BEDROCK_MODEL_ID`, not `PULLFROG_MODEL`.
 *   2. slug from repo config / payload → alias registry. routing slugs
 *      (e.g. `bedrock/byok`) defer to a separate env var (`BEDROCK_MODEL_ID`).
 *      a non-curated value with a slash passes through unchanged as a raw
 *      models.dev specifier (same as `PULLFROG_MODEL`), not auto-selected away.
 *   3. undefined (no slug, or a bare-word non-specifier) — agent auto-selects.
 */
export function resolveModel(ctx: { slug?: string | undefined }): string | undefined {
  const envModel = process.env.PULLFROG_MODEL?.trim();
  if (envModel) {
    return resolveSlug(envModel) ?? envModel;
  }

  const slug = ctx.slug?.trim();
  if (slug) {
    const resolved = resolveSlug(slug);
    if (resolved) {
      return resolved;
    }
    // not a curated alias: a slashed value passes through as a raw models.dev
    // specifier (explicit picks honored); a bare word isn't a valid specifier
    // and downstream would misread it as a Bedrock/Vertex backend ID — auto-select.
    if (slug.includes("/")) {
      log.info(`» "${slug}" is not a curated alias — passing through as a raw model specifier`);
      return slug;
    }
    log.warning(`» unknown model slug "${slug}" — agent will auto-select`);
  }

  return undefined;
}

export function resolveAgent(ctx: {
  model?: string | undefined;
  /** set on router / OSS runs, where the model is served by OpenRouter. those
   * pin no `model`, so without this the auto-select branches below would read
   * a stored Anthropic or OpenAI credential as the routing signal for a run
   * that isn't using it. */
  proxyModel?: string | undefined;
  /** the account opted in to the EXPERIMENTAL codex harness, already ANDed
   * server-side with the global kill switch. false — the default for every
   * account — routes OpenAI models to opencode exactly as before the harness
   * existed. see wiki/codex-agent.md. */
  codexAgent?: boolean | undefined;
}): Agent {
  // 1. explicit env var override (escape hatch). deliberately NOT gated on the
  //    opt-in: an operator who sets PULLFROG_AGENT in their own workflow is
  //    asking for a specific harness, which is what the escape hatch is for.
  const envAgent = process.env.PULLFROG_AGENT?.trim();
  if (envAgent) {
    if (envAgent in agents) {
      return agents[envAgent as keyof typeof agents];
    }
    log.warning(`» unknown PULLFROG_AGENT="${envAgent}" — falling through to auto-select`);
  }

  // 2. proxy runs are OpenRouter-served; only opencode speaks that provider.
  if (ctx.proxyModel) return agents.opencode;

  // 3. Bedrock routing: when BEDROCK_MODEL_ID is the resolved model, route
  //    Anthropic IDs through claude-code (which supports Bedrock natively
  //    once CLAUDE_CODE_USE_BEDROCK=1) and everything else through opencode's
  //    `amazon-bedrock` provider.
  if (ctx.model && hasBedrockAuth() && process.env[BEDROCK_MODEL_ID_ENV]?.trim() === ctx.model) {
    return isBedrockAnthropicId(ctx.model) ? agents.claude : agents.opencode;
  }

  // 4. Vertex routing: same shape as Bedrock, but Anthropic Vertex IDs are
  //    anchored `claude-*` IDs and non-Anthropic models use opencode's
  //    `google-vertex` provider.
  if (ctx.model && hasVertexAuth() && process.env[VERTEX_MODEL_ID_ENV]?.trim() === ctx.model) {
    return isVertexAnthropicId(ctx.model) ? agents.claude : agents.opencode;
  }

  // 5. each vendor's own harness wins for its own models when the matching
  //    credential is present — claude-code for Anthropic, codex for OpenAI.
  if (ctx.model) {
    try {
      const provider = getModelProvider(ctx.model);
      if (provider === "anthropic" && hasClaudeCodeAuth()) return agents.claude;
      if (provider === "openai" && ctx.codexAgent && hasCodexAuth()) return agents.codex;
    } catch {
      // invalid model format — fall through
    }
  }

  // 6. auto-select with no configured model. an account whose only Anthropic
  //    credential is one claude-code alone can present (the `ANTHROPIC_AUTH_TOKEN`
  //    gateway variable, or a `CLAUDE_CODE_OAUTH_TOKEN` subscription) has to take
  //    claude-code: opencode cannot use either, so `autoSelectModel` would rank a
  //    catalog it has no way to authenticate and the run dies on a missing key.
  //    the model is still unknown here — auto-select runs INSIDE the harness — so
  //    this is the last point at which the credential can pick the harness that
  //    matches it. a plain API key stays on opencode, whose ranking across
  //    providers beats guessing from whichever env var we tested first — which is
  //    also why a Codex/OpenAI credential yields to any Anthropic one.
  if (!ctx.model) {
    if (!hasEnvVar("ANTHROPIC_API_KEY") && CLAUDE_CODE_ONLY_CREDENTIALS.some(hasEnvVar)) {
      return agents.claude;
    }
    if (ctx.codexAgent && hasCodexAuth() && !hasClaudeCodeAuth()) return agents.codex;
  }

  // 7. default: OpenCode (universal, supports all providers)
  return agents.opencode;
}