Skip to content

Commit 6585191

Browse files
tea-artistteable-bot
andauthored
[sync] feat(ai): improve LLM provider test with detailed error messages T1518 (#955) (#2369)
Synced from teableio/teable-ee@d230500 Co-authored-by: teable-bot <[email protected]>
1 parent 3c04ec9 commit 6585191

File tree

15 files changed

+726
-172
lines changed

15 files changed

+726
-172
lines changed

apps/nestjs-backend/src/features/ai/ai.service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ export class AiService {
8383
});
8484
}
8585

86-
const provider = Object.entries(modelProviders).find(([key]) =>
87-
type.toLowerCase().includes(key.toLowerCase())
86+
const provider = Object.entries(modelProviders).find(
87+
([key]) => type.toLowerCase() === key.toLowerCase()
8888
)?.[1];
8989

9090
if (!provider) {

apps/nestjs-backend/src/features/ai/util.ts

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,78 @@ import { get } from 'lodash';
1616
import { createOllama } from 'ollama-ai-provider-v2';
1717
import { TASK_MODEL_MAP } from './constant';
1818

19+
/**
20+
* Fix non-standard OpenAI compatible API streaming response.
21+
* Some API proxies return `role: ""` instead of proper format.
22+
* This uses regex replacement which is simpler and more robust than parsing.
23+
*/
24+
const fixStreamText = (text: string): string => {
25+
// Replace "role":"" with nothing (remove the field)
26+
// This regex handles the field whether it's first, middle, or last in the object
27+
// comma followed by role (if last field)
28+
29+
return text
30+
.replace(/"role":"",/g, '') // role followed by comma
31+
.replace(/,"role":""/g, '');
32+
};
33+
34+
/**
35+
* Custom fetch wrapper that fixes non-standard OpenAI compatible API responses.
36+
* Some API proxies return invalid format like `role: ""` instead of `role: "assistant"`.
37+
* This wrapper transforms the streaming response to fix such issues.
38+
*/
39+
const createFixingFetch = (): typeof fetch => {
40+
return async (input, init) => {
41+
const response = await fetch(input, init);
42+
43+
// Only transform if there's a body (streaming responses)
44+
if (!response.body) {
45+
return response;
46+
}
47+
48+
const reader = response.body.getReader();
49+
const decoder = new TextDecoder();
50+
const encoder = new TextEncoder();
51+
52+
const transformedStream = new ReadableStream({
53+
async pull(controller) {
54+
const { done, value } = await reader.read();
55+
56+
if (done) {
57+
controller.close();
58+
return;
59+
}
60+
61+
const text = decoder.decode(value, { stream: true });
62+
const fixedText = fixStreamText(text);
63+
64+
controller.enqueue(encoder.encode(fixedText));
65+
},
66+
});
67+
68+
return new Response(transformedStream, {
69+
status: response.status,
70+
statusText: response.statusText,
71+
headers: response.headers,
72+
});
73+
};
74+
};
75+
76+
/**
77+
* Wrapper for OpenAI compatible providers that:
78+
* 1. Forces Chat Completions API instead of Responses API
79+
* 2. Uses custom fetch to fix non-standard API responses
80+
*/
81+
const createOpenAICompatibleWrapper = (
82+
options: Parameters<typeof createOpenAICompatible>[0]
83+
): ReturnType<typeof createOpenAICompatible> => {
84+
return createOpenAICompatible({
85+
...options,
86+
// Use custom fetch to fix non-standard responses
87+
fetch: createFixingFetch(),
88+
});
89+
};
90+
1991
export const modelProviders = {
2092
[LLMProviderType.OPENAI]: createOpenAI,
2193
[LLMProviderType.ANTHROPIC]: createAnthropic,
@@ -32,7 +104,7 @@ export const modelProviders = {
32104
[LLMProviderType.OLLAMA]: createOllama,
33105
[LLMProviderType.AMAZONBEDROCK]: createAmazonBedrock,
34106
[LLMProviderType.OPENROUTER]: createOpenRouter,
35-
[LLMProviderType.OPENAI_COMPATIBLE]: createOpenAICompatible,
107+
[LLMProviderType.OPENAI_COMPATIBLE]: createOpenAICompatibleWrapper,
36108
} as const;
37109

38110
export const getAdaptedProviderOptions = (

apps/nestjs-backend/src/features/builtin-assets-init/builtin-assets-init.service.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,16 @@ export class BuiltinAssetsInitService implements OnModuleInit {
211211
filePath: ANONYMOUS_USER_AVATAR_PATH,
212212
uploadType: UploadType.Avatar,
213213
},
214+
{
215+
id: 'actTestImage',
216+
filePath: 'static/test/test-image.png',
217+
uploadType: UploadType.ChatFile,
218+
},
219+
{
220+
id: 'actTestPDF',
221+
filePath: 'static/test/test-pdf.pdf',
222+
uploadType: UploadType.ChatFile,
223+
},
214224
];
215225
}
216226

apps/nestjs-backend/src/features/setting/open-api/setting-open-api.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Module } from '@nestjs/common';
22
import { MulterModule } from '@nestjs/platform-express';
33
import multer from 'multer';
4+
import { AttachmentsStorageModule } from '../../attachments/attachments-storage.module';
45
import { StorageModule } from '../../attachments/plugins/storage.module';
56
import { TurnstileModule } from '../../auth/turnstile/turnstile.module';
67
import { SettingModule } from '../setting.module';
@@ -13,6 +14,7 @@ import { SettingOpenApiService } from './setting-open-api.service';
1314
storage: multer.diskStorage({}),
1415
}),
1516
StorageModule,
17+
AttachmentsStorageModule,
1618
SettingModule,
1719
TurnstileModule,
1820
],

0 commit comments

Comments
 (0)