@lobehub/chat 1.136.12 → 1.136.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/.github/workflows/claude-translator.yml +13 -1
  2. package/CHANGELOG.md +34 -0
  3. package/changelog/v1.json +12 -0
  4. package/locales/ar/modelProvider.json +12 -0
  5. package/locales/ar/models.json +39 -24
  6. package/locales/bg-BG/modelProvider.json +12 -0
  7. package/locales/bg-BG/models.json +39 -24
  8. package/locales/de-DE/modelProvider.json +12 -0
  9. package/locales/de-DE/models.json +39 -24
  10. package/locales/en-US/modelProvider.json +12 -0
  11. package/locales/en-US/models.json +39 -24
  12. package/locales/es-ES/modelProvider.json +12 -0
  13. package/locales/es-ES/models.json +39 -24
  14. package/locales/fa-IR/modelProvider.json +12 -0
  15. package/locales/fa-IR/models.json +39 -24
  16. package/locales/fr-FR/modelProvider.json +12 -0
  17. package/locales/fr-FR/models.json +39 -24
  18. package/locales/it-IT/modelProvider.json +12 -0
  19. package/locales/it-IT/models.json +39 -24
  20. package/locales/ja-JP/modelProvider.json +12 -0
  21. package/locales/ja-JP/models.json +39 -24
  22. package/locales/ko-KR/modelProvider.json +12 -0
  23. package/locales/ko-KR/models.json +39 -24
  24. package/locales/nl-NL/modelProvider.json +12 -0
  25. package/locales/nl-NL/models.json +39 -24
  26. package/locales/pl-PL/modelProvider.json +12 -0
  27. package/locales/pl-PL/models.json +39 -24
  28. package/locales/pt-BR/modelProvider.json +12 -0
  29. package/locales/pt-BR/models.json +39 -24
  30. package/locales/ru-RU/modelProvider.json +12 -0
  31. package/locales/ru-RU/models.json +39 -24
  32. package/locales/tr-TR/modelProvider.json +12 -0
  33. package/locales/tr-TR/models.json +39 -24
  34. package/locales/vi-VN/modelProvider.json +12 -0
  35. package/locales/vi-VN/models.json +39 -24
  36. package/locales/zh-CN/modelProvider.json +12 -0
  37. package/locales/zh-CN/models.json +39 -24
  38. package/locales/zh-TW/modelProvider.json +12 -0
  39. package/locales/zh-TW/models.json +39 -24
  40. package/package.json +3 -3
  41. package/packages/const/src/settings/index.ts +1 -0
  42. package/packages/database/package.json +7 -5
  43. package/packages/electron-client-ipc/src/events/index.ts +2 -2
  44. package/packages/electron-client-ipc/src/events/{localFile.ts → localSystem.ts} +25 -6
  45. package/packages/electron-client-ipc/src/types/index.ts +1 -1
  46. package/packages/electron-client-ipc/src/types/{localFile.ts → localSystem.ts} +89 -4
  47. package/packages/file-loaders/package.json +1 -2
  48. package/packages/file-loaders/src/loadFile.ts +4 -1
  49. package/packages/file-loaders/src/loaders/doc/__snapshots__/index.test.ts.snap +46 -0
  50. package/packages/file-loaders/src/loaders/doc/index.test.ts +38 -0
  51. package/packages/file-loaders/src/loaders/doc/index.ts +57 -0
  52. package/packages/file-loaders/src/loaders/docx/index.ts +36 -45
  53. package/packages/file-loaders/src/loaders/index.ts +2 -0
  54. package/packages/file-loaders/src/types/word-extractor.d.ts +9 -0
  55. package/packages/file-loaders/src/types.ts +1 -1
  56. package/packages/model-runtime/src/core/openaiCompatibleFactory/index.test.ts +267 -38
  57. package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +45 -0
  58. package/packages/model-runtime/src/providerTestUtils.ts +0 -5
  59. package/packages/model-runtime/src/providers/anthropic/generateObject.test.ts +57 -44
  60. package/packages/model-runtime/src/providers/anthropic/generateObject.ts +28 -20
  61. package/packages/model-runtime/src/providers/deepseek/index.ts +5 -0
  62. package/packages/model-runtime/src/providers/openai/index.test.ts +0 -5
  63. package/packages/model-runtime/src/providers/openrouter/index.test.ts +3 -3
  64. package/packages/model-runtime/src/providers/openrouter/index.ts +32 -20
  65. package/packages/model-runtime/src/providers/openrouter/type.ts +25 -24
  66. package/packages/model-runtime/src/providers/zhipu/index.test.ts +0 -1
  67. package/packages/model-runtime/src/types/structureOutput.ts +13 -1
  68. package/packages/model-runtime/src/utils/handleOpenAIError.test.ts +0 -5
  69. package/packages/model-runtime/src/utils/handleOpenAIError.ts +2 -2
  70. package/packages/types/src/aiChat.ts +13 -1
  71. package/packages/types/src/index.ts +1 -0
  72. package/src/features/ChatInput/InputEditor/index.tsx +39 -26
  73. package/src/features/Conversation/Messages/Assistant/Tool/Render/LoadingPlaceholder/index.tsx +1 -1
  74. package/src/server/routers/lambda/agent.ts +2 -3
  75. package/src/server/routers/lambda/aiChat.ts +33 -1
  76. package/src/server/routers/lambda/chunk.ts +2 -2
  77. package/src/services/electron/file.ts +1 -2
  78. package/src/services/electron/localFileService.ts +40 -0
  79. package/src/tools/local-system/Placeholder/ListFiles.tsx +23 -0
  80. package/src/tools/local-system/Placeholder/ReadLocalFile.tsx +9 -0
  81. package/src/tools/local-system/Placeholder/SearchFiles.tsx +55 -0
  82. package/src/tools/local-system/Placeholder/index.tsx +25 -0
  83. package/src/tools/placeholders.ts +3 -0
@@ -1,70 +1,61 @@
1
- import { DocxLoader as LangchainDocxLoader } from '@langchain/community/document_loaders/fs/docx';
2
1
  import debug from 'debug';
2
+ import fs from 'node:fs/promises';
3
+ import mammoth from 'mammoth';
3
4
 
4
5
  import type { DocumentPage, FileLoaderInterface } from '../../types';
5
6
 
6
7
  const log = debug('file-loaders:docx');
7
8
 
8
9
  /**
9
- * Loads Word documents (.docx) using the LangChain Community DocxLoader.
10
+ * Loads Word documents (.docx) using mammoth library.
11
+ * Extracts text content and basic metadata from DOCX files.
10
12
  */
11
13
  export class DocxLoader implements FileLoaderInterface {
12
14
  async loadPages(filePath: string): Promise<DocumentPage[]> {
13
15
  log('Loading DOCX file:', filePath);
14
16
  try {
15
- let loader: LangchainDocxLoader;
16
- if (filePath.endsWith('.doc')) {
17
- loader = new LangchainDocxLoader(filePath, { type: 'doc' });
18
- } else {
19
- loader = new LangchainDocxLoader(filePath, { type: 'docx' });
20
- }
21
- log('LangChain DocxLoader created');
22
- const docs = await loader.load(); // Langchain DocxLoader typically loads the whole doc as one
23
- log('DOCX document loaded, parts:', docs.length);
24
-
25
- const pages: DocumentPage[] = docs.map((doc) => {
26
- const pageContent = doc.pageContent || '';
27
- const lines = pageContent.split('\n');
28
- const lineCount = lines.length;
29
- const charCount = pageContent.length;
17
+ // Read file as buffer
18
+ const buffer = await fs.readFile(filePath);
19
+ log('File buffer read, size:', buffer.length);
30
20
 
31
- // Langchain DocxLoader doesn't usually provide page numbers in metadata
32
- // We treat it as a single page
33
- const metadata = {
34
- ...doc.metadata, // Include any other metadata Langchain provides
35
- pageNumber: 1,
36
- };
21
+ // Extract text using mammoth
22
+ const result = await mammoth.extractRawText({ buffer });
23
+ const pageContent = result.value;
24
+ log('Text extracted, length:', pageContent.length);
37
25
 
38
- // @ts-expect-error Remove source if present, as it's handled at the FileDocument level
39
- delete metadata.source;
26
+ // Count lines and characters
27
+ const lines = pageContent.split('\n');
28
+ const lineCount = lines.length;
29
+ const charCount = pageContent.length;
40
30
 
41
- log('DOCX document processed, lines:', lineCount, 'chars:', charCount);
31
+ log('DOCX document processed, lines:', lineCount, 'chars:', charCount);
42
32
 
43
- return {
44
- charCount,
45
- lineCount,
46
- metadata,
47
- pageContent,
48
- };
49
- });
33
+ // Create single page with extracted content
34
+ const page: DocumentPage = {
35
+ charCount,
36
+ lineCount,
37
+ metadata: {
38
+ pageNumber: 1,
39
+ },
40
+ pageContent,
41
+ };
50
42
 
51
- // If docs array is empty (e.g., empty file), create an empty page
52
- if (pages.length === 0) {
53
- log('No content in DOCX document, creating empty page');
54
- pages.push({
55
- charCount: 0,
56
- lineCount: 0,
57
- metadata: { pageNumber: 1 },
58
- pageContent: '',
59
- });
43
+ // Handle warnings if any
44
+ if (result.messages.length > 0) {
45
+ const warnings = result.messages.filter((msg) => msg.type === 'warning');
46
+ if (warnings.length > 0) {
47
+ log('Extraction warnings:', warnings.length);
48
+ warnings.forEach((warning) => log('Warning:', warning.message));
49
+ }
60
50
  }
61
51
 
62
- log('DOCX loading completed, total pages:', pages.length);
63
- return pages;
52
+ log('DOCX loading completed');
53
+ return [page];
64
54
  } catch (e) {
65
55
  const error = e as Error;
66
56
  log('Error encountered while loading DOCX file');
67
- console.error(`Error loading DOCX file ${filePath} using LangChain loader: ${error.message}`);
57
+ console.error(`Error loading DOCX file ${filePath}: ${error.message}`);
58
+
68
59
  const errorPage: DocumentPage = {
69
60
  charCount: 0,
70
61
  lineCount: 0,
@@ -1,4 +1,5 @@
1
1
  import { FileLoaderInterface, SupportedFileType } from '../types';
2
+ import { DocLoader } from './doc';
2
3
  import { DocxLoader } from './docx';
3
4
  // import { EpubLoader } from './epub';
4
5
  import { ExcelLoader } from './excel';
@@ -10,6 +11,7 @@ import { TextLoader } from './text';
10
11
  // Key: file extension (lowercase, without leading dot) or specific type name
11
12
  // Value: Loader Class implementing FileLoaderInterface
12
13
  export const fileLoaders: Record<SupportedFileType, new () => FileLoaderInterface> = {
14
+ doc: DocLoader,
13
15
  docx: DocxLoader,
14
16
  // epub: EpubLoader,
15
17
  excel: ExcelLoader,
@@ -0,0 +1,9 @@
1
+ declare module 'word-extractor' {
2
+ export default class WordExtractor {
3
+ extract(filePath: string): Promise<{
4
+ getBody: () => string;
5
+ getHeaders?: () => Record<string, string> | undefined;
6
+ text?: string;
7
+ }>;
8
+ }
9
+ }
@@ -1,5 +1,5 @@
1
1
  // Define supported file types - consider using an enum or const assertion
2
- export type SupportedFileType = 'pdf' | 'docx' | 'txt' | 'excel' | 'pptx'; // | 'pptx' | 'latex' | 'epub' | 'code' | 'markdown';
2
+ export type SupportedFileType = 'pdf' | 'doc' | 'docx' | 'txt' | 'excel' | 'pptx'; // | 'pptx' | 'latex' | 'epub' | 'code' | 'markdown';
3
3
 
4
4
  /**
5
5
  * 代表一个完整的已加载文件,包含文件级信息和其所有页面/块。
@@ -649,7 +649,6 @@ describe('LobeOpenAICompatibleFactory', () => {
649
649
  it('should return bizErrorType with the cause when OpenAI.APIError is thrown with cause', async () => {
650
650
  // Arrange
651
651
  const errorInfo = {
652
- stack: 'abc',
653
652
  cause: {
654
653
  message: 'api is undefined',
655
654
  },
@@ -670,7 +669,6 @@ describe('LobeOpenAICompatibleFactory', () => {
670
669
  endpoint: defaultBaseURL,
671
670
  error: {
672
671
  cause: { message: 'api is undefined' },
673
- stack: 'abc',
674
672
  },
675
673
  errorType: bizErrorType,
676
674
  provider,
@@ -681,7 +679,6 @@ describe('LobeOpenAICompatibleFactory', () => {
681
679
  it('should return bizErrorType with an cause response with desensitize Url', async () => {
682
680
  // Arrange
683
681
  const errorInfo = {
684
- stack: 'abc',
685
682
  cause: { message: 'api is undefined' },
686
683
  };
687
684
  const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {});
@@ -706,7 +703,6 @@ describe('LobeOpenAICompatibleFactory', () => {
706
703
  endpoint: 'https://api.***.com/v1',
707
704
  error: {
708
705
  cause: { message: 'api is undefined' },
709
- stack: 'abc',
710
706
  },
711
707
  errorType: bizErrorType,
712
708
  provider,
@@ -780,7 +776,6 @@ describe('LobeOpenAICompatibleFactory', () => {
780
776
  name: genericError.name,
781
777
  cause: genericError.cause,
782
778
  message: genericError.message,
783
- stack: genericError.stack,
784
779
  },
785
780
  });
786
781
  }
@@ -1444,8 +1439,13 @@ describe('LobeOpenAICompatibleFactory', () => {
1444
1439
  const payload = {
1445
1440
  messages: [{ content: 'Generate a person object', role: 'user' as const }],
1446
1441
  schema: {
1447
- type: 'object',
1448
- properties: { name: { type: 'string' }, age: { type: 'number' } },
1442
+ name: 'person_extractor',
1443
+ description: 'Extract person information',
1444
+ schema: {
1445
+ type: 'object' as const,
1446
+ properties: { name: { type: 'string' }, age: { type: 'number' } },
1447
+ },
1448
+ strict: true,
1449
1449
  },
1450
1450
  model: 'gpt-4o',
1451
1451
  responseApi: true,
@@ -1476,7 +1476,10 @@ describe('LobeOpenAICompatibleFactory', () => {
1476
1476
 
1477
1477
  const payload = {
1478
1478
  messages: [{ content: 'Generate status', role: 'user' as const }],
1479
- schema: { type: 'object', properties: { status: { type: 'string' } } },
1479
+ schema: {
1480
+ name: 'status_extractor',
1481
+ schema: { type: 'object' as const, properties: { status: { type: 'string' } } },
1482
+ },
1480
1483
  model: 'gpt-4o',
1481
1484
  responseApi: true,
1482
1485
  };
@@ -1513,7 +1516,10 @@ describe('LobeOpenAICompatibleFactory', () => {
1513
1516
 
1514
1517
  const payload = {
1515
1518
  messages: [{ content: 'Generate data', role: 'user' as const }],
1516
- schema: { type: 'object' },
1519
+ schema: {
1520
+ name: 'test_tool',
1521
+ schema: { type: 'object' as const, properties: {} },
1522
+ },
1517
1523
  model: 'gpt-4o',
1518
1524
  responseApi: true,
1519
1525
  };
@@ -1536,7 +1542,10 @@ describe('LobeOpenAICompatibleFactory', () => {
1536
1542
 
1537
1543
  const payload = {
1538
1544
  messages: [{ content: 'Generate data', role: 'user' as const }],
1539
- schema: { type: 'object' },
1545
+ schema: {
1546
+ name: 'test_tool',
1547
+ schema: { type: 'object' as const, properties: {} },
1548
+ },
1540
1549
  model: 'gpt-4o',
1541
1550
  responseApi: true,
1542
1551
  };
@@ -1560,22 +1569,25 @@ describe('LobeOpenAICompatibleFactory', () => {
1560
1569
  const payload = {
1561
1570
  messages: [{ content: 'Generate complex user data', role: 'user' as const }],
1562
1571
  schema: {
1563
- type: 'object',
1564
- properties: {
1565
- user: {
1566
- type: 'object',
1567
- properties: {
1568
- name: { type: 'string' },
1569
- profile: {
1570
- type: 'object',
1571
- properties: {
1572
- age: { type: 'number' },
1573
- preferences: { type: 'array', items: { type: 'string' } },
1572
+ name: 'user_extractor',
1573
+ schema: {
1574
+ type: 'object' as const,
1575
+ properties: {
1576
+ user: {
1577
+ type: 'object',
1578
+ properties: {
1579
+ name: { type: 'string' },
1580
+ profile: {
1581
+ type: 'object',
1582
+ properties: {
1583
+ age: { type: 'number' },
1584
+ preferences: { type: 'array', items: { type: 'string' } },
1585
+ },
1574
1586
  },
1575
1587
  },
1576
1588
  },
1589
+ metadata: { type: 'object' },
1577
1590
  },
1578
- metadata: { type: 'object' },
1579
1591
  },
1580
1592
  },
1581
1593
  model: 'gpt-4o',
@@ -1605,7 +1617,10 @@ describe('LobeOpenAICompatibleFactory', () => {
1605
1617
 
1606
1618
  const payload = {
1607
1619
  messages: [{ content: 'Generate data', role: 'user' as const }],
1608
- schema: { type: 'object' },
1620
+ schema: {
1621
+ name: 'test_tool',
1622
+ schema: { type: 'object' as const, properties: {} },
1623
+ },
1609
1624
  model: 'gpt-4o',
1610
1625
  responseApi: true,
1611
1626
  };
@@ -1634,8 +1649,11 @@ describe('LobeOpenAICompatibleFactory', () => {
1634
1649
  const payload = {
1635
1650
  messages: [{ content: 'Generate a person object', role: 'user' as const }],
1636
1651
  schema: {
1637
- type: 'object',
1638
- properties: { name: { type: 'string' }, age: { type: 'number' } },
1652
+ name: 'person_extractor',
1653
+ schema: {
1654
+ type: 'object' as const,
1655
+ properties: { name: { type: 'string' }, age: { type: 'number' } },
1656
+ },
1639
1657
  },
1640
1658
  model: 'gpt-4o',
1641
1659
  // responseApi: false or undefined - uses chat completions API
@@ -1673,7 +1691,10 @@ describe('LobeOpenAICompatibleFactory', () => {
1673
1691
 
1674
1692
  const payload = {
1675
1693
  messages: [{ content: 'Generate status', role: 'user' as const }],
1676
- schema: { type: 'object', properties: { status: { type: 'string' } } },
1694
+ schema: {
1695
+ name: 'status_extractor',
1696
+ schema: { type: 'object' as const, properties: { status: { type: 'string' } } },
1697
+ },
1677
1698
  model: 'gpt-4o',
1678
1699
  responseApi: false,
1679
1700
  };
@@ -1717,7 +1738,10 @@ describe('LobeOpenAICompatibleFactory', () => {
1717
1738
 
1718
1739
  const payload = {
1719
1740
  messages: [{ content: 'Generate data', role: 'user' as const }],
1720
- schema: { type: 'object' },
1741
+ schema: {
1742
+ name: 'test_tool',
1743
+ schema: { type: 'object' as const, properties: {} },
1744
+ },
1721
1745
  model: 'gpt-4o',
1722
1746
  responseApi: false,
1723
1747
  };
@@ -1748,7 +1772,10 @@ describe('LobeOpenAICompatibleFactory', () => {
1748
1772
 
1749
1773
  const payload = {
1750
1774
  messages: [{ content: 'Generate data', role: 'user' as const }],
1751
- schema: { type: 'object' },
1775
+ schema: {
1776
+ name: 'test_tool',
1777
+ schema: { type: 'object' as const, properties: {} },
1778
+ },
1752
1779
  model: 'gpt-4o',
1753
1780
  responseApi: false,
1754
1781
  };
@@ -1780,19 +1807,22 @@ describe('LobeOpenAICompatibleFactory', () => {
1780
1807
  const payload = {
1781
1808
  messages: [{ content: 'Generate items list', role: 'user' as const }],
1782
1809
  schema: {
1783
- type: 'object',
1784
- properties: {
1785
- items: {
1786
- type: 'array',
1810
+ name: 'abc',
1811
+ schema: {
1812
+ type: 'object' as const,
1813
+ properties: {
1787
1814
  items: {
1788
- type: 'object',
1789
- properties: {
1790
- id: { type: 'number' },
1791
- name: { type: 'string' },
1815
+ type: 'array',
1816
+ items: {
1817
+ type: 'object',
1818
+ properties: {
1819
+ id: { type: 'number' },
1820
+ name: { type: 'string' },
1821
+ },
1792
1822
  },
1793
1823
  },
1824
+ total: { type: 'number' },
1794
1825
  },
1795
- total: { type: 'number' },
1796
1826
  },
1797
1827
  },
1798
1828
  model: 'gpt-4o',
@@ -1816,7 +1846,7 @@ describe('LobeOpenAICompatibleFactory', () => {
1816
1846
 
1817
1847
  const payload = {
1818
1848
  messages: [{ content: 'Generate data', role: 'user' as const }],
1819
- schema: { type: 'object' },
1849
+ schema: { name: 'abc', schema: { type: 'object' } as any },
1820
1850
  model: 'gpt-4o',
1821
1851
  responseApi: false,
1822
1852
  };
@@ -1826,6 +1856,205 @@ describe('LobeOpenAICompatibleFactory', () => {
1826
1856
  );
1827
1857
  });
1828
1858
  });
1859
+
1860
+ describe('tool calling fallback', () => {
1861
+ let instanceWithToolCalling: any;
1862
+
1863
+ beforeEach(() => {
1864
+ const RuntimeClass = createOpenAICompatibleRuntime({
1865
+ baseURL: 'https://api.test.com',
1866
+ generateObject: {
1867
+ useToolsCalling: true,
1868
+ },
1869
+ provider: 'test-provider',
1870
+ });
1871
+
1872
+ instanceWithToolCalling = new RuntimeClass({ apiKey: 'test-key' });
1873
+ });
1874
+
1875
+ it('should use tool calling when configured', async () => {
1876
+ const mockResponse = {
1877
+ choices: [
1878
+ {
1879
+ message: {
1880
+ tool_calls: [
1881
+ {
1882
+ type: 'function' as const,
1883
+ function: {
1884
+ name: 'person_extractor',
1885
+ arguments: '{"name":"Alice","age":28}',
1886
+ },
1887
+ },
1888
+ ],
1889
+ },
1890
+ },
1891
+ ],
1892
+ };
1893
+
1894
+ vi.spyOn(instanceWithToolCalling['client'].chat.completions, 'create').mockResolvedValue(
1895
+ mockResponse as any,
1896
+ );
1897
+
1898
+ const payload = {
1899
+ messages: [{ content: 'Extract person info', role: 'user' as const }],
1900
+ schema: {
1901
+ name: 'person_extractor',
1902
+ description: 'Extract person information',
1903
+ schema: {
1904
+ type: 'object' as const,
1905
+ properties: { name: { type: 'string' }, age: { type: 'number' } },
1906
+ },
1907
+ },
1908
+ model: 'test-model',
1909
+ };
1910
+
1911
+ const result = await instanceWithToolCalling.generateObject(payload);
1912
+
1913
+ expect(instanceWithToolCalling['client'].chat.completions.create).toHaveBeenCalledWith(
1914
+ {
1915
+ messages: payload.messages,
1916
+ model: payload.model,
1917
+ tools: [
1918
+ {
1919
+ type: 'function',
1920
+ function: {
1921
+ name: 'person_extractor',
1922
+ description: 'Extract person information',
1923
+ parameters: payload.schema.schema,
1924
+ },
1925
+ },
1926
+ ],
1927
+ tool_choice: { type: 'function', function: { name: 'person_extractor' } },
1928
+ user: undefined,
1929
+ },
1930
+ { headers: undefined, signal: undefined },
1931
+ );
1932
+
1933
+ expect(result).toEqual({ name: 'Alice', age: 28 });
1934
+ });
1935
+
1936
+ it('should return undefined when no tool call found', async () => {
1937
+ const mockResponse = {
1938
+ choices: [
1939
+ {
1940
+ message: {
1941
+ content: 'Some text response',
1942
+ },
1943
+ },
1944
+ ],
1945
+ };
1946
+
1947
+ vi.spyOn(instanceWithToolCalling['client'].chat.completions, 'create').mockResolvedValue(
1948
+ mockResponse as any,
1949
+ );
1950
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
1951
+
1952
+ const payload = {
1953
+ messages: [{ content: 'Generate data', role: 'user' as const }],
1954
+ schema: {
1955
+ name: 'test_tool',
1956
+ schema: { type: 'object' as const, properties: {} },
1957
+ },
1958
+ model: 'test-model',
1959
+ };
1960
+
1961
+ const result = await instanceWithToolCalling.generateObject(payload);
1962
+
1963
+ expect(consoleSpy).toHaveBeenCalledWith('No tool call found in response');
1964
+ expect(result).toBeUndefined();
1965
+
1966
+ consoleSpy.mockRestore();
1967
+ });
1968
+
1969
+ it('should return undefined when tool call arguments parsing fails', async () => {
1970
+ const mockResponse = {
1971
+ choices: [
1972
+ {
1973
+ message: {
1974
+ tool_calls: [
1975
+ {
1976
+ type: 'function' as const,
1977
+ function: {
1978
+ name: 'test_tool',
1979
+ arguments: 'invalid json',
1980
+ },
1981
+ },
1982
+ ],
1983
+ },
1984
+ },
1985
+ ],
1986
+ };
1987
+
1988
+ vi.spyOn(instanceWithToolCalling['client'].chat.completions, 'create').mockResolvedValue(
1989
+ mockResponse as any,
1990
+ );
1991
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
1992
+
1993
+ const payload = {
1994
+ messages: [{ content: 'Generate data', role: 'user' as const }],
1995
+ schema: {
1996
+ name: 'test_tool',
1997
+ schema: { type: 'object' as const, properties: {} },
1998
+ },
1999
+ model: 'test-model',
2000
+ };
2001
+
2002
+ const result = await instanceWithToolCalling.generateObject(payload);
2003
+
2004
+ expect(consoleSpy).toHaveBeenCalledWith('parse tool call arguments error:', 'invalid json');
2005
+ expect(result).toBeUndefined();
2006
+
2007
+ consoleSpy.mockRestore();
2008
+ });
2009
+
2010
+ it('should handle options correctly with tool calling', async () => {
2011
+ const mockResponse = {
2012
+ choices: [
2013
+ {
2014
+ message: {
2015
+ tool_calls: [
2016
+ {
2017
+ type: 'function' as const,
2018
+ function: {
2019
+ name: 'data_extractor',
2020
+ arguments: '{"data":"test"}',
2021
+ },
2022
+ },
2023
+ ],
2024
+ },
2025
+ },
2026
+ ],
2027
+ };
2028
+
2029
+ vi.spyOn(instanceWithToolCalling['client'].chat.completions, 'create').mockResolvedValue(
2030
+ mockResponse as any,
2031
+ );
2032
+
2033
+ const payload = {
2034
+ messages: [{ content: 'Extract data', role: 'user' as const }],
2035
+ schema: {
2036
+ name: 'data_extractor',
2037
+ schema: { type: 'object' as const, properties: { data: { type: 'string' } } },
2038
+ },
2039
+ model: 'test-model',
2040
+ };
2041
+
2042
+ const options = {
2043
+ headers: { 'X-Custom': 'header' },
2044
+ user: 'test-user',
2045
+ signal: new AbortController().signal,
2046
+ };
2047
+
2048
+ const result = await instanceWithToolCalling.generateObject(payload, options);
2049
+
2050
+ expect(instanceWithToolCalling['client'].chat.completions.create).toHaveBeenCalledWith(
2051
+ expect.any(Object),
2052
+ { headers: options.headers, signal: options.signal },
2053
+ );
2054
+
2055
+ expect(result).toEqual({ data: 'test' });
2056
+ });
2057
+ });
1829
2058
  });
1830
2059
 
1831
2060
  describe('models', () => {
@@ -116,6 +116,12 @@ interface OpenAICompatibleFactoryOptions<T extends Record<string, any> = any> {
116
116
  bizError: ILobeAgentRuntimeErrorType;
117
117
  invalidAPIKey: ILobeAgentRuntimeErrorType;
118
118
  };
119
+ generateObject?: {
120
+ /**
121
+ * Use tool calling to simulate structured output for providers that don't support native structured output
122
+ */
123
+ useToolsCalling?: boolean;
124
+ };
119
125
  models?:
120
126
  | ((params: { client: OpenAI }) => Promise<ChatModelCard[]>)
121
127
  | {
@@ -142,6 +148,7 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
142
148
  customClient,
143
149
  responses,
144
150
  createImage: customCreateImage,
151
+ generateObject: generateObjectConfig,
145
152
  }: OpenAICompatibleFactoryOptions<T>) => {
146
153
  const ErrorType = {
147
154
  bizError: errorType?.bizError || AgentRuntimeErrorType.ProviderBizError,
@@ -391,6 +398,44 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
391
398
  async generateObject(payload: GenerateObjectPayload, options?: GenerateObjectOptions) {
392
399
  const { messages, schema, model, responseApi } = payload;
393
400
 
401
+ // Use tool calling fallback if configured
402
+ if (generateObjectConfig?.useToolsCalling) {
403
+ const tool: ChatCompletionTool = {
404
+ function: {
405
+ description:
406
+ schema.description || 'Generate structured output according to the provided schema',
407
+ name: schema.name || 'structured_output',
408
+ parameters: schema.schema,
409
+ },
410
+ type: 'function',
411
+ };
412
+
413
+ const res = await this.client.chat.completions.create(
414
+ {
415
+ messages,
416
+ model,
417
+ tool_choice: { function: { name: tool.function.name }, type: 'function' },
418
+ tools: [tool],
419
+ user: options?.user,
420
+ },
421
+ { headers: options?.headers, signal: options?.signal },
422
+ );
423
+
424
+ const toolCall = res.choices[0].message.tool_calls?.[0];
425
+
426
+ if (!toolCall || toolCall.type !== 'function') {
427
+ console.error('No tool call found in response');
428
+ return undefined;
429
+ }
430
+
431
+ try {
432
+ return JSON.parse(toolCall.function.arguments);
433
+ } catch {
434
+ console.error('parse tool call arguments error:', toolCall.function.arguments);
435
+ return undefined;
436
+ }
437
+ }
438
+
394
439
  if (responseApi) {
395
440
  const res = await this.client!.responses.create(
396
441
  {
@@ -160,7 +160,6 @@ export const testProvider = ({
160
160
  cause: {
161
161
  message: 'api is undefined',
162
162
  },
163
- stack: 'abc',
164
163
  };
165
164
  const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {});
166
165
 
@@ -178,7 +177,6 @@ export const testProvider = ({
178
177
  endpoint: defaultBaseURL,
179
178
  error: {
180
179
  cause: { message: 'api is undefined' },
181
- stack: 'abc',
182
180
  },
183
181
  errorType: bizErrorType,
184
182
  provider,
@@ -190,7 +188,6 @@ export const testProvider = ({
190
188
  // Arrange
191
189
  const errorInfo = {
192
190
  cause: { message: 'api is undefined' },
193
- stack: 'abc',
194
191
  };
195
192
  const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {});
196
193
 
@@ -214,7 +211,6 @@ export const testProvider = ({
214
211
  endpoint: 'https://api.***.com/v1',
215
212
  error: {
216
213
  cause: { message: 'api is undefined' },
217
- stack: 'abc',
218
214
  },
219
215
  errorType: bizErrorType,
220
216
  provider,
@@ -265,7 +261,6 @@ export const testProvider = ({
265
261
  cause: genericError.cause,
266
262
  message: genericError.message,
267
263
  name: genericError.name,
268
- stack: genericError.stack,
269
264
  },
270
265
  errorType: 'AgentRuntimeError',
271
266
  provider,