Compare commits
1 Commits
feat/201-t
...
fe1bbbe806
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe1bbbe806 |
@@ -285,3 +285,43 @@ describe('AiService.getChatModel role model override', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Provider selection for the `openai` driver (reasoning surfacing). A custom
|
||||
* baseURL means an openai-COMPATIBLE third-party endpoint (z.ai/GLM, DeepSeek,
|
||||
* ...): we must use @ai-sdk/openai-compatible, which maps the streamed
|
||||
* `reasoning_content` to reasoning parts (the official @ai-sdk/openai provider
|
||||
* drops it). Real OpenAI (no baseURL) keeps the official provider. We assert via
|
||||
* the built model's `.provider` tag.
|
||||
*/
|
||||
describe('AiService.getChatModel openai provider selection', () => {
|
||||
function serviceWith(baseUrl: string | undefined) {
|
||||
const aiSettings = {
|
||||
resolve: jest.fn().mockResolvedValue({
|
||||
driver: 'openai',
|
||||
chatModel: 'glm-5.2',
|
||||
apiKey: 'key',
|
||||
baseUrl,
|
||||
}),
|
||||
};
|
||||
return new AiService(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
aiSettings as any,
|
||||
{ find: jest.fn() } as any,
|
||||
{ decryptSecret: jest.fn() } as any,
|
||||
);
|
||||
}
|
||||
|
||||
it('uses the openai-compatible provider when a custom baseURL is set', async () => {
|
||||
const model = await serviceWith('https://api.z.ai/api/coding/paas/v4').getChatModel(
|
||||
'ws-1',
|
||||
);
|
||||
// openai-compatible surfaces reasoning_content; tagged "openai-compatible.*".
|
||||
expect((model as { provider: string }).provider).toContain('openai-compatible');
|
||||
});
|
||||
|
||||
it('uses the official openai provider when there is no baseURL (real OpenAI)', async () => {
|
||||
const model = await serviceWith(undefined).getChatModel('ws-1');
|
||||
expect((model as { provider: string }).provider).toBe('openai.chat');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
type LanguageModel,
|
||||
} from 'ai';
|
||||
import { createOpenAI } from '@ai-sdk/openai';
|
||||
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
||||
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
||||
import { createOllama } from 'ai-sdk-ollama';
|
||||
import { AiSettingsService } from './ai-settings.service';
|
||||
@@ -143,17 +144,29 @@ export class AiService {
|
||||
|
||||
switch (driver) {
|
||||
case 'openai':
|
||||
// baseURL (when set) covers openai-compatible endpoints. Use Chat
|
||||
// Completions (/chat/completions) — the portable OpenAI-compatible
|
||||
// endpoint. The default callable createOpenAI(...)(model) targets the
|
||||
// Responses API (/responses), which OpenAI-compatible gateways
|
||||
// (OpenRouter, etc.) reject on multi-turn requests (history with
|
||||
// assistant messages) → 400.
|
||||
// DIAGNOSTIC (provider ECONNRESET investigation) — temporary: pass the
|
||||
// passive instrumented fetch (logging only; no behavior change).
|
||||
// A custom baseURL means an openai-COMPATIBLE third-party endpoint
|
||||
// (z.ai / GLM, DeepSeek, OpenRouter, ...). Use @ai-sdk/openai-compatible
|
||||
// there: unlike the official @ai-sdk/openai provider, it maps the
|
||||
// provider's streamed `reasoning_content` to reasoning parts, so the
|
||||
// agent's chain-of-thought is surfaced to the UI (and the model is not
|
||||
// silent during a long server-side "thinking" phase). It also targets
|
||||
// Chat Completions (/chat/completions), the portable endpoint that
|
||||
// OpenAI-compatible gateways accept on multi-turn history (the official
|
||||
// provider's default callable targets /responses, which they 400).
|
||||
if (baseUrl) {
|
||||
return createOpenAICompatible({
|
||||
name: 'openai-compatible',
|
||||
apiKey,
|
||||
baseURL: baseUrl,
|
||||
// Passive ECONNRESET telemetry; on the chat path it also carries the
|
||||
// streaming fetch (disabled long-turn timeouts) once #175 lands.
|
||||
fetch: this.aiDiagnosticFetch,
|
||||
})(chatModel);
|
||||
}
|
||||
// Real OpenAI (no custom baseURL): keep the official provider, on Chat
|
||||
// Completions to preserve multi-turn compatibility.
|
||||
return createOpenAI({
|
||||
apiKey,
|
||||
baseURL: baseUrl,
|
||||
fetch: this.aiDiagnosticFetch,
|
||||
}).chat(chatModel);
|
||||
case 'gemini':
|
||||
|
||||
Reference in New Issue
Block a user