Compare commits

..

3 Commits

Author SHA1 Message Date
Nghia Doan
b6a71828da
Merge branch 'dev' into mobile/jan-provider 2025-10-06 19:17:37 +07:00
Nghia Doan
ef1e990d40
Merge branch 'dev' into mobile/jan-provider 2025-10-06 10:19:25 +07:00
Vanalite
79e915d97d feat: Add jan provider extensions for mobile 2025-10-06 09:20:38 +07:00
302 changed files with 4345 additions and 12303 deletions

View File

@ -1,5 +1,5 @@
blank_issues_enabled: true blank_issues_enabled: true
contact_links: contact_links:
- name: Jan Discussions - name: Jan Discussions
url: https://github.com/orgs/janhq/discussions/categories/q-a url: https://github.com/orgs/menloresearch/discussions/categories/q-a
about: Get help, discuss features & roadmap, and share your projects about: Get help, discuss features & roadmap, and share your projects

View File

@ -12,7 +12,7 @@ jobs:
build-and-preview: build-and-preview:
runs-on: [ubuntu-24-04-docker] runs-on: [ubuntu-24-04-docker]
env: env:
MENLO_PLATFORM_BASE_URL: "https://api-dev.jan.ai/v1" JAN_API_BASE: "https://api-dev.jan.ai/v1"
permissions: permissions:
pull-requests: write pull-requests: write
contents: write contents: write
@ -52,7 +52,7 @@ jobs:
- name: Build docker image - name: Build docker image
run: | run: |
docker build --build-arg MENLO_PLATFORM_BASE_URL=${{ env.MENLO_PLATFORM_BASE_URL }} -t ${{ steps.vars.outputs.FULL_IMAGE }} . docker build --build-arg JAN_API_BASE=${{ env.JAN_API_BASE }} -t ${{ steps.vars.outputs.FULL_IMAGE }} .
- name: Push docker image - name: Push docker image
if: github.event_name == 'push' if: github.event_name == 'push'

View File

@ -13,7 +13,7 @@ jobs:
deployments: write deployments: write
pull-requests: write pull-requests: write
env: env:
MENLO_PLATFORM_BASE_URL: "https://api.jan.ai/v1" JAN_API_BASE: "https://api.jan.ai/v1"
GA_MEASUREMENT_ID: "G-YK53MX8M8M" GA_MEASUREMENT_ID: "G-YK53MX8M8M"
CLOUDFLARE_PROJECT_NAME: "jan-server-web" CLOUDFLARE_PROJECT_NAME: "jan-server-web"
steps: steps:
@ -43,7 +43,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: make config-yarn && yarn install && yarn build:core && make build-web-app run: make config-yarn && yarn install && yarn build:core && make build-web-app
env: env:
MENLO_PLATFORM_BASE_URL: ${{ env.MENLO_PLATFORM_BASE_URL }} JAN_API_BASE: ${{ env.JAN_API_BASE }}
GA_MEASUREMENT_ID: ${{ env.GA_MEASUREMENT_ID }} GA_MEASUREMENT_ID: ${{ env.GA_MEASUREMENT_ID }}
- name: Publish to Cloudflare Pages Production - name: Publish to Cloudflare Pages Production

View File

@ -12,7 +12,7 @@ jobs:
build-and-preview: build-and-preview:
runs-on: [ubuntu-24-04-docker] runs-on: [ubuntu-24-04-docker]
env: env:
MENLO_PLATFORM_BASE_URL: "https://api-stag.jan.ai/v1" JAN_API_BASE: "https://api-stag.jan.ai/v1"
permissions: permissions:
pull-requests: write pull-requests: write
contents: write contents: write
@ -52,7 +52,7 @@ jobs:
- name: Build docker image - name: Build docker image
run: | run: |
docker build --build-arg MENLO_PLATFORM_BASE_URL=${{ env.MENLO_PLATFORM_BASE_URL }} -t ${{ steps.vars.outputs.FULL_IMAGE }} . docker build --build-arg JAN_API_BASE=${{ env.JAN_API_BASE }} -t ${{ steps.vars.outputs.FULL_IMAGE }} .
- name: Push docker image - name: Push docker image
if: github.event_name == 'push' if: github.event_name == 'push'

View File

@ -168,62 +168,62 @@ jobs:
AWS_DEFAULT_REGION: ${{ secrets.DELTA_AWS_REGION }} AWS_DEFAULT_REGION: ${{ secrets.DELTA_AWS_REGION }}
AWS_EC2_METADATA_DISABLED: 'true' AWS_EC2_METADATA_DISABLED: 'true'
# noti-discord-nightly-and-update-url-readme: noti-discord-nightly-and-update-url-readme:
# needs: needs:
# [ [
# build-macos, build-macos,
# build-windows-x64, build-windows-x64,
# build-linux-x64, build-linux-x64,
# get-update-version, get-update-version,
# set-public-provider, set-public-provider,
# sync-temp-to-latest, sync-temp-to-latest,
# ] ]
# secrets: inherit secrets: inherit
# if: github.event_name == 'schedule' if: github.event_name == 'schedule'
# uses: ./.github/workflows/template-noti-discord-and-update-url-readme.yml uses: ./.github/workflows/template-noti-discord-and-update-url-readme.yml
# with: with:
# ref: refs/heads/dev ref: refs/heads/dev
# build_reason: Nightly build_reason: Nightly
# push_to_branch: dev push_to_branch: dev
# new_version: ${{ needs.get-update-version.outputs.new_version }} new_version: ${{ needs.get-update-version.outputs.new_version }}
# noti-discord-pre-release-and-update-url-readme: noti-discord-pre-release-and-update-url-readme:
# needs: needs:
# [ [
# build-macos, build-macos,
# build-windows-x64, build-windows-x64,
# build-linux-x64, build-linux-x64,
# get-update-version, get-update-version,
# set-public-provider, set-public-provider,
# sync-temp-to-latest, sync-temp-to-latest,
# ] ]
# secrets: inherit secrets: inherit
# if: github.event_name == 'push' if: github.event_name == 'push'
# uses: ./.github/workflows/template-noti-discord-and-update-url-readme.yml uses: ./.github/workflows/template-noti-discord-and-update-url-readme.yml
# with: with:
# ref: refs/heads/dev ref: refs/heads/dev
# build_reason: Pre-release build_reason: Pre-release
# push_to_branch: dev push_to_branch: dev
# new_version: ${{ needs.get-update-version.outputs.new_version }} new_version: ${{ needs.get-update-version.outputs.new_version }}
# noti-discord-manual-and-update-url-readme: noti-discord-manual-and-update-url-readme:
# needs: needs:
# [ [
# build-macos, build-macos,
# build-windows-x64, build-windows-x64,
# build-linux-x64, build-linux-x64,
# get-update-version, get-update-version,
# set-public-provider, set-public-provider,
# sync-temp-to-latest, sync-temp-to-latest,
# ] ]
# secrets: inherit secrets: inherit
# if: github.event_name == 'workflow_dispatch' && github.event.inputs.public_provider == 'aws-s3' if: github.event_name == 'workflow_dispatch' && github.event.inputs.public_provider == 'aws-s3'
# uses: ./.github/workflows/template-noti-discord-and-update-url-readme.yml uses: ./.github/workflows/template-noti-discord-and-update-url-readme.yml
# with: with:
# ref: refs/heads/dev ref: refs/heads/dev
# build_reason: Manual build_reason: Manual
# push_to_branch: dev push_to_branch: dev
# new_version: ${{ needs.get-update-version.outputs.new_version }} new_version: ${{ needs.get-update-version.outputs.new_version }}
comment-pr-build-url: comment-pr-build-url:
needs: needs:

View File

@ -82,11 +82,11 @@ jobs:
VERSION=${{ needs.get-update-version.outputs.new_version }} VERSION=${{ needs.get-update-version.outputs.new_version }}
PUB_DATE=$(date -u +"%Y-%m-%dT%H:%M:%S.%3NZ") PUB_DATE=$(date -u +"%Y-%m-%dT%H:%M:%S.%3NZ")
LINUX_SIGNATURE="${{ needs.build-linux-x64.outputs.APPIMAGE_SIG }}" LINUX_SIGNATURE="${{ needs.build-linux-x64.outputs.APPIMAGE_SIG }}"
LINUX_URL="https://github.com/janhq/jan/releases/download/v${{ needs.get-update-version.outputs.new_version }}/${{ needs.build-linux-x64.outputs.APPIMAGE_FILE_NAME }}" LINUX_URL="https://github.com/menloresearch/jan/releases/download/v${{ needs.get-update-version.outputs.new_version }}/${{ needs.build-linux-x64.outputs.APPIMAGE_FILE_NAME }}"
WINDOWS_SIGNATURE="${{ needs.build-windows-x64.outputs.WIN_SIG }}" WINDOWS_SIGNATURE="${{ needs.build-windows-x64.outputs.WIN_SIG }}"
WINDOWS_URL="https://github.com/janhq/jan/releases/download/v${{ needs.get-update-version.outputs.new_version }}/${{ needs.build-windows-x64.outputs.FILE_NAME }}" WINDOWS_URL="https://github.com/menloresearch/jan/releases/download/v${{ needs.get-update-version.outputs.new_version }}/${{ needs.build-windows-x64.outputs.FILE_NAME }}"
DARWIN_SIGNATURE="${{ needs.build-macos.outputs.MAC_UNIVERSAL_SIG }}" DARWIN_SIGNATURE="${{ needs.build-macos.outputs.MAC_UNIVERSAL_SIG }}"
DARWIN_URL="https://github.com/janhq/jan/releases/download/v${{ needs.get-update-version.outputs.new_version }}/${{ needs.build-macos.outputs.TAR_NAME }}" DARWIN_URL="https://github.com/menloresearch/jan/releases/download/v${{ needs.get-update-version.outputs.new_version }}/${{ needs.build-macos.outputs.TAR_NAME }}"
jq --arg version "$VERSION" \ jq --arg version "$VERSION" \
--arg pub_date "$PUB_DATE" \ --arg pub_date "$PUB_DATE" \

View File

@ -29,7 +29,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

View File

@ -50,6 +50,6 @@ jobs:
- macOS Universal: https://delta.jan.ai/nightly/Jan-nightly_{{ VERSION }}_universal.dmg - macOS Universal: https://delta.jan.ai/nightly/Jan-nightly_{{ VERSION }}_universal.dmg
- Linux Deb: https://delta.jan.ai/nightly/Jan-nightly_{{ VERSION }}_amd64.deb - Linux Deb: https://delta.jan.ai/nightly/Jan-nightly_{{ VERSION }}_amd64.deb
- Linux AppImage: https://delta.jan.ai/nightly/Jan-nightly_{{ VERSION }}_amd64.AppImage - Linux AppImage: https://delta.jan.ai/nightly/Jan-nightly_{{ VERSION }}_amd64.AppImage
- Github action run: https://github.com/janhq/jan/actions/runs/{{ GITHUB_RUN_ID }} - Github action run: https://github.com/menloresearch/jan/actions/runs/{{ GITHUB_RUN_ID }}
env: env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}

View File

@ -49,8 +49,6 @@ jobs:
# Update tauri.conf.json # Update tauri.conf.json
jq --arg version "${{ inputs.new_version }}" '.version = $version | .bundle.createUpdaterArtifacts = false' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json jq --arg version "${{ inputs.new_version }}" '.version = $version | .bundle.createUpdaterArtifacts = false' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json
mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json
jq '.bundle.windows.nsis.template = "tauri.bundle.windows.nsis.template"' ./src-tauri/tauri.windows.conf.json > /tmp/tauri.windows.conf.json
mv /tmp/tauri.windows.conf.json ./src-tauri/tauri.windows.conf.json
jq '.bundle.windows.signCommand = "echo External build - skipping signature: %1"' ./src-tauri/tauri.windows.conf.json > /tmp/tauri.windows.conf.json jq '.bundle.windows.signCommand = "echo External build - skipping signature: %1"' ./src-tauri/tauri.windows.conf.json > /tmp/tauri.windows.conf.json
mv /tmp/tauri.windows.conf.json ./src-tauri/tauri.windows.conf.json mv /tmp/tauri.windows.conf.json ./src-tauri/tauri.windows.conf.json
jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json
@ -82,36 +80,6 @@ jobs:
echo "---------./src-tauri/Cargo.toml---------" echo "---------./src-tauri/Cargo.toml---------"
cat ./src-tauri/Cargo.toml cat ./src-tauri/Cargo.toml
generate_build_version() {
### Examble
### input 0.5.6 output will be 0.5.6 and 0.5.6.0
### input 0.5.6-rc2-beta output will be 0.5.6 and 0.5.6.2
### input 0.5.6-1213 output will be 0.5.6 and and 0.5.6.1213
local new_version="$1"
local base_version
local t_value
# Check if it has a "-"
if [[ "$new_version" == *-* ]]; then
base_version="${new_version%%-*}" # part before -
suffix="${new_version#*-}" # part after -
# Check if it is rcX-beta
if [[ "$suffix" =~ ^rc([0-9]+)-beta$ ]]; then
t_value="${BASH_REMATCH[1]}"
else
t_value="$suffix"
fi
else
base_version="$new_version"
t_value="0"
fi
# Export two values
new_base_version="$base_version"
new_build_version="${base_version}.${t_value}"
}
generate_build_version ${{ inputs.new_version }}
sed -i "s/jan_version/$new_base_version/g" ./src-tauri/tauri.bundle.windows.nsis.template
sed -i "s/jan_build/$new_build_version/g" ./src-tauri/tauri.bundle.windows.nsis.template
if [ "${{ inputs.channel }}" != "stable" ]; then if [ "${{ inputs.channel }}" != "stable" ]; then
jq '.plugins.updater.endpoints = ["https://delta.jan.ai/${{ inputs.channel }}/latest.json"]' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json jq '.plugins.updater.endpoints = ["https://delta.jan.ai/${{ inputs.channel }}/latest.json"]' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json
mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json
@ -135,14 +103,7 @@ jobs:
chmod +x .github/scripts/rename-workspace.sh chmod +x .github/scripts/rename-workspace.sh
.github/scripts/rename-workspace.sh ./package.json ${{ inputs.channel }} .github/scripts/rename-workspace.sh ./package.json ${{ inputs.channel }}
cat ./package.json cat ./package.json
sed -i "s/jan_productname/Jan-${{ inputs.channel }}/g" ./src-tauri/tauri.bundle.windows.nsis.template
sed -i "s/jan_mainbinaryname/jan-${{ inputs.channel }}/g" ./src-tauri/tauri.bundle.windows.nsis.template
else
sed -i "s/jan_productname/Jan/g" ./src-tauri/tauri.bundle.windows.nsis.template
sed -i "s/jan_mainbinaryname/jan/g" ./src-tauri/tauri.bundle.windows.nsis.template
fi fi
echo "---------nsis.template---------"
cat ./src-tauri/tauri.bundle.windows.nsis.template
- name: Build app - name: Build app
shell: bash shell: bash
run: | run: |

View File

@ -98,15 +98,9 @@ jobs:
# Update tauri.conf.json # Update tauri.conf.json
jq --arg version "${{ inputs.new_version }}" '.version = $version | .bundle.createUpdaterArtifacts = true' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json jq --arg version "${{ inputs.new_version }}" '.version = $version | .bundle.createUpdaterArtifacts = true' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json
mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json
jq '.bundle.windows.nsis.template = "tauri.bundle.windows.nsis.template"' ./src-tauri/tauri.windows.conf.json > /tmp/tauri.windows.conf.json
mv /tmp/tauri.windows.conf.json ./src-tauri/tauri.windows.conf.json
jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json
mv /tmp/package.json web-app/package.json mv /tmp/package.json web-app/package.json
# Add sign commands to tauri.windows.conf.json
jq '.bundle.windows.signCommand = "powershell -ExecutionPolicy Bypass -File ./sign.ps1 %1"' ./src-tauri/tauri.windows.conf.json > /tmp/tauri.windows.conf.json
mv /tmp/tauri.windows.conf.json ./src-tauri/tauri.windows.conf.json
# Update tauri plugin versions # Update tauri plugin versions
jq --arg version "${{ inputs.new_version }}" '.version = $version' ./src-tauri/plugins/tauri-plugin-hardware/package.json > /tmp/package.json jq --arg version "${{ inputs.new_version }}" '.version = $version' ./src-tauri/plugins/tauri-plugin-hardware/package.json > /tmp/package.json
@ -133,35 +127,9 @@ jobs:
echo "---------./src-tauri/Cargo.toml---------" echo "---------./src-tauri/Cargo.toml---------"
cat ./src-tauri/Cargo.toml cat ./src-tauri/Cargo.toml
generate_build_version() { # Add sign commands to tauri.windows.conf.json
### Example jq '.bundle.windows.signCommand = "powershell -ExecutionPolicy Bypass -File ./sign.ps1 %1"' ./src-tauri/tauri.windows.conf.json > /tmp/tauri.windows.conf.json
### input 0.5.6 output will be 0.5.6 and 0.5.6.0 mv /tmp/tauri.windows.conf.json ./src-tauri/tauri.windows.conf.json
### input 0.5.6-rc2-beta output will be 0.5.6 and 0.5.6.2
### input 0.5.6-1213 output will be 0.5.6 and and 0.5.6.1213
local new_version="$1"
local base_version
local t_value
# Check if it has a "-"
if [[ "$new_version" == *-* ]]; then
base_version="${new_version%%-*}" # part before -
suffix="${new_version#*-}" # part after -
# Check if it is rcX-beta
if [[ "$suffix" =~ ^rc([0-9]+)-beta$ ]]; then
t_value="${BASH_REMATCH[1]}"
else
t_value="$suffix"
fi
else
base_version="$new_version"
t_value="0"
fi
# Export two values
new_base_version="$base_version"
new_build_version="${base_version}.${t_value}"
}
generate_build_version ${{ inputs.new_version }}
sed -i "s/jan_version/$new_base_version/g" ./src-tauri/tauri.bundle.windows.nsis.template
sed -i "s/jan_build/$new_build_version/g" ./src-tauri/tauri.bundle.windows.nsis.template
echo "---------tauri.windows.conf.json---------" echo "---------tauri.windows.conf.json---------"
cat ./src-tauri/tauri.windows.conf.json cat ./src-tauri/tauri.windows.conf.json
@ -195,14 +163,7 @@ jobs:
chmod +x .github/scripts/rename-workspace.sh chmod +x .github/scripts/rename-workspace.sh
.github/scripts/rename-workspace.sh ./package.json ${{ inputs.channel }} .github/scripts/rename-workspace.sh ./package.json ${{ inputs.channel }}
cat ./package.json cat ./package.json
sed -i "s/jan_productname/Jan-${{ inputs.channel }}/g" ./src-tauri/tauri.bundle.windows.nsis.template
sed -i "s/jan_mainbinaryname/jan-${{ inputs.channel }}/g" ./src-tauri/tauri.bundle.windows.nsis.template
else
sed -i "s/jan_productname/Jan/g" ./src-tauri/tauri.bundle.windows.nsis.template
sed -i "s/jan_mainbinaryname/jan/g" ./src-tauri/tauri.bundle.windows.nsis.template
fi fi
echo "---------nsis.template---------"
cat ./src-tauri/tauri.bundle.windows.nsis.template
- name: Install AzureSignTool - name: Install AzureSignTool
run: | run: |
@ -289,3 +250,13 @@ jobs:
asset_path: ./src-tauri/target/release/bundle/nsis/${{ steps.metadata.outputs.FILE_NAME }} asset_path: ./src-tauri/target/release/bundle/nsis/${{ steps.metadata.outputs.FILE_NAME }}
asset_name: ${{ steps.metadata.outputs.FILE_NAME }} asset_name: ${{ steps.metadata.outputs.FILE_NAME }}
asset_content_type: application/octet-stream asset_content_type: application/octet-stream
- name: Upload release assert if public provider is github
if: inputs.public_provider == 'github'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: actions/upload-release-asset@v1.0.1
with:
upload_url: ${{ inputs.upload_url }}
asset_path: ./src-tauri/target/release/bundle/msi/${{ steps.metadata.outputs.MSI_FILE_NAME }}
asset_name: ${{ steps.metadata.outputs.MSI_FILE_NAME }}
asset_content_type: application/octet-stream

View File

@ -143,7 +143,7 @@ jan/
**Option 1: The Easy Way (Make)** **Option 1: The Easy Way (Make)**
```bash ```bash
git clone https://github.com/janhq/jan git clone https://github.com/menloresearch/jan
cd jan cd jan
make dev make dev
``` ```
@ -152,8 +152,8 @@ make dev
### Reporting Bugs ### Reporting Bugs
- **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/janhq/jan/issues) - **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/menloresearch/jan/issues)
- If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/janhq/jan/issues/new) - If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/menloresearch/jan/issues/new)
- Include your system specs and error logs - it helps a ton - Include your system specs and error logs - it helps a ton
### Suggesting Enhancements ### Suggesting Enhancements

View File

@ -1,8 +1,8 @@
# Stage 1: Build stage with Node.js and Yarn v4 # Stage 1: Build stage with Node.js and Yarn v4
FROM node:20-alpine AS builder FROM node:20-alpine AS builder
ARG MENLO_PLATFORM_BASE_URL=https://api-dev.menlo.ai/v1 ARG JAN_API_BASE=https://api-dev.jan.ai/v1
ENV MENLO_PLATFORM_BASE_URL=$MENLO_PLATFORM_BASE_URL ENV JAN_API_BASE=$JAN_API_BASE
# Install build dependencies # Install build dependencies
RUN apk add --no-cache \ RUN apk add --no-cache \

View File

@ -117,6 +117,7 @@ lint: install-and-build
test: lint test: lint
yarn download:bin yarn download:bin
ifeq ($(OS),Windows_NT) ifeq ($(OS),Windows_NT)
yarn download:windows-installer
endif endif
yarn test yarn test
yarn copy:assets:tauri yarn copy:assets:tauri

View File

@ -4,10 +4,10 @@
<p align="center"> <p align="center">
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section --> <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
<img alt="GitHub commit activity" src="https://img.shields.io/github/commit-activity/m/janhq/jan"/> <img alt="GitHub commit activity" src="https://img.shields.io/github/commit-activity/m/menloresearch/jan"/>
<img alt="Github Last Commit" src="https://img.shields.io/github/last-commit/janhq/jan"/> <img alt="Github Last Commit" src="https://img.shields.io/github/last-commit/menloresearch/jan"/>
<img alt="Github Contributors" src="https://img.shields.io/github/contributors/janhq/jan"/> <img alt="Github Contributors" src="https://img.shields.io/github/contributors/menloresearch/jan"/>
<img alt="GitHub closed issues" src="https://img.shields.io/github/issues-closed/janhq/jan"/> <img alt="GitHub closed issues" src="https://img.shields.io/github/issues-closed/menloresearch/jan"/>
<img alt="Discord" src="https://img.shields.io/discord/1107178041848909847?label=discord"/> <img alt="Discord" src="https://img.shields.io/discord/1107178041848909847?label=discord"/>
</p> </p>
@ -15,7 +15,7 @@
<a href="https://www.jan.ai/docs/desktop">Getting Started</a> <a href="https://www.jan.ai/docs/desktop">Getting Started</a>
- <a href="https://discord.gg/Exe46xPMbK">Community</a> - <a href="https://discord.gg/Exe46xPMbK">Community</a>
- <a href="https://jan.ai/changelog">Changelog</a> - <a href="https://jan.ai/changelog">Changelog</a>
- <a href="https://github.com/janhq/jan/issues">Bug reports</a> - <a href="https://github.com/menloresearch/jan/issues">Bug reports</a>
</p> </p>
Jan is bringing the best of open-source AI in an easy-to-use product. Download and run LLMs with **full control** and **privacy**. Jan is bringing the best of open-source AI in an easy-to-use product. Download and run LLMs with **full control** and **privacy**.
@ -48,7 +48,7 @@ The easiest way to get started is by downloading one of the following versions f
</table> </table>
Download from [jan.ai](https://jan.ai/) or [GitHub Releases](https://github.com/janhq/jan/releases). Download from [jan.ai](https://jan.ai/) or [GitHub Releases](https://github.com/menloresearch/jan/releases).
## Features ## Features
@ -73,7 +73,7 @@ For those who enjoy the scenic route:
### Run with Make ### Run with Make
```bash ```bash
git clone https://github.com/janhq/jan git clone https://github.com/menloresearch/jan
cd jan cd jan
make dev make dev
``` ```
@ -128,7 +128,7 @@ Contributions welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full spiel
## Contact ## Contact
- **Bugs**: [GitHub Issues](https://github.com/janhq/jan/issues) - **Bugs**: [GitHub Issues](https://github.com/menloresearch/jan/issues)
- **Business**: hello@jan.ai - **Business**: hello@jan.ai
- **Jobs**: hr@jan.ai - **Jobs**: hr@jan.ai
- **General Discussion**: [Discord](https://discord.gg/FTk2MvZwJH) - **General Discussion**: [Discord](https://discord.gg/FTk2MvZwJH)

View File

@ -1,7 +1,7 @@
# Core dependencies # Core dependencies
cua-computer[all]~=0.3.5 cua-computer[all]~=0.3.5
cua-agent[all]~=0.3.0 cua-agent[all]~=0.3.0
cua-agent @ git+https://github.com/janhq/cua.git@compute-agent-0.3.0-patch#subdirectory=libs/python/agent cua-agent @ git+https://github.com/menloresearch/cua.git@compute-agent-0.3.0-patch#subdirectory=libs/python/agent
# ReportPortal integration # ReportPortal integration
reportportal-client~=5.6.5 reportportal-client~=5.6.5

View File

@ -13,7 +13,7 @@ import * as core from '@janhq/core'
## Build an Extension ## Build an Extension
1. Download an extension template, for example, [https://github.com/janhq/extension-template](https://github.com/janhq/extension-template). 1. Download an extension template, for example, [https://github.com/menloresearch/extension-template](https://github.com/menloresearch/extension-template).
2. Update the source code: 2. Update the source code:

View File

@ -31,7 +31,7 @@
"@vitest/coverage-v8": "^2.1.8", "@vitest/coverage-v8": "^2.1.8",
"@vitest/ui": "^2.1.8", "@vitest/ui": "^2.1.8",
"eslint": "8.57.0", "eslint": "8.57.0",
"happy-dom": "^20.0.0", "happy-dom": "^15.11.6",
"pacote": "^21.0.0", "pacote": "^21.0.0",
"react": "19.0.0", "react": "19.0.0",
"request": "^2.88.2", "request": "^2.88.2",

View File

@ -11,8 +11,6 @@ export enum ExtensionTypeEnum {
HuggingFace = 'huggingFace', HuggingFace = 'huggingFace',
Engine = 'engine', Engine = 'engine',
Hardware = 'hardware', Hardware = 'hardware',
RAG = 'rag',
VectorDB = 'vectorDB',
} }
export interface ExtensionType { export interface ExtensionType {

View File

@ -182,7 +182,6 @@ export interface SessionInfo {
port: number // llama-server output port (corrected from portid) port: number // llama-server output port (corrected from portid)
model_id: string //name of the model model_id: string //name of the model
model_path: string // path of the loaded model model_path: string // path of the loaded model
is_embedding: boolean
api_key: string api_key: string
mmproj_path?: string mmproj_path?: string
} }

View File

@ -23,8 +23,3 @@ export { MCPExtension } from './mcp'
* Base AI Engines. * Base AI Engines.
*/ */
export * from './engines' export * from './engines'
export { RAGExtension, RAG_INTERNAL_SERVER } from './rag'
export type { AttachmentInput, IngestAttachmentsResult } from './rag'
export { VectorDBExtension } from './vector-db'
export type { SearchMode, VectorDBStatus, VectorChunkInput, VectorSearchResult, AttachmentFileInfo, VectorDBFileInput, VectorDBIngestOptions } from './vector-db'

View File

@ -1,36 +0,0 @@
import { BaseExtension, ExtensionTypeEnum } from '../extension'
import type { MCPTool, MCPToolCallResult } from '../../types'
import type { AttachmentFileInfo } from './vector-db'
export interface AttachmentInput {
path: string
name?: string
type?: string
size?: number
}
export interface IngestAttachmentsResult {
filesProcessed: number
chunksInserted: number
files: AttachmentFileInfo[]
}
export const RAG_INTERNAL_SERVER = 'rag-internal'
/**
* RAG extension base: exposes RAG tools and orchestration API.
*/
export abstract class RAGExtension extends BaseExtension {
type(): ExtensionTypeEnum | undefined {
return ExtensionTypeEnum.RAG
}
abstract getTools(): Promise<MCPTool[]>
/**
* Lightweight list of tool names for quick routing/lookup.
*/
abstract getToolNames(): Promise<string[]>
abstract callTool(toolName: string, args: Record<string, unknown>): Promise<MCPToolCallResult>
abstract ingestAttachments(threadId: string, files: AttachmentInput[]): Promise<IngestAttachmentsResult>
}

View File

@ -1,82 +0,0 @@
import { BaseExtension, ExtensionTypeEnum } from '../extension'
export type SearchMode = 'auto' | 'ann' | 'linear'
export interface VectorDBStatus {
ann_available: boolean
}
export interface VectorChunkInput {
text: string
embedding: number[]
}
export interface VectorSearchResult {
id: string
text: string
score?: number
file_id: string
chunk_file_order: number
}
export interface AttachmentFileInfo {
id: string
name?: string
path?: string
type?: string
size?: number
chunk_count: number
}
// High-level input types for file ingestion
export interface VectorDBFileInput {
path: string
name?: string
type?: string
size?: number
}
export interface VectorDBIngestOptions {
chunkSize: number
chunkOverlap: number
}
/**
* Vector DB extension base: abstraction over local vector storage and search.
*/
export abstract class VectorDBExtension extends BaseExtension {
type(): ExtensionTypeEnum | undefined {
return ExtensionTypeEnum.VectorDB
}
abstract getStatus(): Promise<VectorDBStatus>
abstract createCollection(threadId: string, dimension: number): Promise<void>
abstract insertChunks(
threadId: string,
fileId: string,
chunks: VectorChunkInput[]
): Promise<void>
abstract ingestFile(
threadId: string,
file: VectorDBFileInput,
opts: VectorDBIngestOptions
): Promise<AttachmentFileInfo>
abstract searchCollection(
threadId: string,
query_embedding: number[],
limit: number,
threshold: number,
mode?: SearchMode,
fileIds?: string[]
): Promise<VectorSearchResult[]>
abstract deleteChunks(threadId: string, ids: string[]): Promise<void>
abstract deleteFile(threadId: string, fileId: string): Promise<void>
abstract deleteCollection(threadId: string): Promise<void>
abstract listAttachments(threadId: string, limit?: number): Promise<AttachmentFileInfo[]>
abstract getChunks(
threadId: string,
fileId: string,
startOrder: number,
endOrder: number
): Promise<VectorSearchResult[]>
}

View File

@ -12,8 +12,6 @@ export type SettingComponentProps = {
extensionName?: string extensionName?: string
requireModelReload?: boolean requireModelReload?: boolean
configType?: ConfigType configType?: ConfigType
titleKey?: string
descriptionKey?: string
} }
export type ConfigType = 'runtime' | 'setting' export type ConfigType = 'runtime' | 'setting'

View File

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

View File

@ -1581,7 +1581,7 @@
}, },
"cover": { "cover": {
"type": "string", "type": "string",
"example": "https://raw.githubusercontent.com/janhq/jan/main/models/trinity-v1.2-7b/cover.png" "example": "https://raw.githubusercontent.com/menloresearch/jan/main/models/trinity-v1.2-7b/cover.png"
}, },
"engine": { "engine": {
"type": "string", "type": "string",

View File

@ -27,7 +27,7 @@ export const APIReference = () => {
<ApiReferenceReact <ApiReferenceReact
configuration={{ configuration={{
spec: { spec: {
url: 'https://raw.githubusercontent.com/janhq/docs/main/public/openapi/jan.json', url: 'https://raw.githubusercontent.com/menloresearch/docs/main/public/openapi/jan.json',
}, },
theme: 'alternate', theme: 'alternate',
hideModels: true, hideModels: true,

View File

@ -57,7 +57,7 @@ const Changelog = () => {
<p className="text-base mt-2 leading-relaxed"> <p className="text-base mt-2 leading-relaxed">
Latest release updates from the Jan team. Check out our&nbsp; Latest release updates from the Jan team. Check out our&nbsp;
<a <a
href="https://github.com/orgs/janhq/projects/30" href="https://github.com/orgs/menloresearch/projects/30"
className="text-blue-600 dark:text-blue-400 cursor-pointer" className="text-blue-600 dark:text-blue-400 cursor-pointer"
> >
Roadmap Roadmap
@ -150,7 +150,7 @@ const Changelog = () => {
<div className="text-center"> <div className="text-center">
<Link <Link
href="https://github.com/janhq/jan/releases" href="https://github.com/menloresearch/jan/releases"
target="_blank" target="_blank"
className="dark:nx-bg-neutral-900 dark:text-white bg-black text-white hover:text-white justify-center dark:border dark:border-neutral-800 flex-shrink-0 px-4 py-3 rounded-xl inline-flex items-center" className="dark:nx-bg-neutral-900 dark:text-white bg-black text-white hover:text-white justify-center dark:border dark:border-neutral-800 flex-shrink-0 px-4 py-3 rounded-xl inline-flex items-center"
> >

View File

@ -72,7 +72,7 @@ export default function CardDownload({ lastRelease }: Props) {
return { return {
...system, ...system,
href: `https://github.com/janhq/jan/releases/download/${lastRelease.tag_name}/${downloadUrl}`, href: `https://github.com/menloresearch/jan/releases/download/${lastRelease.tag_name}/${downloadUrl}`,
size: asset ? formatFileSize(asset.size) : undefined, size: asset ? formatFileSize(asset.size) : undefined,
} }
}) })

View File

@ -139,7 +139,7 @@ const DropdownDownload = ({ lastRelease }: Props) => {
return { return {
...system, ...system,
href: `https://github.com/janhq/jan/releases/download/${lastRelease.tag_name}/${downloadUrl}`, href: `https://github.com/menloresearch/jan/releases/download/${lastRelease.tag_name}/${downloadUrl}`,
size: asset ? formatFileSize(asset.size) : undefined, size: asset ? formatFileSize(asset.size) : undefined,
} }
}) })

View File

@ -23,7 +23,7 @@ const BuiltWithLove = () => {
</div> </div>
<div className="flex flex-col lg:flex-row gap-8 mt-8 items-center justify-center"> <div className="flex flex-col lg:flex-row gap-8 mt-8 items-center justify-center">
<a <a
href="https://github.com/janhq/jan" href="https://github.com/menloresearch/jan"
target="_blank" target="_blank"
className="dark:bg-white bg-black inline-flex w-56 px-4 py-3 rounded-xl cursor-pointer justify-center items-start space-x-4 " className="dark:bg-white bg-black inline-flex w-56 px-4 py-3 rounded-xl cursor-pointer justify-center items-start space-x-4 "
> >

View File

@ -44,7 +44,7 @@ const Hero = () => {
<div className="mt-10 text-center"> <div className="mt-10 text-center">
<div> <div>
<Link <Link
href="https://github.com/janhq/jan/releases" href="https://github.com/menloresearch/jan/releases"
target="_blank" target="_blank"
className="hidden lg:inline-block" className="hidden lg:inline-block"
> >

View File

@ -95,7 +95,7 @@ const Home = () => {
<div className="container mx-auto relative z-10"> <div className="container mx-auto relative z-10">
<div className="flex justify-center items-center mt-14 lg:mt-20 px-4"> <div className="flex justify-center items-center mt-14 lg:mt-20 px-4">
<a <a
href={`https://github.com/janhq/jan/releases/tag/${lastVersion}`} href={`https://github.com/menloresearch/jan/releases/tag/${lastVersion}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="bg-black/40 px-3 lg:px-4 rounded-full h-10 inline-flex items-center max-w-full animate-fade-in delay-100" className="bg-black/40 px-3 lg:px-4 rounded-full h-10 inline-flex items-center max-w-full animate-fade-in delay-100"
@ -270,7 +270,7 @@ const Home = () => {
data-delay="600" data-delay="600"
> >
<a <a
href="https://github.com/janhq/jan" href="https://github.com/menloresearch/jan"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
@ -387,7 +387,7 @@ const Home = () => {
</div> </div>
<a <a
className="hidden md:block" className="hidden md:block"
href="https://github.com/janhq/jan" href="https://github.com/menloresearch/jan"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
@ -413,7 +413,7 @@ const Home = () => {
</p> </p>
<a <a
className="md:hidden mt-4 block w-full" className="md:hidden mt-4 block w-full"
href="https://github.com/janhq/jan" href="https://github.com/menloresearch/jan"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >

View File

@ -95,7 +95,7 @@ const Navbar = ({ noScroll }: { noScroll?: boolean }) => {
})} })}
<li> <li>
<a <a
href="https://github.com/janhq/jan/releases/latest" href="https://github.com/menloresearch/jan/releases/latest"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
@ -141,7 +141,7 @@ const Navbar = ({ noScroll }: { noScroll?: boolean }) => {
<FaLinkedinIn className="size-5" /> <FaLinkedinIn className="size-5" />
</a> </a>
<a <a
href="https://github.com/janhq/jan" href="https://github.com/menloresearch/jan"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="rounded-lg flex items-center justify-center" className="rounded-lg flex items-center justify-center"
@ -156,7 +156,7 @@ const Navbar = ({ noScroll }: { noScroll?: boolean }) => {
{/* Mobile Download Button and Hamburger */} {/* Mobile Download Button and Hamburger */}
<div className="lg:hidden flex items-center gap-3"> <div className="lg:hidden flex items-center gap-3">
<a <a
href="https://github.com/janhq/jan/releases/latest" href="https://github.com/menloresearch/jan/releases/latest"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
@ -278,7 +278,7 @@ const Navbar = ({ noScroll }: { noScroll?: boolean }) => {
<FaLinkedinIn className="size-5" /> <FaLinkedinIn className="size-5" />
</a> </a>
<a <a
href="https://github.com/janhq/jan" href="https://github.com/menloresearch/jan"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-black rounded-lg flex items-center justify-center" className="text-black rounded-lg flex items-center justify-center"
@ -296,7 +296,7 @@ const Navbar = ({ noScroll }: { noScroll?: boolean }) => {
asChild asChild
> >
<a <a
href="https://github.com/janhq/jan/releases/latest" href="https://github.com/menloresearch/jan/releases/latest"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >

View File

@ -120,7 +120,7 @@ export function DropdownButton({
return { return {
...option, ...option,
href: `https://github.com/janhq/jan/releases/download/${lastRelease.tag_name}/${fileName}`, href: `https://github.com/menloresearch/jan/releases/download/${lastRelease.tag_name}/${fileName}`,
size: asset ? formatFileSize(asset.size) : 'N/A', size: asset ? formatFileSize(asset.size) : 'N/A',
} }
}) })

View File

@ -18,7 +18,7 @@ description: Development setup, workflow, and contribution guidelines for Jan Se
1. **Clone Repository** 1. **Clone Repository**
```bash ```bash
git clone https://github.com/janhq/jan-server git clone https://github.com/menloresearch/jan-server
cd jan-server cd jan-server
``` ```

View File

@ -19,7 +19,7 @@ Jan Server currently supports minikube for local development. Production Kuberne
1. **Clone the repository** 1. **Clone the repository**
```bash ```bash
git clone https://github.com/janhq/jan-server git clone https://github.com/menloresearch/jan-server
cd jan-server cd jan-server
``` ```

View File

@ -24,4 +24,4 @@ Fixes 💫
Update your product or download the latest: https://jan.ai Update your product or download the latest: https://jan.ai
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.5). For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.5).

View File

@ -24,4 +24,4 @@ Jan now supports Mistral's new model Codestral. Thanks [Bartowski](https://huggi
More GGUF models can run in Jan - we rebased to llama.cpp b3012.Big thanks to [ggerganov](https://github.com/ggerganov) More GGUF models can run in Jan - we rebased to llama.cpp b3012.Big thanks to [ggerganov](https://github.com/ggerganov)
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.0). For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.0).

View File

@ -28,4 +28,4 @@ Jan now understands LaTeX, allowing users to process and understand complex math
![Latex](https://catalog.jan.ai/docs/jan_update_latex.gif) ![Latex](https://catalog.jan.ai/docs/jan_update_latex.gif)
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.4.12). For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.4.12).

View File

@ -28,4 +28,4 @@ Users can now connect to OpenAI's new model GPT-4o.
![GPT4o](https://catalog.jan.ai/docs/jan_v0_4_13_openai_gpt4o.gif) ![GPT4o](https://catalog.jan.ai/docs/jan_v0_4_13_openai_gpt4o.gif)
For more details, see the [GitHub release notes.](https://github.com/janhq/jan/releases/tag/v0.4.13) For more details, see the [GitHub release notes.](https://github.com/menloresearch/jan/releases/tag/v0.4.13)

View File

@ -16,4 +16,4 @@ More GGUF models can run in Jan - we rebased to llama.cpp b2961.
Huge shoutouts to [ggerganov](https://github.com/ggerganov) and contributors for llama.cpp, and [Bartowski](https://huggingface.co/bartowski) for GGUF models. Huge shoutouts to [ggerganov](https://github.com/ggerganov) and contributors for llama.cpp, and [Bartowski](https://huggingface.co/bartowski) for GGUF models.
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.4.14). For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.4.14).

View File

@ -26,4 +26,4 @@ We've updated to llama.cpp b3088 for better performance - thanks to [GG](https:/
- Reduced chat font weight (back to normal!) - Reduced chat font weight (back to normal!)
- Restored the maximize button - Restored the maximize button
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.1). For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.1).

View File

@ -32,4 +32,4 @@ We've restored the tooltip hover functionality, which makes it easier to access
The right-click options for thread settings are now fully operational again. You can now manage your threads with this fix. The right-click options for thread settings are now fully operational again. You can now manage your threads with this fix.
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.2). For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.2).

View File

@ -23,4 +23,4 @@ We've been working on stability issues over the last few weeks. Jan is now more
- Fixed the GPU memory utilization bar - Fixed the GPU memory utilization bar
- Some UX and copy improvements - Some UX and copy improvements
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.3). For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.3).

View File

@ -32,4 +32,4 @@ Switching between threads used to reset your instruction settings. Thats fixe
### Minor UI Tweaks & Bug Fixes ### Minor UI Tweaks & Bug Fixes
Weve also resolved issues with the input slider on the right panel and tackled several smaller bugs to keep everything running smoothly. Weve also resolved issues with the input slider on the right panel and tackled several smaller bugs to keep everything running smoothly.
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.4). For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.4).

View File

@ -23,4 +23,4 @@ Fixes 💫
Update your product or download the latest: https://jan.ai Update your product or download the latest: https://jan.ai
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.7). For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.7).

View File

@ -22,4 +22,4 @@ Jan v0.5.9 is here: fixing what needed fixing
Update your product or download the latest: https://jan.ai Update your product or download the latest: https://jan.ai
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.9). For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.9).

View File

@ -22,4 +22,4 @@ and various UI/UX enhancements 💫
Update your product or download the latest: https://jan.ai Update your product or download the latest: https://jan.ai
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.8). For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.8).

View File

@ -19,4 +19,4 @@ Jan v0.5.10 is live: Jan is faster, smoother, and more reliable.
Update your product or download the latest: https://jan.ai Update your product or download the latest: https://jan.ai
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.10). For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.10).

View File

@ -23,4 +23,4 @@ Jan v0.5.11 is here - critical issues fixed, Mac installation updated.
Update your product or download the latest: https://jan.ai Update your product or download the latest: https://jan.ai
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.11). For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.11).

View File

@ -25,4 +25,4 @@ Jan v0.5.11 is here - critical issues fixed, Mac installation updated.
Update your product or download the latest: https://jan.ai Update your product or download the latest: https://jan.ai
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.12). For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.12).

View File

@ -20,4 +20,4 @@ import ChangelogHeader from "@/components/Changelog/ChangelogHeader"
Update your product or download the latest: https://jan.ai Update your product or download the latest: https://jan.ai
For more details, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.5.13). For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.13).

View File

@ -33,4 +33,4 @@ Llama
Update your Jan or [download the latest](https://jan.ai/). 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). For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.14).

View File

@ -25,4 +25,4 @@ import ChangelogHeader from "@/components/Changelog/ChangelogHeader"
Update your Jan or [download the latest](https://jan.ai/). 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.15). For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.15).

View File

@ -26,4 +26,4 @@ import ChangelogHeader from "@/components/Changelog/ChangelogHeader"
Update your Jan or [download the latest](https://jan.ai/). 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.16). For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.16).

View File

@ -20,4 +20,4 @@ import ChangelogHeader from "@/components/Changelog/ChangelogHeader"
Update your Jan or [download the latest](https://jan.ai/). 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.17). For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.5.17).

View File

@ -18,4 +18,4 @@ import ChangelogHeader from "@/components/Changelog/ChangelogHeader"
Update your Jan or [download the latest](https://jan.ai/). 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.6.1). For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.6.1).

View File

@ -18,4 +18,4 @@ import ChangelogHeader from "@/components/Changelog/ChangelogHeader"
Update your Jan or [download the latest](https://jan.ai/). 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.6.3). For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.6.3).

View File

@ -23,4 +23,4 @@ new MCP examples.
Update your Jan or [download the latest](https://jan.ai/). 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.6.5). For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.6.5).

View File

@ -116,4 +116,4 @@ integrations. Stay tuned!
Update your Jan or [download the latest](https://jan.ai/). Update your Jan or [download the latest](https://jan.ai/).
For the complete list of changes, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.6.6). For the complete list of changes, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.6.6).

View File

@ -89,4 +89,4 @@ We're continuing to optimize performance for large models, expand MCP integratio
Update your Jan or [download the latest](https://jan.ai/). Update your Jan or [download the latest](https://jan.ai/).
For the complete list of changes, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.6.7). For the complete list of changes, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.6.7).

View File

@ -74,4 +74,4 @@ v0.6.8 focuses on stability and real workflows: major llama.cpp hardening, two n
Update your Jan or [download the latest](https://jan.ai/). Update your Jan or [download the latest](https://jan.ai/).
For the complete list of changes, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.6.8). For the complete list of changes, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.6.8).

View File

@ -1,25 +0,0 @@
---
title: "Jan v0.7.2: Security Update"
version: 0.7.2
description: "Jan v0.7.2 updates the happy-dom dependency to v20.0.0 to address a recently disclosed sandbox vulnerability."
date: 2025-10-16
---
import ChangelogHeader from "@/components/Changelog/ChangelogHeader"
import { Callout } from 'nextra/components'
<ChangelogHeader title="Jan v0.7.2" date="2025-10-16" />
## Jan v0.7.2: Security Update (happy-dom v20)
This release focuses on **security and stability improvements**.
It updates the `happy-dom` dependency to the latest version to address a recently disclosed vulnerability.
### Security Fix
- Updated `happy-dom` to **^20.0.0**, preventing untrusted JavaScript executed within HAPPY DOM from accessing process-level functions and executing arbitrary code outside the intended sandbox.
---
Update your Jan or [download the latest version](https://jan.ai/).
For the complete list of changes, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.7.2).

View File

@ -41,7 +41,7 @@ Jan is an open-source replacement for ChatGPT:
Jan is a full [product suite](https://en.wikipedia.org/wiki/Software_suite) that offers an alternative to Big AI: Jan is a full [product suite](https://en.wikipedia.org/wiki/Software_suite) that offers an alternative to Big AI:
- [Jan Desktop](/docs/desktop/quickstart): macOS, Windows, and Linux apps with offline mode - [Jan Desktop](/docs/desktop/quickstart): macOS, Windows, and Linux apps with offline mode
- [Jan Web](https://chat.menlo.ai): Jan on browser, a direct alternative to chatgpt.com - [Jan Web](https://chat.jan.ai): Jan on browser, a direct alternative to chatgpt.com
- Jan Mobile: iOS and Android apps (Coming Soon) - Jan Mobile: iOS and Android apps (Coming Soon)
- [Jan Server](/docs/server): deploy locally, in your cloud, or on-prem - [Jan Server](/docs/server): deploy locally, in your cloud, or on-prem
- [Jan Models](/docs/models): Open-source models optimized for deep research, tool use, and reasoning - [Jan Models](/docs/models): Open-source models optimized for deep research, tool use, and reasoning

View File

@ -135,5 +135,5 @@ Min-p: 0.0
## 🤝 Community & Support ## 🤝 Community & Support
- **Discussions**: [HuggingFace Community](https://huggingface.co/Menlo/Jan-nano-128k/discussions) - **Discussions**: [HuggingFace Community](https://huggingface.co/Menlo/Jan-nano-128k/discussions)
- **Issues**: [GitHub Repository](https://github.com/janhq/deep-research/issues) - **Issues**: [GitHub Repository](https://github.com/menloresearch/deep-research/issues)
- **Discord**: Join our research community for tips and best practices - **Discord**: Join our research community for tips and best practices

View File

@ -9,7 +9,7 @@ Jan Server is a comprehensive self-hosted AI server platform that provides OpenA
Jan Server is a Kubernetes-native platform consisting of multiple microservices that work together to provide a complete AI infrastructure solution. It offers: Jan Server is a Kubernetes-native platform consisting of multiple microservices that work together to provide a complete AI infrastructure solution. It offers:
![System Architecture Diagram](https://raw.githubusercontent.com/janhq/jan-server/main/docs/Architect.png) ![System Architecture Diagram](https://raw.githubusercontent.com/menloresearch/jan-server/main/docs/Architect.png)
### Key Features ### Key Features
- **OpenAI-Compatible API**: Full compatibility with OpenAI's chat completion API - **OpenAI-Compatible API**: Full compatibility with OpenAI's chat completion API

View File

@ -3,7 +3,7 @@ title: Development
description: Development setup, workflow, and contribution guidelines for Jan Server. description: Development setup, workflow, and contribution guidelines for Jan Server.
--- ---
## Core Domain Models ## Core Domain Models
![Domain Models Diagram](https://github.com/janhq/jan-server/raw/main/apps/jan-api-gateway/docs/System_Design.png) ![Domain Models Diagram](https://github.com/menloresearch/jan-server/raw/main/apps/jan-api-gateway/docs/System_Design.png)
## Development Setup ## Development Setup
### Prerequisites ### Prerequisites
@ -42,7 +42,7 @@ description: Development setup, workflow, and contribution guidelines for Jan Se
1. **Clone Repository** 1. **Clone Repository**
```bash ```bash
git clone https://github.com/janhq/jan-server git clone https://github.com/menloresearch/jan-server
cd jan-server cd jan-server
``` ```

View File

@ -40,7 +40,7 @@ Jan Server is a Kubernetes-native platform consisting of multiple microservices
- **Monitoring & Profiling**: Built-in performance monitoring and health checks - **Monitoring & Profiling**: Built-in performance monitoring and health checks
## System Architecture ## System Architecture
![System Architecture Diagram](https://raw.githubusercontent.com/janhq/jan-server/main/docs/Architect.png) ![System Architecture Diagram](https://raw.githubusercontent.com/menloresearch/jan-server/main/docs/Architect.png)
## Services ## Services
### Jan API Gateway ### Jan API Gateway

View File

@ -19,7 +19,7 @@ keywords:
import Download from "@/components/Download" import Download from "@/components/Download"
export const getStaticProps = async() => { export const getStaticProps = async() => {
const resRelease = await fetch('https://api.github.com/repos/janhq/jan/releases/latest') const resRelease = await fetch('https://api.github.com/repos/menloresearch/jan/releases/latest')
const release = await resRelease.json() const release = await resRelease.json()
return { return {

View File

@ -19,9 +19,9 @@ keywords:
import Home from "@/components/Home" import Home from "@/components/Home"
export const getStaticProps = async() => { export const getStaticProps = async() => {
const resReleaseLatest = await fetch('https://api.github.com/repos/janhq/jan/releases/latest') const resReleaseLatest = await fetch('https://api.github.com/repos/menloresearch/jan/releases/latest')
const resRelease = await fetch('https://api.github.com/repos/janhq/jan/releases?per_page=500') const resRelease = await fetch('https://api.github.com/repos/menloresearch/jan/releases?per_page=500')
const resRepo = await fetch('https://api.github.com/repos/janhq/jan') const resRepo = await fetch('https://api.github.com/repos/menloresearch/jan')
const repo = await resRepo.json() const repo = await resRepo.json()
const latestRelease = await resReleaseLatest.json() const latestRelease = await resReleaseLatest.json()
const release = await resRelease.json() const release = await resRelease.json()

View File

@ -14,12 +14,12 @@ import CTABlog from '@/components/Blog/CTA'
Jan now supports [NVIDIA TensorRT-LLM](https://github.com/NVIDIA/TensorRT-LLM) in addition to [llama.cpp](https://github.com/ggerganov/llama.cpp), making Jan multi-engine and ultra-fast for users with Nvidia GPUs. Jan now supports [NVIDIA TensorRT-LLM](https://github.com/NVIDIA/TensorRT-LLM) in addition to [llama.cpp](https://github.com/ggerganov/llama.cpp), making Jan multi-engine and ultra-fast for users with Nvidia GPUs.
We've been excited for TensorRT-LLM for a while, and [had a lot of fun implementing it](https://github.com/janhq/nitro-tensorrt-llm). As part of the process, we've run some benchmarks, to see how TensorRT-LLM fares on consumer hardware (e.g. [4090s](https://www.nvidia.com/en-us/geforce/graphics-cards/40-series/), [3090s](https://www.nvidia.com/en-us/geforce/graphics-cards/30-series/)) we commonly see in the [Jan's hardware community](https://discord.com/channels/1107178041848909847/1201834752206974996). We've been excited for TensorRT-LLM for a while, and [had a lot of fun implementing it](https://github.com/menloresearch/nitro-tensorrt-llm). As part of the process, we've run some benchmarks, to see how TensorRT-LLM fares on consumer hardware (e.g. [4090s](https://www.nvidia.com/en-us/geforce/graphics-cards/40-series/), [3090s](https://www.nvidia.com/en-us/geforce/graphics-cards/30-series/)) we commonly see in the [Jan's hardware community](https://discord.com/channels/1107178041848909847/1201834752206974996).
<Callout type="info" > <Callout type="info" >
**Give it a try!** Jan's TensorRT-LLM extension is available in Jan v0.4.9. We precompiled some TensorRT-LLM models for you to try: `Mistral 7b`, `TinyLlama-1.1b`, `TinyJensen-1.1b` 😂 **Give it a try!** Jan's TensorRT-LLM extension is available in Jan v0.4.9. We precompiled some TensorRT-LLM models for you to try: `Mistral 7b`, `TinyLlama-1.1b`, `TinyJensen-1.1b` 😂
Bugs or feedback? Let us know on [GitHub](https://github.com/janhq/jan) or via [Discord](https://discord.com/channels/1107178041848909847/1201832734704795688). Bugs or feedback? Let us know on [GitHub](https://github.com/menloresearch/jan) or via [Discord](https://discord.com/channels/1107178041848909847/1201832734704795688).
</Callout> </Callout>
<Callout type="info" > <Callout type="info" >

View File

@ -70,34 +70,34 @@ brief survey of how other players approach deep research:
| Kimi | Interactive synthesis | 50100 | 3060+ | PDF, Interactive website | Free | | Kimi | Interactive synthesis | 50100 | 3060+ | PDF, Interactive website | Free |
In our testing, we used the following prompt to assess the quality of the generated report by In our testing, we used the following prompt to assess the quality of the generated report by
the providers above. You can refer to the reports generated [here](https://github.com/janhq/prompt-experiments). the providers above. You can refer to the reports generated [here](https://github.com/menloresearch/prompt-experiments).
``` ```
Generate a comprehensive report about the state of AI in the past week. Include all Generate a comprehensive report about the state of AI in the past week. Include all
new model releases and notable architectural improvements from a variety of sources. new model releases and notable architectural improvements from a variety of sources.
``` ```
[Google's generated report](https://github.com/janhq/prompt-experiments/blob/main/Gemini%202.5%20Flash%20Report.pdf) was the most verbose, with a whopping 23 pages that reads [Google's generated report](https://github.com/menloresearch/prompt-experiments/blob/main/Gemini%202.5%20Flash%20Report.pdf) was the most verbose, with a whopping 23 pages that reads
like a professional intelligence briefing. It opens with an executive summary, like a professional intelligence briefing. It opens with an executive summary,
systematically categorizes developments, and provides forward-looking strategic systematically categorizes developments, and provides forward-looking strategic
insights—connecting OpenAI's open-weight release to broader democratization trends insights—connecting OpenAI's open-weight release to broader democratization trends
and linking infrastructure investments to competitive positioning. and linking infrastructure investments to competitive positioning.
[OpenAI](https://github.com/janhq/prompt-experiments/blob/main/OpenAI%20Deep%20Research.pdf) produced the most citation-heavy output with 134 references throughout 10 pages [OpenAI](https://github.com/menloresearch/prompt-experiments/blob/main/OpenAI%20Deep%20Research.pdf) produced the most citation-heavy output with 134 references throughout 10 pages
(albeit most of them being from the same source). (albeit most of them being from the same source).
[Perplexity](https://github.com/janhq/prompt-experiments/blob/main/Perplexity%20Deep%20Research.pdf) delivered the most actionable 6-page report that maximizes information [Perplexity](https://github.com/menloresearch/prompt-experiments/blob/main/Perplexity%20Deep%20Research.pdf) delivered the most actionable 6-page report that maximizes information
density while maintaining scannability. Despite being the shortest, it captures all density while maintaining scannability. Despite being the shortest, it captures all
major developments with sufficient context for decision-making. major developments with sufficient context for decision-making.
[Claude](https://github.com/janhq/prompt-experiments/blob/main/Claude%20Deep%20Research.pdf) produced a comprehensive analysis that interestingly ignored the time constraint, [Claude](https://github.com/menloresearch/prompt-experiments/blob/main/Claude%20Deep%20Research.pdf) produced a comprehensive analysis that interestingly ignored the time constraint,
covering an 8-month period from January-August 2025 instead of the requested week (Jul 31-Aug covering an 8-month period from January-August 2025 instead of the requested week (Jul 31-Aug
7th 2025). Rather than cataloging recent events, Claude traced the evolution of trends over months. 7th 2025). Rather than cataloging recent events, Claude traced the evolution of trends over months.
[Grok](https://github.com/janhq/prompt-experiments/blob/main/Grok%203%20Deep%20Research.pdf) produced a well-structured but relatively shallow 5-page academic-style report that [Grok](https://github.com/menloresearch/prompt-experiments/blob/main/Grok%203%20Deep%20Research.pdf) produced a well-structured but relatively shallow 5-page academic-style report that
read more like an event catalog than strategic analysis. read more like an event catalog than strategic analysis.
[Kimi](https://github.com/janhq/prompt-experiments/blob/main/Kimi%20AI%20Deep%20Research.pdf) produced a comprehensive 13-page report with systematic organization covering industry developments, research breakthroughs, and policy changes, but notably lacks proper citations throughout most of the content despite claiming to use 50-100 sources. [Kimi](https://github.com/menloresearch/prompt-experiments/blob/main/Kimi%20AI%20Deep%20Research.pdf) produced a comprehensive 13-page report with systematic organization covering industry developments, research breakthroughs, and policy changes, but notably lacks proper citations throughout most of the content despite claiming to use 50-100 sources.
### Understanding Search Strategies ### Understanding Search Strategies

View File

@ -13,7 +13,7 @@ import CTABlog from '@/components/Blog/CTA'
## Abstract ## Abstract
We present a straightforward approach to customizing small, open-source models using fine-tuning and RAG that outperforms GPT-3.5 for specialized use cases. With it, we achieved superior Q&A results of [technical documentation](https://nitro.jan.ai/docs) for a small codebase [codebase](https://github.com/janhq/nitro). We present a straightforward approach to customizing small, open-source models using fine-tuning and RAG that outperforms GPT-3.5 for specialized use cases. With it, we achieved superior Q&A results of [technical documentation](https://nitro.jan.ai/docs) for a small codebase [codebase](https://github.com/menloresearch/nitro).
In short, (1) extending a general foundation model like [Mistral](https://huggingface.co/mistralai/Mistral-7B-v0.1) with strong math and coding, and (2) training it over a high-quality, synthetic dataset generated from the intended corpus, and (3) adding RAG capabilities, can lead to significant accuracy improvements. In short, (1) extending a general foundation model like [Mistral](https://huggingface.co/mistralai/Mistral-7B-v0.1) with strong math and coding, and (2) training it over a high-quality, synthetic dataset generated from the intended corpus, and (3) adding RAG capabilities, can lead to significant accuracy improvements.
@ -93,11 +93,11 @@ This final model can be found [here on Huggingface](https://huggingface.co/jan-h
As an additional step, we also added [Retrieval Augmented Generation (RAG)](https://blogs.nvidia.com/blog/what-is-retrieval-augmented-generation/) as an experiment parameter. As an additional step, we also added [Retrieval Augmented Generation (RAG)](https://blogs.nvidia.com/blog/what-is-retrieval-augmented-generation/) as an experiment parameter.
A simple RAG setup was done using **[Llamaindex](https://www.llamaindex.ai/)** and the **[bge-en-base-v1.5 embedding](https://huggingface.co/BAAI/bge-base-en-v1.5)** model for efficient documentation retrieval and question-answering. You can find the RAG implementation [here](https://github.com/janhq/open-foundry/blob/main/rag-is-not-enough/rag/nitro_rag.ipynb). A simple RAG setup was done using **[Llamaindex](https://www.llamaindex.ai/)** and the **[bge-en-base-v1.5 embedding](https://huggingface.co/BAAI/bge-base-en-v1.5)** model for efficient documentation retrieval and question-answering. You can find the RAG implementation [here](https://github.com/menloresearch/open-foundry/blob/main/rag-is-not-enough/rag/nitro_rag.ipynb).
## Benchmarking the Results ## Benchmarking the Results
We curated a new set of [50 multiple-choice questions](https://github.com/janhq/open-foundry/blob/main/rag-is-not-enough/rag/mcq_nitro.csv) (MCQ) based on the Nitro docs. The questions had varying levels of difficulty and had trick components that challenged the model's ability to discern misleading information. We curated a new set of [50 multiple-choice questions](https://github.com/menloresearch/open-foundry/blob/main/rag-is-not-enough/rag/mcq_nitro.csv) (MCQ) based on the Nitro docs. The questions had varying levels of difficulty and had trick components that challenged the model's ability to discern misleading information.
![image](https://hackmd.io/_uploads/By9vaE1Ta.png) ![image](https://hackmd.io/_uploads/By9vaE1Ta.png)
@ -121,7 +121,7 @@ We conclude that this combination of model merging + finetuning + RAG yields pro
Anecdotally, weve had some success using this model in practice to onboard new team members to the Nitro codebase. Anecdotally, weve had some success using this model in practice to onboard new team members to the Nitro codebase.
A full research report with more statistics can be found [here](https://github.com/janhq/open-foundry/blob/main/rag-is-not-enough/README.md). A full research report with more statistics can be found [here](https://github.com/menloresearch/open-foundry/blob/main/rag-is-not-enough/README.md).
# References # References

View File

@ -203,7 +203,7 @@ When to choose ChatGPT Plus instead:
Ready to try gpt-oss? Ready to try gpt-oss?
- Download Jan: [https://jan.ai/](https://jan.ai/) - Download Jan: [https://jan.ai/](https://jan.ai/)
- View source code: [https://github.com/janhq/jan](https://github.com/janhq/jan) - View source code: [https://github.com/menloresearch/jan](https://github.com/menloresearch/jan)
- Need help? Check our [local AI guide](/post/run-ai-models-locally) for beginners - Need help? Check our [local AI guide](/post/run-ai-models-locally) for beginners
<CTABlog /> <CTABlog />

View File

@ -4,7 +4,7 @@ title: Support - Jan
# Support # Support
- Bugs & requests: file a GitHub ticket [here](https://github.com/janhq/jan/issues) - Bugs & requests: file a GitHub ticket [here](https://github.com/menloresearch/jan/issues)
- For discussion: join our Discord [here](https://discord.gg/FTk2MvZwJH) - For discussion: join our Discord [here](https://discord.gg/FTk2MvZwJH)
- For business inquiries: email hello@jan.ai - For business inquiries: email hello@jan.ai
- For jobs: please email hr@jan.ai - For jobs: please email hr@jan.ai

View File

@ -31,7 +31,7 @@ const config: DocsThemeConfig = {
</div> </div>
</span> </span>
), ),
docsRepositoryBase: 'https://github.com/janhq/jan/tree/dev/docs', docsRepositoryBase: 'https://github.com/menloresearch/jan/tree/dev/docs',
feedback: { feedback: {
content: 'Question? Give us feedback →', content: 'Question? Give us feedback →',
labels: 'feedback', labels: 'feedback',

View File

@ -16,7 +16,7 @@ import {
ListConversationItemsResponse ListConversationItemsResponse
} from './types' } from './types'
declare const MENLO_PLATFORM_BASE_URL: string declare const JAN_API_BASE: string
export class RemoteApi { export class RemoteApi {
private authService: JanAuthService private authService: JanAuthService
@ -28,7 +28,7 @@ export class RemoteApi {
async createConversation( async createConversation(
data: Conversation data: Conversation
): Promise<ConversationResponse> { ): Promise<ConversationResponse> {
const url = `${MENLO_PLATFORM_BASE_URL}${CONVERSATION_API_ROUTES.CONVERSATIONS}` const url = `${JAN_API_BASE}${CONVERSATION_API_ROUTES.CONVERSATIONS}`
return this.authService.makeAuthenticatedRequest<ConversationResponse>( return this.authService.makeAuthenticatedRequest<ConversationResponse>(
url, url,
@ -43,12 +43,12 @@ export class RemoteApi {
conversationId: string, conversationId: string,
data: Conversation data: Conversation
): Promise<ConversationResponse> { ): Promise<ConversationResponse> {
const url = `${MENLO_PLATFORM_BASE_URL}${CONVERSATION_API_ROUTES.CONVERSATION_BY_ID(conversationId)}` const url = `${JAN_API_BASE}${CONVERSATION_API_ROUTES.CONVERSATION_BY_ID(conversationId)}`
return this.authService.makeAuthenticatedRequest<ConversationResponse>( return this.authService.makeAuthenticatedRequest<ConversationResponse>(
url, url,
{ {
method: 'POST', method: 'PATCH',
body: JSON.stringify(data), body: JSON.stringify(data),
} }
) )
@ -70,7 +70,7 @@ export class RemoteApi {
} }
const queryString = queryParams.toString() const queryString = queryParams.toString()
const url = `${MENLO_PLATFORM_BASE_URL}${CONVERSATION_API_ROUTES.CONVERSATIONS}${queryString ? `?${queryString}` : ''}` const url = `${JAN_API_BASE}${CONVERSATION_API_ROUTES.CONVERSATIONS}${queryString ? `?${queryString}` : ''}`
return this.authService.makeAuthenticatedRequest<ListConversationsResponse>( return this.authService.makeAuthenticatedRequest<ListConversationsResponse>(
url, url,
@ -114,7 +114,7 @@ export class RemoteApi {
} }
async deleteConversation(conversationId: string): Promise<void> { async deleteConversation(conversationId: string): Promise<void> {
const url = `${MENLO_PLATFORM_BASE_URL}${CONVERSATION_API_ROUTES.CONVERSATION_BY_ID(conversationId)}` const url = `${JAN_API_BASE}${CONVERSATION_API_ROUTES.CONVERSATION_BY_ID(conversationId)}`
await this.authService.makeAuthenticatedRequest( await this.authService.makeAuthenticatedRequest(
url, url,
@ -141,7 +141,7 @@ export class RemoteApi {
} }
const queryString = queryParams.toString() const queryString = queryParams.toString()
const url = `${MENLO_PLATFORM_BASE_URL}${CONVERSATION_API_ROUTES.CONVERSATION_ITEMS(conversationId)}${queryString ? `?${queryString}` : ''}` const url = `${JAN_API_BASE}${CONVERSATION_API_ROUTES.CONVERSATION_ITEMS(conversationId)}${queryString ? `?${queryString}` : ''}`
return this.authService.makeAuthenticatedRequest<ListConversationItemsResponse>( return this.authService.makeAuthenticatedRequest<ListConversationItemsResponse>(
url, url,

View File

@ -31,7 +31,7 @@ export interface ConversationResponse {
id: string id: string
object: 'conversation' object: 'conversation'
title?: string title?: string
created_at: number | string created_at: number
metadata: ConversationMetadata metadata: ConversationMetadata
} }
@ -50,7 +50,6 @@ export interface ConversationItemAnnotation {
} }
export interface ConversationItemContent { export interface ConversationItemContent {
type?: string
file?: { file?: {
file_id?: string file_id?: string
mime_type?: string mime_type?: string
@ -63,50 +62,23 @@ export interface ConversationItemContent {
file_id?: string file_id?: string
url?: string url?: string
} }
image_file?: {
file_id?: string
mime_type?: string
}
input_text?: string input_text?: string
output_text?: { output_text?: {
annotations?: ConversationItemAnnotation[] annotations?: ConversationItemAnnotation[]
text?: string text?: string
} }
reasoning_content?: string
text?: { text?: {
value?: string value?: string
text?: string
} }
reasoning_content?: string
tool_calls?: Array<{
id?: string
type?: string type?: string
function?: {
name?: string
arguments?: string
}
}>
tool_call_id?: string
tool_result?: {
content?: Array<{
type?: string
text?: string
output_text?: {
text?: string
}
}>
output_text?: {
text?: string
}
}
text_result?: string
} }
export interface ConversationItem { export interface ConversationItem {
content?: ConversationItemContent[] content?: ConversationItemContent[]
created_at: number | string created_at: number
id: string id: string
object: string object: string
metadata?: Record<string, unknown>
role: string role: string
status?: string status?: string
type?: string type?: string

View File

@ -1,5 +1,5 @@
import { Thread, ThreadAssistantInfo, ThreadMessage, ContentType } from '@janhq/core' import { Thread, ThreadAssistantInfo, ThreadMessage, ContentType } from '@janhq/core'
import { Conversation, ConversationResponse, ConversationItem, ConversationItemContent, ConversationMetadata } from './types' import { Conversation, ConversationResponse, ConversationItem } from './types'
import { DEFAULT_ASSISTANT } from './const' import { DEFAULT_ASSISTANT } from './const'
export class ObjectParser { export class ObjectParser {
@ -7,7 +7,7 @@ export class ObjectParser {
const modelName = thread.assistants?.[0]?.model?.id || undefined const modelName = thread.assistants?.[0]?.model?.id || undefined
const modelProvider = thread.assistants?.[0]?.model?.engine || undefined const modelProvider = thread.assistants?.[0]?.model?.engine || undefined
const isFavorite = thread.metadata?.is_favorite?.toString() || 'false' const isFavorite = thread.metadata?.is_favorite?.toString() || 'false'
let metadata: ConversationMetadata = {} let metadata = {}
if (modelName && modelProvider) { if (modelName && modelProvider) {
metadata = { metadata = {
model_id: modelName, model_id: modelName,
@ -23,14 +23,15 @@ export class ObjectParser {
static conversationToThread(conversation: ConversationResponse): Thread { static conversationToThread(conversation: ConversationResponse): Thread {
const assistants: ThreadAssistantInfo[] = [] const assistants: ThreadAssistantInfo[] = []
const metadata: ConversationMetadata = conversation.metadata || {} if (
conversation.metadata?.model_id &&
if (metadata.model_id && metadata.model_provider) { conversation.metadata?.model_provider
) {
assistants.push({ assistants.push({
...DEFAULT_ASSISTANT, ...DEFAULT_ASSISTANT,
model: { model: {
id: metadata.model_id, id: conversation.metadata.model_id,
engine: metadata.model_provider, engine: conversation.metadata.model_provider,
}, },
}) })
} else { } else {
@ -43,18 +44,16 @@ export class ObjectParser {
}) })
} }
const isFavorite = metadata.is_favorite === 'true' const isFavorite = conversation.metadata?.is_favorite === 'true'
const createdAtMs = parseTimestamp(conversation.created_at)
return { return {
id: conversation.id, id: conversation.id,
title: conversation.title || '', title: conversation.title || '',
assistants, assistants,
created: createdAtMs, created: conversation.created_at,
updated: createdAtMs, updated: conversation.created_at,
model: { model: {
id: metadata.model_id, id: conversation.metadata.model_id,
provider: metadata.model_provider, provider: conversation.metadata.model_provider,
}, },
isFavorite, isFavorite,
metadata: { is_favorite: isFavorite }, metadata: { is_favorite: isFavorite },
@ -66,70 +65,74 @@ export class ObjectParser {
threadId: string threadId: string
): ThreadMessage { ): ThreadMessage {
// Extract text content and metadata from the item // Extract text content and metadata from the item
const textSegments: string[] = [] let textContent = ''
const reasoningSegments: string[] = [] let reasoningContent = ''
const imageUrls: string[] = [] const imageUrls: string[] = []
let toolCalls: any[] = [] let toolCalls: any[] = []
let finishReason = ''
if (item.content && item.content.length > 0) { if (item.content && item.content.length > 0) {
for (const content of item.content) { for (const content of item.content) {
extractContentByType(content, { // Handle text content
onText: (value) => { if (content.text?.value) {
if (value) { textContent = content.text.value
textSegments.push(value)
} }
}, // Handle output_text for assistant messages
onReasoning: (value) => { if (content.output_text?.text) {
if (value) { textContent = content.output_text.text
reasoningSegments.push(value)
} }
}, // Handle reasoning content
onImage: (url) => { if (content.reasoning_content) {
if (url) { reasoningContent = content.reasoning_content
imageUrls.push(url)
} }
}, // Handle image content
onToolCalls: (calls) => { if (content.image?.url) {
toolCalls = calls.map((toolCall) => { imageUrls.push(content.image.url)
const callId = toolCall.id || 'unknown' }
const rawArgs = toolCall.function?.arguments // Extract finish_reason
const normalizedArgs = if (content.finish_reason) {
typeof rawArgs === 'string' finishReason = content.finish_reason
? rawArgs }
: JSON.stringify(rawArgs ?? {}) }
return { }
id: callId,
tool_call_id: callId, // Handle tool calls parsing for assistant messages
if (item.role === 'assistant' && finishReason === 'tool_calls') {
try {
// Tool calls are embedded as JSON string in textContent
const toolCallMatch = textContent.match(/\[.*\]/)
if (toolCallMatch) {
const toolCallsData = JSON.parse(toolCallMatch[0])
toolCalls = toolCallsData.map((toolCall: any) => ({
tool: { tool: {
id: callId, id: toolCall.id || 'unknown',
function: { function: {
name: toolCall.function?.name || 'unknown', name: toolCall.function?.name || 'unknown',
arguments: normalizedArgs, arguments: toolCall.function?.arguments || '{}'
}, },
type: toolCall.type || 'function', type: toolCall.type || 'function'
}, },
response: { response: {
error: '', error: '',
content: [], content: []
}, },
state: 'pending', state: 'ready'
}))
// Remove tool calls JSON from text content, keep only reasoning
textContent = ''
} }
}) } catch (error) {
}, console.error('Failed to parse tool calls:', error)
})
} }
} }
// Format final content with reasoning if present // Format final content with reasoning if present
let finalTextValue = '' let finalTextValue = ''
if (reasoningSegments.length > 0) { if (reasoningContent) {
finalTextValue += `<think>${reasoningSegments.join('\n')}</think>` finalTextValue = `<think>${reasoningContent}</think>`
} }
if (textSegments.length > 0) { if (textContent) {
if (finalTextValue) { finalTextValue += textContent
finalTextValue += '\n'
}
finalTextValue += textSegments.join('\n')
} }
// Build content array for ThreadMessage // Build content array for ThreadMessage
@ -154,26 +157,22 @@ export class ObjectParser {
} }
// Build metadata // Build metadata
const metadata: any = { ...(item.metadata || {}) } const metadata: any = {}
if (toolCalls.length > 0) { if (toolCalls.length > 0) {
metadata.tool_calls = toolCalls metadata.tool_calls = toolCalls
} }
const createdAtMs = parseTimestamp(item.created_at)
// Map status from server format to frontend format // Map status from server format to frontend format
const mappedStatus = item.status === 'completed' ? 'ready' : item.status || 'ready' const mappedStatus = item.status === 'completed' ? 'ready' : item.status || 'ready'
const role = item.role === 'user' || item.role === 'assistant' ? item.role : 'assistant'
return { return {
type: 'text', type: 'text',
id: item.id, id: item.id,
object: 'thread.message', object: 'thread.message',
thread_id: threadId, thread_id: threadId,
role, role: item.role as 'user' | 'assistant',
content: messageContent, content: messageContent,
created_at: createdAtMs, created_at: item.created_at * 1000, // Convert to milliseconds
completed_at: 0, completed_at: 0,
status: mappedStatus, status: mappedStatus,
metadata, metadata,
@ -202,46 +201,25 @@ export const combineConversationItemsToMessages = (
): ThreadMessage[] => { ): ThreadMessage[] => {
const messages: ThreadMessage[] = [] const messages: ThreadMessage[] = []
const toolResponseMap = new Map<string, any>() const toolResponseMap = new Map<string, any>()
const sortedItems = [...items].sort(
(a, b) => parseTimestamp(a.created_at) - parseTimestamp(b.created_at)
)
// First pass: collect tool responses // First pass: collect tool responses
for (const item of sortedItems) { for (const item of items) {
if (item.role === 'tool') { if (item.role === 'tool') {
for (const content of item.content ?? []) { const toolContent = item.content?.[0]?.text?.value || ''
const toolCallId = content.tool_call_id || item.id toolResponseMap.set(item.id, {
const toolResultText =
content.tool_result?.output_text?.text ||
(Array.isArray(content.tool_result?.content)
? content.tool_result?.content
?.map((entry) => entry.text || entry.output_text?.text)
.filter((text): text is string => Boolean(text))
.join('\n')
: undefined)
const toolContent =
content.text?.text ||
content.text?.value ||
content.output_text?.text ||
content.input_text ||
content.text_result ||
toolResultText ||
''
toolResponseMap.set(toolCallId, {
error: '', error: '',
content: [ content: [
{ {
type: 'text', type: 'text',
text: toolContent, text: toolContent
},
],
})
} }
]
})
} }
} }
// Second pass: build messages and merge tool responses // Second pass: build messages and merge tool responses
for (const item of sortedItems) { for (const item of items) {
// Skip tool messages as they will be merged into assistant messages // Skip tool messages as they will be merged into assistant messages
if (item.role === 'tool') { if (item.role === 'tool') {
continue continue
@ -250,35 +228,14 @@ export const combineConversationItemsToMessages = (
const message = ObjectParser.conversationItemToThreadMessage(item, threadId) const message = ObjectParser.conversationItemToThreadMessage(item, threadId)
// If this is an assistant message with tool calls, merge tool responses // If this is an assistant message with tool calls, merge tool responses
if ( if (message.role === 'assistant' && message.metadata?.tool_calls && Array.isArray(message.metadata.tool_calls)) {
message.role === 'assistant' &&
message.metadata?.tool_calls &&
Array.isArray(message.metadata.tool_calls)
) {
const toolCalls = message.metadata.tool_calls as any[] const toolCalls = message.metadata.tool_calls as any[]
let toolResponseIndex = 0
for (const toolCall of toolCalls) { for (const [responseId, responseData] of toolResponseMap.entries()) {
const callId = toolCall.tool_call_id || toolCall.id || toolCall.tool?.id if (toolResponseIndex < toolCalls.length) {
let responseKey: string | undefined toolCalls[toolResponseIndex].response = responseData
let response: any = null toolResponseIndex++
if (callId && toolResponseMap.has(callId)) {
responseKey = callId
response = toolResponseMap.get(callId)
} else {
const iterator = toolResponseMap.entries().next()
if (!iterator.done) {
responseKey = iterator.value[0]
response = iterator.value[1]
}
}
if (response) {
toolCall.response = response
toolCall.state = 'succeeded'
if (responseKey) {
toolResponseMap.delete(responseKey)
}
} }
} }
} }
@ -288,79 +245,3 @@ export const combineConversationItemsToMessages = (
return messages return messages
} }
const parseTimestamp = (value: number | string | undefined): number => {
if (typeof value === 'number') {
// Distinguish between seconds and milliseconds
return value > 1e12 ? value : value * 1000
}
if (typeof value === 'string') {
const parsed = Date.parse(value)
return Number.isNaN(parsed) ? Date.now() : parsed
}
return Date.now()
}
const extractContentByType = (
content: ConversationItemContent,
handlers: {
onText: (value: string) => void
onReasoning: (value: string) => void
onImage: (url: string) => void
onToolCalls: (calls: NonNullable<ConversationItemContent['tool_calls']>) => void
}
) => {
const type = content.type || ''
switch (type) {
case 'input_text':
handlers.onText(content.input_text || '')
break
case 'text':
handlers.onText(content.text?.text || content.text?.value || '')
break
case 'output_text':
handlers.onText(content.output_text?.text || '')
break
case 'reasoning_content':
handlers.onReasoning(content.reasoning_content || '')
break
case 'image':
case 'image_url':
if (content.image?.url) {
handlers.onImage(content.image.url)
}
break
case 'tool_calls':
if (content.tool_calls && Array.isArray(content.tool_calls)) {
handlers.onToolCalls(content.tool_calls)
}
break
case 'tool_result':
if (content.tool_result?.output_text?.text) {
handlers.onText(content.tool_result.output_text.text)
}
break
default:
// Fallback for legacy fields without explicit type
if (content.text?.value || content.text?.text) {
handlers.onText(content.text.value || content.text.text || '')
}
if (content.text_result) {
handlers.onText(content.text_result)
}
if (content.output_text?.text) {
handlers.onText(content.output_text.text)
}
if (content.reasoning_content) {
handlers.onReasoning(content.reasoning_content)
}
if (content.image?.url) {
handlers.onImage(content.image.url)
}
if (content.tool_calls && Array.isArray(content.tool_calls)) {
handlers.onToolCalls(content.tool_calls)
}
break
}
}

View File

@ -4,11 +4,10 @@
*/ */
import { getSharedAuthService, JanAuthService } from '../shared' import { getSharedAuthService, JanAuthService } from '../shared'
import { ApiError } from '../shared/types/errors'
import { JAN_API_ROUTES } from './const'
import { JanModel, janProviderStore } from './store' import { JanModel, janProviderStore } from './store'
import { ApiError } from '../shared/types/errors'
// MENLO_PLATFORM_BASE_URL is defined in vite.config.ts // JAN_API_BASE is defined in vite.config.ts
// Constants // Constants
const TEMPORARY_CHAT_ID = 'temporary-chat' const TEMPORARY_CHAT_ID = 'temporary-chat'
@ -20,7 +19,12 @@ const TEMPORARY_CHAT_ID = 'temporary-chat'
*/ */
function getChatCompletionConfig(request: JanChatCompletionRequest, stream: boolean = false) { function getChatCompletionConfig(request: JanChatCompletionRequest, stream: boolean = false) {
const isTemporaryChat = request.conversation_id === TEMPORARY_CHAT_ID const isTemporaryChat = request.conversation_id === TEMPORARY_CHAT_ID
const endpoint = `${MENLO_PLATFORM_BASE_URL}${JAN_API_ROUTES.CHAT_COMPLETIONS}`
// For temporary chats, use the stateless /chat/completions endpoint
// For regular conversations, use the stateful /conv/chat/completions endpoint
const endpoint = isTemporaryChat
? `${JAN_API_BASE}/chat/completions`
: `${JAN_API_BASE}/conv/chat/completions`
const payload = { const payload = {
...request, ...request,
@ -40,30 +44,9 @@ function getChatCompletionConfig(request: JanChatCompletionRequest, stream: bool
return { endpoint, payload, isTemporaryChat } return { endpoint, payload, isTemporaryChat }
} }
interface JanModelSummary { export interface JanModelsResponse {
id: string
object: string object: string
owned_by: string data: JanModel[]
created?: number
}
interface JanModelsResponse {
object: string
data: JanModelSummary[]
}
interface JanModelCatalogResponse {
id: string
supported_parameters?: {
names?: string[]
default?: Record<string, unknown>
}
extras?: {
supported_parameters?: string[]
default_parameters?: Record<string, unknown>
[key: string]: unknown
}
[key: string]: unknown
} }
export interface JanChatMessage { export interface JanChatMessage {
@ -129,8 +112,6 @@ export interface JanChatCompletionChunk {
export class JanApiClient { export class JanApiClient {
private static instance: JanApiClient private static instance: JanApiClient
private authService: JanAuthService private authService: JanAuthService
private modelsCache: JanModel[] | null = null
private modelsFetchPromise: Promise<JanModel[]> | null = null
private constructor() { private constructor() {
this.authService = getSharedAuthService() this.authService = getSharedAuthService()
@ -143,64 +124,25 @@ export class JanApiClient {
return JanApiClient.instance return JanApiClient.instance
} }
async getModels(options?: { forceRefresh?: boolean }): Promise<JanModel[]> { async getModels(): Promise<JanModel[]> {
try { try {
const forceRefresh = options?.forceRefresh ?? false
if (forceRefresh) {
this.modelsCache = null
} else if (this.modelsCache) {
return this.modelsCache
}
if (this.modelsFetchPromise) {
return this.modelsFetchPromise
}
janProviderStore.setLoadingModels(true) janProviderStore.setLoadingModels(true)
janProviderStore.clearError() janProviderStore.clearError()
this.modelsFetchPromise = (async () => {
const response = await this.authService.makeAuthenticatedRequest<JanModelsResponse>( const response = await this.authService.makeAuthenticatedRequest<JanModelsResponse>(
`${MENLO_PLATFORM_BASE_URL}${JAN_API_ROUTES.MODELS}` `${JAN_API_BASE}/conv/models`
) )
const summaries = response.data || [] const models = response.data || []
const models: JanModel[] = await Promise.all(
summaries.map(async (summary) => {
const supportedParameters = await this.fetchSupportedParameters(summary.id)
const capabilities = this.deriveCapabilitiesFromParameters(supportedParameters)
return {
id: summary.id,
object: summary.object,
owned_by: summary.owned_by,
created: summary.created,
capabilities,
supportedParameters,
}
})
)
this.modelsCache = models
janProviderStore.setModels(models) janProviderStore.setModels(models)
return models return models
})()
return await this.modelsFetchPromise
} catch (error) { } catch (error) {
this.modelsCache = null
this.modelsFetchPromise = null
const errorMessage = error instanceof ApiError ? error.message : const errorMessage = error instanceof ApiError ? error.message :
error instanceof Error ? error.message : 'Failed to fetch models' error instanceof Error ? error.message : 'Failed to fetch models'
janProviderStore.setError(errorMessage) janProviderStore.setError(errorMessage)
janProviderStore.setLoadingModels(false) janProviderStore.setLoadingModels(false)
throw error throw error
} finally {
this.modelsFetchPromise = null
} }
} }
@ -312,7 +254,7 @@ export class JanApiClient {
async initialize(): Promise<void> { async initialize(): Promise<void> {
try { try {
janProviderStore.setAuthenticated(true) janProviderStore.setAuthenticated(true)
// Fetch initial models (cached for subsequent calls) // Fetch initial models
await this.getModels() await this.getModels()
console.log('Jan API client initialized successfully') console.log('Jan API client initialized successfully')
} catch (error) { } catch (error) {
@ -324,52 +266,6 @@ export class JanApiClient {
janProviderStore.setInitializing(false) janProviderStore.setInitializing(false)
} }
} }
private async fetchSupportedParameters(modelId: string): Promise<string[]> {
try {
const endpoint = `${MENLO_PLATFORM_BASE_URL}${JAN_API_ROUTES.MODEL_CATALOGS}/${this.encodeModelIdForCatalog(modelId)}`
const catalog = await this.authService.makeAuthenticatedRequest<JanModelCatalogResponse>(endpoint)
return this.extractSupportedParameters(catalog)
} catch (error) {
console.warn(`Failed to fetch catalog metadata for model "${modelId}":`, error)
return []
}
}
private encodeModelIdForCatalog(modelId: string): string {
return modelId
.split('/')
.map((segment) => encodeURIComponent(segment))
.join('/')
}
private extractSupportedParameters(catalog: JanModelCatalogResponse | null | undefined): string[] {
if (!catalog) {
return []
}
const primaryNames = catalog.supported_parameters?.names
if (Array.isArray(primaryNames) && primaryNames.length > 0) {
return [...new Set(primaryNames)]
}
const extraNames = catalog.extras?.supported_parameters
if (Array.isArray(extraNames) && extraNames.length > 0) {
return [...new Set(extraNames)]
}
return []
}
private deriveCapabilitiesFromParameters(parameters: string[]): string[] {
const capabilities = new Set<string>()
if (parameters.includes('tools')) {
capabilities.add('tools')
}
return Array.from(capabilities)
}
} }
export const janApiClient = JanApiClient.getInstance() export const janApiClient = JanApiClient.getInstance()

View File

@ -1,7 +0,0 @@
export const JAN_API_ROUTES = {
MODELS: '/models',
CHAT_COMPLETIONS: '/chat/completions',
MODEL_CATALOGS: '/models/catalogs',
} as const
export const MODEL_PROVIDER_STORAGE_KEY = 'model-provider'

View File

@ -1,122 +0,0 @@
import type { JanModel } from './store'
import { MODEL_PROVIDER_STORAGE_KEY } from './const'
type StoredModel = {
id?: string
capabilities?: unknown
[key: string]: unknown
}
type StoredProvider = {
provider?: string
models?: StoredModel[]
[key: string]: unknown
}
type StoredState = {
state?: {
providers?: StoredProvider[]
[key: string]: unknown
}
version?: number
[key: string]: unknown
}
const normalizeCapabilities = (capabilities: unknown): string[] => {
if (!Array.isArray(capabilities)) {
return []
}
return [...new Set(capabilities.filter((item): item is string => typeof item === 'string'))].sort(
(a, b) => a.localeCompare(b)
)
}
/**
* Synchronize Jan models stored in localStorage with the latest server state.
* Returns true if the stored data was modified (including being cleared).
*/
export function syncJanModelsLocalStorage(
remoteModels: JanModel[],
storageKey: string = MODEL_PROVIDER_STORAGE_KEY
): boolean {
const rawStorage = localStorage.getItem(storageKey)
if (!rawStorage) {
return false
}
let storedState: StoredState
try {
storedState = JSON.parse(rawStorage) as StoredState
} catch (error) {
console.warn('Failed to parse Jan model storage; clearing entry.', error)
localStorage.removeItem(storageKey)
return true
}
const providers = storedState?.state?.providers
if (!Array.isArray(providers)) {
return false
}
const remoteModelMap = new Map(remoteModels.map((model) => [model.id, model]))
let storageUpdated = false
for (const provider of providers) {
if (provider.provider !== 'jan' || !Array.isArray(provider.models)) {
continue
}
const updatedModels: StoredModel[] = []
for (const model of provider.models) {
const modelId = typeof model.id === 'string' ? model.id : null
if (!modelId) {
storageUpdated = true
continue
}
const remoteModel = remoteModelMap.get(modelId)
if (!remoteModel) {
console.log(`Removing unknown Jan model from localStorage: ${modelId}`)
storageUpdated = true
continue
}
const storedCapabilities = normalizeCapabilities(model.capabilities)
const remoteCapabilities = normalizeCapabilities(remoteModel.capabilities)
const capabilitiesMatch =
storedCapabilities.length === remoteCapabilities.length &&
storedCapabilities.every((cap, index) => cap === remoteCapabilities[index])
if (!capabilitiesMatch) {
console.log(
`Updating capabilities for Jan model ${modelId}:`,
storedCapabilities,
'=>',
remoteCapabilities
)
updatedModels.push({
...model,
capabilities: remoteModel.capabilities,
})
storageUpdated = true
} else {
updatedModels.push(model)
}
}
if (updatedModels.length !== provider.models.length) {
storageUpdated = true
}
provider.models = updatedModels
}
if (storageUpdated) {
localStorage.setItem(storageKey, JSON.stringify(storedState))
}
return storageUpdated
}

View File

@ -14,10 +14,12 @@ import {
ImportOptions, ImportOptions,
} from '@janhq/core' // cspell: disable-line } from '@janhq/core' // cspell: disable-line
import { janApiClient, JanChatMessage } from './api' import { janApiClient, JanChatMessage } from './api'
import { syncJanModelsLocalStorage } from './helpers'
import { janProviderStore } from './store' import { janProviderStore } from './store'
import { ApiError } from '../shared/types/errors' import { ApiError } from '../shared/types/errors'
// Jan models support tools via MCP
const JAN_MODEL_CAPABILITIES = ['tools'] as const
export default class JanProviderWeb extends AIEngine { export default class JanProviderWeb extends AIEngine {
readonly provider = 'jan' readonly provider = 'jan'
private activeSessions: Map<string, SessionInfo> = new Map() private activeSessions: Map<string, SessionInfo> = new Map()
@ -26,11 +28,11 @@ export default class JanProviderWeb extends AIEngine {
console.log('Loading Jan Provider Extension...') console.log('Loading Jan Provider Extension...')
try { try {
// Initialize authentication // Check and clear invalid Jan models (capabilities mismatch)
await janApiClient.initialize() this.validateJanModelsLocalStorage()
// Check and sync stored Jan models against latest catalog data
await this.validateJanModelsLocalStorage()
// Initialize authentication and fetch models
await janApiClient.initialize()
console.log('Jan Provider Extension loaded successfully') console.log('Jan Provider Extension loaded successfully')
} catch (error) { } catch (error) {
console.error('Failed to load Jan Provider Extension:', error) console.error('Failed to load Jan Provider Extension:', error)
@ -41,17 +43,46 @@ export default class JanProviderWeb extends AIEngine {
} }
// Verify Jan models capabilities in localStorage // Verify Jan models capabilities in localStorage
private async validateJanModelsLocalStorage(): Promise<void> { private validateJanModelsLocalStorage() {
try { try {
console.log('Validating Jan models in localStorage...') console.log("Validating Jan models in localStorage...")
const storageKey = 'model-provider'
const data = localStorage.getItem(storageKey)
if (!data) return
const remoteModels = await janApiClient.getModels() const parsed = JSON.parse(data)
const storageUpdated = syncJanModelsLocalStorage(remoteModels) if (!parsed?.state?.providers) return
if (storageUpdated) { // Check if any Jan model has incorrect capabilities
console.log( let hasInvalidModel = false
'Synchronized Jan models in localStorage with server capabilities; reloading...'
) for (const provider of parsed.state.providers) {
if (provider.provider === 'jan' && provider.models) {
for (const model of provider.models) {
console.log(`Checking Jan model: ${model.id}`, model.capabilities)
if (JSON.stringify(model.capabilities) !== JSON.stringify(JAN_MODEL_CAPABILITIES)) {
hasInvalidModel = true
console.log(`Found invalid Jan model: ${model.id}, clearing localStorage`)
break
}
}
}
if (hasInvalidModel) break
}
// If any invalid model found, just clear the storage
if (hasInvalidModel) {
// Force clear the storage
localStorage.removeItem(storageKey)
// Verify it's actually removed
const afterRemoval = localStorage.getItem(storageKey)
// If still present, try setting to empty state
if (afterRemoval) {
// Try alternative clearing method
localStorage.setItem(storageKey, JSON.stringify({ state: { providers: [] }, version: parsed.version || 3 }))
}
console.log('Cleared model-provider from localStorage due to invalid Jan capabilities')
// Force a page reload to ensure clean state
window.location.reload() window.location.reload()
} }
} catch (error) { } catch (error) {
@ -88,7 +119,7 @@ export default class JanProviderWeb extends AIEngine {
path: undefined, // Remote model, no local path path: undefined, // Remote model, no local path
owned_by: model.owned_by, owned_by: model.owned_by,
object: model.object, object: model.object,
capabilities: [...model.capabilities], capabilities: [...JAN_MODEL_CAPABILITIES],
} }
: undefined : undefined
) )
@ -109,7 +140,7 @@ export default class JanProviderWeb extends AIEngine {
path: undefined, // Remote model, no local path path: undefined, // Remote model, no local path
owned_by: model.owned_by, owned_by: model.owned_by,
object: model.object, object: model.object,
capabilities: [...model.capabilities], capabilities: [...JAN_MODEL_CAPABILITIES],
})) }))
} catch (error) { } catch (error) {
console.error('Failed to list Jan models:', error) console.error('Failed to list Jan models:', error)
@ -128,7 +159,6 @@ export default class JanProviderWeb extends AIEngine {
port: 443, // HTTPS port port: 443, // HTTPS port
model_id: modelId, model_id: modelId,
model_path: `remote:${modelId}`, // Indicate this is a remote model model_path: `remote:${modelId}`, // Indicate this is a remote model
is_embedding: false, // assume false here, TODO: might need further implementation
api_key: '', // API key handled by auth service api_key: '', // API key handled by auth service
} }
@ -163,12 +193,8 @@ export default class JanProviderWeb extends AIEngine {
console.error(`Failed to unload Jan session ${sessionId}:`, error) console.error(`Failed to unload Jan session ${sessionId}:`, error)
return { return {
success: false, success: false,
error: error: error instanceof ApiError ? error.message :
error instanceof ApiError error instanceof Error ? error.message : 'Unknown error',
? error.message
: error instanceof Error
? error.message
: 'Unknown error',
} }
} }
} }

View File

@ -9,9 +9,6 @@ export interface JanModel {
id: string id: string
object: string object: string
owned_by: string owned_by: string
created?: number
capabilities: string[]
supportedParameters?: string[]
} }
export interface JanProviderState { export interface JanProviderState {

View File

@ -12,8 +12,8 @@ import { JanMCPOAuthProvider } from './oauth-provider'
import { WebSearchButton } from './components' import { WebSearchButton } from './components'
import type { ComponentType } from 'react' import type { ComponentType } from 'react'
// MENLO_PLATFORM_BASE_URL is defined in vite.config.ts (defaults to 'https://api-dev.menlo.ai/jan/v1') // JAN_API_BASE is defined in vite.config.ts (defaults to 'https://api-dev.jan.ai/jan/v1')
declare const MENLO_PLATFORM_BASE_URL: string declare const JAN_API_BASE: string
export default class MCPExtensionWeb extends MCPExtension { export default class MCPExtensionWeb extends MCPExtension {
private mcpEndpoint = '/mcp' private mcpEndpoint = '/mcp'
@ -77,7 +77,7 @@ export default class MCPExtensionWeb extends MCPExtension {
// Create transport with OAuth provider (handles token refresh automatically) // Create transport with OAuth provider (handles token refresh automatically)
const transport = new StreamableHTTPClientTransport( const transport = new StreamableHTTPClientTransport(
new URL(`${MENLO_PLATFORM_BASE_URL}${this.mcpEndpoint}`), new URL(`${JAN_API_BASE}${this.mcpEndpoint}`),
{ {
authProvider: this.oauthProvider authProvider: this.oauthProvider
// No sessionId needed - server will generate one automatically // No sessionId needed - server will generate one automatically

View File

@ -6,13 +6,13 @@
import { AuthTokens } from './types' import { AuthTokens } from './types'
import { AUTH_ENDPOINTS } from './const' import { AUTH_ENDPOINTS } from './const'
declare const MENLO_PLATFORM_BASE_URL: string declare const JAN_API_BASE: string
/** /**
* Logout user on server * Logout user on server
*/ */
export async function logoutUser(): Promise<void> { export async function logoutUser(): Promise<void> {
const response = await fetch(`${MENLO_PLATFORM_BASE_URL}${AUTH_ENDPOINTS.LOGOUT}`, { const response = await fetch(`${JAN_API_BASE}${AUTH_ENDPOINTS.LOGOUT}`, {
method: 'GET', method: 'GET',
credentials: 'include', credentials: 'include',
headers: { headers: {
@ -29,7 +29,7 @@ export async function logoutUser(): Promise<void> {
* Guest login * Guest login
*/ */
export async function guestLogin(): Promise<AuthTokens> { export async function guestLogin(): Promise<AuthTokens> {
const response = await fetch(`${MENLO_PLATFORM_BASE_URL}${AUTH_ENDPOINTS.GUEST_LOGIN}`, { const response = await fetch(`${JAN_API_BASE}${AUTH_ENDPOINTS.GUEST_LOGIN}`, {
method: 'POST', method: 'POST',
credentials: 'include', credentials: 'include',
headers: { headers: {
@ -51,7 +51,7 @@ export async function guestLogin(): Promise<AuthTokens> {
*/ */
export async function refreshToken(): Promise<AuthTokens> { export async function refreshToken(): Promise<AuthTokens> {
const response = await fetch( const response = await fetch(
`${MENLO_PLATFORM_BASE_URL}${AUTH_ENDPOINTS.REFRESH_TOKEN}`, `${JAN_API_BASE}${AUTH_ENDPOINTS.REFRESH_TOKEN}`,
{ {
method: 'GET', method: 'GET',
credentials: 'include', credentials: 'include',

View File

@ -5,10 +5,10 @@
import { AuthTokens, LoginUrlResponse } from './types' import { AuthTokens, LoginUrlResponse } from './types'
declare const MENLO_PLATFORM_BASE_URL: string declare const JAN_API_BASE: string
export async function getLoginUrl(endpoint: string): Promise<LoginUrlResponse> { export async function getLoginUrl(endpoint: string): Promise<LoginUrlResponse> {
const response: Response = await fetch(`${MENLO_PLATFORM_BASE_URL}${endpoint}`, { const response: Response = await fetch(`${JAN_API_BASE}${endpoint}`, {
method: 'GET', method: 'GET',
credentials: 'include', credentials: 'include',
headers: { headers: {
@ -30,7 +30,7 @@ export async function handleOAuthCallback(
code: string, code: string,
state?: string state?: string
): Promise<AuthTokens> { ): Promise<AuthTokens> {
const response: Response = await fetch(`${MENLO_PLATFORM_BASE_URL}${endpoint}`, { const response: Response = await fetch(`${JAN_API_BASE}${endpoint}`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

@ -3,9 +3,9 @@
* Handles authentication flows for any OAuth provider * Handles authentication flows for any OAuth provider
*/ */
declare const MENLO_PLATFORM_BASE_URL: string declare const JAN_API_BASE: string
import { User, AuthState, AuthBroadcastMessage, AuthTokens } from './types' import { User, AuthState, AuthBroadcastMessage } from './types'
import { import {
AUTH_STORAGE_KEYS, AUTH_STORAGE_KEYS,
AUTH_ENDPOINTS, AUTH_ENDPOINTS,
@ -115,7 +115,7 @@ export class JanAuthService {
// Store tokens and set authenticated state // Store tokens and set authenticated state
this.accessToken = tokens.access_token this.accessToken = tokens.access_token
this.tokenExpiryTime = this.computeTokenExpiry(tokens) this.tokenExpiryTime = Date.now() + tokens.expires_in * 1000
this.setAuthProvider(providerId) this.setAuthProvider(providerId)
this.authBroadcast.broadcastLogin() this.authBroadcast.broadcastLogin()
@ -158,7 +158,7 @@ export class JanAuthService {
const tokens = await refreshToken() const tokens = await refreshToken()
this.accessToken = tokens.access_token this.accessToken = tokens.access_token
this.tokenExpiryTime = this.computeTokenExpiry(tokens) this.tokenExpiryTime = Date.now() + tokens.expires_in * 1000
} catch (error) { } catch (error) {
console.error('Failed to refresh access token:', error) console.error('Failed to refresh access token:', error)
if (error instanceof ApiError && error.isStatus(401)) { if (error instanceof ApiError && error.isStatus(401)) {
@ -343,23 +343,6 @@ export class JanAuthService {
localStorage.removeItem(AUTH_STORAGE_KEYS.AUTH_PROVIDER) localStorage.removeItem(AUTH_STORAGE_KEYS.AUTH_PROVIDER)
} }
private computeTokenExpiry(tokens: AuthTokens): number {
if (tokens.expires_at) {
const expiresAt = new Date(tokens.expires_at).getTime()
if (!Number.isNaN(expiresAt)) {
return expiresAt
}
console.warn('Invalid expires_at format in auth tokens:', tokens.expires_at)
}
if (typeof tokens.expires_in === 'number') {
return Date.now() + tokens.expires_in * 1000
}
console.warn('Auth tokens missing expiry information; defaulting to immediate expiry')
return Date.now()
}
/** /**
* Ensure guest access is available * Ensure guest access is available
*/ */
@ -369,7 +352,7 @@ export class JanAuthService {
if (!this.accessToken || Date.now() > this.tokenExpiryTime) { if (!this.accessToken || Date.now() > this.tokenExpiryTime) {
const tokens = await guestLogin() const tokens = await guestLogin()
this.accessToken = tokens.access_token this.accessToken = tokens.access_token
this.tokenExpiryTime = this.computeTokenExpiry(tokens) this.tokenExpiryTime = Date.now() + tokens.expires_in * 1000
} }
} catch (error) { } catch (error) {
console.error('Failed to ensure guest access:', error) console.error('Failed to ensure guest access:', error)
@ -404,6 +387,7 @@ export class JanAuthService {
case AUTH_EVENTS.LOGOUT: case AUTH_EVENTS.LOGOUT:
// Another tab logged out, clear our state // Another tab logged out, clear our state
this.clearAuthState() this.clearAuthState()
this.ensureGuestAccess().catch(console.error)
break break
} }
}) })
@ -429,7 +413,7 @@ export class JanAuthService {
private async fetchUserProfile(): Promise<User | null> { private async fetchUserProfile(): Promise<User | null> {
try { try {
return await this.makeAuthenticatedRequest<User>( return await this.makeAuthenticatedRequest<User>(
`${MENLO_PLATFORM_BASE_URL}${AUTH_ENDPOINTS.ME}` `${JAN_API_BASE}${AUTH_ENDPOINTS.ME}`
) )
} catch (error) { } catch (error) {
console.error('Failed to fetch user profile:', error) console.error('Failed to fetch user profile:', error)

View File

@ -16,8 +16,7 @@ export type AuthType = ProviderType | 'guest'
export interface AuthTokens { export interface AuthTokens {
access_token: string access_token: string
expires_in?: number expires_in: number
expires_at?: string
object: string object: string
} }

View File

@ -1,5 +1,5 @@
export {} export {}
declare global { declare global {
declare const MENLO_PLATFORM_BASE_URL: string declare const JAN_API_BASE: string
} }

View File

@ -14,6 +14,6 @@ export default defineConfig({
emptyOutDir: false // Don't clean the output directory emptyOutDir: false // Don't clean the output directory
}, },
define: { define: {
MENLO_PLATFORM_BASE_URL: JSON.stringify(process.env.MENLO_PLATFORM_BASE_URL || 'https://api-dev.menlo.ai/v1'), JAN_API_BASE: JSON.stringify(process.env.JAN_API_BASE || 'https://api-dev.jan.ai/v1'),
} }
}) })

View File

@ -70,6 +70,6 @@ There are a few things to keep in mind when writing your extension code:
``` ```
For more information about the Jan Extension Core module, see the For more information about the Jan Extension Core module, see the
[documentation](https://github.com/janhq/jan/blob/main/core/README.md). [documentation](https://github.com/menloresearch/jan/blob/main/core/README.md).
So, what are you waiting for? Go ahead and start customizing your extension! So, what are you waiting for? Go ahead and start customizing your extension!

View File

@ -0,0 +1,45 @@
{
"name": "@janhq/jan-provider-extension",
"productName": "Jan Provider",
"version": "1.0.0",
"description": "Provides remote model inference through Jan API",
"main": "dist/index.js",
"author": "Jan <service@jan.ai>",
"license": "MIT",
"scripts": {
"build": "rolldown -c rolldown.config.mjs",
"build:publish": "rimraf *.tgz --glob || true && yarn build && npm pack && cpx *.tgz ../../pre-install",
"test": "vitest run",
"test:watch": "vitest"
},
"exports": {
".": "./dist/index.js",
"./main": "./dist/module.js"
},
"devDependencies": {
"cpx": "1.5.0",
"rimraf": "6.0.1",
"rolldown": "1.0.0-beta.1",
"ts-loader": "^9.5.0",
"typescript": "5.9.2",
"vitest": "^2.1.8"
},
"dependencies": {
"@janhq/core": "../../core/package.tgz",
"@tauri-apps/plugin-http": "2.5.0",
"zustand": "^5.0.3"
},
"engines": {
"node": ">=18.0.0"
},
"files": [
"dist/*",
"package.json",
"README.md"
],
"bundleDependencies": [],
"installConfig": {
"hoistingLimits": "workspaces"
},
"packageManager": "yarn@4.5.3"
}

View File

@ -1,14 +1,14 @@
import { defineConfig } from 'rolldown' import { defineConfig } from 'rolldown'
import settingJson from './settings.json' with { type: 'json' }
export default defineConfig({ export default defineConfig({
input: 'src/index.ts', input: 'src/index.ts',
output: { output: {
format: 'esm', format: 'esm',
file: 'dist/index.js', file: 'dist/index.js',
inlineDynamicImports: true, // Required for dynamic import of @tauri-apps/plugin-http
}, },
platform: 'browser', platform: 'browser',
define: { define: {
SETTINGS: JSON.stringify(settingJson), JAN_API_BASE: JSON.stringify(process.env.JAN_API_BASE || 'https://api-dev.jan.ai/v1'),
}, },
}) })

View File

@ -0,0 +1,329 @@
/**
* Mobile Authentication Tests
* Verifies Jan Provider can authenticate and fetch models on mobile
*/
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
describe('Jan Provider Mobile Authentication', () => {
const mockFetch = vi.fn()
const originalFetch = global.fetch
beforeEach(() => {
// Setup mobile environment
;(globalThis as any).IS_WEB_APP = false
vi.clearAllMocks()
global.fetch = mockFetch
})
afterEach(() => {
global.fetch = originalFetch
delete (globalThis as any).IS_WEB_APP
})
describe('Guest Login Flow', () => {
it('should perform guest login and get access token', async () => {
// Mock guest login response
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
access_token: 'guest-token-123',
expires_in: 3600,
}),
})
const response = await fetch('https://api.jan.ai/v1/auth/guest-login', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
})
expect(response.ok).toBe(true)
const data = await response.json()
expect(data.access_token).toBe('guest-token-123')
expect(data.expires_in).toBe(3600)
})
it('should handle guest login failure', async () => {
// Mock failed guest login
mockFetch.mockResolvedValueOnce({
ok: false,
status: 500,
statusText: 'Internal Server Error',
})
const response = await fetch('https://api.jan.ai/v1/auth/guest-login', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
})
expect(response.ok).toBe(false)
expect(response.status).toBe(500)
})
})
describe('Authenticated API Requests', () => {
it('should fetch models with guest token', async () => {
const mockModels = {
object: 'list',
data: [
{
id: 'gpt-4o-mini',
object: 'model',
owned_by: 'openai',
},
{
id: 'claude-3-5-sonnet-20241022',
object: 'model',
owned_by: 'anthropic',
},
],
}
// Mock models response
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: 'OK',
json: async () => mockModels,
})
const response = await fetch('https://api.jan.ai/v1/conv/models', {
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer guest-token-123',
},
})
expect(response.ok).toBe(true)
const data = await response.json()
expect(data.data).toHaveLength(2)
expect(data.data[0].id).toBe('gpt-4o-mini')
expect(data.data[1].id).toBe('claude-3-5-sonnet-20241022')
})
it('should handle 401 unauthorized without token', async () => {
// Mock 401 response
mockFetch.mockResolvedValueOnce({
ok: false,
status: 401,
statusText: 'Unauthorized',
text: async () => JSON.stringify({
code: '019947f0-eca1-7474-8ed2-09d6e5389b54',
error: '',
}),
})
const response = await fetch('https://api.jan.ai/v1/conv/models', {
headers: {
'Content-Type': 'application/json',
},
})
expect(response.ok).toBe(false)
expect(response.status).toBe(401)
})
it('should include Bearer token in Authorization header', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ object: 'list', data: [] }),
})
await fetch('https://api.jan.ai/v1/conv/models', {
headers: {
'Authorization': 'Bearer guest-token-123',
},
})
expect(mockFetch).toHaveBeenCalledWith(
'https://api.jan.ai/v1/conv/models',
expect.objectContaining({
headers: expect.objectContaining({
'Authorization': 'Bearer guest-token-123',
}),
})
)
})
})
describe('Token Caching and Renewal', () => {
it('should cache token and reuse until expiry', () => {
const now = Date.now()
const expiresIn = 3600 // 1 hour
const tokenExpiryTime = now + (expiresIn * 1000) - 60000 // 1 min buffer
// First call - should use cached token
const shouldRenew1 = Date.now() >= tokenExpiryTime
expect(shouldRenew1).toBe(false)
// Still within expiry - should use cached token
const futureTime = now + 1800000 // 30 minutes later
const shouldRenew2 = futureTime >= tokenExpiryTime
expect(shouldRenew2).toBe(false)
// After expiry - should renew token
const expiredTime = now + 3600000 // 1 hour later
const shouldRenew3 = expiredTime >= tokenExpiryTime
expect(shouldRenew3).toBe(true)
})
})
describe('API Endpoint Configuration', () => {
it('should use production API for mobile', () => {
const apiBase = 'https://api.jan.ai/v1'
expect(apiBase).toBe('https://api.jan.ai/v1')
expect(apiBase).not.toContain('api-dev')
})
it('should construct correct endpoints', () => {
const apiBase = 'https://api.jan.ai/v1'
const guestLoginEndpoint = `${apiBase}/auth/guest-login`
const modelsEndpoint = `${apiBase}/conv/models`
const chatEndpoint = `${apiBase}/conv/chat/completions`
expect(guestLoginEndpoint).toBe('https://api.jan.ai/v1/auth/guest-login')
expect(modelsEndpoint).toBe('https://api.jan.ai/v1/conv/models')
expect(chatEndpoint).toBe('https://api.jan.ai/v1/conv/chat/completions')
})
})
describe('Platform Detection', () => {
it('should detect Tauri platform correctly', () => {
const IS_WEB_APP = (globalThis as any).IS_WEB_APP
const isTauri =
typeof IS_WEB_APP === 'undefined' ||
(IS_WEB_APP !== true && IS_WEB_APP !== 'true')
expect(isTauri).toBe(true)
})
it('should not detect web as Tauri', () => {
;(globalThis as any).IS_WEB_APP = true
const IS_WEB_APP = (globalThis as any).IS_WEB_APP
const isTauri =
typeof IS_WEB_APP === 'undefined' ||
(IS_WEB_APP !== true && IS_WEB_APP !== 'true')
expect(isTauri).toBe(false)
})
})
describe('Error Handling', () => {
it('should handle network errors gracefully', async () => {
mockFetch.mockRejectedValueOnce(new Error('Network error'))
await expect(
fetch('https://api.jan.ai/v1/conv/models')
).rejects.toThrow('Network error')
})
it('should handle malformed JSON responses', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => {
throw new Error('Invalid JSON')
},
})
const response = await fetch('https://api.jan.ai/v1/conv/models')
await expect(response.json()).rejects.toThrow('Invalid JSON')
})
it('should provide detailed error messages', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 500,
statusText: 'Internal Server Error',
text: async () => 'Server error details',
})
const response = await fetch('https://api.jan.ai/v1/conv/models')
expect(response.ok).toBe(false)
const errorText = await response.text()
expect(errorText).toBe('Server error details')
})
})
})
describe('Integration: Full Authentication Flow', () => {
const mockFetch = vi.fn()
const originalFetch = global.fetch
beforeEach(() => {
;(globalThis as any).IS_WEB_APP = false
vi.clearAllMocks()
global.fetch = mockFetch
})
afterEach(() => {
global.fetch = originalFetch
delete (globalThis as any).IS_WEB_APP
})
it('should complete full flow: guest login -> fetch models -> use models', async () => {
// Step 1: Guest login
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
access_token: 'guest-token-abc',
expires_in: 3600,
}),
})
const loginResponse = await fetch('https://api.jan.ai/v1/auth/guest-login', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
})
expect(loginResponse.ok).toBe(true)
const { access_token } = await loginResponse.json()
// Step 2: Fetch models with token
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
object: 'list',
data: [
{ id: 'gpt-4o-mini', object: 'model', owned_by: 'openai' },
{ id: 'claude-3-5-sonnet-20241022', object: 'model', owned_by: 'anthropic' },
],
}),
})
const modelsResponse = await fetch('https://api.jan.ai/v1/conv/models', {
headers: {
'Authorization': `Bearer ${access_token}`,
},
})
expect(modelsResponse.ok).toBe(true)
const models = await modelsResponse.json()
// Step 3: Verify models can be used
expect(models.data).toHaveLength(2)
expect(models.data[0].id).toBe('gpt-4o-mini')
expect(models.data[1].id).toBe('claude-3-5-sonnet-20241022')
// Verify provider info
const janProvider = {
provider: 'jan',
models: models.data,
active: true,
}
expect(janProvider.provider).toBe('jan')
expect(janProvider.models.length).toBeGreaterThan(0)
expect(janProvider.active).toBe(true)
})
})

View File

@ -0,0 +1,237 @@
/**
* Jan Provider API Client
* Handles API requests to Jan backend for models and chat completions
*/
import { makeAuthenticatedRequest, getAuthHeader } from './auth'
import { janProviderStore } from './store'
import type {
JanModel,
JanModelsResponse,
JanChatCompletionRequest,
JanChatCompletionResponse,
JanChatCompletionChunk,
} from './types'
/**
* Get platform-appropriate fetch (Tauri fetch for mobile, native for web)
*/
async function getPlatformFetch(): Promise<typeof fetch> {
const IS_WEB_APP = (globalThis as any).IS_WEB_APP
const isTauri = typeof IS_WEB_APP === 'undefined' ||
(IS_WEB_APP !== true && IS_WEB_APP !== 'true')
if (isTauri) {
try {
const httpPlugin = await import('@tauri-apps/plugin-http')
return httpPlugin.fetch as typeof fetch
} catch {
return fetch
}
}
return fetch
}
/**
* Get Jan API base URL (production only for mobile)
*/
function getApiBase(): string {
if (typeof (globalThis as any).JAN_API_BASE !== 'undefined') {
return (globalThis as any).JAN_API_BASE
}
if (typeof import.meta !== 'undefined' && import.meta.env?.JAN_API_BASE) {
return import.meta.env.JAN_API_BASE
}
return 'https://api.jan.ai/v1'
}
const TEMPORARY_CHAT_ID = 'temporary-chat'
/**
* Get chat completion endpoint and payload configuration
*/
function getChatCompletionConfig(
request: JanChatCompletionRequest,
stream: boolean = false
) {
const endpoint = `${getApiBase()}/chat/completions`
const payload = {
...request,
stream,
conversation_id: undefined,
}
return { endpoint, payload, isTemporaryChat: request.conversation_id === TEMPORARY_CHAT_ID }
}
export class JanApiClient {
private static instance: JanApiClient
private constructor() {}
static getInstance(): JanApiClient {
if (!JanApiClient.instance) {
JanApiClient.instance = new JanApiClient()
}
return JanApiClient.instance
}
async getModels(): Promise<JanModel[]> {
try {
janProviderStore.setLoadingModels(true)
janProviderStore.clearError()
const apiBase = getApiBase()
const response = await makeAuthenticatedRequest<JanModelsResponse>(
`${apiBase}/conv/models`
)
const models = response.data || []
janProviderStore.setModels(models)
return models
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Failed to fetch models'
janProviderStore.setError(errorMessage)
janProviderStore.setLoadingModels(false)
throw error
}
}
async createChatCompletion(
request: JanChatCompletionRequest
): Promise<JanChatCompletionResponse> {
try {
janProviderStore.clearError()
const { endpoint, payload } = getChatCompletionConfig(request, false)
return await makeAuthenticatedRequest<JanChatCompletionResponse>(
endpoint,
{
method: 'POST',
body: JSON.stringify(payload),
}
)
} catch (error) {
const errorMessage =
error instanceof Error
? error.message
: 'Failed to create chat completion'
janProviderStore.setError(errorMessage)
throw error
}
}
async createStreamingChatCompletion(
request: JanChatCompletionRequest,
onChunk: (chunk: JanChatCompletionChunk) => void,
onComplete?: () => void,
onError?: (error: Error) => void
): Promise<void> {
try {
janProviderStore.clearError()
const authHeader = await getAuthHeader()
const { endpoint, payload } = getChatCompletionConfig(request, true)
const platformFetch = await getPlatformFetch()
const response = await platformFetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...authHeader,
},
body: JSON.stringify(payload),
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(
`API request failed: ${response.status} ${response.statusText} - ${errorText}`
)
}
if (!response.body) {
throw new Error('Response body is null')
}
const reader = response.body.getReader()
const decoder = new TextDecoder()
try {
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) {
break
}
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n')
buffer = lines.pop() || ''
for (const line of lines) {
const trimmedLine = line.trim()
if (trimmedLine.startsWith('data: ')) {
const data = trimmedLine.slice(6).trim()
if (data === '[DONE]') {
onComplete?.()
return
}
try {
const parsedChunk: JanChatCompletionChunk = JSON.parse(data)
onChunk(parsedChunk)
} catch (parseError) {
console.warn(
'Failed to parse SSE chunk:',
parseError,
'Data:',
data
)
}
}
}
}
onComplete?.()
} finally {
reader.releaseLock()
}
} catch (error) {
const err =
error instanceof Error ? error : new Error('Unknown error occurred')
janProviderStore.setError(err.message)
onError?.(err)
throw err
}
}
async initialize(): Promise<void> {
try {
janProviderStore.setAuthenticated(true)
await this.getModels()
console.log('Jan API client initialized successfully')
} catch (error) {
const errorMessage =
error instanceof Error
? error.message
: 'Failed to initialize API client'
janProviderStore.setError(errorMessage)
throw error
} finally {
janProviderStore.setInitializing(false)
}
}
}
export const janApiClient = JanApiClient.getInstance()

View File

@ -0,0 +1,151 @@
/**
* Jan Auth Client for Mobile
* Handles authentication using guest login for mobile platforms
*/
interface AuthTokens {
access_token: string
expires_in: number
}
interface JanAuthService {
getAuthHeader(): Promise<{ Authorization: string }>
makeAuthenticatedRequest<T>(url: string, options?: RequestInit): Promise<T>
initialize(): Promise<void>
isAuthenticated(): boolean
}
declare global {
interface Window {
janAuthService?: JanAuthService
}
}
let guestAccessToken: string | null = null
let guestTokenExpiry: number = 0
function getApiBase(): string {
return 'https://api.jan.ai/v1'
}
/**
* Perform guest login to obtain access token
*/
async function guestLogin(platformFetch: typeof fetch): Promise<AuthTokens> {
const response = await platformFetch(`${getApiBase()}/auth/guest-login`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
})
if (!response.ok) {
throw new Error(`Guest login failed: ${response.status}`)
}
return response.json() as Promise<AuthTokens>
}
/**
* Ensure valid guest access token, refreshing if expired
*/
async function ensureGuestToken(platformFetch: typeof fetch): Promise<string> {
if (guestAccessToken && Date.now() < guestTokenExpiry) {
return guestAccessToken
}
const tokens = await guestLogin(platformFetch)
guestAccessToken = tokens.access_token
guestTokenExpiry = Date.now() + (tokens.expires_in * 1000) - 60000
return guestAccessToken
}
export function getAuthService(): JanAuthService | null {
return window.janAuthService || null
}
/**
* Wait for auth service initialization (web platform)
*/
export async function waitForAuthService(
maxWaitMs: number = 5000
): Promise<JanAuthService | null> {
const startTime = Date.now()
while (Date.now() - startTime < maxWaitMs) {
const authService = getAuthService()
if (authService) return authService
await new Promise((resolve) => setTimeout(resolve, 100))
}
return null
}
/**
* Get platform-appropriate fetch (Tauri fetch for mobile, native for web)
*/
async function getPlatformFetch(): Promise<typeof fetch> {
const IS_WEB_APP = (globalThis as any).IS_WEB_APP
const isTauri = typeof IS_WEB_APP === 'undefined' ||
(IS_WEB_APP !== true && IS_WEB_APP !== 'true')
if (isTauri) {
try {
const httpPlugin = await import('@tauri-apps/plugin-http')
return httpPlugin.fetch as typeof fetch
} catch (error) {
console.warn('Tauri fetch unavailable, using native fetch')
return fetch
}
}
return fetch
}
/**
* Make authenticated request to Jan API with automatic guest login
*/
export async function makeAuthenticatedRequest<T>(
url: string,
options: RequestInit = {}
): Promise<T> {
const authService = await waitForAuthService()
if (authService) {
return authService.makeAuthenticatedRequest<T>(url, options)
}
const platformFetch = await getPlatformFetch()
const accessToken = await ensureGuestToken(platformFetch)
const response = await platformFetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
...options.headers,
},
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`)
}
return response.json()
}
/**
* Get authorization header with guest token
*/
export async function getAuthHeader(): Promise<{ Authorization?: string }> {
const authService = await waitForAuthService()
if (authService) {
return authService.getAuthHeader()
}
const platformFetch = await getPlatformFetch()
const accessToken = await ensureGuestToken(platformFetch)
return { Authorization: `Bearer ${accessToken}` }
}

View File

@ -0,0 +1 @@
export { default } from './provider'

View File

@ -0,0 +1,459 @@
/**
* Jan Provider Extension
* Provides remote model inference through Jan API
* Available on web and mobile platforms (disabled on desktop Tauri)
*/
import {
AIEngine,
modelInfo,
SessionInfo,
UnloadResult,
chatCompletionRequest,
chatCompletion,
chatCompletionChunk,
ImportOptions,
} from '@janhq/core'
import { janApiClient } from './api'
import { janProviderStore } from './store'
import type { JanChatMessage } from './types'
// Jan models support tools via MCP
const JAN_MODEL_CAPABILITIES = ['tools'] as const
export default class JanProviderExtension extends AIEngine {
readonly provider = 'jan'
private activeSessions: Map<string, SessionInfo> = new Map()
private isDesktopTauri: boolean = false
override async onLoad() {
// Detect if we're running on desktop Tauri (not mobile)
this.isDesktopTauri = this.detectDesktopTauri()
// On desktop Tauri, do not load this extension
if (this.isDesktopTauri) {
console.log(
'Jan Provider Extension: Disabled on desktop Tauri (use llamacpp instead)'
)
return
}
console.log('Loading Jan Provider Extension...')
try {
// Check and clear invalid Jan models (capabilities mismatch)
this.validateJanModelsLocalStorage()
// Don't initialize here - wait until auth service is ready
// Models will be fetched lazily when list() is called
console.log('Jan Provider Extension loaded successfully (models will be fetched on demand)')
} catch (error) {
console.error('Failed to load Jan Provider Extension:', error)
throw error
}
super.onLoad()
}
/**
* Detect if we're on desktop Tauri (not mobile)
* Mobile platforms (iOS/Android) will have IS_IOS or IS_ANDROID set
* Web platform will have IS_WEB_APP set to true
* Desktop Tauri will have neither (or IS_WEB_APP = false)
*/
private detectDesktopTauri(): boolean {
// Check if we're in a browser environment
if (typeof window === 'undefined') {
return false
}
// Check for mobile-specific flags
const IS_IOS = (window as any).IS_IOS
const IS_ANDROID = (window as any).IS_ANDROID
const IS_WEB_APP = (window as any).IS_WEB_APP
// If we're on mobile, not desktop
if (IS_IOS === true || IS_ANDROID === true) {
return false
}
// If we're on web app, not desktop
if (IS_WEB_APP === true || IS_WEB_APP === 'true') {
return false
}
// Check if Tauri API is available
const hasTauriAPI = !!(window as any).__TAURI__
// If Tauri API is available and we're not on mobile or web, we're on desktop Tauri
return hasTauriAPI
}
// Verify Jan models capabilities in localStorage
private validateJanModelsLocalStorage() {
try {
console.log('Validating Jan models in localStorage...')
const storageKey = 'model-provider'
const data = localStorage.getItem(storageKey)
if (!data) return
const parsed = JSON.parse(data)
if (!parsed?.state?.providers) return
let hasInvalidModel = false
for (const provider of parsed.state.providers) {
if (provider.provider === 'jan' && provider.models) {
for (const model of provider.models) {
console.log(`Checking Jan model: ${model.id}`, model.capabilities)
if (
JSON.stringify(model.capabilities) !==
JSON.stringify(JAN_MODEL_CAPABILITIES)
) {
hasInvalidModel = true
console.log(
`Found invalid Jan model: ${model.id}, clearing localStorage`
)
break
}
}
}
if (hasInvalidModel) break
}
if (hasInvalidModel) {
localStorage.removeItem(storageKey)
const afterRemoval = localStorage.getItem(storageKey)
if (afterRemoval) {
localStorage.setItem(
storageKey,
JSON.stringify({
state: { providers: [] },
version: parsed.version || 3,
})
)
}
console.log(
'Cleared model-provider from localStorage due to invalid Jan capabilities'
)
window.location.reload()
}
} catch (error) {
console.error('Failed to check Jan models:', error)
}
}
override async onUnload() {
console.log('Unloading Jan Provider Extension...')
for (const sessionId of this.activeSessions.keys()) {
await this.unload(sessionId)
}
janProviderStore.reset()
console.log('Jan Provider Extension unloaded')
}
async get(modelId: string): Promise<modelInfo | undefined> {
if (this.isDesktopTauri) return undefined
return janApiClient
.getModels()
.then((list) => list.find((e) => e.id === modelId))
.then((model) =>
model
? {
id: model.id,
name: model.id,
quant_type: undefined,
providerId: this.provider,
port: 443,
sizeBytes: 0,
tags: [],
path: undefined,
owned_by: model.owned_by,
object: model.object,
capabilities: [...JAN_MODEL_CAPABILITIES],
}
: undefined
)
}
async list(): Promise<modelInfo[]> {
if (this.isDesktopTauri) return []
try {
const janModels = await janApiClient.getModels()
return janModels.map((model) => ({
id: model.id,
name: model.id,
quant_type: undefined,
providerId: this.provider,
port: 443,
sizeBytes: 0,
tags: [],
path: undefined,
owned_by: model.owned_by,
object: model.object,
capabilities: [...JAN_MODEL_CAPABILITIES],
}))
} catch (error) {
console.error('Failed to list Jan models:', error)
throw error
}
}
async load(modelId: string, _settings?: any): Promise<SessionInfo> {
try {
const sessionId = `jan-${modelId}-${Date.now()}`
const sessionInfo: SessionInfo = {
pid: Date.now(),
port: 443,
model_id: modelId,
model_path: `remote:${modelId}`,
api_key: '',
}
this.activeSessions.set(sessionId, sessionInfo)
console.log(
`Jan model session created: ${sessionId} for model ${modelId}`
)
return sessionInfo
} catch (error) {
console.error(`Failed to load Jan model ${modelId}:`, error)
throw error
}
}
async unload(sessionId: string): Promise<UnloadResult> {
try {
const session = this.activeSessions.get(sessionId)
if (!session) {
return {
success: false,
error: `Session ${sessionId} not found`,
}
}
this.activeSessions.delete(sessionId)
console.log(`Jan model session unloaded: ${sessionId}`)
return { success: true }
} catch (error) {
console.error(`Failed to unload Jan session ${sessionId}:`, error)
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
}
}
}
async chat(
opts: chatCompletionRequest,
abortController?: AbortController
): Promise<chatCompletion | AsyncIterable<chatCompletionChunk>> {
try {
if (abortController?.signal?.aborted) {
throw new Error('Request was aborted')
}
const modelId = opts.model
if (!modelId) {
throw new Error('Model ID is required')
}
const janMessages: JanChatMessage[] = opts.messages.map((msg) => ({
role: msg.role as 'system' | 'user' | 'assistant',
content:
typeof msg.content === 'string'
? msg.content
: JSON.stringify(msg.content),
}))
const janRequest = {
model: modelId,
messages: janMessages,
conversation_id: opts.thread_id,
temperature: opts.temperature ?? undefined,
max_tokens: opts.n_predict ?? undefined,
top_p: opts.top_p ?? undefined,
frequency_penalty: opts.frequency_penalty ?? undefined,
presence_penalty: opts.presence_penalty ?? undefined,
stream: opts.stream ?? false,
stop: opts.stop ?? undefined,
tools: opts.tools ?? undefined,
tool_choice: opts.tool_choice ?? undefined,
}
if (opts.stream) {
return this.createStreamingGenerator(janRequest, abortController)
} else {
const response = await janApiClient.createChatCompletion(janRequest)
if (abortController?.signal?.aborted) {
throw new Error('Request was aborted')
}
return {
id: response.id,
object: 'chat.completion' as const,
created: response.created,
model: response.model,
choices: response.choices.map((choice) => ({
index: choice.index,
message: {
role: choice.message.role,
content: choice.message.content,
reasoning: choice.message.reasoning,
reasoning_content: choice.message.reasoning_content,
tool_calls: choice.message.tool_calls,
},
finish_reason: (choice.finish_reason || 'stop') as
| 'stop'
| 'length'
| 'tool_calls'
| 'content_filter'
| 'function_call',
})),
usage: response.usage,
}
}
} catch (error) {
console.error('Jan chat completion failed:', error)
throw error
}
}
private async *createStreamingGenerator(
janRequest: any,
abortController?: AbortController
) {
let resolve: () => void
let reject: (error: Error) => void
const chunks: any[] = []
let isComplete = false
let error: Error | null = null
const promise = new Promise<void>((res, rej) => {
resolve = res
reject = rej
})
const abortListener = () => {
error = new Error('Request was aborted')
reject(error)
}
if (abortController?.signal) {
if (abortController.signal.aborted) {
throw new Error('Request was aborted')
}
abortController.signal.addEventListener('abort', abortListener)
}
try {
janApiClient.createStreamingChatCompletion(
janRequest,
(chunk) => {
if (abortController?.signal?.aborted) {
return
}
const streamChunk = {
id: chunk.id,
object: chunk.object,
created: chunk.created,
model: chunk.model,
choices: chunk.choices.map((choice) => ({
index: choice.index,
delta: {
role: choice.delta.role,
content: choice.delta.content,
reasoning: choice.delta.reasoning,
reasoning_content: choice.delta.reasoning_content,
tool_calls: choice.delta.tool_calls,
},
finish_reason: choice.finish_reason,
})),
}
chunks.push(streamChunk)
},
() => {
isComplete = true
resolve()
},
(err) => {
error = err
reject(err)
}
)
let yieldedIndex = 0
while (!isComplete && !error) {
if (abortController?.signal?.aborted) {
throw new Error('Request was aborted')
}
while (yieldedIndex < chunks.length) {
yield chunks[yieldedIndex]
yieldedIndex++
}
await new Promise((resolve) => setTimeout(resolve, 10))
}
while (yieldedIndex < chunks.length) {
yield chunks[yieldedIndex]
yieldedIndex++
}
if (error) {
throw error
}
await promise
} finally {
if (abortController?.signal) {
abortController.signal.removeEventListener('abort', abortListener)
}
}
}
async delete(modelId: string): Promise<void> {
throw new Error(
`Delete operation not supported for remote Jan API model: ${modelId}`
)
}
async update(modelId: string, _model: Partial<modelInfo>): Promise<void> {
throw new Error(
`Update operation not supported for remote Jan API model: ${modelId}`
)
}
async import(modelId: string, _opts: ImportOptions): Promise<void> {
throw new Error(
`Import operation not supported for remote Jan API model: ${modelId}`
)
}
async abortImport(modelId: string): Promise<void> {
throw new Error(
`Abort import operation not supported for remote Jan API model: ${modelId}`
)
}
async getLoadedModels(): Promise<string[]> {
return Array.from(this.activeSessions.values()).map(
(session) => session.model_id
)
}
async isToolSupported(modelId: string): Promise<boolean> {
console.log(`Checking tool support for Jan model ${modelId}: supported`)
return true
}
}

View File

@ -0,0 +1,90 @@
/**
* Jan Provider Store
* Zustand-based state management for Jan provider authentication and models
*/
import { create } from 'zustand'
import type { JanModel } from './types'
export interface JanProviderState {
isAuthenticated: boolean
isInitializing: boolean
models: JanModel[]
isLoadingModels: boolean
error: string | null
}
export interface JanProviderActions {
setAuthenticated: (isAuthenticated: boolean) => void
setInitializing: (isInitializing: boolean) => void
setModels: (models: JanModel[]) => void
setLoadingModels: (isLoadingModels: boolean) => void
setError: (error: string | null) => void
clearError: () => void
reset: () => void
}
export type JanProviderStore = JanProviderState & JanProviderActions
const initialState: JanProviderState = {
isAuthenticated: false,
isInitializing: true,
models: [],
isLoadingModels: false,
error: null,
}
export const useJanProviderStore = create<JanProviderStore>((set) => ({
...initialState,
setAuthenticated: (isAuthenticated: boolean) =>
set({ isAuthenticated, error: null }),
setInitializing: (isInitializing: boolean) =>
set({ isInitializing }),
setModels: (models: JanModel[]) =>
set({ models, isLoadingModels: false }),
setLoadingModels: (isLoadingModels: boolean) =>
set({ isLoadingModels }),
setError: (error: string | null) =>
set({ error }),
clearError: () =>
set({ error: null }),
reset: () =>
set({
isAuthenticated: false,
isInitializing: false,
models: [],
isLoadingModels: false,
error: null,
}),
}))
// Export a store instance for non-React usage
export const janProviderStore = {
// Store access methods
getState: useJanProviderStore.getState,
setState: useJanProviderStore.setState,
subscribe: useJanProviderStore.subscribe,
// Direct action methods
setAuthenticated: (isAuthenticated: boolean) =>
useJanProviderStore.getState().setAuthenticated(isAuthenticated),
setInitializing: (isInitializing: boolean) =>
useJanProviderStore.getState().setInitializing(isInitializing),
setModels: (models: JanModel[]) =>
useJanProviderStore.getState().setModels(models),
setLoadingModels: (isLoadingModels: boolean) =>
useJanProviderStore.getState().setLoadingModels(isLoadingModels),
setError: (error: string | null) =>
useJanProviderStore.getState().setError(error),
clearError: () =>
useJanProviderStore.getState().clearError(),
reset: () =>
useJanProviderStore.getState().reset(),
}

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