Skip to content

Commit b55bc92

Browse files
committed
add overleaf review insertion and model updates
1 parent 9f93c02 commit b55bc92

38 files changed

Lines changed: 2575 additions & 239 deletions

internal/api/chat/list_supported_models.go

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,22 @@ func (s *ChatServerV1) ListSupportedModels(
2727
var models []*chatv1.SupportedModel
2828
if strings.TrimSpace(settings.OpenAIAPIKey) == "" {
2929
models = []*chatv1.SupportedModel{
30+
{
31+
Name: "GPT-5.4",
32+
Slug: "openai/gpt-5.4",
33+
},
34+
{
35+
Name: "GPT-5.4 Mini",
36+
Slug: "openai/gpt-5.4-mini",
37+
},
38+
{
39+
Name: "GPT-5.4 Nano",
40+
Slug: "openai/gpt-5.4-nano",
41+
},
42+
{
43+
Name: "Claude Opus 4.6",
44+
Slug: "anthropic/claude-opus-4.6",
45+
},
3046
{
3147

3248
Name: "GPT-4o",
@@ -44,28 +60,32 @@ func (s *ChatServerV1) ListSupportedModels(
4460
} else {
4561
models = []*chatv1.SupportedModel{
4662
{
47-
Name: "GPT 4o",
48-
Slug: openai.ChatModelGPT4o,
63+
Name: "GPT-5.4",
64+
Slug: "gpt-5.4",
4965
},
5066
{
51-
Name: "GPT 4.1",
52-
Slug: openai.ChatModelGPT4_1,
67+
Name: "GPT-5.4 Mini",
68+
Slug: "gpt-5.4-mini",
5369
},
5470
{
55-
Name: "GPT 4.1 mini",
56-
Slug: openai.ChatModelGPT4_1Mini,
71+
Name: "GPT-5.4 Nano",
72+
Slug: "gpt-5.4-nano",
5773
},
5874
{
59-
Name: "GPT 5",
60-
Slug: openai.ChatModelGPT5,
75+
Name: "Claude Opus 4.6",
76+
Slug: "anthropic/claude-opus-4.6",
6177
},
6278
{
63-
Name: "GPT 5 mini",
64-
Slug: openai.ChatModelGPT5Mini,
79+
Name: "GPT 4o",
80+
Slug: openai.ChatModelGPT4o,
6581
},
6682
{
67-
Name: "GPT 5 nano",
68-
Slug: openai.ChatModelGPT5Nano,
83+
Name: "GPT 4.1",
84+
Slug: openai.ChatModelGPT4_1,
85+
},
86+
{
87+
Name: "GPT 4.1 mini",
88+
Slug: openai.ChatModelGPT4_1Mini,
6989
},
7090
{
7191
Name: "GPT 5 Chat Latest",

internal/api/chat/list_supported_models_v2.go

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,43 +25,43 @@ type modelConfig struct {
2525
// allModels defines all available models in the system
2626
var allModels = []modelConfig{
2727
{
28-
name: "GPT-5.1",
29-
slugOpenRouter: "openai/gpt-5.1",
30-
slugOpenAI: openai.ChatModelGPT5_1,
31-
totalContext: 400000,
28+
name: "GPT-5.4",
29+
slugOpenRouter: "openai/gpt-5.4",
30+
slugOpenAI: "gpt-5.4",
31+
totalContext: 1050000,
3232
maxOutput: 128000,
33-
inputPrice: 125, // $1.25
34-
outputPrice: 1000, // $10.00
33+
inputPrice: 250, // $2.50
34+
outputPrice: 1500, // $15.00
3535
requireOwnKey: false,
3636
},
3737
{
38-
name: "GPT-5.2",
39-
slugOpenRouter: "openai/gpt-5.2",
40-
slugOpenAI: openai.ChatModelGPT5_2,
38+
name: "GPT-5.4 Mini",
39+
slugOpenRouter: "openai/gpt-5.4-mini",
40+
slugOpenAI: "gpt-5.4-mini",
4141
totalContext: 400000,
4242
maxOutput: 128000,
43-
inputPrice: 175, // $1.75
44-
outputPrice: 1400, // $14.00
45-
requireOwnKey: true,
43+
inputPrice: 75, // $0.75
44+
outputPrice: 450, // $4.50
45+
requireOwnKey: false,
4646
},
4747
{
48-
name: "GPT-5 Mini",
49-
slugOpenRouter: "openai/gpt-5-mini",
50-
slugOpenAI: openai.ChatModelGPT5Mini,
48+
name: "GPT-5.4 Nano",
49+
slugOpenRouter: "openai/gpt-5.4-nano",
50+
slugOpenAI: "gpt-5.4-nano",
5151
totalContext: 400000,
5252
maxOutput: 128000,
53-
inputPrice: 25,
54-
outputPrice: 200,
53+
inputPrice: 20,
54+
outputPrice: 125,
5555
requireOwnKey: false,
5656
},
5757
{
58-
name: "GPT-5 Nano",
59-
slugOpenRouter: "openai/gpt-5-nano",
60-
slugOpenAI: openai.ChatModelGPT5Nano,
61-
totalContext: 400000,
58+
name: "Claude Opus 4.6",
59+
slugOpenRouter: "anthropic/claude-opus-4.6",
60+
slugOpenAI: "",
61+
totalContext: 1000000,
6262
maxOutput: 128000,
63-
inputPrice: 5, // $0.20
64-
outputPrice: 40, // $0.80
63+
inputPrice: 500, // $5.00
64+
outputPrice: 2500, // $25.00
6565
requireOwnKey: false,
6666
},
6767
{

internal/api/project/review.go

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
package project
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"net/http"
10+
"strings"
11+
12+
"paperdebugger/internal/libs/contextutil"
13+
"paperdebugger/internal/libs/shared"
14+
"paperdebugger/internal/models"
15+
projectv1 "paperdebugger/pkg/gen/api/project/v1"
16+
)
17+
18+
type paperScoreRequest struct {
19+
LatexSource string `json:"latexSource"`
20+
Category string `json:"category"`
21+
}
22+
23+
type paperScoreCommentRequest struct {
24+
LatexSource string `json:"latexSource"`
25+
PaperScoreResult *projectv1.PaperScoreResult `json:"paperScoreResult"`
26+
}
27+
28+
func (s *ProjectServer) RunProjectPaperScore(
29+
ctx context.Context,
30+
req *projectv1.RunProjectPaperScoreRequest,
31+
) (*projectv1.RunProjectPaperScoreResponse, error) {
32+
if req.GetProjectId() == "" {
33+
return nil, shared.ErrBadRequest("project_id is required")
34+
}
35+
36+
ctx, fullContent, category, err := s.loadReviewInput(ctx, req.GetProjectId(), req.GetConversationId())
37+
if err != nil {
38+
return nil, err
39+
}
40+
41+
result, err := s.scorePaper(ctx, fullContent, category)
42+
if err != nil {
43+
return nil, shared.ErrInternal(err)
44+
}
45+
46+
return &projectv1.RunProjectPaperScoreResponse{
47+
ProjectId: req.GetProjectId(),
48+
PaperScore: result,
49+
}, nil
50+
}
51+
52+
func (s *ProjectServer) RunProjectPaperScoreComment(
53+
ctx context.Context,
54+
req *projectv1.RunProjectPaperScoreCommentRequest,
55+
) (*projectv1.RunProjectPaperScoreCommentResponse, error) {
56+
if req.GetProjectId() == "" {
57+
return nil, shared.ErrBadRequest("project_id is required")
58+
}
59+
60+
ctx, fullContent, category, err := s.loadReviewInput(ctx, req.GetProjectId(), req.GetConversationId())
61+
if err != nil {
62+
return nil, err
63+
}
64+
65+
scoreResult, err := s.scorePaper(ctx, fullContent, category)
66+
if err != nil {
67+
return nil, shared.ErrInternal(err)
68+
}
69+
70+
commentResult, err := s.generatePaperScoreComments(ctx, fullContent, scoreResult)
71+
if err != nil {
72+
return nil, shared.ErrInternal(err)
73+
}
74+
75+
return &projectv1.RunProjectPaperScoreCommentResponse{
76+
ProjectId: req.GetProjectId(),
77+
Comments: []*projectv1.PaperScoreCommentResult{commentResult},
78+
}, nil
79+
}
80+
81+
func (s *ProjectServer) RunProjectOverleafComment(
82+
ctx context.Context,
83+
req *projectv1.RunProjectOverleafCommentRequest,
84+
) (*projectv1.RunProjectOverleafCommentResponse, error) {
85+
if req.GetProjectId() == "" {
86+
return nil, shared.ErrBadRequest("project_id is required")
87+
}
88+
if strings.TrimSpace(req.GetComment()) == "" {
89+
return nil, shared.ErrBadRequest("comment is required")
90+
}
91+
92+
ctx, _, err := s.loadProject(ctx, req.GetProjectId())
93+
if err != nil {
94+
return nil, err
95+
}
96+
97+
commentResult := &projectv1.PaperScoreCommentResult{
98+
Results: []*projectv1.PaperScoreCommentEntry{
99+
{
100+
Section: req.GetSection(),
101+
AnchorText: req.GetAnchorText(),
102+
Weakness: req.GetComment(),
103+
Importance: req.GetImportance(),
104+
},
105+
},
106+
}
107+
108+
comments, err := s.reverseCommentService.ReverseComments(ctx, commentResult)
109+
if err != nil {
110+
return nil, shared.ErrInternal(err)
111+
}
112+
if len(comments) == 0 {
113+
section := strings.TrimSpace(req.GetSection())
114+
if section == "" {
115+
section = "the requested location"
116+
}
117+
return nil, shared.ErrBadRequest(fmt.Sprintf("unable to locate %s in the project for comment insertion", section))
118+
}
119+
120+
return &projectv1.RunProjectOverleafCommentResponse{
121+
ProjectId: req.GetProjectId(),
122+
Comments: comments,
123+
}, nil
124+
}
125+
126+
func (s *ProjectServer) loadProject(ctx context.Context, projectID string) (context.Context, *models.Project, error) {
127+
actor, err := contextutil.GetActor(ctx)
128+
if err != nil {
129+
return ctx, nil, err
130+
}
131+
132+
ctx = contextutil.SetProjectID(ctx, projectID)
133+
134+
project, err := s.projectService.GetProject(ctx, actor.ID, projectID)
135+
if err != nil {
136+
return ctx, nil, err
137+
}
138+
139+
return ctx, project, nil
140+
}
141+
142+
func (s *ProjectServer) loadReviewInput(ctx context.Context, projectID string, conversationID string) (context.Context, string, string, error) {
143+
ctx, project, err := s.loadProject(ctx, projectID)
144+
if err != nil {
145+
return ctx, "", "", err
146+
}
147+
148+
if conversationID != "" {
149+
ctx = contextutil.SetConversationID(ctx, conversationID)
150+
}
151+
152+
fullContent, err := project.GetFullContent()
153+
if err != nil {
154+
return ctx, "", "", shared.ErrInternal("failed to get paper full content")
155+
}
156+
157+
actor, err := contextutil.GetActor(ctx)
158+
if err != nil {
159+
return ctx, "", "", err
160+
}
161+
162+
projectCategory, err := s.projectService.GetProjectCategory(ctx, actor.ID, projectID)
163+
if err != nil {
164+
return ctx, "", "", shared.ErrInternal(err)
165+
}
166+
167+
return ctx, fullContent, projectCategory.Category, nil
168+
}
169+
170+
func (s *ProjectServer) scorePaper(ctx context.Context, fullContent string, category string) (*projectv1.PaperScoreResult, error) {
171+
result := &projectv1.PaperScoreResult{}
172+
err := s.postReviewJSON(ctx, "paper-score", &paperScoreRequest{
173+
LatexSource: fullContent,
174+
Category: category,
175+
}, result)
176+
if err != nil {
177+
return nil, err
178+
}
179+
return result, nil
180+
}
181+
182+
func (s *ProjectServer) generatePaperScoreComments(
183+
ctx context.Context,
184+
fullContent string,
185+
scoreResult *projectv1.PaperScoreResult,
186+
) (*projectv1.PaperScoreCommentResult, error) {
187+
result := &projectv1.PaperScoreCommentResult{}
188+
err := s.postReviewJSON(ctx, "paper-score-comments", &paperScoreCommentRequest{
189+
LatexSource: fullContent,
190+
PaperScoreResult: scoreResult,
191+
}, result)
192+
if err != nil {
193+
return nil, err
194+
}
195+
return result, nil
196+
}
197+
198+
func (s *ProjectServer) postReviewJSON(ctx context.Context, path string, payload any, out any) error {
199+
body, err := json.Marshal(payload)
200+
if err != nil {
201+
return fmt.Errorf("failed to marshal review request: %w", err)
202+
}
203+
204+
url := strings.TrimRight(s.cfg.MCPServerURL, "/") + "/" + strings.TrimLeft(path, "/")
205+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(body))
206+
if err != nil {
207+
return fmt.Errorf("failed to create review request: %w", err)
208+
}
209+
req.Header.Set("Content-Type", "application/json")
210+
211+
resp, err := (&http.Client{}).Do(req)
212+
if err != nil {
213+
return fmt.Errorf("failed to send review request: %w", err)
214+
}
215+
defer resp.Body.Close()
216+
217+
respBody, err := io.ReadAll(resp.Body)
218+
if err != nil {
219+
return fmt.Errorf("failed to read review response: %w", err)
220+
}
221+
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
222+
return fmt.Errorf("review service returned status %d: %s", resp.StatusCode, strings.TrimSpace(string(respBody)))
223+
}
224+
225+
if err := json.Unmarshal(respBody, out); err != nil {
226+
return fmt.Errorf("failed to decode review response: %w", err)
227+
}
228+
229+
return nil
230+
}

internal/api/project/server.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,22 @@ import (
99

1010
type ProjectServer struct {
1111
projectv1.UnimplementedProjectServiceServer
12-
projectService *services.ProjectService
13-
logger *logger.Logger
14-
cfg *cfg.Cfg
12+
projectService *services.ProjectService
13+
reverseCommentService *services.ReverseCommentService
14+
logger *logger.Logger
15+
cfg *cfg.Cfg
1516
}
1617

1718
func NewProjectServer(
1819
projectService *services.ProjectService,
20+
reverseCommentService *services.ReverseCommentService,
1921
logger *logger.Logger,
2022
cfg *cfg.Cfg,
2123
) projectv1.ProjectServiceServer {
2224
return &ProjectServer{
23-
projectService: projectService,
24-
logger: logger,
25-
cfg: cfg,
25+
projectService: projectService,
26+
reverseCommentService: reverseCommentService,
27+
logger: logger,
28+
cfg: cfg,
2629
}
2730
}

0 commit comments

Comments
 (0)