universal-llm-client 4.5.0 → 4.5.1

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 (174) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +2 -0
  3. package/dist/ai-model.d.ts +0 -1
  4. package/dist/ai-model.js +0 -1
  5. package/dist/auditor.d.ts +0 -1
  6. package/dist/auditor.js +0 -1
  7. package/dist/client.d.ts +0 -1
  8. package/dist/client.js +0 -1
  9. package/dist/gemma-channel.d.ts +0 -1
  10. package/dist/gemma-channel.js +0 -1
  11. package/dist/gemma-diffusion.d.ts +0 -1
  12. package/dist/gemma-diffusion.js +0 -1
  13. package/dist/http.d.ts +0 -1
  14. package/dist/http.js +0 -1
  15. package/dist/index.d.ts +0 -1
  16. package/dist/index.js +0 -1
  17. package/dist/interfaces.d.ts +0 -1
  18. package/dist/interfaces.js +0 -1
  19. package/dist/mcp.d.ts +0 -1
  20. package/dist/mcp.js +0 -1
  21. package/dist/providers/anthropic.d.ts +0 -1
  22. package/dist/providers/anthropic.js +0 -1
  23. package/dist/providers/google.d.ts +0 -1
  24. package/dist/providers/google.js +0 -1
  25. package/dist/providers/index.d.ts +0 -1
  26. package/dist/providers/index.js +0 -1
  27. package/dist/providers/ollama.d.ts +0 -1
  28. package/dist/providers/ollama.js +0 -1
  29. package/dist/providers/openai.d.ts +2 -1
  30. package/dist/providers/openai.js +303 -74
  31. package/dist/router.d.ts +0 -1
  32. package/dist/router.js +0 -1
  33. package/dist/stream-decoder.d.ts +0 -1
  34. package/dist/stream-decoder.js +0 -1
  35. package/dist/structured-output.d.ts +0 -1
  36. package/dist/structured-output.js +0 -1
  37. package/dist/thinking.d.ts +0 -1
  38. package/dist/thinking.js +0 -1
  39. package/dist/tools.d.ts +0 -1
  40. package/dist/tools.js +0 -1
  41. package/dist/zod-adapter.d.ts +0 -1
  42. package/dist/zod-adapter.js +0 -1
  43. package/package.json +1 -2
  44. package/dist/ai-model.d.ts.map +0 -1
  45. package/dist/ai-model.js.map +0 -1
  46. package/dist/auditor.d.ts.map +0 -1
  47. package/dist/auditor.js.map +0 -1
  48. package/dist/client.d.ts.map +0 -1
  49. package/dist/client.js.map +0 -1
  50. package/dist/gemma-channel.d.ts.map +0 -1
  51. package/dist/gemma-channel.js.map +0 -1
  52. package/dist/gemma-diffusion.d.ts.map +0 -1
  53. package/dist/gemma-diffusion.js.map +0 -1
  54. package/dist/http.d.ts.map +0 -1
  55. package/dist/http.js.map +0 -1
  56. package/dist/index.d.ts.map +0 -1
  57. package/dist/index.js.map +0 -1
  58. package/dist/interfaces.d.ts.map +0 -1
  59. package/dist/interfaces.js.map +0 -1
  60. package/dist/mcp.d.ts.map +0 -1
  61. package/dist/mcp.js.map +0 -1
  62. package/dist/providers/anthropic.d.ts.map +0 -1
  63. package/dist/providers/anthropic.js.map +0 -1
  64. package/dist/providers/google.d.ts.map +0 -1
  65. package/dist/providers/google.js.map +0 -1
  66. package/dist/providers/index.d.ts.map +0 -1
  67. package/dist/providers/index.js.map +0 -1
  68. package/dist/providers/ollama.d.ts.map +0 -1
  69. package/dist/providers/ollama.js.map +0 -1
  70. package/dist/providers/openai.d.ts.map +0 -1
  71. package/dist/providers/openai.js.map +0 -1
  72. package/dist/router.d.ts.map +0 -1
  73. package/dist/router.js.map +0 -1
  74. package/dist/stream-decoder.d.ts.map +0 -1
  75. package/dist/stream-decoder.js.map +0 -1
  76. package/dist/structured-output.d.ts.map +0 -1
  77. package/dist/structured-output.js.map +0 -1
  78. package/dist/thinking.d.ts.map +0 -1
  79. package/dist/thinking.js.map +0 -1
  80. package/dist/tools.d.ts.map +0 -1
  81. package/dist/tools.js.map +0 -1
  82. package/dist/zod-adapter.d.ts.map +0 -1
  83. package/dist/zod-adapter.js.map +0 -1
  84. package/src/ai-model.ts +0 -400
  85. package/src/auditor.ts +0 -213
  86. package/src/client.ts +0 -402
  87. package/src/debug/debug-google-streaming.ts +0 -97
  88. package/src/debug/debug-tool-execution.ts +0 -86
  89. package/src/debug/test-lmstudio-tools.ts +0 -155
  90. package/src/demos/README.md +0 -47
  91. package/src/demos/basic/universal-llm-examples.ts +0 -161
  92. package/src/demos/diffusion-gemma/.env +0 -29
  93. package/src/demos/diffusion-gemma/.env.example +0 -27
  94. package/src/demos/diffusion-gemma/CLAUDE.md +0 -95
  95. package/src/demos/diffusion-gemma/README.md +0 -59
  96. package/src/demos/diffusion-gemma/canvas.ts +0 -1606
  97. package/src/demos/diffusion-gemma/docker-compose.yml +0 -29
  98. package/src/demos/diffusion-gemma/probe-stream.ts +0 -51
  99. package/src/demos/diffusion-gemma/probe-tools.ts +0 -55
  100. package/src/demos/diffusion-gemma/server.ts +0 -1205
  101. package/src/demos/diffusion-gemma/start-vllm.sh +0 -98
  102. package/src/demos/mcp/astrid-memory-demo.ts +0 -295
  103. package/src/demos/mcp/astrid-persona-memory.ts +0 -357
  104. package/src/demos/mcp/mcp-mongodb-demo.ts +0 -275
  105. package/src/demos/mcp/simple-astrid-memory.ts +0 -148
  106. package/src/demos/mcp/simple-mcp-demo.ts +0 -68
  107. package/src/demos/mcp/working-mcp-demo.ts +0 -62
  108. package/src/demos/model-alias-demo.ts +0 -0
  109. package/src/demos/tools/RAG_MEMORY_INTEGRATION.md +0 -267
  110. package/src/demos/tools/astrid-memory-demo.ts +0 -270
  111. package/src/demos/tools/astrid-production-memory-clean.ts +0 -785
  112. package/src/demos/tools/astrid-production-memory.ts +0 -558
  113. package/src/demos/tools/basic-translation-test.ts +0 -66
  114. package/src/demos/tools/chromadb-similarity-tuning.ts +0 -390
  115. package/src/demos/tools/clean-multilingual-conversation.ts +0 -209
  116. package/src/demos/tools/clean-translation-test.ts +0 -119
  117. package/src/demos/tools/clean-universal-multilingual-test.ts +0 -131
  118. package/src/demos/tools/complete-rag-demo.ts +0 -369
  119. package/src/demos/tools/complete-tool-demo.ts +0 -132
  120. package/src/demos/tools/demo-tool-calling.ts +0 -124
  121. package/src/demos/tools/dynamic-language-switching-test.ts +0 -251
  122. package/src/demos/tools/hybrid-thinking-test.ts +0 -154
  123. package/src/demos/tools/memory-integration-test.ts +0 -420
  124. package/src/demos/tools/multilingual-memory-system.ts +0 -802
  125. package/src/demos/tools/ondemand-translation-demo.ts +0 -655
  126. package/src/demos/tools/production-tool-demo.ts +0 -245
  127. package/src/demos/tools/revolutionary-multilingual-test.ts +0 -151
  128. package/src/demos/tools/rigorous-language-analysis.ts +0 -218
  129. package/src/demos/tools/test-universal-memory-system.ts +0 -126
  130. package/src/demos/tools/translation-integration-guide.ts +0 -346
  131. package/src/demos/tools/universal-memory-system.ts +0 -560
  132. package/src/gemma-channel.ts +0 -47
  133. package/src/gemma-diffusion.ts +0 -167
  134. package/src/http.ts +0 -261
  135. package/src/index.ts +0 -180
  136. package/src/interfaces.ts +0 -843
  137. package/src/mcp.ts +0 -345
  138. package/src/providers/anthropic.ts +0 -796
  139. package/src/providers/google.ts +0 -840
  140. package/src/providers/index.ts +0 -8
  141. package/src/providers/ollama.ts +0 -503
  142. package/src/providers/openai.ts +0 -587
  143. package/src/router.ts +0 -785
  144. package/src/stream-decoder.ts +0 -535
  145. package/src/structured-output.ts +0 -759
  146. package/src/test-scripts/test-advanced-tools.ts +0 -310
  147. package/src/test-scripts/test-google-deep-research.ts +0 -33
  148. package/src/test-scripts/test-google-streaming-enhanced.ts +0 -147
  149. package/src/test-scripts/test-google-streaming.ts +0 -63
  150. package/src/test-scripts/test-google-system-prompt-comprehensive.ts +0 -189
  151. package/src/test-scripts/test-google-thinking.ts +0 -46
  152. package/src/test-scripts/test-mcp-config.ts +0 -28
  153. package/src/test-scripts/test-mcp-connection.ts +0 -29
  154. package/src/test-scripts/test-system-message-positions.ts +0 -163
  155. package/src/test-scripts/test-system-prompt-improvement-demo.ts +0 -83
  156. package/src/test-scripts/test-tool-calling.ts +0 -231
  157. package/src/test-scripts/test-vllm-qwen36.ts +0 -256
  158. package/src/tests/ai-model.test.ts +0 -1614
  159. package/src/tests/auditor.test.ts +0 -224
  160. package/src/tests/gemma-diffusion.test.ts +0 -115
  161. package/src/tests/http.test.ts +0 -200
  162. package/src/tests/interfaces.test.ts +0 -117
  163. package/src/tests/providers/anthropic.test.ts +0 -118
  164. package/src/tests/providers/google.test.ts +0 -841
  165. package/src/tests/providers/ollama.test.ts +0 -1034
  166. package/src/tests/providers/openai.test.ts +0 -1511
  167. package/src/tests/router.test.ts +0 -254
  168. package/src/tests/stream-decoder.test.ts +0 -263
  169. package/src/tests/structured-output.test.ts +0 -1450
  170. package/src/tests/thinking.test.ts +0 -65
  171. package/src/tests/tools.test.ts +0 -175
  172. package/src/thinking.ts +0 -73
  173. package/src/tools.ts +0 -246
  174. package/src/zod-adapter.ts +0 -72
@@ -1,65 +0,0 @@
1
- /**
2
- * Unit tests for the unified thinking resolver and per-provider budget maps.
3
- */
4
- import { describe, test, expect } from 'bun:test';
5
- import {
6
- resolveThinking,
7
- isOpenAIReasoningModel,
8
- geminiThinkingBudget,
9
- anthropicThinkingBudget,
10
- } from '../thinking.js';
11
-
12
- describe('resolveThinking', () => {
13
- test('returns undefined when neither per-call nor config is set', () => {
14
- expect(resolveThinking(undefined, undefined)).toBeUndefined();
15
- });
16
-
17
- test('boolean true/false map to enabled', () => {
18
- expect(resolveThinking(undefined, true)).toEqual({ enabled: true });
19
- expect(resolveThinking(undefined, false)).toEqual({ enabled: false });
20
- });
21
-
22
- test('a level enables thinking and carries the level', () => {
23
- expect(resolveThinking('high', undefined)).toEqual({ enabled: true, level: 'high' });
24
- expect(resolveThinking(undefined, 'low')).toEqual({ enabled: true, level: 'low' });
25
- });
26
-
27
- test('per-call overrides config', () => {
28
- expect(resolveThinking(false, true)).toEqual({ enabled: false });
29
- expect(resolveThinking('medium', false)).toEqual({ enabled: true, level: 'medium' });
30
- });
31
-
32
- test('ignores unknown strings defensively', () => {
33
- expect(resolveThinking('ultra' as never, undefined)).toBeUndefined();
34
- });
35
- });
36
-
37
- describe('isOpenAIReasoningModel', () => {
38
- test('matches o-series and gpt-5 families', () => {
39
- for (const m of ['o1', 'o3', 'o4-mini', 'gpt-5', 'gpt-5-mini', 'GPT-5']) {
40
- expect(isOpenAIReasoningModel(m)).toBe(true);
41
- }
42
- });
43
- test('does not match chat / vLLM model names', () => {
44
- for (const m of ['gpt-4o', 'qwen3.6-nvfp4', 'gemini-3.5-flash', 'claude-sonnet-4-5']) {
45
- expect(isOpenAIReasoningModel(m)).toBe(false);
46
- }
47
- });
48
- });
49
-
50
- describe('budget maps', () => {
51
- test('gemini 2.5 budget by level (0/-1 semantics)', () => {
52
- expect(geminiThinkingBudget('minimal')).toBe(512);
53
- expect(geminiThinkingBudget('low')).toBe(2048);
54
- expect(geminiThinkingBudget('medium')).toBe(8192);
55
- expect(geminiThinkingBudget('high')).toBe(24576);
56
- expect(geminiThinkingBudget(undefined)).toBe(-1); // dynamic
57
- });
58
-
59
- test('anthropic budget stays >= 1024 and < max_tokens', () => {
60
- expect(anthropicThinkingBudget('high', 32000)).toBe(16384);
61
- expect(anthropicThinkingBudget('high', 4096)).toBe(3072); // clamped to max-1024
62
- expect(anthropicThinkingBudget(undefined, 4096)).toBe(2048); // bare true
63
- expect(anthropicThinkingBudget('low', 4096)).toBe(1024);
64
- });
65
- });
@@ -1,175 +0,0 @@
1
- /**
2
- * Tests for tools.ts — ToolBuilder and ToolExecutor
3
- */
4
- import { describe, it, expect } from 'bun:test';
5
- import { ToolBuilder, ToolExecutor, createTimeTool, createRandomNumberTool } from '../tools.js';
6
-
7
- describe('ToolBuilder', () => {
8
- it('builds a basic tool definition', () => {
9
- const tool = new ToolBuilder('test_tool')
10
- .description('A test tool')
11
- .build();
12
-
13
- expect(tool.type).toBe('function');
14
- expect(tool.function.name).toBe('test_tool');
15
- expect(tool.function.description).toBe('A test tool');
16
- expect(tool.function.parameters.type).toBe('object');
17
- });
18
-
19
- it('adds required parameters', () => {
20
- const tool = new ToolBuilder('search')
21
- .description('Search')
22
- .addParameter('query', 'string', 'Search query', true)
23
- .build();
24
-
25
- expect(tool.function.parameters.properties).toHaveProperty('query');
26
- expect(tool.function.parameters.required).toEqual(['query']);
27
- });
28
-
29
- it('adds optional parameters', () => {
30
- const tool = new ToolBuilder('search')
31
- .description('Search')
32
- .addParameter('query', 'string', 'Search query', true)
33
- .addParameter('limit', 'number', 'Max results', false)
34
- .build();
35
-
36
- expect(tool.function.parameters.properties).toHaveProperty('limit');
37
- expect(tool.function.parameters.required).toEqual(['query']);
38
- });
39
-
40
- it('adds parameters with extra schema properties', () => {
41
- const tool = new ToolBuilder('config')
42
- .description('Configure')
43
- .addParameter('theme', 'string', 'Color theme', false, {
44
- enum: ['light', 'dark'],
45
- })
46
- .build();
47
-
48
- const themeParam = (tool.function.parameters.properties as Record<string, Record<string, unknown>>)?.['theme'];
49
- expect(themeParam?.enum).toEqual(['light', 'dark']);
50
- });
51
-
52
- it('builds function definition only', () => {
53
- const fn = new ToolBuilder('test')
54
- .description('Test')
55
- .buildFunction();
56
-
57
- expect(fn.name).toBe('test');
58
- expect(fn.description).toBe('Test');
59
- });
60
- });
61
-
62
- describe('ToolExecutor', () => {
63
- describe('withTimeout', () => {
64
- it('resolves if handler completes in time', async () => {
65
- const handler = async () => 'result';
66
- const wrapped = ToolExecutor.withTimeout(handler, 1000);
67
- expect(await wrapped({})).toBe('result');
68
- });
69
-
70
- it('rejects if handler exceeds timeout', async () => {
71
- const handler = async () => {
72
- await new Promise(r => setTimeout(r, 500));
73
- return 'too late';
74
- };
75
- const wrapped = ToolExecutor.withTimeout(handler, 50);
76
- expect(wrapped({})).rejects.toThrow('timeout');
77
- });
78
- });
79
-
80
- describe('safe', () => {
81
- it('returns result on success', async () => {
82
- const handler = async () => 42;
83
- const wrapped = ToolExecutor.safe(handler);
84
- expect(await wrapped({})).toBe(42);
85
- });
86
-
87
- it('catches errors and returns error object', async () => {
88
- const handler = async () => { throw new Error('boom'); };
89
- const wrapped = ToolExecutor.safe(handler);
90
- const result = await wrapped({}) as { error: string };
91
- expect(result.error).toBe('boom');
92
- });
93
- });
94
-
95
- describe('withValidation', () => {
96
- it('passes through if all required fields present', async () => {
97
- const handler = async (args: unknown) => args;
98
- const wrapped = ToolExecutor.withValidation(handler, ['name']);
99
- expect(await wrapped({ name: 'test' })).toEqual({ name: 'test' });
100
- });
101
-
102
- it('throws if required field is missing', async () => {
103
- const handler = async () => 'ok';
104
- const wrapped = ToolExecutor.withValidation(handler, ['name']);
105
- expect(wrapped({ other: 'value' })).rejects.toThrow('Missing required argument: name');
106
- });
107
-
108
- it('throws if args is not an object', async () => {
109
- const handler = async () => 'ok';
110
- const wrapped = ToolExecutor.withValidation(handler, ['name']);
111
- expect(wrapped(null)).rejects.toThrow('must be an object');
112
- });
113
- });
114
-
115
- describe('timed', () => {
116
- it('returns result with duration', async () => {
117
- const handler = async () => 'hello';
118
- const wrapped = ToolExecutor.timed(handler);
119
- const result = await wrapped({}) as { result: string; duration: number };
120
- expect(result.result).toBe('hello');
121
- expect(result.duration).toBeGreaterThanOrEqual(0);
122
- });
123
- });
124
-
125
- describe('compose', () => {
126
- it('composes multiple wrappers', async () => {
127
- const handler = async () => 'result';
128
- const composed = ToolExecutor.compose(
129
- handler,
130
- h => ToolExecutor.safe(h),
131
- h => ToolExecutor.withTimeout(h, 5000),
132
- );
133
- expect(await composed({})).toBe('result');
134
- });
135
- });
136
- });
137
-
138
- describe('Common Tools', () => {
139
- describe('createTimeTool', () => {
140
- it('returns a valid tool definition', () => {
141
- const tool = createTimeTool();
142
- expect(tool.name).toBe('get_current_time');
143
- expect(tool.description).toBeTruthy();
144
- expect(tool.parameters.type).toBe('object');
145
- });
146
-
147
- it('handler returns time info', async () => {
148
- const tool = createTimeTool();
149
- const result = await tool.handler({}) as { iso: string; timestamp: number };
150
- expect(result.iso).toBeTruthy();
151
- expect(result.timestamp).toBeGreaterThan(0);
152
- });
153
-
154
- it('handler respects timezone parameter', async () => {
155
- const tool = createTimeTool();
156
- const result = await tool.handler({ timezone: 'America/New_York' }) as { timezone: string };
157
- expect(result.timezone).toBe('America/New_York');
158
- });
159
- });
160
-
161
- describe('createRandomNumberTool', () => {
162
- it('returns a valid tool definition', () => {
163
- const tool = createRandomNumberTool();
164
- expect(tool.name).toBe('random_number');
165
- expect(tool.description).toBeTruthy();
166
- });
167
-
168
- it('handler returns number in range', async () => {
169
- const tool = createRandomNumberTool();
170
- const result = await tool.handler({ min: 1, max: 10 }) as { value: number };
171
- expect(result.value).toBeGreaterThanOrEqual(1);
172
- expect(result.value).toBeLessThanOrEqual(10);
173
- });
174
- });
175
- });
package/src/thinking.ts DELETED
@@ -1,73 +0,0 @@
1
- /**
2
- * Unified thinking/reasoning resolution shared by all providers.
3
- *
4
- * Applications set a single `thinking` value — `true`/`false` or a level
5
- * ('minimal' | 'low' | 'medium' | 'high') — at the model level and/or per call.
6
- * Each provider maps the resolved intent to its native control (Gemini
7
- * `thinkingLevel`/`thinkingBudget`, OpenAI `reasoning_effort`, vLLM
8
- * `enable_thinking`, Anthropic `budget_tokens`, Ollama `think`).
9
- */
10
- import type { ThinkingLevel } from './interfaces.js';
11
-
12
- export interface ResolvedThinking {
13
- /** Whether reasoning should be enabled at all. */
14
- enabled: boolean;
15
- /** Explicit level when the user provided one (absent for a bare `true`). */
16
- level?: ThinkingLevel;
17
- }
18
-
19
- const LEVELS: readonly string[] = ['minimal', 'low', 'medium', 'high'];
20
-
21
- function isLevel(v: unknown): v is ThinkingLevel {
22
- return typeof v === 'string' && LEVELS.includes(v);
23
- }
24
-
25
- /**
26
- * Resolve the effective thinking intent from a per-call value (highest
27
- * precedence) and the model-level config value. Returns `undefined` when
28
- * neither is set, so providers omit the control entirely (and don't perturb
29
- * servers that reject unknown fields).
30
- */
31
- export function resolveThinking(
32
- perCall: boolean | ThinkingLevel | undefined,
33
- config: boolean | ThinkingLevel | undefined,
34
- ): ResolvedThinking | undefined {
35
- const value = perCall ?? config;
36
- if (value === undefined) return undefined;
37
- if (value === false) return { enabled: false };
38
- if (value === true) return { enabled: true };
39
- if (isLevel(value)) return { enabled: true, level: value };
40
- return undefined; // unknown string — ignore defensively
41
- }
42
-
43
- /** Heuristic: OpenAI reasoning models use `reasoning_effort` (o-series, GPT-5). */
44
- export function isOpenAIReasoningModel(model: string): boolean {
45
- return /^(o\d|gpt-5)/i.test(model);
46
- }
47
-
48
- /**
49
- * Gemini 2.5 `thinkingBudget` for a level. 0 disables, -1 is dynamic, and the
50
- * Flash range is 0–24576. A bare `true` (no level) maps to dynamic (-1).
51
- */
52
- export function geminiThinkingBudget(level: ThinkingLevel | undefined): number {
53
- switch (level) {
54
- case 'minimal': return 512;
55
- case 'low': return 2048;
56
- case 'medium': return 8192;
57
- case 'high': return 24576;
58
- default: return -1; // enabled without an explicit level → dynamic
59
- }
60
- }
61
-
62
- /**
63
- * Anthropic extended-thinking `budget_tokens` for a level, kept >= 1024 (the
64
- * API minimum) and < `maxTokens` (the API requires headroom for the answer).
65
- */
66
- export function anthropicThinkingBudget(level: ThinkingLevel | undefined, maxTokens: number): number {
67
- const base = level === 'high' ? 16384
68
- : level === 'medium' ? 4096
69
- : level === 'low' ? 1024
70
- : level === 'minimal' ? 1024
71
- : 2048; // bare `true`
72
- return Math.max(1024, Math.min(base, maxTokens - 1024));
73
- }
package/src/tools.ts DELETED
@@ -1,246 +0,0 @@
1
- /**
2
- * Universal LLM Client v3 — Tool Utilities
3
- *
4
- * ToolBuilder: Type-safe tool definition builder with fluent API.
5
- * ToolExecutor: Execution wrappers with timeout and validation.
6
- */
7
-
8
- import type { LLMFunction, LLMToolDefinition, ToolHandler } from './interfaces.js';
9
-
10
- // ============================================================================
11
- // ToolBuilder
12
- // ============================================================================
13
-
14
- /**
15
- * Fluent builder for LLM tool definitions.
16
- *
17
- * Usage:
18
- * const tool = new ToolBuilder('get_weather')
19
- * .description('Get current weather for a location')
20
- * .addParameter('location', 'string', 'City name', true)
21
- * .addParameter('units', 'string', 'Temperature units', false, { enum: ['celsius', 'fahrenheit'] })
22
- * .build();
23
- */
24
- export class ToolBuilder {
25
- private name: string;
26
- private desc: string = '';
27
- private properties: Record<string, unknown> = {};
28
- private required: string[] = [];
29
-
30
- constructor(name: string) {
31
- this.name = name;
32
- }
33
-
34
- description(desc: string): this {
35
- this.desc = desc;
36
- return this;
37
- }
38
-
39
- addParameter(
40
- name: string,
41
- type: string,
42
- description: string,
43
- isRequired: boolean = false,
44
- extra?: Record<string, unknown>,
45
- ): this {
46
- this.properties[name] = {
47
- type,
48
- description,
49
- ...extra,
50
- };
51
- if (isRequired) {
52
- this.required.push(name);
53
- }
54
- return this;
55
- }
56
-
57
- build(): LLMToolDefinition {
58
- return {
59
- type: 'function',
60
- function: {
61
- name: this.name,
62
- description: this.desc,
63
- parameters: {
64
- type: 'object',
65
- properties: this.properties,
66
- required: this.required.length > 0 ? this.required : undefined,
67
- },
68
- },
69
- };
70
- }
71
-
72
- /** Build and return the function definition only */
73
- buildFunction(): LLMFunction {
74
- return this.build().function;
75
- }
76
- }
77
-
78
- // ============================================================================
79
- // ToolExecutor
80
- // ============================================================================
81
-
82
- /**
83
- * Utility wrappers for creating safe tool handlers.
84
- */
85
- export class ToolExecutor {
86
- /**
87
- * Wrap a handler with a timeout.
88
- * Rejects if the handler doesn't complete within the specified ms.
89
- */
90
- static withTimeout(handler: ToolHandler, timeoutMs: number): ToolHandler {
91
- return async (args: unknown) => {
92
- const result = await Promise.race([
93
- Promise.resolve(handler(args)),
94
- new Promise<never>((_, reject) =>
95
- setTimeout(() => reject(new Error(`Tool execution timeout after ${timeoutMs}ms`)), timeoutMs),
96
- ),
97
- ]);
98
- return result;
99
- };
100
- }
101
-
102
- /**
103
- * Wrap a handler to catch errors and return them as strings
104
- * instead of throwing.
105
- */
106
- static safe(handler: ToolHandler): ToolHandler {
107
- return async (args: unknown) => {
108
- try {
109
- return await handler(args);
110
- } catch (error) {
111
- return {
112
- error: error instanceof Error ? error.message : String(error),
113
- };
114
- }
115
- };
116
- }
117
-
118
- /**
119
- * Wrap a handler with argument validation.
120
- * Checks that required fields are present before execution.
121
- */
122
- static withValidation(
123
- handler: ToolHandler,
124
- requiredFields: string[],
125
- ): ToolHandler {
126
- return async (args: unknown) => {
127
- if (!args || typeof args !== 'object') {
128
- throw new Error('Tool arguments must be an object');
129
- }
130
- const obj = args as Record<string, unknown>;
131
- for (const field of requiredFields) {
132
- if (obj[field] === undefined || obj[field] === null) {
133
- throw new Error(`Missing required argument: ${field}`);
134
- }
135
- }
136
- return handler(args);
137
- };
138
- }
139
-
140
- /**
141
- * Create a handler that measures execution time and
142
- * returns both the result and duration.
143
- */
144
- static timed(handler: ToolHandler): ToolHandler {
145
- return async (args: unknown) => {
146
- const start = Date.now();
147
- const result = await handler(args);
148
- return {
149
- result,
150
- duration: Date.now() - start,
151
- };
152
- };
153
- }
154
-
155
- /**
156
- * Compose multiple wrappers around a handler.
157
- * Applied from right to left (innermost to outermost).
158
- */
159
- static compose(
160
- handler: ToolHandler,
161
- ...wrappers: Array<(h: ToolHandler) => ToolHandler>
162
- ): ToolHandler {
163
- return wrappers.reduceRight((h, wrapper) => wrapper(h), handler);
164
- }
165
- }
166
-
167
- // ============================================================================
168
- // Common Tool Definitions
169
- // ============================================================================
170
-
171
- /**
172
- * Create a get_current_time tool definition and handler.
173
- */
174
- export function createTimeTool(): {
175
- name: string;
176
- description: string;
177
- parameters: LLMFunction['parameters'];
178
- handler: ToolHandler;
179
- } {
180
- return {
181
- name: 'get_current_time',
182
- description: 'Get the current date and time',
183
- parameters: {
184
- type: 'object',
185
- properties: {
186
- timezone: {
187
- type: 'string',
188
- description: 'IANA timezone (e.g. "America/New_York"). Defaults to UTC.',
189
- },
190
- },
191
- },
192
- handler: (args: unknown) => {
193
- const { timezone } = (args ?? {}) as { timezone?: string };
194
- const now = new Date();
195
- try {
196
- return {
197
- iso: now.toISOString(),
198
- formatted: now.toLocaleString('en-US', {
199
- timeZone: timezone || 'UTC',
200
- dateStyle: 'full',
201
- timeStyle: 'long',
202
- }),
203
- timezone: timezone || 'UTC',
204
- timestamp: now.getTime(),
205
- };
206
- } catch {
207
- return {
208
- iso: now.toISOString(),
209
- formatted: now.toUTCString(),
210
- timezone: 'UTC',
211
- timestamp: now.getTime(),
212
- };
213
- }
214
- },
215
- };
216
- }
217
-
218
- /**
219
- * Create a random_number tool definition and handler.
220
- */
221
- export function createRandomNumberTool(): {
222
- name: string;
223
- description: string;
224
- parameters: LLMFunction['parameters'];
225
- handler: ToolHandler;
226
- } {
227
- return {
228
- name: 'random_number',
229
- description: 'Generate a random number within a range',
230
- parameters: {
231
- type: 'object',
232
- properties: {
233
- min: { type: 'number', description: 'Minimum value (default 0)' },
234
- max: { type: 'number', description: 'Maximum value (default 100)' },
235
- },
236
- },
237
- handler: (args: unknown) => {
238
- const { min = 0, max = 100 } = (args ?? {}) as { min?: number; max?: number };
239
- return {
240
- value: Math.floor(Math.random() * (max - min + 1)) + min,
241
- min,
242
- max,
243
- };
244
- },
245
- };
246
- }
@@ -1,72 +0,0 @@
1
- /**
2
- * Zod Adapter for Universal LLM Client
3
- *
4
- * Optional entrypoint for projects that use Zod for schema validation.
5
- * Import from 'universal-llm-client/zod' to use.
6
- *
7
- * @module universal-llm-client/zod
8
- */
9
-
10
- import { z } from 'zod';
11
- import type { SchemaConfig } from './structured-output.js';
12
-
13
- /**
14
- * Create a SchemaConfig from a Zod schema.
15
- *
16
- * This bridges Zod's type-safe schema definitions to the library's
17
- * generic SchemaConfig interface, using Zod 4's native `z.toJSONSchema()`.
18
- *
19
- * @template T The type inferred from the Zod schema
20
- * @param schema The Zod schema
21
- * @param options Optional name and description for LLM guidance
22
- * @returns SchemaConfig ready for use with generateStructured, etc.
23
- *
24
- * @example
25
- * ```typescript
26
- * import { fromZod } from 'universal-llm-client/zod';
27
- * import { z } from 'zod';
28
- *
29
- * const UserSchema = z.object({
30
- * name: z.string(),
31
- * age: z.number(),
32
- * });
33
- *
34
- * const config = fromZod(UserSchema, { name: 'User' });
35
- *
36
- * const user = await model.generateStructured(config, messages);
37
- * // user.name: string, user.age: number (fully typed)
38
- * ```
39
- */
40
- export function fromZod<T>(
41
- schema: z.ZodType<T>,
42
- options?: { name?: string; description?: string },
43
- ): SchemaConfig<T> {
44
- // Convert Zod schema to JSON Schema using Zod 4's native method
45
- const rawJsonSchema = z.toJSONSchema(schema, {
46
- target: 'draft-07',
47
- unrepresentable: 'any',
48
- });
49
-
50
- // Clean up — remove $schema since providers don't need it
51
- const jsonSchema = { ...rawJsonSchema } as Record<string, unknown>;
52
- delete jsonSchema.$schema;
53
-
54
- return {
55
- jsonSchema: jsonSchema as import('./structured-output.js').JSONSchema,
56
- validate: (data: unknown): T => {
57
- const result = schema.safeParse(data);
58
- if (!result.success) {
59
- throw result.error;
60
- }
61
- return result.data;
62
- },
63
- name: options?.name,
64
- description: options?.description,
65
- };
66
- }
67
-
68
- // Re-export z for convenience (users importing from /zod likely want it)
69
- export { z } from 'zod';
70
-
71
- // Re-export SchemaConfig for type usage
72
- export type { SchemaConfig } from './structured-output.js';