@tenex-chat/backend 0.9.4 → 0.9.6
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/README.md +5 -1
- package/dist/daemon-wrapper.cjs +47 -0
- package/dist/index.js +59268 -0
- package/dist/wrapper.js +171 -0
- package/package.json +19 -27
- package/src/agents/AgentRegistry.ts +9 -7
- package/src/agents/AgentStorage.ts +24 -1
- package/src/agents/agent-installer.ts +6 -0
- package/src/agents/agent-loader.ts +7 -2
- package/src/agents/constants.ts +10 -2
- package/src/agents/execution/AgentExecutor.ts +35 -6
- package/src/agents/execution/StreamCallbacks.ts +53 -13
- package/src/agents/execution/StreamExecutionHandler.ts +110 -16
- package/src/agents/execution/StreamSetup.ts +19 -9
- package/src/agents/execution/ToolEventHandlers.ts +112 -0
- package/src/agents/role-categories.ts +53 -0
- package/src/agents/types/runtime.ts +7 -0
- package/src/agents/types/storage.ts +7 -0
- package/src/commands/agent/import/openclaw-distiller.ts +63 -7
- package/src/commands/agent/import/openclaw-reader.ts +54 -0
- package/src/commands/agent/import/openclaw.ts +120 -29
- package/src/commands/agent/index.ts +83 -2
- package/src/commands/setup/display.ts +123 -0
- package/src/commands/setup/embed.ts +13 -13
- package/src/commands/setup/global-system-prompt.ts +15 -17
- package/src/commands/setup/image.ts +17 -20
- package/src/commands/setup/interactive.ts +37 -20
- package/src/commands/setup/llm.ts +12 -7
- package/src/commands/setup/onboarding.ts +1580 -248
- package/src/commands/setup/providers.ts +3 -3
- package/src/conversations/ConversationStore.ts +23 -2
- package/src/conversations/MessageBuilder.ts +51 -73
- package/src/conversations/formatters/utils/conversation-transcript-formatter.ts +425 -0
- package/src/conversations/search/embeddings/ConversationEmbeddingService.ts +40 -98
- package/src/conversations/search/embeddings/ConversationIndexingJob.ts +40 -52
- package/src/conversations/services/ConversationSummarizer.ts +1 -2
- package/src/conversations/types.ts +11 -0
- package/src/daemon/Daemon.ts +78 -57
- package/src/daemon/ProjectRuntime.ts +6 -12
- package/src/daemon/SubscriptionManager.ts +13 -0
- package/src/daemon/index.ts +0 -1
- package/src/event-handler/index.ts +1 -0
- package/src/index.ts +20 -1
- package/src/llm/ChunkHandler.ts +1 -1
- package/src/llm/FinishHandler.ts +28 -4
- package/src/llm/LLMConfigEditor.ts +218 -106
- package/src/llm/index.ts +0 -4
- package/src/llm/meta/MetaModelResolver.ts +3 -18
- package/src/llm/middleware/message-sanitizer.ts +153 -0
- package/src/llm/providers/ollama-models.ts +0 -38
- package/src/llm/service.ts +50 -15
- package/src/llm/types.ts +0 -12
- package/src/llm/utils/ConfigurationManager.ts +88 -465
- package/src/llm/utils/ConfigurationTester.ts +42 -185
- package/src/llm/utils/ModelSelector.ts +156 -92
- package/src/llm/utils/ProviderConfigUI.ts +10 -141
- package/src/llm/utils/models-dev-cache.ts +102 -23
- package/src/llm/utils/provider-select-prompt.ts +284 -0
- package/src/llm/utils/provider-setup.ts +81 -34
- package/src/llm/utils/variant-list-prompt.ts +361 -0
- package/src/nostr/AgentEventDecoder.ts +1 -0
- package/src/nostr/AgentEventEncoder.ts +37 -0
- package/src/nostr/AgentProfilePublisher.ts +13 -0
- package/src/nostr/AgentPublisher.ts +26 -0
- package/src/nostr/kinds.ts +1 -0
- package/src/nostr/ndkClient.ts +4 -1
- package/src/nostr/types.ts +12 -0
- package/src/prompts/fragments/25-rag-instructions.ts +22 -21
- package/src/prompts/fragments/31-agents-md-guidance.ts +7 -21
- package/src/prompts/fragments/index.ts +2 -0
- package/src/prompts/utils/systemPromptBuilder.ts +18 -28
- package/src/services/AgentDefinitionMonitor.ts +8 -0
- package/src/services/ConfigService.ts +34 -0
- package/src/services/PubkeyService.ts +7 -1
- package/src/services/compression/CompressionService.ts +133 -74
- package/src/services/compression/compression-utils.ts +110 -19
- package/src/services/config/types.ts +0 -6
- package/src/services/dispatch/AgentDispatchService.ts +79 -0
- package/src/services/intervention/InterventionService.ts +78 -5
- package/src/services/nip46/Nip46SigningService.ts +30 -1
- package/src/services/projects/ProjectContext.ts +8 -6
- package/src/services/rag/RAGCollectionRegistry.ts +199 -0
- package/src/services/rag/RAGDatabaseService.ts +2 -7
- package/src/services/rag/RAGOperations.ts +25 -45
- package/src/services/rag/RAGService.ts +0 -31
- package/src/services/rag/RagSubscriptionService.ts +71 -122
- package/src/services/rag/rag-utils.ts +13 -0
- package/src/services/ral/RALRegistry.ts +25 -184
- package/src/services/reports/ReportEmbeddingService.ts +63 -113
- package/src/services/search/UnifiedSearchService.ts +115 -4
- package/src/services/search/index.ts +1 -0
- package/src/services/search/projectFilter.ts +20 -4
- package/src/services/search/providers/ConversationSearchProvider.ts +1 -0
- package/src/services/search/providers/GenericCollectionSearchProvider.ts +81 -0
- package/src/services/search/providers/LessonSearchProvider.ts +1 -8
- package/src/services/search/providers/ReportSearchProvider.ts +1 -0
- package/src/services/search/types.ts +24 -3
- package/src/services/trust-pubkeys/SystemPubkeyListService.ts +148 -0
- package/src/services/trust-pubkeys/TrustPubkeyService.ts +70 -9
- package/src/telemetry/setup.ts +2 -13
- package/src/tools/implementations/ask.ts +3 -3
- package/src/tools/implementations/conversation_get.ts +28 -268
- package/src/tools/implementations/fs_grep.ts +6 -6
- package/src/tools/implementations/fs_read.ts +2 -0
- package/src/tools/implementations/fs_write.ts +2 -0
- package/src/tools/implementations/learn.ts +38 -50
- package/src/tools/implementations/rag_add_documents.ts +6 -4
- package/src/tools/implementations/rag_create_collection.ts +37 -4
- package/src/tools/implementations/rag_delete_collection.ts +9 -0
- package/src/tools/implementations/{search.ts → rag_search.ts} +31 -25
- package/src/tools/registry.ts +7 -8
- package/src/tools/types.ts +11 -2
- package/src/tools/utils/transcript-args.ts +13 -0
- package/src/utils/cli-theme.ts +13 -0
- package/src/utils/logger.ts +55 -0
- package/src/utils/metadataKeys.ts +17 -0
- package/src/utils/sqlEscaping.ts +39 -0
- package/src/wrapper.ts +7 -3
- package/dist/src/index.js +0 -46778
- package/dist/tenex-backend-wrapper.cjs +0 -3
- package/src/agents/execution/constants.ts +0 -16
- package/src/agents/execution/index.ts +0 -3
- package/src/agents/index.ts +0 -4
- package/src/commands/agent.ts +0 -215
- package/src/conversations/formatters/DelegationXmlFormatter.ts +0 -64
- package/src/conversations/formatters/index.ts +0 -9
- package/src/conversations/index.ts +0 -2
- package/src/conversations/utils/content-utils.ts +0 -69
- package/src/daemon/UnixSocketTransport.ts +0 -318
- package/src/event-handler/newConversation.ts +0 -165
- package/src/events/NDKProjectStatus.ts +0 -384
- package/src/events/index.ts +0 -4
- package/src/lib/json-parser.ts +0 -30
- package/src/llm/RecordingState.ts +0 -37
- package/src/llm/StreamPublisher.ts +0 -40
- package/src/llm/middleware/flight-recorder.ts +0 -188
- package/src/llm/utils/claudeCodePromptCompiler.ts +0 -141
- package/src/nostr/constants.ts +0 -38
- package/src/prompts/core/index.ts +0 -3
- package/src/prompts/index.ts +0 -21
- package/src/services/image/index.ts +0 -12
- package/src/services/status/index.ts +0 -11
- package/src/telemetry/diagnostics.ts +0 -27
- package/src/tools/implementations/rag_query.ts +0 -107
- package/src/types/index.ts +0 -46
- package/src/utils/agentFetcher.ts +0 -107
- package/src/utils/conversation-utils.ts +0 -1
- package/src/utils/process.ts +0 -49
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { PROVIDER_IDS } from "@/llm/providers/provider-ids";
|
|
2
|
-
import {
|
|
2
|
+
import { hasApiKey } from "@/llm/providers/key-manager";
|
|
3
3
|
import type { TenexLLMs } from "@/services/config/types";
|
|
4
4
|
import chalk from "chalk";
|
|
5
|
-
import
|
|
5
|
+
import * as display from "@/commands/setup/display";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Extended type for editor use - includes providers
|
|
@@ -13,165 +13,34 @@ type TenexLLMsWithProviders = TenexLLMs & {
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* UI utilities for provider configuration
|
|
16
|
-
* Extracted from LLMConfigEditor to separate concerns
|
|
17
16
|
*/
|
|
18
17
|
export class ProviderConfigUI {
|
|
19
|
-
/**
|
|
20
|
-
* Get provider display names
|
|
21
|
-
*/
|
|
22
18
|
static getProviderDisplayName(provider: string): string {
|
|
23
19
|
const names: Record<string, string> = {
|
|
24
20
|
[PROVIDER_IDS.OPENROUTER]: "OpenRouter (300+ models)",
|
|
25
21
|
[PROVIDER_IDS.ANTHROPIC]: "Anthropic (Claude)",
|
|
26
22
|
[PROVIDER_IDS.OPENAI]: "OpenAI (GPT)",
|
|
27
23
|
[PROVIDER_IDS.OLLAMA]: "Ollama (Local models)",
|
|
28
|
-
[PROVIDER_IDS.CLAUDE_CODE]: "Claude Code",
|
|
29
24
|
[PROVIDER_IDS.CODEX_APP_SERVER]: "Codex App Server (GPT-5.1/5.2)",
|
|
30
25
|
};
|
|
31
26
|
return names[provider] || provider;
|
|
32
27
|
}
|
|
33
28
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
*/
|
|
37
|
-
static async configureProvider(
|
|
38
|
-
provider: string,
|
|
39
|
-
currentProviders?: Record<string, { apiKey: string | string[] }>
|
|
40
|
-
): Promise<{ apiKey: string | string[] }> {
|
|
41
|
-
if (provider === PROVIDER_IDS.CLAUDE_CODE || provider === PROVIDER_IDS.CODEX_APP_SERVER) {
|
|
42
|
-
// Agent providers don't require an API key
|
|
43
|
-
console.log(
|
|
44
|
-
chalk.green(
|
|
45
|
-
`✓ ${ProviderConfigUI.getProviderDisplayName(provider)} provider configured (no API key required)`
|
|
46
|
-
)
|
|
47
|
-
);
|
|
48
|
-
return { apiKey: "none" }; // Doesn't use API keys
|
|
49
|
-
}
|
|
50
|
-
if (provider === PROVIDER_IDS.OLLAMA) {
|
|
51
|
-
// For Ollama, ask for base URL instead of API key
|
|
52
|
-
const currentUrl = resolveApiKey(currentProviders?.[provider]?.apiKey) || "local";
|
|
53
|
-
const { ollamaConfig } = await inquirer.prompt([
|
|
54
|
-
{
|
|
55
|
-
type: "select",
|
|
56
|
-
name: "ollamaConfig",
|
|
57
|
-
message: "Ollama configuration:",
|
|
58
|
-
choices: [
|
|
59
|
-
{ name: "Use local Ollama (http://localhost:11434)", value: "local" },
|
|
60
|
-
{ name: "Use custom Ollama URL", value: "custom" },
|
|
61
|
-
],
|
|
62
|
-
default: currentUrl === "local" ? "local" : "custom",
|
|
63
|
-
},
|
|
64
|
-
]);
|
|
65
|
-
|
|
66
|
-
let baseUrl = "local";
|
|
67
|
-
if (ollamaConfig === "custom") {
|
|
68
|
-
const { customUrl } = await inquirer.prompt([
|
|
69
|
-
{
|
|
70
|
-
type: "input",
|
|
71
|
-
name: "customUrl",
|
|
72
|
-
message: "Enter Ollama base URL:",
|
|
73
|
-
default: currentUrl !== "local" ? currentUrl : "http://localhost:11434",
|
|
74
|
-
validate: (input: string) => {
|
|
75
|
-
if (!input.trim()) return "URL is required";
|
|
76
|
-
try {
|
|
77
|
-
new URL(input);
|
|
78
|
-
return true;
|
|
79
|
-
} catch {
|
|
80
|
-
return "Please enter a valid URL";
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
]);
|
|
85
|
-
baseUrl = customUrl;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return { apiKey: baseUrl };
|
|
89
|
-
}
|
|
90
|
-
// For other providers, ask for API key
|
|
91
|
-
const existingKey = currentProviders?.[provider]?.apiKey;
|
|
92
|
-
|
|
93
|
-
// If the provider already has a multi-key array, preserve it
|
|
94
|
-
if (Array.isArray(existingKey) && existingKey.length > 1) {
|
|
95
|
-
console.log(
|
|
96
|
-
chalk.cyan(
|
|
97
|
-
` ℹ ${ProviderConfigUI.getProviderDisplayName(provider)} has ${existingKey.length} API keys configured (multi-key rotation).`
|
|
98
|
-
)
|
|
99
|
-
);
|
|
100
|
-
console.log(
|
|
101
|
-
chalk.gray(
|
|
102
|
-
" To edit multi-key configs, modify providers.json directly."
|
|
103
|
-
)
|
|
104
|
-
);
|
|
105
|
-
const { keep } = await inquirer.prompt([
|
|
106
|
-
{
|
|
107
|
-
type: "confirm",
|
|
108
|
-
name: "keep",
|
|
109
|
-
message: "Keep existing multi-key configuration?",
|
|
110
|
-
default: true,
|
|
111
|
-
},
|
|
112
|
-
]);
|
|
113
|
-
if (keep) {
|
|
114
|
-
return { apiKey: existingKey };
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const currentKey = resolveApiKey(existingKey);
|
|
119
|
-
const { apiKey } = await inquirer.prompt([
|
|
120
|
-
{
|
|
121
|
-
type: "password",
|
|
122
|
-
name: "apiKey",
|
|
123
|
-
message: `Enter API key for ${ProviderConfigUI.getProviderDisplayName(provider)} (press Enter to keep existing):`,
|
|
124
|
-
default: currentKey,
|
|
125
|
-
mask: "*",
|
|
126
|
-
validate: (input: string) => {
|
|
127
|
-
// Allow empty input if there's an existing key
|
|
128
|
-
if (!input.trim() && !currentKey) return "API key is required";
|
|
129
|
-
return true;
|
|
130
|
-
},
|
|
131
|
-
},
|
|
132
|
-
]);
|
|
133
|
-
|
|
134
|
-
return { apiKey: apiKey || currentKey || "" };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Display current configuration status
|
|
139
|
-
*/
|
|
140
|
-
static displayCurrentConfig(llmsConfig: TenexLLMsWithProviders): void {
|
|
141
|
-
console.log(chalk.bold("Configured Providers:"));
|
|
29
|
+
static displayProviders(llmsConfig: TenexLLMsWithProviders): void {
|
|
30
|
+
display.context("Configured Providers");
|
|
142
31
|
const providers = Object.keys(llmsConfig.providers).filter(
|
|
143
|
-
(p) =>
|
|
32
|
+
(p) => {
|
|
33
|
+
const key = llmsConfig.providers[p]?.apiKey;
|
|
34
|
+
return hasApiKey(key) || key === "none";
|
|
35
|
+
},
|
|
144
36
|
);
|
|
145
37
|
if (providers.length === 0) {
|
|
146
38
|
console.log(chalk.gray(" None configured"));
|
|
147
39
|
} else {
|
|
148
40
|
for (const p of providers) {
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
console.log(chalk.bold("\nConfigurations:"));
|
|
154
|
-
const configNames = Object.keys(llmsConfig.configurations);
|
|
155
|
-
if (configNames.length === 0) {
|
|
156
|
-
console.log(chalk.gray(" None"));
|
|
157
|
-
} else {
|
|
158
|
-
for (const name of configNames) {
|
|
159
|
-
const config = llmsConfig.configurations[name];
|
|
160
|
-
const isDefault = name === llmsConfig.default;
|
|
161
|
-
const isSummarization = name === llmsConfig.summarization;
|
|
162
|
-
const marker = isDefault || isSummarization ? chalk.cyan("• ") : " ";
|
|
163
|
-
|
|
164
|
-
const tags: string[] = [];
|
|
165
|
-
if (isDefault) tags.push("default");
|
|
166
|
-
if (isSummarization) tags.push("summarization");
|
|
167
|
-
const tagStr = tags.length > 0 ? chalk.gray(` (${tags.join(", ")})`) : "";
|
|
168
|
-
|
|
169
|
-
// Handle meta models differently - they don't have a single model
|
|
170
|
-
const configDisplay = config.provider === "meta"
|
|
171
|
-
? `meta (${Object.keys((config as { variants: Record<string, unknown> }).variants).length} variants)`
|
|
172
|
-
: `${config.provider}:${"model" in config ? config.model : "unknown"}`;
|
|
173
|
-
console.log(` ${marker}${name}${tagStr}: ${configDisplay}`);
|
|
41
|
+
display.success(ProviderConfigUI.getProviderDisplayName(p));
|
|
174
42
|
}
|
|
175
43
|
}
|
|
44
|
+
display.blank();
|
|
176
45
|
}
|
|
177
46
|
}
|
|
@@ -24,6 +24,17 @@ export interface ModelLimits {
|
|
|
24
24
|
output: number;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Full model info from models.dev
|
|
29
|
+
*/
|
|
30
|
+
export interface ModelsDevModel {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
cost?: { input: number; output: number };
|
|
34
|
+
limit?: { context?: number; output?: number };
|
|
35
|
+
last_updated?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
/**
|
|
28
39
|
* models.dev API response structure
|
|
29
40
|
*/
|
|
@@ -31,10 +42,14 @@ interface ModelsDevResponse {
|
|
|
31
42
|
[provider: string]: {
|
|
32
43
|
models: {
|
|
33
44
|
[modelId: string]: {
|
|
45
|
+
id?: string;
|
|
46
|
+
name?: string;
|
|
47
|
+
cost?: { input: number; output: number };
|
|
34
48
|
limit?: {
|
|
35
49
|
context?: number;
|
|
36
50
|
output?: number;
|
|
37
51
|
};
|
|
52
|
+
last_updated?: string;
|
|
38
53
|
};
|
|
39
54
|
};
|
|
40
55
|
};
|
|
@@ -213,51 +228,115 @@ export async function refreshCache(): Promise<void> {
|
|
|
213
228
|
}
|
|
214
229
|
|
|
215
230
|
/**
|
|
216
|
-
*
|
|
231
|
+
* Resolve the raw model data entry from the cache.
|
|
217
232
|
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
233
|
+
* Lookup order:
|
|
234
|
+
* 1. Direct: cache[mappedProvider].models[model]
|
|
235
|
+
* 2. Vendor split: for "vendor/bare" IDs, try cache[vendor].models[bare]
|
|
236
|
+
* 3. Global scan: search all providers for a matching model ID
|
|
237
|
+
*
|
|
238
|
+
* Steps 2–3 handle proxied providers (OpenRouter, Codex App Server) whose
|
|
239
|
+
* model IDs originate from upstream providers (OpenAI, Anthropic, etc.).
|
|
221
240
|
*/
|
|
222
|
-
|
|
223
|
-
if (!cache)
|
|
224
|
-
return undefined;
|
|
225
|
-
}
|
|
241
|
+
function resolveModelData(provider: string, model: string): { modelId: string; data: ModelsDevResponse[string]["models"][string] } | undefined {
|
|
242
|
+
if (!cache) return undefined;
|
|
226
243
|
|
|
227
|
-
//
|
|
244
|
+
// 1. Direct lookup in the mapped provider section
|
|
228
245
|
const modelsDevProvider = PROVIDER_MAPPING[provider];
|
|
229
|
-
if (modelsDevProvider
|
|
230
|
-
|
|
231
|
-
|
|
246
|
+
if (modelsDevProvider !== null && modelsDevProvider !== undefined) {
|
|
247
|
+
const providerData = cache[modelsDevProvider];
|
|
248
|
+
if (providerData?.models?.[model]) {
|
|
249
|
+
return { modelId: model, data: providerData.models[model] };
|
|
250
|
+
}
|
|
232
251
|
}
|
|
233
252
|
|
|
234
|
-
|
|
235
|
-
if (
|
|
236
|
-
|
|
253
|
+
// 2. If the model ID contains "vendor/bare", try the vendor's section
|
|
254
|
+
if (model.includes("/")) {
|
|
255
|
+
const slashIndex = model.indexOf("/");
|
|
256
|
+
const vendor = model.slice(0, slashIndex);
|
|
257
|
+
const bareModel = model.slice(slashIndex + 1);
|
|
258
|
+
const vendorData = cache[vendor];
|
|
259
|
+
if (vendorData?.models?.[bareModel]) {
|
|
260
|
+
return { modelId: bareModel, data: vendorData.models[bareModel] };
|
|
261
|
+
}
|
|
237
262
|
}
|
|
238
263
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
264
|
+
// 3. Global scan: search every provider for a matching model ID
|
|
265
|
+
for (const section of Object.values(cache)) {
|
|
266
|
+
if (section?.models?.[model]) {
|
|
267
|
+
return { modelId: model, data: section.models[model] };
|
|
268
|
+
}
|
|
242
269
|
}
|
|
243
270
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Get model limits for a specific provider and model
|
|
276
|
+
*
|
|
277
|
+
* @param provider Our provider ID (e.g., "anthropic", "openai", "openrouter")
|
|
278
|
+
* @param model Model ID (e.g., "claude-opus-4-5-20251101", "gpt-4o")
|
|
279
|
+
* @returns Model limits or undefined if not found/unsupported
|
|
280
|
+
*/
|
|
281
|
+
export function getModelLimits(provider: string, model: string): ModelLimits | undefined {
|
|
282
|
+
const resolved = resolveModelData(provider, model);
|
|
283
|
+
if (!resolved?.data?.limit) return undefined;
|
|
284
|
+
|
|
285
|
+
const { context, output } = resolved.data.limit;
|
|
286
|
+
if (context === undefined || output === undefined) return undefined;
|
|
248
287
|
|
|
249
288
|
return { context, output };
|
|
250
289
|
}
|
|
251
290
|
|
|
291
|
+
/**
|
|
292
|
+
* Get full model info from models.dev for a specific provider and model.
|
|
293
|
+
* Returns cost, limits, name, etc. for use in scoring/display.
|
|
294
|
+
*/
|
|
295
|
+
export function getModelInfo(provider: string, model: string): ModelsDevModel | undefined {
|
|
296
|
+
const resolved = resolveModelData(provider, model);
|
|
297
|
+
if (!resolved) return undefined;
|
|
298
|
+
|
|
299
|
+
const { modelId, data } = resolved;
|
|
300
|
+
return {
|
|
301
|
+
id: data.id ?? modelId,
|
|
302
|
+
name: data.name ?? modelId,
|
|
303
|
+
cost: data.cost,
|
|
304
|
+
limit: data.limit,
|
|
305
|
+
last_updated: data.last_updated,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
252
309
|
/**
|
|
253
310
|
* Get just the context window for a model
|
|
254
|
-
* Convenience function for backwards compatibility with context-window-cache
|
|
255
311
|
*/
|
|
256
312
|
export function getContextWindowFromModelsdev(provider: string, model: string): number | undefined {
|
|
257
313
|
const limits = getModelLimits(provider, model);
|
|
258
314
|
return limits?.context;
|
|
259
315
|
}
|
|
260
316
|
|
|
317
|
+
/**
|
|
318
|
+
* Get all models for a provider, sorted by last_updated descending
|
|
319
|
+
*/
|
|
320
|
+
export function getProviderModels(provider: string): ModelsDevModel[] {
|
|
321
|
+
if (!cache) return [];
|
|
322
|
+
|
|
323
|
+
const modelsDevProvider = PROVIDER_MAPPING[provider];
|
|
324
|
+
if (modelsDevProvider === null || modelsDevProvider === undefined) return [];
|
|
325
|
+
|
|
326
|
+
const providerData = cache[modelsDevProvider];
|
|
327
|
+
if (!providerData?.models) return [];
|
|
328
|
+
|
|
329
|
+
return Object.entries(providerData.models)
|
|
330
|
+
.map(([modelId, data]) => ({
|
|
331
|
+
id: data.id ?? modelId,
|
|
332
|
+
name: data.name ?? modelId,
|
|
333
|
+
cost: data.cost,
|
|
334
|
+
limit: data.limit,
|
|
335
|
+
last_updated: data.last_updated,
|
|
336
|
+
}))
|
|
337
|
+
.sort((a, b) => (b.last_updated ?? "").localeCompare(a.last_updated ?? ""));
|
|
338
|
+
}
|
|
339
|
+
|
|
261
340
|
/**
|
|
262
341
|
* Clear in-memory cache (for testing)
|
|
263
342
|
*/
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createPrompt,
|
|
3
|
+
useState,
|
|
4
|
+
useKeypress,
|
|
5
|
+
usePrefix,
|
|
6
|
+
useMemo,
|
|
7
|
+
isUpKey,
|
|
8
|
+
isDownKey,
|
|
9
|
+
isSpaceKey,
|
|
10
|
+
isEnterKey,
|
|
11
|
+
makeTheme,
|
|
12
|
+
type Theme,
|
|
13
|
+
type KeypressEvent,
|
|
14
|
+
} from "@inquirer/core";
|
|
15
|
+
import type { PartialDeep } from "@inquirer/type";
|
|
16
|
+
import { cursorHide } from "@inquirer/ansi";
|
|
17
|
+
import chalk from "chalk";
|
|
18
|
+
import type { ProviderCredentials } from "@/services/config/types";
|
|
19
|
+
import { PROVIDER_IDS } from "@/llm/providers/provider-ids";
|
|
20
|
+
import { ProviderConfigUI } from "@/llm/utils/ProviderConfigUI";
|
|
21
|
+
import * as display from "@/commands/setup/display";
|
|
22
|
+
|
|
23
|
+
// --- Public types ---
|
|
24
|
+
|
|
25
|
+
export type PromptState = {
|
|
26
|
+
providers: Record<string, ProviderCredentials>;
|
|
27
|
+
stash: Record<string, ProviderCredentials>;
|
|
28
|
+
active: number;
|
|
29
|
+
mode: "browse" | "keys";
|
|
30
|
+
keysTarget: string | null;
|
|
31
|
+
keysActive: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type PromptResult =
|
|
35
|
+
| { action: "done"; providers: Record<string, ProviderCredentials> }
|
|
36
|
+
| { action: "add-key"; providerId: string; returnTo: "browse" | "keys"; state: PromptState };
|
|
37
|
+
|
|
38
|
+
export type ProviderSelectConfig = {
|
|
39
|
+
message: string;
|
|
40
|
+
providerIds: string[];
|
|
41
|
+
initialProviders: Record<string, ProviderCredentials>;
|
|
42
|
+
providerHints?: Record<string, string>;
|
|
43
|
+
resumeState?: PromptState;
|
|
44
|
+
theme?: PartialDeep<Theme>;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// --- Helpers ---
|
|
48
|
+
|
|
49
|
+
type Mode = "browse" | "keys";
|
|
50
|
+
|
|
51
|
+
export function getKeys(apiKey: string | string[] | undefined): string[] {
|
|
52
|
+
if (!apiKey) return [];
|
|
53
|
+
if (Array.isArray(apiKey)) return apiKey.filter((k) => k.length > 0);
|
|
54
|
+
return apiKey.length > 0 && apiKey !== "none" ? [apiKey] : [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function needsApiKey(providerId: string): boolean {
|
|
58
|
+
return providerId !== PROVIDER_IDS.CODEX_APP_SERVER;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function isOllama(providerId: string): boolean {
|
|
62
|
+
return providerId === PROVIDER_IDS.OLLAMA;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function formatKeyInfo(apiKey: string | string[] | undefined): string {
|
|
66
|
+
const count = getKeys(apiKey).length;
|
|
67
|
+
if (count === 0) return "";
|
|
68
|
+
return chalk.gray(` [${count} key${count !== 1 ? "s" : ""}]`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function maskKey(providerId: string, key: string): string {
|
|
72
|
+
if (isOllama(providerId)) return key;
|
|
73
|
+
if (key.length <= 4) return "*".repeat(key.length);
|
|
74
|
+
return "*".repeat(key.length - 4) + key.slice(-4);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const CURSOR = chalk.hex("#FFC107")("›");
|
|
78
|
+
const RULE_WIDTH = 30;
|
|
79
|
+
|
|
80
|
+
// --- Prompt ---
|
|
81
|
+
|
|
82
|
+
export default createPrompt<PromptResult, ProviderSelectConfig>((config, done) => {
|
|
83
|
+
const { providerIds, message, resumeState } = config;
|
|
84
|
+
const theme = makeTheme(config.theme);
|
|
85
|
+
const prefix = usePrefix({ status: "idle", theme });
|
|
86
|
+
const doneIndex = providerIds.length;
|
|
87
|
+
|
|
88
|
+
const [active, setActive] = useState(resumeState?.active ?? 0);
|
|
89
|
+
const [providers, setProviders] = useState<Record<string, ProviderCredentials>>(
|
|
90
|
+
() => resumeState?.providers ?? { ...config.initialProviders },
|
|
91
|
+
);
|
|
92
|
+
const [stash, setStash] = useState<Record<string, ProviderCredentials>>(
|
|
93
|
+
() => resumeState?.stash ?? {},
|
|
94
|
+
);
|
|
95
|
+
const [mode, setMode] = useState<Mode>(resumeState?.mode ?? "browse");
|
|
96
|
+
const [keysTarget, setKeysTarget] = useState<string | null>(resumeState?.keysTarget ?? null);
|
|
97
|
+
const [keysActive, setKeysActive] = useState(resumeState?.keysActive ?? 0);
|
|
98
|
+
|
|
99
|
+
const activeProviderId = useMemo(
|
|
100
|
+
() => (active < providerIds.length ? providerIds[active] : null),
|
|
101
|
+
[active],
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
function currentState(): PromptState {
|
|
105
|
+
return { providers, stash, active, mode, keysTarget, keysActive };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function requestAddKey(providerId: string, returnTo: "browse" | "keys") {
|
|
109
|
+
done({ action: "add-key", providerId, returnTo, state: currentState() });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// --- Keypress handlers ---
|
|
113
|
+
|
|
114
|
+
useKeypress((key, rl) => {
|
|
115
|
+
rl.clearLine(0);
|
|
116
|
+
if (mode === "browse") {
|
|
117
|
+
handleBrowse(key);
|
|
118
|
+
} else {
|
|
119
|
+
handleKeys(key);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
function handleBrowse(key: KeypressEvent) {
|
|
124
|
+
if (isUpKey(key)) {
|
|
125
|
+
setActive(Math.max(0, active - 1));
|
|
126
|
+
} else if (isDownKey(key)) {
|
|
127
|
+
setActive(Math.min(doneIndex, active + 1));
|
|
128
|
+
} else if (isSpaceKey(key) && activeProviderId) {
|
|
129
|
+
toggleProvider(activeProviderId);
|
|
130
|
+
} else if (isEnterKey(key)) {
|
|
131
|
+
if (active === doneIndex) {
|
|
132
|
+
done({ action: "done", providers });
|
|
133
|
+
} else if (activeProviderId && activeProviderId in providers && needsApiKey(activeProviderId)) {
|
|
134
|
+
enterKeysMode(activeProviderId);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function toggleProvider(pid: string) {
|
|
140
|
+
const enabled = pid in providers;
|
|
141
|
+
if (enabled) {
|
|
142
|
+
const updated = { ...providers };
|
|
143
|
+
const newStash = { ...stash };
|
|
144
|
+
newStash[pid] = updated[pid]!;
|
|
145
|
+
delete updated[pid];
|
|
146
|
+
setProviders(updated);
|
|
147
|
+
setStash(newStash);
|
|
148
|
+
} else if (!needsApiKey(pid)) {
|
|
149
|
+
setProviders({ ...providers, [pid]: { apiKey: "none" } });
|
|
150
|
+
} else if (stash[pid]) {
|
|
151
|
+
const newStash = { ...stash };
|
|
152
|
+
const restored = newStash[pid]!;
|
|
153
|
+
delete newStash[pid];
|
|
154
|
+
setProviders({ ...providers, [pid]: restored });
|
|
155
|
+
setStash(newStash);
|
|
156
|
+
} else {
|
|
157
|
+
requestAddKey(pid, "browse");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function enterKeysMode(pid: string) {
|
|
162
|
+
setMode("keys");
|
|
163
|
+
setKeysTarget(pid);
|
|
164
|
+
setKeysActive(0);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function exitKeysMode() {
|
|
168
|
+
setMode("browse");
|
|
169
|
+
setKeysTarget(null);
|
|
170
|
+
setKeysActive(0);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function handleKeys(key: KeypressEvent) {
|
|
174
|
+
if (!keysTarget) return;
|
|
175
|
+
|
|
176
|
+
const keys = getKeys(providers[keysTarget]?.apiKey);
|
|
177
|
+
const addIndex = keys.length;
|
|
178
|
+
const backIndex = keys.length + 1;
|
|
179
|
+
|
|
180
|
+
if (isUpKey(key)) {
|
|
181
|
+
setKeysActive(Math.max(0, keysActive - 1));
|
|
182
|
+
} else if (isDownKey(key)) {
|
|
183
|
+
setKeysActive(Math.min(backIndex, keysActive + 1));
|
|
184
|
+
} else if (key.name === "d" && keysActive < keys.length) {
|
|
185
|
+
deleteKey(keysTarget, keysActive, keys);
|
|
186
|
+
} else if (isEnterKey(key)) {
|
|
187
|
+
if (keysActive === addIndex) {
|
|
188
|
+
requestAddKey(keysTarget, "keys");
|
|
189
|
+
} else if (keysActive === backIndex) {
|
|
190
|
+
exitKeysMode();
|
|
191
|
+
}
|
|
192
|
+
} else if (key.name === "escape") {
|
|
193
|
+
exitKeysMode();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function deleteKey(pid: string, index: number, keys: string[]) {
|
|
198
|
+
const remaining = keys.filter((_, i) => i !== index);
|
|
199
|
+
if (remaining.length === 0) {
|
|
200
|
+
const updated = { ...providers };
|
|
201
|
+
delete updated[pid];
|
|
202
|
+
setProviders(updated);
|
|
203
|
+
exitKeysMode();
|
|
204
|
+
} else {
|
|
205
|
+
setProviders({
|
|
206
|
+
...providers,
|
|
207
|
+
[pid]: { ...providers[pid], apiKey: remaining.length === 1 ? remaining[0]! : remaining },
|
|
208
|
+
});
|
|
209
|
+
setKeysActive(Math.min(keysActive, remaining.length - 1));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// --- Rendering ---
|
|
214
|
+
|
|
215
|
+
const styledMessage = theme.style.message(message, "idle");
|
|
216
|
+
const lines: string[] = [`${prefix} ${styledMessage}`];
|
|
217
|
+
|
|
218
|
+
if (mode === "keys" && keysTarget) {
|
|
219
|
+
renderKeysView(lines, keysTarget);
|
|
220
|
+
} else {
|
|
221
|
+
renderBrowseView(lines);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return `${lines.join("\n")}${cursorHide}`;
|
|
225
|
+
|
|
226
|
+
function renderBrowseView(out: string[]) {
|
|
227
|
+
for (let i = 0; i < providerIds.length; i++) {
|
|
228
|
+
const pid = providerIds[i]!;
|
|
229
|
+
const name = ProviderConfigUI.getProviderDisplayName(pid);
|
|
230
|
+
const pfx = i === active ? `${CURSOR} ` : " ";
|
|
231
|
+
const enabled = pid in providers;
|
|
232
|
+
const hint = config.providerHints?.[pid];
|
|
233
|
+
|
|
234
|
+
if (enabled) {
|
|
235
|
+
const keyInfo = formatKeyInfo(providers[pid]?.apiKey);
|
|
236
|
+
out.push(`${pfx}${display.providerCheck(name)}${keyInfo}`);
|
|
237
|
+
} else {
|
|
238
|
+
const hintSuffix = hint ? chalk.dim(` — ${hint}`) : "";
|
|
239
|
+
out.push(`${pfx}${display.providerUncheck(name)}${hintSuffix}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const donePfx = active === doneIndex ? `${CURSOR} ` : " ";
|
|
244
|
+
out.push(`${donePfx}${display.doneLabel()}`);
|
|
245
|
+
|
|
246
|
+
const help = [
|
|
247
|
+
`${chalk.bold("↑↓")} ${chalk.dim("navigate")}`,
|
|
248
|
+
`${chalk.bold("space")} ${chalk.dim("toggle")}`,
|
|
249
|
+
`${chalk.bold("⏎")} ${chalk.dim("manage keys / done")}`,
|
|
250
|
+
];
|
|
251
|
+
out.push(chalk.dim(` ${help.join(chalk.dim(" • "))}`));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function renderKeysView(out: string[], pid: string) {
|
|
255
|
+
const name = ProviderConfigUI.getProviderDisplayName(pid);
|
|
256
|
+
const keys = getKeys(providers[pid]?.apiKey);
|
|
257
|
+
const addIndex = keys.length;
|
|
258
|
+
const backIndex = keys.length + 1;
|
|
259
|
+
|
|
260
|
+
out.push(` ${chalk.bold(name)} ${chalk.dim("— API Keys")}`);
|
|
261
|
+
out.push(` ${chalk.dim("─".repeat(RULE_WIDTH))}`);
|
|
262
|
+
|
|
263
|
+
for (let i = 0; i < keys.length; i++) {
|
|
264
|
+
const pfx = keysActive === i ? `${CURSOR} ` : " ";
|
|
265
|
+
const masked = maskKey(pid, keys[i]!);
|
|
266
|
+
const deleteHint = keysActive === i ? chalk.dim(" d delete") : "";
|
|
267
|
+
out.push(`${pfx}${masked}${deleteHint}`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const addPfx = keysActive === addIndex ? `${CURSOR} ` : " ";
|
|
271
|
+
out.push(`${addPfx}${chalk.dim("+ Add another key")}`);
|
|
272
|
+
|
|
273
|
+
const backPfx = keysActive === backIndex ? `${CURSOR} ` : " ";
|
|
274
|
+
out.push(`${backPfx}${chalk.dim("← Back")}`);
|
|
275
|
+
|
|
276
|
+
const help = [
|
|
277
|
+
`${chalk.bold("↑↓")} ${chalk.dim("navigate")}`,
|
|
278
|
+
`${chalk.bold("d")} ${chalk.dim("delete key")}`,
|
|
279
|
+
`${chalk.bold("⏎")} ${chalk.dim("select")}`,
|
|
280
|
+
`${chalk.bold("esc")} ${chalk.dim("back")}`,
|
|
281
|
+
];
|
|
282
|
+
out.push(chalk.dim(` ${help.join(chalk.dim(" • "))}`));
|
|
283
|
+
}
|
|
284
|
+
});
|