mirror of
https://github.com/willchen96/mike.git
synced 2026-06-20 21:18:07 +02:00
159 lines
5.2 KiB
TypeScript
159 lines
5.2 KiB
TypeScript
import Anthropic from "@anthropic-ai/sdk";
|
|
import type { Tool } from "@anthropic-ai/sdk/resources/messages/messages";
|
|
import type {
|
|
StreamChatParams,
|
|
StreamChatResult,
|
|
NormalizedToolCall,
|
|
NormalizedToolResult,
|
|
} from "./types";
|
|
import { toClaudeTools } from "./tools";
|
|
|
|
type ContentBlock =
|
|
| { type: "text"; text: string }
|
|
| { type: "tool_use"; id: string; name: string; input: unknown }
|
|
| { type: string; [key: string]: unknown };
|
|
|
|
type NativeMessage = {
|
|
role: "user" | "assistant";
|
|
content: string | ContentBlock[];
|
|
};
|
|
|
|
const MAX_TOKENS = 16384;
|
|
|
|
function client(override?: string | null): Anthropic {
|
|
const apiKey = override?.trim() || process.env.ANTHROPIC_API_KEY || "";
|
|
return new Anthropic({ apiKey });
|
|
}
|
|
|
|
function toNativeMessages(
|
|
messages: StreamChatParams["messages"],
|
|
): NativeMessage[] {
|
|
return messages.map((m) => ({ role: m.role, content: m.content }));
|
|
}
|
|
|
|
export async function streamClaude(
|
|
params: StreamChatParams,
|
|
): Promise<StreamChatResult> {
|
|
const {
|
|
model,
|
|
systemPrompt,
|
|
tools = [],
|
|
callbacks = {},
|
|
runTools,
|
|
apiKeys,
|
|
enableThinking,
|
|
} = params;
|
|
const maxIter = params.maxIterations ?? 10;
|
|
const anthropic = client(apiKeys?.claude);
|
|
const claudeTools = toClaudeTools(tools);
|
|
|
|
const messages: NativeMessage[] = toNativeMessages(params.messages);
|
|
let fullText = "";
|
|
|
|
for (let iter = 0; iter < maxIter; iter++) {
|
|
const stream = anthropic.messages.stream({
|
|
model,
|
|
system: systemPrompt,
|
|
messages: messages as Anthropic.MessageParam[],
|
|
tools: claudeTools.length
|
|
? (claudeTools as unknown as Tool[])
|
|
: undefined,
|
|
max_tokens: MAX_TOKENS,
|
|
// Claude 4.x models require `thinking.type: "adaptive"` and
|
|
// drive effort via `output_config.effort` rather than a fixed
|
|
// token budget. We only opt in when the caller requested it.
|
|
...(enableThinking
|
|
? ({
|
|
thinking: { type: "adaptive" },
|
|
output_config: { effort: "high" },
|
|
} as unknown as Record<string, unknown>)
|
|
: {}),
|
|
// Extended thinking requires temperature to be default (omitted).
|
|
});
|
|
|
|
let sawThinking = false;
|
|
|
|
stream.on("text", (delta) => {
|
|
callbacks.onContentDelta?.(delta);
|
|
});
|
|
if (enableThinking) {
|
|
stream.on("thinking", (delta) => {
|
|
sawThinking = true;
|
|
callbacks.onReasoningDelta?.(delta);
|
|
});
|
|
}
|
|
|
|
const final = await stream.finalMessage();
|
|
if (sawThinking) callbacks.onReasoningBlockEnd?.();
|
|
const stopReason = final.stop_reason;
|
|
const assistantBlocks = final.content as ContentBlock[];
|
|
|
|
// Extract text content and tool_use calls from the final assistant
|
|
// message so we can accumulate text and drive the tool-call loop.
|
|
const toolCalls: NormalizedToolCall[] = [];
|
|
for (const block of assistantBlocks) {
|
|
if (block.type === "text") {
|
|
const txt = (block as { text: string }).text;
|
|
if (typeof txt === "string") fullText += txt;
|
|
} else if (block.type === "tool_use") {
|
|
const tu = block as {
|
|
id: string;
|
|
name: string;
|
|
input: unknown;
|
|
};
|
|
const call: NormalizedToolCall = {
|
|
id: tu.id,
|
|
name: tu.name,
|
|
input: (tu.input as Record<string, unknown>) ?? {},
|
|
};
|
|
callbacks.onToolCallStart?.(call);
|
|
toolCalls.push(call);
|
|
}
|
|
}
|
|
|
|
if (stopReason !== "tool_use" || !toolCalls.length || !runTools) {
|
|
break;
|
|
}
|
|
|
|
const results = await runTools(toolCalls);
|
|
|
|
// Record the assistant turn (preserving the original content blocks,
|
|
// which Claude requires on the follow-up) and the user turn that
|
|
// carries the tool_result blocks.
|
|
messages.push({ role: "assistant", content: assistantBlocks });
|
|
messages.push({
|
|
role: "user",
|
|
content: results.map((r) => ({
|
|
type: "tool_result",
|
|
tool_use_id: r.tool_use_id,
|
|
content: r.content,
|
|
})),
|
|
});
|
|
}
|
|
|
|
return { fullText };
|
|
}
|
|
|
|
export async function completeClaudeText(params: {
|
|
model: string;
|
|
systemPrompt?: string;
|
|
user: string;
|
|
maxTokens?: number;
|
|
apiKeys?: { claude?: string | null };
|
|
}): Promise<string> {
|
|
const anthropic = client(params.apiKeys?.claude);
|
|
const resp = await anthropic.messages.create({
|
|
model: params.model,
|
|
max_tokens: params.maxTokens ?? 512,
|
|
system: params.systemPrompt,
|
|
messages: [{ role: "user", content: params.user }],
|
|
});
|
|
const text = resp.content
|
|
.filter((b): b is Anthropic.TextBlock => b.type === "text")
|
|
.map((b) => b.text)
|
|
.join("");
|
|
return text;
|
|
}
|
|
|
|
// Helper re-export for callers wanting to hand normalized results back in.
|
|
export type { NormalizedToolResult };
|