Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 32 additions & 12 deletions internal/api/chat/list_supported_models.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,22 @@ func (s *ChatServerV1) ListSupportedModels(
var models []*chatv1.SupportedModel
if strings.TrimSpace(settings.OpenAIAPIKey) == "" {
models = []*chatv1.SupportedModel{
{
Name: "GPT-5.4",
Slug: "openai/gpt-5.4",
},
{
Name: "GPT-5.4 Mini",
Slug: "openai/gpt-5.4-mini",
},
{
Name: "GPT-5.4 Nano",
Slug: "openai/gpt-5.4-nano",
},
{
Name: "Claude Opus 4.6",
Slug: "anthropic/claude-opus-4.6",
},
{

Name: "GPT-4o",
Expand All @@ -44,28 +60,32 @@ func (s *ChatServerV1) ListSupportedModels(
} else {
models = []*chatv1.SupportedModel{
{
Name: "GPT 4o",
Slug: openai.ChatModelGPT4o,
Name: "GPT-5.4",
Slug: "gpt-5.4",
},
{
Name: "GPT 4.1",
Slug: openai.ChatModelGPT4_1,
Name: "GPT-5.4 Mini",
Slug: "gpt-5.4-mini",
},
{
Name: "GPT 4.1 mini",
Slug: openai.ChatModelGPT4_1Mini,
Name: "GPT-5.4 Nano",
Slug: "gpt-5.4-nano",
},
{
Name: "GPT 5",
Slug: openai.ChatModelGPT5,
Name: "Claude Opus 4.6",
Slug: "anthropic/claude-opus-4.6",
},
{
Name: "GPT 5 mini",
Slug: openai.ChatModelGPT5Mini,
Name: "GPT 4o",
Slug: openai.ChatModelGPT4o,
},
{
Name: "GPT 5 nano",
Slug: openai.ChatModelGPT5Nano,
Name: "GPT 4.1",
Slug: openai.ChatModelGPT4_1,
},
{
Name: "GPT 4.1 mini",
Slug: openai.ChatModelGPT4_1Mini,
},
{
Name: "GPT 5 Chat Latest",
Expand Down
46 changes: 23 additions & 23 deletions internal/api/chat/list_supported_models_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,43 +25,43 @@ type modelConfig struct {
// allModels defines all available models in the system
var allModels = []modelConfig{
{
name: "GPT-5.1",
slugOpenRouter: "openai/gpt-5.1",
slugOpenAI: openai.ChatModelGPT5_1,
totalContext: 400000,
name: "GPT-5.4",
slugOpenRouter: "openai/gpt-5.4",
slugOpenAI: "gpt-5.4",
totalContext: 1050000,
maxOutput: 128000,
inputPrice: 125, // $1.25
outputPrice: 1000, // $10.00
inputPrice: 250, // $2.50
outputPrice: 1500, // $15.00
requireOwnKey: false,
},
{
name: "GPT-5.2",
slugOpenRouter: "openai/gpt-5.2",
slugOpenAI: openai.ChatModelGPT5_2,
name: "GPT-5.4 Mini",
slugOpenRouter: "openai/gpt-5.4-mini",
slugOpenAI: "gpt-5.4-mini",
totalContext: 400000,
maxOutput: 128000,
inputPrice: 175, // $1.75
outputPrice: 1400, // $14.00
requireOwnKey: true,
inputPrice: 75, // $0.75
outputPrice: 450, // $4.50
requireOwnKey: false,
},
{
name: "GPT-5 Mini",
slugOpenRouter: "openai/gpt-5-mini",
slugOpenAI: openai.ChatModelGPT5Mini,
name: "GPT-5.4 Nano",
slugOpenRouter: "openai/gpt-5.4-nano",
slugOpenAI: "gpt-5.4-nano",
totalContext: 400000,
maxOutput: 128000,
inputPrice: 25,
outputPrice: 200,
inputPrice: 20,
outputPrice: 125,
requireOwnKey: false,
},
{
name: "GPT-5 Nano",
slugOpenRouter: "openai/gpt-5-nano",
slugOpenAI: openai.ChatModelGPT5Nano,
totalContext: 400000,
name: "Claude Opus 4.6",
slugOpenRouter: "anthropic/claude-opus-4.6",
slugOpenAI: "",
totalContext: 1000000,
maxOutput: 128000,
inputPrice: 5, // $0.20
outputPrice: 40, // $0.80
inputPrice: 500, // $5.00
outputPrice: 2500, // $25.00
requireOwnKey: false,
},
{
Expand Down
230 changes: 230 additions & 0 deletions internal/api/project/review.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
package project

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"

"paperdebugger/internal/libs/contextutil"
"paperdebugger/internal/libs/shared"
"paperdebugger/internal/models"
projectv1 "paperdebugger/pkg/gen/api/project/v1"
)

type paperScoreRequest struct {
LatexSource string `json:"latexSource"`
Category string `json:"category"`
}

type paperScoreCommentRequest struct {
LatexSource string `json:"latexSource"`
PaperScoreResult *projectv1.PaperScoreResult `json:"paperScoreResult"`
}

func (s *ProjectServer) RunProjectPaperScore(
ctx context.Context,
req *projectv1.RunProjectPaperScoreRequest,
) (*projectv1.RunProjectPaperScoreResponse, error) {
if req.GetProjectId() == "" {
return nil, shared.ErrBadRequest("project_id is required")
}

ctx, fullContent, category, err := s.loadReviewInput(ctx, req.GetProjectId(), req.GetConversationId())
if err != nil {
return nil, err
}

result, err := s.scorePaper(ctx, fullContent, category)
if err != nil {
return nil, shared.ErrInternal(err)
}

return &projectv1.RunProjectPaperScoreResponse{
ProjectId: req.GetProjectId(),
PaperScore: result,
}, nil
}

func (s *ProjectServer) RunProjectPaperScoreComment(
ctx context.Context,
req *projectv1.RunProjectPaperScoreCommentRequest,
) (*projectv1.RunProjectPaperScoreCommentResponse, error) {
if req.GetProjectId() == "" {
return nil, shared.ErrBadRequest("project_id is required")
}

ctx, fullContent, category, err := s.loadReviewInput(ctx, req.GetProjectId(), req.GetConversationId())
if err != nil {
return nil, err
}

scoreResult, err := s.scorePaper(ctx, fullContent, category)
if err != nil {
return nil, shared.ErrInternal(err)
}

commentResult, err := s.generatePaperScoreComments(ctx, fullContent, scoreResult)
if err != nil {
return nil, shared.ErrInternal(err)
}

return &projectv1.RunProjectPaperScoreCommentResponse{
ProjectId: req.GetProjectId(),
Comments: []*projectv1.PaperScoreCommentResult{commentResult},
}, nil
}

func (s *ProjectServer) RunProjectOverleafComment(
ctx context.Context,
req *projectv1.RunProjectOverleafCommentRequest,
) (*projectv1.RunProjectOverleafCommentResponse, error) {
if req.GetProjectId() == "" {
return nil, shared.ErrBadRequest("project_id is required")
}
if strings.TrimSpace(req.GetComment()) == "" {
return nil, shared.ErrBadRequest("comment is required")
}

ctx, _, err := s.loadProject(ctx, req.GetProjectId())
if err != nil {
return nil, err
}

commentResult := &projectv1.PaperScoreCommentResult{
Results: []*projectv1.PaperScoreCommentEntry{
{
Section: req.GetSection(),
AnchorText: req.GetAnchorText(),
Weakness: req.GetComment(),
Importance: req.GetImportance(),
},
},
}

comments, err := s.reverseCommentService.ReverseComments(ctx, commentResult)
if err != nil {
return nil, shared.ErrInternal(err)
}
if len(comments) == 0 {
section := strings.TrimSpace(req.GetSection())
if section == "" {
section = "the requested location"
}
return nil, shared.ErrBadRequest(fmt.Sprintf("unable to locate %s in the project for comment insertion", section))
}

return &projectv1.RunProjectOverleafCommentResponse{
ProjectId: req.GetProjectId(),
Comments: comments,
}, nil
}

func (s *ProjectServer) loadProject(ctx context.Context, projectID string) (context.Context, *models.Project, error) {
actor, err := contextutil.GetActor(ctx)
if err != nil {
return ctx, nil, err
}

ctx = contextutil.SetProjectID(ctx, projectID)

project, err := s.projectService.GetProject(ctx, actor.ID, projectID)
if err != nil {
return ctx, nil, err
}

return ctx, project, nil
}

func (s *ProjectServer) loadReviewInput(ctx context.Context, projectID string, conversationID string) (context.Context, string, string, error) {
ctx, project, err := s.loadProject(ctx, projectID)
if err != nil {
return ctx, "", "", err
}

if conversationID != "" {
ctx = contextutil.SetConversationID(ctx, conversationID)
}

fullContent, err := project.GetFullContent()
if err != nil {
return ctx, "", "", shared.ErrInternal("failed to get paper full content")
}

actor, err := contextutil.GetActor(ctx)
if err != nil {
return ctx, "", "", err
}

projectCategory, err := s.projectService.GetProjectCategory(ctx, actor.ID, projectID)
if err != nil {
return ctx, "", "", shared.ErrInternal(err)
}

return ctx, fullContent, projectCategory.Category, nil
}

func (s *ProjectServer) scorePaper(ctx context.Context, fullContent string, category string) (*projectv1.PaperScoreResult, error) {
result := &projectv1.PaperScoreResult{}
err := s.postReviewJSON(ctx, "paper-score", &paperScoreRequest{
LatexSource: fullContent,
Category: category,
}, result)
if err != nil {
return nil, err
}
return result, nil
}

func (s *ProjectServer) generatePaperScoreComments(
ctx context.Context,
fullContent string,
scoreResult *projectv1.PaperScoreResult,
) (*projectv1.PaperScoreCommentResult, error) {
result := &projectv1.PaperScoreCommentResult{}
err := s.postReviewJSON(ctx, "paper-score-comments", &paperScoreCommentRequest{
LatexSource: fullContent,
PaperScoreResult: scoreResult,
}, result)
if err != nil {
return nil, err
}
return result, nil
}

func (s *ProjectServer) postReviewJSON(ctx context.Context, path string, payload any, out any) error {
body, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("failed to marshal review request: %w", err)
}

url := strings.TrimRight(s.cfg.MCPServerURL, "/") + "/" + strings.TrimLeft(path, "/")
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(body))
if err != nil {
return fmt.Errorf("failed to create review request: %w", err)
}
req.Header.Set("Content-Type", "application/json")

resp, err := (&http.Client{}).Do(req)
if err != nil {
return fmt.Errorf("failed to send review request: %w", err)
}
defer resp.Body.Close()

respBody, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read review response: %w", err)
}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
return fmt.Errorf("review service returned status %d: %s", resp.StatusCode, strings.TrimSpace(string(respBody)))
}

if err := json.Unmarshal(respBody, out); err != nil {
return fmt.Errorf("failed to decode review response: %w", err)
}

return nil
}
15 changes: 9 additions & 6 deletions internal/api/project/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,22 @@ import (

type ProjectServer struct {
projectv1.UnimplementedProjectServiceServer
projectService *services.ProjectService
logger *logger.Logger
cfg *cfg.Cfg
projectService *services.ProjectService
reverseCommentService *services.ReverseCommentService
logger *logger.Logger
cfg *cfg.Cfg
}

func NewProjectServer(
projectService *services.ProjectService,
reverseCommentService *services.ReverseCommentService,
logger *logger.Logger,
cfg *cfg.Cfg,
) projectv1.ProjectServiceServer {
return &ProjectServer{
projectService: projectService,
logger: logger,
cfg: cfg,
projectService: projectService,
reverseCommentService: reverseCommentService,
logger: logger,
cfg: cfg,
}
}
Loading
Loading