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: contact_links:
- name: "\1F4AC Jan Discussions" - 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" about: "Get help, discuss features & roadmap, and share your projects"

View File

@ -9,31 +9,6 @@ jobs:
get-update-version: get-update-version:
uses: ./.github/workflows/template-get-update-version.yml 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: build-macos:
uses: ./.github/workflows/template-build-macos.yml uses: ./.github/workflows/template-build-macos.yml
secrets: inherit secrets: inherit
@ -43,6 +18,8 @@ jobs:
public_provider: github public_provider: github
new_version: ${{ needs.get-update-version.outputs.new_version }} new_version: ${{ needs.get-update-version.outputs.new_version }}
beta: true beta: true
nightly: false
cortex_api_port: "39271"
build-windows-x64: build-windows-x64:
uses: ./.github/workflows/template-build-windows-x64.yml uses: ./.github/workflows/template-build-windows-x64.yml
@ -53,6 +30,8 @@ jobs:
public_provider: github public_provider: github
new_version: ${{ needs.get-update-version.outputs.new_version }} new_version: ${{ needs.get-update-version.outputs.new_version }}
beta: true beta: true
nightly: false
cortex_api_port: "39271"
build-linux-x64: build-linux-x64:
uses: ./.github/workflows/template-build-linux-x64.yml uses: ./.github/workflows/template-build-linux-x64.yml
@ -63,9 +42,11 @@ jobs:
public_provider: github public_provider: github
new_version: ${{ needs.get-update-version.outputs.new_version }} new_version: ${{ needs.get-update-version.outputs.new_version }}
beta: true beta: true
nightly: false
cortex_api_port: "39271"
sync-temp-to-latest: 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 runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
@ -82,19 +63,15 @@ jobs:
AWS_DEFAULT_REGION: ${{ secrets.DELTA_AWS_REGION }} AWS_DEFAULT_REGION: ${{ secrets.DELTA_AWS_REGION }}
AWS_EC2_METADATA_DISABLED: "true" 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: 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 runs-on: ubuntu-latest
steps: steps:
- name: Set version to environment variable - name: Set version to environment variable
run: | 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 - name: Notify Discord
uses: Ilshidur/action-discord@master uses: Ilshidur/action-discord@master
@ -105,6 +82,5 @@ jobs:
- macOS Universal: https://delta.jan.ai/beta/jan-beta-mac-universal-{{ VERSION }}.dmg - 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 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 - 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: env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_JAN_BETA }} DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_JAN_BETA }}

View File

@ -55,6 +55,9 @@ jobs:
ref: ${{ needs.set-public-provider.outputs.ref }} ref: ${{ needs.set-public-provider.outputs.ref }}
public_provider: ${{ needs.set-public-provider.outputs.public_provider }} public_provider: ${{ needs.set-public-provider.outputs.public_provider }}
new_version: ${{ needs.get-update-version.outputs.new_version }} new_version: ${{ needs.get-update-version.outputs.new_version }}
nightly: true
beta: false
cortex_api_port: "39261"
build-windows-x64: build-windows-x64:
uses: ./.github/workflows/template-build-windows-x64.yml uses: ./.github/workflows/template-build-windows-x64.yml
@ -64,8 +67,9 @@ jobs:
ref: ${{ needs.set-public-provider.outputs.ref }} ref: ${{ needs.set-public-provider.outputs.ref }}
public_provider: ${{ needs.set-public-provider.outputs.public_provider }} public_provider: ${{ needs.set-public-provider.outputs.public_provider }}
new_version: ${{ needs.get-update-version.outputs.new_version }} new_version: ${{ needs.get-update-version.outputs.new_version }}
nightly: true
beta: false
cortex_api_port: "39261"
build-linux-x64: build-linux-x64:
uses: ./.github/workflows/template-build-linux-x64.yml uses: ./.github/workflows/template-build-linux-x64.yml
secrets: inherit secrets: inherit
@ -74,6 +78,9 @@ jobs:
ref: ${{ needs.set-public-provider.outputs.ref }} ref: ${{ needs.set-public-provider.outputs.ref }}
public_provider: ${{ needs.set-public-provider.outputs.public_provider }} public_provider: ${{ needs.set-public-provider.outputs.public_provider }}
new_version: ${{ needs.get-update-version.outputs.new_version }} new_version: ${{ needs.get-update-version.outputs.new_version }}
nightly: true
beta: false
cortex_api_port: "39261"
sync-temp-to-latest: sync-temp-to-latest:
needs: [set-public-provider, build-windows-x64, build-linux-x64, build-macos] needs: [set-public-provider, build-windows-x64, build-linux-x64, build-macos]
@ -141,4 +148,3 @@ jobs:
RUN_ID=${{ github.run_id }} 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})." 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" gh pr comment $PR_URL --body "$COMMENT"

View File

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

View File

@ -36,7 +36,7 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.PAT_SERVICE_ACCOUNT }} GITHUB_TOKEN: ${{ secrets.PAT_SERVICE_ACCOUNT }}
run: | 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) latest_prerelease_name=$(cat /tmp/github_api_releases.json | jq -r '.[] | select(.prerelease) | .name' | head -n 1)
get_asset_count() { get_asset_count() {
@ -89,39 +89,39 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
submodules: recursive submodules: recursive
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.PAT_SERVICE_ACCOUNT }} token: ${{ secrets.PAT_SERVICE_ACCOUNT }}
- name: Wait for CI to pass - name: Wait for CI to pass
env: env:
GITHUB_TOKEN: ${{ secrets.PAT_SERVICE_ACCOUNT }} GITHUB_TOKEN: ${{ secrets.PAT_SERVICE_ACCOUNT }}
run: | run: |
pr_number=${{ needs.update-submodule.outputs.pr_number }} pr_number=${{ needs.update-submodule.outputs.pr_number }}
while true; do while true; do
ci_completed=$(gh pr checks $pr_number --json completedAt --jq '.[].completedAt') ci_completed=$(gh pr checks $pr_number --json completedAt --jq '.[].completedAt')
if echo "$ci_completed" | grep -q "0001-01-01T00:00:00Z"; then if echo "$ci_completed" | grep -q "0001-01-01T00:00:00Z"; then
echo "CI is still running, waiting..." echo "CI is still running, waiting..."
sleep 60 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
else else
echo "CI passed, merging PR..." echo "CI has completed, checking states..."
break 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
fi done
done
- name: Merge the PR - name: Merge the PR
env: env:
GITHUB_TOKEN: ${{ secrets.PAT_SERVICE_ACCOUNT }} GITHUB_TOKEN: ${{ secrets.PAT_SERVICE_ACCOUNT }}
run: | run: |
pr_number=${{ needs.update-submodule.outputs.pr_number }} pr_number=${{ needs.update-submodule.outputs.pr_number }}
gh pr merge $pr_number --merge --admin 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 required: false
type: boolean type: boolean
default: false default: false
nightly:
required: false
type: boolean
default: false
cortex_api_port:
required: false
type: string
default: null
secrets: secrets:
DELTA_AWS_S3_BUCKET_NAME: DELTA_AWS_S3_BUCKET_NAME:
required: false required: false
@ -43,6 +51,31 @@ jobs:
with: with:
ref: ${{ inputs.ref }} 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 - name: Installing node
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
@ -83,7 +116,7 @@ jobs:
cat ./electron/package.json cat ./electron/package.json
echo "------------------------" echo "------------------------"
cat ./package.json 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 mv /tmp/package.json electron/package.json
cat electron/package.json cat electron/package.json
@ -115,6 +148,7 @@ jobs:
AWS_MAX_ATTEMPTS: '5' AWS_MAX_ATTEMPTS: '5'
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }} POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }} POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
CORTEX_API_PORT: ${{ inputs.cortex_api_port }}
- name: Build and publish app to github - name: Build and publish app to github
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == false 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 uses: actions/upload-artifact@v4
with: with:
name: jan-linux-amd64-${{ inputs.new_version }}-AppImage 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 required: false
type: boolean type: boolean
default: false default: false
nightly:
required: false
type: boolean
default: false
cortex_api_port:
required: false
type: string
default: null
secrets: secrets:
DELTA_AWS_S3_BUCKET_NAME: DELTA_AWS_S3_BUCKET_NAME:
required: false required: false
@ -52,6 +60,30 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
ref: ${{ inputs.ref }} 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 - name: Installing node
uses: actions/setup-node@v1 uses: actions/setup-node@v1
@ -99,7 +131,7 @@ jobs:
cat ./electron/package.json cat ./electron/package.json
echo "------------------------" echo "------------------------"
cat ./package.json 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 mv /tmp/package.json electron/package.json
cat electron/package.json cat electron/package.json
@ -154,6 +186,7 @@ jobs:
AWS_MAX_ATTEMPTS: '5' AWS_MAX_ATTEMPTS: '5'
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }} POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }} POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
CORTEX_API_PORT: ${{ inputs.cortex_api_port }}
- name: Build and publish app to github - name: Build and publish app to github
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == false 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 uses: actions/upload-artifact@v4
with: with:
name: jan-mac-universal-${{ inputs.new_version }} name: jan-mac-universal-${{ inputs.new_version }}
path: ./electron/dist/*.dmg path: ./electron/dist/*.dmg

View File

@ -23,6 +23,14 @@ on:
required: false required: false
type: boolean type: boolean
default: false default: false
nightly:
required: false
type: boolean
default: false
cortex_api_port:
required: false
type: string
default: null
secrets: secrets:
DELTA_AWS_S3_BUCKET_NAME: DELTA_AWS_S3_BUCKET_NAME:
required: false required: false
@ -52,6 +60,30 @@ jobs:
with: with:
ref: ${{ inputs.ref }} 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 - name: Installing node
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
@ -108,7 +140,7 @@ jobs:
cat ./package.json cat ./package.json
echo "------------------------" echo "------------------------"
cat ./electron/scripts/uninstaller.nsh 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 mv /tmp/package.json electron/package.json
cat electron/package.json cat electron/package.json
@ -153,6 +185,7 @@ jobs:
AWS_MAX_ATTEMPTS: '5' AWS_MAX_ATTEMPTS: '5'
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }} POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }} POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
CORTEX_API_PORT: ${{ inputs.cortex_api_port }}
- name: Build app and publish app to github - 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 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 uses: actions/upload-artifact@v4
with: with:
name: jan-win-x64-${{ inputs.new_version }} name: jan-win-x64-${{ inputs.new_version }}
path: ./electron/dist/*.exe path: ./electron/dist/*.exe

View File

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

View File

@ -34,7 +34,7 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
fetch-depth: "0" fetch-depth: '0'
token: ${{ secrets.PAT_SERVICE_ACCOUNT }} token: ${{ secrets.PAT_SERVICE_ACCOUNT }}
ref: ${{ inputs.ref }} ref: ${{ inputs.ref }}
@ -51,6 +51,6 @@ jobs:
- macOS Universal: https://delta.jan.ai/nightly/jan-nightly-mac-universal-{{ VERSION }}.dmg - 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 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 - 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: 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 ### Reporting Bugs
- **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/janhq/jan/issues). - **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/janhq/jan/issues/new). - 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 ### Suggesting Enhancements
@ -29,4 +29,4 @@ First off, thank you for considering contributing to jan. It's people like you t
## Additional Notes ## Additional Notes
Thank you for contributing to jan! Thank you for contributing to jan!

View File

@ -4,18 +4,18 @@
<p align="center"> <p align="center">
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section --> <!-- 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 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/janhq/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/janhq/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/janhq/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"/> <img alt="Discord" src="https://img.shields.io/discord/1107178041848909847?label=discord"/>
</p> </p>
<p align="center"> <p align="center">
<a href="https://jan.ai/docs/quickstart">Getting Started</a> <a href="https://jan.ai/docs/quickstart">Getting Started</a>
- <a href="https://jan.ai/docs">Docs</a> - <a href="https://jan.ai/docs">Docs</a>
- <a href="https://github.com/janhq/jan/releases">Changelog</a> - <a href="https://github.com/menloresearch/jan/releases">Changelog</a>
- <a href="https://github.com/janhq/jan/issues">Bug reports</a> - <a href="https://github.com/menloresearch/jan/issues">Bug reports</a>
- <a href="https://discord.gg/AsJ8krTT3N">Discord</a> - <a href="https://discord.gg/AsJ8krTT3N">Discord</a>
</p> </p>
@ -23,10 +23,9 @@
⚠️ <b> Jan is currently in Development</b>: Expect breaking changes and bugs! ⚠️ <b> Jan is currently in Development</b>: Expect breaking changes and bugs!
</p> </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 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: From PCs to multi-GPU clusters, Jan & Cortex supports universal architectures:
- [x] NVIDIA GPUs (fast) - [x] NVIDIA GPUs (fast)
@ -36,7 +35,8 @@ From PCs to multi-GPU clusters, Jan & Cortex supports universal architectures:
- [x] Windows x64 - [x] Windows x64
#### Features: #### 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 - Connect to [Remote AI APIs](https://jan.ai/docs/remote-models/openai) like Groq and OpenRouter
- Local API Server with OpenAI-equivalent API - Local API Server with OpenAI-equivalent API
- [Extensions](https://jan.ai/docs/extensions) for customizing Jan - [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"><b>Stable (Recommended)</b></td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://app.jan.ai/download/latest/win-x64'> <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> <b>jan.exe</b>
</a> </a>
</td> </td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://app.jan.ai/download/latest/mac-universal'> <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> <b>jan.dmg</b>
</a> </a>
</td> </td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://app.jan.ai/download/latest/linux-amd64-deb'> <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> <b>jan.deb</b>
</a> </a>
</td> </td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://app.jan.ai/download/latest/linux-amd64-appimage'> <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> <b>jan.AppImage</b>
</a> </a>
</td> </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"><b>Beta (Preview)</b></td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://app.jan.ai/download/beta/win-x64'> <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> <b>jan.exe</b>
</a> </a>
</td> </td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://app.jan.ai/download/beta/mac-universal'> <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> <b>jan.dmg</b>
</a> </a>
</td> </td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://app.jan.ai/download/beta/linux-amd64-deb'> <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> <b>jan.deb</b>
</a> </a>
</td> </td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://app.jan.ai/download/beta/linux-amd64-appimage'> <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> <b>jan.AppImage</b>
</a> </a>
</td> </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"><b>Nightly Build (Experimental)</b></td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://app.jan.ai/download/nightly/win-x64'> <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> <b>jan.exe</b>
</a> </a>
</td> </td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://app.jan.ai/download/nightly/mac-universal'> <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> <b>jan.dmg</b>
</a> </a>
</td> </td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://app.jan.ai/download/nightly/linux-amd64-deb'> <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> <b>jan.deb</b>
</a> </a>
</td> </td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://app.jan.ai/download/nightly/linux-amd64-appimage'> <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> <b>jan.AppImage</b>
</a> </a>
</td> </td>
</tr> </tr>
</table> </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 ## Demo
https://github.com/user-attachments/assets/c3592fa2-c504-4d9d-a885-7e00122a50f3 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 ## Quicklinks
### Jan ### Jan
- [Jan Website](https://jan.ai/) - [Jan Website](https://jan.ai/)
- [Jan GitHub](https://github.com/janhq/jan) - [Jan GitHub](https://github.com/menloresearch/jan)
- [Documentation](https://jan.ai/docs) - [Documentation](https://jan.ai/docs)
- [Jan Changelog](https://jan.ai/changelog) - [Jan Changelog](https://jan.ai/changelog)
- [Jan Blog](https://jan.ai/blog) - [Jan Blog](https://jan.ai/blog)
### Cortex.cpp ### 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. 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 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/) - [Documentation](https://cortex.so/docs/)
- [Models Library](https://cortex.so/models) - [Models Library](https://cortex.so/models)
- API Reference: *Under development* - API Reference: _Under development_
## Requirements for running Jan ## Requirements for running Jan
- **MacOS**: 13 or higher - **MacOS**: 13 or higher
@ -179,17 +179,17 @@ Jan is powered by **Cortex.cpp**. It is a C++ command-line interface (CLI) desig
## Troubleshooting ## Troubleshooting
As Jan is in development mode, you might get stuck on a some common issues: 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 a broken build](https://jan.ai/docs/troubleshooting#broken-build)
- [Troubleshooting NVIDIA GPU](https://jan.ai/docs/troubleshooting#troubleshooting-nvidia-gpu) - [Troubleshooting NVIDIA GPU](https://jan.ai/docs/troubleshooting#troubleshooting-nvidia-gpu)
- [Troubleshooting Something's Amiss](https://jan.ai/docs/troubleshooting#somethings-amiss) - [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: 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). 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. 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 ## Contributing
@ -206,7 +206,7 @@ Contributions are welcome! Please read the [CONTRIBUTING.md](CONTRIBUTING.md) fi
1. **Clone the repository and prepare:** 1. **Clone the repository and prepare:**
```bash ```bash
git clone https://github.com/janhq/jan git clone https://github.com/menloresearch/jan
cd jan cd jan
git checkout -b DESIRED_BRANCH 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. This will start the development server and open the desktop app.
### For production build ### For production build
```bash ```bash
@ -244,7 +242,7 @@ Jan builds on top of other open-source projects:
- Bugs & requests: file a GitHub ticket - Bugs & requests: file a GitHub ticket
- For discussion: join our Discord [here](https://discord.gg/FTk2MvZwJH) - 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 - For jobs: please email hr@jan.ai
## Trust & Safety ## Trust & Safety
@ -254,7 +252,7 @@ Beware of scams!
- We will never request your personal information. - We will never request your personal information.
- Our product is completely free; no paid version exists. - Our product is completely free; no paid version exists.
- We do not have a token or ICO. - 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 ## License

View File

@ -28,7 +28,7 @@
</screenshots> </screenshots>
<url type="homepage">https://jan.ai/</url> <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" /> <content_rating type="oars-1.1" />

View File

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

View File

@ -2,7 +2,6 @@ import { openExternalUrl } from './core'
import { joinPath } from './core' import { joinPath } from './core'
import { openFileExplorer } from './core' import { openFileExplorer } from './core'
import { getJanDataFolderPath } from './core' import { getJanDataFolderPath } from './core'
import { abortDownload } from './core'
import { executeOnMain } from './core' import { executeOnMain } from './core'
describe('test core apis', () => { describe('test core apis', () => {
@ -53,18 +52,6 @@ describe('test core apis', () => {
expect(result).toBe('/path/to/jan/data') 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 () => { it('should execute function on main process', async () => {
const extension = 'testExtension' const extension = 'testExtension'
const method = 'testMethod' const method = 'testMethod'

View File

@ -1,9 +1,4 @@
import { import { SystemInformation } from '../types'
DownloadRequest,
FileStat,
NetworkConfig,
SystemInformation,
} from '../types'
/** /**
* Execute a extension module function in main process * Execute a extension module function in main process
@ -14,42 +9,19 @@ import {
* @returns Promise<any> * @returns Promise<any>
* *
*/ */
const executeOnMain: ( const executeOnMain: (extension: string, method: string, ...args: any[]) => Promise<any> = (
extension: string, extension,
method: string, method,
...args: any[] ...args
) => Promise<any> = (extension, method, ...args) => ) => globalThis.core?.api?.invokeExtensionFunc(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. * Gets Jan's data folder path.
* *
* @returns {Promise<string>} A Promise that resolves with Jan's data folder path. * @returns {Promise<string>} A Promise that resolves with Jan's data folder path.
*/ */
const getJanDataFolderPath = (): Promise<string> => const getJanDataFolderPath = (): Promise<string> => globalThis.core.api?.getJanDataFolderPath()
globalThis.core.api?.getJanDataFolderPath()
/** /**
* Opens the file explorer at a specific path. * 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. * @param path - The file path to retrieve dirname.
* @returns {Promise<string>} A promise that resolves the dirname. * @returns {Promise<string>} A promise that resolves the dirname.
*/ */
const dirName: (path: string) => Promise<string> = (path) => const dirName: (path: string) => Promise<string> = (path) => globalThis.core.api?.dirName(path)
globalThis.core.api?.dirName(path)
/** /**
* Retrieve the basename from an url. * Retrieve the basename from an url.
* @param path - The path to retrieve. * @param path - The path to retrieve.
* @returns {Promise<string>} A promise that resolves with the basename. * @returns {Promise<string>} A promise that resolves with the basename.
*/ */
const baseName: (paths: string) => Promise<string> = (path) => const baseName: (paths: string) => Promise<string> = (path) => globalThis.core.api?.baseName(path)
globalThis.core.api?.baseName(path)
/** /**
* Opens an external URL in the default web browser. * 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. * @returns {Promise<string>} - A promise that resolves with the resource path.
*/ */
const getResourcePath: () => Promise<string> = () => const getResourcePath: () => Promise<string> = () => globalThis.core.api?.getResourcePath()
globalThis.core.api?.getResourcePath()
/** /**
* Gets the user's home path. * Gets the user's home path.
* @returns return user's home path * @returns return user's home path
*/ */
const getUserHomePath = (): Promise<string> => const getUserHomePath = (): Promise<string> => globalThis.core.api?.getUserHomePath()
globalThis.core.api?.getUserHomePath()
/** /**
* Log to file from browser processes. * 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. * @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> = ( const isSubdirectory: (from: string, to: string) => Promise<boolean> = (from: string, to: string) =>
from: string, globalThis.core.api?.isSubdirectory(from, to)
to: string
) => globalThis.core.api?.isSubdirectory(from, to)
/** /**
* Get system information * Get system information
@ -159,8 +125,6 @@ export type RegisterExtensionPoint = (
*/ */
export { export {
executeOnMain, executeOnMain,
downloadFile,
abortDownload,
getJanDataFolderPath, getJanDataFolderPath,
openFileExplorer, openFileExplorer,
getResourcePath, getResourcePath,

View File

@ -39,11 +39,6 @@ describe('BaseExtension', () => {
expect(baseExtension.onUnload).toBeDefined() 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 () => { it('should install the extension', async () => {
await baseExtension.install() await baseExtension.install()
// Add your assertions here // Add your assertions here
@ -84,11 +79,6 @@ describe('BaseExtension', () => {
expect(baseExtension.onUnload).toBeDefined() 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 () => { it('should install the extension', async () => {
await baseExtension.install() await baseExtension.install()
// Add your assertions here // Add your assertions here

View File

@ -12,6 +12,7 @@ export enum ExtensionTypeEnum {
SystemMonitoring = 'systemMonitoring', SystemMonitoring = 'systemMonitoring',
HuggingFace = 'huggingFace', HuggingFace = 'huggingFace',
Engine = 'engine', Engine = 'engine',
Hardware = 'hardware',
} }
export interface ExtensionType { export interface ExtensionType {
@ -23,17 +24,6 @@ export interface Compatibility {
version: string 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. * Represents a base extension.
* This class should be extended by any class that represents an extension. * This class should be extended by any class that represents an extension.
@ -174,15 +164,6 @@ export abstract class BaseExtension implements ExtensionType {
return 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. * Install the prerequisites for the extension.
* *
@ -227,7 +208,7 @@ export abstract class BaseExtension implements ExtensionType {
const settings = await this.getSettings() const settings = await this.getSettings()
const updatedSettings = settings.map((setting) => { let updatedSettings = settings.map((setting) => {
const updatedSetting = componentProps.find( const updatedSetting = componentProps.find(
(componentProp) => componentProp.key === setting.key (componentProp) => componentProp.key === setting.key
) )
@ -237,13 +218,20 @@ export abstract class BaseExtension implements ExtensionType {
return setting return setting
}) })
const settingPath = await joinPath([ if (!updatedSettings.length) updatedSettings = componentProps as SettingComponentProps[]
const settingFolder = await joinPath([
await getJanDataFolderPath(), await getJanDataFolderPath(),
this.settingFolderName, this.settingFolderName,
this.name, 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)) await fs.writeFileSync(settingPath, JSON.stringify(updatedSettings, null, 2))
updatedSettings.forEach((setting) => { 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 { EngineManager } from './EngineManager'
import { AIEngine } from './AIEngine' import { AIEngine } from './AIEngine'
import { InferenceEngine } from '../../../types'
// @ts-ignore // @ts-ignore
class MockAIEngine implements AIEngine { class MockAIEngine implements AIEngine {
@ -40,4 +41,69 @@ describe('EngineManager', () => {
const retrievedEngine = engineManager.get<MockAIEngine>('nonExistentProvider') const retrievedEngine = engineManager.get<MockAIEngine>('nonExistentProvider')
expect(retrievedEngine).toBeUndefined() 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', () => { it('should subscribe to events on load', () => {
engine.onLoad() engine.onLoad()
expect(events.on).toHaveBeenCalledWith(MessageEvent.OnMessageSent, expect.any(Function)) expect(events.on).toHaveBeenCalledWith(
expect(events.on).toHaveBeenCalledWith(InferenceEvent.OnInferenceStopped, expect.any(Function)) MessageEvent.OnMessageSent,
expect.any(Function)
)
expect(events.on).toHaveBeenCalledWith(
InferenceEvent.OnInferenceStopped,
expect.any(Function)
)
}) })
it('should handle inference request', async () => { it('should handle inference request', async () => {
@ -77,7 +83,12 @@ describe('OAIEngine', () => {
expect(events.emit).toHaveBeenCalledWith( expect(events.emit).toHaveBeenCalledWith(
MessageEvent.OnMessageUpdate, MessageEvent.OnMessageUpdate,
expect.objectContaining({ expect.objectContaining({
content: [{ type: ContentType.Text, text: { value: 'test response', annotations: [] } }], content: [
{
type: ContentType.Text,
text: { value: 'test response', annotations: [] },
},
],
status: MessageStatus.Ready, status: MessageStatus.Ready,
}) })
) )
@ -101,11 +112,10 @@ describe('OAIEngine', () => {
await engine.inference(data) await engine.inference(data)
expect(events.emit).toHaveBeenCalledWith( expect(events.emit).toHaveBeenLastCalledWith(
MessageEvent.OnMessageUpdate, MessageEvent.OnMessageUpdate,
expect.objectContaining({ expect.objectContaining({
content: [{ type: ContentType.Text, text: { value: 'test error', annotations: [] } }], status: 'error',
status: MessageStatus.Error,
error_code: 500, error_code: 500,
}) })
) )

View File

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

View File

@ -1,14 +1,17 @@
import { lastValueFrom, Observable } from 'rxjs' import { lastValueFrom, Observable } from 'rxjs'
import { requestInference } from './sse' import { requestInference } from './sse'
import { ReadableStream } from 'stream/web'; import { ReadableStream } from 'stream/web'
describe('requestInference', () => { describe('requestInference', () => {
it('should send a request to the inference server and return an Observable', () => { it('should send a request to the inference server and return an Observable', () => {
// Mock the fetch function // Mock the fetch function
const mockFetch: any = jest.fn(() => const mockFetch: any = jest.fn(() =>
Promise.resolve({ Promise.resolve({
ok: true, ok: true,
json: () => Promise.resolve({ choices: [{ message: { content: 'Generated response' } }] }), json: () =>
Promise.resolve({
choices: [{ message: { content: 'Generated response' } }],
}),
headers: new Headers(), headers: new Headers(),
redirected: false, redirected: false,
status: 200, status: 200,
@ -36,7 +39,10 @@ describe('requestInference', () => {
const mockFetch: any = jest.fn(() => const mockFetch: any = jest.fn(() =>
Promise.resolve({ Promise.resolve({
ok: false, 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(), headers: new Headers(),
redirected: false, redirected: false,
status: 401, status: 401,
@ -56,69 +62,85 @@ describe('requestInference', () => {
// Assert the expected behavior // Assert the expected behavior
expect(result).toBeInstanceOf(Observable) 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', () => { it('should handle a successful response with a transformResponse function', () => {
// Mock the fetch function // Mock the fetch function
const mockFetch: any = jest.fn(() => const mockFetch: any = jest.fn(() =>
Promise.resolve({ Promise.resolve({
ok: true, ok: true,
json: () => Promise.resolve({ choices: [{ message: { content: 'Generated response' } }] }), json: () =>
headers: new Headers(), Promise.resolve({
redirected: false, choices: [{ message: { content: 'Generated response' } }],
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(), headers: new Headers(),
redirected: false, redirected: false,
status: 200, status: 200,
statusText: 'OK', statusText: 'OK',
}) })
); )
jest.spyOn(global, 'fetch').mockImplementation(mockFetch); 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');
});
// 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) => { .then(async (response) => {
if (!response.ok) { if (!response.ok) {
const data = await response.json() if (response.status === 401) {
let errorCode = ErrorCode.Unknown throw {
if (data.error) { code: ErrorCode.InvalidApiKey,
errorCode = data.error.code ?? data.error.type ?? ErrorCode.Unknown message: 'Invalid API Key.',
} else if (response.status === 401) { }
errorCode = ErrorCode.InvalidApiKey
} }
const error = { let data = await response.json()
message: data.error?.message ?? data.message ?? 'Error occurred.', try {
code: errorCode, handleError(data)
} catch (err) {
subscriber.error(err)
return
} }
subscriber.error(error)
subscriber.complete()
return
} }
// There could be overriden stream parameter in the model // There could be overriden stream parameter in the model
// that is set in request body (transformed payload) // that is set in request body (transformed payload)
@ -54,9 +53,10 @@ export function requestInference(
model.parameters?.stream === false model.parameters?.stream === false
) { ) {
const data = await response.json() const data = await response.json()
if (data.error || data.message) { try {
subscriber.error(data.error ?? data) handleError(data)
subscriber.complete() } catch (err) {
subscriber.error(err)
return return
} }
if (transformResponse) { if (transformResponse) {
@ -91,13 +91,10 @@ export function requestInference(
const toParse = cachedLines + line const toParse = cachedLines + line
if (!line.includes('data: [DONE]')) { if (!line.includes('data: [DONE]')) {
const data = JSON.parse(toParse.replace('data: ', '')) const data = JSON.parse(toParse.replace('data: ', ''))
if ( try {
'error' in data || handleError(data)
'message' in data || } catch (err) {
'detail' in data subscriber.error(err)
) {
subscriber.error(data.error ?? data)
subscriber.complete()
return return
} }
content += data.choices[0]?.delta?.content ?? '' content += data.choices[0]?.delta?.content ?? ''
@ -118,3 +115,18 @@ export function requestInference(
.catch((err) => subscriber.error(err)) .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 { ConversationalExtension } from './index';
import { InferenceExtension } from './index'; import { InferenceExtension } from './index';
import { MonitoringExtension } from './index';
import { AssistantExtension } from './index'; import { AssistantExtension } from './index';
import { ModelExtension } from './index'; import { ModelExtension } from './index';
import * as Engines from './index'; import * as Engines from './index';
@ -14,10 +13,6 @@ describe('index.ts exports', () => {
expect(InferenceExtension).toBeDefined(); expect(InferenceExtension).toBeDefined();
}); });
test('should export MonitoringExtension', () => {
expect(MonitoringExtension).toBeDefined();
});
test('should export AssistantExtension', () => { test('should export AssistantExtension', () => {
expect(AssistantExtension).toBeDefined(); expect(AssistantExtension).toBeDefined();
}); });
@ -29,4 +24,4 @@ describe('index.ts exports', () => {
test('should export Engines', () => { test('should export Engines', () => {
expect(Engines).toBeDefined(); expect(Engines).toBeDefined();
}); });
}); });

View File

@ -9,10 +9,7 @@ export { ConversationalExtension } from './conversational'
*/ */
export { InferenceExtension } from './inference' export { InferenceExtension } from './inference'
/**
* Monitoring extension for system monitoring.
*/
export { MonitoringExtension } from './monitoring'
/** /**
* Assistant extension for managing assistants. * Assistant extension for managing assistants.
@ -33,3 +30,8 @@ export * from './engines'
* Engines Management * Engines Management
*/ */
export * from './enginesManagement' 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) 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) => const copyFile: (src: string, dest: string) => Promise<void> = (src, dest) =>
globalThis.core.api?.copyFile(src, dest) globalThis.core.api?.copyFile(src, dest)
/** /**
* Gets the list of gguf files in a directory * Gets the list of gguf files in a directory
* *
* @param path - The paths to the file. * @param path - The paths to the file.
* @returns {Promise<{any}>} - A promise that resolves with the list of gguf and non-gguf files * @returns {Promise<{any}>} - A promise that resolves with the list of gguf and non-gguf files
*/ */
const getGgufFiles: (paths: string[]) => Promise<any> = ( const getGgufFiles: (paths: string[]) => Promise<any> = (paths) =>
paths) => globalThis.core.api?.getGgufFiles(paths) globalThis.core.api?.getGgufFiles(paths)
/** /**
* Gets the file's stats. * Gets the file's stats.

View File

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

View File

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

View File

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

View File

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

View File

@ -16,41 +16,9 @@ export type DownloadState = {
error?: string error?: string
extensionId?: string extensionId?: string
downloadType?: DownloadType | string
localPath?: 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 = { type DownloadTime = {
elapsed: number elapsed: number
remaining: number remaining: number
@ -60,7 +28,6 @@ type DownloadSize = {
total: number total: number
transferred: number transferred: number
} }
/** /**
* The file metadata * 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 thread from './thread';
import * as message from './message'; import * as message from './message';
import * as inference from './inference'; import * as inference from './inference';
import * as monitoring from './monitoring';
import * as file from './file'; import * as file from './file';
import * as config from './config'; import * as config from './config';
import * as huggingface from './huggingface'; import * as huggingface from './huggingface';
@ -18,7 +17,6 @@ import * as setting from './setting';
expect(thread).toBeDefined(); expect(thread).toBeDefined();
expect(message).toBeDefined(); expect(message).toBeDefined();
expect(inference).toBeDefined(); expect(inference).toBeDefined();
expect(monitoring).toBeDefined();
expect(file).toBeDefined(); expect(file).toBeDefined();
expect(config).toBeDefined(); expect(config).toBeDefined();
expect(huggingface).toBeDefined(); expect(huggingface).toBeDefined();

View File

@ -3,7 +3,6 @@ export * from './model'
export * from './thread' export * from './thread'
export * from './message' export * from './message'
export * from './inference' export * from './inference'
export * from './monitoring'
export * from './file' export * from './file'
export * from './config' export * from './config'
export * from './huggingface' export * from './huggingface'
@ -11,3 +10,4 @@ export * from './miscellaneous'
export * from './api' export * from './api'
export * from './setting' export * from './setting'
export * from './engine' 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 './systemResourceInfo'
export * from './promptTemplate' export * from './promptTemplate'
export * from './appUpdate' export * from './appUpdate'
export * from './fileDownloadRequest'
export * from './networkConfig'
export * from './selectFiles' 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 = { export type SystemResourceInfo = {
memAvailable: number memAvailable: number
} }
export type RunMode = 'cpu' | 'gpu'
export type GpuSetting = { export type GpuSetting = {
notify: boolean
run_mode: RunMode
nvidia_driver: {
exist: boolean
version: string
}
cuda: {
exist: boolean
version: string
}
gpus: GpuSettingInfo[] 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 // TODO: This needs to be set based on user toggle in settings
vulkan: boolean vulkan: boolean
cpu?: any
} }
export type GpuSettingInfo = { export type GpuSettingInfo = {
activated: boolean
free_vram: number
id: string id: string
vram: string
name: string name: string
arch?: string total_vram: number
uuid: string
version: string
additional_information?: GpuAdditionalInformation
} }
export type SystemInformation = { export type SystemInformation = {
@ -42,9 +34,6 @@ export type SupportedPlatform = SupportedPlatformTuple[number]
export type OperatingSystemInfo = { export type OperatingSystemInfo = {
platform: SupportedPlatform | 'unknown' platform: SupportedPlatform | 'unknown'
arch: string arch: string
release: string
machine: string
version: string
totalMem: number totalMem: number
freeMem: number freeMem: number
} }

View File

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

View File

@ -61,6 +61,7 @@ export interface ModelSibling {
*/ */
export interface ModelSource { export interface ModelSource {
id: string id: string
author?: string
metadata: Metadata metadata: Metadata
models: ModelSibling[] models: ModelSibling[]
type?: string 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 './threadEntity'
export * from './threadInterface' 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 ## 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 ### Pre-requisites and Installation

View File

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

View File

@ -36,6 +36,7 @@
"path": "^0.12.7", "path": "^0.12.7",
"plop": "^4.0.1", "plop": "^4.0.1",
"plop-helper-date": "^1.0.0", "plop-helper-date": "^1.0.0",
"posthog-js": "^1.194.6",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"react-hook-form": "^7.51.1", "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"?> <?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"> <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</loc><lastmod>2025-03-10T05:06:47.876Z</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</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>2024-09-09T08:19:45.722Z</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>2024-09-09T08:19:45.722Z</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>2024-09-09T08:19:45.722Z</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>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>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>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>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>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>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>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>2025-03-10T05:06:47.877Z</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/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>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>2025-03-10T05:06:47.877Z</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/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>2024-09-09T08:19:45.722Z</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>2024-09-09T08:19:45.722Z</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>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>2025-03-10T05:06:47.877Z</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/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>2024-09-09T08:19:45.722Z</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>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>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>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>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>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>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>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>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>2024-09-09T08:19:45.722Z</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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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/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/cortex</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>2025-03-10T05:06:47.877Z</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/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/cortex/assistants</loc><lastmod>2024-09-09T08:19:45.722Z</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/cortex/build-extension</loc><lastmod>2024-09-09T08:19:45.722Z</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/cortex/cli</loc><lastmod>2024-09-09T08:19:45.722Z</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/cortex/cli/chat</loc><lastmod>2024-09-09T08:19:45.722Z</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/cortex/cli/init</loc><lastmod>2024-09-09T08:19:45.722Z</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/cortex/cli/kill</loc><lastmod>2024-09-09T08:19:45.722Z</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/cortex/cli/models</loc><lastmod>2024-09-09T08:19:45.722Z</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/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/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/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</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>2024-09-09T08:19:45.722Z</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/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/assistants</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>2024-09-09T08:19:45.722Z</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/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</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>2024-09-09T08:19:45.722Z</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/ps</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>2025-03-10T05:06:47.877Z</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/kill</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>2024-09-09T08:19:45.722Z</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/serve</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>2025-03-10T05:06:47.877Z</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/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/cortex-cpp</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>2025-03-10T05:06:47.877Z</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/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/cortex-openvino</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>2025-03-10T05:06:47.877Z</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/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/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/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/embeddings</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>2025-03-10T05:06:47.877Z</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/cli/pull</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>2024-09-09T08:19:45.722Z</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/ext-architecture</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>2025-03-10T05:06:47.877Z</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/command-line</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>2024-09-09T08:19:45.722Z</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/function-calling</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>2025-03-10T05:06:47.877Z</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/cortex-openvino</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>2024-09-09T08:19:45.722Z</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/installation/linux</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>2025-03-10T05:06:47.877Z</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/embeddings</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>2024-09-09T08:19:45.722Z</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/model-operations</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>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>2024-09-09T08:19:45.722Z</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/py-library</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>2025-03-10T05:06:47.877Z</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/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/rag</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>2025-03-10T05:06:47.877Z</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/hardware</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>2024-09-09T08:19:45.722Z</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/text-generation</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>2025-03-10T05:06:47.877Z</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/installation/mac</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>2024-09-09T08:19:45.722Z</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/vision/overview</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>2025-03-10T05:06:47.877Z</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/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/docs/assistants</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>2025-03-10T05:06:47.877Z</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/cortex/quickstart</loc><lastmod>2025-03-10T05:06:47.877Z</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/cortex/rag</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>2024-09-09T08:19:45.722Z</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/docs/desktop</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>2025-03-10T05:06:47.877Z</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/cortex/text-generation</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>2024-09-09T08:19:45.722Z</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/docs/desktop/windows</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>2025-03-10T05:06:47.877Z</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/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/extensions</loc><lastmod>2024-09-09T08:19:45.722Z</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/install-extensions</loc><lastmod>2024-09-09T08:19:45.722Z</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/models</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>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>2024-09-09T08:19:45.722Z</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/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/data-folder</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>2024-09-09T08:19:45.722Z</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/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/desktop/linux</loc><lastmod>2025-03-10T05:06:47.877Z</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/desktop/mac</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>2024-09-09T08:19:45.722Z</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/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/error-codes</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>2024-09-09T08:19:45.722Z</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/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/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/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/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/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/install-engines</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>2024-09-09T08:19:45.722Z</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/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/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/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/models</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>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>2025-03-10T05:06:47.877Z</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/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/threads</loc><lastmod>2024-09-09T08:19:45.722Z</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/tools/retrieval</loc><lastmod>2024-09-09T08:19:45.722Z</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/troubleshooting</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>2025-03-10T05:06:47.877Z</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/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/integrations</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>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>2024-09-09T08:19:45.722Z</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/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/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/integrations/messaging/llmcord</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>2025-03-10T05:06:47.877Z</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/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/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/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/post/bitdefender</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>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>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>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>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>2025-03-10T05:06:47.877Z</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/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/support</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>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> </urlset>

View File

@ -27,7 +27,7 @@ export const APIReference = () => {
<ApiReferenceReact <ApiReferenceReact
configuration={{ configuration={{
spec: { 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', theme: 'alternate',
hideModels: true, hideModels: true,

View File

@ -57,7 +57,7 @@ const Changelog = () => {
<p className="text-base mt-2 leading-relaxed"> <p className="text-base mt-2 leading-relaxed">
Latest release updates from the Jan team. Check out our&nbsp; Latest release updates from the Jan team. Check out our&nbsp;
<a <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" className="text-blue-600 dark:text-blue-400 cursor-pointer"
> >
Roadmap Roadmap
@ -150,7 +150,7 @@ const Changelog = () => {
<div className="text-center"> <div className="text-center">
<Link <Link
href="https://github.com/janhq/jan/releases" href="https://github.com/menloresearch/jan/releases"
target="_blank" 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" 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) .replace('{tag}', tag)
return { return {
...system, ...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) .replace('{tag}', tag)
return { return {
...system, ...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) 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 ThemeImage from '@/components/ThemeImage'
import { AiOutlineGithub } from 'react-icons/ai' import { AiOutlineGithub } from 'react-icons/ai'
import { RiTwitterXFill } from 'react-icons/ri' import { RiTwitterXFill } from 'react-icons/ri'
@ -7,6 +7,7 @@ import { BiLogoDiscordAlt } from 'react-icons/bi'
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import LogoMark from '@/components/LogoMark' import LogoMark from '@/components/LogoMark'
import { FaLinkedin } from 'react-icons/fa' import { FaLinkedin } from 'react-icons/fa'
import posthog from 'posthog-js'
const socials = [ const socials = [
{ {
@ -25,7 +26,7 @@ const socials = [
icon: ( icon: (
<AiOutlineGithub className="text-lg text-black/60 dark:text-white/60" /> <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" />, icon: <FaLinkedin className="text-lg text-black/60 dark:text-white/60" />,
@ -61,7 +62,7 @@ const menus = [
child: [ child: [
{ {
menu: 'Github', menu: 'Github',
path: 'https://github.com/janhq/jan', path: 'https://github.com/menloresearch/jan',
external: true, external: true,
}, },
{ {
@ -94,7 +95,7 @@ const menus = [
}, },
{ {
menu: 'Careers', menu: 'Careers',
path: 'https://homebrew.bamboohr.com/careers', path: 'https://menlo.bamboohr.com/careers',
external: true, external: true,
}, },
], ],
@ -104,6 +105,19 @@ const menus = [
const getCurrentYear = new Date().getFullYear() const getCurrentYear = new Date().getFullYear()
export default function Footer() { 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({ const { register, handleSubmit, reset } = useForm({
defaultValues: { defaultValues: {
email: '', email: '',
@ -237,14 +251,14 @@ export default function Footer() {
) )
})} })}
</div> </div>
<span>&copy;{getCurrentYear}&nbsp;Homebrew Computer Company</span> <span>&copy;{getCurrentYear}&nbsp;Menlo Research</span>
<ThemeImage <ThemeImage
source={{ source={{
light: '/assets/images/general/homebrew-dark.svg', light: '/assets/images/general/menlo.svg',
dark: '/assets/images/general/homebrew-white.svg', dark: '/assets/images/general/menlo.svg',
}} }}
alt="App screenshots" alt="App screenshots"
width={140} width={80}
height={200} height={200}
/> />
</div> </div>

View File

@ -23,7 +23,7 @@ const BuiltWithLove = () => {
</div> </div>
<div className="flex flex-col lg:flex-row gap-8 mt-8 items-center justify-center"> <div className="flex flex-col lg:flex-row gap-8 mt-8 items-center justify-center">
<a <a
href="https://github.com/janhq/jan" href="https://github.com/menloresearch/jan"
target="_blank" 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 " 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 className="mt-10 text-center">
<div> <div>
<Link <Link
href="https://github.com/janhq/jan/releases" href="https://github.com/menloresearch/jan/releases"
target="_blank" target="_blank"
className="hidden lg:inline-block" className="hidden lg:inline-block"
> >

View File

@ -3,7 +3,6 @@ import '@code-hike/mdx/styles.css'
import { Fragment } from "react" import { Fragment } from "react"
import Script from "next/script" import Script from "next/script"
export default function App({ Component, pageProps }) { export default function App({ Component, pageProps }) {
return ( return (
<Fragment> <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: 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) - [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 ## Build in Public
We use GitHub to build in public and welcome anyone to join in. 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 Kanban](https://github.com/orgs/menloresearch/projects/5)
- [Jan's Roadmap](https://github.com/orgs/janhq/projects/5/views/29) - [Jan's Roadmap](https://github.com/orgs/menloresearch/projects/5/views/29)
## Collaboration ## 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/). 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 ## Prerequisites
- [Requirements](https://github.com/janhq/jan?tab=readme-ov-file#requirements-for-running-jan) - [Requirements](https://github.com/menloresearch/jan?tab=readme-ov-file#requirements-for-running-jan)
- [Setting up local env](https://github.com/janhq/jan?tab=readme-ov-file#contributing) - [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 # 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`. 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 ## 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. - [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/janhq/projects/5/views/25): view used during daily standup. Sprints should be up to date. - [Standup Kanban](https://github.com/orgs/menloresearch/projects/5/views/25): view used during daily standup. Sprints should be up to date.
## Organization ## 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 - `Roadmap Labels` tag large, long-term, & strategic projects that can span multiple teams and multiple sprints
- Example label: `roadmap: Jan has Mobile` - Example label: `roadmap: Jan has Mobile`
- `Roadmaps` contain `epics` - `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` track large stories that span 1-2 weeks, and it outlines specs, architecture decisions, designs
- `Epics` contain `tasks` - `Epics` contain `tasks`
- `Epics` should always have 1 owner - `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` track release versions. We use [semantic versioning](https://semver.org/)
- `Milestones` span ~2 weeks and have deadlines - `Milestones` span ~2 weeks and have deadlines
- `Milestones` usually fit within 2-week sprint cycles - `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 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, 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 are usually named per [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/#summary)
- Tasks should always have 1 owner - 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 ## 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. 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 ## 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 ## Pre-requisites and Installation

View File

@ -1,9 +1,9 @@
--- ---
title: Homebrew Computer Company title: Menlo Research
description: We are Homebrew Computer Company, the creators and maintainers of Jan, Cortex and other tools. description: We are Menlo Research, the creators and maintainers of Jan, Cortex and other tools.
keywords: keywords:
[ [
Homebrew Computer Company, Menlo Research,
Jan, Jan,
local AI, local AI,
open-source alternative to chatgpt, open-source alternative to chatgpt,
@ -22,7 +22,7 @@ keywords:
import { Callout } from 'nextra/components' import { Callout } from 'nextra/components'
# Homebrew Computer Company # Menlo Research
![Eniac](./_assets/eniac.jpeg) ![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)_ _[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! - More to come!
<Callout> <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> </Callout>
### Mission ### 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 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 ## Inspirations
@ -93,7 +93,7 @@ We are inspired by and actively try to emulate the paths of companies we admire
- [Obsidian](https://obsidian.md/) - [Obsidian](https://obsidian.md/)
- [Discourse](https://www.discourse.org/about) - [Discourse](https://www.discourse.org/about)
- [Gitlab](https://handbook.gitlab.com/handbook/company/history/#2017-gitlab-storytime) - [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/) - [Ghost](https://ghost.org/docs/contributing/)
- [Lago](https://www.getlago.com/blog/open-source-licensing-and-why-lago-chose-agplv3) - [Lago](https://www.getlago.com/blog/open-source-licensing-and-why-lago-chose-agplv3)
- [Twenty](https://twenty.com/story) - [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'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="🌏"> <Callout emoji="🌏">
Ping us in [Discord](https://discord.gg/AAGQNpJQtH) if you're keen to talk to us! 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 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) 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) ![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) ![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. 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!) - Reduced chat font weight (back to normal!)
- Restored the maximize button - 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. 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 - Fixed the GPU memory utilization bar
- Some UX and copy improvements - 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 ### 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. 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 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 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 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 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 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 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