feat: add CometAPI as a model provider

- Add CometAPI type definitions with support for GPT-5, Claude-4, Gemini-2.5, and other models
- Implement CometAPIHandler extending RouterProvider for OpenAI-compatible API
- Add CometAPI to provider settings and configuration
- Update webview components to support CometAPI provider
- Add CometAPI to dynamic providers list for model fetching

Implements #7688
This commit is contained in:
Roo Code 2025-09-05 02:52:20 +00:00
parent 7935c94827
commit 38a6d24922
9 changed files with 382 additions and 1 deletions

View file

@ -34,6 +34,7 @@ import {
export const providerNames = [
"anthropic",
"claude-code",
"cometapi",
"glama",
"openrouter",
"bedrock",
@ -336,6 +337,12 @@ const vercelAiGatewaySchema = baseProviderSettingsSchema.extend({
vercelAiGatewayModelId: z.string().optional(),
})
const cometApiSchema = baseProviderSettingsSchema.extend({
cometApiBaseUrl: z.string().optional(),
cometApiApiKey: z.string().optional(),
cometApiModelId: z.string().optional(),
})
const defaultSchema = z.object({
apiProvider: z.undefined(),
})
@ -343,6 +350,7 @@ const defaultSchema = z.object({
export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProvider", [
anthropicSchema.merge(z.object({ apiProvider: z.literal("anthropic") })),
claudeCodeSchema.merge(z.object({ apiProvider: z.literal("claude-code") })),
cometApiSchema.merge(z.object({ apiProvider: z.literal("cometapi") })),
glamaSchema.merge(z.object({ apiProvider: z.literal("glama") })),
openRouterSchema.merge(z.object({ apiProvider: z.literal("openrouter") })),
bedrockSchema.merge(z.object({ apiProvider: z.literal("bedrock") })),
@ -384,6 +392,7 @@ export const providerSettingsSchema = z.object({
apiProvider: providerNamesSchema.optional(),
...anthropicSchema.shape,
...claudeCodeSchema.shape,
...cometApiSchema.shape,
...glamaSchema.shape,
...openRouterSchema.shape,
...bedrockSchema.shape,
@ -418,6 +427,7 @@ export const providerSettingsSchema = z.object({
...qwenCodeSchema.shape,
...rooSchema.shape,
...vercelAiGatewaySchema.shape,
...cometApiSchema.shape,
...codebaseIndexProviderSchema.shape,
})
@ -448,6 +458,7 @@ export const MODEL_ID_KEYS: Partial<keyof ProviderSettings>[] = [
"ioIntelligenceModelId",
"vercelAiGatewayModelId",
"deepInfraModelId",
"cometApiModelId",
]
export const getModelId = (settings: ProviderSettings): string | undefined => {
@ -571,6 +582,7 @@ export const MODELS_BY_PROVIDER: Record<
unbound: { id: "unbound", label: "Unbound", models: [] },
deepinfra: { id: "deepinfra", label: "DeepInfra", models: [] },
"vercel-ai-gateway": { id: "vercel-ai-gateway", label: "Vercel AI Gateway", models: [] },
cometapi: { id: "cometapi", label: "CometAPI", models: [] },
}
export const dynamicProviders = [
@ -582,6 +594,7 @@ export const dynamicProviders = [
"unbound",
"deepinfra",
"vercel-ai-gateway",
"cometapi",
] as const satisfies readonly ProviderName[]
export type DynamicProvider = (typeof dynamicProviders)[number]

View file

@ -0,0 +1,200 @@
import type { ModelInfo } from "../model.js"
// Default fallback values for CometAPI when model metadata is not yet loaded.
export const cometApiDefaultModelId = "gpt-5-chat-latest"
export const cometApiDefaultModelInfo: ModelInfo = {
maxTokens: 16384,
contextWindow: 128000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 2.5,
outputPrice: 10,
description: "GPT-5 Chat Latest model with 128K context window.",
}
// Fallback models for CometAPI when the API is unavailable
export const COMETAPI_MODELS: Record<string, ModelInfo> = {
// GPT series
"gpt-5-chat-latest": {
maxTokens: 16384,
contextWindow: 128000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 2.5,
outputPrice: 10,
description: "GPT-5 Chat Latest - Most advanced GPT model",
},
"chatgpt-4o-latest": {
maxTokens: 16384,
contextWindow: 128000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 2.5,
outputPrice: 10,
description: "ChatGPT-4o Latest - Advanced multimodal model",
},
"gpt-5-mini": {
maxTokens: 16384,
contextWindow: 128000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0.15,
outputPrice: 0.6,
description: "GPT-5 Mini - Efficient and cost-effective",
},
"gpt-5-nano": {
maxTokens: 8192,
contextWindow: 128000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.075,
outputPrice: 0.3,
description: "GPT-5 Nano - Ultra-efficient for simple tasks",
},
"gpt-4.1-mini": {
maxTokens: 16384,
contextWindow: 128000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0.15,
outputPrice: 0.6,
description: "GPT-4.1 Mini - Balanced performance and cost",
},
"gpt-4o-mini": {
maxTokens: 16384,
contextWindow: 128000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0.15,
outputPrice: 0.6,
description: "GPT-4o Mini - Efficient multimodal model",
},
// Claude series
"claude-opus-4-1-20250805": {
maxTokens: 8192,
contextWindow: 200000,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 3,
outputPrice: 15,
description: "Claude Opus 4.1 - Most capable Claude model",
},
"claude-sonnet-4-20250514": {
maxTokens: 8192,
contextWindow: 200000,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 1.5,
outputPrice: 7.5,
description: "Claude Sonnet 4 - Balanced Claude model",
},
"claude-3-7-sonnet-latest": {
maxTokens: 8192,
contextWindow: 200000,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 3,
outputPrice: 15,
description: "Claude 3.7 Sonnet - Latest Sonnet version",
},
"claude-3-5-haiku-latest": {
maxTokens: 8192,
contextWindow: 200000,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 0.25,
outputPrice: 1.25,
description: "Claude 3.5 Haiku - Fast and efficient",
},
// Gemini series
"gemini-2.5-pro": {
maxTokens: 8192,
contextWindow: 2097152,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 1.25,
outputPrice: 5,
description: "Gemini 2.5 Pro - Google's most capable model",
},
"gemini-2.5-flash": {
maxTokens: 8192,
contextWindow: 1048576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0.075,
outputPrice: 0.3,
description: "Gemini 2.5 Flash - Fast and efficient",
},
"gemini-2.0-flash": {
maxTokens: 8192,
contextWindow: 1048576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0.075,
outputPrice: 0.3,
description: "Gemini 2.0 Flash - Previous generation flash model",
},
// DeepSeek series
"deepseek-v3.1": {
maxTokens: 8192,
contextWindow: 128000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.14,
outputPrice: 0.28,
description: "DeepSeek V3.1 - Advanced reasoning model",
},
"deepseek-r1-0528": {
maxTokens: 8192,
contextWindow: 128000,
supportsImages: false,
supportsPromptCache: false,
supportsReasoningEffort: true,
inputPrice: 0.55,
outputPrice: 2.19,
description: "DeepSeek R1 - Reasoning-focused model",
},
"deepseek-reasoner": {
maxTokens: 8192,
contextWindow: 128000,
supportsImages: false,
supportsPromptCache: false,
supportsReasoningEffort: true,
inputPrice: 0.55,
outputPrice: 2.19,
description: "DeepSeek Reasoner - Advanced reasoning capabilities",
},
// Other popular models
"grok-4-0709": {
maxTokens: 8192,
contextWindow: 128000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 5,
outputPrice: 15,
description: "Grok 4 - xAI's advanced model",
},
"qwen3-30b-a3b": {
maxTokens: 8192,
contextWindow: 32768,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.5,
outputPrice: 1.5,
description: "Qwen3 30B - Alibaba's large language model",
},
"qwen3-coder-plus-2025-07-22": {
maxTokens: 8192,
contextWindow: 32768,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.5,
outputPrice: 1.5,
description: "Qwen3 Coder Plus - Specialized for coding tasks",
},
}

View file

@ -3,6 +3,7 @@ export * from "./bedrock.js"
export * from "./cerebras.js"
export * from "./chutes.js"
export * from "./claude-code.js"
export * from "./cometapi.js"
export * from "./deepseek.js"
export * from "./doubao.js"
export * from "./featherless.js"

View file

@ -9,6 +9,7 @@ import {
AnthropicHandler,
AwsBedrockHandler,
CerebrasHandler,
CometAPIHandler,
OpenRouterHandler,
VertexHandler,
AnthropicVertexHandler,
@ -95,6 +96,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
return new AnthropicHandler(options)
case "claude-code":
return new ClaudeCodeHandler(options)
case "cometapi":
return new CometAPIHandler(options)
case "glama":
return new GlamaHandler(options)
case "openrouter":

View file

@ -0,0 +1,153 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { cometApiDefaultModelId, cometApiDefaultModelInfo, COMETAPI_MODELS } from "@roo-code/types"
import type { ApiHandlerOptions } from "../../shared/api"
import { calculateApiCostOpenAI } from "../../shared/cost"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { convertToOpenAiMessages } from "../transform/openai-format"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
import { RouterProvider } from "./router-provider"
import { getModelParams } from "../transform/model-params"
import { getModels } from "./fetchers/modelCache"
export class CometAPIHandler extends RouterProvider implements SingleCompletionHandler {
constructor(options: ApiHandlerOptions) {
super({
options: {
...options,
openAiHeaders: {
"X-CometAPI-Source": "roo-code",
"X-CometAPI-Version": `2025-09-05`,
},
},
name: "cometapi",
baseURL: `${options.cometApiBaseUrl || "https://api.cometapi.com/v1"}`,
apiKey: options.cometApiApiKey || "not-provided",
modelId: options.cometApiModelId,
defaultModelId: cometApiDefaultModelId,
defaultModelInfo: cometApiDefaultModelInfo,
})
}
public override async fetchModel() {
// Try to fetch models from API, fallback to static models if API is unavailable
try {
this.models = await getModels({
provider: this.name,
apiKey: this.client.apiKey,
baseUrl: this.client.baseURL,
})
} catch (error) {
// Fallback to static models if API is unavailable
console.warn("Failed to fetch CometAPI models, using fallback models:", error)
this.models = COMETAPI_MODELS
}
return this.getModel()
}
override getModel() {
const id = this.options.cometApiModelId ?? cometApiDefaultModelId
const info = this.models[id] ?? cometApiDefaultModelInfo
const params = getModelParams({
format: "openai",
modelId: id,
model: info,
settings: this.options,
})
return { id, info, ...params }
}
override async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
_metadata?: ApiHandlerCreateMessageMetadata,
): ApiStream {
// Ensure we have up-to-date model metadata
await this.fetchModel()
const { id: modelId, info, reasoningEffort: reasoning_effort } = await this.fetchModel()
const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
model: modelId,
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
stream: true,
stream_options: { include_usage: true },
reasoning_effort,
} as OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
if (this.supportsTemperature(modelId)) {
requestOptions.temperature = this.options.modelTemperature ?? 0
}
if (this.options.includeMaxTokens === true && info.maxTokens) {
;(requestOptions as any).max_completion_tokens = this.options.modelMaxTokens || info.maxTokens
}
const { data: stream } = await this.client.chat.completions.create(requestOptions).withResponse()
let lastUsage: OpenAI.CompletionUsage | undefined
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
yield { type: "text", text: delta.content }
}
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
yield { type: "reasoning", text: (delta.reasoning_content as string | undefined) || "" }
}
if (chunk.usage) {
lastUsage = chunk.usage
}
}
if (lastUsage) {
yield this.processUsageMetrics(lastUsage, info)
}
}
async completePrompt(prompt: string): Promise<string> {
await this.fetchModel()
const { id: modelId, info } = this.getModel()
const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = {
model: modelId,
messages: [{ role: "user", content: prompt }],
}
if (this.supportsTemperature(modelId)) {
requestOptions.temperature = this.options.modelTemperature ?? 0
}
if (this.options.includeMaxTokens === true && info.maxTokens) {
;(requestOptions as any).max_completion_tokens = this.options.modelMaxTokens || info.maxTokens
}
const resp = await this.client.chat.completions.create(requestOptions)
return resp.choices[0]?.message?.content || ""
}
protected processUsageMetrics(usage: any, modelInfo?: any): ApiStreamUsageChunk {
const inputTokens = usage?.prompt_tokens || 0
const outputTokens = usage?.completion_tokens || 0
const cacheWriteTokens = usage?.prompt_tokens_details?.cache_write_tokens || 0
const cacheReadTokens = usage?.prompt_tokens_details?.cached_tokens || 0
const totalCost = modelInfo
? calculateApiCostOpenAI(modelInfo, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens)
: 0
return {
type: "usage",
inputTokens,
outputTokens,
cacheWriteTokens: cacheWriteTokens || undefined,
cacheReadTokens: cacheReadTokens || undefined,
totalCost,
}
}
}

View file

@ -4,6 +4,7 @@ export { AwsBedrockHandler } from "./bedrock"
export { CerebrasHandler } from "./cerebras"
export { ChutesHandler } from "./chutes"
export { ClaudeCodeHandler } from "./claude-code"
export { CometAPIHandler } from "./cometapi"
export { DeepSeekHandler } from "./deepseek"
export { DoubaoHandler } from "./doubao"
export { MoonshotHandler } from "./moonshot"

View file

@ -29,6 +29,7 @@ const routerNames = [
"io-intelligence",
"deepinfra",
"vercel-ai-gateway",
"cometapi",
] as const
export type RouterName = (typeof routerNames)[number]
@ -155,3 +156,4 @@ export type GetModelsOptions =
| { provider: "deepinfra"; apiKey?: string; baseUrl?: string }
| { provider: "io-intelligence"; apiKey: string }
| { provider: "vercel-ai-gateway" }
| { provider: "cometapi"; apiKey?: string; baseUrl?: string }

View file

@ -8,6 +8,8 @@ import {
bedrockModels,
cerebrasDefaultModelId,
cerebrasModels,
cometApiDefaultModelId,
COMETAPI_MODELS,
deepSeekDefaultModelId,
deepSeekModels,
moonshotDefaultModelId,
@ -341,11 +343,16 @@ function getSelectedModel({
const info = routerModels["vercel-ai-gateway"]?.[id]
return { id, info }
}
case "cometapi": {
const id = apiConfiguration.cometApiModelId ?? cometApiDefaultModelId
const info = routerModels.cometapi?.[id] ?? COMETAPI_MODELS[id as keyof typeof COMETAPI_MODELS]
return { id, info }
}
// case "anthropic":
// case "human-relay":
// case "fake-ai":
default: {
provider satisfies "anthropic" | "gemini-cli" | "qwen-code" | "human-relay" | "fake-ai"
provider satisfies "anthropic" | "gemini-cli" | "human-relay" | "fake-ai"
const id = apiConfiguration.apiModelId ?? anthropicDefaultModelId
const baseInfo = anthropicModels[id as keyof typeof anthropicModels]

View file

@ -42,6 +42,7 @@ describe("Model Validation Functions", () => {
deepinfra: {},
"io-intelligence": {},
"vercel-ai-gateway": {},
cometapi: {},
}
const allowAllOrganization: OrganizationAllowList = {