mirror of
https://github.com/zadam/trilium.git
synced 2025-11-11 15:55:52 +01:00
Merge remote-tracking branch 'origin/develop' into fix/show-warning-when-rosetta-2
This commit is contained in:
2
apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
generated
vendored
2
apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
generated
vendored
File diff suppressed because one or more lines are too long
@@ -46,7 +46,7 @@
|
||||
variable to something larger than the integer <code>250</code> (e.g. <code>450</code> in
|
||||
the following example):</p><pre><code class="language-text-x-trilium-auto">export MAX_ALLOWED_FILE_SIZE_MB=450</code></pre>
|
||||
<h3>Disabling Authentication</h3>
|
||||
<p>See <a class="reference-link" href="#root/pOsGYCXsbNQG/Otzi9La2YAUX/_help_0hzsNCP31IAB">Authentication</a>.</p>
|
||||
<p>See <a class="reference-link" href="#root/_help_0hzsNCP31IAB">Authentication</a>.</p>
|
||||
<h2>Reverse Proxy Setup</h2>
|
||||
<p>To configure a reverse proxy for Trilium, you can use either <strong>nginx</strong> or <strong>Apache</strong>.
|
||||
You can also check out the documentation stored in the Reverse proxy folder.</p>
|
||||
|
||||
@@ -10,7 +10,14 @@ vim default.conf</code></pre>
|
||||
</li>
|
||||
<li>
|
||||
<p>Fill the file with the context shown below, part of the setting show be
|
||||
changed. Then you can enjoy your web with HTTPS forced and proxy.</p><pre><code class="language-text-x-trilium-auto"># This part is for proxy and HTTPS configure
|
||||
changed. Then you can enjoy your web with HTTPS forced and proxy.</p><pre><code class="language-text-x-trilium-auto"># This part configures, where your Trilium server is running
|
||||
upstream trilium {
|
||||
zone trilium 64k;
|
||||
server 127.0.0.1:8080; # change it to a different hostname and port if non-default is used
|
||||
keepalive 2;
|
||||
}
|
||||
|
||||
# This part is for proxy and HTTPS configure
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name trilium.example.net; #change trilium.example.net to your domain without HTTPS or HTTP.
|
||||
@@ -29,9 +36,8 @@ server {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_pass http://127.0.0.1:8080; # change it to a different port if non-default is used
|
||||
proxy_pass http://trilium;
|
||||
proxy_read_timeout 90;
|
||||
proxy_redirect http://127.0.0.1:8080 https://trilium.example.net; # change them based on your IP, port and domain
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,16 +58,16 @@ server {
|
||||
<li>add the <code>proxy_cookie_path</code> directive with the same path: this
|
||||
allows you to stay logged in at multiple instances at the same time.</li>
|
||||
</ul><pre><code class="language-text-x-trilium-auto"> location /trilium/instance-one {
|
||||
rewrite /trilium/instance-one/(.*) /$1 break;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_pass http://127.0.0.1:8080; # change it to a different port if non-default is used
|
||||
proxy_pass http://trilium;
|
||||
proxy_cookie_path / /trilium/instance-one
|
||||
proxy_read_timeout 90;
|
||||
proxy_redirect http://127.0.0.1:8080 https://trilium.example.net; # change them based on your IP, port and domain
|
||||
}
|
||||
</code></pre>
|
||||
</li>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
by adding the following to <code>config.ini</code>:</p><pre><code class="language-text-x-trilium-auto">[General]
|
||||
noAuthentication=true</code></pre>
|
||||
<p>Disabling authentication will bypass even the <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/Otzi9La2YAUX/WOcw2SLH6tbX/_help_7DAiwaf8Z7Rz">Multi-Factor Authentication</a> since
|
||||
href="#root/_help_7DAiwaf8Z7Rz">Multi-Factor Authentication</a> since
|
||||
v0.94.1.</p>
|
||||
<h2>Understanding how the session works</h2>
|
||||
<p>Once logged into Trilium, the application will store this information
|
||||
@@ -22,14 +22,14 @@ cookieMaxAge=86400</code></pre>
|
||||
the <em>last interaction with the application</em>.</p>
|
||||
<h2>Viewing active sessions</h2>
|
||||
<p>The login sessions are now stored in the same <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/tC7s2alapj8V/_help_wX4HbRucYSDD">Database</a> as
|
||||
the user data. In order to view which sessions are active, open the
|
||||
<a
|
||||
class="reference-link" href="#root/pOsGYCXsbNQG/tC7s2alapj8V/wX4HbRucYSDD/oyIAJ9PvvwHX/_help_YKWqdJhzi2VY">SQL Console</a> and run the following query:</p><pre><code class="language-text-x-sqlite-schema-trilium">SELECT * FROM sessions</code></pre>
|
||||
href="#root/_help_wX4HbRucYSDD">Database</a> as the user data. In
|
||||
order to view which sessions are active, open the <a class="reference-link"
|
||||
href="#root/_help_YKWqdJhzi2VY">SQL Console</a> and run the following
|
||||
query:</p><pre><code class="language-text-x-trilium-auto">SELECT * FROM sessions</code></pre>
|
||||
<p>Expired sessions are periodically cleaned by the server, generally an
|
||||
hourly interval.</p>
|
||||
<h2>See also</h2>
|
||||
<ul>
|
||||
<li><a class="reference-link" href="#root/pOsGYCXsbNQG/Otzi9La2YAUX/WOcw2SLH6tbX/_help_7DAiwaf8Z7Rz">Multi-Factor Authentication</a>
|
||||
<li><a class="reference-link" href="#root/_help_7DAiwaf8Z7Rz">Multi-Factor Authentication</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -41,10 +41,6 @@ class="admonition warning">
|
||||
the page).</li>
|
||||
</ol>
|
||||
<h3>OpenID</h3>
|
||||
<aside class="admonition note">
|
||||
<p>Currently only compatible with Google. Other services like Authentik and
|
||||
Auth0 are planned on being added.</p>
|
||||
</aside>
|
||||
<p>In order to setup OpenID, you will need to setup a authentication provider.
|
||||
This requires a bit of extra setup. Follow <a href="https://developers.google.com/identity/openid-connect/openid-connect">these instructions</a> to
|
||||
setup an OpenID service through google.</p>
|
||||
@@ -61,4 +57,12 @@ class="admonition warning">
|
||||
<li>Click the “Enable Multi-Factor Authentication” checkbox if not checked</li>
|
||||
<li>Choose “OAuth/OpenID” under MFA Method</li>
|
||||
<li>Refresh the page and login through OpenID provider</li>
|
||||
</ol>
|
||||
</ol>
|
||||
<aside class="admonition note">
|
||||
<p>The default OAuth issuer is Google. To use other services such as Authentik
|
||||
or Auth0, you can configure the settings via <code>oauthIssuerBaseUrl</code>, <code>oauthIssuerName</code>,
|
||||
and <code>oauthIssuerIcon</code> in the <code>config.ini</code> file. Alternatively,
|
||||
these values can be set using environment variables: <code>TRILIUM_OAUTH_ISSUER_BASE_URL</code>, <code>TRILIUM_OAUTH_ISSUER_NAME</code>,
|
||||
and <code>TRILIUM_OAUTH_ISSUER_ICON</code>. <code>oauthIssuerName</code> and <code>oauthIssuerIcon</code> are
|
||||
required for displaying correct issuer information at the Login page.</p>
|
||||
</aside>
|
||||
@@ -135,7 +135,8 @@ body.electron:not(.native-titlebar) {
|
||||
<h2>Custom fonts</h2>
|
||||
<p>Currently the only way to include a custom font is to use <a href="#root/_help_d3fAXQ2diepH">Custom resource providers</a>.
|
||||
Basically import a font into Trilium and assign it <code>#customResourceProvider=fonts/myfont.ttf</code> and
|
||||
then import the font in CSS via <code>/custom/fonts/myfont.ttf</code>.</p>
|
||||
then import the font in CSS via <code>/custom/fonts/myfont.ttf</code>. Use <code>../../../custom/fonts/myfont.ttf</code> if
|
||||
you run your Trilium server on a different path than <code>/</code>.</p>
|
||||
<h2>Dark and light themes</h2>
|
||||
<p>A light theme needs to have the following CSS:</p><pre><code class="language-text-css">:root {
|
||||
--theme-style: light;
|
||||
|
||||
@@ -48,7 +48,7 @@ import OpenAI from "openai";
|
||||
*/
|
||||
async function listModels(req: Request, res: Response) {
|
||||
try {
|
||||
const { baseUrl } = req.body;
|
||||
const { baseUrl } = req.body ?? {};
|
||||
|
||||
// Use provided base URL or default from options
|
||||
const openaiBaseUrl = baseUrl || await options.getOption('openaiBaseUrl') || 'https://api.openai.com/v1';
|
||||
|
||||
@@ -118,7 +118,7 @@ function getRelationBundles(req: Request) {
|
||||
|
||||
function getBundle(req: Request) {
|
||||
const note = becca.getNoteOrThrow(req.params.noteId);
|
||||
const { script, params } = req.body;
|
||||
const { script, params } = req.body ?? {};
|
||||
|
||||
return scriptService.getScriptBundleForFrontend(note, script, params);
|
||||
}
|
||||
|
||||
@@ -94,6 +94,83 @@ describe('configuration_helpers', () => {
|
||||
fullIdentifier: ''
|
||||
});
|
||||
});
|
||||
|
||||
// Tests for special characters in model names
|
||||
it('should handle model names with periods', () => {
|
||||
const result = configHelpers.parseModelIdentifier('gpt-4.1-turbo-preview');
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
modelId: 'gpt-4.1-turbo-preview',
|
||||
fullIdentifier: 'gpt-4.1-turbo-preview'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle model names with provider prefix and periods', () => {
|
||||
const result = configHelpers.parseModelIdentifier('openai:gpt-4.1-turbo');
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
provider: 'openai',
|
||||
modelId: 'gpt-4.1-turbo',
|
||||
fullIdentifier: 'openai:gpt-4.1-turbo'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle model names with multiple colons', () => {
|
||||
const result = configHelpers.parseModelIdentifier('custom:model:v1.2:latest');
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
modelId: 'custom:model:v1.2:latest',
|
||||
fullIdentifier: 'custom:model:v1.2:latest'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle Ollama model names with colons', () => {
|
||||
const result = configHelpers.parseModelIdentifier('ollama:llama3.1:70b-instruct-q4_K_M');
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
provider: 'ollama',
|
||||
modelId: 'llama3.1:70b-instruct-q4_K_M',
|
||||
fullIdentifier: 'ollama:llama3.1:70b-instruct-q4_K_M'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle model names with slashes', () => {
|
||||
const result = configHelpers.parseModelIdentifier('library/mistral:7b-instruct');
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
modelId: 'library/mistral:7b-instruct',
|
||||
fullIdentifier: 'library/mistral:7b-instruct'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle complex model names with special characters', () => {
|
||||
const complexName = 'org/model-v1.2.3:tag@version#variant';
|
||||
const result = configHelpers.parseModelIdentifier(complexName);
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
modelId: complexName,
|
||||
fullIdentifier: complexName
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle model names with @ symbols', () => {
|
||||
const result = configHelpers.parseModelIdentifier('claude-3.5-sonnet@20241022');
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
modelId: 'claude-3.5-sonnet@20241022',
|
||||
fullIdentifier: 'claude-3.5-sonnet@20241022'
|
||||
});
|
||||
});
|
||||
|
||||
it('should not modify or encode special characters', () => {
|
||||
const specialChars = 'model!@#$%^&*()_+-=[]{}|;:\'",.<>?/~`';
|
||||
const result = configHelpers.parseModelIdentifier(specialChars);
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
modelId: specialChars,
|
||||
fullIdentifier: specialChars
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createModelConfig', () => {
|
||||
@@ -155,6 +232,34 @@ describe('configuration_helpers', () => {
|
||||
expect(result).toBe('llama2');
|
||||
expect(optionService.getOption).toHaveBeenCalledWith('ollamaDefaultModel');
|
||||
});
|
||||
|
||||
// Tests for special characters in model names
|
||||
it('should handle OpenAI model names with periods', async () => {
|
||||
const modelName = 'gpt-4.1-turbo-preview';
|
||||
vi.mocked(optionService.getOption).mockReturnValue(modelName);
|
||||
|
||||
const result = await configHelpers.getDefaultModelForProvider('openai');
|
||||
|
||||
expect(result).toBe(modelName);
|
||||
});
|
||||
|
||||
it('should handle Anthropic model names with periods and @ symbols', async () => {
|
||||
const modelName = 'claude-3.5-sonnet@20241022';
|
||||
vi.mocked(optionService.getOption).mockReturnValue(modelName);
|
||||
|
||||
const result = await configHelpers.getDefaultModelForProvider('anthropic');
|
||||
|
||||
expect(result).toBe(modelName);
|
||||
});
|
||||
|
||||
it('should handle Ollama model names with colons and slashes', async () => {
|
||||
const modelName = 'library/llama3.1:70b-instruct-q4_K_M';
|
||||
vi.mocked(optionService.getOption).mockReturnValue(modelName);
|
||||
|
||||
const result = await configHelpers.getDefaultModelForProvider('ollama');
|
||||
|
||||
expect(result).toBe(modelName);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProviderSettings', () => {
|
||||
@@ -381,4 +486,122 @@ describe('configuration_helpers', () => {
|
||||
expect(() => configHelpers.clearConfigurationCache()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getValidModelConfig', () => {
|
||||
it('should handle model names with special characters', async () => {
|
||||
const modelName = 'gpt-4.1-turbo@latest';
|
||||
vi.mocked(optionService.getOption)
|
||||
.mockReturnValueOnce(modelName) // openaiDefaultModel
|
||||
.mockReturnValueOnce('test-key') // openaiApiKey
|
||||
.mockReturnValueOnce('') // openaiBaseUrl
|
||||
.mockReturnValueOnce(''); // openaiDefaultModel
|
||||
|
||||
const result = await configHelpers.getValidModelConfig('openai');
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
model: modelName,
|
||||
provider: 'openai'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle Anthropic model with complex naming', async () => {
|
||||
const modelName = 'claude-3.5-sonnet-20241022';
|
||||
vi.mocked(optionService.getOption)
|
||||
.mockReturnValueOnce(modelName) // anthropicDefaultModel
|
||||
.mockReturnValueOnce('anthropic-key') // anthropicApiKey
|
||||
.mockReturnValueOnce('') // anthropicBaseUrl
|
||||
.mockReturnValueOnce(''); // anthropicDefaultModel
|
||||
|
||||
const result = await configHelpers.getValidModelConfig('anthropic');
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
model: modelName,
|
||||
provider: 'anthropic'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle Ollama model with colons', async () => {
|
||||
const modelName = 'custom/llama3.1:70b-q4_K_M@latest';
|
||||
vi.mocked(optionService.getOption)
|
||||
.mockReturnValueOnce(modelName) // ollamaDefaultModel
|
||||
.mockReturnValueOnce('http://localhost:11434') // ollamaBaseUrl
|
||||
.mockReturnValueOnce(''); // ollamaDefaultModel
|
||||
|
||||
const result = await configHelpers.getValidModelConfig('ollama');
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
model: modelName,
|
||||
provider: 'ollama'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSelectedModelConfig', () => {
|
||||
it('should preserve OpenAI model names with special characters', async () => {
|
||||
const modelName = 'gpt-4.1-turbo-preview@2024';
|
||||
vi.mocked(optionService.getOption)
|
||||
.mockReturnValueOnce('openai') // aiSelectedProvider
|
||||
.mockReturnValueOnce(modelName) // openaiDefaultModel
|
||||
.mockReturnValueOnce('test-key') // openaiApiKey
|
||||
.mockReturnValueOnce('') // openaiBaseUrl
|
||||
.mockReturnValueOnce(''); // openaiDefaultModel
|
||||
|
||||
const result = await configHelpers.getSelectedModelConfig();
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
model: modelName,
|
||||
provider: 'openai'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle model names with URL-like patterns', async () => {
|
||||
const modelName = 'https://models.example.com/gpt-4.1';
|
||||
vi.mocked(optionService.getOption)
|
||||
.mockReturnValueOnce('openai') // aiSelectedProvider
|
||||
.mockReturnValueOnce(modelName) // openaiDefaultModel
|
||||
.mockReturnValueOnce('test-key') // openaiApiKey
|
||||
.mockReturnValueOnce('') // openaiBaseUrl
|
||||
.mockReturnValueOnce(''); // openaiDefaultModel
|
||||
|
||||
const result = await configHelpers.getSelectedModelConfig();
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
model: modelName,
|
||||
provider: 'openai'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle model names that look like file paths', async () => {
|
||||
const modelName = '/models/custom/gpt-4.1.safetensors';
|
||||
vi.mocked(optionService.getOption)
|
||||
.mockReturnValueOnce('ollama') // aiSelectedProvider
|
||||
.mockReturnValueOnce(modelName) // ollamaDefaultModel
|
||||
.mockReturnValueOnce('http://localhost:11434') // ollamaBaseUrl
|
||||
.mockReturnValueOnce(''); // ollamaDefaultModel
|
||||
|
||||
const result = await configHelpers.getSelectedModelConfig();
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
model: modelName,
|
||||
provider: 'ollama'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle model names with all possible special characters', async () => {
|
||||
const modelName = 'model!@#$%^&*()_+-=[]{}|;:\'",.<>?/~`';
|
||||
vi.mocked(optionService.getOption)
|
||||
.mockReturnValueOnce('anthropic') // aiSelectedProvider
|
||||
.mockReturnValueOnce(modelName) // anthropicDefaultModel
|
||||
.mockReturnValueOnce('test-key') // anthropicApiKey
|
||||
.mockReturnValueOnce('') // anthropicBaseUrl
|
||||
.mockReturnValueOnce(''); // anthropicDefaultModel
|
||||
|
||||
const result = await configHelpers.getSelectedModelConfig();
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
model: modelName,
|
||||
provider: 'anthropic'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
389
apps/server/src/services/llm/providers/model_selection.spec.ts
Normal file
389
apps/server/src/services/llm/providers/model_selection.spec.ts
Normal file
@@ -0,0 +1,389 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { OpenAIService } from './openai_service.js';
|
||||
import { AnthropicService } from './anthropic_service.js';
|
||||
import { OllamaService } from './ollama_service.js';
|
||||
import type { ChatCompletionOptions } from '../ai_interface.js';
|
||||
import * as providers from './providers.js';
|
||||
import options from '../../options.js';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('../../options.js', () => ({
|
||||
default: {
|
||||
getOption: vi.fn(),
|
||||
getOptionBool: vi.fn()
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('../../log.js', () => ({
|
||||
default: {
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn()
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('openai', () => ({
|
||||
default: class MockOpenAI {
|
||||
chat = {
|
||||
completions: {
|
||||
create: vi.fn()
|
||||
}
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('@anthropic-ai/sdk', () => ({
|
||||
default: class MockAnthropic {
|
||||
messages = {
|
||||
create: vi.fn()
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('ollama', () => ({
|
||||
Ollama: class MockOllama {
|
||||
chat = vi.fn();
|
||||
show = vi.fn();
|
||||
}
|
||||
}));
|
||||
|
||||
describe('LLM Model Selection with Special Characters', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Set default options
|
||||
vi.mocked(options.getOption).mockImplementation((key: string) => {
|
||||
const optionMap: Record<string, string> = {
|
||||
'aiEnabled': 'true',
|
||||
'aiTemperature': '0.7',
|
||||
'aiSystemPrompt': 'You are a helpful assistant.',
|
||||
'openaiApiKey': 'test-api-key',
|
||||
'openaiBaseUrl': 'https://api.openai.com/v1',
|
||||
'anthropicApiKey': 'test-anthropic-key',
|
||||
'anthropicBaseUrl': 'https://api.anthropic.com',
|
||||
'ollamaBaseUrl': 'http://localhost:11434'
|
||||
};
|
||||
return optionMap[key] || '';
|
||||
});
|
||||
vi.mocked(options.getOptionBool).mockReturnValue(true);
|
||||
});
|
||||
|
||||
describe('OpenAI Model Names', () => {
|
||||
it('should correctly handle model names with periods', async () => {
|
||||
const modelName = 'gpt-4.1-turbo-preview';
|
||||
vi.mocked(options.getOption).mockImplementation((key: string) => {
|
||||
if (key === 'openaiDefaultModel') return modelName;
|
||||
return '';
|
||||
});
|
||||
|
||||
const service = new OpenAIService();
|
||||
const opts: ChatCompletionOptions = {
|
||||
stream: false
|
||||
};
|
||||
|
||||
// Spy on getOpenAIOptions to verify model name is passed correctly
|
||||
const getOpenAIOptionsSpy = vi.spyOn(providers, 'getOpenAIOptions');
|
||||
|
||||
try {
|
||||
await service.generateChatCompletion([{ role: 'user', content: 'test' }], opts);
|
||||
} catch (error) {
|
||||
// Expected to fail due to mocked API
|
||||
}
|
||||
|
||||
expect(getOpenAIOptionsSpy).toHaveBeenCalledWith(opts);
|
||||
const result = getOpenAIOptionsSpy.mock.results[0].value;
|
||||
expect(result.model).toBe(modelName);
|
||||
});
|
||||
|
||||
it('should handle model names with slashes', async () => {
|
||||
const modelName = 'openai/gpt-4/turbo-2024';
|
||||
vi.mocked(options.getOption).mockImplementation((key: string) => {
|
||||
if (key === 'openaiDefaultModel') return modelName;
|
||||
return '';
|
||||
});
|
||||
|
||||
const service = new OpenAIService();
|
||||
const opts: ChatCompletionOptions = {
|
||||
model: modelName,
|
||||
stream: false
|
||||
};
|
||||
|
||||
const getOpenAIOptionsSpy = vi.spyOn(providers, 'getOpenAIOptions');
|
||||
|
||||
try {
|
||||
await service.generateChatCompletion([{ role: 'user', content: 'test' }], opts);
|
||||
} catch (error) {
|
||||
// Expected to fail due to mocked API
|
||||
}
|
||||
|
||||
const result = getOpenAIOptionsSpy.mock.results[0].value;
|
||||
expect(result.model).toBe(modelName);
|
||||
});
|
||||
|
||||
it('should handle model names with colons', async () => {
|
||||
const modelName = 'custom:gpt-4:finetuned';
|
||||
const opts: ChatCompletionOptions = {
|
||||
model: modelName,
|
||||
stream: false
|
||||
};
|
||||
|
||||
const getOpenAIOptionsSpy = vi.spyOn(providers, 'getOpenAIOptions');
|
||||
|
||||
const openaiOptions = providers.getOpenAIOptions(opts);
|
||||
expect(openaiOptions.model).toBe(modelName);
|
||||
});
|
||||
|
||||
it('should handle model names with underscores and hyphens', async () => {
|
||||
const modelName = 'gpt-4_turbo-preview_v2.1';
|
||||
const opts: ChatCompletionOptions = {
|
||||
model: modelName,
|
||||
stream: false
|
||||
};
|
||||
|
||||
const openaiOptions = providers.getOpenAIOptions(opts);
|
||||
expect(openaiOptions.model).toBe(modelName);
|
||||
});
|
||||
|
||||
it('should handle model names with special characters in API request', async () => {
|
||||
const modelName = 'gpt-4.1-turbo@latest';
|
||||
vi.mocked(options.getOption).mockImplementation((key: string) => {
|
||||
if (key === 'openaiDefaultModel') return modelName;
|
||||
if (key === 'openaiApiKey') return 'test-key';
|
||||
if (key === 'openaiBaseUrl') return 'https://api.openai.com/v1';
|
||||
return '';
|
||||
});
|
||||
|
||||
const service = new OpenAIService();
|
||||
|
||||
// Access the private openai client through the service
|
||||
const client = (service as any).getClient('test-key');
|
||||
const createSpy = vi.spyOn(client.chat.completions, 'create');
|
||||
|
||||
try {
|
||||
await service.generateChatCompletion(
|
||||
[{ role: 'user', content: 'test' }],
|
||||
{ stream: false }
|
||||
);
|
||||
} catch (error) {
|
||||
// Expected due to mock
|
||||
}
|
||||
|
||||
expect(createSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
model: modelName
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Anthropic Model Names', () => {
|
||||
it('should correctly handle Anthropic model names with periods', async () => {
|
||||
const modelName = 'claude-3.5-sonnet-20241022';
|
||||
vi.mocked(options.getOption).mockImplementation((key: string) => {
|
||||
if (key === 'anthropicDefaultModel') return modelName;
|
||||
if (key === 'anthropicApiKey') return 'test-key';
|
||||
return '';
|
||||
});
|
||||
|
||||
const opts: ChatCompletionOptions = {
|
||||
stream: false
|
||||
};
|
||||
|
||||
const anthropicOptions = providers.getAnthropicOptions(opts);
|
||||
expect(anthropicOptions.model).toBe(modelName);
|
||||
});
|
||||
|
||||
it('should handle Anthropic model names with colons', async () => {
|
||||
const modelName = 'anthropic:claude-3:opus';
|
||||
const opts: ChatCompletionOptions = {
|
||||
model: modelName,
|
||||
stream: false
|
||||
};
|
||||
|
||||
const anthropicOptions = providers.getAnthropicOptions(opts);
|
||||
expect(anthropicOptions.model).toBe(modelName);
|
||||
});
|
||||
|
||||
it('should handle Anthropic model names in API request', async () => {
|
||||
const modelName = 'claude-3.5-sonnet@beta';
|
||||
vi.mocked(options.getOption).mockImplementation((key: string) => {
|
||||
if (key === 'anthropicDefaultModel') return modelName;
|
||||
if (key === 'anthropicApiKey') return 'test-key';
|
||||
if (key === 'anthropicBaseUrl') return 'https://api.anthropic.com';
|
||||
return '';
|
||||
});
|
||||
|
||||
const service = new AnthropicService();
|
||||
|
||||
// Access the private anthropic client
|
||||
const client = (service as any).getClient('test-key');
|
||||
const createSpy = vi.spyOn(client.messages, 'create');
|
||||
|
||||
try {
|
||||
await service.generateChatCompletion(
|
||||
[{ role: 'user', content: 'test' }],
|
||||
{ stream: false }
|
||||
);
|
||||
} catch (error) {
|
||||
// Expected due to mock
|
||||
}
|
||||
|
||||
expect(createSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
model: modelName
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ollama Model Names', () => {
|
||||
it('should correctly handle Ollama model names with colons', async () => {
|
||||
const modelName = 'llama3.1:70b-instruct-q4_K_M';
|
||||
vi.mocked(options.getOption).mockImplementation((key: string) => {
|
||||
if (key === 'ollamaDefaultModel') return modelName;
|
||||
if (key === 'ollamaBaseUrl') return 'http://localhost:11434';
|
||||
return '';
|
||||
});
|
||||
|
||||
const opts: ChatCompletionOptions = {
|
||||
stream: false
|
||||
};
|
||||
|
||||
const ollamaOptions = await providers.getOllamaOptions(opts);
|
||||
expect(ollamaOptions.model).toBe(modelName);
|
||||
});
|
||||
|
||||
it('should handle Ollama model names with slashes', async () => {
|
||||
const modelName = 'library/mistral:7b-instruct-v0.3';
|
||||
const opts: ChatCompletionOptions = {
|
||||
model: modelName,
|
||||
stream: false
|
||||
};
|
||||
|
||||
const ollamaOptions = await providers.getOllamaOptions(opts);
|
||||
expect(ollamaOptions.model).toBe(modelName);
|
||||
});
|
||||
|
||||
it('should handle Ollama model names with special characters in options', async () => {
|
||||
const modelName = 'custom/llama3.1:70b-q4_K_M@latest';
|
||||
vi.mocked(options.getOption).mockImplementation((key: string) => {
|
||||
if (key === 'ollamaDefaultModel') return modelName;
|
||||
if (key === 'ollamaBaseUrl') return 'http://localhost:11434';
|
||||
return '';
|
||||
});
|
||||
|
||||
// Test that the model name is preserved in the options
|
||||
const opts: ChatCompletionOptions = {
|
||||
stream: false
|
||||
};
|
||||
|
||||
const ollamaOptions = await providers.getOllamaOptions(opts);
|
||||
expect(ollamaOptions.model).toBe(modelName);
|
||||
|
||||
// Also test with model specified in options
|
||||
const optsWithModel: ChatCompletionOptions = {
|
||||
model: 'another/model:v2.0@beta',
|
||||
stream: false
|
||||
};
|
||||
|
||||
const ollamaOptionsWithModel = await providers.getOllamaOptions(optsWithModel);
|
||||
expect(ollamaOptionsWithModel.model).toBe('another/model:v2.0@beta');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Model Name Edge Cases', () => {
|
||||
it('should handle empty model names gracefully', () => {
|
||||
const opts: ChatCompletionOptions = {
|
||||
model: '',
|
||||
stream: false
|
||||
};
|
||||
|
||||
expect(() => providers.getOpenAIOptions(opts)).toThrow('No OpenAI model configured');
|
||||
});
|
||||
|
||||
it('should handle model names with unicode characters', async () => {
|
||||
const modelName = 'gpt-4-日本語-model';
|
||||
const opts: ChatCompletionOptions = {
|
||||
model: modelName,
|
||||
stream: false
|
||||
};
|
||||
|
||||
const openaiOptions = providers.getOpenAIOptions(opts);
|
||||
expect(openaiOptions.model).toBe(modelName);
|
||||
});
|
||||
|
||||
it('should handle model names with spaces (encoded)', async () => {
|
||||
const modelName = 'custom model v2.1';
|
||||
const opts: ChatCompletionOptions = {
|
||||
model: modelName,
|
||||
stream: false
|
||||
};
|
||||
|
||||
const openaiOptions = providers.getOpenAIOptions(opts);
|
||||
expect(openaiOptions.model).toBe(modelName);
|
||||
});
|
||||
|
||||
it('should preserve exact model name without transformation', async () => {
|
||||
const complexModelName = 'org/model-v1.2.3:tag@version#variant';
|
||||
const opts: ChatCompletionOptions = {
|
||||
model: complexModelName,
|
||||
stream: false
|
||||
};
|
||||
|
||||
// Test for all providers
|
||||
const openaiOptions = providers.getOpenAIOptions(opts);
|
||||
expect(openaiOptions.model).toBe(complexModelName);
|
||||
|
||||
const anthropicOptions = providers.getAnthropicOptions(opts);
|
||||
expect(anthropicOptions.model).toBe(complexModelName);
|
||||
|
||||
const ollamaOptions = await providers.getOllamaOptions(opts);
|
||||
expect(ollamaOptions.model).toBe(complexModelName);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Model Configuration Parsing', () => {
|
||||
it('should not confuse provider prefix with model name containing colons', async () => {
|
||||
// This model name has a colon but 'custom' is not a known provider
|
||||
const modelName = 'custom:model:v1.2';
|
||||
const opts: ChatCompletionOptions = {
|
||||
model: modelName,
|
||||
stream: false
|
||||
};
|
||||
|
||||
const openaiOptions = providers.getOpenAIOptions(opts);
|
||||
expect(openaiOptions.model).toBe(modelName);
|
||||
});
|
||||
|
||||
it('should handle provider prefix correctly', async () => {
|
||||
// When model has provider prefix, it should still use the full string
|
||||
const modelName = 'openai:gpt-4.1-turbo';
|
||||
const opts: ChatCompletionOptions = {
|
||||
model: modelName,
|
||||
stream: false
|
||||
};
|
||||
|
||||
const openaiOptions = providers.getOpenAIOptions(opts);
|
||||
expect(openaiOptions.model).toBe(modelName);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Integration with REST API', () => {
|
||||
it('should pass model names correctly through REST chat service', async () => {
|
||||
const modelName = 'gpt-4.1-turbo-preview@latest';
|
||||
|
||||
// Mock the configuration helpers
|
||||
vi.doMock('../config/configuration_helpers.js', () => ({
|
||||
getSelectedModelConfig: vi.fn().mockResolvedValue({
|
||||
model: modelName,
|
||||
provider: 'openai'
|
||||
}),
|
||||
isAIEnabled: vi.fn().mockResolvedValue(true)
|
||||
}));
|
||||
|
||||
const { getSelectedModelConfig } = await import('../config/configuration_helpers.js');
|
||||
const config = await getSelectedModelConfig();
|
||||
|
||||
expect(config?.model).toBe(modelName);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user