Merge branch 'dev' into feat/changelog-v0.5.16

This commit is contained in:
Emre Can Kartal 2025-03-24 12:32:00 +03:00 committed by GitHub
commit cd4be34559
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
491 changed files with 15017 additions and 13255 deletions

View File

@ -3,5 +3,5 @@ blank_issues_enabled: true
contact_links:
- name: "\1F4AC 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

@ -9,31 +9,6 @@ jobs:
get-update-version:
uses: ./.github/workflows/template-get-update-version.yml
create-draft-release:
runs-on: ubuntu-latest
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
version: ${{ steps.get_version.outputs.version }}
permissions:
contents: write
steps:
- name: Extract tag name without v prefix
id: get_version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV && echo "::set-output name=version::${GITHUB_REF#refs/tags/v}"
env:
GITHUB_REF: ${{ github.ref }}
- name: Create Draft Release
id: create_release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
token: ${{ secrets.GITHUB_TOKEN }}
name: "${{ env.VERSION }}"
draft: true
prerelease: false
generate_release_notes: true
build-macos:
uses: ./.github/workflows/template-build-macos.yml
secrets: inherit
@ -43,6 +18,8 @@ jobs:
public_provider: github
new_version: ${{ needs.get-update-version.outputs.new_version }}
beta: true
nightly: false
cortex_api_port: "39271"
build-windows-x64:
uses: ./.github/workflows/template-build-windows-x64.yml
@ -53,6 +30,8 @@ jobs:
public_provider: github
new_version: ${{ needs.get-update-version.outputs.new_version }}
beta: true
nightly: false
cortex_api_port: "39271"
build-linux-x64:
uses: ./.github/workflows/template-build-linux-x64.yml
@ -63,9 +42,11 @@ jobs:
public_provider: github
new_version: ${{ needs.get-update-version.outputs.new_version }}
beta: true
nightly: false
cortex_api_port: "39271"
sync-temp-to-latest:
needs: [build-macos, create-draft-release, build-windows-x64, build-linux-x64]
needs: [build-macos, build-windows-x64, build-linux-x64]
runs-on: ubuntu-latest
permissions:
contents: write
@ -82,19 +63,15 @@ jobs:
AWS_DEFAULT_REGION: ${{ secrets.DELTA_AWS_REGION }}
AWS_EC2_METADATA_DISABLED: "true"
- name: set release to prerelease
run: |
gh release edit v${{ needs.create-draft-release.outputs.version }} --draft=false --prerelease
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
noti-discord-and-update-url-readme:
needs: [build-macos, create-draft-release, build-windows-x64, build-linux-x64, sync-temp-to-latest]
needs: [build-macos, get-update-version, build-windows-x64, build-linux-x64, sync-temp-to-latest]
runs-on: ubuntu-latest
steps:
- name: Set version to environment variable
run: |
echo "VERSION=${{ needs.create-draft-release.outputs.version }}" >> $GITHUB_ENV
VERSION=${{ needs.get-update-version.outputs.new_version }}
VERSION="${VERSION#v}"
echo "VERSION=$VERSION" >> $GITHUB_ENV
- name: Notify Discord
uses: Ilshidur/action-discord@master
@ -105,6 +82,5 @@ jobs:
- macOS Universal: https://delta.jan.ai/beta/jan-beta-mac-universal-{{ VERSION }}.dmg
- Linux Deb: https://delta.jan.ai/beta/jan-beta-linux-amd64-{{ VERSION }}.deb
- Linux AppImage: https://delta.jan.ai/beta/jan-beta-linux-x86_64-{{ VERSION }}.AppImage
- Github Release URL: https://github.com/janhq/jan/releases/tag/v{{ VERSION }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_JAN_BETA }}

View File

@ -55,6 +55,9 @@ jobs:
ref: ${{ needs.set-public-provider.outputs.ref }}
public_provider: ${{ needs.set-public-provider.outputs.public_provider }}
new_version: ${{ needs.get-update-version.outputs.new_version }}
nightly: true
beta: false
cortex_api_port: "39261"
build-windows-x64:
uses: ./.github/workflows/template-build-windows-x64.yml
@ -64,8 +67,9 @@ jobs:
ref: ${{ needs.set-public-provider.outputs.ref }}
public_provider: ${{ needs.set-public-provider.outputs.public_provider }}
new_version: ${{ needs.get-update-version.outputs.new_version }}
nightly: true
beta: false
cortex_api_port: "39261"
build-linux-x64:
uses: ./.github/workflows/template-build-linux-x64.yml
secrets: inherit
@ -74,6 +78,9 @@ jobs:
ref: ${{ needs.set-public-provider.outputs.ref }}
public_provider: ${{ needs.set-public-provider.outputs.public_provider }}
new_version: ${{ needs.get-update-version.outputs.new_version }}
nightly: true
beta: false
cortex_api_port: "39261"
sync-temp-to-latest:
needs: [set-public-provider, build-windows-x64, build-linux-x64, build-macos]
@ -141,4 +148,3 @@ jobs:
RUN_ID=${{ github.run_id }}
COMMENT="This is the build for this pull request. You can download it from the Artifacts section here: [Build URL](https://github.com/${{ github.repository }}/actions/runs/${RUN_ID})."
gh pr comment $PR_URL --body "$COMMENT"

View File

@ -40,6 +40,8 @@ jobs:
with:
ref: ${{ github.ref }}
public_provider: github
beta: false
nightly: false
new_version: ${{ needs.get-update-version.outputs.new_version }}
build-windows-x64:
@ -49,6 +51,8 @@ jobs:
with:
ref: ${{ github.ref }}
public_provider: github
beta: false
nightly: false
new_version: ${{ needs.get-update-version.outputs.new_version }}
build-linux-x64:
@ -58,6 +62,8 @@ jobs:
with:
ref: ${{ github.ref }}
public_provider: github
beta: false
nightly: false
new_version: ${{ needs.get-update-version.outputs.new_version }}
update_release_draft:
@ -82,4 +88,4 @@ jobs:
# config-name: my-config.yml
# disable-autolabeler: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -36,7 +36,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.PAT_SERVICE_ACCOUNT }}
run: |
curl -s https://api.github.com/repos/janhq/cortex/releases > /tmp/github_api_releases.json
curl -s https://api.github.com/repos/menloresearch/cortex/releases > /tmp/github_api_releases.json
latest_prerelease_name=$(cat /tmp/github_api_releases.json | jq -r '.[] | select(.prerelease) | .name' | head -n 1)
get_asset_count() {
@ -89,39 +89,39 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
submodules: recursive
fetch-depth: 0
token: ${{ secrets.PAT_SERVICE_ACCOUNT }}
- name: Checkout repository
uses: actions/checkout@v3
with:
submodules: recursive
fetch-depth: 0
token: ${{ secrets.PAT_SERVICE_ACCOUNT }}
- name: Wait for CI to pass
env:
- name: Wait for CI to pass
env:
GITHUB_TOKEN: ${{ secrets.PAT_SERVICE_ACCOUNT }}
run: |
pr_number=${{ needs.update-submodule.outputs.pr_number }}
while true; do
ci_completed=$(gh pr checks $pr_number --json completedAt --jq '.[].completedAt')
if echo "$ci_completed" | grep -q "0001-01-01T00:00:00Z"; then
echo "CI is still running, waiting..."
sleep 60
else
echo "CI has completed, checking states..."
ci_states=$(gh pr checks $pr_number --json state --jq '.[].state')
if echo "$ci_states" | grep -vqE "SUCCESS|SKIPPED"; then
echo "CI failed, exiting..."
exit 1
run: |
pr_number=${{ needs.update-submodule.outputs.pr_number }}
while true; do
ci_completed=$(gh pr checks $pr_number --json completedAt --jq '.[].completedAt')
if echo "$ci_completed" | grep -q "0001-01-01T00:00:00Z"; then
echo "CI is still running, waiting..."
sleep 60
else
echo "CI passed, merging PR..."
break
echo "CI has completed, checking states..."
ci_states=$(gh pr checks $pr_number --json state --jq '.[].state')
if echo "$ci_states" | grep -vqE "SUCCESS|SKIPPED"; then
echo "CI failed, exiting..."
exit 1
else
echo "CI passed, merging PR..."
break
fi
fi
fi
done
done
- name: Merge the PR
env:
- name: Merge the PR
env:
GITHUB_TOKEN: ${{ secrets.PAT_SERVICE_ACCOUNT }}
run: |
pr_number=${{ needs.update-submodule.outputs.pr_number }}
gh pr merge $pr_number --merge --admin
run: |
pr_number=${{ needs.update-submodule.outputs.pr_number }}
gh pr merge $pr_number --merge --admin

View File

@ -1,39 +0,0 @@
name: build-jan-server
on:
workflow_call:
inputs:
dockerfile_path:
required: false
type: string
default: './Dockerfile'
docker_image_tag:
required: true
type: string
default: 'ghcr.io/janhq/jan-server:dev-latest'
jobs:
build:
runs-on: ubuntu-latest
env:
REGISTRY: ghcr.io
IMAGE_NAME: janhq/jan-server
permissions:
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v3
with:
context: .
file: ${{ inputs.dockerfile_path }}
push: true
tags: ${{ inputs.docker_image_tag }}

View File

@ -23,6 +23,14 @@ on:
required: false
type: boolean
default: false
nightly:
required: false
type: boolean
default: false
cortex_api_port:
required: false
type: string
default: null
secrets:
DELTA_AWS_S3_BUCKET_NAME:
required: false
@ -43,6 +51,31 @@ jobs:
with:
ref: ${{ inputs.ref }}
- name: Replace Icons for Beta Build
if: inputs.beta == true && inputs.nightly != true
shell: bash
run: |
rm -rf electron/icons/*
cp electron/icons_dev/jan-beta-512x512.png electron/icons/512x512.png
cp electron/icons_dev/jan-beta.ico electron/icons/icon.ico
cp electron/icons_dev/jan-beta.png electron/icons/icon.png
cp electron/icons_dev/jan-beta-tray@2x.png electron/icons/icon-tray@2x.png
cp electron/icons_dev/jan-beta-tray.png electron/icons/icon-tray.png
- name: Replace Icons for Nightly Build
if: inputs.nightly == true && inputs.beta != true
shell: bash
run: |
rm -rf electron/icons/*
cp electron/icons_dev/jan-nightly-512x512.png electron/icons/512x512.png
cp electron/icons_dev/jan-nightly.ico electron/icons/icon.ico
cp electron/icons_dev/jan-nightly.png electron/icons/icon.png
cp electron/icons_dev/jan-nightly-tray@2x.png electron/icons/icon-tray@2x.png
cp electron/icons_dev/jan-nightly-tray.png electron/icons/icon-tray.png
- name: Installing node
uses: actions/setup-node@v1
with:
@ -83,7 +116,7 @@ jobs:
cat ./electron/package.json
echo "------------------------"
cat ./package.json
jq '.build.publish = [{"provider": "generic", "url": "https://delta.jan.ai/beta", "channel": "beta"}, {"provider": "github", "owner": "janhq", "repo": "jan", "channel": "beta"}, {"provider": "s3", "acl": null, "bucket": "${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}", "region": "${{ secrets.DELTA_AWS_REGION}}", "path": "temp-beta", "channel": "beta"}]' electron/package.json > /tmp/package.json
jq '.build.publish = [{"provider": "generic", "url": "https://delta.jan.ai/beta", "channel": "beta"}, {"provider": "s3", "acl": null, "bucket": "${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}", "region": "${{ secrets.DELTA_AWS_REGION}}", "path": "temp-beta", "channel": "beta"}]' electron/package.json > /tmp/package.json
mv /tmp/package.json electron/package.json
cat electron/package.json
@ -115,6 +148,7 @@ jobs:
AWS_MAX_ATTEMPTS: '5'
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
CORTEX_API_PORT: ${{ inputs.cortex_api_port }}
- name: Build and publish app to github
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == false
@ -150,4 +184,4 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: jan-linux-amd64-${{ inputs.new_version }}-AppImage
path: ./electron/dist/*.AppImage
path: ./electron/dist/*.AppImage

View File

@ -23,6 +23,14 @@ on:
required: false
type: boolean
default: false
nightly:
required: false
type: boolean
default: false
cortex_api_port:
required: false
type: string
default: null
secrets:
DELTA_AWS_S3_BUCKET_NAME:
required: false
@ -52,6 +60,30 @@ jobs:
uses: actions/checkout@v3
with:
ref: ${{ inputs.ref }}
- name: Replace Icons for Beta Build
if: inputs.beta == true && inputs.nightly != true
shell: bash
run: |
rm -rf electron/icons/*
cp electron/icons_dev/jan-beta-512x512.png electron/icons/512x512.png
cp electron/icons_dev/jan-beta.ico electron/icons/icon.ico
cp electron/icons_dev/jan-beta.png electron/icons/icon.png
cp electron/icons_dev/jan-beta-tray@2x.png electron/icons/icon-tray@2x.png
cp electron/icons_dev/jan-beta-tray.png electron/icons/icon-tray.png
- name: Replace Icons for Nightly Build
if: inputs.nightly == true && inputs.beta != true
shell: bash
run: |
rm -rf electron/icons/*
cp electron/icons_dev/jan-nightly-512x512.png electron/icons/512x512.png
cp electron/icons_dev/jan-nightly.ico electron/icons/icon.ico
cp electron/icons_dev/jan-nightly.png electron/icons/icon.png
cp electron/icons_dev/jan-nightly-tray@2x.png electron/icons/icon-tray@2x.png
cp electron/icons_dev/jan-nightly-tray.png electron/icons/icon-tray.png
- name: Installing node
uses: actions/setup-node@v1
@ -99,7 +131,7 @@ jobs:
cat ./electron/package.json
echo "------------------------"
cat ./package.json
jq '.build.publish = [{"provider": "generic", "url": "https://delta.jan.ai/beta", "channel": "beta"}, {"provider": "github", "owner": "janhq", "repo": "jan", "channel": "beta"}, {"provider": "s3", "acl": null, "bucket": "${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}", "region": "${{ secrets.DELTA_AWS_REGION}}", "path": "temp-beta", "channel": "beta"}]' electron/package.json > /tmp/package.json
jq '.build.publish = [{"provider": "generic", "url": "https://delta.jan.ai/beta", "channel": "beta"}, {"provider": "s3", "acl": null, "bucket": "${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}", "region": "${{ secrets.DELTA_AWS_REGION}}", "path": "temp-beta", "channel": "beta"}]' electron/package.json > /tmp/package.json
mv /tmp/package.json electron/package.json
cat electron/package.json
@ -154,6 +186,7 @@ jobs:
AWS_MAX_ATTEMPTS: '5'
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
CORTEX_API_PORT: ${{ inputs.cortex_api_port }}
- name: Build and publish app to github
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == false
@ -197,4 +230,4 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: jan-mac-universal-${{ inputs.new_version }}
path: ./electron/dist/*.dmg
path: ./electron/dist/*.dmg

View File

@ -23,6 +23,14 @@ on:
required: false
type: boolean
default: false
nightly:
required: false
type: boolean
default: false
cortex_api_port:
required: false
type: string
default: null
secrets:
DELTA_AWS_S3_BUCKET_NAME:
required: false
@ -52,6 +60,30 @@ jobs:
with:
ref: ${{ inputs.ref }}
- name: Replace Icons for Beta Build
if: inputs.beta == true && inputs.nightly != true
shell: bash
run: |
rm -rf electron/icons/*
cp electron/icons_dev/jan-beta-512x512.png electron/icons/512x512.png
cp electron/icons_dev/jan-beta.ico electron/icons/icon.ico
cp electron/icons_dev/jan-beta.png electron/icons/icon.png
cp electron/icons_dev/jan-beta-tray@2x.png electron/icons/icon-tray@2x.png
cp electron/icons_dev/jan-beta-tray.png electron/icons/icon-tray.png
- name: Replace Icons for Nightly Build
if: inputs.nightly == true && inputs.beta != true
shell: bash
run: |
rm -rf electron/icons/*
cp electron/icons_dev/jan-nightly-512x512.png electron/icons/512x512.png
cp electron/icons_dev/jan-nightly.ico electron/icons/icon.ico
cp electron/icons_dev/jan-nightly.png electron/icons/icon.png
cp electron/icons_dev/jan-nightly-tray@2x.png electron/icons/icon-tray@2x.png
cp electron/icons_dev/jan-nightly-tray.png electron/icons/icon-tray.png
- name: Installing node
uses: actions/setup-node@v1
with:
@ -108,7 +140,7 @@ jobs:
cat ./package.json
echo "------------------------"
cat ./electron/scripts/uninstaller.nsh
jq '.build.publish = [{"provider": "generic", "url": "https://delta.jan.ai/beta", "channel": "beta"}, {"provider": "github", "owner": "janhq", "repo": "jan", "channel": "beta"}, {"provider": "s3", "acl": null, "bucket": "${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}", "region": "${{ secrets.DELTA_AWS_REGION}}", "path": "temp-beta", "channel": "beta"}]' electron/package.json > /tmp/package.json
jq '.build.publish = [{"provider": "generic", "url": "https://delta.jan.ai/beta", "channel": "beta"}, {"provider": "s3", "acl": null, "bucket": "${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}", "region": "${{ secrets.DELTA_AWS_REGION}}", "path": "temp-beta", "channel": "beta"}]' electron/package.json > /tmp/package.json
mv /tmp/package.json electron/package.json
cat electron/package.json
@ -153,6 +185,7 @@ jobs:
AWS_MAX_ATTEMPTS: '5'
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
CORTEX_API_PORT: ${{ inputs.cortex_api_port }}
- name: Build app and publish app to github
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == false
@ -193,4 +226,4 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: jan-win-x64-${{ inputs.new_version }}
path: ./electron/dist/*.exe
path: ./electron/dist/*.exe

View File

@ -13,46 +13,46 @@ jobs:
outputs:
new_version: ${{ steps.version_update.outputs.new_version }}
steps:
- name: Install jq
uses: dcarbone/install-jq-action@v2.0.1
- name: Install jq
uses: dcarbone/install-jq-action@v2.0.1
- name: Get tag
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
id: tag
uses: dawidd6/action-get-tag@v1
- name: Get tag
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
id: tag
uses: dawidd6/action-get-tag@v1
- name: Update app version based on latest release tag with build number
id: version_update
run: |
# Function to get the latest release tag
get_latest_tag() {
local retries=0
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)
if [ -n "$tag" ] && [ "$tag" != "null" ]; then
echo $tag
return
else
let retries++
echo "Retrying... ($retries/$max_retries)"
sleep 2
fi
done
echo "Failed to fetch latest tag after $max_retries attempts."
exit 1
}
- name: Update app version based on latest release tag with build number
id: version_update
run: |
# Function to get the latest release tag
get_latest_tag() {
local retries=0
local max_retries=3
local tag
while [ $retries -lt $max_retries ]; do
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
else
let retries++
echo "Retrying... ($retries/$max_retries)"
sleep 2
fi
done
echo "Failed to fetch latest tag after $max_retries attempts."
exit 1
}
if ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') }}; then
echo "Tag detected, set output follow tag"
echo "::set-output name=new_version::${{ steps.tag.outputs.tag }}"
else
# Get the latest release tag from GitHub API
LATEST_TAG=$(get_latest_tag)
# Remove the 'v' and append the build number to the version
new_version="${LATEST_TAG#v}-${GITHUB_RUN_NUMBER}"
echo "New version: $new_version"
echo "::set-output name=new_version::$new_version"
fi
if ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') }}; then
echo "Tag detected, set output follow tag"
echo "::set-output name=new_version::${{ steps.tag.outputs.tag }}"
else
# Get the latest release tag from GitHub API
LATEST_TAG=$(get_latest_tag)
# Remove the 'v' and append the build number to the version
new_version="${LATEST_TAG#v}-${GITHUB_RUN_NUMBER}"
echo "New version: $new_version"
echo "::set-output name=new_version::$new_version"
fi

View File

@ -34,7 +34,7 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: "0"
fetch-depth: '0'
token: ${{ secrets.PAT_SERVICE_ACCOUNT }}
ref: ${{ inputs.ref }}
@ -51,6 +51,6 @@ jobs:
- macOS Universal: https://delta.jan.ai/nightly/jan-nightly-mac-universal-{{ VERSION }}.dmg
- Linux Deb: https://delta.jan.ai/nightly/jan-nightly-linux-amd64-{{ VERSION }}.deb
- Linux AppImage: https://delta.jan.ai/nightly/jan-nightly-linux-x86_64-{{ VERSION }}.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 }}
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}

View File

@ -1 +1 @@
npx oxlint@latest --fix
yarn lint --fix --quiet

View File

@ -6,8 +6,8 @@ First off, thank you for considering contributing to jan. It's people like you t
### 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).
### Suggesting Enhancements
@ -29,4 +29,4 @@ First off, thank you for considering contributing to jan. It's people like you t
## Additional Notes
Thank you for contributing to jan!
Thank you for contributing to jan!

View File

@ -4,18 +4,18 @@
<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>
<p align="center">
<a href="https://jan.ai/docs/quickstart">Getting Started</a>
- <a href="https://jan.ai/docs">Docs</a>
- <a href="https://github.com/janhq/jan/releases">Changelog</a>
- <a href="https://github.com/janhq/jan/issues">Bug reports</a>
- <a href="https://github.com/menloresearch/jan/releases">Changelog</a>
- <a href="https://github.com/menloresearch/jan/issues">Bug reports</a>
- <a href="https://discord.gg/AsJ8krTT3N">Discord</a>
</p>
@ -23,10 +23,9 @@
⚠️ <b> Jan is currently in Development</b>: Expect breaking changes and bugs!
</p>
Jan is a ChatGPT-alternative that runs 100% offline on your device. Our goal is to make it easy for a layperson to download and run LLMs and use AI with **full control** and **privacy**.
Jan is powered by [Cortex](https://github.com/janhq/cortex.cpp), our embeddable local AI engine that runs on any hardware.
Jan is powered by [Cortex](https://github.com/menloresearch/cortex.cpp), our embeddable local AI engine that runs on any hardware.
From PCs to multi-GPU clusters, Jan & Cortex supports universal architectures:
- [x] NVIDIA GPUs (fast)
@ -36,7 +35,8 @@ From PCs to multi-GPU clusters, Jan & Cortex supports universal architectures:
- [x] Windows x64
#### Features:
- [Model Library](https://jan.ai/docs/models/manage-models#add-models) with popular LLMs like Llama, Gemma, Mistral, or Qwen
- [Model Library](https://jan.ai/docs/models/manage-models#add-models) with popular LLMs like Llama, Gemma, Mistral, or Qwen
- Connect to [Remote AI APIs](https://jan.ai/docs/remote-models/openai) like Groq and OpenRouter
- Local API Server with OpenAI-equivalent API
- [Extensions](https://jan.ai/docs/extensions) for customizing Jan
@ -54,25 +54,25 @@ From PCs to multi-GPU clusters, Jan & Cortex supports universal architectures:
<td style="text-align:center"><b>Stable (Recommended)</b></td>
<td style="text-align:center">
<a href='https://app.jan.ai/download/latest/win-x64'>
<img src='https://github.com/janhq/jan/blob/dev/docs/static/img/windows.png' style="height:14px; width: 14px" />
<img src='https://github.com/menloresearch/jan/blob/dev/docs/static/img/windows.png' style="height:14px; width: 14px" />
<b>jan.exe</b>
</a>
</td>
<td style="text-align:center">
<a href='https://app.jan.ai/download/latest/mac-universal'>
<img src='https://github.com/janhq/jan/blob/dev/docs/static/img/mac.png' style="height:15px; width: 15px" />
<img src='https://github.com/menloresearch/jan/blob/dev/docs/static/img/mac.png' style="height:15px; width: 15px" />
<b>jan.dmg</b>
</a>
</td>
<td style="text-align:center">
<a href='https://app.jan.ai/download/latest/linux-amd64-deb'>
<img src='https://github.com/janhq/jan/blob/dev/docs/static/img/linux.png' style="height:14px; width: 14px" />
<img src='https://github.com/menloresearch/jan/blob/dev/docs/static/img/linux.png' style="height:14px; width: 14px" />
<b>jan.deb</b>
</a>
</td>
<td style="text-align:center">
<a href='https://app.jan.ai/download/latest/linux-amd64-appimage'>
<img src='https://github.com/janhq/jan/blob/dev/docs/static/img/linux.png' style="height:14px; width: 14px" />
<img src='https://github.com/menloresearch/jan/blob/dev/docs/static/img/linux.png' style="height:14px; width: 14px" />
<b>jan.AppImage</b>
</a>
</td>
@ -81,25 +81,25 @@ From PCs to multi-GPU clusters, Jan & Cortex supports universal architectures:
<td style="text-align:center"><b>Beta (Preview)</b></td>
<td style="text-align:center">
<a href='https://app.jan.ai/download/beta/win-x64'>
<img src='https://github.com/janhq/jan/blob/dev/docs/static/img/windows.png' style="height:14px; width: 14px" />
<img src='https://github.com/menloresearch/jan/blob/dev/docs/static/img/windows.png' style="height:14px; width: 14px" />
<b>jan.exe</b>
</a>
</td>
<td style="text-align:center">
<a href='https://app.jan.ai/download/beta/mac-universal'>
<img src='https://github.com/janhq/jan/blob/dev/docs/static/img/mac.png' style="height:15px; width: 15px" />
<img src='https://github.com/menloresearch/jan/blob/dev/docs/static/img/mac.png' style="height:15px; width: 15px" />
<b>jan.dmg</b>
</a>
</td>
<td style="text-align:center">
<a href='https://app.jan.ai/download/beta/linux-amd64-deb'>
<img src='https://github.com/janhq/jan/blob/dev/docs/static/img/linux.png' style="height:14px; width: 14px" />
<img src='https://github.com/menloresearch/jan/blob/dev/docs/static/img/linux.png' style="height:14px; width: 14px" />
<b>jan.deb</b>
</a>
</td>
<td style="text-align:center">
<a href='https://app.jan.ai/download/beta/linux-amd64-appimage'>
<img src='https://github.com/janhq/jan/blob/dev/docs/static/img/linux.png' style="height:14px; width: 14px" />
<img src='https://github.com/menloresearch/jan/blob/dev/docs/static/img/linux.png' style="height:14px; width: 14px" />
<b>jan.AppImage</b>
</a>
</td>
@ -108,59 +108,59 @@ From PCs to multi-GPU clusters, Jan & Cortex supports universal architectures:
<td style="text-align:center"><b>Nightly Build (Experimental)</b></td>
<td style="text-align:center">
<a href='https://app.jan.ai/download/nightly/win-x64'>
<img src='https://github.com/janhq/jan/blob/dev/docs/static/img/windows.png' style="height:14px; width: 14px" />
<img src='https://github.com/menloresearch/jan/blob/dev/docs/static/img/windows.png' style="height:14px; width: 14px" />
<b>jan.exe</b>
</a>
</td>
<td style="text-align:center">
<a href='https://app.jan.ai/download/nightly/mac-universal'>
<img src='https://github.com/janhq/jan/blob/dev/docs/static/img/mac.png' style="height:15px; width: 15px" />
<img src='https://github.com/menloresearch/jan/blob/dev/docs/static/img/mac.png' style="height:15px; width: 15px" />
<b>jan.dmg</b>
</a>
</td>
<td style="text-align:center">
<a href='https://app.jan.ai/download/nightly/linux-amd64-deb'>
<img src='https://github.com/janhq/jan/blob/dev/docs/static/img/linux.png' style="height:14px; width: 14px" />
<img src='https://github.com/menloresearch/jan/blob/dev/docs/static/img/linux.png' style="height:14px; width: 14px" />
<b>jan.deb</b>
</a>
</td>
<td style="text-align:center">
<a href='https://app.jan.ai/download/nightly/linux-amd64-appimage'>
<img src='https://github.com/janhq/jan/blob/dev/docs/static/img/linux.png' style="height:14px; width: 14px" />
<img src='https://github.com/menloresearch/jan/blob/dev/docs/static/img/linux.png' style="height:14px; width: 14px" />
<b>jan.AppImage</b>
</a>
</td>
</tr>
</table>
Download the latest version of Jan at https://jan.ai/ or visit the [GitHub Releases](https://github.com/janhq/jan/releases) to download any previous release.
Download the latest version of Jan at https://jan.ai/ or visit the [GitHub Releases](https://github.com/menloresearch/jan/releases) to download any previous release.
## Demo
https://github.com/user-attachments/assets/c3592fa2-c504-4d9d-a885-7e00122a50f3
*Real-time Video: Jan v0.5.7 on a Mac M2, 16GB Sonoma 14.2*
_Real-time Video: Jan v0.5.7 on a Mac M2, 16GB Sonoma 14.2_
## Quicklinks
### Jan
- [Jan Website](https://jan.ai/)
- [Jan GitHub](https://github.com/janhq/jan)
- [Jan GitHub](https://github.com/menloresearch/jan)
- [Documentation](https://jan.ai/docs)
- [Jan Changelog](https://jan.ai/changelog)
- [Jan Blog](https://jan.ai/blog)
### Cortex.cpp
Jan is powered by **Cortex.cpp**. It is a C++ command-line interface (CLI) designed as an alternative to [Ollama](https://ollama.com/). By default, it runs on the llama.cpp engine but also supports other engines, including ONNX and TensorRT-LLM, making it a multi-engine platform.
- [Cortex Website](https://cortex.so/)
- [Cortex GitHub](https://github.com/janhq/cortex.cpp)
- [Cortex GitHub](https://github.com/menloresearch/cortex.cpp)
- [Documentation](https://cortex.so/docs/)
- [Models Library](https://cortex.so/models)
- API Reference: *Under development*
- API Reference: _Under development_
## Requirements for running Jan
- **MacOS**: 13 or higher
@ -179,17 +179,17 @@ Jan is powered by **Cortex.cpp**. It is a C++ command-line interface (CLI) desig
## Troubleshooting
As Jan is in development mode, you might get stuck on a some common issues:
- [Troubleshooting a broken build](https://jan.ai/docs/troubleshooting#broken-build)
- [Troubleshooting NVIDIA GPU](https://jan.ai/docs/troubleshooting#troubleshooting-nvidia-gpu)
- [Troubleshooting Something's Amiss](https://jan.ai/docs/troubleshooting#somethings-amiss)
If you can't find what you need in our troubleshooting guide, feel free reach out to us for extra help:
1. Copy your [error logs & device specifications](https://jan.ai/docs/troubleshooting#how-to-get-error-logs).
2. Go to our [Discord](https://discord.com/invite/FTk2MvZwJH) & send it to **#🆘|get-help** channel for further support.
*Check the logs to ensure the information is what you intend to send. Note that we retain your logs for only 24 hours, so report any issues promptly.*
_Check the logs to ensure the information is what you intend to send. Note that we retain your logs for only 24 hours, so report any issues promptly._
## Contributing
@ -206,7 +206,7 @@ Contributions are welcome! Please read the [CONTRIBUTING.md](CONTRIBUTING.md) fi
1. **Clone the repository and prepare:**
```bash
git clone https://github.com/janhq/jan
git clone https://github.com/menloresearch/jan
cd jan
git checkout -b DESIRED_BRANCH
```
@ -219,8 +219,6 @@ Contributions are welcome! Please read the [CONTRIBUTING.md](CONTRIBUTING.md) fi
This will start the development server and open the desktop app.
### For production build
```bash
@ -244,7 +242,7 @@ Jan builds on top of other open-source projects:
- Bugs & requests: file a GitHub ticket
- For discussion: join our Discord [here](https://discord.gg/FTk2MvZwJH)
- For business inquiries: email hello@jan.ai
- For business inquiries: email hello@jan.ai
- For jobs: please email hr@jan.ai
## Trust & Safety
@ -254,7 +252,7 @@ Beware of scams!
- We will never request your personal information.
- Our product is completely free; no paid version exists.
- We do not have a token or ICO.
- We are a [bootstrapped company](https://en.wikipedia.org/wiki/Bootstrapping), and don't have any external investors (*yet*). We're open to exploring opportunities with strategic partners want to tackle [our mission](https://jan.ai/about#mission) together.
- We are a [bootstrapped company](https://en.wikipedia.org/wiki/Bootstrapping), and don't have any external investors (_yet_). We're open to exploring opportunities with strategic partners want to tackle [our mission](https://jan.ai/about#mission) together.
## License

View File

@ -28,7 +28,7 @@
</screenshots>
<url type="homepage">https://jan.ai/</url>
<url type="bugtracker">https://github.com/janhq/jan/issues</url>
<url type="bugtracker">https://github.com/menloresearch/jan/issues</url>
<content_rating type="oars-1.1" />

View File

@ -8,37 +8,38 @@
```js
// Web / extension runtime
import * as core from "@janhq/core";
import * as core from '@janhq/core'
// Node runtime
import * as node from "@janhq/core/node";
import * as node from '@janhq/core/node'
```
## 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:
1. Open `index.ts` in your code editor.
2. Rename the extension class from `SampleExtension` to your preferred extension name.
3. Import modules from the core package.
```ts
import * as core from "@janhq/core";
import * as core from '@janhq/core'
```
4. In the `onLoad()` method, add your code:
```ts
// Example of listening to app events and providing customized inference logic:
import * as core from "@janhq/core";
import * as core from '@janhq/core'
export default class MyExtension extends BaseExtension {
// On extension load
onLoad() {
core.events.on(MessageEvent.OnMessageSent, (data) => MyExtension.inference(data, this));
core.events.on(MessageEvent.OnMessageSent, (data) => MyExtension.inference(data, this))
}
// Customized inference logic
private static inference(incomingMessage: MessageRequestData) {
// Prepare customized message content
const content: ThreadContent = {
type: ContentType.Text,
@ -46,16 +47,17 @@ import * as node from "@janhq/core/node";
value: "I'm Jan Assistant!",
annotations: [],
},
};
}
// Modify message and send out
const outGoingMessage: ThreadMessage = {
...incomingMessage,
content
};
content,
}
}
}
```
3. Build the extension:
1. Navigate to the extension directory.
2. Install dependencies.
@ -66,4 +68,4 @@ import * as node from "@janhq/core/node";
```bash
yarn build
```
4. Select the generated .tgz from Jan > Settings > Extension > Manual Installation.
4. Select the generated .tgz from Jan > Settings > Extension > Manual Installation.

View File

@ -25,7 +25,6 @@ export default defineConfig([
'@types/pacote',
'@npmcli/arborist',
'ulidx',
'node-fetch',
'fs',
'request',
'crypto',

View File

@ -2,7 +2,6 @@ import { openExternalUrl } from './core'
import { joinPath } from './core'
import { openFileExplorer } from './core'
import { getJanDataFolderPath } from './core'
import { abortDownload } from './core'
import { executeOnMain } from './core'
describe('test core apis', () => {
@ -53,18 +52,6 @@ describe('test core apis', () => {
expect(result).toBe('/path/to/jan/data')
})
it('should abort download', async () => {
const fileName = 'testFile'
globalThis.core = {
api: {
abortDownload: jest.fn().mockResolvedValue('aborted'),
},
}
const result = await abortDownload(fileName)
expect(globalThis.core.api.abortDownload).toHaveBeenCalledWith(fileName)
expect(result).toBe('aborted')
})
it('should execute function on main process', async () => {
const extension = 'testExtension'
const method = 'testMethod'

View File

@ -1,9 +1,4 @@
import {
DownloadRequest,
FileStat,
NetworkConfig,
SystemInformation,
} from '../types'
import { SystemInformation } from '../types'
/**
* Execute a extension module function in main process
@ -14,42 +9,19 @@ import {
* @returns Promise<any>
*
*/
const executeOnMain: (
extension: string,
method: string,
...args: any[]
) => Promise<any> = (extension, method, ...args) =>
globalThis.core?.api?.invokeExtensionFunc(extension, method, ...args)
const executeOnMain: (extension: string, method: string, ...args: any[]) => Promise<any> = (
extension,
method,
...args
) => globalThis.core?.api?.invokeExtensionFunc(extension, method, ...args)
/**
* Downloads a file from a URL and saves it to the local file system.
*
* @param {DownloadRequest} downloadRequest - The request to download the file.
* @param {NetworkConfig} network - Optional object to specify proxy/whether to ignore SSL certificates.
*
* @returns {Promise<any>} A promise that resolves when the file is downloaded.
*/
const downloadFile: (
downloadRequest: DownloadRequest,
network?: NetworkConfig
) => Promise<any> = (downloadRequest, network) =>
globalThis.core?.api?.downloadFile(downloadRequest, network)
/**
* Aborts the download of a specific file.
* @param {string} fileName - The name of the file whose download is to be aborted.
* @returns {Promise<any>} A promise that resolves when the download has been aborted.
*/
const abortDownload: (fileName: string) => Promise<any> = (fileName) =>
globalThis.core.api?.abortDownload(fileName)
/**
* Gets Jan's data folder path.
*
* @returns {Promise<string>} A Promise that resolves with Jan's data folder path.
*/
const getJanDataFolderPath = (): Promise<string> =>
globalThis.core.api?.getJanDataFolderPath()
const getJanDataFolderPath = (): Promise<string> => globalThis.core.api?.getJanDataFolderPath()
/**
* Opens the file explorer at a specific path.
@ -72,16 +44,14 @@ const joinPath: (paths: string[]) => Promise<string> = (paths) =>
* @param path - The file path to retrieve dirname.
* @returns {Promise<string>} A promise that resolves the dirname.
*/
const dirName: (path: string) => Promise<string> = (path) =>
globalThis.core.api?.dirName(path)
const dirName: (path: string) => Promise<string> = (path) => globalThis.core.api?.dirName(path)
/**
* Retrieve the basename from an url.
* @param path - The path to retrieve.
* @returns {Promise<string>} A promise that resolves with the basename.
*/
const baseName: (paths: string) => Promise<string> = (path) =>
globalThis.core.api?.baseName(path)
const baseName: (paths: string) => Promise<string> = (path) => globalThis.core.api?.baseName(path)
/**
* Opens an external URL in the default web browser.
@ -97,15 +67,13 @@ const openExternalUrl: (url: string) => Promise<any> = (url) =>
*
* @returns {Promise<string>} - A promise that resolves with the resource path.
*/
const getResourcePath: () => Promise<string> = () =>
globalThis.core.api?.getResourcePath()
const getResourcePath: () => Promise<string> = () => globalThis.core.api?.getResourcePath()
/**
* Gets the user's home path.
* @returns return user's home path
*/
const getUserHomePath = (): Promise<string> =>
globalThis.core.api?.getUserHomePath()
const getUserHomePath = (): Promise<string> => globalThis.core.api?.getUserHomePath()
/**
* Log to file from browser processes.
@ -123,10 +91,8 @@ const log: (message: string, fileName?: string) => void = (message, fileName) =>
*
* @returns {Promise<boolean>} - A promise that resolves with a boolean indicating whether the path is a subdirectory.
*/
const isSubdirectory: (from: string, to: string) => Promise<boolean> = (
from: string,
to: string
) => globalThis.core.api?.isSubdirectory(from, to)
const isSubdirectory: (from: string, to: string) => Promise<boolean> = (from: string, to: string) =>
globalThis.core.api?.isSubdirectory(from, to)
/**
* Get system information
@ -159,8 +125,6 @@ export type RegisterExtensionPoint = (
*/
export {
executeOnMain,
downloadFile,
abortDownload,
getJanDataFolderPath,
openFileExplorer,
getResourcePath,

View File

@ -39,11 +39,6 @@ describe('BaseExtension', () => {
expect(baseExtension.onUnload).toBeDefined()
})
it('should have installationState() return "NotRequired"', async () => {
const installationState = await baseExtension.installationState()
expect(installationState).toBe('NotRequired')
})
it('should install the extension', async () => {
await baseExtension.install()
// Add your assertions here
@ -84,11 +79,6 @@ describe('BaseExtension', () => {
expect(baseExtension.onUnload).toBeDefined()
})
it('should have installationState() return "NotRequired"', async () => {
const installationState = await baseExtension.installationState()
expect(installationState).toBe('NotRequired')
})
it('should install the extension', async () => {
await baseExtension.install()
// Add your assertions here

View File

@ -12,6 +12,7 @@ export enum ExtensionTypeEnum {
SystemMonitoring = 'systemMonitoring',
HuggingFace = 'huggingFace',
Engine = 'engine',
Hardware = 'hardware',
}
export interface ExtensionType {
@ -23,17 +24,6 @@ export interface Compatibility {
version: string
}
const ALL_INSTALLATION_STATE = [
'NotRequired', // not required.
'Installed', // require and installed. Good to go.
'NotInstalled', // require to be installed.
'Corrupted', // require but corrupted. Need to redownload.
'NotCompatible', // require but not compatible.
] as const
export type InstallationStateTuple = typeof ALL_INSTALLATION_STATE
export type InstallationState = InstallationStateTuple[number]
/**
* Represents a base extension.
* This class should be extended by any class that represents an extension.
@ -174,15 +164,6 @@ export abstract class BaseExtension implements ExtensionType {
return
}
/**
* Determine if the prerequisites for the extension are installed.
*
* @returns {boolean} true if the prerequisites are installed, false otherwise.
*/
async installationState(): Promise<InstallationState> {
return 'NotRequired'
}
/**
* Install the prerequisites for the extension.
*
@ -227,7 +208,7 @@ export abstract class BaseExtension implements ExtensionType {
const settings = await this.getSettings()
const updatedSettings = settings.map((setting) => {
let updatedSettings = settings.map((setting) => {
const updatedSetting = componentProps.find(
(componentProp) => componentProp.key === setting.key
)
@ -237,13 +218,20 @@ export abstract class BaseExtension implements ExtensionType {
return setting
})
const settingPath = await joinPath([
if (!updatedSettings.length) updatedSettings = componentProps as SettingComponentProps[]
const settingFolder = await joinPath([
await getJanDataFolderPath(),
this.settingFolderName,
this.name,
this.settingFileName,
])
if (!(await fs.existsSync(settingFolder))) {
await fs.mkdir(settingFolder)
}
const settingPath = await joinPath([settingFolder, this.settingFileName])
await fs.writeFileSync(settingPath, JSON.stringify(updatedSettings, null, 2))
updatedSettings.forEach((setting) => {

View File

@ -0,0 +1,252 @@
import { ConversationalExtension } from './conversational'
import { ExtensionTypeEnum } from '../extension'
import { Thread, ThreadAssistantInfo, ThreadMessage } from '../../types'
// Mock implementation of ConversationalExtension
class MockConversationalExtension extends ConversationalExtension {
private threads: Thread[] = []
private messages: { [threadId: string]: ThreadMessage[] } = {}
private assistants: { [threadId: string]: ThreadAssistantInfo } = {}
constructor() {
super('http://mock-url.com', 'mock-extension', 'Mock Extension', true, 'A mock extension', '1.0.0')
}
onLoad(): void {
// Mock implementation
}
onUnload(): void {
// Mock implementation
}
async listThreads(): Promise<Thread[]> {
return this.threads
}
async createThread(thread: Partial<Thread>): Promise<Thread> {
const newThread: Thread = {
id: thread.id || `thread-${Date.now()}`,
name: thread.name || 'New Thread',
createdAt: thread.createdAt || new Date().toISOString(),
updatedAt: thread.updatedAt || new Date().toISOString(),
}
this.threads.push(newThread)
this.messages[newThread.id] = []
return newThread
}
async modifyThread(thread: Thread): Promise<void> {
const index = this.threads.findIndex(t => t.id === thread.id)
if (index !== -1) {
this.threads[index] = thread
}
}
async deleteThread(threadId: string): Promise<void> {
this.threads = this.threads.filter(t => t.id !== threadId)
delete this.messages[threadId]
delete this.assistants[threadId]
}
async createMessage(message: Partial<ThreadMessage>): Promise<ThreadMessage> {
if (!message.threadId) throw new Error('Thread ID is required')
const newMessage: ThreadMessage = {
id: message.id || `message-${Date.now()}`,
threadId: message.threadId,
content: message.content || '',
role: message.role || 'user',
createdAt: message.createdAt || new Date().toISOString(),
}
if (!this.messages[message.threadId]) {
this.messages[message.threadId] = []
}
this.messages[message.threadId].push(newMessage)
return newMessage
}
async deleteMessage(threadId: string, messageId: string): Promise<void> {
if (this.messages[threadId]) {
this.messages[threadId] = this.messages[threadId].filter(m => m.id !== messageId)
}
}
async listMessages(threadId: string): Promise<ThreadMessage[]> {
return this.messages[threadId] || []
}
async getThreadAssistant(threadId: string): Promise<ThreadAssistantInfo> {
return this.assistants[threadId] || { modelId: '', threadId }
}
async createThreadAssistant(
threadId: string,
assistant: ThreadAssistantInfo
): Promise<ThreadAssistantInfo> {
this.assistants[threadId] = assistant
return assistant
}
async modifyThreadAssistant(
threadId: string,
assistant: ThreadAssistantInfo
): Promise<ThreadAssistantInfo> {
this.assistants[threadId] = assistant
return assistant
}
async modifyMessage(message: ThreadMessage): Promise<ThreadMessage> {
if (!this.messages[message.threadId]) return message
const index = this.messages[message.threadId].findIndex(m => m.id === message.id)
if (index !== -1) {
this.messages[message.threadId][index] = message
}
return message
}
}
describe('ConversationalExtension', () => {
let extension: MockConversationalExtension
beforeEach(() => {
extension = new MockConversationalExtension()
})
test('should return the correct extension type', () => {
expect(extension.type()).toBe(ExtensionTypeEnum.Conversational)
})
test('should create and list threads', async () => {
const thread = await extension.createThread({ name: 'Test Thread' })
expect(thread.name).toBe('Test Thread')
const threads = await extension.listThreads()
expect(threads).toHaveLength(1)
expect(threads[0].id).toBe(thread.id)
})
test('should modify thread', async () => {
const thread = await extension.createThread({ name: 'Test Thread' })
const modifiedThread = { ...thread, name: 'Modified Thread' }
await extension.modifyThread(modifiedThread)
const threads = await extension.listThreads()
expect(threads[0].name).toBe('Modified Thread')
})
test('should delete thread', async () => {
const thread = await extension.createThread({ name: 'Test Thread' })
await extension.deleteThread(thread.id)
const threads = await extension.listThreads()
expect(threads).toHaveLength(0)
})
test('should create and list messages', async () => {
const thread = await extension.createThread({ name: 'Test Thread' })
const message = await extension.createMessage({
threadId: thread.id,
content: 'Test message',
role: 'user'
})
expect(message.content).toBe('Test message')
const messages = await extension.listMessages(thread.id)
expect(messages).toHaveLength(1)
expect(messages[0].id).toBe(message.id)
})
test('should modify message', async () => {
const thread = await extension.createThread({ name: 'Test Thread' })
const message = await extension.createMessage({
threadId: thread.id,
content: 'Test message',
role: 'user'
})
const modifiedMessage = { ...message, content: 'Modified message' }
await extension.modifyMessage(modifiedMessage)
const messages = await extension.listMessages(thread.id)
expect(messages[0].content).toBe('Modified message')
})
test('should delete message', async () => {
const thread = await extension.createThread({ name: 'Test Thread' })
const message = await extension.createMessage({
threadId: thread.id,
content: 'Test message',
role: 'user'
})
await extension.deleteMessage(thread.id, message.id)
const messages = await extension.listMessages(thread.id)
expect(messages).toHaveLength(0)
})
test('should create and get thread assistant', async () => {
const thread = await extension.createThread({ name: 'Test Thread' })
const assistant: ThreadAssistantInfo = {
threadId: thread.id,
modelId: 'test-model'
}
await extension.createThreadAssistant(thread.id, assistant)
const retrievedAssistant = await extension.getThreadAssistant(thread.id)
expect(retrievedAssistant.modelId).toBe('test-model')
})
test('should modify thread assistant', async () => {
const thread = await extension.createThread({ name: 'Test Thread' })
const assistant: ThreadAssistantInfo = {
threadId: thread.id,
modelId: 'test-model'
}
await extension.createThreadAssistant(thread.id, assistant)
const modifiedAssistant: ThreadAssistantInfo = {
threadId: thread.id,
modelId: 'modified-model'
}
await extension.modifyThreadAssistant(thread.id, modifiedAssistant)
const retrievedAssistant = await extension.getThreadAssistant(thread.id)
expect(retrievedAssistant.modelId).toBe('modified-model')
})
test('should delete thread assistant when thread is deleted', async () => {
const thread = await extension.createThread({ name: 'Test Thread' })
const assistant: ThreadAssistantInfo = {
threadId: thread.id,
modelId: 'test-model'
}
await extension.createThreadAssistant(thread.id, assistant)
await extension.deleteThread(thread.id)
// Creating a new thread with the same ID to test if assistant was deleted
const newThread = await extension.createThread({ id: thread.id, name: 'New Thread' })
const retrievedAssistant = await extension.getThreadAssistant(newThread.id)
expect(retrievedAssistant.modelId).toBe('')
})
})

View File

@ -3,6 +3,7 @@
*/
import { EngineManager } from './EngineManager'
import { AIEngine } from './AIEngine'
import { InferenceEngine } from '../../../types'
// @ts-ignore
class MockAIEngine implements AIEngine {
@ -40,4 +41,69 @@ describe('EngineManager', () => {
const retrievedEngine = engineManager.get<MockAIEngine>('nonExistentProvider')
expect(retrievedEngine).toBeUndefined()
})
describe('cortex engine migration', () => {
test('should map nitro to cortex engine', () => {
const cortexEngine = new MockAIEngine(InferenceEngine.cortex)
// @ts-ignore
engineManager.register(cortexEngine)
// @ts-ignore
const retrievedEngine = engineManager.get<MockAIEngine>(InferenceEngine.nitro)
expect(retrievedEngine).toBe(cortexEngine)
})
test('should map cortex_llamacpp to cortex engine', () => {
const cortexEngine = new MockAIEngine(InferenceEngine.cortex)
// @ts-ignore
engineManager.register(cortexEngine)
// @ts-ignore
const retrievedEngine = engineManager.get<MockAIEngine>(InferenceEngine.cortex_llamacpp)
expect(retrievedEngine).toBe(cortexEngine)
})
test('should map cortex_onnx to cortex engine', () => {
const cortexEngine = new MockAIEngine(InferenceEngine.cortex)
// @ts-ignore
engineManager.register(cortexEngine)
// @ts-ignore
const retrievedEngine = engineManager.get<MockAIEngine>(InferenceEngine.cortex_onnx)
expect(retrievedEngine).toBe(cortexEngine)
})
test('should map cortex_tensorrtllm to cortex engine', () => {
const cortexEngine = new MockAIEngine(InferenceEngine.cortex)
// @ts-ignore
engineManager.register(cortexEngine)
// @ts-ignore
const retrievedEngine = engineManager.get<MockAIEngine>(InferenceEngine.cortex_tensorrtllm)
expect(retrievedEngine).toBe(cortexEngine)
})
})
describe('singleton instance', () => {
test('should return the window.core.engineManager if available', () => {
const mockEngineManager = new EngineManager()
// @ts-ignore
window.core = { engineManager: mockEngineManager }
const instance = EngineManager.instance()
expect(instance).toBe(mockEngineManager)
// Clean up
// @ts-ignore
delete window.core
})
test('should create a new instance if window.core.engineManager is not available', () => {
// @ts-ignore
delete window.core
const instance = EngineManager.instance()
expect(instance).toBeInstanceOf(EngineManager)
})
})
})

View File

@ -38,8 +38,14 @@ describe('OAIEngine', () => {
it('should subscribe to events on load', () => {
engine.onLoad()
expect(events.on).toHaveBeenCalledWith(MessageEvent.OnMessageSent, expect.any(Function))
expect(events.on).toHaveBeenCalledWith(InferenceEvent.OnInferenceStopped, expect.any(Function))
expect(events.on).toHaveBeenCalledWith(
MessageEvent.OnMessageSent,
expect.any(Function)
)
expect(events.on).toHaveBeenCalledWith(
InferenceEvent.OnInferenceStopped,
expect.any(Function)
)
})
it('should handle inference request', async () => {
@ -77,7 +83,12 @@ describe('OAIEngine', () => {
expect(events.emit).toHaveBeenCalledWith(
MessageEvent.OnMessageUpdate,
expect.objectContaining({
content: [{ type: ContentType.Text, text: { value: 'test response', annotations: [] } }],
content: [
{
type: ContentType.Text,
text: { value: 'test response', annotations: [] },
},
],
status: MessageStatus.Ready,
})
)
@ -101,11 +112,10 @@ describe('OAIEngine', () => {
await engine.inference(data)
expect(events.emit).toHaveBeenCalledWith(
expect(events.emit).toHaveBeenLastCalledWith(
MessageEvent.OnMessageUpdate,
expect.objectContaining({
content: [{ type: ContentType.Text, text: { value: 'test error', annotations: [] } }],
status: MessageStatus.Error,
status: 'error',
error_code: 500,
})
)

View File

@ -42,7 +42,9 @@ export abstract class OAIEngine extends AIEngine {
*/
override onLoad() {
super.onLoad()
events.on(MessageEvent.OnMessageSent, (data: MessageRequest) => this.inference(data))
events.on(MessageEvent.OnMessageSent, (data: MessageRequest) =>
this.inference(data)
)
events.on(InferenceEvent.OnInferenceStopped, () => this.stopInference())
}
@ -128,7 +130,9 @@ export abstract class OAIEngine extends AIEngine {
events.emit(MessageEvent.OnMessageUpdate, message)
},
complete: async () => {
message.status = message.content.length ? MessageStatus.Ready : MessageStatus.Error
message.status = message.content.length
? MessageStatus.Ready
: MessageStatus.Error
events.emit(MessageEvent.OnMessageUpdate, message)
},
error: async (err: any) => {
@ -141,7 +145,10 @@ export abstract class OAIEngine extends AIEngine {
message.content[0] = {
type: ContentType.Text,
text: {
value: err.message,
value:
typeof message === 'string'
? err.message
: (JSON.stringify(err.message) ?? err.detail),
annotations: [],
},
}

View File

@ -1,14 +1,17 @@
import { lastValueFrom, Observable } from 'rxjs'
import { requestInference } from './sse'
import { ReadableStream } from 'stream/web';
import { ReadableStream } from 'stream/web'
describe('requestInference', () => {
it('should send a request to the inference server and return an Observable', () => {
// Mock the fetch function
const mockFetch: any = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ choices: [{ message: { content: 'Generated response' } }] }),
json: () =>
Promise.resolve({
choices: [{ message: { content: 'Generated response' } }],
}),
headers: new Headers(),
redirected: false,
status: 200,
@ -36,7 +39,10 @@ describe('requestInference', () => {
const mockFetch: any = jest.fn(() =>
Promise.resolve({
ok: false,
json: () => Promise.resolve({ error: { message: 'Wrong API Key', code: 'invalid_api_key' } }),
json: () =>
Promise.resolve({
error: { message: 'Invalid API Key.', code: 'invalid_api_key' },
}),
headers: new Headers(),
redirected: false,
status: 401,
@ -56,69 +62,85 @@ describe('requestInference', () => {
// Assert the expected behavior
expect(result).toBeInstanceOf(Observable)
expect(lastValueFrom(result)).rejects.toEqual({ message: 'Wrong API Key', code: 'invalid_api_key' })
expect(lastValueFrom(result)).rejects.toEqual({
message: 'Invalid API Key.',
code: 'invalid_api_key',
})
})
})
it('should handle a successful response with a transformResponse function', () => {
// Mock the fetch function
const mockFetch: any = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ choices: [{ message: { content: 'Generated response' } }] }),
headers: new Headers(),
redirected: false,
status: 200,
statusText: 'OK',
})
)
jest.spyOn(global, 'fetch').mockImplementation(mockFetch)
// Define the test inputs
const inferenceUrl = 'https://inference-server.com'
const requestBody = { message: 'Hello' }
const model = { id: 'model-id', parameters: { stream: false } }
const transformResponse = (data: any) => data.choices[0].message.content.toUpperCase()
// Call the function
const result = requestInference(inferenceUrl, requestBody, model, undefined, undefined, transformResponse)
// Assert the expected behavior
expect(result).toBeInstanceOf(Observable)
expect(lastValueFrom(result)).resolves.toEqual('GENERATED RESPONSE')
})
it('should handle a successful response with streaming enabled', () => {
// Mock the fetch function
const mockFetch: any = jest.fn(() =>
Promise.resolve({
ok: true,
body: new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode('data: {"choices": [{"delta": {"content": "Streamed"}}]}'));
controller.enqueue(new TextEncoder().encode('data: [DONE]'));
controller.close();
}
it('should handle a successful response with a transformResponse function', () => {
// Mock the fetch function
const mockFetch: any = jest.fn(() =>
Promise.resolve({
ok: true,
json: () =>
Promise.resolve({
choices: [{ message: { content: 'Generated response' } }],
}),
headers: new Headers(),
redirected: false,
status: 200,
statusText: 'OK',
})
);
jest.spyOn(global, 'fetch').mockImplementation(mockFetch);
// Define the test inputs
const inferenceUrl = 'https://inference-server.com';
const requestBody = { message: 'Hello' };
const model = { id: 'model-id', parameters: { stream: true } };
// Call the function
const result = requestInference(inferenceUrl, requestBody, model);
// Assert the expected behavior
expect(result).toBeInstanceOf(Observable);
expect(lastValueFrom(result)).resolves.toEqual('Streamed');
});
headers: new Headers(),
redirected: false,
status: 200,
statusText: 'OK',
})
)
jest.spyOn(global, 'fetch').mockImplementation(mockFetch)
// Define the test inputs
const inferenceUrl = 'https://inference-server.com'
const requestBody = { message: 'Hello' }
const model = { id: 'model-id', parameters: { stream: false } }
const transformResponse = (data: any) =>
data.choices[0].message.content.toUpperCase()
// Call the function
const result = requestInference(
inferenceUrl,
requestBody,
model,
undefined,
undefined,
transformResponse
)
// Assert the expected behavior
expect(result).toBeInstanceOf(Observable)
expect(lastValueFrom(result)).resolves.toEqual('GENERATED RESPONSE')
})
it('should handle a successful response with streaming enabled', () => {
// Mock the fetch function
const mockFetch: any = jest.fn(() =>
Promise.resolve({
ok: true,
body: new ReadableStream({
start(controller) {
controller.enqueue(
new TextEncoder().encode(
'data: {"choices": [{"delta": {"content": "Streamed"}}]}'
)
)
controller.enqueue(new TextEncoder().encode('data: [DONE]'))
controller.close()
},
}),
headers: new Headers(),
redirected: false,
status: 200,
statusText: 'OK',
})
)
jest.spyOn(global, 'fetch').mockImplementation(mockFetch)
// Define the test inputs
const inferenceUrl = 'https://inference-server.com'
const requestBody = { message: 'Hello' }
const model = { id: 'model-id', parameters: { stream: true } }
// Call the function
const result = requestInference(inferenceUrl, requestBody, model)
// Assert the expected behavior
expect(result).toBeInstanceOf(Observable)
expect(lastValueFrom(result)).resolves.toEqual('Streamed')
})

View File

@ -32,20 +32,19 @@ export function requestInference(
})
.then(async (response) => {
if (!response.ok) {
const data = await response.json()
let errorCode = ErrorCode.Unknown
if (data.error) {
errorCode = data.error.code ?? data.error.type ?? ErrorCode.Unknown
} else if (response.status === 401) {
errorCode = ErrorCode.InvalidApiKey
if (response.status === 401) {
throw {
code: ErrorCode.InvalidApiKey,
message: 'Invalid API Key.',
}
}
const error = {
message: data.error?.message ?? data.message ?? 'Error occurred.',
code: errorCode,
let data = await response.json()
try {
handleError(data)
} catch (err) {
subscriber.error(err)
return
}
subscriber.error(error)
subscriber.complete()
return
}
// There could be overriden stream parameter in the model
// that is set in request body (transformed payload)
@ -54,9 +53,10 @@ export function requestInference(
model.parameters?.stream === false
) {
const data = await response.json()
if (data.error || data.message) {
subscriber.error(data.error ?? data)
subscriber.complete()
try {
handleError(data)
} catch (err) {
subscriber.error(err)
return
}
if (transformResponse) {
@ -91,13 +91,10 @@ export function requestInference(
const toParse = cachedLines + line
if (!line.includes('data: [DONE]')) {
const data = JSON.parse(toParse.replace('data: ', ''))
if (
'error' in data ||
'message' in data ||
'detail' in data
) {
subscriber.error(data.error ?? data)
subscriber.complete()
try {
handleError(data)
} catch (err) {
subscriber.error(err)
return
}
content += data.choices[0]?.delta?.content ?? ''
@ -118,3 +115,18 @@ export function requestInference(
.catch((err) => subscriber.error(err))
})
}
/**
* Handle error and normalize it to a common format.
* @param data
*/
const handleError = (data: any) => {
if (
data.error ||
data.message ||
data.detail ||
(Array.isArray(data) && data.length && data[0].error)
) {
throw data.error ?? data[0]?.error ?? data
}
}

View File

@ -0,0 +1,566 @@
import { EngineManagementExtension } from './enginesManagement'
import { ExtensionTypeEnum } from '../extension'
import {
EngineConfig,
EngineReleased,
EngineVariant,
Engines,
InferenceEngine,
DefaultEngineVariant,
Model
} from '../../types'
// Mock implementation of EngineManagementExtension
class MockEngineManagementExtension extends EngineManagementExtension {
private mockEngines: Engines = {
llama: {
name: 'llama',
variants: [
{
variant: 'cpu',
version: '1.0.0',
path: '/engines/llama/cpu/1.0.0',
installed: true
},
{
variant: 'cuda',
version: '1.0.0',
path: '/engines/llama/cuda/1.0.0',
installed: false
}
],
default: {
variant: 'cpu',
version: '1.0.0'
}
},
gpt4all: {
name: 'gpt4all',
variants: [
{
variant: 'cpu',
version: '2.0.0',
path: '/engines/gpt4all/cpu/2.0.0',
installed: true
}
],
default: {
variant: 'cpu',
version: '2.0.0'
}
}
}
private mockReleases: { [key: string]: EngineReleased[] } = {
'llama-1.0.0': [
{
variant: 'cpu',
version: '1.0.0',
os: ['macos', 'linux', 'windows'],
url: 'https://example.com/llama/1.0.0/cpu'
},
{
variant: 'cuda',
version: '1.0.0',
os: ['linux', 'windows'],
url: 'https://example.com/llama/1.0.0/cuda'
}
],
'llama-1.1.0': [
{
variant: 'cpu',
version: '1.1.0',
os: ['macos', 'linux', 'windows'],
url: 'https://example.com/llama/1.1.0/cpu'
},
{
variant: 'cuda',
version: '1.1.0',
os: ['linux', 'windows'],
url: 'https://example.com/llama/1.1.0/cuda'
}
],
'gpt4all-2.0.0': [
{
variant: 'cpu',
version: '2.0.0',
os: ['macos', 'linux', 'windows'],
url: 'https://example.com/gpt4all/2.0.0/cpu'
}
]
}
private remoteModels: { [engine: string]: Model[] } = {
'llama': [],
'gpt4all': []
}
constructor() {
super('http://mock-url.com', 'mock-engine-extension', 'Mock Engine Extension', true, 'A mock engine extension', '1.0.0')
}
onLoad(): void {
// Mock implementation
}
onUnload(): void {
// Mock implementation
}
async getEngines(): Promise<Engines> {
return JSON.parse(JSON.stringify(this.mockEngines))
}
async getInstalledEngines(name: InferenceEngine): Promise<EngineVariant[]> {
if (!this.mockEngines[name]) {
return []
}
return this.mockEngines[name].variants.filter(variant => variant.installed)
}
async getReleasedEnginesByVersion(
name: InferenceEngine,
version: string,
platform?: string
): Promise<EngineReleased[]> {
const key = `${name}-${version}`
let releases = this.mockReleases[key] || []
if (platform) {
releases = releases.filter(release => release.os.includes(platform))
}
return releases
}
async getLatestReleasedEngine(
name: InferenceEngine,
platform?: string
): Promise<EngineReleased[]> {
// For mock, let's assume latest versions are 1.1.0 for llama and 2.0.0 for gpt4all
const latestVersions = {
'llama': '1.1.0',
'gpt4all': '2.0.0'
}
if (!latestVersions[name]) {
return []
}
return this.getReleasedEnginesByVersion(name, latestVersions[name], platform)
}
async installEngine(
name: string,
engineConfig: EngineConfig
): Promise<{ messages: string }> {
if (!this.mockEngines[name]) {
this.mockEngines[name] = {
name,
variants: [],
default: {
variant: engineConfig.variant,
version: engineConfig.version
}
}
}
// Check if variant already exists
const existingVariantIndex = this.mockEngines[name].variants.findIndex(
v => v.variant === engineConfig.variant && v.version === engineConfig.version
)
if (existingVariantIndex >= 0) {
this.mockEngines[name].variants[existingVariantIndex].installed = true
} else {
this.mockEngines[name].variants.push({
variant: engineConfig.variant,
version: engineConfig.version,
path: `/engines/${name}/${engineConfig.variant}/${engineConfig.version}`,
installed: true
})
}
return { messages: `Successfully installed ${name} ${engineConfig.variant} ${engineConfig.version}` }
}
async addRemoteEngine(
engineConfig: EngineConfig
): Promise<{ messages: string }> {
const name = engineConfig.name || 'remote-engine'
if (!this.mockEngines[name]) {
this.mockEngines[name] = {
name,
variants: [],
default: {
variant: engineConfig.variant,
version: engineConfig.version
}
}
}
this.mockEngines[name].variants.push({
variant: engineConfig.variant,
version: engineConfig.version,
path: engineConfig.path || `/engines/${name}/${engineConfig.variant}/${engineConfig.version}`,
installed: true,
url: engineConfig.url
})
return { messages: `Successfully added remote engine ${name}` }
}
async uninstallEngine(
name: InferenceEngine,
engineConfig: EngineConfig
): Promise<{ messages: string }> {
if (!this.mockEngines[name]) {
return { messages: `Engine ${name} not found` }
}
const variantIndex = this.mockEngines[name].variants.findIndex(
v => v.variant === engineConfig.variant && v.version === engineConfig.version
)
if (variantIndex >= 0) {
this.mockEngines[name].variants[variantIndex].installed = false
// If this was the default variant, reset default
if (
this.mockEngines[name].default.variant === engineConfig.variant &&
this.mockEngines[name].default.version === engineConfig.version
) {
// Find another installed variant to set as default
const installedVariant = this.mockEngines[name].variants.find(v => v.installed)
if (installedVariant) {
this.mockEngines[name].default = {
variant: installedVariant.variant,
version: installedVariant.version
}
} else {
// No installed variants remain, clear default
this.mockEngines[name].default = { variant: '', version: '' }
}
}
return { messages: `Successfully uninstalled ${name} ${engineConfig.variant} ${engineConfig.version}` }
} else {
return { messages: `Variant ${engineConfig.variant} ${engineConfig.version} not found for engine ${name}` }
}
}
async getDefaultEngineVariant(
name: InferenceEngine
): Promise<DefaultEngineVariant> {
if (!this.mockEngines[name]) {
return { variant: '', version: '' }
}
return this.mockEngines[name].default
}
async setDefaultEngineVariant(
name: InferenceEngine,
engineConfig: EngineConfig
): Promise<{ messages: string }> {
if (!this.mockEngines[name]) {
return { messages: `Engine ${name} not found` }
}
const variantExists = this.mockEngines[name].variants.some(
v => v.variant === engineConfig.variant && v.version === engineConfig.version && v.installed
)
if (!variantExists) {
return { messages: `Variant ${engineConfig.variant} ${engineConfig.version} not found or not installed` }
}
this.mockEngines[name].default = {
variant: engineConfig.variant,
version: engineConfig.version
}
return { messages: `Successfully set ${engineConfig.variant} ${engineConfig.version} as default for ${name}` }
}
async updateEngine(
name: InferenceEngine,
engineConfig?: EngineConfig
): Promise<{ messages: string }> {
if (!this.mockEngines[name]) {
return { messages: `Engine ${name} not found` }
}
if (!engineConfig) {
// Assume we're updating to the latest version
return { messages: `Successfully updated ${name} to the latest version` }
}
const variantIndex = this.mockEngines[name].variants.findIndex(
v => v.variant === engineConfig.variant && v.installed
)
if (variantIndex >= 0) {
// Update the version
this.mockEngines[name].variants[variantIndex].version = engineConfig.version
// If this was the default variant, update default version too
if (this.mockEngines[name].default.variant === engineConfig.variant) {
this.mockEngines[name].default.version = engineConfig.version
}
return { messages: `Successfully updated ${name} ${engineConfig.variant} to version ${engineConfig.version}` }
} else {
return { messages: `Installed variant ${engineConfig.variant} not found for engine ${name}` }
}
}
async addRemoteModel(model: Model): Promise<void> {
const engine = model.engine as string
if (!this.remoteModels[engine]) {
this.remoteModels[engine] = []
}
this.remoteModels[engine].push(model)
}
async getRemoteModels(name: InferenceEngine | string): Promise<Model[]> {
return this.remoteModels[name] || []
}
}
describe('EngineManagementExtension', () => {
let extension: MockEngineManagementExtension
beforeEach(() => {
extension = new MockEngineManagementExtension()
})
test('should return the correct extension type', () => {
expect(extension.type()).toBe(ExtensionTypeEnum.Engine)
})
test('should get all engines', async () => {
const engines = await extension.getEngines()
expect(engines).toBeDefined()
expect(engines.llama).toBeDefined()
expect(engines.gpt4all).toBeDefined()
expect(engines.llama.variants).toHaveLength(2)
expect(engines.gpt4all.variants).toHaveLength(1)
})
test('should get installed engines', async () => {
const llamaEngines = await extension.getInstalledEngines('llama')
expect(llamaEngines).toHaveLength(1)
expect(llamaEngines[0].variant).toBe('cpu')
expect(llamaEngines[0].installed).toBe(true)
const gpt4allEngines = await extension.getInstalledEngines('gpt4all')
expect(gpt4allEngines).toHaveLength(1)
expect(gpt4allEngines[0].variant).toBe('cpu')
expect(gpt4allEngines[0].installed).toBe(true)
// Test non-existent engine
const nonExistentEngines = await extension.getInstalledEngines('non-existent' as InferenceEngine)
expect(nonExistentEngines).toHaveLength(0)
})
test('should get released engines by version', async () => {
const llamaReleases = await extension.getReleasedEnginesByVersion('llama', '1.0.0')
expect(llamaReleases).toHaveLength(2)
expect(llamaReleases[0].variant).toBe('cpu')
expect(llamaReleases[1].variant).toBe('cuda')
// Test with platform filter
const llamaLinuxReleases = await extension.getReleasedEnginesByVersion('llama', '1.0.0', 'linux')
expect(llamaLinuxReleases).toHaveLength(2)
const llamaMacReleases = await extension.getReleasedEnginesByVersion('llama', '1.0.0', 'macos')
expect(llamaMacReleases).toHaveLength(1)
expect(llamaMacReleases[0].variant).toBe('cpu')
// Test non-existent version
const nonExistentReleases = await extension.getReleasedEnginesByVersion('llama', '9.9.9')
expect(nonExistentReleases).toHaveLength(0)
})
test('should get latest released engines', async () => {
const latestLlamaReleases = await extension.getLatestReleasedEngine('llama')
expect(latestLlamaReleases).toHaveLength(2)
expect(latestLlamaReleases[0].version).toBe('1.1.0')
// Test with platform filter
const latestLlamaMacReleases = await extension.getLatestReleasedEngine('llama', 'macos')
expect(latestLlamaMacReleases).toHaveLength(1)
expect(latestLlamaMacReleases[0].variant).toBe('cpu')
expect(latestLlamaMacReleases[0].version).toBe('1.1.0')
// Test non-existent engine
const nonExistentReleases = await extension.getLatestReleasedEngine('non-existent' as InferenceEngine)
expect(nonExistentReleases).toHaveLength(0)
})
test('should install engine', async () => {
// Install existing engine variant that is not installed
const result = await extension.installEngine('llama', { variant: 'cuda', version: '1.0.0' })
expect(result.messages).toContain('Successfully installed')
const installedEngines = await extension.getInstalledEngines('llama')
expect(installedEngines).toHaveLength(2)
expect(installedEngines.some(e => e.variant === 'cuda')).toBe(true)
// Install non-existent engine
const newEngineResult = await extension.installEngine('new-engine', { variant: 'cpu', version: '1.0.0' })
expect(newEngineResult.messages).toContain('Successfully installed')
const engines = await extension.getEngines()
expect(engines['new-engine']).toBeDefined()
expect(engines['new-engine'].variants).toHaveLength(1)
expect(engines['new-engine'].variants[0].installed).toBe(true)
})
test('should add remote engine', async () => {
const result = await extension.addRemoteEngine({
name: 'remote-llm',
variant: 'remote',
version: '1.0.0',
url: 'https://example.com/remote-llm-api'
})
expect(result.messages).toContain('Successfully added remote engine')
const engines = await extension.getEngines()
expect(engines['remote-llm']).toBeDefined()
expect(engines['remote-llm'].variants).toHaveLength(1)
expect(engines['remote-llm'].variants[0].url).toBe('https://example.com/remote-llm-api')
})
test('should uninstall engine', async () => {
const result = await extension.uninstallEngine('llama', { variant: 'cpu', version: '1.0.0' })
expect(result.messages).toContain('Successfully uninstalled')
const installedEngines = await extension.getInstalledEngines('llama')
expect(installedEngines).toHaveLength(0)
// Test uninstalling non-existent variant
const nonExistentResult = await extension.uninstallEngine('llama', { variant: 'non-existent', version: '1.0.0' })
expect(nonExistentResult.messages).toContain('not found')
})
test('should handle default variant when uninstalling', async () => {
// First install cuda variant
await extension.installEngine('llama', { variant: 'cuda', version: '1.0.0' })
// Set cuda as default
await extension.setDefaultEngineVariant('llama', { variant: 'cuda', version: '1.0.0' })
// Check that cuda is now default
let defaultVariant = await extension.getDefaultEngineVariant('llama')
expect(defaultVariant.variant).toBe('cuda')
// Uninstall cuda
await extension.uninstallEngine('llama', { variant: 'cuda', version: '1.0.0' })
// Check that default has changed to another installed variant
defaultVariant = await extension.getDefaultEngineVariant('llama')
expect(defaultVariant.variant).toBe('cpu')
// Uninstall all variants
await extension.uninstallEngine('llama', { variant: 'cpu', version: '1.0.0' })
// Check that default is now empty
defaultVariant = await extension.getDefaultEngineVariant('llama')
expect(defaultVariant.variant).toBe('')
expect(defaultVariant.version).toBe('')
})
test('should get default engine variant', async () => {
const llamaDefault = await extension.getDefaultEngineVariant('llama')
expect(llamaDefault.variant).toBe('cpu')
expect(llamaDefault.version).toBe('1.0.0')
// Test non-existent engine
const nonExistentDefault = await extension.getDefaultEngineVariant('non-existent' as InferenceEngine)
expect(nonExistentDefault.variant).toBe('')
expect(nonExistentDefault.version).toBe('')
})
test('should set default engine variant', async () => {
// Install cuda variant
await extension.installEngine('llama', { variant: 'cuda', version: '1.0.0' })
const result = await extension.setDefaultEngineVariant('llama', { variant: 'cuda', version: '1.0.0' })
expect(result.messages).toContain('Successfully set')
const defaultVariant = await extension.getDefaultEngineVariant('llama')
expect(defaultVariant.variant).toBe('cuda')
expect(defaultVariant.version).toBe('1.0.0')
// Test setting non-existent variant as default
const nonExistentResult = await extension.setDefaultEngineVariant('llama', { variant: 'non-existent', version: '1.0.0' })
expect(nonExistentResult.messages).toContain('not found')
})
test('should update engine', async () => {
const result = await extension.updateEngine('llama', { variant: 'cpu', version: '1.1.0' })
expect(result.messages).toContain('Successfully updated')
const engines = await extension.getEngines()
const cpuVariant = engines.llama.variants.find(v => v.variant === 'cpu')
expect(cpuVariant).toBeDefined()
expect(cpuVariant?.version).toBe('1.1.0')
// Default should also be updated since cpu was default
expect(engines.llama.default.version).toBe('1.1.0')
// Test updating non-existent variant
const nonExistentResult = await extension.updateEngine('llama', { variant: 'non-existent', version: '1.1.0' })
expect(nonExistentResult.messages).toContain('not found')
})
test('should add and get remote models', async () => {
const model: Model = {
id: 'remote-model-1',
name: 'Remote Model 1',
path: '/path/to/remote-model',
engine: 'llama',
format: 'gguf',
modelFormat: 'gguf',
source: 'remote',
status: 'ready',
contextLength: 4096,
sizeInGB: 4,
created: new Date().toISOString()
}
await extension.addRemoteModel(model)
const llamaModels = await extension.getRemoteModels('llama')
expect(llamaModels).toHaveLength(1)
expect(llamaModels[0].id).toBe('remote-model-1')
// Test non-existent engine
const nonExistentModels = await extension.getRemoteModels('non-existent')
expect(nonExistentModels).toHaveLength(0)
})
})

View File

@ -0,0 +1,146 @@
import { HardwareManagementExtension } from './hardwareManagement'
import { ExtensionTypeEnum } from '../extension'
import { HardwareInformation } from '../../types'
// Mock implementation of HardwareManagementExtension
class MockHardwareManagementExtension extends HardwareManagementExtension {
private activeGpus: number[] = [0]
private mockHardwareInfo: HardwareInformation = {
cpu: {
manufacturer: 'Mock CPU Manufacturer',
brand: 'Mock CPU',
cores: 8,
physicalCores: 4,
speed: 3.5,
},
memory: {
total: 16 * 1024 * 1024 * 1024, // 16GB in bytes
free: 8 * 1024 * 1024 * 1024, // 8GB in bytes
},
gpus: [
{
id: 0,
vendor: 'Mock GPU Vendor',
model: 'Mock GPU Model 1',
memory: 8 * 1024 * 1024 * 1024, // 8GB in bytes
},
{
id: 1,
vendor: 'Mock GPU Vendor',
model: 'Mock GPU Model 2',
memory: 4 * 1024 * 1024 * 1024, // 4GB in bytes
}
],
active_gpus: [0],
}
constructor() {
super('http://mock-url.com', 'mock-hardware-extension', 'Mock Hardware Extension', true, 'A mock hardware extension', '1.0.0')
}
onLoad(): void {
// Mock implementation
}
onUnload(): void {
// Mock implementation
}
async getHardware(): Promise<HardwareInformation> {
// Return a copy to prevent test side effects
return JSON.parse(JSON.stringify(this.mockHardwareInfo))
}
async setAvtiveGpu(data: { gpus: number[] }): Promise<{
message: string
activated_gpus: number[]
}> {
// Validate GPUs exist
const validGpus = data.gpus.filter(gpuId =>
this.mockHardwareInfo.gpus.some(gpu => gpu.id === gpuId)
)
if (validGpus.length === 0) {
throw new Error('No valid GPUs selected')
}
// Update active GPUs
this.activeGpus = validGpus
this.mockHardwareInfo.active_gpus = validGpus
return {
message: 'GPU activation successful',
activated_gpus: validGpus
}
}
}
describe('HardwareManagementExtension', () => {
let extension: MockHardwareManagementExtension
beforeEach(() => {
extension = new MockHardwareManagementExtension()
})
test('should return the correct extension type', () => {
expect(extension.type()).toBe(ExtensionTypeEnum.Hardware)
})
test('should get hardware information', async () => {
const hardwareInfo = await extension.getHardware()
// Check CPU info
expect(hardwareInfo.cpu).toBeDefined()
expect(hardwareInfo.cpu.manufacturer).toBe('Mock CPU Manufacturer')
expect(hardwareInfo.cpu.cores).toBe(8)
// Check memory info
expect(hardwareInfo.memory).toBeDefined()
expect(hardwareInfo.memory.total).toBe(16 * 1024 * 1024 * 1024)
// Check GPU info
expect(hardwareInfo.gpus).toHaveLength(2)
expect(hardwareInfo.gpus[0].model).toBe('Mock GPU Model 1')
expect(hardwareInfo.gpus[1].model).toBe('Mock GPU Model 2')
// Check active GPUs
expect(hardwareInfo.active_gpus).toEqual([0])
})
test('should set active GPUs', async () => {
const result = await extension.setAvtiveGpu({ gpus: [1] })
expect(result.message).toBe('GPU activation successful')
expect(result.activated_gpus).toEqual([1])
// Verify the change in hardware info
const hardwareInfo = await extension.getHardware()
expect(hardwareInfo.active_gpus).toEqual([1])
})
test('should set multiple active GPUs', async () => {
const result = await extension.setAvtiveGpu({ gpus: [0, 1] })
expect(result.message).toBe('GPU activation successful')
expect(result.activated_gpus).toEqual([0, 1])
// Verify the change in hardware info
const hardwareInfo = await extension.getHardware()
expect(hardwareInfo.active_gpus).toEqual([0, 1])
})
test('should throw error for invalid GPU ids', async () => {
await expect(extension.setAvtiveGpu({ gpus: [999] })).rejects.toThrow('No valid GPUs selected')
})
test('should handle mix of valid and invalid GPU ids', async () => {
const result = await extension.setAvtiveGpu({ gpus: [0, 999] })
// Should only activate valid GPUs
expect(result.activated_gpus).toEqual([0])
// Verify the change in hardware info
const hardwareInfo = await extension.getHardware()
expect(hardwareInfo.active_gpus).toEqual([0])
})
})

View File

@ -0,0 +1,26 @@
import { HardwareInformation } from '../../types'
import { BaseExtension, ExtensionTypeEnum } from '../extension'
/**
* Engine management extension. Persists and retrieves engine management.
* @abstract
* @extends BaseExtension
*/
export abstract class HardwareManagementExtension extends BaseExtension {
type(): ExtensionTypeEnum | undefined {
return ExtensionTypeEnum.Hardware
}
/**
* @returns A Promise that resolves to an object of list hardware.
*/
abstract getHardware(): Promise<HardwareInformation>
/**
* @returns A Promise that resolves to an object of set active gpus.
*/
abstract setAvtiveGpu(data: { gpus: number[] }): Promise<{
message: string
activated_gpus: number[]
}>
}

View File

@ -1,6 +1,5 @@
import { ConversationalExtension } from './index';
import { InferenceExtension } from './index';
import { MonitoringExtension } from './index';
import { AssistantExtension } from './index';
import { ModelExtension } from './index';
import * as Engines from './index';
@ -14,10 +13,6 @@ describe('index.ts exports', () => {
expect(InferenceExtension).toBeDefined();
});
test('should export MonitoringExtension', () => {
expect(MonitoringExtension).toBeDefined();
});
test('should export AssistantExtension', () => {
expect(AssistantExtension).toBeDefined();
});
@ -29,4 +24,4 @@ describe('index.ts exports', () => {
test('should export Engines', () => {
expect(Engines).toBeDefined();
});
});
});

View File

@ -9,10 +9,7 @@ export { ConversationalExtension } from './conversational'
*/
export { InferenceExtension } from './inference'
/**
* Monitoring extension for system monitoring.
*/
export { MonitoringExtension } from './monitoring'
/**
* Assistant extension for managing assistants.
@ -33,3 +30,8 @@ export * from './engines'
* Engines Management
*/
export * from './enginesManagement'
/**
* Hardware Management
*/
export * from './hardwareManagement'

View File

@ -0,0 +1,286 @@
import { ModelExtension } from './model'
import { ExtensionTypeEnum } from '../extension'
import { Model, OptionType, ModelSource } from '../../types'
// Mock implementation of ModelExtension
class MockModelExtension extends ModelExtension {
private models: Model[] = []
private sources: ModelSource[] = []
private loadedModels: Set<string> = new Set()
private modelsPulling: Set<string> = new Set()
constructor() {
super('http://mock-url.com', 'mock-model-extension', 'Mock Model Extension', true, 'A mock model extension', '1.0.0')
}
onLoad(): void {
// Mock implementation
}
onUnload(): void {
// Mock implementation
}
async configurePullOptions(configs: { [key: string]: any }): Promise<any> {
return configs
}
async getModels(): Promise<Model[]> {
return this.models
}
async pullModel(model: string, id?: string, name?: string): Promise<void> {
const modelId = id || `model-${Date.now()}`
this.modelsPulling.add(modelId)
// Simulate model pull by adding it to the model list
const newModel: Model = {
id: modelId,
path: `/models/${model}`,
name: name || model,
source: 'mock-source',
modelFormat: 'mock-format',
engine: 'mock-engine',
format: 'mock-format',
status: 'ready',
contextLength: 2048,
sizeInGB: 2,
created: new Date().toISOString(),
pullProgress: {
percent: 100,
transferred: 0,
total: 0
}
}
this.models.push(newModel)
this.loadedModels.add(modelId)
this.modelsPulling.delete(modelId)
}
async cancelModelPull(modelId: string): Promise<void> {
this.modelsPulling.delete(modelId)
// Remove the model if it's in the pulling state
this.models = this.models.filter(m => m.id !== modelId)
}
async importModel(
model: string,
modelPath: string,
name?: string,
optionType?: OptionType
): Promise<void> {
const newModel: Model = {
id: `model-${Date.now()}`,
path: modelPath,
name: name || model,
source: 'local',
modelFormat: optionType?.format || 'mock-format',
engine: optionType?.engine || 'mock-engine',
format: optionType?.format || 'mock-format',
status: 'ready',
contextLength: optionType?.contextLength || 2048,
sizeInGB: 2,
created: new Date().toISOString(),
}
this.models.push(newModel)
this.loadedModels.add(newModel.id)
}
async updateModel(modelInfo: Partial<Model>): Promise<Model> {
if (!modelInfo.id) throw new Error('Model ID is required')
const index = this.models.findIndex(m => m.id === modelInfo.id)
if (index === -1) throw new Error('Model not found')
this.models[index] = { ...this.models[index], ...modelInfo }
return this.models[index]
}
async deleteModel(modelId: string): Promise<void> {
this.models = this.models.filter(m => m.id !== modelId)
this.loadedModels.delete(modelId)
}
async isModelLoaded(modelId: string): Promise<boolean> {
return this.loadedModels.has(modelId)
}
async getSources(): Promise<ModelSource[]> {
return this.sources
}
async addSource(source: string): Promise<void> {
const newSource: ModelSource = {
id: `source-${Date.now()}`,
url: source,
name: `Source ${this.sources.length + 1}`,
type: 'mock-type'
}
this.sources.push(newSource)
}
async deleteSource(sourceId: string): Promise<void> {
this.sources = this.sources.filter(s => s.id !== sourceId)
}
}
describe('ModelExtension', () => {
let extension: MockModelExtension
beforeEach(() => {
extension = new MockModelExtension()
})
test('should return the correct extension type', () => {
expect(extension.type()).toBe(ExtensionTypeEnum.Model)
})
test('should configure pull options', async () => {
const configs = { apiKey: 'test-key', baseUrl: 'https://test-url.com' }
const result = await extension.configurePullOptions(configs)
expect(result).toEqual(configs)
})
test('should add and get models', async () => {
await extension.pullModel('test-model', 'test-id', 'Test Model')
const models = await extension.getModels()
expect(models).toHaveLength(1)
expect(models[0].id).toBe('test-id')
expect(models[0].name).toBe('Test Model')
})
test('should pull model with default id and name', async () => {
await extension.pullModel('test-model')
const models = await extension.getModels()
expect(models).toHaveLength(1)
expect(models[0].name).toBe('test-model')
})
test('should cancel model pull', async () => {
await extension.pullModel('test-model', 'test-id')
// Verify model exists
let models = await extension.getModels()
expect(models).toHaveLength(1)
// Cancel the pull
await extension.cancelModelPull('test-id')
// Verify model was removed
models = await extension.getModels()
expect(models).toHaveLength(0)
})
test('should import model', async () => {
const optionType: OptionType = {
engine: 'test-engine',
format: 'test-format',
contextLength: 4096
}
await extension.importModel('test-model', '/path/to/model', 'Imported Model', optionType)
const models = await extension.getModels()
expect(models).toHaveLength(1)
expect(models[0].name).toBe('Imported Model')
expect(models[0].engine).toBe('test-engine')
expect(models[0].format).toBe('test-format')
expect(models[0].contextLength).toBe(4096)
})
test('should import model with default values', async () => {
await extension.importModel('test-model', '/path/to/model')
const models = await extension.getModels()
expect(models).toHaveLength(1)
expect(models[0].name).toBe('test-model')
expect(models[0].engine).toBe('mock-engine')
expect(models[0].format).toBe('mock-format')
})
test('should update model', async () => {
await extension.pullModel('test-model', 'test-id', 'Test Model')
const updatedModel = await extension.updateModel({
id: 'test-id',
name: 'Updated Model',
contextLength: 8192
})
expect(updatedModel.name).toBe('Updated Model')
expect(updatedModel.contextLength).toBe(8192)
// Verify changes persisted
const models = await extension.getModels()
expect(models[0].name).toBe('Updated Model')
expect(models[0].contextLength).toBe(8192)
})
test('should throw error when updating non-existent model', async () => {
await expect(extension.updateModel({
id: 'non-existent',
name: 'Updated Model'
})).rejects.toThrow('Model not found')
})
test('should throw error when updating model without ID', async () => {
await expect(extension.updateModel({
name: 'Updated Model'
})).rejects.toThrow('Model ID is required')
})
test('should delete model', async () => {
await extension.pullModel('test-model', 'test-id')
// Verify model exists
let models = await extension.getModels()
expect(models).toHaveLength(1)
// Delete the model
await extension.deleteModel('test-id')
// Verify model was removed
models = await extension.getModels()
expect(models).toHaveLength(0)
})
test('should check if model is loaded', async () => {
await extension.pullModel('test-model', 'test-id')
// Check if model is loaded
const isLoaded = await extension.isModelLoaded('test-id')
expect(isLoaded).toBe(true)
// Check if non-existent model is loaded
const nonExistentLoaded = await extension.isModelLoaded('non-existent')
expect(nonExistentLoaded).toBe(false)
})
test('should add and get sources', async () => {
await extension.addSource('https://test-source.com')
const sources = await extension.getSources()
expect(sources).toHaveLength(1)
expect(sources[0].url).toBe('https://test-source.com')
})
test('should delete source', async () => {
await extension.addSource('https://test-source.com')
// Get the source ID
const sources = await extension.getSources()
const sourceId = sources[0].id
// Delete the source
await extension.deleteSource(sourceId)
// Verify source was removed
const updatedSources = await extension.getSources()
expect(updatedSources).toHaveLength(0)
})
})

View File

@ -1,42 +0,0 @@
import { ExtensionTypeEnum } from '../extension';
import { MonitoringExtension } from './monitoring';
it('should have the correct type', () => {
class TestMonitoringExtension extends MonitoringExtension {
getGpuSetting(): Promise<GpuSetting | undefined> {
throw new Error('Method not implemented.');
}
getResourcesInfo(): Promise<any> {
throw new Error('Method not implemented.');
}
getCurrentLoad(): Promise<any> {
throw new Error('Method not implemented.');
}
getOsInfo(): Promise<OperatingSystemInfo> {
throw new Error('Method not implemented.');
}
}
const monitoringExtension = new TestMonitoringExtension();
expect(monitoringExtension.type()).toBe(ExtensionTypeEnum.SystemMonitoring);
});
it('should create an instance of MonitoringExtension', () => {
class TestMonitoringExtension extends MonitoringExtension {
getGpuSetting(): Promise<GpuSetting | undefined> {
throw new Error('Method not implemented.');
}
getResourcesInfo(): Promise<any> {
throw new Error('Method not implemented.');
}
getCurrentLoad(): Promise<any> {
throw new Error('Method not implemented.');
}
getOsInfo(): Promise<OperatingSystemInfo> {
throw new Error('Method not implemented.');
}
}
const monitoringExtension = new TestMonitoringExtension();
expect(monitoringExtension).toBeInstanceOf(MonitoringExtension);
});

View File

@ -1,20 +0,0 @@
import { BaseExtension, ExtensionTypeEnum } from '../extension'
import { GpuSetting, MonitoringInterface, OperatingSystemInfo } from '../../types'
/**
* Monitoring extension for system monitoring.
* @extends BaseExtension
*/
export abstract class MonitoringExtension extends BaseExtension implements MonitoringInterface {
/**
* Monitoring extension type.
*/
type(): ExtensionTypeEnum | undefined {
return ExtensionTypeEnum.SystemMonitoring
}
abstract getGpuSetting(): Promise<GpuSetting | undefined>
abstract getResourcesInfo(): Promise<any>
abstract getCurrentLoad(): Promise<any>
abstract getOsInfo(): Promise<OperatingSystemInfo>
}

View File

@ -55,17 +55,23 @@ const unlinkSync = (...args: any[]) => globalThis.core.api?.unlinkSync(...args)
*/
const appendFileSync = (...args: any[]) => globalThis.core.api?.appendFileSync(...args)
/**
* Copies a file from the source path to the destination path.
* @param src
* @param dest
* @returns
*/
const copyFile: (src: string, dest: string) => Promise<void> = (src, dest) =>
globalThis.core.api?.copyFile(src, dest)
/**
* Gets the list of gguf files in a directory
*
*
* @param path - The paths to the file.
* @returns {Promise<{any}>} - A promise that resolves with the list of gguf and non-gguf files
*/
const getGgufFiles: (paths: string[]) => Promise<any> = (
paths) => globalThis.core.api?.getGgufFiles(paths)
const getGgufFiles: (paths: string[]) => Promise<any> = (paths) =>
globalThis.core.api?.getGgufFiles(paths)
/**
* Gets the file's stats.

View File

@ -1,25 +1,21 @@
import {
AppRoute,
DownloadRoute,
ExtensionRoute,
FileManagerRoute,
FileSystemRoute,
} from '../../../types/api'
import { Downloader } from '../processors/download'
import { FileSystem } from '../processors/fs'
import { Extension } from '../processors/extension'
import { FSExt } from '../processors/fsExt'
import { App } from '../processors/app'
export class RequestAdapter {
downloader: Downloader
fileSystem: FileSystem
extension: Extension
fsExt: FSExt
app: App
constructor(observer?: Function) {
this.downloader = new Downloader(observer)
this.fileSystem = new FileSystem()
this.extension = new Extension()
this.fsExt = new FSExt()
@ -28,9 +24,7 @@ export class RequestAdapter {
// TODO: Clearer Factory pattern here
process(route: string, ...args: any) {
if (route in DownloadRoute) {
return this.downloader.process(route, ...args)
} else if (route in FileSystemRoute) {
if (route in FileSystemRoute) {
return this.fileSystem.process(route, ...args)
} else if (route in ExtensionRoute) {
return this.extension.process(route, ...args)

View File

@ -1,125 +0,0 @@
import { Downloader } from './download'
import { DownloadEvent } from '../../../types/api'
import { DownloadManager } from '../../helper/download'
jest.mock('../../helper', () => ({
getJanDataFolderPath: jest.fn().mockReturnValue('path/to/folder'),
}))
jest.mock('../../helper/path', () => ({
validatePath: jest.fn().mockReturnValue('path/to/folder'),
normalizeFilePath: () =>
process.platform === 'win32' ? 'C:\\Users\\path\\to\\file.gguf' : '/Users/path/to/file.gguf',
}))
jest.mock(
'request',
jest.fn().mockReturnValue(() => ({
on: jest.fn(),
}))
)
jest.mock('fs', () => ({
createWriteStream: jest.fn(),
}))
const requestMock = jest.fn((options, callback) => {
callback(new Error('Test error'), null)
})
jest.mock('request', () => requestMock)
jest.mock('request-progress', () => {
return jest.fn().mockImplementation(() => {
return {
on: jest.fn().mockImplementation((event, callback) => {
if (event === 'error') {
callback(new Error('Download failed'))
}
return {
on: jest.fn().mockImplementation((event, callback) => {
if (event === 'error') {
callback(new Error('Download failed'))
}
return {
on: jest.fn().mockImplementation((event, callback) => {
if (event === 'error') {
callback(new Error('Download failed'))
}
return { pipe: jest.fn() }
}),
}
}),
}
}),
}
})
})
describe('Downloader', () => {
beforeEach(() => {
jest.resetAllMocks()
})
it('should pause download correctly', () => {
const observer = jest.fn()
const fileName = process.platform === 'win32' ? 'C:\\path\\to\\file' : 'path/to/file'
const downloader = new Downloader(observer)
const pauseMock = jest.fn()
DownloadManager.instance.networkRequests[fileName] = { pause: pauseMock }
downloader.pauseDownload(observer, fileName)
expect(pauseMock).toHaveBeenCalled()
})
it('should resume download correctly', () => {
const observer = jest.fn()
const fileName = process.platform === 'win32' ? 'C:\\path\\to\\file' : 'path/to/file'
const downloader = new Downloader(observer)
const resumeMock = jest.fn()
DownloadManager.instance.networkRequests[fileName] = { resume: resumeMock }
downloader.resumeDownload(observer, fileName)
expect(resumeMock).toHaveBeenCalled()
})
it('should handle aborting a download correctly', () => {
const observer = jest.fn()
const fileName = process.platform === 'win32' ? 'C:\\path\\to\\file' : 'path/to/file'
const downloader = new Downloader(observer)
const abortMock = jest.fn()
DownloadManager.instance.networkRequests[fileName] = { abort: abortMock }
downloader.abortDownload(observer, fileName)
expect(abortMock).toHaveBeenCalled()
expect(observer).toHaveBeenCalledWith(
DownloadEvent.onFileDownloadError,
expect.objectContaining({
error: 'aborted',
})
)
})
it('should handle download fail correctly', () => {
const observer = jest.fn()
const fileName = process.platform === 'win32' ? 'C:\\path\\to\\file' : 'path/to/file.gguf'
const downloader = new Downloader(observer)
downloader.downloadFile(observer, {
localPath: fileName,
url: 'http://127.0.0.1',
})
expect(observer).toHaveBeenCalledWith(
DownloadEvent.onFileDownloadError,
expect.objectContaining({
error: expect.anything(),
})
)
})
})

View File

@ -1,138 +0,0 @@
import { resolve, sep } from 'path'
import { DownloadEvent } from '../../../types/api'
import { normalizeFilePath } from '../../helper/path'
import { getJanDataFolderPath } from '../../helper'
import { DownloadManager } from '../../helper/download'
import { createWriteStream, renameSync } from 'fs'
import { Processor } from './Processor'
import { DownloadRequest, DownloadState, NetworkConfig } from '../../../types'
export class Downloader implements Processor {
observer?: Function
constructor(observer?: Function) {
this.observer = observer
}
process(key: string, ...args: any[]): any {
const instance = this as any
const func = instance[key]
return func(this.observer, ...args)
}
downloadFile(observer: any, downloadRequest: DownloadRequest, network?: NetworkConfig) {
const request = require('request')
const progress = require('request-progress')
const strictSSL = !network?.ignoreSSL
const proxy = network?.proxy?.startsWith('http') ? network.proxy : undefined
const { localPath, url } = downloadRequest
let normalizedPath = localPath
if (typeof localPath === 'string') {
normalizedPath = normalizeFilePath(localPath)
}
const array = normalizedPath.split(sep)
const fileName = array.pop() ?? ''
const modelId = downloadRequest.modelId ?? array.pop() ?? ''
const destination = resolve(getJanDataFolderPath(), normalizedPath)
const rq = request({ url, strictSSL, proxy })
// Put request to download manager instance
DownloadManager.instance.setRequest(normalizedPath, rq)
// Downloading file to a temp file first
const downloadingTempFile = `${destination}.download`
// adding initial download state
const initialDownloadState: DownloadState = {
modelId,
fileName,
percent: 0,
size: {
total: 0,
transferred: 0,
},
children: [],
downloadState: 'downloading',
extensionId: downloadRequest.extensionId,
downloadType: downloadRequest.downloadType,
localPath: normalizedPath,
}
DownloadManager.instance.downloadProgressMap[modelId] = initialDownloadState
DownloadManager.instance.downloadInfo[normalizedPath] = initialDownloadState
if (downloadRequest.downloadType === 'extension') {
observer?.(DownloadEvent.onFileDownloadUpdate, initialDownloadState)
}
progress(rq, {})
.on('progress', (state: any) => {
const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId]
const downloadState: DownloadState = {
...currentDownloadState,
...state,
fileName: fileName,
downloadState: 'downloading',
}
console.debug('progress: ', downloadState)
observer?.(DownloadEvent.onFileDownloadUpdate, downloadState)
DownloadManager.instance.downloadProgressMap[modelId] = downloadState
})
.on('error', (error: Error) => {
const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId]
const downloadState: DownloadState = {
...currentDownloadState,
fileName: fileName,
error: error.message,
downloadState: 'error',
}
observer?.(DownloadEvent.onFileDownloadError, downloadState)
DownloadManager.instance.downloadProgressMap[modelId] = downloadState
})
.on('end', () => {
const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId]
if (
currentDownloadState &&
DownloadManager.instance.networkRequests[normalizedPath] &&
DownloadManager.instance.downloadProgressMap[modelId]?.downloadState !== 'error'
) {
// Finished downloading, rename temp file to actual file
renameSync(downloadingTempFile, destination)
const downloadState: DownloadState = {
...currentDownloadState,
fileName: fileName,
downloadState: 'end',
}
observer?.(DownloadEvent.onFileDownloadSuccess, downloadState)
DownloadManager.instance.downloadProgressMap[modelId] = downloadState
}
})
.pipe(createWriteStream(downloadingTempFile))
}
abortDownload(observer: any, fileName: string) {
const rq = DownloadManager.instance.networkRequests[fileName]
if (rq) {
DownloadManager.instance.networkRequests[fileName] = undefined
rq?.abort()
}
const downloadInfo = DownloadManager.instance.downloadInfo[fileName]
observer?.(DownloadEvent.onFileDownloadError, {
...downloadInfo,
fileName,
error: 'aborted',
})
}
resumeDownload(_observer: any, fileName: any) {
DownloadManager.instance.networkRequests[fileName]?.resume()
}
pauseDownload(_observer: any, fileName: any) {
DownloadManager.instance.networkRequests[fileName]?.pause()
}
}

View File

@ -1,11 +0,0 @@
import { DownloadManager } from './download';
it('should set a network request for a specific file', () => {
const downloadManager = new DownloadManager();
const fileName = 'testFile';
const request = { url: 'http://example.com' };
downloadManager.setRequest(fileName, request);
expect(downloadManager.networkRequests[fileName]).toEqual(request);
});

View File

@ -1,30 +0,0 @@
import { DownloadState } from '../../types'
/**
* Manages file downloads and network requests.
*/
export class DownloadManager {
public networkRequests: Record<string, any> = {}
public static instance: DownloadManager = new DownloadManager()
// store the download information with key is model id
public downloadProgressMap: Record<string, DownloadState> = {}
// store the download information with key is normalized file path
public downloadInfo: Record<string, DownloadState> = {}
constructor() {
if (DownloadManager.instance) {
return DownloadManager.instance
}
}
/**
* Sets a network request for a specific file.
* @param {string} fileName - The name of the file.
* @param {Request | undefined} request - The network request to set, or undefined to clear the request.
*/
setRequest(fileName: string, request: any | undefined) {
this.networkRequests[fileName] = request
}
}

View File

@ -1,5 +1,4 @@
export * from './config'
export * from './download'
export * from './logger'
export * from './module'
export * from './path'

View File

@ -31,6 +31,10 @@ export enum NativeRoute {
startServer = 'startServer',
stopServer = 'stopServer',
appUpdateDownload = 'appUpdateDownload',
appToken = 'appToken',
}
/**
@ -50,6 +54,8 @@ export enum AppRoute {
}
export enum AppEvent {
onAppUpdateNotAvailable = 'onAppUpdateNotAvailable',
onAppUpdateAvailable = 'onAppUpdateAvailable',
onAppUpdateDownloadUpdate = 'onAppUpdateDownloadUpdate',
onAppUpdateDownloadError = 'onAppUpdateDownloadError',
onAppUpdateDownloadSuccess = 'onAppUpdateDownloadSuccess',
@ -61,30 +67,13 @@ export enum AppEvent {
onMainViewStateChange = 'onMainViewStateChange',
}
export enum DownloadRoute {
abortDownload = 'abortDownload',
downloadFile = 'downloadFile',
pauseDownload = 'pauseDownload',
resumeDownload = 'resumeDownload',
getDownloadProgress = 'getDownloadProgress',
}
export enum DownloadEvent {
onFileDownloadUpdate = 'onFileDownloadUpdate',
onFileDownloadError = 'onFileDownloadError',
onFileDownloadSuccess = 'onFileDownloadSuccess',
onFileDownloadStopped = 'onFileDownloadStopped',
onFileDownloadStarted = 'onFileDownloadStarted',
onFileUnzipSuccess = 'onFileUnzipSuccess',
}
export enum LocalImportModelEvent {
onLocalImportModelUpdate = 'onLocalImportModelUpdate',
onLocalImportModelFailed = 'onLocalImportModelFailed',
onLocalImportModelSuccess = 'onLocalImportModelSuccess',
onLocalImportModelFinished = 'onLocalImportModelFinished',
}
export enum ExtensionRoute {
baseExtensions = 'baseExtensions',
getActiveExtensions = 'getActiveExtensions',
@ -127,10 +116,6 @@ export type AppEventFunctions = {
[K in AppEvent]: ApiFunction
}
export type DownloadRouteFunctions = {
[K in DownloadRoute]: ApiFunction
}
export type DownloadEventFunctions = {
[K in DownloadEvent]: ApiFunction
}
@ -150,7 +135,6 @@ export type FileManagerRouteFunctions = {
export type APIFunctions = NativeRouteFunctions &
AppRouteFunctions &
AppEventFunctions &
DownloadRouteFunctions &
DownloadEventFunctions &
ExtensionRouteFunctions &
FileSystemRouteFunctions &
@ -158,7 +142,6 @@ export type APIFunctions = NativeRouteFunctions &
export const CoreRoutes = [
...Object.values(AppRoute),
...Object.values(DownloadRoute),
...Object.values(ExtensionRoute),
...Object.values(FileSystemRoute),
...Object.values(FileManagerRoute),
@ -168,7 +151,6 @@ export const APIRoutes = [...CoreRoutes, ...Object.values(NativeRoute)]
export const APIEvents = [
...Object.values(AppEvent),
...Object.values(DownloadEvent),
...Object.values(LocalImportModelEvent),
]
export type PayloadType = {
messages: ChatCompletionMessage[]

View File

@ -1,4 +1,5 @@
export type AppConfiguration = {
data_folder: string
quick_ask: boolean
distinct_id?: string
}

View File

@ -18,6 +18,7 @@ export type EngineMetadata = {
template?: string
}
}
explore_models_url?: string
}
export type EngineVariant = {

View File

@ -16,41 +16,9 @@ export type DownloadState = {
error?: string
extensionId?: string
downloadType?: DownloadType | string
localPath?: string
}
export type DownloadType = 'model' | 'extension'
export type DownloadRequest = {
/**
* The URL to download the file from.
*/
url: string
/**
* The local path to save the file to.
*/
localPath: string
/**
* The extension ID of the extension that initiated the download.
*
* Can be extension name.
*/
extensionId?: string
/**
* The model ID of the model that initiated the download.
*/
modelId?: string
/**
* The download type.
*/
downloadType?: DownloadType | string
}
type DownloadTime = {
elapsed: number
remaining: number
@ -60,7 +28,6 @@ type DownloadSize = {
total: number
transferred: number
}
/**
* The file metadata
*/

View File

@ -0,0 +1,55 @@
export type Cpu = {
arch: string
cores: number
instructions: string[]
model: string
usage: number
}
export type GpuAdditionalInformation = {
compute_cap: string
driver_version: string
}
export type Gpu = {
activated: boolean
additional_information?: GpuAdditionalInformation
free_vram: number
id: string
name: string
total_vram: number
uuid: string
version: string
}
export type Os = {
name: string
version: string
}
export type Power = {
battery_life: number
charging_status: string
is_power_saving: boolean
}
export type Ram = {
available: number
total: number
type: string
}
export type Storage = {
available: number
total: number
type: string
}
export type HardwareInformation = {
cpu: Cpu
gpus: Gpu[]
os: Os
power: Power
ram: Ram
storage: Storage
}

View File

@ -4,7 +4,6 @@ import * as model from './model';
import * as thread from './thread';
import * as message from './message';
import * as inference from './inference';
import * as monitoring from './monitoring';
import * as file from './file';
import * as config from './config';
import * as huggingface from './huggingface';
@ -18,7 +17,6 @@ import * as setting from './setting';
expect(thread).toBeDefined();
expect(message).toBeDefined();
expect(inference).toBeDefined();
expect(monitoring).toBeDefined();
expect(file).toBeDefined();
expect(config).toBeDefined();
expect(huggingface).toBeDefined();

View File

@ -3,7 +3,6 @@ export * from './model'
export * from './thread'
export * from './message'
export * from './inference'
export * from './monitoring'
export * from './file'
export * from './config'
export * from './huggingface'
@ -11,3 +10,4 @@ export * from './miscellaneous'
export * from './api'
export * from './setting'
export * from './engine'
export * from './hardware'

View File

@ -1,8 +0,0 @@
export type FileDownloadRequest = {
downloadId: string
url: string
localPath: string
fileName: string
displayName: string
metadata: Record<string, string | number>
}

View File

@ -1,6 +1,4 @@
export * from './systemResourceInfo'
export * from './promptTemplate'
export * from './appUpdate'
export * from './fileDownloadRequest'
export * from './networkConfig'
export * from './selectFiles'

View File

@ -1,4 +0,0 @@
export type NetworkConfig = {
proxy?: string
ignoreSSL?: boolean
}

View File

@ -1,33 +1,25 @@
import { GpuAdditionalInformation } from '../hardware'
export type SystemResourceInfo = {
memAvailable: number
}
export type RunMode = 'cpu' | 'gpu'
export type GpuSetting = {
notify: boolean
run_mode: RunMode
nvidia_driver: {
exist: boolean
version: string
}
cuda: {
exist: boolean
version: string
}
gpus: GpuSettingInfo[]
gpu_highest_vram: string
gpus_in_use: string[]
is_initial: boolean
// TODO: This needs to be set based on user toggle in settings
vulkan: boolean
cpu?: any
}
export type GpuSettingInfo = {
activated: boolean
free_vram: number
id: string
vram: string
name: string
arch?: string
total_vram: number
uuid: string
version: string
additional_information?: GpuAdditionalInformation
}
export type SystemInformation = {
@ -42,9 +34,6 @@ export type SupportedPlatform = SupportedPlatformTuple[number]
export type OperatingSystemInfo = {
platform: SupportedPlatform | 'unknown'
arch: string
release: string
machine: string
version: string
totalMem: number
freeMem: number
}

View File

@ -71,7 +71,7 @@ export type Model = {
/**
* The model identifier, modern version of id.
*/
mode?: string
model?: string
/**
* Human-readable name that is used for UI.
@ -150,6 +150,7 @@ export type ModelSettingParams = {
*/
export type ModelRuntimeParams = {
temperature?: number
max_temperature?: number
token_limit?: number
top_k?: number
top_p?: number

View File

@ -61,6 +61,7 @@ export interface ModelSibling {
*/
export interface ModelSource {
id: string
author?: string
metadata: Metadata
models: ModelSibling[]
type?: string

View File

@ -1,13 +0,0 @@
import * as monitoringInterface from './monitoringInterface'
import * as resourceInfo from './resourceInfo'
import * as index from './index'
it('should re-export all symbols from monitoringInterface and resourceInfo', () => {
for (const key in monitoringInterface) {
expect(index[key]).toBe(monitoringInterface[key])
}
for (const key in resourceInfo) {
expect(index[key]).toBe(resourceInfo[key])
}
})

View File

@ -1,2 +0,0 @@
export * from './monitoringInterface'
export * from './resourceInfo'

View File

@ -1,29 +0,0 @@
import { GpuSetting, OperatingSystemInfo } from '../miscellaneous'
/**
* Monitoring extension for system monitoring.
* @extends BaseExtension
*/
export interface MonitoringInterface {
/**
* Returns information about the system resources.
* @returns {Promise<any>} A promise that resolves with the system resources information.
*/
getResourcesInfo(): Promise<any>
/**
* Returns the current system load.
* @returns {Promise<any>} A promise that resolves with the current system load.
*/
getCurrentLoad(): Promise<any>
/**
* Returns the GPU configuration.
*/
getGpuSetting(): Promise<GpuSetting | undefined>
/**
* Returns information about the operating system.
*/
getOsInfo(): Promise<OperatingSystemInfo>
}

View File

@ -1,6 +0,0 @@
export type ResourceInfo = {
mem: {
totalMemory: number
usedMemory: number
}
}

View File

@ -1,3 +1,2 @@
export * from './threadEntity'
export * from './threadInterface'
export * from './threadEvent'

View File

@ -1,6 +0,0 @@
import { ThreadEvent } from './threadEvent';
it('should have the correct values', () => {
expect(ThreadEvent.OnThreadStarted).toBe('OnThreadStarted');
});

View File

@ -1,4 +0,0 @@
export enum ThreadEvent {
/** The `OnThreadStarted` event is emitted when a thread is started. */
OnThreadStarted = 'OnThreadStarted',
}

View File

@ -1 +1,3 @@
GTM_ID=xxxx
GTM_ID=xxxx
POSTHOG_KEY=xxxx
POSTHOG_HOST=xxxx

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

@ -27,6 +27,8 @@ const nextConfig = {
output: 'export',
env: {
GTM_ID: process.env.GTM_ID,
POSTHOG_KEY: process.env.POSTHOG_KEY,
POSTHOG_HOST: process.env.POSTHOG_HOST,
},
transpilePackages: ['@scalar', 'react-tweet'],
images: {

View File

@ -36,6 +36,7 @@
"path": "^0.12.7",
"plop": "^4.0.1",
"plop-helper-date": "^1.0.0",
"posthog-js": "^1.194.6",
"react": "^18",
"react-dom": "^18",
"react-hook-form": "^7.51.1",

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 MiB

View File

@ -0,0 +1,7 @@
<svg width="100" height="26" viewBox="0 0 100 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M83.3068 2.47705L84.8212 0.962708H96.4713L97.9856 2.47705L96.4713 4.0086H84.8212L83.3068 2.47705ZM83.3068 23.5229L84.8212 22.0086H96.4713L97.9856 23.5229L96.4713 25.0373H84.8212L83.3068 23.5229ZM97.9856 12.9914L99.4999 14.5229V22.0086L97.9856 23.5229L96.4713 22.0086V14.5229L97.9856 12.9914ZM83.3068 2.47705L81.7925 4.0086V11.4943L83.3068 12.9914L84.8212 11.4943V4.0086L83.3068 2.47705ZM97.9856 2.47705L96.4713 4.0086V11.4943L97.9856 12.9914L99.4999 11.4943V4.0086L97.9856 2.47705ZM83.3068 23.5229L81.7925 22.0086V14.5229L83.3068 12.9914L84.8212 14.5229V22.0086L83.3068 23.5229Z" fill="#FF5C00"/>
<path d="M62.4849 2.47705V11.477L63.9992 12.9914L65.5135 11.4943V2.47705L63.9992 0.962708L62.4849 2.47705ZM63.9992 23.5229L62.4849 22.0086V14.5229L63.9992 12.9914L65.5135 14.5229V22.0086L63.9992 23.5229ZM78.678 23.5229L77.1636 25.0373H65.5135L63.9992 23.5229L65.5135 22.0086H77.1636L78.678 23.5229Z" fill="#FF5C00"/>
<path d="M41.3184 14.5229L42.8327 13.0086L44.347 14.5229V23.5229L42.8327 25.0373L41.3184 23.5229V14.5229ZM41.3184 2.47705L42.8327 0.962708L44.347 2.47705V11.4943L42.8327 13.0086L41.3184 11.4943V2.47705ZM55.9971 2.47705L57.5115 0.962708L59.0258 2.47705V11.4943L57.5115 13.0086L55.9971 11.4943V2.47705ZM55.9971 14.5229L57.5115 13.0086L59.0258 14.5229V23.5229L57.5115 25.0373L55.9971 23.5229V14.5229ZM44.6568 3.87093L45.4312 1.87475L47.4273 2.66634L51.0239 10.9264L50.2323 12.9226L48.2361 12.131L44.6568 3.87093ZM49.4407 14.9187L50.2323 12.9226L52.2285 13.6969L55.8251 21.957L55.0335 23.9531L53.0373 23.1616L49.4407 14.9187Z" fill="#FF5C00"/>
<path d="M23.1808 23.5229L24.6952 22.0086H36.3453L37.8596 23.5229L36.3453 25.0373H24.6952L23.1808 23.5229ZM23.1808 2.47705L24.6952 0.962708H36.3453L37.8596 2.47705L36.3453 4.0086H24.6952L23.1808 2.47705ZM23.1808 12.9914L24.6952 11.477H36.3453L37.8596 12.9914L36.3453 14.5057H24.6952L23.1808 12.9914ZM23.1808 2.47705L21.6665 4.0086V11.4943L23.1808 12.9914L24.6952 11.4943V4.0086L23.1808 2.47705ZM23.1808 23.5229L21.6665 22.0086V14.5229L23.1808 12.9914L24.6952 14.5229V22.0086L23.1808 23.5229Z" fill="#FF5C00"/>
<path d="M16.6931 2.45984L18.2075 4.0086V11.4943L16.6931 13.0086L15.1788 11.4943V4.0086L16.6931 2.45984ZM9.34512 2.45984L10.8767 4.0086V11.4943L9.34512 13.0086L7.83078 11.4943V4.0086L9.34512 2.45984ZM2.01434 13.0086L0.5 11.4943V4.0086L2.01434 2.45984L3.52868 4.0086V11.4943L2.01434 13.0086ZM0.5 14.5229L2.01434 13.0086L3.52868 14.5229V23.5229L2.01434 25.0373L0.5 23.5229V14.5229ZM16.6931 25.0373L15.1788 23.5229V14.5229L16.6931 13.0086L18.2075 14.5229V23.5229L16.6931 25.0373ZM3.52868 0.962708H7.83078L9.34512 2.45984L7.83078 4.0086H3.52868L2.01434 2.45984L3.52868 0.962708ZM10.8767 0.962708H15.1788L16.6931 2.45984L15.1788 4.0086H10.8767L9.34512 2.45984L10.8767 0.962708Z" fill="#FF5C00"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

File diff suppressed because it is too large Load Diff

View File

@ -1,129 +1,148 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
<url><loc>https://jan.ai</loc><lastmod>2024-09-09T08:19:45.721Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/about</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/about/handbook</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/about/handbook/analytics</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/about/handbook/engineering</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/about/handbook/engineering/ci-cd</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/about/handbook/engineering/qa</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/about/handbook/product-design</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/about/handbook/project-management</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/about/handbook/strategy</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/about/handbook/website-docs</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/about/investors</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/about/team</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/about/vision</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/about/wall-of-love</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/blog</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2023-12-21-faster-inference-across-platform</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-01-16-settings-options-right-panel</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-01-29-local-api-server</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-02-05-jan-data-folder</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-02-26-home-servers-with-helm</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-03-06-ui-revamp-settings</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-03-11-import-models</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-03-19-nitro-tensorrt-llm-extension</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-04-02-groq-api-integration</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-04-15-new-mistral-extension</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-04-25-llama3-command-r-hugginface</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-05-20-llamacpp-upgrade-new-remote-models</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-05-28-cohere-aya-23-8b-35b-phi-3-medium</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-06-21-nvidia-nim-support</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-07-15-claude-3-5-support</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-09-01-llama3-1-gemma2-support</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/architecture</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/assistants</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/build-extension</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli/chat</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli/init</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli/kill</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli/models</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli/models/download</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli/models/get</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli/models/list</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli/models/remove</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli/models/start</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli/models/stop</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli/models/update</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli/ps</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli/pull</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli/run</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli/serve</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/command-line</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cortex-cpp</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cortex-llamacpp</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cortex-openvino</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cortex-python</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cortex-tensorrt-llm</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/embeddings</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/embeddings/overview</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/error-codes</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/ext-architecture</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/fine-tuning</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/fine-tuning/overview</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/function-calling</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/hardware</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/installation</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/installation/linux</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/installation/mac</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/installation/windows</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/model-operations</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/model-operations/overview</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/py-library</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/quickstart</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/rag</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/rag/overview</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/server</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/text-generation</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/ts-library</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/vision</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/vision/overview</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/assistants</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/built-in/llama-cpp</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/built-in/tensorrt-llm</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/data-folder</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/desktop</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/desktop/linux</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/desktop/mac</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/desktop/windows</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/error-codes</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/extensions</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/install-extensions</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/models</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/models/manage-models</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/models/model-parameters</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/quickstart</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/remote-models/anthropic</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/remote-models/azure</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/remote-models/cohere</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/remote-models/generic-openai</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/remote-models/groq</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/remote-models/martian</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/remote-models/mistralai</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/remote-models/nvidia-nim</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/remote-models/openai</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/remote-models/openrouter</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/remote-models/triton</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/settings</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/shortcuts</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/threads</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/tools/retrieval</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/troubleshooting</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/download</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/integrations</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/integrations/coding/continue-dev</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/integrations/function-calling/interpreter</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/integrations/messaging/llmcord</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/integrations/workflow-automation/raycast</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/post/benchmarking-nvidia-tensorrt-llm</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/post/bitdefender</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/post/data-is-moat</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/post/rag-is-not-enough</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/privacy</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/support</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai</loc><lastmod>2025-03-10T05:06:47.876Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/about</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/about/handbook</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/about/handbook/analytics</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/about/handbook/engineering</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/about/handbook/engineering/ci-cd</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/about/handbook/engineering/qa</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/about/handbook/product-design</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/about/handbook/project-management</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/about/handbook/strategy</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/about/handbook/website-docs</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/about/investors</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/about/team</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/about/vision</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/about/wall-of-love</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/blog</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2023-12-21-faster-inference-across-platform</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-01-16-settings-options-right-panel</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-01-29-local-api-server</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-02-05-jan-data-folder</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-02-10-jan-is-more-stable</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-02-26-home-servers-with-helm</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-03-06-ui-revamp-settings</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-03-11-import-models</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-03-19-nitro-tensorrt-llm-extension</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-04-02-groq-api-integration</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-04-15-new-mistral-extension</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-04-25-llama3-command-r-hugginface</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-05-20-llamacpp-upgrade-new-remote-models</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-05-28-cohere-aya-23-8b-35b-phi-3-medium</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-06-21-nvidia-nim-support</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-07-15-claude-3-5-support</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-09-01-llama3-1-gemma2-support</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-09-17-improved-cpu-performance</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-10-24-jan-stable</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-11-22-jan-bugs</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-11.14-jan-supports-qwen-coder</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-12-03-jan-is-faster</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-12-05-jan-hot-fix-mac</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2024-12-30-jan-new-privacy</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2025-01-06-key-issues-resolved</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/changelog/2025-01-23-deepseek-r1-jan</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/architecture</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/assistants</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/build-extension</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli/chat</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli/init</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli/kill</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli/models</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli/models/download</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli/models/get</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli/models/list</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli/models/remove</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli/models/start</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli/models/stop</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli/models/update</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli/ps</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli/pull</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli/run</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cli/serve</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/command-line</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cortex-cpp</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cortex-llamacpp</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cortex-openvino</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cortex-python</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/cortex-tensorrt-llm</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/embeddings</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/embeddings/overview</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/error-codes</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/ext-architecture</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/fine-tuning</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/fine-tuning/overview</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/function-calling</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/hardware</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/installation</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/installation/linux</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/installation/mac</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/installation/windows</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/model-operations</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/model-operations/overview</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/py-library</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/quickstart</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/rag</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/rag/overview</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/server</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/text-generation</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/ts-library</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/vision</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/cortex/vision/overview</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/api-server</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/assistants</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/configure-extensions</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/data-folder</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/desktop</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/desktop/linux</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/desktop/mac</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/desktop/windows</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/error-codes</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/extensions</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/extensions-settings/model-management</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/extensions-settings/system-monitoring</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/install-engines</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/install-extensions</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/local-engines/llama-cpp</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/models</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/models/manage-models</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/models/model-parameters</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/privacy</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/privacy-policy</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/quickstart</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/remote-models/anthropic</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/remote-models/cohere</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/remote-models/deepseek</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/remote-models/google</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/remote-models/groq</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/remote-models/martian</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/remote-models/mistralai</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/remote-models/nvidia-nim</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/remote-models/openai</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/remote-models/openrouter</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/remote-models/triton</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/settings</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/threads</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/tools/retrieval</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/docs/troubleshooting</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/download</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/integrations</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/integrations/coding/continue-dev</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/integrations/coding/tabby</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/integrations/function-calling/interpreter</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/integrations/messaging/llmcord</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/integrations/workflow-automation/n8n</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/post/benchmarking-nvidia-tensorrt-llm</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/post/bitdefender</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/post/data-is-moat</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/post/deepseek-r1-locally</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/post/offline-chatgpt-alternative</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/post/rag-is-not-enough</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/post/run-ai-models-locally</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/privacy</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://jan.ai/support</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
</urlset>

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/5/views/52"
href="https://github.com/orgs/menloresearch/projects/5/views/52"
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

@ -86,7 +86,7 @@ export default function CardDownload({ lastRelease }: Props) {
.replace('{tag}', tag)
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}`,
}
})

View File

@ -149,7 +149,7 @@ const DropdownDownload = ({ lastRelease }: Props) => {
.replace('{tag}', tag)
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}`,
}
})
setSystems(updatedSystems)

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'
import React, { useEffect, useState } from 'react'
import ThemeImage from '@/components/ThemeImage'
import { AiOutlineGithub } from 'react-icons/ai'
import { RiTwitterXFill } from 'react-icons/ri'
@ -7,6 +7,7 @@ import { BiLogoDiscordAlt } from 'react-icons/bi'
import { useForm } from 'react-hook-form'
import LogoMark from '@/components/LogoMark'
import { FaLinkedin } from 'react-icons/fa'
import posthog from 'posthog-js'
const socials = [
{
@ -25,7 +26,7 @@ const socials = [
icon: (
<AiOutlineGithub className="text-lg text-black/60 dark:text-white/60" />
),
href: 'https://github.com/janhq/jan',
href: 'https://github.com/menloresearch/jan',
},
{
icon: <FaLinkedin className="text-lg text-black/60 dark:text-white/60" />,
@ -61,7 +62,7 @@ const menus = [
child: [
{
menu: 'Github',
path: 'https://github.com/janhq/jan',
path: 'https://github.com/menloresearch/jan',
external: true,
},
{
@ -94,7 +95,7 @@ const menus = [
},
{
menu: 'Careers',
path: 'https://homebrew.bamboohr.com/careers',
path: 'https://menlo.bamboohr.com/careers',
external: true,
},
],
@ -104,6 +105,19 @@ const menus = [
const getCurrentYear = new Date().getFullYear()
export default function Footer() {
useEffect(() => {
if (typeof window !== 'undefined') {
posthog.init(process.env.POSTHOG_KEY as string, {
api_host: process.env.POSTHOG_HOST,
disable_session_recording: true,
person_profiles: 'always',
persistence: 'localStorage',
})
posthog.capture('web_page_view', { timestamp: new Date() })
}
}, [])
const { register, handleSubmit, reset } = useForm({
defaultValues: {
email: '',
@ -237,14 +251,14 @@ export default function Footer() {
)
})}
</div>
<span>&copy;{getCurrentYear}&nbsp;Homebrew Computer Company</span>
<span>&copy;{getCurrentYear}&nbsp;Menlo Research</span>
<ThemeImage
source={{
light: '/assets/images/general/homebrew-dark.svg',
dark: '/assets/images/general/homebrew-white.svg',
light: '/assets/images/general/menlo.svg',
dark: '/assets/images/general/menlo.svg',
}}
alt="App screenshots"
width={140}
width={80}
height={200}
/>
</div>

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

@ -3,7 +3,6 @@ import '@code-hike/mdx/styles.css'
import { Fragment } from "react"
import Script from "next/script"
export default function App({ Component, pageProps }) {
return (
<Fragment>

View File

@ -26,19 +26,19 @@ Jan operates on open-source principles, giving everyone the freedom to adjust, p
We embrace open development, showcasing our progress and upcoming features on GitHub, and we encourage your input and contributions:
- [Jan Framework](https://github.com/janhq/jan) (AGPLv3)
- [Jan Framework](https://github.com/menloresearch/jan) (AGPLv3)
- [Jan Desktop Client & Local server](https://jan.ai) (AGPLv3, built on Jan Framework)
- [Nitro: run Local AI](https://github.com/janhq/nitro) (AGPLv3)
- [Nitro: run Local AI](https://github.com/menloresearch/nitro) (AGPLv3)
## Build in Public
We use GitHub to build in public and welcome anyone to join in.
- [Jan's Kanban](https://github.com/orgs/janhq/projects/5)
- [Jan's Roadmap](https://github.com/orgs/janhq/projects/5/views/29)
- [Jan's Kanban](https://github.com/orgs/menloresearch/projects/5)
- [Jan's Roadmap](https://github.com/orgs/menloresearch/projects/5/views/29)
## Collaboration
Our team spans the globe, working remotely to bring Jan to life. We coordinate through Discord and GitHub, valuing asynchronous communication and minimal, purposeful meetings. For collaboration and brainstorming, we utilize tools like [Excalidraw](https://excalidraw.com/) and [Miro](https://miro.com/), ensuring alignment and shared vision through visual storytelling and detailed documentation on [HackMD](https://hackmd.io/).
Check out the [Jan Framework](https://github.com/janhq/jan) and our desktop client & local server at [jan.ai](https://jan.ai), both licensed under AGPLv3 for maximum openness and user freedom.
Check out the [Jan Framework](https://github.com/menloresearch/jan) and our desktop client & local server at [jan.ai](https://jan.ai), both licensed under AGPLv3 for maximum openness and user freedom.

View File

@ -19,5 +19,5 @@ keywords:
## Prerequisites
- [Requirements](https://github.com/janhq/jan?tab=readme-ov-file#requirements-for-running-jan)
- [Setting up local env](https://github.com/janhq/jan?tab=readme-ov-file#contributing)
- [Requirements](https://github.com/menloresearch/jan?tab=readme-ov-file#requirements-for-running-jan)
- [Setting up local env](https://github.com/menloresearch/jan?tab=readme-ov-file#contributing)

View File

@ -20,7 +20,7 @@ import { Callout } from 'nextra/components'
# Project Management
We use the [Jan Monorepo Project](https://github.com/orgs/janhq/projects/5) in Github to manage our roadmap and sprint Kanbans.
We use the [Jan Monorepo Project](https://github.com/orgs/menloresearch/projects/5) in Github to manage our roadmap and sprint Kanbans.
As much as possible, everyone owns their respective `epics` and `tasks`.
@ -30,37 +30,37 @@ As much as possible, everyone owns their respective `epics` and `tasks`.
## Quicklinks
- [High-level roadmap](https://github.com/orgs/janhq/projects/5/views/16): view used at at strategic level, for team wide alignment. Start & end dates reflect engineering implementation cycles. Typically product & design work preceeds these timelines.
- [Standup Kanban](https://github.com/orgs/janhq/projects/5/views/25): view used during daily standup. Sprints should be up to date.
- [High-level roadmap](https://github.com/orgs/menloresearch/projects/5/views/16): view used at at strategic level, for team wide alignment. Start & end dates reflect engineering implementation cycles. Typically product & design work preceeds these timelines.
- [Standup Kanban](https://github.com/orgs/menloresearch/projects/5/views/25): view used during daily standup. Sprints should be up to date.
## Organization
[`Roadmap Labels`](https://github.com/janhq/jan/labels?q=roadmap)
[`Roadmap Labels`](https://github.com/menloresearch/jan/labels?q=roadmap)
- `Roadmap Labels` tag large, long-term, & strategic projects that can span multiple teams and multiple sprints
- Example label: `roadmap: Jan has Mobile`
- `Roadmaps` contain `epics`
[`Epics`](https://github.com/janhq/jan/issues?q=is%3Aissue+is%3Aopen+label%3A%22type%3A+epic%22)
[`Epics`](https://github.com/menloresearch/jan/issues?q=is%3Aissue+is%3Aopen+label%3A%22type%3A+epic%22)
- `Epics` track large stories that span 1-2 weeks, and it outlines specs, architecture decisions, designs
- `Epics` contain `tasks`
- `Epics` should always have 1 owner
[`Milestones`](https://github.com/janhq/jan/milestones)
[`Milestones`](https://github.com/menloresearch/jan/milestones)
- `Milestones` track release versions. We use [semantic versioning](https://semver.org/)
- `Milestones` span ~2 weeks and have deadlines
- `Milestones` usually fit within 2-week sprint cycles
[`Tasks`](https://github.com/janhq/jan/issues)
[`Tasks`](https://github.com/menloresearch/jan/issues)
- Tasks are individual issues (feats, bugs, chores) that can be completed within a few days
- Tasks, except for critical bugs, should always belong to an `epic` (and thus fit into our roadmap)
- Tasks are usually named per [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/#summary)
- Tasks should always have 1 owner
We aim to always sprint on `tasks` that are a part of the [current roadmap](https://github.com/orgs/janhq/projects/5/views/16).
We aim to always sprint on `tasks` that are a part of the [current roadmap](https://github.com/orgs/menloresearch/projects/5/views/16).
## Kanban
@ -80,4 +80,4 @@ We aim to always sprint on `tasks` that are a part of the [current roadmap](http
As a result, our feature prioritization can feel a bit black box at times.
We'd appreciate high quality insights and volunteers for user interviews through [Discord](https://discord.gg/af6SaTdzpx) and [Github](https://github.com/janhq).
We'd appreciate high quality insights and volunteers for user interviews through [Discord](https://discord.gg/af6SaTdzpx) and [Github](https://github.com/menloresearch).

View File

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

View File

@ -1,9 +1,9 @@
---
title: Homebrew Computer Company
description: We are Homebrew Computer Company, the creators and maintainers of Jan, Cortex and other tools.
title: Menlo Research
description: We are Menlo Research, the creators and maintainers of Jan, Cortex and other tools.
keywords:
[
Homebrew Computer Company,
Menlo Research,
Jan,
local AI,
open-source alternative to chatgpt,
@ -22,7 +22,7 @@ keywords:
import { Callout } from 'nextra/components'
# Homebrew Computer Company
# Menlo Research
![Eniac](./_assets/eniac.jpeg)
_[Eniac](https://www.computerhistory.org/revolution/birth-of-the-computer/4/78), the World's First Computer (Photo courtesy of US Army)_
@ -36,7 +36,7 @@ We're a team of AI researchers and engineers. We are the creators and lead maint
- More to come!
<Callout>
The [Homebrew Computer Club](https://en.wikipedia.org/wiki/Homebrew_Computer_Club) was an early computer hobbyist group from 1975 to 1986 that led to Apple and the personal computer revolution.
The [Menlo Research](https://en.wikipedia.org/wiki/Homebrew_Computer_Club) was an early computer hobbyist group from 1975 to 1986 that led to Apple and the personal computer revolution.
</Callout>
### Mission
@ -81,7 +81,7 @@ Our products are designed with [Extension APIs](/docs/extensions), and we do our
We are part of a larger open-source community and are committed to being a good jigsaw puzzle piece. We credit and actively contribute to upstream projects.
We adopt a public-by-default approach to [Project Management](https://github.com/orgs/janhq/projects/5), [Roadmaps](https://github.com/orgs/janhq/projects/5/views/31), and Helpdesk for our products.
We adopt a public-by-default approach to [Project Management](https://github.com/orgs/menloresearch/projects/5), [Roadmaps](https://github.com/orgs/menloresearch/projects/5/views/31), and Helpdesk for our products.
## Inspirations
@ -93,7 +93,7 @@ We are inspired by and actively try to emulate the paths of companies we admire
- [Obsidian](https://obsidian.md/)
- [Discourse](https://www.discourse.org/about)
- [Gitlab](https://handbook.gitlab.com/handbook/company/history/#2017-gitlab-storytime)
- [Redhat](https://www.redhat.com/en/about/development-model)
- [Red Hat](https://www.redhat.com/en/about/development-model)
- [Ghost](https://ghost.org/docs/contributing/)
- [Lago](https://www.getlago.com/blog/open-source-licensing-and-why-lago-chose-agplv3)
- [Twenty](https://twenty.com/story)

View File

@ -20,7 +20,7 @@ import { Cards, Card } from 'nextra/components'
We're a small, fully-remote team, mostly based in Southeast Asia.
We are committed to become a global company. You can check our [Careers page](https://homebrew.bamboohr.com/careers) if you'd like to join us on our adventure.
We are committed to become a global company. You can check our [Careers page](https://menlo.bamboohr.com/careers) if you'd like to join us on our adventure.
<Callout emoji="🌏">
Ping us in [Discord](https://discord.gg/AAGQNpJQtH) if you're keen to talk to us!

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](/assets/images/changelog/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](/assets/images/changelog/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).

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