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: jobs:
test-on-macos: 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] runs-on: [self-hosted, macOS, macos-desktop]
steps: 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 - name: Getting the repo
uses: actions/checkout@v3 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 - name: Installing node
uses: actions/setup-node@v3 uses: actions/setup-node@v3
@ -62,29 +102,24 @@ jobs:
- name: Linter and test - name: Linter and test
run: | run: |
npm config set registry https://registry.npmjs.org --global
yarn config set registry https://registry.npmjs.org --global
make test make test
env: env:
CSC_IDENTITY_AUTO_DISCOVERY: "false" CSC_IDENTITY_AUTO_DISCOVERY: "false"
test-on-windows: test-on-windows:
if: github.event_name == 'push' if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
antivirus-tools: ['mcafee', 'default-windows-security','bit-defender'] antivirus-tools: ['mcafee', 'default-windows-security','bit-defender']
runs-on: windows-desktop-${{ matrix.antivirus-tools }} runs-on: windows-desktop-${{ matrix.antivirus-tools }}
steps: 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 - name: Getting the repo
uses: actions/checkout@v3 uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Installing node - name: Installing node
uses: actions/setup-node@v3 uses: actions/setup-node@v3
@ -97,26 +132,79 @@ jobs:
continue-on-error: true continue-on-error: true
run: | run: |
make clean 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 - name: Linter and test
shell: powershell shell: powershell
run: | run: |
npm config set registry ${{ secrets.NPM_PROXY }} --global
yarn config set registry ${{ secrets.NPM_PROXY }} --global
make test make test
env:
TURBO_API: "${{ secrets.TURBO_API }}"
TURBO_TEAM: "windows"
TURBO_TOKEN: "${{ secrets.TURBO_TOKEN }}"
test-on-windows-pr: 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 runs-on: windows-desktop-default-windows-security
steps: 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 - name: Getting the repo
uses: actions/checkout@v3 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 - name: Installing node
uses: actions/setup-node@v1 uses: actions/setup-node@v1
@ -133,20 +221,65 @@ jobs:
- name: Linter and test - name: Linter and test
shell: powershell shell: powershell
run: | run: |
npm config set registry https://registry.npmjs.org --global
yarn config set registry https://registry.npmjs.org --global
make test make test
test-on-ubuntu: test-on-ubuntu:
runs-on: [self-hosted, Linux, ubuntu-desktop] 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: steps:
- name: "Cleanup build folder"
run: |
ls -la ./
rm -rf ./* || true
rm -rf ./.??* || true
ls -la ./
rm -rf ~/.config/jan
- name: Getting the repo - name: Getting the repo
uses: actions/checkout@v3 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 - name: Installing node
uses: actions/setup-node@v3 uses: actions/setup-node@v3
@ -162,4 +295,6 @@ jobs:
run: | run: |
export DISPLAY=$(w -h | awk 'NR==1 {print $2}') export DISPLAY=$(w -h | awk 'NR==1 {print $2}')
echo -e "Display ID: $DISPLAY" 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: with:
args: | args: |
Jan App ${{ inputs.build_reason }} build artifact version {{ VERSION }}: Jan App ${{ inputs.build_reason }} build artifact version {{ VERSION }}:
- Windows: https://delta.jan.ai/latest/jan-win-x64-{{ VERSION }}.exe - Windows: https://app.jan.ai/download/nightly/win-x64
- macOS Intel: https://delta.jan.ai/latest/jan-mac-x64-{{ VERSION }}.dmg - macOS Intel: https://app.jan.ai/download/nightly/mac-x64
- macOS Apple Silicon: https://delta.jan.ai/latest/jan-mac-arm64-{{ VERSION }}.dmg - macOS Apple Silicon: https://app.jan.ai/download/nightly/mac-arm64
- Linux Deb: https://delta.jan.ai/latest/jan-linux-amd64-{{ VERSION }}.deb - Linux Deb: https://app.jan.ai/download/nightly/linux-amd64-deb
- Linux AppImage: https://delta.jan.ai/latest/jan-linux-x86_64-{{ VERSION }}.AppImage - Linux AppImage: https://app.jan.ai/download/nightly/linux-amd64-appimage
- Github action run: https://github.com/janhq/jan/actions/runs/{{ GITHUB_RUN_ID }} - Github action run: https://github.com/janhq/jan/actions/runs/{{ GITHUB_RUN_ID }}
env: env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} 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 }}

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 .vscode
.idea
.env .env
.idea
# Jan inference # Jan inference
error.log error.log
@ -35,4 +37,4 @@ extensions/*-extension/bin/vulkaninfo
# Turborepo # 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 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/uikit ./uikit/
COPY --from=builder /app/web ./web/ 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/uikit install && yarn workspace @janhq/uikit build
RUN yarn workspace @janhq/web install 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 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/uikit ./uikit/
COPY --from=builder /app/web ./web/ 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/uikit install && yarn workspace @janhq/uikit build
RUN yarn workspace @janhq/web install RUN yarn workspace @janhq/web install

View File

@ -1,5 +1,11 @@
# Makefile for Jan Electron App - Build, Lint, Test, and Clean # 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 # Default target, does nothing
all: all:
@echo "Specify a target to run" @echo "Specify a target to run"
@ -37,6 +43,64 @@ dev: check-file-counts
lint: check-file-counts lint: check-file-counts
yarn lint 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 # Testing
test: lint test: lint
yarn build:test yarn build:test
@ -53,19 +117,24 @@ build: check-file-counts
clean: clean:
ifeq ($(OS),Windows_NT) 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 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 package-lock.json -Recurse -File | Remove-Item -Recurse -Force"
powershell -Command "Remove-Item -Recurse -Force ./pre-install/*.tgz" -powershell -Command "Get-ChildItem -Path . -Include yarn.lock -Recurse -File | Remove-Item -Recurse -Force"
powershell -Command "Remove-Item -Recurse -Force ./electron/pre-install/*.tgz" -powershell -Command "Remove-Item -Recurse -Force ./pre-install/*.tgz"
powershell -Command "if (Test-Path \"$($env:USERPROFILE)\jan\extensions\") { Remove-Item -Path \"$($env:USERPROFILE)\jan\extensions\" -Recurse -Force }" -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) else ifeq ($(shell uname -s),Linux)
find . -name "node_modules" -type d -prune -exec rm -rf '{}' + find . -name "node_modules" -type d -prune -exec rm -rf '{}' +
find . -name ".next" -type d -exec rm -rf '{}' + find . -name ".next" -type d -exec rm -rf '{}' +
find . -name "dist" -type d -exec rm -rf '{}' + find . -name "dist" -type d -exec rm -rf '{}' +
find . -name "build" -type d -exec rm -rf '{}' + find . -name "build" -type d -exec rm -rf '{}' +
find . -name "out" -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 "packake-lock.json" -type f -exec rm -rf '{}' +
find . -name "yarn.lock" -type f -exec rm -rf '{}' +
rm -rf ./pre-install/*.tgz rm -rf ./pre-install/*.tgz
rm -rf ./extensions/*/*.tgz
rm -rf ./electron/pre-install/*.tgz rm -rf ./electron/pre-install/*.tgz
rm -rf "~/jan/extensions" rm -rf "~/jan/extensions"
rm -rf "~/.cache/jan*" rm -rf "~/.cache/jan*"
@ -75,8 +144,11 @@ else
find . -name "dist" -type d -exec rm -rf '{}' + find . -name "dist" -type d -exec rm -rf '{}' +
find . -name "build" -type d -exec rm -rf '{}' + find . -name "build" -type d -exec rm -rf '{}' +
find . -name "out" -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 "packake-lock.json" -type f -exec rm -rf '{}' +
find . -name "yarn.lock" -type f -exec rm -rf '{}' +
rm -rf ./pre-install/*.tgz rm -rf ./pre-install/*.tgz
rm -rf ./extensions/*/*.tgz
rm -rf ./electron/pre-install/*.tgz rm -rf ./electron/pre-install/*.tgz
rm -rf ~/jan/extensions rm -rf ~/jan/extensions
rm -rf ~/Library/Caches/jan* 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"> <tr style="text-align:center">
<td style="text-align:center"><b>Stable (Recommended)</b></td> <td style="text-align:center"><b>Stable (Recommended)</b></td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://github.com/janhq/jan/releases/download/v0.4.9/jan-win-x64-0.4.9.exe'> <a href='https://app.jan.ai/download/latest/win-x64'>
<img src='./docs/static/img/windows.png' style="height:14px; width: 14px" /> <img src='https://github.com/janhq/docs/blob/main/static/img/windows.png' style="height:14px; width: 14px" />
<b>jan.exe</b> <b>jan.exe</b>
</a> </a>
</td> </td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://github.com/janhq/jan/releases/download/v0.4.9/jan-mac-x64-0.4.9.dmg'> <a href='https://app.jan.ai/download/latest/mac-x64'>
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" /> <img src='https://github.com/janhq/docs/blob/main/static/img/mac.png' style="height:15px; width: 15px" />
<b>Intel</b> <b>Intel</b>
</a> </a>
</td> </td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://github.com/janhq/jan/releases/download/v0.4.9/jan-mac-arm64-0.4.9.dmg'> <a href='https://app.jan.ai/download/latest/mac-arm64'>
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" /> <img src='https://github.com/janhq/docs/blob/main/static/img/mac.png' style="height:15px; width: 15px" />
<b>M1/M2</b> <b>M1/M2</b>
</a> </a>
</td> </td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://github.com/janhq/jan/releases/download/v0.4.9/jan-linux-amd64-0.4.9.deb'> <a href='https://app.jan.ai/download/latest/linux-amd64-deb'>
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" /> <img src='https://github.com/janhq/docs/blob/main/static/img/linux.png' style="height:14px; width: 14px" />
<b>jan.deb</b> <b>jan.deb</b>
</a> </a>
</td> </td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://github.com/janhq/jan/releases/download/v0.4.9/jan-linux-x86_64-0.4.9.AppImage'> <a href='https://app.jan.ai/download/latest/linux-amd64-appimage'>
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" /> <img src='https://github.com/janhq/docs/blob/main/static/img/linux.png' style="height:14px; width: 14px" />
<b>jan.AppImage</b> <b>jan.AppImage</b>
</a> </a>
</td> </td>
@ -76,32 +76,32 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute
<tr style="text-align:center"> <tr style="text-align:center">
<td style="text-align:center"><b>Experimental (Nightly Build)</b></td> <td style="text-align:center"><b>Experimental (Nightly Build)</b></td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://delta.jan.ai/latest/jan-win-x64-0.4.9-345.exe'> <a href='https://app.jan.ai/download/nightly/win-x64'>
<img src='./docs/static/img/windows.png' style="height:14px; width: 14px" /> <img src='https://github.com/janhq/docs/blob/main/static/img/windows.png' style="height:14px; width: 14px" />
<b>jan.exe</b> <b>jan.exe</b>
</a> </a>
</td> </td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://delta.jan.ai/latest/jan-mac-x64-0.4.9-345.dmg'> <a href='https://app.jan.ai/download/nightly/mac-x64'>
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" /> <img src='https://github.com/janhq/docs/blob/main/static/img/mac.png' style="height:15px; width: 15px" />
<b>Intel</b> <b>Intel</b>
</a> </a>
</td> </td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://delta.jan.ai/latest/jan-mac-arm64-0.4.9-345.dmg'> <a href='https://app.jan.ai/download/nightly/mac-arm64'>
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" /> <img src='https://github.com/janhq/docs/blob/main/static/img/mac.png' style="height:15px; width: 15px" />
<b>M1/M2</b> <b>M1/M2</b>
</a> </a>
</td> </td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://delta.jan.ai/latest/jan-linux-amd64-0.4.9-345.deb'> <a href='https://app.jan.ai/download/nightly/linux-amd64-deb'>
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" /> <img src='https://github.com/janhq/docs/blob/main/static/img/linux.png' style="height:14px; width: 14px" />
<b>jan.deb</b> <b>jan.deb</b>
</a> </a>
</td> </td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://delta.jan.ai/latest/jan-linux-x86_64-0.4.9-345.AppImage'> <a href='https://app.jan.ai/download/nightly/linux-amd64-appimage'>
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" /> <img src='https://github.com/janhq/docs/blob/main/static/img/linux.png' style="height:14px; width: 14px" />
<b>jan.AppImage</b> <b>jan.AppImage</b>
</a> </a>
</td> </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. - 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 - 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 | | 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 business inquiries: email hello@jan.ai
- For jobs: please email hr@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 ## License
Jan is free and open source, under the AGPLv3 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 // which external modules to include in the bundle
// https://github.com/rollup/rollup-plugin-node-resolve#usage // https://github.com/rollup/rollup-plugin-node-resolve#usage
replace({ replace({
'preventAssignment': true,
'node:crypto': 'crypto', 'node:crypto': 'crypto',
'delimiters': ['"', '"'], 'delimiters': ['"', '"'],
}), }),

View File

@ -1,3 +1,7 @@
import { SettingComponentProps } from '../types'
import { getJanDataFolderPath, joinPath } from './core'
import { fs } from './fs'
export enum ExtensionTypeEnum { export enum ExtensionTypeEnum {
Assistant = 'assistant', Assistant = 'assistant',
Conversational = 'conversational', Conversational = 'conversational',
@ -19,9 +23,9 @@ export interface Compatibility {
const ALL_INSTALLATION_STATE = [ const ALL_INSTALLATION_STATE = [
'NotRequired', // not required. 'NotRequired', // not required.
'Installed', // require and installed. Good to go. 'Installed', // require and installed. Good to go.
'Updatable', // require and installed but need to be updated.
'NotInstalled', // require to be installed. 'NotInstalled', // require to be installed.
'Corrupted', // require but corrupted. Need to redownload. 'Corrupted', // require but corrupted. Need to redownload.
'NotCompatible', // require but not compatible.
] as const ] as const
export type InstallationStateTuple = typeof ALL_INSTALLATION_STATE 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. * This class should be extended by any class that represents an extension.
*/ */
export abstract class BaseExtension implements ExtensionType { 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 the type of the extension.
* @returns {ExtensionType} 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 { type(): ExtensionTypeEnum | undefined {
return undefined return undefined
} }
/** /**
* Called when the extension is loaded. * Called when the extension is loaded.
* Any initialization logic for the extension should be put here. * Any initialization logic for the extension should be put here.
*/ */
abstract onLoad(): void abstract onLoad(): void
/** /**
* Called when the extension is unloaded. * Called when the extension is unloaded.
* Any cleanup logic for the extension should be put here. * Any cleanup logic for the extension should be put here.
@ -60,11 +103,40 @@ export abstract class BaseExtension implements ExtensionType {
return undefined return undefined
} }
/** async registerSettings(settings: SettingComponentProps[]): Promise<void> {
* Determine if the extension is updatable. if (!this.name) {
*/ console.error('Extension name is not defined')
updatable(): boolean { return
return false }
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>} * @returns {Promise<void>}
*/ */
// @ts-ignore async install(): Promise<void> {
async install(...args): Promise<void> {
return 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 * Applicable to all AI Engines
*/ */
export abstract class AIEngine extends BaseExtension { export abstract class AIEngine extends BaseExtension {
private static modelsFolder = 'models'
// The inference engine // The inference engine
abstract provider: string 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.OnModelInit, (model: Model) => this.loadModel(model))
events.on(ModelEvent.OnModelStop, (model: Model) => this.unloadModel(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) 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. * Loads the model.
*/ */
@ -65,40 +101,4 @@ export abstract class AIEngine extends BaseExtension {
* Stop inference * Stop inference
*/ */
stopInference() {} 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 * Inference request
*/ */
override inference(data: MessageRequest) { override async inference(data: MessageRequest) {
if (data.model?.engine?.toString() !== this.provider) return if (data.model?.engine?.toString() !== this.provider) return
const timestamp = Date.now() const timestamp = Date.now()
@ -77,12 +77,14 @@ export abstract class OAIEngine extends AIEngine {
...data.model, ...data.model,
} }
const header = await this.headers()
requestInference( requestInference(
this.inferenceUrl, this.inferenceUrl,
data.messages ?? [], data.messages ?? [],
model, model,
this.controller, this.controller,
this.headers() header
).subscribe({ ).subscribe({
next: (content: any) => { next: (content: any) => {
const messageContent: ThreadContent = { const messageContent: ThreadContent = {
@ -100,7 +102,9 @@ export abstract class OAIEngine extends AIEngine {
events.emit(MessageEvent.OnMessageUpdate, message) events.emit(MessageEvent.OnMessageUpdate, message)
}, },
error: async (err: any) => { 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) { if (this.isCancelled || message.content.length) {
message.status = MessageStatus.Stopped message.status = MessageStatus.Stopped
events.emit(MessageEvent.OnMessageUpdate, message) events.emit(MessageEvent.OnMessageUpdate, message)
@ -131,7 +135,7 @@ export abstract class OAIEngine extends AIEngine {
/** /**
* Headers for the inference request * Headers for the inference request
*/ */
headers(): HeadersInit { async headers(): Promise<HeadersInit> {
return {} return {}
} }
} }

View File

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

View File

@ -36,9 +36,15 @@ export function requestInference(
.then(async (response) => { .then(async (response) => {
if (!response.ok) { if (!response.ok) {
const data = await response.json() 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 = { const error = {
message: data.error?.message ?? 'Error occurred.', message: data.error?.message ?? 'Error occurred.',
code: data.error?.code ?? ErrorCode.Unknown, code: errorCode,
} }
subscriber.error(error) subscriber.error(error)
subscriber.complete() subscriber.complete()
@ -60,14 +66,20 @@ export function requestInference(
} }
const text = decoder.decode(value) const text = decoder.decode(value)
const lines = text.trim().split('\n') const lines = text.trim().split('\n')
let cachedLines = ''
for (const line of lines) { for (const line of lines) {
if (line.startsWith('data: ') && !line.includes('data: [DONE]')) { try {
const data = JSON.parse(line.replace('data: ', '')) const toParse = cachedLines + line
content += data.choices[0]?.delta?.content ?? '' if (!line.includes('data: [DONE]')) {
if (content.startsWith('assistant: ')) { const data = JSON.parse(toParse.replace('data: ', ''))
content = content.replace('assistant: ', '') 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 return ExtensionTypeEnum.SystemMonitoring
} }
abstract getGpuSetting(): Promise<GpuSetting> abstract getGpuSetting(): Promise<GpuSetting | undefined>
abstract getResourcesInfo(): Promise<any> abstract getResourcesInfo(): Promise<any>
abstract getCurrentLoad(): Promise<any> abstract getCurrentLoad(): Promise<any>
abstract getOsInfo(): Promise<OperatingSystemInfo> abstract getOsInfo(): Promise<OperatingSystemInfo>

View File

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

View File

@ -1,9 +1,12 @@
import { basename, isAbsolute, join, relative } from 'path' import { basename, isAbsolute, join, relative } from 'path'
import { Processor } from './Processor' import { Processor } from './Processor'
import { getAppConfigurations as appConfiguration, updateAppConfiguration } from '../../helper' import {
import { log as writeLog, logServer as writeServerLog } from '../../helper/log' log as writeLog,
import { appResourcePath } from '../../helper/path' appResourcePath,
getAppConfigurations as appConfiguration,
updateAppConfiguration,
} from '../../helper'
export class App implements Processor { export class App implements Processor {
observer?: Function observer?: Function
@ -56,13 +59,6 @@ export class App implements Processor {
writeLog(args) writeLog(args)
} }
/**
* Log message to log file.
*/
logServer(args: any) {
writeServerLog(args)
}
getAppConfigurations() { getAppConfigurations() {
return appConfiguration() return appConfiguration()
} }
@ -83,6 +79,7 @@ export class App implements Processor {
isVerboseEnabled: args?.isVerboseEnabled, isVerboseEnabled: args?.isVerboseEnabled,
schemaPath: join(await appResourcePath(), 'docs', 'openapi', 'jan.yaml'), schemaPath: join(await appResourcePath(), 'docs', 'openapi', 'jan.yaml'),
baseDir: join(await appResourcePath(), 'docs', 'openapi'), 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 requestedModel = matchedModels[0]
const engineConfiguration = await getEngineConfiguration(requestedModel.engine) const engineConfiguration = await getEngineConfiguration(requestedModel.engine)
let apiKey: string | undefined = undefined let apiKey: string | undefined = undefined
@ -323,7 +324,7 @@ export const chatCompletions = async (request: any, reply: any) => {
if (engineConfiguration) { if (engineConfiguration) {
apiKey = engineConfiguration.api_key apiKey = engineConfiguration.api_key
apiUrl = engineConfiguration.full_url apiUrl = engineConfiguration.full_url ?? DEFAULT_CHAT_COMPLETION_URL
} }
const headers: Record<string, any> = { const headers: Record<string, any> = {
@ -334,7 +335,6 @@ export const chatCompletions = async (request: any, reply: any) => {
headers['Authorization'] = `Bearer ${apiKey}` headers['Authorization'] = `Bearer ${apiKey}`
headers['api-key'] = apiKey headers['api-key'] = apiKey
} }
console.debug(apiUrl)
if (requestedModel.engine === 'openai' && request.body.stop) { if (requestedModel.engine === 'openai' && request.body.stop) {
// openai only allows max 4 stop words // openai only allows max 4 stop words
@ -352,7 +352,7 @@ export const chatCompletions = async (request: any, reply: any) => {
reply.code(400).send(response) reply.code(400).send(response)
} else { } else {
reply.raw.writeHead(200, { reply.raw.writeHead(200, {
'Content-Type': 'text/event-stream', 'Content-Type': request.body.stream === true ? 'text/event-stream' : 'application/json',
'Cache-Control': 'no-cache', 'Cache-Control': 'no-cache',
'Connection': 'keep-alive', 'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Origin': '*',

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import { AppConfiguration } from '../../types' import { AppConfiguration, SettingComponentProps } from '../../types'
import { join } from 'path' import { join } from 'path'
import fs from 'fs' import fs from 'fs'
import os from 'os' 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) => { export const getEngineConfiguration = async (engineId: string) => {
if (engineId !== 'openai' && engineId !== 'groq') { if (engineId !== 'openai' && engineId !== 'groq') return undefined
return undefined
}
const directoryPath = join(getJanDataFolderPath(), 'engines')
const filePath = join(directoryPath, `${engineId}.json`)
const data = fs.readFileSync(filePath, 'utf-8')
return JSON.parse(data)
}
/** const settingDirectoryPath = join(
* Utility function to get server log path getJanDataFolderPath(),
* 'settings',
* @returns {string} The log path. '@janhq',
*/ engineId === 'openai' ? 'inference-openai-extension' : 'inference-groq-extension',
export const getServerLogPath = (): string => { 'settings.json'
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 content = fs.readFileSync(settingDirectoryPath, 'utf-8')
* Utility function to get app log path const settings: SettingComponentProps[] = JSON.parse(content)
* const apiKeyId = engineId === 'openai' ? 'openai-api-key' : 'groq-api-key'
* @returns {string} The log path. const keySetting = settings.find((setting) => setting.key === apiKeyId)
*/ let fullUrl = settings.find((setting) => setting.key === 'chat-completions-endpoint')
export const getAppLogPath = (): string => { ?.controllerProps.value
const appConfigurations = getAppConfigurations()
const logFolderPath = join(appConfigurations.data_folder, 'logs') let apiKey = keySetting?.controllerProps.value
if (!fs.existsSync(logFolderPath)) { if (typeof apiKey !== 'string') apiKey = ''
fs.mkdirSync(logFolderPath, { recursive: true }) 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 './config'
export * from './download' export * from './download'
export * from './log' export * from './logger'
export * from './module' export * from './module'
export * from './path' export * from './path'
export * from './resource' 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 { SystemResourceInfo } from '../../types'
import { physicalCpuCount } from './config' import { physicalCpuCount } from './config'
import { log } from './log' import { log } from './logger'
export const getSystemResourceInfo = async (): Promise<SystemResourceInfo> => { export const getSystemResourceInfo = async (): Promise<SystemResourceInfo> => {
const cpu = await physicalCpuCount() const cpu = await physicalCpuCount()
const message = `[NITRO]::CPU informations - ${cpu}` log(`[NITRO]::CPU informations - ${cpu}`)
log(message)
return { return {
numCpuPhysicalCore: cpu, numCpuPhysicalCore: cpu,

View File

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

View File

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

View File

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

View File

@ -2,4 +2,5 @@ export * from './systemResourceInfo'
export * from './promptTemplate' export * from './promptTemplate'
export * from './appUpdate' export * from './appUpdate'
export * from './fileDownloadRequest' 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 = { export type SystemInformation = {
gpuSetting: GpuSetting gpuSetting?: GpuSetting
osInfo?: OperatingSystemInfo osInfo?: OperatingSystemInfo
} }

View File

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

View File

@ -1,3 +1,5 @@
import { GpuSetting, OperatingSystemInfo } from '../miscellaneous'
/** /**
* Monitoring extension for system monitoring. * Monitoring extension for system monitoring.
* @extends BaseExtension * @extends BaseExtension
@ -14,4 +16,14 @@ export interface MonitoringInterface {
* @returns {Promise<any>} A promise that resolves with the current system load. * @returns {Promise<any>} A promise that resolves with the current system load.
*/ */
getCurrentLoad(): Promise<any> 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: volumes:
- minio_data:/data - minio_data:/data
ports: ports:
- "9000:9000" - '9000:9000'
- "9001:9001" - '9001:9001'
environment: environment:
# Set the root user and password for Minio # Set the root user and password for Minio
MINIO_ROOT_USER: minioadmin # This acts as AWS_ACCESS_KEY MINIO_ROOT_USER: minioadmin # This acts as AWS_ACCESS_KEY
@ -18,7 +18,7 @@ services:
command: server --console-address ":9001" /data command: server --console-address ":9001" /data
restart: always restart: always
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live']
interval: 30s interval: 30s
timeout: 20s timeout: 20s
retries: 3 retries: 3
@ -43,12 +43,9 @@ services:
# app_cpu service for running the CPU version of the application # app_cpu service for running the CPU version of the application
app_cpu_s3fs: app_cpu_s3fs:
image: jan:latest
volumes: volumes:
- app_data_cpu_s3fs:/app/server/build/jan - app_data_cpu_s3fs:/app/server/build/jan
build: image: ghcr.io/janhq/jan-server:dev-cpu-latest
context: .
dockerfile: Dockerfile
environment: environment:
# Set the AWS access key, secret access key, bucket name, endpoint, and region for app_cpu # Set the AWS access key, secret access key, bucket name, endpoint, and region for app_cpu
AWS_ACCESS_KEY_ID: minioadmin AWS_ACCESS_KEY_ID: minioadmin
@ -61,9 +58,9 @@ services:
profiles: profiles:
- cpu-s3fs - cpu-s3fs
ports: ports:
- "3000:3000" - '3000:3000'
- "1337:1337" - '1337:1337'
- "3928:3928" - '3928:3928'
networks: networks:
vpcbr: vpcbr:
ipv4_address: 10.5.0.3 ipv4_address: 10.5.0.3
@ -74,15 +71,12 @@ services:
resources: resources:
reservations: reservations:
devices: devices:
- driver: nvidia - driver: nvidia
count: all count: all
capabilities: [gpu] capabilities: [gpu]
image: jan-gpu:latest image: ghcr.io/janhq/jan-server:dev-cuda-12.2-latest
volumes: volumes:
- app_data_gpu_s3fs:/app/server/build/jan - app_data_gpu_s3fs:/app/server/build/jan
build:
context: .
dockerfile: Dockerfile.gpu
restart: always restart: always
environment: environment:
# Set the AWS access key, secret access key, bucket name, endpoint, and region for app_gpu # Set the AWS access key, secret access key, bucket name, endpoint, and region for app_gpu
@ -95,29 +89,26 @@ services:
profiles: profiles:
- gpu-s3fs - gpu-s3fs
ports: ports:
- "3000:3000" - '3000:3000'
- "1337:1337" - '1337:1337'
- "3928:3928" - '3928:3928'
networks: networks:
vpcbr: vpcbr:
ipv4_address: 10.5.0.4 ipv4_address: 10.5.0.4
app_cpu_fs: app_cpu_fs:
image: jan:latest image: ghcr.io/janhq/jan-server:dev-cpu-latest
volumes: volumes:
- app_data_cpu_fs:/app/server/build/jan - app_data_cpu_fs:/app/server/build/jan
build:
context: .
dockerfile: Dockerfile
environment: environment:
API_BASE_URL: http://localhost:1337 API_BASE_URL: http://localhost:1337
restart: always restart: always
profiles: profiles:
- cpu-fs - cpu-fs
ports: ports:
- "3000:3000" - '3000:3000'
- "1337:1337" - '1337:1337'
- "3928:3928" - '3928:3928'
networks: networks:
vpcbr: vpcbr:
ipv4_address: 10.5.0.5 ipv4_address: 10.5.0.5
@ -128,24 +119,21 @@ services:
resources: resources:
reservations: reservations:
devices: devices:
- driver: nvidia - driver: nvidia
count: all count: all
capabilities: [gpu] capabilities: [gpu]
image: jan-gpu:latest image: ghcr.io/janhq/jan-server:dev-cuda-12.2-latest
volumes: volumes:
- app_data_gpu_fs:/app/server/build/jan - app_data_gpu_fs:/app/server/build/jan
build:
context: .
dockerfile: Dockerfile.gpu
restart: always restart: always
environment: environment:
API_BASE_URL: http://localhost:1337 API_BASE_URL: http://localhost:1337
profiles: profiles:
- gpu-fs - gpu-fs
ports: ports:
- "3000:3000" - '3000:3000'
- "1337:1337" - '1337:1337'
- "3928:3928" - '3928:3928'
networks: networks:
vpcbr: vpcbr:
ipv4_address: 10.5.0.6 ipv4_address: 10.5.0.6
@ -161,10 +149,9 @@ networks:
vpcbr: vpcbr:
driver: bridge driver: bridge
ipam: ipam:
config: config:
- subnet: 10.5.0.0/16 - subnet: 10.5.0.0/16
gateway: 10.5.0.1 gateway: 10.5.0.1
# Usage: # Usage:
# - Run 'docker compose --profile cpu-s3fs up -d' to start the app_cpu service # - 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 # - Run 'docker compose --profile gpu-s3fs up -d' to start the app_gpu service

View File

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

View File

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

View File

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

View File

@ -45,7 +45,7 @@ class WindowManager {
windowManager.mainWindow?.on('close', function (evt) { windowManager.mainWindow?.on('close', function (evt) {
// Feature Toggle for Quick Ask // Feature Toggle for Quick Ask
if (!getAppConfigurations().quick_ask) return if (!getAppConfigurations().quick_ask) return
if (!isAppQuitting) { if (!isAppQuitting) {
evt.preventDefault() evt.preventDefault()
windowManager.hideMainWindow() windowManager.hideMainWindow()
@ -93,10 +93,22 @@ class WindowManager {
this._quickAskWindowVisible = true this._quickAskWindowVisible = true
} }
closeQuickAskWindow(): void {
if (this._quickAskWindow?.isDestroyed()) return
this._quickAskWindow?.close()
this._quickAskWindow?.destroy()
this._quickAskWindow = undefined
this._quickAskWindowVisible = false
}
isQuickAskWindowVisible(): boolean { isQuickAskWindowVisible(): boolean {
return this._quickAskWindowVisible return this._quickAskWindowVisible
} }
isQuickAskWindowDestroyed(): boolean {
return this._quickAskWindow?.isDestroyed() ?? true
}
expandQuickAskWindow(heightOffset: number): void { expandQuickAskWindow(heightOffset: number): void {
const width = quickAskWindowConfig.width! const width = quickAskWindowConfig.width!
const height = quickAskWindowConfig.height! + heightOffset const height = quickAskWindowConfig.height! + heightOffset
@ -112,10 +124,18 @@ class WindowManager {
} }
cleanUp(): void { cleanUp(): void {
this.mainWindow?.destroy() if (!this.mainWindow?.isDestroyed()) {
this._quickAskWindow?.destroy() this.mainWindow?.close()
this._quickAskWindowVisible = false this.mainWindow?.destroy()
this._mainWindowVisible = false 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/**/*", "renderer/**/*",
"build/**/*.{js,map}", "build/**/*.{js,map}",
"pre-install", "pre-install",
"models/**/*",
"docs/**/*", "docs/**/*",
"scripts/**/*", "scripts/**/*",
"icons/**/*" "icons/**/*"
], ],
"asarUnpack": [ "asarUnpack": [
"pre-install", "pre-install",
"models",
"docs", "docs",
"scripts", "scripts",
"icons" "icons"
@ -110,7 +108,8 @@
"eslint-plugin-react": "^7.34.0", "eslint-plugin-react": "^7.34.0",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",
"run-script-os": "^1.1.6", "run-script-os": "^1.1.6",
"typescript": "^5.3.3" "typescript": "^5.3.3",
"@reportportal/agent-js-playwright": "^5.1.7"
}, },
"installConfig": { "installConfig": {
"hoistingLimits": "workspaces" "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. 🚀 To create your own extension, you can use this repository as a template! Just follow the below instructions:
## Create Your Own Plugin
To create your own plugin, 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 1. Click the Use this template button at the top of the repository
2. Select Create a new 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 ## 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] > [!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 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 The [`package.json`](package.json) file defines metadata about your extension, such as
plugin name, main entry, description and version. 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 The [`src/`](./src/) directory is the heart of your extension! This contains the
source code that will be run when your plugin extension functions are invoked. You can replace 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. 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>`. In `index.ts`, you will see that the extension function will return a `Promise<any>`.
```typescript ```typescript
import { core } from "@janhq/core"; import { events, MessageEvent, MessageRequest } from '@janhq/core'
function onStart(): Promise<any> { 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). [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", "name": "@janhq/assistant-extension",
"productName": "Jan Assistant Extension",
"version": "1.0.1", "version": "1.0.1",
"description": "This extension enables assistants, including Jan, a default assistant that can call all downloaded models", "description": "This extension enables assistants, including Jan, a default assistant that can call all downloaded models",
"main": "dist/index.js", "main": "dist/index.js",

View File

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

View File

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

View File

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

View File

@ -1,13 +1,36 @@
import fs from 'fs' import fs from 'fs'
import path from 'path' 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 // Sec: Do not send engine settings over requests
// Read it manually instead // Read it manually instead
export const readEmbeddingEngine = (engineName: string) => { export const readEmbeddingEngine = (engineName: string) => {
const engineSettings = fs.readFileSync( if (engineName !== 'openai' && engineName !== 'groq') {
path.join(getJanDataFolderPath(), 'engines', `${engineName}.json`), const engineSettings = fs.readFileSync(
'utf-8' path.join(getJanDataFolderPath(), 'engines', `${engineName}.json`),
) 'utf-8'
return JSON.parse(engineSettings) )
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", "name": "@janhq/conversational-extension",
"productName": "Conversational Extension",
"version": "1.0.0", "version": "1.0.0",
"description": "This extension enables conversations and state persistence via your filesystem", "description": "This extension enables conversations and state persistence via your filesystem",
"main": "dist/index.js", "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 1. Click the Use this template button at the top of the repository
2. Select Create a new 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 ## 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] > [!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 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 The [`package.json`](package.json) file defines metadata about your extension, such as
plugin name, main entry, description and version. 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 The [`src/`](./src/) directory is the heart of your extension! This contains the
source code that will be run when your plugin extension functions are invoked. You can replace 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. 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>`. In `index.ts`, you will see that the extension function will return a `Promise<any>`.
```typescript ```typescript
import { core } from "@janhq/core"; import { events, MessageEvent, MessageRequest } from '@janhq/core'
function onStart(): Promise<any> { 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). [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", "name": "@janhq/huggingface-extension",
"productName": "HuggingFace Extension",
"version": "1.0.0", "version": "1.0.0",
"description": "Hugging Face extension for converting HF models to GGUF", "description": "Hugging Face extension for converting HF models to GGUF",
"main": "dist/index.js", "main": "dist/index.js",

View File

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

View File

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

View File

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

View File

@ -32,13 +32,9 @@ export const getQuantizeExecutable = (): string => {
binaryName = 'quantize.exe' binaryName = 'quantize.exe'
} else if (process.platform === 'darwin') { } 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-universal')
binaryFolder = pjoin(binaryFolder, 'mac-arm64')
} else {
binaryFolder = pjoin(binaryFolder, 'mac-x64')
}
} else { } else {
binaryFolder = pjoin(binaryFolder, 'linux-cpu') 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. 🚀 To create your own extension, you can use this repository as a template! Just follow the below instructions:
## Create Your Own Plugin
To create your own plugin, 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 1. Click the Use this template button at the top of the repository
2. Select Create a new 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 ## 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] > [!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 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 The [`package.json`](package.json) file defines metadata about your extension, such as
plugin name, main entry, description and version. 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 The [`src/`](./src/) directory is the heart of your extension! This contains the
source code that will be run when your plugin extension functions are invoked. You can replace 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. 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>`. In `index.ts`, you will see that the extension function will return a `Promise<any>`.
```typescript ```typescript
import { core } from "@janhq/core"; import { events, MessageEvent, MessageRequest } from '@janhq/core'
function onStart(): Promise<any> { 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). [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", "name": "@janhq/inference-groq-extension",
"productName": "Groq Inference Engine Extension",
"version": "1.0.0", "version": "1.0.0",
"description": "This extension enables fast Groq chat completion API calls", "description": "This extension enables fast Groq chat completion API calls",
"main": "dist/index.js", "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 * @module inference-groq-extension/src/index
*/ */
import { import { RemoteOAIEngine, SettingComponentProps } from '@janhq/core'
events,
fs,
AppConfigurationEventName,
joinPath,
RemoteOAIEngine,
} from '@janhq/core'
import { join } from 'path'
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. * 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. * 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. * It also subscribes to events emitted by the @janhq/core package and handles new message requests.
*/ */
export default class JanInferenceGroqExtension extends RemoteOAIEngine { export default class JanInferenceGroqExtension extends RemoteOAIEngine {
private readonly _engineDir = 'file://engines' inferenceUrl: string = ''
private readonly _engineMetadataFileName = 'groq.json'
inferenceUrl: string = COMPLETION_URL
provider = 'groq' provider = 'groq'
apiKey = ''
private _engineSettings = { override async onLoad(): Promise<void> {
full_url: COMPLETION_URL,
api_key: 'gsk-<your key here>',
}
/**
* Subscribes to events emitted by the @janhq/core package.
*/
async onLoad() {
super.onLoad() super.onLoad()
if (!(await fs.existsSync(this._engineDir))) { // Register Settings
await fs.mkdir(this._engineDir) this.registerSettings(SETTINGS)
} this.registerModels(MODELS)
this.writeDefaultEngineSettings() // Retrieve API Key Setting
this.apiKey = await this.getSetting<string>(Settings.apiKey, '')
const settingsFilePath = await joinPath([ this.inferenceUrl = await this.getSetting<string>(
this._engineDir, Settings.chatCompletionsEndPoint,
this._engineMetadataFileName, ''
])
// Events subscription
events.on(
AppConfigurationEventName.OnConfigurationUpdate,
(settingsKey: string) => {
// Update settings on changes
if (settingsKey === settingsFilePath) this.writeDefaultEngineSettings()
}
) )
if (this.inferenceUrl.length === 0) {
SETTINGS.forEach((setting) => {
if (setting.key === Settings.chatCompletionsEndPoint) {
this.inferenceUrl = setting.controllerProps.value as string
}
})
}
} }
async writeDefaultEngineSettings() { onSettingUpdate<T>(key: string, value: T): void {
try { if (key === Settings.apiKey) {
const engineFile = join(this._engineDir, this._engineMetadataFileName) this.apiKey = value as string
if (await fs.existsSync(engineFile)) { } else if (key === Settings.chatCompletionsEndPoint) {
const engine = await fs.readFileSync(engineFile, 'utf-8') if (typeof value !== 'string') return
this._engineSettings =
typeof engine === 'object' ? engine : JSON.parse(engine) if (value.trim().length === 0) {
this.inferenceUrl = this._engineSettings.full_url SETTINGS.forEach((setting) => {
this.apiKey = this._engineSettings.api_key if (setting.key === Settings.chatCompletionsEndPoint) {
this.inferenceUrl = setting.controllerProps.value as string
}
})
} else { } else {
await fs.writeFileSync( this.inferenceUrl = value
engineFile,
JSON.stringify(this._engineSettings, null, 2)
)
} }
} catch (err) {
console.error(err)
} }
} }
} }

View File

@ -1,6 +1,8 @@
const path = require('path') const path = require('path')
const webpack = require('webpack') const webpack = require('webpack')
const packageJson = require('./package.json') const packageJson = require('./package.json')
const settingJson = require('./resources/settings.json')
const modelsJson = require('./resources/models.json')
module.exports = { module.exports = {
experiments: { outputModule: true }, experiments: { outputModule: true },
@ -17,8 +19,9 @@ module.exports = {
}, },
plugins: [ plugins: [
new webpack.DefinePlugin({ new webpack.DefinePlugin({
MODELS: JSON.stringify(modelsJson),
SETTINGS: JSON.stringify(settingJson),
MODULE: JSON.stringify(`${packageJson.name}/${packageJson.module}`), MODULE: JSON.stringify(`${packageJson.name}/${packageJson.module}`),
COMPLETION_URL: JSON.stringify('https://api.groq.com/openai/v1/chat/completions'),
}), }),
], ],
output: { 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. 🚀 To create your own extension, you can use this repository as a template! Just follow the below instructions:
## Create Your Own Plugin
To create your own plugin, 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 1. Click the Use this template button at the top of the repository
2. Select Create a new 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 ## 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] > [!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 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 The [`package.json`](package.json) file defines metadata about your extension, such as
plugin name, main entry, description and version. 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 The [`src/`](./src/) directory is the heart of your extension! This contains the
source code that will be run when your plugin extension functions are invoked. You can replace 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. 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>`. In `index.ts`, you will see that the extension function will return a `Promise<any>`.
```typescript ```typescript
import { core } from '@janhq/core' import { events, MessageEvent, MessageRequest } from '@janhq/core'
function onStart(): Promise<any> { 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). [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 @echo off
set /p NITRO_VERSION=<./bin/version.txt 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", "name": "@janhq/inference-nitro-extension",
"productName": "Nitro Inference Engine Extension",
"version": "1.0.0", "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", "main": "dist/index.js",
"node": "dist/node/index.cjs.js", "node": "dist/node/index.cjs.js",
"author": "Jan <service@jan.ai>", "author": "Jan <service@jan.ai>",
@ -9,8 +10,8 @@
"scripts": { "scripts": {
"test": "jest", "test": "jest",
"build": "tsc --module commonjs && rollup -c rollup.config.ts", "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: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-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: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:win32": "download.bat",
"downloadnitro": "run-script-os", "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", "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-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-replace": "^5.0.5", "@rollup/plugin-replace": "^5.0.5",
"@types/decompress": "^4.2.7",
"@types/jest": "^29.5.12", "@types/jest": "^29.5.12",
"@types/node": "^20.11.4", "@types/node": "^20.11.4",
"@types/os-utils": "^0.0.4", "@types/os-utils": "^0.0.4",
@ -47,10 +49,12 @@
}, },
"dependencies": { "dependencies": {
"@janhq/core": "file:../../core", "@janhq/core": "file:../../core",
"decompress": "^4.2.1",
"fetch-retry": "^5.0.6", "fetch-retry": "^5.0.6",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"tcp-port-used": "^1.0.2", "tcp-port-used": "^1.0.2",
"terminate": "^2.6.1",
"ulidx": "^2.3.0" "ulidx": "^2.3.0"
}, },
"engines": { "engines": {
@ -64,6 +68,7 @@
"bundleDependencies": [ "bundleDependencies": [
"tcp-port-used", "tcp-port-used",
"fetch-retry", "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