diff --git a/internal/api/chat/create_conversation_message_stream_v2.go b/internal/api/chat/create_conversation_message_stream_v2.go index 85f68c99..d1dfb3e6 100644 --- a/internal/api/chat/create_conversation_message_stream_v2.go +++ b/internal/api/chat/create_conversation_message_stream_v2.go @@ -2,6 +2,7 @@ package chat import ( "context" + "fmt" "paperdebugger/internal/api/mapper" "paperdebugger/internal/libs/contextutil" "paperdebugger/internal/libs/shared" @@ -281,14 +282,21 @@ func (s *ChatServerV2) CreateConversationMessageStream( var llmProvider *models.LLMProviderConfig var customModel *models.CustomModel customModel = nil - for i := range settings.CustomModels { - if settings.CustomModels[i].Slug == modelSlug { - customModel = &settings.CustomModels[i] + + 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 } - // Usage is the same as ChatCompletion, just passing the stream parameter - if customModel == nil { // User did not specify API key for this model llmProvider = &models.LLMProviderConfig{ diff --git a/internal/api/chat/list_supported_models_v2.go b/internal/api/chat/list_supported_models_v2.go index 5db6fb6f..ac1c4e6b 100644 --- a/internal/api/chat/list_supported_models_v2.go +++ b/internal/api/chat/list_supported_models_v2.go @@ -222,7 +222,9 @@ func (s *ChatServerV2) ListSupportedModels( var models []*chatv2.SupportedModel 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), diff --git a/pkg/gen/api/chat/v2/chat.pb.go b/pkg/gen/api/chat/v2/chat.pb.go index 2f66c2e6..22fc2786 100644 --- a/pkg/gen/api/chat/v2/chat.pb.go +++ b/pkg/gen/api/chat/v2/chat.pb.go @@ -1035,6 +1035,7 @@ type SupportedModel struct { 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 } @@ -1132,6 +1133,13 @@ func (x *SupportedModel) GetIsCustom() bool { 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 @@ -1635,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 } @@ -1718,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"` @@ -2069,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\"\xbb\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" + @@ -2081,8 +2097,11 @@ const file_chat_v2_chat_proto_rawDesc = "" + "\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\x01\x12\x1b\n" + - "\tis_custom\x18\t \x01(\bR\bisCustomB\x12\n" + - "\x10_disabled_reason\"\x1c\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" + @@ -2113,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" + @@ -2123,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/proto/chat/v2/chat.proto b/proto/chat/v2/chat.proto index 4ec44427..a9754a1e 100644 --- a/proto/chat/v2/chat.proto +++ b/proto/chat/v2/chat.proto @@ -137,6 +137,7 @@ message SupportedModel { 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 { @@ -223,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/webapp/_webapp/src/hooks/useLanguageModels.ts b/webapp/_webapp/src/hooks/useLanguageModels.ts index 676104b3..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; @@ -39,6 +40,7 @@ const fallbackModels: Model[] = [ ]; const mapSupportedModelToModel = (supportedModel: SupportedModel): Model => ({ + id: supportedModel.id || undefined, name: supportedModel.name, slug: supportedModel.slug, provider: extractProvider(supportedModel.slug), @@ -53,7 +55,7 @@ const mapSupportedModelToModel = (supportedModel: SupportedModel): Model => ({ export const useLanguageModels = () => { const { currentConversation, setCurrentConversation } = useConversationStore(); - const { setLastUsedModelSlug } = useConversationUiStore(); + const { lastUsedCustomModelId, setLastUsedModelSlug, setLastUsedCustomModelId } = useConversationUiStore(); const { data: supportedModelsResponse } = useListSupportedModelsQuery(); const models: Model[] = useMemo(() => { @@ -64,19 +66,25 @@ export const useLanguageModels = () => { }, [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 f270c782..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("ChJjaGF0L3YyL2NoYXQucHJvdG8SB2NoYXQudjIiUAoTTWVzc2FnZVR5cGVUb29sQ2FsbBIMCgRuYW1lGAEgASgJEgwKBGFyZ3MYAiABKAkSDgoGcmVzdWx0GAMgASgJEg0KBWVycm9yGAQgASgJIkEKI01lc3NhZ2VUeXBlVG9vbENhbGxQcmVwYXJlQXJndW1lbnRzEgwKBG5hbWUYASABKAkSDAoEYXJncxgCIAEoCSIkChFNZXNzYWdlVHlwZVN5c3RlbRIPCgdjb250ZW50GAEgASgJImEKFE1lc3NhZ2VUeXBlQXNzaXN0YW50Eg8KB2NvbnRlbnQYASABKAkSEgoKbW9kZWxfc2x1ZxgCIAEoCRIWCglyZWFzb25pbmcYAyABKAlIAIgBAUIMCgpfcmVhc29uaW5nInoKD01lc3NhZ2VUeXBlVXNlchIPCgdjb250ZW50GAEgASgJEhoKDXNlbGVjdGVkX3RleHQYAiABKAlIAIgBARIYCgtzdXJyb3VuZGluZxgHIAEoCUgBiAEBQhAKDl9zZWxlY3RlZF90ZXh0Qg4KDF9zdXJyb3VuZGluZyIpChJNZXNzYWdlVHlwZVVua25vd24SEwoLZGVzY3JpcHRpb24YASABKAki5AIKDk1lc3NhZ2VQYXlsb2FkEiwKBnN5c3RlbRgBIAEoCzIaLmNoYXQudjIuTWVzc2FnZVR5cGVTeXN0ZW1IABIoCgR1c2VyGAIgASgLMhguY2hhdC52Mi5NZXNzYWdlVHlwZVVzZXJIABIyCglhc3Npc3RhbnQYAyABKAsyHS5jaGF0LnYyLk1lc3NhZ2VUeXBlQXNzaXN0YW50SAASUwobdG9vbF9jYWxsX3ByZXBhcmVfYXJndW1lbnRzGAQgASgLMiwuY2hhdC52Mi5NZXNzYWdlVHlwZVRvb2xDYWxsUHJlcGFyZUFyZ3VtZW50c0gAEjEKCXRvb2xfY2FsbBgFIAEoCzIcLmNoYXQudjIuTWVzc2FnZVR5cGVUb29sQ2FsbEgAEi4KB3Vua25vd24YBiABKAsyGy5jaGF0LnYyLk1lc3NhZ2VUeXBlVW5rbm93bkgAQg4KDG1lc3NhZ2VfdHlwZSJaCgdNZXNzYWdlEhIKCm1lc3NhZ2VfaWQYASABKAkSKAoHcGF5bG9hZBgCIAEoCzIXLmNoYXQudjIuTWVzc2FnZVBheWxvYWQSEQoJdGltZXN0YW1wGAMgASgDImEKDENvbnZlcnNhdGlvbhIKCgJpZBgBIAEoCRINCgV0aXRsZRgCIAEoCRISCgptb2RlbF9zbHVnGAMgASgJEiIKCG1lc3NhZ2VzGAQgAygLMhAuY2hhdC52Mi5NZXNzYWdlIkIKGExpc3RDb252ZXJzYXRpb25zUmVxdWVzdBIXCgpwcm9qZWN0X2lkGAEgASgJSACIAQFCDQoLX3Byb2plY3RfaWQiSQoZTGlzdENvbnZlcnNhdGlvbnNSZXNwb25zZRIsCg1jb252ZXJzYXRpb25zGAEgAygLMhUuY2hhdC52Mi5Db252ZXJzYXRpb24iMQoWR2V0Q29udmVyc2F0aW9uUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkiRgoXR2V0Q29udmVyc2F0aW9uUmVzcG9uc2USKwoMY29udmVyc2F0aW9uGAEgASgLMhUuY2hhdC52Mi5Db252ZXJzYXRpb24iQwoZVXBkYXRlQ29udmVyc2F0aW9uUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDQoFdGl0bGUYAiABKAkiSQoaVXBkYXRlQ29udmVyc2F0aW9uUmVzcG9uc2USKwoMY29udmVyc2F0aW9uGAEgASgLMhUuY2hhdC52Mi5Db252ZXJzYXRpb24iNAoZRGVsZXRlQ29udmVyc2F0aW9uUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkiHAoaRGVsZXRlQ29udmVyc2F0aW9uUmVzcG9uc2Ui2QEKDlN1cHBvcnRlZE1vZGVsEgwKBG5hbWUYASABKAkSDAoEc2x1ZxgCIAEoCRIVCg10b3RhbF9jb250ZXh0GAMgASgDEhIKCm1heF9vdXRwdXQYBCABKAMSEwoLaW5wdXRfcHJpY2UYBSABKAMSFAoMb3V0cHV0X3ByaWNlGAYgASgDEhAKCGRpc2FibGVkGAcgASgIEhwKD2Rpc2FibGVkX3JlYXNvbhgIIAEoCUgAiAEBEhEKCWlzX2N1c3RvbRgJIAEoCEISChBfZGlzYWJsZWRfcmVhc29uIhwKGkxpc3RTdXBwb3J0ZWRNb2RlbHNSZXF1ZXN0IkYKG0xpc3RTdXBwb3J0ZWRNb2RlbHNSZXNwb25zZRInCgZtb2RlbHMYASADKAsyFy5jaGF0LnYyLlN1cHBvcnRlZE1vZGVsIkMKFFN0cmVhbUluaXRpYWxpemF0aW9uEhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRISCgptb2RlbF9zbHVnGAIgASgJIk8KD1N0cmVhbVBhcnRCZWdpbhISCgptZXNzYWdlX2lkGAEgASgJEigKB3BheWxvYWQYAyABKAsyFy5jaGF0LnYyLk1lc3NhZ2VQYXlsb2FkIjEKDE1lc3NhZ2VDaHVuaxISCgptZXNzYWdlX2lkGAEgASgJEg0KBWRlbHRhGAIgASgJIjMKDlJlYXNvbmluZ0NodW5rEhIKCm1lc3NhZ2VfaWQYASABKAkSDQoFZGVsdGEYAiABKAkiOgoTSW5jb21wbGV0ZUluZGljYXRvchIOCgZyZWFzb24YASABKAkSEwoLcmVzcG9uc2VfaWQYAiABKAkiTQoNU3RyZWFtUGFydEVuZBISCgptZXNzYWdlX2lkGAEgASgJEigKB3BheWxvYWQYAyABKAsyFy5jaGF0LnYyLk1lc3NhZ2VQYXlsb2FkIi0KElN0cmVhbUZpbmFsaXphdGlvbhIXCg9jb252ZXJzYXRpb25faWQYASABKAkiJAoLU3RyZWFtRXJyb3ISFQoNZXJyb3JfbWVzc2FnZRgBIAEoCSLLAgomQ3JlYXRlQ29udmVyc2F0aW9uTWVzc2FnZVN0cmVhbVJlcXVlc3QSEgoKcHJvamVjdF9pZBgBIAEoCRIcCg9jb252ZXJzYXRpb25faWQYAiABKAlIAIgBARISCgptb2RlbF9zbHVnGAMgASgJEhQKDHVzZXJfbWVzc2FnZRgEIAEoCRIfChJ1c2VyX3NlbGVjdGVkX3RleHQYBSABKAlIAYgBARI5ChFjb252ZXJzYXRpb25fdHlwZRgGIAEoDjIZLmNoYXQudjIuQ29udmVyc2F0aW9uVHlwZUgCiAEBEhgKC3N1cnJvdW5kaW5nGAggASgJSAOIAQFCEgoQX2NvbnZlcnNhdGlvbl9pZEIVChNfdXNlcl9zZWxlY3RlZF90ZXh0QhQKEl9jb252ZXJzYXRpb25fdHlwZUIOCgxfc3Vycm91bmRpbmci8wMKJ0NyZWF0ZUNvbnZlcnNhdGlvbk1lc3NhZ2VTdHJlYW1SZXNwb25zZRI+ChVzdHJlYW1faW5pdGlhbGl6YXRpb24YASABKAsyHS5jaGF0LnYyLlN0cmVhbUluaXRpYWxpemF0aW9uSAASNQoRc3RyZWFtX3BhcnRfYmVnaW4YAiABKAsyGC5jaGF0LnYyLlN0cmVhbVBhcnRCZWdpbkgAEi4KDW1lc3NhZ2VfY2h1bmsYAyABKAsyFS5jaGF0LnYyLk1lc3NhZ2VDaHVua0gAEjwKFGluY29tcGxldGVfaW5kaWNhdG9yGAQgASgLMhwuY2hhdC52Mi5JbmNvbXBsZXRlSW5kaWNhdG9ySAASMQoPc3RyZWFtX3BhcnRfZW5kGAUgASgLMhYuY2hhdC52Mi5TdHJlYW1QYXJ0RW5kSAASOgoTc3RyZWFtX2ZpbmFsaXphdGlvbhgGIAEoCzIbLmNoYXQudjIuU3RyZWFtRmluYWxpemF0aW9uSAASLAoMc3RyZWFtX2Vycm9yGAcgASgLMhQuY2hhdC52Mi5TdHJlYW1FcnJvckgAEjIKD3JlYXNvbmluZ19jaHVuaxgIIAEoCzIXLmNoYXQudjIuUmVhc29uaW5nQ2h1bmtIAEISChByZXNwb25zZV9wYXlsb2FkIj4KFkdldENpdGF0aW9uS2V5c1JlcXVlc3QSEAoIc2VudGVuY2UYASABKAkSEgoKcHJvamVjdF9pZBgCIAEoCSIwChdHZXRDaXRhdGlvbktleXNSZXNwb25zZRIVCg1jaXRhdGlvbl9rZXlzGAEgAygJKlIKEENvbnZlcnNhdGlvblR5cGUSIQodQ09OVkVSU0FUSU9OX1RZUEVfVU5TUEVDSUZJRUQQABIbChdDT05WRVJTQVRJT05fVFlQRV9ERUJVRxABMqcICgtDaGF0U2VydmljZRKDAQoRTGlzdENvbnZlcnNhdGlvbnMSIS5jaGF0LnYyLkxpc3RDb252ZXJzYXRpb25zUmVxdWVzdBoiLmNoYXQudjIuTGlzdENvbnZlcnNhdGlvbnNSZXNwb25zZSIngtPkkwIhEh8vX3BkL2FwaS92Mi9jaGF0cy9jb252ZXJzYXRpb25zEo8BCg9HZXRDb252ZXJzYXRpb24SHy5jaGF0LnYyLkdldENvbnZlcnNhdGlvblJlcXVlc3QaIC5jaGF0LnYyLkdldENvbnZlcnNhdGlvblJlc3BvbnNlIjmC0+STAjMSMS9fcGQvYXBpL3YyL2NoYXRzL2NvbnZlcnNhdGlvbnMve2NvbnZlcnNhdGlvbl9pZH0SwgEKH0NyZWF0ZUNvbnZlcnNhdGlvbk1lc3NhZ2VTdHJlYW0SLy5jaGF0LnYyLkNyZWF0ZUNvbnZlcnNhdGlvbk1lc3NhZ2VTdHJlYW1SZXF1ZXN0GjAuY2hhdC52Mi5DcmVhdGVDb252ZXJzYXRpb25NZXNzYWdlU3RyZWFtUmVzcG9uc2UiOoLT5JMCNDoBKiIvL19wZC9hcGkvdjIvY2hhdHMvY29udmVyc2F0aW9ucy9tZXNzYWdlcy9zdHJlYW0wARKbAQoSVXBkYXRlQ29udmVyc2F0aW9uEiIuY2hhdC52Mi5VcGRhdGVDb252ZXJzYXRpb25SZXF1ZXN0GiMuY2hhdC52Mi5VcGRhdGVDb252ZXJzYXRpb25SZXNwb25zZSI8gtPkkwI2OgEqMjEvX3BkL2FwaS92Mi9jaGF0cy9jb252ZXJzYXRpb25zL3tjb252ZXJzYXRpb25faWR9EpgBChJEZWxldGVDb252ZXJzYXRpb24SIi5jaGF0LnYyLkRlbGV0ZUNvbnZlcnNhdGlvblJlcXVlc3QaIy5jaGF0LnYyLkRlbGV0ZUNvbnZlcnNhdGlvblJlc3BvbnNlIjmC0+STAjMqMS9fcGQvYXBpL3YyL2NoYXRzL2NvbnZlcnNhdGlvbnMve2NvbnZlcnNhdGlvbl9pZH0SggEKE0xpc3RTdXBwb3J0ZWRNb2RlbHMSIy5jaGF0LnYyLkxpc3RTdXBwb3J0ZWRNb2RlbHNSZXF1ZXN0GiQuY2hhdC52Mi5MaXN0U3VwcG9ydGVkTW9kZWxzUmVzcG9uc2UiIILT5JMCGhIYL19wZC9hcGkvdjIvY2hhdHMvbW9kZWxzEn0KD0dldENpdGF0aW9uS2V5cxIfLmNoYXQudjIuR2V0Q2l0YXRpb25LZXlzUmVxdWVzdBogLmNoYXQudjIuR2V0Q2l0YXRpb25LZXlzUmVzcG9uc2UiJ4LT5JMCIRIfL19wZC9hcGkvdjIvY2hhdHMvY2l0YXRpb24ta2V5c0J/Cgtjb20uY2hhdC52MkIJQ2hhdFByb3RvUAFaKHBhcGVyZGVidWdnZXIvcGtnL2dlbi9hcGkvY2hhdC92MjtjaGF0djKiAgNDWFiqAgdDaGF0LlYyygIHQ2hhdFxWMuICE0NoYXRcVjJcR1BCTWV0YWRhdGHqAghDaGF0OjpWMmIGcHJvdG8z", [file_google_api_annotations]); + fileDesc("ChJjaGF0L3YyL2NoYXQucHJvdG8SB2NoYXQudjIiUAoTTWVzc2FnZVR5cGVUb29sQ2FsbBIMCgRuYW1lGAEgASgJEgwKBGFyZ3MYAiABKAkSDgoGcmVzdWx0GAMgASgJEg0KBWVycm9yGAQgASgJIkEKI01lc3NhZ2VUeXBlVG9vbENhbGxQcmVwYXJlQXJndW1lbnRzEgwKBG5hbWUYASABKAkSDAoEYXJncxgCIAEoCSIkChFNZXNzYWdlVHlwZVN5c3RlbRIPCgdjb250ZW50GAEgASgJImEKFE1lc3NhZ2VUeXBlQXNzaXN0YW50Eg8KB2NvbnRlbnQYASABKAkSEgoKbW9kZWxfc2x1ZxgCIAEoCRIWCglyZWFzb25pbmcYAyABKAlIAIgBAUIMCgpfcmVhc29uaW5nInoKD01lc3NhZ2VUeXBlVXNlchIPCgdjb250ZW50GAEgASgJEhoKDXNlbGVjdGVkX3RleHQYAiABKAlIAIgBARIYCgtzdXJyb3VuZGluZxgHIAEoCUgBiAEBQhAKDl9zZWxlY3RlZF90ZXh0Qg4KDF9zdXJyb3VuZGluZyIpChJNZXNzYWdlVHlwZVVua25vd24SEwoLZGVzY3JpcHRpb24YASABKAki5AIKDk1lc3NhZ2VQYXlsb2FkEiwKBnN5c3RlbRgBIAEoCzIaLmNoYXQudjIuTWVzc2FnZVR5cGVTeXN0ZW1IABIoCgR1c2VyGAIgASgLMhguY2hhdC52Mi5NZXNzYWdlVHlwZVVzZXJIABIyCglhc3Npc3RhbnQYAyABKAsyHS5jaGF0LnYyLk1lc3NhZ2VUeXBlQXNzaXN0YW50SAASUwobdG9vbF9jYWxsX3ByZXBhcmVfYXJndW1lbnRzGAQgASgLMiwuY2hhdC52Mi5NZXNzYWdlVHlwZVRvb2xDYWxsUHJlcGFyZUFyZ3VtZW50c0gAEjEKCXRvb2xfY2FsbBgFIAEoCzIcLmNoYXQudjIuTWVzc2FnZVR5cGVUb29sQ2FsbEgAEi4KB3Vua25vd24YBiABKAsyGy5jaGF0LnYyLk1lc3NhZ2VUeXBlVW5rbm93bkgAQg4KDG1lc3NhZ2VfdHlwZSJaCgdNZXNzYWdlEhIKCm1lc3NhZ2VfaWQYASABKAkSKAoHcGF5bG9hZBgCIAEoCzIXLmNoYXQudjIuTWVzc2FnZVBheWxvYWQSEQoJdGltZXN0YW1wGAMgASgDImEKDENvbnZlcnNhdGlvbhIKCgJpZBgBIAEoCRINCgV0aXRsZRgCIAEoCRISCgptb2RlbF9zbHVnGAMgASgJEiIKCG1lc3NhZ2VzGAQgAygLMhAuY2hhdC52Mi5NZXNzYWdlIkIKGExpc3RDb252ZXJzYXRpb25zUmVxdWVzdBIXCgpwcm9qZWN0X2lkGAEgASgJSACIAQFCDQoLX3Byb2plY3RfaWQiSQoZTGlzdENvbnZlcnNhdGlvbnNSZXNwb25zZRIsCg1jb252ZXJzYXRpb25zGAEgAygLMhUuY2hhdC52Mi5Db252ZXJzYXRpb24iMQoWR2V0Q29udmVyc2F0aW9uUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkiRgoXR2V0Q29udmVyc2F0aW9uUmVzcG9uc2USKwoMY29udmVyc2F0aW9uGAEgASgLMhUuY2hhdC52Mi5Db252ZXJzYXRpb24iQwoZVXBkYXRlQ29udmVyc2F0aW9uUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDQoFdGl0bGUYAiABKAkiSQoaVXBkYXRlQ29udmVyc2F0aW9uUmVzcG9uc2USKwoMY29udmVyc2F0aW9uGAEgASgLMhUuY2hhdC52Mi5Db252ZXJzYXRpb24iNAoZRGVsZXRlQ29udmVyc2F0aW9uUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkiHAoaRGVsZXRlQ29udmVyc2F0aW9uUmVzcG9uc2Ui8QEKDlN1cHBvcnRlZE1vZGVsEgwKBG5hbWUYASABKAkSDAoEc2x1ZxgCIAEoCRIVCg10b3RhbF9jb250ZXh0GAMgASgDEhIKCm1heF9vdXRwdXQYBCABKAMSEwoLaW5wdXRfcHJpY2UYBSABKAMSFAoMb3V0cHV0X3ByaWNlGAYgASgDEhAKCGRpc2FibGVkGAcgASgIEhwKD2Rpc2FibGVkX3JlYXNvbhgIIAEoCUgAiAEBEhEKCWlzX2N1c3RvbRgJIAEoCBIPCgJpZBgKIAEoCUgBiAEBQhIKEF9kaXNhYmxlZF9yZWFzb25CBQoDX2lkIhwKGkxpc3RTdXBwb3J0ZWRNb2RlbHNSZXF1ZXN0IkYKG0xpc3RTdXBwb3J0ZWRNb2RlbHNSZXNwb25zZRInCgZtb2RlbHMYASADKAsyFy5jaGF0LnYyLlN1cHBvcnRlZE1vZGVsIkMKFFN0cmVhbUluaXRpYWxpemF0aW9uEhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRISCgptb2RlbF9zbHVnGAIgASgJIk8KD1N0cmVhbVBhcnRCZWdpbhISCgptZXNzYWdlX2lkGAEgASgJEigKB3BheWxvYWQYAyABKAsyFy5jaGF0LnYyLk1lc3NhZ2VQYXlsb2FkIjEKDE1lc3NhZ2VDaHVuaxISCgptZXNzYWdlX2lkGAEgASgJEg0KBWRlbHRhGAIgASgJIjMKDlJlYXNvbmluZ0NodW5rEhIKCm1lc3NhZ2VfaWQYASABKAkSDQoFZGVsdGEYAiABKAkiOgoTSW5jb21wbGV0ZUluZGljYXRvchIOCgZyZWFzb24YASABKAkSEwoLcmVzcG9uc2VfaWQYAiABKAkiTQoNU3RyZWFtUGFydEVuZBISCgptZXNzYWdlX2lkGAEgASgJEigKB3BheWxvYWQYAyABKAsyFy5jaGF0LnYyLk1lc3NhZ2VQYXlsb2FkIi0KElN0cmVhbUZpbmFsaXphdGlvbhIXCg9jb252ZXJzYXRpb25faWQYASABKAkiJAoLU3RyZWFtRXJyb3ISFQoNZXJyb3JfbWVzc2FnZRgBIAEoCSL9AgomQ3JlYXRlQ29udmVyc2F0aW9uTWVzc2FnZVN0cmVhbVJlcXVlc3QSEgoKcHJvamVjdF9pZBgBIAEoCRIcCg9jb252ZXJzYXRpb25faWQYAiABKAlIAIgBARISCgptb2RlbF9zbHVnGAMgASgJEhQKDHVzZXJfbWVzc2FnZRgEIAEoCRIfChJ1c2VyX3NlbGVjdGVkX3RleHQYBSABKAlIAYgBARI5ChFjb252ZXJzYXRpb25fdHlwZRgGIAEoDjIZLmNoYXQudjIuQ29udmVyc2F0aW9uVHlwZUgCiAEBEhgKC3N1cnJvdW5kaW5nGAggASgJSAOIAQESHAoPY3VzdG9tX21vZGVsX2lkGAkgASgJSASIAQFCEgoQX2NvbnZlcnNhdGlvbl9pZEIVChNfdXNlcl9zZWxlY3RlZF90ZXh0QhQKEl9jb252ZXJzYXRpb25fdHlwZUIOCgxfc3Vycm91bmRpbmdCEgoQX2N1c3RvbV9tb2RlbF9pZCLzAwonQ3JlYXRlQ29udmVyc2F0aW9uTWVzc2FnZVN0cmVhbVJlc3BvbnNlEj4KFXN0cmVhbV9pbml0aWFsaXphdGlvbhgBIAEoCzIdLmNoYXQudjIuU3RyZWFtSW5pdGlhbGl6YXRpb25IABI1ChFzdHJlYW1fcGFydF9iZWdpbhgCIAEoCzIYLmNoYXQudjIuU3RyZWFtUGFydEJlZ2luSAASLgoNbWVzc2FnZV9jaHVuaxgDIAEoCzIVLmNoYXQudjIuTWVzc2FnZUNodW5rSAASPAoUaW5jb21wbGV0ZV9pbmRpY2F0b3IYBCABKAsyHC5jaGF0LnYyLkluY29tcGxldGVJbmRpY2F0b3JIABIxCg9zdHJlYW1fcGFydF9lbmQYBSABKAsyFi5jaGF0LnYyLlN0cmVhbVBhcnRFbmRIABI6ChNzdHJlYW1fZmluYWxpemF0aW9uGAYgASgLMhsuY2hhdC52Mi5TdHJlYW1GaW5hbGl6YXRpb25IABIsCgxzdHJlYW1fZXJyb3IYByABKAsyFC5jaGF0LnYyLlN0cmVhbUVycm9ySAASMgoPcmVhc29uaW5nX2NodW5rGAggASgLMhcuY2hhdC52Mi5SZWFzb25pbmdDaHVua0gAQhIKEHJlc3BvbnNlX3BheWxvYWQiPgoWR2V0Q2l0YXRpb25LZXlzUmVxdWVzdBIQCghzZW50ZW5jZRgBIAEoCRISCgpwcm9qZWN0X2lkGAIgASgJIjAKF0dldENpdGF0aW9uS2V5c1Jlc3BvbnNlEhUKDWNpdGF0aW9uX2tleXMYASADKAkqUgoQQ29udmVyc2F0aW9uVHlwZRIhCh1DT05WRVJTQVRJT05fVFlQRV9VTlNQRUNJRklFRBAAEhsKF0NPTlZFUlNBVElPTl9UWVBFX0RFQlVHEAEypwgKC0NoYXRTZXJ2aWNlEoMBChFMaXN0Q29udmVyc2F0aW9ucxIhLmNoYXQudjIuTGlzdENvbnZlcnNhdGlvbnNSZXF1ZXN0GiIuY2hhdC52Mi5MaXN0Q29udmVyc2F0aW9uc1Jlc3BvbnNlIieC0+STAiESHy9fcGQvYXBpL3YyL2NoYXRzL2NvbnZlcnNhdGlvbnMSjwEKD0dldENvbnZlcnNhdGlvbhIfLmNoYXQudjIuR2V0Q29udmVyc2F0aW9uUmVxdWVzdBogLmNoYXQudjIuR2V0Q29udmVyc2F0aW9uUmVzcG9uc2UiOYLT5JMCMxIxL19wZC9hcGkvdjIvY2hhdHMvY29udmVyc2F0aW9ucy97Y29udmVyc2F0aW9uX2lkfRLCAQofQ3JlYXRlQ29udmVyc2F0aW9uTWVzc2FnZVN0cmVhbRIvLmNoYXQudjIuQ3JlYXRlQ29udmVyc2F0aW9uTWVzc2FnZVN0cmVhbVJlcXVlc3QaMC5jaGF0LnYyLkNyZWF0ZUNvbnZlcnNhdGlvbk1lc3NhZ2VTdHJlYW1SZXNwb25zZSI6gtPkkwI0OgEqIi8vX3BkL2FwaS92Mi9jaGF0cy9jb252ZXJzYXRpb25zL21lc3NhZ2VzL3N0cmVhbTABEpsBChJVcGRhdGVDb252ZXJzYXRpb24SIi5jaGF0LnYyLlVwZGF0ZUNvbnZlcnNhdGlvblJlcXVlc3QaIy5jaGF0LnYyLlVwZGF0ZUNvbnZlcnNhdGlvblJlc3BvbnNlIjyC0+STAjY6ASoyMS9fcGQvYXBpL3YyL2NoYXRzL2NvbnZlcnNhdGlvbnMve2NvbnZlcnNhdGlvbl9pZH0SmAEKEkRlbGV0ZUNvbnZlcnNhdGlvbhIiLmNoYXQudjIuRGVsZXRlQ29udmVyc2F0aW9uUmVxdWVzdBojLmNoYXQudjIuRGVsZXRlQ29udmVyc2F0aW9uUmVzcG9uc2UiOYLT5JMCMyoxL19wZC9hcGkvdjIvY2hhdHMvY29udmVyc2F0aW9ucy97Y29udmVyc2F0aW9uX2lkfRKCAQoTTGlzdFN1cHBvcnRlZE1vZGVscxIjLmNoYXQudjIuTGlzdFN1cHBvcnRlZE1vZGVsc1JlcXVlc3QaJC5jaGF0LnYyLkxpc3RTdXBwb3J0ZWRNb2RlbHNSZXNwb25zZSIggtPkkwIaEhgvX3BkL2FwaS92Mi9jaGF0cy9tb2RlbHMSfQoPR2V0Q2l0YXRpb25LZXlzEh8uY2hhdC52Mi5HZXRDaXRhdGlvbktleXNSZXF1ZXN0GiAuY2hhdC52Mi5HZXRDaXRhdGlvbktleXNSZXNwb25zZSIngtPkkwIhEh8vX3BkL2FwaS92Mi9jaGF0cy9jaXRhdGlvbi1rZXlzQn8KC2NvbS5jaGF0LnYyQglDaGF0UHJvdG9QAVoocGFwZXJkZWJ1Z2dlci9wa2cvZ2VuL2FwaS9jaGF0L3YyO2NoYXR2MqICA0NYWKoCB0NoYXQuVjLKAgdDaGF0XFYy4gITQ2hhdFxWMlxHUEJNZXRhZGF0YeoCCENoYXQ6OlYyYgZwcm90bzM", [file_google_api_annotations]); /** * @generated from message chat.v2.MessageTypeToolCall @@ -474,6 +474,13 @@ export type SupportedModel = Message$1<"chat.v2.SupportedModel"> & { * @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; }; /** @@ -749,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/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) => { if (item.disabled) return; - setModel(models.find((m) => m.slug === item.value)!); + + 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..5eec50bc 100644 --- a/webapp/_webapp/src/views/chat/footer/toolbar/selection.tsx +++ b/webapp/_webapp/src/views/chat/footer/toolbar/selection.tsx @@ -12,6 +12,8 @@ export type SelectionItem = { value: T; disabled?: boolean; disabledReason?: string; + id?: string; + isCustom?: boolean; }; type SelectionProps = { 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 4d5c854d..126f0d5d 100644 --- a/webapp/_webapp/src/views/settings/sections/api-key-settings.tsx +++ b/webapp/_webapp/src/views/settings/sections/api-key-settings.tsx @@ -105,6 +105,8 @@ type CustomModelSectionProps = NewCustomModelSectionProps | ExistingCustomModelS const CustomModelSection = ({ isNew, onChange, model: customModel }: CustomModelSectionProps) => { 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 || ""); const [slug, setSlug] = useState(customModel?.slug ?? ""); const [apiKey, setApiKey] = useState(customModel?.apiKey || ""); @@ -126,11 +128,13 @@ const CustomModelSection = ({ isNew, onChange, model: customModel }: CustomModel const errorInputClassName = "!border-red-500 focus:!border-red-500"; const handleOnChange = async (isDelete: boolean) => { + if (isProcessing) return; + + const isSaveAction = !isDelete; + if ( - modelName.trim().length < 1 || - slug.trim().length < 1 || - baseUrl.trim().length < 1 || - apiKey.trim().length < 1 + isSaveAction && + (modelName.trim().length < 1 || slug.trim().length < 1 || baseUrl.trim().length < 1 || apiKey.trim().length < 1) ) { setIsModelNameValid(modelName.trim().length > 0); setIsSlugValid(slug.trim().length > 0); @@ -139,32 +143,40 @@ const CustomModelSection = ({ isNew, onChange, model: customModel }: CustomModel return; } - await onChange( - { - id: id, - name: modelName.trim(), - baseUrl: baseUrl.trim(), - slug: slug.trim(), - apiKey: apiKey.trim(), - contextWindow: contextWindow, - maxOutput: maxOutput, - inputPrice: inputPrice, - outputPrice: outputPrice, - }, - isDelete, - ); + setIsProcessing(true); + setProcessingAction(isDelete ? "delete" : "save"); - if (isNew) { - setModelName(""); - setBaseUrl(""); - setSlug(""); - setApiKey(""); - setContextWindow(0); - setMaxOutput(0); - setInputPrice(0); - setOutputPrice(0); - } else { - setIsEditing(false); + 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, + }, + isDelete, + ); + + if (isNew) { + setModelName(""); + setBaseUrl(""); + setSlug(""); + setApiKey(""); + setContextWindow(0); + setMaxOutput(0); + setInputPrice(0); + setOutputPrice(0); + } else if (isSaveAction) { + setIsEditing(false); + } + } finally { + setIsProcessing(false); + setProcessingAction(null); } }; @@ -176,7 +188,7 @@ const CustomModelSection = ({ isNew, onChange, model: customModel }: CustomModel value={modelName} placeholder="My Model" type="text" - disabled={!isEditing} + disabled={!isEditing || isProcessing} onChange={(e) => { setIsModelNameValid(true); setModelName(e.target.value); @@ -185,8 +197,16 @@ const CustomModelSection = ({ isNew, onChange, model: customModel }: CustomModel {isNew ? ( - ) : ( @@ -200,14 +220,33 @@ const CustomModelSection = ({ isNew, onChange, model: customModel }: CustomModel setIsEditing(true); } }} - className="p-1 hover:bg-default-100 rounded" + disabled={isProcessing} + className="p-1 hover:bg-default-100 rounded disabled:opacity-60" > - + - @@ -221,7 +260,7 @@ const CustomModelSection = ({ isNew, onChange, model: customModel }: CustomModel value={slug} placeholder="e.g., gemini-2.5-flash" type="text" - disabled={!isEditing} + disabled={!isEditing || isProcessing} onChange={(e) => { setIsSlugValid(true); setSlug(e.target.value); @@ -236,7 +275,7 @@ const CustomModelSection = ({ isNew, onChange, model: customModel }: CustomModel value={baseUrl} placeholder="An OpenAI-compatible endpoint" type="text" - disabled={!isEditing} + disabled={!isEditing || isProcessing} onChange={(e) => { setIsBaseUrlValid(true); setBaseUrl(e.target.value); @@ -251,7 +290,7 @@ const CustomModelSection = ({ isNew, onChange, model: customModel }: CustomModel value={apiKey} placeholder="Your API Key" type={!isEditing && !isNew ? "password" : "text"} - disabled={!isEditing} + disabled={!isEditing || isProcessing} onChange={(e) => { setIsApiKeyValid(true); setApiKey(e.target.value); @@ -278,7 +317,7 @@ const CustomModelSection = ({ isNew, onChange, model: customModel }: CustomModel type="number" min={0} step="1" - disabled={!isEditing} + disabled={!isEditing || isProcessing} onChange={(e) => setContextWindow(e.target.value === "" ? 0 : Math.trunc(Number(e.target.value)))} /> @@ -291,7 +330,7 @@ const CustomModelSection = ({ isNew, onChange, model: customModel }: CustomModel type="number" min={0} step="1" - disabled={!isEditing} + disabled={!isEditing || isProcessing} onChange={(e) => setMaxOutput(e.target.value === "" ? 0 : Math.trunc(Number(e.target.value)))} /> @@ -304,7 +343,7 @@ const CustomModelSection = ({ isNew, onChange, model: customModel }: CustomModel type="number" min={0} step="1" - disabled={!isEditing} + disabled={!isEditing || isProcessing} onChange={(e) => setInputPrice(e.target.value === "" ? 0 : Math.trunc(Number(e.target.value)))} /> @@ -318,7 +357,7 @@ const CustomModelSection = ({ isNew, onChange, model: customModel }: CustomModel min={0} step="1" pattern="[0-9]*" - disabled={!isEditing} + disabled={!isEditing || isProcessing} onChange={(e) => setOutputPrice(e.target.value === "" ? 0 : Math.trunc(Number(e.target.value)))} />