Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 72ab03e

Browse files
committed
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
1 parent 953c777 commit 72ab03e

19 files changed

Lines changed: 506 additions & 0 deletions

File tree

packages/types/src/provider-settings.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export const dynamicProviders = [
4646
"litellm",
4747
"deepinfra",
4848
"io-intelligence",
49+
"keywords-ai",
4950
"requesty",
5051
"unbound",
5152
"roo",
@@ -399,6 +400,13 @@ const ioIntelligenceSchema = apiModelIdProviderModelSchema.extend({
399400
ioIntelligenceApiKey: z.string().optional(),
400401
})
401402

403+
const keywordsAiSchema = baseProviderSettingsSchema.extend({
404+
keywordsAiApiKey: z.string().optional(),
405+
keywordsAiBaseUrl: z.string().optional(),
406+
keywordsAiModelId: z.string().optional(),
407+
keywordsAiEnableLogging: z.boolean().optional(),
408+
})
409+
402410
const qwenCodeSchema = apiModelIdProviderModelSchema.extend({
403411
qwenCodeOauthPath: z.string().optional(),
404412
})
@@ -455,6 +463,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
455463
fireworksSchema.merge(z.object({ apiProvider: z.literal("fireworks") })),
456464
featherlessSchema.merge(z.object({ apiProvider: z.literal("featherless") })),
457465
ioIntelligenceSchema.merge(z.object({ apiProvider: z.literal("io-intelligence") })),
466+
keywordsAiSchema.merge(z.object({ apiProvider: z.literal("keywords-ai") })),
458467
qwenCodeSchema.merge(z.object({ apiProvider: z.literal("qwen-code") })),
459468
rooSchema.merge(z.object({ apiProvider: z.literal("roo") })),
460469
vercelAiGatewaySchema.merge(z.object({ apiProvider: z.literal("vercel-ai-gateway") })),
@@ -496,6 +505,7 @@ export const providerSettingsSchema = z.object({
496505
...fireworksSchema.shape,
497506
...featherlessSchema.shape,
498507
...ioIntelligenceSchema.shape,
508+
...keywordsAiSchema.shape,
499509
...qwenCodeSchema.shape,
500510
...rooSchema.shape,
501511
...vercelAiGatewaySchema.shape,
@@ -530,6 +540,7 @@ export const modelIdKeys = [
530540
"litellmModelId",
531541
"huggingFaceModelId",
532542
"ioIntelligenceModelId",
543+
"keywordsAiModelId",
533544
"vercelAiGatewayModelId",
534545
"deepInfraModelId",
535546
] as const satisfies readonly (keyof ProviderSettings)[]
@@ -582,6 +593,7 @@ export const modelIdKeysByProvider: Record<TypicalProvider, ModelIdKey> = {
582593
fireworks: "apiModelId",
583594
featherless: "apiModelId",
584595
"io-intelligence": "ioIntelligenceModelId",
596+
"keywords-ai": "keywordsAiModelId",
585597
roo: "apiModelId",
586598
"vercel-ai-gateway": "vercelAiGatewayModelId",
587599
}
@@ -716,6 +728,7 @@ export const MODELS_BY_PROVIDER: Record<
716728
litellm: { id: "litellm", label: "LiteLLM", models: [] },
717729
openrouter: { id: "openrouter", label: "OpenRouter", models: [] },
718730
requesty: { id: "requesty", label: "Requesty", models: [] },
731+
"keywords-ai": { id: "keywords-ai", label: "Keywords AI", models: [] },
719732
unbound: { id: "unbound", label: "Unbound", models: [] },
720733
deepinfra: { id: "deepinfra", label: "DeepInfra", models: [] },
721734
"vercel-ai-gateway": { id: "vercel-ai-gateway", label: "Vercel AI Gateway", models: [] },

packages/types/src/providers/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export * from "./gemini.js"
1111
export * from "./groq.js"
1212
export * from "./huggingface.js"
1313
export * from "./io-intelligence.js"
14+
export * from "./keywords-ai.js"
1415
export * from "./lite-llm.js"
1516
export * from "./lm-studio.js"
1617
export * from "./mistral.js"
@@ -45,6 +46,7 @@ import { fireworksDefaultModelId } from "./fireworks.js"
4546
import { geminiDefaultModelId } from "./gemini.js"
4647
import { groqDefaultModelId } from "./groq.js"
4748
import { ioIntelligenceDefaultModelId } from "./io-intelligence.js"
49+
import { keywordsAiDefaultModelId } from "./keywords-ai.js"
4850
import { litellmDefaultModelId } from "./lite-llm.js"
4951
import { mistralDefaultModelId } from "./mistral.js"
5052
import { moonshotDefaultModelId } from "./moonshot.js"
@@ -136,6 +138,8 @@ export function getProviderDefaultModelId(
136138
return featherlessDefaultModelId
137139
case "io-intelligence":
138140
return ioIntelligenceDefaultModelId
141+
case "keywords-ai":
142+
return keywordsAiDefaultModelId
139143
case "roo":
140144
return rooDefaultModelId
141145
case "qwen-code":
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { ModelInfo } from "../model.js"
2+
3+
// Keywords AI
4+
// https://keywordsai.co
5+
export const keywordsAiDefaultModelId = "gpt-4o"
6+
7+
export const keywordsAiDefaultModelInfo: ModelInfo = {
8+
maxTokens: 16384,
9+
contextWindow: 128_000,
10+
supportsImages: true,
11+
supportsPromptCache: false,
12+
inputPrice: 2.5,
13+
outputPrice: 10.0,
14+
description:
15+
"GPT-4o is OpenAI's most advanced multimodal model that's faster and cheaper than GPT-4 Turbo with stronger vision capabilities.",
16+
}

src/api/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
QwenCodeHandler,
3333
SambaNovaHandler,
3434
IOIntelligenceHandler,
35+
KeywordsAiHandler,
3536
DoubaoHandler,
3637
ZAiHandler,
3738
FireworksHandler,
@@ -185,6 +186,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
185186
return new FireworksHandler(options)
186187
case "io-intelligence":
187188
return new IOIntelligenceHandler(options)
189+
case "keywords-ai":
190+
return new KeywordsAiHandler(options)
188191
case "roo":
189192
// Never throw exceptions from provider constructors
190193
// The provider-proxy server will handle authentication and return appropriate error codes
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import axios from "axios"
2+
3+
import type { ModelInfo } from "@roo-code/types"
4+
5+
import { parseApiPrice } from "../../../shared/cost"
6+
7+
const KEYWORDS_AI_DEFAULT_BASE_URL = "https://api.keywordsai.co/api"
8+
9+
export async function getKeywordsAiModels(apiKey?: string, baseUrl?: string): Promise<Record<string, ModelInfo>> {
10+
const models: Record<string, ModelInfo> = {}
11+
12+
try {
13+
const headers: Record<string, string> = {}
14+
15+
if (apiKey) {
16+
headers["Authorization"] = `Bearer ${apiKey}`
17+
}
18+
19+
const resolvedBaseUrl = baseUrl || KEYWORDS_AI_DEFAULT_BASE_URL
20+
const modelsUrl = new URL("v1/models", resolvedBaseUrl)
21+
22+
const response = await axios.get(modelsUrl.toString(), { headers })
23+
const rawModels = response.data.data
24+
25+
for (const rawModel of rawModels) {
26+
// Determine reasoning capabilities based on model ID
27+
const reasoningBudget =
28+
rawModel.supports_reasoning && (rawModel.id.includes("claude") || rawModel.id.includes("gemini-2.5"))
29+
const reasoningEffort =
30+
rawModel.supports_reasoning &&
31+
(rawModel.id.includes("openai") ||
32+
rawModel.id.includes("gpt") ||
33+
rawModel.id.includes("o1") ||
34+
rawModel.id.includes("o3"))
35+
36+
const modelInfo: ModelInfo = {
37+
maxTokens: rawModel.max_output_tokens || rawModel.max_tokens,
38+
contextWindow: rawModel.context_window || rawModel.context_length,
39+
supportsPromptCache: rawModel.supports_caching || false,
40+
supportsImages: rawModel.supports_vision || false,
41+
supportsReasoningBudget: reasoningBudget,
42+
supportsReasoningEffort: reasoningEffort,
43+
inputPrice: parseApiPrice(rawModel.input_price || rawModel.input_cost),
44+
outputPrice: parseApiPrice(rawModel.output_price || rawModel.output_cost),
45+
description: rawModel.description,
46+
cacheWritesPrice: parseApiPrice(rawModel.caching_price || rawModel.cache_write_price),
47+
cacheReadsPrice: parseApiPrice(rawModel.cached_price || rawModel.cache_read_price),
48+
}
49+
50+
models[rawModel.id] = modelInfo
51+
}
52+
} catch (error) {
53+
console.error(
54+
`Error fetching Keywords AI models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
55+
)
56+
}
57+
58+
return models
59+
}

src/api/providers/fetchers/modelCache.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { GetModelsOptions } from "../../../shared/api"
2525
import { getOllamaModels } from "./ollama"
2626
import { getLMStudioModels } from "./lmstudio"
2727
import { getIOIntelligenceModels } from "./io-intelligence"
28+
import { getKeywordsAiModels } from "./keywords-ai"
2829
import { getDeepInfraModels } from "./deepinfra"
2930
import { getHuggingFaceModels } from "./huggingface"
3031
import { getRooModels } from "./roo"
@@ -93,6 +94,9 @@ async function fetchModelsFromProvider(options: GetModelsOptions): Promise<Model
9394
case "io-intelligence":
9495
models = await getIOIntelligenceModels(options.apiKey)
9596
break
97+
case "keywords-ai":
98+
models = await getKeywordsAiModels(options.apiKey, options.baseUrl)
99+
break
96100
case "vercel-ai-gateway":
97101
models = await getVercelAiGatewayModels()
98102
break

src/api/providers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export { GeminiHandler } from "./gemini"
1111
export { GroqHandler } from "./groq"
1212
export { HuggingFaceHandler } from "./huggingface"
1313
export { IOIntelligenceHandler } from "./io-intelligence"
14+
export { KeywordsAiHandler } from "./keywords-ai"
1415
export { LiteLLMHandler } from "./lite-llm"
1516
export { LmStudioHandler } from "./lm-studio"
1617
export { MistralHandler } from "./mistral"

0 commit comments

Comments
 (0)