mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
feat: add Keywords AI as a new LLM provider
- Add Keywords AI provider with gateway API and observability features - Implement KeywordsAiHandler using OpenAI-compatible API - Add Enable Logging toggle (sends disable_log: true when disabled) - Support dynamic model fetching from Keywords AI API - Add UI settings component with API key, base URL, and logging options - Add i18n translations for Keywords AI settings Closes #10962
This commit is contained in:
parent
953c7773c0
commit
72ab03ecd6
19 changed files with 506 additions and 0 deletions
|
|
@ -46,6 +46,7 @@ export const dynamicProviders = [
|
|||
"litellm",
|
||||
"deepinfra",
|
||||
"io-intelligence",
|
||||
"keywords-ai",
|
||||
"requesty",
|
||||
"unbound",
|
||||
"roo",
|
||||
|
|
@ -399,6 +400,13 @@ const ioIntelligenceSchema = apiModelIdProviderModelSchema.extend({
|
|||
ioIntelligenceApiKey: z.string().optional(),
|
||||
})
|
||||
|
||||
const keywordsAiSchema = baseProviderSettingsSchema.extend({
|
||||
keywordsAiApiKey: z.string().optional(),
|
||||
keywordsAiBaseUrl: z.string().optional(),
|
||||
keywordsAiModelId: z.string().optional(),
|
||||
keywordsAiEnableLogging: z.boolean().optional(),
|
||||
})
|
||||
|
||||
const qwenCodeSchema = apiModelIdProviderModelSchema.extend({
|
||||
qwenCodeOauthPath: z.string().optional(),
|
||||
})
|
||||
|
|
@ -455,6 +463,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
|
|||
fireworksSchema.merge(z.object({ apiProvider: z.literal("fireworks") })),
|
||||
featherlessSchema.merge(z.object({ apiProvider: z.literal("featherless") })),
|
||||
ioIntelligenceSchema.merge(z.object({ apiProvider: z.literal("io-intelligence") })),
|
||||
keywordsAiSchema.merge(z.object({ apiProvider: z.literal("keywords-ai") })),
|
||||
qwenCodeSchema.merge(z.object({ apiProvider: z.literal("qwen-code") })),
|
||||
rooSchema.merge(z.object({ apiProvider: z.literal("roo") })),
|
||||
vercelAiGatewaySchema.merge(z.object({ apiProvider: z.literal("vercel-ai-gateway") })),
|
||||
|
|
@ -496,6 +505,7 @@ export const providerSettingsSchema = z.object({
|
|||
...fireworksSchema.shape,
|
||||
...featherlessSchema.shape,
|
||||
...ioIntelligenceSchema.shape,
|
||||
...keywordsAiSchema.shape,
|
||||
...qwenCodeSchema.shape,
|
||||
...rooSchema.shape,
|
||||
...vercelAiGatewaySchema.shape,
|
||||
|
|
@ -530,6 +540,7 @@ export const modelIdKeys = [
|
|||
"litellmModelId",
|
||||
"huggingFaceModelId",
|
||||
"ioIntelligenceModelId",
|
||||
"keywordsAiModelId",
|
||||
"vercelAiGatewayModelId",
|
||||
"deepInfraModelId",
|
||||
] as const satisfies readonly (keyof ProviderSettings)[]
|
||||
|
|
@ -582,6 +593,7 @@ export const modelIdKeysByProvider: Record<TypicalProvider, ModelIdKey> = {
|
|||
fireworks: "apiModelId",
|
||||
featherless: "apiModelId",
|
||||
"io-intelligence": "ioIntelligenceModelId",
|
||||
"keywords-ai": "keywordsAiModelId",
|
||||
roo: "apiModelId",
|
||||
"vercel-ai-gateway": "vercelAiGatewayModelId",
|
||||
}
|
||||
|
|
@ -716,6 +728,7 @@ export const MODELS_BY_PROVIDER: Record<
|
|||
litellm: { id: "litellm", label: "LiteLLM", models: [] },
|
||||
openrouter: { id: "openrouter", label: "OpenRouter", models: [] },
|
||||
requesty: { id: "requesty", label: "Requesty", models: [] },
|
||||
"keywords-ai": { id: "keywords-ai", label: "Keywords AI", models: [] },
|
||||
unbound: { id: "unbound", label: "Unbound", models: [] },
|
||||
deepinfra: { id: "deepinfra", label: "DeepInfra", models: [] },
|
||||
"vercel-ai-gateway": { id: "vercel-ai-gateway", label: "Vercel AI Gateway", models: [] },
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export * from "./gemini.js"
|
|||
export * from "./groq.js"
|
||||
export * from "./huggingface.js"
|
||||
export * from "./io-intelligence.js"
|
||||
export * from "./keywords-ai.js"
|
||||
export * from "./lite-llm.js"
|
||||
export * from "./lm-studio.js"
|
||||
export * from "./mistral.js"
|
||||
|
|
@ -45,6 +46,7 @@ import { fireworksDefaultModelId } from "./fireworks.js"
|
|||
import { geminiDefaultModelId } from "./gemini.js"
|
||||
import { groqDefaultModelId } from "./groq.js"
|
||||
import { ioIntelligenceDefaultModelId } from "./io-intelligence.js"
|
||||
import { keywordsAiDefaultModelId } from "./keywords-ai.js"
|
||||
import { litellmDefaultModelId } from "./lite-llm.js"
|
||||
import { mistralDefaultModelId } from "./mistral.js"
|
||||
import { moonshotDefaultModelId } from "./moonshot.js"
|
||||
|
|
@ -136,6 +138,8 @@ export function getProviderDefaultModelId(
|
|||
return featherlessDefaultModelId
|
||||
case "io-intelligence":
|
||||
return ioIntelligenceDefaultModelId
|
||||
case "keywords-ai":
|
||||
return keywordsAiDefaultModelId
|
||||
case "roo":
|
||||
return rooDefaultModelId
|
||||
case "qwen-code":
|
||||
|
|
|
|||
16
packages/types/src/providers/keywords-ai.ts
Normal file
16
packages/types/src/providers/keywords-ai.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import type { ModelInfo } from "../model.js"
|
||||
|
||||
// Keywords AI
|
||||
// https://keywordsai.co
|
||||
export const keywordsAiDefaultModelId = "gpt-4o"
|
||||
|
||||
export const keywordsAiDefaultModelInfo: ModelInfo = {
|
||||
maxTokens: 16384,
|
||||
contextWindow: 128_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 2.5,
|
||||
outputPrice: 10.0,
|
||||
description:
|
||||
"GPT-4o is OpenAI's most advanced multimodal model that's faster and cheaper than GPT-4 Turbo with stronger vision capabilities.",
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@ import {
|
|||
QwenCodeHandler,
|
||||
SambaNovaHandler,
|
||||
IOIntelligenceHandler,
|
||||
KeywordsAiHandler,
|
||||
DoubaoHandler,
|
||||
ZAiHandler,
|
||||
FireworksHandler,
|
||||
|
|
@ -185,6 +186,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
|
|||
return new FireworksHandler(options)
|
||||
case "io-intelligence":
|
||||
return new IOIntelligenceHandler(options)
|
||||
case "keywords-ai":
|
||||
return new KeywordsAiHandler(options)
|
||||
case "roo":
|
||||
// Never throw exceptions from provider constructors
|
||||
// The provider-proxy server will handle authentication and return appropriate error codes
|
||||
|
|
|
|||
59
src/api/providers/fetchers/keywords-ai.ts
Normal file
59
src/api/providers/fetchers/keywords-ai.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import axios from "axios"
|
||||
|
||||
import type { ModelInfo } from "@roo-code/types"
|
||||
|
||||
import { parseApiPrice } from "../../../shared/cost"
|
||||
|
||||
const KEYWORDS_AI_DEFAULT_BASE_URL = "https://api.keywordsai.co/api"
|
||||
|
||||
export async function getKeywordsAiModels(apiKey?: string, baseUrl?: string): Promise<Record<string, ModelInfo>> {
|
||||
const models: Record<string, ModelInfo> = {}
|
||||
|
||||
try {
|
||||
const headers: Record<string, string> = {}
|
||||
|
||||
if (apiKey) {
|
||||
headers["Authorization"] = `Bearer ${apiKey}`
|
||||
}
|
||||
|
||||
const resolvedBaseUrl = baseUrl || KEYWORDS_AI_DEFAULT_BASE_URL
|
||||
const modelsUrl = new URL("v1/models", resolvedBaseUrl)
|
||||
|
||||
const response = await axios.get(modelsUrl.toString(), { headers })
|
||||
const rawModels = response.data.data
|
||||
|
||||
for (const rawModel of rawModels) {
|
||||
// Determine reasoning capabilities based on model ID
|
||||
const reasoningBudget =
|
||||
rawModel.supports_reasoning && (rawModel.id.includes("claude") || rawModel.id.includes("gemini-2.5"))
|
||||
const reasoningEffort =
|
||||
rawModel.supports_reasoning &&
|
||||
(rawModel.id.includes("openai") ||
|
||||
rawModel.id.includes("gpt") ||
|
||||
rawModel.id.includes("o1") ||
|
||||
rawModel.id.includes("o3"))
|
||||
|
||||
const modelInfo: ModelInfo = {
|
||||
maxTokens: rawModel.max_output_tokens || rawModel.max_tokens,
|
||||
contextWindow: rawModel.context_window || rawModel.context_length,
|
||||
supportsPromptCache: rawModel.supports_caching || false,
|
||||
supportsImages: rawModel.supports_vision || false,
|
||||
supportsReasoningBudget: reasoningBudget,
|
||||
supportsReasoningEffort: reasoningEffort,
|
||||
inputPrice: parseApiPrice(rawModel.input_price || rawModel.input_cost),
|
||||
outputPrice: parseApiPrice(rawModel.output_price || rawModel.output_cost),
|
||||
description: rawModel.description,
|
||||
cacheWritesPrice: parseApiPrice(rawModel.caching_price || rawModel.cache_write_price),
|
||||
cacheReadsPrice: parseApiPrice(rawModel.cached_price || rawModel.cache_read_price),
|
||||
}
|
||||
|
||||
models[rawModel.id] = modelInfo
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error fetching Keywords AI models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
|
||||
)
|
||||
}
|
||||
|
||||
return models
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ import { GetModelsOptions } from "../../../shared/api"
|
|||
import { getOllamaModels } from "./ollama"
|
||||
import { getLMStudioModels } from "./lmstudio"
|
||||
import { getIOIntelligenceModels } from "./io-intelligence"
|
||||
import { getKeywordsAiModels } from "./keywords-ai"
|
||||
import { getDeepInfraModels } from "./deepinfra"
|
||||
import { getHuggingFaceModels } from "./huggingface"
|
||||
import { getRooModels } from "./roo"
|
||||
|
|
@ -93,6 +94,9 @@ async function fetchModelsFromProvider(options: GetModelsOptions): Promise<Model
|
|||
case "io-intelligence":
|
||||
models = await getIOIntelligenceModels(options.apiKey)
|
||||
break
|
||||
case "keywords-ai":
|
||||
models = await getKeywordsAiModels(options.apiKey, options.baseUrl)
|
||||
break
|
||||
case "vercel-ai-gateway":
|
||||
models = await getVercelAiGatewayModels()
|
||||
break
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export { GeminiHandler } from "./gemini"
|
|||
export { GroqHandler } from "./groq"
|
||||
export { HuggingFaceHandler } from "./huggingface"
|
||||
export { IOIntelligenceHandler } from "./io-intelligence"
|
||||
export { KeywordsAiHandler } from "./keywords-ai"
|
||||
export { LiteLLMHandler } from "./lite-llm"
|
||||
export { LmStudioHandler } from "./lm-studio"
|
||||
export { MistralHandler } from "./mistral"
|
||||
|
|
|
|||
219
src/api/providers/keywords-ai.ts
Normal file
219
src/api/providers/keywords-ai.ts
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
|
||||
import { type ModelInfo, type ModelRecord, keywordsAiDefaultModelId, keywordsAiDefaultModelInfo } from "@roo-code/types"
|
||||
|
||||
import type { ApiHandlerOptions } from "../../shared/api"
|
||||
import { calculateApiCostOpenAI } from "../../shared/cost"
|
||||
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
|
||||
import { getModelParams } from "../transform/model-params"
|
||||
import { AnthropicReasoningParams } from "../transform/reasoning"
|
||||
|
||||
import { DEFAULT_HEADERS } from "./constants"
|
||||
import { getModels } from "./fetchers/modelCache"
|
||||
import { BaseProvider } from "./base-provider"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import { handleOpenAIError } from "./utils/openai-error-handler"
|
||||
import { applyRouterToolPreferences } from "./utils/router-tool-preferences"
|
||||
|
||||
const KEYWORDS_AI_DEFAULT_BASE_URL = "https://api.keywordsai.co/api"
|
||||
|
||||
// Keywords AI usage includes an extra field for token details.
|
||||
interface KeywordsAiUsage extends OpenAI.CompletionUsage {
|
||||
prompt_tokens_details?: {
|
||||
caching_tokens?: number
|
||||
cached_tokens?: number
|
||||
}
|
||||
total_cost?: number
|
||||
}
|
||||
|
||||
type KeywordsAiChatCompletionParamsStreaming = OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming & {
|
||||
disable_log?: boolean
|
||||
thinking?: AnthropicReasoningParams
|
||||
}
|
||||
|
||||
type KeywordsAiChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & {
|
||||
disable_log?: boolean
|
||||
thinking?: AnthropicReasoningParams
|
||||
}
|
||||
|
||||
export class KeywordsAiHandler extends BaseProvider implements SingleCompletionHandler {
|
||||
protected options: ApiHandlerOptions
|
||||
protected models: ModelRecord = {}
|
||||
private client: OpenAI
|
||||
private baseURL: string
|
||||
private readonly providerName = "Keywords AI"
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
super()
|
||||
|
||||
this.options = options
|
||||
this.baseURL = options.keywordsAiBaseUrl || KEYWORDS_AI_DEFAULT_BASE_URL
|
||||
|
||||
const apiKey = this.options.keywordsAiApiKey ?? "not-provided"
|
||||
|
||||
this.client = new OpenAI({
|
||||
baseURL: this.baseURL,
|
||||
apiKey: apiKey,
|
||||
defaultHeaders: DEFAULT_HEADERS,
|
||||
})
|
||||
}
|
||||
|
||||
public async fetchModel() {
|
||||
this.models = await getModels({
|
||||
provider: "keywords-ai",
|
||||
baseUrl: this.baseURL,
|
||||
apiKey: this.options.keywordsAiApiKey,
|
||||
})
|
||||
return this.getModel()
|
||||
}
|
||||
|
||||
override getModel() {
|
||||
const id = this.options.keywordsAiModelId ?? keywordsAiDefaultModelId
|
||||
const cachedInfo = this.models[id] ?? keywordsAiDefaultModelInfo
|
||||
let info: ModelInfo = cachedInfo
|
||||
|
||||
// Apply tool preferences for models accessed through routers (OpenAI, Gemini)
|
||||
info = applyRouterToolPreferences(id, info)
|
||||
|
||||
const params = getModelParams({
|
||||
format: "anthropic",
|
||||
modelId: id,
|
||||
model: info,
|
||||
settings: this.options,
|
||||
})
|
||||
|
||||
return { id, info, ...params }
|
||||
}
|
||||
|
||||
protected processUsageMetrics(usage: any, modelInfo?: ModelInfo): ApiStreamUsageChunk {
|
||||
const keywordsAiUsage = usage as KeywordsAiUsage
|
||||
const inputTokens = keywordsAiUsage?.prompt_tokens || 0
|
||||
const outputTokens = keywordsAiUsage?.completion_tokens || 0
|
||||
const cacheWriteTokens = keywordsAiUsage?.prompt_tokens_details?.caching_tokens || 0
|
||||
const cacheReadTokens = keywordsAiUsage?.prompt_tokens_details?.cached_tokens || 0
|
||||
const { totalCost } = modelInfo
|
||||
? calculateApiCostOpenAI(modelInfo, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens)
|
||||
: { totalCost: 0 }
|
||||
|
||||
return {
|
||||
type: "usage",
|
||||
inputTokens: inputTokens,
|
||||
outputTokens: outputTokens,
|
||||
cacheWriteTokens: cacheWriteTokens,
|
||||
cacheReadTokens: cacheReadTokens,
|
||||
totalCost: totalCost,
|
||||
}
|
||||
}
|
||||
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
const {
|
||||
id: model,
|
||||
info,
|
||||
maxTokens: max_tokens,
|
||||
temperature,
|
||||
reasoningEffort: reasoning_effort,
|
||||
reasoning: thinking,
|
||||
} = await this.fetchModel()
|
||||
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
// Map extended efforts to OpenAI Chat Completions-accepted values (omit unsupported)
|
||||
const allowedEffort = (["low", "medium", "high"] as const).includes(reasoning_effort as any)
|
||||
? (reasoning_effort as OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming["reasoning_effort"])
|
||||
: undefined
|
||||
|
||||
// When keywordsAiEnableLogging is false (or explicitly disabled), set disable_log: true
|
||||
const disableLog = this.options.keywordsAiEnableLogging === false
|
||||
|
||||
const completionParams: KeywordsAiChatCompletionParamsStreaming = {
|
||||
messages: openAiMessages,
|
||||
model,
|
||||
max_tokens,
|
||||
temperature,
|
||||
...(allowedEffort && { reasoning_effort: allowedEffort }),
|
||||
...(thinking && { thinking }),
|
||||
...(disableLog && { disable_log: true }),
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
tools: this.convertToolsForOpenAI(metadata?.tools),
|
||||
tool_choice: metadata?.tool_choice,
|
||||
}
|
||||
|
||||
let stream
|
||||
try {
|
||||
// With streaming params type, SDK returns an async iterable stream
|
||||
stream = await this.client.chat.completions.create(completionParams)
|
||||
} catch (error) {
|
||||
throw handleOpenAIError(error, this.providerName)
|
||||
}
|
||||
let lastUsage: any = 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) || "" }
|
||||
}
|
||||
|
||||
// Handle native tool calls
|
||||
if (delta && "tool_calls" in delta && Array.isArray(delta.tool_calls)) {
|
||||
for (const toolCall of delta.tool_calls) {
|
||||
yield {
|
||||
type: "tool_call_partial",
|
||||
index: toolCall.index,
|
||||
id: toolCall.id,
|
||||
name: toolCall.function?.name,
|
||||
arguments: toolCall.function?.arguments,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
lastUsage = chunk.usage
|
||||
}
|
||||
}
|
||||
|
||||
if (lastUsage) {
|
||||
yield this.processUsageMetrics(lastUsage, info)
|
||||
}
|
||||
}
|
||||
|
||||
async completePrompt(prompt: string): Promise<string> {
|
||||
const { id: model, maxTokens: max_tokens, temperature } = await this.fetchModel()
|
||||
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [{ role: "system", content: prompt }]
|
||||
|
||||
// When keywordsAiEnableLogging is false (or explicitly disabled), set disable_log: true
|
||||
const disableLog = this.options.keywordsAiEnableLogging === false
|
||||
|
||||
const completionParams: KeywordsAiChatCompletionParams = {
|
||||
model,
|
||||
max_tokens,
|
||||
messages: openAiMessages,
|
||||
temperature: temperature,
|
||||
...(disableLog && { disable_log: true }),
|
||||
}
|
||||
|
||||
let response: OpenAI.Chat.ChatCompletion
|
||||
try {
|
||||
response = await this.client.chat.completions.create(completionParams)
|
||||
} catch (error) {
|
||||
throw handleOpenAIError(error, this.providerName)
|
||||
}
|
||||
return response.choices[0]?.message.content || ""
|
||||
}
|
||||
}
|
||||
|
|
@ -874,6 +874,7 @@ export const webviewMessageHandler = async (
|
|||
lmstudio: {},
|
||||
roo: {},
|
||||
chutes: {},
|
||||
"keywords-ai": {},
|
||||
}
|
||||
|
||||
const safeGetModels = async (options: GetModelsOptions): Promise<ModelRecord> => {
|
||||
|
|
|
|||
|
|
@ -175,6 +175,7 @@ const dynamicProviderExtras = {
|
|||
litellm: {} as { apiKey: string; baseUrl: string },
|
||||
deepinfra: {} as { apiKey?: string; baseUrl?: string },
|
||||
"io-intelligence": {} as { apiKey: string },
|
||||
"keywords-ai": {} as { apiKey?: string; baseUrl?: string },
|
||||
requesty: {} as { apiKey?: string; baseUrl?: string },
|
||||
unbound: {} as { apiKey?: string },
|
||||
ollama: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
litellmDefaultModelId,
|
||||
openAiNativeDefaultModelId,
|
||||
openAiCodexDefaultModelId,
|
||||
keywordsAiDefaultModelId,
|
||||
anthropicDefaultModelId,
|
||||
doubaoDefaultModelId,
|
||||
qwenCodeDefaultModelId,
|
||||
|
|
@ -83,6 +84,7 @@ import {
|
|||
Groq,
|
||||
HuggingFace,
|
||||
IOIntelligence,
|
||||
KeywordsAi,
|
||||
LMStudio,
|
||||
LiteLLM,
|
||||
Mistral,
|
||||
|
|
@ -340,6 +342,7 @@ const ApiOptions = ({
|
|||
openrouter: { field: "openRouterModelId", default: openRouterDefaultModelId },
|
||||
unbound: { field: "unboundModelId", default: unboundDefaultModelId },
|
||||
requesty: { field: "requestyModelId", default: requestyDefaultModelId },
|
||||
"keywords-ai": { field: "keywordsAiModelId", default: keywordsAiDefaultModelId },
|
||||
litellm: { field: "litellmModelId", default: litellmDefaultModelId },
|
||||
anthropic: { field: "apiModelId", default: anthropicDefaultModelId },
|
||||
cerebras: { field: "apiModelId", default: cerebrasDefaultModelId },
|
||||
|
|
@ -526,6 +529,18 @@ const ApiOptions = ({
|
|||
/>
|
||||
)}
|
||||
|
||||
{selectedProvider === "keywords-ai" && (
|
||||
<KeywordsAi
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
routerModels={routerModels}
|
||||
refetchRouterModels={refetchRouterModels}
|
||||
organizationAllowList={organizationAllowList}
|
||||
modelValidationError={modelValidationError}
|
||||
simplifySettings={fromWelcomeView}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedProvider === "unbound" && (
|
||||
<Unbound
|
||||
apiConfiguration={apiConfiguration}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ type ModelIdKey = keyof Pick<
|
|||
| "deepInfraModelId"
|
||||
| "ioIntelligenceModelId"
|
||||
| "vercelAiGatewayModelId"
|
||||
| "keywordsAiModelId"
|
||||
| "apiModelId"
|
||||
| "ollamaModelId"
|
||||
| "lmStudioModelId"
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ export const PROVIDERS = [
|
|||
{ value: "xai", label: "xAI (Grok)", proxy: false },
|
||||
{ value: "groq", label: "Groq", proxy: false },
|
||||
{ value: "huggingface", label: "Hugging Face", proxy: false },
|
||||
{ value: "keywords-ai", label: "Keywords AI", proxy: false },
|
||||
{ value: "chutes", label: "Chutes AI", proxy: false },
|
||||
{ value: "litellm", label: "LiteLLM", proxy: true },
|
||||
{ value: "sambanova", label: "SambaNova", proxy: false },
|
||||
|
|
|
|||
148
webview-ui/src/components/settings/providers/KeywordsAi.tsx
Normal file
148
webview-ui/src/components/settings/providers/KeywordsAi.tsx
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
import { useCallback, useEffect, useState } from "react"
|
||||
import { VSCodeCheckbox, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
import {
|
||||
type ProviderSettings,
|
||||
type OrganizationAllowList,
|
||||
type RouterModels,
|
||||
keywordsAiDefaultModelId,
|
||||
} from "@roo-code/types"
|
||||
|
||||
import { vscode } from "@src/utils/vscode"
|
||||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
import { Button } from "@src/components/ui"
|
||||
|
||||
import { inputEventTransform } from "../transforms"
|
||||
import { ModelPicker } from "../ModelPicker"
|
||||
|
||||
type KeywordsAiProps = {
|
||||
apiConfiguration: ProviderSettings
|
||||
setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
|
||||
routerModels?: RouterModels
|
||||
refetchRouterModels: () => void
|
||||
organizationAllowList: OrganizationAllowList
|
||||
modelValidationError?: string
|
||||
simplifySettings?: boolean
|
||||
}
|
||||
|
||||
export const KeywordsAi = ({
|
||||
apiConfiguration,
|
||||
setApiConfigurationField,
|
||||
routerModels,
|
||||
organizationAllowList,
|
||||
modelValidationError,
|
||||
simplifySettings,
|
||||
}: KeywordsAiProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
|
||||
const [keywordsAiEndpointSelected, setKeywordsAiEndpointSelected] = useState(!!apiConfiguration.keywordsAiBaseUrl)
|
||||
|
||||
// This ensures that the "Use custom URL" checkbox is hidden when the user deletes the URL.
|
||||
useEffect(() => {
|
||||
setKeywordsAiEndpointSelected(!!apiConfiguration?.keywordsAiBaseUrl)
|
||||
}, [apiConfiguration?.keywordsAiBaseUrl])
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
<K extends keyof ProviderSettings, E>(
|
||||
field: K,
|
||||
transform: (event: E) => ProviderSettings[K] = inputEventTransform,
|
||||
) =>
|
||||
(event: E | Event) => {
|
||||
setApiConfigurationField(field, transform(event as E))
|
||||
},
|
||||
[setApiConfigurationField],
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.keywordsAiApiKey || ""}
|
||||
type="password"
|
||||
onInput={handleInputChange("keywordsAiApiKey")}
|
||||
placeholder={t("settings:providers.keywordsAi.getApiKey")}
|
||||
className="w-full">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<label className="block font-medium">{t("settings:providers.keywordsAi.apiKey")}</label>
|
||||
</div>
|
||||
</VSCodeTextField>
|
||||
<div className="text-sm text-vscode-descriptionForeground -mt-2">
|
||||
{t("settings:providers.apiKeyStorageNotice")}
|
||||
</div>
|
||||
<a
|
||||
href="https://platform.keywordsai.co/platform/api/api-keys"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center justify-center whitespace-nowrap text-sm font-medium focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-9 rounded-md px-3 w-full"
|
||||
style={{
|
||||
width: "100%",
|
||||
textDecoration: "none",
|
||||
color: "var(--vscode-button-foreground)",
|
||||
backgroundColor: "var(--vscode-button-background)",
|
||||
}}>
|
||||
{t("settings:providers.keywordsAi.getApiKey")}
|
||||
</a>
|
||||
|
||||
<VSCodeCheckbox
|
||||
checked={apiConfiguration.keywordsAiEnableLogging !== false}
|
||||
onChange={(e: any) => {
|
||||
const isChecked = e.target.checked === true
|
||||
setApiConfigurationField("keywordsAiEnableLogging", isChecked)
|
||||
}}>
|
||||
{t("settings:providers.keywordsAi.enableLogging")}
|
||||
</VSCodeCheckbox>
|
||||
<div className="text-sm text-vscode-descriptionForeground -mt-2">
|
||||
{t("settings:providers.keywordsAi.enableLoggingDescription")}
|
||||
</div>
|
||||
|
||||
<VSCodeCheckbox
|
||||
checked={keywordsAiEndpointSelected}
|
||||
onChange={(e: any) => {
|
||||
const isChecked = e.target.checked === true
|
||||
if (!isChecked) {
|
||||
setApiConfigurationField("keywordsAiBaseUrl", undefined)
|
||||
}
|
||||
|
||||
setKeywordsAiEndpointSelected(isChecked)
|
||||
}}>
|
||||
{t("settings:providers.keywordsAi.useCustomBaseUrl")}
|
||||
</VSCodeCheckbox>
|
||||
{keywordsAiEndpointSelected && (
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.keywordsAiBaseUrl || ""}
|
||||
type="text"
|
||||
onInput={handleInputChange("keywordsAiBaseUrl")}
|
||||
placeholder="https://api.keywordsai.co/api"
|
||||
className="w-full">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<label className="block font-medium">{t("settings:providers.keywordsAi.baseUrl")}</label>
|
||||
</div>
|
||||
</VSCodeTextField>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
vscode.postMessage({
|
||||
type: "requestRouterModels",
|
||||
values: { provider: "keywords-ai", refresh: true },
|
||||
})
|
||||
}}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="codicon codicon-refresh" />
|
||||
{t("settings:providers.refreshModels.label")}
|
||||
</div>
|
||||
</Button>
|
||||
<ModelPicker
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
defaultModelId={keywordsAiDefaultModelId}
|
||||
models={routerModels?.["keywords-ai"] ?? {}}
|
||||
modelIdKey="keywordsAiModelId"
|
||||
serviceName="Keywords AI"
|
||||
serviceUrl="https://keywordsai.co"
|
||||
organizationAllowList={organizationAllowList}
|
||||
errorMessage={modelValidationError}
|
||||
simplifySettings={simplifySettings}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ export { Gemini } from "./Gemini"
|
|||
export { Groq } from "./Groq"
|
||||
export { HuggingFace } from "./HuggingFace"
|
||||
export { IOIntelligence } from "./IOIntelligence"
|
||||
export { KeywordsAi } from "./KeywordsAi"
|
||||
export { LMStudio } from "./LMStudio"
|
||||
export { Mistral } from "./Mistral"
|
||||
export { Moonshot } from "./Moonshot"
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ export const isStaticModelProvider = (provider: ProviderName): boolean => {
|
|||
export const PROVIDERS_WITH_CUSTOM_MODEL_UI: ProviderName[] = [
|
||||
"openrouter",
|
||||
"requesty",
|
||||
"keywords-ai",
|
||||
"unbound",
|
||||
"deepinfra",
|
||||
"openai", // OpenAI Compatible
|
||||
|
|
|
|||
|
|
@ -368,6 +368,15 @@ function getSelectedModel({
|
|||
const info = routerModels["vercel-ai-gateway"]?.[id]
|
||||
return { id, info }
|
||||
}
|
||||
case "keywords-ai": {
|
||||
const id = getValidatedModelId(
|
||||
apiConfiguration.keywordsAiModelId,
|
||||
routerModels["keywords-ai"],
|
||||
defaultModelId,
|
||||
)
|
||||
const info = routerModels["keywords-ai"]?.[id]
|
||||
return { id, info }
|
||||
}
|
||||
// case "anthropic":
|
||||
// case "fake-ai":
|
||||
default: {
|
||||
|
|
|
|||
|
|
@ -362,6 +362,14 @@
|
|||
"getFireworksApiKey": "Get Fireworks API Key",
|
||||
"featherlessApiKey": "Featherless API Key",
|
||||
"getFeatherlessApiKey": "Get Featherless API Key",
|
||||
"keywordsAi": {
|
||||
"apiKey": "Keywords AI API Key",
|
||||
"getApiKey": "Get Keywords AI API Key",
|
||||
"enableLogging": "Enable Logging",
|
||||
"enableLoggingDescription": "When enabled, Keywords AI will log your requests for observability and analytics. Disable to opt out of logging.",
|
||||
"useCustomBaseUrl": "Use custom base URL",
|
||||
"baseUrl": "Base URL"
|
||||
},
|
||||
"ioIntelligenceApiKey": "IO Intelligence API Key",
|
||||
"ioIntelligenceApiKeyPlaceholder": "Enter your IO Intelligence API key",
|
||||
"getIoIntelligenceApiKey": "Get IO Intelligence API Key",
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ describe("Model Validation Functions", () => {
|
|||
huggingface: {},
|
||||
roo: {},
|
||||
chutes: {},
|
||||
"keywords-ai": {},
|
||||
}
|
||||
|
||||
const allowAllOrganization: OrganizationAllowList = {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue