Compare commits

...

20 Commits

Author SHA1 Message Date
Louis
f1c9a5c1b2
fix: inconsistent data folder path 2025-10-07 12:11:50 +07:00
Nguyen Ngoc Minh
dc4de43516
Merge pull request #6728 from menloresearch/fix/anthropic-model-load
Fix: Anthropic request to add models
2025-10-03 15:41:06 +00:00
Vanalite
9046927bf3 fix: optional custom header 2025-10-03 22:05:52 +07:00
Vanalite
7fd83e0bf8 feat: add migration for cohere provider 2025-10-03 21:55:28 +07:00
Faisal Amir
d3ea61b2e9 chore: update setting base url also 2025-10-03 21:49:42 +07:00
Faisal Amir
651ba446bf chore: migration base_url anthropic 2025-10-03 21:40:59 +07:00
Vanalite
7bee282875 fix: Fix Cohere base url 2025-10-03 21:05:09 +07:00
Vanalite
86c7122f90 fix: Add header to anthropic get models api 2025-10-03 20:54:54 +07:00
Faisal Amir
c378d765fc
Merge pull request #6727 from menloresearch/fix/prompt-token
fix: prompt token
2025-10-03 20:12:12 +07:00
Faisal Amir
fdf239352a chore: check model running 2025-10-03 19:52:15 +07:00
Faisal Amir
40c5953fea fix:exclude enable thinking from FE 2025-10-03 19:06:01 +07:00
Faisal Amir
5382e9666e fix: prompt token 2025-10-03 19:01:19 +07:00
Nguyen Ngoc Minh
404f40cc23
Merge pull request #6725 from menloresearch/ci/remove-upload-msi-step
ci: remove upload msi artifact
2025-10-03 06:13:22 +00:00
Minh141120
3ac83fdc08 ci: remove upload msi artifact 2025-10-03 13:11:45 +07:00
Nguyen Ngoc Minh
a568afdff1
Merge pull request #6721 from menloresearch/chore/use-custom-nsis-template
chore: use custom nsis template
2025-10-03 05:50:21 +00:00
Minh141120
14abd6068a refacotr: clean up custom nsis template 2025-10-03 12:29:04 +07:00
Minh141120
d3a5ff9a74 chore: remove install windows deps from test 2025-10-03 12:06:57 +07:00
Minh141120
d7a1a9a529 feat: add logic for uninstall old jan version 2025-10-03 11:50:11 +07:00
Minh141120
4c4e795a3d chore: use custom nsis template 2025-10-03 11:20:35 +07:00
Nghia Doan
c14e1ea00f
Merge pull request #6715 from menloresearch/fix/get-model-capabilities-correctly
fix: Extract model capabilities correctly for various providers on various platforms
2025-10-03 10:51:07 +07:00
22 changed files with 1350 additions and 229 deletions

View File

@ -49,6 +49,8 @@ jobs:
# Update 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
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
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
@ -80,6 +82,36 @@ jobs:
echo "---------./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
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
@ -103,7 +135,14 @@ jobs:
chmod +x .github/scripts/rename-workspace.sh
.github/scripts/rename-workspace.sh ./package.json ${{ inputs.channel }}
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
echo "---------nsis.template---------"
cat ./src-tauri/tauri.bundle.windows.nsis.template
- name: Build app
shell: bash
run: |

View File

@ -98,9 +98,15 @@ jobs:
# Update 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
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
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
jq --arg version "${{ inputs.new_version }}" '.version = $version' ./src-tauri/plugins/tauri-plugin-hardware/package.json > /tmp/package.json
@ -127,9 +133,35 @@ jobs:
echo "---------./src-tauri/Cargo.toml---------"
cat ./src-tauri/Cargo.toml
# 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
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
echo "---------tauri.windows.conf.json---------"
cat ./src-tauri/tauri.windows.conf.json
@ -163,7 +195,14 @@ jobs:
chmod +x .github/scripts/rename-workspace.sh
.github/scripts/rename-workspace.sh ./package.json ${{ inputs.channel }}
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
echo "---------nsis.template---------"
cat ./src-tauri/tauri.bundle.windows.nsis.template
- name: Install AzureSignTool
run: |
@ -234,8 +273,6 @@ jobs:
# Upload for tauri updater
aws s3 cp ./${{ steps.metadata.outputs.FILE_NAME }} s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/${{ steps.metadata.outputs.FILE_NAME }}
aws s3 cp ./${{ steps.metadata.outputs.FILE_NAME }}.sig s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/${{ steps.metadata.outputs.FILE_NAME }}.sig
aws s3 cp ./src-tauri/target/release/bundle/msi/${{ steps.metadata.outputs.MSI_FILE_NAME }} s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/${{ steps.metadata.outputs.MSI_FILE_NAME }}
env:
AWS_ACCESS_KEY_ID: ${{ secrets.DELTA_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.DELTA_AWS_SECRET_ACCESS_KEY }}

View File

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

View File

@ -332,12 +332,14 @@ export default class llamacpp_extension extends AIEngine {
)
// Clear the invalid stored preference
this.clearStoredBackendType()
bestAvailableBackendString =
await this.determineBestBackend(version_backends)
bestAvailableBackendString = await this.determineBestBackend(
version_backends
)
}
} else {
bestAvailableBackendString =
await this.determineBestBackend(version_backends)
bestAvailableBackendString = await this.determineBestBackend(
version_backends
)
}
let settings = structuredClone(SETTINGS)
@ -2151,7 +2153,12 @@ export default class llamacpp_extension extends AIEngine {
if (mmprojPath && !this.isAbsolutePath(mmprojPath))
mmprojPath = await joinPath([await getJanDataFolderPath(), path])
try {
const result = await planModelLoadInternal(path, this.memoryMode, mmprojPath, requestedCtx)
const result = await planModelLoadInternal(
path,
this.memoryMode,
mmprojPath,
requestedCtx
)
return result
} catch (e) {
throw new Error(String(e))
@ -2279,12 +2286,18 @@ export default class llamacpp_extension extends AIEngine {
}
// Calculate text tokens
const messages = JSON.stringify({ messages: opts.messages })
// Use chat_template_kwargs from opts if provided, otherwise default to disable enable_thinking
const tokenizeRequest = {
messages: opts.messages,
chat_template_kwargs: opts.chat_template_kwargs || {
enable_thinking: false,
},
}
let parseResponse = await fetch(`${baseUrl}/apply-template`, {
method: 'POST',
headers: headers,
body: messages,
body: JSON.stringify(tokenizeRequest),
})
if (!parseResponse.ok) {

View File

@ -27,8 +27,7 @@
"copy:assets:tauri": "cpx \"pre-install/*.tgz\" \"src-tauri/resources/pre-install/\" && cpx \"LICENSE\" \"src-tauri/resources/\"",
"download:lib": "node ./scripts/download-lib.mjs",
"download:bin": "node ./scripts/download-bin.mjs",
"download:windows-installer": "node ./scripts/download-win-installer-deps.mjs",
"build:tauri:win32": "yarn download:bin && yarn download:lib && yarn download:windows-installer && yarn tauri build",
"build:tauri:win32": "yarn download:bin && yarn download:lib && yarn tauri build",
"build:tauri:linux": "yarn download:bin && yarn download:lib && NO_STRIP=1 ./src-tauri/build-utils/shim-linuxdeploy.sh yarn tauri build && ./src-tauri/build-utils/buildAppImage.sh",
"build:tauri:darwin": "yarn download:bin && yarn tauri build --target universal-apple-darwin",
"build:tauri": "yarn build:icon && yarn copy:assets:tauri && run-script-os",

View File

@ -1,83 +0,0 @@
console.log('Downloading Windows installer dependencies...')
// scripts/download-win-installer-deps.mjs
import https from 'https'
import fs, { mkdirSync } from 'fs'
import os from 'os'
import path from 'path'
import { copySync } from 'cpx'
function download(url, dest) {
return new Promise((resolve, reject) => {
console.log(`Downloading ${url} to ${dest}`)
const file = fs.createWriteStream(dest)
https
.get(url, (response) => {
console.log(`Response status code: ${response.statusCode}`)
if (
response.statusCode >= 300 &&
response.statusCode < 400 &&
response.headers.location
) {
// Handle redirect
const redirectURL = response.headers.location
console.log(`Redirecting to ${redirectURL}`)
download(redirectURL, dest).then(resolve, reject) // Recursive call
return
} else if (response.statusCode !== 200) {
reject(`Failed to get '${url}' (${response.statusCode})`)
return
}
response.pipe(file)
file.on('finish', () => {
file.close(resolve)
})
})
.on('error', (err) => {
fs.unlink(dest, () => reject(err.message))
})
})
}
async function main() {
console.log('Starting Windows installer dependencies download')
const platform = os.platform() // 'darwin', 'linux', 'win32'
const arch = os.arch() // 'x64', 'arm64', etc.
if (arch != 'x64') return
const libDir = 'src-tauri/resources/lib'
const tempDir = 'scripts/dist'
try {
mkdirSync('scripts/dist')
} catch (err) {
// Expect EEXIST error if the directory already exists
}
// Download VC++ Redistributable 17
if (platform == 'win32') {
const vcFilename = 'vc_redist.x64.exe'
const vcUrl = 'https://aka.ms/vs/17/release/vc_redist.x64.exe'
console.log(`Downloading VC++ Redistributable...`)
const vcSavePath = path.join(tempDir, vcFilename)
if (!fs.existsSync(vcSavePath)) {
await download(vcUrl, vcSavePath)
}
// copy to tauri resources
try {
copySync(vcSavePath, libDir)
} catch (err) {
// Expect EEXIST error
}
}
console.log('Windows installer dependencies downloads completed.')
}
main().catch((err) => {
console.error('Error:', err)
process.exit(1)
})

View File

@ -93,7 +93,7 @@ pub fn get_jan_data_folder_path<R: Runtime>(app_handle: tauri::AppHandle<R>) ->
#[tauri::command]
pub fn get_configuration_file_path<R: Runtime>(app_handle: tauri::AppHandle<R>) -> PathBuf {
let app_path = app_handle.path().app_data_dir().unwrap_or_else(|err| {
let app_path = std::fs::canonicalize(app_handle.path().app_data_dir().unwrap_or_else(|err| {
log::error!(
"Failed to get app data directory: {}. Using home directory instead.",
err
@ -107,7 +107,7 @@ pub fn get_configuration_file_path<R: Runtime>(app_handle: tauri::AppHandle<R>)
.expect("Failed to determine the home directory");
PathBuf::from(home_dir)
});
})).unwrap();
let package_name = env!("CARGO_PKG_NAME");
#[cfg(target_os = "linux")]
@ -138,13 +138,15 @@ pub fn get_configuration_file_path<R: Runtime>(app_handle: tauri::AppHandle<R>)
#[tauri::command]
pub fn default_data_folder_path<R: Runtime>(app_handle: tauri::AppHandle<R>) -> String {
let mut path = app_handle.path().data_dir().unwrap();
let mut path = app_handle.path().app_data_dir().unwrap();
let app_name = std::env::var("APP_NAME")
.unwrap_or_else(|_| app_handle.config().product_name.clone().unwrap());
path.push(app_name);
path.push("data");
path = std::fs::canonicalize(path).unwrap();
let mut path_str = path.to_str().unwrap().to_string();
if let Some(stripped) = path.to_str().unwrap().to_string().strip_suffix(".ai.app") {
@ -152,6 +154,7 @@ pub fn default_data_folder_path<R: Runtime>(app_handle: tauri::AppHandle<R>) ->
}
path_str
}
#[tauri::command]

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,9 @@
{
"bundle": {
"targets": ["nsis", "msi"],
"resources": ["resources/pre-install/**/*", "resources/lib/vulkan-1.dll", "resources/LICENSE", "resources/lib/vc_redist.x64.exe"],
"resources": ["resources/pre-install/**/*", "resources/lib/vulkan-1.dll", "resources/LICENSE"],
"externalBin": ["resources/bin/bun", "resources/bin/uv"],
"windows": {
"nsis": {
"installerHooks": "./windows/hooks.nsh",
"installerIcon": "icons/icon.ico"
},
"webviewInstallMode": {
"silent": true,
"type": "downloadBootstrapper"

View File

@ -1,65 +0,0 @@
!macro NSIS_HOOK_POSTINSTALL
; Check if Visual C++ Redistributable is already installed
ReadRegStr $0 HKLM "SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64" "Version"
${If} $0 == ""
; Try alternative registry location
ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\VisualStudio\14.0\VC\Runtimes\x64" "Version"
${EndIf}
${If} $0 == ""
; VC++ Redistributable not found, need to install
DetailPrint "Visual C++ Redistributable not found, installing from bundled file..."
; Install from bundled EXE if not installed
${If} ${FileExists} "$INSTDIR\resources\lib\vc_redist.x64.exe"
DetailPrint "Installing Visual C++ Redistributable..."
; Copy to TEMP folder and then execute installer
CopyFiles "$INSTDIR\resources\lib\vc_redist.x64.exe" "$TEMP\vc_redist.x64.exe"
ExecWait '"$TEMP\vc_redist.x64.exe" /quiet /norestart' $1
; Check whether installation process exited successfully (code 0) or not
${If} $1 == 0
DetailPrint "Visual C++ Redistributable installed successfully"
${ElseIf} $1 == 1638
DetailPrint "Visual C++ Redistributable already installed (newer version)"
${ElseIf} $1 == 3010
DetailPrint "Visual C++ Redistributable installed successfully (restart required)"
${Else}
DetailPrint "Visual C++ installation failed with exit code: $1"
${EndIf}
; Clean up setup files from TEMP and your installed app
Delete "$TEMP\vc_redist.x64.exe"
Delete "$INSTDIR\resources\lib\vc_redist.x64.exe"
${Else}
DetailPrint "Visual C++ Redistributable not found at expected location: $INSTDIR\resources\lib\vc_redist.x64.exe"
${EndIf}
${Else}
DetailPrint "Visual C++ Redistributable already installed (version: $0)"
${EndIf}
; ---- Copy LICENSE to install root ----
${If} ${FileExists} "$INSTDIR\resources\LICENSE"
CopyFiles /SILENT "$INSTDIR\resources\LICENSE" "$INSTDIR\LICENSE"
DetailPrint "Copied LICENSE to install root"
; Optional cleanup - remove from resources folder
Delete "$INSTDIR\resources\LICENSE"
${Else}
DetailPrint "LICENSE not found at expected location: $INSTDIR\resources\LICENSE"
${EndIf}
; ---- Copy vulkan-1.dll to install root ----
${If} ${FileExists} "$INSTDIR\resources\lib\vulkan-1.dll"
CopyFiles /SILENT "$INSTDIR\resources\lib\vulkan-1.dll" "$INSTDIR\vulkan-1.dll"
DetailPrint "Copied vulkan-1.dll to install root"
; Optional cleanup - remove from resources folder
Delete "$INSTDIR\resources\lib\vulkan-1.dll"
; Only remove the lib directory if it's empty after removing both files
RMDir "$INSTDIR\resources\lib"
${Else}
DetailPrint "vulkan-1.dll not found at expected location: $INSTDIR\resources\lib\vulkan-1.dll"
${EndIf}
!macroend

View File

@ -82,7 +82,7 @@
"remark-math": "6.0.0",
"sonner": "2.0.5",
"tailwindcss": "4.1.4",
"token.js": "npm:token.js-fork@0.7.27",
"token.js": "npm:token.js-fork@0.7.29",
"tw-animate-css": "1.2.8",
"ulidx": "2.4.1",
"unified": "11.0.5",

View File

@ -96,7 +96,7 @@ export const predefinedProviders = [
{
active: true,
api_key: '',
base_url: 'https://api.anthropic.com',
base_url: 'https://api.anthropic.com/v1',
provider: 'anthropic',
explore_models_url:
'https://docs.anthropic.com/en/docs/about-claude/models',
@ -127,11 +127,21 @@ export const predefinedProviders = [
},
],
models: [],
custom_header: [
{
header: 'anthropic-version',
value: '2023-06-01'
},
{
header: 'anthropic-dangerous-direct-browser-access',
value: 'true'
}
]
},
{
active: true,
api_key: '',
base_url: 'https://api.cohere.ai/compatibility/v1',
base_url: 'https://api.cohere.ai/v1',
explore_models_url: 'https://docs.cohere.com/v2/docs/models',
provider: 'cohere',
settings: [

View File

@ -129,7 +129,10 @@ const ChatInput = ({
const activeModels = await serviceHub
.models()
.getActiveModels('llamacpp')
setHasActiveModels(activeModels.length > 0)
const hasMatchingActiveModel = activeModels.some(
(model) => String(model) === selectedModel?.id
)
setHasActiveModels(activeModels.length > 0 && hasMatchingActiveModel)
} catch (error) {
console.error('Failed to get active models:', error)
setHasActiveModels(false)
@ -142,7 +145,7 @@ const ChatInput = ({
const intervalId = setInterval(checkActiveModels, 3000)
return () => clearInterval(intervalId)
}, [serviceHub])
}, [serviceHub, selectedModel?.id])
// Check for mmproj existence or vision capability when model changes
useEffect(() => {

View File

@ -15,8 +15,7 @@ import { IconPlus } from '@tabler/icons-react'
import { useState } from 'react'
import { getProviderTitle } from '@/lib/utils'
import { useTranslation } from '@/i18n/react-i18next-compat'
import { ModelCapabilities } from '@/types/models'
import { models as providerModels } from 'token.js'
import { getModelCapabilities } from '@/lib/models'
import { toast } from 'sonner'
type DialogAddModelProps = {
@ -52,23 +51,7 @@ export const DialogAddModel = ({ provider, trigger }: DialogAddModelProps) => {
id: modelId,
model: modelId,
name: modelId,
capabilities: [
ModelCapabilities.COMPLETION,
(
providerModels[
provider.provider as unknown as keyof typeof providerModels
]?.supportsToolCalls as unknown as string[]
)?.includes(modelId)
? ModelCapabilities.TOOLS
: undefined,
(
providerModels[
provider.provider as unknown as keyof typeof providerModels
]?.supportsImages as unknown as string[]
)?.includes(modelId)
? ModelCapabilities.VISION
: undefined,
].filter(Boolean) as string[],
capabilities: getModelCapabilities(provider.provider, modelId),
version: '1.0',
}

View File

@ -320,9 +320,82 @@ export const useModelProvider = create<ModelProviderState>()(
})
}
if (version <= 3 && state?.providers) {
state.providers.forEach((provider) => {
// Migrate Anthropic provider base URL and add custom headers
if (provider.provider === 'anthropic') {
if (provider.base_url === 'https://api.anthropic.com') {
provider.base_url = 'https://api.anthropic.com/v1'
}
// Update base-url in settings
if (provider.settings) {
const baseUrlSetting = provider.settings.find(
(s) => s.key === 'base-url'
)
if (
baseUrlSetting?.controller_props?.value ===
'https://api.anthropic.com'
) {
baseUrlSetting.controller_props.value =
'https://api.anthropic.com/v1'
}
if (
baseUrlSetting?.controller_props?.placeholder ===
'https://api.anthropic.com'
) {
baseUrlSetting.controller_props.placeholder =
'https://api.anthropic.com/v1'
}
}
if (!provider.custom_header) {
provider.custom_header = [
{
header: 'anthropic-version',
value: '2023-06-01',
},
{
header: 'anthropic-dangerous-direct-browser-access',
value: 'true',
},
]
}
}
if (provider.provider === 'cohere') {
if (provider.base_url === 'https://api.cohere.ai/compatibility/v1') {
provider.base_url = 'https://api.cohere.ai/v1'
}
// Update base-url in settings
if (provider.settings) {
const baseUrlSetting = provider.settings.find(
(s) => s.key === 'base-url'
)
if (
baseUrlSetting?.controller_props?.value ===
'https://api.cohere.ai/compatibility/v1'
) {
baseUrlSetting.controller_props.value =
'https://api.cohere.ai/v1'
}
if (
baseUrlSetting?.controller_props?.placeholder ===
'https://api.cohere.ai/compatibility/v1'
) {
baseUrlSetting.controller_props.placeholder =
'https://api.cohere.ai/v1'
}
}
}
})
}
return state
},
version: 3,
version: 4,
}
)
)

View File

@ -5,19 +5,30 @@ import {
removeYamlFrontMatter,
extractModelName,
extractModelRepo,
getModelCapabilities,
} from '../models'
import { ModelCapabilities } from '@/types/models'
// Mock the token.js module
vi.mock('token.js', () => ({
models: {
openai: {
models: ['gpt-3.5-turbo', 'gpt-4'],
supportsToolCalls: ['gpt-3.5-turbo', 'gpt-4'],
supportsImages: ['gpt-4-vision-preview'],
},
anthropic: {
models: ['claude-3-sonnet', 'claude-3-haiku'],
supportsToolCalls: ['claude-3-sonnet'],
supportsImages: ['claude-3-sonnet', 'claude-3-haiku'],
},
mistral: {
models: ['mistral-7b', 'mistral-8x7b'],
supportsToolCalls: ['mistral-8x7b'],
},
// Provider with no capability arrays
cohere: {
models: ['command', 'command-light'],
},
},
}))
@ -223,3 +234,74 @@ describe('extractModelRepo', () => {
)
})
})
describe('getModelCapabilities', () => {
it('returns completion capability for all models', () => {
const capabilities = getModelCapabilities('openai', 'gpt-3.5-turbo')
expect(capabilities).toContain(ModelCapabilities.COMPLETION)
})
it('includes tools capability when model supports it', () => {
const capabilities = getModelCapabilities('openai', 'gpt-3.5-turbo')
expect(capabilities).toContain(ModelCapabilities.TOOLS)
expect(capabilities).toContain(ModelCapabilities.COMPLETION)
})
it('excludes tools capability when model does not support it', () => {
const capabilities = getModelCapabilities('mistral', 'mistral-7b')
expect(capabilities).not.toContain(ModelCapabilities.TOOLS)
expect(capabilities).toContain(ModelCapabilities.COMPLETION)
})
it('includes vision capability when model supports it', () => {
const capabilities = getModelCapabilities('openai', 'gpt-4-vision-preview')
expect(capabilities).toContain(ModelCapabilities.VISION)
expect(capabilities).toContain(ModelCapabilities.COMPLETION)
})
it('excludes vision capability when model does not support it', () => {
const capabilities = getModelCapabilities('openai', 'gpt-3.5-turbo')
expect(capabilities).not.toContain(ModelCapabilities.VISION)
})
it('includes both tools and vision when model supports both', () => {
const capabilities = getModelCapabilities('anthropic', 'claude-3-sonnet')
expect(capabilities).toContain(ModelCapabilities.COMPLETION)
expect(capabilities).toContain(ModelCapabilities.TOOLS)
expect(capabilities).toContain(ModelCapabilities.VISION)
})
it('handles provider with no capability arrays gracefully', () => {
const capabilities = getModelCapabilities('cohere', 'command')
expect(capabilities).toEqual([ModelCapabilities.COMPLETION])
expect(capabilities).not.toContain(ModelCapabilities.TOOLS)
expect(capabilities).not.toContain(ModelCapabilities.VISION)
})
it('handles unknown provider gracefully', () => {
const capabilities = getModelCapabilities('openrouter', 'some-model')
expect(capabilities).toEqual([ModelCapabilities.COMPLETION])
expect(capabilities).not.toContain(ModelCapabilities.TOOLS)
expect(capabilities).not.toContain(ModelCapabilities.VISION)
})
it('handles model not in capability list', () => {
const capabilities = getModelCapabilities('anthropic', 'claude-3-haiku')
expect(capabilities).toContain(ModelCapabilities.COMPLETION)
expect(capabilities).toContain(ModelCapabilities.VISION)
expect(capabilities).not.toContain(ModelCapabilities.TOOLS)
})
it('returns only completion for provider with partial capability data', () => {
// Mistral has supportsToolCalls but no supportsImages
const capabilities = getModelCapabilities('mistral', 'mistral-7b')
expect(capabilities).toEqual([ModelCapabilities.COMPLETION])
})
it('handles model that supports tools but not vision', () => {
const capabilities = getModelCapabilities('mistral', 'mistral-8x7b')
expect(capabilities).toContain(ModelCapabilities.COMPLETION)
expect(capabilities).toContain(ModelCapabilities.TOOLS)
expect(capabilities).not.toContain(ModelCapabilities.VISION)
})
})

View File

@ -1,4 +1,5 @@
import { models } from 'token.js'
import { ModelCapabilities } from '@/types/models'
export const defaultModel = (provider?: string) => {
if (!provider || !Object.keys(models).includes(provider)) {
@ -10,6 +11,38 @@ export const defaultModel = (provider?: string) => {
)[0]
}
/**
* Determines model capabilities based on provider configuration from token.js
* @param providerName - The provider name (e.g., 'openai', 'anthropic', 'openrouter')
* @param modelId - The model ID to check capabilities for
* @returns Array of model capabilities
*/
export const getModelCapabilities = (
providerName: string,
modelId: string
): string[] => {
const providerConfig =
models[providerName as unknown as keyof typeof models]
const supportsToolCalls = Array.isArray(
providerConfig?.supportsToolCalls as unknown
)
? (providerConfig.supportsToolCalls as unknown as string[])
: []
const supportsImages = Array.isArray(
providerConfig?.supportsImages as unknown
)
? (providerConfig.supportsImages as unknown as string[])
: []
return [
ModelCapabilities.COMPLETION,
supportsToolCalls.includes(modelId) ? ModelCapabilities.TOOLS : undefined,
supportsImages.includes(modelId) ? ModelCapabilities.VISION : undefined,
].filter(Boolean) as string[]
}
/**
* This utility is to extract cortexso model description from README.md file
* @returns

View File

@ -39,7 +39,7 @@ function ModelProviders() {
toast.error(t('providerAlreadyExists', { name }))
return
}
const newProvider = {
const newProvider: ProviderObject = {
provider: name,
active: true,
models: [],

View File

@ -578,6 +578,9 @@ export class DefaultModelsService implements ModelsService {
}
}>
}>
chat_template_kwargs?: {
enable_thinking: boolean
}
}) => Promise<number>
}
@ -654,6 +657,9 @@ export class DefaultModelsService implements ModelsService {
return await engine.getTokensCount({
model: modelId,
messages: transformedMessages,
chat_template_kwargs: {
enable_thinking: false,
},
})
}

View File

@ -10,6 +10,7 @@ import { modelSettings } from '@/lib/predefined'
import { ExtensionManager } from '@/lib/extension'
import { fetch as fetchTauri } from '@tauri-apps/plugin-http'
import { DefaultProvidersService } from './default'
import { getModelCapabilities } from '@/lib/models'
export class TauriProvidersService extends DefaultProvidersService {
fetch(): typeof fetch {
@ -26,32 +27,16 @@ export class TauriProvidersService extends DefaultProvidersService {
provider.provider as unknown as keyof typeof providerModels
].models as unknown as string[]
if (Array.isArray(builtInModels))
if (Array.isArray(builtInModels)) {
models = builtInModels.map((model) => {
const modelManifest = models.find((e) => e.id === model)
// TODO: Check chat_template for tool call support
const capabilities = [
ModelCapabilities.COMPLETION,
(
providerModels[
provider.provider as unknown as keyof typeof providerModels
]?.supportsToolCalls as unknown as string[]
)?.includes(model)
? ModelCapabilities.TOOLS
: undefined,
(
providerModels[
provider.provider as unknown as keyof typeof providerModels
]?.supportsImages as unknown as string[]
)?.includes(model)
? ModelCapabilities.VISION
: undefined,
].filter(Boolean) as string[]
return {
...(modelManifest ?? { id: model, name: model }),
capabilities,
capabilities: getModelCapabilities(provider.provider, model),
} as Model
})
}
}
return {
@ -166,6 +151,12 @@ export class TauriProvidersService extends DefaultProvidersService {
headers['Authorization'] = `Bearer ${provider.api_key}`
}
if (provider.custom_header) {
provider.custom_header.forEach((header) => {
headers[header.header] = header.value
})
}
// Always use Tauri's fetch to avoid CORS issues
const response = await fetchTauri(`${provider.base_url}/models`, {
method: 'GET',

View File

@ -11,6 +11,7 @@ import { ExtensionManager } from '@/lib/extension'
import type { ProvidersService } from './types'
import { PlatformFeatures } from '@/lib/platform/const'
import { PlatformFeature } from '@/lib/platform/types'
import { getModelCapabilities } from '@/lib/models'
export class WebProvidersService implements ProvidersService {
async getProviders(): Promise<ModelProvider[]> {
@ -88,19 +89,9 @@ export class WebProvidersService implements ProvidersService {
models = builtInModels.map((model) => {
const modelManifest = models.find((e) => e.id === model)
// TODO: Check chat_template for tool call support
const capabilities = [
ModelCapabilities.COMPLETION,
(
providerModels[
provider.provider as unknown as keyof typeof providerModels
]?.supportsToolCalls as unknown as string[]
)?.includes(model)
? ModelCapabilities.TOOLS
: undefined,
].filter(Boolean) as string[]
return {
...(modelManifest ?? { id: model, name: model }),
capabilities,
capabilities: getModelCapabilities(provider.provider, model),
} as Model
})
}

View File

@ -48,6 +48,7 @@ type ProviderObject = {
settings: ProviderSetting[]
models: Model[]
persist?: boolean
custom_header?: ProviderCustomHeader[] | null
}
/**
@ -71,3 +72,8 @@ type ProxyOptions = {
verifyHostSSL: boolean
noProxy: string
}
type ProviderCustomHeader = {
header: string
value: string
}