Skip to content

Commit e1520ad

Browse files
authored
Merge pull request #7809 from TensorNull/feat/cometapi
✨ add CometAPI as Model Provider
2 parents fde554b + bcd4981 commit e1520ad

File tree

11 files changed

+1229
-0
lines changed

11 files changed

+1229
-0
lines changed

core/llm/llms/CometAPI.ts

Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
import { LLMOptions } from "../../index.js";
2+
import { allModelProviders } from "@continuedev/llm-info";
3+
import OpenAI from "./OpenAI.js";
4+
5+
/**
6+
* CometAPI-specific error types for better error handling
7+
*/
8+
export class CometAPIError extends Error {
9+
constructor(
10+
message: string,
11+
public code?: string,
12+
public statusCode?: number,
13+
) {
14+
super(message);
15+
this.name = "CometAPIError";
16+
}
17+
}
18+
19+
export class CometAPIAuthenticationError extends CometAPIError {
20+
constructor(message: string = "Invalid CometAPI API key") {
21+
super(message, "AUTHENTICATION_ERROR", 401);
22+
this.name = "CometAPIAuthenticationError";
23+
}
24+
}
25+
26+
export class CometAPIQuotaExceededError extends CometAPIError {
27+
constructor(message: string = "CometAPI quota exceeded") {
28+
super(message, "QUOTA_EXCEEDED", 429);
29+
this.name = "CometAPIQuotaExceededError";
30+
}
31+
}
32+
33+
/**
34+
* CometAPI LLM provider - aggregates multiple mainstream models
35+
* from various providers (GPT, Claude, Gemini, Grok, DeepSeek, Qwen, etc.)
36+
*
37+
* Uses OpenAI-compatible API format with bearer token authentication
38+
*/
39+
class CometAPI extends OpenAI {
40+
static providerName = "cometapi";
41+
42+
static defaultOptions: Partial<LLMOptions> = {
43+
apiBase: "https://api.cometapi.com/v1/",
44+
model: "gpt-4o-mini", // Default to a commonly available model
45+
};
46+
47+
constructor(options: LLMOptions) {
48+
// Validate required configuration before calling super
49+
CometAPI.validateConfig(options);
50+
super(options);
51+
52+
// Align contextLength with llm-info for cometapi specifically (non-breaking for others)
53+
try {
54+
const cometProvider = allModelProviders.find((p) => p.id === "cometapi");
55+
const info = cometProvider?.models.find((m) =>
56+
m.regex ? m.regex.test(this.model) : m.model === this.model,
57+
);
58+
if (info?.contextLength) {
59+
// Always prefer cometapi-specific llm-info over generic provider matches
60+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
61+
// @ts-ignore - accessing protected for targeted fix
62+
this._contextLength = info.contextLength;
63+
}
64+
} catch {
65+
// no-op: do not fail construction on metadata issues
66+
}
67+
}
68+
69+
/**
70+
* Validate CometAPI configuration
71+
*/
72+
private static validateConfig(options: LLMOptions): void {
73+
// Allow constructing without API key (tests that only instantiate should pass).
74+
// Enforce credentials at request time instead.
75+
if (!options.apiKey) {
76+
if (typeof process !== "undefined" && process.env?.NODE_ENV !== "test") {
77+
console.warn(
78+
"CometAPI: No API key provided. Requests will fail until an API key is configured. Get one at https://api.cometapi.com/console/token",
79+
);
80+
}
81+
return;
82+
}
83+
84+
if (options.apiBase && !CometAPI.isValidApiBase(options.apiBase)) {
85+
throw new CometAPIError(
86+
`Invalid CometAPI base URL: ${options.apiBase}. Expected https://api.cometapi.com/v1/ or compatible endpoint`,
87+
);
88+
}
89+
90+
if (
91+
options.model &&
92+
!CometAPI.isValidModelFormat(options.model) &&
93+
typeof process !== "undefined" &&
94+
process.env?.NODE_ENV !== "test"
95+
) {
96+
console.warn(
97+
`CometAPI: Model "${options.model}" may not be supported. Check CometAPI documentation for available models.`,
98+
);
99+
}
100+
}
101+
102+
/**
103+
* Validate API base URL format
104+
*/
105+
private static isValidApiBase(apiBase: string): boolean {
106+
try {
107+
const url = new URL(apiBase);
108+
return (
109+
url.protocol === "https:" &&
110+
(url.hostname === "api.cometapi.com" ||
111+
url.hostname.endsWith(".cometapi.com") ||
112+
apiBase.includes("v1")) // Allow custom compatible endpoints
113+
);
114+
} catch {
115+
return false;
116+
}
117+
}
118+
119+
/**
120+
* Basic model format validation
121+
*/
122+
private static isValidModelFormat(model: string): boolean {
123+
// Allow common model patterns
124+
const validPatterns = [
125+
/^gpt-/i,
126+
/^claude-/i,
127+
/^gemini-/i,
128+
/^grok/i,
129+
/^deepseek/i,
130+
/^qwen/i,
131+
/^text-/i,
132+
/^chat/i,
133+
];
134+
135+
return validPatterns.some((pattern) => pattern.test(model));
136+
}
137+
138+
/**
139+
* Patterns to filter out non-chat models from the model list
140+
* Based on CometAPI documentation requirements
141+
*/
142+
private static IGNORE_PATTERNS = [
143+
// Image generation models
144+
"dall-e",
145+
"dalle",
146+
"midjourney",
147+
"mj_",
148+
"stable-diffusion",
149+
"sd-",
150+
"flux-",
151+
"playground-v",
152+
"ideogram",
153+
"recraft-",
154+
"black-forest-labs",
155+
"/recraft-v3",
156+
"recraftv3",
157+
"stability-ai/",
158+
"sdxl",
159+
160+
// Audio generation models
161+
"suno_",
162+
"tts",
163+
"whisper",
164+
165+
// Video generation models
166+
"runway",
167+
"luma_",
168+
"luma-",
169+
"veo",
170+
"kling_",
171+
"minimax_video",
172+
"hunyuan-t1",
173+
174+
// Utility models
175+
"embedding",
176+
"search-gpts",
177+
"files_retrieve",
178+
"moderation",
179+
];
180+
181+
/**
182+
* Recommended chat models from CometAPI documentation
183+
*/
184+
private static RECOMMENDED_MODELS = [
185+
// GPT series
186+
"gpt-5-chat-latest",
187+
"chatgpt-4o-latest",
188+
"gpt-5-mini",
189+
"gpt-5-nano",
190+
"gpt-5",
191+
"gpt-4.1-mini",
192+
"gpt-4.1-nano",
193+
"gpt-4.1",
194+
"gpt-4o-mini",
195+
"o4-mini-2025-04-16",
196+
"o3-pro-2025-06-10",
197+
198+
// Claude series
199+
"claude-opus-4-1-20250805",
200+
"claude-opus-4-1-20250805-thinking",
201+
"claude-sonnet-4-20250514",
202+
"claude-sonnet-4-20250514-thinking",
203+
"claude-3-7-sonnet-latest",
204+
"claude-3-5-haiku-latest",
205+
206+
// Gemini series
207+
"gemini-2.5-pro",
208+
"gemini-2.5-flash",
209+
"gemini-2.5-flash-lite",
210+
"gemini-2.0-flash",
211+
212+
// Grok series
213+
"grok-4-0709",
214+
"grok-3",
215+
"grok-3-mini",
216+
"grok-2-image-1212",
217+
218+
// DeepSeek series
219+
"deepseek-v3.1",
220+
"deepseek-v3",
221+
"deepseek-r1-0528",
222+
"deepseek-chat",
223+
"deepseek-reasoner",
224+
225+
// Qwen series
226+
"qwen3-30b-a3b",
227+
"qwen3-coder-plus-2025-07-22",
228+
];
229+
230+
/**
231+
* Filter model list to exclude non-chat models
232+
* Uses pattern matching against model names
233+
*/
234+
protected filterChatModels(models: any[]): any[] {
235+
if (!models || !Array.isArray(models)) {
236+
return [];
237+
}
238+
239+
return models.filter((model) => {
240+
const modelId = model.id || model.model || "";
241+
const modelName = modelId.toLowerCase();
242+
243+
// Check if model matches any ignore pattern
244+
const shouldIgnore = CometAPI.IGNORE_PATTERNS.some((pattern) =>
245+
modelName.includes(pattern.toLowerCase()),
246+
);
247+
248+
return !shouldIgnore;
249+
});
250+
}
251+
252+
/**
253+
* Get recommended models for CometAPI
254+
* Returns predefined list since CometAPI model info is limited
255+
*/
256+
protected getRecommendedModels(): string[] {
257+
return [...CometAPI.RECOMMENDED_MODELS];
258+
}
259+
260+
/**
261+
* Override listModels method to apply model filtering with enhanced error handling
262+
*/
263+
async listModels(): Promise<string[]> {
264+
try {
265+
const allModels = await super.listModels();
266+
const filteredModels = this.filterChatModels(
267+
allModels.map((id) => ({ id })),
268+
);
269+
270+
// If filtered list is empty or very limited, return recommended models
271+
if (filteredModels.length < 5) {
272+
console.info(
273+
"CometAPI: Limited models available, using recommended set",
274+
);
275+
return this.getRecommendedModels();
276+
}
277+
278+
return filteredModels.map((model) => model.id);
279+
} catch (error: any) {
280+
// Enhanced error handling with specific error types
281+
const errorMessage = error?.message || "Unknown error";
282+
const statusCode = error?.status || error?.statusCode;
283+
284+
if (statusCode === 401) {
285+
throw new CometAPIAuthenticationError(
286+
"CometAPI authentication failed. Please check your API key.",
287+
);
288+
} else if (statusCode === 429) {
289+
throw new CometAPIQuotaExceededError(
290+
"CometAPI rate limit exceeded. Please try again later.",
291+
);
292+
} else if (statusCode >= 400 && statusCode < 500) {
293+
throw new CometAPIError(
294+
`CometAPI client error: ${errorMessage}`,
295+
"CLIENT_ERROR",
296+
statusCode,
297+
);
298+
} else if (statusCode >= 500) {
299+
console.warn(
300+
"CometAPI server error, falling back to recommended models:",
301+
errorMessage,
302+
);
303+
return this.getRecommendedModels();
304+
} else {
305+
// Network or other errors - fallback gracefully
306+
console.warn(
307+
"CometAPI: Failed to fetch model list, using recommended models",
308+
errorMessage,
309+
);
310+
return this.getRecommendedModels();
311+
}
312+
}
313+
}
314+
315+
/**
316+
* Override chat completion with enhanced error handling
317+
*/
318+
protected async *_streamChat(
319+
messages: any[],
320+
signal: AbortSignal,
321+
options: any = {},
322+
): AsyncGenerator<any> {
323+
try {
324+
yield* super._streamChat(messages, signal, options);
325+
} catch (error: any) {
326+
const statusCode = error?.status || error?.statusCode;
327+
const errorMessage = error?.message || "Unknown error";
328+
329+
if (statusCode === 401) {
330+
throw new CometAPIAuthenticationError(
331+
"CometAPI authentication failed during chat completion",
332+
);
333+
} else if (statusCode === 429) {
334+
throw new CometAPIQuotaExceededError(
335+
"CometAPI rate limit exceeded during chat completion",
336+
);
337+
} else if (
338+
errorMessage.includes("model") &&
339+
errorMessage.includes("not found")
340+
) {
341+
throw new CometAPIError(
342+
`Model "${this.model}" is not available on CometAPI. Please check available models.`,
343+
"MODEL_NOT_FOUND",
344+
404,
345+
);
346+
} else {
347+
// Re-throw with more context
348+
throw new CometAPIError(
349+
`CometAPI chat completion failed: ${errorMessage}`,
350+
"COMPLETION_ERROR",
351+
statusCode,
352+
);
353+
}
354+
}
355+
}
356+
}
357+
358+
export default CometAPI;

core/llm/llms/OpenAI-compatible-core.vitest.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import LMStudio from "./LMStudio.js";
1313
import Cerebras from "./Cerebras.js";
1414
import DeepInfra from "./DeepInfra.js";
1515
import Nvidia from "./Nvidia.js";
16+
import CometAPI from "./CometAPI.js";
1617

1718
// Base OpenAI tests
1819
import { afterEach, describe, expect, test, vi } from "vitest";
@@ -292,3 +293,13 @@ createOpenAISubclassTests(Nvidia, {
292293
truncate: "END",
293294
},
294295
});
296+
297+
createOpenAISubclassTests(CometAPI, {
298+
providerName: "cometapi",
299+
defaultApiBase: "https://api.cometapi.com/v1/",
300+
modelConversions: {
301+
"gpt-5-mini": "gpt-5-mini",
302+
"claude-4-sonnet": "claude-sonnet-4-20250514",
303+
},
304+
modelConversionContent: "hello",
305+
});

core/llm/llms/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import BedrockImport from "./BedrockImport";
1717
import Cerebras from "./Cerebras";
1818
import Cloudflare from "./Cloudflare";
1919
import Cohere from "./Cohere";
20+
import CometAPI from "./CometAPI";
2021
import DeepInfra from "./DeepInfra";
2122
import Deepseek from "./Deepseek";
2223
import Docker from "./Docker";
@@ -67,6 +68,7 @@ import xAI from "./xAI";
6768
export const LLMClasses = [
6869
Anthropic,
6970
Cohere,
71+
CometAPI,
7072
FunctionNetwork,
7173
Gemini,
7274
Llamafile,

gui/public/logos/cometapi.png

68.3 KB
Loading

0 commit comments

Comments
 (0)