Compare commits

..

1 Commits

Author SHA1 Message Date
Minh141120
ad351d62c6 feat: add rpm package linux 2025-10-08 17:39:11 +07:00
249 changed files with 1502 additions and 10572 deletions

View File

@ -1,5 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Jan Discussions
url: https://github.com/orgs/janhq/discussions/categories/q-a
url: https://github.com/orgs/menloresearch/discussions/categories/q-a
about: Get help, discuss features & roadmap, and share your projects

View File

@ -12,7 +12,7 @@ jobs:
build-and-preview:
runs-on: [ubuntu-24-04-docker]
env:
MENLO_PLATFORM_BASE_URL: "https://api-dev.jan.ai/v1"
JAN_API_BASE: "https://api-dev.menlo.ai/v1"
permissions:
pull-requests: write
contents: write
@ -52,7 +52,7 @@ jobs:
- name: Build docker image
run: |
docker build --build-arg MENLO_PLATFORM_BASE_URL=${{ env.MENLO_PLATFORM_BASE_URL }} -t ${{ steps.vars.outputs.FULL_IMAGE }} .
docker build --build-arg JAN_API_BASE=${{ env.JAN_API_BASE }} -t ${{ steps.vars.outputs.FULL_IMAGE }} .
- name: Push docker image
if: github.event_name == 'push'

View File

@ -13,7 +13,7 @@ jobs:
deployments: write
pull-requests: write
env:
MENLO_PLATFORM_BASE_URL: "https://api.jan.ai/v1"
JAN_API_BASE: "https://api.menlo.ai/v1"
GA_MEASUREMENT_ID: "G-YK53MX8M8M"
CLOUDFLARE_PROJECT_NAME: "jan-server-web"
steps:
@ -43,7 +43,7 @@ jobs:
- name: Install dependencies
run: make config-yarn && yarn install && yarn build:core && make build-web-app
env:
MENLO_PLATFORM_BASE_URL: ${{ env.MENLO_PLATFORM_BASE_URL }}
JAN_API_BASE: ${{ env.JAN_API_BASE }}
GA_MEASUREMENT_ID: ${{ env.GA_MEASUREMENT_ID }}
- name: Publish to Cloudflare Pages Production

View File

@ -12,7 +12,7 @@ jobs:
build-and-preview:
runs-on: [ubuntu-24-04-docker]
env:
MENLO_PLATFORM_BASE_URL: "https://api-stag.jan.ai/v1"
JAN_API_BASE: "https://api-stag.menlo.ai/v1"
permissions:
pull-requests: write
contents: write
@ -52,7 +52,7 @@ jobs:
- name: Build docker image
run: |
docker build --build-arg MENLO_PLATFORM_BASE_URL=${{ env.MENLO_PLATFORM_BASE_URL }} -t ${{ steps.vars.outputs.FULL_IMAGE }} .
docker build --build-arg JAN_API_BASE=${{ env.JAN_API_BASE }} -t ${{ steps.vars.outputs.FULL_IMAGE }} .
- name: Push docker image
if: github.event_name == 'push'

View File

@ -168,62 +168,62 @@ jobs:
AWS_DEFAULT_REGION: ${{ secrets.DELTA_AWS_REGION }}
AWS_EC2_METADATA_DISABLED: 'true'
# noti-discord-nightly-and-update-url-readme:
# needs:
# [
# build-macos,
# build-windows-x64,
# build-linux-x64,
# get-update-version,
# set-public-provider,
# sync-temp-to-latest,
# ]
# secrets: inherit
# if: github.event_name == 'schedule'
# uses: ./.github/workflows/template-noti-discord-and-update-url-readme.yml
# with:
# ref: refs/heads/dev
# build_reason: Nightly
# push_to_branch: dev
# new_version: ${{ needs.get-update-version.outputs.new_version }}
noti-discord-nightly-and-update-url-readme:
needs:
[
build-macos,
build-windows-x64,
build-linux-x64,
get-update-version,
set-public-provider,
sync-temp-to-latest,
]
secrets: inherit
if: github.event_name == 'schedule'
uses: ./.github/workflows/template-noti-discord-and-update-url-readme.yml
with:
ref: refs/heads/dev
build_reason: Nightly
push_to_branch: dev
new_version: ${{ needs.get-update-version.outputs.new_version }}
# noti-discord-pre-release-and-update-url-readme:
# needs:
# [
# build-macos,
# build-windows-x64,
# build-linux-x64,
# get-update-version,
# set-public-provider,
# sync-temp-to-latest,
# ]
# secrets: inherit
# if: github.event_name == 'push'
# uses: ./.github/workflows/template-noti-discord-and-update-url-readme.yml
# with:
# ref: refs/heads/dev
# build_reason: Pre-release
# push_to_branch: dev
# new_version: ${{ needs.get-update-version.outputs.new_version }}
noti-discord-pre-release-and-update-url-readme:
needs:
[
build-macos,
build-windows-x64,
build-linux-x64,
get-update-version,
set-public-provider,
sync-temp-to-latest,
]
secrets: inherit
if: github.event_name == 'push'
uses: ./.github/workflows/template-noti-discord-and-update-url-readme.yml
with:
ref: refs/heads/dev
build_reason: Pre-release
push_to_branch: dev
new_version: ${{ needs.get-update-version.outputs.new_version }}
# noti-discord-manual-and-update-url-readme:
# needs:
# [
# build-macos,
# build-windows-x64,
# build-linux-x64,
# get-update-version,
# set-public-provider,
# sync-temp-to-latest,
# ]
# secrets: inherit
# if: github.event_name == 'workflow_dispatch' && github.event.inputs.public_provider == 'aws-s3'
# uses: ./.github/workflows/template-noti-discord-and-update-url-readme.yml
# with:
# ref: refs/heads/dev
# build_reason: Manual
# push_to_branch: dev
# new_version: ${{ needs.get-update-version.outputs.new_version }}
noti-discord-manual-and-update-url-readme:
needs:
[
build-macos,
build-windows-x64,
build-linux-x64,
get-update-version,
set-public-provider,
sync-temp-to-latest,
]
secrets: inherit
if: github.event_name == 'workflow_dispatch' && github.event.inputs.public_provider == 'aws-s3'
uses: ./.github/workflows/template-noti-discord-and-update-url-readme.yml
with:
ref: refs/heads/dev
build_reason: Manual
push_to_branch: dev
new_version: ${{ needs.get-update-version.outputs.new_version }}
comment-pr-build-url:
needs:

View File

@ -82,11 +82,11 @@ jobs:
VERSION=${{ needs.get-update-version.outputs.new_version }}
PUB_DATE=$(date -u +"%Y-%m-%dT%H:%M:%S.%3NZ")
LINUX_SIGNATURE="${{ needs.build-linux-x64.outputs.APPIMAGE_SIG }}"
LINUX_URL="https://github.com/janhq/jan/releases/download/v${{ needs.get-update-version.outputs.new_version }}/${{ needs.build-linux-x64.outputs.APPIMAGE_FILE_NAME }}"
LINUX_URL="https://github.com/menloresearch/jan/releases/download/v${{ needs.get-update-version.outputs.new_version }}/${{ needs.build-linux-x64.outputs.APPIMAGE_FILE_NAME }}"
WINDOWS_SIGNATURE="${{ needs.build-windows-x64.outputs.WIN_SIG }}"
WINDOWS_URL="https://github.com/janhq/jan/releases/download/v${{ needs.get-update-version.outputs.new_version }}/${{ needs.build-windows-x64.outputs.FILE_NAME }}"
WINDOWS_URL="https://github.com/menloresearch/jan/releases/download/v${{ needs.get-update-version.outputs.new_version }}/${{ needs.build-windows-x64.outputs.FILE_NAME }}"
DARWIN_SIGNATURE="${{ needs.build-macos.outputs.MAC_UNIVERSAL_SIG }}"
DARWIN_URL="https://github.com/janhq/jan/releases/download/v${{ needs.get-update-version.outputs.new_version }}/${{ needs.build-macos.outputs.TAR_NAME }}"
DARWIN_URL="https://github.com/menloresearch/jan/releases/download/v${{ needs.get-update-version.outputs.new_version }}/${{ needs.build-macos.outputs.TAR_NAME }}"
jq --arg version "$VERSION" \
--arg pub_date "$PUB_DATE" \

View File

@ -29,7 +29,7 @@ jobs:
local max_retries=3
local tag
while [ $retries -lt $max_retries ]; do
tag=$(curl -s https://api.github.com/repos/janhq/jan/releases/latest | jq -r .tag_name)
tag=$(curl -s https://api.github.com/repos/menloresearch/jan/releases/latest | jq -r .tag_name)
if [ -n "$tag" ] && [ "$tag" != "null" ]; then
echo $tag
return

View File

@ -50,6 +50,6 @@ jobs:
- macOS Universal: https://delta.jan.ai/nightly/Jan-nightly_{{ VERSION }}_universal.dmg
- Linux Deb: https://delta.jan.ai/nightly/Jan-nightly_{{ VERSION }}_amd64.deb
- Linux AppImage: https://delta.jan.ai/nightly/Jan-nightly_{{ VERSION }}_amd64.AppImage
- Github action run: https://github.com/janhq/jan/actions/runs/{{ GITHUB_RUN_ID }}
- Github action run: https://github.com/menloresearch/jan/actions/runs/{{ GITHUB_RUN_ID }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}

View File

@ -143,7 +143,7 @@ jan/
**Option 1: The Easy Way (Make)**
```bash
git clone https://github.com/janhq/jan
git clone https://github.com/menloresearch/jan
cd jan
make dev
```
@ -152,8 +152,8 @@ make dev
### Reporting Bugs
- **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/janhq/jan/issues)
- If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/janhq/jan/issues/new)
- **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/menloresearch/jan/issues)
- If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/menloresearch/jan/issues/new)
- Include your system specs and error logs - it helps a ton
### Suggesting Enhancements

View File

@ -1,8 +1,8 @@
# Stage 1: Build stage with Node.js and Yarn v4
FROM node:20-alpine AS builder
ARG MENLO_PLATFORM_BASE_URL=https://api-dev.menlo.ai/v1
ENV MENLO_PLATFORM_BASE_URL=$MENLO_PLATFORM_BASE_URL
ARG JAN_API_BASE=https://api-dev.jan.ai/v1
ENV JAN_API_BASE=$JAN_API_BASE
# Install build dependencies
RUN apk add --no-cache \

View File

@ -4,10 +4,10 @@
<p align="center">
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
<img alt="GitHub commit activity" src="https://img.shields.io/github/commit-activity/m/janhq/jan"/>
<img alt="Github Last Commit" src="https://img.shields.io/github/last-commit/janhq/jan"/>
<img alt="Github Contributors" src="https://img.shields.io/github/contributors/janhq/jan"/>
<img alt="GitHub closed issues" src="https://img.shields.io/github/issues-closed/janhq/jan"/>
<img alt="GitHub commit activity" src="https://img.shields.io/github/commit-activity/m/menloresearch/jan"/>
<img alt="Github Last Commit" src="https://img.shields.io/github/last-commit/menloresearch/jan"/>
<img alt="Github Contributors" src="https://img.shields.io/github/contributors/menloresearch/jan"/>
<img alt="GitHub closed issues" src="https://img.shields.io/github/issues-closed/menloresearch/jan"/>
<img alt="Discord" src="https://img.shields.io/discord/1107178041848909847?label=discord"/>
</p>
@ -15,7 +15,7 @@
<a href="https://www.jan.ai/docs/desktop">Getting Started</a>
- <a href="https://discord.gg/Exe46xPMbK">Community</a>
- <a href="https://jan.ai/changelog">Changelog</a>
- <a href="https://github.com/janhq/jan/issues">Bug reports</a>
- <a href="https://github.com/menloresearch/jan/issues">Bug reports</a>
</p>
Jan is bringing the best of open-source AI in an easy-to-use product. Download and run LLMs with **full control** and **privacy**.
@ -48,7 +48,7 @@ The easiest way to get started is by downloading one of the following versions f
</table>
Download from [jan.ai](https://jan.ai/) or [GitHub Releases](https://github.com/janhq/jan/releases).
Download from [jan.ai](https://jan.ai/) or [GitHub Releases](https://github.com/menloresearch/jan/releases).
## Features
@ -73,7 +73,7 @@ For those who enjoy the scenic route:
### Run with Make
```bash
git clone https://github.com/janhq/jan
git clone https://github.com/menloresearch/jan
cd jan
make dev
```
@ -128,7 +128,7 @@ Contributions welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full spiel
## Contact
- **Bugs**: [GitHub Issues](https://github.com/janhq/jan/issues)
- **Bugs**: [GitHub Issues](https://github.com/menloresearch/jan/issues)
- **Business**: hello@jan.ai
- **Jobs**: hr@jan.ai
- **General Discussion**: [Discord](https://discord.gg/FTk2MvZwJH)

View File

@ -1,7 +1,7 @@
# Core dependencies
cua-computer[all]~=0.3.5
cua-agent[all]~=0.3.0
cua-agent @ git+https://github.com/janhq/cua.git@compute-agent-0.3.0-patch#subdirectory=libs/python/agent
cua-agent @ git+https://github.com/menloresearch/cua.git@compute-agent-0.3.0-patch#subdirectory=libs/python/agent
# ReportPortal integration
reportportal-client~=5.6.5

View File

@ -13,7 +13,7 @@ import * as core from '@janhq/core'
## Build an Extension
1. Download an extension template, for example, [https://github.com/janhq/extension-template](https://github.com/janhq/extension-template).
1. Download an extension template, for example, [https://github.com/menloresearch/extension-template](https://github.com/menloresearch/extension-template).
2. Update the source code:

View File

@ -31,7 +31,7 @@
"@vitest/coverage-v8": "^2.1.8",
"@vitest/ui": "^2.1.8",
"eslint": "8.57.0",
"happy-dom": "^20.0.0",
"happy-dom": "^15.11.6",
"pacote": "^21.0.0",
"react": "19.0.0",
"request": "^2.88.2",

View File

@ -11,8 +11,6 @@ export enum ExtensionTypeEnum {
HuggingFace = 'huggingFace',
Engine = 'engine',
Hardware = 'hardware',
RAG = 'rag',
VectorDB = 'vectorDB',
}
export interface ExtensionType {

View File

@ -182,7 +182,6 @@ export interface SessionInfo {
port: number // llama-server output port (corrected from portid)
model_id: string //name of the model
model_path: string // path of the loaded model
is_embedding: boolean
api_key: string
mmproj_path?: string
}

View File

@ -23,8 +23,3 @@ export { MCPExtension } from './mcp'
* Base AI Engines.
*/
export * from './engines'
export { RAGExtension, RAG_INTERNAL_SERVER } from './rag'
export type { AttachmentInput, IngestAttachmentsResult } from './rag'
export { VectorDBExtension } from './vector-db'
export type { SearchMode, VectorDBStatus, VectorChunkInput, VectorSearchResult, AttachmentFileInfo, VectorDBFileInput, VectorDBIngestOptions } from './vector-db'

View File

@ -1,36 +0,0 @@
import { BaseExtension, ExtensionTypeEnum } from '../extension'
import type { MCPTool, MCPToolCallResult } from '../../types'
import type { AttachmentFileInfo } from './vector-db'
export interface AttachmentInput {
path: string
name?: string
type?: string
size?: number
}
export interface IngestAttachmentsResult {
filesProcessed: number
chunksInserted: number
files: AttachmentFileInfo[]
}
export const RAG_INTERNAL_SERVER = 'rag-internal'
/**
* RAG extension base: exposes RAG tools and orchestration API.
*/
export abstract class RAGExtension extends BaseExtension {
type(): ExtensionTypeEnum | undefined {
return ExtensionTypeEnum.RAG
}
abstract getTools(): Promise<MCPTool[]>
/**
* Lightweight list of tool names for quick routing/lookup.
*/
abstract getToolNames(): Promise<string[]>
abstract callTool(toolName: string, args: Record<string, unknown>): Promise<MCPToolCallResult>
abstract ingestAttachments(threadId: string, files: AttachmentInput[]): Promise<IngestAttachmentsResult>
}

View File

@ -1,82 +0,0 @@
import { BaseExtension, ExtensionTypeEnum } from '../extension'
export type SearchMode = 'auto' | 'ann' | 'linear'
export interface VectorDBStatus {
ann_available: boolean
}
export interface VectorChunkInput {
text: string
embedding: number[]
}
export interface VectorSearchResult {
id: string
text: string
score?: number
file_id: string
chunk_file_order: number
}
export interface AttachmentFileInfo {
id: string
name?: string
path?: string
type?: string
size?: number
chunk_count: number
}
// High-level input types for file ingestion
export interface VectorDBFileInput {
path: string
name?: string
type?: string
size?: number
}
export interface VectorDBIngestOptions {
chunkSize: number
chunkOverlap: number
}
/**
* Vector DB extension base: abstraction over local vector storage and search.
*/
export abstract class VectorDBExtension extends BaseExtension {
type(): ExtensionTypeEnum | undefined {
return ExtensionTypeEnum.VectorDB
}
abstract getStatus(): Promise<VectorDBStatus>
abstract createCollection(threadId: string, dimension: number): Promise<void>
abstract insertChunks(
threadId: string,
fileId: string,
chunks: VectorChunkInput[]
): Promise<void>
abstract ingestFile(
threadId: string,
file: VectorDBFileInput,
opts: VectorDBIngestOptions
): Promise<AttachmentFileInfo>
abstract searchCollection(
threadId: string,
query_embedding: number[],
limit: number,
threshold: number,
mode?: SearchMode,
fileIds?: string[]
): Promise<VectorSearchResult[]>
abstract deleteChunks(threadId: string, ids: string[]): Promise<void>
abstract deleteFile(threadId: string, fileId: string): Promise<void>
abstract deleteCollection(threadId: string): Promise<void>
abstract listAttachments(threadId: string, limit?: number): Promise<AttachmentFileInfo[]>
abstract getChunks(
threadId: string,
fileId: string,
startOrder: number,
endOrder: number
): Promise<VectorSearchResult[]>
}

View File

@ -12,8 +12,6 @@ export type SettingComponentProps = {
extensionName?: string
requireModelReload?: boolean
configType?: ConfigType
titleKey?: string
descriptionKey?: string
}
export type ConfigType = 'runtime' | 'setting'

View File

@ -18,7 +18,7 @@ We try to **keep routes consistent** to maintain SEO.
## How to Contribute
Refer to the [Contributing Guide](https://github.com/janhq/jan/blob/main/CONTRIBUTING.md) for more comprehensive information on how to contribute to the Jan project.
Refer to the [Contributing Guide](https://github.com/menloresearch/jan/blob/main/CONTRIBUTING.md) for more comprehensive information on how to contribute to the Jan project.
### Pre-requisites and Installation

View File

@ -1581,7 +1581,7 @@
},
"cover": {
"type": "string",
"example": "https://raw.githubusercontent.com/janhq/jan/main/models/trinity-v1.2-7b/cover.png"
"example": "https://raw.githubusercontent.com/menloresearch/jan/main/models/trinity-v1.2-7b/cover.png"
},
"engine": {
"type": "string",

View File

@ -27,7 +27,7 @@ export const APIReference = () => {
<ApiReferenceReact
configuration={{
spec: {
url: 'https://raw.githubusercontent.com/janhq/docs/main/public/openapi/jan.json',
url: 'https://raw.githubusercontent.com/menloresearch/docs/main/public/openapi/jan.json',
},
theme: 'alternate',
hideModels: true,

View File

@ -57,7 +57,7 @@ const Changelog = () => {
<p className="text-base mt-2 leading-relaxed">
Latest release updates from the Jan team. Check out our&nbsp;
<a
href="https://github.com/orgs/janhq/projects/30"
href="https://github.com/orgs/menloresearch/projects/30"
className="text-blue-600 dark:text-blue-400 cursor-pointer"
>
Roadmap
@ -150,7 +150,7 @@ const Changelog = () => {
<div className="text-center">
<Link
href="https://github.com/janhq/jan/releases"
href="https://github.com/menloresearch/jan/releases"
target="_blank"
className="dark:nx-bg-neutral-900 dark:text-white bg-black text-white hover:text-white justify-center dark:border dark:border-neutral-800 flex-shrink-0 px-4 py-3 rounded-xl inline-flex items-center"
>

View File

@ -72,7 +72,7 @@ export default function CardDownload({ lastRelease }: Props) {
return {
...system,
href: `https://github.com/janhq/jan/releases/download/${lastRelease.tag_name}/${downloadUrl}`,
href: `https://github.com/menloresearch/jan/releases/download/${lastRelease.tag_name}/${downloadUrl}`,
size: asset ? formatFileSize(asset.size) : undefined,
}
})

View File

@ -139,7 +139,7 @@ const DropdownDownload = ({ lastRelease }: Props) => {
return {
...system,
href: `https://github.com/janhq/jan/releases/download/${lastRelease.tag_name}/${downloadUrl}`,
href: `https://github.com/menloresearch/jan/releases/download/${lastRelease.tag_name}/${downloadUrl}`,
size: asset ? formatFileSize(asset.size) : undefined,
}
})

View File

@ -23,7 +23,7 @@ const BuiltWithLove = () => {
</div>
<div className="flex flex-col lg:flex-row gap-8 mt-8 items-center justify-center">
<a
href="https://github.com/janhq/jan"
href="https://github.com/menloresearch/jan"
target="_blank"
className="dark:bg-white bg-black inline-flex w-56 px-4 py-3 rounded-xl cursor-pointer justify-center items-start space-x-4 "
>

View File

@ -44,7 +44,7 @@ const Hero = () => {
<div className="mt-10 text-center">
<div>
<Link
href="https://github.com/janhq/jan/releases"
href="https://github.com/menloresearch/jan/releases"
target="_blank"
className="hidden lg:inline-block"
>

View File

@ -95,7 +95,7 @@ const Home = () => {
<div className="container mx-auto relative z-10">
<div className="flex justify-center items-center mt-14 lg:mt-20 px-4">
<a
href={`https://github.com/janhq/jan/releases/tag/${lastVersion}`}
href={`https://github.com/menloresearch/jan/releases/tag/${lastVersion}`}
target="_blank"
rel="noopener noreferrer"
className="bg-black/40 px-3 lg:px-4 rounded-full h-10 inline-flex items-center max-w-full animate-fade-in delay-100"
@ -270,7 +270,7 @@ const Home = () => {
data-delay="600"
>
<a
href="https://github.com/janhq/jan"
href="https://github.com/menloresearch/jan"
target="_blank"
rel="noopener noreferrer"
>
@ -387,7 +387,7 @@ const Home = () => {
</div>
<a
className="hidden md:block"
href="https://github.com/janhq/jan"
href="https://github.com/menloresearch/jan"
target="_blank"
rel="noopener noreferrer"
>
@ -413,7 +413,7 @@ const Home = () => {
</p>
<a
className="md:hidden mt-4 block w-full"
href="https://github.com/janhq/jan"
href="https://github.com/menloresearch/jan"
target="_blank"
rel="noopener noreferrer"
>

View File

@ -95,7 +95,7 @@ const Navbar = ({ noScroll }: { noScroll?: boolean }) => {
})}
<li>
<a
href="https://github.com/janhq/jan/releases/latest"
href="https://github.com/menloresearch/jan/releases/latest"
target="_blank"
rel="noopener noreferrer"
>
@ -141,7 +141,7 @@ const Navbar = ({ noScroll }: { noScroll?: boolean }) => {
<FaLinkedinIn className="size-5" />
</a>
<a
href="https://github.com/janhq/jan"
href="https://github.com/menloresearch/jan"
target="_blank"
rel="noopener noreferrer"
className="rounded-lg flex items-center justify-center"
@ -156,7 +156,7 @@ const Navbar = ({ noScroll }: { noScroll?: boolean }) => {
{/* Mobile Download Button and Hamburger */}
<div className="lg:hidden flex items-center gap-3">
<a
href="https://github.com/janhq/jan/releases/latest"
href="https://github.com/menloresearch/jan/releases/latest"
target="_blank"
rel="noopener noreferrer"
>
@ -278,7 +278,7 @@ const Navbar = ({ noScroll }: { noScroll?: boolean }) => {
<FaLinkedinIn className="size-5" />
</a>
<a
href="https://github.com/janhq/jan"
href="https://github.com/menloresearch/jan"
target="_blank"
rel="noopener noreferrer"
className="text-black rounded-lg flex items-center justify-center"
@ -296,7 +296,7 @@ const Navbar = ({ noScroll }: { noScroll?: boolean }) => {
asChild
>
<a
href="https://github.com/janhq/jan/releases/latest"
href="https://github.com/menloresearch/jan/releases/latest"
target="_blank"
rel="noopener noreferrer"
>

View File

@ -120,7 +120,7 @@ export function DropdownButton({
return {
...option,
href: `https://github.com/janhq/jan/releases/download/${lastRelease.tag_name}/${fileName}`,
href: `https://github.com/menloresearch/jan/releases/download/${lastRelease.tag_name}/${fileName}`,
size: asset ? formatFileSize(asset.size) : 'N/A',
}
})

View File

@ -18,7 +18,7 @@ description: Development setup, workflow, and contribution guidelines for Jan Se
1. **Clone Repository**
```bash
git clone https://github.com/janhq/jan-server
git clone https://github.com/menloresearch/jan-server
cd jan-server
```

View File

@ -19,7 +19,7 @@ Jan Server currently supports minikube for local development. Production Kuberne
1. **Clone the repository**
```bash
git clone https://github.com/janhq/jan-server
git clone https://github.com/menloresearch/jan-server
cd jan-server
```

View File

@ -24,4 +24,4 @@ Fixes 💫
Update your product or download the latest: https://jan.ai
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.5).
For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.5).

View File

@ -24,4 +24,4 @@ Jan now supports Mistral's new model Codestral. Thanks [Bartowski](https://huggi
More GGUF models can run in Jan - we rebased to llama.cpp b3012.Big thanks to [ggerganov](https://github.com/ggerganov)
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.0).
For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.0).

View File

@ -28,4 +28,4 @@ Jan now understands LaTeX, allowing users to process and understand complex math
![Latex](https://catalog.jan.ai/docs/jan_update_latex.gif)
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.4.12).
For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.4.12).

View File

@ -28,4 +28,4 @@ Users can now connect to OpenAI's new model GPT-4o.
![GPT4o](https://catalog.jan.ai/docs/jan_v0_4_13_openai_gpt4o.gif)
For more details, see the [GitHub release notes.](https://github.com/janhq/jan/releases/tag/v0.4.13)
For more details, see the [GitHub release notes.](https://github.com/menloresearch/jan/releases/tag/v0.4.13)

View File

@ -16,4 +16,4 @@ More GGUF models can run in Jan - we rebased to llama.cpp b2961.
Huge shoutouts to [ggerganov](https://github.com/ggerganov) and contributors for llama.cpp, and [Bartowski](https://huggingface.co/bartowski) for GGUF models.
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.4.14).
For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.4.14).

View File

@ -26,4 +26,4 @@ We've updated to llama.cpp b3088 for better performance - thanks to [GG](https:/
- Reduced chat font weight (back to normal!)
- Restored the maximize button
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.1).
For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.1).

View File

@ -32,4 +32,4 @@ We've restored the tooltip hover functionality, which makes it easier to access
The right-click options for thread settings are now fully operational again. You can now manage your threads with this fix.
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.2).
For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.2).

View File

@ -23,4 +23,4 @@ We've been working on stability issues over the last few weeks. Jan is now more
- Fixed the GPU memory utilization bar
- Some UX and copy improvements
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.3).
For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.3).

View File

@ -32,4 +32,4 @@ Switching between threads used to reset your instruction settings. Thats fixe
### Minor UI Tweaks & Bug Fixes
Weve also resolved issues with the input slider on the right panel and tackled several smaller bugs to keep everything running smoothly.
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.4).
For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.4).

View File

@ -23,4 +23,4 @@ Fixes 💫
Update your product or download the latest: https://jan.ai
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.7).
For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.7).

View File

@ -22,4 +22,4 @@ Jan v0.5.9 is here: fixing what needed fixing
Update your product or download the latest: https://jan.ai
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.9).
For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.9).

View File

@ -22,4 +22,4 @@ and various UI/UX enhancements 💫
Update your product or download the latest: https://jan.ai
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.8).
For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.8).

View File

@ -19,4 +19,4 @@ Jan v0.5.10 is live: Jan is faster, smoother, and more reliable.
Update your product or download the latest: https://jan.ai
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.10).
For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.10).

View File

@ -23,4 +23,4 @@ Jan v0.5.11 is here - critical issues fixed, Mac installation updated.
Update your product or download the latest: https://jan.ai
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.11).
For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.11).

View File

@ -25,4 +25,4 @@ Jan v0.5.11 is here - critical issues fixed, Mac installation updated.
Update your product or download the latest: https://jan.ai
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.12).
For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.12).

View File

@ -20,4 +20,4 @@ import ChangelogHeader from "@/components/Changelog/ChangelogHeader"
Update your product or download the latest: https://jan.ai
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.13).
For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.13).

View File

@ -33,4 +33,4 @@ Llama
Update your Jan or [download the latest](https://jan.ai/).
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.14).
For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.14).

View File

@ -25,4 +25,4 @@ import ChangelogHeader from "@/components/Changelog/ChangelogHeader"
Update your Jan or [download the latest](https://jan.ai/).
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.15).
For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.15).

View File

@ -26,4 +26,4 @@ import ChangelogHeader from "@/components/Changelog/ChangelogHeader"
Update your Jan or [download the latest](https://jan.ai/).
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.16).
For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.16).

View File

@ -20,4 +20,4 @@ import ChangelogHeader from "@/components/Changelog/ChangelogHeader"
Update your Jan or [download the latest](https://jan.ai/).
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.17).
For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.17).

View File

@ -18,4 +18,4 @@ import ChangelogHeader from "@/components/Changelog/ChangelogHeader"
Update your Jan or [download the latest](https://jan.ai/).
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.6.1).
For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.6.1).

View File

@ -18,4 +18,4 @@ import ChangelogHeader from "@/components/Changelog/ChangelogHeader"
Update your Jan or [download the latest](https://jan.ai/).
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.6.3).
For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.6.3).

View File

@ -23,4 +23,4 @@ new MCP examples.
Update your Jan or [download the latest](https://jan.ai/).
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.6.5).
For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.6.5).

View File

@ -116,4 +116,4 @@ integrations. Stay tuned!
Update your Jan or [download the latest](https://jan.ai/).
For the complete list of changes, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.6.6).
For the complete list of changes, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.6.6).

View File

@ -89,4 +89,4 @@ We're continuing to optimize performance for large models, expand MCP integratio
Update your Jan or [download the latest](https://jan.ai/).
For the complete list of changes, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.6.7).
For the complete list of changes, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.6.7).

View File

@ -74,4 +74,4 @@ v0.6.8 focuses on stability and real workflows: major llama.cpp hardening, two n
Update your Jan or [download the latest](https://jan.ai/).
For the complete list of changes, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.6.8).
For the complete list of changes, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.6.8).

View File

@ -1,25 +0,0 @@
---
title: "Jan v0.7.2: Security Update"
version: 0.7.2
description: "Jan v0.7.2 updates the happy-dom dependency to v20.0.0 to address a recently disclosed sandbox vulnerability."
date: 2025-10-16
---
import ChangelogHeader from "@/components/Changelog/ChangelogHeader"
import { Callout } from 'nextra/components'
<ChangelogHeader title="Jan v0.7.2" date="2025-10-16" />
## Jan v0.7.2: Security Update (happy-dom v20)
This release focuses on **security and stability improvements**.
It updates the `happy-dom` dependency to the latest version to address a recently disclosed vulnerability.
### Security Fix
- Updated `happy-dom` to **^20.0.0**, preventing untrusted JavaScript executed within HAPPY DOM from accessing process-level functions and executing arbitrary code outside the intended sandbox.
---
Update your Jan or [download the latest version](https://jan.ai/).
For the complete list of changes, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.7.2).

View File

@ -41,7 +41,7 @@ Jan is an open-source replacement for ChatGPT:
Jan is a full [product suite](https://en.wikipedia.org/wiki/Software_suite) that offers an alternative to Big AI:
- [Jan Desktop](/docs/desktop/quickstart): macOS, Windows, and Linux apps with offline mode
- [Jan Web](https://chat.menlo.ai): Jan on browser, a direct alternative to chatgpt.com
- [Jan Web](https://chat.jan.ai): Jan on browser, a direct alternative to chatgpt.com
- Jan Mobile: iOS and Android apps (Coming Soon)
- [Jan Server](/docs/server): deploy locally, in your cloud, or on-prem
- [Jan Models](/docs/models): Open-source models optimized for deep research, tool use, and reasoning

View File

@ -135,5 +135,5 @@ Min-p: 0.0
## 🤝 Community & Support
- **Discussions**: [HuggingFace Community](https://huggingface.co/Menlo/Jan-nano-128k/discussions)
- **Issues**: [GitHub Repository](https://github.com/janhq/deep-research/issues)
- **Issues**: [GitHub Repository](https://github.com/menloresearch/deep-research/issues)
- **Discord**: Join our research community for tips and best practices

View File

@ -9,7 +9,7 @@ Jan Server is a comprehensive self-hosted AI server platform that provides OpenA
Jan Server is a Kubernetes-native platform consisting of multiple microservices that work together to provide a complete AI infrastructure solution. It offers:
![System Architecture Diagram](https://raw.githubusercontent.com/janhq/jan-server/main/docs/Architect.png)
![System Architecture Diagram](https://raw.githubusercontent.com/menloresearch/jan-server/main/docs/Architect.png)
### Key Features
- **OpenAI-Compatible API**: Full compatibility with OpenAI's chat completion API

View File

@ -3,7 +3,7 @@ title: Development
description: Development setup, workflow, and contribution guidelines for Jan Server.
---
## Core Domain Models
![Domain Models Diagram](https://github.com/janhq/jan-server/raw/main/apps/jan-api-gateway/docs/System_Design.png)
![Domain Models Diagram](https://github.com/menloresearch/jan-server/raw/main/apps/jan-api-gateway/docs/System_Design.png)
## Development Setup
### Prerequisites
@ -42,7 +42,7 @@ description: Development setup, workflow, and contribution guidelines for Jan Se
1. **Clone Repository**
```bash
git clone https://github.com/janhq/jan-server
git clone https://github.com/menloresearch/jan-server
cd jan-server
```

View File

@ -40,7 +40,7 @@ Jan Server is a Kubernetes-native platform consisting of multiple microservices
- **Monitoring & Profiling**: Built-in performance monitoring and health checks
## System Architecture
![System Architecture Diagram](https://raw.githubusercontent.com/janhq/jan-server/main/docs/Architect.png)
![System Architecture Diagram](https://raw.githubusercontent.com/menloresearch/jan-server/main/docs/Architect.png)
## Services
### Jan API Gateway

View File

@ -19,7 +19,7 @@ keywords:
import Download from "@/components/Download"
export const getStaticProps = async() => {
const resRelease = await fetch('https://api.github.com/repos/janhq/jan/releases/latest')
const resRelease = await fetch('https://api.github.com/repos/menloresearch/jan/releases/latest')
const release = await resRelease.json()
return {

View File

@ -19,9 +19,9 @@ keywords:
import Home from "@/components/Home"
export const getStaticProps = async() => {
const resReleaseLatest = await fetch('https://api.github.com/repos/janhq/jan/releases/latest')
const resRelease = await fetch('https://api.github.com/repos/janhq/jan/releases?per_page=500')
const resRepo = await fetch('https://api.github.com/repos/janhq/jan')
const resReleaseLatest = await fetch('https://api.github.com/repos/menloresearch/jan/releases/latest')
const resRelease = await fetch('https://api.github.com/repos/menloresearch/jan/releases?per_page=500')
const resRepo = await fetch('https://api.github.com/repos/menloresearch/jan')
const repo = await resRepo.json()
const latestRelease = await resReleaseLatest.json()
const release = await resRelease.json()

View File

@ -14,12 +14,12 @@ import CTABlog from '@/components/Blog/CTA'
Jan now supports [NVIDIA TensorRT-LLM](https://github.com/NVIDIA/TensorRT-LLM) in addition to [llama.cpp](https://github.com/ggerganov/llama.cpp), making Jan multi-engine and ultra-fast for users with Nvidia GPUs.
We've been excited for TensorRT-LLM for a while, and [had a lot of fun implementing it](https://github.com/janhq/nitro-tensorrt-llm). As part of the process, we've run some benchmarks, to see how TensorRT-LLM fares on consumer hardware (e.g. [4090s](https://www.nvidia.com/en-us/geforce/graphics-cards/40-series/), [3090s](https://www.nvidia.com/en-us/geforce/graphics-cards/30-series/)) we commonly see in the [Jan's hardware community](https://discord.com/channels/1107178041848909847/1201834752206974996).
We've been excited for TensorRT-LLM for a while, and [had a lot of fun implementing it](https://github.com/menloresearch/nitro-tensorrt-llm). As part of the process, we've run some benchmarks, to see how TensorRT-LLM fares on consumer hardware (e.g. [4090s](https://www.nvidia.com/en-us/geforce/graphics-cards/40-series/), [3090s](https://www.nvidia.com/en-us/geforce/graphics-cards/30-series/)) we commonly see in the [Jan's hardware community](https://discord.com/channels/1107178041848909847/1201834752206974996).
<Callout type="info" >
**Give it a try!** Jan's TensorRT-LLM extension is available in Jan v0.4.9. We precompiled some TensorRT-LLM models for you to try: `Mistral 7b`, `TinyLlama-1.1b`, `TinyJensen-1.1b` 😂
Bugs or feedback? Let us know on [GitHub](https://github.com/janhq/jan) or via [Discord](https://discord.com/channels/1107178041848909847/1201832734704795688).
Bugs or feedback? Let us know on [GitHub](https://github.com/menloresearch/jan) or via [Discord](https://discord.com/channels/1107178041848909847/1201832734704795688).
</Callout>
<Callout type="info" >

View File

@ -70,34 +70,34 @@ brief survey of how other players approach deep research:
| Kimi | Interactive synthesis | 50100 | 3060+ | PDF, Interactive website | Free |
In our testing, we used the following prompt to assess the quality of the generated report by
the providers above. You can refer to the reports generated [here](https://github.com/janhq/prompt-experiments).
the providers above. You can refer to the reports generated [here](https://github.com/menloresearch/prompt-experiments).
```
Generate a comprehensive report about the state of AI in the past week. Include all
new model releases and notable architectural improvements from a variety of sources.
```
[Google's generated report](https://github.com/janhq/prompt-experiments/blob/main/Gemini%202.5%20Flash%20Report.pdf) was the most verbose, with a whopping 23 pages that reads
[Google's generated report](https://github.com/menloresearch/prompt-experiments/blob/main/Gemini%202.5%20Flash%20Report.pdf) was the most verbose, with a whopping 23 pages that reads
like a professional intelligence briefing. It opens with an executive summary,
systematically categorizes developments, and provides forward-looking strategic
insights—connecting OpenAI's open-weight release to broader democratization trends
and linking infrastructure investments to competitive positioning.
[OpenAI](https://github.com/janhq/prompt-experiments/blob/main/OpenAI%20Deep%20Research.pdf) produced the most citation-heavy output with 134 references throughout 10 pages
[OpenAI](https://github.com/menloresearch/prompt-experiments/blob/main/OpenAI%20Deep%20Research.pdf) produced the most citation-heavy output with 134 references throughout 10 pages
(albeit most of them being from the same source).
[Perplexity](https://github.com/janhq/prompt-experiments/blob/main/Perplexity%20Deep%20Research.pdf) delivered the most actionable 6-page report that maximizes information
[Perplexity](https://github.com/menloresearch/prompt-experiments/blob/main/Perplexity%20Deep%20Research.pdf) delivered the most actionable 6-page report that maximizes information
density while maintaining scannability. Despite being the shortest, it captures all
major developments with sufficient context for decision-making.
[Claude](https://github.com/janhq/prompt-experiments/blob/main/Claude%20Deep%20Research.pdf) produced a comprehensive analysis that interestingly ignored the time constraint,
[Claude](https://github.com/menloresearch/prompt-experiments/blob/main/Claude%20Deep%20Research.pdf) produced a comprehensive analysis that interestingly ignored the time constraint,
covering an 8-month period from January-August 2025 instead of the requested week (Jul 31-Aug
7th 2025). Rather than cataloging recent events, Claude traced the evolution of trends over months.
[Grok](https://github.com/janhq/prompt-experiments/blob/main/Grok%203%20Deep%20Research.pdf) produced a well-structured but relatively shallow 5-page academic-style report that
[Grok](https://github.com/menloresearch/prompt-experiments/blob/main/Grok%203%20Deep%20Research.pdf) produced a well-structured but relatively shallow 5-page academic-style report that
read more like an event catalog than strategic analysis.
[Kimi](https://github.com/janhq/prompt-experiments/blob/main/Kimi%20AI%20Deep%20Research.pdf) produced a comprehensive 13-page report with systematic organization covering industry developments, research breakthroughs, and policy changes, but notably lacks proper citations throughout most of the content despite claiming to use 50-100 sources.
[Kimi](https://github.com/menloresearch/prompt-experiments/blob/main/Kimi%20AI%20Deep%20Research.pdf) produced a comprehensive 13-page report with systematic organization covering industry developments, research breakthroughs, and policy changes, but notably lacks proper citations throughout most of the content despite claiming to use 50-100 sources.
### Understanding Search Strategies

View File

@ -13,7 +13,7 @@ import CTABlog from '@/components/Blog/CTA'
## Abstract
We present a straightforward approach to customizing small, open-source models using fine-tuning and RAG that outperforms GPT-3.5 for specialized use cases. With it, we achieved superior Q&A results of [technical documentation](https://nitro.jan.ai/docs) for a small codebase [codebase](https://github.com/janhq/nitro).
We present a straightforward approach to customizing small, open-source models using fine-tuning and RAG that outperforms GPT-3.5 for specialized use cases. With it, we achieved superior Q&A results of [technical documentation](https://nitro.jan.ai/docs) for a small codebase [codebase](https://github.com/menloresearch/nitro).
In short, (1) extending a general foundation model like [Mistral](https://huggingface.co/mistralai/Mistral-7B-v0.1) with strong math and coding, and (2) training it over a high-quality, synthetic dataset generated from the intended corpus, and (3) adding RAG capabilities, can lead to significant accuracy improvements.
@ -93,11 +93,11 @@ This final model can be found [here on Huggingface](https://huggingface.co/jan-h
As an additional step, we also added [Retrieval Augmented Generation (RAG)](https://blogs.nvidia.com/blog/what-is-retrieval-augmented-generation/) as an experiment parameter.
A simple RAG setup was done using **[Llamaindex](https://www.llamaindex.ai/)** and the **[bge-en-base-v1.5 embedding](https://huggingface.co/BAAI/bge-base-en-v1.5)** model for efficient documentation retrieval and question-answering. You can find the RAG implementation [here](https://github.com/janhq/open-foundry/blob/main/rag-is-not-enough/rag/nitro_rag.ipynb).
A simple RAG setup was done using **[Llamaindex](https://www.llamaindex.ai/)** and the **[bge-en-base-v1.5 embedding](https://huggingface.co/BAAI/bge-base-en-v1.5)** model for efficient documentation retrieval and question-answering. You can find the RAG implementation [here](https://github.com/menloresearch/open-foundry/blob/main/rag-is-not-enough/rag/nitro_rag.ipynb).
## Benchmarking the Results
We curated a new set of [50 multiple-choice questions](https://github.com/janhq/open-foundry/blob/main/rag-is-not-enough/rag/mcq_nitro.csv) (MCQ) based on the Nitro docs. The questions had varying levels of difficulty and had trick components that challenged the model's ability to discern misleading information.
We curated a new set of [50 multiple-choice questions](https://github.com/menloresearch/open-foundry/blob/main/rag-is-not-enough/rag/mcq_nitro.csv) (MCQ) based on the Nitro docs. The questions had varying levels of difficulty and had trick components that challenged the model's ability to discern misleading information.
![image](https://hackmd.io/_uploads/By9vaE1Ta.png)
@ -121,7 +121,7 @@ We conclude that this combination of model merging + finetuning + RAG yields pro
Anecdotally, weve had some success using this model in practice to onboard new team members to the Nitro codebase.
A full research report with more statistics can be found [here](https://github.com/janhq/open-foundry/blob/main/rag-is-not-enough/README.md).
A full research report with more statistics can be found [here](https://github.com/menloresearch/open-foundry/blob/main/rag-is-not-enough/README.md).
# References

View File

@ -203,7 +203,7 @@ When to choose ChatGPT Plus instead:
Ready to try gpt-oss?
- Download Jan: [https://jan.ai/](https://jan.ai/)
- View source code: [https://github.com/janhq/jan](https://github.com/janhq/jan)
- View source code: [https://github.com/menloresearch/jan](https://github.com/menloresearch/jan)
- Need help? Check our [local AI guide](/post/run-ai-models-locally) for beginners
<CTABlog />

View File

@ -4,7 +4,7 @@ title: Support - Jan
# Support
- Bugs & requests: file a GitHub ticket [here](https://github.com/janhq/jan/issues)
- Bugs & requests: file a GitHub ticket [here](https://github.com/menloresearch/jan/issues)
- For discussion: join our Discord [here](https://discord.gg/FTk2MvZwJH)
- For business inquiries: email hello@jan.ai
- For jobs: please email hr@jan.ai

View File

@ -31,7 +31,7 @@ const config: DocsThemeConfig = {
</div>
</span>
),
docsRepositoryBase: 'https://github.com/janhq/jan/tree/dev/docs',
docsRepositoryBase: 'https://github.com/menloresearch/jan/tree/dev/docs',
feedback: {
content: 'Question? Give us feedback →',
labels: 'feedback',

View File

@ -16,7 +16,7 @@ import {
ListConversationItemsResponse
} from './types'
declare const MENLO_PLATFORM_BASE_URL: string
declare const JAN_API_BASE: string
export class RemoteApi {
private authService: JanAuthService
@ -28,7 +28,7 @@ export class RemoteApi {
async createConversation(
data: Conversation
): Promise<ConversationResponse> {
const url = `${MENLO_PLATFORM_BASE_URL}${CONVERSATION_API_ROUTES.CONVERSATIONS}`
const url = `${JAN_API_BASE}${CONVERSATION_API_ROUTES.CONVERSATIONS}`
return this.authService.makeAuthenticatedRequest<ConversationResponse>(
url,
@ -43,12 +43,12 @@ export class RemoteApi {
conversationId: string,
data: Conversation
): Promise<ConversationResponse> {
const url = `${MENLO_PLATFORM_BASE_URL}${CONVERSATION_API_ROUTES.CONVERSATION_BY_ID(conversationId)}`
const url = `${JAN_API_BASE}${CONVERSATION_API_ROUTES.CONVERSATION_BY_ID(conversationId)}`
return this.authService.makeAuthenticatedRequest<ConversationResponse>(
url,
{
method: 'POST',
method: 'PATCH',
body: JSON.stringify(data),
}
)
@ -70,7 +70,7 @@ export class RemoteApi {
}
const queryString = queryParams.toString()
const url = `${MENLO_PLATFORM_BASE_URL}${CONVERSATION_API_ROUTES.CONVERSATIONS}${queryString ? `?${queryString}` : ''}`
const url = `${JAN_API_BASE}${CONVERSATION_API_ROUTES.CONVERSATIONS}${queryString ? `?${queryString}` : ''}`
return this.authService.makeAuthenticatedRequest<ListConversationsResponse>(
url,
@ -114,7 +114,7 @@ export class RemoteApi {
}
async deleteConversation(conversationId: string): Promise<void> {
const url = `${MENLO_PLATFORM_BASE_URL}${CONVERSATION_API_ROUTES.CONVERSATION_BY_ID(conversationId)}`
const url = `${JAN_API_BASE}${CONVERSATION_API_ROUTES.CONVERSATION_BY_ID(conversationId)}`
await this.authService.makeAuthenticatedRequest(
url,
@ -141,7 +141,7 @@ export class RemoteApi {
}
const queryString = queryParams.toString()
const url = `${MENLO_PLATFORM_BASE_URL}${CONVERSATION_API_ROUTES.CONVERSATION_ITEMS(conversationId)}${queryString ? `?${queryString}` : ''}`
const url = `${JAN_API_BASE}${CONVERSATION_API_ROUTES.CONVERSATION_ITEMS(conversationId)}${queryString ? `?${queryString}` : ''}`
return this.authService.makeAuthenticatedRequest<ListConversationItemsResponse>(
url,

View File

@ -31,7 +31,7 @@ export interface ConversationResponse {
id: string
object: 'conversation'
title?: string
created_at: number | string
created_at: number
metadata: ConversationMetadata
}
@ -50,7 +50,6 @@ export interface ConversationItemAnnotation {
}
export interface ConversationItemContent {
type?: string
file?: {
file_id?: string
mime_type?: string
@ -63,50 +62,23 @@ export interface ConversationItemContent {
file_id?: string
url?: string
}
image_file?: {
file_id?: string
mime_type?: string
}
input_text?: string
output_text?: {
annotations?: ConversationItemAnnotation[]
text?: string
}
reasoning_content?: string
text?: {
value?: string
text?: string
}
reasoning_content?: string
tool_calls?: Array<{
id?: string
type?: string
function?: {
name?: string
arguments?: string
}
}>
tool_call_id?: string
tool_result?: {
content?: Array<{
type?: string
text?: string
output_text?: {
text?: string
}
}>
output_text?: {
text?: string
}
}
text_result?: string
type?: string
}
export interface ConversationItem {
content?: ConversationItemContent[]
created_at: number | string
created_at: number
id: string
object: string
metadata?: Record<string, unknown>
role: string
status?: string
type?: string

View File

@ -1,5 +1,5 @@
import { Thread, ThreadAssistantInfo, ThreadMessage, ContentType } from '@janhq/core'
import { Conversation, ConversationResponse, ConversationItem, ConversationItemContent, ConversationMetadata } from './types'
import { Conversation, ConversationResponse, ConversationItem } from './types'
import { DEFAULT_ASSISTANT } from './const'
export class ObjectParser {
@ -7,7 +7,7 @@ export class ObjectParser {
const modelName = thread.assistants?.[0]?.model?.id || undefined
const modelProvider = thread.assistants?.[0]?.model?.engine || undefined
const isFavorite = thread.metadata?.is_favorite?.toString() || 'false'
let metadata: ConversationMetadata = {}
let metadata = {}
if (modelName && modelProvider) {
metadata = {
model_id: modelName,
@ -23,14 +23,15 @@ export class ObjectParser {
static conversationToThread(conversation: ConversationResponse): Thread {
const assistants: ThreadAssistantInfo[] = []
const metadata: ConversationMetadata = conversation.metadata || {}
if (metadata.model_id && metadata.model_provider) {
if (
conversation.metadata?.model_id &&
conversation.metadata?.model_provider
) {
assistants.push({
...DEFAULT_ASSISTANT,
model: {
id: metadata.model_id,
engine: metadata.model_provider,
id: conversation.metadata.model_id,
engine: conversation.metadata.model_provider,
},
})
} else {
@ -43,18 +44,16 @@ export class ObjectParser {
})
}
const isFavorite = metadata.is_favorite === 'true'
const createdAtMs = parseTimestamp(conversation.created_at)
const isFavorite = conversation.metadata?.is_favorite === 'true'
return {
id: conversation.id,
title: conversation.title || '',
assistants,
created: createdAtMs,
updated: createdAtMs,
created: conversation.created_at,
updated: conversation.created_at,
model: {
id: metadata.model_id,
provider: metadata.model_provider,
id: conversation.metadata.model_id,
provider: conversation.metadata.model_provider,
},
isFavorite,
metadata: { is_favorite: isFavorite },
@ -66,70 +65,74 @@ export class ObjectParser {
threadId: string
): ThreadMessage {
// Extract text content and metadata from the item
const textSegments: string[] = []
const reasoningSegments: string[] = []
let textContent = ''
let reasoningContent = ''
const imageUrls: string[] = []
let toolCalls: any[] = []
let finishReason = ''
if (item.content && item.content.length > 0) {
for (const content of item.content) {
extractContentByType(content, {
onText: (value) => {
if (value) {
textSegments.push(value)
}
},
onReasoning: (value) => {
if (value) {
reasoningSegments.push(value)
}
},
onImage: (url) => {
if (url) {
imageUrls.push(url)
}
},
onToolCalls: (calls) => {
toolCalls = calls.map((toolCall) => {
const callId = toolCall.id || 'unknown'
const rawArgs = toolCall.function?.arguments
const normalizedArgs =
typeof rawArgs === 'string'
? rawArgs
: JSON.stringify(rawArgs ?? {})
return {
id: callId,
tool_call_id: callId,
tool: {
id: callId,
function: {
name: toolCall.function?.name || 'unknown',
arguments: normalizedArgs,
},
type: toolCall.type || 'function',
},
response: {
error: '',
content: [],
},
state: 'pending',
}
})
},
})
// Handle text content
if (content.text?.value) {
textContent = content.text.value
}
// Handle output_text for assistant messages
if (content.output_text?.text) {
textContent = content.output_text.text
}
// Handle reasoning content
if (content.reasoning_content) {
reasoningContent = content.reasoning_content
}
// Handle image content
if (content.image?.url) {
imageUrls.push(content.image.url)
}
// Extract finish_reason
if (content.finish_reason) {
finishReason = content.finish_reason
}
}
}
// Handle tool calls parsing for assistant messages
if (item.role === 'assistant' && finishReason === 'tool_calls') {
try {
// Tool calls are embedded as JSON string in textContent
const toolCallMatch = textContent.match(/\[.*\]/)
if (toolCallMatch) {
const toolCallsData = JSON.parse(toolCallMatch[0])
toolCalls = toolCallsData.map((toolCall: any) => ({
tool: {
id: toolCall.id || 'unknown',
function: {
name: toolCall.function?.name || 'unknown',
arguments: toolCall.function?.arguments || '{}'
},
type: toolCall.type || 'function'
},
response: {
error: '',
content: []
},
state: 'ready'
}))
// Remove tool calls JSON from text content, keep only reasoning
textContent = ''
}
} catch (error) {
console.error('Failed to parse tool calls:', error)
}
}
// Format final content with reasoning if present
let finalTextValue = ''
if (reasoningSegments.length > 0) {
finalTextValue += `<think>${reasoningSegments.join('\n')}</think>`
if (reasoningContent) {
finalTextValue = `<think>${reasoningContent}</think>`
}
if (textSegments.length > 0) {
if (finalTextValue) {
finalTextValue += '\n'
}
finalTextValue += textSegments.join('\n')
if (textContent) {
finalTextValue += textContent
}
// Build content array for ThreadMessage
@ -154,26 +157,22 @@ export class ObjectParser {
}
// Build metadata
const metadata: any = { ...(item.metadata || {}) }
const metadata: any = {}
if (toolCalls.length > 0) {
metadata.tool_calls = toolCalls
}
const createdAtMs = parseTimestamp(item.created_at)
// Map status from server format to frontend format
const mappedStatus = item.status === 'completed' ? 'ready' : item.status || 'ready'
const role = item.role === 'user' || item.role === 'assistant' ? item.role : 'assistant'
return {
type: 'text',
id: item.id,
object: 'thread.message',
thread_id: threadId,
role,
role: item.role as 'user' | 'assistant',
content: messageContent,
created_at: createdAtMs,
created_at: item.created_at * 1000, // Convert to milliseconds
completed_at: 0,
status: mappedStatus,
metadata,
@ -202,46 +201,25 @@ export const combineConversationItemsToMessages = (
): ThreadMessage[] => {
const messages: ThreadMessage[] = []
const toolResponseMap = new Map<string, any>()
const sortedItems = [...items].sort(
(a, b) => parseTimestamp(a.created_at) - parseTimestamp(b.created_at)
)
// First pass: collect tool responses
for (const item of sortedItems) {
for (const item of items) {
if (item.role === 'tool') {
for (const content of item.content ?? []) {
const toolCallId = content.tool_call_id || item.id
const toolResultText =
content.tool_result?.output_text?.text ||
(Array.isArray(content.tool_result?.content)
? content.tool_result?.content
?.map((entry) => entry.text || entry.output_text?.text)
.filter((text): text is string => Boolean(text))
.join('\n')
: undefined)
const toolContent =
content.text?.text ||
content.text?.value ||
content.output_text?.text ||
content.input_text ||
content.text_result ||
toolResultText ||
''
toolResponseMap.set(toolCallId, {
error: '',
content: [
{
type: 'text',
text: toolContent,
},
],
})
}
const toolContent = item.content?.[0]?.text?.value || ''
toolResponseMap.set(item.id, {
error: '',
content: [
{
type: 'text',
text: toolContent
}
]
})
}
}
// Second pass: build messages and merge tool responses
for (const item of sortedItems) {
for (const item of items) {
// Skip tool messages as they will be merged into assistant messages
if (item.role === 'tool') {
continue
@ -250,35 +228,14 @@ export const combineConversationItemsToMessages = (
const message = ObjectParser.conversationItemToThreadMessage(item, threadId)
// If this is an assistant message with tool calls, merge tool responses
if (
message.role === 'assistant' &&
message.metadata?.tool_calls &&
Array.isArray(message.metadata.tool_calls)
) {
if (message.role === 'assistant' && message.metadata?.tool_calls && Array.isArray(message.metadata.tool_calls)) {
const toolCalls = message.metadata.tool_calls as any[]
let toolResponseIndex = 0
for (const toolCall of toolCalls) {
const callId = toolCall.tool_call_id || toolCall.id || toolCall.tool?.id
let responseKey: string | undefined
let response: any = null
if (callId && toolResponseMap.has(callId)) {
responseKey = callId
response = toolResponseMap.get(callId)
} else {
const iterator = toolResponseMap.entries().next()
if (!iterator.done) {
responseKey = iterator.value[0]
response = iterator.value[1]
}
}
if (response) {
toolCall.response = response
toolCall.state = 'succeeded'
if (responseKey) {
toolResponseMap.delete(responseKey)
}
for (const [responseId, responseData] of toolResponseMap.entries()) {
if (toolResponseIndex < toolCalls.length) {
toolCalls[toolResponseIndex].response = responseData
toolResponseIndex++
}
}
}
@ -288,79 +245,3 @@ export const combineConversationItemsToMessages = (
return messages
}
const parseTimestamp = (value: number | string | undefined): number => {
if (typeof value === 'number') {
// Distinguish between seconds and milliseconds
return value > 1e12 ? value : value * 1000
}
if (typeof value === 'string') {
const parsed = Date.parse(value)
return Number.isNaN(parsed) ? Date.now() : parsed
}
return Date.now()
}
const extractContentByType = (
content: ConversationItemContent,
handlers: {
onText: (value: string) => void
onReasoning: (value: string) => void
onImage: (url: string) => void
onToolCalls: (calls: NonNullable<ConversationItemContent['tool_calls']>) => void
}
) => {
const type = content.type || ''
switch (type) {
case 'input_text':
handlers.onText(content.input_text || '')
break
case 'text':
handlers.onText(content.text?.text || content.text?.value || '')
break
case 'output_text':
handlers.onText(content.output_text?.text || '')
break
case 'reasoning_content':
handlers.onReasoning(content.reasoning_content || '')
break
case 'image':
case 'image_url':
if (content.image?.url) {
handlers.onImage(content.image.url)
}
break
case 'tool_calls':
if (content.tool_calls && Array.isArray(content.tool_calls)) {
handlers.onToolCalls(content.tool_calls)
}
break
case 'tool_result':
if (content.tool_result?.output_text?.text) {
handlers.onText(content.tool_result.output_text.text)
}
break
default:
// Fallback for legacy fields without explicit type
if (content.text?.value || content.text?.text) {
handlers.onText(content.text.value || content.text.text || '')
}
if (content.text_result) {
handlers.onText(content.text_result)
}
if (content.output_text?.text) {
handlers.onText(content.output_text.text)
}
if (content.reasoning_content) {
handlers.onReasoning(content.reasoning_content)
}
if (content.image?.url) {
handlers.onImage(content.image.url)
}
if (content.tool_calls && Array.isArray(content.tool_calls)) {
handlers.onToolCalls(content.tool_calls)
}
break
}
}

View File

@ -4,11 +4,10 @@
*/
import { getSharedAuthService, JanAuthService } from '../shared'
import { ApiError } from '../shared/types/errors'
import { JAN_API_ROUTES } from './const'
import { JanModel, janProviderStore } from './store'
import { ApiError } from '../shared/types/errors'
// MENLO_PLATFORM_BASE_URL is defined in vite.config.ts
// JAN_API_BASE is defined in vite.config.ts
// Constants
const TEMPORARY_CHAT_ID = 'temporary-chat'
@ -20,7 +19,12 @@ const TEMPORARY_CHAT_ID = 'temporary-chat'
*/
function getChatCompletionConfig(request: JanChatCompletionRequest, stream: boolean = false) {
const isTemporaryChat = request.conversation_id === TEMPORARY_CHAT_ID
const endpoint = `${MENLO_PLATFORM_BASE_URL}${JAN_API_ROUTES.CHAT_COMPLETIONS}`
// For temporary chats, use the stateless /chat/completions endpoint
// For regular conversations, use the stateful /conv/chat/completions endpoint
const endpoint = isTemporaryChat
? `${JAN_API_BASE}/chat/completions`
: `${JAN_API_BASE}/conv/chat/completions`
const payload = {
...request,
@ -40,30 +44,9 @@ function getChatCompletionConfig(request: JanChatCompletionRequest, stream: bool
return { endpoint, payload, isTemporaryChat }
}
interface JanModelSummary {
id: string
export interface JanModelsResponse {
object: string
owned_by: string
created?: number
}
interface JanModelsResponse {
object: string
data: JanModelSummary[]
}
interface JanModelCatalogResponse {
id: string
supported_parameters?: {
names?: string[]
default?: Record<string, unknown>
}
extras?: {
supported_parameters?: string[]
default_parameters?: Record<string, unknown>
[key: string]: unknown
}
[key: string]: unknown
data: JanModel[]
}
export interface JanChatMessage {
@ -129,8 +112,6 @@ export interface JanChatCompletionChunk {
export class JanApiClient {
private static instance: JanApiClient
private authService: JanAuthService
private modelsCache: JanModel[] | null = null
private modelsFetchPromise: Promise<JanModel[]> | null = null
private constructor() {
this.authService = getSharedAuthService()
@ -143,64 +124,25 @@ export class JanApiClient {
return JanApiClient.instance
}
async getModels(options?: { forceRefresh?: boolean }): Promise<JanModel[]> {
async getModels(): Promise<JanModel[]> {
try {
const forceRefresh = options?.forceRefresh ?? false
if (forceRefresh) {
this.modelsCache = null
} else if (this.modelsCache) {
return this.modelsCache
}
if (this.modelsFetchPromise) {
return this.modelsFetchPromise
}
janProviderStore.setLoadingModels(true)
janProviderStore.clearError()
this.modelsFetchPromise = (async () => {
const response = await this.authService.makeAuthenticatedRequest<JanModelsResponse>(
`${MENLO_PLATFORM_BASE_URL}${JAN_API_ROUTES.MODELS}`
)
const response = await this.authService.makeAuthenticatedRequest<JanModelsResponse>(
`${JAN_API_BASE}/conv/models`
)
const summaries = response.data || []
const models: JanModel[] = await Promise.all(
summaries.map(async (summary) => {
const supportedParameters = await this.fetchSupportedParameters(summary.id)
const capabilities = this.deriveCapabilitiesFromParameters(supportedParameters)
return {
id: summary.id,
object: summary.object,
owned_by: summary.owned_by,
created: summary.created,
capabilities,
supportedParameters,
}
})
)
this.modelsCache = models
janProviderStore.setModels(models)
return models
})()
return await this.modelsFetchPromise
const models = response.data || []
janProviderStore.setModels(models)
return models
} catch (error) {
this.modelsCache = null
this.modelsFetchPromise = null
const errorMessage = error instanceof ApiError ? error.message :
error instanceof Error ? error.message : 'Failed to fetch models'
janProviderStore.setError(errorMessage)
janProviderStore.setLoadingModels(false)
throw error
} finally {
this.modelsFetchPromise = null
}
}
@ -312,7 +254,7 @@ export class JanApiClient {
async initialize(): Promise<void> {
try {
janProviderStore.setAuthenticated(true)
// Fetch initial models (cached for subsequent calls)
// Fetch initial models
await this.getModels()
console.log('Jan API client initialized successfully')
} catch (error) {
@ -324,52 +266,6 @@ export class JanApiClient {
janProviderStore.setInitializing(false)
}
}
private async fetchSupportedParameters(modelId: string): Promise<string[]> {
try {
const endpoint = `${MENLO_PLATFORM_BASE_URL}${JAN_API_ROUTES.MODEL_CATALOGS}/${this.encodeModelIdForCatalog(modelId)}`
const catalog = await this.authService.makeAuthenticatedRequest<JanModelCatalogResponse>(endpoint)
return this.extractSupportedParameters(catalog)
} catch (error) {
console.warn(`Failed to fetch catalog metadata for model "${modelId}":`, error)
return []
}
}
private encodeModelIdForCatalog(modelId: string): string {
return modelId
.split('/')
.map((segment) => encodeURIComponent(segment))
.join('/')
}
private extractSupportedParameters(catalog: JanModelCatalogResponse | null | undefined): string[] {
if (!catalog) {
return []
}
const primaryNames = catalog.supported_parameters?.names
if (Array.isArray(primaryNames) && primaryNames.length > 0) {
return [...new Set(primaryNames)]
}
const extraNames = catalog.extras?.supported_parameters
if (Array.isArray(extraNames) && extraNames.length > 0) {
return [...new Set(extraNames)]
}
return []
}
private deriveCapabilitiesFromParameters(parameters: string[]): string[] {
const capabilities = new Set<string>()
if (parameters.includes('tools')) {
capabilities.add('tools')
}
return Array.from(capabilities)
}
}
export const janApiClient = JanApiClient.getInstance()

View File

@ -1,7 +0,0 @@
export const JAN_API_ROUTES = {
MODELS: '/models',
CHAT_COMPLETIONS: '/chat/completions',
MODEL_CATALOGS: '/models/catalogs',
} as const
export const MODEL_PROVIDER_STORAGE_KEY = 'model-provider'

View File

@ -1,122 +0,0 @@
import type { JanModel } from './store'
import { MODEL_PROVIDER_STORAGE_KEY } from './const'
type StoredModel = {
id?: string
capabilities?: unknown
[key: string]: unknown
}
type StoredProvider = {
provider?: string
models?: StoredModel[]
[key: string]: unknown
}
type StoredState = {
state?: {
providers?: StoredProvider[]
[key: string]: unknown
}
version?: number
[key: string]: unknown
}
const normalizeCapabilities = (capabilities: unknown): string[] => {
if (!Array.isArray(capabilities)) {
return []
}
return [...new Set(capabilities.filter((item): item is string => typeof item === 'string'))].sort(
(a, b) => a.localeCompare(b)
)
}
/**
* Synchronize Jan models stored in localStorage with the latest server state.
* Returns true if the stored data was modified (including being cleared).
*/
export function syncJanModelsLocalStorage(
remoteModels: JanModel[],
storageKey: string = MODEL_PROVIDER_STORAGE_KEY
): boolean {
const rawStorage = localStorage.getItem(storageKey)
if (!rawStorage) {
return false
}
let storedState: StoredState
try {
storedState = JSON.parse(rawStorage) as StoredState
} catch (error) {
console.warn('Failed to parse Jan model storage; clearing entry.', error)
localStorage.removeItem(storageKey)
return true
}
const providers = storedState?.state?.providers
if (!Array.isArray(providers)) {
return false
}
const remoteModelMap = new Map(remoteModels.map((model) => [model.id, model]))
let storageUpdated = false
for (const provider of providers) {
if (provider.provider !== 'jan' || !Array.isArray(provider.models)) {
continue
}
const updatedModels: StoredModel[] = []
for (const model of provider.models) {
const modelId = typeof model.id === 'string' ? model.id : null
if (!modelId) {
storageUpdated = true
continue
}
const remoteModel = remoteModelMap.get(modelId)
if (!remoteModel) {
console.log(`Removing unknown Jan model from localStorage: ${modelId}`)
storageUpdated = true
continue
}
const storedCapabilities = normalizeCapabilities(model.capabilities)
const remoteCapabilities = normalizeCapabilities(remoteModel.capabilities)
const capabilitiesMatch =
storedCapabilities.length === remoteCapabilities.length &&
storedCapabilities.every((cap, index) => cap === remoteCapabilities[index])
if (!capabilitiesMatch) {
console.log(
`Updating capabilities for Jan model ${modelId}:`,
storedCapabilities,
'=>',
remoteCapabilities
)
updatedModels.push({
...model,
capabilities: remoteModel.capabilities,
})
storageUpdated = true
} else {
updatedModels.push(model)
}
}
if (updatedModels.length !== provider.models.length) {
storageUpdated = true
}
provider.models = updatedModels
}
if (storageUpdated) {
localStorage.setItem(storageKey, JSON.stringify(storedState))
}
return storageUpdated
}

View File

@ -14,10 +14,12 @@ import {
ImportOptions,
} from '@janhq/core' // cspell: disable-line
import { janApiClient, JanChatMessage } from './api'
import { syncJanModelsLocalStorage } from './helpers'
import { janProviderStore } from './store'
import { ApiError } from '../shared/types/errors'
// Jan models support tools via MCP
const JAN_MODEL_CAPABILITIES = ['tools'] as const
export default class JanProviderWeb extends AIEngine {
readonly provider = 'jan'
private activeSessions: Map<string, SessionInfo> = new Map()
@ -26,11 +28,11 @@ export default class JanProviderWeb extends AIEngine {
console.log('Loading Jan Provider Extension...')
try {
// Initialize authentication
await janApiClient.initialize()
// Check and sync stored Jan models against latest catalog data
await this.validateJanModelsLocalStorage()
// Check and clear invalid Jan models (capabilities mismatch)
this.validateJanModelsLocalStorage()
// Initialize authentication and fetch models
await janApiClient.initialize()
console.log('Jan Provider Extension loaded successfully')
} catch (error) {
console.error('Failed to load Jan Provider Extension:', error)
@ -41,17 +43,46 @@ export default class JanProviderWeb extends AIEngine {
}
// Verify Jan models capabilities in localStorage
private async validateJanModelsLocalStorage(): Promise<void> {
private validateJanModelsLocalStorage() {
try {
console.log('Validating Jan models in localStorage...')
console.log("Validating Jan models in localStorage...")
const storageKey = 'model-provider'
const data = localStorage.getItem(storageKey)
if (!data) return
const remoteModels = await janApiClient.getModels()
const storageUpdated = syncJanModelsLocalStorage(remoteModels)
const parsed = JSON.parse(data)
if (!parsed?.state?.providers) return
if (storageUpdated) {
console.log(
'Synchronized Jan models in localStorage with server capabilities; reloading...'
)
// Check if any Jan model has incorrect capabilities
let hasInvalidModel = false
for (const provider of parsed.state.providers) {
if (provider.provider === 'jan' && provider.models) {
for (const model of provider.models) {
console.log(`Checking Jan model: ${model.id}`, model.capabilities)
if (JSON.stringify(model.capabilities) !== JSON.stringify(JAN_MODEL_CAPABILITIES)) {
hasInvalidModel = true
console.log(`Found invalid Jan model: ${model.id}, clearing localStorage`)
break
}
}
}
if (hasInvalidModel) break
}
// If any invalid model found, just clear the storage
if (hasInvalidModel) {
// Force clear the storage
localStorage.removeItem(storageKey)
// Verify it's actually removed
const afterRemoval = localStorage.getItem(storageKey)
// If still present, try setting to empty state
if (afterRemoval) {
// Try alternative clearing method
localStorage.setItem(storageKey, JSON.stringify({ state: { providers: [] }, version: parsed.version || 3 }))
}
console.log('Cleared model-provider from localStorage due to invalid Jan capabilities')
// Force a page reload to ensure clean state
window.location.reload()
}
} catch (error) {
@ -88,7 +119,7 @@ export default class JanProviderWeb extends AIEngine {
path: undefined, // Remote model, no local path
owned_by: model.owned_by,
object: model.object,
capabilities: [...model.capabilities],
capabilities: [...JAN_MODEL_CAPABILITIES],
}
: undefined
)
@ -109,7 +140,7 @@ export default class JanProviderWeb extends AIEngine {
path: undefined, // Remote model, no local path
owned_by: model.owned_by,
object: model.object,
capabilities: [...model.capabilities],
capabilities: [...JAN_MODEL_CAPABILITIES],
}))
} catch (error) {
console.error('Failed to list Jan models:', error)
@ -128,7 +159,6 @@ export default class JanProviderWeb extends AIEngine {
port: 443, // HTTPS port
model_id: modelId,
model_path: `remote:${modelId}`, // Indicate this is a remote model
is_embedding: false, // assume false here, TODO: might need further implementation
api_key: '', // API key handled by auth service
}
@ -163,12 +193,8 @@ export default class JanProviderWeb extends AIEngine {
console.error(`Failed to unload Jan session ${sessionId}:`, error)
return {
success: false,
error:
error instanceof ApiError
? error.message
: error instanceof Error
? error.message
: 'Unknown error',
error: error instanceof ApiError ? error.message :
error instanceof Error ? error.message : 'Unknown error',
}
}
}

View File

@ -9,9 +9,6 @@ export interface JanModel {
id: string
object: string
owned_by: string
created?: number
capabilities: string[]
supportedParameters?: string[]
}
export interface JanProviderState {

View File

@ -12,8 +12,8 @@ import { JanMCPOAuthProvider } from './oauth-provider'
import { WebSearchButton } from './components'
import type { ComponentType } from 'react'
// MENLO_PLATFORM_BASE_URL is defined in vite.config.ts (defaults to 'https://api-dev.menlo.ai/jan/v1')
declare const MENLO_PLATFORM_BASE_URL: string
// JAN_API_BASE is defined in vite.config.ts (defaults to 'https://api-dev.jan.ai/jan/v1')
declare const JAN_API_BASE: string
export default class MCPExtensionWeb extends MCPExtension {
private mcpEndpoint = '/mcp'
@ -77,7 +77,7 @@ export default class MCPExtensionWeb extends MCPExtension {
// Create transport with OAuth provider (handles token refresh automatically)
const transport = new StreamableHTTPClientTransport(
new URL(`${MENLO_PLATFORM_BASE_URL}${this.mcpEndpoint}`),
new URL(`${JAN_API_BASE}${this.mcpEndpoint}`),
{
authProvider: this.oauthProvider
// No sessionId needed - server will generate one automatically

View File

@ -6,13 +6,13 @@
import { AuthTokens } from './types'
import { AUTH_ENDPOINTS } from './const'
declare const MENLO_PLATFORM_BASE_URL: string
declare const JAN_API_BASE: string
/**
* Logout user on server
*/
export async function logoutUser(): Promise<void> {
const response = await fetch(`${MENLO_PLATFORM_BASE_URL}${AUTH_ENDPOINTS.LOGOUT}`, {
const response = await fetch(`${JAN_API_BASE}${AUTH_ENDPOINTS.LOGOUT}`, {
method: 'GET',
credentials: 'include',
headers: {
@ -29,7 +29,7 @@ export async function logoutUser(): Promise<void> {
* Guest login
*/
export async function guestLogin(): Promise<AuthTokens> {
const response = await fetch(`${MENLO_PLATFORM_BASE_URL}${AUTH_ENDPOINTS.GUEST_LOGIN}`, {
const response = await fetch(`${JAN_API_BASE}${AUTH_ENDPOINTS.GUEST_LOGIN}`, {
method: 'POST',
credentials: 'include',
headers: {
@ -51,7 +51,7 @@ export async function guestLogin(): Promise<AuthTokens> {
*/
export async function refreshToken(): Promise<AuthTokens> {
const response = await fetch(
`${MENLO_PLATFORM_BASE_URL}${AUTH_ENDPOINTS.REFRESH_TOKEN}`,
`${JAN_API_BASE}${AUTH_ENDPOINTS.REFRESH_TOKEN}`,
{
method: 'GET',
credentials: 'include',

View File

@ -5,10 +5,10 @@
import { AuthTokens, LoginUrlResponse } from './types'
declare const MENLO_PLATFORM_BASE_URL: string
declare const JAN_API_BASE: string
export async function getLoginUrl(endpoint: string): Promise<LoginUrlResponse> {
const response: Response = await fetch(`${MENLO_PLATFORM_BASE_URL}${endpoint}`, {
const response: Response = await fetch(`${JAN_API_BASE}${endpoint}`, {
method: 'GET',
credentials: 'include',
headers: {
@ -30,7 +30,7 @@ export async function handleOAuthCallback(
code: string,
state?: string
): Promise<AuthTokens> {
const response: Response = await fetch(`${MENLO_PLATFORM_BASE_URL}${endpoint}`, {
const response: Response = await fetch(`${JAN_API_BASE}${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View File

@ -3,9 +3,9 @@
* Handles authentication flows for any OAuth provider
*/
declare const MENLO_PLATFORM_BASE_URL: string
declare const JAN_API_BASE: string
import { User, AuthState, AuthBroadcastMessage, AuthTokens } from './types'
import { User, AuthState, AuthBroadcastMessage } from './types'
import {
AUTH_STORAGE_KEYS,
AUTH_ENDPOINTS,
@ -115,7 +115,7 @@ export class JanAuthService {
// Store tokens and set authenticated state
this.accessToken = tokens.access_token
this.tokenExpiryTime = this.computeTokenExpiry(tokens)
this.tokenExpiryTime = Date.now() + tokens.expires_in * 1000
this.setAuthProvider(providerId)
this.authBroadcast.broadcastLogin()
@ -158,7 +158,7 @@ export class JanAuthService {
const tokens = await refreshToken()
this.accessToken = tokens.access_token
this.tokenExpiryTime = this.computeTokenExpiry(tokens)
this.tokenExpiryTime = Date.now() + tokens.expires_in * 1000
} catch (error) {
console.error('Failed to refresh access token:', error)
if (error instanceof ApiError && error.isStatus(401)) {
@ -343,23 +343,6 @@ export class JanAuthService {
localStorage.removeItem(AUTH_STORAGE_KEYS.AUTH_PROVIDER)
}
private computeTokenExpiry(tokens: AuthTokens): number {
if (tokens.expires_at) {
const expiresAt = new Date(tokens.expires_at).getTime()
if (!Number.isNaN(expiresAt)) {
return expiresAt
}
console.warn('Invalid expires_at format in auth tokens:', tokens.expires_at)
}
if (typeof tokens.expires_in === 'number') {
return Date.now() + tokens.expires_in * 1000
}
console.warn('Auth tokens missing expiry information; defaulting to immediate expiry')
return Date.now()
}
/**
* Ensure guest access is available
*/
@ -369,7 +352,7 @@ export class JanAuthService {
if (!this.accessToken || Date.now() > this.tokenExpiryTime) {
const tokens = await guestLogin()
this.accessToken = tokens.access_token
this.tokenExpiryTime = this.computeTokenExpiry(tokens)
this.tokenExpiryTime = Date.now() + tokens.expires_in * 1000
}
} catch (error) {
console.error('Failed to ensure guest access:', error)
@ -404,6 +387,7 @@ export class JanAuthService {
case AUTH_EVENTS.LOGOUT:
// Another tab logged out, clear our state
this.clearAuthState()
this.ensureGuestAccess().catch(console.error)
break
}
})
@ -429,7 +413,7 @@ export class JanAuthService {
private async fetchUserProfile(): Promise<User | null> {
try {
return await this.makeAuthenticatedRequest<User>(
`${MENLO_PLATFORM_BASE_URL}${AUTH_ENDPOINTS.ME}`
`${JAN_API_BASE}${AUTH_ENDPOINTS.ME}`
)
} catch (error) {
console.error('Failed to fetch user profile:', error)

View File

@ -16,8 +16,7 @@ export type AuthType = ProviderType | 'guest'
export interface AuthTokens {
access_token: string
expires_in?: number
expires_at?: string
expires_in: number
object: string
}

View File

@ -1,5 +1,5 @@
export {}
declare global {
declare const MENLO_PLATFORM_BASE_URL: string
declare const JAN_API_BASE: string
}

View File

@ -14,6 +14,6 @@ export default defineConfig({
emptyOutDir: false // Don't clean the output directory
},
define: {
MENLO_PLATFORM_BASE_URL: JSON.stringify(process.env.MENLO_PLATFORM_BASE_URL || 'https://api-dev.menlo.ai/v1'),
JAN_API_BASE: JSON.stringify(process.env.JAN_API_BASE || 'https://api-dev.jan.ai/v1'),
}
})

View File

@ -70,6 +70,6 @@ There are a few things to keep in mind when writing your extension code:
```
For more information about the Jan Extension Core module, see the
[documentation](https://github.com/janhq/jan/blob/main/core/README.md).
[documentation](https://github.com/menloresearch/jan/blob/main/core/README.md).
So, what are you waiting for? Go ahead and start customizing your extension!

View File

@ -56,7 +56,7 @@ async function fetchRemoteSupportedBackends(
supportedBackends: string[]
): Promise<{ version: string; backend: string }[]> {
// Pull the latest releases from the repo
const { releases } = await _fetchGithubReleases('janhq', 'llama.cpp')
const { releases } = await _fetchGithubReleases('menloresearch', 'llama.cpp')
releases.sort((a, b) => b.tag_name.localeCompare(a.tag_name))
releases.splice(10) // keep only the latest 10 releases
@ -98,7 +98,7 @@ export async function listSupportedBackends(): Promise<
const sysType = `${os_type}-${arch}`
let supportedBackends = []
// NOTE: janhq's tags for llama.cpp builds are a bit different
// NOTE: menloresearch's tags for llama.cpp builds are a bit different
// TODO: fetch versions from the server?
// TODO: select CUDA version based on driver version
if (sysType == 'windows-x86_64') {
@ -156,13 +156,8 @@ export async function listSupportedBackends(): Promise<
supportedBackends.push('macos-arm64')
}
// get latest backends from Github
let remoteBackendVersions = []
try {
remoteBackendVersions =
const remoteBackendVersions =
await fetchRemoteSupportedBackends(supportedBackends)
} catch (e) {
console.debug(`Not able to get remote backends, Jan might be offline or network problem: ${String(e)}`)
}
// Get locally installed versions
const localBackendVersions = await getLocalInstalledBackends()
@ -247,7 +242,7 @@ export async function downloadBackend(
// Build URLs per source
const backendUrl =
source === 'github'
? `https://github.com/janhq/llama.cpp/releases/download/${version}/llama-${version}-bin-${backend}.tar.gz`
? `https://github.com/menloresearch/llama.cpp/releases/download/${version}/llama-${version}-bin-${backend}.tar.gz`
: `https://catalog.jan.ai/llama.cpp/releases/${version}/llama-${version}-bin-${backend}.tar.gz`
const downloadItems = [
@ -263,7 +258,7 @@ export async function downloadBackend(
downloadItems.push({
url:
source === 'github'
? `https://github.com/janhq/llama.cpp/releases/download/${version}/cudart-llama-bin-${platformName}-cu11.7-x64.tar.gz`
? `https://github.com/menloresearch/llama.cpp/releases/download/${version}/cudart-llama-bin-${platformName}-cu11.7-x64.tar.gz`
: `https://catalog.jan.ai/llama.cpp/releases/${version}/cudart-llama-bin-${platformName}-cu11.7-x64.tar.gz`,
save_path: await joinPath([libDir, 'cuda11.tar.gz']),
proxy: proxyConfig,
@ -272,7 +267,7 @@ export async function downloadBackend(
downloadItems.push({
url:
source === 'github'
? `https://github.com/janhq/llama.cpp/releases/download/${version}/cudart-llama-bin-${platformName}-cu12.0-x64.tar.gz`
? `https://github.com/menloresearch/llama.cpp/releases/download/${version}/cudart-llama-bin-${platformName}-cu12.0-x64.tar.gz`
: `https://catalog.jan.ai/llama.cpp/releases/${version}/cudart-llama-bin-${platformName}-cu12.0-x64.tar.gz`,
save_path: await joinPath([libDir, 'cuda12.tar.gz']),
proxy: proxyConfig,

View File

@ -333,12 +333,14 @@ export default class llamacpp_extension extends AIEngine {
)
// Clear the invalid stored preference
this.clearStoredBackendType()
bestAvailableBackendString =
await this.determineBestBackend(version_backends)
bestAvailableBackendString = await this.determineBestBackend(
version_backends
)
}
} else {
bestAvailableBackendString =
await this.determineBestBackend(version_backends)
bestAvailableBackendString = await this.determineBestBackend(
version_backends
)
}
let settings = structuredClone(SETTINGS)
@ -1528,7 +1530,6 @@ export default class llamacpp_extension extends AIEngine {
if (
this.autoUnload &&
!isEmbedding &&
(loadedModels.length > 0 || otherLoadingPromises.length > 0)
) {
// Wait for OTHER loading models to finish, then unload everything
@ -1536,33 +1537,10 @@ export default class llamacpp_extension extends AIEngine {
await Promise.all(otherLoadingPromises)
}
// Now unload all loaded Text models excluding embedding models
// Now unload all loaded models
const allLoadedModels = await this.getLoadedModels()
if (allLoadedModels.length > 0) {
const sessionInfos: (SessionInfo | null)[] = await Promise.all(
allLoadedModels.map(async (modelId) => {
try {
return await this.findSessionByModel(modelId)
} catch (e) {
logger.warn(`Unable to find session for model "${modelId}": ${e}`)
return null // treat as “noteligible for unload”
}
})
)
logger.info(JSON.stringify(sessionInfos))
const nonEmbeddingModels: string[] = sessionInfos
.filter(
(s): s is SessionInfo => s !== null && s.is_embedding === false
)
.map((s) => s.model_id)
if (nonEmbeddingModels.length > 0) {
await Promise.all(
nonEmbeddingModels.map((modelId) => this.unload(modelId))
)
}
await Promise.all(allLoadedModels.map((model) => this.unload(model)))
}
}
const args: string[] = []
@ -1660,7 +1638,7 @@ export default class llamacpp_extension extends AIEngine {
if (cfg.no_kv_offload) args.push('--no-kv-offload')
if (isEmbedding) {
args.push('--embedding')
args.push('--pooling', 'mean')
args.push('--pooling mean')
} else {
if (cfg.ctx_size > 0) args.push('--ctx-size', String(cfg.ctx_size))
if (cfg.n_predict > 0) args.push('--n-predict', String(cfg.n_predict))
@ -1699,7 +1677,6 @@ export default class llamacpp_extension extends AIEngine {
libraryPath,
args,
envs,
isEmbedding,
}
)
return sInfo
@ -2035,69 +2012,6 @@ export default class llamacpp_extension extends AIEngine {
libraryPath,
envs,
})
// On Linux with AMD GPUs, llama.cpp via Vulkan may report UMA (shared) memory as device-local.
// For clearer UX, override with dedicated VRAM from the hardware plugin when available.
try {
const sysInfo = await getSystemInfo()
if (sysInfo?.os_type === 'linux' && Array.isArray(sysInfo.gpus)) {
const usage = await getSystemUsage()
if (usage && Array.isArray(usage.gpus)) {
const uuidToUsage: Record<string, { total_memory: number; used_memory: number }> = {}
for (const u of usage.gpus as any[]) {
if (u && typeof u.uuid === 'string') {
uuidToUsage[u.uuid] = u
}
}
const indexToAmdUuid = new Map<number, string>()
for (const gpu of sysInfo.gpus as any[]) {
const vendorStr =
typeof gpu?.vendor === 'string'
? gpu.vendor
: typeof gpu?.vendor === 'object' && gpu.vendor !== null
? String(gpu.vendor)
: ''
if (
vendorStr.toUpperCase().includes('AMD') &&
gpu?.vulkan_info &&
typeof gpu.vulkan_info.index === 'number' &&
typeof gpu.uuid === 'string'
) {
indexToAmdUuid.set(gpu.vulkan_info.index, gpu.uuid)
}
}
if (indexToAmdUuid.size > 0) {
const adjusted = dList.map((dev) => {
if (dev.id?.startsWith('Vulkan')) {
const match = /^Vulkan(\d+)/.exec(dev.id)
if (match) {
const vIdx = Number(match[1])
const uuid = indexToAmdUuid.get(vIdx)
if (uuid) {
const u = uuidToUsage[uuid]
if (
u &&
typeof u.total_memory === 'number' &&
typeof u.used_memory === 'number'
) {
const total = Math.max(0, Math.floor(u.total_memory))
const free = Math.max(0, Math.floor(u.total_memory - u.used_memory))
return { ...dev, mem: total, free }
}
}
}
}
return dev
})
return adjusted
}
}
}
} catch (e) {
logger.warn('Device memory override (AMD/Linux) failed:', e)
}
return dList
} catch (error) {
logger.error('Failed to query devices:\n', error)
@ -2106,7 +2020,6 @@ export default class llamacpp_extension extends AIEngine {
}
async embed(text: string[]): Promise<EmbeddingResponse> {
// Ensure the sentence-transformer model is present
let sInfo = await this.findSessionByModel('sentence-transformer-mini')
if (!sInfo) {
const downloadedModelList = await this.list()
@ -2120,45 +2033,30 @@ export default class llamacpp_extension extends AIEngine {
'https://huggingface.co/second-state/All-MiniLM-L6-v2-Embedding-GGUF/resolve/main/all-MiniLM-L6-v2-ggml-model-f16.gguf?download=true',
})
}
// Load specifically in embedding mode
sInfo = await this.load('sentence-transformer-mini', undefined, true)
sInfo = await this.load('sentence-transformer-mini')
}
const attemptRequest = async (session: SessionInfo) => {
const baseUrl = `http://localhost:${session.port}/v1/embeddings`
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${session.api_key}`,
}
const body = JSON.stringify({
input: text,
model: session.model_id,
encoding_format: 'float',
})
const response = await fetch(baseUrl, {
method: 'POST',
headers,
body,
})
return response
}
// First try with the existing session (may have been started without --embedding previously)
let response = await attemptRequest(sInfo)
// If embeddings endpoint is not available (501), reload with embedding mode and retry once
if (response.status === 501) {
try {
await this.unload('sentence-transformer-mini')
} catch {}
sInfo = await this.load('sentence-transformer-mini', undefined, true)
response = await attemptRequest(sInfo)
const baseUrl = `http://localhost:${sInfo.port}/v1/embeddings`
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${sInfo.api_key}`,
}
const body = JSON.stringify({
input: text,
model: sInfo.model_id,
encoding_format: 'float',
})
const response = await fetch(baseUrl, {
method: 'POST',
headers,
body,
})
if (!response.ok) {
const errorData = await response.json().catch(() => null)
throw new Error(
`API request failed with status ${response.status}: ${JSON.stringify(errorData)}`
`API request failed with status ${response.status}: ${JSON.stringify(
errorData
)}`
)
}
const responseData = await response.json()

View File

@ -1,33 +0,0 @@
{
"name": "@janhq/rag-extension",
"productName": "RAG Tools",
"version": "0.1.0",
"description": "Registers RAG tools and orchestrates retrieval across parser, embeddings, and vector DB",
"main": "dist/index.js",
"module": "dist/module.js",
"author": "Jan <service@jan.ai>",
"license": "AGPL-3.0",
"scripts": {
"build": "rolldown -c rolldown.config.mjs",
"build:publish": "rimraf *.tgz --glob || true && yarn build && npm pack && cpx *.tgz ../../pre-install"
},
"devDependencies": {
"cpx": "1.5.0",
"rimraf": "6.0.1",
"rolldown": "1.0.0-beta.1",
"typescript": "5.9.2"
},
"dependencies": {
"@janhq/core": "../../core/package.tgz",
"@janhq/tauri-plugin-rag-api": "link:../../src-tauri/plugins/tauri-plugin-rag",
"@janhq/tauri-plugin-vector-db-api": "link:../../src-tauri/plugins/tauri-plugin-vector-db"
},
"files": [
"dist/*",
"package.json"
],
"installConfig": {
"hoistingLimits": "workspaces"
},
"packageManager": "yarn@4.5.3"
}

View File

@ -1,14 +0,0 @@
import { defineConfig } from 'rolldown'
import settingJson from './settings.json' with { type: 'json' }
export default defineConfig({
input: 'src/index.ts',
output: {
format: 'esm',
file: 'dist/index.js',
},
platform: 'browser',
define: {
SETTINGS: JSON.stringify(settingJson),
},
})

View File

@ -1,58 +0,0 @@
[
{
"key": "enabled",
"titleKey": "settings:attachments.enable",
"descriptionKey": "settings:attachments.enableDesc",
"controllerType": "checkbox",
"controllerProps": { "value": true }
},
{
"key": "max_file_size_mb",
"titleKey": "settings:attachments.maxFile",
"descriptionKey": "settings:attachments.maxFileDesc",
"controllerType": "input",
"controllerProps": { "value": 20, "type": "number", "min": 1, "max": 200, "step": 1, "textAlign": "right" }
},
{
"key": "retrieval_limit",
"titleKey": "settings:attachments.topK",
"descriptionKey": "settings:attachments.topKDesc",
"controllerType": "input",
"controllerProps": { "value": 3, "type": "number", "min": 1, "max": 20, "step": 1, "textAlign": "right" }
},
{
"key": "retrieval_threshold",
"titleKey": "settings:attachments.threshold",
"descriptionKey": "settings:attachments.thresholdDesc",
"controllerType": "input",
"controllerProps": { "value": 0.3, "type": "number", "min": 0, "max": 1, "step": 0.01, "textAlign": "right" }
},
{
"key": "chunk_size_tokens",
"titleKey": "settings:attachments.chunkSize",
"descriptionKey": "settings:attachments.chunkSizeDesc",
"controllerType": "input",
"controllerProps": { "value": 512, "type": "number", "min": 64, "max": 8192, "step": 64, "textAlign": "right" }
},
{
"key": "overlap_tokens",
"titleKey": "settings:attachments.chunkOverlap",
"descriptionKey": "settings:attachments.chunkOverlapDesc",
"controllerType": "input",
"controllerProps": { "value": 64, "type": "number", "min": 0, "max": 1024, "step": 16, "textAlign": "right" }
},
{
"key": "search_mode",
"titleKey": "settings:attachments.searchMode",
"descriptionKey": "settings:attachments.searchModeDesc",
"controllerType": "dropdown",
"controllerProps": {
"value": "auto",
"options": [
{ "name": "Auto (recommended)", "value": "auto" },
{ "name": "ANN (sqlite-vec)", "value": "ann" },
{ "name": "Linear", "value": "linear" }
]
}
}
]

View File

@ -1,5 +0,0 @@
import type { SettingComponentProps } from '@janhq/core'
declare global {
const SETTINGS: SettingComponentProps[]
}
export {}

View File

@ -1,14 +0,0 @@
import type { BaseExtension, ExtensionTypeEnum } from '@janhq/core'
declare global {
interface Window {
core?: {
extensionManager: {
get<T = BaseExtension>(type: ExtensionTypeEnum): T | undefined
getByName(name: string): BaseExtension | undefined
}
}
}
}
export {}

View File

@ -1,305 +0,0 @@
import { RAGExtension, MCPTool, MCPToolCallResult, ExtensionTypeEnum, VectorDBExtension, type AttachmentInput, type SettingComponentProps, AIEngine, type AttachmentFileInfo } from '@janhq/core'
import './env.d'
import { getRAGTools, RETRIEVE, LIST_ATTACHMENTS, GET_CHUNKS } from './tools'
export default class RagExtension extends RAGExtension {
private config = {
enabled: true,
retrievalLimit: 3,
retrievalThreshold: 0.3,
chunkSizeTokens: 512,
overlapTokens: 64,
searchMode: 'auto' as 'auto' | 'ann' | 'linear',
maxFileSizeMB: 20,
}
async onLoad(): Promise<void> {
const settings = structuredClone(SETTINGS) as SettingComponentProps[]
await this.registerSettings(settings)
this.config.enabled = await this.getSetting('enabled', this.config.enabled)
this.config.maxFileSizeMB = await this.getSetting('max_file_size_mb', this.config.maxFileSizeMB)
this.config.retrievalLimit = await this.getSetting('retrieval_limit', this.config.retrievalLimit)
this.config.retrievalThreshold = await this.getSetting('retrieval_threshold', this.config.retrievalThreshold)
this.config.chunkSizeTokens = await this.getSetting('chunk_size_tokens', this.config.chunkSizeTokens)
this.config.overlapTokens = await this.getSetting('overlap_tokens', this.config.overlapTokens)
this.config.searchMode = await this.getSetting('search_mode', this.config.searchMode)
// Check ANN availability on load
try {
const vec = window.core?.extensionManager.get(ExtensionTypeEnum.VectorDB) as unknown as VectorDBExtension
if (vec?.getStatus) {
const status = await vec.getStatus()
console.log('[RAG] Vector DB ANN support:', status.ann_available ? '✓ AVAILABLE' : '✗ NOT AVAILABLE')
if (!status.ann_available) {
console.warn('[RAG] Warning: sqlite-vec not loaded. Collections will use slower linear search.')
}
}
} catch (e) {
console.error('[RAG] Failed to check ANN status:', e)
}
}
onUnload(): void {}
async getTools(): Promise<MCPTool[]> {
return getRAGTools(this.config.retrievalLimit)
}
async getToolNames(): Promise<string[]> {
// Keep this in sync with getTools() but without building full schemas
return [LIST_ATTACHMENTS, RETRIEVE, GET_CHUNKS]
}
async callTool(toolName: string, args: Record<string, unknown>): Promise<MCPToolCallResult> {
switch (toolName) {
case LIST_ATTACHMENTS:
return this.listAttachments(args)
case RETRIEVE:
return this.retrieve(args)
case GET_CHUNKS:
return this.getChunks(args)
default:
return {
error: `Unknown tool: ${toolName}`,
content: [{ type: 'text', text: `Unknown tool: ${toolName}` }],
}
}
}
private async listAttachments(args: Record<string, unknown>): Promise<MCPToolCallResult> {
const threadId = String(args['thread_id'] || '')
if (!threadId) {
return { error: 'Missing thread_id', content: [{ type: 'text', text: 'Missing thread_id' }] }
}
try {
const vec = window.core?.extensionManager.get(ExtensionTypeEnum.VectorDB) as unknown as VectorDBExtension
if (!vec?.listAttachments) {
return { error: 'Vector DB extension missing listAttachments', content: [{ type: 'text', text: 'Vector DB extension missing listAttachments' }] }
}
const files = await vec.listAttachments(threadId)
return {
error: '',
content: [
{
type: 'text',
text: JSON.stringify({ thread_id: threadId, attachments: files || [] }),
},
],
}
} catch (e) {
const msg = e instanceof Error ? e.message : String(e)
return { error: msg, content: [{ type: 'text', text: `List attachments failed: ${msg}` }] }
}
}
private async retrieve(args: Record<string, unknown>): Promise<MCPToolCallResult> {
const threadId = String(args['thread_id'] || '')
const query = String(args['query'] || '')
const fileIds = args['file_ids'] as string[] | undefined
const s = this.config
const topK = (args['top_k'] as number) || s.retrievalLimit || 3
const threshold = s.retrievalThreshold ?? 0.3
const mode: 'auto' | 'ann' | 'linear' = s.searchMode || 'auto'
if (s.enabled === false) {
return {
error: 'Attachments feature disabled',
content: [
{
type: 'text',
text: 'Attachments are disabled in Settings. Enable them to use retrieval.',
},
],
}
}
if (!threadId || !query) {
return {
error: 'Missing thread_id or query',
content: [{ type: 'text', text: 'Missing required parameters' }],
}
}
try {
// Resolve extensions
const vec = window.core?.extensionManager.get(ExtensionTypeEnum.VectorDB) as unknown as VectorDBExtension
if (!vec?.searchCollection) {
return {
error: 'RAG dependencies not available',
content: [
{ type: 'text', text: 'Vector DB extension not available' },
],
}
}
const queryEmb = (await this.embedTexts([query]))?.[0]
if (!queryEmb) {
return {
error: 'Failed to compute embeddings',
content: [{ type: 'text', text: 'Failed to compute embeddings' }],
}
}
const results = await vec.searchCollection(
threadId,
queryEmb,
topK,
threshold,
mode,
fileIds
)
const payload = {
thread_id: threadId,
query,
citations: results?.map((r: any) => ({
id: r.id,
text: r.text,
score: r.score,
file_id: r.file_id,
chunk_file_order: r.chunk_file_order
})) ?? [],
mode,
}
return { error: '', content: [{ type: 'text', text: JSON.stringify(payload) }] }
} catch (e) {
console.error('[RAG] Retrieve error:', e)
let msg = 'Unknown error'
if (e instanceof Error) {
msg = e.message
} else if (typeof e === 'string') {
msg = e
} else if (e && typeof e === 'object') {
msg = JSON.stringify(e)
}
return { error: msg, content: [{ type: 'text', text: `Retrieve failed: ${msg}` }] }
}
}
private async getChunks(args: Record<string, unknown>): Promise<MCPToolCallResult> {
const threadId = String(args['thread_id'] || '')
const fileId = String(args['file_id'] || '')
const startOrder = args['start_order'] as number | undefined
const endOrder = args['end_order'] as number | undefined
if (!threadId || !fileId || startOrder === undefined || endOrder === undefined) {
return {
error: 'Missing thread_id, file_id, start_order, or end_order',
content: [{ type: 'text', text: 'Missing required parameters' }],
}
}
try {
const vec = window.core?.extensionManager.get(ExtensionTypeEnum.VectorDB) as unknown as VectorDBExtension
if (!vec?.getChunks) {
return {
error: 'Vector DB extension not available',
content: [{ type: 'text', text: 'Vector DB extension not available' }],
}
}
const chunks = await vec.getChunks(threadId, fileId, startOrder, endOrder)
const payload = {
thread_id: threadId,
file_id: fileId,
chunks: chunks || [],
}
return { error: '', content: [{ type: 'text', text: JSON.stringify(payload) }] }
} catch (e) {
const msg = e instanceof Error ? e.message : String(e)
return { error: msg, content: [{ type: 'text', text: `Get chunks failed: ${msg}` }] }
}
}
// Desktop-only ingestion by file paths
async ingestAttachments(
threadId: string,
files: AttachmentInput[]
): Promise<{ filesProcessed: number; chunksInserted: number; files: AttachmentFileInfo[] }> {
if (!threadId || !Array.isArray(files) || files.length === 0) {
return { filesProcessed: 0, chunksInserted: 0, files: [] }
}
// Respect feature flag: do nothing when disabled
if (this.config.enabled === false) {
return { filesProcessed: 0, chunksInserted: 0, files: [] }
}
const vec = window.core?.extensionManager.get(ExtensionTypeEnum.VectorDB) as unknown as VectorDBExtension
if (!vec?.createCollection || !vec?.insertChunks) {
throw new Error('Vector DB extension not available')
}
// Load settings
const s = this.config
const maxSize = (s?.enabled === false ? 0 : s?.maxFileSizeMB) || undefined
const chunkSize = s?.chunkSizeTokens as number | undefined
const chunkOverlap = s?.overlapTokens as number | undefined
let totalChunks = 0
const processedFiles: AttachmentFileInfo[] = []
for (const f of files) {
if (!f?.path) continue
if (maxSize && f.size && f.size > maxSize * 1024 * 1024) {
throw new Error(`File '${f.name}' exceeds size limit (${f.size} bytes > ${maxSize} MB).`)
}
const fileName = f.name || f.path.split(/[\\/]/).pop()
// Preferred/required path: let Vector DB extension handle full file ingestion
const canIngestFile = typeof (vec as any)?.ingestFile === 'function'
if (!canIngestFile) {
console.error('[RAG] Vector DB extension missing ingestFile; cannot ingest document')
continue
}
const info = await (vec as VectorDBExtension).ingestFile(
threadId,
{ path: f.path, name: fileName, type: f.type, size: f.size },
{ chunkSize: chunkSize ?? 512, chunkOverlap: chunkOverlap ?? 64 }
)
totalChunks += Number(info?.chunk_count || 0)
processedFiles.push(info)
}
// Return files we ingested with real IDs directly from ingestFile
return { filesProcessed: processedFiles.length, chunksInserted: totalChunks, files: processedFiles }
}
onSettingUpdate<T>(key: string, value: T): void {
switch (key) {
case 'enabled':
this.config.enabled = Boolean(value)
break
case 'max_file_size_mb':
this.config.maxFileSizeMB = Number(value)
break
case 'retrieval_limit':
this.config.retrievalLimit = Number(value)
break
case 'retrieval_threshold':
this.config.retrievalThreshold = Number(value)
break
case 'chunk_size_tokens':
this.config.chunkSizeTokens = Number(value)
break
case 'overlap_tokens':
this.config.overlapTokens = Number(value)
break
case 'search_mode':
this.config.searchMode = String(value) as 'auto' | 'ann' | 'linear'
break
}
}
// Locally implement embedding logic (previously in embeddings-extension)
private async embedTexts(texts: string[]): Promise<number[][]> {
const llm = window.core?.extensionManager.getByName('@janhq/llamacpp-extension') as AIEngine & { embed?: (texts: string[]) => Promise<{ data: Array<{ embedding: number[]; index: number }> }> }
if (!llm?.embed) throw new Error('llamacpp extension not available')
const res = await llm.embed(texts)
const data: Array<{ embedding: number[]; index: number }> = res?.data || []
const out: number[][] = new Array(texts.length)
for (const item of data) out[item.index] = item.embedding
return out
}
}

View File

@ -1,58 +0,0 @@
import { MCPTool, RAG_INTERNAL_SERVER } from '@janhq/core'
// Tool names
export const RETRIEVE = 'retrieve'
export const LIST_ATTACHMENTS = 'list_attachments'
export const GET_CHUNKS = 'get_chunks'
export function getRAGTools(retrievalLimit: number): MCPTool[] {
const maxTopK = Math.max(1, Number(retrievalLimit ?? 3))
return [
{
name: LIST_ATTACHMENTS,
description:
'List files attached to the current thread. Thread is inferred automatically; you may optionally provide {"scope":"thread"}. Returns basic file info (name/path).',
inputSchema: {
type: 'object',
properties: {
scope: { type: 'string', enum: ['thread'], description: 'Retrieval scope; currently only thread is supported' },
},
required: ['scope'],
},
server: RAG_INTERNAL_SERVER,
},
{
name: RETRIEVE,
description:
'Retrieve relevant snippets from locally attached, indexed documents. Use query only; do not pass raw document content. Thread context is inferred automatically; you may optionally provide {"scope":"thread"}. Use file_ids to search within specific files only.',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'User query to search for' },
top_k: { type: 'number', description: 'Optional: Max citations to return. Adjust as needed.', minimum: 1, maximum: maxTopK, default: retrievalLimit ?? 3 },
scope: { type: 'string', enum: ['thread'], description: 'Retrieval scope; currently only thread is supported' },
file_ids: { type: 'array', items: { type: 'string' }, description: 'Optional: Filter search to specific file IDs from list_attachments' },
},
required: ['query', 'scope'],
},
server: RAG_INTERNAL_SERVER,
},
{
name: GET_CHUNKS,
description:
'Retrieve chunks from a file by their order range. For a single chunk, use start_order = end_order. Thread context is inferred automatically; you may optionally provide {"scope":"thread"}. Use sparingly; intended for advanced usage. Prefer using retrieve instead for relevance-based fetching.',
inputSchema: {
type: 'object',
properties: {
file_id: { type: 'string', description: 'File ID from list_attachments' },
start_order: { type: 'number', description: 'Start of chunk range (inclusive, 0-indexed)' },
end_order: { type: 'number', description: 'End of chunk range (inclusive, 0-indexed). For single chunk, use start_order = end_order.' },
scope: { type: 'string', enum: ['thread'], description: 'Retrieval scope; currently only thread is supported' },
},
required: ['file_id', 'start_order', 'end_order', 'scope'],
},
server: RAG_INTERNAL_SERVER,
},
]
}

View File

@ -1,33 +0,0 @@
{
"name": "@janhq/vector-db-extension",
"productName": "Vector DB",
"version": "0.1.0",
"description": "Vector DB integration using sqlite-vec if available with linear fallback",
"main": "dist/index.js",
"module": "dist/module.js",
"author": "Jan <service@jan.ai>",
"license": "AGPL-3.0",
"scripts": {
"build": "rolldown -c rolldown.config.mjs",
"build:publish": "rimraf *.tgz --glob || true && yarn build && npm pack && cpx *.tgz ../../pre-install"
},
"devDependencies": {
"cpx": "1.5.0",
"rimraf": "6.0.1",
"rolldown": "1.0.0-beta.1",
"typescript": "5.9.2"
},
"dependencies": {
"@janhq/core": "../../core/package.tgz",
"@janhq/tauri-plugin-rag-api": "link:../../src-tauri/plugins/tauri-plugin-rag",
"@janhq/tauri-plugin-vector-db-api": "link:../../src-tauri/plugins/tauri-plugin-vector-db"
},
"files": [
"dist/*",
"package.json"
],
"installConfig": {
"hoistingLimits": "workspaces"
},
"packageManager": "yarn@4.5.3"
}

View File

@ -1,11 +0,0 @@
import { defineConfig } from 'rolldown'
export default defineConfig({
input: 'src/index.ts',
output: {
format: 'esm',
file: 'dist/index.js',
},
platform: 'browser',
define: {},
})

Some files were not shown because too many files have changed in this diff Show More