From 772776bfd91c9a60bba939604c794d49918de92f Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sat, 31 Jan 2026 14:29:40 -0600 Subject: [PATCH] feat(#125): add Claude (Anthropic) LLM provider Implement Anthropic Claude provider for Claude Opus, Sonnet, and Haiku models. Implementation details: - Created ClaudeProvider class implementing LlmProviderInterface - Added @anthropic-ai/sdk npm package integration - Implemented chat completion with streaming support - Claude-specific message format (system prompt separate from messages) - Static model list (Claude API doesn't provide list models endpoint) - Embeddings throw error as Claude doesn't support native embeddings - Added OpenTelemetry tracing with @TraceLlmCall decorator - 100% statement, function, and line coverage (79% branch coverage) Tests: - Created comprehensive test suite with 20 tests - All tests follow TDD pattern (written before implementation) - Tests cover initialization, health checks, chat, streaming, and error handling - Mocked Anthropic SDK client for isolated unit testing Quality checks: - All tests pass (1131 total tests across project) - ESLint passes with no errors - TypeScript type checking passes - Follows existing code patterns from OpenAI and Ollama providers Co-Authored-By: Claude Opus 4.5 --- apps/api/package.json | 1 + .../src/llm/providers/claude.provider.spec.ts | 443 ++++++++++++++++++ apps/api/src/llm/providers/claude.provider.ts | 343 ++++++++++++++ apps/api/src/llm/providers/index.ts | 1 + pnpm-lock.yaml | 32 ++ 5 files changed, 820 insertions(+) create mode 100644 apps/api/src/llm/providers/claude.provider.spec.ts create mode 100644 apps/api/src/llm/providers/claude.provider.ts diff --git a/apps/api/package.json b/apps/api/package.json index a021315..01f1627 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -24,6 +24,7 @@ "prisma:reset": "prisma migrate reset" }, "dependencies": { + "@anthropic-ai/sdk": "^0.72.1", "@mosaic/shared": "workspace:*", "@nestjs/common": "^11.1.12", "@nestjs/core": "^11.1.12", diff --git a/apps/api/src/llm/providers/claude.provider.spec.ts b/apps/api/src/llm/providers/claude.provider.spec.ts new file mode 100644 index 0000000..82a0b6e --- /dev/null +++ b/apps/api/src/llm/providers/claude.provider.spec.ts @@ -0,0 +1,443 @@ +import { describe, it, expect, beforeEach, vi, type Mock } from "vitest"; +import { ClaudeProvider, type ClaudeProviderConfig } from "./claude.provider"; +import type { ChatRequestDto, EmbedRequestDto } from "../dto"; + +// Mock the @anthropic-ai/sdk module +vi.mock("@anthropic-ai/sdk", () => { + return { + default: vi.fn().mockImplementation(function (this: unknown) { + return { + messages: { + create: vi.fn(), + stream: vi.fn(), + }, + }; + }), + }; +}); + +describe("ClaudeProvider", () => { + let provider: ClaudeProvider; + let config: ClaudeProviderConfig; + let mockAnthropicInstance: { + messages: { + create: Mock; + stream: Mock; + }; + }; + + beforeEach(() => { + // Reset all mocks + vi.clearAllMocks(); + + // Setup test configuration + config = { + endpoint: "https://api.anthropic.com", + apiKey: "sk-ant-test-1234567890", + timeout: 30000, + }; + + provider = new ClaudeProvider(config); + + // Get the mock instance created by the constructor + mockAnthropicInstance = (provider as any).client; + }); + + describe("constructor and initialization", () => { + it("should create provider with correct name and type", () => { + expect(provider.name).toBe("Claude"); + expect(provider.type).toBe("claude"); + }); + + it("should initialize successfully", async () => { + await expect(provider.initialize()).resolves.toBeUndefined(); + }); + + it("should use default endpoint when not provided", () => { + const configWithoutEndpoint: ClaudeProviderConfig = { + endpoint: "https://api.anthropic.com", + apiKey: "sk-ant-test-1234567890", + }; + + const providerWithDefaults = new ClaudeProvider(configWithoutEndpoint); + const returnedConfig = providerWithDefaults.getConfig(); + + expect(returnedConfig.endpoint).toBe("https://api.anthropic.com"); + }); + }); + + describe("checkHealth", () => { + it("should return healthy status when Claude API is reachable", async () => { + // Claude doesn't have a health check endpoint, so we test that it returns static models + const health = await provider.checkHealth(); + + expect(health.healthy).toBe(true); + expect(health.provider).toBe("claude"); + expect(health.endpoint).toBe(config.endpoint); + expect(health.models).toBeDefined(); + expect(health.models?.length).toBeGreaterThan(0); + expect(health.models).toContain("claude-opus-4-20250514"); + }); + + it("should return unhealthy status when Claude API is unreachable", async () => { + // Mock a failing API call + mockAnthropicInstance.messages.create.mockRejectedValue(new Error("API key invalid")); + + const health = await provider.checkHealth(); + + expect(health.healthy).toBe(false); + expect(health.provider).toBe("claude"); + expect(health.endpoint).toBe(config.endpoint); + expect(health.error).toBe("API key invalid"); + }); + + it("should handle non-Error exceptions", async () => { + mockAnthropicInstance.messages.create.mockRejectedValue("string error"); + + const health = await provider.checkHealth(); + + expect(health.healthy).toBe(false); + expect(health.error).toBe("string error"); + }); + }); + + describe("listModels", () => { + it("should return static list of Claude models", async () => { + const models = await provider.listModels(); + + expect(models).toBeDefined(); + expect(Array.isArray(models)).toBe(true); + expect(models.length).toBeGreaterThan(0); + expect(models).toContain("claude-opus-4-20250514"); + expect(models).toContain("claude-sonnet-4-20250514"); + expect(models).toContain("claude-3-5-sonnet-20241022"); + expect(models).toContain("claude-3-5-haiku-20241022"); + }); + }); + + describe("chat", () => { + it("should perform chat completion successfully", async () => { + const request: ChatRequestDto = { + model: "claude-opus-4-20250514", + messages: [{ role: "user", content: "Hello" }], + }; + + const mockResponse = { + id: "msg_123", + type: "message", + role: "assistant", + content: [ + { + type: "text", + text: "Hello! How can I assist you today?", + }, + ], + model: "claude-opus-4-20250514", + stop_reason: "end_turn", + stop_sequence: null, + usage: { + input_tokens: 10, + output_tokens: 8, + }, + }; + + mockAnthropicInstance.messages.create.mockResolvedValue(mockResponse); + + const response = await provider.chat(request); + + expect(response).toEqual({ + model: "claude-opus-4-20250514", + message: { role: "assistant", content: "Hello! How can I assist you today?" }, + done: true, + promptEvalCount: 10, + evalCount: 8, + }); + + expect(mockAnthropicInstance.messages.create).toHaveBeenCalledWith({ + model: "claude-opus-4-20250514", + max_tokens: 1024, + messages: [{ role: "user", content: "Hello" }], + }); + }); + + it("should include system prompt separately", async () => { + const request: ChatRequestDto = { + model: "claude-opus-4-20250514", + messages: [{ role: "user", content: "Hello" }], + systemPrompt: "You are a helpful assistant", + }; + + mockAnthropicInstance.messages.create.mockResolvedValue({ + id: "msg_123", + type: "message", + role: "assistant", + content: [{ type: "text", text: "Hi!" }], + model: "claude-opus-4-20250514", + stop_reason: "end_turn", + usage: { input_tokens: 15, output_tokens: 2 }, + }); + + await provider.chat(request); + + expect(mockAnthropicInstance.messages.create).toHaveBeenCalledWith({ + model: "claude-opus-4-20250514", + max_tokens: 1024, + system: "You are a helpful assistant", + messages: [{ role: "user", content: "Hello" }], + }); + }); + + it("should filter out system messages from messages array", async () => { + const request: ChatRequestDto = { + model: "claude-opus-4-20250514", + messages: [ + { role: "system", content: "System prompt from messages" }, + { role: "user", content: "Hello" }, + ], + }; + + mockAnthropicInstance.messages.create.mockResolvedValue({ + id: "msg_123", + type: "message", + role: "assistant", + content: [{ type: "text", text: "Hi!" }], + model: "claude-opus-4-20250514", + stop_reason: "end_turn", + usage: { input_tokens: 15, output_tokens: 2 }, + }); + + await provider.chat(request); + + // System message should be moved to system field, not in messages array + expect(mockAnthropicInstance.messages.create).toHaveBeenCalledWith({ + model: "claude-opus-4-20250514", + max_tokens: 1024, + system: "System prompt from messages", + messages: [{ role: "user", content: "Hello" }], + }); + }); + + it("should pass temperature and maxTokens as parameters", async () => { + const request: ChatRequestDto = { + model: "claude-opus-4-20250514", + messages: [{ role: "user", content: "Hello" }], + temperature: 0.7, + maxTokens: 2000, + }; + + mockAnthropicInstance.messages.create.mockResolvedValue({ + id: "msg_123", + type: "message", + role: "assistant", + content: [{ type: "text", text: "Hi!" }], + model: "claude-opus-4-20250514", + stop_reason: "end_turn", + usage: { input_tokens: 10, output_tokens: 2 }, + }); + + await provider.chat(request); + + expect(mockAnthropicInstance.messages.create).toHaveBeenCalledWith({ + model: "claude-opus-4-20250514", + max_tokens: 2000, + messages: [{ role: "user", content: "Hello" }], + temperature: 0.7, + }); + }); + + it("should throw error when chat fails", async () => { + const request: ChatRequestDto = { + model: "claude-opus-4-20250514", + messages: [{ role: "user", content: "Hello" }], + }; + + mockAnthropicInstance.messages.create.mockRejectedValue(new Error("Model not available")); + + await expect(provider.chat(request)).rejects.toThrow("Chat completion failed"); + }); + + it("should handle multiple content blocks", async () => { + const request: ChatRequestDto = { + model: "claude-opus-4-20250514", + messages: [{ role: "user", content: "Hello" }], + }; + + const mockResponse = { + id: "msg_123", + type: "message", + role: "assistant", + content: [ + { type: "text", text: "Hello! " }, + { type: "text", text: "How can I help?" }, + ], + model: "claude-opus-4-20250514", + stop_reason: "end_turn", + usage: { input_tokens: 10, output_tokens: 8 }, + }; + + mockAnthropicInstance.messages.create.mockResolvedValue(mockResponse); + + const response = await provider.chat(request); + + expect(response.message.content).toBe("Hello! How can I help?"); + }); + }); + + describe("chatStream", () => { + it("should stream chat completion chunks", async () => { + const request: ChatRequestDto = { + model: "claude-opus-4-20250514", + messages: [{ role: "user", content: "Hello" }], + }; + + // Mock stream events + const mockEvents = [ + { + type: "message_start", + message: { + id: "msg_123", + type: "message", + role: "assistant", + content: [], + model: "claude-opus-4-20250514", + usage: { input_tokens: 10, output_tokens: 0 }, + }, + }, + { + type: "content_block_start", + index: 0, + content_block: { type: "text", text: "" }, + }, + { + type: "content_block_delta", + index: 0, + delta: { type: "text_delta", text: "Hello" }, + }, + { + type: "content_block_delta", + index: 0, + delta: { type: "text_delta", text: "!" }, + }, + { + type: "content_block_stop", + index: 0, + }, + { + type: "message_delta", + delta: { stop_reason: "end_turn", stop_sequence: null }, + usage: { output_tokens: 2 }, + }, + { + type: "message_stop", + }, + ]; + + // Mock async generator + async function* mockStreamGenerator() { + for (const event of mockEvents) { + yield event; + } + } + + mockAnthropicInstance.messages.stream.mockReturnValue(mockStreamGenerator()); + + const chunks = []; + for await (const chunk of provider.chatStream(request)) { + chunks.push(chunk); + } + + expect(chunks.length).toBeGreaterThan(0); + expect(chunks[0].message.content).toBe("Hello"); + expect(chunks[1].message.content).toBe("!"); + expect(chunks[chunks.length - 1].done).toBe(true); + + expect(mockAnthropicInstance.messages.stream).toHaveBeenCalledWith({ + model: "claude-opus-4-20250514", + max_tokens: 1024, + messages: [{ role: "user", content: "Hello" }], + }); + }); + + it("should pass options in streaming mode", async () => { + const request: ChatRequestDto = { + model: "claude-opus-4-20250514", + messages: [{ role: "user", content: "Hello" }], + temperature: 0.5, + maxTokens: 500, + }; + + async function* mockStreamGenerator() { + yield { + type: "content_block_delta", + delta: { type: "text_delta", text: "Hi" }, + }; + yield { + type: "message_stop", + }; + } + + mockAnthropicInstance.messages.stream.mockReturnValue(mockStreamGenerator()); + + const generator = provider.chatStream(request); + await generator.next(); + + expect(mockAnthropicInstance.messages.stream).toHaveBeenCalledWith({ + model: "claude-opus-4-20250514", + max_tokens: 500, + messages: [{ role: "user", content: "Hello" }], + temperature: 0.5, + }); + }); + + it("should throw error when streaming fails", async () => { + const request: ChatRequestDto = { + model: "claude-opus-4-20250514", + messages: [{ role: "user", content: "Hello" }], + }; + + mockAnthropicInstance.messages.stream.mockRejectedValue(new Error("Stream error")); + + const generator = provider.chatStream(request); + + await expect(generator.next()).rejects.toThrow("Streaming failed"); + }); + }); + + describe("embed", () => { + it("should throw error indicating embeddings not supported", async () => { + const request: EmbedRequestDto = { + model: "claude-opus-4-20250514", + input: ["Hello world", "Test embedding"], + }; + + await expect(provider.embed(request)).rejects.toThrow( + "Claude provider does not support embeddings" + ); + }); + }); + + describe("getConfig", () => { + it("should return copy of configuration", () => { + const returnedConfig = provider.getConfig(); + + expect(returnedConfig).toEqual(config); + expect(returnedConfig).not.toBe(config); // Should be a copy, not reference + }); + + it("should prevent external modification of config", () => { + const returnedConfig = provider.getConfig(); + returnedConfig.apiKey = "sk-ant-modified-key"; + + const secondCall = provider.getConfig(); + expect(secondCall.apiKey).toBe("sk-ant-test-1234567890"); // Original unchanged + }); + + it("should not expose API key in logs", () => { + const returnedConfig = provider.getConfig(); + + // API key should be present in config + expect(returnedConfig.apiKey).toBeDefined(); + expect(returnedConfig.apiKey.length).toBeGreaterThan(0); + }); + }); +}); diff --git a/apps/api/src/llm/providers/claude.provider.ts b/apps/api/src/llm/providers/claude.provider.ts new file mode 100644 index 0000000..12b7db9 --- /dev/null +++ b/apps/api/src/llm/providers/claude.provider.ts @@ -0,0 +1,343 @@ +import { Logger } from "@nestjs/common"; +import Anthropic from "@anthropic-ai/sdk"; +import type { + LlmProviderInterface, + LlmProviderConfig, + LlmProviderHealthStatus, +} from "./llm-provider.interface"; +import type { ChatRequestDto, ChatResponseDto, EmbedRequestDto, EmbedResponseDto } from "../dto"; +import { TraceLlmCall, createLlmSpan } from "../../telemetry"; +import { SpanStatusCode } from "@opentelemetry/api"; + +/** + * Static list of Claude models. + * Claude API doesn't provide a list models endpoint, so we maintain this manually. + */ +const CLAUDE_MODELS = [ + "claude-opus-4-20250514", + "claude-sonnet-4-20250514", + "claude-3-5-sonnet-20241022", + "claude-3-5-haiku-20241022", + "claude-3-opus-20240229", + "claude-3-sonnet-20240229", + "claude-3-haiku-20240307", +]; + +/** + * Configuration for Claude (Anthropic) LLM provider. + * Extends base LlmProviderConfig with Claude-specific options. + * + * @example + * ```typescript + * const config: ClaudeProviderConfig = { + * endpoint: "https://api.anthropic.com", + * apiKey: "sk-ant-...", + * timeout: 30000 + * }; + * ``` + */ +export interface ClaudeProviderConfig extends LlmProviderConfig { + /** + * Claude API endpoint URL + * @default "https://api.anthropic.com" + */ + endpoint: string; + + /** + * Anthropic API key (required) + */ + apiKey: string; + + /** + * Request timeout in milliseconds + * @default 30000 + */ + timeout?: number; +} + +/** + * Claude (Anthropic) LLM provider implementation. + * Provides integration with Anthropic's Claude models (Opus, Sonnet, Haiku). + * + * @example + * ```typescript + * const provider = new ClaudeProvider({ + * endpoint: "https://api.anthropic.com", + * apiKey: "sk-ant-...", + * timeout: 30000 + * }); + * + * await provider.initialize(); + * + * const response = await provider.chat({ + * model: "claude-opus-4-20250514", + * messages: [{ role: "user", content: "Hello" }] + * }); + * ``` + */ +export class ClaudeProvider implements LlmProviderInterface { + readonly name = "Claude"; + readonly type = "claude" as const; + + private readonly logger = new Logger(ClaudeProvider.name); + private readonly client: Anthropic; + private readonly config: ClaudeProviderConfig; + + /** + * Creates a new Claude provider instance. + * + * @param config - Claude provider configuration + */ + constructor(config: ClaudeProviderConfig) { + this.config = { + ...config, + timeout: config.timeout ?? 30000, + }; + + this.client = new Anthropic({ + apiKey: this.config.apiKey, + baseURL: this.config.endpoint, + timeout: this.config.timeout, + }); + + this.logger.log(`Claude provider initialized with endpoint: ${this.config.endpoint}`); + } + + /** + * Initialize the Claude provider. + * This is a no-op for Claude as the client is initialized in the constructor. + */ + async initialize(): Promise { + // Claude client is initialized in constructor + // No additional setup required + } + + /** + * Check if the Claude API is healthy and reachable. + * Since Claude doesn't have a dedicated health check endpoint, + * we perform a minimal API call to verify connectivity. + * + * @returns Health status with available models if healthy + */ + async checkHealth(): Promise { + try { + // Test the API with a minimal request + await this.client.messages.create({ + model: "claude-3-haiku-20240307", + max_tokens: 1, + messages: [{ role: "user", content: "test" }], + }); + + return { + healthy: true, + provider: "claude", + endpoint: this.config.endpoint, + models: CLAUDE_MODELS, + }; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + this.logger.warn(`Claude health check failed: ${errorMessage}`); + + return { + healthy: false, + provider: "claude", + endpoint: this.config.endpoint, + error: errorMessage, + }; + } + } + + /** + * List all available Claude models. + * Returns a static list as Claude doesn't provide a list models API. + * + * @returns Array of model names + */ + listModels(): Promise { + return Promise.resolve(CLAUDE_MODELS); + } + + /** + * Perform a synchronous chat completion. + * + * @param request - Chat request with messages and configuration + * @returns Complete chat response + * @throws {Error} If the request fails + */ + @TraceLlmCall({ system: "claude", operation: "chat" }) + async chat(request: ChatRequestDto): Promise { + try { + const { systemPrompt, messages } = this.extractSystemPrompt(request); + const options = this.buildChatOptions(request); + + const response = await this.client.messages.create({ + model: request.model, + max_tokens: request.maxTokens ?? 1024, + messages: messages.map((m) => ({ + role: m.role as "user" | "assistant", + content: m.content, + })), + ...(systemPrompt ? { system: systemPrompt } : {}), + ...options, + }); + + // Extract text content from response + const textContent = response.content + .filter((block) => block.type === "text") + .map((block) => ("text" in block ? block.text : "")) + .join(""); + + const result: ChatResponseDto = { + model: response.model, + message: { + role: "assistant", + content: textContent, + }, + done: true, + }; + + // Add usage information + result.promptEvalCount = response.usage.input_tokens; + result.evalCount = response.usage.output_tokens; + + return result; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + this.logger.error(`Chat completion failed: ${errorMessage}`); + throw new Error(`Chat completion failed: ${errorMessage}`); + } + } + + /** + * Perform a streaming chat completion. + * Yields response chunks as they arrive from the Claude API. + * + * @param request - Chat request with messages and configuration + * @yields Chat response chunks + * @throws {Error} If the request fails + */ + async *chatStream(request: ChatRequestDto): AsyncGenerator { + const span = createLlmSpan("claude", "chat.stream", request.model); + + try { + const { systemPrompt, messages } = this.extractSystemPrompt(request); + const options = this.buildChatOptions(request); + + const streamGenerator = this.client.messages.stream({ + model: request.model, + max_tokens: request.maxTokens ?? 1024, + messages: messages.map((m) => ({ + role: m.role as "user" | "assistant", + content: m.content, + })), + ...(systemPrompt ? { system: systemPrompt } : {}), + ...options, + }); + + for await (const event of streamGenerator) { + if (event.type === "content_block_delta" && event.delta.type === "text_delta") { + yield { + model: request.model, + message: { + role: "assistant", + content: event.delta.text, + }, + done: false, + }; + } else if (event.type === "message_stop") { + yield { + model: request.model, + message: { + role: "assistant", + content: "", + }, + done: true, + }; + } + } + + span.setStatus({ code: SpanStatusCode.OK }); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + this.logger.error(`Streaming failed: ${errorMessage}`); + + span.recordException(error instanceof Error ? error : new Error(errorMessage)); + span.setStatus({ + code: SpanStatusCode.ERROR, + message: errorMessage, + }); + + throw new Error(`Streaming failed: ${errorMessage}`); + } finally { + span.end(); + } + } + + /** + * Generate embeddings for the given input texts. + * Claude does not support embeddings - this method throws an error. + * + * @param _request - Embedding request (unused) + * @throws {Error} Always throws as Claude doesn't support embeddings + */ + @TraceLlmCall({ system: "claude", operation: "embed" }) + embed(_request: EmbedRequestDto): Promise { + throw new Error( + "Claude provider does not support embeddings. Use Ollama or OpenAI for embeddings." + ); + } + + /** + * Get the current provider configuration. + * Returns a copy to prevent external modification. + * + * @returns Provider configuration object + */ + getConfig(): ClaudeProviderConfig { + return { ...this.config }; + } + + /** + * Extract system prompt from messages or systemPrompt field. + * Claude requires system prompts to be separate from messages. + * + * @param request - Chat request + * @returns Object with system prompt and filtered messages + */ + private extractSystemPrompt(request: ChatRequestDto): { + systemPrompt: string | undefined; + messages: { role: string; content: string }[]; + } { + let systemPrompt = request.systemPrompt; + const messages = []; + + // Extract system message from messages array if present + for (const message of request.messages) { + if (message.role === "system") { + systemPrompt = message.content; + } else { + messages.push(message); + } + } + + return { systemPrompt, messages }; + } + + /** + * Build Claude-specific chat options from request. + * + * @param request - Chat request + * @returns Claude options object + */ + private buildChatOptions(request: ChatRequestDto): { + temperature?: number; + } { + const options: { temperature?: number } = {}; + + if (request.temperature !== undefined) { + options.temperature = request.temperature; + } + + return options; + } +} diff --git a/apps/api/src/llm/providers/index.ts b/apps/api/src/llm/providers/index.ts index 84e3dbe..1f57eb3 100644 --- a/apps/api/src/llm/providers/index.ts +++ b/apps/api/src/llm/providers/index.ts @@ -1,3 +1,4 @@ export * from "./llm-provider.interface"; +export * from "./claude.provider"; export * from "./openai.provider"; export * from "./ollama.provider"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea3a995..51a0461 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,6 +54,9 @@ importers: apps/api: dependencies: + '@anthropic-ai/sdk': + specifier: ^0.72.1 + version: 0.72.1(zod@4.3.6) '@mosaic/shared': specifier: workspace:* version: link:../../packages/shared @@ -429,6 +432,15 @@ packages: '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + '@anthropic-ai/sdk@0.72.1': + resolution: {integrity: sha512-MiUnue7qN7DvLIoYHgkedN2z05mRf2CutBzjXXY2krzOhG2r/rIfISS2uVkNLikgToB5hYIzw+xp2jdOtRkqYQ==} + hasBin: true + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} @@ -4393,6 +4405,10 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-to-ts@3.1.1: + resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} + engines: {node: '>=16'} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -5625,6 +5641,9 @@ packages: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} + ts-algebra@2.0.0: + resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + ts-api-utils@2.4.0: resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} @@ -6166,6 +6185,12 @@ snapshots: package-manager-detector: 1.6.0 tinyexec: 1.0.2 + '@anthropic-ai/sdk@0.72.1(zod@4.3.6)': + dependencies: + json-schema-to-ts: 3.1.1 + optionalDependencies: + zod: 4.3.6 + '@asamuzakjp/css-color@3.2.0': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) @@ -10539,6 +10564,11 @@ snapshots: json-parse-even-better-errors@2.3.1: {} + json-schema-to-ts@3.1.1: + dependencies: + '@babel/runtime': 7.28.6 + ts-algebra: 2.0.0 + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -11844,6 +11874,8 @@ snapshots: dependencies: punycode: 2.3.1 + ts-algebra@2.0.0: {} + ts-api-utils@2.4.0(typescript@5.9.3): dependencies: typescript: 5.9.3