From fcf346333818dd51418237148a9d75b5d9a64944 Mon Sep 17 00:00:00 2001
From: "tokamak-pm[bot]" <264983013+tokamak-pm[bot]@users.noreply.github.com>
Date: Tue, 7 Apr 2026 08:38:16 +0000
Subject: [PATCH 1/4] fix(llm-api): add warnings for insecure default connector
secrets
Authored-By: tokamak-pm[bot] <264983013+tokamak-pm[bot]@users.noreply.github.com>
---
.../llm-api/internal/infrastructure/connector/provider.go | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/services/llm-api/internal/infrastructure/connector/provider.go b/services/llm-api/internal/infrastructure/connector/provider.go
index 0bbc4ba1..fe5a6f04 100644
--- a/services/llm-api/internal/infrastructure/connector/provider.go
+++ b/services/llm-api/internal/infrastructure/connector/provider.go
@@ -6,6 +6,7 @@ import (
"github.com/google/wire"
"github.com/rs/zerolog"
+ "github.com/rs/zerolog/log"
"gorm.io/gorm"
"jan-server/services/llm-api/internal/config"
@@ -23,7 +24,7 @@ var ProviderSet = wire.NewSet(
// ProvideTokenEncryptor creates a token encryptor from config.
func ProvideTokenEncryptor(cfg *config.Config) (*TokenEncryptor, error) {
if cfg.ConnectorTokenEncryptionKey == "" {
- // Generate a default key for development (not for production!)
+ log.Warn().Msg("ConnectorTokenEncryptionKey is not set, using insecure default key. Do NOT use this in production.")
cfg.ConnectorTokenEncryptionKey = "0000000000000000000000000000000000000000000000000000000000000000"
}
@@ -64,7 +65,7 @@ func ProvideConnectorService(
}
}
if len(stateHMAC) == 0 {
- // Use a default for development
+ logger.Warn().Msg("OAuthStateSecret is not set, using insecure default secret. Do NOT use this in production.")
stateHMAC = []byte("development-oauth-state-secret-key-32b")
}
From a9e577783466346fe41e7320b2e6f05487d2a10b Mon Sep 17 00:00:00 2001
From: "tokamak-pm[bot]" <264983013+tokamak-pm[bot]@users.noreply.github.com>
Date: Tue, 7 Apr 2026 08:38:34 +0000
Subject: [PATCH 2/4] fix(web): prevent open redirect bypass in
isAllowedExternalRedirect
Replace regex-based URL validation with proper URL parsing to prevent
userinfo-based open redirect bypasses (e.g., http://localhost:8080@evil.com).
Extract shared utility to apps/web/src/lib/redirect-utils.ts and add
missing accessToken null check in __root.tsx handleCloseModal.
Authored-By: tokamak-pm[bot] <264983013+tokamak-pm[bot]@users.noreply.github.com>
---
apps/web/src/components/form/login.tsx | 6 +-----
apps/web/src/lib/redirect-utils.ts | 12 ++++++++++++
apps/web/src/routes/__root.tsx | 7 ++-----
apps/web/src/routes/auth/callback.tsx | 4 +---
apps/web/src/routes/login.tsx | 6 +-----
5 files changed, 17 insertions(+), 18 deletions(-)
create mode 100644 apps/web/src/lib/redirect-utils.ts
diff --git a/apps/web/src/components/form/login.tsx b/apps/web/src/components/form/login.tsx
index e24a5482..3739217a 100644
--- a/apps/web/src/components/form/login.tsx
+++ b/apps/web/src/components/form/login.tsx
@@ -8,6 +8,7 @@ import {
import { Input } from "@janhq/interfaces/input";
import { Google } from "@janhq/interfaces/svgs/google";
import { buildGoogleAuthUrl } from "@/lib/oauth";
+import { isAllowedExternalRedirect } from "@/lib/redirect-utils";
import { useState } from "react";
import { useAuth } from "@/stores/auth-store";
import { useRouter } from "@tanstack/react-router";
@@ -30,11 +31,6 @@ export function LoginForm({
const { loginWithOAuth } = useAuth();
const router = useRouter();
- const isAllowedExternalRedirect = (value: string) => {
- // Allow localhost with any port for development
- return /^http:\/\/localhost:\d+/.test(value);
- };
-
const getRedirectUrl = () => {
const url = new URL(window.location.href);
const redirectParam = url.searchParams.get(URL_PARAM.REDIRECT);
diff --git a/apps/web/src/lib/redirect-utils.ts b/apps/web/src/lib/redirect-utils.ts
new file mode 100644
index 00000000..7c668d7a
--- /dev/null
+++ b/apps/web/src/lib/redirect-utils.ts
@@ -0,0 +1,12 @@
+/**
+ * Validates whether a URL is an allowed external redirect target.
+ * Uses proper URL parsing to prevent userinfo-based open redirect bypasses.
+ */
+export const isAllowedExternalRedirect = (value: string): boolean => {
+ try {
+ const url = new URL(value);
+ return url.protocol === 'http:' && url.hostname === 'localhost';
+ } catch {
+ return false;
+ }
+};
diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx
index 6b8ac2e5..8e25130a 100644
--- a/apps/web/src/routes/__root.tsx
+++ b/apps/web/src/routes/__root.tsx
@@ -12,6 +12,7 @@ import { SettingsDialog } from "@/components/settings/settings-dialog";
import { CreateProject } from "@/components/projects/create-project";
import { SearchDialog } from "@/components/search/search-dialog";
import { useAuth } from "@/stores/auth-store";
+import { isAllowedExternalRedirect } from "@/lib/redirect-utils";
import { useRightSidebarStore } from "@/stores/right-sidebar-store";
import { ThemeProvider } from "@/components/themes/theme-provider";
import { Toaster } from "@janhq/interfaces/sonner";
@@ -136,14 +137,10 @@ function RootLayout() {
const searchSection = searchParams.get(URL_PARAM.SEARCH);
const isSearchOpen = !!searchSection;
- const isAllowedExternalRedirect = (value: string) => {
- // Allow localhost with any port for development
- return /^http:\/\/localhost:\d+/.test(value);
- };
-
const handleCloseModal = (redirectUrl?: string) => {
// Case 1: External redirect (e.g., http://localhost:29999 from install script)
if (redirectUrl && isAllowedExternalRedirect(redirectUrl)) {
+ if (!accessToken) return;
const bearerToken = `Bearer ${accessToken}`;
const encodedToken = btoa(bearerToken);
const target = new URL(redirectUrl);
diff --git a/apps/web/src/routes/auth/callback.tsx b/apps/web/src/routes/auth/callback.tsx
index 06efa9ec..0c8687ff 100644
--- a/apps/web/src/routes/auth/callback.tsx
+++ b/apps/web/src/routes/auth/callback.tsx
@@ -2,6 +2,7 @@ import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useEffect, useRef, useState } from "react";
import { useAuth } from "@/stores/auth-store";
import { retrieveOAuthState, exchangeCodeForTokens } from "@/lib/oauth";
+import { isAllowedExternalRedirect } from "@/lib/redirect-utils";
export const Route = createFileRoute("/auth/callback")({
component: OAuthCallbackPage,
@@ -55,9 +56,6 @@ function OAuthCallbackPage() {
loginWithOAuth(tokens);
console.log("OAuth login successful");
// Navigate to the original URL or home
- const isAllowedExternalRedirect = (value: string) =>
- /^http:\/\/localhost:\d+/.test(value);
-
if (oauthData.redirectUrl && isAllowedExternalRedirect(oauthData.redirectUrl)) {
const bearerToken = `Bearer ${tokens.access_token}`;
const encodedToken = btoa(bearerToken);
diff --git a/apps/web/src/routes/login.tsx b/apps/web/src/routes/login.tsx
index cf0ad203..9c1782c6 100644
--- a/apps/web/src/routes/login.tsx
+++ b/apps/web/src/routes/login.tsx
@@ -1,6 +1,7 @@
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useEffect } from "react";
import { useAuth } from "@/stores/auth-store";
+import { isAllowedExternalRedirect } from "@/lib/redirect-utils";
import { URL_PARAM } from "@/constants";
export const Route = createFileRoute("/login" as "/")({
@@ -13,11 +14,6 @@ function LoginRoute() {
const isGuest = useAuth((state) => state.isGuest);
const accessToken = useAuth((state) => state.accessToken);
- const isAllowedExternalRedirect = (value: string) => {
- // Allow localhost with any port for development
- return /^http:\/\/localhost:\d+/.test(value);
- };
-
useEffect(() => {
if (!isAuthenticated || isGuest) {
return;
From 8aad6b9d96b84cf2595ebac52aa1815e2641362c Mon Sep 17 00:00:00 2001
From: "tokamak-pm[bot]" <264983013+tokamak-pm[bot]@users.noreply.github.com>
Date: Tue, 7 Apr 2026 08:42:45 +0000
Subject: [PATCH 3/4] docs: add recent commits review report for PRs #484-#486
Authored-By: tokamak-pm[bot] <264983013+tokamak-pm[bot]@users.noreply.github.com>
---
docs/reviews/recent-commits-review.md | 263 ++++++++++++++++++++++++++
1 file changed, 263 insertions(+)
create mode 100644 docs/reviews/recent-commits-review.md
diff --git a/docs/reviews/recent-commits-review.md b/docs/reviews/recent-commits-review.md
new file mode 100644
index 00000000..addea3a4
--- /dev/null
+++ b/docs/reviews/recent-commits-review.md
@@ -0,0 +1,263 @@
+# Recent Commits Review — 2026-04-07
+
+> Automated review of commits merged to main (PRs #484–#486).
+> Phase 1 fixes have already been applied for critical security issues.
+> This report documents remaining findings that require team discussion or larger refactors.
+
+---
+
+## Finding 1: API Key Access to Inactive Models Lacks Per-Model Authorization
+
+| Field | Value |
+|---|---|
+| **Severity** | CRITICAL (design concern) |
+| **Source PR** | #485 |
+| **Priority** | P1 — requires team decision before fix |
+
+### Affected Files
+- `services/llm-api/internal/interfaces/httpserver/handlers/chathandler/chat_handler.go` (lines 195–202)
+- `services/llm-api/internal/interfaces/httpserver/handlers/messageshandler/messages_handler.go` (lines 78–86, 462–469)
+- `services/llm-api/internal/interfaces/httpserver/handlers/modelhandler/provider_handler.go` (lines 196–220)
+
+### Description
+
+When a request arrives with API key authentication (`X-Auth-Method: apikey`), the chat and messages handlers bypass the active-model filter and call `SelectProviderModelForModelPublicIDIncludingInactive`, which uses `FindByModelKey` (no active filter) instead of `FindActiveByModelKey`. This allows any valid API key to access any model — including inactive/disabled models — regardless of which project, organization, or scope the API key belongs to.
+
+The regular (non-API-key) code path correctly restricts access to active models only via `SelectProviderModelForModelPublicID`, which calls `FindActiveByModelKey`. However, the "including inactive" variant performs no additional authorization: it does not verify that the API key has permission to use the specific model or that the model belongs to the same project/org as the key.
+
+This pattern is duplicated identically in three locations: the OpenAI-compatible chat handler, the Anthropic messages handler's `CreateMessage`, and its `CountTokens` method.
+
+### Evidence
+
+**`chathandler/chat_handler.go` (lines 195–202):**
+```go
+isAPIKeyAuth := strings.EqualFold(reqCtx.GetHeader("X-Auth-Method"), "apikey")
+var selectedProviderModel *domainmodel.ProviderModel
+var selectedProvider *domainmodel.Provider
+if isAPIKeyAuth {
+ selectedProviderModel, selectedProvider, err = h.providerHandler.SelectProviderModelForModelPublicIDIncludingInactive(ctx, request.Model)
+} else {
+ selectedProviderModel, selectedProvider, err = h.providerHandler.SelectProviderModelForModelPublicID(ctx, request.Model)
+}
+```
+
+**`messageshandler/messages_handler.go` (lines 78–86) — identical pattern in `CreateMessage`:**
+```go
+isAPIKeyAuth := strings.EqualFold(reqCtx.GetHeader("X-Auth-Method"), "apikey")
+var selectedProviderModel *domainmodel.ProviderModel
+var selectedProvider *domainmodel.Provider
+var err error
+if isAPIKeyAuth {
+ selectedProviderModel, selectedProvider, err = h.providerHandler.SelectProviderModelForModelPublicIDIncludingInactive(ctx, request.Model)
+} else {
+ selectedProviderModel, selectedProvider, err = h.providerHandler.SelectProviderModelForModelPublicID(ctx, request.Model)
+}
+```
+
+**`messageshandler/messages_handler.go` (lines 462–469) — identical pattern in `CountTokens`:**
+```go
+isAPIKeyAuth := strings.EqualFold(reqCtx.GetHeader("X-Auth-Method"), "apikey")
+var selectedProviderModel *domainmodel.ProviderModel
+var err error
+if isAPIKeyAuth {
+ selectedProviderModel, _, err = h.providerHandler.SelectProviderModelForModelPublicIDIncludingInactive(ctx, request.Model)
+} else {
+ selectedProviderModel, _, err = h.providerHandler.SelectProviderModelForModelPublicID(ctx, request.Model)
+}
+```
+
+**`modelhandler/provider_handler.go` (lines 196–220) — the "including inactive" method has no authorization check:**
+```go
+func (providerHandler *ProviderHandler) SelectProviderModelForModelPublicIDIncludingInactive(ctx context.Context, modelPublicID string) (*domainmodel.ProviderModel, *domainmodel.Provider, error) {
+ if strings.TrimSpace(modelPublicID) == "" {
+ return nil, nil, platformerrors.NewError(...)
+ }
+
+ providerModels, err := providerHandler.providerModelService.FindByModelKey(ctx, modelPublicID)
+ // ... no ownership/scope check — returns any matching model regardless of API key identity
+}
+```
+
+Compare with the active-only variant (line 149) which calls `FindActiveByModelKey` — but neither variant checks API key scope.
+
+### Recommended Fix
+
+This requires a team decision on the authorization model before implementation. Options include:
+
+1. **Project-scoped API keys:** Verify that the API key belongs to the same project as the requested model. This requires associating API keys with projects and filtering models by project ownership.
+2. **Explicit model allowlists per API key:** Each API key has a list of model IDs it can access, checked at selection time.
+3. **Remove inactive model access for API keys:** If there is no business requirement for API keys to access inactive models, use the same `FindActiveByModelKey` path for all auth methods.
+
+Until a decision is made, consider adding an audit log entry whenever an API key accesses an inactive model to measure the scope of this gap.
+
+---
+
+## Finding 2: Duplicate Redirect Logic Across Frontend Files
+
+| Field | Value |
+|---|---|
+| **Severity** | MEDIUM |
+| **Source PR** | #486 |
+| **Priority** | P3 — cleanup, no security risk after Phase 1 fix |
+
+### Affected Files
+- `apps/web/src/components/form/login.tsx`
+- `apps/web/src/routes/__root.tsx`
+- `apps/web/src/routes/auth/callback.tsx`
+- `apps/web/src/routes/login.tsx`
+- `apps/web/src/lib/redirect-utils.ts` (shared utility, added in Phase 1)
+
+### Description
+
+Phase 1 successfully extracted `isAllowedExternalRedirect` into `apps/web/src/lib/redirect-utils.ts`, and all four files now import from that shared location. The security-sensitive URL validation is no longer duplicated.
+
+However, **redirect handling logic** is still duplicated across the files. The same three-step redirect pattern — (1) check for external localhost redirect and append encoded token, (2) check for internal path redirect starting with `/`, (3) fall back to home — is implemented independently in three places with slight variations. Additionally, `getRedirectUrl` in `login.tsx` (form component) and `getRedirectTarget` in `__root.tsx` perform similar URL-parameter extraction.
+
+### Evidence
+
+**External redirect + token pattern — duplicated in 3 files:**
+
+`apps/web/src/routes/login.tsx` (lines 26–36):
+```tsx
+if (redirectUrl && isAllowedExternalRedirect(redirectUrl)) {
+ if (!accessToken) {
+ return;
+ }
+ const bearerToken = `Bearer ${accessToken}`;
+ const encodedToken = btoa(bearerToken);
+ const target = new URL(redirectUrl);
+ target.searchParams.set("token", encodedToken);
+ window.location.href = target.toString();
+ return;
+}
+```
+
+`apps/web/src/routes/__root.tsx` (lines 142–149, inside `handleCloseModal`):
+```tsx
+if (redirectUrl && isAllowedExternalRedirect(redirectUrl)) {
+ if (!accessToken) return;
+ const bearerToken = `Bearer ${accessToken}`;
+ const encodedToken = btoa(bearerToken);
+ const target = new URL(redirectUrl);
+ target.searchParams.set("token", encodedToken);
+ window.location.href = target.toString();
+ return;
+}
+```
+
+`apps/web/src/routes/auth/callback.tsx` (lines 59–66):
+```tsx
+if (oauthData.redirectUrl && isAllowedExternalRedirect(oauthData.redirectUrl)) {
+ const bearerToken = `Bearer ${tokens.access_token}`;
+ const encodedToken = btoa(bearerToken);
+ const target = new URL(oauthData.redirectUrl);
+ target.searchParams.set("token", encodedToken);
+ window.location.href = target.toString();
+ return;
+}
+```
+
+**Redirect URL extraction — duplicated in 2 files:**
+
+`apps/web/src/components/form/login.tsx` (lines 34–57, `getRedirectUrl`):
+```tsx
+const getRedirectUrl = () => {
+ const url = new URL(window.location.href);
+ const redirectParam = url.searchParams.get(URL_PARAM.REDIRECT);
+ if (redirectParam && (redirectParam.startsWith("/") || isAllowedExternalRedirect(redirectParam))) {
+ return redirectParam;
+ }
+ // ... additional cases
+};
+```
+
+`apps/web/src/routes/__root.tsx` (lines 83–90, `getRedirectTarget`):
+```tsx
+const getRedirectTarget = () => {
+ const url = new URL(window.location.href);
+ const redirectParam = url.searchParams.get(URL_PARAM.REDIRECT);
+ if (redirectParam && redirectParam.startsWith("/")) {
+ return redirectParam;
+ }
+ return url.pathname + url.search;
+};
+```
+
+### Recommended Fix
+
+Consolidate the duplicated logic into `apps/web/src/lib/redirect-utils.ts`:
+
+1. Add a `performExternalRedirect(redirectUrl: string, accessToken: string)` function that handles the token-encoding-and-redirect pattern.
+2. Add a `getRedirectUrl()` function that extracts and validates the redirect parameter from the current URL.
+3. Update all four files to use the shared functions, eliminating the duplicated code.
+
+This is a cleanup task with no security risk (Phase 1 already secured the validation). Priority is P3.
+
+---
+
+## Finding 3: LoginRoute Renders Blank Page During Redirect
+
+| Field | Value |
+|---|---|
+| **Severity** | LOW |
+| **Source PR** | #486 |
+| **Priority** | P4 — UX polish |
+
+### Affected Files
+- `apps/web/src/routes/login.tsx` (line 47)
+
+### Description
+
+The `LoginRoute` component returns `null` when the user is already authenticated and a redirect is being processed. During the brief period between rendering and the `useEffect` firing to perform the redirect, users see a completely blank page. This is particularly noticeable on slower connections or when the redirect involves an external URL (which requires a full page navigation).
+
+The OAuth callback page (`auth/callback.tsx`) already handles this correctly by showing a spinner with "Completing authentication..." text.
+
+### Evidence
+
+`apps/web/src/routes/login.tsx` (lines 11–48):
+```tsx
+function LoginRoute() {
+ const navigate = useNavigate();
+ const isAuthenticated = useAuth((state) => state.isAuthenticated);
+ const isGuest = useAuth((state) => state.isGuest);
+ const accessToken = useAuth((state) => state.accessToken);
+
+ useEffect(() => {
+ if (!isAuthenticated || isGuest) {
+ return;
+ }
+ // ... redirect logic
+ }, [accessToken, isAuthenticated, isGuest, navigate]);
+
+ return null; // <-- blank page during redirect
+}
+```
+
+For comparison, `apps/web/src/routes/auth/callback.tsx` (lines 102–108) shows the correct pattern:
+```tsx
+return (
+
+
+
+
Completing authentication...
+
+
+);
+```
+
+### Recommended Fix
+
+Replace `return null` with a loading indicator. The spinner pattern from `auth/callback.tsx` can be reused directly:
+
+```tsx
+return (
+
+);
+```
+
+Alternatively, extract the spinner into a shared `LoadingScreen` component to avoid further duplication (no such shared component currently exists in the codebase).
From c97c6bcf9a5e6100c555afccc03ef2b19d9e9999 Mon Sep 17 00:00:00 2001
From: "tokamak-pm[bot]" <264983013+tokamak-pm[bot]@users.noreply.github.com>
Date: Tue, 7 Apr 2026 08:43:37 +0000
Subject: [PATCH 4/4] Review commits since the previous automation run. If that
context is una
---
services/llm-api/cmd/server/wire_gen.go | 2 +-
.../internal/domain/connector/service.go | 12 +-
.../internal/domain/conversation/item.go | 4 +-
.../conversation/message_action_service.go | 20 +--
.../internal/domain/document/entity.go | 22 ++--
.../internal/domain/model/provider_model.go | 20 +--
.../domain/prompt/deep_research_module.go | 6 +-
.../internal/domain/prompttemplate/service.go | 1 -
.../llm-api/internal/domain/share/slug.go | 2 +-
.../connector/token_encryptor.go | 6 +-
.../database/dbschema/conversation.go | 26 ++--
.../database/dbschema/conversation_share.go | 30 ++---
.../database/dbschema/document_content.go | 24 ++--
.../database/dbschema/provider.go | 82 ++++++------
.../conversation_repository.go | 8 +-
.../repository/repository_provider.go | 2 +-
.../infrastructure/infrastructure_provider.go | 2 +-
.../handlers/connectorhandler/handler.go | 26 ++--
.../conversationhandler/branch_handler.go | 7 +-
.../handlers/modelhandler/provider_handler.go | 22 ++--
.../requests/messages/anthropic_request.go | 8 +-
.../httpserver/requests/models/model.go | 40 +++---
.../httpserver/responses/model/model.go | 120 +++++++++---------
.../responses/projectres/responses.go | 22 ++--
.../v1/admin/model/admin_model_route.go | 16 +--
.../routes/v1/conversation/branch_route.go | 10 +-
26 files changed, 270 insertions(+), 270 deletions(-)
diff --git a/services/llm-api/cmd/server/wire_gen.go b/services/llm-api/cmd/server/wire_gen.go
index d0d6dadc..e9684ba4 100644
--- a/services/llm-api/cmd/server/wire_gen.go
+++ b/services/llm-api/cmd/server/wire_gen.go
@@ -21,6 +21,7 @@ import (
"jan-server/services/llm-api/internal/domain/user"
"jan-server/services/llm-api/internal/domain/usersettings"
"jan-server/services/llm-api/internal/infrastructure"
+ "jan-server/services/llm-api/internal/infrastructure/connector"
"jan-server/services/llm-api/internal/infrastructure/crontab"
"jan-server/services/llm-api/internal/infrastructure/database/repository/apikeyrepo"
"jan-server/services/llm-api/internal/infrastructure/database/repository/conversationrepo"
@@ -34,7 +35,6 @@ import (
"jan-server/services/llm-api/internal/infrastructure/database/repository/tokenusagerepo"
"jan-server/services/llm-api/internal/infrastructure/database/repository/userrepo"
"jan-server/services/llm-api/internal/infrastructure/database/repository/usersettingsrepo"
- "jan-server/services/llm-api/internal/infrastructure/connector"
"jan-server/services/llm-api/internal/infrastructure/inference"
"jan-server/services/llm-api/internal/infrastructure/logger"
"jan-server/services/llm-api/internal/interfaces/httpserver"
diff --git a/services/llm-api/internal/domain/connector/service.go b/services/llm-api/internal/domain/connector/service.go
index 4edd608d..42faa128 100644
--- a/services/llm-api/internal/domain/connector/service.go
+++ b/services/llm-api/internal/domain/connector/service.go
@@ -70,12 +70,12 @@ type ServiceConfig struct {
// Service provides connector domain operations.
type Service struct {
- repo Repository
- encryptor TokenEncryptor
- provider OAuthProvider
- config ServiceConfig
- stateHMAC []byte
- logger zerolog.Logger
+ repo Repository
+ encryptor TokenEncryptor
+ provider OAuthProvider
+ config ServiceConfig
+ stateHMAC []byte
+ logger zerolog.Logger
}
// NewService creates a new connector service.
diff --git a/services/llm-api/internal/domain/conversation/item.go b/services/llm-api/internal/domain/conversation/item.go
index 4ed0c930..d51a1024 100644
--- a/services/llm-api/internal/domain/conversation/item.go
+++ b/services/llm-api/internal/domain/conversation/item.go
@@ -549,8 +549,8 @@ type ImageContent struct {
// File content for attachments
type FileContent struct {
FileID string `json:"file_id,omitempty"`
- URL string `json:"url,omitempty"` // Direct URL to the file
- Name string `json:"name,omitempty"` // Display name / filename
+ URL string `json:"url,omitempty"` // Direct URL to the file
+ Name string `json:"name,omitempty"` // Display name / filename
MimeType string `json:"mime_type,omitempty"`
Size int64 `json:"size,omitempty"`
Detail string `json:"detail,omitempty"` // "auto", "low", "high" for processing detail level
diff --git a/services/llm-api/internal/domain/conversation/message_action_service.go b/services/llm-api/internal/domain/conversation/message_action_service.go
index 18293137..d06d6cf5 100644
--- a/services/llm-api/internal/domain/conversation/message_action_service.go
+++ b/services/llm-api/internal/domain/conversation/message_action_service.go
@@ -22,18 +22,18 @@ func NewMessageActionService(convRepo ConversationRepository) *MessageActionServ
// EditResult contains the result of an edit message operation
type EditResult struct {
- NewBranch string `json:"new_branch"` // Always "MAIN" after swap
- OldMainBackup string `json:"old_main_backup"` // Backup name for old MAIN
- UserItem *Item `json:"user_item"`
- ConversationID string `json:"conversation_id"`
+ NewBranch string `json:"new_branch"` // Always "MAIN" after swap
+ OldMainBackup string `json:"old_main_backup"` // Backup name for old MAIN
+ UserItem *Item `json:"user_item"`
+ ConversationID string `json:"conversation_id"`
}
// RegenerateResult contains the result of a regenerate operation
type RegenerateResult struct {
- NewBranch string `json:"new_branch"` // Always "MAIN" after swap
- OldMainBackup string `json:"old_main_backup"` // Backup name for old MAIN
- ConvID string `json:"conversation_id"`
- UserItemID string `json:"user_item_id"` // The user message to regenerate from
+ NewBranch string `json:"new_branch"` // Always "MAIN" after swap
+ OldMainBackup string `json:"old_main_backup"` // Backup name for old MAIN
+ ConvID string `json:"conversation_id"`
+ UserItemID string `json:"user_item_id"` // The user message to regenerate from
}
// EditMessage creates a new branch from the edited message point
@@ -234,8 +234,8 @@ func (s *MessageActionService) RegenerateResponse(ctx context.Context, conv *Con
// DeleteResult contains the result of a delete message operation
type DeleteResult struct {
- NewBranch string `json:"new_branch"` // Always "MAIN" after swap
- OldMainBackup string `json:"old_main_backup"` // Backup name for old MAIN
+ NewBranch string `json:"new_branch"` // Always "MAIN" after swap
+ OldMainBackup string `json:"old_main_backup"` // Backup name for old MAIN
}
// DeleteMessage deletes a message by creating a new branch without it
diff --git a/services/llm-api/internal/domain/document/entity.go b/services/llm-api/internal/domain/document/entity.go
index 355c77a7..e3c59b92 100644
--- a/services/llm-api/internal/domain/document/entity.go
+++ b/services/llm-api/internal/domain/document/entity.go
@@ -39,17 +39,17 @@ type DocumentContent struct {
// ProjectFile represents a file attached to a project
type ProjectFile struct {
- ID uint `json:"-"`
- PublicID string `json:"id"`
- Object string `json:"object"` // Always "project_file"
- ProjectID uint `json:"-"`
- DocumentContentID *uint `json:"-"`
- DocumentContent *DocumentContent `json:"document_content,omitempty"`
- DisplayOrder int `json:"display_order"`
- CreatedBy uint `json:"-"`
- DeletedAt *time.Time `json:"-"`
- CreatedAt time.Time `json:"created_at"`
- UpdatedAt time.Time `json:"updated_at"`
+ ID uint `json:"-"`
+ PublicID string `json:"id"`
+ Object string `json:"object"` // Always "project_file"
+ ProjectID uint `json:"-"`
+ DocumentContentID *uint `json:"-"`
+ DocumentContent *DocumentContent `json:"document_content,omitempty"`
+ DisplayOrder int `json:"display_order"`
+ CreatedBy uint `json:"-"`
+ DeletedAt *time.Time `json:"-"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
}
// DocumentContentFilter is used to filter document content queries
diff --git a/services/llm-api/internal/domain/model/provider_model.go b/services/llm-api/internal/domain/model/provider_model.go
index 47831373..386e0e02 100644
--- a/services/llm-api/internal/domain/model/provider_model.go
+++ b/services/llm-api/internal/domain/model/provider_model.go
@@ -112,17 +112,17 @@ func (e *ValidationError) Error() string {
// ProviderModelFilter defines optional conditions for querying provider models.
type ProviderModelFilter struct {
- IDs *[]uint
- PublicID *string
- ProviderIDs *[]uint
- ProviderID *uint
- ModelCatalogID *uint
- ModelPublicID *string
- ModelPublicIDs *[]string
+ IDs *[]uint
+ PublicID *string
+ ProviderIDs *[]uint
+ ProviderID *uint
+ ModelCatalogID *uint
+ ModelPublicID *string
+ ModelPublicIDs *[]string
ProviderOriginalModelID *string
- Active *bool
- SupportsImages *bool
- SearchText *string
+ Active *bool
+ SupportsImages *bool
+ SearchText *string
}
// ProviderModelRepository abstracts persistence for provider models.
diff --git a/services/llm-api/internal/domain/prompt/deep_research_module.go b/services/llm-api/internal/domain/prompt/deep_research_module.go
index 6774335f..058d73df 100644
--- a/services/llm-api/internal/domain/prompt/deep_research_module.go
+++ b/services/llm-api/internal/domain/prompt/deep_research_module.go
@@ -18,8 +18,8 @@ const (
// DeepResearchModule injects the Deep Research system prompt when enabled
type DeepResearchModule struct {
- templateService *prompttemplate.Service
- modelPromptService *modelprompttemplate.Service
+ templateService *prompttemplate.Service
+ modelPromptService *modelprompttemplate.Service
}
// NewDeepResearchModule creates a new deep research module
@@ -112,7 +112,7 @@ func (m *DeepResearchModule) Apply(ctx context.Context, promptCtx *Context, mess
log.Debug().
Str("model_catalog_id", *promptCtx.ModelCatalogID).
Msg("DeepResearchModule: Attempting to load model-specific template")
-
+
template, source, err := m.modelPromptService.GetTemplateForModelByKey(ctx, *promptCtx.ModelCatalogID, prompttemplate.TemplateKeyDeepResearch)
if err == nil && template != nil && template.IsActive {
promptContent = template.Content
diff --git a/services/llm-api/internal/domain/prompttemplate/service.go b/services/llm-api/internal/domain/prompttemplate/service.go
index 0d3e443f..3b59297d 100644
--- a/services/llm-api/internal/domain/prompttemplate/service.go
+++ b/services/llm-api/internal/domain/prompttemplate/service.go
@@ -407,4 +407,3 @@ func (s *Service) EnsureDefaultTemplates(ctx context.Context) error {
}
return nil
}
-
diff --git a/services/llm-api/internal/domain/share/slug.go b/services/llm-api/internal/domain/share/slug.go
index c5d32d0c..a612bb59 100644
--- a/services/llm-api/internal/domain/share/slug.go
+++ b/services/llm-api/internal/domain/share/slug.go
@@ -105,7 +105,7 @@ func GenerateSharePublicID() (string, error) {
if idx < 10 {
result[i] = byte('0' + idx)
} else {
- result[i] = byte('a' + (idx - 10) % 26)
+ result[i] = byte('a' + (idx-10)%26)
}
}
diff --git a/services/llm-api/internal/infrastructure/connector/token_encryptor.go b/services/llm-api/internal/infrastructure/connector/token_encryptor.go
index 2a66823e..41a078b4 100644
--- a/services/llm-api/internal/infrastructure/connector/token_encryptor.go
+++ b/services/llm-api/internal/infrastructure/connector/token_encryptor.go
@@ -12,9 +12,9 @@ import (
// TokenEncryptor provides AES-256-GCM encryption for OAuth tokens.
type TokenEncryptor struct {
- currentKey []byte
- currentKeyID string
- previousKey []byte
+ currentKey []byte
+ currentKeyID string
+ previousKey []byte
previousKeyID string
}
diff --git a/services/llm-api/internal/infrastructure/database/dbschema/conversation.go b/services/llm-api/internal/infrastructure/database/dbschema/conversation.go
index 26018869..a41f2689 100644
--- a/services/llm-api/internal/infrastructure/database/dbschema/conversation.go
+++ b/services/llm-api/internal/infrastructure/database/dbschema/conversation.go
@@ -77,22 +77,22 @@ type ConversationItem struct {
RatingComment *string `gorm:"type:text"`
// OpenAI-compatible fields (added in migration 000009)
- CallID *string `gorm:"type:varchar(50);index:idx_conversation_items_call_id"`
- ServerLabel *string `gorm:"type:varchar(255);index:idx_conversation_items_server_label"`
- ApprovalRequestID *string `gorm:"type:varchar(50);index:idx_conversation_items_approval_request_id"`
- Arguments *string `gorm:"type:text"`
- Output *string `gorm:"type:text"`
- Error *string `gorm:"type:text"`
- Action JSONAction `gorm:"type:jsonb"`
- Tools JSONMcpTools `gorm:"type:jsonb"`
+ CallID *string `gorm:"type:varchar(50);index:idx_conversation_items_call_id"`
+ ServerLabel *string `gorm:"type:varchar(255);index:idx_conversation_items_server_label"`
+ ApprovalRequestID *string `gorm:"type:varchar(50);index:idx_conversation_items_approval_request_id"`
+ Arguments *string `gorm:"type:text"`
+ Output *string `gorm:"type:text"`
+ Error *string `gorm:"type:text"`
+ Action JSONAction `gorm:"type:jsonb"`
+ Tools JSONMcpTools `gorm:"type:jsonb"`
PendingSafetyChecks JSONSafetyChecks `gorm:"type:jsonb"`
AcknowledgedSafetyChecks JSONSafetyChecks `gorm:"type:jsonb"`
- Approve *bool `gorm:"type:boolean"`
- Reason *string `gorm:"type:text"`
- Commands JSONCommands `gorm:"type:jsonb"`
- MaxOutputLength *int64 `gorm:"type:bigint"`
+ Approve *bool `gorm:"type:boolean"`
+ Reason *string `gorm:"type:text"`
+ Commands JSONCommands `gorm:"type:jsonb"`
+ MaxOutputLength *int64 `gorm:"type:bigint"`
ShellOutputs JSONShellOutputs `gorm:"type:jsonb"`
- Operation JSONOperation `gorm:"type:jsonb"`
+ Operation JSONOperation `gorm:"type:jsonb"`
}
// JSONMap is a custom type for map[string]string stored as JSON
diff --git a/services/llm-api/internal/infrastructure/database/dbschema/conversation_share.go b/services/llm-api/internal/infrastructure/database/dbschema/conversation_share.go
index 68f0a288..b223ccdf 100644
--- a/services/llm-api/internal/infrastructure/database/dbschema/conversation_share.go
+++ b/services/llm-api/internal/infrastructure/database/dbschema/conversation_share.go
@@ -16,21 +16,21 @@ func init() {
// ConversationShare represents the database schema for conversation shares
type ConversationShare struct {
BaseModel
- PublicID string `gorm:"type:varchar(64);uniqueIndex;not null"`
- Slug string `gorm:"type:varchar(30);uniqueIndex;not null"`
- ConversationID uint `gorm:"index:idx_conversation_shares_conversation_id;not null"`
- Conversation Conversation `gorm:"foreignKey:ConversationID"`
- OwnerUserID uint `gorm:"index:idx_conversation_shares_owner_user_id;not null"`
- User User `gorm:"foreignKey:OwnerUserID"`
- ItemPublicID *string `gorm:"type:varchar(64)"`
- Title *string `gorm:"type:varchar(256)"`
- Visibility string `gorm:"type:varchar(20);not null;default:'unlisted'"`
- RevokedAt *time.Time `gorm:"type:timestamp"`
- ViewCount int `gorm:"not null;default:0"`
- LastViewedAt *time.Time `gorm:"type:timestamp"`
- SnapshotVersion int `gorm:"not null;default:1"`
- Snapshot JSONSnapshot `gorm:"type:jsonb;not null"`
- ShareOptions JSONShareOptions `gorm:"type:jsonb"`
+ PublicID string `gorm:"type:varchar(64);uniqueIndex;not null"`
+ Slug string `gorm:"type:varchar(30);uniqueIndex;not null"`
+ ConversationID uint `gorm:"index:idx_conversation_shares_conversation_id;not null"`
+ Conversation Conversation `gorm:"foreignKey:ConversationID"`
+ OwnerUserID uint `gorm:"index:idx_conversation_shares_owner_user_id;not null"`
+ User User `gorm:"foreignKey:OwnerUserID"`
+ ItemPublicID *string `gorm:"type:varchar(64)"`
+ Title *string `gorm:"type:varchar(256)"`
+ Visibility string `gorm:"type:varchar(20);not null;default:'unlisted'"`
+ RevokedAt *time.Time `gorm:"type:timestamp"`
+ ViewCount int `gorm:"not null;default:0"`
+ LastViewedAt *time.Time `gorm:"type:timestamp"`
+ SnapshotVersion int `gorm:"not null;default:1"`
+ Snapshot JSONSnapshot `gorm:"type:jsonb;not null"`
+ ShareOptions JSONShareOptions `gorm:"type:jsonb"`
}
// TableName returns the custom table name for conversation shares
diff --git a/services/llm-api/internal/infrastructure/database/dbschema/document_content.go b/services/llm-api/internal/infrastructure/database/dbschema/document_content.go
index 65d5fbce..b5f9620f 100644
--- a/services/llm-api/internal/infrastructure/database/dbschema/document_content.go
+++ b/services/llm-api/internal/infrastructure/database/dbschema/document_content.go
@@ -14,21 +14,21 @@ func init() {
// DocumentContent represents the database schema for document OCR content
type DocumentContent struct {
- ID uint `gorm:"primarykey"`
- PublicID string `gorm:"uniqueIndex;size:64;not null"`
- MediaObjectID string `gorm:"index;size:40;not null"`
- UserID uint `gorm:"index;not null"`
- Filename *string `gorm:"size:512"`
- MimeType *string `gorm:"size:128"`
+ ID uint `gorm:"primarykey"`
+ PublicID string `gorm:"uniqueIndex;size:64;not null"`
+ MediaObjectID string `gorm:"index;size:40;not null"`
+ UserID uint `gorm:"index;not null"`
+ Filename *string `gorm:"size:512"`
+ MimeType *string `gorm:"size:128"`
FileSize *int64
- ProcessingStatus string `gorm:"index;size:32;not null;default:'pending'"`
- ExtractedText *string `gorm:"type:text"`
- ExtractionModel *string `gorm:"size:128"`
+ ProcessingStatus string `gorm:"index;size:32;not null;default:'pending'"`
+ ExtractedText *string `gorm:"type:text"`
+ ExtractionModel *string `gorm:"size:128"`
PageCount *int
WordCount *int
- ErrorMessage *string `gorm:"type:text"`
- CreatedAt time.Time `gorm:"not null"`
- UpdatedAt time.Time `gorm:"not null"`
+ ErrorMessage *string `gorm:"type:text"`
+ CreatedAt time.Time `gorm:"not null"`
+ UpdatedAt time.Time `gorm:"not null"`
}
// TableName specifies the table name for DocumentContent
diff --git a/services/llm-api/internal/infrastructure/database/dbschema/provider.go b/services/llm-api/internal/infrastructure/database/dbschema/provider.go
index b402a9cb..a63a9334 100644
--- a/services/llm-api/internal/infrastructure/database/dbschema/provider.go
+++ b/services/llm-api/internal/infrastructure/database/dbschema/provider.go
@@ -17,20 +17,20 @@ func init() {
type Provider struct {
BaseModel
- PublicID string `gorm:"size:64;not null;uniqueIndex"`
- DisplayName string `gorm:"size:255;not null"`
- Kind string `gorm:"size:64;not null;index;index:idx_provider_active_kind,priority:2"`
- Category string `gorm:"size:20;not null;default:'llm';index"` // "llm" or "image"
- BaseURL string `gorm:"size:512"`
- Endpoints datatypes.JSON `gorm:"type:jsonb"`
- EncryptedAPIKey string `gorm:"type:text"`
- APIKeyHint *string `gorm:"size:128"`
- IsModerated *bool `gorm:"not null;default:false;index"`
- Active *bool `gorm:"not null;default:true;index;index:idx_provider_active_kind,priority:1"`
- DefaultImageGenerate *bool `gorm:"column:default_provider_image_generate;not null;default:false"`
- DefaultImageEdit *bool `gorm:"column:default_provider_image_edit;not null;default:false"`
- Metadata datatypes.JSON `gorm:"type:jsonb"`
- LastSyncedAt *time.Time `gorm:"index"`
+ PublicID string `gorm:"size:64;not null;uniqueIndex"`
+ DisplayName string `gorm:"size:255;not null"`
+ Kind string `gorm:"size:64;not null;index;index:idx_provider_active_kind,priority:2"`
+ Category string `gorm:"size:20;not null;default:'llm';index"` // "llm" or "image"
+ BaseURL string `gorm:"size:512"`
+ Endpoints datatypes.JSON `gorm:"type:jsonb"`
+ EncryptedAPIKey string `gorm:"type:text"`
+ APIKeyHint *string `gorm:"size:128"`
+ IsModerated *bool `gorm:"not null;default:false;index"`
+ Active *bool `gorm:"not null;default:true;index;index:idx_provider_active_kind,priority:1"`
+ DefaultImageGenerate *bool `gorm:"column:default_provider_image_generate;not null;default:false"`
+ DefaultImageEdit *bool `gorm:"column:default_provider_image_edit;not null;default:false"`
+ Metadata datatypes.JSON `gorm:"type:jsonb"`
+ LastSyncedAt *time.Time `gorm:"index"`
}
func NewSchemaProvider(p *domainmodel.Provider) *Provider {
@@ -68,20 +68,20 @@ func NewSchemaProvider(p *domainmodel.Provider) *Provider {
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
},
- PublicID: p.PublicID,
- DisplayName: p.DisplayName,
- Kind: string(p.Kind),
- Category: category,
- BaseURL: p.BaseURL,
- Endpoints: endpointsJSON,
- EncryptedAPIKey: p.EncryptedAPIKey,
- APIKeyHint: p.APIKeyHint,
- IsModerated: &isModerated,
- Active: &active,
+ PublicID: p.PublicID,
+ DisplayName: p.DisplayName,
+ Kind: string(p.Kind),
+ Category: category,
+ BaseURL: p.BaseURL,
+ Endpoints: endpointsJSON,
+ EncryptedAPIKey: p.EncryptedAPIKey,
+ APIKeyHint: p.APIKeyHint,
+ IsModerated: &isModerated,
+ Active: &active,
DefaultImageGenerate: &defaultImageGenerate,
DefaultImageEdit: &defaultImageEdit,
- Metadata: metadataJSON,
- LastSyncedAt: p.LastSyncedAt,
+ Metadata: metadataJSON,
+ LastSyncedAt: p.LastSyncedAt,
}
}
@@ -131,22 +131,22 @@ func (p *Provider) EtoD() *domainmodel.Provider {
}
return &domainmodel.Provider{
- ID: p.ID,
- PublicID: p.PublicID,
- DisplayName: p.DisplayName,
- Kind: domainmodel.ProviderKind(p.Kind),
- Category: category,
- BaseURL: p.BaseURL,
- Endpoints: endpoints,
- EncryptedAPIKey: p.EncryptedAPIKey,
- APIKeyHint: p.APIKeyHint,
- IsModerated: isModerated,
- Active: active,
+ ID: p.ID,
+ PublicID: p.PublicID,
+ DisplayName: p.DisplayName,
+ Kind: domainmodel.ProviderKind(p.Kind),
+ Category: category,
+ BaseURL: p.BaseURL,
+ Endpoints: endpoints,
+ EncryptedAPIKey: p.EncryptedAPIKey,
+ APIKeyHint: p.APIKeyHint,
+ IsModerated: isModerated,
+ Active: active,
DefaultImageGenerate: defaultImageGenerate,
DefaultImageEdit: defaultImageEdit,
- Metadata: metadata,
- LastSyncedAt: p.LastSyncedAt,
- CreatedAt: p.CreatedAt,
- UpdatedAt: p.UpdatedAt,
+ Metadata: metadata,
+ LastSyncedAt: p.LastSyncedAt,
+ CreatedAt: p.CreatedAt,
+ UpdatedAt: p.UpdatedAt,
}
}
diff --git a/services/llm-api/internal/infrastructure/database/repository/conversationrepo/conversation_repository.go b/services/llm-api/internal/infrastructure/database/repository/conversationrepo/conversation_repository.go
index 1ba7b816..7826bc42 100644
--- a/services/llm-api/internal/infrastructure/database/repository/conversationrepo/conversation_repository.go
+++ b/services/llm-api/internal/infrastructure/database/repository/conversationrepo/conversation_repository.go
@@ -287,7 +287,7 @@ func (repo *ConversationGormRepository) GetItemByCallIDAndType(ctx context.Conte
func (repo *ConversationGormRepository) UpdateItem(ctx context.Context, conversationID uint, item *conversation.Item) error {
q := repo.db.GetQuery(ctx)
entity := dbschema.NewSchemaConversationItem(item)
-
+
_, err := q.ConversationItem.WithContext(ctx).
Where(q.ConversationItem.ID.Eq(item.ID)).
Where(q.ConversationItem.ConversationID.Eq(conversationID)).
@@ -317,7 +317,7 @@ func (repo *ConversationGormRepository) DeleteItem(ctx context.Context, conversa
func (repo *ConversationGormRepository) CountItems(ctx context.Context, conversationID uint, branchName string) (int, error) {
q := repo.db.GetQuery(ctx)
sql := q.ConversationItem.WithContext(ctx)
-
+
// Apply filter with branch name for proper per-branch counting
filter := conversation.ItemFilter{
ConversationID: &conversationID,
@@ -394,7 +394,7 @@ func (repo *ConversationGormRepository) DeleteBranch(ctx context.Context, conver
}
q := repo.db.GetQuery(ctx)
-
+
// Delete all items in this branch first
_, err := q.ConversationItem.WithContext(ctx).
Where(q.ConversationItem.ConversationID.Eq(conversationID)).
@@ -447,7 +447,7 @@ func (repo *ConversationGormRepository) GetBranchItems(ctx context.Context, conv
q := repo.db.GetQuery(ctx)
sql := q.ConversationItem.WithContext(ctx)
-
+
// Apply filter with branch name
filter := conversation.ItemFilter{
ConversationID: &conversationID,
diff --git a/services/llm-api/internal/infrastructure/database/repository/repository_provider.go b/services/llm-api/internal/infrastructure/database/repository/repository_provider.go
index 767e91e6..aa9eab8d 100644
--- a/services/llm-api/internal/infrastructure/database/repository/repository_provider.go
+++ b/services/llm-api/internal/infrastructure/database/repository/repository_provider.go
@@ -5,8 +5,8 @@ import (
"jan-server/services/llm-api/internal/infrastructure/database/repository/conversationrepo"
"jan-server/services/llm-api/internal/infrastructure/database/repository/documentrepo"
"jan-server/services/llm-api/internal/infrastructure/database/repository/mcptoolrepo"
- "jan-server/services/llm-api/internal/infrastructure/database/repository/modelrepo"
"jan-server/services/llm-api/internal/infrastructure/database/repository/modelprompttemplaterepo"
+ "jan-server/services/llm-api/internal/infrastructure/database/repository/modelrepo"
"jan-server/services/llm-api/internal/infrastructure/database/repository/projectrepo"
"jan-server/services/llm-api/internal/infrastructure/database/repository/prompttemplaterepo"
"jan-server/services/llm-api/internal/infrastructure/database/repository/sharerepo"
diff --git a/services/llm-api/internal/infrastructure/infrastructure_provider.go b/services/llm-api/internal/infrastructure/infrastructure_provider.go
index c0f4c536..7ba27192 100644
--- a/services/llm-api/internal/infrastructure/infrastructure_provider.go
+++ b/services/llm-api/internal/infrastructure/infrastructure_provider.go
@@ -14,6 +14,7 @@ import (
"jan-server/services/llm-api/internal/application/audit"
"jan-server/services/llm-api/internal/config"
"jan-server/services/llm-api/internal/infrastructure/auth"
+ "jan-server/services/llm-api/internal/infrastructure/connector"
"jan-server/services/llm-api/internal/infrastructure/crontab"
"jan-server/services/llm-api/internal/infrastructure/database"
"jan-server/services/llm-api/internal/infrastructure/database/repository"
@@ -22,7 +23,6 @@ import (
"jan-server/services/llm-api/internal/infrastructure/keycloak"
"jan-server/services/llm-api/internal/infrastructure/kong"
"jan-server/services/llm-api/internal/infrastructure/logger"
- "jan-server/services/llm-api/internal/infrastructure/connector"
"jan-server/services/llm-api/internal/infrastructure/mediaclient"
memclient "jan-server/services/llm-api/internal/infrastructure/memory"
)
diff --git a/services/llm-api/internal/interfaces/httpserver/handlers/connectorhandler/handler.go b/services/llm-api/internal/interfaces/httpserver/handlers/connectorhandler/handler.go
index e9a86416..77544bbf 100644
--- a/services/llm-api/internal/interfaces/httpserver/handlers/connectorhandler/handler.go
+++ b/services/llm-api/internal/interfaces/httpserver/handlers/connectorhandler/handler.go
@@ -268,11 +268,11 @@ func (h *ConnectorHandler) Connect(c *gin.Context) {
}
c.JSON(http.StatusOK, ConnectResponse{
- Connected: true,
- Username: conn.ProviderUsername,
- Email: conn.ProviderEmail,
- AvatarURL: conn.ProviderAvatarURL,
- ConnectedAt: conn.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
+ Connected: true,
+ Username: conn.ProviderUsername,
+ Email: conn.ProviderEmail,
+ AvatarURL: conn.ProviderAvatarURL,
+ ConnectedAt: conn.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
})
}
@@ -379,14 +379,14 @@ func (h *ConnectorHandler) GetStatus(c *gin.Context) {
}
c.JSON(http.StatusOK, StatusResponse{
- Connected: conn.IsConnected,
- Enabled: h.service.IsEnabled(connectorType),
- Status: status,
- Username: conn.ProviderUsername,
- Email: conn.ProviderEmail,
- LastError: conn.LastError,
- LastSyncAt: formatTimePtr(conn.LastSyncAt),
- ConnectedAt: conn.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
+ Connected: conn.IsConnected,
+ Enabled: h.service.IsEnabled(connectorType),
+ Status: status,
+ Username: conn.ProviderUsername,
+ Email: conn.ProviderEmail,
+ LastError: conn.LastError,
+ LastSyncAt: formatTimePtr(conn.LastSyncAt),
+ ConnectedAt: conn.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
})
}
diff --git a/services/llm-api/internal/interfaces/httpserver/handlers/conversationhandler/branch_handler.go b/services/llm-api/internal/interfaces/httpserver/handlers/conversationhandler/branch_handler.go
index ad26bfb6..468124c5 100644
--- a/services/llm-api/internal/interfaces/httpserver/handlers/conversationhandler/branch_handler.go
+++ b/services/llm-api/internal/interfaces/httpserver/handlers/conversationhandler/branch_handler.go
@@ -242,7 +242,7 @@ func (h *BranchHandler) EditMessage(ctx context.Context, conv *conversation.Conv
}
response := &EditMessageResponse{
- Branch: result.NewBranch, // Always "MAIN"
+ Branch: result.NewBranch, // Always "MAIN"
OldMainBackup: result.OldMainBackup,
BranchCreated: true, // Edit always creates a new branch (which becomes MAIN)
UserItem: result.UserItem,
@@ -265,7 +265,7 @@ func (h *BranchHandler) RegenerateMessage(ctx context.Context, conv *conversatio
}
response := &RegenerateMessageResponse{
- Branch: result.NewBranch, // Always "MAIN"
+ Branch: result.NewBranch, // Always "MAIN"
OldMainBackup: result.OldMainBackup,
BranchCreated: true, // Regenerate always creates a new branch (which becomes MAIN)
UserItemID: result.UserItemID,
@@ -288,12 +288,13 @@ func (h *BranchHandler) DeleteMessage(ctx context.Context, conv *conversation.Co
}
return &DeleteMessageResponse{
- Branch: result.NewBranch, // Always "MAIN"
+ Branch: result.NewBranch, // Always "MAIN"
OldMainBackup: result.OldMainBackup,
BranchCreated: true,
Deleted: true,
}, nil
}
+
// ===============================================
// Helper Functions
// ===============================================
diff --git a/services/llm-api/internal/interfaces/httpserver/handlers/modelhandler/provider_handler.go b/services/llm-api/internal/interfaces/httpserver/handlers/modelhandler/provider_handler.go
index c218af46..e176254c 100644
--- a/services/llm-api/internal/interfaces/httpserver/handlers/modelhandler/provider_handler.go
+++ b/services/llm-api/internal/interfaces/httpserver/handlers/modelhandler/provider_handler.go
@@ -56,14 +56,14 @@ func (providerHandler *ProviderHandler) RegisterProvider(addProviderRequest requ
}
result, err := providerHandler.providerService.RegisterProvider(ctx, domainmodel.RegisterProviderInput{
- Name: addProviderRequest.Name,
- Vendor: addProviderRequest.Vendor,
- BaseURL: addProviderRequest.BaseURL,
- Endpoints: endpoints,
- APIKey: addProviderRequest.APIKey,
- Metadata: addProviderRequest.Metadata,
- Active: active,
- Category: domainmodel.ProviderCategory(addProviderRequest.Category),
+ Name: addProviderRequest.Name,
+ Vendor: addProviderRequest.Vendor,
+ BaseURL: addProviderRequest.BaseURL,
+ Endpoints: endpoints,
+ APIKey: addProviderRequest.APIKey,
+ Metadata: addProviderRequest.Metadata,
+ Active: active,
+ Category: domainmodel.ProviderCategory(addProviderRequest.Category),
DefaultImageGenerate: defaultImageGenerate,
DefaultImageEdit: defaultImageEdit,
})
@@ -355,9 +355,9 @@ func (h *ProviderHandler) UpdateProvider(
}
return nil
}(),
- APIKey: req.APIKey,
- Metadata: req.Metadata,
- Active: req.Active,
+ APIKey: req.APIKey,
+ Metadata: req.Metadata,
+ Active: req.Active,
DefaultImageGenerate: req.DefaultProviderImageGenerate,
DefaultImageEdit: req.DefaultProviderImageEdit,
}
diff --git a/services/llm-api/internal/interfaces/httpserver/requests/messages/anthropic_request.go b/services/llm-api/internal/interfaces/httpserver/requests/messages/anthropic_request.go
index 131bf615..0e7ed317 100644
--- a/services/llm-api/internal/interfaces/httpserver/requests/messages/anthropic_request.go
+++ b/services/llm-api/internal/interfaces/httpserver/requests/messages/anthropic_request.go
@@ -124,15 +124,15 @@ func (c *AnthropicContent) GetText() string {
// - Simple: "system": "You are a helpful assistant"
// - Array: "system": [{"type": "text", "text": "You are...", "cache_control": {...}}]
type AnthropicSystemPrompt struct {
- Text string `json:"-"` // For simple string system prompt
+ Text string `json:"-"` // For simple string system prompt
Blocks []AnthropicSystemContentBlock `json:"-"` // For array of content blocks
}
// AnthropicSystemContentBlock represents a system content block (supports cache_control)
type AnthropicSystemContentBlock struct {
- Type string `json:"type"`
- Text string `json:"text,omitempty"`
- CacheControl *AnthropicCacheControl `json:"cache_control,omitempty"`
+ Type string `json:"type"`
+ Text string `json:"text,omitempty"`
+ CacheControl *AnthropicCacheControl `json:"cache_control,omitempty"`
}
// AnthropicCacheControl for prompt caching
diff --git a/services/llm-api/internal/interfaces/httpserver/requests/models/model.go b/services/llm-api/internal/interfaces/httpserver/requests/models/model.go
index c27027c6..6b1fd538 100644
--- a/services/llm-api/internal/interfaces/httpserver/requests/models/model.go
+++ b/services/llm-api/internal/interfaces/httpserver/requests/models/model.go
@@ -8,29 +8,29 @@ import (
)
type AddProviderRequest struct {
- Name string `json:"name" binding:"required"`
- Vendor string `json:"vendor" binding:"required"`
- BaseURL string `json:"base_url"`
- URL string `json:"url"`
- Endpoints []EndpointDTO `json:"endpoints"`
- APIKey string `json:"api_key"`
- Metadata map[string]string `json:"metadata"`
- Active *bool `json:"active"`
- Category string `json:"category"` // "llm" or "image", defaults to "llm"
- DefaultProviderImageGenerate *bool `json:"default_provider_image_generate"`
- DefaultProviderImageEdit *bool `json:"default_provider_image_edit"`
+ Name string `json:"name" binding:"required"`
+ Vendor string `json:"vendor" binding:"required"`
+ BaseURL string `json:"base_url"`
+ URL string `json:"url"`
+ Endpoints []EndpointDTO `json:"endpoints"`
+ APIKey string `json:"api_key"`
+ Metadata map[string]string `json:"metadata"`
+ Active *bool `json:"active"`
+ Category string `json:"category"` // "llm" or "image", defaults to "llm"
+ DefaultProviderImageGenerate *bool `json:"default_provider_image_generate"`
+ DefaultProviderImageEdit *bool `json:"default_provider_image_edit"`
}
type UpdateProviderRequest struct {
- Name *string `json:"name"`
- BaseURL *string `json:"base_url"`
- URL *string `json:"url"`
- Endpoints []EndpointDTO `json:"endpoints"`
- APIKey *string `json:"api_key"`
- Metadata *map[string]string `json:"metadata"`
- Active *bool `json:"active"`
- DefaultProviderImageGenerate *bool `json:"default_provider_image_generate"`
- DefaultProviderImageEdit *bool `json:"default_provider_image_edit"`
+ Name *string `json:"name"`
+ BaseURL *string `json:"base_url"`
+ URL *string `json:"url"`
+ Endpoints []EndpointDTO `json:"endpoints"`
+ APIKey *string `json:"api_key"`
+ Metadata *map[string]string `json:"metadata"`
+ Active *bool `json:"active"`
+ DefaultProviderImageGenerate *bool `json:"default_provider_image_generate"`
+ DefaultProviderImageEdit *bool `json:"default_provider_image_edit"`
}
type EndpointDTO struct {
diff --git a/services/llm-api/internal/interfaces/httpserver/responses/model/model.go b/services/llm-api/internal/interfaces/httpserver/responses/model/model.go
index 8bb7ae9c..8f6b87df 100644
--- a/services/llm-api/internal/interfaces/httpserver/responses/model/model.go
+++ b/services/llm-api/internal/interfaces/httpserver/responses/model/model.go
@@ -52,45 +52,45 @@ type ModelWithProviderResponseList struct {
}
type ProviderResponse struct {
- ID string `json:"id"`
- Name string `json:"name"`
- Vendor string `json:"vendor"`
- BaseURL string `json:"base_url"`
- Endpoints []EndpointResponse `json:"endpoints,omitempty"`
- Active bool `json:"active"`
- Category string `json:"category"`
- DefaultProviderImageGenerate bool `json:"default_provider_image_generate"`
- DefaultProviderImageEdit bool `json:"default_provider_image_edit"`
- Metadata map[string]string `json:"metadata,omitempty"`
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Vendor string `json:"vendor"`
+ BaseURL string `json:"base_url"`
+ Endpoints []EndpointResponse `json:"endpoints,omitempty"`
+ Active bool `json:"active"`
+ Category string `json:"category"`
+ DefaultProviderImageGenerate bool `json:"default_provider_image_generate"`
+ DefaultProviderImageEdit bool `json:"default_provider_image_edit"`
+ Metadata map[string]string `json:"metadata,omitempty"`
}
type ProviderWithModelCountResponse struct {
- ID string `json:"id"`
- Name string `json:"name"`
- Vendor string `json:"vendor"`
- BaseURL string `json:"base_url"`
- Endpoints []EndpointResponse `json:"endpoints,omitempty"`
- Active bool `json:"active"`
- Category string `json:"category"`
- DefaultProviderImageGenerate bool `json:"default_provider_image_generate"`
- DefaultProviderImageEdit bool `json:"default_provider_image_edit"`
- ModelCount int64 `json:"model_count"`
- ModelActiveCount int64 `json:"model_active_count"`
- Metadata map[string]string `json:"metadata,omitempty"`
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Vendor string `json:"vendor"`
+ BaseURL string `json:"base_url"`
+ Endpoints []EndpointResponse `json:"endpoints,omitempty"`
+ Active bool `json:"active"`
+ Category string `json:"category"`
+ DefaultProviderImageGenerate bool `json:"default_provider_image_generate"`
+ DefaultProviderImageEdit bool `json:"default_provider_image_edit"`
+ ModelCount int64 `json:"model_count"`
+ ModelActiveCount int64 `json:"model_active_count"`
+ Metadata map[string]string `json:"metadata,omitempty"`
}
type ProviderWithModelsResponse struct {
- ID string `json:"id"`
- Name string `json:"name"`
- Vendor string `json:"vendor"`
- BaseURL string `json:"base_url"`
- Endpoints []EndpointResponse `json:"endpoints,omitempty"`
- Models []ModelResponse `json:"models"`
- Active bool `json:"active"`
- Category string `json:"category"`
- DefaultProviderImageGenerate bool `json:"default_provider_image_generate"`
- DefaultProviderImageEdit bool `json:"default_provider_image_edit"`
- Metadata map[string]string `json:"metadata,omitempty"`
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Vendor string `json:"vendor"`
+ BaseURL string `json:"base_url"`
+ Endpoints []EndpointResponse `json:"endpoints,omitempty"`
+ Models []ModelResponse `json:"models"`
+ Active bool `json:"active"`
+ Category string `json:"category"`
+ DefaultProviderImageGenerate bool `json:"default_provider_image_generate"`
+ DefaultProviderImageEdit bool `json:"default_provider_image_edit"`
+ Metadata map[string]string `json:"metadata,omitempty"`
}
type ProviderResponseList struct {
@@ -255,16 +255,16 @@ func BuildModelResponseList(
func BuildProviderResponse(provider *domainmodel.Provider) ProviderResponse {
return ProviderResponse{
- ID: provider.PublicID,
- Name: provider.DisplayName,
- Vendor: strings.ToLower(string(provider.Kind)),
- BaseURL: provider.BaseURL,
- Endpoints: buildEndpointResponses(provider.GetEndpoints()),
- Active: provider.Active,
- Category: string(provider.Category),
+ ID: provider.PublicID,
+ Name: provider.DisplayName,
+ Vendor: strings.ToLower(string(provider.Kind)),
+ BaseURL: provider.BaseURL,
+ Endpoints: buildEndpointResponses(provider.GetEndpoints()),
+ Active: provider.Active,
+ Category: string(provider.Category),
DefaultProviderImageGenerate: provider.DefaultImageGenerate,
DefaultProviderImageEdit: provider.DefaultImageEdit,
- Metadata: provider.Metadata,
+ Metadata: provider.Metadata,
}
}
@@ -274,18 +274,18 @@ func BuildProviderWithModelCountResponse(
activeCount int64,
) ProviderWithModelCountResponse {
return ProviderWithModelCountResponse{
- ID: provider.PublicID,
- Name: provider.DisplayName,
- Vendor: strings.ToLower(string(provider.Kind)),
- BaseURL: provider.BaseURL,
- Endpoints: buildEndpointResponses(provider.GetEndpoints()),
- Active: provider.Active,
- Category: string(provider.Category),
+ ID: provider.PublicID,
+ Name: provider.DisplayName,
+ Vendor: strings.ToLower(string(provider.Kind)),
+ BaseURL: provider.BaseURL,
+ Endpoints: buildEndpointResponses(provider.GetEndpoints()),
+ Active: provider.Active,
+ Category: string(provider.Category),
DefaultProviderImageGenerate: provider.DefaultImageGenerate,
DefaultProviderImageEdit: provider.DefaultImageEdit,
- ModelCount: modelCount,
- ModelActiveCount: activeCount,
- Metadata: provider.Metadata,
+ ModelCount: modelCount,
+ ModelActiveCount: activeCount,
+ Metadata: provider.Metadata,
}
}
@@ -314,17 +314,17 @@ func BuildProviderWithModelsResponse(
})
}
return &ProviderWithModelsResponse{
- ID: provider.PublicID,
- Name: provider.DisplayName,
- Vendor: strings.ToLower(string(provider.Kind)),
- BaseURL: provider.BaseURL,
- Endpoints: buildEndpointResponses(provider.GetEndpoints()),
- Models: modelResponses,
- Active: provider.Active,
- Category: string(provider.Category),
+ ID: provider.PublicID,
+ Name: provider.DisplayName,
+ Vendor: strings.ToLower(string(provider.Kind)),
+ BaseURL: provider.BaseURL,
+ Endpoints: buildEndpointResponses(provider.GetEndpoints()),
+ Models: modelResponses,
+ Active: provider.Active,
+ Category: string(provider.Category),
DefaultProviderImageGenerate: provider.DefaultImageGenerate,
DefaultProviderImageEdit: provider.DefaultImageEdit,
- Metadata: provider.Metadata,
+ Metadata: provider.Metadata,
}
}
diff --git a/services/llm-api/internal/interfaces/httpserver/responses/projectres/responses.go b/services/llm-api/internal/interfaces/httpserver/responses/projectres/responses.go
index 8c4a18e1..d720c4da 100644
--- a/services/llm-api/internal/interfaces/httpserver/responses/projectres/responses.go
+++ b/services/llm-api/internal/interfaces/httpserver/responses/projectres/responses.go
@@ -19,13 +19,13 @@ type ProjectResponse struct {
// ProjectListResponse represents a paginated list of projects
type ProjectListResponse struct {
- Object string `json:"object"`
- Data []ProjectResponse `json:"data"`
- FirstID string `json:"first_id,omitempty"`
- LastID string `json:"last_id,omitempty"`
- NextCursor *string `json:"next_cursor,omitempty"`
- HasMore bool `json:"has_more"`
- Total int64 `json:"total"`
+ Object string `json:"object"`
+ Data []ProjectResponse `json:"data"`
+ FirstID string `json:"first_id,omitempty"`
+ LastID string `json:"last_id,omitempty"`
+ NextCursor *string `json:"next_cursor,omitempty"`
+ HasMore bool `json:"has_more"`
+ Total int64 `json:"total"`
}
// ProjectDeletedResponse represents the delete confirmation response
@@ -64,10 +64,10 @@ func NewProjectListResponse(projects []*project.Project, hasMore bool, nextCurso
}
resp := &ProjectListResponse{
- Object: "list",
- Data: data,
- HasMore: hasMore,
- Total: total,
+ Object: "list",
+ Data: data,
+ HasMore: hasMore,
+ Total: total,
NextCursor: nextCursor,
}
diff --git a/services/llm-api/internal/interfaces/httpserver/routes/v1/admin/model/admin_model_route.go b/services/llm-api/internal/interfaces/httpserver/routes/v1/admin/model/admin_model_route.go
index d865d4e9..577dfd54 100644
--- a/services/llm-api/internal/interfaces/httpserver/routes/v1/admin/model/admin_model_route.go
+++ b/services/llm-api/internal/interfaces/httpserver/routes/v1/admin/model/admin_model_route.go
@@ -18,9 +18,9 @@ const HeaderIncludeProviderData = "X-PROVIDER-DATA"
const MaxExceptModelsLimit = 1000
type AdminModelRoute struct {
- modelHandler *modelHandler.ModelHandler
- modelCatalogHandler *modelHandler.ModelCatalogHandler
- providerModelHandler *modelHandler.ProviderModelHandler
+ modelHandler *modelHandler.ModelHandler
+ modelCatalogHandler *modelHandler.ModelCatalogHandler
+ providerModelHandler *modelHandler.ProviderModelHandler
modelPromptTemplateHandler *modelprompthandler.ModelPromptTemplateHandler
}
@@ -31,9 +31,9 @@ func NewAdminModelRoute(
modelPromptTemplateHandler *modelprompthandler.ModelPromptTemplateHandler,
) *AdminModelRoute {
return &AdminModelRoute{
- modelHandler: modelHandler,
- modelCatalogHandler: modelCatalogHandler,
- providerModelHandler: providerModelHandler,
+ modelHandler: modelHandler,
+ modelCatalogHandler: modelCatalogHandler,
+ providerModelHandler: providerModelHandler,
modelPromptTemplateHandler: modelPromptTemplateHandler,
}
}
@@ -45,7 +45,7 @@ func (route *AdminModelRoute) RegisterRouter(router *gin.RouterGroup) {
catalogRoute := modelsRoute.Group("catalogs")
catalogRoute.GET("", route.ListModelCatalogs)
catalogRoute.POST("/bulk-toggle", route.BulkToggleModelCatalogs)
-
+
// Model Prompt Template endpoints - using dedicated sub-routes with action prefix
// Format: /prompt-templates/list/*model_id, /prompt-templates/assign/*model_id, etc.
promptTemplatesRoute := modelsRoute.Group("prompt-templates")
@@ -54,7 +54,7 @@ func (route *AdminModelRoute) RegisterRouter(router *gin.RouterGroup) {
promptTemplatesRoute.GET("/effective/*model_id", route.modelPromptTemplateHandler.GetEffective)
promptTemplatesRoute.PATCH("/update/:template_key/*model_id", route.modelPromptTemplateHandler.Update)
promptTemplatesRoute.DELETE("/unassign/:template_key/*model_id", route.modelPromptTemplateHandler.Unassign)
-
+
// Model Catalog detail endpoints (wildcard for IDs with slashes)
catalogRoute.GET("/*model_public_id", route.GetModelCatalog)
catalogRoute.PATCH("/*model_public_id", route.UpdateModelCatalog)
diff --git a/services/llm-api/internal/interfaces/httpserver/routes/v1/conversation/branch_route.go b/services/llm-api/internal/interfaces/httpserver/routes/v1/conversation/branch_route.go
index 042e3513..ed93babf 100644
--- a/services/llm-api/internal/interfaces/httpserver/routes/v1/conversation/branch_route.go
+++ b/services/llm-api/internal/interfaces/httpserver/routes/v1/conversation/branch_route.go
@@ -12,9 +12,9 @@ import (
)
type BranchRoute struct {
- handler *conversationhandler.ConversationHandler
- branchHandler *conversationhandler.BranchHandler
- authHandler *authhandler.AuthHandler
+ handler *conversationhandler.ConversationHandler
+ branchHandler *conversationhandler.BranchHandler
+ authHandler *authhandler.AuthHandler
}
func NewBranchRoute(
@@ -31,14 +31,14 @@ func NewBranchRoute(
func (route *BranchRoute) RegisterRouter(router gin.IRouter) {
conversations := router.Group("/conversations")
-
+
// Branch CRUD endpoints
conversations.GET("/:conv_public_id/branches", route.authHandler.WithAppUserAuthChain(route.handler.ConversationMiddleware(), route.listBranches)...)
conversations.POST("/:conv_public_id/branches", route.authHandler.WithAppUserAuthChain(route.handler.ConversationMiddleware(), route.createBranch)...)
conversations.GET("/:conv_public_id/branches/:branch_name", route.authHandler.WithAppUserAuthChain(route.handler.ConversationMiddleware(), route.getBranch)...)
conversations.DELETE("/:conv_public_id/branches/:branch_name", route.authHandler.WithAppUserAuthChain(route.handler.ConversationMiddleware(), route.deleteBranch)...)
conversations.POST("/:conv_public_id/branches/:branch_name/activate", route.authHandler.WithAppUserAuthChain(route.handler.ConversationMiddleware(), route.activateBranch)...)
-
+
// Message action endpoints
conversations.POST("/:conv_public_id/items/:item_id/edit", route.authHandler.WithAppUserAuthChain(route.handler.ConversationMiddleware(), route.editMessage)...)
conversations.POST("/:conv_public_id/items/:item_id/regenerate", route.authHandler.WithAppUserAuthChain(route.handler.ConversationMiddleware(), route.regenerateMessage)...)