diff --git a/docs/docs/03-configuration/02-different-ai-providers.md b/docs/docs/03-configuration/02-different-ai-providers.md index 518d79bb7..b0bf2c0d0 100644 --- a/docs/docs/03-configuration/02-different-ai-providers.md +++ b/docs/docs/03-configuration/02-different-ai-providers.md @@ -14,6 +14,31 @@ OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # INFERENCE_IMAGE_MODEL=gpt-4o-mini ``` +## Anthropic (Claude) + +Karakeep has a native Anthropic provider (it does **not** go through Anthropic's +OpenAI-compatibility endpoint), which means structured tagging output is enforced +by the API. + +``` +ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxxxxxxxxxxxxxx +# Optional: point at a gateway/proxy instead of api.anthropic.com +# ANTHROPIC_BASE_URL=https://your-gateway.example.com +# Optional: override the default model (claude-haiku-4-5) +INFERENCE_TEXT_MODEL=claude-haiku-4-5 +INFERENCE_IMAGE_MODEL=claude-haiku-4-5 +``` + +Notes: + +- If `ANTHROPIC_API_KEY` is set, the Anthropic provider is used. If + `OPENAI_API_KEY` is also set, OpenAI takes precedence — unset it to use Claude. +- The default model is `claude-haiku-4-5` (cheap and fast, suited to + high-volume auto-tagging). Set `INFERENCE_TEXT_MODEL` / `INFERENCE_IMAGE_MODEL` + to any Claude model (e.g. `claude-sonnet-4-6`) to override. +- **Embeddings are not supported** by Anthropic. Semantic search needs a + separate embedding provider (OpenAI or Ollama). + ## Ollama Ollama is a local LLM provider that you can use to run your own LLM server. You'll need to pass ollama's address to karakeep and you need to ensure that it's accessible from within the karakeep container (e.g. no localhost addresses). diff --git a/packages/shared/config.ts b/packages/shared/config.ts index 2a2b96ba9..74f7b5399 100644 --- a/packages/shared/config.ts +++ b/packages/shared/config.ts @@ -58,6 +58,8 @@ const allEnv = z.object({ TURNSTILE_SITE_KEY: z.string().optional(), TURNSTILE_SECRET_KEY: z.string().optional(), OPENAI_API_KEY: z.string().optional(), + ANTHROPIC_API_KEY: z.string().optional(), + ANTHROPIC_BASE_URL: z.string().url().optional(), OPENAI_BASE_URL: z.string().url().optional(), OPENAI_PROXY_URL: z.string().url().optional(), OPENAI_TIMEOUT_SEC: z.coerce.number().positive().optional(), @@ -303,11 +305,16 @@ const serverConfigSchema = allEnv.transform((val, ctx) => { : undefined, }, inference: { - isConfigured: !!val.OPENAI_API_KEY || !!val.OLLAMA_BASE_URL, + isConfigured: + !!val.OPENAI_API_KEY || + !!val.ANTHROPIC_API_KEY || + !!val.OLLAMA_BASE_URL, numWorkers: val.INFERENCE_NUM_WORKERS, jobTimeoutSec: val.INFERENCE_JOB_TIMEOUT_SEC, fetchTimeoutSec: val.INFERENCE_FETCH_TIMEOUT_SEC, openAIApiKey: val.OPENAI_API_KEY, + anthropicApiKey: val.ANTHROPIC_API_KEY, + anthropicBaseUrl: val.ANTHROPIC_BASE_URL, openAIBaseUrl: val.OPENAI_BASE_URL, openAIProxyUrl: val.OPENAI_PROXY_URL, openAITimeoutSec: val.OPENAI_TIMEOUT_SEC, diff --git a/packages/shared/inference.test.ts b/packages/shared/inference.test.ts new file mode 100644 index 000000000..5ab57a3ef --- /dev/null +++ b/packages/shared/inference.test.ts @@ -0,0 +1,108 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { z } from "zod"; + +// Mock the Anthropic SDK: default export is a class exposing messages.create. +const createMock = vi.fn(); +vi.mock("@anthropic-ai/sdk", () => ({ + default: class { + messages = { create: createMock }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(public opts: any) {} + }, +})); + +import { AnthropicInferenceClient } from "./inference"; + +function makeClient(overrides = {}) { + return new AnthropicInferenceClient({ + apiKey: "test-key", + textModel: "gpt-4.1-mini", + imageModel: "gpt-4o-mini", + maxOutputTokens: 100, + outputSchema: "structured", + ...overrides, + }); +} + +beforeEach(() => { + createMock.mockReset(); + createMock.mockResolvedValue({ + content: [{ type: "text", text: '{"tags":["a"]}' }], + usage: { input_tokens: 10, output_tokens: 5 }, + }); +}); + +describe("AnthropicInferenceClient text inference", () => { + it("substitutes the Claude default when the model is the OpenAI default", async () => { + const client = makeClient(); + await client.inferFromText("hi", { schema: null }); + expect(createMock.mock.calls[0][0].model).toBe("claude-haiku-4-5"); + }); + + it("preserves an explicitly configured Claude model", async () => { + const client = makeClient({ textModel: "claude-sonnet-4-6" }); + await client.inferFromText("hi", { schema: null }); + expect(createMock.mock.calls[0][0].model).toBe("claude-sonnet-4-6"); + }); + + it("sends max_tokens and the user message, and returns text + summed tokens", async () => { + const client = makeClient(); + const res = await client.inferFromText("hello", { schema: null }); + const body = createMock.mock.calls[0][0]; + expect(body.max_tokens).toBe(100); + expect(body.messages).toEqual([{ role: "user", content: "hello" }]); + expect(res.response).toBe('{"tags":["a"]}'); + expect(res.totalTokens).toBe(15); + }); + + it("attaches output_config json_schema in structured mode when a schema is given", async () => { + const client = makeClient(); + await client.inferFromText("hi", { + schema: z.object({ tags: z.array(z.string()) }), + }); + const body = createMock.mock.calls[0][0]; + expect(body.output_config.format.type).toBe("json_schema"); + expect(body.output_config.format.schema).toBeTypeOf("object"); + }); + + it("omits output_config in plain mode", async () => { + const client = makeClient({ outputSchema: "plain" }); + await client.inferFromText("hi", { + schema: z.object({ tags: z.array(z.string()) }), + }); + expect(createMock.mock.calls[0][0].output_config).toBeUndefined(); + }); + + it("omits output_config when structured mode has no schema (e.g. summarization)", async () => { + const client = makeClient(); + await client.inferFromText("summarize", { schema: null }); + expect(createMock.mock.calls[0][0].output_config).toBeUndefined(); + }); +}); + +describe("AnthropicInferenceClient image inference", () => { + it("builds a base64 image content block with the given media type", async () => { + const client = makeClient({ outputSchema: "plain" }); + await client.inferFromImage("describe", "image/png", "BASE64DATA", { + schema: null, + }); + const body = createMock.mock.calls[0][0]; + expect(body.model).toBe("claude-haiku-4-5"); + expect(body.messages[0].content).toEqual([ + { type: "text", text: "describe" }, + { + type: "image", + source: { type: "base64", media_type: "image/png", data: "BASE64DATA" }, + }, + ]); + }); +}); + +describe("AnthropicInferenceClient embeddings", () => { + it("rejects with a clear unsupported error", async () => { + const client = makeClient(); + await expect(client.generateEmbeddingFromText(["x"])).rejects.toThrow( + /does not provide an embeddings API/, + ); + }); +}); diff --git a/packages/shared/inference.ts b/packages/shared/inference.ts index a7c7e9c8c..abd4730a8 100644 --- a/packages/shared/inference.ts +++ b/packages/shared/inference.ts @@ -1,3 +1,4 @@ +import Anthropic from "@anthropic-ai/sdk"; import { Ollama } from "ollama"; import OpenAI from "openai"; import { zodResponseFormat } from "openai/helpers/zod"; @@ -157,6 +158,10 @@ export class InferenceClientFactory { return OpenAIInferenceClient.fromConfig(); } + if (serverConfig.inference.anthropicApiKey) { + return AnthropicInferenceClient.fromConfig(); + } + if (serverConfig.inference.ollamaBaseUrl) { return OllamaInferenceClient.fromConfig(); } @@ -164,6 +169,52 @@ export class InferenceClientFactory { } } +const ANTHROPIC_DEFAULT_MODEL = "claude-haiku-4-5"; +const OPENAI_DEFAULT_TEXT_MODEL = "gpt-4.1-mini"; +const OPENAI_DEFAULT_IMAGE_MODEL = "gpt-4o-mini"; + +// If the configured model is still Karakeep's global OpenAI default, fall back to +// a Claude model so that a zero-config Anthropic setup works (and we never send a +// gpt-* id to Anthropic, which would 404). +function resolveAnthropicModel(model: string, openAIDefault: string): string { + if (model === openAIDefault) { + logger.info( + `[inference] No Claude model set for the Anthropic provider; defaulting to ${ANTHROPIC_DEFAULT_MODEL}. Set INFERENCE_TEXT_MODEL/INFERENCE_IMAGE_MODEL to override.`, + ); + return ANTHROPIC_DEFAULT_MODEL; + } + return model; +} + +// Anthropic has no json_object mode. We use native Structured Outputs +// (output_config.format) whenever a schema is supplied and the mode wants JSON. +function buildAnthropicOutputConfig( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + schema: z.ZodSchema | null, + outputSchema: "structured" | "json" | "plain", +) { + if (!schema || outputSchema === "plain") { + return undefined; + } + return { + format: { + type: "json_schema" as const, + schema: z.toJSONSchema(schema), + }, + }; +} + +function extractAnthropicText(message: Anthropic.Message): string { + const text = message.content + .filter((b): b is Anthropic.TextBlock => b.type === "text") + .map((b) => b.text) + .join(""); + if (!text) { + throw new Error(`Got no text content from Anthropic`); + } + return text; +} + export class OpenAIInferenceClient implements InferenceClient { openAI: OpenAI; private config: OpenAIInferenceConfig; @@ -316,6 +367,134 @@ export class OpenAIInferenceClient implements InferenceClient { } } +export interface AnthropicInferenceConfig { + apiKey: string; + baseURL?: string; + textModel: string; + imageModel: string; + maxOutputTokens: number; + outputSchema: "structured" | "json" | "plain"; +} + +export class AnthropicInferenceClient implements InferenceClient { + private anthropic: Anthropic; + private config: AnthropicInferenceConfig; + private textModel: string; + private imageModel: string; + + constructor(config: AnthropicInferenceConfig) { + this.config = config; + this.textModel = resolveAnthropicModel( + config.textModel, + OPENAI_DEFAULT_TEXT_MODEL, + ); + this.imageModel = resolveAnthropicModel( + config.imageModel, + OPENAI_DEFAULT_IMAGE_MODEL, + ); + this.anthropic = new Anthropic({ + apiKey: config.apiKey, + baseURL: config.baseURL, + }); + } + + static fromConfig(): AnthropicInferenceClient { + return new AnthropicInferenceClient({ + apiKey: serverConfig.inference.anthropicApiKey!, + baseURL: serverConfig.inference.anthropicBaseUrl, + textModel: serverConfig.inference.textModel, + imageModel: serverConfig.inference.imageModel, + maxOutputTokens: serverConfig.inference.maxOutputTokens, + outputSchema: serverConfig.inference.outputSchema, + }); + } + + async inferFromText( + prompt: string, + _opts: Partial, + ): Promise { + const optsWithDefaults: InferenceOptions = { + ...defaultInferenceOptions, + ..._opts, + }; + const outputConfig = buildAnthropicOutputConfig( + optsWithDefaults.schema, + this.config.outputSchema, + ); + const message = await this.anthropic.messages.create( + { + model: this.textModel, + max_tokens: this.config.maxOutputTokens, + messages: [{ role: "user", content: prompt }], + ...(outputConfig ? { output_config: outputConfig } : {}), + }, + { signal: optsWithDefaults.abortSignal }, + ); + return { + response: extractAnthropicText(message), + totalTokens: + (message.usage.input_tokens ?? 0) + (message.usage.output_tokens ?? 0), + }; + } + + async inferFromImage( + prompt: string, + contentType: string, + image: string, + _opts: Partial, + ): Promise { + const optsWithDefaults: InferenceOptions = { + ...defaultInferenceOptions, + ..._opts, + }; + const outputConfig = buildAnthropicOutputConfig( + optsWithDefaults.schema, + this.config.outputSchema, + ); + const message = await this.anthropic.messages.create( + { + model: this.imageModel, + max_tokens: this.config.maxOutputTokens, + messages: [ + { + role: "user", + content: [ + { type: "text", text: prompt }, + { + type: "image", + source: { + type: "base64", + media_type: contentType as + | "image/jpeg" + | "image/png" + | "image/gif" + | "image/webp", + data: image, + }, + }, + ], + }, + ], + ...(outputConfig ? { output_config: outputConfig } : {}), + }, + { signal: optsWithDefaults.abortSignal }, + ); + return { + response: extractAnthropicText(message), + totalTokens: + (message.usage.input_tokens ?? 0) + (message.usage.output_tokens ?? 0), + }; + } + + generateEmbeddingFromText(_inputs: string[]): Promise { + return Promise.reject( + new Error( + "Anthropic does not provide an embeddings API. Configure a separate embedding provider (e.g. OpenAI or Ollama) for semantic search.", + ), + ); + } +} + export interface OllamaInferenceConfig { baseUrl: string; textModel: string; diff --git a/packages/shared/package.json b/packages/shared/package.json index ab8e66853..7276d33d8 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -5,6 +5,7 @@ "private": true, "type": "module", "dependencies": { + "@anthropic-ai/sdk": "^0.104.1", "@aws-sdk/client-s3": "^3.1014.0", "glob": "^11.0.0", "html-to-text": "^9.0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 01d6208ab..10110b679 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -710,19 +710,19 @@ importers: version: 5.1.11 next: specifier: 16.2.6 - version: 16.2.6(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.89.1) + version: 16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.89.1) next-auth: specifier: ^4.24.11 - version: 4.24.11(next@16.2.6(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.89.1))(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 4.24.11(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.89.1))(react-dom@19.2.6(react@19.2.6))(react@19.2.6) next-i18next: specifier: ^15.3.1 - version: 15.3.1(i18next@23.16.5)(next@16.2.6(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.89.1))(react-i18next@15.1.1(i18next@23.16.5)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6) + version: 15.3.1(i18next@23.16.5)(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.89.1))(react-i18next@15.1.1(i18next@23.16.5)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) nuqs: specifier: ^2.4.3 - version: 2.4.3(next@16.2.6(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.89.1))(react-router-dom@6.22.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-router@6.22.1(react@19.2.6))(react@19.2.6) + version: 2.4.3(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.89.1))(react-router-dom@6.22.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-router@6.22.1(react@19.2.6))(react@19.2.6) react: specifier: 19.2.6 version: 19.2.6 @@ -1348,6 +1348,9 @@ importers: packages/shared: dependencies: + '@anthropic-ai/sdk': + specifier: ^0.104.1 + version: 0.104.1(zod@4.3.6) '@aws-sdk/client-s3': specifier: ^3.1014.0 version: 3.1014.0 @@ -1785,8 +1788,8 @@ packages: '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} - '@anthropic-ai/sdk@0.90.0': - resolution: {integrity: sha512-MzZtPabJF1b0FTDl6Z6H5ljphPwACLGP13lu8MTiB8jXaW/YXlpOp+Po2cVou3MPM5+f5toyLnul9whKCy7fBg==} + '@anthropic-ai/sdk@0.104.1': + resolution: {integrity: sha512-gGACa/+IaiXzRRmF96aOhamoBgapKRBiFWbmmTFP8aMkpaEcuStF+Q61bjo4vPxBM7gqWJNZqsngslRdnLHv0Q==} hasBin: true peerDependencies: zod: ^3.25.0 || ^4.0.0 @@ -7260,6 +7263,9 @@ packages: resolution: {integrity: sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==} engines: {node: '>=18.0.0'} + '@stablelib/base64@1.0.1': + resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -9664,10 +9670,6 @@ packages: defu@6.1.6: resolution: {integrity: sha512-f8mefEW4WIVg4LckePx3mALjQSPQgFlg9U8yaPdlsbdYcHQyj9n2zL2LJEA52smeYxOvmd/nB7TpMtHGMTHcug==} - degenerator@5.0.1: - resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} - engines: {node: '>= 14'} - delaunator@5.1.0: resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} @@ -10622,6 +10624,9 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + fast-string-truncated-width@1.2.1: resolution: {integrity: sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow==} @@ -11973,6 +11978,10 @@ packages: resolution: {integrity: sha512-CTUKmIlPJbsWfzRRnOXz+0MjIqvnleIXwFTzz+t9T86HnYX/Rozria6ZVGLktAU9e+NygNljveP+yxqtQp/Q4w==} engines: {node: '>=12.0.0'} + 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==} @@ -12006,12 +12015,6 @@ packages: resolution: {integrity: sha512-BTznj0owIt2CBAH/LTo7+1I5pMvl1e1033LRl/HUowlZmJOIhzC0zbX5bxMngLkfT4WnzPP26QnW5wMr2g9tsQ==} hasBin: true - jwa@2.0.1: - resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} - - jws@4.0.1: - resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} - katex@0.16.47: resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==} hasBin: true @@ -13237,8 +13240,6 @@ packages: nested-error-stacks@2.0.1: resolution: {integrity: sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==} - netmask@2.1.1: {} - next-auth@4.24.11: resolution: {integrity: sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==} peerDependencies: @@ -16078,6 +16079,9 @@ packages: resolution: {integrity: sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==} engines: {node: '>=6'} + standardwebhooks@1.0.0: + resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} + statuses@1.5.0: resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} engines: {node: '>= 0.6'} @@ -17866,9 +17870,10 @@ snapshots: package-manager-detector: 1.6.0 tinyexec: 1.0.4 - '@anthropic-ai/sdk@0.90.0(zod@4.3.6)': + '@anthropic-ai/sdk@0.104.1(zod@4.3.6)': dependencies: json-schema-to-ts: 3.1.1 + standardwebhooks: 1.0.0 optionalDependencies: zod: 4.3.6 @@ -25056,6 +25061,8 @@ snapshots: dependencies: tslib: 2.8.1 + '@stablelib/base64@1.0.1': {} + '@standard-schema/spec@1.1.0': {} '@standard-schema/utils@0.3.0': {} @@ -27948,12 +27955,6 @@ snapshots: defu@6.1.6: {} - degenerator@5.0.1: - dependencies: - ast-types: 0.13.4 - escodegen: 2.1.0 - esprima: 4.0.1 - delaunator@5.1.0: dependencies: robust-predicates: 3.0.3 @@ -28942,6 +28943,8 @@ snapshots: fast-safe-stringify@2.1.1: {} + fast-sha256@1.3.0: {} + fast-string-truncated-width@1.2.1: {} fast-string-width@1.1.0: @@ -30471,6 +30474,11 @@ snapshots: json-schema-compare: 0.2.2 lodash: 4.17.23 + json-schema-to-ts@3.1.1: + dependencies: + '@babel/runtime': 7.29.2 + ts-algebra: 2.0.0 + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -30499,17 +30507,6 @@ snapshots: jsonrepair@3.13.3: {} - jwa@2.0.1: - dependencies: - buffer-equal-constant-time: 1.0.1 - ecdsa-sig-formatter: 1.0.11 - safe-buffer: 5.2.1 - - jws@4.0.1: - dependencies: - jwa: 2.0.1 - safe-buffer: 5.2.1 - katex@0.16.47: dependencies: commander: 8.3.0 @@ -32503,15 +32500,13 @@ snapshots: nested-error-stacks@2.0.1: {} - netmask@2.1.1: {} - - next-auth@4.24.11(next@16.2.6(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.89.1))(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + next-auth@4.24.11(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.89.1))(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: '@babel/runtime': 7.27.6 '@panva/hkdf': 1.2.1 cookie: 0.7.2 jose: 4.15.9 - next: 16.2.6(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.89.1) + next: 16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.89.1) oauth: 0.9.15 openid-client: 5.7.1 preact: 10.11.3 @@ -32520,7 +32515,7 @@ snapshots: react-dom: 19.2.6(react@19.2.6) uuid: 8.3.2 - next-i18next@15.3.1(i18next@23.16.5)(next@16.2.6(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.89.1))(react-i18next@15.1.1(i18next@23.16.5)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6): + next-i18next@15.3.1(i18next@23.16.5)(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.89.1))(react-i18next@15.1.1(i18next@23.16.5)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6): dependencies: '@babel/runtime': 7.27.6 '@types/hoist-non-react-statics': 3.3.6 @@ -32528,7 +32523,7 @@ snapshots: hoist-non-react-statics: 3.3.2 i18next: 23.16.5 i18next-fs-backend: 2.6.0 - next: 16.2.6(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.89.1) + next: 16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.89.1) react: 19.2.6 react-i18next: 15.1.1(i18next@23.16.5)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -32537,7 +32532,7 @@ snapshots: react: 19.2.6 react-dom: 19.2.6(react@19.2.6) - next@16.2.6(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.89.1): + next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.89.1): dependencies: '@next/env': 16.2.6 '@swc/helpers': 0.5.15 @@ -32546,7 +32541,7 @@ snapshots: postcss: 8.4.31 react: 19.2.6 react-dom: 19.2.6(react@19.2.6) - styled-jsx: 5.1.6(@babel/core@7.26.0)(react@19.2.6) + styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.6) optionalDependencies: '@next/swc-darwin-arm64': 16.2.6 '@next/swc-darwin-x64': 16.2.6 @@ -32691,12 +32686,12 @@ snapshots: nullthrows@1.1.1: {} - nuqs@2.4.3(next@16.2.6(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.89.1))(react-router-dom@6.22.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-router@6.22.1(react@19.2.6))(react@19.2.6): + nuqs@2.4.3(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.89.1))(react-router-dom@6.22.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-router@6.22.1(react@19.2.6))(react@19.2.6): dependencies: mitt: 3.0.1 react: 19.2.6 optionalDependencies: - next: 16.2.6(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.89.1) + next: 16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.89.1) react-router: 6.22.1(react@19.2.6) react-router-dom: 6.22.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -36011,6 +36006,11 @@ snapshots: dependencies: type-fest: 0.7.1 + standardwebhooks@1.0.0: + dependencies: + '@stablelib/base64': 1.0.1 + fast-sha256: 1.3.0 + statuses@1.5.0: {} statuses@2.0.1: {} @@ -36145,12 +36145,12 @@ snapshots: dependencies: inline-style-parser: 0.2.4 - styled-jsx@5.1.6(@babel/core@7.26.0)(react@19.2.6): + styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.6): dependencies: client-only: 0.0.1 react: 19.2.6 optionalDependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.0 stylehacks@6.1.1(postcss@8.5.6): dependencies: