Merge pull request #2682 from janhq/dev

Release cut 0.4.11
This commit is contained in:
Van Pham 2024-04-11 13:16:59 +07:00 committed by GitHub
commit 9e7bdc7f2a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
232 changed files with 4954 additions and 6648 deletions

View File

@ -1,31 +0,0 @@
name: "Clean Cloudflare R2 nightly build artifacts older than 10 days"
on:
schedule:
- cron: "0 0 * * *" # every day at 00:00
workflow_dispatch:
jobs:
clean-cloudflare-r2:
runs-on: ubuntu-latest
environment: production
steps:
- name: install-aws-cli-action
uses: unfor19/install-aws-cli-action@v1
- name: Delete object older than 10 days
run: |
# Get the list of objects in the 'latest' folder
OBJECTS=$(aws s3api list-objects --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --prefix "latest/" --query 'Contents[?LastModified<`'$(date -d "$current_date -10 days" -u +"%Y-%m-%dT%H:%M:%SZ")'`].{Key: Key}' --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com | jq -c .)
# Create a JSON file for the delete operation
echo "{\"Objects\": $OBJECTS, \"Quiet\": false}" > delete.json
# Delete the objects
echo q | aws s3api delete-objects --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --delete file://delete.json --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com
# Remove the JSON file
rm delete.json
env:
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
AWS_EC2_METADATA_DISABLED: "true"

View File

@ -38,17 +38,57 @@ on:
jobs:
test-on-macos:
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || github.event_name == 'push' || github.event_name == 'workflow_dispatch'
runs-on: [self-hosted, macOS, macos-desktop]
steps:
- name: "Cleanup build folder"
run: |
ls -la ./
rm -rf ./* || true
rm -rf ./.??* || true
ls -la ./
rm -rf ~/Library/Application\ Support/jan
- name: Getting the repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Installing node
uses: actions/setup-node@v3
with:
node-version: 20
- name: "Cleanup cache"
continue-on-error: true
run: |
make clean
- name: Get Commit Message for PR
if : github.event_name == 'pull_request'
run: |
echo "REPORT_PORTAL_DESCRIPTION=${{github.event.after}})" >> $GITHUB_ENV
- name: Get Commit Message for push event
if : github.event_name == 'push'
run: |
echo "REPORT_PORTAL_DESCRIPTION=${{github.sha}})" >> $GITHUB_ENV
- name: "Config report portal"
run: |
make update-playwright-config REPORT_PORTAL_URL=${{ secrets.REPORT_PORTAL_URL }} REPORT_PORTAL_API_KEY=${{ secrets.REPORT_PORTAL_API_KEY }} REPORT_PORTAL_PROJECT_NAME=${{ secrets.REPORT_PORTAL_PROJECT_NAME }} REPORT_PORTAL_LAUNCH_NAME="Jan App macos" REPORT_PORTAL_DESCRIPTION="${{env.REPORT_PORTAL_DESCRIPTION}}"
- name: Linter and test
run: |
npm config set registry ${{ secrets.NPM_PROXY }} --global
yarn config set registry ${{ secrets.NPM_PROXY }} --global
make test
env:
CSC_IDENTITY_AUTO_DISCOVERY: "false"
TURBO_API: "${{ secrets.TURBO_API }}"
TURBO_TEAM: "macos"
TURBO_TOKEN: "${{ secrets.TURBO_TOKEN }}"
test-on-macos-pr-target:
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository
runs-on: [self-hosted, macOS, macos-desktop]
steps:
- name: Getting the repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Installing node
uses: actions/setup-node@v3
@ -62,29 +102,24 @@ jobs:
- name: Linter and test
run: |
npm config set registry https://registry.npmjs.org --global
yarn config set registry https://registry.npmjs.org --global
make test
env:
CSC_IDENTITY_AUTO_DISCOVERY: "false"
test-on-windows:
if: github.event_name == 'push'
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
strategy:
fail-fast: false
matrix:
antivirus-tools: ['mcafee', 'default-windows-security','bit-defender']
runs-on: windows-desktop-${{ matrix.antivirus-tools }}
steps:
- name: Clean workspace
run: |
Remove-Item -Path "\\?\$(Get-Location)\*" -Force -Recurse
$path = "$Env:APPDATA\jan"
if (Test-Path $path) {
Remove-Item "\\?\$path" -Recurse -Force
} else {
Write-Output "Folder does not exist."
}
- name: Getting the repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Installing node
uses: actions/setup-node@v3
@ -97,26 +132,79 @@ jobs:
continue-on-error: true
run: |
make clean
- name: Get Commit Message for push event
if : github.event_name == 'push'
shell: bash
run: |
echo "REPORT_PORTAL_DESCRIPTION=${{github.sha}}" >> $GITHUB_ENV
- name: "Config report portal"
shell: bash
run: |
make update-playwright-config REPORT_PORTAL_URL=${{ secrets.REPORT_PORTAL_URL }} REPORT_PORTAL_API_KEY=${{ secrets.REPORT_PORTAL_API_KEY }} REPORT_PORTAL_PROJECT_NAME=${{ secrets.REPORT_PORTAL_PROJECT_NAME }} REPORT_PORTAL_LAUNCH_NAME="Jan App Windows ${{ matrix.antivirus-tools }}" REPORT_PORTAL_DESCRIPTION="${{env.REPORT_PORTAL_DESCRIPTION}}"
- name: Linter and test
shell: powershell
run: |
npm config set registry ${{ secrets.NPM_PROXY }} --global
yarn config set registry ${{ secrets.NPM_PROXY }} --global
make test
env:
TURBO_API: "${{ secrets.TURBO_API }}"
TURBO_TEAM: "windows"
TURBO_TOKEN: "${{ secrets.TURBO_TOKEN }}"
test-on-windows-pr:
if: github.event_name == 'pull_request'
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)
runs-on: windows-desktop-default-windows-security
steps:
- name: Clean workspace
run: |
Remove-Item -Path "\\?\$(Get-Location)\*" -Force -Recurse
$path = "$Env:APPDATA\jan"
if (Test-Path $path) {
Remove-Item "\\?\$path" -Recurse -Force
} else {
Write-Output "Folder does not exist."
}
- name: Getting the repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Installing node
uses: actions/setup-node@v1
with:
node-version: 20
# Clean cache, continue on error
- name: "Cleanup cache"
shell: powershell
continue-on-error: true
run: |
make clean
- name: Get Commit Message for PR
if : github.event_name == 'pull_request'
shell: bash
run: |
echo "REPORT_PORTAL_DESCRIPTION=${{github.event.after}}" >> $GITHUB_ENV
- name: "Config report portal"
shell: bash
run: |
make update-playwright-config REPORT_PORTAL_URL=${{ secrets.REPORT_PORTAL_URL }} REPORT_PORTAL_API_KEY=${{ secrets.REPORT_PORTAL_API_KEY }} REPORT_PORTAL_PROJECT_NAME=${{ secrets.REPORT_PORTAL_PROJECT_NAME }} REPORT_PORTAL_LAUNCH_NAME="Jan App Windows" REPORT_PORTAL_DESCRIPTION="${{env.REPORT_PORTAL_DESCRIPTION}}"
- name: Linter and test
shell: powershell
run: |
npm config set registry ${{ secrets.NPM_PROXY }} --global
yarn config set registry ${{ secrets.NPM_PROXY }} --global
make test
env:
TURBO_API: "${{ secrets.TURBO_API }}"
TURBO_TEAM: "windows"
TURBO_TOKEN: "${{ secrets.TURBO_TOKEN }}"
test-on-windows-pr-target:
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository
runs-on: windows-desktop-default-windows-security
steps:
- name: Getting the repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Installing node
uses: actions/setup-node@v1
@ -133,20 +221,65 @@ jobs:
- name: Linter and test
shell: powershell
run: |
npm config set registry https://registry.npmjs.org --global
yarn config set registry https://registry.npmjs.org --global
make test
test-on-ubuntu:
runs-on: [self-hosted, Linux, ubuntu-desktop]
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || github.event_name == 'push' || github.event_name == 'workflow_dispatch'
steps:
- name: "Cleanup build folder"
run: |
ls -la ./
rm -rf ./* || true
rm -rf ./.??* || true
ls -la ./
rm -rf ~/.config/jan
- name: Getting the repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Installing node
uses: actions/setup-node@v3
with:
node-version: 20
- name: "Cleanup cache"
continue-on-error: true
run: |
make clean
- name: Get Commit Message for PR
if : github.event_name == 'pull_request'
run: |
echo "REPORT_PORTAL_DESCRIPTION=${{github.event.after}}" >> $GITHUB_ENV
- name: Get Commit Message for push event
if : github.event_name == 'push'
run: |
echo "REPORT_PORTAL_DESCRIPTION=${{github.sha}}" >> $GITHUB_ENV
- name: "Config report portal"
shell: bash
run: |
make update-playwright-config REPORT_PORTAL_URL=${{ secrets.REPORT_PORTAL_URL }} REPORT_PORTAL_API_KEY=${{ secrets.REPORT_PORTAL_API_KEY }} REPORT_PORTAL_PROJECT_NAME=${{ secrets.REPORT_PORTAL_PROJECT_NAME }} REPORT_PORTAL_LAUNCH_NAME="Jan App Linux" REPORT_PORTAL_DESCRIPTION="${{env.REPORT_PORTAL_DESCRIPTION}}"
- name: Linter and test
run: |
export DISPLAY=$(w -h | awk 'NR==1 {print $2}')
echo -e "Display ID: $DISPLAY"
npm config set registry ${{ secrets.NPM_PROXY }} --global
yarn config set registry ${{ secrets.NPM_PROXY }} --global
make test
env:
TURBO_API: "${{ secrets.TURBO_API }}"
TURBO_TEAM: "linux"
TURBO_TOKEN: "${{ secrets.TURBO_TOKEN }}"
test-on-ubuntu-pr-target:
runs-on: [self-hosted, Linux, ubuntu-desktop]
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository
steps:
- name: Getting the repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Installing node
uses: actions/setup-node@v3
@ -162,4 +295,6 @@ jobs:
run: |
export DISPLAY=$(w -h | awk 'NR==1 {print $2}')
echo -e "Display ID: $DISPLAY"
make test
npm config set registry https://registry.npmjs.org --global
yarn config set registry https://registry.npmjs.org --global
make test

View File

@ -47,27 +47,11 @@ jobs:
with:
args: |
Jan App ${{ inputs.build_reason }} build artifact version {{ VERSION }}:
- Windows: https://delta.jan.ai/latest/jan-win-x64-{{ VERSION }}.exe
- macOS Intel: https://delta.jan.ai/latest/jan-mac-x64-{{ VERSION }}.dmg
- macOS Apple Silicon: https://delta.jan.ai/latest/jan-mac-arm64-{{ VERSION }}.dmg
- Linux Deb: https://delta.jan.ai/latest/jan-linux-amd64-{{ VERSION }}.deb
- Linux AppImage: https://delta.jan.ai/latest/jan-linux-x86_64-{{ VERSION }}.AppImage
- Windows: https://app.jan.ai/download/nightly/win-x64
- macOS Intel: https://app.jan.ai/download/nightly/mac-x64
- macOS Apple Silicon: https://app.jan.ai/download/nightly/mac-arm64
- Linux Deb: https://app.jan.ai/download/nightly/linux-amd64-deb
- Linux AppImage: https://app.jan.ai/download/nightly/linux-amd64-appimage
- Github action run: https://github.com/janhq/jan/actions/runs/{{ GITHUB_RUN_ID }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
- name: Update README.md with artifact URL
run: |
sed -i "s|<a href='https://delta.jan.ai/latest/jan-win-x64-.*'>|<a href='https://delta.jan.ai/latest/jan-win-x64-${{ inputs.new_version }}.exe'>|" README.md
sed -i "s|<a href='https://delta.jan.ai/latest/jan-mac-x64-.*'>|<a href='https://delta.jan.ai/latest/jan-mac-x64-${{ inputs.new_version }}.dmg'>|" README.md
sed -i "s|<a href='https://delta.jan.ai/latest/jan-mac-arm64-.*'>|<a href='https://delta.jan.ai/latest/jan-mac-arm64-${{ inputs.new_version }}.dmg'>|" README.md
sed -i "s|<a href='https://delta.jan.ai/latest/jan-linux-amd64-.*'>|<a href='https://delta.jan.ai/latest/jan-linux-amd64-${{ inputs.new_version }}.deb'>|" README.md
sed -i "s|<a href='https://delta.jan.ai/latest/jan-linux-x86_64-.*'>|<a href='https://delta.jan.ai/latest/jan-linux-x86_64-${{ inputs.new_version }}.AppImage'>|" README.md
cat README.md
git config --global user.email "service@jan.ai"
git config --global user.name "Service Account"
git add README.md
git commit -m "${GITHUB_REPOSITORY}: Update README.md with nightly build artifact URL"
git -c http.extraheader="AUTHORIZATION: bearer ${{ secrets.PAT_SERVICE_ACCOUNT }}" push origin HEAD:${{ inputs.push_to_branch }}
env:
GITHUB_RUN_ID: ${{ github.run_id }}
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}

View File

@ -1,49 +0,0 @@
name: Update Download URLs
on:
release:
types:
- published
workflow_dispatch:
jobs:
update-readme:
runs-on: ubuntu-latest
environment: production
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: "0"
token: ${{ secrets.PAT_SERVICE_ACCOUNT }}
ref: dev
- name: Get Latest Release
uses: pozetroninc/github-action-get-latest-release@v0.7.0
id: get-latest-release
with:
repository: ${{ github.repository }}
- name: Update Download URLs in README.md
run: |
echo "Latest Release: ${{ steps.get-latest-release.outputs.release }}"
tag=$(/bin/echo -n "${{ steps.get-latest-release.outputs.release }}")
echo "Tag: $tag"
# Remove the v prefix
release=${tag:1}
echo "Release: $release"
sed -i "s|<a href='https://github.com/janhq/jan/releases/download/v.*/jan-win-x64-.*'>|<a href='https://github.com/janhq/jan/releases/download/v${release}/jan-win-x64-${release}.exe'>|" README.md
sed -i "s|<a href='https://github.com/janhq/jan/releases/download/v.*/jan-mac-x64-.*'>|<a href='https://github.com/janhq/jan/releases/download/v${release}/jan-mac-x64-${release}.dmg'>|" README.md
sed -i "s|<a href='https://github.com/janhq/jan/releases/download/v.*/jan-mac-arm64-.*'>|<a href='https://github.com/janhq/jan/releases/download/v${release}/jan-mac-arm64-${release}.dmg'>|" README.md
sed -i "s|<a href='https://github.com/janhq/jan/releases/download/v.*/jan-linux-amd64-.*'>|<a href='https://github.com/janhq/jan/releases/download/v${release}/jan-linux-amd64-${release}.deb'>|" README.md
sed -i "s|<a href='https://github.com/janhq/jan/releases/download/v.*/jan-linux-x86_64-.*'>|<a href='https://github.com/janhq/jan/releases/download/v${release}/jan-linux-x86_64-${release}.AppImage'>|" README.md
- name: Commit and Push changes
if: github.event_name == 'release'
run: |
git config --global user.email "service@jan.ai"
git config --global user.name "Service Account"
git add README.md
git commit -m "Update README.md with Stable Download URLs"
git -c http.extraheader="AUTHORIZATION: bearer ${{ secrets.PAT_SERVICE_ACCOUNT }}" push origin HEAD:dev

4
.gitignore vendored
View File

@ -1,5 +1,7 @@
.vscode
.idea
.env
.idea
# Jan inference
error.log
@ -35,4 +37,4 @@ extensions/*-extension/bin/vulkaninfo
# Turborepo
.turbo
.turbo

View File

@ -41,7 +41,6 @@ COPY --from=builder /app/pre-install ./pre-install/
# Copy the package.json, yarn.lock, and output of web yarn space to leverage Docker cache
COPY --from=builder /app/uikit ./uikit/
COPY --from=builder /app/web ./web/
COPY --from=builder /app/models ./models/
RUN yarn workspace @janhq/uikit install && yarn workspace @janhq/uikit build
RUN yarn workspace @janhq/web install

View File

@ -65,7 +65,6 @@ COPY --from=builder /app/pre-install ./pre-install/
# Copy the package.json, yarn.lock, and output of web yarn space to leverage Docker cache
COPY --from=builder /app/uikit ./uikit/
COPY --from=builder /app/web ./web/
COPY --from=builder /app/models ./models/
RUN yarn workspace @janhq/uikit install && yarn workspace @janhq/uikit build
RUN yarn workspace @janhq/web install

View File

@ -1,5 +1,11 @@
# Makefile for Jan Electron App - Build, Lint, Test, and Clean
REPORT_PORTAL_URL ?= ""
REPORT_PORTAL_API_KEY ?= ""
REPORT_PORTAL_PROJECT_NAME ?= ""
REPORT_PORTAL_LAUNCH_NAME ?= "Jan App"
REPORT_PORTAL_DESCRIPTION ?= "Jan App report"
# Default target, does nothing
all:
@echo "Specify a target to run"
@ -37,6 +43,64 @@ dev: check-file-counts
lint: check-file-counts
yarn lint
update-playwright-config:
ifeq ($(OS),Windows_NT)
echo -e "const RPconfig = {\n\
apiKey: '$(REPORT_PORTAL_API_KEY)',\n\
endpoint: '$(REPORT_PORTAL_URL)',\n\
project: '$(REPORT_PORTAL_PROJECT_NAME)',\n\
launch: '$(REPORT_PORTAL_LAUNCH_NAME)',\n\
attributes: [\n\
{\n\
key: 'key',\n\
value: 'value',\n\
},\n\
{\n\
value: 'value',\n\
},\n\
],\n\
description: '$(REPORT_PORTAL_DESCRIPTION)',\n\
}\n$$(cat electron/playwright.config.ts)" > electron/playwright.config.ts;
sed -i "s/^ reporter: .*/ reporter: [['@reportportal\/agent-js-playwright', RPconfig]],/" electron/playwright.config.ts
else ifeq ($(shell uname -s),Linux)
echo "const RPconfig = {\n\
apiKey: '$(REPORT_PORTAL_API_KEY)',\n\
endpoint: '$(REPORT_PORTAL_URL)',\n\
project: '$(REPORT_PORTAL_PROJECT_NAME)',\n\
launch: '$(REPORT_PORTAL_LAUNCH_NAME)',\n\
attributes: [\n\
{\n\
key: 'key',\n\
value: 'value',\n\
},\n\
{\n\
value: 'value',\n\
},\n\
],\n\
description: '$(REPORT_PORTAL_DESCRIPTION)',\n\
}\n$$(cat electron/playwright.config.ts)" > electron/playwright.config.ts;
sed -i "s/^ reporter: .*/ reporter: [['@reportportal\/agent-js-playwright', RPconfig]],/" electron/playwright.config.ts
else
echo "const RPconfig = {\n\
apiKey: '$(REPORT_PORTAL_API_KEY)',\n\
endpoint: '$(REPORT_PORTAL_URL)',\n\
project: '$(REPORT_PORTAL_PROJECT_NAME)',\n\
launch: '$(REPORT_PORTAL_LAUNCH_NAME)',\n\
attributes: [\n\
{\n\
key: 'key',\n\
value: 'value',\n\
},\n\
{\n\
value: 'value',\n\
},\n\
],\n\
description: '$(REPORT_PORTAL_DESCRIPTION)',\n\
}\n$$(cat electron/playwright.config.ts)" > electron/playwright.config.ts;
sed -i '' "s|^ reporter: .*| reporter: [['@reportportal\/agent-js-playwright', RPconfig]],|" electron/playwright.config.ts
endif
# Testing
test: lint
yarn build:test
@ -53,19 +117,24 @@ build: check-file-counts
clean:
ifeq ($(OS),Windows_NT)
powershell -Command "Get-ChildItem -Path . -Include node_modules, .next, dist, build, out -Recurse -Directory | Remove-Item -Recurse -Force"
powershell -Command "Get-ChildItem -Path . -Include package-lock.json -Recurse -File | Remove-Item -Recurse -Force"
powershell -Command "Remove-Item -Recurse -Force ./pre-install/*.tgz"
powershell -Command "Remove-Item -Recurse -Force ./electron/pre-install/*.tgz"
powershell -Command "if (Test-Path \"$($env:USERPROFILE)\jan\extensions\") { Remove-Item -Path \"$($env:USERPROFILE)\jan\extensions\" -Recurse -Force }"
-powershell -Command "Get-ChildItem -Path . -Include node_modules, .next, dist, build, out, .turbo -Recurse -Directory | Remove-Item -Recurse -Force"
-powershell -Command "Get-ChildItem -Path . -Include package-lock.json -Recurse -File | Remove-Item -Recurse -Force"
-powershell -Command "Get-ChildItem -Path . -Include yarn.lock -Recurse -File | Remove-Item -Recurse -Force"
-powershell -Command "Remove-Item -Recurse -Force ./pre-install/*.tgz"
-powershell -Command "Remove-Item -Recurse -Force ./extensions/*/*.tgz"
-powershell -Command "Remove-Item -Recurse -Force ./electron/pre-install/*.tgz"
-powershell -Command "if (Test-Path \"$($env:USERPROFILE)\jan\extensions\") { Remove-Item -Path \"$($env:USERPROFILE)\jan\extensions\" -Recurse -Force }"
else ifeq ($(shell uname -s),Linux)
find . -name "node_modules" -type d -prune -exec rm -rf '{}' +
find . -name ".next" -type d -exec rm -rf '{}' +
find . -name "dist" -type d -exec rm -rf '{}' +
find . -name "build" -type d -exec rm -rf '{}' +
find . -name "out" -type d -exec rm -rf '{}' +
find . -name ".turbo" -type d -exec rm -rf '{}' +
find . -name "packake-lock.json" -type f -exec rm -rf '{}' +
find . -name "yarn.lock" -type f -exec rm -rf '{}' +
rm -rf ./pre-install/*.tgz
rm -rf ./extensions/*/*.tgz
rm -rf ./electron/pre-install/*.tgz
rm -rf "~/jan/extensions"
rm -rf "~/.cache/jan*"
@ -75,8 +144,11 @@ else
find . -name "dist" -type d -exec rm -rf '{}' +
find . -name "build" -type d -exec rm -rf '{}' +
find . -name "out" -type d -exec rm -rf '{}' +
find . -name ".turbo" -type d -exec rm -rf '{}' +
find . -name "packake-lock.json" -type f -exec rm -rf '{}' +
find . -name "yarn.lock" -type f -exec rm -rf '{}' +
rm -rf ./pre-install/*.tgz
rm -rf ./extensions/*/*.tgz
rm -rf ./electron/pre-install/*.tgz
rm -rf ~/jan/extensions
rm -rf ~/Library/Caches/jan*

View File

@ -43,32 +43,32 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute
<tr style="text-align:center">
<td style="text-align:center"><b>Stable (Recommended)</b></td>
<td style="text-align:center">
<a href='https://github.com/janhq/jan/releases/download/v0.4.9/jan-win-x64-0.4.9.exe'>
<img src='./docs/static/img/windows.png' style="height:14px; width: 14px" />
<a href='https://app.jan.ai/download/latest/win-x64'>
<img src='https://github.com/janhq/docs/blob/main/static/img/windows.png' style="height:14px; width: 14px" />
<b>jan.exe</b>
</a>
</td>
<td style="text-align:center">
<a href='https://github.com/janhq/jan/releases/download/v0.4.9/jan-mac-x64-0.4.9.dmg'>
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
<a href='https://app.jan.ai/download/latest/mac-x64'>
<img src='https://github.com/janhq/docs/blob/main/static/img/mac.png' style="height:15px; width: 15px" />
<b>Intel</b>
</a>
</td>
<td style="text-align:center">
<a href='https://github.com/janhq/jan/releases/download/v0.4.9/jan-mac-arm64-0.4.9.dmg'>
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
<a href='https://app.jan.ai/download/latest/mac-arm64'>
<img src='https://github.com/janhq/docs/blob/main/static/img/mac.png' style="height:15px; width: 15px" />
<b>M1/M2</b>
</a>
</td>
<td style="text-align:center">
<a href='https://github.com/janhq/jan/releases/download/v0.4.9/jan-linux-amd64-0.4.9.deb'>
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
<a href='https://app.jan.ai/download/latest/linux-amd64-deb'>
<img src='https://github.com/janhq/docs/blob/main/static/img/linux.png' style="height:14px; width: 14px" />
<b>jan.deb</b>
</a>
</td>
<td style="text-align:center">
<a href='https://github.com/janhq/jan/releases/download/v0.4.9/jan-linux-x86_64-0.4.9.AppImage'>
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
<a href='https://app.jan.ai/download/latest/linux-amd64-appimage'>
<img src='https://github.com/janhq/docs/blob/main/static/img/linux.png' style="height:14px; width: 14px" />
<b>jan.AppImage</b>
</a>
</td>
@ -76,32 +76,32 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute
<tr style="text-align:center">
<td style="text-align:center"><b>Experimental (Nightly Build)</b></td>
<td style="text-align:center">
<a href='https://delta.jan.ai/latest/jan-win-x64-0.4.9-345.exe'>
<img src='./docs/static/img/windows.png' style="height:14px; width: 14px" />
<a href='https://app.jan.ai/download/nightly/win-x64'>
<img src='https://github.com/janhq/docs/blob/main/static/img/windows.png' style="height:14px; width: 14px" />
<b>jan.exe</b>
</a>
</td>
<td style="text-align:center">
<a href='https://delta.jan.ai/latest/jan-mac-x64-0.4.9-345.dmg'>
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
<a href='https://app.jan.ai/download/nightly/mac-x64'>
<img src='https://github.com/janhq/docs/blob/main/static/img/mac.png' style="height:15px; width: 15px" />
<b>Intel</b>
</a>
</td>
<td style="text-align:center">
<a href='https://delta.jan.ai/latest/jan-mac-arm64-0.4.9-345.dmg'>
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
<a href='https://app.jan.ai/download/nightly/mac-arm64'>
<img src='https://github.com/janhq/docs/blob/main/static/img/mac.png' style="height:15px; width: 15px" />
<b>M1/M2</b>
</a>
</td>
<td style="text-align:center">
<a href='https://delta.jan.ai/latest/jan-linux-amd64-0.4.9-345.deb'>
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
<a href='https://app.jan.ai/download/nightly/linux-amd64-deb'>
<img src='https://github.com/janhq/docs/blob/main/static/img/linux.png' style="height:14px; width: 14px" />
<b>jan.deb</b>
</a>
</td>
<td style="text-align:center">
<a href='https://delta.jan.ai/latest/jan-linux-x86_64-0.4.9-345.AppImage'>
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
<a href='https://app.jan.ai/download/nightly/linux-amd64-appimage'>
<img src='https://github.com/janhq/docs/blob/main/static/img/linux.png' style="height:14px; width: 14px" />
<b>jan.AppImage</b>
</a>
</td>
@ -240,6 +240,7 @@ This will build the app MacOS m1/m2 for production (with code signing already do
- If you intend to run Jan in GPU mode, you need to install `nvidia-driver` and `nvidia-docker2`. Follow the instruction [here](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) for installation.
- Run Jan in Docker mode
> User can choose between `docker-compose.yml` with latest prebuilt docker image or `docker-compose-dev.yml` with local docker build
| Docker compose Profile | Description |
| ---------------------- | -------------------------------------------- |
@ -336,6 +337,15 @@ Jan builds on top of other open-source projects:
- For business inquiries: email hello@jan.ai
- For jobs: please email hr@jan.ai
## Trust & Safety
Beware of scams.
- We will never ask you for personal info
- We are a free product; there's no paid version
- We don't have a token or ICO
- We are not actively fundraising or seeking donations
## License
Jan is free and open source, under the AGPLv3 license.

View File

@ -30,6 +30,7 @@ export default [
// which external modules to include in the bundle
// https://github.com/rollup/rollup-plugin-node-resolve#usage
replace({
'preventAssignment': true,
'node:crypto': 'crypto',
'delimiters': ['"', '"'],
}),

View File

@ -1,3 +1,7 @@
import { SettingComponentProps } from '../types'
import { getJanDataFolderPath, joinPath } from './core'
import { fs } from './fs'
export enum ExtensionTypeEnum {
Assistant = 'assistant',
Conversational = 'conversational',
@ -19,9 +23,9 @@ export interface Compatibility {
const ALL_INSTALLATION_STATE = [
'NotRequired', // not required.
'Installed', // require and installed. Good to go.
'Updatable', // require and installed but need to be updated.
'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
@ -32,6 +36,43 @@ export type InstallationState = InstallationStateTuple[number]
* This class should be extended by any class that represents an extension.
*/
export abstract class BaseExtension implements ExtensionType {
protected settingFolderName = 'settings'
protected settingFileName = 'settings.json'
/** @type {string} Name of the extension. */
name: string
/** @type {string} Product Name of the extension. */
productName?: string
/** @type {string} The URL of the extension to load. */
url: string
/** @type {boolean} Whether the extension is activated or not. */
active
/** @type {string} Extension's description. */
description
/** @type {string} Extension's version. */
version
constructor(
url: string,
name: string,
productName?: string,
active?: boolean,
description?: string,
version?: string
) {
this.name = name
this.productName = productName
this.url = url
this.active = active
this.description = description
this.version = version
}
/**
* Returns the type of the extension.
* @returns {ExtensionType} The type of the extension
@ -40,11 +81,13 @@ export abstract class BaseExtension implements ExtensionType {
type(): ExtensionTypeEnum | undefined {
return undefined
}
/**
* Called when the extension is loaded.
* Any initialization logic for the extension should be put here.
*/
abstract onLoad(): void
/**
* Called when the extension is unloaded.
* Any cleanup logic for the extension should be put here.
@ -60,11 +103,40 @@ export abstract class BaseExtension implements ExtensionType {
return undefined
}
/**
* Determine if the extension is updatable.
*/
updatable(): boolean {
return false
async registerSettings(settings: SettingComponentProps[]): Promise<void> {
if (!this.name) {
console.error('Extension name is not defined')
return
}
const extensionSettingFolderPath = await joinPath([
await getJanDataFolderPath(),
'settings',
this.name,
])
settings.forEach((setting) => {
setting.extensionName = this.name
})
try {
await fs.mkdir(extensionSettingFolderPath)
const settingFilePath = await joinPath([extensionSettingFolderPath, this.settingFileName])
if (await fs.existsSync(settingFilePath)) return
await fs.writeFileSync(settingFilePath, JSON.stringify(settings, null, 2))
} catch (err) {
console.error(err)
}
}
async getSetting<T>(key: string, defaultValue: T) {
const keySetting = (await this.getSettings()).find((setting) => setting.key === key)
const value = keySetting?.controllerProps.value
return (value as T) ?? defaultValue
}
onSettingUpdate<T>(key: string, value: T) {
return
}
/**
@ -81,8 +153,59 @@ export abstract class BaseExtension implements ExtensionType {
*
* @returns {Promise<void>}
*/
// @ts-ignore
async install(...args): Promise<void> {
async install(): Promise<void> {
return
}
async getSettings(): Promise<SettingComponentProps[]> {
if (!this.name) return []
const settingPath = await joinPath([
await getJanDataFolderPath(),
this.settingFolderName,
this.name,
this.settingFileName,
])
try {
const content = await fs.readFileSync(settingPath, 'utf-8')
const settings: SettingComponentProps[] = JSON.parse(content)
return settings
} catch (err) {
console.warn(err)
return []
}
}
async updateSettings(componentProps: Partial<SettingComponentProps>[]): Promise<void> {
if (!this.name) return
const settings = await this.getSettings()
const updatedSettings = settings.map((setting) => {
const updatedSetting = componentProps.find(
(componentProp) => componentProp.key === setting.key
)
if (updatedSetting && updatedSetting.controllerProps) {
setting.controllerProps.value = updatedSetting.controllerProps.value
}
return setting
})
const settingPath = await joinPath([
await getJanDataFolderPath(),
this.settingFolderName,
this.name,
this.settingFileName,
])
await fs.writeFileSync(settingPath, JSON.stringify(updatedSettings, null, 2))
updatedSettings.forEach((setting) => {
this.onSettingUpdate<typeof setting.controllerProps.value>(
setting.key,
setting.controllerProps.value
)
})
}
}

View File

@ -10,6 +10,8 @@ import { EngineManager } from './EngineManager'
* Applicable to all AI Engines
*/
export abstract class AIEngine extends BaseExtension {
private static modelsFolder = 'models'
// The inference engine
abstract provider: string
@ -21,15 +23,6 @@ export abstract class AIEngine extends BaseExtension {
events.on(ModelEvent.OnModelInit, (model: Model) => this.loadModel(model))
events.on(ModelEvent.OnModelStop, (model: Model) => this.unloadModel(model))
this.prePopulateModels()
}
/**
* Defines models
*/
models(): Promise<Model[]> {
return Promise.resolve([])
}
/**
@ -39,6 +32,49 @@ export abstract class AIEngine extends BaseExtension {
EngineManager.instance().register(this)
}
async registerModels(models: Model[]): Promise<void> {
const modelFolderPath = await joinPath([await getJanDataFolderPath(), AIEngine.modelsFolder])
let shouldNotifyModelUpdate = false
for (const model of models) {
const modelPath = await joinPath([modelFolderPath, model.id])
const isExist = await fs.existsSync(modelPath)
if (isExist) {
await this.migrateModelIfNeeded(model, modelPath)
continue
}
await fs.mkdir(modelPath)
await fs.writeFileSync(
await joinPath([modelPath, 'model.json']),
JSON.stringify(model, null, 2)
)
shouldNotifyModelUpdate = true
}
if (shouldNotifyModelUpdate) {
events.emit(ModelEvent.OnModelsUpdate, {})
}
}
async migrateModelIfNeeded(model: Model, modelPath: string): Promise<void> {
try {
const modelJson = await fs.readFileSync(await joinPath([modelPath, 'model.json']), 'utf-8')
const currentModel: Model = JSON.parse(modelJson)
if (currentModel.version !== model.version) {
await fs.writeFileSync(
await joinPath([modelPath, 'model.json']),
JSON.stringify(model, null, 2)
)
events.emit(ModelEvent.OnModelsUpdate, {})
}
} catch (error) {
console.warn('Error while try to migrating model', error)
}
}
/**
* Loads the model.
*/
@ -65,40 +101,4 @@ export abstract class AIEngine extends BaseExtension {
* Stop inference
*/
stopInference() {}
/**
* Pre-populate models to App Data Folder
*/
prePopulateModels(): Promise<void> {
const modelFolder = 'models'
return this.models().then((models) => {
const prePoluateOperations = models.map((model) =>
getJanDataFolderPath()
.then((janDataFolder) =>
// Attempt to create the model folder
joinPath([janDataFolder, modelFolder, model.id]).then((path) =>
fs
.mkdir(path)
.catch()
.then(() => path)
)
)
.then((path) => joinPath([path, 'model.json']))
.then((path) => {
// Do not overwite existing model.json
return fs.existsSync(path).then((exist: any) => {
if (!exist) return fs.writeFileSync(path, JSON.stringify(model, null, 2))
})
})
.catch((e: Error) => {
console.error('Error', e)
})
)
Promise.all(prePoluateOperations).then(() =>
// Emit event to update models
// So the UI can update the models list
events.emit(ModelEvent.OnModelsUpdate, {})
)
})
}
}

View File

@ -48,7 +48,7 @@ export abstract class OAIEngine extends AIEngine {
/*
* Inference request
*/
override inference(data: MessageRequest) {
override async inference(data: MessageRequest) {
if (data.model?.engine?.toString() !== this.provider) return
const timestamp = Date.now()
@ -77,12 +77,14 @@ export abstract class OAIEngine extends AIEngine {
...data.model,
}
const header = await this.headers()
requestInference(
this.inferenceUrl,
data.messages ?? [],
model,
this.controller,
this.headers()
header
).subscribe({
next: (content: any) => {
const messageContent: ThreadContent = {
@ -100,7 +102,9 @@ export abstract class OAIEngine extends AIEngine {
events.emit(MessageEvent.OnMessageUpdate, message)
},
error: async (err: any) => {
console.error(`Inference error: ${JSON.stringify(err, null, 2)}`)
console.debug('inference url: ', this.inferenceUrl)
console.debug('header: ', header)
console.error(`Inference error:`, JSON.stringify(err))
if (this.isCancelled || message.content.length) {
message.status = MessageStatus.Stopped
events.emit(MessageEvent.OnMessageUpdate, message)
@ -131,7 +135,7 @@ export abstract class OAIEngine extends AIEngine {
/**
* Headers for the inference request
*/
headers(): HeadersInit {
async headers(): Promise<HeadersInit> {
return {}
}
}

View File

@ -5,8 +5,7 @@ import { OAIEngine } from './OAIEngine'
* Added the implementation of loading and unloading model (applicable to local inference providers)
*/
export abstract class RemoteOAIEngine extends OAIEngine {
// The inference engine
abstract apiKey: string
apiKey?: string
/**
* On extension load, subscribe to events.
*/
@ -17,10 +16,12 @@ export abstract class RemoteOAIEngine extends OAIEngine {
/**
* Headers for the inference request
*/
override headers(): HeadersInit {
override async headers(): Promise<HeadersInit> {
return {
'Authorization': `Bearer ${this.apiKey}`,
'api-key': `${this.apiKey}`,
...(this.apiKey && {
'Authorization': `Bearer ${this.apiKey}`,
'api-key': `${this.apiKey}`,
}),
}
}
}

View File

@ -36,9 +36,15 @@ export function requestInference(
.then(async (response) => {
if (!response.ok) {
const data = await response.json()
let errorCode = ErrorCode.Unknown;
if (data.error) {
errorCode = data.error.code ?? data.error.type ?? ErrorCode.Unknown
} else if (response.status === 401) {
errorCode = ErrorCode.InvalidApiKey;
}
const error = {
message: data.error?.message ?? 'Error occurred.',
code: data.error?.code ?? ErrorCode.Unknown,
code: errorCode,
}
subscriber.error(error)
subscriber.complete()
@ -60,14 +66,20 @@ export function requestInference(
}
const text = decoder.decode(value)
const lines = text.trim().split('\n')
let cachedLines = ''
for (const line of lines) {
if (line.startsWith('data: ') && !line.includes('data: [DONE]')) {
const data = JSON.parse(line.replace('data: ', ''))
content += data.choices[0]?.delta?.content ?? ''
if (content.startsWith('assistant: ')) {
content = content.replace('assistant: ', '')
try {
const toParse = cachedLines + line
if (!line.includes('data: [DONE]')) {
const data = JSON.parse(toParse.replace('data: ', ''))
content += data.choices[0]?.delta?.content ?? ''
if (content.startsWith('assistant: ')) {
content = content.replace('assistant: ', '')
}
if (content !== '') subscriber.next(content)
}
subscriber.next(content)
} catch {
cachedLines = line
}
}
}

View File

@ -13,7 +13,7 @@ export abstract class MonitoringExtension extends BaseExtension implements Monit
return ExtensionTypeEnum.SystemMonitoring
}
abstract getGpuSetting(): Promise<GpuSetting>
abstract getGpuSetting(): Promise<GpuSetting | undefined>
abstract getResourcesInfo(): Promise<any>
abstract getCurrentLoad(): Promise<any>
abstract getOsInfo(): Promise<OperatingSystemInfo>

View File

@ -5,19 +5,16 @@ export type Handler = (route: string, args: any) => any
export class RequestHandler {
handler: Handler
adataper: RequestAdapter
adapter: RequestAdapter
constructor(handler: Handler, observer?: Function) {
this.handler = handler
this.adataper = new RequestAdapter(observer)
this.adapter = new RequestAdapter(observer)
}
handle() {
CoreRoutes.map((route) => {
this.handler(route, async (...args: any[]) => {
const values = await this.adataper.process(route, ...args)
return values
})
this.handler(route, async (...args: any[]) => this.adapter.process(route, ...args))
})
}
}

View File

@ -1,9 +1,12 @@
import { basename, isAbsolute, join, relative } from 'path'
import { Processor } from './Processor'
import { getAppConfigurations as appConfiguration, updateAppConfiguration } from '../../helper'
import { log as writeLog, logServer as writeServerLog } from '../../helper/log'
import { appResourcePath } from '../../helper/path'
import {
log as writeLog,
appResourcePath,
getAppConfigurations as appConfiguration,
updateAppConfiguration,
} from '../../helper'
export class App implements Processor {
observer?: Function
@ -56,13 +59,6 @@ export class App implements Processor {
writeLog(args)
}
/**
* Log message to log file.
*/
logServer(args: any) {
writeServerLog(args)
}
getAppConfigurations() {
return appConfiguration()
}
@ -83,6 +79,7 @@ export class App implements Processor {
isVerboseEnabled: args?.isVerboseEnabled,
schemaPath: join(await appResourcePath(), 'docs', 'openapi', 'jan.yaml'),
baseDir: join(await appResourcePath(), 'docs', 'openapi'),
prefix: args?.prefix,
})
}

View File

@ -316,6 +316,7 @@ export const chatCompletions = async (request: any, reply: any) => {
}
const requestedModel = matchedModels[0]
const engineConfiguration = await getEngineConfiguration(requestedModel.engine)
let apiKey: string | undefined = undefined
@ -323,7 +324,7 @@ export const chatCompletions = async (request: any, reply: any) => {
if (engineConfiguration) {
apiKey = engineConfiguration.api_key
apiUrl = engineConfiguration.full_url
apiUrl = engineConfiguration.full_url ?? DEFAULT_CHAT_COMPLETION_URL
}
const headers: Record<string, any> = {
@ -334,7 +335,6 @@ export const chatCompletions = async (request: any, reply: any) => {
headers['Authorization'] = `Bearer ${apiKey}`
headers['api-key'] = apiKey
}
console.debug(apiUrl)
if (requestedModel.engine === 'openai' && request.body.stop) {
// openai only allows max 4 stop words
@ -352,7 +352,7 @@ export const chatCompletions = async (request: any, reply: any) => {
reply.code(400).send(response)
} else {
reply.raw.writeHead(200, {
'Content-Type': 'text/event-stream',
'Content-Type': request.body.stream === true ? 'text/event-stream' : 'application/json',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',

View File

@ -1,7 +1,11 @@
import fs from 'fs'
import { join } from 'path'
import { getJanDataFolderPath, getJanExtensionsPath, getSystemResourceInfo } from '../../../helper'
import { logServer } from '../../../helper/log'
import {
getJanDataFolderPath,
getJanExtensionsPath,
getSystemResourceInfo,
log,
} from '../../../helper'
import { ChildProcessWithoutNullStreams, spawn } from 'child_process'
import { Model, ModelSettingParams, PromptTemplate } from '../../../../types'
import {
@ -69,7 +73,7 @@ const runModel = async (modelId: string, settingParams?: ModelSettingParams): Pr
}),
}
logServer(`[NITRO]::Debug: Nitro model settings: ${JSON.stringify(nitroModelSettings)}`)
log(`[SERVER]::Debug: Nitro model settings: ${JSON.stringify(nitroModelSettings)}`)
// Convert settings.prompt_template to system_prompt, user_prompt, ai_prompt
if (modelMetadata.settings.prompt_template) {
@ -140,7 +144,7 @@ const runNitroAndLoadModel = async (modelId: string, modelSettings: NitroModelSe
}
const spawnNitroProcess = async (): Promise<void> => {
logServer(`[NITRO]::Debug: Spawning Nitro subprocess...`)
log(`[SERVER]::Debug: Spawning Nitro subprocess...`)
let binaryFolder = join(
getJanExtensionsPath(),
@ -155,8 +159,8 @@ const spawnNitroProcess = async (): Promise<void> => {
const args: string[] = ['1', LOCAL_HOST, NITRO_DEFAULT_PORT.toString()]
// Execute the binary
logServer(
`[NITRO]::Debug: Spawn nitro at path: ${executableOptions.executablePath}, and args: ${args}`
log(
`[SERVER]::Debug: Spawn nitro at path: ${executableOptions.executablePath}, and args: ${args}`
)
subprocess = spawn(
executableOptions.executablePath,
@ -172,20 +176,20 @@ const spawnNitroProcess = async (): Promise<void> => {
// Handle subprocess output
subprocess.stdout.on('data', (data: any) => {
logServer(`[NITRO]::Debug: ${data}`)
log(`[SERVER]::Debug: ${data}`)
})
subprocess.stderr.on('data', (data: any) => {
logServer(`[NITRO]::Error: ${data}`)
log(`[SERVER]::Error: ${data}`)
})
subprocess.on('close', (code: any) => {
logServer(`[NITRO]::Debug: Nitro exited with code: ${code}`)
log(`[SERVER]::Debug: Nitro exited with code: ${code}`)
subprocess = undefined
})
tcpPortUsed.waitUntilUsed(NITRO_DEFAULT_PORT, 300, 30000).then(() => {
logServer(`[NITRO]::Debug: Nitro is ready`)
log(`[SERVER]::Debug: Nitro is ready`)
})
}
@ -227,13 +231,9 @@ const executableNitroFile = (): NitroExecutableOptions => {
binaryName = 'nitro.exe'
} else if (process.platform === 'darwin') {
/**
* For MacOS: mac-arm64 (Silicon), mac-x64 (InteL)
* For MacOS: mac-universal both Silicon and InteL
*/
if (process.arch === 'arm64') {
binaryFolder = join(binaryFolder, 'mac-arm64')
} else {
binaryFolder = join(binaryFolder, 'mac-x64')
}
binaryFolder = join(binaryFolder, 'mac-universal')
} else {
/**
* For Linux: linux-cpu, linux-cuda-11-7, linux-cuda-12-0
@ -271,7 +271,7 @@ const validateModelStatus = async (): Promise<void> => {
retries: 5,
retryDelay: 500,
}).then(async (res: Response) => {
logServer(`[NITRO]::Debug: Validate model state success with response ${JSON.stringify(res)}`)
log(`[SERVER]::Debug: Validate model state success with response ${JSON.stringify(res)}`)
// If the response is OK, check model_loaded status.
if (res.ok) {
const body = await res.json()
@ -286,7 +286,7 @@ const validateModelStatus = async (): Promise<void> => {
}
const loadLLMModel = async (settings: NitroModelSettings): Promise<Response> => {
logServer(`[NITRO]::Debug: Loading model with params ${JSON.stringify(settings)}`)
log(`[SERVER]::Debug: Loading model with params ${JSON.stringify(settings)}`)
const fetchRT = require('fetch-retry')
const fetchRetry = fetchRT(fetch)
@ -300,11 +300,11 @@ const loadLLMModel = async (settings: NitroModelSettings): Promise<Response> =>
retryDelay: 500,
})
.then((res: any) => {
logServer(`[NITRO]::Debug: Load model success with response ${JSON.stringify(res)}`)
log(`[SERVER]::Debug: Load model success with response ${JSON.stringify(res)}`)
return Promise.resolve(res)
})
.catch((err: any) => {
logServer(`[NITRO]::Error: Load model failed with error ${err}`)
log(`[SERVER]::Error: Load model failed with error ${err}`)
return Promise.reject(err)
})
}
@ -327,7 +327,7 @@ export const stopModel = async (_modelId: string) => {
})
}, 5000)
const tcpPortUsed = require('tcp-port-used')
logServer(`[NITRO]::Debug: Request to kill Nitro`)
log(`[SERVER]::Debug: Request to kill Nitro`)
fetch(NITRO_HTTP_KILL_URL, {
method: 'DELETE',
@ -341,7 +341,7 @@ export const stopModel = async (_modelId: string) => {
// don't need to do anything, we still kill the subprocess
})
.then(() => tcpPortUsed.waitUntilFree(NITRO_DEFAULT_PORT, 300, 5000))
.then(() => logServer(`[NITRO]::Debug: Nitro process is terminated`))
.then(() => log(`[SERVER]::Debug: Nitro process is terminated`))
.then(() =>
resolve({
message: 'Model stopped',

View File

@ -11,6 +11,7 @@ export default class Extension {
* @property {string} origin Original specification provided to fetch the package.
* @property {Object} installOptions Options provided to pacote when fetching the manifest.
* @property {name} name The name of the extension as defined in the manifest.
* @property {name} productName The display name of the extension as defined in the manifest.
* @property {string} url Electron URL where the package can be accessed.
* @property {string} version Version of the package as defined in the manifest.
* @property {string} main The entry point as defined in the main entry of the manifest.
@ -19,6 +20,7 @@ export default class Extension {
origin?: string
installOptions: any
name?: string
productName?: string
url?: string
version?: string
main?: string
@ -42,7 +44,7 @@ export default class Extension {
const Arborist = require('@npmcli/arborist')
const defaultOpts = {
version: false,
fullMetadata: false,
fullMetadata: true,
Arborist,
}
@ -77,6 +79,7 @@ export default class Extension {
return pacote.manifest(this.specifier, this.installOptions).then((mnf) => {
// set the Package properties based on the it's manifest
this.name = mnf.name
this.productName = mnf.productName as string | undefined
this.version = mnf.version
this.main = mnf.main
this.description = mnf.description

View File

@ -1,4 +1,4 @@
import { AppConfiguration } from '../../types'
import { AppConfiguration, SettingComponentProps } from '../../types'
import { join } from 'path'
import fs from 'fs'
import os from 'os'
@ -125,40 +125,32 @@ const exec = async (command: string): Promise<string> => {
})
}
// a hacky way to get the api key. we should comes up with a better
// way to handle this
export const getEngineConfiguration = async (engineId: string) => {
if (engineId !== 'openai' && engineId !== 'groq') {
return undefined
}
const directoryPath = join(getJanDataFolderPath(), 'engines')
const filePath = join(directoryPath, `${engineId}.json`)
const data = fs.readFileSync(filePath, 'utf-8')
return JSON.parse(data)
}
if (engineId !== 'openai' && engineId !== 'groq') return undefined
/**
* Utility function to get server log path
*
* @returns {string} The log path.
*/
export const getServerLogPath = (): string => {
const appConfigurations = getAppConfigurations()
const logFolderPath = join(appConfigurations.data_folder, 'logs')
if (!fs.existsSync(logFolderPath)) {
fs.mkdirSync(logFolderPath, { recursive: true })
}
return join(logFolderPath, 'server.log')
}
const settingDirectoryPath = join(
getJanDataFolderPath(),
'settings',
'@janhq',
engineId === 'openai' ? 'inference-openai-extension' : 'inference-groq-extension',
'settings.json'
)
/**
* Utility function to get app log path
*
* @returns {string} The log path.
*/
export const getAppLogPath = (): string => {
const appConfigurations = getAppConfigurations()
const logFolderPath = join(appConfigurations.data_folder, 'logs')
if (!fs.existsSync(logFolderPath)) {
fs.mkdirSync(logFolderPath, { recursive: true })
const content = fs.readFileSync(settingDirectoryPath, 'utf-8')
const settings: SettingComponentProps[] = JSON.parse(content)
const apiKeyId = engineId === 'openai' ? 'openai-api-key' : 'groq-api-key'
const keySetting = settings.find((setting) => setting.key === apiKeyId)
let fullUrl = settings.find((setting) => setting.key === 'chat-completions-endpoint')
?.controllerProps.value
let apiKey = keySetting?.controllerProps.value
if (typeof apiKey !== 'string') apiKey = ''
if (typeof fullUrl !== 'string') fullUrl = ''
return {
api_key: apiKey,
full_url: fullUrl,
}
return join(logFolderPath, 'app.log')
}

View File

@ -1,6 +1,6 @@
export * from './config'
export * from './download'
export * from './log'
export * from './logger'
export * from './module'
export * from './path'
export * from './resource'

View File

@ -1,37 +0,0 @@
import fs from 'fs'
import util from 'util'
import { getAppLogPath, getServerLogPath } from './config'
export const log = (message: string) => {
const path = getAppLogPath()
if (!message.startsWith('[')) {
message = `[APP]::${message}`
}
message = `${new Date().toISOString()} ${message}`
writeLog(message, path)
}
export const logServer = (message: string) => {
const path = getServerLogPath()
if (!message.startsWith('[')) {
message = `[SERVER]::${message}`
}
message = `${new Date().toISOString()} ${message}`
writeLog(message, path)
}
const writeLog = (message: string, logPath: string) => {
if (!fs.existsSync(logPath)) {
fs.writeFileSync(logPath, message)
} else {
const logFile = fs.createWriteStream(logPath, {
flags: 'a',
})
logFile.write(util.format(message) + '\n')
logFile.close()
console.debug(message)
}
}

View File

@ -0,0 +1,81 @@
// Abstract Logger class that all loggers should extend.
export abstract class Logger {
// Each logger must have a unique name.
abstract name: string
/**
* Log message to log file.
* This method should be overridden by subclasses to provide specific logging behavior.
*/
abstract log(args: any): void
}
// LoggerManager is a singleton class that manages all registered loggers.
export class LoggerManager {
// Map of registered loggers, keyed by their names.
public loggers = new Map<string, Logger>()
// Array to store logs that are queued before the loggers are registered.
queuedLogs: any[] = []
// Flag to indicate whether flushLogs is currently running.
private isFlushing = false
// Register a new logger. If a logger with the same name already exists, it will be replaced.
register(logger: Logger) {
this.loggers.set(logger.name, logger)
}
// Unregister a logger by its name.
unregister(name: string) {
this.loggers.delete(name)
}
get(name: string) {
return this.loggers.get(name)
}
// Flush queued logs to all registered loggers.
flushLogs() {
// If flushLogs is already running, do nothing.
if (this.isFlushing) {
return
}
this.isFlushing = true
while (this.queuedLogs.length > 0 && this.loggers.size > 0) {
const log = this.queuedLogs.shift()
this.loggers.forEach((logger) => {
logger.log(log)
})
}
this.isFlushing = false
}
// Log message using all registered loggers.
log(args: any) {
this.queuedLogs.push(args)
this.flushLogs()
}
/**
* The instance of the logger.
* If an instance doesn't exist, it creates a new one.
* This ensures that there is only one LoggerManager instance at any time.
*/
static instance(): LoggerManager {
let instance: LoggerManager | undefined = global.core?.logger
if (!instance) {
instance = new LoggerManager()
if (!global.core) global.core = {}
global.core.logger = instance
}
return instance
}
}
export const log = (...args: any) => {
LoggerManager.instance().log(args)
}

View File

@ -1,11 +1,10 @@
import { SystemResourceInfo } from '../../types'
import { physicalCpuCount } from './config'
import { log } from './log'
import { log } from './logger'
export const getSystemResourceInfo = async (): Promise<SystemResourceInfo> => {
const cpu = await physicalCpuCount()
const message = `[NITRO]::CPU informations - ${cpu}`
log(message)
log(`[NITRO]::CPU informations - ${cpu}`)
return {
numCpuPhysicalCore: cpu,

View File

@ -7,7 +7,7 @@ export enum NativeRoute {
openAppDirectory = 'openAppDirectory',
openFileExplore = 'openFileExplorer',
selectDirectory = 'selectDirectory',
selectModelFiles = 'selectModelFiles',
selectFiles = 'selectFiles',
relaunch = 'relaunch',
hideQuickAskWindow = 'hideQuickAskWindow',

View File

@ -9,3 +9,4 @@ export * from './config'
export * from './huggingface'
export * from './miscellaneous'
export * from './api'
export * from './setting'

View File

@ -85,6 +85,8 @@ export enum ErrorCode {
InsufficientQuota = 'insufficient_quota',
InvalidRequestError = 'invalid_request_error',
Unknown = 'unknown',
}

View File

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

View File

@ -0,0 +1,37 @@
export type SelectFileOption = {
/**
* The title of the dialog.
*/
title?: string
/**
* Whether the dialog allows multiple selection.
*/
allowMultiple?: boolean
buttonLabel?: string
selectDirectory?: boolean
props?: SelectFileProp[]
filters?: FilterOption[]
}
export type FilterOption = {
name: string
extensions: string[]
}
export const SelectFilePropTuple = [
'openFile',
'openDirectory',
'multiSelections',
'showHiddenFiles',
'createDirectory',
'promptToCreate',
'noResolveAliases',
'treatPackageAsDirectory',
'dontAddToRecent',
] as const
export type SelectFileProp = (typeof SelectFilePropTuple)[number]

View File

@ -32,7 +32,7 @@ export type GpuSettingInfo = {
}
export type SystemInformation = {
gpuSetting: GpuSetting
gpuSetting?: GpuSetting
osInfo?: OperatingSystemInfo
}

View File

@ -41,7 +41,7 @@ export type Model = {
/**
* The version of the model.
*/
version: number
version: string
/**
* The format of the model.

View File

@ -1,3 +1,5 @@
import { GpuSetting, OperatingSystemInfo } from '../miscellaneous'
/**
* Monitoring extension for system monitoring.
* @extends BaseExtension
@ -14,4 +16,14 @@ export interface MonitoringInterface {
* @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

@ -0,0 +1 @@
export * from './settingComponent'

View File

@ -0,0 +1,34 @@
export type SettingComponentProps = {
key: string
title: string
description: string
controllerType: ControllerType
controllerProps: SliderComponentProps | CheckboxComponentProps | InputComponentProps
extensionName?: string
requireModelReload?: boolean
configType?: ConfigType
}
export type ConfigType = 'runtime' | 'setting'
export type ControllerType = 'slider' | 'checkbox' | 'input'
export type InputType = 'password' | 'text' | 'email' | 'number' | 'tel' | 'url'
export type InputComponentProps = {
placeholder: string
value: string
type?: InputType
}
export type SliderComponentProps = {
min: number
max: number
step: number
value: number
}
export type CheckboxComponentProps = {
value: boolean
}

171
docker-compose-dev.yml Normal file
View File

@ -0,0 +1,171 @@
# Docker Compose file for setting up Minio, createbuckets, app_cpu, and app_gpu services
version: '3.7'
services:
# Minio service for object storage
minio:
image: minio/minio
volumes:
- minio_data:/data
ports:
- '9000:9000'
- '9001:9001'
environment:
# Set the root user and password for Minio
MINIO_ROOT_USER: minioadmin # This acts as AWS_ACCESS_KEY
MINIO_ROOT_PASSWORD: minioadmin # This acts as AWS_SECRET_ACCESS_KEY
command: server --console-address ":9001" /data
restart: always
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live']
interval: 30s
timeout: 20s
retries: 3
networks:
vpcbr:
ipv4_address: 10.5.0.2
# createbuckets service to create a bucket and set its policy
createbuckets:
image: minio/mc
depends_on:
- minio
entrypoint: >
/bin/sh -c "
/usr/bin/mc alias set myminio http://minio:9000 minioadmin minioadmin;
/usr/bin/mc mb myminio/mybucket;
/usr/bin/mc policy set public myminio/mybucket;
exit 0;
"
networks:
vpcbr:
# app_cpu service for running the CPU version of the application
app_cpu_s3fs:
image: jan:latest
volumes:
- app_data_cpu_s3fs:/app/server/build/jan
build:
context: .
dockerfile: Dockerfile
environment:
# Set the AWS access key, secret access key, bucket name, endpoint, and region for app_cpu
AWS_ACCESS_KEY_ID: minioadmin
AWS_SECRET_ACCESS_KEY: minioadmin
S3_BUCKET_NAME: mybucket
AWS_ENDPOINT: http://10.5.0.2:9000
AWS_REGION: us-east-1
API_BASE_URL: http://localhost:1337
restart: always
profiles:
- cpu-s3fs
ports:
- '3000:3000'
- '1337:1337'
- '3928:3928'
networks:
vpcbr:
ipv4_address: 10.5.0.3
# app_gpu service for running the GPU version of the application
app_gpu_s3fs:
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
image: jan-gpu:latest
volumes:
- app_data_gpu_s3fs:/app/server/build/jan
build:
context: .
dockerfile: Dockerfile.gpu
restart: always
environment:
# Set the AWS access key, secret access key, bucket name, endpoint, and region for app_gpu
AWS_ACCESS_KEY_ID: minioadmin
AWS_SECRET_ACCESS_KEY: minioadmin
S3_BUCKET_NAME: mybucket
AWS_ENDPOINT: http://10.5.0.2:9000
AWS_REGION: us-east-1
API_BASE_URL: http://localhost:1337
profiles:
- gpu-s3fs
ports:
- '3000:3000'
- '1337:1337'
- '3928:3928'
networks:
vpcbr:
ipv4_address: 10.5.0.4
app_cpu_fs:
image: jan:latest
volumes:
- app_data_cpu_fs:/app/server/build/jan
build:
context: .
dockerfile: Dockerfile
environment:
API_BASE_URL: http://localhost:1337
restart: always
profiles:
- cpu-fs
ports:
- '3000:3000'
- '1337:1337'
- '3928:3928'
networks:
vpcbr:
ipv4_address: 10.5.0.5
# app_gpu service for running the GPU version of the application
app_gpu_fs:
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
image: jan-gpu:latest
volumes:
- app_data_gpu_fs:/app/server/build/jan
build:
context: .
dockerfile: Dockerfile.gpu
restart: always
environment:
API_BASE_URL: http://localhost:1337
profiles:
- gpu-fs
ports:
- '3000:3000'
- '1337:1337'
- '3928:3928'
networks:
vpcbr:
ipv4_address: 10.5.0.6
volumes:
minio_data:
app_data_cpu_s3fs:
app_data_gpu_s3fs:
app_data_cpu_fs:
app_data_gpu_fs:
networks:
vpcbr:
driver: bridge
ipam:
config:
- subnet: 10.5.0.0/16
gateway: 10.5.0.1
# Usage:
# - Run 'docker compose -f docker-compose-dev.yml --profile cpu-s3fs up -d' to start the app_cpu service
# - Run 'docker compose -f docker-compose-dev.yml --profile gpu-s3fs up -d' to start the app_gpu service
# - Run 'docker compose -f docker-compose-dev.yml --profile cpu-fs up -d' to start the app_cpu service
# - Run 'docker compose -f docker-compose-dev.yml --profile gpu-fs up -d' to start the app_gpu service

View File

@ -9,8 +9,8 @@ services:
volumes:
- minio_data:/data
ports:
- "9000:9000"
- "9001:9001"
- '9000:9000'
- '9001:9001'
environment:
# Set the root user and password for Minio
MINIO_ROOT_USER: minioadmin # This acts as AWS_ACCESS_KEY
@ -18,7 +18,7 @@ services:
command: server --console-address ":9001" /data
restart: always
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live']
interval: 30s
timeout: 20s
retries: 3
@ -43,12 +43,9 @@ services:
# app_cpu service for running the CPU version of the application
app_cpu_s3fs:
image: jan:latest
volumes:
- app_data_cpu_s3fs:/app/server/build/jan
build:
context: .
dockerfile: Dockerfile
image: ghcr.io/janhq/jan-server:dev-cpu-latest
environment:
# Set the AWS access key, secret access key, bucket name, endpoint, and region for app_cpu
AWS_ACCESS_KEY_ID: minioadmin
@ -61,9 +58,9 @@ services:
profiles:
- cpu-s3fs
ports:
- "3000:3000"
- "1337:1337"
- "3928:3928"
- '3000:3000'
- '1337:1337'
- '3928:3928'
networks:
vpcbr:
ipv4_address: 10.5.0.3
@ -74,15 +71,12 @@ services:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
image: jan-gpu:latest
- driver: nvidia
count: all
capabilities: [gpu]
image: ghcr.io/janhq/jan-server:dev-cuda-12.2-latest
volumes:
- app_data_gpu_s3fs:/app/server/build/jan
build:
context: .
dockerfile: Dockerfile.gpu
restart: always
environment:
# Set the AWS access key, secret access key, bucket name, endpoint, and region for app_gpu
@ -95,29 +89,26 @@ services:
profiles:
- gpu-s3fs
ports:
- "3000:3000"
- "1337:1337"
- "3928:3928"
- '3000:3000'
- '1337:1337'
- '3928:3928'
networks:
vpcbr:
ipv4_address: 10.5.0.4
app_cpu_fs:
image: jan:latest
image: ghcr.io/janhq/jan-server:dev-cpu-latest
volumes:
- app_data_cpu_fs:/app/server/build/jan
build:
context: .
dockerfile: Dockerfile
environment:
API_BASE_URL: http://localhost:1337
restart: always
profiles:
- cpu-fs
ports:
- "3000:3000"
- "1337:1337"
- "3928:3928"
- '3000:3000'
- '1337:1337'
- '3928:3928'
networks:
vpcbr:
ipv4_address: 10.5.0.5
@ -128,24 +119,21 @@ services:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
image: jan-gpu:latest
- driver: nvidia
count: all
capabilities: [gpu]
image: ghcr.io/janhq/jan-server:dev-cuda-12.2-latest
volumes:
- app_data_gpu_fs:/app/server/build/jan
build:
context: .
dockerfile: Dockerfile.gpu
restart: always
environment:
API_BASE_URL: http://localhost:1337
profiles:
- gpu-fs
ports:
- "3000:3000"
- "1337:1337"
- "3928:3928"
- '3000:3000'
- '1337:1337'
- '3928:3928'
networks:
vpcbr:
ipv4_address: 10.5.0.6
@ -161,10 +149,9 @@ networks:
vpcbr:
driver: bridge
ipam:
config:
- subnet: 10.5.0.0/16
gateway: 10.5.0.1
config:
- subnet: 10.5.0.0/16
gateway: 10.5.0.1
# Usage:
# - Run 'docker compose --profile cpu-s3fs up -d' to start the app_cpu service
# - Run 'docker compose --profile gpu-s3fs up -d' to start the app_gpu service

View File

@ -6,8 +6,11 @@ import {
getJanDataFolderPath,
getJanExtensionsPath,
init,
AppEvent, NativeRoute,
AppEvent,
NativeRoute,
SelectFileProp,
} from '@janhq/core/node'
import { SelectFileOption } from '@janhq/core/.'
export function handleAppIPCs() {
/**
@ -84,23 +87,39 @@ export function handleAppIPCs() {
}
})
ipcMain.handle(NativeRoute.selectModelFiles, async () => {
const mainWindow = windowManager.mainWindow
if (!mainWindow) {
console.error('No main window found')
return
}
const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {
title: 'Select model files',
buttonLabel: 'Select',
properties: ['openFile', 'openDirectory', 'multiSelections'],
})
if (canceled) {
return
}
ipcMain.handle(
NativeRoute.selectFiles,
async (_event, option?: SelectFileOption) => {
const mainWindow = windowManager.mainWindow
if (!mainWindow) {
console.error('No main window found')
return
}
return filePaths
})
const title = option?.title ?? 'Select files'
const buttonLabel = option?.buttonLabel ?? 'Select'
const props: SelectFileProp[] = ['openFile']
if (option?.allowMultiple) {
props.push('multiSelections')
}
if (option?.selectDirectory) {
props.push('openDirectory')
}
console.debug(`Select files with props: ${props}`)
const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {
title,
buttonLabel,
properties: props,
filters: option?.filters,
})
if (canceled) return
return filePaths
}
)
ipcMain.handle(
NativeRoute.hideQuickAskWindow,

View File

@ -39,6 +39,7 @@ export function handleAppUpdates() {
})
if (action.response === 0) {
trayManager.destroyCurrentTray()
windowManager.closeQuickAskWindow()
waitingToInstallVersion = _info?.version
autoUpdater.quitAndInstall()
}

View File

@ -1,4 +1,4 @@
import { app, BrowserWindow, Tray } from 'electron'
import { app, BrowserWindow } from 'electron'
import { join } from 'path'
/**
@ -11,7 +11,7 @@ import { getAppConfigurations, log } from '@janhq/core/node'
* IPC Handlers
**/
import { injectHandler } from './handlers/common'
import { handleAppUpdates, waitingToInstallVersion } from './handlers/update'
import { handleAppUpdates } from './handlers/update'
import { handleAppIPCs } from './handlers/native'
/**
@ -24,11 +24,10 @@ import { cleanUpAndQuit } from './utils/clean'
import { setupExtensions } from './utils/extension'
import { setupCore } from './utils/setup'
import { setupReactDevTool } from './utils/dev'
import { cleanLogs } from './utils/log'
import { registerShortcut } from './utils/selectedText'
import { trayManager } from './managers/tray'
import { logSystemInfo } from './utils/system'
import { registerGlobalShortcuts } from './utils/shortcut'
const preloadPath = join(__dirname, 'preload.js')
const rendererPath = join(__dirname, '..', 'renderer')
@ -38,8 +37,6 @@ const mainPath = join(rendererPath, 'index.html')
const mainUrl = 'http://localhost:3000'
const quickAskUrl = `${mainUrl}/search`
const quickAskHotKey = 'CommandOrControl+J'
const gotTheLock = app.requestSingleInstanceLock()
app
@ -60,6 +57,7 @@ app
.then(handleAppUpdates)
.then(() => process.env.CI !== 'e2e' && createQuickAskWindow())
.then(createMainWindow)
.then(registerGlobalShortcuts)
.then(() => {
if (!app.isPackaged) {
windowManager.mainWindow?.webContents.openDevTools()
@ -76,16 +74,11 @@ app
}
})
})
.then(() => cleanLogs())
app.on('second-instance', (_event, _commandLine, _workingDirectory) => {
windowManager.showMainWindow()
})
app.on('ready', () => {
registerGlobalShortcuts()
})
app.on('before-quit', function (evt) {
trayManager.destroyCurrentTray()
})
@ -96,7 +89,11 @@ app.once('quit', () => {
app.once('window-all-closed', () => {
// Feature Toggle for Quick Ask
if (getAppConfigurations().quick_ask && !waitingToInstallVersion) return
if (
getAppConfigurations().quick_ask &&
!windowManager.isQuickAskWindowDestroyed()
)
return
cleanUpAndQuit()
})
@ -112,26 +109,6 @@ function createMainWindow() {
windowManager.createMainWindow(preloadPath, startUrl)
}
function registerGlobalShortcuts() {
const ret = registerShortcut(quickAskHotKey, (selectedText: string) => {
// Feature Toggle for Quick Ask
if (!getAppConfigurations().quick_ask) return
if (!windowManager.isQuickAskWindowVisible()) {
windowManager.showQuickAskWindow()
windowManager.sendQuickAskSelectedText(selectedText)
} else {
windowManager.hideQuickAskWindow()
}
})
if (!ret) {
console.error('Global shortcut registration failed')
} else {
console.log('Global shortcut registered successfully')
}
}
/**
* Handles various IPC messages from the renderer process.
*/

View File

@ -45,7 +45,7 @@ class WindowManager {
windowManager.mainWindow?.on('close', function (evt) {
// Feature Toggle for Quick Ask
if (!getAppConfigurations().quick_ask) return
if (!isAppQuitting) {
evt.preventDefault()
windowManager.hideMainWindow()
@ -93,10 +93,22 @@ class WindowManager {
this._quickAskWindowVisible = true
}
closeQuickAskWindow(): void {
if (this._quickAskWindow?.isDestroyed()) return
this._quickAskWindow?.close()
this._quickAskWindow?.destroy()
this._quickAskWindow = undefined
this._quickAskWindowVisible = false
}
isQuickAskWindowVisible(): boolean {
return this._quickAskWindowVisible
}
isQuickAskWindowDestroyed(): boolean {
return this._quickAskWindow?.isDestroyed() ?? true
}
expandQuickAskWindow(heightOffset: number): void {
const width = quickAskWindowConfig.width!
const height = quickAskWindowConfig.height! + heightOffset
@ -112,10 +124,18 @@ class WindowManager {
}
cleanUp(): void {
this.mainWindow?.destroy()
this._quickAskWindow?.destroy()
this._quickAskWindowVisible = false
this._mainWindowVisible = false
if (!this.mainWindow?.isDestroyed()) {
this.mainWindow?.close()
this.mainWindow?.destroy()
this.mainWindow = undefined
this._mainWindowVisible = false
}
if (!this._quickAskWindow?.isDestroyed()) {
this._quickAskWindow?.close()
this._quickAskWindow?.destroy()
this._quickAskWindow = undefined
this._quickAskWindowVisible = false
}
}
}

View File

@ -14,14 +14,12 @@
"renderer/**/*",
"build/**/*.{js,map}",
"pre-install",
"models/**/*",
"docs/**/*",
"scripts/**/*",
"icons/**/*"
],
"asarUnpack": [
"pre-install",
"models",
"docs",
"scripts",
"icons"
@ -110,7 +108,8 @@
"eslint-plugin-react": "^7.34.0",
"rimraf": "^5.0.5",
"run-script-os": "^1.1.6",
"typescript": "^5.3.3"
"typescript": "^5.3.3",
"@reportportal/agent-js-playwright": "^5.1.7"
},
"installConfig": {
"hoistingLimits": "workspaces"

View File

@ -1,67 +0,0 @@
import { getJanDataFolderPath } from '@janhq/core/node'
import * as fs from 'fs'
import * as path from 'path'
export function cleanLogs(
maxFileSizeBytes?: number | undefined,
daysToKeep?: number | undefined,
delayMs?: number | undefined
): void {
const size = maxFileSizeBytes ?? 1 * 1024 * 1024 // 1 MB
const days = daysToKeep ?? 7 // 7 days
const delays = delayMs ?? 10000 // 10 seconds
const logDirectory = path.join(getJanDataFolderPath(), 'logs')
// Perform log cleaning
const currentDate = new Date()
fs.readdir(logDirectory, (err, files) => {
if (err) {
console.error('Error reading log directory:', err)
return
}
files.forEach((file) => {
const filePath = path.join(logDirectory, file)
fs.stat(filePath, (err, stats) => {
if (err) {
console.error('Error getting file stats:', err)
return
}
// Check size
if (stats.size > size) {
fs.unlink(filePath, (err) => {
if (err) {
console.error('Error deleting log file:', err)
return
}
console.debug(
`Deleted log file due to exceeding size limit: ${filePath}`
)
})
} else {
// Check age
const creationDate = new Date(stats.ctime)
const daysDifference = Math.floor(
(currentDate.getTime() - creationDate.getTime()) /
(1000 * 3600 * 24)
)
if (daysDifference > days) {
fs.unlink(filePath, (err) => {
if (err) {
console.error('Error deleting log file:', err)
return
}
console.debug(`Deleted old log file: ${filePath}`)
})
}
}
})
})
})
// Schedule the next execution with doubled delays
setTimeout(() => {
cleanLogs(maxFileSizeBytes, daysToKeep, delays * 2)
}, delays)
}

View File

@ -0,0 +1,24 @@
import { getAppConfigurations } from '@janhq/core/node'
import { registerShortcut } from './selectedText'
import { windowManager } from '../managers/window'
// TODO: Retrieve from config later
const quickAskHotKey = 'CommandOrControl+J'
export function registerGlobalShortcuts() {
if (!getAppConfigurations().quick_ask) return
const ret = registerShortcut(quickAskHotKey, (selectedText: string) => {
// Feature Toggle for Quick Ask
if (!windowManager.isQuickAskWindowVisible()) {
windowManager.showQuickAskWindow()
windowManager.sendQuickAskSelectedText(selectedText)
} else {
windowManager.hideQuickAskWindow()
}
})
if (!ret) {
console.error('Global shortcut registration failed')
} else {
console.log('Global shortcut registered successfully')
}
}

View File

@ -1,14 +1,10 @@
# Jan Assistant plugin
# Create a Jan Extension using Typescript
Created using Jan app example
Use this template to bootstrap the creation of a TypeScript Jan extension. 🚀
# Create a Jan Plugin using Typescript
## Create Your Own Extension
Use this template to bootstrap the creation of a TypeScript Jan plugin. 🚀
## Create Your Own Plugin
To create your own plugin, you can use this repository as a template! Just follow the below instructions:
To create your own extension, you can use this repository as a template! Just follow the below instructions:
1. Click the Use this template button at the top of the repository
2. Select Create a new repository
@ -18,7 +14,7 @@ To create your own plugin, you can use this repository as a template! Just follo
## Initial Setup
After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your plugin.
After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your extension.
> [!NOTE]
>
@ -43,35 +39,37 @@ After you've cloned the repository to your local machine or codespace, you'll ne
1. :white_check_mark: Check your artifact
There will be a tgz file in your plugin directory now
There will be a tgz file in your extension directory now
## Update the Plugin Metadata
## Update the Extension Metadata
The [`package.json`](package.json) file defines metadata about your plugin, such as
plugin name, main entry, description and version.
The [`package.json`](package.json) file defines metadata about your extension, such as
extension name, main entry, description and version.
When you copy this repository, update `package.json` with the name, description for your plugin.
When you copy this repository, update `package.json` with the name, description for your extension.
## Update the Plugin Code
## Update the Extension Code
The [`src/`](./src/) directory is the heart of your plugin! This contains the
source code that will be run when your plugin extension functions are invoked. You can replace the
The [`src/`](./src/) directory is the heart of your extension! This contains the
source code that will be run when your extension functions are invoked. You can replace the
contents of this directory with your own code.
There are a few things to keep in mind when writing your plugin code:
There are a few things to keep in mind when writing your extension code:
- Most Jan Plugin Extension functions are processed asynchronously.
- Most Jan Extension functions are processed asynchronously.
In `index.ts`, you will see that the extension function will return a `Promise<any>`.
```typescript
import { core } from "@janhq/core";
import { events, MessageEvent, MessageRequest } from '@janhq/core'
function onStart(): Promise<any> {
return core.invokePluginFunc(MODULE_PATH, "run", 0);
return events.on(MessageEvent.OnMessageSent, (data: MessageRequest) =>
this.inference(data)
)
}
```
For more information about the Jan Plugin Core module, see the
For more information about the Jan Extension Core module, see the
[documentation](https://github.com/janhq/jan/blob/main/core/README.md).
So, what are you waiting for? Go ahead and start customizing your plugin!
So, what are you waiting for? Go ahead and start customizing your extension!

View File

@ -1,5 +1,6 @@
{
"name": "@janhq/assistant-extension",
"productName": "Jan Assistant Extension",
"version": "1.0.1",
"description": "This extension enables assistants, including Jan, a default assistant that can call all downloaded models",
"main": "dist/index.js",

View File

@ -7,12 +7,10 @@ import replace from '@rollup/plugin-replace'
const packageJson = require('./package.json')
const pkg = require('./package.json')
export default [
{
input: `src/index.ts`,
output: [{ file: pkg.main, format: 'es', sourcemap: true }],
output: [{ file: packageJson.main, format: 'es', sourcemap: true }],
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
external: [],
watch: {
@ -20,8 +18,8 @@ export default [
},
plugins: [
replace({
preventAssignment: true,
NODE: JSON.stringify(`${packageJson.name}/${packageJson.node}`),
EXTENSION_NAME: JSON.stringify(packageJson.name),
VERSION: JSON.stringify(packageJson.version),
}),
// Allow json resolution
@ -36,7 +34,7 @@ export default [
// https://github.com/rollup/rollup-plugin-node-resolve#usage
resolve({
extensions: ['.js', '.ts', '.svelte'],
browser: true
browser: true,
}),
// Resolve source maps to the original source

View File

@ -1,3 +1,2 @@
declare const NODE: string
declare const EXTENSION_NAME: string
declare const VERSION: string

View File

@ -21,7 +21,7 @@ export default class JanAssistantExtension extends AssistantExtension {
JanAssistantExtension._homeDir
)
if (
localStorage.getItem(`${EXTENSION_NAME}-version`) !== VERSION ||
localStorage.getItem(`${this.name}-version`) !== VERSION ||
!assistantDirExist
) {
if (!assistantDirExist) await fs.mkdir(JanAssistantExtension._homeDir)
@ -29,7 +29,7 @@ export default class JanAssistantExtension extends AssistantExtension {
// Write assistant metadata
await this.createJanAssistant()
// Finished migration
localStorage.setItem(`${EXTENSION_NAME}-version`, VERSION)
localStorage.setItem(`${this.name}-version`, VERSION)
// Update the assistant list
events.emit(AssistantEvent.OnAssistantsUpdate, {})
}

View File

@ -1,13 +1,36 @@
import fs from 'fs'
import path from 'path'
import { getJanDataFolderPath } from '@janhq/core/node'
import { SettingComponentProps, getJanDataFolderPath } from '@janhq/core/node'
// Sec: Do not send engine settings over requests
// Read it manually instead
export const readEmbeddingEngine = (engineName: string) => {
const engineSettings = fs.readFileSync(
path.join(getJanDataFolderPath(), 'engines', `${engineName}.json`),
'utf-8'
)
return JSON.parse(engineSettings)
if (engineName !== 'openai' && engineName !== 'groq') {
const engineSettings = fs.readFileSync(
path.join(getJanDataFolderPath(), 'engines', `${engineName}.json`),
'utf-8'
)
return JSON.parse(engineSettings)
} else {
const settingDirectoryPath = path.join(
getJanDataFolderPath(),
'settings',
engineName === 'openai'
? 'inference-openai-extension'
: 'inference-groq-extension',
'settings.json'
)
const content = fs.readFileSync(settingDirectoryPath, 'utf-8')
const settings: SettingComponentProps[] = JSON.parse(content)
const apiKeyId = engineName === 'openai' ? 'openai-api-key' : 'groq-api-key'
const keySetting = settings.find((setting) => setting.key === apiKeyId)
let apiKey = keySetting?.controllerProps.value
if (typeof apiKey !== 'string') apiKey = ''
return {
api_key: apiKey,
}
}
}

View File

@ -1,5 +1,6 @@
{
"name": "@janhq/conversational-extension",
"productName": "Conversational Extension",
"version": "1.0.0",
"description": "This extension enables conversations and state persistence via your filesystem",
"main": "dist/index.js",

View File

@ -1,10 +1,10 @@
# Create a Jan Plugin using Typescript
# Create a Jan Extension using Typescript
Use this template to bootstrap the creation of a TypeScript Jan plugin. 🚀
Use this template to bootstrap the creation of a TypeScript Jan extension. 🚀
## Create Your Own Plugin
## Create Your Own Extension
To create your own plugin, you can use this repository as a template! Just follow the below instructions:
To create your own extension, you can use this repository as a template! Just follow the below instructions:
1. Click the Use this template button at the top of the repository
2. Select Create a new repository
@ -14,7 +14,7 @@ To create your own plugin, you can use this repository as a template! Just follo
## Initial Setup
After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your plugin.
After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your extension.
> [!NOTE]
>
@ -39,35 +39,37 @@ After you've cloned the repository to your local machine or codespace, you'll ne
1. :white_check_mark: Check your artifact
There will be a tgz file in your plugin directory now
There will be a tgz file in your extension directory now
## Update the Plugin Metadata
## Update the Extension Metadata
The [`package.json`](package.json) file defines metadata about your plugin, such as
plugin name, main entry, description and version.
The [`package.json`](package.json) file defines metadata about your extension, such as
extension name, main entry, description and version.
When you copy this repository, update `package.json` with the name, description for your plugin.
When you copy this repository, update `package.json` with the name, description for your extension.
## Update the Plugin Code
## Update the Extension Code
The [`src/`](./src/) directory is the heart of your plugin! This contains the
source code that will be run when your plugin extension functions are invoked. You can replace the
The [`src/`](./src/) directory is the heart of your extension! This contains the
source code that will be run when your extension functions are invoked. You can replace the
contents of this directory with your own code.
There are a few things to keep in mind when writing your plugin code:
There are a few things to keep in mind when writing your extension code:
- Most Jan Plugin Extension functions are processed asynchronously.
- Most Jan Extension functions are processed asynchronously.
In `index.ts`, you will see that the extension function will return a `Promise<any>`.
```typescript
import { core } from "@janhq/core";
import { events, MessageEvent, MessageRequest } from '@janhq/core'
function onStart(): Promise<any> {
return core.invokePluginFunc(MODULE_PATH, "run", 0);
return events.on(MessageEvent.OnMessageSent, (data: MessageRequest) =>
this.inference(data)
)
}
```
For more information about the Jan Plugin Core module, see the
For more information about the Jan Extension Core module, see the
[documentation](https://github.com/janhq/jan/blob/main/core/README.md).
So, what are you waiting for? Go ahead and start customizing your plugin!
So, what are you waiting for? Go ahead and start customizing your extension!

View File

@ -1,5 +1,6 @@
{
"name": "@janhq/huggingface-extension",
"productName": "HuggingFace Extension",
"version": "1.0.0",
"description": "Hugging Face extension for converting HF models to GGUF",
"main": "dist/index.js",

View File

@ -18,7 +18,7 @@ export default [
},
plugins: [
replace({
EXTENSION_NAME: JSON.stringify(packageJson.name),
preventAssignment: true,
NODE_MODULE_PATH: JSON.stringify(
`${packageJson.name}/${packageJson.node}`
),

View File

@ -1,2 +1 @@
declare const EXTENSION_NAME: string
declare const NODE_MODULE_PATH: string

View File

@ -338,7 +338,7 @@ export default class JanHuggingFaceExtension extends HuggingFaceExtension {
const metadata: Model = {
object: 'model',
version: 1,
version: '1.0',
format: 'gguf',
sources: [
{

View File

@ -32,13 +32,9 @@ export const getQuantizeExecutable = (): string => {
binaryName = 'quantize.exe'
} else if (process.platform === 'darwin') {
/**
* For MacOS: mac-arm64 (Silicon), mac-x64 (InteL)
* For MacOS: mac-universal both Silicon and InteL
*/
if (process.arch === 'arm64') {
binaryFolder = pjoin(binaryFolder, 'mac-arm64')
} else {
binaryFolder = pjoin(binaryFolder, 'mac-x64')
}
binaryFolder = pjoin(binaryFolder, 'mac-universal')
} else {
binaryFolder = pjoin(binaryFolder, 'linux-cpu')
}

View File

@ -1,14 +1,10 @@
# Jan inference plugin
# Create a Jan Extension using Typescript
Created using Jan app example
Use this template to bootstrap the creation of a TypeScript Jan extension. 🚀
# Create a Jan Plugin using Typescript
## Create Your Own Extension
Use this template to bootstrap the creation of a TypeScript Jan plugin. 🚀
## Create Your Own Plugin
To create your own plugin, you can use this repository as a template! Just follow the below instructions:
To create your own extension, you can use this repository as a template! Just follow the below instructions:
1. Click the Use this template button at the top of the repository
2. Select Create a new repository
@ -18,7 +14,7 @@ To create your own plugin, you can use this repository as a template! Just follo
## Initial Setup
After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your plugin.
After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your extension.
> [!NOTE]
>
@ -43,36 +39,37 @@ After you've cloned the repository to your local machine or codespace, you'll ne
1. :white_check_mark: Check your artifact
There will be a tgz file in your plugin directory now
There will be a tgz file in your extension directory now
## Update the Plugin Metadata
## Update the Extension Metadata
The [`package.json`](package.json) file defines metadata about your plugin, such as
plugin name, main entry, description and version.
The [`package.json`](package.json) file defines metadata about your extension, such as
extension name, main entry, description and version.
When you copy this repository, update `package.json` with the name, description for your plugin.
When you copy this repository, update `package.json` with the name, description for your extension.
## Update the Plugin Code
## Update the Extension Code
The [`src/`](./src/) directory is the heart of your plugin! This contains the
source code that will be run when your plugin extension functions are invoked. You can replace the
The [`src/`](./src/) directory is the heart of your extension! This contains the
source code that will be run when your extension functions are invoked. You can replace the
contents of this directory with your own code.
There are a few things to keep in mind when writing your plugin code:
There are a few things to keep in mind when writing your extension code:
- Most Jan Plugin Extension functions are processed asynchronously.
- Most Jan Extension functions are processed asynchronously.
In `index.ts`, you will see that the extension function will return a `Promise<any>`.
```typescript
import { core } from "@janhq/core";
import { events, MessageEvent, MessageRequest } from '@janhq/core'
function onStart(): Promise<any> {
return core.invokePluginFunc(MODULE_PATH, "run", 0);
return events.on(MessageEvent.OnMessageSent, (data: MessageRequest) =>
this.inference(data)
)
}
```
For more information about the Jan Plugin Core module, see the
For more information about the Jan Extension Core module, see the
[documentation](https://github.com/janhq/jan/blob/main/core/README.md).
So, what are you waiting for? Go ahead and start customizing your plugin!
So, what are you waiting for? Go ahead and start customizing your extension!

View File

@ -1,5 +1,6 @@
{
"name": "@janhq/inference-groq-extension",
"productName": "Groq Inference Engine Extension",
"version": "1.0.0",
"description": "This extension enables fast Groq chat completion API calls",
"main": "dist/index.js",

View File

@ -0,0 +1,58 @@
[
{
"sources": [
{
"url": "https://groq.com"
}
],
"id": "llama2-70b-4096",
"object": "model",
"name": "Groq Llama 2 70b",
"version": "1.0",
"description": "Groq Llama 2 70b with supercharged speed!",
"format": "api",
"settings": {
"text_model": false
},
"parameters": {
"max_tokens": 4096,
"temperature": 0.7,
"top_p": 1,
"stop": null,
"stream": true
},
"metadata": {
"author": "Meta",
"tags": ["General", "Big Context Length"]
},
"engine": "groq"
},
{
"sources": [
{
"url": "https://groq.com"
}
],
"id": "mixtral-8x7b-32768",
"object": "model",
"name": "Groq Mixtral 8x7b Instruct",
"version": "1.0",
"description": "Groq Mixtral 8x7b Instruct is Mixtral with supercharged speed!",
"format": "api",
"settings": {
"text_model": false
},
"parameters": {
"max_tokens": 4096,
"temperature": 0.7,
"top_p": 1,
"stop": null,
"stream": true
},
"metadata": {
"author": "Mistral",
"tags": ["General", "Big Context Length"]
},
"engine": "groq"
}
]

View File

@ -0,0 +1,23 @@
[
{
"key": "chat-completions-endpoint",
"title": "Chat Completions Endpoint",
"description": "The endpoint to use for chat completions. See the [Groq documentation](https://console.groq.com/docs/openai) for more information.",
"controllerType": "input",
"controllerProps": {
"placeholder": "https://api.groq.com/openai/v1/chat/completions",
"value": "https://api.groq.com/openai/v1/chat/completions"
}
},
{
"key": "groq-api-key",
"title": "API Key",
"description": "The Groq API uses API keys for authentication. Visit your [API Keys](https://console.groq.com/keys) page to retrieve the API key you'll use in your requests.",
"controllerType": "input",
"controllerProps": {
"placeholder": "gsk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"value": "",
"type": "password"
}
}
]

View File

@ -6,78 +6,62 @@
* @module inference-groq-extension/src/index
*/
import {
events,
fs,
AppConfigurationEventName,
joinPath,
RemoteOAIEngine,
} from '@janhq/core'
import { join } from 'path'
import { RemoteOAIEngine, SettingComponentProps } from '@janhq/core'
declare const COMPLETION_URL: string
declare const SETTINGS: Array<any>
declare const MODELS: Array<any>
enum Settings {
apiKey = 'groq-api-key',
chatCompletionsEndPoint = 'chat-completions-endpoint',
}
/**
* A class that implements the InferenceExtension interface from the @janhq/core package.
* The class provides methods for initializing and stopping a model, and for making inference requests.
* It also subscribes to events emitted by the @janhq/core package and handles new message requests.
*/
export default class JanInferenceGroqExtension extends RemoteOAIEngine {
private readonly _engineDir = 'file://engines'
private readonly _engineMetadataFileName = 'groq.json'
inferenceUrl: string = COMPLETION_URL
inferenceUrl: string = ''
provider = 'groq'
apiKey = ''
private _engineSettings = {
full_url: COMPLETION_URL,
api_key: 'gsk-<your key here>',
}
/**
* Subscribes to events emitted by the @janhq/core package.
*/
async onLoad() {
override async onLoad(): Promise<void> {
super.onLoad()
if (!(await fs.existsSync(this._engineDir))) {
await fs.mkdir(this._engineDir)
}
// Register Settings
this.registerSettings(SETTINGS)
this.registerModels(MODELS)
this.writeDefaultEngineSettings()
const settingsFilePath = await joinPath([
this._engineDir,
this._engineMetadataFileName,
])
// Events subscription
events.on(
AppConfigurationEventName.OnConfigurationUpdate,
(settingsKey: string) => {
// Update settings on changes
if (settingsKey === settingsFilePath) this.writeDefaultEngineSettings()
}
// Retrieve API Key Setting
this.apiKey = await this.getSetting<string>(Settings.apiKey, '')
this.inferenceUrl = await this.getSetting<string>(
Settings.chatCompletionsEndPoint,
''
)
if (this.inferenceUrl.length === 0) {
SETTINGS.forEach((setting) => {
if (setting.key === Settings.chatCompletionsEndPoint) {
this.inferenceUrl = setting.controllerProps.value as string
}
})
}
}
async writeDefaultEngineSettings() {
try {
const engineFile = join(this._engineDir, this._engineMetadataFileName)
if (await fs.existsSync(engineFile)) {
const engine = await fs.readFileSync(engineFile, 'utf-8')
this._engineSettings =
typeof engine === 'object' ? engine : JSON.parse(engine)
this.inferenceUrl = this._engineSettings.full_url
this.apiKey = this._engineSettings.api_key
onSettingUpdate<T>(key: string, value: T): void {
if (key === Settings.apiKey) {
this.apiKey = value as string
} else if (key === Settings.chatCompletionsEndPoint) {
if (typeof value !== 'string') return
if (value.trim().length === 0) {
SETTINGS.forEach((setting) => {
if (setting.key === Settings.chatCompletionsEndPoint) {
this.inferenceUrl = setting.controllerProps.value as string
}
})
} else {
await fs.writeFileSync(
engineFile,
JSON.stringify(this._engineSettings, null, 2)
)
this.inferenceUrl = value
}
} catch (err) {
console.error(err)
}
}
}

View File

@ -1,6 +1,8 @@
const path = require('path')
const webpack = require('webpack')
const packageJson = require('./package.json')
const settingJson = require('./resources/settings.json')
const modelsJson = require('./resources/models.json')
module.exports = {
experiments: { outputModule: true },
@ -17,8 +19,9 @@ module.exports = {
},
plugins: [
new webpack.DefinePlugin({
MODELS: JSON.stringify(modelsJson),
SETTINGS: JSON.stringify(settingJson),
MODULE: JSON.stringify(`${packageJson.name}/${packageJson.module}`),
COMPLETION_URL: JSON.stringify('https://api.groq.com/openai/v1/chat/completions'),
}),
],
output: {

View File

@ -0,0 +1,79 @@
# Mistral Engine Extension
Created using Jan extension example
# Create a Jan Extension using Typescript
Use this template to bootstrap the creation of a TypeScript Jan extension. 🚀
## Create Your Own Extension
To create your own extension, you can use this repository as a template! Just follow the below instructions:
1. Click the Use this template button at the top of the repository
2. Select Create a new repository
3. Select an owner and name for your new repository
4. Click Create repository
5. Clone your new repository
## Initial Setup
After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your extension.
> [!NOTE]
>
> You'll need to have a reasonably modern version of
> [Node.js](https://nodejs.org) handy. If you are using a version manager like
> [`nodenv`](https://github.com/nodenv/nodenv) or
> [`nvm`](https://github.com/nvm-sh/nvm), you can run `nodenv install` in the
> root of your repository to install the version specified in
> [`package.json`](./package.json). Otherwise, 20.x or later should work!
1. :hammer_and_wrench: Install the dependencies
```bash
npm install
```
1. :building_construction: Package the TypeScript for distribution
```bash
npm run bundle
```
1. :white_check_mark: Check your artifact
There will be a tgz file in your extension directory now
## Update the Extension Metadata
The [`package.json`](package.json) file defines metadata about your extension, such as
extension name, main entry, description and version.
When you copy this repository, update `package.json` with the name, description for your extension.
## Update the Extension Code
The [`src/`](./src/) directory is the heart of your extension! This contains the
source code that will be run when your extension functions are invoked. You can replace the
contents of this directory with your own code.
There are a few things to keep in mind when writing your extension code:
- Most Jan Extension functions are processed asynchronously.
In `index.ts`, you will see that the extension function will return a `Promise<any>`.
```typescript
import { events, MessageEvent, MessageRequest } from '@janhq/core'
function onStart(): Promise<any> {
return events.on(MessageEvent.OnMessageSent, (data: MessageRequest) =>
this.inference(data)
)
}
```
For more information about the Jan Extension Core module, see the
[documentation](https://github.com/janhq/jan/blob/main/core/README.md).
So, what are you waiting for? Go ahead and start customizing your extension!

View File

@ -0,0 +1,43 @@
{
"name": "@janhq/inference-mistral-extension",
"productName": "Mistral AI Inference Engine Extension",
"version": "1.0.0",
"description": "This extension enables Mistral chat completion API calls",
"main": "dist/index.js",
"module": "dist/module.js",
"engine": "mistral",
"author": "Jan <service@jan.ai>",
"license": "AGPL-3.0",
"scripts": {
"build": "tsc -b . && webpack --config webpack.config.js",
"build:publish": "rimraf *.tgz --glob && yarn build && npm pack && cpx *.tgz ../../pre-install"
},
"exports": {
".": "./dist/index.js",
"./main": "./dist/module.js"
},
"devDependencies": {
"cpx": "^1.5.0",
"rimraf": "^3.0.2",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4",
"ts-loader": "^9.5.0"
},
"dependencies": {
"@janhq/core": "file:../../core",
"fetch-retry": "^5.0.6",
"path-browserify": "^1.0.1",
"ulidx": "^2.3.0"
},
"engines": {
"node": ">=18.0.0"
},
"files": [
"dist/*",
"package.json",
"README.md"
],
"bundleDependencies": [
"fetch-retry"
]
}

View File

@ -0,0 +1,85 @@
[
{
"sources": [
{
"url": "https://docs.mistral.ai/api/"
}
],
"id": "mistral-small-latest",
"object": "model",
"name": "Mistral Small",
"version": "1.0",
"description": "Mistral Small is the ideal choice for simpe tasks that one can do in builk - like Classification, Customer Support, or Text Generation. It offers excellent performance at an affordable price point.",
"format": "api",
"settings": {},
"parameters": {
"max_tokens": 4096,
"temperature": 0.7
},
"metadata": {
"author": "Mistral",
"tags": [
"Classification",
"Customer Support",
"Text Generation"
]
},
"engine": "mistral"
},
{
"sources": [
{
"url": "https://docs.mistral.ai/api/"
}
],
"id": "mistral-medium-latest",
"object": "model",
"name": "Mistral Medium",
"version": "1.0",
"description": "Mistral Medium is the ideal for intermediate tasks that require moderate reasoning - like Data extraction, Summarizing a Document, Writing a Job Description, or Writing Product Descriptions. Mistral Medium strikes a balance between performance and capability, making it suitable for a wide range of tasks that only require language transformaion",
"format": "api",
"settings": {},
"parameters": {
"max_tokens": 4096,
"temperature": 0.7
},
"metadata": {
"author": "Mistral",
"tags": [
"Data extraction",
"Summarizing a Document",
"Writing a Job Description",
"Writing Product Descriptions"
]
},
"engine": "mistral"
},
{
"sources": [
{
"url": "https://docs.mistral.ai/api/"
}
],
"id": "mistral-large-latest",
"object": "model",
"name": "Mistral Large",
"version": "1.0",
"description": "Mistral Large is ideal for complex tasks that require large reasoning capabilities or are highly specialized - like Synthetic Text Generation, Code Generation, RAG, or Agents.",
"format": "api",
"settings": {},
"parameters": {
"max_tokens": 4096,
"temperature": 0.7
},
"metadata": {
"author": "Mistral",
"tags": [
"Text Generation",
"Code Generation",
"RAG",
"Agents"
]
},
"engine": "mistral"
}
]

View File

@ -0,0 +1,23 @@
[
{
"key": "chat-completions-endpoint",
"title": "Chat Completions Endpoint",
"description": "The endpoint to use for chat completions. See the [Mistral API documentation](https://docs.mistral.ai/api/#operation/createChatCompletion) for more information.",
"controllerType": "input",
"controllerProps": {
"placeholder": "https://api.mistral.ai/v1/chat/completions",
"value": "https://api.mistral.ai/v1/chat/completions"
}
},
{
"key": "mistral-api-key",
"title": "API Key",
"description": "The Mistral API uses API keys for authentication. Visit your [API Keys](https://console.mistral.ai/api-keys/) page to retrieve the API key you'll use in your requests.",
"controllerType": "input",
"controllerProps": {
"placeholder": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"value": "",
"type": "password"
}
}
]

View File

@ -0,0 +1,66 @@
/**
* @file This file exports a class that implements the InferenceExtension interface from the @janhq/core package.
* The class provides methods for initializing and stopping a model, and for making inference requests.
* It also subscribes to events emitted by the @janhq/core package and handles new message requests.
* @version 1.0.0
* @module inference-mistral-extension/src/index
*/
import { RemoteOAIEngine } from '@janhq/core'
declare const SETTINGS: Array<any>
declare const MODELS: Array<any>
enum Settings {
apiKey = 'mistral-api-key',
chatCompletionsEndPoint = 'chat-completions-endpoint',
}
/**
* A class that implements the InferenceExtension interface from the @janhq/core package.
* The class provides methods for initializing and stopping a model, and for making inference requests.
* It also subscribes to events emitted by the @janhq/core package and handles new message requests.
*/
export default class JanInferenceMistralExtension extends RemoteOAIEngine {
inferenceUrl: string = ''
provider: string = 'mistral'
override async onLoad(): Promise<void> {
super.onLoad()
// Register Settings
this.registerSettings(SETTINGS)
this.registerModels(MODELS)
this.apiKey = await this.getSetting<string>(Settings.apiKey, '')
this.inferenceUrl = await this.getSetting<string>(
Settings.chatCompletionsEndPoint,
''
)
if (this.inferenceUrl.length === 0) {
SETTINGS.forEach((setting) => {
if (setting.key === Settings.chatCompletionsEndPoint) {
this.inferenceUrl = setting.controllerProps.value as string
}
})
}
}
onSettingUpdate<T>(key: string, value: T): void {
if (key === Settings.apiKey) {
this.apiKey = value as string
} else if (key === Settings.chatCompletionsEndPoint) {
if (typeof value !== 'string') return
if (value.trim().length === 0) {
SETTINGS.forEach((setting) => {
if (setting.key === Settings.chatCompletionsEndPoint) {
this.inferenceUrl = setting.controllerProps.value as string
}
})
} else {
this.inferenceUrl = value
}
}
}
}

View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "es2016",
"module": "ES6",
"moduleResolution": "node",
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": false,
"skipLibCheck": true,
"rootDir": "./src"
},
"include": ["./src"]
}

View File

@ -0,0 +1,42 @@
const path = require('path')
const webpack = require('webpack')
const packageJson = require('./package.json')
const settingJson = require('./resources/settings.json')
const modelsJson = require('./resources/models.json')
module.exports = {
experiments: { outputModule: true },
entry: './src/index.ts', // Adjust the entry point to match your project's main file
mode: 'production',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
plugins: [
new webpack.DefinePlugin({
SETTINGS: JSON.stringify(settingJson),
ENGINE: JSON.stringify(packageJson.engine),
MODELS: JSON.stringify(modelsJson),
}),
],
output: {
filename: 'index.js', // Adjust the output file name as needed
path: path.resolve(__dirname, 'dist'),
library: { type: 'module' }, // Specify ESM output format
},
resolve: {
extensions: ['.ts', '.js'],
fallback: {
path: require.resolve('path-browserify'),
},
},
optimization: {
minimize: false,
},
// Add loaders and other configuration as needed for your project
}

View File

@ -1,14 +1,10 @@
# Jan inference plugin
# Create a Jan Extension using Typescript
Created using Jan app example
Use this template to bootstrap the creation of a TypeScript Jan extension. 🚀
# Create a Jan Plugin using Typescript
## Create Your Own Extension
Use this template to bootstrap the creation of a TypeScript Jan plugin. 🚀
## Create Your Own Plugin
To create your own plugin, you can use this repository as a template! Just follow the below instructions:
To create your own extension, you can use this repository as a template! Just follow the below instructions:
1. Click the Use this template button at the top of the repository
2. Select Create a new repository
@ -18,7 +14,7 @@ To create your own plugin, you can use this repository as a template! Just follo
## Initial Setup
After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your plugin.
After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your extension.
> [!NOTE]
>
@ -43,35 +39,37 @@ After you've cloned the repository to your local machine or codespace, you'll ne
1. :white_check_mark: Check your artifact
There will be a tgz file in your plugin directory now
There will be a tgz file in your extension directory now
## Update the Plugin Metadata
## Update the Extension Metadata
The [`package.json`](package.json) file defines metadata about your plugin, such as
plugin name, main entry, description and version.
The [`package.json`](package.json) file defines metadata about your extension, such as
extension name, main entry, description and version.
When you copy this repository, update `package.json` with the name, description for your plugin.
When you copy this repository, update `package.json` with the name, description for your extension.
## Update the Plugin Code
## Update the Extension Code
The [`src/`](./src/) directory is the heart of your plugin! This contains the
source code that will be run when your plugin extension functions are invoked. You can replace the
The [`src/`](./src/) directory is the heart of your extension! This contains the
source code that will be run when your extension functions are invoked. You can replace the
contents of this directory with your own code.
There are a few things to keep in mind when writing your plugin code:
There are a few things to keep in mind when writing your extension code:
- Most Jan Plugin Extension functions are processed asynchronously.
- Most Jan Extension functions are processed asynchronously.
In `index.ts`, you will see that the extension function will return a `Promise<any>`.
```typescript
import { core } from '@janhq/core'
import { events, MessageEvent, MessageRequest } from '@janhq/core'
function onStart(): Promise<any> {
return core.invokePluginFunc(MODULE_PATH, 'run', 0)
return events.on(MessageEvent.OnMessageSent, (data: MessageRequest) =>
this.inference(data)
)
}
```
For more information about the Jan Plugin Core module, see the
For more information about the Jan Extension Core module, see the
[documentation](https://github.com/janhq/jan/blob/main/core/README.md).
So, what are you waiting for? Go ahead and start customizing your plugin!
So, what are you waiting for? Go ahead and start customizing your extension!

View File

@ -1 +1 @@
0.3.14
0.3.21

View File

@ -1,3 +1,3 @@
@echo off
set /p NITRO_VERSION=<./bin/version.txt
.\node_modules\.bin\download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64-cuda-12-0.tar.gz -e --strip 1 -o ./bin/win-cuda-12-0 && .\node_modules\.bin\download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64-cuda-11-7.tar.gz -e --strip 1 -o ./bin/win-cuda-11-7 && .\node_modules\.bin\download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64.tar.gz -e --strip 1 -o ./bin/win-cpu && .\node_modules\.bin\download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64-vulkan.tar.gz -e --strip 1 -o ./bin/win-vulkan
.\node_modules\.bin\download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64-avx2-cuda-12-0.tar.gz -e --strip 1 -o ./bin/win-cuda-12-0 && .\node_modules\.bin\download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64-avx2-cuda-11-7.tar.gz -e --strip 1 -o ./bin/win-cuda-11-7 && .\node_modules\.bin\download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64-avx2.tar.gz -e --strip 1 -o ./bin/win-cpu && .\node_modules\.bin\download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64-vulkan.tar.gz -e --strip 1 -o ./bin/win-vulkan

View File

@ -1,7 +1,8 @@
{
"name": "@janhq/inference-nitro-extension",
"productName": "Nitro Inference Engine Extension",
"version": "1.0.0",
"description": "This extension embeds Nitro, a lightweight (3mb) inference engine written in C++. See nitro.jan.ai",
"description": "This extension embeds Nitro, a lightweight (3mb) inference engine written in C++. See https://nitro.jan.ai.\nUse this setting if you encounter errors related to **CUDA toolkit** during application execution.",
"main": "dist/index.js",
"node": "dist/node/index.cjs.js",
"author": "Jan <service@jan.ai>",
@ -9,8 +10,8 @@
"scripts": {
"test": "jest",
"build": "tsc --module commonjs && rollup -c rollup.config.ts",
"downloadnitro:linux": "NITRO_VERSION=$(cat ./bin/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64.tar.gz -e --strip 1 -o ./bin/linux-cpu && chmod +x ./bin/linux-cpu/nitro && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64-cuda-12-0.tar.gz -e --strip 1 -o ./bin/linux-cuda-12-0 && chmod +x ./bin/linux-cuda-12-0/nitro && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64-cuda-11-7.tar.gz -e --strip 1 -o ./bin/linux-cuda-11-7 && chmod +x ./bin/linux-cuda-11-7/nitro && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64-vulkan.tar.gz -e --strip 1 -o ./bin/linux-vulkan && chmod +x ./bin/linux-vulkan/nitro",
"downloadnitro:darwin": "NITRO_VERSION=$(cat ./bin/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-arm64.tar.gz -e --strip 1 -o ./bin/mac-arm64 && chmod +x ./bin/mac-arm64/nitro && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-amd64.tar.gz -e --strip 1 -o ./bin/mac-x64 && chmod +x ./bin/mac-x64/nitro",
"downloadnitro:linux": "NITRO_VERSION=$(cat ./bin/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64-avx2.tar.gz -e --strip 1 -o ./bin/linux-cpu && chmod +x ./bin/linux-cpu/nitro && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64-cuda-12-0.tar.gz -e --strip 1 -o ./bin/linux-cuda-12-0 && chmod +x ./bin/linux-cuda-12-0/nitro && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64-cuda-11-7.tar.gz -e --strip 1 -o ./bin/linux-cuda-11-7 && chmod +x ./bin/linux-cuda-11-7/nitro && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64-vulkan.tar.gz -e --strip 1 -o ./bin/linux-vulkan && chmod +x ./bin/linux-vulkan/nitro",
"downloadnitro:darwin": "NITRO_VERSION=$(cat ./bin/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-universal.tar.gz -o ./bin/ && mkdir -p ./bin/mac-universal && tar -zxvf ./bin/nitro-${NITRO_VERSION}-mac-universal.tar.gz --strip-components=1 -C ./bin/mac-universal && rm -rf ./bin/nitro-${NITRO_VERSION}-mac-universal.tar.gz && chmod +x ./bin/mac-universal/nitro",
"downloadnitro:win32": "download.bat",
"downloadnitro": "run-script-os",
"build:publish:darwin": "rimraf *.tgz --glob && yarn build && npm run downloadnitro && ../../.github/scripts/auto-sign.sh && cpx \"bin/**\" \"dist/bin\" && npm pack && cpx *.tgz ../../pre-install",
@ -29,6 +30,7 @@
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-replace": "^5.0.5",
"@types/decompress": "^4.2.7",
"@types/jest": "^29.5.12",
"@types/node": "^20.11.4",
"@types/os-utils": "^0.0.4",
@ -47,10 +49,12 @@
},
"dependencies": {
"@janhq/core": "file:../../core",
"decompress": "^4.2.1",
"fetch-retry": "^5.0.6",
"path-browserify": "^1.0.1",
"rxjs": "^7.8.1",
"tcp-port-used": "^1.0.2",
"terminate": "^2.6.1",
"ulidx": "^2.3.0"
},
"engines": {
@ -64,6 +68,7 @@
"bundleDependencies": [
"tcp-port-used",
"fetch-retry",
"@janhq/core"
"@janhq/core",
"decompress"
]
}

View File

@ -0,0 +1,33 @@
[
{
"key": "test",
"title": "Test",
"description": "Test",
"controllerType": "input",
"controllerProps": {
"placeholder": "Test",
"value": ""
}
},
{
"key": "embedding",
"title": "Embedding",
"description": "Whether to enable embedding.",
"controllerType": "checkbox",
"controllerProps": {
"value": true
}
},
{
"key": "ctx_len",
"title": "Context Length",
"description": "The context length for model operations varies; the maximum depends on the specific model used.",
"controllerType": "slider",
"controllerProps": {
"min": 0,
"max": 4096,
"step": 128,
"value": 4096
}
}
]

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