feat(core): add anthropic provider (#11911)
Close [AI-40](https://linear.app/affine-design/issue/AI-40). ### What changed? - Add anthropic provider. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added support for Anthropic as a new provider option in Copilot, enabling integration with Anthropic's text generation models. - Users can now configure Anthropic provider settings, including API key management, through the admin interface. - **Chores** - Updated dependencies to include the Anthropic SDK for backend operations. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -27,6 +27,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@affine/server-native": "workspace:*",
|
"@affine/server-native": "workspace:*",
|
||||||
|
"@ai-sdk/anthropic": "^1.2.10",
|
||||||
"@ai-sdk/google": "^1.2.10",
|
"@ai-sdk/google": "^1.2.10",
|
||||||
"@ai-sdk/openai": "^1.3.18",
|
"@ai-sdk/openai": "^1.3.18",
|
||||||
"@ai-sdk/perplexity": "^1.1.6",
|
"@ai-sdk/perplexity": "^1.1.6",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
StorageJSONSchema,
|
StorageJSONSchema,
|
||||||
StorageProviderConfig,
|
StorageProviderConfig,
|
||||||
} from '../../base';
|
} from '../../base';
|
||||||
|
import { AnthropicConfig } from './providers/anthropic';
|
||||||
import type { FalConfig } from './providers/fal';
|
import type { FalConfig } from './providers/fal';
|
||||||
import { GeminiConfig } from './providers/gemini';
|
import { GeminiConfig } from './providers/gemini';
|
||||||
import { OpenAIConfig } from './providers/openai';
|
import { OpenAIConfig } from './providers/openai';
|
||||||
@@ -21,6 +22,7 @@ declare global {
|
|||||||
fal: ConfigItem<FalConfig>;
|
fal: ConfigItem<FalConfig>;
|
||||||
gemini: ConfigItem<GeminiConfig>;
|
gemini: ConfigItem<GeminiConfig>;
|
||||||
perplexity: ConfigItem<PerplexityConfig>;
|
perplexity: ConfigItem<PerplexityConfig>;
|
||||||
|
anthropic: ConfigItem<AnthropicConfig>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -56,6 +58,12 @@ defineModuleConfig('copilot', {
|
|||||||
apiKey: '',
|
apiKey: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'providers.anthropic': {
|
||||||
|
desc: 'The config for the anthropic provider.',
|
||||||
|
default: {
|
||||||
|
apiKey: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
unsplash: {
|
unsplash: {
|
||||||
desc: 'The config for the unsplash key.',
|
desc: 'The config for the unsplash key.',
|
||||||
default: {
|
default: {
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import {
|
||||||
|
AnthropicProvider as AnthropicSDKProvider,
|
||||||
|
createAnthropic,
|
||||||
|
} from '@ai-sdk/anthropic';
|
||||||
|
import { AISDKError, generateText, streamText } from 'ai';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CopilotPromptInvalid,
|
||||||
|
CopilotProviderSideError,
|
||||||
|
metrics,
|
||||||
|
UserFriendlyError,
|
||||||
|
} from '../../../base';
|
||||||
|
import { CopilotProvider } from './provider';
|
||||||
|
import {
|
||||||
|
ChatMessageRole,
|
||||||
|
CopilotCapability,
|
||||||
|
CopilotChatOptions,
|
||||||
|
CopilotProviderType,
|
||||||
|
CopilotTextToTextProvider,
|
||||||
|
PromptMessage,
|
||||||
|
} from './types';
|
||||||
|
import { chatToGPTMessage } from './utils';
|
||||||
|
|
||||||
|
export type AnthropicConfig = {
|
||||||
|
apiKey: string;
|
||||||
|
baseUrl?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class AnthropicProvider
|
||||||
|
extends CopilotProvider<AnthropicConfig>
|
||||||
|
implements CopilotTextToTextProvider
|
||||||
|
{
|
||||||
|
override readonly type = CopilotProviderType.Anthropic;
|
||||||
|
override readonly capabilities = [CopilotCapability.TextToText];
|
||||||
|
override readonly models = ['claude-3-7-sonnet-20250219'];
|
||||||
|
|
||||||
|
#instance!: AnthropicSDKProvider;
|
||||||
|
|
||||||
|
override configured(): boolean {
|
||||||
|
return !!this.config.apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override setup() {
|
||||||
|
super.setup();
|
||||||
|
this.#instance = createAnthropic({
|
||||||
|
apiKey: this.config.apiKey,
|
||||||
|
baseURL: this.config.baseUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async checkParams({
|
||||||
|
messages,
|
||||||
|
model,
|
||||||
|
}: {
|
||||||
|
messages?: PromptMessage[];
|
||||||
|
model: string;
|
||||||
|
}) {
|
||||||
|
if (!(await this.isModelAvailable(model))) {
|
||||||
|
throw new CopilotPromptInvalid(`Invalid model: ${model}`);
|
||||||
|
}
|
||||||
|
if (Array.isArray(messages) && messages.length > 0) {
|
||||||
|
if (
|
||||||
|
messages.some(
|
||||||
|
m =>
|
||||||
|
// check non-object
|
||||||
|
typeof m !== 'object' ||
|
||||||
|
!m ||
|
||||||
|
// check content
|
||||||
|
typeof m.content !== 'string' ||
|
||||||
|
// content and attachments must exist at least one
|
||||||
|
((!m.content || !m.content.trim()) &&
|
||||||
|
(!Array.isArray(m.attachments) || !m.attachments.length))
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new CopilotPromptInvalid('Empty message content');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
messages.some(
|
||||||
|
m =>
|
||||||
|
typeof m.role !== 'string' ||
|
||||||
|
!m.role ||
|
||||||
|
!ChatMessageRole.includes(m.role)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new CopilotPromptInvalid('Invalid message role');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleError(e: any) {
|
||||||
|
if (e instanceof UserFriendlyError) {
|
||||||
|
return e;
|
||||||
|
} else if (e instanceof AISDKError) {
|
||||||
|
this.logger.error('Throw error from ai sdk:', e);
|
||||||
|
return new CopilotProviderSideError({
|
||||||
|
provider: this.type,
|
||||||
|
kind: e.name || 'unknown',
|
||||||
|
message: e.message,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return new CopilotProviderSideError({
|
||||||
|
provider: this.type,
|
||||||
|
kind: 'unexpected_response',
|
||||||
|
message: e?.message || 'Unexpected anthropic response',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== text to text ======
|
||||||
|
async generateText(
|
||||||
|
messages: PromptMessage[],
|
||||||
|
model: string = 'claude-3-7-sonnet-20250219',
|
||||||
|
options: CopilotChatOptions = {}
|
||||||
|
): Promise<string> {
|
||||||
|
await this.checkParams({ messages, model });
|
||||||
|
|
||||||
|
try {
|
||||||
|
metrics.ai.counter('chat_text_calls').add(1, { model });
|
||||||
|
|
||||||
|
const [system, msgs] = await chatToGPTMessage(messages);
|
||||||
|
|
||||||
|
const modelInstance = this.#instance(model);
|
||||||
|
const { text } = await generateText({
|
||||||
|
model: modelInstance,
|
||||||
|
system,
|
||||||
|
messages: msgs,
|
||||||
|
abortSignal: options.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!text) throw new Error('Failed to generate text');
|
||||||
|
return text.trim();
|
||||||
|
} catch (e: any) {
|
||||||
|
metrics.ai.counter('chat_text_errors').add(1, { model });
|
||||||
|
throw this.handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async *generateTextStream(
|
||||||
|
messages: PromptMessage[],
|
||||||
|
model: string = 'claude-3-7-sonnet-20250219',
|
||||||
|
options: CopilotChatOptions = {}
|
||||||
|
): AsyncIterable<string> {
|
||||||
|
await this.checkParams({ messages, model });
|
||||||
|
|
||||||
|
try {
|
||||||
|
metrics.ai.counter('chat_text_stream_calls').add(1, { model });
|
||||||
|
const [system, msgs] = await chatToGPTMessage(messages);
|
||||||
|
|
||||||
|
const { textStream } = streamText({
|
||||||
|
model: this.#instance(model),
|
||||||
|
system,
|
||||||
|
messages: msgs,
|
||||||
|
abortSignal: options.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const message of textStream) {
|
||||||
|
if (message) {
|
||||||
|
yield message;
|
||||||
|
if (options.signal?.aborted) {
|
||||||
|
await textStream.cancel();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
metrics.ai.counter('chat_text_stream_errors').add(1, { model });
|
||||||
|
throw this.handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
import { ServerFeature, ServerService } from '../../../core';
|
import { ServerFeature, ServerService } from '../../../core';
|
||||||
|
import type { AnthropicProvider } from './anthropic';
|
||||||
import type { FalProvider } from './fal';
|
import type { FalProvider } from './fal';
|
||||||
import type { GeminiProvider } from './gemini';
|
import type { GeminiProvider } from './gemini';
|
||||||
import type { OpenAIProvider } from './openai';
|
import type { OpenAIProvider } from './openai';
|
||||||
@@ -13,6 +14,7 @@ import {
|
|||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
type TypedProvider = {
|
type TypedProvider = {
|
||||||
|
[CopilotProviderType.Anthropic]: AnthropicProvider;
|
||||||
[CopilotProviderType.Gemini]: GeminiProvider;
|
[CopilotProviderType.Gemini]: GeminiProvider;
|
||||||
[CopilotProviderType.OpenAI]: OpenAIProvider;
|
[CopilotProviderType.OpenAI]: OpenAIProvider;
|
||||||
[CopilotProviderType.Perplexity]: PerplexityProvider;
|
[CopilotProviderType.Perplexity]: PerplexityProvider;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AnthropicProvider } from './anthropic';
|
||||||
import { FalProvider } from './fal';
|
import { FalProvider } from './fal';
|
||||||
import { GeminiProvider } from './gemini';
|
import { GeminiProvider } from './gemini';
|
||||||
import { OpenAIProvider } from './openai';
|
import { OpenAIProvider } from './openai';
|
||||||
@@ -8,8 +9,10 @@ export const CopilotProviders = [
|
|||||||
FalProvider,
|
FalProvider,
|
||||||
GeminiProvider,
|
GeminiProvider,
|
||||||
PerplexityProvider,
|
PerplexityProvider,
|
||||||
|
AnthropicProvider,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export { AnthropicProvider } from './anthropic';
|
||||||
export { CopilotProviderFactory } from './factory';
|
export { CopilotProviderFactory } from './factory';
|
||||||
export { FalProvider } from './fal';
|
export { FalProvider } from './fal';
|
||||||
export { GeminiProvider } from './gemini';
|
export { GeminiProvider } from './gemini';
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { z } from 'zod';
|
|||||||
import { type CopilotProvider } from './provider';
|
import { type CopilotProvider } from './provider';
|
||||||
|
|
||||||
export enum CopilotProviderType {
|
export enum CopilotProviderType {
|
||||||
|
Anthropic = 'anthropic',
|
||||||
FAL = 'fal',
|
FAL = 'fal',
|
||||||
Gemini = 'gemini',
|
Gemini = 'gemini',
|
||||||
OpenAI = 'openai',
|
OpenAI = 'openai',
|
||||||
|
|||||||
@@ -233,6 +233,10 @@
|
|||||||
"type": "Object",
|
"type": "Object",
|
||||||
"desc": "The config for the perplexity provider."
|
"desc": "The config for the perplexity provider."
|
||||||
},
|
},
|
||||||
|
"providers.anthropic": {
|
||||||
|
"type": "Object",
|
||||||
|
"desc": "The config for the anthropic provider."
|
||||||
|
},
|
||||||
"unsplash": {
|
"unsplash": {
|
||||||
"type": "Object",
|
"type": "Object",
|
||||||
"desc": "The config for the unsplash key."
|
"desc": "The config for the unsplash key."
|
||||||
|
|||||||
13
yarn.lock
13
yarn.lock
@@ -889,6 +889,7 @@ __metadata:
|
|||||||
"@affine-tools/utils": "workspace:*"
|
"@affine-tools/utils": "workspace:*"
|
||||||
"@affine/graphql": "workspace:*"
|
"@affine/graphql": "workspace:*"
|
||||||
"@affine/server-native": "workspace:*"
|
"@affine/server-native": "workspace:*"
|
||||||
|
"@ai-sdk/anthropic": "npm:^1.2.10"
|
||||||
"@ai-sdk/google": "npm:^1.2.10"
|
"@ai-sdk/google": "npm:^1.2.10"
|
||||||
"@ai-sdk/openai": "npm:^1.3.18"
|
"@ai-sdk/openai": "npm:^1.3.18"
|
||||||
"@ai-sdk/perplexity": "npm:^1.1.6"
|
"@ai-sdk/perplexity": "npm:^1.1.6"
|
||||||
@@ -1052,6 +1053,18 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
|
"@ai-sdk/anthropic@npm:^1.2.10":
|
||||||
|
version: 1.2.10
|
||||||
|
resolution: "@ai-sdk/anthropic@npm:1.2.10"
|
||||||
|
dependencies:
|
||||||
|
"@ai-sdk/provider": "npm:1.1.3"
|
||||||
|
"@ai-sdk/provider-utils": "npm:2.2.7"
|
||||||
|
peerDependencies:
|
||||||
|
zod: ^3.0.0
|
||||||
|
checksum: 10/4701f83886592e635fab36d8026e7b764f444f40a2c636340fcf267d8d1c846c7ee710b548a782cec6ecafac1d967be74d4e87d6f6f780b0725a1a580cab77d3
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@ai-sdk/google@npm:^1.2.10":
|
"@ai-sdk/google@npm:^1.2.10":
|
||||||
version: 1.2.11
|
version: 1.2.11
|
||||||
resolution: "@ai-sdk/google@npm:1.2.11"
|
resolution: "@ai-sdk/google@npm:1.2.11"
|
||||||
|
|||||||
Reference in New Issue
Block a user