deepagentsdk 0.9.2
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 +21 -0
- package/README.md +159 -0
- package/package.json +95 -0
- package/src/agent.ts +1230 -0
- package/src/backends/composite.ts +273 -0
- package/src/backends/filesystem.ts +692 -0
- package/src/backends/index.ts +22 -0
- package/src/backends/local-sandbox.ts +175 -0
- package/src/backends/persistent.ts +593 -0
- package/src/backends/sandbox.ts +510 -0
- package/src/backends/state.ts +244 -0
- package/src/backends/utils.ts +287 -0
- package/src/checkpointer/file-saver.ts +98 -0
- package/src/checkpointer/index.ts +5 -0
- package/src/checkpointer/kv-saver.ts +82 -0
- package/src/checkpointer/memory-saver.ts +82 -0
- package/src/checkpointer/types.ts +125 -0
- package/src/cli/components/ApiKeyInput.tsx +300 -0
- package/src/cli/components/FilePreview.tsx +237 -0
- package/src/cli/components/Input.tsx +277 -0
- package/src/cli/components/Message.tsx +93 -0
- package/src/cli/components/ModelSelection.tsx +338 -0
- package/src/cli/components/SlashMenu.tsx +101 -0
- package/src/cli/components/StatusBar.tsx +89 -0
- package/src/cli/components/Subagent.tsx +91 -0
- package/src/cli/components/TodoList.tsx +133 -0
- package/src/cli/components/ToolApproval.tsx +70 -0
- package/src/cli/components/ToolCall.tsx +144 -0
- package/src/cli/components/ToolCallSummary.tsx +175 -0
- package/src/cli/components/Welcome.tsx +75 -0
- package/src/cli/components/index.ts +24 -0
- package/src/cli/hooks/index.ts +12 -0
- package/src/cli/hooks/useAgent.ts +933 -0
- package/src/cli/index.tsx +1066 -0
- package/src/cli/theme.ts +205 -0
- package/src/cli/utils/model-list.ts +365 -0
- package/src/constants/errors.ts +29 -0
- package/src/constants/limits.ts +195 -0
- package/src/index.ts +176 -0
- package/src/middleware/agent-memory.ts +330 -0
- package/src/prompts.ts +196 -0
- package/src/skills/index.ts +2 -0
- package/src/skills/load.ts +191 -0
- package/src/skills/types.ts +53 -0
- package/src/tools/execute.ts +167 -0
- package/src/tools/filesystem.ts +418 -0
- package/src/tools/index.ts +39 -0
- package/src/tools/subagent.ts +443 -0
- package/src/tools/todos.ts +101 -0
- package/src/tools/web.ts +567 -0
- package/src/types/backend.ts +177 -0
- package/src/types/core.ts +220 -0
- package/src/types/events.ts +429 -0
- package/src/types/index.ts +94 -0
- package/src/types/structured-output.ts +43 -0
- package/src/types/subagent.ts +96 -0
- package/src/types.ts +22 -0
- package/src/utils/approval.ts +213 -0
- package/src/utils/events.ts +416 -0
- package/src/utils/eviction.ts +181 -0
- package/src/utils/index.ts +34 -0
- package/src/utils/model-parser.ts +38 -0
- package/src/utils/patch-tool-calls.ts +233 -0
- package/src/utils/project-detection.ts +32 -0
- package/src/utils/summarization.ts +254 -0
package/src/cli/theme.ts
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme constants for the Ink CLI.
|
|
3
|
+
* Colors, emoji, and styling values.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Color palette for the CLI.
|
|
8
|
+
* Uses chalk-compatible color names.
|
|
9
|
+
*/
|
|
10
|
+
export const colors = {
|
|
11
|
+
// Primary colors
|
|
12
|
+
primary: "cyan",
|
|
13
|
+
secondary: "magenta",
|
|
14
|
+
accent: "yellow",
|
|
15
|
+
|
|
16
|
+
// Status colors
|
|
17
|
+
success: "green",
|
|
18
|
+
error: "red",
|
|
19
|
+
warning: "yellow",
|
|
20
|
+
info: "blue",
|
|
21
|
+
|
|
22
|
+
// Text colors
|
|
23
|
+
muted: "gray",
|
|
24
|
+
highlight: "white",
|
|
25
|
+
|
|
26
|
+
// Semantic colors
|
|
27
|
+
tool: "magenta",
|
|
28
|
+
file: "blue",
|
|
29
|
+
user: "cyan",
|
|
30
|
+
agent: "green",
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Status emoji for todo items and events.
|
|
35
|
+
*/
|
|
36
|
+
export const emoji = {
|
|
37
|
+
// Todo statuses
|
|
38
|
+
pending: "⏳",
|
|
39
|
+
in_progress: "🔄",
|
|
40
|
+
completed: "✅",
|
|
41
|
+
cancelled: "❌",
|
|
42
|
+
|
|
43
|
+
// UI elements
|
|
44
|
+
robot: "🤖",
|
|
45
|
+
thinking: "💭",
|
|
46
|
+
tool: "🔧",
|
|
47
|
+
file: "📁",
|
|
48
|
+
edit: "✏️",
|
|
49
|
+
todo: "📋",
|
|
50
|
+
done: "🎉",
|
|
51
|
+
error: "💥",
|
|
52
|
+
warning: "⚠️",
|
|
53
|
+
info: "ℹ️",
|
|
54
|
+
user: "👤",
|
|
55
|
+
subagent: "🤝",
|
|
56
|
+
key: "🔑",
|
|
57
|
+
model: "🧠",
|
|
58
|
+
} as const;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Box border styles for different UI elements.
|
|
62
|
+
*/
|
|
63
|
+
export const borders = {
|
|
64
|
+
panel: "round",
|
|
65
|
+
preview: "single",
|
|
66
|
+
alert: "double",
|
|
67
|
+
} as const;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Slash commands configuration.
|
|
71
|
+
*/
|
|
72
|
+
export interface SlashCommand {
|
|
73
|
+
command: string;
|
|
74
|
+
aliases: string[];
|
|
75
|
+
description: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const SLASH_COMMANDS: SlashCommand[] = [
|
|
79
|
+
{
|
|
80
|
+
command: "/todos",
|
|
81
|
+
aliases: ["/t", "/todo"],
|
|
82
|
+
description: "Show current todo list",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
command: "/files",
|
|
86
|
+
aliases: ["/f", "/file"],
|
|
87
|
+
description: "Show files in working directory",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
command: "/read",
|
|
91
|
+
aliases: ["/r"],
|
|
92
|
+
description: "Read a file (usage: /read <path>)",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
command: "/apikey",
|
|
96
|
+
aliases: ["/key", "/api"],
|
|
97
|
+
description: "Manage API keys (interactive)",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
command: "/model",
|
|
101
|
+
aliases: [],
|
|
102
|
+
description: "Show available models or change model (usage: /model [model-name])",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
command: "/features",
|
|
106
|
+
aliases: ["/feat"],
|
|
107
|
+
description: "Show enabled features (caching, eviction, summarization)",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
command: "/tokens",
|
|
111
|
+
aliases: ["/tok"],
|
|
112
|
+
description: "Show estimated token count for conversation",
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
command: "/cache",
|
|
116
|
+
aliases: [],
|
|
117
|
+
description: "Toggle prompt caching (usage: /cache on|off)",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
command: "/eviction",
|
|
121
|
+
aliases: ["/evict"],
|
|
122
|
+
description: "Toggle tool result eviction (usage: /eviction on|off)",
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
command: "/summarize",
|
|
126
|
+
aliases: ["/sum"],
|
|
127
|
+
description: "Toggle auto-summarization (usage: /summarize on|off)",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
command: "/approve",
|
|
131
|
+
aliases: [],
|
|
132
|
+
description: "Toggle auto-approve mode for tool executions",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
command: "/clear",
|
|
136
|
+
aliases: ["/c"],
|
|
137
|
+
description: "Clear chat history",
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
command: "/sessions",
|
|
141
|
+
aliases: ["/session-list"],
|
|
142
|
+
description: "List saved sessions",
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
command: "/session",
|
|
146
|
+
aliases: [],
|
|
147
|
+
description: "Session management (usage: /session clear)",
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
command: "/help",
|
|
151
|
+
aliases: ["/h", "/?"],
|
|
152
|
+
description: "Show help",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
command: "/quit",
|
|
156
|
+
aliases: ["/q", "/exit"],
|
|
157
|
+
description: "Exit the CLI",
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Filter commands by prefix.
|
|
163
|
+
*/
|
|
164
|
+
export function filterCommands(prefix?: string): SlashCommand[] {
|
|
165
|
+
if (!prefix) {
|
|
166
|
+
return SLASH_COMMANDS;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const normalizedPrefix = prefix.startsWith("/") ? prefix : `/${prefix}`;
|
|
170
|
+
|
|
171
|
+
return SLASH_COMMANDS.filter((cmd) => {
|
|
172
|
+
if (cmd.command.toLowerCase().startsWith(normalizedPrefix.toLowerCase())) {
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
return cmd.aliases.some((alias) =>
|
|
176
|
+
alias.toLowerCase().startsWith(normalizedPrefix.toLowerCase())
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Parse a slash command from input.
|
|
183
|
+
*/
|
|
184
|
+
export function parseCommand(input: string): {
|
|
185
|
+
isCommand: boolean;
|
|
186
|
+
command?: string;
|
|
187
|
+
args?: string;
|
|
188
|
+
} {
|
|
189
|
+
const trimmed = input.trim();
|
|
190
|
+
|
|
191
|
+
if (!trimmed.startsWith("/")) {
|
|
192
|
+
return { isCommand: false };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const parts = trimmed.slice(1).split(/\s+/);
|
|
196
|
+
const command = parts[0]?.toLowerCase();
|
|
197
|
+
const args = parts.slice(1).join(" ");
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
isCommand: true,
|
|
201
|
+
command,
|
|
202
|
+
args: args || undefined,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility to detect available API keys and fetch models from provider APIs.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface AvailableModel {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
provider: "anthropic" | "openai";
|
|
9
|
+
description?: string;
|
|
10
|
+
createdAt?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface FetchModelsResult {
|
|
14
|
+
models: AvailableModel[];
|
|
15
|
+
error?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Cache Implementation
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
interface CacheEntry {
|
|
23
|
+
models: AvailableModel[];
|
|
24
|
+
timestamp: number;
|
|
25
|
+
apiKeyHash: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const modelCache: {
|
|
29
|
+
anthropic?: CacheEntry;
|
|
30
|
+
openai?: CacheEntry;
|
|
31
|
+
} = {};
|
|
32
|
+
|
|
33
|
+
// Cache TTL: 24 hours in milliseconds
|
|
34
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Simple hash function for API key to detect changes.
|
|
38
|
+
*/
|
|
39
|
+
function hashApiKey(key: string | undefined): string {
|
|
40
|
+
if (!key) return "";
|
|
41
|
+
// Just use first 10 and last 4 chars as a simple fingerprint
|
|
42
|
+
return `${key.substring(0, 10)}...${key.substring(key.length - 4)}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if cache is valid for a provider.
|
|
47
|
+
*/
|
|
48
|
+
function isCacheValid(provider: "anthropic" | "openai"): boolean {
|
|
49
|
+
const entry = modelCache[provider];
|
|
50
|
+
if (!entry) return false;
|
|
51
|
+
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
if (now - entry.timestamp > CACHE_TTL_MS) return false;
|
|
54
|
+
|
|
55
|
+
// Check if API key changed
|
|
56
|
+
const currentKeyHash =
|
|
57
|
+
provider === "anthropic"
|
|
58
|
+
? hashApiKey(process.env.ANTHROPIC_API_KEY)
|
|
59
|
+
: hashApiKey(process.env.OPENAI_API_KEY);
|
|
60
|
+
|
|
61
|
+
return entry.apiKeyHash === currentKeyHash;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get cached models for a provider.
|
|
66
|
+
*/
|
|
67
|
+
function getCachedModels(provider: "anthropic" | "openai"): AvailableModel[] | null {
|
|
68
|
+
if (isCacheValid(provider)) {
|
|
69
|
+
return modelCache[provider]!.models;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Set cached models for a provider.
|
|
76
|
+
*/
|
|
77
|
+
function setCachedModels(provider: "anthropic" | "openai", models: AvailableModel[]): void {
|
|
78
|
+
const apiKeyHash =
|
|
79
|
+
provider === "anthropic"
|
|
80
|
+
? hashApiKey(process.env.ANTHROPIC_API_KEY)
|
|
81
|
+
: hashApiKey(process.env.OPENAI_API_KEY);
|
|
82
|
+
|
|
83
|
+
modelCache[provider] = {
|
|
84
|
+
models,
|
|
85
|
+
timestamp: Date.now(),
|
|
86
|
+
apiKeyHash,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Clear cache for a specific provider or all providers.
|
|
92
|
+
*/
|
|
93
|
+
export function clearModelCache(provider?: "anthropic" | "openai"): void {
|
|
94
|
+
if (provider) {
|
|
95
|
+
delete modelCache[provider];
|
|
96
|
+
} else {
|
|
97
|
+
delete modelCache.anthropic;
|
|
98
|
+
delete modelCache.openai;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ============================================================================
|
|
103
|
+
// Provider Detection
|
|
104
|
+
// ============================================================================
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Detect which API keys are available in the environment.
|
|
108
|
+
*/
|
|
109
|
+
export function detectAvailableProviders(): {
|
|
110
|
+
anthropic: boolean;
|
|
111
|
+
openai: boolean;
|
|
112
|
+
} {
|
|
113
|
+
return {
|
|
114
|
+
anthropic: !!process.env.ANTHROPIC_API_KEY,
|
|
115
|
+
openai: !!process.env.OPENAI_API_KEY,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ============================================================================
|
|
120
|
+
// Anthropic API
|
|
121
|
+
// ============================================================================
|
|
122
|
+
|
|
123
|
+
interface AnthropicModelInfo {
|
|
124
|
+
id: string;
|
|
125
|
+
display_name: string;
|
|
126
|
+
created_at: string;
|
|
127
|
+
type: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface AnthropicModelsResponse {
|
|
131
|
+
data: AnthropicModelInfo[];
|
|
132
|
+
has_more: boolean;
|
|
133
|
+
first_id: string;
|
|
134
|
+
last_id: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Fetch available models from Anthropic API.
|
|
139
|
+
* Returns empty array if no API key is set.
|
|
140
|
+
*/
|
|
141
|
+
export async function fetchAnthropicModels(): Promise<FetchModelsResult> {
|
|
142
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
143
|
+
if (!apiKey) {
|
|
144
|
+
return { models: [], error: "No Anthropic API key configured" };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Check cache first
|
|
148
|
+
const cached = getCachedModels("anthropic");
|
|
149
|
+
if (cached) {
|
|
150
|
+
return { models: cached };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const response = await fetch("https://api.anthropic.com/v1/models?limit=100", {
|
|
155
|
+
method: "GET",
|
|
156
|
+
headers: {
|
|
157
|
+
"X-Api-Key": apiKey,
|
|
158
|
+
"anthropic-version": "2023-06-01",
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
const errorText = await response.text();
|
|
164
|
+
if (response.status === 401) {
|
|
165
|
+
return { models: [], error: "Invalid Anthropic API key" };
|
|
166
|
+
}
|
|
167
|
+
return { models: [], error: `Anthropic API error: ${response.status} ${errorText}` };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const data = (await response.json()) as AnthropicModelsResponse;
|
|
171
|
+
|
|
172
|
+
const models: AvailableModel[] = data.data.map((model) => ({
|
|
173
|
+
id: `anthropic/${model.id}`,
|
|
174
|
+
name: model.id,
|
|
175
|
+
provider: "anthropic" as const,
|
|
176
|
+
description: model.display_name,
|
|
177
|
+
createdAt: model.created_at,
|
|
178
|
+
}));
|
|
179
|
+
|
|
180
|
+
// Sort by created_at descending (newest first)
|
|
181
|
+
models.sort((a, b) => {
|
|
182
|
+
if (!a.createdAt || !b.createdAt) return 0;
|
|
183
|
+
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Cache the results
|
|
187
|
+
setCachedModels("anthropic", models);
|
|
188
|
+
|
|
189
|
+
return { models };
|
|
190
|
+
} catch (error) {
|
|
191
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
192
|
+
return { models: [], error: `Failed to fetch Anthropic models: ${message}` };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ============================================================================
|
|
197
|
+
// OpenAI API
|
|
198
|
+
// ============================================================================
|
|
199
|
+
|
|
200
|
+
interface OpenAIModelInfo {
|
|
201
|
+
id: string;
|
|
202
|
+
object: string;
|
|
203
|
+
created: number;
|
|
204
|
+
owned_by: string;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
interface OpenAIModelsResponse {
|
|
208
|
+
object: string;
|
|
209
|
+
data: OpenAIModelInfo[];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Fetch available models from OpenAI API.
|
|
214
|
+
* Returns empty array if no API key is set.
|
|
215
|
+
*/
|
|
216
|
+
export async function fetchOpenAIModels(): Promise<FetchModelsResult> {
|
|
217
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
218
|
+
if (!apiKey) {
|
|
219
|
+
return { models: [], error: "No OpenAI API key configured" };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Check cache first
|
|
223
|
+
const cached = getCachedModels("openai");
|
|
224
|
+
if (cached) {
|
|
225
|
+
return { models: cached };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const response = await fetch("https://api.openai.com/v1/models", {
|
|
230
|
+
method: "GET",
|
|
231
|
+
headers: {
|
|
232
|
+
Authorization: `Bearer ${apiKey}`,
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
if (!response.ok) {
|
|
237
|
+
const errorText = await response.text();
|
|
238
|
+
if (response.status === 401) {
|
|
239
|
+
return { models: [], error: "Invalid OpenAI API key" };
|
|
240
|
+
}
|
|
241
|
+
return { models: [], error: `OpenAI API error: ${response.status} ${errorText}` };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const data = (await response.json()) as OpenAIModelsResponse;
|
|
245
|
+
|
|
246
|
+
// Filter to only include chat models (gpt-*)
|
|
247
|
+
const chatModels = data.data.filter(
|
|
248
|
+
(model) =>
|
|
249
|
+
model.id.startsWith("gpt-") ||
|
|
250
|
+
model.id.startsWith("o1") ||
|
|
251
|
+
model.id.startsWith("o3") ||
|
|
252
|
+
model.id.includes("chatgpt")
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const models: AvailableModel[] = chatModels.map((model) => ({
|
|
256
|
+
id: `openai/${model.id}`,
|
|
257
|
+
name: model.id,
|
|
258
|
+
provider: "openai" as const,
|
|
259
|
+
description: getOpenAIModelDescription(model.id),
|
|
260
|
+
createdAt: new Date(model.created * 1000).toISOString(),
|
|
261
|
+
}));
|
|
262
|
+
|
|
263
|
+
// Sort by created descending (newest first)
|
|
264
|
+
models.sort((a, b) => {
|
|
265
|
+
if (!a.createdAt || !b.createdAt) return 0;
|
|
266
|
+
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Cache the results
|
|
270
|
+
setCachedModels("openai", models);
|
|
271
|
+
|
|
272
|
+
return { models };
|
|
273
|
+
} catch (error) {
|
|
274
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
275
|
+
return { models: [], error: `Failed to fetch OpenAI models: ${message}` };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Generate a description for OpenAI models based on their ID.
|
|
281
|
+
*/
|
|
282
|
+
function getOpenAIModelDescription(modelId: string): string {
|
|
283
|
+
if (modelId.includes("gpt-4o-mini")) return "Fast and affordable GPT-4 variant";
|
|
284
|
+
if (modelId.includes("gpt-4o")) return "Latest GPT-4 optimized model";
|
|
285
|
+
if (modelId.includes("gpt-4-turbo")) return "GPT-4 Turbo";
|
|
286
|
+
if (modelId.includes("gpt-4")) return "GPT-4";
|
|
287
|
+
if (modelId.includes("gpt-3.5-turbo")) return "Fast and affordable";
|
|
288
|
+
if (modelId.startsWith("o1")) return "OpenAI o1 reasoning model";
|
|
289
|
+
if (modelId.startsWith("o3")) return "OpenAI o3 reasoning model";
|
|
290
|
+
return "";
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ============================================================================
|
|
294
|
+
// Combined Functions
|
|
295
|
+
// ============================================================================
|
|
296
|
+
|
|
297
|
+
export interface GetModelsResult {
|
|
298
|
+
models: AvailableModel[];
|
|
299
|
+
errors: { provider: string; error: string }[];
|
|
300
|
+
loading: boolean;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Get list of available models from all configured providers.
|
|
305
|
+
* Only fetches from providers that have API keys configured.
|
|
306
|
+
*/
|
|
307
|
+
export async function getAvailableModels(): Promise<GetModelsResult> {
|
|
308
|
+
const providers = detectAvailableProviders();
|
|
309
|
+
const allModels: AvailableModel[] = [];
|
|
310
|
+
const errors: { provider: string; error: string }[] = [];
|
|
311
|
+
|
|
312
|
+
// Fetch from both providers in parallel
|
|
313
|
+
const promises: Promise<void>[] = [];
|
|
314
|
+
|
|
315
|
+
if (providers.anthropic) {
|
|
316
|
+
promises.push(
|
|
317
|
+
fetchAnthropicModels().then((result) => {
|
|
318
|
+
allModels.push(...result.models);
|
|
319
|
+
if (result.error) {
|
|
320
|
+
errors.push({ provider: "Anthropic", error: result.error });
|
|
321
|
+
}
|
|
322
|
+
})
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (providers.openai) {
|
|
327
|
+
promises.push(
|
|
328
|
+
fetchOpenAIModels().then((result) => {
|
|
329
|
+
allModels.push(...result.models);
|
|
330
|
+
if (result.error) {
|
|
331
|
+
errors.push({ provider: "OpenAI", error: result.error });
|
|
332
|
+
}
|
|
333
|
+
})
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
await Promise.all(promises);
|
|
338
|
+
|
|
339
|
+
return { models: allModels, errors, loading: false };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Get models grouped by provider.
|
|
344
|
+
*/
|
|
345
|
+
export async function getModelsByProvider(): Promise<{
|
|
346
|
+
anthropic?: AvailableModel[];
|
|
347
|
+
openai?: AvailableModel[];
|
|
348
|
+
errors: { provider: string; error: string }[];
|
|
349
|
+
}> {
|
|
350
|
+
const result = await getAvailableModels();
|
|
351
|
+
const grouped: {
|
|
352
|
+
anthropic?: AvailableModel[];
|
|
353
|
+
openai?: AvailableModel[];
|
|
354
|
+
errors: { provider: string; error: string }[];
|
|
355
|
+
} = { errors: result.errors };
|
|
356
|
+
|
|
357
|
+
for (const model of result.models) {
|
|
358
|
+
if (!grouped[model.provider]) {
|
|
359
|
+
grouped[model.provider] = [];
|
|
360
|
+
}
|
|
361
|
+
grouped[model.provider]!.push(model);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return grouped;
|
|
365
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized error message constants for backend operations
|
|
3
|
+
* Reduces duplication across 6 backend implementations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const FILE_NOT_FOUND = (path: string) =>
|
|
7
|
+
`Error: File '${path}' not found`;
|
|
8
|
+
|
|
9
|
+
export const FILE_ALREADY_EXISTS = (path: string) =>
|
|
10
|
+
`Cannot write to ${path} because it already exists. Read and then make an edit, or write to a new path.`;
|
|
11
|
+
|
|
12
|
+
export const STRING_NOT_FOUND = (path: string, string: string) =>
|
|
13
|
+
`Error: String not found in file: '${path}'\n\n${string}`;
|
|
14
|
+
|
|
15
|
+
export const INVALID_REGEX = (message: string) =>
|
|
16
|
+
`Invalid regex pattern: ${message}`;
|
|
17
|
+
|
|
18
|
+
export const WEB_SEARCH_ERROR = (message: string) =>
|
|
19
|
+
`Web search error: ${message}`;
|
|
20
|
+
|
|
21
|
+
export const REQUEST_TIMEOUT = (timeout: number) =>
|
|
22
|
+
`Request timed out after ${timeout} seconds`;
|
|
23
|
+
|
|
24
|
+
export const SYSTEM_REMINDER_FILE_EMPTY =
|
|
25
|
+
'System reminder: File exists but has empty contents';
|
|
26
|
+
|
|
27
|
+
// Generic errors
|
|
28
|
+
export const OPERATION_ERROR = (operation: string, message: string) =>
|
|
29
|
+
`Operation error: ${operation} - ${message}`;
|