universal-llm-client 4.1.0 → 4.3.0

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 (103) hide show
  1. package/CHANGELOG.md +139 -103
  2. package/LICENSE +21 -21
  3. package/README.md +591 -591
  4. package/dist/ai-model.js.map +1 -1
  5. package/dist/auditor.js.map +1 -1
  6. package/dist/client.js.map +1 -1
  7. package/dist/http.js.map +1 -1
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +1 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/interfaces.d.ts +20 -0
  13. package/dist/interfaces.d.ts.map +1 -1
  14. package/dist/interfaces.js.map +1 -1
  15. package/dist/mcp.js.map +1 -1
  16. package/dist/providers/anthropic.js.map +1 -1
  17. package/dist/providers/google.d.ts.map +1 -1
  18. package/dist/providers/google.js +2 -0
  19. package/dist/providers/google.js.map +1 -1
  20. package/dist/providers/index.js.map +1 -1
  21. package/dist/providers/ollama.js.map +1 -1
  22. package/dist/providers/openai.js.map +1 -1
  23. package/dist/router.js.map +1 -1
  24. package/dist/stream-decoder.js.map +1 -1
  25. package/dist/structured-output.d.ts +24 -1
  26. package/dist/structured-output.d.ts.map +1 -1
  27. package/dist/structured-output.js +58 -5
  28. package/dist/structured-output.js.map +1 -1
  29. package/dist/tools.js.map +1 -1
  30. package/dist/zod-adapter.js.map +1 -1
  31. package/package.json +115 -116
  32. package/src/ai-model.ts +0 -350
  33. package/src/auditor.ts +0 -213
  34. package/src/client.ts +0 -402
  35. package/src/debug/debug-google-streaming.ts +0 -97
  36. package/src/debug/debug-tool-execution.ts +0 -86
  37. package/src/debug/test-lmstudio-tools.ts +0 -155
  38. package/src/demos/README.md +0 -47
  39. package/src/demos/basic/universal-llm-examples.ts +0 -161
  40. package/src/demos/mcp/astrid-memory-demo.ts +0 -295
  41. package/src/demos/mcp/astrid-persona-memory.ts +0 -357
  42. package/src/demos/mcp/mcp-mongodb-demo.ts +0 -275
  43. package/src/demos/mcp/simple-astrid-memory.ts +0 -148
  44. package/src/demos/mcp/simple-mcp-demo.ts +0 -68
  45. package/src/demos/mcp/working-mcp-demo.ts +0 -62
  46. package/src/demos/model-alias-demo.ts +0 -0
  47. package/src/demos/tools/RAG_MEMORY_INTEGRATION.md +0 -267
  48. package/src/demos/tools/astrid-memory-demo.ts +0 -270
  49. package/src/demos/tools/astrid-production-memory-clean.ts +0 -785
  50. package/src/demos/tools/astrid-production-memory.ts +0 -558
  51. package/src/demos/tools/basic-translation-test.ts +0 -66
  52. package/src/demos/tools/chromadb-similarity-tuning.ts +0 -390
  53. package/src/demos/tools/clean-multilingual-conversation.ts +0 -209
  54. package/src/demos/tools/clean-translation-test.ts +0 -119
  55. package/src/demos/tools/clean-universal-multilingual-test.ts +0 -131
  56. package/src/demos/tools/complete-rag-demo.ts +0 -369
  57. package/src/demos/tools/complete-tool-demo.ts +0 -132
  58. package/src/demos/tools/demo-tool-calling.ts +0 -124
  59. package/src/demos/tools/dynamic-language-switching-test.ts +0 -251
  60. package/src/demos/tools/hybrid-thinking-test.ts +0 -154
  61. package/src/demos/tools/memory-integration-test.ts +0 -420
  62. package/src/demos/tools/multilingual-memory-system.ts +0 -802
  63. package/src/demos/tools/ondemand-translation-demo.ts +0 -655
  64. package/src/demos/tools/production-tool-demo.ts +0 -245
  65. package/src/demos/tools/revolutionary-multilingual-test.ts +0 -151
  66. package/src/demos/tools/rigorous-language-analysis.ts +0 -218
  67. package/src/demos/tools/test-universal-memory-system.ts +0 -126
  68. package/src/demos/tools/translation-integration-guide.ts +0 -346
  69. package/src/demos/tools/universal-memory-system.ts +0 -560
  70. package/src/http.ts +0 -247
  71. package/src/index.ts +0 -160
  72. package/src/interfaces.ts +0 -657
  73. package/src/mcp.ts +0 -345
  74. package/src/providers/anthropic.ts +0 -762
  75. package/src/providers/google.ts +0 -620
  76. package/src/providers/index.ts +0 -8
  77. package/src/providers/ollama.ts +0 -469
  78. package/src/providers/openai.ts +0 -392
  79. package/src/router.ts +0 -780
  80. package/src/stream-decoder.ts +0 -361
  81. package/src/structured-output.ts +0 -702
  82. package/src/test-scripts/test-advanced-tools.ts +0 -310
  83. package/src/test-scripts/test-google-streaming-enhanced.ts +0 -147
  84. package/src/test-scripts/test-google-streaming.ts +0 -63
  85. package/src/test-scripts/test-google-system-prompt-comprehensive.ts +0 -189
  86. package/src/test-scripts/test-mcp-config.ts +0 -28
  87. package/src/test-scripts/test-mcp-connection.ts +0 -29
  88. package/src/test-scripts/test-system-message-positions.ts +0 -163
  89. package/src/test-scripts/test-system-prompt-improvement-demo.ts +0 -83
  90. package/src/test-scripts/test-tool-calling.ts +0 -231
  91. package/src/tests/ai-model.test.ts +0 -1614
  92. package/src/tests/auditor.test.ts +0 -224
  93. package/src/tests/http.test.ts +0 -200
  94. package/src/tests/interfaces.test.ts +0 -117
  95. package/src/tests/providers/google.test.ts +0 -660
  96. package/src/tests/providers/ollama.test.ts +0 -954
  97. package/src/tests/providers/openai.test.ts +0 -1122
  98. package/src/tests/router.test.ts +0 -254
  99. package/src/tests/stream-decoder.test.ts +0 -179
  100. package/src/tests/structured-output.test.ts +0 -1340
  101. package/src/tests/tools.test.ts +0 -175
  102. package/src/tools.ts +0 -246
  103. package/src/zod-adapter.ts +0 -72
@@ -1,254 +0,0 @@
1
- /**
2
- * Tests for router.ts — Failover Engine
3
- */
4
- import { describe, it, expect, mock, beforeEach } from 'bun:test';
5
- import { Router, type ProviderEntry } from '../router.js';
6
- import { BaseLLMClient } from '../client.js';
7
- import { BufferedAuditor } from '../auditor.js';
8
- import type {
9
- LLMChatMessage,
10
- LLMChatResponse,
11
- ChatOptions,
12
- DecodedEvent,
13
- } from '../interfaces.js';
14
-
15
- // ============================================================================
16
- // Mock Client
17
- // ============================================================================
18
-
19
- class MockClient extends BaseLLMClient {
20
- public chatFn: (messages: LLMChatMessage[]) => Promise<LLMChatResponse>;
21
- public embedFn: (text: string) => Promise<number[]>;
22
- public modelsFn: () => Promise<string[]>;
23
-
24
- constructor(id: string, opts?: {
25
- chatFn?: (messages: LLMChatMessage[]) => Promise<LLMChatResponse>;
26
- embedFn?: (text: string) => Promise<number[]>;
27
- }) {
28
- super({
29
- model: `mock-${id}`,
30
- url: `http://mock-${id}`,
31
- apiType: 'openai' as never,
32
- });
33
- this.chatFn = opts?.chatFn ?? (async () => ({
34
- message: { role: 'assistant' as const, content: `Response from ${id}` },
35
- provider: id,
36
- }));
37
- this.embedFn = opts?.embedFn ?? (async () => [1, 2, 3]);
38
- this.modelsFn = async () => [`mock-${id}`];
39
- }
40
-
41
- async chat(messages: LLMChatMessage[]): Promise<LLMChatResponse> {
42
- return this.chatFn(messages);
43
- }
44
-
45
- async *chatStream(): AsyncGenerator<DecodedEvent, LLMChatResponse | void, unknown> {
46
- yield { type: 'text', content: 'streamed' };
47
- return { message: { role: 'assistant', content: 'streamed' }, provider: 'mock' };
48
- }
49
-
50
- async getModels(): Promise<string[]> {
51
- return this.modelsFn();
52
- }
53
-
54
- async embed(text: string): Promise<number[]> {
55
- return this.embedFn(text);
56
- }
57
- }
58
-
59
- // ============================================================================
60
- // Tests
61
- // ============================================================================
62
-
63
- describe('Router', () => {
64
- let router: Router;
65
- let auditor: BufferedAuditor;
66
-
67
- beforeEach(() => {
68
- auditor = new BufferedAuditor();
69
- router = new Router({ auditor, retriesPerProvider: 1, maxFailures: 2, cooldownMs: 100 });
70
- });
71
-
72
- describe('provider management', () => {
73
- it('adds providers and sorts by priority', () => {
74
- const clientA = new MockClient('a');
75
- const clientB = new MockClient('b');
76
-
77
- router.addProvider({ id: 'a', client: clientA, priority: 2 });
78
- router.addProvider({ id: 'b', client: clientB, priority: 1 });
79
-
80
- const status = router.getStatus();
81
- expect(status).toHaveLength(2);
82
- // b has lower priority number = tried first
83
- expect(status[0]!.id).toBe('b');
84
- expect(status[1]!.id).toBe('a');
85
- });
86
-
87
- it('removes providers', () => {
88
- router.addProvider({ id: 'a', client: new MockClient('a'), priority: 0 });
89
- router.addProvider({ id: 'b', client: new MockClient('b'), priority: 1 });
90
- router.removeProvider('a');
91
-
92
- expect(router.getStatus()).toHaveLength(1);
93
- expect(router.getStatus()[0]!.id).toBe('b');
94
- });
95
- });
96
-
97
- describe('execution with failover', () => {
98
- it('uses the highest-priority provider', async () => {
99
- const clientA = new MockClient('a');
100
- const clientB = new MockClient('b');
101
-
102
- router.addProvider({ id: 'a', client: clientA, priority: 0 });
103
- router.addProvider({ id: 'b', client: clientB, priority: 1 });
104
-
105
- const result = await router.chat([{ role: 'user', content: 'test' }]);
106
- expect(result.provider).toBe('a');
107
- });
108
-
109
- it('fails over to next provider on error', async () => {
110
- const clientA = new MockClient('a', {
111
- chatFn: async () => { throw new Error('A failed'); },
112
- });
113
- const clientB = new MockClient('b');
114
-
115
- router.addProvider({ id: 'a', client: clientA, priority: 0 });
116
- router.addProvider({ id: 'b', client: clientB, priority: 1 });
117
-
118
- const result = await router.chat([{ role: 'user', content: 'test' }]);
119
- expect(result.provider).toBe('b');
120
-
121
- // Check audit events
122
- const events = auditor.getEvents();
123
- const failoverEvents = events.filter(e => e.type === 'failover');
124
- expect(failoverEvents.length).toBeGreaterThan(0);
125
- });
126
-
127
- it('retries within a provider before failover', async () => {
128
- let attempts = 0;
129
- const clientA = new MockClient('a', {
130
- chatFn: async () => {
131
- attempts++;
132
- if (attempts === 1) throw new Error('Transient failure');
133
- return {
134
- message: { role: 'assistant' as const, content: 'recovered' },
135
- provider: 'a',
136
- };
137
- },
138
- });
139
-
140
- router.addProvider({ id: 'a', client: clientA, priority: 0 });
141
- const result = await router.chat([{ role: 'user', content: 'test' }]);
142
-
143
- expect(result.provider).toBe('a');
144
- expect(attempts).toBe(2); // first attempt + 1 retry
145
- });
146
-
147
- it('throws when all providers fail', async () => {
148
- const clientA = new MockClient('a', {
149
- chatFn: async () => { throw new Error('A failed'); },
150
- });
151
- const clientB = new MockClient('b', {
152
- chatFn: async () => { throw new Error('B failed'); },
153
- });
154
-
155
- router.addProvider({ id: 'a', client: clientA, priority: 0 });
156
- router.addProvider({ id: 'b', client: clientB, priority: 1 });
157
-
158
- expect(router.chat([{ role: 'user', content: 'test' }])).rejects.toThrow();
159
- });
160
-
161
- it('throws when no providers configured', async () => {
162
- expect(router.chat([{ role: 'user', content: 'test' }])).rejects.toThrow(
163
- 'No available LLM providers'
164
- );
165
- });
166
- });
167
-
168
- describe('health tracking', () => {
169
- it('marks unhealthy after max failures', async () => {
170
- const failing = new MockClient('fail', {
171
- chatFn: async () => { throw new Error('always fails'); },
172
- });
173
- const backup = new MockClient('backup');
174
-
175
- router.addProvider({ id: 'fail', client: failing, priority: 0 });
176
- router.addProvider({ id: 'backup', client: backup, priority: 1 });
177
-
178
- // Each chat call records 1 failure for 'fail', maxFailures is 2
179
- await router.chat([{ role: 'user', content: 'test 1' }]);
180
- await router.chat([{ role: 'user', content: 'test 2' }]);
181
-
182
- const status = router.getStatus();
183
- const failStatus = status.find(s => s.id === 'fail');
184
- expect(failStatus!.healthy).toBe(false);
185
- });
186
-
187
- it('recovers after cooldown expires', async () => {
188
- let callCount = 0;
189
- const failing = new MockClient('fail', {
190
- chatFn: async () => {
191
- callCount++;
192
- if (callCount <= 4) throw new Error('failing');
193
- return {
194
- message: { role: 'assistant' as const, content: 'recovered' },
195
- provider: 'fail',
196
- };
197
- },
198
- });
199
- const backup = new MockClient('backup');
200
-
201
- router.addProvider({ id: 'fail', client: failing, priority: 0 });
202
- router.addProvider({ id: 'backup', client: backup, priority: 1 });
203
-
204
- // First call: fail → backup
205
- await router.chat([{ role: 'user', content: '1' }]);
206
-
207
- // Wait for cooldown (100ms in test config)
208
- await new Promise(r => setTimeout(r, 150));
209
-
210
- // After cooldown, fail should be tried again
211
- const status = router.getStatus();
212
- const failStatus = status.find(s => s.id === 'fail');
213
- expect(failStatus!.healthy).toBe(true);
214
- });
215
- });
216
-
217
- describe('tool registration', () => {
218
- it('broadcasts tool registration to all providers', () => {
219
- const clientA = new MockClient('a');
220
- const clientB = new MockClient('b');
221
-
222
- router.addProvider({ id: 'a', client: clientA, priority: 0 });
223
- router.addProvider({ id: 'b', client: clientB, priority: 1 });
224
-
225
- router.registerTool(
226
- 'test_tool',
227
- 'A test tool',
228
- { type: 'object', properties: {} },
229
- async () => 'result',
230
- );
231
-
232
- // Both clients should have the tool registered
233
- const defsA = clientA.getToolDefinitions();
234
- const defsB = clientB.getToolDefinitions();
235
- expect(defsA).toHaveLength(1);
236
- expect(defsB).toHaveLength(1);
237
- expect(defsA[0]!.function.name).toBe('test_tool');
238
- });
239
- });
240
-
241
- describe('model aggregation', () => {
242
- it('aggregates models from all providers', async () => {
243
- const clientA = new MockClient('a');
244
- const clientB = new MockClient('b');
245
-
246
- router.addProvider({ id: 'a', client: clientA, priority: 0 });
247
- router.addProvider({ id: 'b', client: clientB, priority: 1 });
248
-
249
- const models = await router.getModels();
250
- expect(models).toContain('mock-a');
251
- expect(models).toContain('mock-b');
252
- });
253
- });
254
- });
@@ -1,179 +0,0 @@
1
- /**
2
- * Tests for stream-decoder.ts — Pluggable reasoning strategies
3
- */
4
- import { describe, it, expect } from 'bun:test';
5
- import {
6
- PassthroughDecoder,
7
- StandardChatDecoder,
8
- InterleavedReasoningDecoder,
9
- createDecoder,
10
- type DecodedEvent,
11
- } from '../stream-decoder.js';
12
-
13
- describe('PassthroughDecoder', () => {
14
- it('emits all tokens as text events', () => {
15
- const events: DecodedEvent[] = [];
16
- const decoder = new PassthroughDecoder(e => events.push(e));
17
-
18
- decoder.push('Hello');
19
- decoder.push(' world');
20
- decoder.flush();
21
-
22
- expect(events).toHaveLength(2);
23
- expect(events[0]).toEqual({ type: 'text', content: 'Hello' });
24
- expect(events[1]).toEqual({ type: 'text', content: ' world' });
25
- });
26
-
27
- it('returns clean content', () => {
28
- const decoder = new PassthroughDecoder(() => {});
29
- decoder.push('Hello ');
30
- decoder.push('world');
31
- decoder.flush();
32
- expect(decoder.getCleanContent()).toBe('Hello world');
33
- });
34
-
35
- it('returns no reasoning', () => {
36
- const decoder = new PassthroughDecoder(() => {});
37
- decoder.push('test');
38
- expect(decoder.getReasoning()).toBeUndefined();
39
- });
40
- });
41
-
42
- describe('StandardChatDecoder', () => {
43
- it('emits text events', () => {
44
- const events: DecodedEvent[] = [];
45
- const decoder = new StandardChatDecoder(e => events.push(e));
46
-
47
- decoder.push('Hello');
48
- expect(events).toHaveLength(1);
49
- expect(events[0]).toEqual({ type: 'text', content: 'Hello' });
50
- });
51
-
52
- it('emits reasoning events', () => {
53
- const events: DecodedEvent[] = [];
54
- const decoder = new StandardChatDecoder(e => events.push(e));
55
-
56
- decoder.pushReasoning('Thinking...');
57
- expect(events).toHaveLength(1);
58
- expect(events[0]).toEqual({ type: 'thinking', content: 'Thinking...' });
59
- });
60
-
61
- it('emits tool call events', () => {
62
- const events: DecodedEvent[] = [];
63
- const decoder = new StandardChatDecoder(e => events.push(e));
64
-
65
- const calls = [{
66
- id: 'call_1',
67
- type: 'function' as const,
68
- function: { name: 'get_time', arguments: '{}' },
69
- }];
70
- decoder.pushToolCalls(calls);
71
-
72
- expect(events).toHaveLength(1);
73
- expect(events[0]!.type).toBe('tool_call');
74
- });
75
-
76
- it('tracks both content and reasoning', () => {
77
- const decoder = new StandardChatDecoder(() => {});
78
- decoder.push('Text');
79
- decoder.pushReasoning('Reason');
80
- decoder.flush();
81
-
82
- expect(decoder.getCleanContent()).toBe('Text');
83
- expect(decoder.getReasoning()).toBe('Reason');
84
- });
85
- });
86
-
87
- describe('InterleavedReasoningDecoder', () => {
88
- it('extracts think tags from text', () => {
89
- const events: DecodedEvent[] = [];
90
- const decoder = new InterleavedReasoningDecoder(e => events.push(e));
91
-
92
- decoder.push('<think>I should analyze this</think>The answer is 42');
93
- decoder.flush();
94
-
95
- const thinkEvents = events.filter(e => e.type === 'thinking');
96
- const textEvents = events.filter(e => e.type === 'text');
97
-
98
- expect(thinkEvents.length).toBeGreaterThan(0);
99
- expect(textEvents.length).toBeGreaterThan(0);
100
- expect(decoder.getReasoning()).toBe('I should analyze this');
101
- expect(decoder.getCleanContent()).toBe('The answer is 42');
102
- });
103
-
104
- it('handles think tags split across chunks', () => {
105
- const events: DecodedEvent[] = [];
106
- const decoder = new InterleavedReasoningDecoder(e => events.push(e));
107
-
108
- decoder.push('<thi');
109
- decoder.push('nk>Split reasoning</th');
110
- decoder.push('ink>Done');
111
- decoder.flush();
112
-
113
- expect(decoder.getReasoning()).toBe('Split reasoning');
114
- expect(decoder.getCleanContent()).toBe('Done');
115
- });
116
-
117
- it('handles progress tags', () => {
118
- const events: DecodedEvent[] = [];
119
- const decoder = new InterleavedReasoningDecoder(e => events.push(e));
120
-
121
- decoder.push('<progress>Loading data</progress>Complete');
122
- decoder.flush();
123
-
124
- const progressEvents = events.filter(e => e.type === 'progress');
125
- expect(progressEvents.length).toBeGreaterThan(0);
126
- expect(decoder.getCleanContent()).toBe('Complete');
127
- });
128
-
129
- it('handles plain text with no tags', () => {
130
- const events: DecodedEvent[] = [];
131
- const decoder = new InterleavedReasoningDecoder(e => events.push(e));
132
-
133
- decoder.push('Just plain text');
134
- decoder.flush();
135
-
136
- expect(decoder.getCleanContent()).toBe('Just plain text');
137
- expect(decoder.getReasoning()).toBeUndefined();
138
- });
139
-
140
- it('handles empty reasoning', () => {
141
- const decoder = new InterleavedReasoningDecoder(() => {});
142
- decoder.push('<think></think>Content');
143
- decoder.flush();
144
-
145
- expect(decoder.getCleanContent()).toBe('Content');
146
- expect(decoder.getReasoning()).toBeUndefined();
147
- });
148
-
149
- it('handles multiple think blocks', () => {
150
- const decoder = new InterleavedReasoningDecoder(() => {});
151
-
152
- decoder.push('<think>First thought</think>Text<think>Second thought</think>More text');
153
- decoder.flush();
154
-
155
- expect(decoder.getReasoning()).toBe('First thoughtSecond thought');
156
- expect(decoder.getCleanContent()).toBe('TextMore text');
157
- });
158
- });
159
-
160
- describe('createDecoder', () => {
161
- it('creates passthrough decoder', () => {
162
- const decoder = createDecoder('passthrough', () => {});
163
- expect(decoder).toBeInstanceOf(PassthroughDecoder);
164
- });
165
-
166
- it('creates standard-chat decoder', () => {
167
- const decoder = createDecoder('standard-chat', () => {});
168
- expect(decoder).toBeInstanceOf(StandardChatDecoder);
169
- });
170
-
171
- it('creates interleaved-reasoning decoder', () => {
172
- const decoder = createDecoder('interleaved-reasoning', () => {});
173
- expect(decoder).toBeInstanceOf(InterleavedReasoningDecoder);
174
- });
175
-
176
- it('throws for unknown type', () => {
177
- expect(() => createDecoder('unknown' as never, () => {})).toThrow('Unknown decoder type');
178
- });
179
- });