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:
Roo Code 2026-01-26 04:16:34 +00:00
parent 953c7773c0
commit 72ab03ecd6
19 changed files with 506 additions and 0 deletions

View file

@ -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: [] },

View file

@ -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":

View 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.",
}

View file

@ -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

View 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
}

View file

@ -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

View file

@ -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"

View 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 || ""
}
}

View file

@ -874,6 +874,7 @@ export const webviewMessageHandler = async (
lmstudio: {},
roo: {},
chutes: {},
"keywords-ai": {},
}
const safeGetModels = async (options: GetModelsOptions): Promise<ModelRecord> => {

View file

@ -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

View file

@ -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}

View file

@ -36,6 +36,7 @@ type ModelIdKey = keyof Pick<
| "deepInfraModelId"
| "ioIntelligenceModelId"
| "vercelAiGatewayModelId"
| "keywordsAiModelId"
| "apiModelId"
| "ollamaModelId"
| "lmStudioModelId"

View file

@ -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 },

View 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}
/>
</>
)
}

View file

@ -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"

View file

@ -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

View file

@ -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: {

View file

@ -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",

View file

@ -49,6 +49,7 @@ describe("Model Validation Functions", () => {
huggingface: {},
roo: {},
chutes: {},
"keywords-ai": {},
}
const allowAllOrganization: OrganizationAllowList = {