trustgraph/ts/packages/flow/src/model/text-completion/claude.ts
2026-06-06 10:33:10 -05:00

116 lines
3.2 KiB
TypeScript

/**
* Anthropic Claude text completion service.
*
* Python reference: trustgraph-flow/trustgraph/model/text_completion/claude/llm.py
*/
import { AnthropicClient, AnthropicLanguageModel } from "@effect/ai-anthropic";
import { NodeRuntime } from "@effect/platform-node";
import {
makeLlmService,
makeFlowProcessorProgram,
makeLlmSpecs,
type Llm,
type LlmProvider,
type ProcessorConfig,
} from "@trustgraph/base";
import { Effect, Layer, Redacted } from "effect";
import { FetchHttpClient } from "effect/unstable/http";
import {
makeLanguageModelProvider,
makeTextCompletionLayer,
optionalStringConfig,
requiredString,
type TextCompletionConfigError,
type TextCompletionRuntimeError,
} from "./common.ts";
export type ClaudeProcessorConfig = ProcessorConfig & {
model?: string;
apiKey?: string;
temperature?: number;
maxOutput?: number;
};
type ResolvedClaudeConfig = {
readonly defaultModel: string;
readonly defaultTemperature: number;
readonly maxOutput: number;
readonly apiKey: string;
};
const loadClaudeConfig = Effect.fn("loadClaudeConfig")(function* (config: ClaudeProcessorConfig) {
const apiKey = yield* requiredString(
config.apiKey ?? (yield* optionalStringConfig("Claude", "CLAUDE_KEY")),
"Claude",
"CLAUDE_KEY",
"Claude API key not specified",
);
return {
defaultModel: config.model ?? "claude-sonnet-4-20250514",
defaultTemperature: config.temperature ?? 0.0,
maxOutput: config.maxOutput ?? 8192,
apiKey,
} satisfies ResolvedClaudeConfig;
});
const makeClaudeLayer = (apiKey: string) =>
AnthropicClient.layer({
apiKey: Redacted.make(apiKey),
}).pipe(
Layer.provide(FetchHttpClient.layer),
);
export function makeClaudeProvider(
config: ClaudeProcessorConfig,
): LlmProvider<TextCompletionRuntimeError> {
return Effect.runSync(Effect.scoped(makeClaudeProviderEffect(config)));
}
export const makeClaudeProviderEffect = Effect.fn("makeClaudeProvider")(function* (
config: ClaudeProcessorConfig,
) {
const resolved = yield* loadClaudeConfig(config);
const context = yield* Layer.build(makeClaudeLayer(resolved.apiKey));
yield* Effect.log("[Claude] LLM service initialized");
return makeLanguageModelProvider({
provider: "Claude",
defaultModel: resolved.defaultModel,
defaultTemperature: resolved.defaultTemperature,
context,
makeLanguageModel: ({ model, temperature }) =>
AnthropicLanguageModel.make({
model,
config: {
max_tokens: resolved.maxOutput,
temperature,
},
}),
});
});
export type ClaudeProcessor = ReturnType<typeof makeClaudeProcessor>;
export function makeClaudeProcessor(
config: ClaudeProcessorConfig,
): ReturnType<typeof makeLlmService> {
return makeLlmService(config, makeClaudeProvider(config));
}
export const ClaudeProcessor = makeClaudeProcessor;
export const program = makeFlowProcessorProgram<
ClaudeProcessorConfig,
TextCompletionConfigError | TextCompletionRuntimeError,
Llm
>({
id: "text-completion",
specs: () => makeLlmSpecs(),
layer: (config) => makeTextCompletionLayer(makeClaudeProviderEffect(config)),
});
export function runMain(): void {
NodeRuntime.runMain(program);
}