nova-terminal-assistant 0.1.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.
Potentially problematic release.
This version of nova-terminal-assistant might be problematic. Click here for more details.
- package/README.md +358 -0
- package/bin/nova +38 -0
- package/bin/nova.js +12 -0
- package/package.json +67 -0
- package/src/cli/commands/SmartCompletion.ts +458 -0
- package/src/cli/index.ts +5 -0
- package/src/cli/startup/IFlowRepl.ts +212 -0
- package/src/cli/startup/InkBasedRepl.ts +1056 -0
- package/src/cli/startup/InteractiveRepl.ts +2833 -0
- package/src/cli/startup/NovaApp.ts +1861 -0
- package/src/cli/startup/index.ts +4 -0
- package/src/cli/startup/parseArgs.ts +293 -0
- package/src/cli/test-modules.ts +27 -0
- package/src/cli/ui/IFlowDropdown.ts +425 -0
- package/src/cli/ui/ModernReplUI.ts +276 -0
- package/src/cli/ui/SimpleSelector2.ts +215 -0
- package/src/cli/ui/components/ConfirmDialog.ts +176 -0
- package/src/cli/ui/components/ErrorPanel.ts +364 -0
- package/src/cli/ui/components/InkAppRunner.tsx +67 -0
- package/src/cli/ui/components/InkComponents.tsx +613 -0
- package/src/cli/ui/components/NovaInkApp.tsx +312 -0
- package/src/cli/ui/components/ProgressBar.ts +177 -0
- package/src/cli/ui/components/ProgressIndicator.ts +298 -0
- package/src/cli/ui/components/QuickActions.ts +396 -0
- package/src/cli/ui/components/SimpleErrorPanel.ts +231 -0
- package/src/cli/ui/components/StatusBar.ts +194 -0
- package/src/cli/ui/components/ThinkingBlockRenderer.ts +401 -0
- package/src/cli/ui/components/index.ts +27 -0
- package/src/cli/ui/ink-prototype.tsx +347 -0
- package/src/cli/utils/CliUI.ts +336 -0
- package/src/cli/utils/CompletionHelper.ts +388 -0
- package/src/cli/utils/EnhancedCompleter.test.ts +226 -0
- package/src/cli/utils/EnhancedCompleter.ts +513 -0
- package/src/cli/utils/ErrorEnhancer.ts +429 -0
- package/src/cli/utils/OutputFormatter.ts +193 -0
- package/src/cli/utils/index.ts +9 -0
- package/src/core/agents/AgentOrchestrator.ts +515 -0
- package/src/core/agents/index.ts +17 -0
- package/src/core/audit/AuditLogger.ts +509 -0
- package/src/core/audit/index.ts +11 -0
- package/src/core/auth/AuthManager.d.ts.map +1 -0
- package/src/core/auth/AuthManager.ts +138 -0
- package/src/core/auth/index.d.ts.map +1 -0
- package/src/core/auth/index.ts +2 -0
- package/src/core/config/ConfigManager.d.ts.map +1 -0
- package/src/core/config/ConfigManager.test.ts +183 -0
- package/src/core/config/ConfigManager.ts +1219 -0
- package/src/core/config/index.d.ts.map +1 -0
- package/src/core/config/index.ts +1 -0
- package/src/core/context/ContextBuilder.d.ts.map +1 -0
- package/src/core/context/ContextBuilder.ts +171 -0
- package/src/core/context/ContextCompressor.d.ts.map +1 -0
- package/src/core/context/ContextCompressor.ts +642 -0
- package/src/core/context/LayeredMemoryManager.ts +657 -0
- package/src/core/context/MemoryDiscovery.d.ts.map +1 -0
- package/src/core/context/MemoryDiscovery.ts +175 -0
- package/src/core/context/defaultSystemPrompt.d.ts.map +1 -0
- package/src/core/context/defaultSystemPrompt.ts +35 -0
- package/src/core/context/index.d.ts.map +1 -0
- package/src/core/context/index.ts +22 -0
- package/src/core/extensions/SkillGenerator.ts +421 -0
- package/src/core/extensions/SkillInstaller.d.ts.map +1 -0
- package/src/core/extensions/SkillInstaller.ts +257 -0
- package/src/core/extensions/SkillRegistry.d.ts.map +1 -0
- package/src/core/extensions/SkillRegistry.ts +361 -0
- package/src/core/extensions/SkillValidator.ts +525 -0
- package/src/core/extensions/index.ts +15 -0
- package/src/core/index.d.ts.map +1 -0
- package/src/core/index.ts +42 -0
- package/src/core/mcp/McpManager.d.ts.map +1 -0
- package/src/core/mcp/McpManager.ts +632 -0
- package/src/core/mcp/index.d.ts.map +1 -0
- package/src/core/mcp/index.ts +2 -0
- package/src/core/model/ModelClient.d.ts.map +1 -0
- package/src/core/model/ModelClient.ts +217 -0
- package/src/core/model/ModelConnectionTester.ts +363 -0
- package/src/core/model/ModelValidator.ts +348 -0
- package/src/core/model/index.d.ts.map +1 -0
- package/src/core/model/index.ts +6 -0
- package/src/core/model/providers/AnthropicProvider.d.ts.map +1 -0
- package/src/core/model/providers/AnthropicProvider.ts +279 -0
- package/src/core/model/providers/CodingPlanProvider.d.ts.map +1 -0
- package/src/core/model/providers/CodingPlanProvider.ts +210 -0
- package/src/core/model/providers/OllamaCloudProvider.d.ts.map +1 -0
- package/src/core/model/providers/OllamaCloudProvider.ts +405 -0
- package/src/core/model/providers/OllamaManager.d.ts.map +1 -0
- package/src/core/model/providers/OllamaManager.ts +201 -0
- package/src/core/model/providers/OllamaProvider.d.ts.map +1 -0
- package/src/core/model/providers/OllamaProvider.ts +73 -0
- package/src/core/model/providers/OpenAICompatibleProvider.d.ts.map +1 -0
- package/src/core/model/providers/OpenAICompatibleProvider.ts +327 -0
- package/src/core/model/providers/OpenAIProvider.d.ts.map +1 -0
- package/src/core/model/providers/OpenAIProvider.ts +29 -0
- package/src/core/model/providers/index.d.ts.map +1 -0
- package/src/core/model/providers/index.ts +12 -0
- package/src/core/model/types.d.ts.map +1 -0
- package/src/core/model/types.ts +77 -0
- package/src/core/security/ApprovalManager.d.ts.map +1 -0
- package/src/core/security/ApprovalManager.ts +174 -0
- package/src/core/security/FileFilter.d.ts.map +1 -0
- package/src/core/security/FileFilter.ts +141 -0
- package/src/core/security/HookExecutor.d.ts.map +1 -0
- package/src/core/security/HookExecutor.ts +178 -0
- package/src/core/security/SandboxExecutor.ts +447 -0
- package/src/core/security/index.d.ts.map +1 -0
- package/src/core/security/index.ts +8 -0
- package/src/core/session/AgentLoop.d.ts.map +1 -0
- package/src/core/session/AgentLoop.ts +501 -0
- package/src/core/session/SessionManager.d.ts.map +1 -0
- package/src/core/session/SessionManager.test.ts +183 -0
- package/src/core/session/SessionManager.ts +460 -0
- package/src/core/session/index.d.ts.map +1 -0
- package/src/core/session/index.ts +3 -0
- package/src/core/telemetry/Telemetry.d.ts.map +1 -0
- package/src/core/telemetry/Telemetry.ts +90 -0
- package/src/core/telemetry/TelemetryService.ts +531 -0
- package/src/core/telemetry/index.d.ts.map +1 -0
- package/src/core/telemetry/index.ts +12 -0
- package/src/core/testing/AutoFixer.ts +385 -0
- package/src/core/testing/ErrorAnalyzer.ts +499 -0
- package/src/core/testing/TestRunner.ts +265 -0
- package/src/core/testing/agent-cli-tests.ts +538 -0
- package/src/core/testing/index.ts +11 -0
- package/src/core/tools/ToolRegistry.d.ts.map +1 -0
- package/src/core/tools/ToolRegistry.test.ts +206 -0
- package/src/core/tools/ToolRegistry.ts +260 -0
- package/src/core/tools/impl/EditFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/EditFileTool.ts +97 -0
- package/src/core/tools/impl/ListDirectoryTool.d.ts.map +1 -0
- package/src/core/tools/impl/ListDirectoryTool.ts +142 -0
- package/src/core/tools/impl/MemoryTool.d.ts.map +1 -0
- package/src/core/tools/impl/MemoryTool.ts +102 -0
- package/src/core/tools/impl/ReadFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/ReadFileTool.ts +58 -0
- package/src/core/tools/impl/SearchContentTool.d.ts.map +1 -0
- package/src/core/tools/impl/SearchContentTool.ts +94 -0
- package/src/core/tools/impl/SearchFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/SearchFileTool.ts +61 -0
- package/src/core/tools/impl/ShellTool.d.ts.map +1 -0
- package/src/core/tools/impl/ShellTool.ts +118 -0
- package/src/core/tools/impl/TaskTool.d.ts.map +1 -0
- package/src/core/tools/impl/TaskTool.ts +207 -0
- package/src/core/tools/impl/TodoTool.d.ts.map +1 -0
- package/src/core/tools/impl/TodoTool.ts +122 -0
- package/src/core/tools/impl/WebFetchTool.d.ts.map +1 -0
- package/src/core/tools/impl/WebFetchTool.ts +103 -0
- package/src/core/tools/impl/WebSearchTool.d.ts.map +1 -0
- package/src/core/tools/impl/WebSearchTool.ts +89 -0
- package/src/core/tools/impl/WriteFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/WriteFileTool.ts +49 -0
- package/src/core/tools/impl/index.d.ts.map +1 -0
- package/src/core/tools/impl/index.ts +16 -0
- package/src/core/tools/index.d.ts.map +1 -0
- package/src/core/tools/index.ts +7 -0
- package/src/core/tools/schemas/execution.d.ts.map +1 -0
- package/src/core/tools/schemas/execution.ts +42 -0
- package/src/core/tools/schemas/file.d.ts.map +1 -0
- package/src/core/tools/schemas/file.ts +119 -0
- package/src/core/tools/schemas/index.d.ts.map +1 -0
- package/src/core/tools/schemas/index.ts +11 -0
- package/src/core/tools/schemas/memory.d.ts.map +1 -0
- package/src/core/tools/schemas/memory.ts +52 -0
- package/src/core/tools/schemas/orchestration.d.ts.map +1 -0
- package/src/core/tools/schemas/orchestration.ts +44 -0
- package/src/core/tools/schemas/search.d.ts.map +1 -0
- package/src/core/tools/schemas/search.ts +112 -0
- package/src/core/tools/schemas/todo.d.ts.map +1 -0
- package/src/core/tools/schemas/todo.ts +32 -0
- package/src/core/tools/schemas/web.d.ts.map +1 -0
- package/src/core/tools/schemas/web.ts +86 -0
- package/src/core/types/config.d.ts.map +1 -0
- package/src/core/types/config.ts +200 -0
- package/src/core/types/errors.d.ts.map +1 -0
- package/src/core/types/errors.ts +204 -0
- package/src/core/types/index.d.ts.map +1 -0
- package/src/core/types/index.ts +8 -0
- package/src/core/types/session.d.ts.map +1 -0
- package/src/core/types/session.ts +216 -0
- package/src/core/types/tools.d.ts.map +1 -0
- package/src/core/types/tools.ts +157 -0
- package/src/core/utils/CheckpointManager.d.ts.map +1 -0
- package/src/core/utils/CheckpointManager.ts +327 -0
- package/src/core/utils/Logger.d.ts.map +1 -0
- package/src/core/utils/Logger.ts +98 -0
- package/src/core/utils/RetryManager.ts +471 -0
- package/src/core/utils/TokenCounter.d.ts.map +1 -0
- package/src/core/utils/TokenCounter.ts +414 -0
- package/src/core/utils/VectorMemoryStore.ts +440 -0
- package/src/core/utils/helpers.d.ts.map +1 -0
- package/src/core/utils/helpers.ts +89 -0
- package/src/core/utils/index.d.ts.map +1 -0
- package/src/core/utils/index.ts +19 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ModelClient - Unified LLM client abstraction
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
Message,
|
|
7
|
+
ContentBlock,
|
|
8
|
+
ToolUseContent,
|
|
9
|
+
TextContent,
|
|
10
|
+
ToolCallId,
|
|
11
|
+
SessionId,
|
|
12
|
+
} from '../types/session.js';
|
|
13
|
+
import type { ToolDefinition } from '../types/tools.js';
|
|
14
|
+
import { ModelError, RateLimitError, NovaError } from '../types/errors.js';
|
|
15
|
+
import { AnthropicProvider } from './providers/AnthropicProvider.js';
|
|
16
|
+
import { OpenAIProvider } from './providers/OpenAIProvider.js';
|
|
17
|
+
import { OllamaProvider } from './providers/OllamaProvider.js';
|
|
18
|
+
import { OllamaCloudProvider } from './providers/OllamaCloudProvider.js';
|
|
19
|
+
import { CodingPlanProvider, type CodingPlanPlatform } from './providers/CodingPlanProvider.js';
|
|
20
|
+
import type { ModelProvider, ModelRequestOptions, ModelResponse, StreamEvent } from './types.js';
|
|
21
|
+
|
|
22
|
+
export interface CreateModelClientOptions {
|
|
23
|
+
provider: 'anthropic' | 'openai' | 'azure' | 'ollama' | 'ollama-cloud' | 'coding-plan' | 'custom';
|
|
24
|
+
apiKey?: string;
|
|
25
|
+
baseUrl?: string;
|
|
26
|
+
model: string;
|
|
27
|
+
maxTokens?: number;
|
|
28
|
+
temperature?: number;
|
|
29
|
+
organizationId?: string;
|
|
30
|
+
apiVersion?: string;
|
|
31
|
+
/** Coding Plan platform (required when provider is 'coding-plan') */
|
|
32
|
+
codingPlanPlatform?: CodingPlanPlatform;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class ModelClient {
|
|
36
|
+
private provider: ModelProvider;
|
|
37
|
+
private model: string;
|
|
38
|
+
private maxTokens: number;
|
|
39
|
+
private temperature: number;
|
|
40
|
+
|
|
41
|
+
constructor(options: CreateModelClientOptions) {
|
|
42
|
+
this.model = options.model;
|
|
43
|
+
this.maxTokens = options.maxTokens ?? 4096;
|
|
44
|
+
this.temperature = options.temperature ?? 0.7;
|
|
45
|
+
|
|
46
|
+
switch (options.provider) {
|
|
47
|
+
case 'anthropic':
|
|
48
|
+
if (!options.apiKey) {
|
|
49
|
+
throw new NovaError('API key is required for Anthropic provider', 'AUTH_ERROR');
|
|
50
|
+
}
|
|
51
|
+
this.provider = new AnthropicProvider({
|
|
52
|
+
apiKey: options.apiKey,
|
|
53
|
+
baseUrl: options.baseUrl,
|
|
54
|
+
model: options.model,
|
|
55
|
+
apiVersion: options.apiVersion,
|
|
56
|
+
});
|
|
57
|
+
break;
|
|
58
|
+
|
|
59
|
+
case 'openai':
|
|
60
|
+
if (!options.apiKey) {
|
|
61
|
+
throw new NovaError('API key is required for OpenAI provider', 'AUTH_ERROR');
|
|
62
|
+
}
|
|
63
|
+
this.provider = new OpenAIProvider({
|
|
64
|
+
apiKey: options.apiKey,
|
|
65
|
+
baseUrl: options.baseUrl,
|
|
66
|
+
organizationId: options.organizationId,
|
|
67
|
+
model: options.model,
|
|
68
|
+
});
|
|
69
|
+
break;
|
|
70
|
+
|
|
71
|
+
case 'azure':
|
|
72
|
+
if (!options.apiKey) {
|
|
73
|
+
throw new NovaError('API key is required for Azure OpenAI provider', 'AUTH_ERROR');
|
|
74
|
+
}
|
|
75
|
+
// Azure uses OpenAI-compatible SDK with custom baseUrl and apiVersion
|
|
76
|
+
this.provider = new OpenAIProvider({
|
|
77
|
+
apiKey: options.apiKey,
|
|
78
|
+
baseUrl: options.baseUrl,
|
|
79
|
+
organizationId: options.organizationId,
|
|
80
|
+
model: options.model,
|
|
81
|
+
});
|
|
82
|
+
break;
|
|
83
|
+
|
|
84
|
+
case 'ollama':
|
|
85
|
+
// Ollama doesn't require an API key; uses localhost by default
|
|
86
|
+
this.provider = new OllamaProvider({
|
|
87
|
+
baseUrl: options.baseUrl || 'http://localhost:11434',
|
|
88
|
+
model: options.model,
|
|
89
|
+
});
|
|
90
|
+
break;
|
|
91
|
+
|
|
92
|
+
case 'ollama-cloud':
|
|
93
|
+
// Ollama Cloud: hosted models via ollama.com with API key
|
|
94
|
+
if (!options.apiKey) {
|
|
95
|
+
throw new NovaError('API key is required for Ollama Cloud provider. Run: nova auth set ollama-cloud', 'AUTH_ERROR');
|
|
96
|
+
}
|
|
97
|
+
this.provider = new OllamaCloudProvider({
|
|
98
|
+
apiKey: options.apiKey,
|
|
99
|
+
baseUrl: options.baseUrl || 'https://ollama.com',
|
|
100
|
+
model: options.model,
|
|
101
|
+
});
|
|
102
|
+
break;
|
|
103
|
+
|
|
104
|
+
case 'coding-plan':
|
|
105
|
+
// Coding Plan: Chinese AI coding platforms (Alibaba, Tencent, Volcengine, etc.)
|
|
106
|
+
if (!options.apiKey) {
|
|
107
|
+
throw new NovaError('API key is required for Coding Plan provider. Get your key from the platform console.', 'AUTH_ERROR');
|
|
108
|
+
}
|
|
109
|
+
if (!options.codingPlanPlatform) {
|
|
110
|
+
throw new NovaError('codingPlanPlatform is required for Coding Plan provider. Options: alibaba, tencent, volcengine, baidu, kimi, zhipu, minimax', 'AUTH_ERROR');
|
|
111
|
+
}
|
|
112
|
+
this.provider = new CodingPlanProvider({
|
|
113
|
+
platform: options.codingPlanPlatform,
|
|
114
|
+
apiKey: options.apiKey,
|
|
115
|
+
model: options.model,
|
|
116
|
+
customBaseUrl: options.baseUrl,
|
|
117
|
+
});
|
|
118
|
+
break;
|
|
119
|
+
|
|
120
|
+
case 'custom':
|
|
121
|
+
// Custom provider: uses OpenAI-compatible SDK with user-provided baseUrl + apiKey
|
|
122
|
+
// This covers DeepSeek, Google Gemini (OpenAI compat), Together AI, Groq, etc.
|
|
123
|
+
this.provider = new OpenAIProvider({
|
|
124
|
+
apiKey: options.apiKey || 'no-key-required',
|
|
125
|
+
baseUrl: options.baseUrl,
|
|
126
|
+
model: options.model,
|
|
127
|
+
});
|
|
128
|
+
break;
|
|
129
|
+
|
|
130
|
+
default:
|
|
131
|
+
throw new NovaError(`Provider "${options.provider}" is not supported`, 'NOT_IMPLEMENTED');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Send a non-streaming completion request */
|
|
136
|
+
async complete(
|
|
137
|
+
messages: Message[],
|
|
138
|
+
tools: ToolDefinition[],
|
|
139
|
+
sessionId: SessionId,
|
|
140
|
+
options?: Partial<ModelRequestOptions>
|
|
141
|
+
): Promise<ModelResponse> {
|
|
142
|
+
try {
|
|
143
|
+
const requestOpts: ModelRequestOptions = {
|
|
144
|
+
model: this.model,
|
|
145
|
+
maxTokens: options?.maxTokens ?? this.maxTokens,
|
|
146
|
+
temperature: options?.temperature ?? this.temperature,
|
|
147
|
+
tools,
|
|
148
|
+
sessionId,
|
|
149
|
+
stopSequences: options?.stopSequences,
|
|
150
|
+
systemPrompt: options?.systemPrompt,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
return await this.provider.complete(messages, requestOpts);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
if (err instanceof NovaError) throw err;
|
|
156
|
+
throw new ModelError(
|
|
157
|
+
`Model request failed: ${(err as Error).message}`,
|
|
158
|
+
undefined,
|
|
159
|
+
this.provider.name,
|
|
160
|
+
{ originalError: err }
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Send a streaming completion request */
|
|
166
|
+
async *stream(
|
|
167
|
+
messages: Message[],
|
|
168
|
+
tools: ToolDefinition[],
|
|
169
|
+
sessionId: SessionId,
|
|
170
|
+
options?: Partial<ModelRequestOptions>
|
|
171
|
+
): AsyncGenerator<StreamEvent> {
|
|
172
|
+
try {
|
|
173
|
+
const requestOpts: ModelRequestOptions = {
|
|
174
|
+
model: this.model,
|
|
175
|
+
maxTokens: options?.maxTokens ?? this.maxTokens,
|
|
176
|
+
temperature: options?.temperature ?? this.temperature,
|
|
177
|
+
tools,
|
|
178
|
+
sessionId,
|
|
179
|
+
stopSequences: options?.stopSequences,
|
|
180
|
+
systemPrompt: options?.systemPrompt,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
yield* this.provider.stream(messages, requestOpts);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
if (err instanceof NovaError) throw err;
|
|
186
|
+
const error = new ModelError(
|
|
187
|
+
`Model stream failed: ${(err as Error).message}`,
|
|
188
|
+
undefined,
|
|
189
|
+
this.provider.name,
|
|
190
|
+
{ originalError: err }
|
|
191
|
+
);
|
|
192
|
+
yield { type: 'error', error };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Count tokens for a message array (approximate) */
|
|
197
|
+
async countTokens(messages: Message[]): Promise<number> {
|
|
198
|
+
return this.provider.countTokens(messages);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Get the current model name */
|
|
202
|
+
getModel(): string {
|
|
203
|
+
return this.model;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Get the provider name */
|
|
207
|
+
getProviderName(): string {
|
|
208
|
+
return this.provider.name;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Update model parameters */
|
|
212
|
+
updateOptions(options: { maxTokens?: number; temperature?: number; model?: string }): void {
|
|
213
|
+
if (options.maxTokens !== undefined) this.maxTokens = options.maxTokens;
|
|
214
|
+
if (options.temperature !== undefined) this.temperature = options.temperature;
|
|
215
|
+
if (options.model !== undefined) this.model = options.model;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ModelConnectionTester - Test model provider connectivity
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import type { AuthManager } from '../auth/AuthManager.js';
|
|
6
|
+
import type { ConfigManager } from '../config/ConfigManager.js';
|
|
7
|
+
import { OllamaManager } from './providers/OllamaManager.js';
|
|
8
|
+
|
|
9
|
+
export interface ModelConnectionStatus {
|
|
10
|
+
provider: string;
|
|
11
|
+
model: string;
|
|
12
|
+
status: 'configured' | 'unconfigured' | 'available' | 'unavailable' | 'error';
|
|
13
|
+
message?: string;
|
|
14
|
+
latency?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ProviderConnectionStatus {
|
|
18
|
+
provider: string;
|
|
19
|
+
type: string;
|
|
20
|
+
status: 'configured' | 'unconfigured' | 'partial' | 'unavailable' | 'error';
|
|
21
|
+
hasCredentials: boolean;
|
|
22
|
+
models: ModelConnectionStatus[];
|
|
23
|
+
message?: string;
|
|
24
|
+
latency?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Test connectivity for model providers
|
|
29
|
+
*/
|
|
30
|
+
export class ModelConnectionTester {
|
|
31
|
+
private authManager: AuthManager;
|
|
32
|
+
private configManager: ConfigManager;
|
|
33
|
+
private cache: Map<string, ProviderConnectionStatus> = new Map();
|
|
34
|
+
private cacheTimeout = 60000; // 1 minute cache
|
|
35
|
+
private lastTestTime = 0;
|
|
36
|
+
|
|
37
|
+
constructor(authManager: AuthManager, configManager: ConfigManager) {
|
|
38
|
+
this.authManager = authManager;
|
|
39
|
+
this.configManager = configManager;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Test all providers and return their status
|
|
44
|
+
*/
|
|
45
|
+
async testAllProviders(): Promise<ProviderConnectionStatus[]> {
|
|
46
|
+
const config = this.configManager.getConfig();
|
|
47
|
+
const results: ProviderConnectionStatus[] = [];
|
|
48
|
+
|
|
49
|
+
for (const [providerName, providerConfig] of Object.entries(config.models.providers)) {
|
|
50
|
+
const status = await this.testProvider(providerName, providerConfig.type);
|
|
51
|
+
results.push(status);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.lastTestTime = Date.now();
|
|
55
|
+
return results;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Test a single provider
|
|
60
|
+
*/
|
|
61
|
+
async testProvider(providerName: string, providerType: string): Promise<ProviderConnectionStatus> {
|
|
62
|
+
// Check cache
|
|
63
|
+
const cached = this.cache.get(providerName);
|
|
64
|
+
if (cached && Date.now() - this.lastTestTime < this.cacheTimeout) {
|
|
65
|
+
return cached;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const config = this.configManager.getConfig();
|
|
69
|
+
const providerConfig = config.models.providers[providerName];
|
|
70
|
+
|
|
71
|
+
if (!providerConfig) {
|
|
72
|
+
return {
|
|
73
|
+
provider: providerName,
|
|
74
|
+
type: providerType,
|
|
75
|
+
status: 'unconfigured',
|
|
76
|
+
hasCredentials: false,
|
|
77
|
+
models: [],
|
|
78
|
+
message: 'Provider not found in configuration',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const hasCredentials = this.checkCredentials(providerName, providerType);
|
|
83
|
+
const models: ModelConnectionStatus[] = [];
|
|
84
|
+
let providerStatus: ProviderConnectionStatus['status'] = 'unconfigured';
|
|
85
|
+
let message: string | undefined;
|
|
86
|
+
let latency: number | undefined;
|
|
87
|
+
|
|
88
|
+
// For Ollama, test actual connectivity
|
|
89
|
+
if (providerType === 'ollama') {
|
|
90
|
+
const startTime = Date.now();
|
|
91
|
+
try {
|
|
92
|
+
const ollamaCreds = this.authManager.getCredentials('ollama');
|
|
93
|
+
const baseUrl = ollamaCreds?.baseUrl || process.env.OLLAMA_HOST || 'http://localhost:11434';
|
|
94
|
+
const manager = new OllamaManager(baseUrl);
|
|
95
|
+
const isRunning = await manager.ping();
|
|
96
|
+
latency = Date.now() - startTime;
|
|
97
|
+
|
|
98
|
+
if (isRunning) {
|
|
99
|
+
providerStatus = 'configured';
|
|
100
|
+
message = `Running at ${baseUrl}`;
|
|
101
|
+
|
|
102
|
+
// List available models
|
|
103
|
+
const ollamaModels = await manager.listModels();
|
|
104
|
+
for (const model of ollamaModels) {
|
|
105
|
+
models.push({
|
|
106
|
+
provider: providerName,
|
|
107
|
+
model: model.name,
|
|
108
|
+
status: 'available',
|
|
109
|
+
latency,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
providerStatus = 'unavailable';
|
|
114
|
+
message = `Ollama not running at ${baseUrl}`;
|
|
115
|
+
}
|
|
116
|
+
} catch (err) {
|
|
117
|
+
providerStatus = 'error';
|
|
118
|
+
message = `Failed to connect: ${(err as Error).message}`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// For ollama-cloud, check credentials
|
|
122
|
+
else if (providerType === 'ollama-cloud') {
|
|
123
|
+
if (hasCredentials) {
|
|
124
|
+
providerStatus = 'configured';
|
|
125
|
+
message = 'API key configured';
|
|
126
|
+
|
|
127
|
+
// Add models from config
|
|
128
|
+
for (const [modelId, modelConfig] of Object.entries(providerConfig.models)) {
|
|
129
|
+
models.push({
|
|
130
|
+
provider: providerName,
|
|
131
|
+
model: modelId,
|
|
132
|
+
status: 'configured',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
providerStatus = 'unconfigured';
|
|
137
|
+
message = 'No API key configured';
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// For other providers (anthropic, openai, etc.)
|
|
141
|
+
else {
|
|
142
|
+
if (hasCredentials) {
|
|
143
|
+
// Try to actually test the connection
|
|
144
|
+
const startTime = Date.now();
|
|
145
|
+
const connectionTest = await this.testApiConnection(providerName, providerConfig);
|
|
146
|
+
latency = Date.now() - startTime;
|
|
147
|
+
|
|
148
|
+
if (connectionTest.success) {
|
|
149
|
+
providerStatus = 'available';
|
|
150
|
+
message = connectionTest.message || `Connected (${latency}ms)`;
|
|
151
|
+
} else {
|
|
152
|
+
providerStatus = 'error';
|
|
153
|
+
message = connectionTest.message || 'Connection failed';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Add models from config with individual status
|
|
157
|
+
for (const [modelId, modelConfig] of Object.entries(providerConfig.models)) {
|
|
158
|
+
models.push({
|
|
159
|
+
provider: providerName,
|
|
160
|
+
model: modelId,
|
|
161
|
+
status: connectionTest.success ? 'available' : 'error',
|
|
162
|
+
message: connectionTest.success ? undefined : connectionTest.message,
|
|
163
|
+
latency: connectionTest.success ? latency : undefined,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
providerStatus = 'unconfigured';
|
|
168
|
+
message = 'No API key configured';
|
|
169
|
+
|
|
170
|
+
// Still list models but mark as unconfigured
|
|
171
|
+
for (const [modelId, modelConfig] of Object.entries(providerConfig.models)) {
|
|
172
|
+
models.push({
|
|
173
|
+
provider: providerName,
|
|
174
|
+
model: modelId,
|
|
175
|
+
status: 'unconfigured',
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const result: ProviderConnectionStatus = {
|
|
182
|
+
provider: providerName,
|
|
183
|
+
type: providerType,
|
|
184
|
+
status: providerStatus,
|
|
185
|
+
hasCredentials,
|
|
186
|
+
models,
|
|
187
|
+
message,
|
|
188
|
+
latency,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
this.cache.set(providerName, result);
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Quick check if a provider has credentials
|
|
197
|
+
*/
|
|
198
|
+
checkCredentials(providerName: string, providerType: string): boolean {
|
|
199
|
+
// Ollama local doesn't need credentials
|
|
200
|
+
if (providerType === 'ollama') {
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Check auth manager
|
|
205
|
+
if (this.authManager.hasCredentials(providerName)) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Check for common env vars
|
|
210
|
+
const envKeyMap: Record<string, string> = {
|
|
211
|
+
anthropic: 'ANTHROPIC_API_KEY',
|
|
212
|
+
openai: 'OPENAI_API_KEY',
|
|
213
|
+
google: 'GOOGLE_API_KEY',
|
|
214
|
+
deepseek: 'DEEPSEEK_API_KEY',
|
|
215
|
+
qwen: 'DASHSCOPE_API_KEY',
|
|
216
|
+
glm: 'ZHIPU_API_KEY',
|
|
217
|
+
moonshot: 'MOONSHOT_API_KEY',
|
|
218
|
+
baichuan: 'BAICHUAN_API_KEY',
|
|
219
|
+
yi: 'YI_API_KEY',
|
|
220
|
+
minimax: 'MINIMAX_API_KEY',
|
|
221
|
+
groq: 'GROQ_API_KEY',
|
|
222
|
+
mistral: 'MISTRAL_API_KEY',
|
|
223
|
+
together: 'TOGETHER_API_KEY',
|
|
224
|
+
perplexity: 'PERPLEXITY_API_KEY',
|
|
225
|
+
siliconflow: 'SILICONFLOW_API_KEY',
|
|
226
|
+
'ollama-cloud': 'OLLAMA_CLOUD_API_KEY',
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const envKey = envKeyMap[providerName] || envKeyMap[providerType] || `${providerName.toUpperCase()}_API_KEY`;
|
|
230
|
+
return !!process.env[envKey];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get cached status without testing
|
|
235
|
+
*/
|
|
236
|
+
getCachedStatus(providerName: string): ProviderConnectionStatus | undefined {
|
|
237
|
+
return this.cache.get(providerName);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Clear cache
|
|
242
|
+
*/
|
|
243
|
+
clearCache(): void {
|
|
244
|
+
this.cache.clear();
|
|
245
|
+
this.lastTestTime = 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Test a specific model's connectivity (for providers that support it)
|
|
250
|
+
*/
|
|
251
|
+
async testModel(providerName: string, modelId: string): Promise<ModelConnectionStatus> {
|
|
252
|
+
const config = this.configManager.getConfig();
|
|
253
|
+
const providerConfig = config.models.providers[providerName];
|
|
254
|
+
|
|
255
|
+
if (!providerConfig) {
|
|
256
|
+
return {
|
|
257
|
+
provider: providerName,
|
|
258
|
+
model: modelId,
|
|
259
|
+
status: 'unconfigured',
|
|
260
|
+
message: 'Provider not found',
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const hasCredentials = this.checkCredentials(providerName, providerConfig.type);
|
|
265
|
+
|
|
266
|
+
if (!hasCredentials && providerConfig.type !== 'ollama') {
|
|
267
|
+
return {
|
|
268
|
+
provider: providerName,
|
|
269
|
+
model: modelId,
|
|
270
|
+
status: 'unconfigured',
|
|
271
|
+
message: 'No API key configured',
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// For Ollama, check if model is installed
|
|
276
|
+
if (providerConfig.type === 'ollama') {
|
|
277
|
+
try {
|
|
278
|
+
const ollamaCreds = this.authManager.getCredentials('ollama');
|
|
279
|
+
const baseUrl = ollamaCreds?.baseUrl || process.env.OLLAMA_HOST || 'http://localhost:11434';
|
|
280
|
+
const manager = new OllamaManager(baseUrl);
|
|
281
|
+
|
|
282
|
+
const isRunning = await manager.ping();
|
|
283
|
+
if (!isRunning) {
|
|
284
|
+
return {
|
|
285
|
+
provider: providerName,
|
|
286
|
+
model: modelId,
|
|
287
|
+
status: 'unavailable',
|
|
288
|
+
message: 'Ollama not running',
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const modelInfo = await manager.showModel(modelId);
|
|
293
|
+
return {
|
|
294
|
+
provider: providerName,
|
|
295
|
+
model: modelId,
|
|
296
|
+
status: 'available',
|
|
297
|
+
message: modelInfo.details?.parameter_size || 'Available',
|
|
298
|
+
};
|
|
299
|
+
} catch (err) {
|
|
300
|
+
return {
|
|
301
|
+
provider: providerName,
|
|
302
|
+
model: modelId,
|
|
303
|
+
status: 'unavailable',
|
|
304
|
+
message: `Model not found: ${(err as Error).message}`,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// For other providers, we just check credentials
|
|
310
|
+
return {
|
|
311
|
+
provider: providerName,
|
|
312
|
+
model: modelId,
|
|
313
|
+
status: hasCredentials ? 'configured' : 'unconfigured',
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Test actual API connection for a provider
|
|
319
|
+
*/
|
|
320
|
+
private async testApiConnection(
|
|
321
|
+
providerName: string,
|
|
322
|
+
providerConfig: any
|
|
323
|
+
): Promise<{ success: boolean; message?: string }> {
|
|
324
|
+
try {
|
|
325
|
+
const creds = this.authManager.getCredentials(providerName);
|
|
326
|
+
const apiKey = creds?.apiKey || process.env[`${providerName.toUpperCase()}_API_KEY`];
|
|
327
|
+
const baseUrl = creds?.baseUrl || providerConfig.baseUrl;
|
|
328
|
+
|
|
329
|
+
if (!apiKey) {
|
|
330
|
+
return { success: false, message: 'No API key available' };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// For OpenAI-compatible providers, try to list models
|
|
334
|
+
if (providerConfig.type === 'custom' || providerConfig.type === 'openai' ||
|
|
335
|
+
providerConfig.baseUrl?.includes('openai') || providerConfig.baseUrl) {
|
|
336
|
+
try {
|
|
337
|
+
const { OpenAICompatibleProvider } = await import('./providers/OpenAICompatibleProvider.js');
|
|
338
|
+
const tester = new (class extends OpenAICompatibleProvider {
|
|
339
|
+
constructor() {
|
|
340
|
+
super({ apiKey, baseUrl, model: 'probe' });
|
|
341
|
+
}
|
|
342
|
+
get name() { return providerName; }
|
|
343
|
+
})();
|
|
344
|
+
|
|
345
|
+
const models = await tester.listModels();
|
|
346
|
+
if (models.length > 0) {
|
|
347
|
+
return { success: true, message: `${models.length} models available` };
|
|
348
|
+
}
|
|
349
|
+
return { success: true, message: 'Connected' };
|
|
350
|
+
} catch (err) {
|
|
351
|
+
// If listModels fails, try a simple completion as fallback
|
|
352
|
+
return { success: false, message: `API error: ${(err as Error).message}` };
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// For other providers (Anthropic, etc.), just check credentials for now
|
|
357
|
+
// They could be tested with specific provider SDK calls
|
|
358
|
+
return { success: true, message: 'API key configured' };
|
|
359
|
+
} catch (err) {
|
|
360
|
+
return { success: false, message: `Connection failed: ${(err as Error).message}` };
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|