Ctrl K
ComunidadGuías…

server/claude-bedrock.ts

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

Archivo original ↗Preguntar a KiBot
// The AWS SDK inside Claude Code resolves a profile's SSO or credential_process itself; OpenMausBot stores no AWS secret.
import { randomUUID } from "node:crypto";
import { join } from "node:path";
import { z } from "zod";

import type { ClaudeBedrockInfo } from "../shared/claude-endpoint.ts";
import { assertSeparateClaudeAccount } from "./claude-accounts.ts";
import { DATA_DIR, persistableInstanceConfigs, type AppConfig } from "./config.ts";
import type { InstanceConfig, InstanceConfigMap } from "./contracts.ts";
import { CLAUDE_MODEL_ID } from "./drivers/claude-model-extras.ts";

const displayName = z.string().trim().min(1).max(80).refine((value) => !/\p{Cc}/u.test(value), "Name cannot contain control characters");
const awsProfile = z.string().trim().regex(/^[A-Za-z0-9._+=,@-]{1,128}$/, "Enter the AWS profile name from this computer's ~/.aws config.");
const awsRegion = z.string().trim().regex(/^[a-z]{2}(-[a-z]+)+-\d$/, "Enter an AWS region, such as us-east-1.");
const model = z.string().trim().max(160).regex(CLAUDE_MODEL_ID, "Use the model id Bedrock serves.")
  .refine((value) => value !== "default", "Claude Code reserves `default`; use the model id Bedrock serves.");
const optionalModel = z.union([model, z.literal("")]);

export const claudeBedrockSchema = z.object({
  displayName,
  awsProfile,
  awsRegion,
  model,
  opusModel: optionalModel.optional(),
  sonnetModel: optionalModel.optional(),
  haikuModel: optionalModel.optional(),
  smallFastModel: optionalModel.optional(),
}).strict();

export const claudeBedrockUpdateSchema = z.object({
  displayName: displayName.optional(),
  awsProfile: awsProfile.optional(),
  awsRegion: awsRegion.optional(),
  model: model.optional(),
  opusModel: optionalModel.optional(),
  sonnetModel: optionalModel.optional(),
  haikuModel: optionalModel.optional(),
  smallFastModel: optionalModel.optional(),
}).strict().refine((value) => Object.keys(value).length > 0, "No settings supplied");

type BedrockFields = z.infer<typeof claudeBedrockUpdateSchema>;
type ResolvedBedrockFields = BedrockFields & { awsProfile: string; awsRegion: string };

const TIERS = [
  ["opusModel", "ANTHROPIC_DEFAULT_OPUS_MODEL"],
  ["sonnetModel", "ANTHROPIC_DEFAULT_SONNET_MODEL"],
  ["haikuModel", "ANTHROPIC_DEFAULT_HAIKU_MODEL"],
  ["smallFastModel", "ANTHROPIC_SMALL_FAST_MODEL"],
] as const;

function rawConfig(entry: InstanceConfig): Record<string, unknown> {
  return entry.config && typeof entry.config === "object" && !Array.isArray(entry.config)
    ? entry.config as Record<string, unknown> : {};
}

/** A Claude instance that runs Claude Code on Amazon Bedrock, rather than a named Claude account or a custom endpoint. */
export function isClaudeBedrock(entry: InstanceConfig | undefined): boolean {
  return entry?.driver === "claudeAgent" && rawConfig(entry).bedrock === true;
}

export type { ClaudeBedrockInfo };

/** What the settings UI may know about a Bedrock instance: its profile, region and models; never a secret. */
export function claudeBedrockInfo(entry: InstanceConfig): ClaudeBedrockInfo {
  const config = rawConfig(entry);
  const environment = entry.environment ?? {};
  return {
    awsProfile: environment.AWS_PROFILE ?? "",
    awsRegion: environment.AWS_REGION ?? "",
    model: environment.ANTHROPIC_MODEL ?? "",
    models: Array.isArray(config.endpointModels) ? [...config.endpointModels] : [],
    opusModel: environment.ANTHROPIC_DEFAULT_OPUS_MODEL ?? "",
    sonnetModel: environment.ANTHROPIC_DEFAULT_SONNET_MODEL ?? "",
    haikuModel: environment.ANTHROPIC_DEFAULT_HAIKU_MODEL ?? "",
    smallFastModel: environment.ANTHROPIC_SMALL_FAST_MODEL ?? "",
  };
}

/** Adds each Bedrock instance's profile, region and models to the engine list; no secret ever leaves the server. */
export function withClaudeBedrockInfo(described: unknown, configs: InstanceConfigMap): unknown {
  if (!Array.isArray(described)) return described;
  return described.map((instance: { instanceId?: string }) => {
    const entry = instance.instanceId ? configs[instance.instanceId] : undefined;
    return entry && isClaudeBedrock(entry) ? { ...instance, claudeBedrock: claudeBedrockInfo(entry) } : instance;
  });
}

function bedrockEnvironment(fields: ResolvedBedrockFields): Record<string, string> {
  const environment: Record<string, string> = {
    CLAUDE_CODE_USE_BEDROCK: "1",
    AWS_PROFILE: fields.awsProfile,
    AWS_REGION: fields.awsRegion,
    ...(fields.model ? { ANTHROPIC_MODEL: fields.model.trim() } : {}),
  };
  for (const [field, key] of TIERS) {
    const value = fields[field]?.trim();
    if (value) environment[key] = value;
  }
  return environment;
}

function bedrockModels(fields: ResolvedBedrockFields): string[] {
  const tiers = TIERS.map(([field]) => fields[field]?.trim() ?? "").filter(Boolean);
  return [...new Set([(fields.model ?? "").trim(), ...tiers].filter(Boolean))];
}

function inheritedCli(instances: InstanceConfigMap): string | undefined {
  for (const entry of Object.values(instances)) {
    if (entry.driver !== "claudeAgent" || isClaudeBedrock(entry)) continue;
    const cli = rawConfig(entry).cli;
    if (typeof cli === "string" && cli) return cli;
  }
  return undefined;
}

export function newClaudeBedrock(cfg: AppConfig, body: z.infer<typeof claudeBedrockSchema>) {
  const instances = persistableInstanceConfigs(cfg);
  const instanceId = `claude-${randomUUID()}`;
  const cli = inheritedCli(instances);
  const entry: InstanceConfig = {
    driver: "claudeAgent",
    displayName: body.displayName,
    access: "custom",
    config: {
      ...(cli ? { cli } : {}),
      configDir: join(DATA_DIR, "providers", instanceId),
      bedrock: true,
      endpointModels: bedrockModels(body),
    },
    environment: bedrockEnvironment(body),
  };
  assertSeparateClaudeAccount(instances, instanceId, entry);
  instances[instanceId] = entry;
  return { instanceId, instances };
}

export function updateClaudeBedrock(cfg: AppConfig, instanceId: string, body: BedrockFields) {
  const instances = persistableInstanceConfigs(cfg);
  const entry = instances[instanceId];
  if (!entry) throw Object.assign(new Error("unknown instance"), { status: 404 });
  if (!isClaudeBedrock(entry)) throw Object.assign(new Error("This Claude instance is not an Amazon Bedrock engine."), { status: 400 });
  const saved = claudeBedrockInfo(entry);
  if (!saved.awsProfile || !saved.awsRegion) throw Object.assign(new Error("This engine is missing its AWS profile or region. Remove it and add it again."), { status: 409 });
  const fields: ResolvedBedrockFields = {
    awsProfile: body.awsProfile ?? saved.awsProfile,
    awsRegion: body.awsRegion ?? saved.awsRegion,
    model: body.model ?? entry.environment?.ANTHROPIC_MODEL,
  };
  if (body.displayName !== undefined) fields.displayName = body.displayName;
  for (const [field, key] of TIERS) {
    const value = body[field] !== undefined ? body[field] : entry.environment?.[key];
    if (value !== undefined) fields[field] = value;
  }
  instances[instanceId] = {
    ...entry,
    ...(fields.displayName ? { displayName: fields.displayName } : {}),
    environment: bedrockEnvironment(fields),
    config: {
      ...rawConfig(entry),
      bedrock: true,
      endpointModels: fields.model && (body.model !== undefined || TIERS.some(([field]) => body[field] !== undefined))
        ? bedrockModels(fields) : saved.models,
    },
  };
  return { instances };
}