feat: support course-specific Qwen endpoints

This commit is contained in:
2026-10-14 15:12:08 +09:00
parent e3f0073124
commit f43ecd0f2a
8 changed files with 38 additions and 5 deletions
+1
View File
@@ -66,6 +66,7 @@ export class AgentService {
provider: setting.provider as AiProvider,
model: setting.model,
apiKey: this.credentialService.decrypt(setting.encryptedApiKey),
endpointUrl: setting.endpointUrl,
json: true,
system: this.systemPrompt(),
messages: [{
+4 -3
View File
@@ -10,7 +10,7 @@ describe('AiProviderService', () => {
jest.restoreAllMocks();
});
it('calls the Qwen OpenAI-compatible chat endpoint', async () => {
it('prefers the course Qwen endpoint over the server default', async () => {
const fetchMock = jest.fn().mockResolvedValue({
ok: true,
status: 200,
@@ -20,20 +20,21 @@ describe('AiProviderService', () => {
}),
});
global.fetch = fetchMock as typeof fetch;
const config = { get: jest.fn().mockReturnValue('https://example.test/compatible-mode/v1') } as unknown as ConfigService;
const config = { get: jest.fn().mockReturnValue('https://server-default.test/compatible-mode/v1') } as unknown as ConfigService;
const service = new AiProviderService(config);
const result = await service.complete({
provider: 'QWEN',
model: 'qwen3.7-plus',
apiKey: 'qwen-secret',
endpointUrl: 'https://course-endpoint.test/compatible-mode/v1',
system: 'system prompt',
messages: [{ role: 'user', content: 'hello' }],
json: true,
});
expect(result.content).toBe('{"ok":true}');
expect(fetchMock).toHaveBeenCalledWith('https://example.test/compatible-mode/v1/chat/completions', expect.objectContaining({
expect(fetchMock).toHaveBeenCalledWith('https://course-endpoint.test/compatible-mode/v1/chat/completions', expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({ Authorization: 'Bearer qwen-secret' }),
}));
+2 -1
View File
@@ -7,6 +7,7 @@ type CompletionInput = {
provider: AiProvider;
model: string;
apiKey: string;
endpointUrl?: string | null;
system: string;
messages: Array<{ role: 'user' | 'assistant'; content: string }>;
json?: boolean;
@@ -105,7 +106,7 @@ export class AiProviderService {
}
private async qwen(input: CompletionInput): Promise<AiCompletionResult> {
const configuredUrl = this.configService.get<string>('QWEN_API_BASE_URL')?.trim();
const configuredUrl = input.endpointUrl?.trim() || this.configService.get<string>('QWEN_API_BASE_URL')?.trim();
const normalizedUrl = configuredUrl?.replace(/\/+$/, '');
const endpoint = normalizedUrl
? `${normalizedUrl}${normalizedUrl.endsWith('/chat/completions') ? '' : '/chat/completions'}`
@@ -14,6 +14,11 @@ export class UpdateCourseAiSettingDto {
@MaxLength(120)
model: string;
@IsOptional()
@IsString()
@MaxLength(500)
endpointUrl?: string;
@IsOptional()
@IsString()
@MaxLength(500)