All files / src/providers/anthropic Chat.ts

82.75% Statements 48/58
63.26% Branches 31/49
100% Functions 5/5
84.9% Lines 45/53

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171                        18x 18x       12x 12x   12x 12x   12x 12x 1x 1x       1x     1x 1x                           12x   12x                 12x                     12x         12x 2x               12x 14x     12x             12x 2x     12x 12x   12x                   12x 1x     11x 11x 11x     11x 11x 11x   11x 12x 10x 10x 2x             2x 2x                     11x                   12x       12x                
import { ChatRequest, ChatResponse, ThinkingResult } from "../Provider.js";
import { AnthropicMessageRequest, AnthropicMessageResponse } from "./types.js";
import { ToolCall } from "../../chat/Tool.js";
import { Capabilities } from "./Capabilities.js";
import { handleAnthropicError } from "./Errors.js";
import { ModelRegistry } from "../../models/ModelRegistry.js";
import { logger } from "../../utils/logger.js";
import { fetchWithTimeout } from "../../utils/fetch.js";
import { formatSystemPrompt, formatMessages } from "./Utils.js";
 
export class AnthropicChat {
  constructor(
    private readonly baseUrl: string,
    private readonly apiKey: string
  ) {}
 
  async execute(request: ChatRequest): Promise<ChatResponse> {
    const model = request.model;
    const maxTokens = request.max_tokens || Capabilities.getMaxOutputTokens(model) || 4096;
 
    const systemPrompt = formatSystemPrompt(request.messages);
    const messages = formatMessages(request.messages);
 
    let system = systemPrompt;
    if (request.response_format) {
      let schemaText = "";
      Eif (
        request.response_format.type === "json_schema" &&
        request.response_format.json_schema?.schema
      ) {
        schemaText =
          "\nSchema:\n" + JSON.stringify(request.response_format.json_schema.schema, null, 2);
      }
      const instruction = `CRITICAL: Respond ONLY with a valid JSON object matching the requested schema.${schemaText}\n\nDo not include any other text or explanation.`;
      system = system ? `${system}\n\n${instruction}` : instruction;
    }
 
    const {
      model: _model,
      messages: _messages,
      tools: _tools,
      temperature: _temp,
      max_tokens: _max,
      response_format: _format,
      thinking: _thinking,
      headers: _headers,
      requestTimeout,
      ...rest
    } = request;
 
    const body: AnthropicMessageRequest = {
      model: model,
      messages: messages,
      max_tokens: maxTokens,
      system: system,
      stream: false,
      ...rest
    };
 
    Iif (_thinking?.budget) {
      body.thinking = {
        type: "enabled",
        budget_tokens: _thinking.budget
      };
      // Extended thinking models require a larger max_tokens
      if (!request.max_tokens) {
        body.max_tokens = Math.max(maxTokens, _thinking.budget + 1024);
      }
    }
 
    Iif (request.temperature !== undefined) {
      body.temperature = request.temperature;
    }
 
    // Map tools
    if (request.tools && request.tools.length > 0) {
      body.tools = request.tools.map((tool) => ({
        name: tool.function.name,
        description: tool.function.description,
        input_schema: tool.function.parameters
      }));
    }
 
    // Check if any message contains PDF content to add beta header
    const hasPdf = messages.some(
      (msg) => Array.isArray(msg.content) && msg.content.some((block) => block.type === "document")
    );
 
    const headers: Record<string, string> = {
      "x-api-key": this.apiKey,
      "anthropic-version": "2023-06-01",
      "content-type": "application/json",
      ...request.headers
    };
 
    if (hasPdf) {
      headers["anthropic-beta"] = "pdfs-2024-09-25";
    }
 
    const url = `${this.baseUrl}/messages`;
    logger.logRequest("Anthropic", "POST", url, body);
 
    const response = await fetchWithTimeout(
      url,
      {
        method: "POST",
        headers: headers,
        body: JSON.stringify(body)
      },
      requestTimeout
    );
 
    if (!response.ok) {
      await handleAnthropicError(response, model);
    }
 
    const json = (await response.json()) as AnthropicMessageResponse;
    logger.logResponse("Anthropic", response.status, response.statusText, json);
    const contentBlocks = json.content;
 
    // Extract text content and tool calls
    let content: string | null = null;
    let thinkingResult: ThinkingResult | undefined = undefined;
    const toolCalls: ToolCall[] = [];
 
    for (const block of contentBlocks) {
      if (block.type === "text") {
        Eif (content === null) content = "";
        content += block.text;
      I} else if (block.type === "thinking") {
        // Handle thinking block (Claude 3.7)
        if (!thinkingResult) thinkingResult = { text: "" };
        if (block.thinking) {
          thinkingResult.text = (thinkingResult.text || "") + block.thinking;
        }
        if (block.signature) thinkingResult.signature = block.signature;
      E} else if (block.type === "tool_use") {
        toolCalls.push({
          id: block.id!,
          type: "function",
          function: {
            name: block.name!,
            arguments: JSON.stringify(block.input)
          }
        });
      }
    }
 
    const usage = json.usage
      ? {
          input_tokens: json.usage.input_tokens,
          output_tokens: json.usage.output_tokens,
          total_tokens: json.usage.input_tokens + json.usage.output_tokens,
          cached_tokens: json.usage.cache_read_input_tokens,
          cache_creation_tokens: json.usage.cache_creation_input_tokens
        }
      : undefined;
 
    const calculatedUsage = usage
      ? ModelRegistry.calculateCost(usage, model, "anthropic")
      : undefined;
 
    return {
      content,
      usage: calculatedUsage,
      thinking: thinkingResult,
      tool_calls: toolCalls.length > 0 ? toolCalls : undefined
    };
  }
}