@shykaruu/jarvis-brain 0.4.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.
- package/LICENSE +153 -0
- package/README.md +428 -0
- package/bin/jarvis.ts +449 -0
- package/package.json +79 -0
- package/roles/activity-observer.yaml +60 -0
- package/roles/ceo-founder.yaml +144 -0
- package/roles/chief-of-staff.yaml +158 -0
- package/roles/dev-lead.yaml +182 -0
- package/roles/executive-assistant.yaml +77 -0
- package/roles/marketing-director.yaml +168 -0
- package/roles/personal-assistant.yaml +266 -0
- package/roles/research-specialist.yaml +60 -0
- package/roles/specialists/content-writer.yaml +53 -0
- package/roles/specialists/customer-support.yaml +57 -0
- package/roles/specialists/data-analyst.yaml +57 -0
- package/roles/specialists/financial-analyst.yaml +56 -0
- package/roles/specialists/hr-specialist.yaml +55 -0
- package/roles/specialists/legal-advisor.yaml +58 -0
- package/roles/specialists/marketing-strategist.yaml +56 -0
- package/roles/specialists/project-coordinator.yaml +55 -0
- package/roles/specialists/research-analyst.yaml +58 -0
- package/roles/specialists/software-engineer.yaml +57 -0
- package/roles/specialists/system-administrator.yaml +57 -0
- package/roles/system-admin.yaml +76 -0
- package/scripts/ensure-bun.cjs +16 -0
- package/src/actions/README.md +421 -0
- package/src/actions/app-control/desktop-controller.test.ts +26 -0
- package/src/actions/app-control/desktop-controller.ts +438 -0
- package/src/actions/app-control/interface.ts +64 -0
- package/src/actions/app-control/linux.ts +273 -0
- package/src/actions/app-control/macos.ts +54 -0
- package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
- package/src/actions/app-control/sidecar-launcher.ts +286 -0
- package/src/actions/app-control/windows.ts +44 -0
- package/src/actions/browser/cdp.ts +138 -0
- package/src/actions/browser/chrome-launcher.ts +261 -0
- package/src/actions/browser/session.ts +506 -0
- package/src/actions/browser/stealth.ts +49 -0
- package/src/actions/index.ts +20 -0
- package/src/actions/terminal/executor.ts +157 -0
- package/src/actions/terminal/wsl-bridge.ts +126 -0
- package/src/actions/test.ts +93 -0
- package/src/actions/tools/agents.ts +363 -0
- package/src/actions/tools/builtin.ts +950 -0
- package/src/actions/tools/commitments.ts +192 -0
- package/src/actions/tools/content.ts +217 -0
- package/src/actions/tools/delegate.ts +147 -0
- package/src/actions/tools/desktop.test.ts +55 -0
- package/src/actions/tools/desktop.ts +305 -0
- package/src/actions/tools/documents.ts +169 -0
- package/src/actions/tools/goals.ts +376 -0
- package/src/actions/tools/local-tools-guard.ts +31 -0
- package/src/actions/tools/registry.ts +173 -0
- package/src/actions/tools/research.ts +111 -0
- package/src/actions/tools/sidecar-list.ts +57 -0
- package/src/actions/tools/sidecar-route.ts +105 -0
- package/src/actions/tools/workflows.ts +216 -0
- package/src/agents/agent.ts +132 -0
- package/src/agents/delegation.ts +107 -0
- package/src/agents/hierarchy.ts +113 -0
- package/src/agents/index.ts +19 -0
- package/src/agents/messaging.ts +125 -0
- package/src/agents/orchestrator.ts +592 -0
- package/src/agents/role-discovery.ts +61 -0
- package/src/agents/sub-agent-runner.ts +309 -0
- package/src/agents/task-manager.ts +151 -0
- package/src/authority/approval-delivery.ts +59 -0
- package/src/authority/approval.ts +196 -0
- package/src/authority/audit.ts +158 -0
- package/src/authority/authority.test.ts +519 -0
- package/src/authority/deferred-executor.ts +103 -0
- package/src/authority/emergency.ts +66 -0
- package/src/authority/engine.ts +301 -0
- package/src/authority/index.ts +12 -0
- package/src/authority/learning.ts +111 -0
- package/src/authority/tool-action-map.ts +74 -0
- package/src/awareness/analytics.ts +466 -0
- package/src/awareness/awareness.test.ts +332 -0
- package/src/awareness/capture-engine.ts +305 -0
- package/src/awareness/context-graph.ts +130 -0
- package/src/awareness/context-tracker.ts +349 -0
- package/src/awareness/index.ts +25 -0
- package/src/awareness/intelligence.ts +321 -0
- package/src/awareness/ocr-engine.ts +88 -0
- package/src/awareness/service.ts +528 -0
- package/src/awareness/struggle-detector.ts +342 -0
- package/src/awareness/suggestion-engine.ts +476 -0
- package/src/awareness/types.ts +201 -0
- package/src/cli/autostart.ts +417 -0
- package/src/cli/deps.ts +449 -0
- package/src/cli/doctor.ts +238 -0
- package/src/cli/helpers.ts +401 -0
- package/src/cli/onboard.ts +827 -0
- package/src/cli/uninstall.test.ts +37 -0
- package/src/cli/uninstall.ts +202 -0
- package/src/comms/README.md +329 -0
- package/src/comms/auth-error.html +48 -0
- package/src/comms/channels/discord.ts +228 -0
- package/src/comms/channels/signal.ts +56 -0
- package/src/comms/channels/telegram.ts +316 -0
- package/src/comms/channels/whatsapp.ts +60 -0
- package/src/comms/channels.test.ts +173 -0
- package/src/comms/dashboard-auth.ts +75 -0
- package/src/comms/desktop-notify.ts +114 -0
- package/src/comms/example.ts +129 -0
- package/src/comms/index.ts +129 -0
- package/src/comms/streaming.ts +149 -0
- package/src/comms/voice.test.ts +504 -0
- package/src/comms/voice.ts +341 -0
- package/src/comms/websocket.test.ts +409 -0
- package/src/comms/websocket.ts +669 -0
- package/src/config/README.md +389 -0
- package/src/config/index.ts +6 -0
- package/src/config/loader.test.ts +183 -0
- package/src/config/loader.ts +148 -0
- package/src/config/types.ts +293 -0
- package/src/daemon/README.md +232 -0
- package/src/daemon/agent-service-interface.ts +9 -0
- package/src/daemon/agent-service.ts +667 -0
- package/src/daemon/api-routes.ts +3067 -0
- package/src/daemon/background-agent-service.ts +396 -0
- package/src/daemon/background-agent.test.ts +78 -0
- package/src/daemon/channel-service.ts +201 -0
- package/src/daemon/commitment-executor.ts +297 -0
- package/src/daemon/dashboard-auth.test.ts +170 -0
- package/src/daemon/event-classifier.ts +239 -0
- package/src/daemon/event-coalescer.ts +123 -0
- package/src/daemon/event-reactor.ts +214 -0
- package/src/daemon/flock.c +7 -0
- package/src/daemon/health.ts +220 -0
- package/src/daemon/index.ts +1070 -0
- package/src/daemon/llm-settings.test.ts +78 -0
- package/src/daemon/llm-settings.ts +450 -0
- package/src/daemon/observer-service.ts +150 -0
- package/src/daemon/pid.test.ts +283 -0
- package/src/daemon/pid.ts +224 -0
- package/src/daemon/research-queue.ts +155 -0
- package/src/daemon/services.ts +175 -0
- package/src/daemon/ws-service.ts +926 -0
- package/src/global.d.ts +4 -0
- package/src/goals/accountability.ts +240 -0
- package/src/goals/awareness-bridge.ts +185 -0
- package/src/goals/estimator.ts +185 -0
- package/src/goals/events.ts +28 -0
- package/src/goals/goals.test.ts +400 -0
- package/src/goals/integration.test.ts +329 -0
- package/src/goals/nl-builder.test.ts +220 -0
- package/src/goals/nl-builder.ts +256 -0
- package/src/goals/rhythm.test.ts +177 -0
- package/src/goals/rhythm.ts +275 -0
- package/src/goals/service.test.ts +135 -0
- package/src/goals/service.ts +407 -0
- package/src/goals/types.ts +106 -0
- package/src/goals/workflow-bridge.ts +96 -0
- package/src/integrations/google-api.ts +134 -0
- package/src/integrations/google-auth.ts +175 -0
- package/src/llm/README.md +291 -0
- package/src/llm/anthropic.ts +400 -0
- package/src/llm/gemini.ts +380 -0
- package/src/llm/groq.ts +406 -0
- package/src/llm/history.ts +147 -0
- package/src/llm/index.ts +21 -0
- package/src/llm/manager.ts +226 -0
- package/src/llm/ollama.ts +316 -0
- package/src/llm/openai.ts +411 -0
- package/src/llm/openrouter.ts +390 -0
- package/src/llm/provider.test.ts +487 -0
- package/src/llm/provider.ts +61 -0
- package/src/llm/test.ts +88 -0
- package/src/observers/README.md +278 -0
- package/src/observers/calendar.ts +113 -0
- package/src/observers/clipboard.ts +136 -0
- package/src/observers/email.ts +109 -0
- package/src/observers/example.ts +58 -0
- package/src/observers/file-watcher.ts +124 -0
- package/src/observers/index.ts +159 -0
- package/src/observers/notifications.ts +197 -0
- package/src/observers/observers.test.ts +203 -0
- package/src/observers/processes.ts +225 -0
- package/src/personality/README.md +61 -0
- package/src/personality/adapter.ts +196 -0
- package/src/personality/index.ts +20 -0
- package/src/personality/learner.ts +209 -0
- package/src/personality/model.ts +132 -0
- package/src/personality/personality.test.ts +236 -0
- package/src/roles/README.md +252 -0
- package/src/roles/authority.ts +120 -0
- package/src/roles/example-usage.ts +198 -0
- package/src/roles/index.ts +42 -0
- package/src/roles/loader.ts +143 -0
- package/src/roles/prompt-builder.ts +218 -0
- package/src/roles/test-multi.ts +102 -0
- package/src/roles/test-role.yaml +77 -0
- package/src/roles/test-utils.ts +93 -0
- package/src/roles/test.ts +106 -0
- package/src/roles/tool-guide.ts +195 -0
- package/src/roles/types.ts +36 -0
- package/src/roles/utils.ts +200 -0
- package/src/scripts/google-setup.ts +168 -0
- package/src/sidecar/connection.ts +179 -0
- package/src/sidecar/index.ts +6 -0
- package/src/sidecar/manager.ts +542 -0
- package/src/sidecar/protocol.ts +85 -0
- package/src/sidecar/rpc.ts +161 -0
- package/src/sidecar/scheduler.ts +136 -0
- package/src/sidecar/types.ts +112 -0
- package/src/sidecar/validator.ts +144 -0
- package/src/sites/builder-tools.ts +215 -0
- package/src/sites/dev-server-manager.ts +286 -0
- package/src/sites/fixtures/security-test-site/.jarvis-project.json +6 -0
- package/src/sites/fixtures/security-test-site/Makefile +15 -0
- package/src/sites/fixtures/security-test-site/README.md +18 -0
- package/src/sites/fixtures/security-test-site/index.html +12 -0
- package/src/sites/fixtures/security-test-site/index.ts +16 -0
- package/src/sites/fixtures/security-test-site/package.json +13 -0
- package/src/sites/fixtures/security-test-site/src/app.tsx +780 -0
- package/src/sites/fixtures/security-test-site/tsconfig.json +10 -0
- package/src/sites/git-manager.ts +240 -0
- package/src/sites/github-manager.ts +355 -0
- package/src/sites/index.ts +25 -0
- package/src/sites/project-manager.ts +389 -0
- package/src/sites/proxy.ts +133 -0
- package/src/sites/service.ts +136 -0
- package/src/sites/templates.ts +169 -0
- package/src/sites/types.ts +89 -0
- package/src/user/profile-followup.test.ts +84 -0
- package/src/user/profile-followup.ts +185 -0
- package/src/user/profile.ts +224 -0
- package/src/vault/README.md +110 -0
- package/src/vault/awareness.ts +341 -0
- package/src/vault/commitments.ts +299 -0
- package/src/vault/content-pipeline.ts +270 -0
- package/src/vault/conversations.ts +173 -0
- package/src/vault/dashboard-sessions.ts +44 -0
- package/src/vault/documents.ts +130 -0
- package/src/vault/entities.ts +185 -0
- package/src/vault/extractor.test.ts +356 -0
- package/src/vault/extractor.ts +345 -0
- package/src/vault/facts.ts +190 -0
- package/src/vault/goals.ts +477 -0
- package/src/vault/index.ts +87 -0
- package/src/vault/keychain.ts +99 -0
- package/src/vault/observations.ts +115 -0
- package/src/vault/relationships.ts +178 -0
- package/src/vault/retrieval.test.ts +139 -0
- package/src/vault/retrieval.ts +258 -0
- package/src/vault/schema.ts +709 -0
- package/src/vault/settings.ts +38 -0
- package/src/vault/user-profile.test.ts +113 -0
- package/src/vault/user-profile.ts +176 -0
- package/src/vault/vectors.ts +92 -0
- package/src/vault/webapp-template-seeds.ts +116 -0
- package/src/vault/webapp-templates.ts +244 -0
- package/src/vault/workflows.ts +403 -0
- package/src/workflows/auto-suggest.ts +290 -0
- package/src/workflows/engine.ts +366 -0
- package/src/workflows/events.ts +24 -0
- package/src/workflows/executor.ts +207 -0
- package/src/workflows/nl-builder.ts +198 -0
- package/src/workflows/nodes/actions/agent-task.ts +73 -0
- package/src/workflows/nodes/actions/calendar-action.ts +85 -0
- package/src/workflows/nodes/actions/code-execution.ts +73 -0
- package/src/workflows/nodes/actions/discord.ts +77 -0
- package/src/workflows/nodes/actions/file-write.ts +73 -0
- package/src/workflows/nodes/actions/gmail.ts +69 -0
- package/src/workflows/nodes/actions/http-request.ts +117 -0
- package/src/workflows/nodes/actions/notification.ts +85 -0
- package/src/workflows/nodes/actions/run-tool.ts +55 -0
- package/src/workflows/nodes/actions/send-message.ts +82 -0
- package/src/workflows/nodes/actions/shell-command.ts +76 -0
- package/src/workflows/nodes/actions/telegram.ts +60 -0
- package/src/workflows/nodes/builtin.ts +119 -0
- package/src/workflows/nodes/error/error-handler.ts +37 -0
- package/src/workflows/nodes/error/fallback.ts +47 -0
- package/src/workflows/nodes/error/retry.ts +82 -0
- package/src/workflows/nodes/logic/delay.ts +42 -0
- package/src/workflows/nodes/logic/if-else.ts +41 -0
- package/src/workflows/nodes/logic/loop.ts +90 -0
- package/src/workflows/nodes/logic/merge.ts +38 -0
- package/src/workflows/nodes/logic/race.ts +40 -0
- package/src/workflows/nodes/logic/switch.ts +59 -0
- package/src/workflows/nodes/logic/template-render.ts +53 -0
- package/src/workflows/nodes/logic/variable-get.ts +37 -0
- package/src/workflows/nodes/logic/variable-set.ts +59 -0
- package/src/workflows/nodes/registry.ts +99 -0
- package/src/workflows/nodes/transform/aggregate.ts +99 -0
- package/src/workflows/nodes/transform/csv-parse.ts +70 -0
- package/src/workflows/nodes/transform/json-parse.ts +63 -0
- package/src/workflows/nodes/transform/map-filter.ts +84 -0
- package/src/workflows/nodes/transform/regex-match.ts +89 -0
- package/src/workflows/nodes/triggers/calendar.ts +33 -0
- package/src/workflows/nodes/triggers/clipboard.ts +32 -0
- package/src/workflows/nodes/triggers/cron.ts +40 -0
- package/src/workflows/nodes/triggers/email.ts +40 -0
- package/src/workflows/nodes/triggers/file-change.ts +45 -0
- package/src/workflows/nodes/triggers/git.ts +46 -0
- package/src/workflows/nodes/triggers/manual.ts +23 -0
- package/src/workflows/nodes/triggers/poll.ts +81 -0
- package/src/workflows/nodes/triggers/process.ts +44 -0
- package/src/workflows/nodes/triggers/screen-event.ts +37 -0
- package/src/workflows/nodes/triggers/webhook.ts +39 -0
- package/src/workflows/safe-eval.ts +139 -0
- package/src/workflows/template.ts +118 -0
- package/src/workflows/triggers/cron.ts +311 -0
- package/src/workflows/triggers/manager.ts +285 -0
- package/src/workflows/triggers/observer-bridge.ts +172 -0
- package/src/workflows/triggers/poller.ts +201 -0
- package/src/workflows/triggers/screen-condition.ts +218 -0
- package/src/workflows/triggers/triggers.test.ts +740 -0
- package/src/workflows/triggers/webhook.ts +191 -0
- package/src/workflows/types.ts +133 -0
- package/src/workflows/variables.ts +72 -0
- package/src/workflows/workflows.test.ts +383 -0
- package/src/workflows/yaml.ts +104 -0
- package/ui/dist/index-3gr23jt9.js +112614 -0
- package/ui/dist/index-9vmj8127.css +14239 -0
- package/ui/dist/index-hy9pc1gm.js +112873 -0
- package/ui/dist/index-j2ep5d1w.js +112374 -0
- package/ui/dist/index-jt00vjqs.js +112858 -0
- package/ui/dist/index-k9ymx5qb.js +112374 -0
- package/ui/dist/index.html +16 -0
- package/ui/public/audio/pcm-capture-processor.js +11 -0
- package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
- package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
- package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
- package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
- package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import { test, expect, describe, beforeEach, afterEach, mock } from 'bun:test';
|
|
2
|
+
import { AnthropicProvider } from './anthropic.ts';
|
|
3
|
+
import { OpenAIProvider, isOfficialOpenAI, resolveOpenAIBaseUrl } from './openai.ts';
|
|
4
|
+
import { GroqProvider } from './groq.ts';
|
|
5
|
+
import { OllamaProvider } from './ollama.ts';
|
|
6
|
+
import { OpenRouterProvider } from './openrouter.ts';
|
|
7
|
+
import { LLMManager } from './manager.ts';
|
|
8
|
+
import { guardImageSize, type LLMMessage, type ContentBlock } from './provider.ts';
|
|
9
|
+
import { isToolResult, type ToolResult } from '../actions/tools/registry.ts';
|
|
10
|
+
|
|
11
|
+
describe('LLM Provider Types', () => {
|
|
12
|
+
test('AnthropicProvider can be instantiated', () => {
|
|
13
|
+
const provider = new AnthropicProvider('test-key', 'test-model');
|
|
14
|
+
expect(provider.name).toBe('anthropic');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('OpenAIProvider can be instantiated', () => {
|
|
18
|
+
const provider = new OpenAIProvider('test-key', 'test-model');
|
|
19
|
+
expect(provider.name).toBe('openai');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('GroqProvider can be instantiated', () => {
|
|
23
|
+
const provider = new GroqProvider('test-key', 'test-model');
|
|
24
|
+
expect(provider.name).toBe('groq');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('OllamaProvider can be instantiated', () => {
|
|
28
|
+
const provider = new OllamaProvider('http://localhost:11434', 'llama3');
|
|
29
|
+
expect(provider.name).toBe('ollama');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('OpenRouterProvider can be instantiated', () => {
|
|
33
|
+
const provider = new OpenRouterProvider('test-key', 'anthropic/claude-sonnet-4');
|
|
34
|
+
expect(provider.name).toBe('openrouter');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('LLMManager', () => {
|
|
39
|
+
test('can register providers', () => {
|
|
40
|
+
const manager = new LLMManager();
|
|
41
|
+
const anthropic = new AnthropicProvider('test-key');
|
|
42
|
+
|
|
43
|
+
manager.registerProvider(anthropic);
|
|
44
|
+
expect(manager.getProvider('anthropic')).toBe(anthropic);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('sets first registered provider as primary', () => {
|
|
48
|
+
const manager = new LLMManager();
|
|
49
|
+
const anthropic = new AnthropicProvider('test-key');
|
|
50
|
+
|
|
51
|
+
manager.registerProvider(anthropic);
|
|
52
|
+
// Primary is set automatically
|
|
53
|
+
expect(manager.getProvider('anthropic')).toBeDefined();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('can change primary provider', () => {
|
|
57
|
+
const manager = new LLMManager();
|
|
58
|
+
const anthropic = new AnthropicProvider('test-key-1');
|
|
59
|
+
const openai = new OpenAIProvider('test-key-2');
|
|
60
|
+
|
|
61
|
+
manager.registerProvider(anthropic);
|
|
62
|
+
manager.registerProvider(openai);
|
|
63
|
+
manager.setPrimary('openai');
|
|
64
|
+
|
|
65
|
+
// Should not throw
|
|
66
|
+
expect(manager.getProvider('openai')).toBeDefined();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('throws when setting non-existent provider as primary', () => {
|
|
70
|
+
const manager = new LLMManager();
|
|
71
|
+
expect(() => manager.setPrimary('nonexistent')).toThrow();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('can set fallback chain', () => {
|
|
75
|
+
const manager = new LLMManager();
|
|
76
|
+
const anthropic = new AnthropicProvider('test-key-1');
|
|
77
|
+
const openai = new OpenAIProvider('test-key-2');
|
|
78
|
+
|
|
79
|
+
manager.registerProvider(anthropic);
|
|
80
|
+
manager.registerProvider(openai);
|
|
81
|
+
manager.setPrimary('anthropic');
|
|
82
|
+
manager.setFallbackChain(['openai']);
|
|
83
|
+
|
|
84
|
+
// Should not throw
|
|
85
|
+
expect(manager.getProvider('anthropic')).toBeDefined();
|
|
86
|
+
expect(manager.getProvider('openai')).toBeDefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('throws when setting non-existent fallback provider', () => {
|
|
90
|
+
const manager = new LLMManager();
|
|
91
|
+
const anthropic = new AnthropicProvider('test-key');
|
|
92
|
+
|
|
93
|
+
manager.registerProvider(anthropic);
|
|
94
|
+
expect(() => manager.setFallbackChain(['nonexistent'])).toThrow();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('Message Types', () => {
|
|
99
|
+
test('LLMMessage has correct structure', () => {
|
|
100
|
+
const message: LLMMessage = {
|
|
101
|
+
role: 'user',
|
|
102
|
+
content: 'Hello',
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
expect(message.role).toBe('user');
|
|
106
|
+
expect(message.content).toBe('Hello');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('supports all message roles', () => {
|
|
110
|
+
const messages: LLMMessage[] = [
|
|
111
|
+
{ role: 'system', content: 'You are helpful' },
|
|
112
|
+
{ role: 'user', content: 'Hello' },
|
|
113
|
+
{ role: 'assistant', content: 'Hi there' },
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
expect(messages).toHaveLength(3);
|
|
117
|
+
expect(messages[0]!.role).toBe('system');
|
|
118
|
+
expect(messages[1]!.role).toBe('user');
|
|
119
|
+
expect(messages[2]!.role).toBe('assistant');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('Provider URLs', () => {
|
|
124
|
+
test('AnthropicProvider uses correct API URL', () => {
|
|
125
|
+
const provider = new AnthropicProvider('test-key') as any;
|
|
126
|
+
expect(provider.apiUrl).toBe('https://api.anthropic.com/v1/messages');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('OpenAIProvider uses correct API URL', () => {
|
|
130
|
+
const provider = new OpenAIProvider('test-key') as any;
|
|
131
|
+
expect(provider.apiUrl).toBe('https://api.openai.com/v1/chat/completions');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('OpenAIProvider uses custom compatible base URL as the request base', () => {
|
|
135
|
+
const provider = new OpenAIProvider('test-key', 'test-model', 'https://gateway.example.com/v1/') as any;
|
|
136
|
+
expect(provider.baseUrl).toBe('https://gateway.example.com/v1/');
|
|
137
|
+
expect(provider.apiUrl).toBe('https://gateway.example.com/v1/chat/completions');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('OpenAIProvider preserves custom path prefixes in compatible base URLs', () => {
|
|
141
|
+
const provider = new OpenAIProvider('test-key', 'test-model', 'https://gateway.example.com/openai/') as any;
|
|
142
|
+
expect(provider.baseUrl).toBe('https://gateway.example.com/openai/');
|
|
143
|
+
expect(provider.apiUrl).toBe('https://gateway.example.com/openai/chat/completions');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('OpenRouterProvider uses correct API URL', () => {
|
|
147
|
+
const provider = new OpenRouterProvider('test-key') as any;
|
|
148
|
+
expect(provider.apiUrl).toBe('https://openrouter.ai/api/v1/chat/completions');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('GroqProvider uses correct API URL', () => {
|
|
152
|
+
const provider = new GroqProvider('test-key') as any;
|
|
153
|
+
expect(provider.apiUrl).toBe('https://api.groq.com/openai/v1/chat/completions');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('OllamaProvider uses correct base URL', () => {
|
|
157
|
+
const provider = new OllamaProvider() as any;
|
|
158
|
+
expect(provider.baseUrl).toBe('http://localhost:11434');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('OllamaProvider removes trailing slash from base URL', () => {
|
|
162
|
+
const provider = new OllamaProvider('http://localhost:11434/') as any;
|
|
163
|
+
expect(provider.baseUrl).toBe('http://localhost:11434');
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('OpenAI base URL resolution', () => {
|
|
168
|
+
test('uses official OpenAI API when omitted', () => {
|
|
169
|
+
expect(resolveOpenAIBaseUrl().toString()).toBe('https://api.openai.com/v1/');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('adds a trailing slash so relative endpoints resolve consistently', () => {
|
|
173
|
+
expect(resolveOpenAIBaseUrl('https://gateway.example.com/v1').toString()).toBe('https://gateway.example.com/v1/');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('preserves the provided base path', () => {
|
|
177
|
+
expect(resolveOpenAIBaseUrl('https://gateway.example.com/openai').toString()).toBe('https://gateway.example.com/openai/');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('rejects invalid URLs', () => {
|
|
181
|
+
expect(() => resolveOpenAIBaseUrl('not-a-url')).toThrow();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('treats the default endpoint as official OpenAI', () => {
|
|
185
|
+
expect(isOfficialOpenAI()).toBe(true);
|
|
186
|
+
expect(isOfficialOpenAI('https://api.openai.com/v1')).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('treats custom compatible gateways as non-official OpenAI', () => {
|
|
190
|
+
expect(isOfficialOpenAI('https://gateway.example.com/openai')).toBe(false);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('Default Models', () => {
|
|
195
|
+
test('AnthropicProvider has correct default model', () => {
|
|
196
|
+
const provider = new AnthropicProvider('test-key') as any;
|
|
197
|
+
expect(provider.defaultModel).toBe('claude-sonnet-4-5-20250929');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('OpenAIProvider has correct default model', () => {
|
|
201
|
+
const provider = new OpenAIProvider('test-key') as any;
|
|
202
|
+
expect(provider.defaultModel).toBe('gpt-4o');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('OllamaProvider has correct default model', () => {
|
|
206
|
+
const provider = new OllamaProvider() as any;
|
|
207
|
+
expect(provider.defaultModel).toBe('llama3');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('GroqProvider has correct default model', () => {
|
|
211
|
+
const provider = new GroqProvider('test-key') as any;
|
|
212
|
+
expect(provider.defaultModel).toBe('llama-3.3-70b-versatile');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('OpenRouterProvider has correct default model', () => {
|
|
216
|
+
const provider = new OpenRouterProvider('test-key') as any;
|
|
217
|
+
expect(provider.defaultModel).toBe('anthropic/claude-sonnet-4');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('can override default models', () => {
|
|
221
|
+
const anthropic = new AnthropicProvider('key', 'custom-model') as any;
|
|
222
|
+
const openai = new OpenAIProvider('key', 'custom-model') as any;
|
|
223
|
+
const groq = new GroqProvider('key', 'custom-model') as any;
|
|
224
|
+
const ollama = new OllamaProvider('http://localhost:11434', 'custom-model') as any;
|
|
225
|
+
const openrouter = new OpenRouterProvider('key', 'custom-model') as any;
|
|
226
|
+
|
|
227
|
+
expect(anthropic.defaultModel).toBe('custom-model');
|
|
228
|
+
expect(openai.defaultModel).toBe('custom-model');
|
|
229
|
+
expect(groq.defaultModel).toBe('custom-model');
|
|
230
|
+
expect(ollama.defaultModel).toBe('custom-model');
|
|
231
|
+
expect(openrouter.defaultModel).toBe('custom-model');
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe('Vision Support', () => {
|
|
236
|
+
describe('guardImageSize', () => {
|
|
237
|
+
test('passes text blocks through unchanged', () => {
|
|
238
|
+
const block: ContentBlock = { type: 'text', text: 'hello' };
|
|
239
|
+
expect(guardImageSize(block)).toBe(block);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('passes small images through unchanged', () => {
|
|
243
|
+
const block: ContentBlock = {
|
|
244
|
+
type: 'image',
|
|
245
|
+
source: { type: 'base64', media_type: 'image/png', data: 'abc123' },
|
|
246
|
+
};
|
|
247
|
+
expect(guardImageSize(block)).toBe(block);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('replaces oversized images with text warning', () => {
|
|
251
|
+
const bigData = 'x'.repeat(6 * 1024 * 1024); // 6 MB
|
|
252
|
+
const block: ContentBlock = {
|
|
253
|
+
type: 'image',
|
|
254
|
+
source: { type: 'base64', media_type: 'image/png', data: bigData },
|
|
255
|
+
};
|
|
256
|
+
const result = guardImageSize(block);
|
|
257
|
+
expect(result.type).toBe('text');
|
|
258
|
+
expect((result as { type: 'text'; text: string }).text).toContain('too large');
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe('isToolResult', () => {
|
|
263
|
+
test('returns true for valid ToolResult', () => {
|
|
264
|
+
const tr: ToolResult = {
|
|
265
|
+
content: [{ type: 'text', text: 'hello' }],
|
|
266
|
+
};
|
|
267
|
+
expect(isToolResult(tr)).toBe(true);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('returns false for plain string', () => {
|
|
271
|
+
expect(isToolResult('hello')).toBe(false);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('returns false for null', () => {
|
|
275
|
+
expect(isToolResult(null)).toBe(false);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test('returns false for object without content array', () => {
|
|
279
|
+
expect(isToolResult({ content: 'not an array' })).toBe(false);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test('returns false for object with no content field', () => {
|
|
283
|
+
expect(isToolResult({ data: 'something' })).toBe(false);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe('ContentBlock in LLMMessage', () => {
|
|
288
|
+
test('LLMMessage accepts string content', () => {
|
|
289
|
+
const msg: LLMMessage = { role: 'user', content: 'Hello' };
|
|
290
|
+
expect(typeof msg.content).toBe('string');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test('LLMMessage accepts ContentBlock[] content', () => {
|
|
294
|
+
const msg: LLMMessage = {
|
|
295
|
+
role: 'tool',
|
|
296
|
+
content: [
|
|
297
|
+
{ type: 'text', text: 'Screenshot captured' },
|
|
298
|
+
{ type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'abc' } },
|
|
299
|
+
],
|
|
300
|
+
tool_call_id: 'test-id',
|
|
301
|
+
};
|
|
302
|
+
expect(Array.isArray(msg.content)).toBe(true);
|
|
303
|
+
expect((msg.content as ContentBlock[]).length).toBe(2);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe('Tool Call Conversion', () => {
|
|
309
|
+
const toolUseConversation: LLMMessage[] = [
|
|
310
|
+
{ role: 'system', content: 'You are helpful' },
|
|
311
|
+
{ role: 'user', content: 'What time is it?' },
|
|
312
|
+
{
|
|
313
|
+
role: 'assistant',
|
|
314
|
+
content: '',
|
|
315
|
+
tool_calls: [
|
|
316
|
+
{ id: 'call_1', name: 'get_time', arguments: { timezone: 'UTC' } },
|
|
317
|
+
],
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
role: 'tool',
|
|
321
|
+
content: '2026-03-30T12:00:00Z',
|
|
322
|
+
tool_call_id: 'call_1',
|
|
323
|
+
},
|
|
324
|
+
];
|
|
325
|
+
|
|
326
|
+
test('OpenAIProvider preserves tool_calls on assistant messages', () => {
|
|
327
|
+
const provider = new OpenAIProvider('test-key') as any;
|
|
328
|
+
const converted = provider.convertMessages(toolUseConversation);
|
|
329
|
+
|
|
330
|
+
const assistant = converted[2];
|
|
331
|
+
expect(assistant.role).toBe('assistant');
|
|
332
|
+
expect(assistant.tool_calls).toBeDefined();
|
|
333
|
+
expect(assistant.tool_calls).toHaveLength(1);
|
|
334
|
+
expect(assistant.tool_calls[0].id).toBe('call_1');
|
|
335
|
+
expect(assistant.tool_calls[0].type).toBe('function');
|
|
336
|
+
expect(assistant.tool_calls[0].function.name).toBe('get_time');
|
|
337
|
+
expect(assistant.tool_calls[0].function.arguments).toBe('{"timezone":"UTC"}');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test('OpenAIProvider preserves tool_call_id on tool messages', () => {
|
|
341
|
+
const provider = new OpenAIProvider('test-key') as any;
|
|
342
|
+
const converted = provider.convertMessages(toolUseConversation);
|
|
343
|
+
|
|
344
|
+
const tool = converted[3];
|
|
345
|
+
expect(tool.role).toBe('tool');
|
|
346
|
+
expect(tool.tool_call_id).toBe('call_1');
|
|
347
|
+
expect(tool.content).toBe('2026-03-30T12:00:00Z');
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test('GroqProvider preserves tool_calls on assistant messages', () => {
|
|
351
|
+
const provider = new GroqProvider('test-key') as any;
|
|
352
|
+
const converted = provider.convertMessages(toolUseConversation);
|
|
353
|
+
|
|
354
|
+
const assistant = converted[2];
|
|
355
|
+
expect(assistant.role).toBe('assistant');
|
|
356
|
+
expect(assistant.tool_calls).toBeDefined();
|
|
357
|
+
expect(assistant.tool_calls).toHaveLength(1);
|
|
358
|
+
expect(assistant.tool_calls[0].id).toBe('call_1');
|
|
359
|
+
expect(assistant.tool_calls[0].type).toBe('function');
|
|
360
|
+
expect(assistant.tool_calls[0].function.name).toBe('get_time');
|
|
361
|
+
expect(assistant.tool_calls[0].function.arguments).toBe('{"timezone":"UTC"}');
|
|
362
|
+
expect(assistant.content).toBeNull();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test('GroqProvider preserves tool_call_id on tool messages', () => {
|
|
366
|
+
const provider = new GroqProvider('test-key') as any;
|
|
367
|
+
const converted = provider.convertMessages(toolUseConversation);
|
|
368
|
+
|
|
369
|
+
const tool = converted[3];
|
|
370
|
+
expect(tool.role).toBe('tool');
|
|
371
|
+
expect(tool.tool_call_id).toBe('call_1');
|
|
372
|
+
expect(tool.content).toBe('2026-03-30T12:00:00Z');
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test('Messages without tool_calls omit the field', () => {
|
|
376
|
+
const provider = new OpenAIProvider('test-key') as any;
|
|
377
|
+
const converted = provider.convertMessages([
|
|
378
|
+
{ role: 'user', content: 'Hello' },
|
|
379
|
+
{ role: 'assistant', content: 'Hi there' },
|
|
380
|
+
]);
|
|
381
|
+
expect(converted[0].tool_calls).toBeUndefined();
|
|
382
|
+
expect(converted[1].tool_calls).toBeUndefined();
|
|
383
|
+
expect(converted[0].tool_call_id).toBeUndefined();
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
describe('Groq request shaping', () => {
|
|
388
|
+
const originalFetch = globalThis.fetch;
|
|
389
|
+
|
|
390
|
+
beforeEach(() => {
|
|
391
|
+
globalThis.fetch = mock(async (_url: string, init?: RequestInit) => {
|
|
392
|
+
return new Response(JSON.stringify({
|
|
393
|
+
id: 'cmpl_test',
|
|
394
|
+
object: 'chat.completion',
|
|
395
|
+
created: Date.now(),
|
|
396
|
+
model: 'llama-test',
|
|
397
|
+
choices: [
|
|
398
|
+
{
|
|
399
|
+
index: 0,
|
|
400
|
+
message: { role: 'assistant', content: 'ok' },
|
|
401
|
+
finish_reason: 'stop',
|
|
402
|
+
},
|
|
403
|
+
],
|
|
404
|
+
usage: {
|
|
405
|
+
prompt_tokens: 10,
|
|
406
|
+
completion_tokens: 5,
|
|
407
|
+
total_tokens: 15,
|
|
408
|
+
},
|
|
409
|
+
}), {
|
|
410
|
+
status: 200,
|
|
411
|
+
headers: {
|
|
412
|
+
'Content-Type': 'application/json',
|
|
413
|
+
'x-test-body': typeof init?.body === 'string' ? init.body : '',
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
}) as unknown as typeof fetch;
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
afterEach(() => {
|
|
420
|
+
globalThis.fetch = originalFetch;
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test('GroqProvider uses Groq-compatible tool fields', async () => {
|
|
424
|
+
const provider = new GroqProvider('test-key') as any;
|
|
425
|
+
const messages: LLMMessage[] = [
|
|
426
|
+
{ role: 'system', content: 'You are helpful.' },
|
|
427
|
+
{ role: 'user', content: 'Use the weather tool.' },
|
|
428
|
+
];
|
|
429
|
+
|
|
430
|
+
await provider.chat(messages, {
|
|
431
|
+
max_tokens: 321,
|
|
432
|
+
tools: [
|
|
433
|
+
{
|
|
434
|
+
name: 'weather_lookup',
|
|
435
|
+
description: 'Look up weather',
|
|
436
|
+
parameters: {
|
|
437
|
+
type: 'object',
|
|
438
|
+
properties: {
|
|
439
|
+
city: { type: 'string' },
|
|
440
|
+
},
|
|
441
|
+
required: ['city'],
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
],
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
const fetchMock = globalThis.fetch as unknown as ReturnType<typeof mock>;
|
|
448
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
449
|
+
const init = fetchMock.mock.calls[0]?.[1] as RequestInit;
|
|
450
|
+
const body = JSON.parse(String(init.body));
|
|
451
|
+
|
|
452
|
+
expect(body.max_completion_tokens).toBe(321);
|
|
453
|
+
expect(body.max_tokens).toBeUndefined();
|
|
454
|
+
expect(body.tool_choice).toBe('auto');
|
|
455
|
+
expect(body.parallel_tool_calls).toBe(true);
|
|
456
|
+
expect(body.tools).toHaveLength(1);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
test('GroqProvider trims oversized history but keeps system and latest turn', async () => {
|
|
460
|
+
const provider = new GroqProvider('test-key') as any;
|
|
461
|
+
const long = 'x'.repeat(12_000);
|
|
462
|
+
const messages: LLMMessage[] = [
|
|
463
|
+
{ role: 'system', content: 'System prompt' },
|
|
464
|
+
{ role: 'user', content: long },
|
|
465
|
+
{ role: 'assistant', content: long },
|
|
466
|
+
{ role: 'user', content: 'latest question' },
|
|
467
|
+
];
|
|
468
|
+
|
|
469
|
+
await provider.chat(messages, {
|
|
470
|
+
tools: [
|
|
471
|
+
{
|
|
472
|
+
name: 'delegate_task',
|
|
473
|
+
description: 'Delegate focused work',
|
|
474
|
+
parameters: { type: 'object', properties: {}, required: [] },
|
|
475
|
+
},
|
|
476
|
+
],
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
const fetchMock = globalThis.fetch as unknown as ReturnType<typeof mock>;
|
|
480
|
+
const init = fetchMock.mock.calls[0]?.[1] as RequestInit;
|
|
481
|
+
const body = JSON.parse(String(init.body));
|
|
482
|
+
|
|
483
|
+
expect(body.messages[0].role).toBe('system');
|
|
484
|
+
expect(body.messages.at(-1).content).toBe('latest question');
|
|
485
|
+
expect(body.messages.length).toBeLessThan(messages.length);
|
|
486
|
+
});
|
|
487
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type ContentBlock =
|
|
2
|
+
| { type: 'text'; text: string }
|
|
3
|
+
| { type: 'image'; source: { type: 'base64'; media_type: string; data: string } };
|
|
4
|
+
|
|
5
|
+
export type LLMMessage = {
|
|
6
|
+
role: 'system' | 'user' | 'assistant' | 'tool';
|
|
7
|
+
content: string | ContentBlock[];
|
|
8
|
+
tool_calls?: LLMToolCall[]; // present on assistant messages with tool use
|
|
9
|
+
tool_call_id?: string; // present on tool result messages
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type LLMTool = {
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
parameters: Record<string, unknown>; // JSON Schema
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type LLMToolCall = {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
arguments: Record<string, unknown>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type LLMResponse = {
|
|
25
|
+
content: string;
|
|
26
|
+
tool_calls: LLMToolCall[];
|
|
27
|
+
usage: { input_tokens: number; output_tokens: number };
|
|
28
|
+
model: string;
|
|
29
|
+
finish_reason: 'stop' | 'tool_use' | 'length' | 'error';
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type LLMStreamEvent =
|
|
33
|
+
| { type: 'text'; text: string }
|
|
34
|
+
| { type: 'tool_call'; tool_call: LLMToolCall }
|
|
35
|
+
| { type: 'done'; response: LLMResponse }
|
|
36
|
+
| { type: 'error'; error: string };
|
|
37
|
+
|
|
38
|
+
export type LLMOptions = {
|
|
39
|
+
model?: string;
|
|
40
|
+
temperature?: number;
|
|
41
|
+
max_tokens?: number;
|
|
42
|
+
tools?: LLMTool[];
|
|
43
|
+
stream?: boolean;
|
|
44
|
+
tool_choice?: 'auto' | 'none' | 'required'; // 'auto' enables tool calling when available
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export interface LLMProvider {
|
|
48
|
+
name: string;
|
|
49
|
+
chat(messages: LLMMessage[], options?: LLMOptions): Promise<LLMResponse>;
|
|
50
|
+
stream(messages: LLMMessage[], options?: LLMOptions): AsyncIterable<LLMStreamEvent>;
|
|
51
|
+
listModels(): Promise<string[]>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const MAX_IMAGE_BYTES = 5 * 1024 * 1024; // 5 MB base64 limit
|
|
55
|
+
|
|
56
|
+
export function guardImageSize(block: ContentBlock): ContentBlock {
|
|
57
|
+
if (block.type === 'image' && block.source.data.length > MAX_IMAGE_BYTES) {
|
|
58
|
+
return { type: 'text', text: '[Image too large to send — saved to disk instead]' };
|
|
59
|
+
}
|
|
60
|
+
return block;
|
|
61
|
+
}
|
package/src/llm/test.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manual test file for LLM providers
|
|
3
|
+
*
|
|
4
|
+
* Run with: bun run src/llm/test.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { LLMManager, AnthropicProvider, OpenAIProvider, OllamaProvider } from './index.ts';
|
|
8
|
+
import { loadConfig } from '../config/index.ts';
|
|
9
|
+
|
|
10
|
+
async function testProviders() {
|
|
11
|
+
console.log('Loading config...');
|
|
12
|
+
const config = await loadConfig();
|
|
13
|
+
|
|
14
|
+
const manager = new LLMManager();
|
|
15
|
+
|
|
16
|
+
// Register providers based on config
|
|
17
|
+
if (config.llm.anthropic?.api_key) {
|
|
18
|
+
const anthropic = new AnthropicProvider(
|
|
19
|
+
config.llm.anthropic.api_key,
|
|
20
|
+
config.llm.anthropic.model
|
|
21
|
+
);
|
|
22
|
+
manager.registerProvider(anthropic);
|
|
23
|
+
console.log('Registered Anthropic provider');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (config.llm.openai?.api_key) {
|
|
27
|
+
const openai = new OpenAIProvider(
|
|
28
|
+
config.llm.openai.api_key,
|
|
29
|
+
config.llm.openai.model,
|
|
30
|
+
config.llm.openai.base_url,
|
|
31
|
+
);
|
|
32
|
+
manager.registerProvider(openai);
|
|
33
|
+
console.log('Registered OpenAI provider');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (config.llm.ollama) {
|
|
37
|
+
const ollama = new OllamaProvider(
|
|
38
|
+
config.llm.ollama.base_url,
|
|
39
|
+
config.llm.ollama.model
|
|
40
|
+
);
|
|
41
|
+
manager.registerProvider(ollama);
|
|
42
|
+
console.log('Registered Ollama provider');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Set primary and fallbacks
|
|
46
|
+
manager.setPrimary(config.llm.primary);
|
|
47
|
+
manager.setFallbackChain(config.llm.fallback);
|
|
48
|
+
|
|
49
|
+
console.log(`\nPrimary provider: ${config.llm.primary}`);
|
|
50
|
+
console.log(`Fallback chain: ${config.llm.fallback.join(', ')}\n`);
|
|
51
|
+
|
|
52
|
+
// Test basic chat
|
|
53
|
+
console.log('Testing chat...');
|
|
54
|
+
const messages = [
|
|
55
|
+
{ role: 'system' as const, content: 'You are a helpful assistant.' },
|
|
56
|
+
{ role: 'user' as const, content: 'Say hello in exactly 5 words.' },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const response = await manager.chat(messages);
|
|
61
|
+
console.log('Response:', response.content);
|
|
62
|
+
console.log('Model:', response.model);
|
|
63
|
+
console.log('Usage:', response.usage);
|
|
64
|
+
console.log('Finish reason:', response.finish_reason);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error('Chat failed:', err);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Test streaming
|
|
70
|
+
console.log('\n\nTesting streaming...');
|
|
71
|
+
try {
|
|
72
|
+
for await (const event of manager.stream(messages)) {
|
|
73
|
+
if (event.type === 'text') {
|
|
74
|
+
process.stdout.write(event.text);
|
|
75
|
+
} else if (event.type === 'done') {
|
|
76
|
+
console.log('\n\nStream completed!');
|
|
77
|
+
console.log('Model:', event.response.model);
|
|
78
|
+
console.log('Usage:', event.response.usage);
|
|
79
|
+
} else if (event.type === 'error') {
|
|
80
|
+
console.error('Stream error:', event.error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.error('Stream failed:', err);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
testProviders().catch(console.error);
|