From 38a6d2492286f6781f3d187b05eb9f58f9be74f9 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Fri, 5 Sep 2025 02:52:20 +0000 Subject: [PATCH] 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 --- packages/types/src/provider-settings.ts | 13 ++ packages/types/src/providers/cometapi.ts | 200 ++++++++++++++++++ packages/types/src/providers/index.ts | 1 + src/api/index.ts | 3 + src/api/providers/cometapi.ts | 153 ++++++++++++++ src/api/providers/index.ts | 1 + src/shared/api.ts | 2 + .../components/ui/hooks/useSelectedModel.ts | 9 +- .../src/utils/__tests__/validate.test.ts | 1 + 9 files changed, 382 insertions(+), 1 deletion(-) create mode 100644 packages/types/src/providers/cometapi.ts create mode 100644 src/api/providers/cometapi.ts diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index d1831163f8..3044c15eae 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -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[] = [ "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] diff --git a/packages/types/src/providers/cometapi.ts b/packages/types/src/providers/cometapi.ts new file mode 100644 index 0000000000..1e5e0418f1 --- /dev/null +++ b/packages/types/src/providers/cometapi.ts @@ -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 = { + // 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", + }, +} diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index 21e43aaa99..6cdc4f4d1f 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -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" diff --git a/src/api/index.ts b/src/api/index.ts index ac00967676..b8451bce65 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -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": diff --git a/src/api/providers/cometapi.ts b/src/api/providers/cometapi.ts new file mode 100644 index 0000000000..3bd43f7808 --- /dev/null +++ b/src/api/providers/cometapi.ts @@ -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 { + 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, + } + } +} diff --git a/src/api/providers/index.ts b/src/api/providers/index.ts index 85d877b6bc..5984d74361 100644 --- a/src/api/providers/index.ts +++ b/src/api/providers/index.ts @@ -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" diff --git a/src/shared/api.ts b/src/shared/api.ts index eb3ae124a8..639e68d36b 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -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 } diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index b7fe4ff03d..6b29cf1337 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -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] diff --git a/webview-ui/src/utils/__tests__/validate.test.ts b/webview-ui/src/utils/__tests__/validate.test.ts index c9fb7bfd42..1699242ed5 100644 --- a/webview-ui/src/utils/__tests__/validate.test.ts +++ b/webview-ui/src/utils/__tests__/validate.test.ts @@ -42,6 +42,7 @@ describe("Model Validation Functions", () => { deepinfra: {}, "io-intelligence": {}, "vercel-ai-gateway": {}, + cometapi: {}, } const allowAllOrganization: OrganizationAllowList = {