import Anthropic from "@anthropic-ai/sdk"; // A Claude subscription (Pro/Max) does not include Anthropic API access — that's // billed separately at console.anthropic.com. NGC_API_KEY routes chat calls // through NVIDIA's hosted NIM catalog instead. Verified directly (2026-07-13): // the hosted https://integrate.api.nvidia.com service speaks OpenAI's // /v1/chat/completions format — its /v1/messages (Anthropic-compatible) path // 404s, so that compatibility mode is apparently a self-hosted NIM container // feature, not available on the shared hosted API. Do not switch back to the // Anthropic SDK against NVIDIA's endpoint without re-verifying. const NIM_CHAT_URL = "https://integrate.api.nvidia.com/v1/chat/completions"; const DEFAULT_NIM_MODEL = "nvidia/nemotron-3-ultra-550b-a55b"; const DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-5"; export const MISSING_KEY_MESSAGE = "No model API key configured on the server. Add NGC_API_KEY (NVIDIA NIM, from build.nvidia.com) or ANTHROPIC_API_KEY as a repository secret to enable this."; export type ChatMessage = { role: "user" | "assistant"; content: string }; export type ToolSchema = { name: string; description: string; input_schema: Record; }; export type ChatResult = { text?: string; toolInput?: unknown }; async function callNim( apiKey: string, model: string, system: string, messages: ChatMessage[], maxTokens: number, tool?: ToolSchema, ): Promise { const body: Record = { model, max_tokens: maxTokens, messages: [{ role: "system", content: system }, ...messages], }; if (tool) { body.tools = [ { type: "function", function: { name: tool.name, description: tool.description, parameters: tool.input_schema } }, ]; body.tool_choice = { type: "function", function: { name: tool.name } }; } const res = await fetch(NIM_CHAT_URL, { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!res.ok) { const errText = await res.text().catch(() => res.statusText); throw new Error(`NVIDIA NIM request failed (${res.status}): ${errText.slice(0, 500)}`); } const data = await res.json(); const choice = data.choices?.[0]?.message; if (!choice) throw new Error("NVIDIA NIM returned no choices."); const toolCall = choice.tool_calls?.[0]; if (toolCall) { try { return { toolInput: JSON.parse(toolCall.function.arguments) }; } catch { throw new Error("NVIDIA NIM returned a tool call with unparseable arguments."); } } return { text: choice.content ?? "" }; } async function callAnthropic( apiKey: string, model: string, system: string, messages: ChatMessage[], maxTokens: number, tool?: ToolSchema, ): Promise { const client = new Anthropic({ apiKey }); const response = await client.messages.create({ model, max_tokens: maxTokens, system, messages, ...(tool ? { tools: [tool as unknown as Anthropic.Tool], tool_choice: { type: "tool" as const, name: tool.name }, } : {}), }); if (tool) { const toolUse = response.content.find( (b): b is Anthropic.ToolUseBlock => b.type === "tool_use", ); if (!toolUse) throw new Error("Model did not return a structured result."); return { toolInput: toolUse.input }; } const text = response.content .filter((b): b is Anthropic.TextBlock => b.type === "text") .map((b) => b.text) .join("\n"); return { text }; } export async function callChatModel(opts: { system: string; messages: ChatMessage[]; maxTokens: number; tool?: ToolSchema; }): Promise { const ngcKey = process.env.NGC_API_KEY; if (ngcKey) { const model = process.env.NGC_MODEL || DEFAULT_NIM_MODEL; return callNim(ngcKey, model, opts.system, opts.messages, opts.maxTokens, opts.tool); } const anthropicKey = process.env.ANTHROPIC_API_KEY; if (anthropicKey) { const model = process.env.ANTHROPIC_MODEL || DEFAULT_ANTHROPIC_MODEL; return callAnthropic(anthropicKey, model, opts.system, opts.messages, opts.maxTokens, opts.tool); } throw new Error(MISSING_KEY_MESSAGE); } export function hasModelKey(): boolean { return Boolean(process.env.NGC_API_KEY || process.env.ANTHROPIC_API_KEY); }