@llmindset/hf-mcp 0.1.16
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/dist/dataset-detail.d.ts +26 -0
- package/dist/dataset-detail.d.ts.map +1 -0
- package/dist/dataset-detail.js +157 -0
- package/dist/dataset-detail.js.map +1 -0
- package/dist/dataset-search.d.ts +62 -0
- package/dist/dataset-search.d.ts.map +1 -0
- package/dist/dataset-search.js +158 -0
- package/dist/dataset-search.js.map +1 -0
- package/dist/duplicate-space.d.ts +75 -0
- package/dist/duplicate-space.d.ts.map +1 -0
- package/dist/duplicate-space.js +189 -0
- package/dist/duplicate-space.js.map +1 -0
- package/dist/error-messages.d.ts +4 -0
- package/dist/error-messages.d.ts.map +1 -0
- package/dist/error-messages.js +30 -0
- package/dist/error-messages.js.map +1 -0
- package/dist/hf-api-call.d.ts +18 -0
- package/dist/hf-api-call.d.ts.map +1 -0
- package/dist/hf-api-call.js +105 -0
- package/dist/hf-api-call.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/model-detail.d.ts +26 -0
- package/dist/model-detail.d.ts.map +1 -0
- package/dist/model-detail.js +224 -0
- package/dist/model-detail.js.map +1 -0
- package/dist/model-search.d.ts +64 -0
- package/dist/model-search.d.ts.map +1 -0
- package/dist/model-search.js +161 -0
- package/dist/model-search.js.map +1 -0
- package/dist/paper-search.d.ts +58 -0
- package/dist/paper-search.d.ts.map +1 -0
- package/dist/paper-search.js +114 -0
- package/dist/paper-search.js.map +1 -0
- package/dist/paper-summary.d.ts +35 -0
- package/dist/paper-summary.d.ts.map +1 -0
- package/dist/paper-summary.js +187 -0
- package/dist/paper-summary.js.map +1 -0
- package/dist/space-files.d.ts +44 -0
- package/dist/space-files.d.ts.map +1 -0
- package/dist/space-files.js +242 -0
- package/dist/space-files.js.map +1 -0
- package/dist/space-info.d.ts +56 -0
- package/dist/space-info.d.ts.map +1 -0
- package/dist/space-info.js +135 -0
- package/dist/space-info.js.map +1 -0
- package/dist/space-search.d.ts +71 -0
- package/dist/space-search.d.ts.map +1 -0
- package/dist/space-search.js +95 -0
- package/dist/space-search.js.map +1 -0
- package/dist/tool-ids.d.ts +23 -0
- package/dist/tool-ids.d.ts.map +1 -0
- package/dist/tool-ids.js +55 -0
- package/dist/tool-ids.js.map +1 -0
- package/dist/user-summary.d.ts +56 -0
- package/dist/user-summary.d.ts.map +1 -0
- package/dist/user-summary.js +271 -0
- package/dist/user-summary.js.map +1 -0
- package/dist/utilities.d.ts +8 -0
- package/dist/utilities.d.ts.map +1 -0
- package/dist/utilities.js +53 -0
- package/dist/utilities.js.map +1 -0
- package/eslint.config.js +43 -0
- package/package.json +47 -0
- package/src/dataset-detail.ts +257 -0
- package/src/dataset-search.ts +237 -0
- package/src/duplicate-space.ts +263 -0
- package/src/error-messages.ts +57 -0
- package/src/hf-api-call.ts +182 -0
- package/src/index.ts +18 -0
- package/src/model-detail.ts +359 -0
- package/src/model-search.ts +231 -0
- package/src/paper-search.ts +188 -0
- package/src/paper-summary.ts +303 -0
- package/src/space-files.ts +325 -0
- package/src/space-info.ts +190 -0
- package/src/space-search.ts +177 -0
- package/src/tool-ids.ts +84 -0
- package/src/user-summary.ts +421 -0
- package/src/utilities.ts +64 -0
- package/test/duplicate-space.spec.ts +41 -0
- package/test/fixtures/paper_result_kazakh.json +854 -0
- package/test/fixtures/space-result.json +263 -0
- package/test/paper-search.spec.ts +57 -0
- package/test/paper-summary.spec.ts +113 -0
- package/test/space-files.spec.ts +232 -0
- package/test/space-search.spec.ts +29 -0
- package/test/user-summary.spec.ts +131 -0
- package/tsconfig.json +31 -0
- package/vitest.config.ts +11 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { HfApiCall } from './hf-api-call.js';
|
|
3
|
+
import { explain } from './error-messages.js';
|
|
4
|
+
import { NO_TOKEN_INSTRUCTIONS } from './utilities.js';
|
|
5
|
+
|
|
6
|
+
export interface SpaceInfo {
|
|
7
|
+
runtime?: {
|
|
8
|
+
hardware?:
|
|
9
|
+
| {
|
|
10
|
+
current?: string;
|
|
11
|
+
requested?: string;
|
|
12
|
+
}
|
|
13
|
+
| string;
|
|
14
|
+
};
|
|
15
|
+
gated?: boolean;
|
|
16
|
+
models?: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SpaceVariable {
|
|
20
|
+
key: string;
|
|
21
|
+
value: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DuplicateSpaceParams {
|
|
26
|
+
sourceSpaceId: string;
|
|
27
|
+
newSpaceName?: string;
|
|
28
|
+
hardware?: 'freecpu' | 'zerogpu';
|
|
29
|
+
private?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DuplicateSpaceResult {
|
|
33
|
+
url: string;
|
|
34
|
+
spaceId: string;
|
|
35
|
+
hardware: string;
|
|
36
|
+
private: boolean;
|
|
37
|
+
variablesCopied: number;
|
|
38
|
+
instructions: string;
|
|
39
|
+
hardwareWarning?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const DUPLICATE_SPACE_TOOL_CONFIG = {
|
|
43
|
+
name: 'duplicate_space',
|
|
44
|
+
description: '', // This will be dynamically set with username
|
|
45
|
+
schema: z.object({
|
|
46
|
+
sourceSpaceId: z.string().min(1).describe("Space ID to copy (e.g., 'username/space-name')"),
|
|
47
|
+
newSpaceId: z.string().optional().describe('Name for the new space (optional, defaults to source space-name)'),
|
|
48
|
+
hardware: z
|
|
49
|
+
.enum(['freecpu', 'zerogpu'])
|
|
50
|
+
.optional()
|
|
51
|
+
.describe('Either "freecpu" or "zerogpu" (defaults based on source). Both options are in the free tier.'),
|
|
52
|
+
private: z
|
|
53
|
+
.boolean()
|
|
54
|
+
.optional()
|
|
55
|
+
.default(true)
|
|
56
|
+
.describe('Check with User whether the new space should be public or private.'),
|
|
57
|
+
}),
|
|
58
|
+
annotations: {
|
|
59
|
+
title: 'Duplicate Hugging Face Space',
|
|
60
|
+
destructiveHint: false,
|
|
61
|
+
readOnlyHint: false,
|
|
62
|
+
openWorldHint: true,
|
|
63
|
+
},
|
|
64
|
+
} as const;
|
|
65
|
+
|
|
66
|
+
// Hardware mapping constants
|
|
67
|
+
const HARDWARE_MAP = {
|
|
68
|
+
freecpu: 'cpu-basic',
|
|
69
|
+
zerogpu: 'zero-a10g',
|
|
70
|
+
} as const;
|
|
71
|
+
|
|
72
|
+
const FREE_HARDWARE = ['cpu-basic', 'zero-a10g'];
|
|
73
|
+
|
|
74
|
+
export class DuplicateSpaceTool extends HfApiCall<DuplicateSpaceParams, DuplicateSpaceResult> {
|
|
75
|
+
private username?: string;
|
|
76
|
+
|
|
77
|
+
constructor(hfToken?: string, username?: string) {
|
|
78
|
+
super('https://huggingface.co/api', hfToken);
|
|
79
|
+
this.username = username;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
static createToolConfig(
|
|
83
|
+
username?: string
|
|
84
|
+
): Omit<typeof DUPLICATE_SPACE_TOOL_CONFIG, 'description'> & { description: string } {
|
|
85
|
+
const description = username
|
|
86
|
+
? `Duplicate a Hugging Face Space. Target space will be created as ${username}/<new-space-name>.`
|
|
87
|
+
: NO_TOKEN_INSTRUCTIONS;
|
|
88
|
+
return {
|
|
89
|
+
...DUPLICATE_SPACE_TOOL_CONFIG,
|
|
90
|
+
description,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
normalizeSpaceName(spaceName: string): string {
|
|
95
|
+
// If already has a slash, check if it's trying to use a different username
|
|
96
|
+
if (spaceName.includes('/')) {
|
|
97
|
+
const [providedUser, spaceNamePart] = spaceName.split('/');
|
|
98
|
+
if (providedUser !== this.username) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`Invalid space ID: ${spaceName}. You can only create spaces in your own namespace. Try "${this.username || 'your-username'}/${spaceNamePart || 'space-name'}"`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return spaceName;
|
|
104
|
+
}
|
|
105
|
+
// Otherwise, prepend with username
|
|
106
|
+
return `${this.username || 'unknown'}/${spaceName}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async getSpaceInfo(spaceId: string): Promise<SpaceInfo> {
|
|
110
|
+
const url = `${this.apiUrl}/spaces/${spaceId}`;
|
|
111
|
+
return this.fetchFromApi(url);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async getSpaceVariables(spaceId: string): Promise<Record<string, { value: string; description?: string }>> {
|
|
115
|
+
const url = `${this.apiUrl}/spaces/${spaceId}/variables`;
|
|
116
|
+
try {
|
|
117
|
+
return await this.fetchFromApi(url);
|
|
118
|
+
} catch {
|
|
119
|
+
// If we can't access variables (private space or no permissions), return empty
|
|
120
|
+
return {};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async duplicate(params: DuplicateSpaceParams): Promise<DuplicateSpaceResult> {
|
|
125
|
+
const { sourceSpaceId, newSpaceName, hardware, private: isPrivate = true } = params;
|
|
126
|
+
|
|
127
|
+
if (!this.username) throw new Error(NO_TOKEN_INSTRUCTIONS);
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
// Step 1: Get source space info
|
|
131
|
+
let sourceInfo: SpaceInfo;
|
|
132
|
+
try {
|
|
133
|
+
sourceInfo = await this.getSpaceInfo(sourceSpaceId);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
// Explain the error and rethrow
|
|
136
|
+
throw explain(error, `Could not access source space "${sourceSpaceId}"`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Step 2: Get variables from source space
|
|
140
|
+
const sourceVars = await this.getSpaceVariables(sourceSpaceId);
|
|
141
|
+
const variables: SpaceVariable[] = Object.entries(sourceVars).map(([key, varInfo]) => ({
|
|
142
|
+
key,
|
|
143
|
+
value: varInfo.value,
|
|
144
|
+
description: varInfo.description,
|
|
145
|
+
}));
|
|
146
|
+
|
|
147
|
+
// Step 3: Determine hardware
|
|
148
|
+
// Extract hardware string from either object or string format
|
|
149
|
+
let sourceHardwareStr: string | undefined;
|
|
150
|
+
if (typeof sourceInfo.runtime?.hardware === 'object') {
|
|
151
|
+
sourceHardwareStr = sourceInfo.runtime.hardware.current || sourceInfo.runtime.hardware.requested;
|
|
152
|
+
} else {
|
|
153
|
+
sourceHardwareStr = sourceInfo.runtime?.hardware;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let selectedHardware: string;
|
|
157
|
+
let hardwareKey: 'freecpu' | 'zerogpu';
|
|
158
|
+
|
|
159
|
+
if (hardware) {
|
|
160
|
+
selectedHardware = HARDWARE_MAP[hardware];
|
|
161
|
+
hardwareKey = hardware;
|
|
162
|
+
} else {
|
|
163
|
+
// Auto-detect based on source
|
|
164
|
+
if (sourceHardwareStr === 'zero-a10g') {
|
|
165
|
+
selectedHardware = 'zero-a10g';
|
|
166
|
+
hardwareKey = 'zerogpu';
|
|
167
|
+
} else if (sourceHardwareStr === 'cpu-basic' || !sourceHardwareStr) {
|
|
168
|
+
selectedHardware = 'cpu-basic';
|
|
169
|
+
hardwareKey = 'freecpu';
|
|
170
|
+
} else {
|
|
171
|
+
// For any paid hardware, default to ZeroGPU (free tier)
|
|
172
|
+
selectedHardware = 'zero-a10g';
|
|
173
|
+
hardwareKey = 'zerogpu';
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Step 4: Determine target space ID
|
|
178
|
+
let targetId: string;
|
|
179
|
+
if (newSpaceName) {
|
|
180
|
+
targetId = this.normalizeSpaceName(newSpaceName);
|
|
181
|
+
} else {
|
|
182
|
+
// Use same name as source
|
|
183
|
+
const sourceName = sourceSpaceId.split('/')[1];
|
|
184
|
+
targetId = `${this.username}/${sourceName || ''}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Step 5: Check for warnings
|
|
188
|
+
let hardwareWarning: string | undefined;
|
|
189
|
+
if (sourceHardwareStr && !FREE_HARDWARE.includes(sourceHardwareStr)) {
|
|
190
|
+
hardwareWarning = `Note: The source space uses '${sourceHardwareStr}' which is paid hardware. Your duplicated space is set to '${hardwareKey}'. "+
|
|
191
|
+
"You may need to upgrade in Settings to run this space or achieve the same performance.`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let gatedWarning: string | undefined;
|
|
195
|
+
if (sourceInfo.gated) {
|
|
196
|
+
gatedWarning = `🔐 The model in this space is 'gated' - you may need to accept the licensing conditions before use.`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Step 6: Make the duplication request
|
|
200
|
+
const url = `${this.apiUrl}/spaces/${sourceSpaceId}/duplicate`;
|
|
201
|
+
const payload = {
|
|
202
|
+
repository: targetId,
|
|
203
|
+
private: isPrivate,
|
|
204
|
+
hardware: selectedHardware,
|
|
205
|
+
variables: variables.length > 0 ? variables : undefined,
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const response = await this.fetchFromApi<{ url: string }>(url, {
|
|
209
|
+
method: 'POST',
|
|
210
|
+
body: JSON.stringify(payload),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Step 7: Construct response
|
|
214
|
+
const warnings: string[] = [];
|
|
215
|
+
if (hardwareWarning) warnings.push(hardwareWarning);
|
|
216
|
+
if (gatedWarning) warnings.push(gatedWarning);
|
|
217
|
+
|
|
218
|
+
const result: DuplicateSpaceResult = {
|
|
219
|
+
url: response.url,
|
|
220
|
+
spaceId: targetId,
|
|
221
|
+
hardware: hardwareKey,
|
|
222
|
+
private: isPrivate,
|
|
223
|
+
variablesCopied: variables.length,
|
|
224
|
+
instructions: this.formatInstructions(response.url, hardwareKey, isPrivate, warnings),
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
if (hardwareWarning) {
|
|
228
|
+
result.hardwareWarning = hardwareWarning;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return result;
|
|
232
|
+
} catch (error) {
|
|
233
|
+
// Explain the error and rethrow
|
|
234
|
+
throw explain(error, 'Failed to duplicate space');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private formatInstructions(url: string, hardware: string, isPrivate: boolean, warnings: string[]): string {
|
|
239
|
+
let instructions = `✅ 🤗 Space successfully duplicated!
|
|
240
|
+
|
|
241
|
+
🔗 Your new space: ${url}
|
|
242
|
+
|
|
243
|
+
⚙️ To configure your space:
|
|
244
|
+
1. Go to ${url}
|
|
245
|
+
2. Click on 'Settings' in the top right
|
|
246
|
+
3. Configure any additional settings as needed
|
|
247
|
+
|
|
248
|
+
Hardware: ${hardware} | Visibility: ${isPrivate ? 'Private' : 'Public'}`;
|
|
249
|
+
|
|
250
|
+
if (warnings.length > 0) {
|
|
251
|
+
instructions += '\n\n⚠️ Warnings:';
|
|
252
|
+
warnings.forEach((warning) => {
|
|
253
|
+
instructions += `\n- ${warning}`;
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return instructions;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export const formatDuplicateResult = (result: DuplicateSpaceResult): string => {
|
|
262
|
+
return result.instructions;
|
|
263
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common HTTP error messages for Hugging Face API operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { HfApiError } from './hf-api-call.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Friendly explanations for common HTTP error codes
|
|
9
|
+
*/
|
|
10
|
+
export const FRIENDLY_ERROR_EXPLANATIONS: Record<number, string> = {
|
|
11
|
+
400: 'The request was invalid. Check your HF_TOKEN permissions, or that you have not exceeded the maximum number of spaces allowed..',
|
|
12
|
+
401: 'Authentication failed. Please check that your Hugging Face token is valid. Check your Hugging Face token permissions.',
|
|
13
|
+
403: 'You do not have permission to access this resource. Check your Hugging Face token permissions.',
|
|
14
|
+
404: 'The requested resource does not exist.',
|
|
15
|
+
409: 'A resource with this name already exists.',
|
|
16
|
+
422: 'The provided configuration is invalid.',
|
|
17
|
+
429: 'Duplication failed due to rate limits.',
|
|
18
|
+
500: 'Hugging Face is experiencing issues. Please try again later.',
|
|
19
|
+
502: 'The server gateway is temporarily unavailable.',
|
|
20
|
+
503: 'The service is temporarily down for maintenance.',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get friendly explanation for a given HTTP status code
|
|
25
|
+
*/
|
|
26
|
+
export function getFriendlyExplanation(status: number): string {
|
|
27
|
+
// Check for exact match first
|
|
28
|
+
if (FRIENDLY_ERROR_EXPLANATIONS[status]) {
|
|
29
|
+
return FRIENDLY_ERROR_EXPLANATIONS[status];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Handle 5xx errors generically
|
|
33
|
+
if (status >= 500) {
|
|
34
|
+
return 'Hugging Face is experiencing issues. Please try again later.';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Default for unknown errors
|
|
38
|
+
return 'An unexpected error occurred. Please try again.';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Explain an error by enhancing it with friendly explanation if it's an HfApiError
|
|
43
|
+
* Otherwise returns the original error unchanged
|
|
44
|
+
* @param error - The error to explain
|
|
45
|
+
* @param context - Optional context about what was being attempted
|
|
46
|
+
* @returns The enhanced HfApiError or original error
|
|
47
|
+
*/
|
|
48
|
+
export function explain(error: unknown, context?: string): unknown {
|
|
49
|
+
// Only enhance HfApiError instances
|
|
50
|
+
if (error instanceof HfApiError) {
|
|
51
|
+
const friendlyExplanation = getFriendlyExplanation(error.status);
|
|
52
|
+
return error.withImprovedMessage(friendlyExplanation, context);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Return any other error unchanged
|
|
56
|
+
return error;
|
|
57
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom error class that includes HTTP status information
|
|
3
|
+
*/
|
|
4
|
+
export class HfApiError extends Error {
|
|
5
|
+
constructor(
|
|
6
|
+
message: string,
|
|
7
|
+
public readonly status: number,
|
|
8
|
+
public readonly statusText: string,
|
|
9
|
+
public readonly responseBody?: string
|
|
10
|
+
) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'HfApiError';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Format the error with a friendly explanation followed by the original error
|
|
17
|
+
* @param friendlyExplanation - User-friendly explanation
|
|
18
|
+
* @param context - Optional context about what was being attempted
|
|
19
|
+
* @returns Formatted error message
|
|
20
|
+
*/
|
|
21
|
+
formatWithExplanation(friendlyExplanation: string, context?: string): string {
|
|
22
|
+
let formatted = '';
|
|
23
|
+
|
|
24
|
+
// Add context if provided
|
|
25
|
+
if (context) {
|
|
26
|
+
formatted = `${context}. `;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Add friendly explanation
|
|
30
|
+
formatted += friendlyExplanation;
|
|
31
|
+
|
|
32
|
+
// Add original error message on new line
|
|
33
|
+
formatted += `\n\n${this.message}`;
|
|
34
|
+
|
|
35
|
+
// Add response body details if available and different from message
|
|
36
|
+
if (this.responseBody) {
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(this.responseBody) as { error?: string; message?: string; detail?: string };
|
|
39
|
+
const errorDetail = parsed.error || parsed.message || parsed.detail;
|
|
40
|
+
if (errorDetail && !this.message.includes(errorDetail)) {
|
|
41
|
+
formatted += `\n${errorDetail}`;
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// If not JSON, add raw response if it's not too long and not already in message
|
|
45
|
+
if (this.responseBody.length < 200 && !this.message.includes(this.responseBody)) {
|
|
46
|
+
formatted += `\n${this.responseBody}`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return formatted;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create a new HfApiError with an improved message while preserving all other properties
|
|
56
|
+
* @param friendlyExplanation - User-friendly explanation
|
|
57
|
+
* @param context - Optional context about what was being attempted
|
|
58
|
+
* @returns New HfApiError with improved message
|
|
59
|
+
*/
|
|
60
|
+
withImprovedMessage(friendlyExplanation: string, context?: string): HfApiError {
|
|
61
|
+
const improvedMessage = this.formatWithExplanation(friendlyExplanation, context);
|
|
62
|
+
return new HfApiError(improvedMessage, this.status, this.statusText, this.responseBody);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Base API client for Hugging Face HTTP APIs
|
|
68
|
+
*
|
|
69
|
+
* @template TParams - Type for API parameters
|
|
70
|
+
* @template TResponse - Type for API response
|
|
71
|
+
*/
|
|
72
|
+
export class HfApiCall<TParams = Record<string, string | undefined>, TResponse = unknown> {
|
|
73
|
+
protected readonly apiUrl: string;
|
|
74
|
+
protected readonly hfToken: string | undefined;
|
|
75
|
+
protected readonly apiTimeout: number;
|
|
76
|
+
|
|
77
|
+
/** nb reversed order from superclasses on basis that hfToken is more likely to be configured */
|
|
78
|
+
constructor(apiUrl: string, hfToken?: string) {
|
|
79
|
+
this.apiUrl = apiUrl;
|
|
80
|
+
this.hfToken = hfToken;
|
|
81
|
+
// Default to 12.5 seconds if HF_API_TIMEOUT is not set
|
|
82
|
+
this.apiTimeout = process.env.HF_API_TIMEOUT ? parseInt(process.env.HF_API_TIMEOUT, 10) : 12500;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Fetches data from the API with proper error handling and authentication
|
|
87
|
+
*
|
|
88
|
+
* @template T - Response type (defaults to TResponse)
|
|
89
|
+
* @param url - The URL to fetch from
|
|
90
|
+
* @param options - Fetch options
|
|
91
|
+
* @returns The parsed JSON response
|
|
92
|
+
*/
|
|
93
|
+
protected async fetchFromApi<T = TResponse>(url: URL | string, options?: globalThis.RequestInit): Promise<T> {
|
|
94
|
+
try {
|
|
95
|
+
const headers: Record<string, string> = {
|
|
96
|
+
Accept: 'application/json',
|
|
97
|
+
...((options?.headers as Record<string, string>) || {}),
|
|
98
|
+
};
|
|
99
|
+
if (this.hfToken) {
|
|
100
|
+
headers['Authorization'] = `Bearer ${this.hfToken}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Add timeout using AbortController
|
|
104
|
+
const controller = new AbortController();
|
|
105
|
+
const timeoutId = setTimeout(() => controller.abort(), this.apiTimeout);
|
|
106
|
+
|
|
107
|
+
const response = await fetch(url.toString(), {
|
|
108
|
+
...options,
|
|
109
|
+
headers,
|
|
110
|
+
signal: controller.signal,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
clearTimeout(timeoutId);
|
|
114
|
+
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
// Try to get error details from response body
|
|
117
|
+
let responseBody: string | undefined;
|
|
118
|
+
try {
|
|
119
|
+
responseBody = await response.text();
|
|
120
|
+
} catch {
|
|
121
|
+
// Ignore if we can't read the body
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
throw new HfApiError(
|
|
125
|
+
`API request failed: ${response.status.toString()} ${response.statusText}`,
|
|
126
|
+
response.status,
|
|
127
|
+
response.statusText,
|
|
128
|
+
responseBody
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return (await response.json()) as T;
|
|
133
|
+
} catch (error) {
|
|
134
|
+
// Re-throw HfApiError as-is to preserve status information
|
|
135
|
+
if (error instanceof HfApiError) {
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
// Handle timeout errors
|
|
139
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
140
|
+
throw new Error(`API request timed out after ${this.apiTimeout}ms`);
|
|
141
|
+
}
|
|
142
|
+
// Wrap other errors
|
|
143
|
+
if (error instanceof Error) {
|
|
144
|
+
throw new Error(`API request failed: ${error.message}`);
|
|
145
|
+
}
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Builds a URL with query parameters
|
|
152
|
+
*
|
|
153
|
+
* @param params - Key-value pairs of query parameters
|
|
154
|
+
* @returns A URL object with the query parameters appended
|
|
155
|
+
*/
|
|
156
|
+
protected buildUrl(params: TParams): URL {
|
|
157
|
+
const url = new URL(this.apiUrl);
|
|
158
|
+
|
|
159
|
+
// Iterate over params in a type-safe way
|
|
160
|
+
for (const key in params) {
|
|
161
|
+
const value = params[key as keyof TParams];
|
|
162
|
+
if (value !== undefined) {
|
|
163
|
+
url.searchParams.append(key, String(value));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return url;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Builds a URL with the given parameters and makes an API request
|
|
172
|
+
*
|
|
173
|
+
* @template T - Response type (defaults to TResponse)
|
|
174
|
+
* @param params - The parameters to include in the URL
|
|
175
|
+
* @param options - Additional fetch options
|
|
176
|
+
* @returns The parsed JSON response
|
|
177
|
+
*/
|
|
178
|
+
protected async callApi<T = TResponse>(params: TParams, options?: globalThis.RequestInit): Promise<T> {
|
|
179
|
+
const url = this.buildUrl(params);
|
|
180
|
+
return this.fetchFromApi<T>(url, options);
|
|
181
|
+
}
|
|
182
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Export all modules - types are included in each module
|
|
2
|
+
export * from './hf-api-call.js';
|
|
3
|
+
export * from './error-messages.js';
|
|
4
|
+
export * from './space-search.js';
|
|
5
|
+
export * from './model-search.js';
|
|
6
|
+
export * from './model-detail.js';
|
|
7
|
+
export * from './utilities.js';
|
|
8
|
+
export * from './paper-search.js';
|
|
9
|
+
export * from './dataset-search.js';
|
|
10
|
+
export * from './dataset-detail.js';
|
|
11
|
+
export * from './duplicate-space.js';
|
|
12
|
+
export * from './space-info.js';
|
|
13
|
+
export * from './space-files.js';
|
|
14
|
+
export * from './user-summary.js';
|
|
15
|
+
export * from './paper-summary.js';
|
|
16
|
+
|
|
17
|
+
// Export tool IDs for external use - these are the canonical tool identifiers
|
|
18
|
+
export * from './tool-ids.js';
|