Skip to content

Commit 2fba738

Browse files
authored
feat: user provided api key & bug fix. (#46)
1 parent 2b3086c commit 2fba738

58 files changed

Lines changed: 1303 additions & 266 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/release.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ jobs:
2222
run: |
2323
export PD_API_ENDPOINT=https://app.paperdebugger.com
2424
export BETA_BUILD=false
25+
export GRAFANA_API_KEY=${{ secrets.GRAFANA_API_KEY }}
2526
cd webapp/_webapp
2627
npm install
2728
npm run build

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ require (
1313
github.com/google/wire v0.7.0
1414
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2
1515
github.com/joho/godotenv v1.5.1
16-
github.com/openai/openai-go/v2 v2.1.1
16+
github.com/openai/openai-go/v2 v2.7.1
1717
github.com/samber/lo v1.51.0
1818
github.com/stretchr/testify v1.10.0
1919
go.mongodb.org/mongo-driver/v2 v2.3.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
8888
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
8989
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
9090
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
91-
github.com/openai/openai-go/v2 v2.1.1 h1:/RMA/V3D+yF/Cc4jHXFt6lkqSOWRf5roRi+DvZaDYQI=
92-
github.com/openai/openai-go/v2 v2.1.1/go.mod h1:sIUkR+Cu/PMUVkSKhkk742PRURkQOCFhiwJ7eRSBqmk=
91+
github.com/openai/openai-go/v2 v2.7.1 h1:/tfvTJhfv7hTSL8mWwc5VL4WLLSDL5yn9VqVykdu9r8=
92+
github.com/openai/openai-go/v2 v2.7.1/go.mod h1:jrJs23apqJKKbT+pqtFgNKpRju/KP9zpUTZhz3GElQE=
9393
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
9494
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
9595
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

internal/api/chat/create_conversation_message.go

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -180,20 +180,20 @@ func (s *ChatServer) appendConversationMessage(
180180

181181
// 如果 conversationId 是 "", 就创建新对话,否则就追加消息到对话
182182
// conversationType 可以在一次 conversation 中多次切换
183-
func (s *ChatServer) prepare(ctx context.Context, projectId string, conversationId string, userMessage string, userSelectedText string, languageModel models.LanguageModel, conversationType chatv1.ConversationType) (context.Context, *models.Conversation, error) {
183+
func (s *ChatServer) prepare(ctx context.Context, projectId string, conversationId string, userMessage string, userSelectedText string, languageModel models.LanguageModel, conversationType chatv1.ConversationType) (context.Context, *models.Conversation, *models.Settings, error) {
184184
actor, err := contextutil.GetActor(ctx)
185185
if err != nil {
186-
return ctx, nil, err
186+
return ctx, nil, nil, err
187187
}
188188

189189
project, err := s.projectService.GetProject(ctx, actor.ID, projectId)
190190
if err != nil && err != mongo.ErrNoDocuments {
191-
return ctx, nil, err
191+
return ctx, nil, nil, err
192192
}
193193

194194
userInstructions, err := s.userService.GetUserInstructions(ctx, actor.ID)
195195
if err != nil {
196-
return ctx, nil, err
196+
return ctx, nil, nil, err
197197
}
198198

199199
var latexFullSource string
@@ -202,12 +202,12 @@ func (s *ChatServer) prepare(ctx context.Context, projectId string, conversation
202202
latexFullSource = "latex_full_source is not available in debug mode"
203203
default:
204204
if project == nil || project.IsOutOfDate() {
205-
return ctx, nil, shared.ErrProjectOutOfDate("project is out of date")
205+
return ctx, nil, nil, shared.ErrProjectOutOfDate("project is out of date")
206206
}
207207

208208
latexFullSource, err = project.GetFullContent()
209209
if err != nil {
210-
return ctx, nil, err
210+
return ctx, nil, nil, err
211211
}
212212
}
213213

@@ -238,34 +238,44 @@ func (s *ChatServer) prepare(ctx context.Context, projectId string, conversation
238238
}
239239

240240
if err != nil {
241-
return ctx, nil, err
241+
return ctx, nil, nil, err
242242
}
243243

244244
ctx = contextutil.SetProjectID(ctx, conversation.ProjectID)
245245
ctx = contextutil.SetConversationID(ctx, conversation.ID.Hex())
246246

247-
return ctx, conversation, nil
247+
settings, err := s.userService.GetUserSettings(ctx, actor.ID)
248+
if err != nil {
249+
return ctx, conversation, nil, err
250+
}
251+
252+
return ctx, conversation, settings, nil
248253
}
249254

250255
// Deprecated: Use CreateConversationMessageStream instead.
251256
func (s *ChatServer) CreateConversationMessage(
252257
ctx context.Context,
253258
req *chatv1.CreateConversationMessageRequest,
254259
) (*chatv1.CreateConversationMessageResponse, error) {
255-
ctx, conversation, err := s.prepare(
260+
languageModel := models.LanguageModel(req.GetLanguageModel())
261+
ctx, conversation, settings, err := s.prepare(
256262
ctx,
257263
req.GetProjectId(),
258264
req.GetConversationId(),
259265
req.GetUserMessage(),
260266
req.GetUserSelectedText(),
261-
models.LanguageModel(req.GetLanguageModel()),
267+
languageModel,
262268
req.GetConversationType(),
263269
)
264270
if err != nil {
265271
return nil, err
266272
}
267273

268-
openaiChatHistory, inappChatHistory, err := s.aiClient.ChatCompletion(ctx, conversation.LanguageModel, conversation.OpenaiChatHistory)
274+
llmProvider := &models.LLMProviderConfig{
275+
Endpoint: s.cfg.OpenAIBaseURL,
276+
APIKey: settings.OpenAIAPIKey,
277+
}
278+
openaiChatHistory, inappChatHistory, err := s.aiClient.ChatCompletion(ctx, languageModel, conversation.OpenaiChatHistory, llmProvider)
269279
if err != nil {
270280
return nil, err
271281
}
@@ -290,7 +300,7 @@ func (s *ChatServer) CreateConversationMessage(
290300
for i, bsonMsg := range conversation.InappChatHistory {
291301
protoMessages[i] = mapper.BSONToChatMessage(bsonMsg)
292302
}
293-
title, err := s.aiClient.GetConversationTitle(ctx, protoMessages)
303+
title, err := s.aiClient.GetConversationTitle(ctx, protoMessages, llmProvider)
294304
if err != nil {
295305
s.logger.Error("Failed to get conversation title", "error", err, "conversationID", conversation.ID.Hex())
296306
return

internal/api/chat/create_conversation_message_stream.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,28 @@ func (s *ChatServer) CreateConversationMessageStream(
2424
stream chatv1.ChatService_CreateConversationMessageStreamServer,
2525
) error {
2626
ctx := stream.Context()
27-
ctx, conversation, err := s.prepare(
27+
28+
languageModel := models.LanguageModel(req.GetLanguageModel())
29+
ctx, conversation, settings, err := s.prepare(
2830
ctx,
2931
req.GetProjectId(),
3032
req.GetConversationId(),
3133
req.GetUserMessage(),
3234
req.GetUserSelectedText(),
33-
models.LanguageModel(req.GetLanguageModel()),
35+
languageModel,
3436
req.GetConversationType(),
3537
)
3638
if err != nil {
3739
return s.sendStreamError(stream, err)
3840
}
3941

4042
// 用法跟 ChatCompletion 一样,只是传递了 stream 参数
41-
openaiChatHistory, inappChatHistory, err := s.aiClient.ChatCompletionStream(ctx, stream, conversation.ID.Hex(), conversation.LanguageModel, conversation.OpenaiChatHistory)
43+
llmProvider := &models.LLMProviderConfig{
44+
Endpoint: s.cfg.OpenAIBaseURL,
45+
APIKey: settings.OpenAIAPIKey,
46+
}
47+
48+
openaiChatHistory, inappChatHistory, err := s.aiClient.ChatCompletionStream(ctx, stream, conversation.ID.Hex(), languageModel, conversation.OpenaiChatHistory, llmProvider)
4249
if err != nil {
4350
return s.sendStreamError(stream, err)
4451
}
@@ -64,7 +71,7 @@ func (s *ChatServer) CreateConversationMessageStream(
6471
for i, bsonMsg := range conversation.InappChatHistory {
6572
protoMessages[i] = mapper.BSONToChatMessage(bsonMsg)
6673
}
67-
title, err := s.aiClient.GetConversationTitle(ctx, protoMessages)
74+
title, err := s.aiClient.GetConversationTitle(ctx, protoMessages, llmProvider)
6875
if err != nil {
6976
s.logger.Error("Failed to get conversation title", "error", err, "conversationID", conversation.ID.Hex())
7077
return
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package chat
2+
3+
import (
4+
"context"
5+
"strings"
6+
7+
"paperdebugger/internal/libs/contextutil"
8+
chatv1 "paperdebugger/pkg/gen/api/chat/v1"
9+
10+
"github.com/openai/openai-go/v2"
11+
)
12+
13+
func (s *ChatServer) ListSupportedModels(
14+
ctx context.Context,
15+
req *chatv1.ListSupportedModelsRequest,
16+
) (*chatv1.ListSupportedModelsResponse, error) {
17+
actor, err := contextutil.GetActor(ctx)
18+
if err != nil {
19+
return nil, err
20+
}
21+
22+
settings, err := s.userService.GetUserSettings(ctx, actor.ID)
23+
if err != nil {
24+
return nil, err
25+
}
26+
27+
var models []*chatv1.SupportedModel
28+
if strings.TrimSpace(settings.OpenAIAPIKey) == "" {
29+
models = []*chatv1.SupportedModel{
30+
{
31+
32+
Name: "GPT-4o",
33+
Slug: openai.ChatModelGPT4o,
34+
},
35+
{
36+
Name: "GPT-4.1",
37+
Slug: openai.ChatModelGPT4_1,
38+
},
39+
{
40+
Name: "GPT-4.1-mini",
41+
Slug: openai.ChatModelGPT4_1Mini,
42+
},
43+
}
44+
} else {
45+
models = []*chatv1.SupportedModel{
46+
{
47+
Name: "GPT 4o",
48+
Slug: openai.ChatModelGPT4o,
49+
},
50+
{
51+
Name: "GPT 4.1",
52+
Slug: openai.ChatModelGPT4_1,
53+
},
54+
{
55+
Name: "GPT 4.1 mini",
56+
Slug: openai.ChatModelGPT4_1Mini,
57+
},
58+
{
59+
Name: "GPT 5",
60+
Slug: openai.ChatModelGPT5,
61+
},
62+
{
63+
Name: "GPT 5 mini",
64+
Slug: openai.ChatModelGPT5Mini,
65+
},
66+
{
67+
Name: "GPT 5 nano",
68+
Slug: openai.ChatModelGPT5Nano,
69+
},
70+
{
71+
Name: "GPT 5 Chat Latest",
72+
Slug: openai.ChatModelGPT5ChatLatest,
73+
},
74+
{
75+
Name: "o1",
76+
Slug: openai.ChatModelO1,
77+
},
78+
{
79+
Name: "o1 mini",
80+
Slug: openai.ChatModelO1Mini,
81+
},
82+
{
83+
Name: "o3",
84+
Slug: openai.ChatModelO3,
85+
},
86+
{
87+
Name: "o3 mini",
88+
Slug: openai.ChatModelO3Mini,
89+
},
90+
{
91+
Name: "o4 mini",
92+
Slug: openai.ChatModelO4Mini,
93+
},
94+
{
95+
Name: "Codex Mini Latest",
96+
Slug: openai.ChatModelCodexMiniLatest,
97+
},
98+
}
99+
}
100+
101+
return &chatv1.ListSupportedModelsResponse{
102+
Models: models,
103+
}, nil
104+
}

internal/api/mapper/user.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ func MapProtoSettingsToModel(settings *userv1.Settings) *models.Settings {
1212
EnableCompletion: settings.EnableCompletion,
1313
FullDocumentRag: settings.FullDocumentRag,
1414
ShowedOnboarding: settings.ShowedOnboarding,
15+
OpenAIAPIKey: settings.OpenaiApiKey,
1516
}
1617
}
1718

@@ -22,5 +23,6 @@ func MapModelSettingsToProto(settings *models.Settings) *userv1.Settings {
2223
EnableCompletion: settings.EnableCompletion,
2324
FullDocumentRag: settings.FullDocumentRag,
2425
ShowedOnboarding: settings.ShowedOnboarding,
26+
OpenaiApiKey: settings.OpenAIAPIKey,
2527
}
2628
}

internal/models/language_model.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,20 @@ func (x LanguageModel) Name() string {
3838
return openai.ChatModelGPT5Mini
3939
case chatv1.LanguageModel_LANGUAGE_MODEL_OPENAI_GPT5_NANO:
4040
return openai.ChatModelGPT5Nano
41+
case chatv1.LanguageModel_LANGUAGE_MODEL_OPENAI_GPT5_CHAT_LATEST:
42+
return openai.ChatModelGPT5ChatLatest
43+
case chatv1.LanguageModel_LANGUAGE_MODEL_OPENAI_O1:
44+
return openai.ChatModelO1
45+
case chatv1.LanguageModel_LANGUAGE_MODEL_OPENAI_O1_MINI:
46+
return openai.ChatModelO1Mini
47+
case chatv1.LanguageModel_LANGUAGE_MODEL_OPENAI_O3:
48+
return openai.ChatModelO3
49+
case chatv1.LanguageModel_LANGUAGE_MODEL_OPENAI_O3_MINI:
50+
return openai.ChatModelO3Mini
51+
case chatv1.LanguageModel_LANGUAGE_MODEL_OPENAI_O4_MINI:
52+
return openai.ChatModelO4Mini
53+
case chatv1.LanguageModel_LANGUAGE_MODEL_OPENAI_CODEX_MINI_LATEST:
54+
return openai.ChatModelCodexMiniLatest
4155
default:
4256
return openai.ChatModelGPT5
4357
}

internal/models/llm_provider.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package models
2+
3+
// LLMProviderConfig holds the configuration for LLM API calls.
4+
// If both Endpoint and APIKey are empty, the system default will be used.
5+
type LLMProviderConfig struct {
6+
Endpoint string
7+
APIKey string
8+
ModelName string
9+
}
10+
11+
// IsCustom returns true if the user has configured custom LLM provider settings.
12+
func (c *LLMProviderConfig) IsCustom() bool {
13+
return c != nil && c.APIKey != ""
14+
}

internal/models/user.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ package models
33
import "go.mongodb.org/mongo-driver/v2/bson"
44

55
type Settings struct {
6-
ShowShortcutsAfterSelection bool `bson:"show_shortcuts_after_selection"`
7-
FullWidthPaperDebuggerButton bool `bson:"full_width_paper_debugger_button"`
8-
EnableCompletion bool `bson:"enable_completion"`
9-
FullDocumentRag bool `bson:"full_document_rag"`
10-
ShowedOnboarding bool `bson:"showed_onboarding"`
6+
ShowShortcutsAfterSelection bool `bson:"show_shortcuts_after_selection"`
7+
FullWidthPaperDebuggerButton bool `bson:"full_width_paper_debugger_button"`
8+
EnableCompletion bool `bson:"enable_completion"`
9+
FullDocumentRag bool `bson:"full_document_rag"`
10+
ShowedOnboarding bool `bson:"showed_onboarding"`
11+
OpenAIAPIKey string `bson:"openai_api_key"`
1112
}
1213

1314
type User struct {

0 commit comments

Comments
 (0)