chore: sync Release/v0.5.16 into main (#4834)
* feat: Jan Hub Revamp (#4491) * feat: model hub revamp UI * chore: model description - consistent markdown css * chore: add model versions dropdown * chore: integrate APIs - model sources * chore: update model display name * chore: lint fix * chore: page transition animation * feat: model search dropdown - deeplink * chore: bump cortex version * chore: add remote model sources * chore: model download state * chore: fix model metadata label * chore: polish model detail page markdown * test: fix test cases * chore: initialize default Hub model sources * chore: fix model stats * chore: clean up click outside and inside hooks * feat: change hub banner * chore: lint fix * chore: fix css long model id * chore: sync cortex engine version (#4536) * chore: rotate model hub banner on app launch until set (#4542) * blog: add DeepSeek R1 local installation guide (#4552) * docs: add DeepSeek R1 local installation guide - Add comprehensive guide for running DeepSeek R1 locally - Include step-by-step instructions with screenshots - Add VRAM requirements and model selection guide - Include system prompt setup instructions * docs: add comprehensive guide on running AI models locally * docs: address PR feedback for DeepSeek R1 and local AI guides - Improve language and terminology throughout - Add Linux support information - Enhance technical explanations - Update introduction for better flow - Fix parameters section in run-ai-models-locally.mdx --------- Co-authored-by: Louis <louis@jan.ai> * enhancement: code snippet color and bakground should depend on native theme (#4566) * chore: correct conversational PATCH methods with latest cortex update (#4568) * fix: exclude yml into totaldownload start next version (#4572) * enhancement: adjust hub ui in different themes (#4574) * enhancement: toggle change cover hub banner style (#4579) * chore: update style codeblock (#4599) * docs: improve local AI guides content and linking (#4600) * docs: add DeepSeek R1 local installation guide - Add comprehensive guide for running DeepSeek R1 locally - Include step-by-step instructions with screenshots - Add VRAM requirements and model selection guide - Include system prompt setup instructions * docs: add comprehensive guide on running AI models locally * docs: address PR feedback for DeepSeek R1 and local AI guides - Improve language and terminology throughout - Add Linux support information - Enhance technical explanations - Update introduction for better flow - Fix parameters section in run-ai-models-locally.mdx * docs: improve local AI guides content and linking - Update titles and introductions for better SEO - Add opinionated guidance section for beginners - Link DeepSeek guide with general local AI guide - Fix typos and improve readability * fix: remove git conflict markers from deepseek guide frontmatter --------- Co-authored-by: Louis <louis@jan.ai> * chore: open URL from model detail page should open in an external browser (#4611) * chore: open URL from model detail page should open in an external browser * chore: remove unused param * Added guide for cloud model installation * Added DeepSeek & Google guides, updated some images * blog: improve local AI guide for beginners (#4610) * docs: add DeepSeek R1 local installation guide - Add comprehensive guide for running DeepSeek R1 locally - Include step-by-step instructions with screenshots - Add VRAM requirements and model selection guide - Include system prompt setup instructions * docs: add comprehensive guide on running AI models locally * docs: address PR feedback for DeepSeek R1 and local AI guides - Improve language and terminology throughout - Add Linux support information - Enhance technical explanations - Update introduction for better flow - Fix parameters section in run-ai-models-locally.mdx * docs: improve local AI guides content and linking - Update titles and introductions for better SEO - Add opinionated guidance section for beginners - Link DeepSeek guide with general local AI guide - Fix typos and improve readability * fix: remove git conflict markers from deepseek guide frontmatter * docs: improve local AI guide for beginners Key improvements: - Add detailed explanation of GGUF and why it's needed - Improve content structure and readability - Add visual guides with SEO-friendly images - Enhance llama.cpp explanation with GitHub link - Fix heading hierarchy for better navigation - Add practical examples and common questions - Update image paths and captions for better SEO Technical details: - Add proper image alt text and captions - Link to llama.cpp GitHub repository - Clarify model size requirements - Simplify hardware requirements section - Improve heading structure (h1-h5) - Add step-by-step model installation guide * docs: add offline ChatGPT alternative guide with Jan - Add comprehensive guide on using Jan as offline ChatGPT alternative - Include step-by-step instructions for setup - Add images for document chat feature - Optimize content for SEO with relevant keywords * docs: update description to emphasize computer-local aspect --------- Co-authored-by: Louis <louis@jan.ai> * docs: add new changelog posts and images (#4601) * docs: add DeepSeek R1 local installation guide - Add comprehensive guide for running DeepSeek R1 locally - Include step-by-step instructions with screenshots - Add VRAM requirements and model selection guide - Include system prompt setup instructions * docs: add comprehensive guide on running AI models locally * docs: address PR feedback for DeepSeek R1 and local AI guides - Improve language and terminology throughout - Add Linux support information - Enhance technical explanations - Update introduction for better flow - Fix parameters section in run-ai-models-locally.mdx * docs: improve local AI guides content and linking - Update titles and introductions for better SEO - Add opinionated guidance section for beginners - Link DeepSeek guide with general local AI guide - Fix typos and improve readability * docs: add new changelog posts and images - Add DeepSeek R1 changelog (v0.5.14) - Add key issues resolved changelog (v0.5.13) - Add corresponding changelog images --------- Co-authored-by: Louis <louis@jan.ai> * Fix typo for Red Hat company name (#4637) "Red Hat" is the correct company name, not "Redhat". * feat: app updater with changelog (#4631) * feat: ui modal app updater with changelog * chore: update action when click update now * chore: update handler actions * chore: fix linter * Update all images & some wrong parts * chore: typo * chore: lint fix * fix: format compact number utils (#4695) * chore: Hub UI and markdown CSS * feat: allow users to refresh cloud model list (#4698) * feat: allow users to refresh cloud model list * chore: reusable model list refresh * chore: clean up * refactor: different Jan builds should have different Cortex server port (#4699) * refactor: different Jan instances should have different Cortex server port configurations * chore: update workflow to use env input * chore: update env for cortex port setting * chore: change app logo for jan beta and nightly version * feat: Jan Model Hub should stay updated. (#4707) * feat: Jan Model Hub should stay updated. * chore: polish provider description * feat: preserve token speed in the thread (#4711) * feat: preserve token speed in the thread * chore: lint fix * chore: streaming should be turned on by default (#4712) * fix: should disable start model button when there is a model is loading (#4713) * chore: update pipeline change app logo for build nightly (#4709) * chore: change app logo for build nightly * chore: add nightly option to rename icons * chore: add rename icons for nightly and beta build * chore: remove rename icons job * feat: Jan Model Hub filter options and responsiveness (#4714) * feat: Jan Model Hub filter options and responsiveness * chore: fix display unit * chore: fix optional wrapping * chore: correct joi component's test * chore: sticky model hub filter panel (#4715) * chore: migrate engine settings on update (#4719) * chore: migrate engine settings on update * chore: queue engine migration to ensure it only execute when server is on * chore: ensure queue is empty instead of running in the queue * enhancement: update pexelated icon on windows platform (#4721) * chore: fix model hub sorting (#4722) * chore: fix model hub sorting * chore: linter fix * fix: should not select vulkan by default when there are Nvidia GPUs detected (#4720) * chore: enhance onboarding screen's models (#4723) * chore: enhance onboarding screen's models * chore: lint fix * chore: correct lint fix command * chore: fix tests * enhancement: scrollbar setting options (#4726) * enhancement: scrollbar setting options * chore: fix linter * refactor: clean up legacy predefined models (#4730) * refactor: clean up legacy predefined models * chore: fix onboarding filtered models * chore: correct channel name to ask for help * fix: flow app updater manual check from native menu (#4731) * chore: update hub UI (#4734) * chore: update hub ui based feedback * chore: update hub ui * chore: code block ui * chore: update bg color * chore: decrease margin codeblock * enhancement: improve chat thread (#4736) * enhancement: improve chat thread * chore: fix linter * fix: linter * chore: fix linter * fix: chore failed test * fix: chat body scrollbar (#4737) * fix: chat body scrollbar * chore: update lock file * fix: remove PluggableList * fix: onboarding screen should show persisted cloud providers (#4738) * chore: remove hard coded recommendation models and use cortexso featured tags (#4741) * chore: remove hard coded recommendation models and use cortexso featured tags * chore: polish model detail page * chore: fix test * fix: hub ui no result search found (#4739) * fix: hub button download (#4742) * fix: hub button download and use * chore: fix linter * fix: remove the button download on top of model page * enhancement: receommended label engine variant (#4740) * fix: correct default engine variant setup on first launch (#4747) * fix: there is a case where app selects incorrect engine variant first launch * refactor: clean up legacy settings hook * fix: cannot click dropdown appearance (#4750) * fix: app check available update (#4751) * fix: some endpoints are invisible (#4752) * chore: correct model author with new cortexso update (#4754) * chore: hub UI tooltip filter, max model size and search result (#4753) * chore: fix hub ui tooltip, max-filter, and search result * chore: fix linter * fix: deeplink does not work sometime (#4755) * fix: deep link does not work sometime and reduce redundant request * chore: bump cortex fix * typo fix (#4748) * chore: handle list number and disc readme hgf (#4756) * chore: handle list number and disc readme hgf * chore: fix space hover message toolbar * chore: handle reset state filter hub revamp * fix: Jan Quick Ask window capture input issues (#4758) * fix: first message padding is off * fix: correct jan discord channel * feat: add openai 4.5 preview and anthropic claude 3.7 sonnet models * fix: image upload button does not work - refresh models list persist current selected engine (#4768) * fix: list space styled and hidden message toolbar when editmode (#4773) * refactor: clean up deprecated components and events (#4769) * fix: quickask ui color and alignment on mymodel list (#4774) * fix: quickask ui color and alignment on mymodel list * fix: color scheme quickask * chore: bump cortex version to fix model sources issue (#4775) * chore: sync initial hub models (#4778) * chore: sync initial hub models * fix: openai request template * fix: naming nightly and beta build (#4779) * fix: naming nightly and beta build * chore: enhance replace icons for beta and nightly build logic * chore: update icon linux build * chore: add debug step * chore: remove specific icon linux build * refactor: clean up legacy vision model settings (#4777) * analytics: integrate posthog into Jan web (#4788) * fix: engine version update - cortex version bump - update tests (#4787) * fix: anthropic response template correction * fix: image preview overlap toolbar message (#4790) * fix: cohere response template correction for proper markdown parsing * fix: hub UI issue render readme * chore: fix linter * fix: result search scrollable * fix: app image - could not load model * chore: bump cortex version (#4793) * enhancement: add checkmark selected model * fix: render desc hub model list * chore linter * fix: table markdown * chore: do not symlink engine on linux * fix: update career url * chore: correct cohere response template * chore: bump llama.cpp to support gemma3 * fix: minor ui issue * fix: scroll setting preferences * chore: fix padding * fix: clear logs should not delete the folder (#4806) * chore: bump cortex 1.0.11-rc10 * chore: bump to latest cortex release * feat: Cortex API Authorization * chore: correct CI CD repo name * chore: correct new menloresearch repo name * feat: rotate api token for each run (#4820) * feat: rotate api token for each run * chore: correct github repo url * chore: correct github api url * chore: should not filter out models first launch * chore: bump cortex release * chore: should get hardware information on launch (#4821) * chore: should have an option to not revalidate hardware information * chore: cortex.cpp gpu activation could cause a race condition (#4825) * fix: jan beta logo displayed in jan release (#4828) --------- Co-authored-by: Emre Can Kartal <159995642+eckartal@users.noreply.github.com> Co-authored-by: Faisal Amir <urmauur@gmail.com> Co-authored-by: Ashley <tuyethantt@gmail.com> Co-authored-by: Ashley <89722390+imtuyethan@users.noreply.github.com> Co-authored-by: ddri <davedri@gmail.com> Co-authored-by: Minh141120 <minh.itptit@gmail.com> Co-authored-by: Nguyen Ngoc Minh <91668012+Minh141120@users.noreply.github.com> Co-authored-by: Minh <david@menlo.ai> Co-authored-by: David <davidpt.janai@gmail.com> Co-authored-by: Matt <matthewbcool@gmail.com>
@ -18,6 +18,8 @@ jobs:
|
|||||||
public_provider: github
|
public_provider: github
|
||||||
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
||||||
beta: true
|
beta: true
|
||||||
|
nightly: false
|
||||||
|
cortex_api_port: "39271"
|
||||||
|
|
||||||
build-windows-x64:
|
build-windows-x64:
|
||||||
uses: ./.github/workflows/template-build-windows-x64.yml
|
uses: ./.github/workflows/template-build-windows-x64.yml
|
||||||
@ -28,6 +30,8 @@ jobs:
|
|||||||
public_provider: github
|
public_provider: github
|
||||||
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
||||||
beta: true
|
beta: true
|
||||||
|
nightly: false
|
||||||
|
cortex_api_port: "39271"
|
||||||
|
|
||||||
build-linux-x64:
|
build-linux-x64:
|
||||||
uses: ./.github/workflows/template-build-linux-x64.yml
|
uses: ./.github/workflows/template-build-linux-x64.yml
|
||||||
@ -38,6 +42,8 @@ jobs:
|
|||||||
public_provider: github
|
public_provider: github
|
||||||
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
||||||
beta: true
|
beta: true
|
||||||
|
nightly: false
|
||||||
|
cortex_api_port: "39271"
|
||||||
|
|
||||||
sync-temp-to-latest:
|
sync-temp-to-latest:
|
||||||
needs: [build-macos, build-windows-x64, build-linux-x64]
|
needs: [build-macos, build-windows-x64, build-linux-x64]
|
||||||
|
|||||||
12
.github/workflows/jan-electron-build-nightly.yml
vendored
@ -55,6 +55,9 @@ jobs:
|
|||||||
ref: ${{ needs.set-public-provider.outputs.ref }}
|
ref: ${{ needs.set-public-provider.outputs.ref }}
|
||||||
public_provider: ${{ needs.set-public-provider.outputs.public_provider }}
|
public_provider: ${{ needs.set-public-provider.outputs.public_provider }}
|
||||||
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
||||||
|
nightly: true
|
||||||
|
beta: false
|
||||||
|
cortex_api_port: "39261"
|
||||||
|
|
||||||
build-windows-x64:
|
build-windows-x64:
|
||||||
uses: ./.github/workflows/template-build-windows-x64.yml
|
uses: ./.github/workflows/template-build-windows-x64.yml
|
||||||
@ -64,8 +67,9 @@ jobs:
|
|||||||
ref: ${{ needs.set-public-provider.outputs.ref }}
|
ref: ${{ needs.set-public-provider.outputs.ref }}
|
||||||
public_provider: ${{ needs.set-public-provider.outputs.public_provider }}
|
public_provider: ${{ needs.set-public-provider.outputs.public_provider }}
|
||||||
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
||||||
|
nightly: true
|
||||||
|
beta: false
|
||||||
|
cortex_api_port: "39261"
|
||||||
build-linux-x64:
|
build-linux-x64:
|
||||||
uses: ./.github/workflows/template-build-linux-x64.yml
|
uses: ./.github/workflows/template-build-linux-x64.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
@ -74,6 +78,9 @@ jobs:
|
|||||||
ref: ${{ needs.set-public-provider.outputs.ref }}
|
ref: ${{ needs.set-public-provider.outputs.ref }}
|
||||||
public_provider: ${{ needs.set-public-provider.outputs.public_provider }}
|
public_provider: ${{ needs.set-public-provider.outputs.public_provider }}
|
||||||
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
||||||
|
nightly: true
|
||||||
|
beta: false
|
||||||
|
cortex_api_port: "39261"
|
||||||
|
|
||||||
sync-temp-to-latest:
|
sync-temp-to-latest:
|
||||||
needs: [set-public-provider, build-windows-x64, build-linux-x64, build-macos]
|
needs: [set-public-provider, build-windows-x64, build-linux-x64, build-macos]
|
||||||
@ -141,4 +148,3 @@ jobs:
|
|||||||
RUN_ID=${{ github.run_id }}
|
RUN_ID=${{ github.run_id }}
|
||||||
COMMENT="This is the build for this pull request. You can download it from the Artifacts section here: [Build URL](https://github.com/${{ github.repository }}/actions/runs/${RUN_ID})."
|
COMMENT="This is the build for this pull request. You can download it from the Artifacts section here: [Build URL](https://github.com/${{ github.repository }}/actions/runs/${RUN_ID})."
|
||||||
gh pr comment $PR_URL --body "$COMMENT"
|
gh pr comment $PR_URL --body "$COMMENT"
|
||||||
|
|
||||||
6
.github/workflows/jan-electron-build.yml
vendored
@ -40,6 +40,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
ref: ${{ github.ref }}
|
ref: ${{ github.ref }}
|
||||||
public_provider: github
|
public_provider: github
|
||||||
|
beta: false
|
||||||
|
nightly: false
|
||||||
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
||||||
|
|
||||||
build-windows-x64:
|
build-windows-x64:
|
||||||
@ -49,6 +51,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
ref: ${{ github.ref }}
|
ref: ${{ github.ref }}
|
||||||
public_provider: github
|
public_provider: github
|
||||||
|
beta: false
|
||||||
|
nightly: false
|
||||||
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
||||||
|
|
||||||
build-linux-x64:
|
build-linux-x64:
|
||||||
@ -58,6 +62,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
ref: ${{ github.ref }}
|
ref: ${{ github.ref }}
|
||||||
public_provider: github
|
public_provider: github
|
||||||
|
beta: false
|
||||||
|
nightly: false
|
||||||
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
||||||
|
|
||||||
update_release_draft:
|
update_release_draft:
|
||||||
|
|||||||
@ -36,7 +36,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.PAT_SERVICE_ACCOUNT }}
|
GITHUB_TOKEN: ${{ secrets.PAT_SERVICE_ACCOUNT }}
|
||||||
run: |
|
run: |
|
||||||
curl -s https://api.github.com/repos/janhq/cortex/releases > /tmp/github_api_releases.json
|
curl -s https://api.github.com/repos/menloresearch/cortex/releases > /tmp/github_api_releases.json
|
||||||
latest_prerelease_name=$(cat /tmp/github_api_releases.json | jq -r '.[] | select(.prerelease) | .name' | head -n 1)
|
latest_prerelease_name=$(cat /tmp/github_api_releases.json | jq -r '.[] | select(.prerelease) | .name' | head -n 1)
|
||||||
|
|
||||||
get_asset_count() {
|
get_asset_count() {
|
||||||
|
|||||||
34
.github/workflows/template-build-linux-x64.yml
vendored
@ -23,6 +23,14 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
|
nightly:
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
cortex_api_port:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: null
|
||||||
secrets:
|
secrets:
|
||||||
DELTA_AWS_S3_BUCKET_NAME:
|
DELTA_AWS_S3_BUCKET_NAME:
|
||||||
required: false
|
required: false
|
||||||
@ -43,6 +51,31 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
ref: ${{ inputs.ref }}
|
ref: ${{ inputs.ref }}
|
||||||
|
|
||||||
|
- name: Replace Icons for Beta Build
|
||||||
|
if: inputs.beta == true && inputs.nightly != true
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
rm -rf electron/icons/*
|
||||||
|
|
||||||
|
cp electron/icons_dev/jan-beta-512x512.png electron/icons/512x512.png
|
||||||
|
cp electron/icons_dev/jan-beta.ico electron/icons/icon.ico
|
||||||
|
cp electron/icons_dev/jan-beta.png electron/icons/icon.png
|
||||||
|
cp electron/icons_dev/jan-beta-tray@2x.png electron/icons/icon-tray@2x.png
|
||||||
|
cp electron/icons_dev/jan-beta-tray.png electron/icons/icon-tray.png
|
||||||
|
|
||||||
|
- name: Replace Icons for Nightly Build
|
||||||
|
if: inputs.nightly == true && inputs.beta != true
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
rm -rf electron/icons/*
|
||||||
|
|
||||||
|
cp electron/icons_dev/jan-nightly-512x512.png electron/icons/512x512.png
|
||||||
|
cp electron/icons_dev/jan-nightly.ico electron/icons/icon.ico
|
||||||
|
cp electron/icons_dev/jan-nightly.png electron/icons/icon.png
|
||||||
|
cp electron/icons_dev/jan-nightly-tray@2x.png electron/icons/icon-tray@2x.png
|
||||||
|
cp electron/icons_dev/jan-nightly-tray.png electron/icons/icon-tray.png
|
||||||
|
|
||||||
|
|
||||||
- name: Installing node
|
- name: Installing node
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
@ -115,6 +148,7 @@ jobs:
|
|||||||
AWS_MAX_ATTEMPTS: '5'
|
AWS_MAX_ATTEMPTS: '5'
|
||||||
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
|
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
|
||||||
POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
|
POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
|
||||||
|
CORTEX_API_PORT: ${{ inputs.cortex_api_port }}
|
||||||
|
|
||||||
- name: Build and publish app to github
|
- name: Build and publish app to github
|
||||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == false
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == false
|
||||||
|
|||||||
33
.github/workflows/template-build-macos.yml
vendored
@ -23,6 +23,14 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
|
nightly:
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
cortex_api_port:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: null
|
||||||
secrets:
|
secrets:
|
||||||
DELTA_AWS_S3_BUCKET_NAME:
|
DELTA_AWS_S3_BUCKET_NAME:
|
||||||
required: false
|
required: false
|
||||||
@ -53,6 +61,30 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
ref: ${{ inputs.ref }}
|
ref: ${{ inputs.ref }}
|
||||||
|
|
||||||
|
- name: Replace Icons for Beta Build
|
||||||
|
if: inputs.beta == true && inputs.nightly != true
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
rm -rf electron/icons/*
|
||||||
|
|
||||||
|
cp electron/icons_dev/jan-beta-512x512.png electron/icons/512x512.png
|
||||||
|
cp electron/icons_dev/jan-beta.ico electron/icons/icon.ico
|
||||||
|
cp electron/icons_dev/jan-beta.png electron/icons/icon.png
|
||||||
|
cp electron/icons_dev/jan-beta-tray@2x.png electron/icons/icon-tray@2x.png
|
||||||
|
cp electron/icons_dev/jan-beta-tray.png electron/icons/icon-tray.png
|
||||||
|
|
||||||
|
- name: Replace Icons for Nightly Build
|
||||||
|
if: inputs.nightly == true && inputs.beta != true
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
rm -rf electron/icons/*
|
||||||
|
|
||||||
|
cp electron/icons_dev/jan-nightly-512x512.png electron/icons/512x512.png
|
||||||
|
cp electron/icons_dev/jan-nightly.ico electron/icons/icon.ico
|
||||||
|
cp electron/icons_dev/jan-nightly.png electron/icons/icon.png
|
||||||
|
cp electron/icons_dev/jan-nightly-tray@2x.png electron/icons/icon-tray@2x.png
|
||||||
|
cp electron/icons_dev/jan-nightly-tray.png electron/icons/icon-tray.png
|
||||||
|
|
||||||
- name: Installing node
|
- name: Installing node
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
@ -154,6 +186,7 @@ jobs:
|
|||||||
AWS_MAX_ATTEMPTS: '5'
|
AWS_MAX_ATTEMPTS: '5'
|
||||||
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
|
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
|
||||||
POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
|
POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
|
||||||
|
CORTEX_API_PORT: ${{ inputs.cortex_api_port }}
|
||||||
|
|
||||||
- name: Build and publish app to github
|
- name: Build and publish app to github
|
||||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == false
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == false
|
||||||
|
|||||||
33
.github/workflows/template-build-windows-x64.yml
vendored
@ -23,6 +23,14 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
|
nightly:
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
cortex_api_port:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: null
|
||||||
secrets:
|
secrets:
|
||||||
DELTA_AWS_S3_BUCKET_NAME:
|
DELTA_AWS_S3_BUCKET_NAME:
|
||||||
required: false
|
required: false
|
||||||
@ -52,6 +60,30 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
ref: ${{ inputs.ref }}
|
ref: ${{ inputs.ref }}
|
||||||
|
|
||||||
|
- name: Replace Icons for Beta Build
|
||||||
|
if: inputs.beta == true && inputs.nightly != true
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
rm -rf electron/icons/*
|
||||||
|
|
||||||
|
cp electron/icons_dev/jan-beta-512x512.png electron/icons/512x512.png
|
||||||
|
cp electron/icons_dev/jan-beta.ico electron/icons/icon.ico
|
||||||
|
cp electron/icons_dev/jan-beta.png electron/icons/icon.png
|
||||||
|
cp electron/icons_dev/jan-beta-tray@2x.png electron/icons/icon-tray@2x.png
|
||||||
|
cp electron/icons_dev/jan-beta-tray.png electron/icons/icon-tray.png
|
||||||
|
|
||||||
|
- name: Replace Icons for Nightly Build
|
||||||
|
if: inputs.nightly == true && inputs.beta != true
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
rm -rf electron/icons/*
|
||||||
|
|
||||||
|
cp electron/icons_dev/jan-nightly-512x512.png electron/icons/512x512.png
|
||||||
|
cp electron/icons_dev/jan-nightly.ico electron/icons/icon.ico
|
||||||
|
cp electron/icons_dev/jan-nightly.png electron/icons/icon.png
|
||||||
|
cp electron/icons_dev/jan-nightly-tray@2x.png electron/icons/icon-tray@2x.png
|
||||||
|
cp electron/icons_dev/jan-nightly-tray.png electron/icons/icon-tray.png
|
||||||
|
|
||||||
- name: Installing node
|
- name: Installing node
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
@ -153,6 +185,7 @@ jobs:
|
|||||||
AWS_MAX_ATTEMPTS: '5'
|
AWS_MAX_ATTEMPTS: '5'
|
||||||
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
|
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
|
||||||
POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
|
POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
|
||||||
|
CORTEX_API_PORT: ${{ inputs.cortex_api_port }}
|
||||||
|
|
||||||
- name: Build app and publish app to github
|
- name: Build app and publish app to github
|
||||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == false
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == false
|
||||||
|
|||||||
@ -30,7 +30,7 @@ jobs:
|
|||||||
local max_retries=3
|
local max_retries=3
|
||||||
local tag
|
local tag
|
||||||
while [ $retries -lt $max_retries ]; do
|
while [ $retries -lt $max_retries ]; do
|
||||||
tag=$(curl -s https://api.github.com/repos/janhq/jan/releases/latest | jq -r .tag_name)
|
tag=$(curl -s https://api.github.com/repos/menloresearch/jan/releases/latest | jq -r .tag_name)
|
||||||
if [ -n "$tag" ] && [ "$tag" != "null" ]; then
|
if [ -n "$tag" ] && [ "$tag" != "null" ]; then
|
||||||
echo $tag
|
echo $tag
|
||||||
return
|
return
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
npx oxlint@latest --fix
|
yarn lint --fix --quiet
|
||||||
@ -25,7 +25,6 @@ export default defineConfig([
|
|||||||
'@types/pacote',
|
'@types/pacote',
|
||||||
'@npmcli/arborist',
|
'@npmcli/arborist',
|
||||||
'ulidx',
|
'ulidx',
|
||||||
'node-fetch',
|
|
||||||
'fs',
|
'fs',
|
||||||
'request',
|
'request',
|
||||||
'crypto',
|
'crypto',
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { openExternalUrl } from './core'
|
|||||||
import { joinPath } from './core'
|
import { joinPath } from './core'
|
||||||
import { openFileExplorer } from './core'
|
import { openFileExplorer } from './core'
|
||||||
import { getJanDataFolderPath } from './core'
|
import { getJanDataFolderPath } from './core'
|
||||||
import { abortDownload } from './core'
|
|
||||||
import { executeOnMain } from './core'
|
import { executeOnMain } from './core'
|
||||||
|
|
||||||
describe('test core apis', () => {
|
describe('test core apis', () => {
|
||||||
@ -53,18 +52,6 @@ describe('test core apis', () => {
|
|||||||
expect(result).toBe('/path/to/jan/data')
|
expect(result).toBe('/path/to/jan/data')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should abort download', async () => {
|
|
||||||
const fileName = 'testFile'
|
|
||||||
globalThis.core = {
|
|
||||||
api: {
|
|
||||||
abortDownload: jest.fn().mockResolvedValue('aborted'),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const result = await abortDownload(fileName)
|
|
||||||
expect(globalThis.core.api.abortDownload).toHaveBeenCalledWith(fileName)
|
|
||||||
expect(result).toBe('aborted')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should execute function on main process', async () => {
|
it('should execute function on main process', async () => {
|
||||||
const extension = 'testExtension'
|
const extension = 'testExtension'
|
||||||
const method = 'testMethod'
|
const method = 'testMethod'
|
||||||
|
|||||||
@ -1,9 +1,4 @@
|
|||||||
import {
|
import { SystemInformation } from '../types'
|
||||||
DownloadRequest,
|
|
||||||
FileStat,
|
|
||||||
NetworkConfig,
|
|
||||||
SystemInformation,
|
|
||||||
} from '../types'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute a extension module function in main process
|
* Execute a extension module function in main process
|
||||||
@ -14,42 +9,19 @@ import {
|
|||||||
* @returns Promise<any>
|
* @returns Promise<any>
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const executeOnMain: (
|
const executeOnMain: (extension: string, method: string, ...args: any[]) => Promise<any> = (
|
||||||
extension: string,
|
extension,
|
||||||
method: string,
|
method,
|
||||||
...args: any[]
|
...args
|
||||||
) => Promise<any> = (extension, method, ...args) =>
|
) => globalThis.core?.api?.invokeExtensionFunc(extension, method, ...args)
|
||||||
globalThis.core?.api?.invokeExtensionFunc(extension, method, ...args)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Downloads a file from a URL and saves it to the local file system.
|
|
||||||
*
|
|
||||||
* @param {DownloadRequest} downloadRequest - The request to download the file.
|
|
||||||
* @param {NetworkConfig} network - Optional object to specify proxy/whether to ignore SSL certificates.
|
|
||||||
*
|
|
||||||
* @returns {Promise<any>} A promise that resolves when the file is downloaded.
|
|
||||||
*/
|
|
||||||
const downloadFile: (
|
|
||||||
downloadRequest: DownloadRequest,
|
|
||||||
network?: NetworkConfig
|
|
||||||
) => Promise<any> = (downloadRequest, network) =>
|
|
||||||
globalThis.core?.api?.downloadFile(downloadRequest, network)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Aborts the download of a specific file.
|
|
||||||
* @param {string} fileName - The name of the file whose download is to be aborted.
|
|
||||||
* @returns {Promise<any>} A promise that resolves when the download has been aborted.
|
|
||||||
*/
|
|
||||||
const abortDownload: (fileName: string) => Promise<any> = (fileName) =>
|
|
||||||
globalThis.core.api?.abortDownload(fileName)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets Jan's data folder path.
|
* Gets Jan's data folder path.
|
||||||
*
|
*
|
||||||
* @returns {Promise<string>} A Promise that resolves with Jan's data folder path.
|
* @returns {Promise<string>} A Promise that resolves with Jan's data folder path.
|
||||||
*/
|
*/
|
||||||
const getJanDataFolderPath = (): Promise<string> =>
|
const getJanDataFolderPath = (): Promise<string> => globalThis.core.api?.getJanDataFolderPath()
|
||||||
globalThis.core.api?.getJanDataFolderPath()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the file explorer at a specific path.
|
* Opens the file explorer at a specific path.
|
||||||
@ -72,16 +44,14 @@ const joinPath: (paths: string[]) => Promise<string> = (paths) =>
|
|||||||
* @param path - The file path to retrieve dirname.
|
* @param path - The file path to retrieve dirname.
|
||||||
* @returns {Promise<string>} A promise that resolves the dirname.
|
* @returns {Promise<string>} A promise that resolves the dirname.
|
||||||
*/
|
*/
|
||||||
const dirName: (path: string) => Promise<string> = (path) =>
|
const dirName: (path: string) => Promise<string> = (path) => globalThis.core.api?.dirName(path)
|
||||||
globalThis.core.api?.dirName(path)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the basename from an url.
|
* Retrieve the basename from an url.
|
||||||
* @param path - The path to retrieve.
|
* @param path - The path to retrieve.
|
||||||
* @returns {Promise<string>} A promise that resolves with the basename.
|
* @returns {Promise<string>} A promise that resolves with the basename.
|
||||||
*/
|
*/
|
||||||
const baseName: (paths: string) => Promise<string> = (path) =>
|
const baseName: (paths: string) => Promise<string> = (path) => globalThis.core.api?.baseName(path)
|
||||||
globalThis.core.api?.baseName(path)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens an external URL in the default web browser.
|
* Opens an external URL in the default web browser.
|
||||||
@ -97,15 +67,13 @@ const openExternalUrl: (url: string) => Promise<any> = (url) =>
|
|||||||
*
|
*
|
||||||
* @returns {Promise<string>} - A promise that resolves with the resource path.
|
* @returns {Promise<string>} - A promise that resolves with the resource path.
|
||||||
*/
|
*/
|
||||||
const getResourcePath: () => Promise<string> = () =>
|
const getResourcePath: () => Promise<string> = () => globalThis.core.api?.getResourcePath()
|
||||||
globalThis.core.api?.getResourcePath()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the user's home path.
|
* Gets the user's home path.
|
||||||
* @returns return user's home path
|
* @returns return user's home path
|
||||||
*/
|
*/
|
||||||
const getUserHomePath = (): Promise<string> =>
|
const getUserHomePath = (): Promise<string> => globalThis.core.api?.getUserHomePath()
|
||||||
globalThis.core.api?.getUserHomePath()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log to file from browser processes.
|
* Log to file from browser processes.
|
||||||
@ -123,10 +91,8 @@ const log: (message: string, fileName?: string) => void = (message, fileName) =>
|
|||||||
*
|
*
|
||||||
* @returns {Promise<boolean>} - A promise that resolves with a boolean indicating whether the path is a subdirectory.
|
* @returns {Promise<boolean>} - A promise that resolves with a boolean indicating whether the path is a subdirectory.
|
||||||
*/
|
*/
|
||||||
const isSubdirectory: (from: string, to: string) => Promise<boolean> = (
|
const isSubdirectory: (from: string, to: string) => Promise<boolean> = (from: string, to: string) =>
|
||||||
from: string,
|
globalThis.core.api?.isSubdirectory(from, to)
|
||||||
to: string
|
|
||||||
) => globalThis.core.api?.isSubdirectory(from, to)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get system information
|
* Get system information
|
||||||
@ -159,8 +125,6 @@ export type RegisterExtensionPoint = (
|
|||||||
*/
|
*/
|
||||||
export {
|
export {
|
||||||
executeOnMain,
|
executeOnMain,
|
||||||
downloadFile,
|
|
||||||
abortDownload,
|
|
||||||
getJanDataFolderPath,
|
getJanDataFolderPath,
|
||||||
openFileExplorer,
|
openFileExplorer,
|
||||||
getResourcePath,
|
getResourcePath,
|
||||||
|
|||||||
@ -39,11 +39,6 @@ describe('BaseExtension', () => {
|
|||||||
expect(baseExtension.onUnload).toBeDefined()
|
expect(baseExtension.onUnload).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should have installationState() return "NotRequired"', async () => {
|
|
||||||
const installationState = await baseExtension.installationState()
|
|
||||||
expect(installationState).toBe('NotRequired')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should install the extension', async () => {
|
it('should install the extension', async () => {
|
||||||
await baseExtension.install()
|
await baseExtension.install()
|
||||||
// Add your assertions here
|
// Add your assertions here
|
||||||
@ -84,11 +79,6 @@ describe('BaseExtension', () => {
|
|||||||
expect(baseExtension.onUnload).toBeDefined()
|
expect(baseExtension.onUnload).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should have installationState() return "NotRequired"', async () => {
|
|
||||||
const installationState = await baseExtension.installationState()
|
|
||||||
expect(installationState).toBe('NotRequired')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should install the extension', async () => {
|
it('should install the extension', async () => {
|
||||||
await baseExtension.install()
|
await baseExtension.install()
|
||||||
// Add your assertions here
|
// Add your assertions here
|
||||||
|
|||||||
@ -24,17 +24,6 @@ export interface Compatibility {
|
|||||||
version: string
|
version: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const ALL_INSTALLATION_STATE = [
|
|
||||||
'NotRequired', // not required.
|
|
||||||
'Installed', // require and installed. Good to go.
|
|
||||||
'NotInstalled', // require to be installed.
|
|
||||||
'Corrupted', // require but corrupted. Need to redownload.
|
|
||||||
'NotCompatible', // require but not compatible.
|
|
||||||
] as const
|
|
||||||
|
|
||||||
export type InstallationStateTuple = typeof ALL_INSTALLATION_STATE
|
|
||||||
export type InstallationState = InstallationStateTuple[number]
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a base extension.
|
* Represents a base extension.
|
||||||
* This class should be extended by any class that represents an extension.
|
* This class should be extended by any class that represents an extension.
|
||||||
@ -175,15 +164,6 @@ export abstract class BaseExtension implements ExtensionType {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if the prerequisites for the extension are installed.
|
|
||||||
*
|
|
||||||
* @returns {boolean} true if the prerequisites are installed, false otherwise.
|
|
||||||
*/
|
|
||||||
async installationState(): Promise<InstallationState> {
|
|
||||||
return 'NotRequired'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Install the prerequisites for the extension.
|
* Install the prerequisites for the extension.
|
||||||
*
|
*
|
||||||
@ -228,7 +208,7 @@ export abstract class BaseExtension implements ExtensionType {
|
|||||||
|
|
||||||
const settings = await this.getSettings()
|
const settings = await this.getSettings()
|
||||||
|
|
||||||
const updatedSettings = settings.map((setting) => {
|
let updatedSettings = settings.map((setting) => {
|
||||||
const updatedSetting = componentProps.find(
|
const updatedSetting = componentProps.find(
|
||||||
(componentProp) => componentProp.key === setting.key
|
(componentProp) => componentProp.key === setting.key
|
||||||
)
|
)
|
||||||
@ -238,13 +218,20 @@ export abstract class BaseExtension implements ExtensionType {
|
|||||||
return setting
|
return setting
|
||||||
})
|
})
|
||||||
|
|
||||||
const settingPath = await joinPath([
|
if (!updatedSettings.length) updatedSettings = componentProps as SettingComponentProps[]
|
||||||
|
|
||||||
|
const settingFolder = await joinPath([
|
||||||
await getJanDataFolderPath(),
|
await getJanDataFolderPath(),
|
||||||
this.settingFolderName,
|
this.settingFolderName,
|
||||||
this.name,
|
this.name,
|
||||||
this.settingFileName,
|
|
||||||
])
|
])
|
||||||
|
|
||||||
|
if (!(await fs.existsSync(settingFolder))) {
|
||||||
|
await fs.mkdir(settingFolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingPath = await joinPath([settingFolder, this.settingFileName])
|
||||||
|
|
||||||
await fs.writeFileSync(settingPath, JSON.stringify(updatedSettings, null, 2))
|
await fs.writeFileSync(settingPath, JSON.stringify(updatedSettings, null, 2))
|
||||||
|
|
||||||
updatedSettings.forEach((setting) => {
|
updatedSettings.forEach((setting) => {
|
||||||
|
|||||||
252
core/src/browser/extensions/conversational.test.ts
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
import { ConversationalExtension } from './conversational'
|
||||||
|
import { ExtensionTypeEnum } from '../extension'
|
||||||
|
import { Thread, ThreadAssistantInfo, ThreadMessage } from '../../types'
|
||||||
|
|
||||||
|
// Mock implementation of ConversationalExtension
|
||||||
|
class MockConversationalExtension extends ConversationalExtension {
|
||||||
|
private threads: Thread[] = []
|
||||||
|
private messages: { [threadId: string]: ThreadMessage[] } = {}
|
||||||
|
private assistants: { [threadId: string]: ThreadAssistantInfo } = {}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super('http://mock-url.com', 'mock-extension', 'Mock Extension', true, 'A mock extension', '1.0.0')
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad(): void {
|
||||||
|
// Mock implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnload(): void {
|
||||||
|
// Mock implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
async listThreads(): Promise<Thread[]> {
|
||||||
|
return this.threads
|
||||||
|
}
|
||||||
|
|
||||||
|
async createThread(thread: Partial<Thread>): Promise<Thread> {
|
||||||
|
const newThread: Thread = {
|
||||||
|
id: thread.id || `thread-${Date.now()}`,
|
||||||
|
name: thread.name || 'New Thread',
|
||||||
|
createdAt: thread.createdAt || new Date().toISOString(),
|
||||||
|
updatedAt: thread.updatedAt || new Date().toISOString(),
|
||||||
|
}
|
||||||
|
this.threads.push(newThread)
|
||||||
|
this.messages[newThread.id] = []
|
||||||
|
return newThread
|
||||||
|
}
|
||||||
|
|
||||||
|
async modifyThread(thread: Thread): Promise<void> {
|
||||||
|
const index = this.threads.findIndex(t => t.id === thread.id)
|
||||||
|
if (index !== -1) {
|
||||||
|
this.threads[index] = thread
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteThread(threadId: string): Promise<void> {
|
||||||
|
this.threads = this.threads.filter(t => t.id !== threadId)
|
||||||
|
delete this.messages[threadId]
|
||||||
|
delete this.assistants[threadId]
|
||||||
|
}
|
||||||
|
|
||||||
|
async createMessage(message: Partial<ThreadMessage>): Promise<ThreadMessage> {
|
||||||
|
if (!message.threadId) throw new Error('Thread ID is required')
|
||||||
|
|
||||||
|
const newMessage: ThreadMessage = {
|
||||||
|
id: message.id || `message-${Date.now()}`,
|
||||||
|
threadId: message.threadId,
|
||||||
|
content: message.content || '',
|
||||||
|
role: message.role || 'user',
|
||||||
|
createdAt: message.createdAt || new Date().toISOString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.messages[message.threadId]) {
|
||||||
|
this.messages[message.threadId] = []
|
||||||
|
}
|
||||||
|
|
||||||
|
this.messages[message.threadId].push(newMessage)
|
||||||
|
return newMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteMessage(threadId: string, messageId: string): Promise<void> {
|
||||||
|
if (this.messages[threadId]) {
|
||||||
|
this.messages[threadId] = this.messages[threadId].filter(m => m.id !== messageId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async listMessages(threadId: string): Promise<ThreadMessage[]> {
|
||||||
|
return this.messages[threadId] || []
|
||||||
|
}
|
||||||
|
|
||||||
|
async getThreadAssistant(threadId: string): Promise<ThreadAssistantInfo> {
|
||||||
|
return this.assistants[threadId] || { modelId: '', threadId }
|
||||||
|
}
|
||||||
|
|
||||||
|
async createThreadAssistant(
|
||||||
|
threadId: string,
|
||||||
|
assistant: ThreadAssistantInfo
|
||||||
|
): Promise<ThreadAssistantInfo> {
|
||||||
|
this.assistants[threadId] = assistant
|
||||||
|
return assistant
|
||||||
|
}
|
||||||
|
|
||||||
|
async modifyThreadAssistant(
|
||||||
|
threadId: string,
|
||||||
|
assistant: ThreadAssistantInfo
|
||||||
|
): Promise<ThreadAssistantInfo> {
|
||||||
|
this.assistants[threadId] = assistant
|
||||||
|
return assistant
|
||||||
|
}
|
||||||
|
|
||||||
|
async modifyMessage(message: ThreadMessage): Promise<ThreadMessage> {
|
||||||
|
if (!this.messages[message.threadId]) return message
|
||||||
|
|
||||||
|
const index = this.messages[message.threadId].findIndex(m => m.id === message.id)
|
||||||
|
if (index !== -1) {
|
||||||
|
this.messages[message.threadId][index] = message
|
||||||
|
}
|
||||||
|
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ConversationalExtension', () => {
|
||||||
|
let extension: MockConversationalExtension
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
extension = new MockConversationalExtension()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should return the correct extension type', () => {
|
||||||
|
expect(extension.type()).toBe(ExtensionTypeEnum.Conversational)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should create and list threads', async () => {
|
||||||
|
const thread = await extension.createThread({ name: 'Test Thread' })
|
||||||
|
expect(thread.name).toBe('Test Thread')
|
||||||
|
|
||||||
|
const threads = await extension.listThreads()
|
||||||
|
expect(threads).toHaveLength(1)
|
||||||
|
expect(threads[0].id).toBe(thread.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should modify thread', async () => {
|
||||||
|
const thread = await extension.createThread({ name: 'Test Thread' })
|
||||||
|
const modifiedThread = { ...thread, name: 'Modified Thread' }
|
||||||
|
|
||||||
|
await extension.modifyThread(modifiedThread)
|
||||||
|
|
||||||
|
const threads = await extension.listThreads()
|
||||||
|
expect(threads[0].name).toBe('Modified Thread')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should delete thread', async () => {
|
||||||
|
const thread = await extension.createThread({ name: 'Test Thread' })
|
||||||
|
|
||||||
|
await extension.deleteThread(thread.id)
|
||||||
|
|
||||||
|
const threads = await extension.listThreads()
|
||||||
|
expect(threads).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should create and list messages', async () => {
|
||||||
|
const thread = await extension.createThread({ name: 'Test Thread' })
|
||||||
|
|
||||||
|
const message = await extension.createMessage({
|
||||||
|
threadId: thread.id,
|
||||||
|
content: 'Test message',
|
||||||
|
role: 'user'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(message.content).toBe('Test message')
|
||||||
|
|
||||||
|
const messages = await extension.listMessages(thread.id)
|
||||||
|
expect(messages).toHaveLength(1)
|
||||||
|
expect(messages[0].id).toBe(message.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should modify message', async () => {
|
||||||
|
const thread = await extension.createThread({ name: 'Test Thread' })
|
||||||
|
|
||||||
|
const message = await extension.createMessage({
|
||||||
|
threadId: thread.id,
|
||||||
|
content: 'Test message',
|
||||||
|
role: 'user'
|
||||||
|
})
|
||||||
|
|
||||||
|
const modifiedMessage = { ...message, content: 'Modified message' }
|
||||||
|
|
||||||
|
await extension.modifyMessage(modifiedMessage)
|
||||||
|
|
||||||
|
const messages = await extension.listMessages(thread.id)
|
||||||
|
expect(messages[0].content).toBe('Modified message')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should delete message', async () => {
|
||||||
|
const thread = await extension.createThread({ name: 'Test Thread' })
|
||||||
|
|
||||||
|
const message = await extension.createMessage({
|
||||||
|
threadId: thread.id,
|
||||||
|
content: 'Test message',
|
||||||
|
role: 'user'
|
||||||
|
})
|
||||||
|
|
||||||
|
await extension.deleteMessage(thread.id, message.id)
|
||||||
|
|
||||||
|
const messages = await extension.listMessages(thread.id)
|
||||||
|
expect(messages).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should create and get thread assistant', async () => {
|
||||||
|
const thread = await extension.createThread({ name: 'Test Thread' })
|
||||||
|
|
||||||
|
const assistant: ThreadAssistantInfo = {
|
||||||
|
threadId: thread.id,
|
||||||
|
modelId: 'test-model'
|
||||||
|
}
|
||||||
|
|
||||||
|
await extension.createThreadAssistant(thread.id, assistant)
|
||||||
|
|
||||||
|
const retrievedAssistant = await extension.getThreadAssistant(thread.id)
|
||||||
|
expect(retrievedAssistant.modelId).toBe('test-model')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should modify thread assistant', async () => {
|
||||||
|
const thread = await extension.createThread({ name: 'Test Thread' })
|
||||||
|
|
||||||
|
const assistant: ThreadAssistantInfo = {
|
||||||
|
threadId: thread.id,
|
||||||
|
modelId: 'test-model'
|
||||||
|
}
|
||||||
|
|
||||||
|
await extension.createThreadAssistant(thread.id, assistant)
|
||||||
|
|
||||||
|
const modifiedAssistant: ThreadAssistantInfo = {
|
||||||
|
threadId: thread.id,
|
||||||
|
modelId: 'modified-model'
|
||||||
|
}
|
||||||
|
|
||||||
|
await extension.modifyThreadAssistant(thread.id, modifiedAssistant)
|
||||||
|
|
||||||
|
const retrievedAssistant = await extension.getThreadAssistant(thread.id)
|
||||||
|
expect(retrievedAssistant.modelId).toBe('modified-model')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should delete thread assistant when thread is deleted', async () => {
|
||||||
|
const thread = await extension.createThread({ name: 'Test Thread' })
|
||||||
|
|
||||||
|
const assistant: ThreadAssistantInfo = {
|
||||||
|
threadId: thread.id,
|
||||||
|
modelId: 'test-model'
|
||||||
|
}
|
||||||
|
|
||||||
|
await extension.createThreadAssistant(thread.id, assistant)
|
||||||
|
await extension.deleteThread(thread.id)
|
||||||
|
|
||||||
|
// Creating a new thread with the same ID to test if assistant was deleted
|
||||||
|
const newThread = await extension.createThread({ id: thread.id, name: 'New Thread' })
|
||||||
|
const retrievedAssistant = await extension.getThreadAssistant(newThread.id)
|
||||||
|
|
||||||
|
expect(retrievedAssistant.modelId).toBe('')
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -3,6 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
import { EngineManager } from './EngineManager'
|
import { EngineManager } from './EngineManager'
|
||||||
import { AIEngine } from './AIEngine'
|
import { AIEngine } from './AIEngine'
|
||||||
|
import { InferenceEngine } from '../../../types'
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
class MockAIEngine implements AIEngine {
|
class MockAIEngine implements AIEngine {
|
||||||
@ -40,4 +41,69 @@ describe('EngineManager', () => {
|
|||||||
const retrievedEngine = engineManager.get<MockAIEngine>('nonExistentProvider')
|
const retrievedEngine = engineManager.get<MockAIEngine>('nonExistentProvider')
|
||||||
expect(retrievedEngine).toBeUndefined()
|
expect(retrievedEngine).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('cortex engine migration', () => {
|
||||||
|
test('should map nitro to cortex engine', () => {
|
||||||
|
const cortexEngine = new MockAIEngine(InferenceEngine.cortex)
|
||||||
|
// @ts-ignore
|
||||||
|
engineManager.register(cortexEngine)
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const retrievedEngine = engineManager.get<MockAIEngine>(InferenceEngine.nitro)
|
||||||
|
expect(retrievedEngine).toBe(cortexEngine)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should map cortex_llamacpp to cortex engine', () => {
|
||||||
|
const cortexEngine = new MockAIEngine(InferenceEngine.cortex)
|
||||||
|
// @ts-ignore
|
||||||
|
engineManager.register(cortexEngine)
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const retrievedEngine = engineManager.get<MockAIEngine>(InferenceEngine.cortex_llamacpp)
|
||||||
|
expect(retrievedEngine).toBe(cortexEngine)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should map cortex_onnx to cortex engine', () => {
|
||||||
|
const cortexEngine = new MockAIEngine(InferenceEngine.cortex)
|
||||||
|
// @ts-ignore
|
||||||
|
engineManager.register(cortexEngine)
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const retrievedEngine = engineManager.get<MockAIEngine>(InferenceEngine.cortex_onnx)
|
||||||
|
expect(retrievedEngine).toBe(cortexEngine)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should map cortex_tensorrtllm to cortex engine', () => {
|
||||||
|
const cortexEngine = new MockAIEngine(InferenceEngine.cortex)
|
||||||
|
// @ts-ignore
|
||||||
|
engineManager.register(cortexEngine)
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const retrievedEngine = engineManager.get<MockAIEngine>(InferenceEngine.cortex_tensorrtllm)
|
||||||
|
expect(retrievedEngine).toBe(cortexEngine)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('singleton instance', () => {
|
||||||
|
test('should return the window.core.engineManager if available', () => {
|
||||||
|
const mockEngineManager = new EngineManager()
|
||||||
|
// @ts-ignore
|
||||||
|
window.core = { engineManager: mockEngineManager }
|
||||||
|
|
||||||
|
const instance = EngineManager.instance()
|
||||||
|
expect(instance).toBe(mockEngineManager)
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
// @ts-ignore
|
||||||
|
delete window.core
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should create a new instance if window.core.engineManager is not available', () => {
|
||||||
|
// @ts-ignore
|
||||||
|
delete window.core
|
||||||
|
|
||||||
|
const instance = EngineManager.instance()
|
||||||
|
expect(instance).toBeInstanceOf(EngineManager)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
566
core/src/browser/extensions/enginesManagement.test.ts
Normal file
@ -0,0 +1,566 @@
|
|||||||
|
import { EngineManagementExtension } from './enginesManagement'
|
||||||
|
import { ExtensionTypeEnum } from '../extension'
|
||||||
|
import {
|
||||||
|
EngineConfig,
|
||||||
|
EngineReleased,
|
||||||
|
EngineVariant,
|
||||||
|
Engines,
|
||||||
|
InferenceEngine,
|
||||||
|
DefaultEngineVariant,
|
||||||
|
Model
|
||||||
|
} from '../../types'
|
||||||
|
|
||||||
|
// Mock implementation of EngineManagementExtension
|
||||||
|
class MockEngineManagementExtension extends EngineManagementExtension {
|
||||||
|
private mockEngines: Engines = {
|
||||||
|
llama: {
|
||||||
|
name: 'llama',
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
variant: 'cpu',
|
||||||
|
version: '1.0.0',
|
||||||
|
path: '/engines/llama/cpu/1.0.0',
|
||||||
|
installed: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant: 'cuda',
|
||||||
|
version: '1.0.0',
|
||||||
|
path: '/engines/llama/cuda/1.0.0',
|
||||||
|
installed: false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
default: {
|
||||||
|
variant: 'cpu',
|
||||||
|
version: '1.0.0'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
gpt4all: {
|
||||||
|
name: 'gpt4all',
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
variant: 'cpu',
|
||||||
|
version: '2.0.0',
|
||||||
|
path: '/engines/gpt4all/cpu/2.0.0',
|
||||||
|
installed: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
default: {
|
||||||
|
variant: 'cpu',
|
||||||
|
version: '2.0.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private mockReleases: { [key: string]: EngineReleased[] } = {
|
||||||
|
'llama-1.0.0': [
|
||||||
|
{
|
||||||
|
variant: 'cpu',
|
||||||
|
version: '1.0.0',
|
||||||
|
os: ['macos', 'linux', 'windows'],
|
||||||
|
url: 'https://example.com/llama/1.0.0/cpu'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant: 'cuda',
|
||||||
|
version: '1.0.0',
|
||||||
|
os: ['linux', 'windows'],
|
||||||
|
url: 'https://example.com/llama/1.0.0/cuda'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'llama-1.1.0': [
|
||||||
|
{
|
||||||
|
variant: 'cpu',
|
||||||
|
version: '1.1.0',
|
||||||
|
os: ['macos', 'linux', 'windows'],
|
||||||
|
url: 'https://example.com/llama/1.1.0/cpu'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant: 'cuda',
|
||||||
|
version: '1.1.0',
|
||||||
|
os: ['linux', 'windows'],
|
||||||
|
url: 'https://example.com/llama/1.1.0/cuda'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'gpt4all-2.0.0': [
|
||||||
|
{
|
||||||
|
variant: 'cpu',
|
||||||
|
version: '2.0.0',
|
||||||
|
os: ['macos', 'linux', 'windows'],
|
||||||
|
url: 'https://example.com/gpt4all/2.0.0/cpu'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private remoteModels: { [engine: string]: Model[] } = {
|
||||||
|
'llama': [],
|
||||||
|
'gpt4all': []
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super('http://mock-url.com', 'mock-engine-extension', 'Mock Engine Extension', true, 'A mock engine extension', '1.0.0')
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad(): void {
|
||||||
|
// Mock implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnload(): void {
|
||||||
|
// Mock implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEngines(): Promise<Engines> {
|
||||||
|
return JSON.parse(JSON.stringify(this.mockEngines))
|
||||||
|
}
|
||||||
|
|
||||||
|
async getInstalledEngines(name: InferenceEngine): Promise<EngineVariant[]> {
|
||||||
|
if (!this.mockEngines[name]) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.mockEngines[name].variants.filter(variant => variant.installed)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getReleasedEnginesByVersion(
|
||||||
|
name: InferenceEngine,
|
||||||
|
version: string,
|
||||||
|
platform?: string
|
||||||
|
): Promise<EngineReleased[]> {
|
||||||
|
const key = `${name}-${version}`
|
||||||
|
let releases = this.mockReleases[key] || []
|
||||||
|
|
||||||
|
if (platform) {
|
||||||
|
releases = releases.filter(release => release.os.includes(platform))
|
||||||
|
}
|
||||||
|
|
||||||
|
return releases
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLatestReleasedEngine(
|
||||||
|
name: InferenceEngine,
|
||||||
|
platform?: string
|
||||||
|
): Promise<EngineReleased[]> {
|
||||||
|
// For mock, let's assume latest versions are 1.1.0 for llama and 2.0.0 for gpt4all
|
||||||
|
const latestVersions = {
|
||||||
|
'llama': '1.1.0',
|
||||||
|
'gpt4all': '2.0.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!latestVersions[name]) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getReleasedEnginesByVersion(name, latestVersions[name], platform)
|
||||||
|
}
|
||||||
|
|
||||||
|
async installEngine(
|
||||||
|
name: string,
|
||||||
|
engineConfig: EngineConfig
|
||||||
|
): Promise<{ messages: string }> {
|
||||||
|
if (!this.mockEngines[name]) {
|
||||||
|
this.mockEngines[name] = {
|
||||||
|
name,
|
||||||
|
variants: [],
|
||||||
|
default: {
|
||||||
|
variant: engineConfig.variant,
|
||||||
|
version: engineConfig.version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if variant already exists
|
||||||
|
const existingVariantIndex = this.mockEngines[name].variants.findIndex(
|
||||||
|
v => v.variant === engineConfig.variant && v.version === engineConfig.version
|
||||||
|
)
|
||||||
|
|
||||||
|
if (existingVariantIndex >= 0) {
|
||||||
|
this.mockEngines[name].variants[existingVariantIndex].installed = true
|
||||||
|
} else {
|
||||||
|
this.mockEngines[name].variants.push({
|
||||||
|
variant: engineConfig.variant,
|
||||||
|
version: engineConfig.version,
|
||||||
|
path: `/engines/${name}/${engineConfig.variant}/${engineConfig.version}`,
|
||||||
|
installed: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return { messages: `Successfully installed ${name} ${engineConfig.variant} ${engineConfig.version}` }
|
||||||
|
}
|
||||||
|
|
||||||
|
async addRemoteEngine(
|
||||||
|
engineConfig: EngineConfig
|
||||||
|
): Promise<{ messages: string }> {
|
||||||
|
const name = engineConfig.name || 'remote-engine'
|
||||||
|
|
||||||
|
if (!this.mockEngines[name]) {
|
||||||
|
this.mockEngines[name] = {
|
||||||
|
name,
|
||||||
|
variants: [],
|
||||||
|
default: {
|
||||||
|
variant: engineConfig.variant,
|
||||||
|
version: engineConfig.version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mockEngines[name].variants.push({
|
||||||
|
variant: engineConfig.variant,
|
||||||
|
version: engineConfig.version,
|
||||||
|
path: engineConfig.path || `/engines/${name}/${engineConfig.variant}/${engineConfig.version}`,
|
||||||
|
installed: true,
|
||||||
|
url: engineConfig.url
|
||||||
|
})
|
||||||
|
|
||||||
|
return { messages: `Successfully added remote engine ${name}` }
|
||||||
|
}
|
||||||
|
|
||||||
|
async uninstallEngine(
|
||||||
|
name: InferenceEngine,
|
||||||
|
engineConfig: EngineConfig
|
||||||
|
): Promise<{ messages: string }> {
|
||||||
|
if (!this.mockEngines[name]) {
|
||||||
|
return { messages: `Engine ${name} not found` }
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantIndex = this.mockEngines[name].variants.findIndex(
|
||||||
|
v => v.variant === engineConfig.variant && v.version === engineConfig.version
|
||||||
|
)
|
||||||
|
|
||||||
|
if (variantIndex >= 0) {
|
||||||
|
this.mockEngines[name].variants[variantIndex].installed = false
|
||||||
|
|
||||||
|
// If this was the default variant, reset default
|
||||||
|
if (
|
||||||
|
this.mockEngines[name].default.variant === engineConfig.variant &&
|
||||||
|
this.mockEngines[name].default.version === engineConfig.version
|
||||||
|
) {
|
||||||
|
// Find another installed variant to set as default
|
||||||
|
const installedVariant = this.mockEngines[name].variants.find(v => v.installed)
|
||||||
|
if (installedVariant) {
|
||||||
|
this.mockEngines[name].default = {
|
||||||
|
variant: installedVariant.variant,
|
||||||
|
version: installedVariant.version
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No installed variants remain, clear default
|
||||||
|
this.mockEngines[name].default = { variant: '', version: '' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { messages: `Successfully uninstalled ${name} ${engineConfig.variant} ${engineConfig.version}` }
|
||||||
|
} else {
|
||||||
|
return { messages: `Variant ${engineConfig.variant} ${engineConfig.version} not found for engine ${name}` }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDefaultEngineVariant(
|
||||||
|
name: InferenceEngine
|
||||||
|
): Promise<DefaultEngineVariant> {
|
||||||
|
if (!this.mockEngines[name]) {
|
||||||
|
return { variant: '', version: '' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.mockEngines[name].default
|
||||||
|
}
|
||||||
|
|
||||||
|
async setDefaultEngineVariant(
|
||||||
|
name: InferenceEngine,
|
||||||
|
engineConfig: EngineConfig
|
||||||
|
): Promise<{ messages: string }> {
|
||||||
|
if (!this.mockEngines[name]) {
|
||||||
|
return { messages: `Engine ${name} not found` }
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantExists = this.mockEngines[name].variants.some(
|
||||||
|
v => v.variant === engineConfig.variant && v.version === engineConfig.version && v.installed
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!variantExists) {
|
||||||
|
return { messages: `Variant ${engineConfig.variant} ${engineConfig.version} not found or not installed` }
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mockEngines[name].default = {
|
||||||
|
variant: engineConfig.variant,
|
||||||
|
version: engineConfig.version
|
||||||
|
}
|
||||||
|
|
||||||
|
return { messages: `Successfully set ${engineConfig.variant} ${engineConfig.version} as default for ${name}` }
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateEngine(
|
||||||
|
name: InferenceEngine,
|
||||||
|
engineConfig?: EngineConfig
|
||||||
|
): Promise<{ messages: string }> {
|
||||||
|
if (!this.mockEngines[name]) {
|
||||||
|
return { messages: `Engine ${name} not found` }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!engineConfig) {
|
||||||
|
// Assume we're updating to the latest version
|
||||||
|
return { messages: `Successfully updated ${name} to the latest version` }
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantIndex = this.mockEngines[name].variants.findIndex(
|
||||||
|
v => v.variant === engineConfig.variant && v.installed
|
||||||
|
)
|
||||||
|
|
||||||
|
if (variantIndex >= 0) {
|
||||||
|
// Update the version
|
||||||
|
this.mockEngines[name].variants[variantIndex].version = engineConfig.version
|
||||||
|
|
||||||
|
// If this was the default variant, update default version too
|
||||||
|
if (this.mockEngines[name].default.variant === engineConfig.variant) {
|
||||||
|
this.mockEngines[name].default.version = engineConfig.version
|
||||||
|
}
|
||||||
|
|
||||||
|
return { messages: `Successfully updated ${name} ${engineConfig.variant} to version ${engineConfig.version}` }
|
||||||
|
} else {
|
||||||
|
return { messages: `Installed variant ${engineConfig.variant} not found for engine ${name}` }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async addRemoteModel(model: Model): Promise<void> {
|
||||||
|
const engine = model.engine as string
|
||||||
|
|
||||||
|
if (!this.remoteModels[engine]) {
|
||||||
|
this.remoteModels[engine] = []
|
||||||
|
}
|
||||||
|
|
||||||
|
this.remoteModels[engine].push(model)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRemoteModels(name: InferenceEngine | string): Promise<Model[]> {
|
||||||
|
return this.remoteModels[name] || []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('EngineManagementExtension', () => {
|
||||||
|
let extension: MockEngineManagementExtension
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
extension = new MockEngineManagementExtension()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should return the correct extension type', () => {
|
||||||
|
expect(extension.type()).toBe(ExtensionTypeEnum.Engine)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should get all engines', async () => {
|
||||||
|
const engines = await extension.getEngines()
|
||||||
|
|
||||||
|
expect(engines).toBeDefined()
|
||||||
|
expect(engines.llama).toBeDefined()
|
||||||
|
expect(engines.gpt4all).toBeDefined()
|
||||||
|
expect(engines.llama.variants).toHaveLength(2)
|
||||||
|
expect(engines.gpt4all.variants).toHaveLength(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should get installed engines', async () => {
|
||||||
|
const llamaEngines = await extension.getInstalledEngines('llama')
|
||||||
|
|
||||||
|
expect(llamaEngines).toHaveLength(1)
|
||||||
|
expect(llamaEngines[0].variant).toBe('cpu')
|
||||||
|
expect(llamaEngines[0].installed).toBe(true)
|
||||||
|
|
||||||
|
const gpt4allEngines = await extension.getInstalledEngines('gpt4all')
|
||||||
|
|
||||||
|
expect(gpt4allEngines).toHaveLength(1)
|
||||||
|
expect(gpt4allEngines[0].variant).toBe('cpu')
|
||||||
|
expect(gpt4allEngines[0].installed).toBe(true)
|
||||||
|
|
||||||
|
// Test non-existent engine
|
||||||
|
const nonExistentEngines = await extension.getInstalledEngines('non-existent' as InferenceEngine)
|
||||||
|
expect(nonExistentEngines).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should get released engines by version', async () => {
|
||||||
|
const llamaReleases = await extension.getReleasedEnginesByVersion('llama', '1.0.0')
|
||||||
|
|
||||||
|
expect(llamaReleases).toHaveLength(2)
|
||||||
|
expect(llamaReleases[0].variant).toBe('cpu')
|
||||||
|
expect(llamaReleases[1].variant).toBe('cuda')
|
||||||
|
|
||||||
|
// Test with platform filter
|
||||||
|
const llamaLinuxReleases = await extension.getReleasedEnginesByVersion('llama', '1.0.0', 'linux')
|
||||||
|
|
||||||
|
expect(llamaLinuxReleases).toHaveLength(2)
|
||||||
|
|
||||||
|
const llamaMacReleases = await extension.getReleasedEnginesByVersion('llama', '1.0.0', 'macos')
|
||||||
|
|
||||||
|
expect(llamaMacReleases).toHaveLength(1)
|
||||||
|
expect(llamaMacReleases[0].variant).toBe('cpu')
|
||||||
|
|
||||||
|
// Test non-existent version
|
||||||
|
const nonExistentReleases = await extension.getReleasedEnginesByVersion('llama', '9.9.9')
|
||||||
|
expect(nonExistentReleases).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should get latest released engines', async () => {
|
||||||
|
const latestLlamaReleases = await extension.getLatestReleasedEngine('llama')
|
||||||
|
|
||||||
|
expect(latestLlamaReleases).toHaveLength(2)
|
||||||
|
expect(latestLlamaReleases[0].version).toBe('1.1.0')
|
||||||
|
|
||||||
|
// Test with platform filter
|
||||||
|
const latestLlamaMacReleases = await extension.getLatestReleasedEngine('llama', 'macos')
|
||||||
|
|
||||||
|
expect(latestLlamaMacReleases).toHaveLength(1)
|
||||||
|
expect(latestLlamaMacReleases[0].variant).toBe('cpu')
|
||||||
|
expect(latestLlamaMacReleases[0].version).toBe('1.1.0')
|
||||||
|
|
||||||
|
// Test non-existent engine
|
||||||
|
const nonExistentReleases = await extension.getLatestReleasedEngine('non-existent' as InferenceEngine)
|
||||||
|
expect(nonExistentReleases).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should install engine', async () => {
|
||||||
|
// Install existing engine variant that is not installed
|
||||||
|
const result = await extension.installEngine('llama', { variant: 'cuda', version: '1.0.0' })
|
||||||
|
|
||||||
|
expect(result.messages).toContain('Successfully installed')
|
||||||
|
|
||||||
|
const installedEngines = await extension.getInstalledEngines('llama')
|
||||||
|
expect(installedEngines).toHaveLength(2)
|
||||||
|
expect(installedEngines.some(e => e.variant === 'cuda')).toBe(true)
|
||||||
|
|
||||||
|
// Install non-existent engine
|
||||||
|
const newEngineResult = await extension.installEngine('new-engine', { variant: 'cpu', version: '1.0.0' })
|
||||||
|
|
||||||
|
expect(newEngineResult.messages).toContain('Successfully installed')
|
||||||
|
|
||||||
|
const engines = await extension.getEngines()
|
||||||
|
expect(engines['new-engine']).toBeDefined()
|
||||||
|
expect(engines['new-engine'].variants).toHaveLength(1)
|
||||||
|
expect(engines['new-engine'].variants[0].installed).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should add remote engine', async () => {
|
||||||
|
const result = await extension.addRemoteEngine({
|
||||||
|
name: 'remote-llm',
|
||||||
|
variant: 'remote',
|
||||||
|
version: '1.0.0',
|
||||||
|
url: 'https://example.com/remote-llm-api'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.messages).toContain('Successfully added remote engine')
|
||||||
|
|
||||||
|
const engines = await extension.getEngines()
|
||||||
|
expect(engines['remote-llm']).toBeDefined()
|
||||||
|
expect(engines['remote-llm'].variants).toHaveLength(1)
|
||||||
|
expect(engines['remote-llm'].variants[0].url).toBe('https://example.com/remote-llm-api')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should uninstall engine', async () => {
|
||||||
|
const result = await extension.uninstallEngine('llama', { variant: 'cpu', version: '1.0.0' })
|
||||||
|
|
||||||
|
expect(result.messages).toContain('Successfully uninstalled')
|
||||||
|
|
||||||
|
const installedEngines = await extension.getInstalledEngines('llama')
|
||||||
|
expect(installedEngines).toHaveLength(0)
|
||||||
|
|
||||||
|
// Test uninstalling non-existent variant
|
||||||
|
const nonExistentResult = await extension.uninstallEngine('llama', { variant: 'non-existent', version: '1.0.0' })
|
||||||
|
|
||||||
|
expect(nonExistentResult.messages).toContain('not found')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle default variant when uninstalling', async () => {
|
||||||
|
// First install cuda variant
|
||||||
|
await extension.installEngine('llama', { variant: 'cuda', version: '1.0.0' })
|
||||||
|
|
||||||
|
// Set cuda as default
|
||||||
|
await extension.setDefaultEngineVariant('llama', { variant: 'cuda', version: '1.0.0' })
|
||||||
|
|
||||||
|
// Check that cuda is now default
|
||||||
|
let defaultVariant = await extension.getDefaultEngineVariant('llama')
|
||||||
|
expect(defaultVariant.variant).toBe('cuda')
|
||||||
|
|
||||||
|
// Uninstall cuda
|
||||||
|
await extension.uninstallEngine('llama', { variant: 'cuda', version: '1.0.0' })
|
||||||
|
|
||||||
|
// Check that default has changed to another installed variant
|
||||||
|
defaultVariant = await extension.getDefaultEngineVariant('llama')
|
||||||
|
expect(defaultVariant.variant).toBe('cpu')
|
||||||
|
|
||||||
|
// Uninstall all variants
|
||||||
|
await extension.uninstallEngine('llama', { variant: 'cpu', version: '1.0.0' })
|
||||||
|
|
||||||
|
// Check that default is now empty
|
||||||
|
defaultVariant = await extension.getDefaultEngineVariant('llama')
|
||||||
|
expect(defaultVariant.variant).toBe('')
|
||||||
|
expect(defaultVariant.version).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should get default engine variant', async () => {
|
||||||
|
const llamaDefault = await extension.getDefaultEngineVariant('llama')
|
||||||
|
|
||||||
|
expect(llamaDefault.variant).toBe('cpu')
|
||||||
|
expect(llamaDefault.version).toBe('1.0.0')
|
||||||
|
|
||||||
|
// Test non-existent engine
|
||||||
|
const nonExistentDefault = await extension.getDefaultEngineVariant('non-existent' as InferenceEngine)
|
||||||
|
expect(nonExistentDefault.variant).toBe('')
|
||||||
|
expect(nonExistentDefault.version).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should set default engine variant', async () => {
|
||||||
|
// Install cuda variant
|
||||||
|
await extension.installEngine('llama', { variant: 'cuda', version: '1.0.0' })
|
||||||
|
|
||||||
|
const result = await extension.setDefaultEngineVariant('llama', { variant: 'cuda', version: '1.0.0' })
|
||||||
|
|
||||||
|
expect(result.messages).toContain('Successfully set')
|
||||||
|
|
||||||
|
const defaultVariant = await extension.getDefaultEngineVariant('llama')
|
||||||
|
expect(defaultVariant.variant).toBe('cuda')
|
||||||
|
expect(defaultVariant.version).toBe('1.0.0')
|
||||||
|
|
||||||
|
// Test setting non-existent variant as default
|
||||||
|
const nonExistentResult = await extension.setDefaultEngineVariant('llama', { variant: 'non-existent', version: '1.0.0' })
|
||||||
|
|
||||||
|
expect(nonExistentResult.messages).toContain('not found')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should update engine', async () => {
|
||||||
|
const result = await extension.updateEngine('llama', { variant: 'cpu', version: '1.1.0' })
|
||||||
|
|
||||||
|
expect(result.messages).toContain('Successfully updated')
|
||||||
|
|
||||||
|
const engines = await extension.getEngines()
|
||||||
|
const cpuVariant = engines.llama.variants.find(v => v.variant === 'cpu')
|
||||||
|
expect(cpuVariant).toBeDefined()
|
||||||
|
expect(cpuVariant?.version).toBe('1.1.0')
|
||||||
|
|
||||||
|
// Default should also be updated since cpu was default
|
||||||
|
expect(engines.llama.default.version).toBe('1.1.0')
|
||||||
|
|
||||||
|
// Test updating non-existent variant
|
||||||
|
const nonExistentResult = await extension.updateEngine('llama', { variant: 'non-existent', version: '1.1.0' })
|
||||||
|
|
||||||
|
expect(nonExistentResult.messages).toContain('not found')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should add and get remote models', async () => {
|
||||||
|
const model: Model = {
|
||||||
|
id: 'remote-model-1',
|
||||||
|
name: 'Remote Model 1',
|
||||||
|
path: '/path/to/remote-model',
|
||||||
|
engine: 'llama',
|
||||||
|
format: 'gguf',
|
||||||
|
modelFormat: 'gguf',
|
||||||
|
source: 'remote',
|
||||||
|
status: 'ready',
|
||||||
|
contextLength: 4096,
|
||||||
|
sizeInGB: 4,
|
||||||
|
created: new Date().toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
await extension.addRemoteModel(model)
|
||||||
|
|
||||||
|
const llamaModels = await extension.getRemoteModels('llama')
|
||||||
|
expect(llamaModels).toHaveLength(1)
|
||||||
|
expect(llamaModels[0].id).toBe('remote-model-1')
|
||||||
|
|
||||||
|
// Test non-existent engine
|
||||||
|
const nonExistentModels = await extension.getRemoteModels('non-existent')
|
||||||
|
expect(nonExistentModels).toHaveLength(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
146
core/src/browser/extensions/hardwareManagement.test.ts
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import { HardwareManagementExtension } from './hardwareManagement'
|
||||||
|
import { ExtensionTypeEnum } from '../extension'
|
||||||
|
import { HardwareInformation } from '../../types'
|
||||||
|
|
||||||
|
// Mock implementation of HardwareManagementExtension
|
||||||
|
class MockHardwareManagementExtension extends HardwareManagementExtension {
|
||||||
|
private activeGpus: number[] = [0]
|
||||||
|
private mockHardwareInfo: HardwareInformation = {
|
||||||
|
cpu: {
|
||||||
|
manufacturer: 'Mock CPU Manufacturer',
|
||||||
|
brand: 'Mock CPU',
|
||||||
|
cores: 8,
|
||||||
|
physicalCores: 4,
|
||||||
|
speed: 3.5,
|
||||||
|
},
|
||||||
|
memory: {
|
||||||
|
total: 16 * 1024 * 1024 * 1024, // 16GB in bytes
|
||||||
|
free: 8 * 1024 * 1024 * 1024, // 8GB in bytes
|
||||||
|
},
|
||||||
|
gpus: [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
vendor: 'Mock GPU Vendor',
|
||||||
|
model: 'Mock GPU Model 1',
|
||||||
|
memory: 8 * 1024 * 1024 * 1024, // 8GB in bytes
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
vendor: 'Mock GPU Vendor',
|
||||||
|
model: 'Mock GPU Model 2',
|
||||||
|
memory: 4 * 1024 * 1024 * 1024, // 4GB in bytes
|
||||||
|
}
|
||||||
|
],
|
||||||
|
active_gpus: [0],
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super('http://mock-url.com', 'mock-hardware-extension', 'Mock Hardware Extension', true, 'A mock hardware extension', '1.0.0')
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad(): void {
|
||||||
|
// Mock implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnload(): void {
|
||||||
|
// Mock implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHardware(): Promise<HardwareInformation> {
|
||||||
|
// Return a copy to prevent test side effects
|
||||||
|
return JSON.parse(JSON.stringify(this.mockHardwareInfo))
|
||||||
|
}
|
||||||
|
|
||||||
|
async setAvtiveGpu(data: { gpus: number[] }): Promise<{
|
||||||
|
message: string
|
||||||
|
activated_gpus: number[]
|
||||||
|
}> {
|
||||||
|
// Validate GPUs exist
|
||||||
|
const validGpus = data.gpus.filter(gpuId =>
|
||||||
|
this.mockHardwareInfo.gpus.some(gpu => gpu.id === gpuId)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (validGpus.length === 0) {
|
||||||
|
throw new Error('No valid GPUs selected')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update active GPUs
|
||||||
|
this.activeGpus = validGpus
|
||||||
|
this.mockHardwareInfo.active_gpus = validGpus
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'GPU activation successful',
|
||||||
|
activated_gpus: validGpus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('HardwareManagementExtension', () => {
|
||||||
|
let extension: MockHardwareManagementExtension
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
extension = new MockHardwareManagementExtension()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should return the correct extension type', () => {
|
||||||
|
expect(extension.type()).toBe(ExtensionTypeEnum.Hardware)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should get hardware information', async () => {
|
||||||
|
const hardwareInfo = await extension.getHardware()
|
||||||
|
|
||||||
|
// Check CPU info
|
||||||
|
expect(hardwareInfo.cpu).toBeDefined()
|
||||||
|
expect(hardwareInfo.cpu.manufacturer).toBe('Mock CPU Manufacturer')
|
||||||
|
expect(hardwareInfo.cpu.cores).toBe(8)
|
||||||
|
|
||||||
|
// Check memory info
|
||||||
|
expect(hardwareInfo.memory).toBeDefined()
|
||||||
|
expect(hardwareInfo.memory.total).toBe(16 * 1024 * 1024 * 1024)
|
||||||
|
|
||||||
|
// Check GPU info
|
||||||
|
expect(hardwareInfo.gpus).toHaveLength(2)
|
||||||
|
expect(hardwareInfo.gpus[0].model).toBe('Mock GPU Model 1')
|
||||||
|
expect(hardwareInfo.gpus[1].model).toBe('Mock GPU Model 2')
|
||||||
|
|
||||||
|
// Check active GPUs
|
||||||
|
expect(hardwareInfo.active_gpus).toEqual([0])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should set active GPUs', async () => {
|
||||||
|
const result = await extension.setAvtiveGpu({ gpus: [1] })
|
||||||
|
|
||||||
|
expect(result.message).toBe('GPU activation successful')
|
||||||
|
expect(result.activated_gpus).toEqual([1])
|
||||||
|
|
||||||
|
// Verify the change in hardware info
|
||||||
|
const hardwareInfo = await extension.getHardware()
|
||||||
|
expect(hardwareInfo.active_gpus).toEqual([1])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should set multiple active GPUs', async () => {
|
||||||
|
const result = await extension.setAvtiveGpu({ gpus: [0, 1] })
|
||||||
|
|
||||||
|
expect(result.message).toBe('GPU activation successful')
|
||||||
|
expect(result.activated_gpus).toEqual([0, 1])
|
||||||
|
|
||||||
|
// Verify the change in hardware info
|
||||||
|
const hardwareInfo = await extension.getHardware()
|
||||||
|
expect(hardwareInfo.active_gpus).toEqual([0, 1])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should throw error for invalid GPU ids', async () => {
|
||||||
|
await expect(extension.setAvtiveGpu({ gpus: [999] })).rejects.toThrow('No valid GPUs selected')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle mix of valid and invalid GPU ids', async () => {
|
||||||
|
const result = await extension.setAvtiveGpu({ gpus: [0, 999] })
|
||||||
|
|
||||||
|
// Should only activate valid GPUs
|
||||||
|
expect(result.activated_gpus).toEqual([0])
|
||||||
|
|
||||||
|
// Verify the change in hardware info
|
||||||
|
const hardwareInfo = await extension.getHardware()
|
||||||
|
expect(hardwareInfo.active_gpus).toEqual([0])
|
||||||
|
})
|
||||||
|
})
|
||||||
286
core/src/browser/extensions/model.test.ts
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
import { ModelExtension } from './model'
|
||||||
|
import { ExtensionTypeEnum } from '../extension'
|
||||||
|
import { Model, OptionType, ModelSource } from '../../types'
|
||||||
|
|
||||||
|
// Mock implementation of ModelExtension
|
||||||
|
class MockModelExtension extends ModelExtension {
|
||||||
|
private models: Model[] = []
|
||||||
|
private sources: ModelSource[] = []
|
||||||
|
private loadedModels: Set<string> = new Set()
|
||||||
|
private modelsPulling: Set<string> = new Set()
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super('http://mock-url.com', 'mock-model-extension', 'Mock Model Extension', true, 'A mock model extension', '1.0.0')
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad(): void {
|
||||||
|
// Mock implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnload(): void {
|
||||||
|
// Mock implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
async configurePullOptions(configs: { [key: string]: any }): Promise<any> {
|
||||||
|
return configs
|
||||||
|
}
|
||||||
|
|
||||||
|
async getModels(): Promise<Model[]> {
|
||||||
|
return this.models
|
||||||
|
}
|
||||||
|
|
||||||
|
async pullModel(model: string, id?: string, name?: string): Promise<void> {
|
||||||
|
const modelId = id || `model-${Date.now()}`
|
||||||
|
this.modelsPulling.add(modelId)
|
||||||
|
|
||||||
|
// Simulate model pull by adding it to the model list
|
||||||
|
const newModel: Model = {
|
||||||
|
id: modelId,
|
||||||
|
path: `/models/${model}`,
|
||||||
|
name: name || model,
|
||||||
|
source: 'mock-source',
|
||||||
|
modelFormat: 'mock-format',
|
||||||
|
engine: 'mock-engine',
|
||||||
|
format: 'mock-format',
|
||||||
|
status: 'ready',
|
||||||
|
contextLength: 2048,
|
||||||
|
sizeInGB: 2,
|
||||||
|
created: new Date().toISOString(),
|
||||||
|
pullProgress: {
|
||||||
|
percent: 100,
|
||||||
|
transferred: 0,
|
||||||
|
total: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.models.push(newModel)
|
||||||
|
this.loadedModels.add(modelId)
|
||||||
|
this.modelsPulling.delete(modelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancelModelPull(modelId: string): Promise<void> {
|
||||||
|
this.modelsPulling.delete(modelId)
|
||||||
|
// Remove the model if it's in the pulling state
|
||||||
|
this.models = this.models.filter(m => m.id !== modelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
async importModel(
|
||||||
|
model: string,
|
||||||
|
modelPath: string,
|
||||||
|
name?: string,
|
||||||
|
optionType?: OptionType
|
||||||
|
): Promise<void> {
|
||||||
|
const newModel: Model = {
|
||||||
|
id: `model-${Date.now()}`,
|
||||||
|
path: modelPath,
|
||||||
|
name: name || model,
|
||||||
|
source: 'local',
|
||||||
|
modelFormat: optionType?.format || 'mock-format',
|
||||||
|
engine: optionType?.engine || 'mock-engine',
|
||||||
|
format: optionType?.format || 'mock-format',
|
||||||
|
status: 'ready',
|
||||||
|
contextLength: optionType?.contextLength || 2048,
|
||||||
|
sizeInGB: 2,
|
||||||
|
created: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
this.models.push(newModel)
|
||||||
|
this.loadedModels.add(newModel.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateModel(modelInfo: Partial<Model>): Promise<Model> {
|
||||||
|
if (!modelInfo.id) throw new Error('Model ID is required')
|
||||||
|
|
||||||
|
const index = this.models.findIndex(m => m.id === modelInfo.id)
|
||||||
|
if (index === -1) throw new Error('Model not found')
|
||||||
|
|
||||||
|
this.models[index] = { ...this.models[index], ...modelInfo }
|
||||||
|
return this.models[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteModel(modelId: string): Promise<void> {
|
||||||
|
this.models = this.models.filter(m => m.id !== modelId)
|
||||||
|
this.loadedModels.delete(modelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
async isModelLoaded(modelId: string): Promise<boolean> {
|
||||||
|
return this.loadedModels.has(modelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSources(): Promise<ModelSource[]> {
|
||||||
|
return this.sources
|
||||||
|
}
|
||||||
|
|
||||||
|
async addSource(source: string): Promise<void> {
|
||||||
|
const newSource: ModelSource = {
|
||||||
|
id: `source-${Date.now()}`,
|
||||||
|
url: source,
|
||||||
|
name: `Source ${this.sources.length + 1}`,
|
||||||
|
type: 'mock-type'
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sources.push(newSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteSource(sourceId: string): Promise<void> {
|
||||||
|
this.sources = this.sources.filter(s => s.id !== sourceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ModelExtension', () => {
|
||||||
|
let extension: MockModelExtension
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
extension = new MockModelExtension()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should return the correct extension type', () => {
|
||||||
|
expect(extension.type()).toBe(ExtensionTypeEnum.Model)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should configure pull options', async () => {
|
||||||
|
const configs = { apiKey: 'test-key', baseUrl: 'https://test-url.com' }
|
||||||
|
const result = await extension.configurePullOptions(configs)
|
||||||
|
expect(result).toEqual(configs)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should add and get models', async () => {
|
||||||
|
await extension.pullModel('test-model', 'test-id', 'Test Model')
|
||||||
|
|
||||||
|
const models = await extension.getModels()
|
||||||
|
expect(models).toHaveLength(1)
|
||||||
|
expect(models[0].id).toBe('test-id')
|
||||||
|
expect(models[0].name).toBe('Test Model')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should pull model with default id and name', async () => {
|
||||||
|
await extension.pullModel('test-model')
|
||||||
|
|
||||||
|
const models = await extension.getModels()
|
||||||
|
expect(models).toHaveLength(1)
|
||||||
|
expect(models[0].name).toBe('test-model')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should cancel model pull', async () => {
|
||||||
|
await extension.pullModel('test-model', 'test-id')
|
||||||
|
|
||||||
|
// Verify model exists
|
||||||
|
let models = await extension.getModels()
|
||||||
|
expect(models).toHaveLength(1)
|
||||||
|
|
||||||
|
// Cancel the pull
|
||||||
|
await extension.cancelModelPull('test-id')
|
||||||
|
|
||||||
|
// Verify model was removed
|
||||||
|
models = await extension.getModels()
|
||||||
|
expect(models).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should import model', async () => {
|
||||||
|
const optionType: OptionType = {
|
||||||
|
engine: 'test-engine',
|
||||||
|
format: 'test-format',
|
||||||
|
contextLength: 4096
|
||||||
|
}
|
||||||
|
|
||||||
|
await extension.importModel('test-model', '/path/to/model', 'Imported Model', optionType)
|
||||||
|
|
||||||
|
const models = await extension.getModels()
|
||||||
|
expect(models).toHaveLength(1)
|
||||||
|
expect(models[0].name).toBe('Imported Model')
|
||||||
|
expect(models[0].engine).toBe('test-engine')
|
||||||
|
expect(models[0].format).toBe('test-format')
|
||||||
|
expect(models[0].contextLength).toBe(4096)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should import model with default values', async () => {
|
||||||
|
await extension.importModel('test-model', '/path/to/model')
|
||||||
|
|
||||||
|
const models = await extension.getModels()
|
||||||
|
expect(models).toHaveLength(1)
|
||||||
|
expect(models[0].name).toBe('test-model')
|
||||||
|
expect(models[0].engine).toBe('mock-engine')
|
||||||
|
expect(models[0].format).toBe('mock-format')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should update model', async () => {
|
||||||
|
await extension.pullModel('test-model', 'test-id', 'Test Model')
|
||||||
|
|
||||||
|
const updatedModel = await extension.updateModel({
|
||||||
|
id: 'test-id',
|
||||||
|
name: 'Updated Model',
|
||||||
|
contextLength: 8192
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(updatedModel.name).toBe('Updated Model')
|
||||||
|
expect(updatedModel.contextLength).toBe(8192)
|
||||||
|
|
||||||
|
// Verify changes persisted
|
||||||
|
const models = await extension.getModels()
|
||||||
|
expect(models[0].name).toBe('Updated Model')
|
||||||
|
expect(models[0].contextLength).toBe(8192)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should throw error when updating non-existent model', async () => {
|
||||||
|
await expect(extension.updateModel({
|
||||||
|
id: 'non-existent',
|
||||||
|
name: 'Updated Model'
|
||||||
|
})).rejects.toThrow('Model not found')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should throw error when updating model without ID', async () => {
|
||||||
|
await expect(extension.updateModel({
|
||||||
|
name: 'Updated Model'
|
||||||
|
})).rejects.toThrow('Model ID is required')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should delete model', async () => {
|
||||||
|
await extension.pullModel('test-model', 'test-id')
|
||||||
|
|
||||||
|
// Verify model exists
|
||||||
|
let models = await extension.getModels()
|
||||||
|
expect(models).toHaveLength(1)
|
||||||
|
|
||||||
|
// Delete the model
|
||||||
|
await extension.deleteModel('test-id')
|
||||||
|
|
||||||
|
// Verify model was removed
|
||||||
|
models = await extension.getModels()
|
||||||
|
expect(models).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should check if model is loaded', async () => {
|
||||||
|
await extension.pullModel('test-model', 'test-id')
|
||||||
|
|
||||||
|
// Check if model is loaded
|
||||||
|
const isLoaded = await extension.isModelLoaded('test-id')
|
||||||
|
expect(isLoaded).toBe(true)
|
||||||
|
|
||||||
|
// Check if non-existent model is loaded
|
||||||
|
const nonExistentLoaded = await extension.isModelLoaded('non-existent')
|
||||||
|
expect(nonExistentLoaded).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should add and get sources', async () => {
|
||||||
|
await extension.addSource('https://test-source.com')
|
||||||
|
|
||||||
|
const sources = await extension.getSources()
|
||||||
|
expect(sources).toHaveLength(1)
|
||||||
|
expect(sources[0].url).toBe('https://test-source.com')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should delete source', async () => {
|
||||||
|
await extension.addSource('https://test-source.com')
|
||||||
|
|
||||||
|
// Get the source ID
|
||||||
|
const sources = await extension.getSources()
|
||||||
|
const sourceId = sources[0].id
|
||||||
|
|
||||||
|
// Delete the source
|
||||||
|
await extension.deleteSource(sourceId)
|
||||||
|
|
||||||
|
// Verify source was removed
|
||||||
|
const updatedSources = await extension.getSources()
|
||||||
|
expect(updatedSources).toHaveLength(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -1,10 +1,13 @@
|
|||||||
import { BaseExtension, ExtensionTypeEnum } from '../extension'
|
import { BaseExtension, ExtensionTypeEnum } from '../extension'
|
||||||
import { Model, ModelInterface, OptionType } from '../../types'
|
import { Model, ModelInterface, ModelSource, OptionType } from '../../types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model extension for managing models.
|
* Model extension for managing models.
|
||||||
*/
|
*/
|
||||||
export abstract class ModelExtension extends BaseExtension implements ModelInterface {
|
export abstract class ModelExtension
|
||||||
|
extends BaseExtension
|
||||||
|
implements ModelInterface
|
||||||
|
{
|
||||||
/**
|
/**
|
||||||
* Model extension type.
|
* Model extension type.
|
||||||
*/
|
*/
|
||||||
@ -25,4 +28,16 @@ export abstract class ModelExtension extends BaseExtension implements ModelInter
|
|||||||
abstract updateModel(modelInfo: Partial<Model>): Promise<Model>
|
abstract updateModel(modelInfo: Partial<Model>): Promise<Model>
|
||||||
abstract deleteModel(model: string): Promise<void>
|
abstract deleteModel(model: string): Promise<void>
|
||||||
abstract isModelLoaded(model: string): Promise<boolean>
|
abstract isModelLoaded(model: string): Promise<boolean>
|
||||||
|
/**
|
||||||
|
* Get model sources
|
||||||
|
*/
|
||||||
|
abstract getSources(): Promise<ModelSource[]>
|
||||||
|
/**
|
||||||
|
* Add a model source
|
||||||
|
*/
|
||||||
|
abstract addSource(source: string): Promise<void>
|
||||||
|
/**
|
||||||
|
* Delete a model source
|
||||||
|
*/
|
||||||
|
abstract deleteSource(source: string): Promise<void>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,6 +55,12 @@ const unlinkSync = (...args: any[]) => globalThis.core.api?.unlinkSync(...args)
|
|||||||
*/
|
*/
|
||||||
const appendFileSync = (...args: any[]) => globalThis.core.api?.appendFileSync(...args)
|
const appendFileSync = (...args: any[]) => globalThis.core.api?.appendFileSync(...args)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies a file from the source path to the destination path.
|
||||||
|
* @param src
|
||||||
|
* @param dest
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
const copyFile: (src: string, dest: string) => Promise<void> = (src, dest) =>
|
const copyFile: (src: string, dest: string) => Promise<void> = (src, dest) =>
|
||||||
globalThis.core.api?.copyFile(src, dest)
|
globalThis.core.api?.copyFile(src, dest)
|
||||||
|
|
||||||
@ -64,8 +70,8 @@ const copyFile: (src: string, dest: string) => Promise<void> = (src, dest) =>
|
|||||||
* @param path - The paths to the file.
|
* @param path - The paths to the file.
|
||||||
* @returns {Promise<{any}>} - A promise that resolves with the list of gguf and non-gguf files
|
* @returns {Promise<{any}>} - A promise that resolves with the list of gguf and non-gguf files
|
||||||
*/
|
*/
|
||||||
const getGgufFiles: (paths: string[]) => Promise<any> = (
|
const getGgufFiles: (paths: string[]) => Promise<any> = (paths) =>
|
||||||
paths) => globalThis.core.api?.getGgufFiles(paths)
|
globalThis.core.api?.getGgufFiles(paths)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the file's stats.
|
* Gets the file's stats.
|
||||||
|
|||||||
@ -1,25 +1,21 @@
|
|||||||
import {
|
import {
|
||||||
AppRoute,
|
AppRoute,
|
||||||
DownloadRoute,
|
|
||||||
ExtensionRoute,
|
ExtensionRoute,
|
||||||
FileManagerRoute,
|
FileManagerRoute,
|
||||||
FileSystemRoute,
|
FileSystemRoute,
|
||||||
} from '../../../types/api'
|
} from '../../../types/api'
|
||||||
import { Downloader } from '../processors/download'
|
|
||||||
import { FileSystem } from '../processors/fs'
|
import { FileSystem } from '../processors/fs'
|
||||||
import { Extension } from '../processors/extension'
|
import { Extension } from '../processors/extension'
|
||||||
import { FSExt } from '../processors/fsExt'
|
import { FSExt } from '../processors/fsExt'
|
||||||
import { App } from '../processors/app'
|
import { App } from '../processors/app'
|
||||||
|
|
||||||
export class RequestAdapter {
|
export class RequestAdapter {
|
||||||
downloader: Downloader
|
|
||||||
fileSystem: FileSystem
|
fileSystem: FileSystem
|
||||||
extension: Extension
|
extension: Extension
|
||||||
fsExt: FSExt
|
fsExt: FSExt
|
||||||
app: App
|
app: App
|
||||||
|
|
||||||
constructor(observer?: Function) {
|
constructor(observer?: Function) {
|
||||||
this.downloader = new Downloader(observer)
|
|
||||||
this.fileSystem = new FileSystem()
|
this.fileSystem = new FileSystem()
|
||||||
this.extension = new Extension()
|
this.extension = new Extension()
|
||||||
this.fsExt = new FSExt()
|
this.fsExt = new FSExt()
|
||||||
@ -28,9 +24,7 @@ export class RequestAdapter {
|
|||||||
|
|
||||||
// TODO: Clearer Factory pattern here
|
// TODO: Clearer Factory pattern here
|
||||||
process(route: string, ...args: any) {
|
process(route: string, ...args: any) {
|
||||||
if (route in DownloadRoute) {
|
if (route in FileSystemRoute) {
|
||||||
return this.downloader.process(route, ...args)
|
|
||||||
} else if (route in FileSystemRoute) {
|
|
||||||
return this.fileSystem.process(route, ...args)
|
return this.fileSystem.process(route, ...args)
|
||||||
} else if (route in ExtensionRoute) {
|
} else if (route in ExtensionRoute) {
|
||||||
return this.extension.process(route, ...args)
|
return this.extension.process(route, ...args)
|
||||||
|
|||||||
@ -1,125 +0,0 @@
|
|||||||
import { Downloader } from './download'
|
|
||||||
import { DownloadEvent } from '../../../types/api'
|
|
||||||
import { DownloadManager } from '../../helper/download'
|
|
||||||
|
|
||||||
jest.mock('../../helper', () => ({
|
|
||||||
getJanDataFolderPath: jest.fn().mockReturnValue('path/to/folder'),
|
|
||||||
}))
|
|
||||||
|
|
||||||
jest.mock('../../helper/path', () => ({
|
|
||||||
validatePath: jest.fn().mockReturnValue('path/to/folder'),
|
|
||||||
normalizeFilePath: () =>
|
|
||||||
process.platform === 'win32' ? 'C:\\Users\\path\\to\\file.gguf' : '/Users/path/to/file.gguf',
|
|
||||||
}))
|
|
||||||
|
|
||||||
jest.mock(
|
|
||||||
'request',
|
|
||||||
jest.fn().mockReturnValue(() => ({
|
|
||||||
on: jest.fn(),
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
|
|
||||||
jest.mock('fs', () => ({
|
|
||||||
createWriteStream: jest.fn(),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const requestMock = jest.fn((options, callback) => {
|
|
||||||
callback(new Error('Test error'), null)
|
|
||||||
})
|
|
||||||
jest.mock('request', () => requestMock)
|
|
||||||
|
|
||||||
jest.mock('request-progress', () => {
|
|
||||||
return jest.fn().mockImplementation(() => {
|
|
||||||
return {
|
|
||||||
on: jest.fn().mockImplementation((event, callback) => {
|
|
||||||
if (event === 'error') {
|
|
||||||
callback(new Error('Download failed'))
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
on: jest.fn().mockImplementation((event, callback) => {
|
|
||||||
if (event === 'error') {
|
|
||||||
callback(new Error('Download failed'))
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
on: jest.fn().mockImplementation((event, callback) => {
|
|
||||||
if (event === 'error') {
|
|
||||||
callback(new Error('Download failed'))
|
|
||||||
}
|
|
||||||
return { pipe: jest.fn() }
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Downloader', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.resetAllMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should pause download correctly', () => {
|
|
||||||
const observer = jest.fn()
|
|
||||||
const fileName = process.platform === 'win32' ? 'C:\\path\\to\\file' : 'path/to/file'
|
|
||||||
|
|
||||||
const downloader = new Downloader(observer)
|
|
||||||
const pauseMock = jest.fn()
|
|
||||||
DownloadManager.instance.networkRequests[fileName] = { pause: pauseMock }
|
|
||||||
|
|
||||||
downloader.pauseDownload(observer, fileName)
|
|
||||||
|
|
||||||
expect(pauseMock).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should resume download correctly', () => {
|
|
||||||
const observer = jest.fn()
|
|
||||||
const fileName = process.platform === 'win32' ? 'C:\\path\\to\\file' : 'path/to/file'
|
|
||||||
|
|
||||||
const downloader = new Downloader(observer)
|
|
||||||
const resumeMock = jest.fn()
|
|
||||||
DownloadManager.instance.networkRequests[fileName] = { resume: resumeMock }
|
|
||||||
|
|
||||||
downloader.resumeDownload(observer, fileName)
|
|
||||||
|
|
||||||
expect(resumeMock).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle aborting a download correctly', () => {
|
|
||||||
const observer = jest.fn()
|
|
||||||
const fileName = process.platform === 'win32' ? 'C:\\path\\to\\file' : 'path/to/file'
|
|
||||||
|
|
||||||
const downloader = new Downloader(observer)
|
|
||||||
const abortMock = jest.fn()
|
|
||||||
DownloadManager.instance.networkRequests[fileName] = { abort: abortMock }
|
|
||||||
|
|
||||||
downloader.abortDownload(observer, fileName)
|
|
||||||
|
|
||||||
expect(abortMock).toHaveBeenCalled()
|
|
||||||
expect(observer).toHaveBeenCalledWith(
|
|
||||||
DownloadEvent.onFileDownloadError,
|
|
||||||
expect.objectContaining({
|
|
||||||
error: 'aborted',
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle download fail correctly', () => {
|
|
||||||
const observer = jest.fn()
|
|
||||||
const fileName = process.platform === 'win32' ? 'C:\\path\\to\\file' : 'path/to/file.gguf'
|
|
||||||
|
|
||||||
const downloader = new Downloader(observer)
|
|
||||||
|
|
||||||
downloader.downloadFile(observer, {
|
|
||||||
localPath: fileName,
|
|
||||||
url: 'http://127.0.0.1',
|
|
||||||
})
|
|
||||||
expect(observer).toHaveBeenCalledWith(
|
|
||||||
DownloadEvent.onFileDownloadError,
|
|
||||||
expect.objectContaining({
|
|
||||||
error: expect.anything(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@ -1,138 +0,0 @@
|
|||||||
import { resolve, sep } from 'path'
|
|
||||||
import { DownloadEvent } from '../../../types/api'
|
|
||||||
import { normalizeFilePath } from '../../helper/path'
|
|
||||||
import { getJanDataFolderPath } from '../../helper'
|
|
||||||
import { DownloadManager } from '../../helper/download'
|
|
||||||
import { createWriteStream, renameSync } from 'fs'
|
|
||||||
import { Processor } from './Processor'
|
|
||||||
import { DownloadRequest, DownloadState, NetworkConfig } from '../../../types'
|
|
||||||
|
|
||||||
export class Downloader implements Processor {
|
|
||||||
observer?: Function
|
|
||||||
|
|
||||||
constructor(observer?: Function) {
|
|
||||||
this.observer = observer
|
|
||||||
}
|
|
||||||
|
|
||||||
process(key: string, ...args: any[]): any {
|
|
||||||
const instance = this as any
|
|
||||||
const func = instance[key]
|
|
||||||
return func(this.observer, ...args)
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadFile(observer: any, downloadRequest: DownloadRequest, network?: NetworkConfig) {
|
|
||||||
const request = require('request')
|
|
||||||
const progress = require('request-progress')
|
|
||||||
|
|
||||||
const strictSSL = !network?.ignoreSSL
|
|
||||||
const proxy = network?.proxy?.startsWith('http') ? network.proxy : undefined
|
|
||||||
|
|
||||||
const { localPath, url } = downloadRequest
|
|
||||||
let normalizedPath = localPath
|
|
||||||
if (typeof localPath === 'string') {
|
|
||||||
normalizedPath = normalizeFilePath(localPath)
|
|
||||||
}
|
|
||||||
const array = normalizedPath.split(sep)
|
|
||||||
const fileName = array.pop() ?? ''
|
|
||||||
const modelId = downloadRequest.modelId ?? array.pop() ?? ''
|
|
||||||
|
|
||||||
const destination = resolve(getJanDataFolderPath(), normalizedPath)
|
|
||||||
const rq = request({ url, strictSSL, proxy })
|
|
||||||
|
|
||||||
// Put request to download manager instance
|
|
||||||
DownloadManager.instance.setRequest(normalizedPath, rq)
|
|
||||||
|
|
||||||
// Downloading file to a temp file first
|
|
||||||
const downloadingTempFile = `${destination}.download`
|
|
||||||
|
|
||||||
// adding initial download state
|
|
||||||
const initialDownloadState: DownloadState = {
|
|
||||||
modelId,
|
|
||||||
fileName,
|
|
||||||
percent: 0,
|
|
||||||
size: {
|
|
||||||
total: 0,
|
|
||||||
transferred: 0,
|
|
||||||
},
|
|
||||||
children: [],
|
|
||||||
downloadState: 'downloading',
|
|
||||||
extensionId: downloadRequest.extensionId,
|
|
||||||
downloadType: downloadRequest.downloadType,
|
|
||||||
localPath: normalizedPath,
|
|
||||||
}
|
|
||||||
DownloadManager.instance.downloadProgressMap[modelId] = initialDownloadState
|
|
||||||
DownloadManager.instance.downloadInfo[normalizedPath] = initialDownloadState
|
|
||||||
|
|
||||||
if (downloadRequest.downloadType === 'extension') {
|
|
||||||
observer?.(DownloadEvent.onFileDownloadUpdate, initialDownloadState)
|
|
||||||
}
|
|
||||||
|
|
||||||
progress(rq, {})
|
|
||||||
.on('progress', (state: any) => {
|
|
||||||
const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId]
|
|
||||||
const downloadState: DownloadState = {
|
|
||||||
...currentDownloadState,
|
|
||||||
...state,
|
|
||||||
fileName: fileName,
|
|
||||||
downloadState: 'downloading',
|
|
||||||
}
|
|
||||||
console.debug('progress: ', downloadState)
|
|
||||||
observer?.(DownloadEvent.onFileDownloadUpdate, downloadState)
|
|
||||||
DownloadManager.instance.downloadProgressMap[modelId] = downloadState
|
|
||||||
})
|
|
||||||
.on('error', (error: Error) => {
|
|
||||||
const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId]
|
|
||||||
const downloadState: DownloadState = {
|
|
||||||
...currentDownloadState,
|
|
||||||
fileName: fileName,
|
|
||||||
error: error.message,
|
|
||||||
downloadState: 'error',
|
|
||||||
}
|
|
||||||
|
|
||||||
observer?.(DownloadEvent.onFileDownloadError, downloadState)
|
|
||||||
DownloadManager.instance.downloadProgressMap[modelId] = downloadState
|
|
||||||
})
|
|
||||||
.on('end', () => {
|
|
||||||
const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId]
|
|
||||||
if (
|
|
||||||
currentDownloadState &&
|
|
||||||
DownloadManager.instance.networkRequests[normalizedPath] &&
|
|
||||||
DownloadManager.instance.downloadProgressMap[modelId]?.downloadState !== 'error'
|
|
||||||
) {
|
|
||||||
// Finished downloading, rename temp file to actual file
|
|
||||||
renameSync(downloadingTempFile, destination)
|
|
||||||
const downloadState: DownloadState = {
|
|
||||||
...currentDownloadState,
|
|
||||||
fileName: fileName,
|
|
||||||
downloadState: 'end',
|
|
||||||
}
|
|
||||||
observer?.(DownloadEvent.onFileDownloadSuccess, downloadState)
|
|
||||||
DownloadManager.instance.downloadProgressMap[modelId] = downloadState
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.pipe(createWriteStream(downloadingTempFile))
|
|
||||||
}
|
|
||||||
|
|
||||||
abortDownload(observer: any, fileName: string) {
|
|
||||||
const rq = DownloadManager.instance.networkRequests[fileName]
|
|
||||||
if (rq) {
|
|
||||||
DownloadManager.instance.networkRequests[fileName] = undefined
|
|
||||||
rq?.abort()
|
|
||||||
}
|
|
||||||
|
|
||||||
const downloadInfo = DownloadManager.instance.downloadInfo[fileName]
|
|
||||||
observer?.(DownloadEvent.onFileDownloadError, {
|
|
||||||
...downloadInfo,
|
|
||||||
fileName,
|
|
||||||
error: 'aborted',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
resumeDownload(_observer: any, fileName: any) {
|
|
||||||
DownloadManager.instance.networkRequests[fileName]?.resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
pauseDownload(_observer: any, fileName: any) {
|
|
||||||
DownloadManager.instance.networkRequests[fileName]?.pause()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import { DownloadManager } from './download';
|
|
||||||
|
|
||||||
it('should set a network request for a specific file', () => {
|
|
||||||
const downloadManager = new DownloadManager();
|
|
||||||
const fileName = 'testFile';
|
|
||||||
const request = { url: 'http://example.com' };
|
|
||||||
|
|
||||||
downloadManager.setRequest(fileName, request);
|
|
||||||
|
|
||||||
expect(downloadManager.networkRequests[fileName]).toEqual(request);
|
|
||||||
});
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
import { DownloadState } from '../../types'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manages file downloads and network requests.
|
|
||||||
*/
|
|
||||||
export class DownloadManager {
|
|
||||||
public networkRequests: Record<string, any> = {}
|
|
||||||
|
|
||||||
public static instance: DownloadManager = new DownloadManager()
|
|
||||||
|
|
||||||
// store the download information with key is model id
|
|
||||||
public downloadProgressMap: Record<string, DownloadState> = {}
|
|
||||||
|
|
||||||
// store the download information with key is normalized file path
|
|
||||||
public downloadInfo: Record<string, DownloadState> = {}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
if (DownloadManager.instance) {
|
|
||||||
return DownloadManager.instance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Sets a network request for a specific file.
|
|
||||||
* @param {string} fileName - The name of the file.
|
|
||||||
* @param {Request | undefined} request - The network request to set, or undefined to clear the request.
|
|
||||||
*/
|
|
||||||
setRequest(fileName: string, request: any | undefined) {
|
|
||||||
this.networkRequests[fileName] = request
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,5 +1,4 @@
|
|||||||
export * from './config'
|
export * from './config'
|
||||||
export * from './download'
|
|
||||||
export * from './logger'
|
export * from './logger'
|
||||||
export * from './module'
|
export * from './module'
|
||||||
export * from './path'
|
export * from './path'
|
||||||
|
|||||||
@ -31,6 +31,10 @@ export enum NativeRoute {
|
|||||||
|
|
||||||
startServer = 'startServer',
|
startServer = 'startServer',
|
||||||
stopServer = 'stopServer',
|
stopServer = 'stopServer',
|
||||||
|
|
||||||
|
appUpdateDownload = 'appUpdateDownload',
|
||||||
|
|
||||||
|
appToken = 'appToken',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -50,6 +54,8 @@ export enum AppRoute {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum AppEvent {
|
export enum AppEvent {
|
||||||
|
onAppUpdateNotAvailable = 'onAppUpdateNotAvailable',
|
||||||
|
onAppUpdateAvailable = 'onAppUpdateAvailable',
|
||||||
onAppUpdateDownloadUpdate = 'onAppUpdateDownloadUpdate',
|
onAppUpdateDownloadUpdate = 'onAppUpdateDownloadUpdate',
|
||||||
onAppUpdateDownloadError = 'onAppUpdateDownloadError',
|
onAppUpdateDownloadError = 'onAppUpdateDownloadError',
|
||||||
onAppUpdateDownloadSuccess = 'onAppUpdateDownloadSuccess',
|
onAppUpdateDownloadSuccess = 'onAppUpdateDownloadSuccess',
|
||||||
@ -61,30 +67,13 @@ export enum AppEvent {
|
|||||||
onMainViewStateChange = 'onMainViewStateChange',
|
onMainViewStateChange = 'onMainViewStateChange',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum DownloadRoute {
|
|
||||||
abortDownload = 'abortDownload',
|
|
||||||
downloadFile = 'downloadFile',
|
|
||||||
pauseDownload = 'pauseDownload',
|
|
||||||
resumeDownload = 'resumeDownload',
|
|
||||||
getDownloadProgress = 'getDownloadProgress',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum DownloadEvent {
|
export enum DownloadEvent {
|
||||||
onFileDownloadUpdate = 'onFileDownloadUpdate',
|
onFileDownloadUpdate = 'onFileDownloadUpdate',
|
||||||
onFileDownloadError = 'onFileDownloadError',
|
onFileDownloadError = 'onFileDownloadError',
|
||||||
onFileDownloadSuccess = 'onFileDownloadSuccess',
|
onFileDownloadSuccess = 'onFileDownloadSuccess',
|
||||||
onFileDownloadStopped = 'onFileDownloadStopped',
|
onFileDownloadStopped = 'onFileDownloadStopped',
|
||||||
onFileDownloadStarted = 'onFileDownloadStarted',
|
onFileDownloadStarted = 'onFileDownloadStarted',
|
||||||
onFileUnzipSuccess = 'onFileUnzipSuccess',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum LocalImportModelEvent {
|
|
||||||
onLocalImportModelUpdate = 'onLocalImportModelUpdate',
|
|
||||||
onLocalImportModelFailed = 'onLocalImportModelFailed',
|
|
||||||
onLocalImportModelSuccess = 'onLocalImportModelSuccess',
|
|
||||||
onLocalImportModelFinished = 'onLocalImportModelFinished',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ExtensionRoute {
|
export enum ExtensionRoute {
|
||||||
baseExtensions = 'baseExtensions',
|
baseExtensions = 'baseExtensions',
|
||||||
getActiveExtensions = 'getActiveExtensions',
|
getActiveExtensions = 'getActiveExtensions',
|
||||||
@ -127,10 +116,6 @@ export type AppEventFunctions = {
|
|||||||
[K in AppEvent]: ApiFunction
|
[K in AppEvent]: ApiFunction
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DownloadRouteFunctions = {
|
|
||||||
[K in DownloadRoute]: ApiFunction
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DownloadEventFunctions = {
|
export type DownloadEventFunctions = {
|
||||||
[K in DownloadEvent]: ApiFunction
|
[K in DownloadEvent]: ApiFunction
|
||||||
}
|
}
|
||||||
@ -150,7 +135,6 @@ export type FileManagerRouteFunctions = {
|
|||||||
export type APIFunctions = NativeRouteFunctions &
|
export type APIFunctions = NativeRouteFunctions &
|
||||||
AppRouteFunctions &
|
AppRouteFunctions &
|
||||||
AppEventFunctions &
|
AppEventFunctions &
|
||||||
DownloadRouteFunctions &
|
|
||||||
DownloadEventFunctions &
|
DownloadEventFunctions &
|
||||||
ExtensionRouteFunctions &
|
ExtensionRouteFunctions &
|
||||||
FileSystemRouteFunctions &
|
FileSystemRouteFunctions &
|
||||||
@ -158,7 +142,6 @@ export type APIFunctions = NativeRouteFunctions &
|
|||||||
|
|
||||||
export const CoreRoutes = [
|
export const CoreRoutes = [
|
||||||
...Object.values(AppRoute),
|
...Object.values(AppRoute),
|
||||||
...Object.values(DownloadRoute),
|
|
||||||
...Object.values(ExtensionRoute),
|
...Object.values(ExtensionRoute),
|
||||||
...Object.values(FileSystemRoute),
|
...Object.values(FileSystemRoute),
|
||||||
...Object.values(FileManagerRoute),
|
...Object.values(FileManagerRoute),
|
||||||
@ -168,7 +151,6 @@ export const APIRoutes = [...CoreRoutes, ...Object.values(NativeRoute)]
|
|||||||
export const APIEvents = [
|
export const APIEvents = [
|
||||||
...Object.values(AppEvent),
|
...Object.values(AppEvent),
|
||||||
...Object.values(DownloadEvent),
|
...Object.values(DownloadEvent),
|
||||||
...Object.values(LocalImportModelEvent),
|
|
||||||
]
|
]
|
||||||
export type PayloadType = {
|
export type PayloadType = {
|
||||||
messages: ChatCompletionMessage[]
|
messages: ChatCompletionMessage[]
|
||||||
|
|||||||
@ -16,41 +16,9 @@ export type DownloadState = {
|
|||||||
|
|
||||||
error?: string
|
error?: string
|
||||||
extensionId?: string
|
extensionId?: string
|
||||||
downloadType?: DownloadType | string
|
|
||||||
localPath?: string
|
localPath?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DownloadType = 'model' | 'extension'
|
|
||||||
|
|
||||||
export type DownloadRequest = {
|
|
||||||
/**
|
|
||||||
* The URL to download the file from.
|
|
||||||
*/
|
|
||||||
url: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The local path to save the file to.
|
|
||||||
*/
|
|
||||||
localPath: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The extension ID of the extension that initiated the download.
|
|
||||||
*
|
|
||||||
* Can be extension name.
|
|
||||||
*/
|
|
||||||
extensionId?: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The model ID of the model that initiated the download.
|
|
||||||
*/
|
|
||||||
modelId?: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The download type.
|
|
||||||
*/
|
|
||||||
downloadType?: DownloadType | string
|
|
||||||
}
|
|
||||||
|
|
||||||
type DownloadTime = {
|
type DownloadTime = {
|
||||||
elapsed: number
|
elapsed: number
|
||||||
remaining: number
|
remaining: number
|
||||||
@ -60,7 +28,6 @@ type DownloadSize = {
|
|||||||
total: number
|
total: number
|
||||||
transferred: number
|
transferred: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The file metadata
|
* The file metadata
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
export type FileDownloadRequest = {
|
|
||||||
downloadId: string
|
|
||||||
url: string
|
|
||||||
localPath: string
|
|
||||||
fileName: string
|
|
||||||
displayName: string
|
|
||||||
metadata: Record<string, string | number>
|
|
||||||
}
|
|
||||||
@ -1,6 +1,4 @@
|
|||||||
export * from './systemResourceInfo'
|
export * from './systemResourceInfo'
|
||||||
export * from './promptTemplate'
|
export * from './promptTemplate'
|
||||||
export * from './appUpdate'
|
export * from './appUpdate'
|
||||||
export * from './fileDownloadRequest'
|
|
||||||
export * from './networkConfig'
|
|
||||||
export * from './selectFiles'
|
export * from './selectFiles'
|
||||||
|
|||||||
@ -1,4 +0,0 @@
|
|||||||
export type NetworkConfig = {
|
|
||||||
proxy?: string
|
|
||||||
ignoreSSL?: boolean
|
|
||||||
}
|
|
||||||
@ -2,3 +2,4 @@ export * from './modelEntity'
|
|||||||
export * from './modelInterface'
|
export * from './modelInterface'
|
||||||
export * from './modelEvent'
|
export * from './modelEvent'
|
||||||
export * from './modelImport'
|
export * from './modelImport'
|
||||||
|
export * from './modelSource'
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Model } from './modelEntity'
|
import { Model } from './modelEntity'
|
||||||
import { OptionType } from './modelImport'
|
import { OptionType } from './modelImport'
|
||||||
|
import { ModelSource } from './modelSource'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model extension for managing models.
|
* Model extension for managing models.
|
||||||
@ -50,4 +51,17 @@ export interface ModelInterface {
|
|||||||
name?: string,
|
name?: string,
|
||||||
optionType?: OptionType
|
optionType?: OptionType
|
||||||
): Promise<void>
|
): Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get model sources
|
||||||
|
*/
|
||||||
|
getSources(): Promise<ModelSource[]>
|
||||||
|
/**
|
||||||
|
* Add a model source
|
||||||
|
*/
|
||||||
|
addSource(source: string): Promise<void>
|
||||||
|
/**
|
||||||
|
* Delete a model source
|
||||||
|
*/
|
||||||
|
deleteSource(source: string): Promise<void>
|
||||||
}
|
}
|
||||||
|
|||||||
68
core/src/types/model/modelSource.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* GGUF Metadata of the model source
|
||||||
|
*/
|
||||||
|
export interface GGUF {
|
||||||
|
architecture: string
|
||||||
|
bos_token: string
|
||||||
|
chat_template: string
|
||||||
|
context_length: number
|
||||||
|
eos_token: string
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Card Metadata of the model source
|
||||||
|
*/
|
||||||
|
export interface CardData {
|
||||||
|
license: string
|
||||||
|
pipeline_tag: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model Metadata of the model source
|
||||||
|
*/
|
||||||
|
export interface Metadata {
|
||||||
|
author: string
|
||||||
|
cardData: CardData
|
||||||
|
createdAt: string
|
||||||
|
description: string
|
||||||
|
disabled: boolean
|
||||||
|
downloads: number
|
||||||
|
gated: boolean
|
||||||
|
gguf: GGUF
|
||||||
|
id: string
|
||||||
|
inference: string
|
||||||
|
lastModified: string
|
||||||
|
likes: number
|
||||||
|
modelId: string
|
||||||
|
pipeline_tag: string
|
||||||
|
private: boolean
|
||||||
|
sha: string
|
||||||
|
siblings: Array<{
|
||||||
|
rfilename: string
|
||||||
|
size: number
|
||||||
|
}>
|
||||||
|
spaces: string[]
|
||||||
|
tags: string[]
|
||||||
|
usedStorage: number
|
||||||
|
apiKey?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model source sibling information
|
||||||
|
*/
|
||||||
|
export interface ModelSibling {
|
||||||
|
id: string
|
||||||
|
size: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model source object
|
||||||
|
*/
|
||||||
|
export interface ModelSource {
|
||||||
|
id: string
|
||||||
|
author?: string
|
||||||
|
metadata: Metadata
|
||||||
|
models: ModelSibling[]
|
||||||
|
type?: string
|
||||||
|
}
|
||||||
@ -1,3 +1,2 @@
|
|||||||
export * from './threadEntity'
|
export * from './threadEntity'
|
||||||
export * from './threadInterface'
|
export * from './threadInterface'
|
||||||
export * from './threadEvent'
|
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
|
|
||||||
import { ThreadEvent } from './threadEvent';
|
|
||||||
|
|
||||||
it('should have the correct values', () => {
|
|
||||||
expect(ThreadEvent.OnThreadStarted).toBe('OnThreadStarted');
|
|
||||||
});
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export enum ThreadEvent {
|
|
||||||
/** The `OnThreadStarted` event is emitted when a thread is started. */
|
|
||||||
OnThreadStarted = 'OnThreadStarted',
|
|
||||||
}
|
|
||||||
@ -1 +1,3 @@
|
|||||||
GTM_ID=xxxx
|
GTM_ID=xxxx
|
||||||
|
POSTHOG_KEY=xxxx
|
||||||
|
POSTHOG_HOST=xxxx
|
||||||
@ -27,6 +27,8 @@ const nextConfig = {
|
|||||||
output: 'export',
|
output: 'export',
|
||||||
env: {
|
env: {
|
||||||
GTM_ID: process.env.GTM_ID,
|
GTM_ID: process.env.GTM_ID,
|
||||||
|
POSTHOG_KEY: process.env.POSTHOG_KEY,
|
||||||
|
POSTHOG_HOST: process.env.POSTHOG_HOST,
|
||||||
},
|
},
|
||||||
transpilePackages: ['@scalar', 'react-tweet'],
|
transpilePackages: ['@scalar', 'react-tweet'],
|
||||||
images: {
|
images: {
|
||||||
|
|||||||
@ -36,6 +36,7 @@
|
|||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"plop": "^4.0.1",
|
"plop": "^4.0.1",
|
||||||
"plop-helper-date": "^1.0.0",
|
"plop-helper-date": "^1.0.0",
|
||||||
|
"posthog-js": "^1.194.6",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-hook-form": "^7.51.1",
|
"react-hook-form": "^7.51.1",
|
||||||
|
|||||||
BIN
docs/public/assets/images/changelog/jan-v0-5-13.gif
Normal file
|
After Width: | Height: | Size: 5.0 MiB |
BIN
docs/public/assets/images/changelog/jan-v0-5-14-deepseek-r1.gif
Normal file
|
After Width: | Height: | Size: 5.0 MiB |
@ -1,129 +1,148 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
|
||||||
<url><loc>https://jan.ai</loc><lastmod>2024-09-09T08:19:45.721Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai</loc><lastmod>2025-03-10T05:06:47.876Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/about</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/about</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/about/handbook</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/about/handbook</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/about/handbook/analytics</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/about/handbook/analytics</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/about/handbook/engineering</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/about/handbook/engineering</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/about/handbook/engineering/ci-cd</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/about/handbook/engineering/ci-cd</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/about/handbook/engineering/qa</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/about/handbook/engineering/qa</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/about/handbook/product-design</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/about/handbook/product-design</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/about/handbook/project-management</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/about/handbook/project-management</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/about/handbook/strategy</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/about/handbook/strategy</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/about/handbook/website-docs</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/about/handbook/website-docs</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/about/investors</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/about/investors</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/about/team</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/about/team</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/about/vision</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/about/vision</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/about/wall-of-love</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/about/wall-of-love</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/blog</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/blog</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/changelog</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/changelog</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/changelog/2023-12-21-faster-inference-across-platform</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/changelog/2023-12-21-faster-inference-across-platform</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/changelog/2024-01-16-settings-options-right-panel</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/changelog/2024-01-16-settings-options-right-panel</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/changelog/2024-01-29-local-api-server</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/changelog/2024-01-29-local-api-server</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/changelog/2024-02-05-jan-data-folder</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/changelog/2024-02-05-jan-data-folder</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/changelog/2024-02-26-home-servers-with-helm</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/changelog/2024-02-10-jan-is-more-stable</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/changelog/2024-03-06-ui-revamp-settings</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/changelog/2024-02-26-home-servers-with-helm</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/changelog/2024-03-11-import-models</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/changelog/2024-03-06-ui-revamp-settings</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/changelog/2024-03-19-nitro-tensorrt-llm-extension</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/changelog/2024-03-11-import-models</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/changelog/2024-04-02-groq-api-integration</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/changelog/2024-03-19-nitro-tensorrt-llm-extension</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/changelog/2024-04-15-new-mistral-extension</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/changelog/2024-04-02-groq-api-integration</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/changelog/2024-04-25-llama3-command-r-hugginface</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/changelog/2024-04-15-new-mistral-extension</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/changelog/2024-05-20-llamacpp-upgrade-new-remote-models</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/changelog/2024-04-25-llama3-command-r-hugginface</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/changelog/2024-05-28-cohere-aya-23-8b-35b-phi-3-medium</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/changelog/2024-05-20-llamacpp-upgrade-new-remote-models</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/changelog/2024-06-21-nvidia-nim-support</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/changelog/2024-05-28-cohere-aya-23-8b-35b-phi-3-medium</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/changelog/2024-07-15-claude-3-5-support</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/changelog/2024-06-21-nvidia-nim-support</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/changelog/2024-09-01-llama3-1-gemma2-support</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/changelog/2024-07-15-claude-3-5-support</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/changelog/2024-09-01-llama3-1-gemma2-support</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/architecture</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/changelog/2024-09-17-improved-cpu-performance</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/assistants</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/changelog/2024-10-24-jan-stable</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/build-extension</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/changelog/2024-11-22-jan-bugs</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/cli</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/changelog/2024-11.14-jan-supports-qwen-coder</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/cli/chat</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/changelog/2024-12-03-jan-is-faster</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/cli/init</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/changelog/2024-12-05-jan-hot-fix-mac</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/cli/kill</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/changelog/2024-12-30-jan-new-privacy</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/cli/models</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/changelog/2025-01-06-key-issues-resolved</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/cli/models/download</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/changelog/2025-01-23-deepseek-r1-jan</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/cli/models/get</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/cli/models/list</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/architecture</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/cli/models/remove</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/assistants</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/cli/models/start</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/build-extension</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/cli/models/stop</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/cli</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/cli/models/update</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/cli/chat</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/cli/ps</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/cli/init</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/cli/pull</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/cli/kill</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/cli/run</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/cli/models</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/cli/serve</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/cli/models/download</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/command-line</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/cli/models/get</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/cortex-cpp</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/cli/models/list</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/cortex-llamacpp</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/cli/models/remove</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/cortex-openvino</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/cli/models/start</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/cortex-python</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/cli/models/stop</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/cortex-tensorrt-llm</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/cli/models/update</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/embeddings</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/cli/ps</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/embeddings/overview</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/cli/pull</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/error-codes</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/cli/run</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/ext-architecture</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/cli/serve</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/fine-tuning</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/command-line</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/fine-tuning/overview</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/cortex-cpp</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/function-calling</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/cortex-llamacpp</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/hardware</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/cortex-openvino</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/installation</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/cortex-python</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/installation/linux</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/cortex-tensorrt-llm</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/installation/mac</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/embeddings</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/installation/windows</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/embeddings/overview</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/model-operations</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/error-codes</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/model-operations/overview</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/ext-architecture</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/py-library</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/fine-tuning</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/quickstart</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/fine-tuning/overview</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/rag</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/function-calling</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/rag/overview</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/hardware</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/server</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/installation</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/text-generation</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/installation/linux</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/ts-library</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/installation/mac</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/vision</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/installation/windows</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/cortex/vision/overview</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/model-operations</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/model-operations/overview</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs/assistants</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/py-library</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs/built-in/llama-cpp</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/quickstart</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs/built-in/tensorrt-llm</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/rag</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs/data-folder</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/rag/overview</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs/desktop</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/server</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs/desktop/linux</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/text-generation</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs/desktop/mac</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/ts-library</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs/desktop/windows</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/vision</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs/error-codes</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/cortex/vision/overview</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs/extensions</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs/install-extensions</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/api-server</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs/models</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/assistants</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs/models/manage-models</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/configure-extensions</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs/models/model-parameters</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/data-folder</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs/quickstart</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/desktop</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs/remote-models/anthropic</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/desktop/linux</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs/remote-models/azure</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/desktop/mac</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs/remote-models/cohere</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/desktop/windows</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs/remote-models/generic-openai</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/error-codes</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs/remote-models/groq</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/extensions</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs/remote-models/martian</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/extensions-settings/model-management</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs/remote-models/mistralai</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/extensions-settings/system-monitoring</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs/remote-models/nvidia-nim</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/install-engines</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs/remote-models/openai</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/install-extensions</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs/remote-models/openrouter</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/local-engines/llama-cpp</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs/remote-models/triton</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/models</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs/settings</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/models/manage-models</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs/shortcuts</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/models/model-parameters</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs/threads</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/privacy</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs/tools/retrieval</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/privacy-policy</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/docs/troubleshooting</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/quickstart</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/download</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/remote-models/anthropic</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/integrations</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/remote-models/cohere</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/integrations/coding/continue-dev</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/remote-models/deepseek</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/integrations/function-calling/interpreter</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/remote-models/google</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/integrations/messaging/llmcord</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/remote-models/groq</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/integrations/workflow-automation/raycast</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/remote-models/martian</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/post/benchmarking-nvidia-tensorrt-llm</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/remote-models/mistralai</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/post/bitdefender</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/remote-models/nvidia-nim</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/post/data-is-moat</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/remote-models/openai</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/post/rag-is-not-enough</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/remote-models/openrouter</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/privacy</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/remote-models/triton</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
<url><loc>https://jan.ai/support</loc><lastmod>2024-09-09T08:19:45.722Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
<url><loc>https://jan.ai/docs/settings</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
|
<url><loc>https://jan.ai/docs/threads</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
|
<url><loc>https://jan.ai/docs/tools/retrieval</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
|
<url><loc>https://jan.ai/docs/troubleshooting</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
|
<url><loc>https://jan.ai/download</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
|
<url><loc>https://jan.ai/integrations</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
|
<url><loc>https://jan.ai/integrations/coding/continue-dev</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
|
<url><loc>https://jan.ai/integrations/coding/tabby</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
|
<url><loc>https://jan.ai/integrations/function-calling/interpreter</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
|
<url><loc>https://jan.ai/integrations/messaging/llmcord</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
|
<url><loc>https://jan.ai/integrations/workflow-automation/n8n</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
|
<url><loc>https://jan.ai/post/benchmarking-nvidia-tensorrt-llm</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
|
<url><loc>https://jan.ai/post/bitdefender</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
|
<url><loc>https://jan.ai/post/data-is-moat</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
|
<url><loc>https://jan.ai/post/deepseek-r1-locally</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
|
<url><loc>https://jan.ai/post/offline-chatgpt-alternative</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
|
<url><loc>https://jan.ai/post/rag-is-not-enough</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
|
<url><loc>https://jan.ai/post/run-ai-models-locally</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
|
<url><loc>https://jan.ai/privacy</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
|
<url><loc>https://jan.ai/support</loc><lastmod>2025-03-10T05:06:47.877Z</lastmod><changefreq>daily</changefreq><priority>1</priority></url>
|
||||||
</urlset>
|
</urlset>
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import ThemeImage from '@/components/ThemeImage'
|
import ThemeImage from '@/components/ThemeImage'
|
||||||
import { AiOutlineGithub } from 'react-icons/ai'
|
import { AiOutlineGithub } from 'react-icons/ai'
|
||||||
import { RiTwitterXFill } from 'react-icons/ri'
|
import { RiTwitterXFill } from 'react-icons/ri'
|
||||||
@ -7,6 +7,7 @@ import { BiLogoDiscordAlt } from 'react-icons/bi'
|
|||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import LogoMark from '@/components/LogoMark'
|
import LogoMark from '@/components/LogoMark'
|
||||||
import { FaLinkedin } from 'react-icons/fa'
|
import { FaLinkedin } from 'react-icons/fa'
|
||||||
|
import posthog from 'posthog-js'
|
||||||
|
|
||||||
const socials = [
|
const socials = [
|
||||||
{
|
{
|
||||||
@ -94,7 +95,7 @@ const menus = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
menu: 'Careers',
|
menu: 'Careers',
|
||||||
path: 'https://homebrew.bamboohr.com/careers',
|
path: 'https://menlo.bamboohr.com/careers',
|
||||||
external: true,
|
external: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -104,6 +105,19 @@ const menus = [
|
|||||||
const getCurrentYear = new Date().getFullYear()
|
const getCurrentYear = new Date().getFullYear()
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
posthog.init(process.env.POSTHOG_KEY as string, {
|
||||||
|
api_host: process.env.POSTHOG_HOST,
|
||||||
|
disable_session_recording: true,
|
||||||
|
person_profiles: 'always',
|
||||||
|
persistence: 'localStorage',
|
||||||
|
})
|
||||||
|
|
||||||
|
posthog.capture('web_page_view', { timestamp: new Date() })
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const { register, handleSubmit, reset } = useForm({
|
const { register, handleSubmit, reset } = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: '',
|
email: '',
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import '@code-hike/mdx/styles.css'
|
|||||||
import { Fragment } from "react"
|
import { Fragment } from "react"
|
||||||
import Script from "next/script"
|
import Script from "next/script"
|
||||||
|
|
||||||
|
|
||||||
export default function App({ Component, pageProps }) {
|
export default function App({ Component, pageProps }) {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
|||||||
@ -93,7 +93,7 @@ We are inspired by and actively try to emulate the paths of companies we admire
|
|||||||
- [Obsidian](https://obsidian.md/)
|
- [Obsidian](https://obsidian.md/)
|
||||||
- [Discourse](https://www.discourse.org/about)
|
- [Discourse](https://www.discourse.org/about)
|
||||||
- [Gitlab](https://handbook.gitlab.com/handbook/company/history/#2017-gitlab-storytime)
|
- [Gitlab](https://handbook.gitlab.com/handbook/company/history/#2017-gitlab-storytime)
|
||||||
- [Redhat](https://www.redhat.com/en/about/development-model)
|
- [Red Hat](https://www.redhat.com/en/about/development-model)
|
||||||
- [Ghost](https://ghost.org/docs/contributing/)
|
- [Ghost](https://ghost.org/docs/contributing/)
|
||||||
- [Lago](https://www.getlago.com/blog/open-source-licensing-and-why-lago-chose-agplv3)
|
- [Lago](https://www.getlago.com/blog/open-source-licensing-and-why-lago-chose-agplv3)
|
||||||
- [Twenty](https://twenty.com/story)
|
- [Twenty](https://twenty.com/story)
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import { Cards, Card } from 'nextra/components'
|
|||||||
|
|
||||||
We're a small, fully-remote team, mostly based in Southeast Asia.
|
We're a small, fully-remote team, mostly based in Southeast Asia.
|
||||||
|
|
||||||
We are committed to become a global company. You can check our [Careers page](https://homebrew.bamboohr.com/careers) if you'd like to join us on our adventure.
|
We are committed to become a global company. You can check our [Careers page](https://menlo.bamboohr.com/careers) if you'd like to join us on our adventure.
|
||||||
|
|
||||||
<Callout emoji="🌏">
|
<Callout emoji="🌏">
|
||||||
Ping us in [Discord](https://discord.gg/AAGQNpJQtH) if you're keen to talk to us!
|
Ping us in [Discord](https://discord.gg/AAGQNpJQtH) if you're keen to talk to us!
|
||||||
|
|||||||
23
docs/src/pages/changelog/2025-01-06-key-issues-resolved.mdx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
title: "A few key issues have been solved!"
|
||||||
|
version: 0.5.13
|
||||||
|
description: "Jan v0.5.13 is here: A few key issues have been solved."
|
||||||
|
date: 2025-01-06
|
||||||
|
ogImage: "/assets/images/changelog/jan-v0-5-13.gif"
|
||||||
|
---
|
||||||
|
|
||||||
|
import ChangelogHeader from "@/components/Changelog/ChangelogHeader"
|
||||||
|
|
||||||
|
<ChangelogHeader title= "Jan v0.5.13: A few key issues have been solved!" date="2025-01-06" ogImage= "/assets/images/changelog/jan-v0-5-13.gif" />
|
||||||
|
|
||||||
|
👋 Jan v0.5.13 is here: A few key issues have been solved!
|
||||||
|
|
||||||
|
### Highlights 🎉
|
||||||
|
|
||||||
|
- Resolved model loading issues on MacOS Intel
|
||||||
|
- Fixed app resetting max_tokens to 8192 on new threads - now uses model settings
|
||||||
|
- Fixed Vulkan settings visibility for some users
|
||||||
|
|
||||||
|
Update your product or download the latest: https://jan.ai
|
||||||
|
|
||||||
|
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.13).
|
||||||
36
docs/src/pages/changelog/2025-01-23-deepseek-r1-jan.mdx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
title: "Run DeepSeek R1 Distills error-free!"
|
||||||
|
version: 0.5.14
|
||||||
|
description: "Jan v0.5.14 is out: Run DeepSeek R1 Distills error-free!"
|
||||||
|
date: 2025-01-23
|
||||||
|
ogImage: "/assets/images/changelog/jan-v0-5-14-deepseek-r1.gif"
|
||||||
|
---
|
||||||
|
|
||||||
|
import ChangelogHeader from "@/components/Changelog/ChangelogHeader"
|
||||||
|
|
||||||
|
<ChangelogHeader title= "Jan v0.5.14: Run DeepSeek R1 Distills error-free!" date="2024-01-23" ogImage= "/assets/images/changelog/jan-v0-5-14-deepseek-r1.gif" />
|
||||||
|
|
||||||
|
👋 Jan v0.5.14 is out: Run DeepSeek R1 Distills error-free!
|
||||||
|
|
||||||
|
You can run DeepSeek R1 distills in Jan error-free. Follow our [step-by-step guide to run DeepSeek R1 locally](/post/deepseek-r1-locally) and get this AI model running on your device in minutes.
|
||||||
|
|
||||||
|
llama.cpp version updated via Cortex—thanks to GG & llama.cpp community!
|
||||||
|
|
||||||
|
- Paste GGUF links into Jan Hub to download
|
||||||
|
- Already downloaded the model but facing issues? Update Jan.
|
||||||
|
|
||||||
|
Models:
|
||||||
|
|
||||||
|
Qwen
|
||||||
|
- DeepSeek-R1-Distill-Qwen-1.5B-GGUF: https://huggingface.co/bartowski/DeepSeek-R1-Distill-Qwen-1.5B-GGUF
|
||||||
|
- DeepSeek-R1-Distill-Qwen-7B-GGUF: https://huggingface.co/bartowski/DeepSeek-R1-Distill-Qwen-7B-GGUF
|
||||||
|
- DeepSeek-R1-Distill-Qwen-14B-GGUF: https://huggingface.co/bartowski/DeepSeek-R1-Distill-Qwen-14B-GGUF
|
||||||
|
- DeepSeek-R1-Distill-Qwen-32B-GGUF: https://huggingface.co/bartowski/DeepSeek-R1-Distill-Qwen-32B-GGUF
|
||||||
|
|
||||||
|
Llama
|
||||||
|
- DeepSeek-R1-Distill-Llama-8B-GGUF: https://huggingface.co/bartowski/DeepSeek-R1-Distill-Llama-8B-GGUF
|
||||||
|
- DeepSeek-R1-Distill-Llama-70B-GGUF: https://huggingface.co/bartowski/DeepSeek-R1-Distill-Llama-70B-GGUF
|
||||||
|
|
||||||
|
Update your Jan or [download the latest](https://jan.ai/).
|
||||||
|
|
||||||
|
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.14).
|
||||||
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 237 KiB After Width: | Height: | Size: 237 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 146 KiB |
BIN
docs/src/pages/docs/_assets/deepseek.png
Normal file
|
After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 162 KiB |
BIN
docs/src/pages/docs/_assets/extensions-05.png
Normal file
|
After Width: | Height: | Size: 185 KiB |
BIN
docs/src/pages/docs/_assets/extensions-06.png
Normal file
|
After Width: | Height: | Size: 185 KiB |
BIN
docs/src/pages/docs/_assets/extensions-07.png
Normal file
|
After Width: | Height: | Size: 187 KiB |
BIN
docs/src/pages/docs/_assets/extensions-08.png
Normal file
|
After Width: | Height: | Size: 187 KiB |
BIN
docs/src/pages/docs/_assets/extensions-09.png
Normal file
|
After Width: | Height: | Size: 192 KiB |
BIN
docs/src/pages/docs/_assets/extensions-10.png
Normal file
|
After Width: | Height: | Size: 187 KiB |
BIN
docs/src/pages/docs/_assets/google.png
Normal file
|
After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 182 KiB After Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 453 KiB After Width: | Height: | Size: 452 KiB |
|
Before Width: | Height: | Size: 207 KiB After Width: | Height: | Size: 207 KiB |
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 302 KiB After Width: | Height: | Size: 302 KiB |
|
Before Width: | Height: | Size: 303 KiB After Width: | Height: | Size: 301 KiB |
|
Before Width: | Height: | Size: 190 KiB After Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 174 KiB |
BIN
docs/src/pages/docs/_assets/model-management-08.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
docs/src/pages/docs/_assets/model-management-09.png
Normal file
|
After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 155 KiB After Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 168 KiB |