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 ( +
+
+
+

Redirecting...

+
+
+); +``` + +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)...)