mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
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:
parent
7935c94827
commit
38a6d24922
9 changed files with 382 additions and 1 deletions
|
|
@ -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]
|
||||
|
|
|
|||
200
packages/types/src/providers/cometapi.ts
Normal file
200
packages/types/src/providers/cometapi.ts
Normal 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",
|
||||
},
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
153
src/api/providers/cometapi.ts
Normal file
153
src/api/providers/cometapi.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ describe("Model Validation Functions", () => {
|
|||
deepinfra: {},
|
||||
"io-intelligence": {},
|
||||
"vercel-ai-gateway": {},
|
||||
cometapi: {},
|
||||
}
|
||||
|
||||
const allowAllOrganization: OrganizationAllowList = {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue