diff --git a/internal/api/chat/create_conversation_message_stream_v2.go b/internal/api/chat/create_conversation_message_stream_v2.go index b82adf5d..3537ec79 100644 --- a/internal/api/chat/create_conversation_message_stream_v2.go +++ b/internal/api/chat/create_conversation_message_stream_v2.go @@ -2,12 +2,14 @@ package chat import ( "context" + "fmt" "paperdebugger/internal/api/mapper" "paperdebugger/internal/libs/contextutil" "paperdebugger/internal/libs/shared" "paperdebugger/internal/models" "paperdebugger/internal/services" chatv2 "paperdebugger/pkg/gen/api/chat/v2" + "strings" "github.com/google/uuid" "github.com/openai/openai-go/v3" @@ -276,12 +278,50 @@ func (s *ChatServerV2) CreateConversationMessageStream( return s.sendStreamError(stream, err) } - // Usage is the same as ChatCompletion, just passing the stream parameter - llmProvider := &models.LLMProviderConfig{ - APIKey: settings.OpenAIAPIKey, + // Check if user has an API key for requested model + var llmProvider *models.LLMProviderConfig + var customModel *models.CustomModel + customModel = nil + + customModelID := req.GetCustomModelId() + if customModelID != "" { + for i := range settings.CustomModels { + if settings.CustomModels[i].Id.Hex() == customModelID { + customModel = &settings.CustomModels[i] + break + } + } + if customModel == nil { + return s.sendStreamError(stream, fmt.Errorf("custom model not found: %q", customModelID)) + } + modelSlug = customModel.Slug + } + + if customModel == nil { + // User did not specify API key for this model + llmProvider = &models.LLMProviderConfig{ + APIKey: "", + IsCustomModel: false, + } + } else { + customModel.BaseUrl = strings.ToLower(customModel.BaseUrl) + + if strings.Contains(customModel.BaseUrl, "paperdebugger.com") { + customModel.BaseUrl = "" + } + if !strings.HasPrefix(customModel.BaseUrl, "https://") { + customModel.BaseUrl = strings.Replace(customModel.BaseUrl, "http://", "", 1) + customModel.BaseUrl = "https://" + customModel.BaseUrl + } + + llmProvider = &models.LLMProviderConfig{ + APIKey: customModel.APIKey, + Endpoint: customModel.BaseUrl, + IsCustomModel: true, + } } - openaiChatHistory, inappChatHistory, err := s.aiClientV2.ChatCompletionStreamV2(ctx, stream, conversation.ID.Hex(), modelSlug, conversation.OpenaiChatHistoryCompletion, llmProvider) + openaiChatHistory, inappChatHistory, err := s.aiClientV2.ChatCompletionStreamV2(ctx, stream, conversation.ID.Hex(), modelSlug, conversation.OpenaiChatHistoryCompletion, llmProvider, customModel) if err != nil { return s.sendStreamError(stream, err) } @@ -307,7 +347,7 @@ func (s *ChatServerV2) CreateConversationMessageStream( for i, bsonMsg := range conversation.InappChatHistory { protoMessages[i] = mapper.BSONToChatMessageV2(bsonMsg) } - title, err := s.aiClientV2.GetConversationTitleV2(ctx, protoMessages, llmProvider) + title, err := s.aiClientV2.GetConversationTitleV2(ctx, protoMessages, llmProvider, modelSlug, customModel) if err != nil { s.logger.Error("Failed to get conversation title", "error", err, "conversationID", conversation.ID.Hex()) return diff --git a/internal/api/chat/list_supported_models_v2.go b/internal/api/chat/list_supported_models_v2.go index 1fb54575..ac1c4e6b 100644 --- a/internal/api/chat/list_supported_models_v2.go +++ b/internal/api/chat/list_supported_models_v2.go @@ -2,7 +2,6 @@ package chat import ( "context" - "strings" "paperdebugger/internal/libs/contextutil" chatv2 "paperdebugger/pkg/gen/api/chat/v2" @@ -220,22 +219,26 @@ func (s *ChatServerV2) ListSupportedModels( return nil, err } - hasOwnAPIKey := strings.TrimSpace(settings.OpenAIAPIKey) != "" - var models []*chatv2.SupportedModel - for _, config := range allModels { - // Choose the appropriate slug based on whether user has their own API key. - // - // Some models are only available via OpenRouter; for those, slugOpenAI may be empty. - // In that case, keep using the OpenRouter slug to avoid returning an empty model slug. - slug := config.slugOpenRouter - if hasOwnAPIKey && strings.TrimSpace(config.slugOpenAI) != "" { - slug = config.slugOpenAI - } + for _, model := range settings.CustomModels { + modelID := model.Id.Hex() + models = append(models, &chatv2.SupportedModel{ + Id: &modelID, + Name: model.Name, + Slug: model.Slug, + TotalContext: int64(model.ContextWindow), + MaxOutput: int64(model.MaxOutput), + InputPrice: int64(model.InputPrice), + OutputPrice: int64(model.OutputPrice), + IsCustom: true, + }) + } + + for _, config := range allModels { model := &chatv2.SupportedModel{ Name: config.name, - Slug: slug, + Slug: config.slugOpenRouter, TotalContext: config.totalContext, MaxOutput: config.maxOutput, InputPrice: config.inputPrice, @@ -243,9 +246,8 @@ func (s *ChatServerV2) ListSupportedModels( } // If model requires own key but user hasn't provided one, mark as disabled - if config.requireOwnKey && !hasOwnAPIKey { - model.Disabled = true - model.DisabledReason = stringPtr("Requires your own OpenAI API key. Configure it in Settings.") + if config.requireOwnKey { + continue } models = append(models, model) diff --git a/internal/api/mapper/user.go b/internal/api/mapper/user.go index 78c98ef3..09d31aa7 100644 --- a/internal/api/mapper/user.go +++ b/internal/api/mapper/user.go @@ -3,26 +3,75 @@ package mapper import ( "paperdebugger/internal/models" userv1 "paperdebugger/pkg/gen/api/user/v1" + + "go.mongodb.org/mongo-driver/v2/bson" ) func MapProtoSettingsToModel(settings *userv1.Settings) *models.Settings { + // Map the slice of custom models + customModels := make([]models.CustomModel, len(settings.CustomModels)) + for i, m := range settings.CustomModels { + var id bson.ObjectID + + id, err := bson.ObjectIDFromHex(m.Id) + if err != nil { + id = bson.NewObjectID() + } + + customModels[i] = models.CustomModel{ + Id: id, + Slug: m.Slug, + Name: m.Name, + BaseUrl: m.BaseUrl, + APIKey: m.ApiKey, + ContextWindow: m.ContextWindow, + MaxOutput: m.MaxOutput, + InputPrice: m.InputPrice, + OutputPrice: m.OutputPrice, + Temperature: m.Temperature, + ParallelToolCalls: m.ParallelToolCalls, + Store: m.Store, + } + } + return &models.Settings{ ShowShortcutsAfterSelection: settings.ShowShortcutsAfterSelection, FullWidthPaperDebuggerButton: settings.FullWidthPaperDebuggerButton, - EnableCitationSuggestion: settings.EnableCitationSuggestion, + EnableCitationSuggestion: settings.EnableCitationSuggestion, FullDocumentRag: settings.FullDocumentRag, ShowedOnboarding: settings.ShowedOnboarding, OpenAIAPIKey: settings.OpenaiApiKey, + CustomModels: customModels, } } func MapModelSettingsToProto(settings *models.Settings) *userv1.Settings { + // Map the slice back to Proto + customModels := make([]*userv1.CustomModel, len(settings.CustomModels)) + for i, m := range settings.CustomModels { + customModels[i] = &userv1.CustomModel{ + Id: m.Id.Hex(), + Slug: m.Slug, + Name: m.Name, + BaseUrl: m.BaseUrl, + ApiKey: m.APIKey, + ContextWindow: m.ContextWindow, + MaxOutput: m.MaxOutput, + InputPrice: m.InputPrice, + OutputPrice: m.OutputPrice, + Temperature: m.Temperature, + ParallelToolCalls: m.ParallelToolCalls, + Store: m.Store, + } + } + return &userv1.Settings{ ShowShortcutsAfterSelection: settings.ShowShortcutsAfterSelection, FullWidthPaperDebuggerButton: settings.FullWidthPaperDebuggerButton, - EnableCitationSuggestion: settings.EnableCitationSuggestion, + EnableCitationSuggestion: settings.EnableCitationSuggestion, FullDocumentRag: settings.FullDocumentRag, ShowedOnboarding: settings.ShowedOnboarding, OpenaiApiKey: settings.OpenAIAPIKey, + CustomModels: customModels, } } diff --git a/internal/models/llm_provider.go b/internal/models/llm_provider.go index 06f6b0e5..0c085fda 100644 --- a/internal/models/llm_provider.go +++ b/internal/models/llm_provider.go @@ -2,10 +2,13 @@ package models // LLMProviderConfig holds the configuration for LLM API calls. // If both Endpoint and APIKey are empty, the system default will be used. +// If IsCustomModel is true, the user-requested slug with corresponding +// API keys and endpoint should be used. type LLMProviderConfig struct { - Endpoint string - APIKey string - ModelName string + Endpoint string + APIKey string + ModelName string + IsCustomModel bool } // IsCustom returns true if the user has configured custom LLM provider settings. diff --git a/internal/models/user.go b/internal/models/user.go index 22e03ad2..a0350301 100644 --- a/internal/models/user.go +++ b/internal/models/user.go @@ -2,13 +2,29 @@ package models import "go.mongodb.org/mongo-driver/v2/bson" +type CustomModel struct { + Id bson.ObjectID `bson:"_id"` + Slug string `bson:"slug"` + Name string `bson:"name"` + BaseUrl string `bson:"base_url"` + APIKey string `bson:"api_key"` + ContextWindow int32 `bson:"context_window"` + MaxOutput int32 `bson:"max_output"` + InputPrice int32 `bson:"input_price"` + OutputPrice int32 `bson:"output_price"` + Temperature float32 `bson:"temperature"` + ParallelToolCalls bool `bson:"parallel_tool_calls"` + Store bool `bson:"store"` +} + type Settings struct { - ShowShortcutsAfterSelection bool `bson:"show_shortcuts_after_selection"` - FullWidthPaperDebuggerButton bool `bson:"full_width_paper_debugger_button"` - EnableCitationSuggestion bool `bson:"enable_citation_suggestion"` - FullDocumentRag bool `bson:"full_document_rag"` - ShowedOnboarding bool `bson:"showed_onboarding"` - OpenAIAPIKey string `bson:"openai_api_key"` + ShowShortcutsAfterSelection bool `bson:"show_shortcuts_after_selection"` + FullWidthPaperDebuggerButton bool `bson:"full_width_paper_debugger_button"` + EnableCitationSuggestion bool `bson:"enable_citation_suggestion"` + FullDocumentRag bool `bson:"full_document_rag"` + ShowedOnboarding bool `bson:"showed_onboarding"` + OpenAIAPIKey string `bson:"openai_api_key"` + CustomModels []CustomModel `bson:"custom_models"` } type User struct { diff --git a/internal/services/toolkit/client/client_v2.go b/internal/services/toolkit/client/client_v2.go index 87a1e26a..d32e01f1 100644 --- a/internal/services/toolkit/client/client_v2.go +++ b/internal/services/toolkit/client/client_v2.go @@ -32,18 +32,20 @@ func (a *AIClientV2) GetOpenAIClient(llmConfig *models.LLMProviderConfig) *opena var Endpoint string = llmConfig.Endpoint var APIKey string = llmConfig.APIKey - if Endpoint == "" { - if APIKey != "" { - // User provided their own API key, use the OpenAI-compatible endpoint - Endpoint = a.cfg.OpenAIBaseURL // standard openai base url - } else { - // suffix needed for cloudflare gateway - Endpoint = a.cfg.InferenceBaseURL + "/openrouter" + if !llmConfig.IsCustomModel { + if Endpoint == "" { + if APIKey != "" { + // User provided their own API key, use the OpenAI-compatible endpoint + Endpoint = a.cfg.OpenAIBaseURL // standard openai base url + } else { + // suffix needed for cloudflare gateway + Endpoint = a.cfg.InferenceBaseURL + "/openrouter" + } } - } - if APIKey == "" { - APIKey = a.cfg.InferenceAPIKey + if APIKey == "" { + APIKey = a.cfg.InferenceAPIKey + } } opts := []option.RequestOption{ diff --git a/internal/services/toolkit/client/completion_v2.go b/internal/services/toolkit/client/completion_v2.go index f10082bf..7266d669 100644 --- a/internal/services/toolkit/client/completion_v2.go +++ b/internal/services/toolkit/client/completion_v2.go @@ -25,8 +25,8 @@ import ( // 1. The full chat history sent to the language model (including any tool call results). // 2. The incremental chat history visible to the user (including tool call results and assistant responses). // 3. An error, if any occurred during the process. -func (a *AIClientV2) ChatCompletionV2(ctx context.Context, modelSlug string, messages OpenAIChatHistory, llmProvider *models.LLMProviderConfig) (OpenAIChatHistory, AppChatHistory, error) { - openaiChatHistory, inappChatHistory, err := a.ChatCompletionStreamV2(ctx, nil, "", modelSlug, messages, llmProvider) +func (a *AIClientV2) ChatCompletionV2(ctx context.Context, modelSlug string, messages OpenAIChatHistory, llmProvider *models.LLMProviderConfig, customModel *models.CustomModel) (OpenAIChatHistory, AppChatHistory, error) { + openaiChatHistory, inappChatHistory, err := a.ChatCompletionStreamV2(ctx, nil, "", modelSlug, messages, llmProvider, customModel) if err != nil { return nil, nil, err } @@ -54,7 +54,7 @@ func (a *AIClientV2) ChatCompletionV2(ctx context.Context, modelSlug string, mes // - If tool calls are required, it handles them and appends the results to the chat history, then continues the loop. // - If no tool calls are needed, it appends the assistant's response and exits the loop. // - Finally, it returns the updated chat histories and any error encountered. -func (a *AIClientV2) ChatCompletionStreamV2(ctx context.Context, callbackStream chatv2.ChatService_CreateConversationMessageStreamServer, conversationId string, modelSlug string, messages OpenAIChatHistory, llmProvider *models.LLMProviderConfig) (OpenAIChatHistory, AppChatHistory, error) { +func (a *AIClientV2) ChatCompletionStreamV2(ctx context.Context, callbackStream chatv2.ChatService_CreateConversationMessageStreamServer, conversationId string, modelSlug string, messages OpenAIChatHistory, llmProvider *models.LLMProviderConfig, customModel *models.CustomModel) (OpenAIChatHistory, AppChatHistory, error) { openaiChatHistory := messages inappChatHistory := AppChatHistory{} @@ -66,7 +66,7 @@ func (a *AIClientV2) ChatCompletionStreamV2(ctx context.Context, callbackStream }() oaiClient := a.GetOpenAIClient(llmProvider) - params := getDefaultParamsV2(modelSlug, a.toolCallHandler.Registry) + params := getDefaultParamsV2(modelSlug, a.toolCallHandler.Registry, customModel) for { params.Messages = openaiChatHistory diff --git a/internal/services/toolkit/client/get_citation_keys.go b/internal/services/toolkit/client/get_citation_keys.go index 1995d590..2344d49d 100644 --- a/internal/services/toolkit/client/get_citation_keys.go +++ b/internal/services/toolkit/client/get_citation_keys.go @@ -244,7 +244,7 @@ func (a *AIClientV2) GetCitationKeys(ctx context.Context, sentence string, userI _, resp, err := a.ChatCompletionV2(ctx, "gpt-5.2", OpenAIChatHistory{ openai.SystemMessage("You are a helpful assistant that suggests relevant citation keys."), openai.UserMessage(message), - }, llmProvider) + }, llmProvider, nil) if err != nil { return nil, err diff --git a/internal/services/toolkit/client/get_conversation_title_v2.go b/internal/services/toolkit/client/get_conversation_title_v2.go index 6c92f0c2..27840c7c 100644 --- a/internal/services/toolkit/client/get_conversation_title_v2.go +++ b/internal/services/toolkit/client/get_conversation_title_v2.go @@ -13,7 +13,7 @@ import ( "github.com/samber/lo" ) -func (a *AIClientV2) GetConversationTitleV2(ctx context.Context, inappChatHistory []*chatv2.Message, llmProvider *models.LLMProviderConfig) (string, error) { +func (a *AIClientV2) GetConversationTitleV2(ctx context.Context, inappChatHistory []*chatv2.Message, llmProvider *models.LLMProviderConfig, modelSlug string, customModel *models.CustomModel) (string, error) { messages := lo.Map(inappChatHistory, func(message *chatv2.Message, _ int) string { if _, ok := message.Payload.MessageType.(*chatv2.MessagePayload_Assistant); ok { return fmt.Sprintf("Assistant: %s", message.Payload.GetAssistant().GetContent()) @@ -29,10 +29,16 @@ func (a *AIClientV2) GetConversationTitleV2(ctx context.Context, inappChatHistor message := strings.Join(messages, "\n") message = fmt.Sprintf("%s\nBased on above conversation, generate a short, clear, and descriptive title that summarizes the main topic or purpose of the discussion. The title should be concise, specific, and use natural language. Avoid vague or generic titles. Use abbreviation and short words if possible. Use 3-5 words if possible. Give me the title only, no other text including any other words.", message) - _, resp, err := a.ChatCompletionV2(ctx, "gpt-5-nano", OpenAIChatHistory{ + // Default model if user is not using their own + modelToUse := "gpt-5-nano" + if llmProvider.IsCustomModel { + modelToUse = modelSlug + } + + _, resp, err := a.ChatCompletionV2(ctx, modelToUse, OpenAIChatHistory{ openai.SystemMessage("You are a helpful assistant that generates a title for a conversation."), openai.UserMessage(message), - }, llmProvider) + }, llmProvider, customModel) if err != nil { return "", err } diff --git a/internal/services/toolkit/client/utils_v2.go b/internal/services/toolkit/client/utils_v2.go index 69e73071..884b91eb 100644 --- a/internal/services/toolkit/client/utils_v2.go +++ b/internal/services/toolkit/client/utils_v2.go @@ -10,6 +10,7 @@ import ( "paperdebugger/internal/libs/cfg" "paperdebugger/internal/libs/db" "paperdebugger/internal/libs/logger" + "paperdebugger/internal/models" "paperdebugger/internal/services" "paperdebugger/internal/services/toolkit/registry" filetools "paperdebugger/internal/services/toolkit/tools/files" @@ -53,7 +54,7 @@ func appendAssistantTextResponseV2(openaiChatHistory *OpenAIChatHistory, inappCh }) } -func getDefaultParamsV2(modelSlug string, toolRegistry *registry.ToolRegistryV2) openaiv3.ChatCompletionNewParams { +func getDefaultParamsV2(modelSlug string, toolRegistry *registry.ToolRegistryV2, customModel *models.CustomModel) openaiv3.ChatCompletionNewParams { var reasoningModels = []string{ "gpt-5", "gpt-5-mini", @@ -66,6 +67,25 @@ func getDefaultParamsV2(modelSlug string, toolRegistry *registry.ToolRegistryV2) "o1", "codex-mini-latest", } + + if customModel != nil { + params := openaiv3.ChatCompletionNewParams{ + Model: customModel.Slug, + Temperature: openaiv3.Float(float64(customModel.Temperature)), + MaxCompletionTokens: openaiv3.Int(int64(customModel.MaxOutput)), + Tools: toolRegistry.GetTools(), + ParallelToolCalls: openaiv3.Bool(customModel.ParallelToolCalls), + } + + // Store param should only be included if it is true + // Some providers like Gemini might not support the param at all even if false + if customModel.Store { + params.Store = openaiv3.Bool(customModel.Store) + } + + return params + } + for _, model := range reasoningModels { if strings.Contains(modelSlug, model) { return openaiv3.ChatCompletionNewParams{ diff --git a/pkg/gen/api/chat/v2/chat.pb.go b/pkg/gen/api/chat/v2/chat.pb.go index 0d312c55..22fc2786 100644 --- a/pkg/gen/api/chat/v2/chat.pb.go +++ b/pkg/gen/api/chat/v2/chat.pb.go @@ -7,13 +7,12 @@ package chatv2 import ( - reflect "reflect" - sync "sync" - unsafe "unsafe" - _ "google.golang.org/genproto/googleapis/api/annotations" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" ) const ( @@ -1035,6 +1034,8 @@ type SupportedModel struct { OutputPrice int64 `protobuf:"varint,6,opt,name=output_price,json=outputPrice,proto3" json:"output_price,omitempty"` // in cents per 1M tokens Disabled bool `protobuf:"varint,7,opt,name=disabled,proto3" json:"disabled,omitempty"` // If true, the model is disabled and cannot be used DisabledReason *string `protobuf:"bytes,8,opt,name=disabled_reason,json=disabledReason,proto3,oneof" json:"disabled_reason,omitempty"` // The reason why the model is disabled + IsCustom bool `protobuf:"varint,9,opt,name=is_custom,json=isCustom,proto3" json:"is_custom,omitempty"` + Id *string `protobuf:"bytes,10,opt,name=id,proto3,oneof" json:"id,omitempty"` // Custom model unique ID (empty for built-in models) unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1125,6 +1126,20 @@ func (x *SupportedModel) GetDisabledReason() string { return "" } +func (x *SupportedModel) GetIsCustom() bool { + if x != nil { + return x.IsCustom + } + return false +} + +func (x *SupportedModel) GetId() string { + if x != nil && x.Id != nil { + return *x.Id + } + return "" +} + type ListSupportedModelsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -1628,6 +1643,7 @@ type CreateConversationMessageStreamRequest struct { UserSelectedText *string `protobuf:"bytes,5,opt,name=user_selected_text,json=userSelectedText,proto3,oneof" json:"user_selected_text,omitempty"` ConversationType *ConversationType `protobuf:"varint,6,opt,name=conversation_type,json=conversationType,proto3,enum=chat.v2.ConversationType,oneof" json:"conversation_type,omitempty"` Surrounding *string `protobuf:"bytes,8,opt,name=surrounding,proto3,oneof" json:"surrounding,omitempty"` + CustomModelId *string `protobuf:"bytes,9,opt,name=custom_model_id,json=customModelId,proto3,oneof" json:"custom_model_id,omitempty"` // Selected custom model ID unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1711,6 +1727,13 @@ func (x *CreateConversationMessageStreamRequest) GetSurrounding() string { return "" } +func (x *CreateConversationMessageStreamRequest) GetCustomModelId() string { + if x != nil && x.CustomModelId != nil { + return *x.CustomModelId + } + return "" +} + // Response for streaming a message within an existing conversation type CreateConversationMessageStreamResponse struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -2062,7 +2085,7 @@ const file_chat_v2_chat_proto_rawDesc = "" + "\fconversation\x18\x01 \x01(\v2\x15.chat.v2.ConversationR\fconversation\"D\n" + "\x19DeleteConversationRequest\x12'\n" + "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\"\x1c\n" + - "\x1aDeleteConversationResponse\"\x9e\x02\n" + + "\x1aDeleteConversationResponse\"\xd7\x02\n" + "\x0eSupportedModel\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" + "\x04slug\x18\x02 \x01(\tR\x04slug\x12#\n" + @@ -2073,8 +2096,12 @@ const file_chat_v2_chat_proto_rawDesc = "" + "inputPrice\x12!\n" + "\foutput_price\x18\x06 \x01(\x03R\voutputPrice\x12\x1a\n" + "\bdisabled\x18\a \x01(\bR\bdisabled\x12,\n" + - "\x0fdisabled_reason\x18\b \x01(\tH\x00R\x0edisabledReason\x88\x01\x01B\x12\n" + - "\x10_disabled_reason\"\x1c\n" + + "\x0fdisabled_reason\x18\b \x01(\tH\x00R\x0edisabledReason\x88\x01\x01\x12\x1b\n" + + "\tis_custom\x18\t \x01(\bR\bisCustom\x12\x13\n" + + "\x02id\x18\n" + + " \x01(\tH\x01R\x02id\x88\x01\x01B\x12\n" + + "\x10_disabled_reasonB\x05\n" + + "\x03_id\"\x1c\n" + "\x1aListSupportedModelsRequest\"N\n" + "\x1bListSupportedModelsResponse\x12/\n" + "\x06models\x18\x01 \x03(\v2\x17.chat.v2.SupportedModelR\x06models\"^\n" + @@ -2105,7 +2132,7 @@ const file_chat_v2_chat_proto_rawDesc = "" + "\x12StreamFinalization\x12'\n" + "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\"2\n" + "\vStreamError\x12#\n" + - "\rerror_message\x18\x01 \x01(\tR\ferrorMessage\"\xaf\x03\n" + + "\rerror_message\x18\x01 \x01(\tR\ferrorMessage\"\xf0\x03\n" + "&CreateConversationMessageStreamRequest\x12\x1d\n" + "\n" + "project_id\x18\x01 \x01(\tR\tprojectId\x12,\n" + @@ -2115,11 +2142,13 @@ const file_chat_v2_chat_proto_rawDesc = "" + "\fuser_message\x18\x04 \x01(\tR\vuserMessage\x121\n" + "\x12user_selected_text\x18\x05 \x01(\tH\x01R\x10userSelectedText\x88\x01\x01\x12K\n" + "\x11conversation_type\x18\x06 \x01(\x0e2\x19.chat.v2.ConversationTypeH\x02R\x10conversationType\x88\x01\x01\x12%\n" + - "\vsurrounding\x18\b \x01(\tH\x03R\vsurrounding\x88\x01\x01B\x12\n" + + "\vsurrounding\x18\b \x01(\tH\x03R\vsurrounding\x88\x01\x01\x12+\n" + + "\x0fcustom_model_id\x18\t \x01(\tH\x04R\rcustomModelId\x88\x01\x01B\x12\n" + "\x10_conversation_idB\x15\n" + "\x13_user_selected_textB\x14\n" + "\x12_conversation_typeB\x0e\n" + - "\f_surrounding\"\xfd\x04\n" + + "\f_surroundingB\x12\n" + + "\x10_custom_model_id\"\xfd\x04\n" + "'CreateConversationMessageStreamResponse\x12T\n" + "\x15stream_initialization\x18\x01 \x01(\v2\x1d.chat.v2.StreamInitializationH\x00R\x14streamInitialization\x12F\n" + "\x11stream_part_begin\x18\x02 \x01(\v2\x18.chat.v2.StreamPartBeginH\x00R\x0fstreamPartBegin\x12<\n" + diff --git a/pkg/gen/api/user/v1/user.pb.go b/pkg/gen/api/user/v1/user.pb.go index 41752992..ef08a101 100644 --- a/pkg/gen/api/user/v1/user.pb.go +++ b/pkg/gen/api/user/v1/user.pb.go @@ -615,6 +615,138 @@ func (*DeletePromptResponse) Descriptor() ([]byte, []int) { return file_user_v1_user_proto_rawDescGZIP(), []int{11} } +type CustomModel struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Slug string `protobuf:"bytes,2,opt,name=slug,proto3" json:"slug,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + BaseUrl string `protobuf:"bytes,4,opt,name=base_url,json=baseUrl,proto3" json:"base_url,omitempty"` + ApiKey string `protobuf:"bytes,5,opt,name=api_key,json=apiKey,proto3" json:"api_key,omitempty"` + ContextWindow int32 `protobuf:"varint,6,opt,name=context_window,json=contextWindow,proto3" json:"context_window,omitempty"` + MaxOutput int32 `protobuf:"varint,7,opt,name=max_output,json=maxOutput,proto3" json:"max_output,omitempty"` + InputPrice int32 `protobuf:"varint,8,opt,name=input_price,json=inputPrice,proto3" json:"input_price,omitempty"` + OutputPrice int32 `protobuf:"varint,9,opt,name=output_price,json=outputPrice,proto3" json:"output_price,omitempty"` + Temperature float32 `protobuf:"fixed32,10,opt,name=temperature,proto3" json:"temperature,omitempty"` + ParallelToolCalls bool `protobuf:"varint,11,opt,name=parallel_tool_calls,json=parallelToolCalls,proto3" json:"parallel_tool_calls,omitempty"` + Store bool `protobuf:"varint,12,opt,name=store,proto3" json:"store,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CustomModel) Reset() { + *x = CustomModel{} + mi := &file_user_v1_user_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CustomModel) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CustomModel) ProtoMessage() {} + +func (x *CustomModel) ProtoReflect() protoreflect.Message { + mi := &file_user_v1_user_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CustomModel.ProtoReflect.Descriptor instead. +func (*CustomModel) Descriptor() ([]byte, []int) { + return file_user_v1_user_proto_rawDescGZIP(), []int{12} +} + +func (x *CustomModel) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *CustomModel) GetSlug() string { + if x != nil { + return x.Slug + } + return "" +} + +func (x *CustomModel) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CustomModel) GetBaseUrl() string { + if x != nil { + return x.BaseUrl + } + return "" +} + +func (x *CustomModel) GetApiKey() string { + if x != nil { + return x.ApiKey + } + return "" +} + +func (x *CustomModel) GetContextWindow() int32 { + if x != nil { + return x.ContextWindow + } + return 0 +} + +func (x *CustomModel) GetMaxOutput() int32 { + if x != nil { + return x.MaxOutput + } + return 0 +} + +func (x *CustomModel) GetInputPrice() int32 { + if x != nil { + return x.InputPrice + } + return 0 +} + +func (x *CustomModel) GetOutputPrice() int32 { + if x != nil { + return x.OutputPrice + } + return 0 +} + +func (x *CustomModel) GetTemperature() float32 { + if x != nil { + return x.Temperature + } + return 0 +} + +func (x *CustomModel) GetParallelToolCalls() bool { + if x != nil { + return x.ParallelToolCalls + } + return false +} + +func (x *CustomModel) GetStore() bool { + if x != nil { + return x.Store + } + return false +} + type Settings struct { state protoimpl.MessageState `protogen:"open.v1"` ShowShortcutsAfterSelection bool `protobuf:"varint,1,opt,name=show_shortcuts_after_selection,json=showShortcutsAfterSelection,proto3" json:"show_shortcuts_after_selection,omitempty"` @@ -623,13 +755,14 @@ type Settings struct { FullDocumentRag bool `protobuf:"varint,4,opt,name=full_document_rag,json=fullDocumentRag,proto3" json:"full_document_rag,omitempty"` ShowedOnboarding bool `protobuf:"varint,5,opt,name=showed_onboarding,json=showedOnboarding,proto3" json:"showed_onboarding,omitempty"` OpenaiApiKey string `protobuf:"bytes,6,opt,name=openai_api_key,json=openaiApiKey,proto3" json:"openai_api_key,omitempty"` + CustomModels []*CustomModel `protobuf:"bytes,7,rep,name=custom_models,json=customModels,proto3" json:"custom_models,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Settings) Reset() { *x = Settings{} - mi := &file_user_v1_user_proto_msgTypes[12] + mi := &file_user_v1_user_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -641,7 +774,7 @@ func (x *Settings) String() string { func (*Settings) ProtoMessage() {} func (x *Settings) ProtoReflect() protoreflect.Message { - mi := &file_user_v1_user_proto_msgTypes[12] + mi := &file_user_v1_user_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -654,7 +787,7 @@ func (x *Settings) ProtoReflect() protoreflect.Message { // Deprecated: Use Settings.ProtoReflect.Descriptor instead. func (*Settings) Descriptor() ([]byte, []int) { - return file_user_v1_user_proto_rawDescGZIP(), []int{12} + return file_user_v1_user_proto_rawDescGZIP(), []int{13} } func (x *Settings) GetShowShortcutsAfterSelection() bool { @@ -699,6 +832,13 @@ func (x *Settings) GetOpenaiApiKey() string { return "" } +func (x *Settings) GetCustomModels() []*CustomModel { + if x != nil { + return x.CustomModels + } + return nil +} + type GetSettingsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -707,7 +847,7 @@ type GetSettingsRequest struct { func (x *GetSettingsRequest) Reset() { *x = GetSettingsRequest{} - mi := &file_user_v1_user_proto_msgTypes[13] + mi := &file_user_v1_user_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -719,7 +859,7 @@ func (x *GetSettingsRequest) String() string { func (*GetSettingsRequest) ProtoMessage() {} func (x *GetSettingsRequest) ProtoReflect() protoreflect.Message { - mi := &file_user_v1_user_proto_msgTypes[13] + mi := &file_user_v1_user_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -732,7 +872,7 @@ func (x *GetSettingsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSettingsRequest.ProtoReflect.Descriptor instead. func (*GetSettingsRequest) Descriptor() ([]byte, []int) { - return file_user_v1_user_proto_rawDescGZIP(), []int{13} + return file_user_v1_user_proto_rawDescGZIP(), []int{14} } type GetSettingsResponse struct { @@ -744,7 +884,7 @@ type GetSettingsResponse struct { func (x *GetSettingsResponse) Reset() { *x = GetSettingsResponse{} - mi := &file_user_v1_user_proto_msgTypes[14] + mi := &file_user_v1_user_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -756,7 +896,7 @@ func (x *GetSettingsResponse) String() string { func (*GetSettingsResponse) ProtoMessage() {} func (x *GetSettingsResponse) ProtoReflect() protoreflect.Message { - mi := &file_user_v1_user_proto_msgTypes[14] + mi := &file_user_v1_user_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -769,7 +909,7 @@ func (x *GetSettingsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSettingsResponse.ProtoReflect.Descriptor instead. func (*GetSettingsResponse) Descriptor() ([]byte, []int) { - return file_user_v1_user_proto_rawDescGZIP(), []int{14} + return file_user_v1_user_proto_rawDescGZIP(), []int{15} } func (x *GetSettingsResponse) GetSettings() *Settings { @@ -788,7 +928,7 @@ type UpdateSettingsRequest struct { func (x *UpdateSettingsRequest) Reset() { *x = UpdateSettingsRequest{} - mi := &file_user_v1_user_proto_msgTypes[15] + mi := &file_user_v1_user_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -800,7 +940,7 @@ func (x *UpdateSettingsRequest) String() string { func (*UpdateSettingsRequest) ProtoMessage() {} func (x *UpdateSettingsRequest) ProtoReflect() protoreflect.Message { - mi := &file_user_v1_user_proto_msgTypes[15] + mi := &file_user_v1_user_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -813,7 +953,7 @@ func (x *UpdateSettingsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateSettingsRequest.ProtoReflect.Descriptor instead. func (*UpdateSettingsRequest) Descriptor() ([]byte, []int) { - return file_user_v1_user_proto_rawDescGZIP(), []int{15} + return file_user_v1_user_proto_rawDescGZIP(), []int{16} } func (x *UpdateSettingsRequest) GetSettings() *Settings { @@ -832,7 +972,7 @@ type UpdateSettingsResponse struct { func (x *UpdateSettingsResponse) Reset() { *x = UpdateSettingsResponse{} - mi := &file_user_v1_user_proto_msgTypes[16] + mi := &file_user_v1_user_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -844,7 +984,7 @@ func (x *UpdateSettingsResponse) String() string { func (*UpdateSettingsResponse) ProtoMessage() {} func (x *UpdateSettingsResponse) ProtoReflect() protoreflect.Message { - mi := &file_user_v1_user_proto_msgTypes[16] + mi := &file_user_v1_user_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -857,7 +997,7 @@ func (x *UpdateSettingsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateSettingsResponse.ProtoReflect.Descriptor instead. func (*UpdateSettingsResponse) Descriptor() ([]byte, []int) { - return file_user_v1_user_proto_rawDescGZIP(), []int{16} + return file_user_v1_user_proto_rawDescGZIP(), []int{17} } func (x *UpdateSettingsResponse) GetSettings() *Settings { @@ -875,7 +1015,7 @@ type ResetSettingsRequest struct { func (x *ResetSettingsRequest) Reset() { *x = ResetSettingsRequest{} - mi := &file_user_v1_user_proto_msgTypes[17] + mi := &file_user_v1_user_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -887,7 +1027,7 @@ func (x *ResetSettingsRequest) String() string { func (*ResetSettingsRequest) ProtoMessage() {} func (x *ResetSettingsRequest) ProtoReflect() protoreflect.Message { - mi := &file_user_v1_user_proto_msgTypes[17] + mi := &file_user_v1_user_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -900,7 +1040,7 @@ func (x *ResetSettingsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ResetSettingsRequest.ProtoReflect.Descriptor instead. func (*ResetSettingsRequest) Descriptor() ([]byte, []int) { - return file_user_v1_user_proto_rawDescGZIP(), []int{17} + return file_user_v1_user_proto_rawDescGZIP(), []int{18} } type ResetSettingsResponse struct { @@ -912,7 +1052,7 @@ type ResetSettingsResponse struct { func (x *ResetSettingsResponse) Reset() { *x = ResetSettingsResponse{} - mi := &file_user_v1_user_proto_msgTypes[18] + mi := &file_user_v1_user_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -924,7 +1064,7 @@ func (x *ResetSettingsResponse) String() string { func (*ResetSettingsResponse) ProtoMessage() {} func (x *ResetSettingsResponse) ProtoReflect() protoreflect.Message { - mi := &file_user_v1_user_proto_msgTypes[18] + mi := &file_user_v1_user_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -937,7 +1077,7 @@ func (x *ResetSettingsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ResetSettingsResponse.ProtoReflect.Descriptor instead. func (*ResetSettingsResponse) Descriptor() ([]byte, []int) { - return file_user_v1_user_proto_rawDescGZIP(), []int{18} + return file_user_v1_user_proto_rawDescGZIP(), []int{19} } func (x *ResetSettingsResponse) GetSettings() *Settings { @@ -955,7 +1095,7 @@ type GetUserInstructionsRequest struct { func (x *GetUserInstructionsRequest) Reset() { *x = GetUserInstructionsRequest{} - mi := &file_user_v1_user_proto_msgTypes[19] + mi := &file_user_v1_user_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -967,7 +1107,7 @@ func (x *GetUserInstructionsRequest) String() string { func (*GetUserInstructionsRequest) ProtoMessage() {} func (x *GetUserInstructionsRequest) ProtoReflect() protoreflect.Message { - mi := &file_user_v1_user_proto_msgTypes[19] + mi := &file_user_v1_user_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -980,7 +1120,7 @@ func (x *GetUserInstructionsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetUserInstructionsRequest.ProtoReflect.Descriptor instead. func (*GetUserInstructionsRequest) Descriptor() ([]byte, []int) { - return file_user_v1_user_proto_rawDescGZIP(), []int{19} + return file_user_v1_user_proto_rawDescGZIP(), []int{20} } type GetUserInstructionsResponse struct { @@ -992,7 +1132,7 @@ type GetUserInstructionsResponse struct { func (x *GetUserInstructionsResponse) Reset() { *x = GetUserInstructionsResponse{} - mi := &file_user_v1_user_proto_msgTypes[20] + mi := &file_user_v1_user_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1004,7 +1144,7 @@ func (x *GetUserInstructionsResponse) String() string { func (*GetUserInstructionsResponse) ProtoMessage() {} func (x *GetUserInstructionsResponse) ProtoReflect() protoreflect.Message { - mi := &file_user_v1_user_proto_msgTypes[20] + mi := &file_user_v1_user_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1017,7 +1157,7 @@ func (x *GetUserInstructionsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetUserInstructionsResponse.ProtoReflect.Descriptor instead. func (*GetUserInstructionsResponse) Descriptor() ([]byte, []int) { - return file_user_v1_user_proto_rawDescGZIP(), []int{20} + return file_user_v1_user_proto_rawDescGZIP(), []int{21} } func (x *GetUserInstructionsResponse) GetInstructions() string { @@ -1036,7 +1176,7 @@ type UpsertUserInstructionsRequest struct { func (x *UpsertUserInstructionsRequest) Reset() { *x = UpsertUserInstructionsRequest{} - mi := &file_user_v1_user_proto_msgTypes[21] + mi := &file_user_v1_user_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1048,7 +1188,7 @@ func (x *UpsertUserInstructionsRequest) String() string { func (*UpsertUserInstructionsRequest) ProtoMessage() {} func (x *UpsertUserInstructionsRequest) ProtoReflect() protoreflect.Message { - mi := &file_user_v1_user_proto_msgTypes[21] + mi := &file_user_v1_user_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1061,7 +1201,7 @@ func (x *UpsertUserInstructionsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpsertUserInstructionsRequest.ProtoReflect.Descriptor instead. func (*UpsertUserInstructionsRequest) Descriptor() ([]byte, []int) { - return file_user_v1_user_proto_rawDescGZIP(), []int{21} + return file_user_v1_user_proto_rawDescGZIP(), []int{22} } func (x *UpsertUserInstructionsRequest) GetInstructions() string { @@ -1080,7 +1220,7 @@ type UpsertUserInstructionsResponse struct { func (x *UpsertUserInstructionsResponse) Reset() { *x = UpsertUserInstructionsResponse{} - mi := &file_user_v1_user_proto_msgTypes[22] + mi := &file_user_v1_user_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1092,7 +1232,7 @@ func (x *UpsertUserInstructionsResponse) String() string { func (*UpsertUserInstructionsResponse) ProtoMessage() {} func (x *UpsertUserInstructionsResponse) ProtoReflect() protoreflect.Message { - mi := &file_user_v1_user_proto_msgTypes[22] + mi := &file_user_v1_user_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1105,7 +1245,7 @@ func (x *UpsertUserInstructionsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpsertUserInstructionsResponse.ProtoReflect.Descriptor instead. func (*UpsertUserInstructionsResponse) Descriptor() ([]byte, []int) { - return file_user_v1_user_proto_rawDescGZIP(), []int{22} + return file_user_v1_user_proto_rawDescGZIP(), []int{23} } func (x *UpsertUserInstructionsResponse) GetInstructions() string { @@ -1153,14 +1293,31 @@ const file_user_v1_user_proto_rawDesc = "" + "\x06prompt\x18\x01 \x01(\v2\x0f.user.v1.PromptR\x06prompt\"2\n" + "\x13DeletePromptRequest\x12\x1b\n" + "\tprompt_id\x18\x01 \x01(\tR\bpromptId\"\x16\n" + - "\x14DeletePromptResponse\"\xd4\x02\n" + + "\x14DeletePromptResponse\"\xeb\x02\n" + + "\vCustomModel\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + + "\x04slug\x18\x02 \x01(\tR\x04slug\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\x12\x19\n" + + "\bbase_url\x18\x04 \x01(\tR\abaseUrl\x12\x17\n" + + "\aapi_key\x18\x05 \x01(\tR\x06apiKey\x12%\n" + + "\x0econtext_window\x18\x06 \x01(\x05R\rcontextWindow\x12\x1d\n" + + "\n" + + "max_output\x18\a \x01(\x05R\tmaxOutput\x12\x1f\n" + + "\vinput_price\x18\b \x01(\x05R\n" + + "inputPrice\x12!\n" + + "\foutput_price\x18\t \x01(\x05R\voutputPrice\x12 \n" + + "\vtemperature\x18\n" + + " \x01(\x02R\vtemperature\x12.\n" + + "\x13parallel_tool_calls\x18\v \x01(\bR\x11parallelToolCalls\x12\x14\n" + + "\x05store\x18\f \x01(\bR\x05store\"\x8f\x03\n" + "\bSettings\x12C\n" + "\x1eshow_shortcuts_after_selection\x18\x01 \x01(\bR\x1bshowShortcutsAfterSelection\x12F\n" + " full_width_paper_debugger_button\x18\x02 \x01(\bR\x1cfullWidthPaperDebuggerButton\x12<\n" + "\x1aenable_citation_suggestion\x18\x03 \x01(\bR\x18enableCitationSuggestion\x12*\n" + "\x11full_document_rag\x18\x04 \x01(\bR\x0ffullDocumentRag\x12+\n" + "\x11showed_onboarding\x18\x05 \x01(\bR\x10showedOnboarding\x12$\n" + - "\x0eopenai_api_key\x18\x06 \x01(\tR\fopenaiApiKey\"\x14\n" + + "\x0eopenai_api_key\x18\x06 \x01(\tR\fopenaiApiKey\x129\n" + + "\rcustom_models\x18\a \x03(\v2\x14.user.v1.CustomModelR\fcustomModels\"\x14\n" + "\x12GetSettingsRequest\"D\n" + "\x13GetSettingsResponse\x12-\n" + "\bsettings\x18\x01 \x01(\v2\x11.user.v1.SettingsR\bsettings\"F\n" + @@ -1204,7 +1361,7 @@ func file_user_v1_user_proto_rawDescGZIP() []byte { return file_user_v1_user_proto_rawDescData } -var file_user_v1_user_proto_msgTypes = make([]protoimpl.MessageInfo, 23) +var file_user_v1_user_proto_msgTypes = make([]protoimpl.MessageInfo, 24) var file_user_v1_user_proto_goTypes = []any{ (*User)(nil), // 0: user.v1.User (*GetUserRequest)(nil), // 1: user.v1.GetUserRequest @@ -1218,55 +1375,57 @@ var file_user_v1_user_proto_goTypes = []any{ (*UpdatePromptResponse)(nil), // 9: user.v1.UpdatePromptResponse (*DeletePromptRequest)(nil), // 10: user.v1.DeletePromptRequest (*DeletePromptResponse)(nil), // 11: user.v1.DeletePromptResponse - (*Settings)(nil), // 12: user.v1.Settings - (*GetSettingsRequest)(nil), // 13: user.v1.GetSettingsRequest - (*GetSettingsResponse)(nil), // 14: user.v1.GetSettingsResponse - (*UpdateSettingsRequest)(nil), // 15: user.v1.UpdateSettingsRequest - (*UpdateSettingsResponse)(nil), // 16: user.v1.UpdateSettingsResponse - (*ResetSettingsRequest)(nil), // 17: user.v1.ResetSettingsRequest - (*ResetSettingsResponse)(nil), // 18: user.v1.ResetSettingsResponse - (*GetUserInstructionsRequest)(nil), // 19: user.v1.GetUserInstructionsRequest - (*GetUserInstructionsResponse)(nil), // 20: user.v1.GetUserInstructionsResponse - (*UpsertUserInstructionsRequest)(nil), // 21: user.v1.UpsertUserInstructionsRequest - (*UpsertUserInstructionsResponse)(nil), // 22: user.v1.UpsertUserInstructionsResponse - (*timestamppb.Timestamp)(nil), // 23: google.protobuf.Timestamp + (*CustomModel)(nil), // 12: user.v1.CustomModel + (*Settings)(nil), // 13: user.v1.Settings + (*GetSettingsRequest)(nil), // 14: user.v1.GetSettingsRequest + (*GetSettingsResponse)(nil), // 15: user.v1.GetSettingsResponse + (*UpdateSettingsRequest)(nil), // 16: user.v1.UpdateSettingsRequest + (*UpdateSettingsResponse)(nil), // 17: user.v1.UpdateSettingsResponse + (*ResetSettingsRequest)(nil), // 18: user.v1.ResetSettingsRequest + (*ResetSettingsResponse)(nil), // 19: user.v1.ResetSettingsResponse + (*GetUserInstructionsRequest)(nil), // 20: user.v1.GetUserInstructionsRequest + (*GetUserInstructionsResponse)(nil), // 21: user.v1.GetUserInstructionsResponse + (*UpsertUserInstructionsRequest)(nil), // 22: user.v1.UpsertUserInstructionsRequest + (*UpsertUserInstructionsResponse)(nil), // 23: user.v1.UpsertUserInstructionsResponse + (*timestamppb.Timestamp)(nil), // 24: google.protobuf.Timestamp } var file_user_v1_user_proto_depIdxs = []int32{ 0, // 0: user.v1.GetUserResponse.user:type_name -> user.v1.User - 23, // 1: user.v1.Prompt.created_at:type_name -> google.protobuf.Timestamp - 23, // 2: user.v1.Prompt.updated_at:type_name -> google.protobuf.Timestamp + 24, // 1: user.v1.Prompt.created_at:type_name -> google.protobuf.Timestamp + 24, // 2: user.v1.Prompt.updated_at:type_name -> google.protobuf.Timestamp 3, // 3: user.v1.ListPromptsResponse.prompts:type_name -> user.v1.Prompt 3, // 4: user.v1.CreatePromptResponse.prompt:type_name -> user.v1.Prompt 3, // 5: user.v1.UpdatePromptResponse.prompt:type_name -> user.v1.Prompt - 12, // 6: user.v1.GetSettingsResponse.settings:type_name -> user.v1.Settings - 12, // 7: user.v1.UpdateSettingsRequest.settings:type_name -> user.v1.Settings - 12, // 8: user.v1.UpdateSettingsResponse.settings:type_name -> user.v1.Settings - 12, // 9: user.v1.ResetSettingsResponse.settings:type_name -> user.v1.Settings - 1, // 10: user.v1.UserService.GetUser:input_type -> user.v1.GetUserRequest - 4, // 11: user.v1.UserService.ListPrompts:input_type -> user.v1.ListPromptsRequest - 6, // 12: user.v1.UserService.CreatePrompt:input_type -> user.v1.CreatePromptRequest - 8, // 13: user.v1.UserService.UpdatePrompt:input_type -> user.v1.UpdatePromptRequest - 19, // 14: user.v1.UserService.GetUserInstructions:input_type -> user.v1.GetUserInstructionsRequest - 21, // 15: user.v1.UserService.UpsertUserInstructions:input_type -> user.v1.UpsertUserInstructionsRequest - 10, // 16: user.v1.UserService.DeletePrompt:input_type -> user.v1.DeletePromptRequest - 13, // 17: user.v1.UserService.GetSettings:input_type -> user.v1.GetSettingsRequest - 15, // 18: user.v1.UserService.UpdateSettings:input_type -> user.v1.UpdateSettingsRequest - 17, // 19: user.v1.UserService.ResetSettings:input_type -> user.v1.ResetSettingsRequest - 2, // 20: user.v1.UserService.GetUser:output_type -> user.v1.GetUserResponse - 5, // 21: user.v1.UserService.ListPrompts:output_type -> user.v1.ListPromptsResponse - 7, // 22: user.v1.UserService.CreatePrompt:output_type -> user.v1.CreatePromptResponse - 9, // 23: user.v1.UserService.UpdatePrompt:output_type -> user.v1.UpdatePromptResponse - 20, // 24: user.v1.UserService.GetUserInstructions:output_type -> user.v1.GetUserInstructionsResponse - 22, // 25: user.v1.UserService.UpsertUserInstructions:output_type -> user.v1.UpsertUserInstructionsResponse - 11, // 26: user.v1.UserService.DeletePrompt:output_type -> user.v1.DeletePromptResponse - 14, // 27: user.v1.UserService.GetSettings:output_type -> user.v1.GetSettingsResponse - 16, // 28: user.v1.UserService.UpdateSettings:output_type -> user.v1.UpdateSettingsResponse - 18, // 29: user.v1.UserService.ResetSettings:output_type -> user.v1.ResetSettingsResponse - 20, // [20:30] is the sub-list for method output_type - 10, // [10:20] is the sub-list for method input_type - 10, // [10:10] is the sub-list for extension type_name - 10, // [10:10] is the sub-list for extension extendee - 0, // [0:10] is the sub-list for field type_name + 12, // 6: user.v1.Settings.custom_models:type_name -> user.v1.CustomModel + 13, // 7: user.v1.GetSettingsResponse.settings:type_name -> user.v1.Settings + 13, // 8: user.v1.UpdateSettingsRequest.settings:type_name -> user.v1.Settings + 13, // 9: user.v1.UpdateSettingsResponse.settings:type_name -> user.v1.Settings + 13, // 10: user.v1.ResetSettingsResponse.settings:type_name -> user.v1.Settings + 1, // 11: user.v1.UserService.GetUser:input_type -> user.v1.GetUserRequest + 4, // 12: user.v1.UserService.ListPrompts:input_type -> user.v1.ListPromptsRequest + 6, // 13: user.v1.UserService.CreatePrompt:input_type -> user.v1.CreatePromptRequest + 8, // 14: user.v1.UserService.UpdatePrompt:input_type -> user.v1.UpdatePromptRequest + 20, // 15: user.v1.UserService.GetUserInstructions:input_type -> user.v1.GetUserInstructionsRequest + 22, // 16: user.v1.UserService.UpsertUserInstructions:input_type -> user.v1.UpsertUserInstructionsRequest + 10, // 17: user.v1.UserService.DeletePrompt:input_type -> user.v1.DeletePromptRequest + 14, // 18: user.v1.UserService.GetSettings:input_type -> user.v1.GetSettingsRequest + 16, // 19: user.v1.UserService.UpdateSettings:input_type -> user.v1.UpdateSettingsRequest + 18, // 20: user.v1.UserService.ResetSettings:input_type -> user.v1.ResetSettingsRequest + 2, // 21: user.v1.UserService.GetUser:output_type -> user.v1.GetUserResponse + 5, // 22: user.v1.UserService.ListPrompts:output_type -> user.v1.ListPromptsResponse + 7, // 23: user.v1.UserService.CreatePrompt:output_type -> user.v1.CreatePromptResponse + 9, // 24: user.v1.UserService.UpdatePrompt:output_type -> user.v1.UpdatePromptResponse + 21, // 25: user.v1.UserService.GetUserInstructions:output_type -> user.v1.GetUserInstructionsResponse + 23, // 26: user.v1.UserService.UpsertUserInstructions:output_type -> user.v1.UpsertUserInstructionsResponse + 11, // 27: user.v1.UserService.DeletePrompt:output_type -> user.v1.DeletePromptResponse + 15, // 28: user.v1.UserService.GetSettings:output_type -> user.v1.GetSettingsResponse + 17, // 29: user.v1.UserService.UpdateSettings:output_type -> user.v1.UpdateSettingsResponse + 19, // 30: user.v1.UserService.ResetSettings:output_type -> user.v1.ResetSettingsResponse + 21, // [21:31] is the sub-list for method output_type + 11, // [11:21] is the sub-list for method input_type + 11, // [11:11] is the sub-list for extension type_name + 11, // [11:11] is the sub-list for extension extendee + 0, // [0:11] is the sub-list for field type_name } func init() { file_user_v1_user_proto_init() } @@ -1280,7 +1439,7 @@ func file_user_v1_user_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_user_v1_user_proto_rawDesc), len(file_user_v1_user_proto_rawDesc)), NumEnums: 0, - NumMessages: 23, + NumMessages: 24, NumExtensions: 0, NumServices: 1, }, diff --git a/proto/chat/v2/chat.proto b/proto/chat/v2/chat.proto index 779fe913..a9754a1e 100644 --- a/proto/chat/v2/chat.proto +++ b/proto/chat/v2/chat.proto @@ -136,6 +136,8 @@ message SupportedModel { int64 output_price = 6; // in cents per 1M tokens bool disabled = 7; // If true, the model is disabled and cannot be used optional string disabled_reason = 8; // The reason why the model is disabled + bool is_custom = 9; + optional string id = 10; // Custom model unique ID (empty for built-in models) } message ListSupportedModelsRequest { @@ -222,6 +224,7 @@ message CreateConversationMessageStreamRequest { optional string user_selected_text = 5; optional ConversationType conversation_type = 6; optional string surrounding = 8; + optional string custom_model_id = 9; // Selected custom model ID } // Response for streaming a message within an existing conversation diff --git a/proto/user/v1/user.proto b/proto/user/v1/user.proto index fc7f02b5..fa5606fa 100644 --- a/proto/user/v1/user.proto +++ b/proto/user/v1/user.proto @@ -114,6 +114,21 @@ message DeletePromptRequest { message DeletePromptResponse {} +message CustomModel { + string id = 1; + string slug = 2; + string name = 3; + string base_url = 4; + string api_key = 5; + int32 context_window = 6; + int32 max_output = 7; + int32 input_price = 8; + int32 output_price = 9; + float temperature = 10; + bool parallel_tool_calls = 11; + bool store = 12; +} + message Settings { bool show_shortcuts_after_selection = 1; bool full_width_paper_debugger_button = 2; @@ -121,6 +136,7 @@ message Settings { bool full_document_rag = 4; bool showed_onboarding = 5; string openai_api_key = 6; + repeated CustomModel custom_models = 7; } message GetSettingsRequest {} diff --git a/webapp/_webapp/src/hooks/useLanguageModels.ts b/webapp/_webapp/src/hooks/useLanguageModels.ts index a45b3761..f836de17 100644 --- a/webapp/_webapp/src/hooks/useLanguageModels.ts +++ b/webapp/_webapp/src/hooks/useLanguageModels.ts @@ -5,6 +5,7 @@ import { useListSupportedModelsQuery } from "../query"; import { useConversationUiStore } from "../stores/conversation/conversation-ui-store"; export type Model = { + id?: string; name: string; slug: string; provider: string; @@ -14,6 +15,7 @@ export type Model = { outputPrice: number; disabled: boolean; disabledReason?: string; + isCustom: boolean; }; // Extract provider from model slug (e.g., "openai/gpt-4.1" -> "openai") @@ -33,10 +35,12 @@ const fallbackModels: Model[] = [ inputPrice: 200, outputPrice: 800, disabled: false, + isCustom: false, }, ]; const mapSupportedModelToModel = (supportedModel: SupportedModel): Model => ({ + id: supportedModel.id || undefined, name: supportedModel.name, slug: supportedModel.slug, provider: extractProvider(supportedModel.slug), @@ -46,34 +50,41 @@ const mapSupportedModelToModel = (supportedModel: SupportedModel): Model => ({ outputPrice: Number(supportedModel.outputPrice), disabled: supportedModel.disabled, disabledReason: supportedModel.disabledReason, + isCustom: supportedModel.isCustom, }); export const useLanguageModels = () => { const { currentConversation, setCurrentConversation } = useConversationStore(); - const { setLastUsedModelSlug } = useConversationUiStore(); + const { lastUsedCustomModelId, setLastUsedModelSlug, setLastUsedCustomModelId } = useConversationUiStore(); const { data: supportedModelsResponse } = useListSupportedModelsQuery(); const models: Model[] = useMemo(() => { if (supportedModelsResponse?.models && supportedModelsResponse.models.length > 0) { - return supportedModelsResponse.models.map(mapSupportedModelToModel); + return supportedModelsResponse.models.map(mapSupportedModelToModel).filter((m) => !m.disabled || m.isCustom); } return fallbackModels; }, [supportedModelsResponse]); const currentModel = useMemo(() => { - const model = models.find((m) => m.slug === currentConversation.modelSlug); + if (lastUsedCustomModelId) { + const customModel = models.find((m) => m.isCustom && m.id === lastUsedCustomModelId); + if (customModel) return customModel; + } + + const model = models.find((m) => !m.isCustom && m.slug === currentConversation.modelSlug); return model || models[0]; - }, [models, currentConversation.modelSlug]); + }, [models, currentConversation.modelSlug, lastUsedCustomModelId]); const setModel = useCallback( (model: Model) => { setLastUsedModelSlug(model.slug); + setLastUsedCustomModelId(model.isCustom ? (model.id ?? "") : ""); setCurrentConversation({ ...currentConversation, modelSlug: model.slug, }); }, - [setCurrentConversation, currentConversation, setLastUsedModelSlug], + [setCurrentConversation, currentConversation, setLastUsedModelSlug, setLastUsedCustomModelId], ); return { models, currentModel, setModel }; diff --git a/webapp/_webapp/src/hooks/useSendMessageStream.ts b/webapp/_webapp/src/hooks/useSendMessageStream.ts index 17aaa795..4d2056d3 100644 --- a/webapp/_webapp/src/hooks/useSendMessageStream.ts +++ b/webapp/_webapp/src/hooks/useSendMessageStream.ts @@ -41,6 +41,7 @@ import { useAuthStore } from "../stores/auth-store"; import { useDevtoolStore } from "../stores/devtool-store"; import { useSelectionStore } from "../stores/selection-store"; import { useSettingStore } from "../stores/setting-store"; +import { useConversationUiStore } from "../stores/conversation/conversation-ui-store"; import { useSync } from "./useSync"; import { useAdapter } from "../adapters"; import { getProjectId } from "../libs/helpers"; @@ -86,6 +87,7 @@ export function useSendMessageStream(): UseSendMessageStreamResult { const surroundingText = useSelectionStore((s) => s.surroundingText); const alwaysSyncProject = useDevtoolStore((s) => s.alwaysSyncProject); const conversationMode = useSettingStore((s) => s.conversationMode); + const lastUsedCustomModelId = useConversationUiStore((s) => s.lastUsedCustomModelId); /** * Add the user message to the streaming state. @@ -165,6 +167,7 @@ export function useSendMessageStream(): UseSendMessageStreamResult { projectId, conversationId: currentConversation.id, modelSlug: currentConversation.modelSlug, + customModelId: lastUsedCustomModelId || undefined, surroundingText: surroundingText ?? undefined, conversationMode: conversationMode === "debug" ? "debug" : "default", }; @@ -251,6 +254,7 @@ export function useSendMessageStream(): UseSendMessageStreamResult { alwaysSyncProject, conversationMode, surroundingText, + lastUsedCustomModelId, addUserMessageToStream, truncateConversationIfEditing, ], diff --git a/webapp/_webapp/src/pkg/gen/apiclient/chat/v2/chat_pb.ts b/webapp/_webapp/src/pkg/gen/apiclient/chat/v2/chat_pb.ts index 0cb75815..2a79b357 100644 --- a/webapp/_webapp/src/pkg/gen/apiclient/chat/v2/chat_pb.ts +++ b/webapp/_webapp/src/pkg/gen/apiclient/chat/v2/chat_pb.ts @@ -11,7 +11,7 @@ import type { Message as Message$1 } from "@bufbuild/protobuf"; * Describes the file chat/v2/chat.proto. */ export const file_chat_v2_chat: GenFile = /*@__PURE__*/ - fileDesc("ChJjaGF0L3YyL2NoYXQucHJvdG8SB2NoYXQudjIiUAoTTWVzc2FnZVR5cGVUb29sQ2FsbBIMCgRuYW1lGAEgASgJEgwKBGFyZ3MYAiABKAkSDgoGcmVzdWx0GAMgASgJEg0KBWVycm9yGAQgASgJIkEKI01lc3NhZ2VUeXBlVG9vbENhbGxQcmVwYXJlQXJndW1lbnRzEgwKBG5hbWUYASABKAkSDAoEYXJncxgCIAEoCSIkChFNZXNzYWdlVHlwZVN5c3RlbRIPCgdjb250ZW50GAEgASgJImEKFE1lc3NhZ2VUeXBlQXNzaXN0YW50Eg8KB2NvbnRlbnQYASABKAkSEgoKbW9kZWxfc2x1ZxgCIAEoCRIWCglyZWFzb25pbmcYAyABKAlIAIgBAUIMCgpfcmVhc29uaW5nInoKD01lc3NhZ2VUeXBlVXNlchIPCgdjb250ZW50GAEgASgJEhoKDXNlbGVjdGVkX3RleHQYAiABKAlIAIgBARIYCgtzdXJyb3VuZGluZxgHIAEoCUgBiAEBQhAKDl9zZWxlY3RlZF90ZXh0Qg4KDF9zdXJyb3VuZGluZyIpChJNZXNzYWdlVHlwZVVua25vd24SEwoLZGVzY3JpcHRpb24YASABKAki5AIKDk1lc3NhZ2VQYXlsb2FkEiwKBnN5c3RlbRgBIAEoCzIaLmNoYXQudjIuTWVzc2FnZVR5cGVTeXN0ZW1IABIoCgR1c2VyGAIgASgLMhguY2hhdC52Mi5NZXNzYWdlVHlwZVVzZXJIABIyCglhc3Npc3RhbnQYAyABKAsyHS5jaGF0LnYyLk1lc3NhZ2VUeXBlQXNzaXN0YW50SAASUwobdG9vbF9jYWxsX3ByZXBhcmVfYXJndW1lbnRzGAQgASgLMiwuY2hhdC52Mi5NZXNzYWdlVHlwZVRvb2xDYWxsUHJlcGFyZUFyZ3VtZW50c0gAEjEKCXRvb2xfY2FsbBgFIAEoCzIcLmNoYXQudjIuTWVzc2FnZVR5cGVUb29sQ2FsbEgAEi4KB3Vua25vd24YBiABKAsyGy5jaGF0LnYyLk1lc3NhZ2VUeXBlVW5rbm93bkgAQg4KDG1lc3NhZ2VfdHlwZSJaCgdNZXNzYWdlEhIKCm1lc3NhZ2VfaWQYASABKAkSKAoHcGF5bG9hZBgCIAEoCzIXLmNoYXQudjIuTWVzc2FnZVBheWxvYWQSEQoJdGltZXN0YW1wGAMgASgDImEKDENvbnZlcnNhdGlvbhIKCgJpZBgBIAEoCRINCgV0aXRsZRgCIAEoCRISCgptb2RlbF9zbHVnGAMgASgJEiIKCG1lc3NhZ2VzGAQgAygLMhAuY2hhdC52Mi5NZXNzYWdlIkIKGExpc3RDb252ZXJzYXRpb25zUmVxdWVzdBIXCgpwcm9qZWN0X2lkGAEgASgJSACIAQFCDQoLX3Byb2plY3RfaWQiSQoZTGlzdENvbnZlcnNhdGlvbnNSZXNwb25zZRIsCg1jb252ZXJzYXRpb25zGAEgAygLMhUuY2hhdC52Mi5Db252ZXJzYXRpb24iMQoWR2V0Q29udmVyc2F0aW9uUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkiRgoXR2V0Q29udmVyc2F0aW9uUmVzcG9uc2USKwoMY29udmVyc2F0aW9uGAEgASgLMhUuY2hhdC52Mi5Db252ZXJzYXRpb24iQwoZVXBkYXRlQ29udmVyc2F0aW9uUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDQoFdGl0bGUYAiABKAkiSQoaVXBkYXRlQ29udmVyc2F0aW9uUmVzcG9uc2USKwoMY29udmVyc2F0aW9uGAEgASgLMhUuY2hhdC52Mi5Db252ZXJzYXRpb24iNAoZRGVsZXRlQ29udmVyc2F0aW9uUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkiHAoaRGVsZXRlQ29udmVyc2F0aW9uUmVzcG9uc2UixgEKDlN1cHBvcnRlZE1vZGVsEgwKBG5hbWUYASABKAkSDAoEc2x1ZxgCIAEoCRIVCg10b3RhbF9jb250ZXh0GAMgASgDEhIKCm1heF9vdXRwdXQYBCABKAMSEwoLaW5wdXRfcHJpY2UYBSABKAMSFAoMb3V0cHV0X3ByaWNlGAYgASgDEhAKCGRpc2FibGVkGAcgASgIEhwKD2Rpc2FibGVkX3JlYXNvbhgIIAEoCUgAiAEBQhIKEF9kaXNhYmxlZF9yZWFzb24iHAoaTGlzdFN1cHBvcnRlZE1vZGVsc1JlcXVlc3QiRgobTGlzdFN1cHBvcnRlZE1vZGVsc1Jlc3BvbnNlEicKBm1vZGVscxgBIAMoCzIXLmNoYXQudjIuU3VwcG9ydGVkTW9kZWwiQwoUU3RyZWFtSW5pdGlhbGl6YXRpb24SFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEhIKCm1vZGVsX3NsdWcYAiABKAkiTwoPU3RyZWFtUGFydEJlZ2luEhIKCm1lc3NhZ2VfaWQYASABKAkSKAoHcGF5bG9hZBgDIAEoCzIXLmNoYXQudjIuTWVzc2FnZVBheWxvYWQiMQoMTWVzc2FnZUNodW5rEhIKCm1lc3NhZ2VfaWQYASABKAkSDQoFZGVsdGEYAiABKAkiMwoOUmVhc29uaW5nQ2h1bmsSEgoKbWVzc2FnZV9pZBgBIAEoCRINCgVkZWx0YRgCIAEoCSI6ChNJbmNvbXBsZXRlSW5kaWNhdG9yEg4KBnJlYXNvbhgBIAEoCRITCgtyZXNwb25zZV9pZBgCIAEoCSJNCg1TdHJlYW1QYXJ0RW5kEhIKCm1lc3NhZ2VfaWQYASABKAkSKAoHcGF5bG9hZBgDIAEoCzIXLmNoYXQudjIuTWVzc2FnZVBheWxvYWQiLQoSU3RyZWFtRmluYWxpemF0aW9uEhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCSIkCgtTdHJlYW1FcnJvchIVCg1lcnJvcl9tZXNzYWdlGAEgASgJIssCCiZDcmVhdGVDb252ZXJzYXRpb25NZXNzYWdlU3RyZWFtUmVxdWVzdBISCgpwcm9qZWN0X2lkGAEgASgJEhwKD2NvbnZlcnNhdGlvbl9pZBgCIAEoCUgAiAEBEhIKCm1vZGVsX3NsdWcYAyABKAkSFAoMdXNlcl9tZXNzYWdlGAQgASgJEh8KEnVzZXJfc2VsZWN0ZWRfdGV4dBgFIAEoCUgBiAEBEjkKEWNvbnZlcnNhdGlvbl90eXBlGAYgASgOMhkuY2hhdC52Mi5Db252ZXJzYXRpb25UeXBlSAKIAQESGAoLc3Vycm91bmRpbmcYCCABKAlIA4gBAUISChBfY29udmVyc2F0aW9uX2lkQhUKE191c2VyX3NlbGVjdGVkX3RleHRCFAoSX2NvbnZlcnNhdGlvbl90eXBlQg4KDF9zdXJyb3VuZGluZyLzAwonQ3JlYXRlQ29udmVyc2F0aW9uTWVzc2FnZVN0cmVhbVJlc3BvbnNlEj4KFXN0cmVhbV9pbml0aWFsaXphdGlvbhgBIAEoCzIdLmNoYXQudjIuU3RyZWFtSW5pdGlhbGl6YXRpb25IABI1ChFzdHJlYW1fcGFydF9iZWdpbhgCIAEoCzIYLmNoYXQudjIuU3RyZWFtUGFydEJlZ2luSAASLgoNbWVzc2FnZV9jaHVuaxgDIAEoCzIVLmNoYXQudjIuTWVzc2FnZUNodW5rSAASPAoUaW5jb21wbGV0ZV9pbmRpY2F0b3IYBCABKAsyHC5jaGF0LnYyLkluY29tcGxldGVJbmRpY2F0b3JIABIxCg9zdHJlYW1fcGFydF9lbmQYBSABKAsyFi5jaGF0LnYyLlN0cmVhbVBhcnRFbmRIABI6ChNzdHJlYW1fZmluYWxpemF0aW9uGAYgASgLMhsuY2hhdC52Mi5TdHJlYW1GaW5hbGl6YXRpb25IABIsCgxzdHJlYW1fZXJyb3IYByABKAsyFC5jaGF0LnYyLlN0cmVhbUVycm9ySAASMgoPcmVhc29uaW5nX2NodW5rGAggASgLMhcuY2hhdC52Mi5SZWFzb25pbmdDaHVua0gAQhIKEHJlc3BvbnNlX3BheWxvYWQiPgoWR2V0Q2l0YXRpb25LZXlzUmVxdWVzdBIQCghzZW50ZW5jZRgBIAEoCRISCgpwcm9qZWN0X2lkGAIgASgJIjAKF0dldENpdGF0aW9uS2V5c1Jlc3BvbnNlEhUKDWNpdGF0aW9uX2tleXMYASADKAkqUgoQQ29udmVyc2F0aW9uVHlwZRIhCh1DT05WRVJTQVRJT05fVFlQRV9VTlNQRUNJRklFRBAAEhsKF0NPTlZFUlNBVElPTl9UWVBFX0RFQlVHEAEypwgKC0NoYXRTZXJ2aWNlEoMBChFMaXN0Q29udmVyc2F0aW9ucxIhLmNoYXQudjIuTGlzdENvbnZlcnNhdGlvbnNSZXF1ZXN0GiIuY2hhdC52Mi5MaXN0Q29udmVyc2F0aW9uc1Jlc3BvbnNlIieC0+STAiESHy9fcGQvYXBpL3YyL2NoYXRzL2NvbnZlcnNhdGlvbnMSjwEKD0dldENvbnZlcnNhdGlvbhIfLmNoYXQudjIuR2V0Q29udmVyc2F0aW9uUmVxdWVzdBogLmNoYXQudjIuR2V0Q29udmVyc2F0aW9uUmVzcG9uc2UiOYLT5JMCMxIxL19wZC9hcGkvdjIvY2hhdHMvY29udmVyc2F0aW9ucy97Y29udmVyc2F0aW9uX2lkfRLCAQofQ3JlYXRlQ29udmVyc2F0aW9uTWVzc2FnZVN0cmVhbRIvLmNoYXQudjIuQ3JlYXRlQ29udmVyc2F0aW9uTWVzc2FnZVN0cmVhbVJlcXVlc3QaMC5jaGF0LnYyLkNyZWF0ZUNvbnZlcnNhdGlvbk1lc3NhZ2VTdHJlYW1SZXNwb25zZSI6gtPkkwI0OgEqIi8vX3BkL2FwaS92Mi9jaGF0cy9jb252ZXJzYXRpb25zL21lc3NhZ2VzL3N0cmVhbTABEpsBChJVcGRhdGVDb252ZXJzYXRpb24SIi5jaGF0LnYyLlVwZGF0ZUNvbnZlcnNhdGlvblJlcXVlc3QaIy5jaGF0LnYyLlVwZGF0ZUNvbnZlcnNhdGlvblJlc3BvbnNlIjyC0+STAjY6ASoyMS9fcGQvYXBpL3YyL2NoYXRzL2NvbnZlcnNhdGlvbnMve2NvbnZlcnNhdGlvbl9pZH0SmAEKEkRlbGV0ZUNvbnZlcnNhdGlvbhIiLmNoYXQudjIuRGVsZXRlQ29udmVyc2F0aW9uUmVxdWVzdBojLmNoYXQudjIuRGVsZXRlQ29udmVyc2F0aW9uUmVzcG9uc2UiOYLT5JMCMyoxL19wZC9hcGkvdjIvY2hhdHMvY29udmVyc2F0aW9ucy97Y29udmVyc2F0aW9uX2lkfRKCAQoTTGlzdFN1cHBvcnRlZE1vZGVscxIjLmNoYXQudjIuTGlzdFN1cHBvcnRlZE1vZGVsc1JlcXVlc3QaJC5jaGF0LnYyLkxpc3RTdXBwb3J0ZWRNb2RlbHNSZXNwb25zZSIggtPkkwIaEhgvX3BkL2FwaS92Mi9jaGF0cy9tb2RlbHMSfQoPR2V0Q2l0YXRpb25LZXlzEh8uY2hhdC52Mi5HZXRDaXRhdGlvbktleXNSZXF1ZXN0GiAuY2hhdC52Mi5HZXRDaXRhdGlvbktleXNSZXNwb25zZSIngtPkkwIhEh8vX3BkL2FwaS92Mi9jaGF0cy9jaXRhdGlvbi1rZXlzQn8KC2NvbS5jaGF0LnYyQglDaGF0UHJvdG9QAVoocGFwZXJkZWJ1Z2dlci9wa2cvZ2VuL2FwaS9jaGF0L3YyO2NoYXR2MqICA0NYWKoCB0NoYXQuVjLKAgdDaGF0XFYy4gITQ2hhdFxWMlxHUEJNZXRhZGF0YeoCCENoYXQ6OlYyYgZwcm90bzM", [file_google_api_annotations]); + fileDesc("ChJjaGF0L3YyL2NoYXQucHJvdG8SB2NoYXQudjIiUAoTTWVzc2FnZVR5cGVUb29sQ2FsbBIMCgRuYW1lGAEgASgJEgwKBGFyZ3MYAiABKAkSDgoGcmVzdWx0GAMgASgJEg0KBWVycm9yGAQgASgJIkEKI01lc3NhZ2VUeXBlVG9vbENhbGxQcmVwYXJlQXJndW1lbnRzEgwKBG5hbWUYASABKAkSDAoEYXJncxgCIAEoCSIkChFNZXNzYWdlVHlwZVN5c3RlbRIPCgdjb250ZW50GAEgASgJImEKFE1lc3NhZ2VUeXBlQXNzaXN0YW50Eg8KB2NvbnRlbnQYASABKAkSEgoKbW9kZWxfc2x1ZxgCIAEoCRIWCglyZWFzb25pbmcYAyABKAlIAIgBAUIMCgpfcmVhc29uaW5nInoKD01lc3NhZ2VUeXBlVXNlchIPCgdjb250ZW50GAEgASgJEhoKDXNlbGVjdGVkX3RleHQYAiABKAlIAIgBARIYCgtzdXJyb3VuZGluZxgHIAEoCUgBiAEBQhAKDl9zZWxlY3RlZF90ZXh0Qg4KDF9zdXJyb3VuZGluZyIpChJNZXNzYWdlVHlwZVVua25vd24SEwoLZGVzY3JpcHRpb24YASABKAki5AIKDk1lc3NhZ2VQYXlsb2FkEiwKBnN5c3RlbRgBIAEoCzIaLmNoYXQudjIuTWVzc2FnZVR5cGVTeXN0ZW1IABIoCgR1c2VyGAIgASgLMhguY2hhdC52Mi5NZXNzYWdlVHlwZVVzZXJIABIyCglhc3Npc3RhbnQYAyABKAsyHS5jaGF0LnYyLk1lc3NhZ2VUeXBlQXNzaXN0YW50SAASUwobdG9vbF9jYWxsX3ByZXBhcmVfYXJndW1lbnRzGAQgASgLMiwuY2hhdC52Mi5NZXNzYWdlVHlwZVRvb2xDYWxsUHJlcGFyZUFyZ3VtZW50c0gAEjEKCXRvb2xfY2FsbBgFIAEoCzIcLmNoYXQudjIuTWVzc2FnZVR5cGVUb29sQ2FsbEgAEi4KB3Vua25vd24YBiABKAsyGy5jaGF0LnYyLk1lc3NhZ2VUeXBlVW5rbm93bkgAQg4KDG1lc3NhZ2VfdHlwZSJaCgdNZXNzYWdlEhIKCm1lc3NhZ2VfaWQYASABKAkSKAoHcGF5bG9hZBgCIAEoCzIXLmNoYXQudjIuTWVzc2FnZVBheWxvYWQSEQoJdGltZXN0YW1wGAMgASgDImEKDENvbnZlcnNhdGlvbhIKCgJpZBgBIAEoCRINCgV0aXRsZRgCIAEoCRISCgptb2RlbF9zbHVnGAMgASgJEiIKCG1lc3NhZ2VzGAQgAygLMhAuY2hhdC52Mi5NZXNzYWdlIkIKGExpc3RDb252ZXJzYXRpb25zUmVxdWVzdBIXCgpwcm9qZWN0X2lkGAEgASgJSACIAQFCDQoLX3Byb2plY3RfaWQiSQoZTGlzdENvbnZlcnNhdGlvbnNSZXNwb25zZRIsCg1jb252ZXJzYXRpb25zGAEgAygLMhUuY2hhdC52Mi5Db252ZXJzYXRpb24iMQoWR2V0Q29udmVyc2F0aW9uUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkiRgoXR2V0Q29udmVyc2F0aW9uUmVzcG9uc2USKwoMY29udmVyc2F0aW9uGAEgASgLMhUuY2hhdC52Mi5Db252ZXJzYXRpb24iQwoZVXBkYXRlQ29udmVyc2F0aW9uUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDQoFdGl0bGUYAiABKAkiSQoaVXBkYXRlQ29udmVyc2F0aW9uUmVzcG9uc2USKwoMY29udmVyc2F0aW9uGAEgASgLMhUuY2hhdC52Mi5Db252ZXJzYXRpb24iNAoZRGVsZXRlQ29udmVyc2F0aW9uUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkiHAoaRGVsZXRlQ29udmVyc2F0aW9uUmVzcG9uc2Ui8QEKDlN1cHBvcnRlZE1vZGVsEgwKBG5hbWUYASABKAkSDAoEc2x1ZxgCIAEoCRIVCg10b3RhbF9jb250ZXh0GAMgASgDEhIKCm1heF9vdXRwdXQYBCABKAMSEwoLaW5wdXRfcHJpY2UYBSABKAMSFAoMb3V0cHV0X3ByaWNlGAYgASgDEhAKCGRpc2FibGVkGAcgASgIEhwKD2Rpc2FibGVkX3JlYXNvbhgIIAEoCUgAiAEBEhEKCWlzX2N1c3RvbRgJIAEoCBIPCgJpZBgKIAEoCUgBiAEBQhIKEF9kaXNhYmxlZF9yZWFzb25CBQoDX2lkIhwKGkxpc3RTdXBwb3J0ZWRNb2RlbHNSZXF1ZXN0IkYKG0xpc3RTdXBwb3J0ZWRNb2RlbHNSZXNwb25zZRInCgZtb2RlbHMYASADKAsyFy5jaGF0LnYyLlN1cHBvcnRlZE1vZGVsIkMKFFN0cmVhbUluaXRpYWxpemF0aW9uEhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRISCgptb2RlbF9zbHVnGAIgASgJIk8KD1N0cmVhbVBhcnRCZWdpbhISCgptZXNzYWdlX2lkGAEgASgJEigKB3BheWxvYWQYAyABKAsyFy5jaGF0LnYyLk1lc3NhZ2VQYXlsb2FkIjEKDE1lc3NhZ2VDaHVuaxISCgptZXNzYWdlX2lkGAEgASgJEg0KBWRlbHRhGAIgASgJIjMKDlJlYXNvbmluZ0NodW5rEhIKCm1lc3NhZ2VfaWQYASABKAkSDQoFZGVsdGEYAiABKAkiOgoTSW5jb21wbGV0ZUluZGljYXRvchIOCgZyZWFzb24YASABKAkSEwoLcmVzcG9uc2VfaWQYAiABKAkiTQoNU3RyZWFtUGFydEVuZBISCgptZXNzYWdlX2lkGAEgASgJEigKB3BheWxvYWQYAyABKAsyFy5jaGF0LnYyLk1lc3NhZ2VQYXlsb2FkIi0KElN0cmVhbUZpbmFsaXphdGlvbhIXCg9jb252ZXJzYXRpb25faWQYASABKAkiJAoLU3RyZWFtRXJyb3ISFQoNZXJyb3JfbWVzc2FnZRgBIAEoCSL9AgomQ3JlYXRlQ29udmVyc2F0aW9uTWVzc2FnZVN0cmVhbVJlcXVlc3QSEgoKcHJvamVjdF9pZBgBIAEoCRIcCg9jb252ZXJzYXRpb25faWQYAiABKAlIAIgBARISCgptb2RlbF9zbHVnGAMgASgJEhQKDHVzZXJfbWVzc2FnZRgEIAEoCRIfChJ1c2VyX3NlbGVjdGVkX3RleHQYBSABKAlIAYgBARI5ChFjb252ZXJzYXRpb25fdHlwZRgGIAEoDjIZLmNoYXQudjIuQ29udmVyc2F0aW9uVHlwZUgCiAEBEhgKC3N1cnJvdW5kaW5nGAggASgJSAOIAQESHAoPY3VzdG9tX21vZGVsX2lkGAkgASgJSASIAQFCEgoQX2NvbnZlcnNhdGlvbl9pZEIVChNfdXNlcl9zZWxlY3RlZF90ZXh0QhQKEl9jb252ZXJzYXRpb25fdHlwZUIOCgxfc3Vycm91bmRpbmdCEgoQX2N1c3RvbV9tb2RlbF9pZCLzAwonQ3JlYXRlQ29udmVyc2F0aW9uTWVzc2FnZVN0cmVhbVJlc3BvbnNlEj4KFXN0cmVhbV9pbml0aWFsaXphdGlvbhgBIAEoCzIdLmNoYXQudjIuU3RyZWFtSW5pdGlhbGl6YXRpb25IABI1ChFzdHJlYW1fcGFydF9iZWdpbhgCIAEoCzIYLmNoYXQudjIuU3RyZWFtUGFydEJlZ2luSAASLgoNbWVzc2FnZV9jaHVuaxgDIAEoCzIVLmNoYXQudjIuTWVzc2FnZUNodW5rSAASPAoUaW5jb21wbGV0ZV9pbmRpY2F0b3IYBCABKAsyHC5jaGF0LnYyLkluY29tcGxldGVJbmRpY2F0b3JIABIxCg9zdHJlYW1fcGFydF9lbmQYBSABKAsyFi5jaGF0LnYyLlN0cmVhbVBhcnRFbmRIABI6ChNzdHJlYW1fZmluYWxpemF0aW9uGAYgASgLMhsuY2hhdC52Mi5TdHJlYW1GaW5hbGl6YXRpb25IABIsCgxzdHJlYW1fZXJyb3IYByABKAsyFC5jaGF0LnYyLlN0cmVhbUVycm9ySAASMgoPcmVhc29uaW5nX2NodW5rGAggASgLMhcuY2hhdC52Mi5SZWFzb25pbmdDaHVua0gAQhIKEHJlc3BvbnNlX3BheWxvYWQiPgoWR2V0Q2l0YXRpb25LZXlzUmVxdWVzdBIQCghzZW50ZW5jZRgBIAEoCRISCgpwcm9qZWN0X2lkGAIgASgJIjAKF0dldENpdGF0aW9uS2V5c1Jlc3BvbnNlEhUKDWNpdGF0aW9uX2tleXMYASADKAkqUgoQQ29udmVyc2F0aW9uVHlwZRIhCh1DT05WRVJTQVRJT05fVFlQRV9VTlNQRUNJRklFRBAAEhsKF0NPTlZFUlNBVElPTl9UWVBFX0RFQlVHEAEypwgKC0NoYXRTZXJ2aWNlEoMBChFMaXN0Q29udmVyc2F0aW9ucxIhLmNoYXQudjIuTGlzdENvbnZlcnNhdGlvbnNSZXF1ZXN0GiIuY2hhdC52Mi5MaXN0Q29udmVyc2F0aW9uc1Jlc3BvbnNlIieC0+STAiESHy9fcGQvYXBpL3YyL2NoYXRzL2NvbnZlcnNhdGlvbnMSjwEKD0dldENvbnZlcnNhdGlvbhIfLmNoYXQudjIuR2V0Q29udmVyc2F0aW9uUmVxdWVzdBogLmNoYXQudjIuR2V0Q29udmVyc2F0aW9uUmVzcG9uc2UiOYLT5JMCMxIxL19wZC9hcGkvdjIvY2hhdHMvY29udmVyc2F0aW9ucy97Y29udmVyc2F0aW9uX2lkfRLCAQofQ3JlYXRlQ29udmVyc2F0aW9uTWVzc2FnZVN0cmVhbRIvLmNoYXQudjIuQ3JlYXRlQ29udmVyc2F0aW9uTWVzc2FnZVN0cmVhbVJlcXVlc3QaMC5jaGF0LnYyLkNyZWF0ZUNvbnZlcnNhdGlvbk1lc3NhZ2VTdHJlYW1SZXNwb25zZSI6gtPkkwI0OgEqIi8vX3BkL2FwaS92Mi9jaGF0cy9jb252ZXJzYXRpb25zL21lc3NhZ2VzL3N0cmVhbTABEpsBChJVcGRhdGVDb252ZXJzYXRpb24SIi5jaGF0LnYyLlVwZGF0ZUNvbnZlcnNhdGlvblJlcXVlc3QaIy5jaGF0LnYyLlVwZGF0ZUNvbnZlcnNhdGlvblJlc3BvbnNlIjyC0+STAjY6ASoyMS9fcGQvYXBpL3YyL2NoYXRzL2NvbnZlcnNhdGlvbnMve2NvbnZlcnNhdGlvbl9pZH0SmAEKEkRlbGV0ZUNvbnZlcnNhdGlvbhIiLmNoYXQudjIuRGVsZXRlQ29udmVyc2F0aW9uUmVxdWVzdBojLmNoYXQudjIuRGVsZXRlQ29udmVyc2F0aW9uUmVzcG9uc2UiOYLT5JMCMyoxL19wZC9hcGkvdjIvY2hhdHMvY29udmVyc2F0aW9ucy97Y29udmVyc2F0aW9uX2lkfRKCAQoTTGlzdFN1cHBvcnRlZE1vZGVscxIjLmNoYXQudjIuTGlzdFN1cHBvcnRlZE1vZGVsc1JlcXVlc3QaJC5jaGF0LnYyLkxpc3RTdXBwb3J0ZWRNb2RlbHNSZXNwb25zZSIggtPkkwIaEhgvX3BkL2FwaS92Mi9jaGF0cy9tb2RlbHMSfQoPR2V0Q2l0YXRpb25LZXlzEh8uY2hhdC52Mi5HZXRDaXRhdGlvbktleXNSZXF1ZXN0GiAuY2hhdC52Mi5HZXRDaXRhdGlvbktleXNSZXNwb25zZSIngtPkkwIhEh8vX3BkL2FwaS92Mi9jaGF0cy9jaXRhdGlvbi1rZXlzQn8KC2NvbS5jaGF0LnYyQglDaGF0UHJvdG9QAVoocGFwZXJkZWJ1Z2dlci9wa2cvZ2VuL2FwaS9jaGF0L3YyO2NoYXR2MqICA0NYWKoCB0NoYXQuVjLKAgdDaGF0XFYy4gITQ2hhdFxWMlxHUEJNZXRhZGF0YeoCCENoYXQ6OlYyYgZwcm90bzM", [file_google_api_annotations]); /** * @generated from message chat.v2.MessageTypeToolCall @@ -469,6 +469,18 @@ export type SupportedModel = Message$1<"chat.v2.SupportedModel"> & { * @generated from field: optional string disabled_reason = 8; */ disabledReason?: string; + + /** + * @generated from field: bool is_custom = 9; + */ + isCustom: boolean; + + /** + * Custom model unique ID (empty for built-in models) + * + * @generated from field: optional string id = 10; + */ + id?: string; }; /** @@ -744,6 +756,13 @@ export type CreateConversationMessageStreamRequest = Message$1<"chat.v2.CreateCo * @generated from field: optional string surrounding = 8; */ surrounding?: string; + + /** + * Selected custom model ID + * + * @generated from field: optional string custom_model_id = 9; + */ + customModelId?: string; }; /** diff --git a/webapp/_webapp/src/pkg/gen/apiclient/user/v1/user_pb.ts b/webapp/_webapp/src/pkg/gen/apiclient/user/v1/user_pb.ts index 38f267c6..38ffdd34 100644 --- a/webapp/_webapp/src/pkg/gen/apiclient/user/v1/user_pb.ts +++ b/webapp/_webapp/src/pkg/gen/apiclient/user/v1/user_pb.ts @@ -13,7 +13,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file user/v1/user.proto. */ export const file_user_v1_user: GenFile = /*@__PURE__*/ - fileDesc("ChJ1c2VyL3YxL3VzZXIucHJvdG8SB3VzZXIudjEiQAoEVXNlchIKCgJpZBgBIAEoCRINCgVlbWFpbBgCIAEoCRIMCgRuYW1lGAMgASgJEg8KB3BpY3R1cmUYBCABKAkiEAoOR2V0VXNlclJlcXVlc3QiLgoPR2V0VXNlclJlc3BvbnNlEhsKBHVzZXIYASABKAsyDS51c2VyLnYxLlVzZXIirAEKBlByb21wdBIKCgJpZBgBIAEoCRIuCgpjcmVhdGVkX2F0GAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIuCgp1cGRhdGVkX2F0GAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBINCgV0aXRsZRgEIAEoCRIPCgdjb250ZW50GAUgASgJEhYKDmlzX3VzZXJfcHJvbXB0GAYgASgIIhQKEkxpc3RQcm9tcHRzUmVxdWVzdCI3ChNMaXN0UHJvbXB0c1Jlc3BvbnNlEiAKB3Byb21wdHMYASADKAsyDy51c2VyLnYxLlByb21wdCI1ChNDcmVhdGVQcm9tcHRSZXF1ZXN0Eg0KBXRpdGxlGAEgASgJEg8KB2NvbnRlbnQYAiABKAkiNwoUQ3JlYXRlUHJvbXB0UmVzcG9uc2USHwoGcHJvbXB0GAEgASgLMg8udXNlci52MS5Qcm9tcHQiSAoTVXBkYXRlUHJvbXB0UmVxdWVzdBIRCglwcm9tcHRfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDwoHY29udGVudBgDIAEoCSI3ChRVcGRhdGVQcm9tcHRSZXNwb25zZRIfCgZwcm9tcHQYASABKAsyDy51c2VyLnYxLlByb21wdCIoChNEZWxldGVQcm9tcHRSZXF1ZXN0EhEKCXByb21wdF9pZBgBIAEoCSIWChREZWxldGVQcm9tcHRSZXNwb25zZSLOAQoIU2V0dGluZ3MSJgoec2hvd19zaG9ydGN1dHNfYWZ0ZXJfc2VsZWN0aW9uGAEgASgIEigKIGZ1bGxfd2lkdGhfcGFwZXJfZGVidWdnZXJfYnV0dG9uGAIgASgIEiIKGmVuYWJsZV9jaXRhdGlvbl9zdWdnZXN0aW9uGAMgASgIEhkKEWZ1bGxfZG9jdW1lbnRfcmFnGAQgASgIEhkKEXNob3dlZF9vbmJvYXJkaW5nGAUgASgIEhYKDm9wZW5haV9hcGlfa2V5GAYgASgJIhQKEkdldFNldHRpbmdzUmVxdWVzdCI6ChNHZXRTZXR0aW5nc1Jlc3BvbnNlEiMKCHNldHRpbmdzGAEgASgLMhEudXNlci52MS5TZXR0aW5ncyI8ChVVcGRhdGVTZXR0aW5nc1JlcXVlc3QSIwoIc2V0dGluZ3MYASABKAsyES51c2VyLnYxLlNldHRpbmdzIj0KFlVwZGF0ZVNldHRpbmdzUmVzcG9uc2USIwoIc2V0dGluZ3MYASABKAsyES51c2VyLnYxLlNldHRpbmdzIhYKFFJlc2V0U2V0dGluZ3NSZXF1ZXN0IjwKFVJlc2V0U2V0dGluZ3NSZXNwb25zZRIjCghzZXR0aW5ncxgBIAEoCzIRLnVzZXIudjEuU2V0dGluZ3MiHAoaR2V0VXNlckluc3RydWN0aW9uc1JlcXVlc3QiMwobR2V0VXNlckluc3RydWN0aW9uc1Jlc3BvbnNlEhQKDGluc3RydWN0aW9ucxgBIAEoCSI1Ch1VcHNlcnRVc2VySW5zdHJ1Y3Rpb25zUmVxdWVzdBIUCgxpbnN0cnVjdGlvbnMYASABKAkiNgoeVXBzZXJ0VXNlckluc3RydWN0aW9uc1Jlc3BvbnNlEhQKDGluc3RydWN0aW9ucxgBIAEoCTKDCgoLVXNlclNlcnZpY2USXQoHR2V0VXNlchIXLnVzZXIudjEuR2V0VXNlclJlcXVlc3QaGC51c2VyLnYxLkdldFVzZXJSZXNwb25zZSIfgtPkkwIZEhcvX3BkL2FwaS92MS91c2Vycy9Ac2VsZhJxCgtMaXN0UHJvbXB0cxIbLnVzZXIudjEuTGlzdFByb21wdHNSZXF1ZXN0GhwudXNlci52MS5MaXN0UHJvbXB0c1Jlc3BvbnNlIieC0+STAiESHy9fcGQvYXBpL3YxL3VzZXJzL0BzZWxmL3Byb21wdHMSdwoMQ3JlYXRlUHJvbXB0EhwudXNlci52MS5DcmVhdGVQcm9tcHRSZXF1ZXN0Gh0udXNlci52MS5DcmVhdGVQcm9tcHRSZXNwb25zZSIqgtPkkwIkOgEqIh8vX3BkL2FwaS92MS91c2Vycy9Ac2VsZi9wcm9tcHRzEoMBCgxVcGRhdGVQcm9tcHQSHC51c2VyLnYxLlVwZGF0ZVByb21wdFJlcXVlc3QaHS51c2VyLnYxLlVwZGF0ZVByb21wdFJlc3BvbnNlIjaC0+STAjA6ASoaKy9fcGQvYXBpL3YxL3VzZXJzL0BzZWxmL3Byb21wdHMve3Byb21wdF9pZH0SjgEKE0dldFVzZXJJbnN0cnVjdGlvbnMSIy51c2VyLnYxLkdldFVzZXJJbnN0cnVjdGlvbnNSZXF1ZXN0GiQudXNlci52MS5HZXRVc2VySW5zdHJ1Y3Rpb25zUmVzcG9uc2UiLILT5JMCJhIkL19wZC9hcGkvdjEvdXNlcnMvQHNlbGYvaW5zdHJ1Y3Rpb25zEpoBChZVcHNlcnRVc2VySW5zdHJ1Y3Rpb25zEiYudXNlci52MS5VcHNlcnRVc2VySW5zdHJ1Y3Rpb25zUmVxdWVzdBonLnVzZXIudjEuVXBzZXJ0VXNlckluc3RydWN0aW9uc1Jlc3BvbnNlIi+C0+STAik6ASoiJC9fcGQvYXBpL3YxL3VzZXJzL0BzZWxmL2luc3RydWN0aW9ucxKAAQoMRGVsZXRlUHJvbXB0EhwudXNlci52MS5EZWxldGVQcm9tcHRSZXF1ZXN0Gh0udXNlci52MS5EZWxldGVQcm9tcHRSZXNwb25zZSIzgtPkkwItKisvX3BkL2FwaS92MS91c2Vycy9Ac2VsZi9wcm9tcHRzL3twcm9tcHRfaWR9EnIKC0dldFNldHRpbmdzEhsudXNlci52MS5HZXRTZXR0aW5nc1JlcXVlc3QaHC51c2VyLnYxLkdldFNldHRpbmdzUmVzcG9uc2UiKILT5JMCIhIgL19wZC9hcGkvdjEvdXNlcnMvQHNlbGYvc2V0dGluZ3MSfgoOVXBkYXRlU2V0dGluZ3MSHi51c2VyLnYxLlVwZGF0ZVNldHRpbmdzUmVxdWVzdBofLnVzZXIudjEuVXBkYXRlU2V0dGluZ3NSZXNwb25zZSIrgtPkkwIlOgEqGiAvX3BkL2FwaS92MS91c2Vycy9Ac2VsZi9zZXR0aW5ncxJ+Cg1SZXNldFNldHRpbmdzEh0udXNlci52MS5SZXNldFNldHRpbmdzUmVxdWVzdBoeLnVzZXIudjEuUmVzZXRTZXR0aW5nc1Jlc3BvbnNlIi6C0+STAigiJi9fcGQvYXBpL3YxL3VzZXJzL0BzZWxmL3NldHRpbmdzL3Jlc2V0Qn8KC2NvbS51c2VyLnYxQglVc2VyUHJvdG9QAVoocGFwZXJkZWJ1Z2dlci9wa2cvZ2VuL2FwaS91c2VyL3YxO3VzZXJ2MaICA1VYWKoCB1VzZXIuVjHKAgdVc2VyXFYx4gITVXNlclxWMVxHUEJNZXRhZGF0YeoCCFVzZXI6OlYxYgZwcm90bzM", [file_google_api_annotations, file_google_protobuf_timestamp]); + fileDesc("ChJ1c2VyL3YxL3VzZXIucHJvdG8SB3VzZXIudjEiQAoEVXNlchIKCgJpZBgBIAEoCRINCgVlbWFpbBgCIAEoCRIMCgRuYW1lGAMgASgJEg8KB3BpY3R1cmUYBCABKAkiEAoOR2V0VXNlclJlcXVlc3QiLgoPR2V0VXNlclJlc3BvbnNlEhsKBHVzZXIYASABKAsyDS51c2VyLnYxLlVzZXIirAEKBlByb21wdBIKCgJpZBgBIAEoCRIuCgpjcmVhdGVkX2F0GAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIuCgp1cGRhdGVkX2F0GAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBINCgV0aXRsZRgEIAEoCRIPCgdjb250ZW50GAUgASgJEhYKDmlzX3VzZXJfcHJvbXB0GAYgASgIIhQKEkxpc3RQcm9tcHRzUmVxdWVzdCI3ChNMaXN0UHJvbXB0c1Jlc3BvbnNlEiAKB3Byb21wdHMYASADKAsyDy51c2VyLnYxLlByb21wdCI1ChNDcmVhdGVQcm9tcHRSZXF1ZXN0Eg0KBXRpdGxlGAEgASgJEg8KB2NvbnRlbnQYAiABKAkiNwoUQ3JlYXRlUHJvbXB0UmVzcG9uc2USHwoGcHJvbXB0GAEgASgLMg8udXNlci52MS5Qcm9tcHQiSAoTVXBkYXRlUHJvbXB0UmVxdWVzdBIRCglwcm9tcHRfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDwoHY29udGVudBgDIAEoCSI3ChRVcGRhdGVQcm9tcHRSZXNwb25zZRIfCgZwcm9tcHQYASABKAsyDy51c2VyLnYxLlByb21wdCIoChNEZWxldGVQcm9tcHRSZXF1ZXN0EhEKCXByb21wdF9pZBgBIAEoCSIWChREZWxldGVQcm9tcHRSZXNwb25zZSLwAQoLQ3VzdG9tTW9kZWwSCgoCaWQYASABKAkSDAoEc2x1ZxgCIAEoCRIMCgRuYW1lGAMgASgJEhAKCGJhc2VfdXJsGAQgASgJEg8KB2FwaV9rZXkYBSABKAkSFgoOY29udGV4dF93aW5kb3cYBiABKAUSEgoKbWF4X291dHB1dBgHIAEoBRITCgtpbnB1dF9wcmljZRgIIAEoBRIUCgxvdXRwdXRfcHJpY2UYCSABKAUSEwoLdGVtcGVyYXR1cmUYCiABKAISGwoTcGFyYWxsZWxfdG9vbF9jYWxscxgLIAEoCBINCgVzdG9yZRgMIAEoCCL7AQoIU2V0dGluZ3MSJgoec2hvd19zaG9ydGN1dHNfYWZ0ZXJfc2VsZWN0aW9uGAEgASgIEigKIGZ1bGxfd2lkdGhfcGFwZXJfZGVidWdnZXJfYnV0dG9uGAIgASgIEiIKGmVuYWJsZV9jaXRhdGlvbl9zdWdnZXN0aW9uGAMgASgIEhkKEWZ1bGxfZG9jdW1lbnRfcmFnGAQgASgIEhkKEXNob3dlZF9vbmJvYXJkaW5nGAUgASgIEhYKDm9wZW5haV9hcGlfa2V5GAYgASgJEisKDWN1c3RvbV9tb2RlbHMYByADKAsyFC51c2VyLnYxLkN1c3RvbU1vZGVsIhQKEkdldFNldHRpbmdzUmVxdWVzdCI6ChNHZXRTZXR0aW5nc1Jlc3BvbnNlEiMKCHNldHRpbmdzGAEgASgLMhEudXNlci52MS5TZXR0aW5ncyI8ChVVcGRhdGVTZXR0aW5nc1JlcXVlc3QSIwoIc2V0dGluZ3MYASABKAsyES51c2VyLnYxLlNldHRpbmdzIj0KFlVwZGF0ZVNldHRpbmdzUmVzcG9uc2USIwoIc2V0dGluZ3MYASABKAsyES51c2VyLnYxLlNldHRpbmdzIhYKFFJlc2V0U2V0dGluZ3NSZXF1ZXN0IjwKFVJlc2V0U2V0dGluZ3NSZXNwb25zZRIjCghzZXR0aW5ncxgBIAEoCzIRLnVzZXIudjEuU2V0dGluZ3MiHAoaR2V0VXNlckluc3RydWN0aW9uc1JlcXVlc3QiMwobR2V0VXNlckluc3RydWN0aW9uc1Jlc3BvbnNlEhQKDGluc3RydWN0aW9ucxgBIAEoCSI1Ch1VcHNlcnRVc2VySW5zdHJ1Y3Rpb25zUmVxdWVzdBIUCgxpbnN0cnVjdGlvbnMYASABKAkiNgoeVXBzZXJ0VXNlckluc3RydWN0aW9uc1Jlc3BvbnNlEhQKDGluc3RydWN0aW9ucxgBIAEoCTKDCgoLVXNlclNlcnZpY2USXQoHR2V0VXNlchIXLnVzZXIudjEuR2V0VXNlclJlcXVlc3QaGC51c2VyLnYxLkdldFVzZXJSZXNwb25zZSIfgtPkkwIZEhcvX3BkL2FwaS92MS91c2Vycy9Ac2VsZhJxCgtMaXN0UHJvbXB0cxIbLnVzZXIudjEuTGlzdFByb21wdHNSZXF1ZXN0GhwudXNlci52MS5MaXN0UHJvbXB0c1Jlc3BvbnNlIieC0+STAiESHy9fcGQvYXBpL3YxL3VzZXJzL0BzZWxmL3Byb21wdHMSdwoMQ3JlYXRlUHJvbXB0EhwudXNlci52MS5DcmVhdGVQcm9tcHRSZXF1ZXN0Gh0udXNlci52MS5DcmVhdGVQcm9tcHRSZXNwb25zZSIqgtPkkwIkOgEqIh8vX3BkL2FwaS92MS91c2Vycy9Ac2VsZi9wcm9tcHRzEoMBCgxVcGRhdGVQcm9tcHQSHC51c2VyLnYxLlVwZGF0ZVByb21wdFJlcXVlc3QaHS51c2VyLnYxLlVwZGF0ZVByb21wdFJlc3BvbnNlIjaC0+STAjA6ASoaKy9fcGQvYXBpL3YxL3VzZXJzL0BzZWxmL3Byb21wdHMve3Byb21wdF9pZH0SjgEKE0dldFVzZXJJbnN0cnVjdGlvbnMSIy51c2VyLnYxLkdldFVzZXJJbnN0cnVjdGlvbnNSZXF1ZXN0GiQudXNlci52MS5HZXRVc2VySW5zdHJ1Y3Rpb25zUmVzcG9uc2UiLILT5JMCJhIkL19wZC9hcGkvdjEvdXNlcnMvQHNlbGYvaW5zdHJ1Y3Rpb25zEpoBChZVcHNlcnRVc2VySW5zdHJ1Y3Rpb25zEiYudXNlci52MS5VcHNlcnRVc2VySW5zdHJ1Y3Rpb25zUmVxdWVzdBonLnVzZXIudjEuVXBzZXJ0VXNlckluc3RydWN0aW9uc1Jlc3BvbnNlIi+C0+STAik6ASoiJC9fcGQvYXBpL3YxL3VzZXJzL0BzZWxmL2luc3RydWN0aW9ucxKAAQoMRGVsZXRlUHJvbXB0EhwudXNlci52MS5EZWxldGVQcm9tcHRSZXF1ZXN0Gh0udXNlci52MS5EZWxldGVQcm9tcHRSZXNwb25zZSIzgtPkkwItKisvX3BkL2FwaS92MS91c2Vycy9Ac2VsZi9wcm9tcHRzL3twcm9tcHRfaWR9EnIKC0dldFNldHRpbmdzEhsudXNlci52MS5HZXRTZXR0aW5nc1JlcXVlc3QaHC51c2VyLnYxLkdldFNldHRpbmdzUmVzcG9uc2UiKILT5JMCIhIgL19wZC9hcGkvdjEvdXNlcnMvQHNlbGYvc2V0dGluZ3MSfgoOVXBkYXRlU2V0dGluZ3MSHi51c2VyLnYxLlVwZGF0ZVNldHRpbmdzUmVxdWVzdBofLnVzZXIudjEuVXBkYXRlU2V0dGluZ3NSZXNwb25zZSIrgtPkkwIlOgEqGiAvX3BkL2FwaS92MS91c2Vycy9Ac2VsZi9zZXR0aW5ncxJ+Cg1SZXNldFNldHRpbmdzEh0udXNlci52MS5SZXNldFNldHRpbmdzUmVxdWVzdBoeLnVzZXIudjEuUmVzZXRTZXR0aW5nc1Jlc3BvbnNlIi6C0+STAigiJi9fcGQvYXBpL3YxL3VzZXJzL0BzZWxmL3NldHRpbmdzL3Jlc2V0Qn8KC2NvbS51c2VyLnYxQglVc2VyUHJvdG9QAVoocGFwZXJkZWJ1Z2dlci9wa2cvZ2VuL2FwaS91c2VyL3YxO3VzZXJ2MaICA1VYWKoCB1VzZXIuVjHKAgdVc2VyXFYx4gITVXNlclxWMVxHUEJNZXRhZGF0YeoCCFVzZXI6OlYxYgZwcm90bzM", [file_google_api_annotations, file_google_protobuf_timestamp]); /** * @generated from message user.v1.User @@ -262,6 +262,78 @@ export type DeletePromptResponse = Message<"user.v1.DeletePromptResponse"> & { export const DeletePromptResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_user_v1_user, 11); +/** + * @generated from message user.v1.CustomModel + */ +export type CustomModel = Message<"user.v1.CustomModel"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * @generated from field: string slug = 2; + */ + slug: string; + + /** + * @generated from field: string name = 3; + */ + name: string; + + /** + * @generated from field: string base_url = 4; + */ + baseUrl: string; + + /** + * @generated from field: string api_key = 5; + */ + apiKey: string; + + /** + * @generated from field: int32 context_window = 6; + */ + contextWindow: number; + + /** + * @generated from field: int32 max_output = 7; + */ + maxOutput: number; + + /** + * @generated from field: int32 input_price = 8; + */ + inputPrice: number; + + /** + * @generated from field: int32 output_price = 9; + */ + outputPrice: number; + + /** + * @generated from field: float temperature = 10; + */ + temperature: number; + + /** + * @generated from field: bool parallel_tool_calls = 11; + */ + parallelToolCalls: boolean; + + /** + * @generated from field: bool store = 12; + */ + store: boolean; +}; + +/** + * Describes the message user.v1.CustomModel. + * Use `create(CustomModelSchema)` to create a new message. + */ +export const CustomModelSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_user_v1_user, 12); + /** * @generated from message user.v1.Settings */ @@ -295,6 +367,11 @@ export type Settings = Message<"user.v1.Settings"> & { * @generated from field: string openai_api_key = 6; */ openaiApiKey: string; + + /** + * @generated from field: repeated user.v1.CustomModel custom_models = 7; + */ + customModels: CustomModel[]; }; /** @@ -302,7 +379,7 @@ export type Settings = Message<"user.v1.Settings"> & { * Use `create(SettingsSchema)` to create a new message. */ export const SettingsSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_user_v1_user, 12); + messageDesc(file_user_v1_user, 13); /** * @generated from message user.v1.GetSettingsRequest @@ -315,7 +392,7 @@ export type GetSettingsRequest = Message<"user.v1.GetSettingsRequest"> & { * Use `create(GetSettingsRequestSchema)` to create a new message. */ export const GetSettingsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_user_v1_user, 13); + messageDesc(file_user_v1_user, 14); /** * @generated from message user.v1.GetSettingsResponse @@ -332,7 +409,7 @@ export type GetSettingsResponse = Message<"user.v1.GetSettingsResponse"> & { * Use `create(GetSettingsResponseSchema)` to create a new message. */ export const GetSettingsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_user_v1_user, 14); + messageDesc(file_user_v1_user, 15); /** * @generated from message user.v1.UpdateSettingsRequest @@ -349,7 +426,7 @@ export type UpdateSettingsRequest = Message<"user.v1.UpdateSettingsRequest"> & { * Use `create(UpdateSettingsRequestSchema)` to create a new message. */ export const UpdateSettingsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_user_v1_user, 15); + messageDesc(file_user_v1_user, 16); /** * @generated from message user.v1.UpdateSettingsResponse @@ -366,7 +443,7 @@ export type UpdateSettingsResponse = Message<"user.v1.UpdateSettingsResponse"> & * Use `create(UpdateSettingsResponseSchema)` to create a new message. */ export const UpdateSettingsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_user_v1_user, 16); + messageDesc(file_user_v1_user, 17); /** * @generated from message user.v1.ResetSettingsRequest @@ -379,7 +456,7 @@ export type ResetSettingsRequest = Message<"user.v1.ResetSettingsRequest"> & { * Use `create(ResetSettingsRequestSchema)` to create a new message. */ export const ResetSettingsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_user_v1_user, 17); + messageDesc(file_user_v1_user, 18); /** * @generated from message user.v1.ResetSettingsResponse @@ -396,7 +473,7 @@ export type ResetSettingsResponse = Message<"user.v1.ResetSettingsResponse"> & { * Use `create(ResetSettingsResponseSchema)` to create a new message. */ export const ResetSettingsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_user_v1_user, 18); + messageDesc(file_user_v1_user, 19); /** * @generated from message user.v1.GetUserInstructionsRequest @@ -409,7 +486,7 @@ export type GetUserInstructionsRequest = Message<"user.v1.GetUserInstructionsReq * Use `create(GetUserInstructionsRequestSchema)` to create a new message. */ export const GetUserInstructionsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_user_v1_user, 19); + messageDesc(file_user_v1_user, 20); /** * @generated from message user.v1.GetUserInstructionsResponse @@ -426,7 +503,7 @@ export type GetUserInstructionsResponse = Message<"user.v1.GetUserInstructionsRe * Use `create(GetUserInstructionsResponseSchema)` to create a new message. */ export const GetUserInstructionsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_user_v1_user, 20); + messageDesc(file_user_v1_user, 21); /** * @generated from message user.v1.UpsertUserInstructionsRequest @@ -443,7 +520,7 @@ export type UpsertUserInstructionsRequest = Message<"user.v1.UpsertUserInstructi * Use `create(UpsertUserInstructionsRequestSchema)` to create a new message. */ export const UpsertUserInstructionsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_user_v1_user, 21); + messageDesc(file_user_v1_user, 22); /** * @generated from message user.v1.UpsertUserInstructionsResponse @@ -460,7 +537,7 @@ export type UpsertUserInstructionsResponse = Message<"user.v1.UpsertUserInstruct * Use `create(UpsertUserInstructionsResponseSchema)` to create a new message. */ export const UpsertUserInstructionsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_user_v1_user, 22); + messageDesc(file_user_v1_user, 23); /** * @generated from service user.v1.UserService diff --git a/webapp/_webapp/src/stores/conversation/conversation-ui-store.ts b/webapp/_webapp/src/stores/conversation/conversation-ui-store.ts index 8728c1fd..19a33de1 100644 --- a/webapp/_webapp/src/stores/conversation/conversation-ui-store.ts +++ b/webapp/_webapp/src/stores/conversation/conversation-ui-store.ts @@ -64,6 +64,9 @@ interface ConversationUiStore { lastUsedModelSlug: string; setLastUsedModelSlug: (lastUsedModelSlug: string) => void; + lastUsedCustomModelId: string; + setLastUsedCustomModelId: (lastUsedCustomModelId: string) => void; + resetPosition: () => void; } @@ -120,6 +123,9 @@ export const useConversationUiStore = create()( lastUsedModelSlug: "openai/gpt-4.1", setLastUsedModelSlug: (lastUsedModelSlug: string) => set({ lastUsedModelSlug }), + lastUsedCustomModelId: "", + setLastUsedCustomModelId: (lastUsedCustomModelId: string) => set({ lastUsedCustomModelId }), + resetPosition: () => { set({ floatingX: 100, diff --git a/webapp/_webapp/src/stores/setting-store.ts b/webapp/_webapp/src/stores/setting-store.ts index 407b36c5..c9e3b647 100644 --- a/webapp/_webapp/src/stores/setting-store.ts +++ b/webapp/_webapp/src/stores/setting-store.ts @@ -63,6 +63,7 @@ const defaultSettings: PlainMessage = { fullDocumentRag: false, showedOnboarding: true, openaiApiKey: "", + customModels: [], }; export const useSettingStore = create()((set, get) => ({ diff --git a/webapp/_webapp/src/utils/stream-request-builder.ts b/webapp/_webapp/src/utils/stream-request-builder.ts index 7e132f47..6c9cef26 100644 --- a/webapp/_webapp/src/utils/stream-request-builder.ts +++ b/webapp/_webapp/src/utils/stream-request-builder.ts @@ -31,7 +31,8 @@ export interface StreamRequestParams { surroundingText?: string; /** Conversation mode (debug or default) */ conversationMode: "debug" | "default"; - /** Parent message ID for message editing/branching */ + /** User-specified custom model ID for the conversation */ + customModelId?: string; } // ============================================================================ @@ -68,6 +69,7 @@ export function buildStreamRequest(params: StreamRequestParams): PlainMessage[] = useMemo(() => { - return models.map((model) => ({ + const customModels = models.filter((m) => m.isCustom); + const builtInModels = models.filter((m) => !m.isCustom); + + const mapToItem = (model: (typeof models)[number]): SelectionItem => ({ title: model.name, - subtitle: model.slug, + subtitle: `${model.slug}${model.isCustom ? " (Custom)" : ""}`, value: model.slug, disabled: model.disabled, disabledReason: model.disabledReason, - })); + id: model.id ?? undefined, + isCustom: model.isCustom, + }); + + const customItems = customModels.map(mapToItem); + const builtInItems = builtInModels.map(mapToItem); + + if (customItems.length > 0 && builtInItems.length > 0) { + return [ + ...customItems, + { + title: "divider", + value: "__divider__" as string, + disabled: true, + isDivider: true, + }, + ...builtInItems, + ]; + } + + return [...customItems, ...builtInItems]; }, [models]); const onSelect = useCallback( (item: SelectionItem) => { - if (item.disabled) return; - setModel(models.find((m) => m.slug === item.value)!); + if (item.disabled || item.isDivider) return; + + const selectedModel = item.isCustom + ? ((item.id ? models.find((m) => m.id === item.id) : undefined) ?? models.find((m) => m.slug === item.value)) + : models.find((m) => m.slug === item.value); + if (!selectedModel) return; + + setModel(selectedModel); onSelectModel(); inputRef.current?.focus(); }, diff --git a/webapp/_webapp/src/views/chat/footer/toolbar/selection.tsx b/webapp/_webapp/src/views/chat/footer/toolbar/selection.tsx index 2e91bf5b..1928d824 100644 --- a/webapp/_webapp/src/views/chat/footer/toolbar/selection.tsx +++ b/webapp/_webapp/src/views/chat/footer/toolbar/selection.tsx @@ -12,6 +12,9 @@ export type SelectionItem = { value: T; disabled?: boolean; disabledReason?: string; + id?: string; + isCustom?: boolean; + isDivider?: boolean; }; type SelectionProps = { @@ -171,77 +174,99 @@ export function Selection({ items, initialValue, onSelect, onClose }: Selecti heightCollapseRequired || minimalistMode ? "p-0 max-h-[100px]" : "p-2 max-h-[200px]", )} > - {items?.map((item, idx) => ( -
{ - if (item.disabled) return; - googleAnalytics.fireEvent(user?.id, `select_${normalizeName(item.title)}`, {}); - onSelect?.(item); - }} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - if (item.disabled) return; - googleAnalytics.fireEvent(user?.id, `select_${normalizeName(item.title)}`, {}); - onSelect?.(item); - } - }} - onMouseEnter={() => { - if (!isKeyboardNavigation && !item.disabled) { - setSelectedIdx(idx); - } - }} - > + {items?.map((item, idx) => { + if (item.isDivider) { + return ( +
+
+
+ ); + } + + return (
{ + if (item.disabled) return; + googleAnalytics.fireEvent(user?.id, `select_${normalizeName(item.title)}`, {}); + onSelect?.(item); + }} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + if (item.disabled) return; + googleAnalytics.fireEvent(user?.id, `select_${normalizeName(item.title)}`, {}); + onSelect?.(item); + } + }} + onMouseEnter={() => { + if (!isKeyboardNavigation && !item.disabled) { + setSelectedIdx(idx); + } + }} > - {item.title} - {item.disabled && ( - - - - )} - {item.subtitle && ( - - {item.subtitle} - - )} -
- {(item.description || item.disabledReason) && (
- {item.disabledReason || item.description} + {item.title} + {item.disabled && ( + + + + )} + {item.subtitle && ( + + {(() => { + const CUSTOM_SUFFIX = " (Custom)"; + if (item.subtitle.endsWith(CUSTOM_SUFFIX)) { + const main = item.subtitle.slice(0, -CUSTOM_SUFFIX.length); + return ( + <> + {main} + {CUSTOM_SUFFIX.trim()} + + ); + } + return <>{item.subtitle}; + })()} + + )}
- )} -
- ))} + {(item.description || item.disabledReason) && ( +
+ {item.disabledReason || item.description} +
+ )} +
+ ); + })} ); } diff --git a/webapp/_webapp/src/views/settings/sections/api-key-settings.tsx b/webapp/_webapp/src/views/settings/sections/api-key-settings.tsx index 82f5cd90..eb69e4d8 100644 --- a/webapp/_webapp/src/views/settings/sections/api-key-settings.tsx +++ b/webapp/_webapp/src/views/settings/sections/api-key-settings.tsx @@ -1,21 +1,599 @@ +import { Fragment, useEffect, useState } from "react"; +import { Icon } from "@iconify/react"; +import { Modal } from "../../../components/modal"; import { SettingsSectionContainer, SettingsSectionTitle } from "./components"; -import { createSettingsTextInput } from "../setting-text-input"; - -const ApiKeyInput = createSettingsTextInput("openaiApiKey"); +import { Accordion, AccordionItem, Button, Tooltip } from "@heroui/react"; +import { useSettingStore } from "../../../stores/setting-store"; export const ApiKeySettings = () => { + const { updateSettings, settings } = useSettingStore(); + + const [isShowModal, setIsShowModal] = useState(false); + + const handleCustomModelChange = async (newModel: CustomModel, isDelete: boolean) => { + const otherCustomModels = Array.from(settings?.customModels || []).filter((model) => model.id != newModel.id); + + if (isDelete) { + await updateSettings({ + customModels: otherCustomModels, + }); + } else { + const hasDuplicate = otherCustomModels.some( + (model) => + model.name.trim().toLowerCase() === newModel.name.trim().toLowerCase() && + model.slug.trim().toLowerCase() === newModel.slug.trim().toLowerCase(), + ); + + if (hasDuplicate) { + throw new Error("A model with the same name and slug already exists."); + } + + await updateSettings({ + customModels: [ + ...otherCustomModels, + { + id: newModel.id, + name: newModel.name, + baseUrl: newModel.baseUrl, + slug: newModel.slug, + apiKey: newModel.apiKey, + contextWindow: newModel.contextWindow, + maxOutput: newModel.maxOutput, + inputPrice: newModel.inputPrice, + outputPrice: newModel.outputPrice, + temperature: newModel.temperature, + parallelToolCalls: newModel.parallelToolCalls, + store: newModel.store, + }, + ], + }); + } + }; + return ( - Bring Your Own Key (BYOK) -
- + Bring Your Own Key (BYOK) -{" "} + + You can use your custom models and API keys by selecting it in the chat tab. +
+ Your models are labeled with "(Custom)". + + } + placement="top" + delay={100} + > + + {settings?.customModels.length ?? 0} Model(s) Found + +
+ + + setIsShowModal(isOpen)} + content={ +
+ + {Array.from(settings?.customModels || []) + .sort((m1, m2) => m1.name.localeCompare(m2.name)) + .map((m) => ( + +
+ +
+ ))} +
+ } + /> + + ); +}; + +type CustomModel = { + id: string; + name: string; + baseUrl: string; + slug: string; + apiKey: string; + contextWindow: number; + maxOutput: number; + temperature: number; + parallelToolCalls: boolean; + store: boolean; + inputPrice: number; + outputPrice: number; +}; + +type NewCustomModelSectionProps = { + isNew: true; + onChange: (model: CustomModel, isDelete: boolean) => Promise; + model?: never; +}; + +type ExistingCustomModelSectionProps = { + isNew: false; + onChange: (model: CustomModel, isDelete: boolean) => Promise; + model: CustomModel; +}; + +type CustomModelSectionProps = NewCustomModelSectionProps | ExistingCustomModelSectionProps; + +const CustomModelSection = ({ isNew, onChange, model: customModel }: CustomModelSectionProps) => { + const defaults = { + modelName: "My Model", + slug: "", + baseUrl: "", + apiKey: "", + maxOutput: 4000, + temperature: 0.7, + parallelToolCalls: true, + store: false, + contextWindow: 0, + inputPrice: 0, + outputPrice: 0, + }; + + const id = customModel?.id || ""; + const [isEditing, setIsEditing] = useState(isNew); + const [isProcessing, setIsProcessing] = useState(false); + const [processingAction, setProcessingAction] = useState<"save" | "delete" | null>(null); + const [baseUrl, setBaseUrl] = useState(customModel?.baseUrl || defaults.baseUrl); + const [slug, setSlug] = useState(customModel?.slug ?? defaults.slug); + const [apiKey, setApiKey] = useState(customModel?.apiKey || defaults.apiKey); + const [contextWindow, setContextWindow] = useState(customModel?.contextWindow ?? defaults.contextWindow); + const [maxOutput, setMaxOutput] = useState(customModel?.maxOutput ?? defaults.maxOutput); + const [temperature, setTemperature] = useState(customModel?.temperature ?? defaults.temperature); + const [parallelToolCalls, setParallelToolCalls] = useState( + customModel?.parallelToolCalls ?? defaults.parallelToolCalls, + ); + const [store, setStore] = useState(customModel?.store ?? defaults.store); + const [inputPrice, setInputPrice] = useState(customModel?.inputPrice ?? defaults.inputPrice); + const [outputPrice, setOutputPrice] = useState(customModel?.outputPrice ?? defaults.outputPrice); + const [modelName, setModelName] = useState(customModel?.name || defaults.modelName); + const [isModelNameValid, setIsModelNameValid] = useState(true); + const [isSlugValid, setIsSlugValid] = useState(true); + const [isBaseUrlValid, setIsBaseUrlValid] = useState(true); + const [isApiKeyValid, setIsApiKeyValid] = useState(true); + const [submitError, setSubmitError] = useState(null); + + const borderedInputClassName = "rnd-cancel px-2 py-1 border !border-gray-200 dark:!border-default-200 rounded-md"; + const baseClassName = "bg-transparent p-1 focus:outline-none disabled:opacity-70"; + const modelNameInputClassName = `${baseClassName} ${isEditing || isNew ? borderedInputClassName : ""} text-sm text-default-900 font-medium flex-1 truncate mr-1`; + const labelClassName = `${baseClassName} text-xs text-default-900 w-auto`; + const detailInputClassName = `${baseClassName} ${isEditing || isNew ? borderedInputClassName : ""} flex-1 noselect focus:outline-none text-xs text-default-700 placeholder:text-default-400`; + const errorInputClassName = "!border-red-500 focus:!border-red-500"; + + useEffect(() => { + if (isNew || !customModel) return; + if (isEditing) return; + + setModelName(customModel.name || defaults.modelName); + setBaseUrl(customModel.baseUrl || defaults.baseUrl); + setSlug(customModel.slug || defaults.slug); + setApiKey(customModel.apiKey || defaults.apiKey); + setContextWindow(customModel.contextWindow ?? defaults.contextWindow); + setMaxOutput(customModel.maxOutput ?? defaults.maxOutput); + setInputPrice(customModel.inputPrice ?? defaults.inputPrice); + setOutputPrice(customModel.outputPrice ?? defaults.outputPrice); + setTemperature(customModel.temperature ?? defaults.temperature); + setParallelToolCalls(customModel.parallelToolCalls ?? defaults.parallelToolCalls); + setStore(customModel.store ?? defaults.store); + }, [isNew, isEditing, customModel]); + + const handleOnChange = async (isDelete: boolean) => { + if (isProcessing) return; + + const isSaveAction = !isDelete; + + if (isSaveAction) { + // Input validation + const missingFields: string[] = []; + if (modelName.trim().length < 1) missingFields.push("Model Name"); + if (slug.trim().length < 1) missingFields.push("Slug"); + if (baseUrl.trim().length < 1) missingFields.push("Base URL"); + if (apiKey.trim().length < 1) missingFields.push("API Key"); + + if (missingFields.length > 0) { + setIsModelNameValid(modelName.trim().length > 0); + setIsSlugValid(slug.trim().length > 0); + setIsBaseUrlValid(baseUrl.trim().length > 0); + setIsApiKeyValid(apiKey.trim().length > 0); + setSubmitError(`Please fill in required fields: ${missingFields.join(", ")}.`); + return; + } + + if (maxOutput < 1) { + setSubmitError("Max Output cannot be less than 1."); + return; + } + } + + setSubmitError(null); + setIsProcessing(true); + setProcessingAction(isDelete ? "delete" : "save"); + + try { + await onChange( + { + id: id, + name: modelName.trim(), + baseUrl: baseUrl.trim(), + slug: slug.trim(), + apiKey: apiKey.trim(), + contextWindow: contextWindow, + maxOutput: maxOutput, + inputPrice: inputPrice, + outputPrice: outputPrice, + temperature: temperature, + parallelToolCalls: parallelToolCalls, + store: store, + }, + isDelete, + ); + + if (isNew) { + setModelName(defaults.modelName); + setBaseUrl(defaults.baseUrl); + setSlug(defaults.slug); + setApiKey(defaults.apiKey); + setContextWindow(defaults.contextWindow); + setMaxOutput(defaults.maxOutput); + setInputPrice(defaults.inputPrice); + setOutputPrice(defaults.outputPrice); + setTemperature(defaults.temperature); + setParallelToolCalls(defaults.parallelToolCalls); + setStore(defaults.store); + } else if (isSaveAction) { + setIsEditing(false); + } + } catch (error) { + setSubmitError(error instanceof Error ? error.message : "Failed to save new model."); + } finally { + setIsProcessing(false); + setProcessingAction(null); + } + }; + + return ( +
+
+ { + setIsModelNameValid(true); + setSubmitError(null); + setModelName(e.target.value); + }} + > + + {isNew ? ( + + + + ) : ( +
+ + + + + + +
+ )} +
+ +
+ + Slugs are unique, short identifiers for AI models in API calls. +
+ Common examples: +
+ - gemini-2.5-flash +
+ - MiniMax-M2.5 +
+ - glm-4.7 +
+ - gpt-5.1 +
+ - openai/gpt-5.1 (OpenRouter) +
+
+ PaperDebugger currently only supports models that support the Chat Completions API. +
+ } + placement="bottom" + delay={100} + > + + + { + setIsSlugValid(true); + setSubmitError(null); + setSlug(e.target.value); + }} />
- + +
+ + Only OpenAI-compatible endpoints are supported currently. +
+ Common examples: +
+ - https://api.anthropic.com/v1/ +
+ - https://api.openai.com/v1 +
+ - https://generativelanguage.googleapis.com/v1beta/openai/ +
+
+ } + placement="bottom" + delay={100} + > + + + { + setIsBaseUrlValid(true); + setSubmitError(null); + setBaseUrl(e.target.value); + }} + /> +
+ +
+ + { + setIsApiKeyValid(true); + setSubmitError(null); + setApiKey(e.target.value); + }} + /> +
+ +
+ + + + setMaxOutput(e.target.value === "" ? 0 : Math.trunc(Number(e.target.value)))} + /> +
+ +
+ + Hyperparameter that controls the randomness and creativity of text generation. +
+ Higher values make outputs more creative, diverse, or random +
+ } + placement="bottom" + delay={100} + > + + + setTemperature(e.target.value === "" ? 0 : Number(e.target.value))} + /> + + +
+ + + + setParallelToolCalls(e.target.checked)} + /> +
+ +
+ + + + setStore(e.target.checked)} + /> +
+ + + + + + } + classNames={{ + trigger: "px-1 py-0 min-h-0", + content: "pt-1 pb-1", + }} + > +
+
+ + setContextWindow(e.target.value === "" ? 0 : Math.trunc(Number(e.target.value)))} + /> +
+ +
+ + setInputPrice(e.target.value === "" ? 0 : Math.trunc(Number(e.target.value)))} + /> +
+ +
+ + setOutputPrice(e.target.value === "" ? 0 : Math.trunc(Number(e.target.value)))} + /> +
+
+
+
+ + {submitError &&
{submitError}
} + ); };