Commit 0ad0fec8 authored by boojack's avatar boojack

feat(ai): add Anthropic provider option

parent d87539a1
...@@ -10,6 +10,8 @@ const ( ...@@ -10,6 +10,8 @@ const (
ProviderOpenAICompatible ProviderType = "OPENAI_COMPATIBLE" ProviderOpenAICompatible ProviderType = "OPENAI_COMPATIBLE"
// ProviderGemini is Google's Gemini API. // ProviderGemini is Google's Gemini API.
ProviderGemini ProviderType = "GEMINI" ProviderGemini ProviderType = "GEMINI"
// ProviderAnthropic is Anthropic's API.
ProviderAnthropic ProviderType = "ANTHROPIC"
) )
// ProviderConfig configures a callable AI provider connection. // ProviderConfig configures a callable AI provider connection.
......
...@@ -233,6 +233,7 @@ message InstanceSetting { ...@@ -233,6 +233,7 @@ message InstanceSetting {
OPENAI = 1; OPENAI = 1;
OPENAI_COMPATIBLE = 2; OPENAI_COMPATIBLE = 2;
GEMINI = 3; GEMINI = 3;
ANTHROPIC = 4;
} }
} }
......
...@@ -100,6 +100,7 @@ const ( ...@@ -100,6 +100,7 @@ const (
InstanceSetting_OPENAI InstanceSetting_AIProviderType = 1 InstanceSetting_OPENAI InstanceSetting_AIProviderType = 1
InstanceSetting_OPENAI_COMPATIBLE InstanceSetting_AIProviderType = 2 InstanceSetting_OPENAI_COMPATIBLE InstanceSetting_AIProviderType = 2
InstanceSetting_GEMINI InstanceSetting_AIProviderType = 3 InstanceSetting_GEMINI InstanceSetting_AIProviderType = 3
InstanceSetting_ANTHROPIC InstanceSetting_AIProviderType = 4
) )
// Enum value maps for InstanceSetting_AIProviderType. // Enum value maps for InstanceSetting_AIProviderType.
...@@ -109,12 +110,14 @@ var ( ...@@ -109,12 +110,14 @@ var (
1: "OPENAI", 1: "OPENAI",
2: "OPENAI_COMPATIBLE", 2: "OPENAI_COMPATIBLE",
3: "GEMINI", 3: "GEMINI",
4: "ANTHROPIC",
} }
InstanceSetting_AIProviderType_value = map[string]int32{ InstanceSetting_AIProviderType_value = map[string]int32{
"AI_PROVIDER_TYPE_UNSPECIFIED": 0, "AI_PROVIDER_TYPE_UNSPECIFIED": 0,
"OPENAI": 1, "OPENAI": 1,
"OPENAI_COMPATIBLE": 2, "OPENAI_COMPATIBLE": 2,
"GEMINI": 3, "GEMINI": 3,
"ANTHROPIC": 4,
} }
) )
...@@ -1411,7 +1414,7 @@ const file_api_v1_instance_service_proto_rawDesc = "" + ...@@ -1411,7 +1414,7 @@ const file_api_v1_instance_service_proto_rawDesc = "" +
"\x04demo\x18\x03 \x01(\bR\x04demo\x12!\n" + "\x04demo\x18\x03 \x01(\bR\x04demo\x12!\n" +
"\finstance_url\x18\x06 \x01(\tR\vinstanceUrl\x12(\n" + "\finstance_url\x18\x06 \x01(\tR\vinstanceUrl\x12(\n" +
"\x05admin\x18\a \x01(\v2\x12.memos.api.v1.UserR\x05admin\"\x1b\n" + "\x05admin\x18\a \x01(\v2\x12.memos.api.v1.UserR\x05admin\"\x1b\n" +
"\x19GetInstanceProfileRequest\"\xd3\x1a\n" + "\x19GetInstanceProfileRequest\"\xe2\x1a\n" +
"\x0fInstanceSetting\x12\x17\n" + "\x0fInstanceSetting\x12\x17\n" +
"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12W\n" + "\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12W\n" +
"\x0fgeneral_setting\x18\x02 \x01(\v2,.memos.api.v1.InstanceSetting.GeneralSettingH\x00R\x0egeneralSetting\x12W\n" + "\x0fgeneral_setting\x18\x02 \x01(\v2,.memos.api.v1.InstanceSetting.GeneralSettingH\x00R\x0egeneralSetting\x12W\n" +
...@@ -1499,14 +1502,15 @@ const file_api_v1_instance_service_proto_rawDesc = "" + ...@@ -1499,14 +1502,15 @@ const file_api_v1_instance_service_proto_rawDesc = "" +
"\fMEMO_RELATED\x10\x03\x12\b\n" + "\fMEMO_RELATED\x10\x03\x12\b\n" +
"\x04TAGS\x10\x04\x12\x10\n" + "\x04TAGS\x10\x04\x12\x10\n" +
"\fNOTIFICATION\x10\x05\x12\x06\n" + "\fNOTIFICATION\x10\x05\x12\x06\n" +
"\x02AI\x10\x06\"a\n" + "\x02AI\x10\x06\"p\n" +
"\x0eAIProviderType\x12 \n" + "\x0eAIProviderType\x12 \n" +
"\x1cAI_PROVIDER_TYPE_UNSPECIFIED\x10\x00\x12\n" + "\x1cAI_PROVIDER_TYPE_UNSPECIFIED\x10\x00\x12\n" +
"\n" + "\n" +
"\x06OPENAI\x10\x01\x12\x15\n" + "\x06OPENAI\x10\x01\x12\x15\n" +
"\x11OPENAI_COMPATIBLE\x10\x02\x12\n" + "\x11OPENAI_COMPATIBLE\x10\x02\x12\n" +
"\n" + "\n" +
"\x06GEMINI\x10\x03:a\xeaA^\n" + "\x06GEMINI\x10\x03\x12\r\n" +
"\tANTHROPIC\x10\x04:a\xeaA^\n" +
"\x1cmemos.api.v1/InstanceSetting\x12\x1binstance/settings/{setting}*\x10instanceSettings2\x0finstanceSettingB\a\n" + "\x1cmemos.api.v1/InstanceSetting\x12\x1binstance/settings/{setting}*\x10instanceSettings2\x0finstanceSettingB\a\n" +
"\x05value\"U\n" + "\x05value\"U\n" +
"\x19GetInstanceSettingRequest\x128\n" + "\x19GetInstanceSettingRequest\x128\n" +
......
...@@ -2421,6 +2421,7 @@ components: ...@@ -2421,6 +2421,7 @@ components:
- OPENAI - OPENAI
- OPENAI_COMPATIBLE - OPENAI_COMPATIBLE
- GEMINI - GEMINI
- ANTHROPIC
type: string type: string
format: enum format: enum
endpoint: endpoint:
......
...@@ -100,6 +100,7 @@ const ( ...@@ -100,6 +100,7 @@ const (
AIProviderType_OPENAI AIProviderType = 1 AIProviderType_OPENAI AIProviderType = 1
AIProviderType_OPENAI_COMPATIBLE AIProviderType = 2 AIProviderType_OPENAI_COMPATIBLE AIProviderType = 2
AIProviderType_GEMINI AIProviderType = 3 AIProviderType_GEMINI AIProviderType = 3
AIProviderType_ANTHROPIC AIProviderType = 4
) )
// Enum value maps for AIProviderType. // Enum value maps for AIProviderType.
...@@ -109,12 +110,14 @@ var ( ...@@ -109,12 +110,14 @@ var (
1: "OPENAI", 1: "OPENAI",
2: "OPENAI_COMPATIBLE", 2: "OPENAI_COMPATIBLE",
3: "GEMINI", 3: "GEMINI",
4: "ANTHROPIC",
} }
AIProviderType_value = map[string]int32{ AIProviderType_value = map[string]int32{
"AI_PROVIDER_TYPE_UNSPECIFIED": 0, "AI_PROVIDER_TYPE_UNSPECIFIED": 0,
"OPENAI": 1, "OPENAI": 1,
"OPENAI_COMPATIBLE": 2, "OPENAI_COMPATIBLE": 2,
"GEMINI": 3, "GEMINI": 3,
"ANTHROPIC": 4,
} }
) )
...@@ -1321,14 +1324,15 @@ const file_store_instance_setting_proto_rawDesc = "" + ...@@ -1321,14 +1324,15 @@ const file_store_instance_setting_proto_rawDesc = "" +
"\fMEMO_RELATED\x10\x04\x12\b\n" + "\fMEMO_RELATED\x10\x04\x12\b\n" +
"\x04TAGS\x10\x05\x12\x10\n" + "\x04TAGS\x10\x05\x12\x10\n" +
"\fNOTIFICATION\x10\x06\x12\x06\n" + "\fNOTIFICATION\x10\x06\x12\x06\n" +
"\x02AI\x10\a*a\n" + "\x02AI\x10\a*p\n" +
"\x0eAIProviderType\x12 \n" + "\x0eAIProviderType\x12 \n" +
"\x1cAI_PROVIDER_TYPE_UNSPECIFIED\x10\x00\x12\n" + "\x1cAI_PROVIDER_TYPE_UNSPECIFIED\x10\x00\x12\n" +
"\n" + "\n" +
"\x06OPENAI\x10\x01\x12\x15\n" + "\x06OPENAI\x10\x01\x12\x15\n" +
"\x11OPENAI_COMPATIBLE\x10\x02\x12\n" + "\x11OPENAI_COMPATIBLE\x10\x02\x12\n" +
"\n" + "\n" +
"\x06GEMINI\x10\x03B\x9f\x01\n" + "\x06GEMINI\x10\x03\x12\r\n" +
"\tANTHROPIC\x10\x04B\x9f\x01\n" +
"\x0fcom.memos.storeB\x14InstanceSettingProtoP\x01Z)github.com/usememos/memos/proto/gen/store\xa2\x02\x03MSX\xaa\x02\vMemos.Store\xca\x02\vMemos\\Store\xe2\x02\x17Memos\\Store\\GPBMetadata\xea\x02\fMemos::Storeb\x06proto3" "\x0fcom.memos.storeB\x14InstanceSettingProtoP\x01Z)github.com/usememos/memos/proto/gen/store\xa2\x02\x03MSX\xaa\x02\vMemos.Store\xca\x02\vMemos\\Store\xe2\x02\x17Memos\\Store\\GPBMetadata\xea\x02\fMemos::Storeb\x06proto3"
var ( var (
......
...@@ -167,4 +167,5 @@ enum AIProviderType { ...@@ -167,4 +167,5 @@ enum AIProviderType {
OPENAI = 1; OPENAI = 1;
OPENAI_COMPATIBLE = 2; OPENAI_COMPATIBLE = 2;
GEMINI = 3; GEMINI = 3;
ANTHROPIC = 4;
} }
...@@ -170,6 +170,8 @@ func convertAIProviderTypeFromStore(providerType storepb.AIProviderType) ai.Prov ...@@ -170,6 +170,8 @@ func convertAIProviderTypeFromStore(providerType storepb.AIProviderType) ai.Prov
return ai.ProviderOpenAICompatible return ai.ProviderOpenAICompatible
case storepb.AIProviderType_GEMINI: case storepb.AIProviderType_GEMINI:
return ai.ProviderGemini return ai.ProviderGemini
case storepb.AIProviderType_ANTHROPIC:
return ai.ProviderAnthropic
default: default:
return "" return ""
} }
......
...@@ -523,6 +523,9 @@ func (s *APIV1Service) prepareInstanceAISettingForUpdate(ctx context.Context, se ...@@ -523,6 +523,9 @@ func (s *APIV1Service) prepareInstanceAISettingForUpdate(ctx context.Context, se
if provider.Type == storepb.AIProviderType_OPENAI && provider.Endpoint == "" { if provider.Type == storepb.AIProviderType_OPENAI && provider.Endpoint == "" {
provider.Endpoint = "https://api.openai.com/v1" provider.Endpoint = "https://api.openai.com/v1"
} }
if provider.Type == storepb.AIProviderType_ANTHROPIC && provider.Endpoint == "" {
provider.Endpoint = "https://api.anthropic.com/v1"
}
if provider.Type == storepb.AIProviderType_OPENAI_COMPATIBLE && provider.Endpoint == "" { if provider.Type == storepb.AIProviderType_OPENAI_COMPATIBLE && provider.Endpoint == "" {
return errors.Errorf("provider %q endpoint is required", provider.Id) return errors.Errorf("provider %q endpoint is required", provider.Id)
} }
......
...@@ -154,6 +154,47 @@ func TestTranscribe(t *testing.T) { ...@@ -154,6 +154,47 @@ func TestTranscribe(t *testing.T) {
require.Equal(t, "gemini transcript", resp.Text) require.Equal(t, "gemini transcript", resp.Text)
}) })
t.Run("rejects Anthropic transcription as unsupported", func(t *testing.T) {
ts := NewTestService(t)
defer ts.Cleanup()
user, err := ts.CreateRegularUser(ctx, "anthropic-user")
require.NoError(t, err)
userCtx := ts.CreateUserContext(ctx, user.ID)
_, err = ts.Store.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{
Key: storepb.InstanceSettingKey_AI,
Value: &storepb.InstanceSetting_AiSetting{
AiSetting: &storepb.InstanceAISetting{
Providers: []*storepb.AIProviderConfig{
{
Id: "anthropic-main",
Title: "Anthropic",
Type: storepb.AIProviderType_ANTHROPIC,
Endpoint: "https://api.anthropic.com/v1",
ApiKey: "sk-ant-test",
Models: []string{"claude-sonnet-4-5"},
DefaultModel: "claude-sonnet-4-5",
},
},
},
},
})
require.NoError(t, err)
_, err = ts.Service.Transcribe(userCtx, &v1pb.TranscribeRequest{
ProviderId: "anthropic-main",
Config: &v1pb.TranscriptionConfig{},
Audio: &v1pb.TranscriptionAudio{
Source: &v1pb.TranscriptionAudio_Content{Content: []byte("RIFF")},
Filename: "voice.wav",
ContentType: "audio/wav",
},
})
require.Error(t, err)
require.Contains(t, err.Error(), "capability unsupported")
})
t.Run("rejects unconfigured model", func(t *testing.T) { t.Run("rejects unconfigured model", func(t *testing.T) {
ts := NewTestService(t) ts := NewTestService(t)
defer ts.Cleanup() defer ts.Cleanup()
......
...@@ -624,4 +624,39 @@ func TestUpdateInstanceSetting(t *testing.T) { ...@@ -624,4 +624,39 @@ func TestUpdateInstanceSetting(t *testing.T) {
require.Equal(t, []string{"gpt-5.4-mini", "gpt-5.4"}, stored.GetProviders()[0].GetModels()) require.Equal(t, []string{"gpt-5.4-mini", "gpt-5.4"}, stored.GetProviders()[0].GetModels())
require.Equal(t, "gpt-5.4-mini", stored.GetProviders()[0].GetDefaultModel()) require.Equal(t, "gpt-5.4-mini", stored.GetProviders()[0].GetDefaultModel())
}) })
t.Run("UpdateInstanceSetting - Anthropic provider gets default endpoint", func(t *testing.T) {
ts := NewTestService(t)
defer ts.Cleanup()
hostUser, err := ts.CreateHostUser(ctx, "admin")
require.NoError(t, err)
adminCtx := ts.CreateUserContext(ctx, hostUser.ID)
_, err = ts.Service.UpdateInstanceSetting(adminCtx, &v1pb.UpdateInstanceSettingRequest{
Setting: &v1pb.InstanceSetting{
Name: "instance/settings/AI",
Value: &v1pb.InstanceSetting_AiSetting{
AiSetting: &v1pb.InstanceSetting_AISetting{
Providers: []*v1pb.InstanceSetting_AIProviderConfig{
{
Id: "anthropic-main",
Title: "Anthropic",
Type: v1pb.InstanceSetting_ANTHROPIC,
ApiKey: "sk-ant-test",
Models: []string{"claude-sonnet-4-5"},
DefaultModel: "claude-sonnet-4-5",
},
},
},
},
},
})
require.NoError(t, err)
stored, err := ts.Store.GetInstanceAISetting(ctx)
require.NoError(t, err)
require.Len(t, stored.GetProviders(), 1)
require.Equal(t, "https://api.anthropic.com/v1", stored.GetProviders()[0].GetEndpoint())
})
} }
...@@ -42,6 +42,7 @@ const providerTypeOptions = [ ...@@ -42,6 +42,7 @@ const providerTypeOptions = [
InstanceSetting_AIProviderType.OPENAI, InstanceSetting_AIProviderType.OPENAI,
InstanceSetting_AIProviderType.OPENAI_COMPATIBLE, InstanceSetting_AIProviderType.OPENAI_COMPATIBLE,
InstanceSetting_AIProviderType.GEMINI, InstanceSetting_AIProviderType.GEMINI,
InstanceSetting_AIProviderType.ANTHROPIC,
]; ];
const createProviderID = () => { const createProviderID = () => {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment