LAYER / src /lib /llm-client.ts
Claude
Switch Samantha to NVIDIA NIM (Nemotron 3 Ultra) as primary model
fc3642a unverified
Raw
History Blame Contribute Delete
4.39 kB
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<string, unknown>;
};
export type ChatResult = { text?: string; toolInput?: unknown };
async function callNim(
apiKey: string,
model: string,
system: string,
messages: ChatMessage[],
maxTokens: number,
tool?: ToolSchema,
): Promise<ChatResult> {
const body: Record<string, unknown> = {
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<ChatResult> {
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<ChatResult> {
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);
}