Skip to main content
Glama
by microsoft
anthropic.ts21.1 kB
import { ChatCompletionHandler, LanguageModel, ListModelsFunction, } from "./chat" import { ANTHROPIC_MAX_TOKEN, MODEL_PROVIDER_ANTHROPIC, MODEL_PROVIDER_ANTHROPIC_BEDROCK, } from "./constants" import { parseModelIdentifier } from "./models" import { NotSupportedError, serializeError } from "./error" import { approximateTokens } from "./tokens" import { resolveTokenEncoder } from "./encoders" import type { Anthropic } from "@anthropic-ai/sdk" import { ChatCompletionResponse, ChatCompletionToolCall, ChatCompletionUsage, ChatCompletionMessageParam, ChatCompletionAssistantMessageParam, ChatCompletionUserMessageParam, ChatCompletionTool, ChatFinishReason, ChatCompletionContentPartImage, ChatCompletionSystemMessageParam, ChatCompletionToolMessageParam, ChatCompletionContentPart, ChatCompletionContentPartRefusal, ChatCompletionsProgressReport, } from "./chattypes" import { logError } from "./util" import { resolveHttpProxyAgent } from "./proxy" import { ProxyAgent } from "undici" import { MarkdownTrace } from "./trace" import { createFetch, FetchType } from "./fetch" import { JSONLLMTryParse } from "./json5" import { LanguageModelConfiguration } from "./server/messages" import { deleteUndefinedValues } from "./cleaners" import debug from "debug" import { providerFeatures } from "./features" const dbg = debug("genaiscript:anthropic") const dbgMessages = debug("genaiscript:anthropic:msg") const convertFinishReason = ( stopReason: Anthropic.Message["stop_reason"] ): ChatFinishReason => { switch (stopReason) { case "end_turn": return "stop" case "max_tokens": return "length" case "stop_sequence": return "stop" case "tool_use": return "tool_calls" default: return undefined } } const convertUsage = ( usage: Anthropic.Messages.Usage | undefined ): ChatCompletionUsage | undefined => { if (!usage) return undefined const res = { prompt_tokens: usage.input_tokens + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0), completion_tokens: usage.output_tokens, total_tokens: usage.input_tokens + usage.output_tokens, } as ChatCompletionUsage if (usage.cache_read_input_tokens) res.prompt_tokens_details = { cached_tokens: usage.cache_read_input_tokens, } return res } const adjustUsage = ( usage: ChatCompletionUsage, outputTokens: Anthropic.MessageDeltaUsage ): ChatCompletionUsage => { return { ...usage, completion_tokens: usage.completion_tokens + outputTokens.output_tokens, total_tokens: usage.total_tokens + outputTokens.output_tokens, } } const convertMessages = ( messages: ChatCompletionMessageParam[], emitThinking: boolean ): Anthropic.MessageParam[] => { const res: Anthropic.MessageParam[] = [] dbgMessages(`converting %d messages`, messages.length) for (let i = 0; i < messages.length; ++i) { const message = messages[i] const msg = convertSingleMessage(message, emitThinking) if (msg.content === "") { dbgMessages(`empty message`, msg) continue // no message } const last = res.at(-1) if (last?.role !== msg.role) res.push(msg) else { if (typeof last.content === "string") last.content = [ { type: "text", text: last.content, }, ] if (typeof msg.content === "string") last.content.push({ type: "text", text: msg.content }) else last.content.push(...msg.content) } } // filter out empty text messages return res.filter((msg) => Array.isArray(msg.content) ? msg.content.length > 0 : msg.content !== "" ) } const convertSingleMessage = ( msg: ChatCompletionMessageParam, emitThinking: boolean ): Anthropic.MessageParam => { const { role } = msg if (!role) { return { role: "user", content: [{ type: "text", text: JSON.stringify(msg) }], } } else if (msg.role === "assistant") { return convertAssistantMessage(msg, emitThinking) } else if (role === "tool") { return convertToolResultMessage(msg) } else if (role === "function") throw new NotSupportedError("function message not supported") return convertStandardMessage(msg) } function toCacheControl(msg: ChatCompletionMessageParam): { type: "ephemeral" } { return msg.cacheControl === "ephemeral" ? { type: "ephemeral" } : undefined } const convertAssistantMessage = ( msg: ChatCompletionAssistantMessageParam, emitThinking: boolean ): Anthropic.MessageParam => { return { role: "assistant", content: [ msg.reasoning_content && emitThinking ? ({ type: "thinking", thinking: msg.reasoning_content, signature: msg.signature, } satisfies Anthropic.ThinkingBlockParam) : undefined, ...((convertStandardMessage(msg)?.content || []) as any), ...(msg.tool_calls || []).map( (tool) => deleteUndefinedValues({ type: "tool_use", id: tool.id, input: JSONLLMTryParse(tool.function.arguments), name: tool.function.name, cache_control: toCacheControl(msg), }) satisfies Anthropic.ToolUseBlockParam ), ].filter((x) => !!x), } } const convertToolResultMessage = ( msg: ChatCompletionToolMessageParam ): Anthropic.MessageParam => { return { role: "user", content: [ deleteUndefinedValues({ type: "tool_result", tool_use_id: msg.tool_call_id, content: msg.content, cache_control: toCacheControl(msg), } satisfies Anthropic.ToolResultBlockParam), ], } } const convertBlockParam = ( block: ChatCompletionContentPart | ChatCompletionContentPartRefusal, cache_control?: { type: "ephemeral" } ) => { if (typeof block === "string") { return { type: "text", text: block, cache_control, } satisfies Anthropic.TextBlockParam } else if (block.type === "text") { if (!block.text) return undefined return { type: "text", text: block.text, cache_control, } satisfies Anthropic.TextBlockParam } else if (block.type === "image_url") { return convertImageUrlBlock(block) } // audio? // Handle other types or return a default else return { type: "text", text: JSON.stringify(block), } satisfies Anthropic.TextBlockParam } const convertStandardMessage = ( msg: | ChatCompletionSystemMessageParam | ChatCompletionAssistantMessageParam | ChatCompletionUserMessageParam ): Anthropic.MessageParam => { const role = msg.role === "assistant" ? "assistant" : "user" let res: Anthropic.MessageParam if (Array.isArray(msg.content)) { const cache_control = toCacheControl(msg) res = { role, content: msg.content .map((block) => convertBlockParam(block, cache_control)) .filter((t) => !!t) .map(deleteUndefinedValues), } } else if (typeof msg.content === "string") { res = { role, content: [ deleteUndefinedValues({ type: "text", text: msg.content, cache_control: toCacheControl(msg), }) satisfies Anthropic.TextBlockParam, ], } } return res } const convertImageUrlBlock = ( block: ChatCompletionContentPartImage ): Anthropic.ImageBlockParam => { return { type: "image", source: { type: "base64", media_type: block.image_url.url.startsWith("data:image/png") ? "image/png" : "image/jpeg", data: block.image_url.url.split(",")[1], }, } } const convertTools = ( tools?: ChatCompletionTool[] ): Anthropic.Messages.Tool[] | undefined => { if (!tools) return undefined return tools.map( (tool) => ({ name: tool.function.name, description: tool.function.description, input_schema: { type: "object", ...tool.function.parameters, }, }) satisfies Anthropic.Messages.Tool ) } const completerFactory = ( resolver: ( trace: MarkdownTrace, cfg: LanguageModelConfiguration, httpAgent: ProxyAgent, fetch: FetchType ) => Promise<Omit<Anthropic.Beta.Messages, "batches" | "countTokens">> ) => { const completion: ChatCompletionHandler = async ( req, cfg, options, trace ) => { const { requestOptions, partialCb, cancellationToken, inner, retry, maxDelay, retryDelay, } = options const { headers } = requestOptions || {} const { provider, model, reasoningEffort } = parseModelIdentifier( req.model ) const { encode: encoder } = await resolveTokenEncoder(model) const fetch = await createFetch({ trace, retries: retry, retryDelay, maxDelay, cancellationToken, }) // https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching#how-to-implement-prompt-caching const caching = /sonnet|haiku|opus/i.test(model) && req.messages.some((m) => m.cacheControl === "ephemeral") const httpAgent = resolveHttpProxyAgent() const messagesApi = await resolver(trace, cfg, httpAgent, fetch) dbg("caching", caching) trace.itemValue(`caching`, caching) let numTokens = 0 let chatResp = "" let reasoningChatResp = "" let signature = "" let finishReason: ChatCompletionResponse["finishReason"] let usage: ChatCompletionResponse["usage"] | undefined const toolCalls: ChatCompletionToolCall[] = [] const tools = convertTools(req.tools) let temperature = req.temperature let top_p = req.top_p let tool_choice: Anthropic.Beta.MessageCreateParams["tool_choice"] = req.tool_choice === "auto" ? { type: "auto" } : req.tool_choice === "none" ? { type: "none" } : req.tool_choice !== "required" && typeof req.tool_choice === "object" ? { type: "tool", name: req.tool_choice.function.name, } : undefined let thinking: Anthropic.ThinkingConfigParam = undefined const reasoningEfforts = providerFeatures(provider)?.reasoningEfforts const budget_tokens = reasoningEfforts[req.reasoning_effort || reasoningEffort] let max_tokens = req.max_tokens if (budget_tokens && (!max_tokens || max_tokens < budget_tokens)) max_tokens = budget_tokens + ANTHROPIC_MAX_TOKEN max_tokens = max_tokens || ANTHROPIC_MAX_TOKEN if (budget_tokens) { temperature = undefined top_p = undefined thinking = { type: "enabled", budget_tokens, } } const messages = convertMessages(req.messages, !!thinking) const mreq: Anthropic.Beta.MessageCreateParams = deleteUndefinedValues({ model, tools, messages, max_tokens, temperature, top_p, tool_choice, thinking, stream: true, }) // https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#extended-output-capabilities-beta if (/claude-3-7-sonnet/.test(model) && max_tokens >= 128000) { dbg("enabling 128k output") mreq.betas = ["output-128k-2025-02-19"] } dbgMessages(`messages: %O`, messages) trace.detailsFenced("✉️ body", mreq, "json") trace.appendContent("\n") try { const stream = messagesApi.stream({ ...mreq, ...headers }) for await (const chunk of stream) { if (cancellationToken?.isCancellationRequested) { finishReason = "cancel" break } dbg(chunk.type) dbgMessages(`%O`, chunk) let chunkContent = "" let reasoningContent = "" switch (chunk.type) { case "message_start": usage = convertUsage( chunk.message.usage as Anthropic.Usage ) break case "content_block_start": if (chunk.content_block.type === "tool_use") { toolCalls[chunk.index] = { id: chunk.content_block.id, name: chunk.content_block.name, arguments: "", } } break case "content_block_delta": switch (chunk.delta.type) { case "signature_delta": signature = chunk.delta.signature break case "thinking_delta": reasoningContent = chunk.delta.thinking trace.appendToken(reasoningContent) reasoningChatResp += reasoningContent trace.appendToken(chunkContent) break case "text_delta": if (!chunk.delta.text) dbg(`empty text_delta`, chunk) else { chunkContent = chunk.delta.text numTokens += approximateTokens( chunkContent, { encoder } ) chatResp += chunkContent trace.appendToken(chunkContent) } break case "input_json_delta": toolCalls[chunk.index].arguments += chunk.delta.partial_json } break case "content_block_stop": { break } case "message_delta": if (chunk.delta.stop_reason) { finishReason = convertFinishReason( chunk.delta.stop_reason ) } if (chunk.usage) { usage = adjustUsage(usage, chunk.usage) } break case "message_stop": { break } } if (chunkContent || reasoningContent) { const progress = deleteUndefinedValues({ responseSoFar: chatResp, reasoningSoFar: reasoningContent, tokensSoFar: numTokens, responseChunk: chunkContent, reasoningChunk: reasoningContent, inner, } satisfies ChatCompletionsProgressReport) partialCb?.(progress) } } } catch (e) { finishReason = "fail" logError(e) trace.error("error while processing event", serializeError(e)) } trace.appendContent("\n\n") trace.itemValue(`🏁 finish reason`, finishReason) if (usage?.total_tokens) { trace.itemValue( `🪙 tokens`, `${usage.total_tokens} total, ${usage.prompt_tokens} prompt, ${usage.completion_tokens} completion` ) } return { text: chatResp, reasoning: reasoningChatResp, signature, finishReason, usage, model, toolCalls: toolCalls.filter((x) => x !== undefined), } satisfies ChatCompletionResponse } return completion } const listModels: ListModelsFunction = async (cfg, options) => { try { const Anthropic = (await import("@anthropic-ai/sdk")).default const anthropic = new Anthropic({ baseURL: cfg.base, apiKey: cfg.token, fetch, }) // Parse and format the response into LanguageModelInfo objects const res = await anthropic.models.list({ limit: 999 }) return { ok: true, models: res.data .filter(({ type }) => type === "model") .map( (model) => ({ id: model.id, details: model.display_name, }) satisfies LanguageModelInfo ), } } catch (e) { return { ok: false, error: serializeError(e) } } } export const AnthropicModel = Object.freeze<LanguageModel>({ completer: completerFactory(async (trace, cfg, httpAgent, fetch) => { const Anthropic = (await import("@anthropic-ai/sdk")).default const anthropic = new Anthropic({ baseURL: cfg.base, apiKey: cfg.token, fetch, fetchOptions: { dispatcher: httpAgent, } as RequestInit as any, }) if (anthropic.baseURL) trace.itemValue( `url`, `[${anthropic.baseURL}](${anthropic.baseURL})` ) const messagesApi = anthropic.beta.messages return messagesApi }), id: MODEL_PROVIDER_ANTHROPIC, listModels, }) export const AnthropicBedrockModel = Object.freeze<LanguageModel>({ completer: completerFactory(async (trace, cfg, httpAgent, fetch) => { const AnthropicBedrock = (await import("@anthropic-ai/bedrock-sdk")) .AnthropicBedrock const anthropic = new AnthropicBedrock({ baseURL: cfg.base, fetch, fetchOptions: { dispatcher: httpAgent, } as RequestInit as any, }) if (anthropic.baseURL) trace.itemValue( `url`, `[${anthropic.baseURL}](${anthropic.baseURL})` ) return anthropic.beta.messages }), id: MODEL_PROVIDER_ANTHROPIC_BEDROCK, listModels: async () => { return { ok: true, models: [ { id: "anthropic.claude-3-7-sonnet-20250219-v1:0", details: "Claude 3.7 Sonnet", }, { id: "anthropic.claude-3-5-haiku-20241022-v1:0", details: "Claude 3.5 Haiku", }, { id: "anthropic.claude-3-5-sonnet-20241022-v2:0", details: "Claude 3.5 Sonnet v2", }, { id: "anthropic.claude-3-5-sonnet-20240620-v1:0", details: "Claude 3.5 Sonnet", }, { id: "anthropic.claude-3-opus-20240229-v1:0", details: "Claude 3 Opus", }, { id: "anthropic.claude-3-sonnet-20240229-v1:0", details: "Claude 3 Sonnet", }, { id: "anthropic.claude-3-haiku-20240307-v1:0", details: "Claude 3 Haiku", }, ], } }, })

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/microsoft/genaiscript'

If you have feedback or need assistance with the MCP directory API, please join our Discord server