Ctrl K
ComunidadGuías…

server/claude-endpoints.test.ts

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

Archivo original ↗Preguntar a KiBot
import { join } from "node:path";
import { describe, expect, it } from "vitest";

import { DATA_DIR } from "./config.ts";
import {
  claudeEndpointSchema,
  claudeEndpointUpdateSchema,
  claudeEndpointInfo,
  isClaudeEndpoint,
  newClaudeEndpoint,
  updateClaudeEndpoint,
  withClaudeEndpointInfo,
} from "./claude-endpoints.ts";
import { ClaudeDriver } from "./drivers/claude.ts";

const BASE = {
  displayName: "DeepSeek",
  baseUrl: "https://api.deepseek.com/anthropic/",
  token: "sk-fixture-secret",
  model: "deepseek-chat",
};

const cfg = () => ({
  instances: {
    claude: { driver: "claudeAgent", environment: { ANTHROPIC_AUTH_TOKEN: "parent-secret" }, config: { cli: "claude-wrapper" } },
    other: { driver: "codex", config: { cli: "codex-wrapper" } },
  },
}) as never;

describe("custom Claude endpoint", () => {
  it("uses the official Anthropic API key header without a bearer token", () => {
    const { instanceId, instances } = newClaudeEndpoint(cfg(), { ...BASE, baseUrl: "https://api.anthropic.com", model: "claude-sonnet-5" });
    expect(instances[instanceId].environment?.ANTHROPIC_API_KEY).toBe(BASE.token);
    expect(instances[instanceId].environment?.ANTHROPIC_AUTH_TOKEN).toBeUndefined();
  });

  it("creates its own instance with the endpoint's routing, its own directory and no parent credentials", () => {
    const source = cfg();
    const { instanceId, instances } = newClaudeEndpoint(source, { ...BASE, opusModel: "deepseek-reasoner", sonnetModel: "" });
    const entry = instances[instanceId];

    expect(instanceId).toMatch(/^claude-[0-9a-f-]{36}$/);
    expect(entry).toMatchObject({
      driver: "claudeAgent",
      displayName: "DeepSeek",
      access: "custom",
      config: {
        cli: "claude-wrapper",
        configDir: join(DATA_DIR, "providers", instanceId),
        requireApiKey: true,
        endpointModels: ["deepseek-chat", "deepseek-reasoner"],
      },
      environment: {
        ANTHROPIC_BASE_URL: "https://api.deepseek.com/anthropic",
        ANTHROPIC_AUTH_TOKEN: "sk-fixture-secret",
        ANTHROPIC_API_KEY: "sk-fixture-secret",
        ANTHROPIC_MODEL: "deepseek-chat",
        ANTHROPIC_DEFAULT_OPUS_MODEL: "deepseek-reasoner",
      },
    });
    expect(entry.environment).not.toHaveProperty("ANTHROPIC_DEFAULT_SONNET_MODEL");
    // The caller's config is left as it was: the fleet is copied, not edited.
    expect(Object.keys((source as { instances: object }).instances)).toEqual(["claude", "other"]);
    expect((source as { instances: Record<string, { environment?: object }> }).instances.claude.environment).toEqual({ ANTHROPIC_AUTH_TOKEN: "parent-secret" });
    // A second endpoint never shares the account's executable environment or directory.
    const second = newClaudeEndpoint({ instances }, BASE);
    expect(second.instanceId).not.toBe(instanceId);
    expect((second.instances[instanceId] as { environment: object }).environment).toEqual(entry.environment);
  });

  it("inherits the installed binary from whichever Claude account sets one", () => {
    const { instanceId, instances } = newClaudeEndpoint({
      instances: {
        claude: { driver: "claudeAgent", config: {} },
        "claude-work": { driver: "claudeAgent", config: { cli: "/opt/claude/bin/claude" } },
      },
    } as never, BASE);
    expect((instances[instanceId].config as { cli?: string }).cli).toBe("/opt/claude/bin/claude");
  });

  it("never inherits a Bedrock engine's binary; falls back to whichever other account sets one", () => {
    const bedrockOnly = newClaudeEndpoint({
      instances: { bedrock: { driver: "claudeAgent", config: { cli: "/opt/bedrock/bin/claude", bedrock: true } } },
    } as never, BASE);
    expect((bedrockOnly.instances[bedrockOnly.instanceId].config as { cli?: string }).cli).toBeUndefined();

    const withFallback = newClaudeEndpoint({
      instances: {
        bedrock: { driver: "claudeAgent", config: { cli: "/opt/bedrock/bin/claude", bedrock: true } },
        "claude-work": { driver: "claudeAgent", config: { cli: "/opt/claude/bin/claude" } },
      },
    } as never, BASE);
    expect((withFallback.instances[withFallback.instanceId].config as { cli?: string }).cli).toBe("/opt/claude/bin/claude");
  });

  it("reports only the address and models to a client, never the token", () => {
    const { instanceId, instances } = newClaudeEndpoint(cfg(), BASE);
    const info = claudeEndpointInfo(instances[instanceId]);
    expect(info).toEqual({
      baseUrl: BASE.baseUrl.replace(/\/$/u, ""),
      models: ["deepseek-chat"],
      tokenSaved: true,
      opusModel: "", sonnetModel: "", haikuModel: "", smallFastModel: "",
    });
    const described = withClaudeEndpointInfo([{ instanceId }, { instanceId: "claude" }], instances) as Array<Record<string, unknown>>;
    expect(JSON.stringify(described)).not.toContain("sk-fixture-secret");
    expect(described[0]).toMatchObject({ url: "https://api.deepseek.com/anthropic", claudeEndpoint: { tokenSaved: true } });
    expect(described[1]).not.toHaveProperty("claudeEndpoint");
    expect(isClaudeEndpoint(instances[instanceId])).toBe(true);
    expect(isClaudeEndpoint(instances.claude)).toBe(false);
  });

  it("edits in place, keeps an omitted token, and clears a model mapping left empty", () => {
    const { instanceId, instances } = newClaudeEndpoint(cfg(), { ...BASE, opusModel: "deepseek-reasoner" });
    const updated = updateClaudeEndpoint({ instances }, instanceId, {
      model: "deepseek-reasoner",
      opusModel: "",
    });
    const entry = updated.instances[instanceId];
    expect(entry.environment).toMatchObject({
      ANTHROPIC_BASE_URL: "https://api.deepseek.com/anthropic",
      ANTHROPIC_AUTH_TOKEN: "sk-fixture-secret",
      ANTHROPIC_MODEL: "deepseek-reasoner",
    });
    expect(entry.environment).not.toHaveProperty("ANTHROPIC_DEFAULT_OPUS_MODEL");
    expect((entry.config as { endpointModels: string[] }).endpointModels).toEqual(["deepseek-reasoner"]);
    expect(updateClaudeEndpoint({ instances: updated.instances }, instanceId, { token: "sk-replaced" })
      .instances[instanceId].environment).toMatchObject({ ANTHROPIC_AUTH_TOKEN: "sk-replaced", ANTHROPIC_API_KEY: "sk-replaced" });
    // Nothing else in the fleet moves.
    expect(updated.instances.claude).toEqual(instances.claude);
  });

  it("keeps a discovered catalog when only a model mapping changes", () => {
    const { instanceId, instances } = newClaudeEndpoint(cfg(), { ...BASE, model: undefined });
    (instances[instanceId].config as { endpointModels: string[] }).endpointModels = ["model-a", "model-b"];
    for (const opusModel of ["model-b", ""]) {
      const entry = updateClaudeEndpoint({ instances }, instanceId, { opusModel }).instances[instanceId];
      expect((entry.config as { endpointModels: string[] }).endpointModels).toEqual(["model-a", "model-b"]);
    }
  });

  it("requires the token again when the address changes", () => {
    const { instanceId, instances } = newClaudeEndpoint(cfg(), BASE);
    expect(() => updateClaudeEndpoint({ instances }, instanceId, { baseUrl: "https://api.example.test/anthropic" }))
      .toThrow(/token again for the new address/);
    const moved = updateClaudeEndpoint({ instances }, instanceId, { baseUrl: "https://api.example.test/anthropic", token: "sk-moved" });
    expect(moved.instances[instanceId].environment).toMatchObject({
      ANTHROPIC_BASE_URL: "https://api.example.test/anthropic",
      ANTHROPIC_AUTH_TOKEN: "sk-moved",
    });
    // The saved address, however written, still needs no token.
    expect(updateClaudeEndpoint({ instances }, instanceId, { baseUrl: BASE.baseUrl }).instances[instanceId].environment)
      .toMatchObject({ ANTHROPIC_AUTH_TOKEN: "sk-fixture-secret" });
  });

  it("rejects a non-endpoint Claude instance, an unknown id and an endpoint with no token", () => {
    const { instances } = newClaudeEndpoint(cfg(), BASE);
    expect(() => updateClaudeEndpoint({ instances }, "claude", { model: "deepseek-chat" })).toThrow(/not a custom endpoint/);
    expect(() => updateClaudeEndpoint({ instances }, "missing", { model: "deepseek-chat" })).toThrow(/unknown instance/);
    const stripped = { instances: { ...instances, ghost: { driver: "claudeAgent", config: { endpointModels: ["m"] }, environment: { ANTHROPIC_BASE_URL: "https://x.test" } } } };
    expect(() => updateClaudeEndpoint(stripped as never, "ghost", { model: "m" })).toThrow(/token/i);
  });

  it("validates the address, token and model ids before anything is saved", () => {
    for (const body of [
      { ...BASE, baseUrl: "ftp://api.example.test" },
      { ...BASE, baseUrl: "https://user:pass@api.example.test" },
      { ...BASE, baseUrl: "not a url" },
      { ...BASE, token: "" },
      { ...BASE, token: "has\nnewline" },
      { ...BASE, model: "has space" },
      { ...BASE, model: "" },
      { ...BASE, model: "default" },
      { ...BASE, displayName: "Bad\nname" },
      { ...BASE, environment: { ANTHROPIC_BASE_URL: "https://x.test" } },
    ]) {
      expect(claudeEndpointSchema.safeParse(body).success, JSON.stringify(body)).toBe(false);
    }
    expect(claudeEndpointSchema.safeParse(BASE).success).toBe(true);
    expect(claudeEndpointUpdateSchema.safeParse({}).success).toBe(false);
    expect(claudeEndpointUpdateSchema.safeParse({ token: "" }).success).toBe(false);
    expect(claudeEndpointUpdateSchema.safeParse({ model: "deepseek-reasoner" }).success).toBe(true);
  });

});

describe("Claude endpoint catalog", () => {
  const decoded = (raw: Record<string, unknown>) => ClaudeDriver.decodeConfig(raw);

  it("serves only the endpoint's models, the first one the default", async () => {
    const instance = await ClaudeDriver.create({
      instanceId: "claude-endpoint-fixture",
      displayName: "DeepSeek",
      enabled: true,
      environment: {
        ANTHROPIC_BASE_URL: "https://api.deepseek.com/anthropic",
        ANTHROPIC_AUTH_TOKEN: "sk-fixture",
        ANTHROPIC_API_KEY: "sk-fixture",
        ANTHROPIC_MODEL: "deepseek-chat",
      },
      config: decoded({ configDir: "/tmp/omb-endpoint-fixture", requireApiKey: true, endpointModels: ["deepseek-chat", "deepseek-reasoner"] }),
    });
    try {
      expect(instance.models).toEqual({
        default: "deepseek-chat",
        options: [{ id: "deepseek-chat", label: "deepseek-chat", custom: true }, { id: "deepseek-reasoner", label: "deepseek-reasoner", custom: true }],
      });
    } finally {
      await instance.dispose();
    }
  });

  it("keeps hosted model assignment and endpoint models apart", () => {
    expect(() => decoded({ managed: true, endpointModels: ["deepseek-chat"] })).toThrow(/Invalid Claude endpoint models/);
    expect(() => decoded({ managedModels: ["claude-sonnet-5"], endpointModels: ["deepseek-chat"] })).toThrow(/Invalid hosted Claude models/);
    expect(() => decoded({ endpointModels: [] })).toThrow(/Invalid Claude endpoint models/);
    expect(() => decoded({ endpointModels: ["has space"] })).toThrow(/Invalid Claude endpoint models/);
    expect(decoded({ endpointModels: ["deepseek-chat[1m]"] }).endpointModels).toEqual(["deepseek-chat[1m]"]);
  });
});