mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Support custom model configuration files
This commit is contained in:
parent
f93aafefe1
commit
5de3431905
25 changed files with 1594 additions and 169 deletions
39
packages/types/src/custom-models.ts
Normal file
39
packages/types/src/custom-models.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { z } from "zod"
|
||||
|
||||
/**
|
||||
* Schema for custom model information
|
||||
* Defines the properties that can be specified for custom models
|
||||
*/
|
||||
export const customModelInfoSchema = z.object({
|
||||
maxTokens: z.number().positive().optional(),
|
||||
contextWindow: z.number().positive(),
|
||||
supportsImages: z.boolean().optional(),
|
||||
supportsPromptCache: z.boolean(), // Required in ModelInfo
|
||||
supportsTemperature: z.boolean().optional(),
|
||||
inputPrice: z.number().nonnegative().optional(),
|
||||
outputPrice: z.number().nonnegative().optional(),
|
||||
cacheWritesPrice: z.number().nonnegative().optional(),
|
||||
cacheReadsPrice: z.number().nonnegative().optional(),
|
||||
description: z.string().optional(),
|
||||
supportsReasoningEffort: z.boolean().optional(),
|
||||
supportsReasoningBudget: z.boolean().optional(),
|
||||
requiredReasoningBudget: z.boolean().optional(),
|
||||
reasoningEffort: z.string().optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Schema for a custom models file
|
||||
* The file is a simple record of model IDs to model information
|
||||
* The provider is determined by the filename (e.g., openrouter.json)
|
||||
*/
|
||||
export const customModelsFileSchema = z.record(z.string(), customModelInfoSchema)
|
||||
|
||||
/**
|
||||
* Type for the content of a custom models file
|
||||
*/
|
||||
export type CustomModelsFile = z.infer<typeof customModelsFileSchema>
|
||||
|
||||
/**
|
||||
* Type for custom model information
|
||||
*/
|
||||
export type CustomModelInfo = z.infer<typeof customModelInfoSchema>
|
||||
|
|
@ -2,6 +2,7 @@ export * from "./api.js"
|
|||
export * from "./cloud.js"
|
||||
export * from "./codebase-index.js"
|
||||
export * from "./cookie-consent.js"
|
||||
export * from "./custom-models.js"
|
||||
export * from "./events.js"
|
||||
export * from "./experiment.js"
|
||||
export * from "./followup.js"
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { safeJsonParse } from "../../shared/safeJsonParse"
|
|||
import { ApiStream } from "../transform/stream"
|
||||
import { addCacheBreakpoints } from "../transform/caching/vertex"
|
||||
import { getModelParams } from "../transform/model-params"
|
||||
import { getProviderModelsSync } from "./model-lookup"
|
||||
|
||||
import { BaseProvider } from "./base-provider"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
|
|
@ -164,8 +165,9 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
|
|||
|
||||
getModel() {
|
||||
const modelId = this.options.apiModelId
|
||||
let id = modelId && modelId in vertexModels ? (modelId as VertexModelId) : vertexDefaultModelId
|
||||
const info: ModelInfo = vertexModels[id]
|
||||
const models = getProviderModelsSync("vertex", vertexModels as Record<string, ModelInfo>)
|
||||
let id = modelId && modelId in models ? (modelId as VertexModelId) : vertexDefaultModelId
|
||||
const info: ModelInfo = models[id]
|
||||
const params = getModelParams({ format: "anthropic", modelId: id, model: info, settings: this.options })
|
||||
|
||||
// The `:thinking` suffix indicates that the model is a "Hybrid"
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import type { ApiHandlerOptions } from "../../shared/api"
|
|||
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { getModelParams } from "../transform/model-params"
|
||||
import { getProviderModelsSync } from "./model-lookup"
|
||||
|
||||
import { BaseProvider } from "./base-provider"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
|
|
@ -249,8 +250,9 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
|
|||
|
||||
getModel() {
|
||||
const modelId = this.options.apiModelId
|
||||
let id = modelId && modelId in anthropicModels ? (modelId as AnthropicModelId) : anthropicDefaultModelId
|
||||
let info: ModelInfo = anthropicModels[id]
|
||||
const models = getProviderModelsSync("anthropic", anthropicModels as Record<string, ModelInfo>)
|
||||
let id = modelId && modelId in models ? (modelId as AnthropicModelId) : anthropicDefaultModelId
|
||||
let info: ModelInfo = models[id]
|
||||
|
||||
// If 1M context beta is enabled for Claude Sonnet 4 or 4.5, update the model info
|
||||
if ((id === "claude-sonnet-4-20250514" || id === "claude-sonnet-4-5") && this.options.anthropicBeta1MContext) {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import {
|
|||
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { BaseProvider } from "./base-provider"
|
||||
import { getProviderModelsSync } from "./model-lookup"
|
||||
import { logger } from "../../utils/logging"
|
||||
import { Package } from "../../shared/package"
|
||||
import { MultiPointStrategy } from "../transform/cache-strategy/multi-point-strategy"
|
||||
|
|
@ -899,19 +900,22 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
|
|||
|
||||
//Prompt Router responses come back in a different sequence and the model used is in the response and must be fetched by name
|
||||
getModelById(modelId: string, modelType?: string): { id: BedrockModelId | string; info: ModelInfo } {
|
||||
// Try to find the model in bedrockModels
|
||||
// Get merged models (static + custom)
|
||||
const models = getProviderModelsSync("bedrock", bedrockModels as Record<string, ModelInfo>)
|
||||
|
||||
// Try to find the model in merged models
|
||||
const baseModelId = this.parseBaseModelId(modelId) as BedrockModelId
|
||||
|
||||
let model
|
||||
if (baseModelId in bedrockModels) {
|
||||
if (baseModelId in models) {
|
||||
//Do a deep copy of the model info so that later in the code the model id and maxTokens can be set.
|
||||
// The bedrockModels array is a constant and updating the model ID from the returned invokedModelID value
|
||||
// The models array is a constant and updating the model ID from the returned invokedModelID value
|
||||
// in a prompt router response isn't possible on the constant.
|
||||
model = { id: baseModelId, info: JSON.parse(JSON.stringify(bedrockModels[baseModelId])) }
|
||||
model = { id: baseModelId, info: JSON.parse(JSON.stringify(models[baseModelId])) }
|
||||
} else if (modelType && modelType.includes("router")) {
|
||||
model = {
|
||||
id: bedrockDefaultPromptRouterModelId,
|
||||
info: JSON.parse(JSON.stringify(bedrockModels[bedrockDefaultPromptRouterModelId])),
|
||||
info: JSON.parse(JSON.stringify(models[bedrockDefaultPromptRouterModelId])),
|
||||
}
|
||||
} else {
|
||||
// Use heuristics for model info, then allow overrides from ProviderSettings
|
||||
|
|
@ -919,7 +923,7 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
|
|||
model = {
|
||||
id: bedrockDefaultModelId,
|
||||
info: {
|
||||
...JSON.parse(JSON.stringify(bedrockModels[bedrockDefaultModelId])),
|
||||
...JSON.parse(JSON.stringify(models[bedrockDefaultModelId])),
|
||||
...guessed,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
222
src/api/providers/fetchers/__tests__/modelCache.custom.spec.ts
Normal file
222
src/api/providers/fetchers/__tests__/modelCache.custom.spec.ts
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
|
||||
import { getModels, flushModels } from "../modelCache"
|
||||
import * as customModels from "../../../../services/custom-models"
|
||||
import * as openrouter from "../openrouter"
|
||||
|
||||
// Mock file data storage
|
||||
const mockReadFileData: Record<string, any> = {}
|
||||
|
||||
// Mock the custom models service
|
||||
vi.mock("../../../../services/custom-models", () => ({
|
||||
getCustomModelsForProvider: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock the openrouter fetcher
|
||||
vi.mock("../openrouter", () => ({
|
||||
getOpenRouterModels: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock other dependencies
|
||||
vi.mock("../../../../utils/path", () => ({
|
||||
getWorkspacePath: vi.fn(() => "/test/workspace"),
|
||||
}))
|
||||
|
||||
vi.mock("../../../../core/config/ContextProxy", () => ({
|
||||
ContextProxy: {
|
||||
instance: {
|
||||
globalStorageUri: {
|
||||
fsPath: "/test/storage",
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("../../../../utils/storage", () => ({
|
||||
getCacheDirectoryPath: vi.fn(() => "/test/cache"),
|
||||
}))
|
||||
|
||||
// Mock safeWriteJson to populate our mock file data
|
||||
vi.mock("../../../../utils/safeWriteJson", () => ({
|
||||
safeWriteJson: vi.fn((filePath: string, data: any) => {
|
||||
mockReadFileData[filePath] = data
|
||||
return Promise.resolve()
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock fs.readFile to return the models that were written
|
||||
vi.mock("fs/promises", () => ({
|
||||
default: {
|
||||
readFile: vi.fn((filePath: string) => {
|
||||
const data = mockReadFileData[filePath]
|
||||
if (!data) throw new Error("File not found")
|
||||
return Promise.resolve(JSON.stringify(data))
|
||||
}),
|
||||
},
|
||||
readFile: vi.fn((filePath: string) => {
|
||||
const data = mockReadFileData[filePath]
|
||||
if (!data) throw new Error("File not found")
|
||||
return Promise.resolve(JSON.stringify(data))
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock("../../../../utils/fs", () => ({
|
||||
fileExistsAtPath: vi.fn((filePath: string) => {
|
||||
return Promise.resolve(filePath in mockReadFileData)
|
||||
}),
|
||||
}))
|
||||
|
||||
describe("Model Cache with Custom Models", () => {
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
// Clear both memory cache and mock file cache before each test
|
||||
await flushModels("openrouter")
|
||||
// Clear the mock file cache
|
||||
Object.keys(mockReadFileData).forEach((key) => delete mockReadFileData[key])
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it("should merge custom models with provider-fetched models", async () => {
|
||||
const providerModels = {
|
||||
"openai/gpt-4": {
|
||||
maxTokens: 8000,
|
||||
contextWindow: 128000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
},
|
||||
}
|
||||
|
||||
const customModelDefs = {
|
||||
"custom/my-model": {
|
||||
maxTokens: 4096,
|
||||
contextWindow: 32000,
|
||||
supportsPromptCache: false,
|
||||
description: "My custom model",
|
||||
},
|
||||
}
|
||||
|
||||
vi.mocked(openrouter.getOpenRouterModels).mockResolvedValueOnce(providerModels)
|
||||
vi.mocked(customModels.getCustomModelsForProvider).mockResolvedValueOnce(customModelDefs)
|
||||
|
||||
const result = await getModels({ provider: "openrouter" })
|
||||
|
||||
expect(result).toEqual({
|
||||
...providerModels,
|
||||
...customModelDefs,
|
||||
})
|
||||
expect(openrouter.getOpenRouterModels).toHaveBeenCalledTimes(1)
|
||||
expect(customModels.getCustomModelsForProvider).toHaveBeenCalledWith("openrouter", "/test/workspace")
|
||||
})
|
||||
|
||||
it("should allow custom models to override provider models", async () => {
|
||||
const providerModels = {
|
||||
"openai/gpt-4": {
|
||||
maxTokens: 8000,
|
||||
contextWindow: 128000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
},
|
||||
}
|
||||
|
||||
const customModelDefs = {
|
||||
"openai/gpt-4": {
|
||||
maxTokens: 16000, // Override max tokens
|
||||
contextWindow: 128000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
description: "Custom GPT-4 with higher token limit",
|
||||
},
|
||||
}
|
||||
|
||||
vi.mocked(openrouter.getOpenRouterModels).mockResolvedValueOnce(providerModels)
|
||||
vi.mocked(customModels.getCustomModelsForProvider).mockResolvedValueOnce(customModelDefs)
|
||||
|
||||
const result = await getModels({ provider: "openrouter" })
|
||||
|
||||
expect(result["openai/gpt-4"]).toEqual(customModelDefs["openai/gpt-4"])
|
||||
expect(result["openai/gpt-4"].maxTokens).toBe(16000)
|
||||
})
|
||||
|
||||
it("should handle empty custom models gracefully", async () => {
|
||||
const providerModels = {
|
||||
"openai/gpt-4": {
|
||||
maxTokens: 8000,
|
||||
contextWindow: 128000,
|
||||
supportsPromptCache: false,
|
||||
},
|
||||
}
|
||||
|
||||
vi.mocked(openrouter.getOpenRouterModels).mockResolvedValueOnce(providerModels)
|
||||
vi.mocked(customModels.getCustomModelsForProvider).mockResolvedValueOnce({})
|
||||
|
||||
const result = await getModels({ provider: "openrouter" })
|
||||
|
||||
expect(result).toEqual(providerModels)
|
||||
})
|
||||
|
||||
it("should work when provider returns no models", async () => {
|
||||
const customModelDefs = {
|
||||
"custom/model-1": {
|
||||
maxTokens: 4096,
|
||||
contextWindow: 32000,
|
||||
supportsPromptCache: false,
|
||||
},
|
||||
}
|
||||
|
||||
vi.mocked(openrouter.getOpenRouterModels).mockResolvedValueOnce({})
|
||||
vi.mocked(customModels.getCustomModelsForProvider).mockResolvedValueOnce(customModelDefs)
|
||||
|
||||
const result = await getModels({ provider: "openrouter" })
|
||||
|
||||
expect(result).toEqual(customModelDefs)
|
||||
})
|
||||
|
||||
it("should handle errors in custom models loading gracefully", async () => {
|
||||
const providerModels = {
|
||||
"openai/gpt-4": {
|
||||
maxTokens: 8000,
|
||||
contextWindow: 128000,
|
||||
supportsPromptCache: false,
|
||||
},
|
||||
}
|
||||
|
||||
vi.mocked(openrouter.getOpenRouterModels).mockResolvedValueOnce(providerModels)
|
||||
vi.mocked(customModels.getCustomModelsForProvider).mockRejectedValueOnce(
|
||||
new Error("Failed to load custom models"),
|
||||
)
|
||||
|
||||
// The error in loading custom models should cause the overall fetch to fail
|
||||
await expect(getModels({ provider: "openrouter" })).rejects.toThrow("Failed to load custom models")
|
||||
})
|
||||
|
||||
it("should flush cache for specific provider", async () => {
|
||||
const providerModels = {
|
||||
"openai/gpt-4": {
|
||||
maxTokens: 8000,
|
||||
contextWindow: 128000,
|
||||
supportsPromptCache: false,
|
||||
},
|
||||
}
|
||||
|
||||
// First call - should fetch
|
||||
vi.mocked(openrouter.getOpenRouterModels).mockResolvedValueOnce(providerModels)
|
||||
vi.mocked(customModels.getCustomModelsForProvider).mockResolvedValueOnce({})
|
||||
await getModels({ provider: "openrouter" })
|
||||
expect(openrouter.getOpenRouterModels).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Second call - should use cache (no new mocks needed)
|
||||
await getModels({ provider: "openrouter" })
|
||||
expect(openrouter.getOpenRouterModels).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Flush cache
|
||||
await flushModels("openrouter")
|
||||
|
||||
// Third call - should fetch again (set up mock again)
|
||||
vi.mocked(openrouter.getOpenRouterModels).mockResolvedValueOnce(providerModels)
|
||||
vi.mocked(customModels.getCustomModelsForProvider).mockResolvedValueOnce({})
|
||||
await getModels({ provider: "openrouter" })
|
||||
expect(openrouter.getOpenRouterModels).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
|
@ -11,6 +11,8 @@ import { ContextProxy } from "../../../core/config/ContextProxy"
|
|||
import { getCacheDirectoryPath } from "../../../utils/storage"
|
||||
import type { RouterName, ModelRecord } from "../../../shared/api"
|
||||
import { fileExistsAtPath } from "../../../utils/fs"
|
||||
import { getCustomModelsForProvider } from "../../../services/custom-models"
|
||||
import { getWorkspacePath } from "../../../utils/path"
|
||||
|
||||
import { getOpenRouterModels } from "./openrouter"
|
||||
import { getVercelAiGatewayModels } from "./vercel-ai-gateway"
|
||||
|
|
@ -118,6 +120,10 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
|
|||
}
|
||||
}
|
||||
|
||||
// Load and merge custom models
|
||||
const customModels = await getCustomModelsForProvider(provider, getWorkspacePath())
|
||||
models = { ...models, ...customModels }
|
||||
|
||||
// Cache the fetched models (even if empty, to signify a successful fetch with no models).
|
||||
memoryCache.set(provider, models)
|
||||
|
||||
|
|
|
|||
143
src/api/providers/model-lookup.ts
Normal file
143
src/api/providers/model-lookup.ts
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import type { ModelInfo, ProviderName } from "@roo-code/types"
|
||||
|
||||
import {
|
||||
getModelsForStaticProvider,
|
||||
getStaticProviderNames,
|
||||
isStaticProvider,
|
||||
} from "../../services/custom-models/static-providers"
|
||||
import { getWorkspacePath } from "../../utils/path"
|
||||
|
||||
/**
|
||||
* Cache for merged static provider models
|
||||
* Pre-loaded during extension activation for synchronous access
|
||||
*/
|
||||
const mergedModelsCache = new Map<ProviderName, Record<string, ModelInfo>>()
|
||||
let isPreloaded = false
|
||||
|
||||
/**
|
||||
* Get models for a provider, including custom models from .roo/models/
|
||||
* This works for both static providers (anthropic, bedrock, etc.) and is used
|
||||
* to look up model information in provider handlers.
|
||||
*
|
||||
* @param provider The provider name
|
||||
* @param staticModels The base static models dictionary
|
||||
* @returns Promise of merged models (static + custom)
|
||||
*/
|
||||
export async function getProviderModels(
|
||||
provider: ProviderName,
|
||||
staticModels: Record<string, ModelInfo>,
|
||||
): Promise<Record<string, ModelInfo>> {
|
||||
// Check cache first
|
||||
if (mergedModelsCache.has(provider)) {
|
||||
return mergedModelsCache.get(provider)!
|
||||
}
|
||||
|
||||
try {
|
||||
const cwd = getWorkspacePath()
|
||||
const mergedModels = await getModelsForStaticProvider(provider, cwd)
|
||||
|
||||
// Cache the result
|
||||
mergedModelsCache.set(provider, mergedModels)
|
||||
|
||||
return mergedModels
|
||||
} catch (error) {
|
||||
console.error(`[ModelLookup] Error loading custom models for ${provider}:`, error)
|
||||
// Fallback to static models only
|
||||
return staticModels
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-load custom models for all static providers
|
||||
* Should be called during extension activation
|
||||
* @param cwd Current working directory
|
||||
*/
|
||||
export async function preloadStaticProviderModels(cwd: string): Promise<void> {
|
||||
try {
|
||||
// Get all static provider names from the source of truth
|
||||
const staticProviders = getStaticProviderNames()
|
||||
|
||||
// Load custom models for each static provider
|
||||
await Promise.all(
|
||||
staticProviders.map(async (provider) => {
|
||||
try {
|
||||
const models = await getModelsForStaticProvider(provider, cwd)
|
||||
mergedModelsCache.set(provider, models)
|
||||
} catch (error) {
|
||||
console.error(`[ModelLookup] Error preloading custom models for ${provider}:`, error)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
isPreloaded = true
|
||||
console.log(`[ModelLookup] Preloaded custom models for ${staticProviders.length} static providers`)
|
||||
} catch (error) {
|
||||
console.error("[ModelLookup] Error during preload:", error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get models synchronously for a provider (uses pre-loaded cache)
|
||||
* Falls back to static models if not pre-loaded or provider not found
|
||||
* @param provider The provider name
|
||||
* @param staticModels The base static models dictionary
|
||||
* @returns Merged models (static + custom)
|
||||
*/
|
||||
export function getProviderModelsSync(
|
||||
provider: ProviderName,
|
||||
staticModels: Record<string, ModelInfo>,
|
||||
): Record<string, ModelInfo> {
|
||||
if (!isPreloaded || !mergedModelsCache.has(provider)) {
|
||||
// Not preloaded yet or not in cache, return static models only
|
||||
return staticModels
|
||||
}
|
||||
|
||||
return mergedModelsCache.get(provider)!
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cache for a specific provider or all providers
|
||||
* @param provider Optional provider to clear cache for. If not provided, clears all.
|
||||
*/
|
||||
export function clearProviderModelsCache(provider?: ProviderName): void {
|
||||
if (provider) {
|
||||
mergedModelsCache.delete(provider)
|
||||
} else {
|
||||
mergedModelsCache.clear()
|
||||
isPreloaded = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a model ID exists in the provider's models (including custom models)
|
||||
* @param provider The provider name
|
||||
* @param modelId The model ID to check
|
||||
* @param staticModels The base static models dictionary
|
||||
* @returns Promise of boolean indicating if model exists
|
||||
*/
|
||||
export async function hasModel(
|
||||
provider: ProviderName,
|
||||
modelId: string,
|
||||
staticModels: Record<string, ModelInfo>,
|
||||
): Promise<boolean> {
|
||||
const models = await getProviderModels(provider, staticModels)
|
||||
return modelId in models
|
||||
}
|
||||
|
||||
export { isPreloaded }
|
||||
|
||||
/**
|
||||
* Get model info for a specific model ID
|
||||
* @param provider The provider name
|
||||
* @param modelId The model ID
|
||||
* @param staticModels The base static models dictionary
|
||||
* @returns Promise of ModelInfo or undefined if not found
|
||||
*/
|
||||
export async function getModelInfo(
|
||||
provider: ProviderName,
|
||||
modelId: string,
|
||||
staticModels: Record<string, ModelInfo>,
|
||||
): Promise<ModelInfo | undefined> {
|
||||
const models = await getProviderModels(provider, staticModels)
|
||||
return models[modelId]
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ import { calculateApiCostOpenAI } from "../../shared/cost"
|
|||
|
||||
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
|
||||
import { getModelParams } from "../transform/model-params"
|
||||
import { getProviderModelsSync } from "./model-lookup"
|
||||
|
||||
import { BaseProvider } from "./base-provider"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
|
|
@ -1220,11 +1221,11 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
|
||||
override getModel() {
|
||||
const modelId = this.options.apiModelId
|
||||
const models = getProviderModelsSync("openai-native", openAiNativeModels as Record<string, ModelInfo>)
|
||||
|
||||
let id =
|
||||
modelId && modelId in openAiNativeModels ? (modelId as OpenAiNativeModelId) : openAiNativeDefaultModelId
|
||||
let id = modelId && modelId in models ? (modelId as OpenAiNativeModelId) : openAiNativeDefaultModelId
|
||||
|
||||
const info: ModelInfo = openAiNativeModels[id]
|
||||
const info: ModelInfo = models[id]
|
||||
|
||||
const params = getModelParams({
|
||||
format: "openai",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import OpenAI from "openai"
|
||||
|
||||
import type { ModelInfo } from "@roo-code/types"
|
||||
import type { ModelInfo, DynamicProvider, LocalProvider } from "@roo-code/types"
|
||||
|
||||
import { ApiHandlerOptions, RouterName, ModelRecord } from "../../shared/api"
|
||||
import { ApiHandlerOptions, ModelRecord } from "../../shared/api"
|
||||
|
||||
import { BaseProvider } from "./base-provider"
|
||||
import { getModels } from "./fetchers/modelCache"
|
||||
|
|
@ -10,7 +10,7 @@ import { getModels } from "./fetchers/modelCache"
|
|||
import { DEFAULT_HEADERS } from "./constants"
|
||||
|
||||
type RouterProviderOptions = {
|
||||
name: RouterName
|
||||
name: DynamicProvider | LocalProvider
|
||||
baseURL: string
|
||||
apiKey?: string
|
||||
modelId?: string
|
||||
|
|
@ -21,7 +21,7 @@ type RouterProviderOptions = {
|
|||
|
||||
export abstract class RouterProvider extends BaseProvider {
|
||||
protected readonly options: ApiHandlerOptions
|
||||
protected readonly name: RouterName
|
||||
protected readonly name: DynamicProvider | LocalProvider
|
||||
protected models: ModelRecord = {}
|
||||
protected readonly modelId?: string
|
||||
protected readonly defaultModelId: string
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { type ModelInfo, type VertexModelId, vertexDefaultModelId, vertexModels
|
|||
import type { ApiHandlerOptions } from "../../shared/api"
|
||||
|
||||
import { getModelParams } from "../transform/model-params"
|
||||
import { getProviderModelsSync } from "./model-lookup"
|
||||
|
||||
import { GeminiHandler } from "./gemini"
|
||||
import { SingleCompletionHandler } from "../index"
|
||||
|
|
@ -14,8 +15,9 @@ export class VertexHandler extends GeminiHandler implements SingleCompletionHand
|
|||
|
||||
override getModel() {
|
||||
const modelId = this.options.apiModelId
|
||||
let id = modelId && modelId in vertexModels ? (modelId as VertexModelId) : vertexDefaultModelId
|
||||
const info: ModelInfo = vertexModels[id]
|
||||
const models = getProviderModelsSync("vertex", vertexModels as Record<string, ModelInfo>)
|
||||
let id = modelId && modelId in models ? (modelId as VertexModelId) : vertexDefaultModelId
|
||||
const info: ModelInfo = models[id]
|
||||
const params = getModelParams({ format: "gemini", modelId: id, model: info, settings: this.options })
|
||||
|
||||
// The `:thinking` suffix indicates that the model is a "Hybrid"
|
||||
|
|
|
|||
|
|
@ -2703,26 +2703,49 @@ describe("ClineProvider - Router Models", () => {
|
|||
})
|
||||
expect(getModels).toHaveBeenCalledWith({ provider: "chutes" })
|
||||
|
||||
// Verify response was sent
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
type: "routerModels",
|
||||
routerModels: {
|
||||
deepinfra: mockModels,
|
||||
openrouter: mockModels,
|
||||
requesty: mockModels,
|
||||
glama: mockModels,
|
||||
unbound: mockModels,
|
||||
roo: mockModels,
|
||||
chutes: mockModels,
|
||||
litellm: mockModels,
|
||||
ollama: {},
|
||||
lmstudio: {},
|
||||
"vercel-ai-gateway": mockModels,
|
||||
huggingface: {},
|
||||
"io-intelligence": {},
|
||||
},
|
||||
values: undefined,
|
||||
})
|
||||
// Verify response was sent with static providers containing their actual model dictionaries
|
||||
expect(mockPostMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: "routerModels",
|
||||
routerModels: expect.objectContaining({
|
||||
deepinfra: mockModels,
|
||||
openrouter: mockModels,
|
||||
requesty: mockModels,
|
||||
glama: mockModels,
|
||||
unbound: mockModels,
|
||||
roo: mockModels,
|
||||
chutes: mockModels,
|
||||
litellm: mockModels,
|
||||
ollama: {},
|
||||
lmstudio: {},
|
||||
"vercel-ai-gateway": mockModels,
|
||||
huggingface: {},
|
||||
"io-intelligence": {},
|
||||
// Static providers will have their actual model dictionaries
|
||||
anthropic: expect.any(Object),
|
||||
bedrock: expect.any(Object),
|
||||
cerebras: expect.any(Object),
|
||||
"claude-code": expect.any(Object),
|
||||
deepseek: expect.any(Object),
|
||||
doubao: expect.any(Object),
|
||||
featherless: expect.any(Object),
|
||||
fireworks: expect.any(Object),
|
||||
gemini: expect.any(Object),
|
||||
groq: expect.any(Object),
|
||||
minimax: expect.any(Object),
|
||||
mistral: expect.any(Object),
|
||||
moonshot: expect.any(Object),
|
||||
"openai-native": expect.any(Object),
|
||||
"qwen-code": expect.any(Object),
|
||||
sambanova: expect.any(Object),
|
||||
vertex: expect.any(Object),
|
||||
"vscode-lm": expect.any(Object),
|
||||
xai: expect.any(Object),
|
||||
zai: expect.any(Object),
|
||||
}),
|
||||
values: undefined,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
test("handles requestRouterModels with individual provider failures", async () => {
|
||||
|
|
@ -2760,25 +2783,48 @@ describe("ClineProvider - Router Models", () => {
|
|||
await messageHandler({ type: "requestRouterModels" })
|
||||
|
||||
// Verify main response includes successful providers and empty objects for failed ones
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
type: "routerModels",
|
||||
routerModels: {
|
||||
deepinfra: mockModels,
|
||||
openrouter: mockModels,
|
||||
requesty: {},
|
||||
glama: mockModels,
|
||||
unbound: {},
|
||||
roo: mockModels,
|
||||
chutes: {},
|
||||
ollama: {},
|
||||
lmstudio: {},
|
||||
litellm: {},
|
||||
"vercel-ai-gateway": mockModels,
|
||||
huggingface: {},
|
||||
"io-intelligence": {},
|
||||
},
|
||||
values: undefined,
|
||||
})
|
||||
expect(mockPostMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: "routerModels",
|
||||
routerModels: expect.objectContaining({
|
||||
deepinfra: mockModels,
|
||||
openrouter: mockModels,
|
||||
requesty: {},
|
||||
glama: mockModels,
|
||||
unbound: {},
|
||||
roo: mockModels,
|
||||
chutes: {},
|
||||
ollama: {},
|
||||
lmstudio: {},
|
||||
litellm: {},
|
||||
"vercel-ai-gateway": mockModels,
|
||||
huggingface: {},
|
||||
"io-intelligence": {},
|
||||
// Static providers will have their actual model dictionaries
|
||||
anthropic: expect.any(Object),
|
||||
bedrock: expect.any(Object),
|
||||
cerebras: expect.any(Object),
|
||||
"claude-code": expect.any(Object),
|
||||
deepseek: expect.any(Object),
|
||||
doubao: expect.any(Object),
|
||||
featherless: expect.any(Object),
|
||||
fireworks: expect.any(Object),
|
||||
gemini: expect.any(Object),
|
||||
groq: expect.any(Object),
|
||||
minimax: expect.any(Object),
|
||||
mistral: expect.any(Object),
|
||||
moonshot: expect.any(Object),
|
||||
"openai-native": expect.any(Object),
|
||||
"qwen-code": expect.any(Object),
|
||||
sambanova: expect.any(Object),
|
||||
vertex: expect.any(Object),
|
||||
"vscode-lm": expect.any(Object),
|
||||
xai: expect.any(Object),
|
||||
zai: expect.any(Object),
|
||||
}),
|
||||
values: undefined,
|
||||
}),
|
||||
)
|
||||
|
||||
// Verify error messages were sent for failed providers
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
|
|
@ -2884,25 +2930,48 @@ describe("ClineProvider - Router Models", () => {
|
|||
)
|
||||
|
||||
// Verify response includes empty object for LiteLLM
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
type: "routerModels",
|
||||
routerModels: {
|
||||
deepinfra: mockModels,
|
||||
openrouter: mockModels,
|
||||
requesty: mockModels,
|
||||
glama: mockModels,
|
||||
unbound: mockModels,
|
||||
roo: mockModels,
|
||||
chutes: mockModels,
|
||||
litellm: {},
|
||||
ollama: {},
|
||||
lmstudio: {},
|
||||
"vercel-ai-gateway": mockModels,
|
||||
huggingface: {},
|
||||
"io-intelligence": {},
|
||||
},
|
||||
values: undefined,
|
||||
})
|
||||
expect(mockPostMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: "routerModels",
|
||||
routerModels: expect.objectContaining({
|
||||
deepinfra: mockModels,
|
||||
openrouter: mockModels,
|
||||
requesty: mockModels,
|
||||
glama: mockModels,
|
||||
unbound: mockModels,
|
||||
roo: mockModels,
|
||||
chutes: mockModels,
|
||||
litellm: {},
|
||||
ollama: {},
|
||||
lmstudio: {},
|
||||
"vercel-ai-gateway": mockModels,
|
||||
huggingface: {},
|
||||
"io-intelligence": {},
|
||||
// Static providers will have their actual model dictionaries
|
||||
anthropic: expect.any(Object),
|
||||
bedrock: expect.any(Object),
|
||||
cerebras: expect.any(Object),
|
||||
"claude-code": expect.any(Object),
|
||||
deepseek: expect.any(Object),
|
||||
doubao: expect.any(Object),
|
||||
featherless: expect.any(Object),
|
||||
fireworks: expect.any(Object),
|
||||
gemini: expect.any(Object),
|
||||
groq: expect.any(Object),
|
||||
minimax: expect.any(Object),
|
||||
mistral: expect.any(Object),
|
||||
moonshot: expect.any(Object),
|
||||
"openai-native": expect.any(Object),
|
||||
"qwen-code": expect.any(Object),
|
||||
sambanova: expect.any(Object),
|
||||
vertex: expect.any(Object),
|
||||
"vscode-lm": expect.any(Object),
|
||||
xai: expect.any(Object),
|
||||
zai: expect.any(Object),
|
||||
}),
|
||||
values: undefined,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
test("handles requestLmStudioModels with proper response", async () => {
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ describe("webviewMessageHandler - requestRouterModels provider filter", () => {
|
|||
// Only methods used by this code path
|
||||
postMessageToWebview: vi.fn(),
|
||||
getState: vi.fn().mockResolvedValue({ apiConfiguration: {} }),
|
||||
getCurrentTask: vi.fn().mockReturnValue({ cwd: "/mock/workspace" }),
|
||||
cwd: "/mock/workspace",
|
||||
contextProxy: {
|
||||
getValue: vi.fn(),
|
||||
setValue: vi.fn(),
|
||||
|
|
@ -112,14 +114,18 @@ describe("webviewMessageHandler - requestRouterModels provider filter", () => {
|
|||
const payload = call[0]
|
||||
const routerModels = payload.routerModels as Record<string, Record<string, any>>
|
||||
|
||||
// Only "roo" key should be present
|
||||
const keys = Object.keys(routerModels)
|
||||
expect(keys).toEqual(["roo"])
|
||||
// Verify "roo" is present with its models
|
||||
expect(Object.keys(routerModels.roo || {})).toContain("roo/sonnet")
|
||||
|
||||
// getModels should have been called exactly once for roo
|
||||
// getModels should have been called exactly once for roo (dynamic provider)
|
||||
const providersCalled = getModelsMock.mock.calls.map((c: any[]) => c[0]?.provider)
|
||||
expect(providersCalled).toEqual(["roo"])
|
||||
|
||||
// Static providers should also be present (they're always loaded)
|
||||
const keys = Object.keys(routerModels)
|
||||
expect(keys).toContain("roo") // The requested provider
|
||||
expect(keys).toContain("anthropic") // Static providers are always included
|
||||
expect(keys.length).toBeGreaterThan(1) // More than just the requested provider
|
||||
})
|
||||
|
||||
it("defaults to aggregate fetching when no provider filter is sent", async () => {
|
||||
|
|
@ -156,12 +162,18 @@ describe("webviewMessageHandler - requestRouterModels provider filter", () => {
|
|||
)
|
||||
expect(call).toBeTruthy()
|
||||
const routerModels = call[0].routerModels as Record<string, Record<string, any>>
|
||||
const keys = Object.keys(routerModels)
|
||||
|
||||
expect(keys).toEqual(["openrouter"])
|
||||
// Verify "openrouter" is present with its models
|
||||
expect(Object.keys(routerModels.openrouter || {})).toContain("openrouter/qwen2.5")
|
||||
|
||||
// getModels should have been called exactly once for openrouter (dynamic provider)
|
||||
const providersCalled = getModelsMock.mock.calls.map((c: any[]) => c[0]?.provider)
|
||||
expect(providersCalled).toEqual(["openrouter"])
|
||||
|
||||
// Static providers should also be present (they're always loaded)
|
||||
const keys = Object.keys(routerModels)
|
||||
expect(keys).toContain("openrouter") // The requested provider
|
||||
expect(keys).toContain("anthropic") // Static providers are always included
|
||||
expect(keys.length).toBeGreaterThan(1) // More than just the requested provider
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -238,26 +238,49 @@ describe("webviewMessageHandler - requestRouterModels", () => {
|
|||
// Note: huggingface is not fetched in requestRouterModels - it has its own handler
|
||||
// Note: io-intelligence is not fetched because no API key is provided in the mock state
|
||||
|
||||
// Verify response was sent
|
||||
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
|
||||
type: "routerModels",
|
||||
routerModels: {
|
||||
deepinfra: mockModels,
|
||||
openrouter: mockModels,
|
||||
requesty: mockModels,
|
||||
glama: mockModels,
|
||||
unbound: mockModels,
|
||||
litellm: mockModels,
|
||||
roo: mockModels,
|
||||
chutes: mockModels,
|
||||
ollama: {},
|
||||
lmstudio: {},
|
||||
"vercel-ai-gateway": mockModels,
|
||||
huggingface: {},
|
||||
"io-intelligence": {},
|
||||
},
|
||||
values: undefined,
|
||||
})
|
||||
// Verify response was sent with static providers containing their actual model dictionaries
|
||||
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: "routerModels",
|
||||
routerModels: expect.objectContaining({
|
||||
deepinfra: mockModels,
|
||||
openrouter: mockModels,
|
||||
requesty: mockModels,
|
||||
glama: mockModels,
|
||||
unbound: mockModels,
|
||||
litellm: mockModels,
|
||||
roo: mockModels,
|
||||
chutes: mockModels,
|
||||
ollama: {},
|
||||
lmstudio: {},
|
||||
"vercel-ai-gateway": mockModels,
|
||||
huggingface: {},
|
||||
"io-intelligence": {},
|
||||
// Static providers will have their actual model dictionaries
|
||||
anthropic: expect.any(Object),
|
||||
bedrock: expect.any(Object),
|
||||
cerebras: expect.any(Object),
|
||||
"claude-code": expect.any(Object),
|
||||
deepseek: expect.any(Object),
|
||||
doubao: expect.any(Object),
|
||||
featherless: expect.any(Object),
|
||||
fireworks: expect.any(Object),
|
||||
gemini: expect.any(Object),
|
||||
groq: expect.any(Object),
|
||||
minimax: expect.any(Object),
|
||||
mistral: expect.any(Object),
|
||||
moonshot: expect.any(Object),
|
||||
"openai-native": expect.any(Object),
|
||||
"qwen-code": expect.any(Object),
|
||||
sambanova: expect.any(Object),
|
||||
vertex: expect.any(Object),
|
||||
"vscode-lm": expect.any(Object),
|
||||
xai: expect.any(Object),
|
||||
zai: expect.any(Object),
|
||||
}),
|
||||
values: undefined,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("handles LiteLLM models with values from message when config is missing", async () => {
|
||||
|
|
@ -333,25 +356,48 @@ describe("webviewMessageHandler - requestRouterModels", () => {
|
|||
)
|
||||
|
||||
// Verify response includes empty object for LiteLLM
|
||||
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
|
||||
type: "routerModels",
|
||||
routerModels: {
|
||||
deepinfra: mockModels,
|
||||
openrouter: mockModels,
|
||||
requesty: mockModels,
|
||||
glama: mockModels,
|
||||
unbound: mockModels,
|
||||
roo: mockModels,
|
||||
chutes: mockModels,
|
||||
litellm: {},
|
||||
ollama: {},
|
||||
lmstudio: {},
|
||||
"vercel-ai-gateway": mockModels,
|
||||
huggingface: {},
|
||||
"io-intelligence": {},
|
||||
},
|
||||
values: undefined,
|
||||
})
|
||||
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: "routerModels",
|
||||
routerModels: expect.objectContaining({
|
||||
deepinfra: mockModels,
|
||||
openrouter: mockModels,
|
||||
requesty: mockModels,
|
||||
glama: mockModels,
|
||||
unbound: mockModels,
|
||||
roo: mockModels,
|
||||
chutes: mockModels,
|
||||
litellm: {},
|
||||
ollama: {},
|
||||
lmstudio: {},
|
||||
"vercel-ai-gateway": mockModels,
|
||||
huggingface: {},
|
||||
"io-intelligence": {},
|
||||
// Static providers will have their actual model dictionaries
|
||||
anthropic: expect.any(Object),
|
||||
bedrock: expect.any(Object),
|
||||
cerebras: expect.any(Object),
|
||||
"claude-code": expect.any(Object),
|
||||
deepseek: expect.any(Object),
|
||||
doubao: expect.any(Object),
|
||||
featherless: expect.any(Object),
|
||||
fireworks: expect.any(Object),
|
||||
gemini: expect.any(Object),
|
||||
groq: expect.any(Object),
|
||||
minimax: expect.any(Object),
|
||||
mistral: expect.any(Object),
|
||||
moonshot: expect.any(Object),
|
||||
"openai-native": expect.any(Object),
|
||||
"qwen-code": expect.any(Object),
|
||||
sambanova: expect.any(Object),
|
||||
vertex: expect.any(Object),
|
||||
"vscode-lm": expect.any(Object),
|
||||
xai: expect.any(Object),
|
||||
zai: expect.any(Object),
|
||||
}),
|
||||
values: undefined,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("handles individual provider failures gracefully", async () => {
|
||||
|
|
@ -426,6 +472,27 @@ describe("webviewMessageHandler - requestRouterModels", () => {
|
|||
"vercel-ai-gateway": mockModels,
|
||||
huggingface: {},
|
||||
"io-intelligence": {},
|
||||
// Static providers
|
||||
anthropic: {},
|
||||
bedrock: {},
|
||||
cerebras: {},
|
||||
"claude-code": {},
|
||||
deepseek: {},
|
||||
doubao: {},
|
||||
featherless: {},
|
||||
fireworks: {},
|
||||
gemini: {},
|
||||
groq: {},
|
||||
minimax: {},
|
||||
mistral: {},
|
||||
moonshot: {},
|
||||
"openai-native": {},
|
||||
"qwen-code": {},
|
||||
sambanova: {},
|
||||
vertex: {},
|
||||
"vscode-lm": {},
|
||||
xai: {},
|
||||
zai: {},
|
||||
},
|
||||
values: undefined,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -24,8 +24,9 @@ import { ClineProvider } from "./ClineProvider"
|
|||
import { handleCheckpointRestoreOperation } from "./checkpointRestoreHandler"
|
||||
import { changeLanguage, t } from "../../i18n"
|
||||
import { Package } from "../../shared/package"
|
||||
import { type RouterName, type ModelRecord, toRouterName } from "../../shared/api"
|
||||
import { type RouterName, type ModelRecord, toRouterName, staticProvidersWithCustomModels } from "../../shared/api"
|
||||
import { MessageEnhancer } from "./messageEnhancer"
|
||||
import { getModelsForStaticProvider } from "../../services/custom-models/static-providers"
|
||||
|
||||
import {
|
||||
type WebviewMessage,
|
||||
|
|
@ -764,6 +765,7 @@ export const webviewMessageHandler = async (
|
|||
const routerModels: Record<RouterName, ModelRecord> = providerFilter
|
||||
? ({} as Record<RouterName, ModelRecord>)
|
||||
: {
|
||||
// Dynamic providers
|
||||
openrouter: {},
|
||||
"vercel-ai-gateway": {},
|
||||
huggingface: {},
|
||||
|
|
@ -773,10 +775,32 @@ export const webviewMessageHandler = async (
|
|||
requesty: {},
|
||||
unbound: {},
|
||||
glama: {},
|
||||
ollama: {},
|
||||
lmstudio: {},
|
||||
roo: {},
|
||||
chutes: {},
|
||||
// Local providers
|
||||
ollama: {},
|
||||
lmstudio: {},
|
||||
// Static providers (for custom models support)
|
||||
anthropic: {},
|
||||
bedrock: {},
|
||||
vertex: {},
|
||||
gemini: {},
|
||||
"openai-native": {},
|
||||
mistral: {},
|
||||
deepseek: {},
|
||||
doubao: {},
|
||||
moonshot: {},
|
||||
minimax: {},
|
||||
xai: {},
|
||||
groq: {},
|
||||
cerebras: {},
|
||||
sambanova: {},
|
||||
fireworks: {},
|
||||
featherless: {},
|
||||
"qwen-code": {},
|
||||
"claude-code": {},
|
||||
zai: {},
|
||||
"vscode-lm": {},
|
||||
}
|
||||
|
||||
const safeGetModels = async (options: GetModelsOptions): Promise<ModelRecord> => {
|
||||
|
|
@ -884,6 +908,25 @@ export const webviewMessageHandler = async (
|
|||
}
|
||||
})
|
||||
|
||||
// Fetch static provider models (includes custom models from .roo/models/)
|
||||
const cwd = getCurrentCwd()
|
||||
const staticProviderPromises = staticProvidersWithCustomModels.map(async (staticProvider) => {
|
||||
try {
|
||||
const models = await getModelsForStaticProvider(staticProvider, cwd)
|
||||
return { provider: staticProvider, models }
|
||||
} catch (error) {
|
||||
console.error(`Error fetching static provider models for ${staticProvider}:`, error)
|
||||
return { provider: staticProvider, models: {} as ModelRecord }
|
||||
}
|
||||
})
|
||||
|
||||
const staticResults = await Promise.allSettled(staticProviderPromises)
|
||||
staticResults.forEach((result) => {
|
||||
if (result.status === "fulfilled") {
|
||||
routerModels[result.value.provider] = result.value.models
|
||||
}
|
||||
})
|
||||
|
||||
provider.postMessageToWebview({
|
||||
type: "routerModels",
|
||||
routerModels,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ import { CodeIndexManager } from "./services/code-index/manager"
|
|||
import { MdmService } from "./services/mdm/MdmService"
|
||||
import { migrateSettings } from "./utils/migrateSettings"
|
||||
import { autoImportSettings } from "./utils/autoImportSettings"
|
||||
import { setupCustomModelsWatcher } from "./services/custom-models/watcher"
|
||||
import { preloadStaticProviderModels } from "./api/providers/model-lookup"
|
||||
import { getWorkspacePath } from "./utils/path"
|
||||
import { API } from "./extension/api"
|
||||
|
||||
import {
|
||||
|
|
@ -254,6 +257,27 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||
)
|
||||
}
|
||||
|
||||
// Preload custom models for static providers
|
||||
try {
|
||||
await preloadStaticProviderModels(getWorkspacePath())
|
||||
outputChannel.appendLine("[CustomModels] Static provider models preloaded")
|
||||
} catch (error) {
|
||||
outputChannel.appendLine(
|
||||
`[CustomModels] Failed to preload static provider models: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
|
||||
// Setup custom models file watcher for hot-reloading
|
||||
try {
|
||||
const customModelsWatcher = setupCustomModelsWatcher(getWorkspacePath())
|
||||
context.subscriptions.push(customModelsWatcher)
|
||||
outputChannel.appendLine("[CustomModels] File watcher initialized")
|
||||
} catch (error) {
|
||||
outputChannel.appendLine(
|
||||
`[CustomModels] Failed to setup file watcher: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
|
||||
registerCommands({ context, outputChannel, provider })
|
||||
|
||||
/**
|
||||
|
|
|
|||
261
src/services/custom-models/__tests__/index.spec.ts
Normal file
261
src/services/custom-models/__tests__/index.spec.ts
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
|
||||
import * as path from "path"
|
||||
|
||||
// Use vi.hoisted to ensure mocks are available during hoisting
|
||||
const { mockReadFile, mockFileExists, mockGetGlobalRooDirectory, mockGetProjectRooDirectoryForCwd } = vi.hoisted(
|
||||
() => ({
|
||||
mockReadFile: vi.fn(),
|
||||
mockFileExists: vi.fn(),
|
||||
mockGetGlobalRooDirectory: vi.fn(),
|
||||
mockGetProjectRooDirectoryForCwd: vi.fn(),
|
||||
}),
|
||||
)
|
||||
|
||||
// Mock fs/promises module
|
||||
vi.mock("fs/promises", () => ({
|
||||
default: {
|
||||
readFile: mockReadFile,
|
||||
},
|
||||
readFile: mockReadFile,
|
||||
}))
|
||||
|
||||
// Mock the roo-config module
|
||||
vi.mock("../../roo-config", () => ({
|
||||
getGlobalRooDirectory: mockGetGlobalRooDirectory,
|
||||
getProjectRooDirectoryForCwd: mockGetProjectRooDirectoryForCwd,
|
||||
fileExists: mockFileExists,
|
||||
}))
|
||||
|
||||
// Import after mocks
|
||||
import { getCustomModelsForProvider } from "../index"
|
||||
|
||||
describe("getCustomModelsForProvider", () => {
|
||||
const mockCwd = "/test/workspace"
|
||||
const mockGlobalDir = "/home/user/.roo"
|
||||
const mockProjectDir = "/test/workspace/.roo"
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockGetGlobalRooDirectory.mockReturnValue(mockGlobalDir)
|
||||
mockGetProjectRooDirectoryForCwd.mockReturnValue(mockProjectDir)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it("should return empty object when no files exist", async () => {
|
||||
mockFileExists.mockResolvedValue(false)
|
||||
|
||||
const result = await getCustomModelsForProvider("openrouter", mockCwd)
|
||||
|
||||
expect(result).toEqual({})
|
||||
expect(mockFileExists).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it("should load models from global file only", async () => {
|
||||
const globalModels = {
|
||||
"custom/model-1": {
|
||||
contextWindow: 32000,
|
||||
maxTokens: 4096,
|
||||
supportsPromptCache: false,
|
||||
description: "Global model",
|
||||
},
|
||||
}
|
||||
|
||||
mockFileExists.mockImplementation(async (filePath: string) => {
|
||||
return filePath === path.join(mockGlobalDir, "models", "openrouter.json")
|
||||
})
|
||||
|
||||
mockReadFile.mockImplementation(async (filePath: any) => {
|
||||
if (filePath === path.join(mockGlobalDir, "models", "openrouter.json")) {
|
||||
return JSON.stringify(globalModels)
|
||||
}
|
||||
throw new Error("File not found")
|
||||
})
|
||||
|
||||
const result = await getCustomModelsForProvider("openrouter", mockCwd)
|
||||
|
||||
expect(result).toEqual(globalModels)
|
||||
})
|
||||
|
||||
it("should load models from project file only", async () => {
|
||||
const projectModels = {
|
||||
"custom/project-model": {
|
||||
contextWindow: 64000,
|
||||
maxTokens: 8192,
|
||||
supportsPromptCache: false,
|
||||
description: "Project model",
|
||||
},
|
||||
}
|
||||
|
||||
mockFileExists.mockImplementation(async (filePath: string) => {
|
||||
return filePath === path.join(mockProjectDir, "models", "openrouter.json")
|
||||
})
|
||||
|
||||
mockReadFile.mockImplementation(async (filePath: any) => {
|
||||
if (filePath === path.join(mockProjectDir, "models", "openrouter.json")) {
|
||||
return JSON.stringify(projectModels)
|
||||
}
|
||||
throw new Error("File not found")
|
||||
})
|
||||
|
||||
const result = await getCustomModelsForProvider("openrouter", mockCwd)
|
||||
|
||||
expect(result).toEqual(projectModels)
|
||||
})
|
||||
|
||||
it("should merge global and project models with project overriding global", async () => {
|
||||
const globalModels = {
|
||||
"custom/model-1": {
|
||||
contextWindow: 32000,
|
||||
maxTokens: 4096,
|
||||
supportsPromptCache: false,
|
||||
description: "Global model 1",
|
||||
},
|
||||
"custom/model-2": {
|
||||
contextWindow: 64000,
|
||||
maxTokens: 8192,
|
||||
supportsPromptCache: false,
|
||||
description: "Global model 2",
|
||||
},
|
||||
}
|
||||
|
||||
const projectModels = {
|
||||
"custom/model-1": {
|
||||
contextWindow: 128000,
|
||||
maxTokens: 16384,
|
||||
supportsPromptCache: true,
|
||||
description: "Project override for model 1",
|
||||
},
|
||||
"custom/model-3": {
|
||||
contextWindow: 16000,
|
||||
maxTokens: 2048,
|
||||
supportsPromptCache: false,
|
||||
description: "Project-only model",
|
||||
},
|
||||
}
|
||||
|
||||
mockFileExists.mockResolvedValue(true)
|
||||
|
||||
mockReadFile.mockImplementation(async (filePath: any) => {
|
||||
if (filePath === path.join(mockGlobalDir, "models", "openrouter.json")) {
|
||||
return JSON.stringify(globalModels)
|
||||
}
|
||||
if (filePath === path.join(mockProjectDir, "models", "openrouter.json")) {
|
||||
return JSON.stringify(projectModels)
|
||||
}
|
||||
throw new Error("File not found")
|
||||
})
|
||||
|
||||
const result = await getCustomModelsForProvider("openrouter", mockCwd)
|
||||
|
||||
expect(result).toEqual({
|
||||
"custom/model-1": projectModels["custom/model-1"], // Project overrides
|
||||
"custom/model-2": globalModels["custom/model-2"], // From global
|
||||
"custom/model-3": projectModels["custom/model-3"], // From project
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle invalid JSON gracefully", async () => {
|
||||
mockFileExists.mockResolvedValue(true)
|
||||
mockReadFile.mockResolvedValue("invalid json {")
|
||||
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {})
|
||||
|
||||
const result = await getCustomModelsForProvider("openrouter", mockCwd)
|
||||
|
||||
expect(result).toEqual({})
|
||||
expect(consoleSpy).toHaveBeenCalled()
|
||||
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
|
||||
it("should handle invalid schema gracefully", async () => {
|
||||
const invalidModels = {
|
||||
"custom/model-1": {
|
||||
// Missing required contextWindow
|
||||
maxTokens: 4096,
|
||||
},
|
||||
}
|
||||
|
||||
mockFileExists.mockResolvedValue(true)
|
||||
mockReadFile.mockResolvedValue(JSON.stringify(invalidModels))
|
||||
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {})
|
||||
|
||||
const result = await getCustomModelsForProvider("openrouter", mockCwd)
|
||||
|
||||
expect(result).toEqual({})
|
||||
expect(consoleSpy).toHaveBeenCalled()
|
||||
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
|
||||
it("should work with different providers", async () => {
|
||||
const glamaModels = {
|
||||
"custom-glama": {
|
||||
contextWindow: 200000,
|
||||
maxTokens: 8192,
|
||||
supportsPromptCache: true,
|
||||
description: "Custom Glama",
|
||||
},
|
||||
}
|
||||
|
||||
mockFileExists.mockImplementation(async (filePath: string) => {
|
||||
return filePath === path.join(mockGlobalDir, "models", "glama.json")
|
||||
})
|
||||
|
||||
mockReadFile.mockImplementation(async (filePath: any) => {
|
||||
if (filePath === path.join(mockGlobalDir, "models", "glama.json")) {
|
||||
return JSON.stringify(glamaModels)
|
||||
}
|
||||
throw new Error("File not found")
|
||||
})
|
||||
|
||||
const result = await getCustomModelsForProvider("glama", mockCwd)
|
||||
|
||||
expect(result).toEqual(glamaModels)
|
||||
})
|
||||
|
||||
it("should validate all ModelInfo fields", async () => {
|
||||
const completeModel = {
|
||||
"complete/model": {
|
||||
contextWindow: 32000,
|
||||
maxTokens: 4096,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0.001,
|
||||
outputPrice: 0.002,
|
||||
cacheWritesPrice: 0.0005,
|
||||
cacheReadsPrice: 0.0001,
|
||||
description: "Complete model with all fields",
|
||||
supportsReasoningEffort: true,
|
||||
supportsReasoningBudget: true,
|
||||
requiredReasoningBudget: false,
|
||||
reasoningEffort: "high",
|
||||
},
|
||||
}
|
||||
|
||||
mockFileExists.mockResolvedValue(true)
|
||||
mockReadFile.mockResolvedValue(JSON.stringify(completeModel))
|
||||
|
||||
const result = await getCustomModelsForProvider("openrouter", mockCwd)
|
||||
|
||||
expect(result).toEqual(completeModel)
|
||||
})
|
||||
|
||||
it("should handle file read errors gracefully", async () => {
|
||||
mockFileExists.mockResolvedValue(true)
|
||||
mockReadFile.mockRejectedValue(new Error("Permission denied"))
|
||||
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {})
|
||||
|
||||
const result = await getCustomModelsForProvider("openrouter", mockCwd)
|
||||
|
||||
expect(result).toEqual({})
|
||||
expect(consoleSpy).toHaveBeenCalled()
|
||||
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
58
src/services/custom-models/index.ts
Normal file
58
src/services/custom-models/index.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import * as path from "path"
|
||||
import * as fs from "fs/promises"
|
||||
|
||||
import { customModelsFileSchema } from "@roo-code/types"
|
||||
|
||||
import { getGlobalRooDirectory, getProjectRooDirectoryForCwd, fileExists } from "../roo-config"
|
||||
import type { RouterName, ModelRecord } from "../../shared/api"
|
||||
|
||||
const MODELS_DIR = "models"
|
||||
|
||||
/**
|
||||
* Load custom models for a specific provider from a single JSON file
|
||||
* @param filePath Path to the JSON file
|
||||
* @returns ModelRecord or empty object if file doesn't exist/is invalid
|
||||
*/
|
||||
async function loadModelsFromFile(filePath: string): Promise<ModelRecord> {
|
||||
try {
|
||||
if (!(await fileExists(filePath))) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const content = await fs.readFile(filePath, "utf-8")
|
||||
const parsed = JSON.parse(content)
|
||||
|
||||
const result = customModelsFileSchema.safeParse(parsed)
|
||||
|
||||
if (!result.success) {
|
||||
console.error(`[CustomModels] Invalid schema in ${filePath}:`, result.error)
|
||||
return {}
|
||||
}
|
||||
|
||||
return result.data as ModelRecord
|
||||
} catch (error) {
|
||||
console.error(`[CustomModels] Error loading ${filePath}:`, error)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom models for a provider by merging global and project files
|
||||
* @param provider The provider slug (e.g., "openrouter")
|
||||
* @param cwd Current working directory for project path
|
||||
* @returns Merged ModelRecord with project overriding global
|
||||
*/
|
||||
export async function getCustomModelsForProvider(provider: RouterName, cwd: string): Promise<ModelRecord> {
|
||||
const filename = `${provider}.json`
|
||||
|
||||
const globalPath = path.join(getGlobalRooDirectory(), MODELS_DIR, filename)
|
||||
const projectPath = path.join(getProjectRooDirectoryForCwd(cwd), MODELS_DIR, filename)
|
||||
|
||||
const globalModels = await loadModelsFromFile(globalPath)
|
||||
const projectModels = await loadModelsFromFile(projectPath)
|
||||
|
||||
// Merge: project overrides global
|
||||
return { ...globalModels, ...projectModels }
|
||||
}
|
||||
|
||||
export * from "./static-providers"
|
||||
136
src/services/custom-models/static-providers.ts
Normal file
136
src/services/custom-models/static-providers.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import {
|
||||
anthropicModels,
|
||||
bedrockModels,
|
||||
cerebrasModels,
|
||||
claudeCodeModels,
|
||||
deepSeekModels,
|
||||
doubaoModels,
|
||||
featherlessModels,
|
||||
fireworksModels,
|
||||
geminiModels,
|
||||
groqModels,
|
||||
ioIntelligenceModels,
|
||||
mistralModels,
|
||||
moonshotModels,
|
||||
openAiNativeModels,
|
||||
qwenCodeModels,
|
||||
sambaNovaModels,
|
||||
vertexModels,
|
||||
vscodeLlmModels,
|
||||
xaiModels,
|
||||
internationalZAiModels,
|
||||
minimaxModels,
|
||||
type ProviderName,
|
||||
type ModelInfo,
|
||||
} from "@roo-code/types"
|
||||
|
||||
import { getCustomModelsForProvider } from "./index"
|
||||
|
||||
// Single source of truth for static provider names
|
||||
// This const tuple provides proper type narrowing for StaticProviderWithCustomModels
|
||||
export const staticProviderNames = [
|
||||
"anthropic",
|
||||
"bedrock",
|
||||
"cerebras",
|
||||
"claude-code",
|
||||
"deepseek",
|
||||
"doubao",
|
||||
"featherless",
|
||||
"fireworks",
|
||||
"gemini",
|
||||
"groq",
|
||||
"io-intelligence",
|
||||
"mistral",
|
||||
"moonshot",
|
||||
"minimax",
|
||||
"openai-native",
|
||||
"qwen-code",
|
||||
"sambanova",
|
||||
"vertex",
|
||||
"vscode-lm",
|
||||
"xai",
|
||||
"zai",
|
||||
] as const
|
||||
|
||||
// Map of provider names to their static model dictionaries
|
||||
const STATIC_MODEL_DICTIONARIES: Record<(typeof staticProviderNames)[number], Record<string, ModelInfo>> = {
|
||||
anthropic: anthropicModels as Record<string, ModelInfo>,
|
||||
bedrock: bedrockModels as Record<string, ModelInfo>,
|
||||
cerebras: cerebrasModels as Record<string, ModelInfo>,
|
||||
"claude-code": claudeCodeModels as Record<string, ModelInfo>,
|
||||
deepseek: deepSeekModels as Record<string, ModelInfo>,
|
||||
doubao: doubaoModels as Record<string, ModelInfo>,
|
||||
featherless: featherlessModels as Record<string, ModelInfo>,
|
||||
fireworks: fireworksModels as Record<string, ModelInfo>,
|
||||
gemini: geminiModels as Record<string, ModelInfo>,
|
||||
groq: groqModels as Record<string, ModelInfo>,
|
||||
"io-intelligence": ioIntelligenceModels as Record<string, ModelInfo>,
|
||||
mistral: mistralModels as Record<string, ModelInfo>,
|
||||
moonshot: moonshotModels as Record<string, ModelInfo>,
|
||||
minimax: minimaxModels as Record<string, ModelInfo>,
|
||||
"openai-native": openAiNativeModels as Record<string, ModelInfo>,
|
||||
"qwen-code": qwenCodeModels as Record<string, ModelInfo>,
|
||||
sambanova: sambaNovaModels as Record<string, ModelInfo>,
|
||||
vertex: vertexModels as Record<string, ModelInfo>,
|
||||
"vscode-lm": vscodeLlmModels as Record<string, ModelInfo>,
|
||||
xai: xaiModels as Record<string, ModelInfo>,
|
||||
zai: internationalZAiModels as Record<string, ModelInfo>,
|
||||
}
|
||||
|
||||
/**
|
||||
* Get models for a static provider, merging built-in models with custom models
|
||||
* @param provider The provider name
|
||||
* @param cwd Current working directory for project path
|
||||
* @returns Merged record of models
|
||||
*/
|
||||
export async function getModelsForStaticProvider(
|
||||
provider: ProviderName,
|
||||
cwd: string,
|
||||
): Promise<Record<string, ModelInfo>> {
|
||||
const staticModels =
|
||||
(STATIC_MODEL_DICTIONARIES as Partial<Record<ProviderName, Record<string, ModelInfo>>>)[provider] || {}
|
||||
const customModels = await getCustomModelsForProvider(provider as any, cwd)
|
||||
|
||||
// Merge: custom models override static models
|
||||
return { ...staticModels, ...customModels }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a provider is a static provider (has hard-coded models)
|
||||
* @param provider The provider name
|
||||
* @returns True if the provider has static models
|
||||
*/
|
||||
export function isStaticProvider(provider: ProviderName): boolean {
|
||||
return provider in STATIC_MODEL_DICTIONARIES
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all static provider names (providers with hard-coded models)
|
||||
* @returns Array of static provider names
|
||||
*/
|
||||
export function getStaticProviderNames(): readonly ProviderName[] {
|
||||
return staticProviderNames
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all provider names that support custom models (both static and dynamic)
|
||||
* @returns Array of provider names
|
||||
*/
|
||||
export function getSupportedProviders(): ProviderName[] {
|
||||
return [
|
||||
...staticProviderNames,
|
||||
// Dynamic providers from modelCache.ts
|
||||
"openrouter",
|
||||
"requesty",
|
||||
"glama",
|
||||
"unbound",
|
||||
"litellm",
|
||||
"ollama",
|
||||
"lmstudio",
|
||||
"deepinfra",
|
||||
"vercel-ai-gateway",
|
||||
"huggingface",
|
||||
"roo",
|
||||
"chutes",
|
||||
] as ProviderName[]
|
||||
}
|
||||
60
src/services/custom-models/watcher.ts
Normal file
60
src/services/custom-models/watcher.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
|
||||
import { getGlobalRooDirectory, getProjectRooDirectoryForCwd } from "../roo-config"
|
||||
import { flushModels } from "../../api/providers/fetchers/modelCache"
|
||||
import { clearProviderModelsCache, preloadStaticProviderModels } from "../../api/providers/model-lookup"
|
||||
import { getWorkspacePath } from "../../utils/path"
|
||||
import type { RouterName } from "../../shared/api"
|
||||
import type { ProviderName } from "@roo-code/types"
|
||||
|
||||
const MODELS_DIR = "models"
|
||||
|
||||
/**
|
||||
* Setup file watchers for custom model JSON files
|
||||
* Watches both global and project-local .roo/models/ directories
|
||||
* @param cwd Current working directory for project path
|
||||
* @returns Disposable to clean up watchers
|
||||
*/
|
||||
export function setupCustomModelsWatcher(cwd: string): vscode.Disposable {
|
||||
const globalPath = path.join(getGlobalRooDirectory(), MODELS_DIR)
|
||||
const projectPath = path.join(getProjectRooDirectoryForCwd(cwd), MODELS_DIR)
|
||||
|
||||
const globalWatcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(globalPath, "*.json"))
|
||||
|
||||
const projectWatcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(projectPath, "*.json"))
|
||||
|
||||
const handleChange = async (uri: vscode.Uri) => {
|
||||
// Extract provider from filename (e.g., "openrouter.json" → "openrouter")
|
||||
const filename = path.basename(uri.fsPath, ".json") as ProviderName
|
||||
console.log(`[CustomModels] Detected change in custom models for provider: ${filename}`)
|
||||
|
||||
// Clear cache for dynamic providers
|
||||
try {
|
||||
await flushModels(filename as RouterName)
|
||||
} catch (error) {
|
||||
// Not a dynamic provider, that's fine
|
||||
}
|
||||
|
||||
// Clear cache for static providers and re-preload
|
||||
clearProviderModelsCache(filename)
|
||||
|
||||
// Re-preload all static provider models to ensure cache is updated
|
||||
try {
|
||||
await preloadStaticProviderModels(getWorkspacePath())
|
||||
} catch (error) {
|
||||
console.error(`[CustomModels] Error reloading static provider models:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
return vscode.Disposable.from(
|
||||
globalWatcher.onDidChange(handleChange),
|
||||
globalWatcher.onDidCreate(handleChange),
|
||||
globalWatcher.onDidDelete(handleChange),
|
||||
projectWatcher.onDidChange(handleChange),
|
||||
projectWatcher.onDidCreate(handleChange),
|
||||
projectWatcher.onDidDelete(handleChange),
|
||||
globalWatcher,
|
||||
projectWatcher,
|
||||
)
|
||||
}
|
||||
|
|
@ -3,12 +3,15 @@ import {
|
|||
type ProviderSettings,
|
||||
type DynamicProvider,
|
||||
type LocalProvider,
|
||||
type ProviderName,
|
||||
ANTHROPIC_DEFAULT_MAX_TOKENS,
|
||||
CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS,
|
||||
isDynamicProvider,
|
||||
isLocalProvider,
|
||||
} from "@roo-code/types"
|
||||
|
||||
import { staticProviderNames } from "../services/custom-models/static-providers"
|
||||
|
||||
// ApiHandlerOptions
|
||||
// Extend ProviderSettings (minus apiProvider) with handler-specific toggles.
|
||||
export type ApiHandlerOptions = Omit<ProviderSettings, "apiProvider"> & {
|
||||
|
|
@ -26,11 +29,22 @@ export type ApiHandlerOptions = Omit<ProviderSettings, "apiProvider"> & {
|
|||
ollamaNumCtx?: number
|
||||
}
|
||||
|
||||
// RouterName
|
||||
// StaticProvider - providers with hard-coded model definitions that support custom models
|
||||
// Imported from the single source of truth in static-providers.ts
|
||||
|
||||
export type RouterName = DynamicProvider | LocalProvider
|
||||
export const staticProvidersWithCustomModels = staticProviderNames
|
||||
|
||||
export const isRouterName = (value: string): value is RouterName => isDynamicProvider(value) || isLocalProvider(value)
|
||||
export type StaticProviderWithCustomModels = (typeof staticProvidersWithCustomModels)[number]
|
||||
|
||||
export const isStaticProviderWithCustomModels = (key: string): key is StaticProviderWithCustomModels =>
|
||||
(staticProvidersWithCustomModels as readonly string[]).includes(key)
|
||||
|
||||
// RouterName - includes dynamic, local, and static providers that support custom models
|
||||
|
||||
export type RouterName = DynamicProvider | LocalProvider | StaticProviderWithCustomModels
|
||||
|
||||
export const isRouterName = (value: string): value is RouterName =>
|
||||
isDynamicProvider(value) || isLocalProvider(value) || isStaticProviderWithCustomModels(value)
|
||||
|
||||
export function toRouterName(value?: string): RouterName {
|
||||
if (value && isRouterName(value)) {
|
||||
|
|
@ -148,9 +162,8 @@ type CommonFetchParams = {
|
|||
baseUrl?: string
|
||||
}
|
||||
|
||||
// Exhaustive, value-level map for all dynamic providers.
|
||||
// If a new dynamic provider is added in packages/types, this will fail to compile
|
||||
// until a corresponding entry is added here.
|
||||
// Exhaustive, value-level map for dynamic and local providers that can be fetched.
|
||||
// Static providers don't need entries here as they're loaded differently.
|
||||
const dynamicProviderExtras = {
|
||||
openrouter: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type
|
||||
"vercel-ai-gateway": {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type
|
||||
|
|
@ -165,10 +178,10 @@ const dynamicProviderExtras = {
|
|||
lmstudio: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type
|
||||
roo: {} as { apiKey?: string; baseUrl?: string },
|
||||
chutes: {} as { apiKey?: string },
|
||||
} as const satisfies Record<RouterName, object>
|
||||
} as const satisfies Record<DynamicProvider | LocalProvider, object>
|
||||
|
||||
// Build the dynamic options union from the map, intersected with CommonFetchParams
|
||||
// so extra fields are always allowed while required ones are enforced.
|
||||
export type GetModelsOptions = {
|
||||
[P in keyof typeof dynamicProviderExtras]: ({ provider: P } & (typeof dynamicProviderExtras)[P]) & CommonFetchParams
|
||||
}[RouterName]
|
||||
}[DynamicProvider | LocalProvider]
|
||||
|
|
|
|||
|
|
@ -270,7 +270,8 @@ const ApiOptions = ({
|
|||
}, [apiConfiguration, routerModels, organizationAllowList, setErrorMessage])
|
||||
|
||||
const selectedProviderModels = useMemo(() => {
|
||||
const models = MODELS_BY_PROVIDER[selectedProvider]
|
||||
// Prefer routerModels (includes custom models) over static MODELS_BY_PROVIDER
|
||||
const models = (routerModels as any)?.[selectedProvider] ?? MODELS_BY_PROVIDER[selectedProvider]
|
||||
if (!models) return []
|
||||
|
||||
const filteredModels = filterModels(models, selectedProvider, organizationAllowList)
|
||||
|
|
@ -292,7 +293,7 @@ const ApiOptions = ({
|
|||
: []
|
||||
|
||||
return availableModels
|
||||
}, [selectedProvider, organizationAllowList, selectedModelId])
|
||||
}, [selectedProvider, organizationAllowList, selectedModelId, routerModels])
|
||||
|
||||
const onProviderChange = useCallback(
|
||||
(value: ProviderName) => {
|
||||
|
|
@ -422,17 +423,17 @@ const ApiOptions = ({
|
|||
return true
|
||||
}
|
||||
|
||||
// Check if this is a static provider (has models in MODELS_BY_PROVIDER)
|
||||
const staticModels = MODELS_BY_PROVIDER[value as ProviderName]
|
||||
// Check if this provider has models (prefer routerModels for custom model support)
|
||||
const models = (routerModels as any)?.[value] ?? MODELS_BY_PROVIDER[value as ProviderName]
|
||||
|
||||
// If it's a static provider, check if it has any models after filtering
|
||||
if (staticModels) {
|
||||
const filteredModels = filterModels(staticModels, value as ProviderName, organizationAllowList)
|
||||
if (models) {
|
||||
const filteredModels = filterModels(models, value as ProviderName, organizationAllowList)
|
||||
// Hide the provider if it has no models after filtering
|
||||
return filteredModels && Object.keys(filteredModels).length > 0
|
||||
}
|
||||
|
||||
// If it's a dynamic provider (not in MODELS_BY_PROVIDER), always show it
|
||||
// If provider has no models yet (dynamic provider not yet fetched), always show it
|
||||
// to avoid race conditions with async model fetching
|
||||
return true
|
||||
})
|
||||
|
|
@ -441,7 +442,7 @@ const ApiOptions = ({
|
|||
value,
|
||||
label,
|
||||
}))
|
||||
}, [organizationAllowList, apiConfiguration.apiProvider])
|
||||
}, [organizationAllowList, apiConfiguration.apiProvider, routerModels])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
|
|
|
|||
|
|
@ -59,6 +59,27 @@ describe("useSelectedModel", () => {
|
|||
unbound: {},
|
||||
litellm: {},
|
||||
"io-intelligence": {},
|
||||
// Static providers (empty since not testing them here)
|
||||
anthropic: {},
|
||||
bedrock: {},
|
||||
vertex: {},
|
||||
gemini: {},
|
||||
"openai-native": {},
|
||||
mistral: {},
|
||||
deepseek: {},
|
||||
doubao: {},
|
||||
moonshot: {},
|
||||
minimax: {},
|
||||
xai: {},
|
||||
groq: {},
|
||||
cerebras: {},
|
||||
sambanova: {},
|
||||
fireworks: {},
|
||||
featherless: {},
|
||||
"qwen-code": {},
|
||||
"claude-code": {},
|
||||
zai: {},
|
||||
"vscode-lm": {},
|
||||
},
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
|
|
@ -123,6 +144,27 @@ describe("useSelectedModel", () => {
|
|||
unbound: {},
|
||||
litellm: {},
|
||||
"io-intelligence": {},
|
||||
// Static providers (empty since not testing them here)
|
||||
anthropic: {},
|
||||
bedrock: {},
|
||||
vertex: {},
|
||||
gemini: {},
|
||||
"openai-native": {},
|
||||
mistral: {},
|
||||
deepseek: {},
|
||||
doubao: {},
|
||||
moonshot: {},
|
||||
minimax: {},
|
||||
xai: {},
|
||||
groq: {},
|
||||
cerebras: {},
|
||||
sambanova: {},
|
||||
fireworks: {},
|
||||
featherless: {},
|
||||
"qwen-code": {},
|
||||
"claude-code": {},
|
||||
zai: {},
|
||||
"vscode-lm": {},
|
||||
},
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
|
|
@ -191,6 +233,27 @@ describe("useSelectedModel", () => {
|
|||
unbound: {},
|
||||
litellm: {},
|
||||
"io-intelligence": {},
|
||||
// Static providers (empty since not testing them here)
|
||||
anthropic: {},
|
||||
bedrock: {},
|
||||
vertex: {},
|
||||
gemini: {},
|
||||
"openai-native": {},
|
||||
mistral: {},
|
||||
deepseek: {},
|
||||
doubao: {},
|
||||
moonshot: {},
|
||||
minimax: {},
|
||||
xai: {},
|
||||
groq: {},
|
||||
cerebras: {},
|
||||
sambanova: {},
|
||||
fireworks: {},
|
||||
featherless: {},
|
||||
"qwen-code": {},
|
||||
"claude-code": {},
|
||||
zai: {},
|
||||
"vscode-lm": {},
|
||||
},
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
|
|
@ -246,6 +309,27 @@ describe("useSelectedModel", () => {
|
|||
unbound: {},
|
||||
litellm: {},
|
||||
"io-intelligence": {},
|
||||
// Static providers (empty since not testing them here)
|
||||
anthropic: {},
|
||||
bedrock: {},
|
||||
vertex: {},
|
||||
gemini: {},
|
||||
"openai-native": {},
|
||||
mistral: {},
|
||||
deepseek: {},
|
||||
doubao: {},
|
||||
moonshot: {},
|
||||
minimax: {},
|
||||
xai: {},
|
||||
groq: {},
|
||||
cerebras: {},
|
||||
sambanova: {},
|
||||
fireworks: {},
|
||||
featherless: {},
|
||||
"qwen-code": {},
|
||||
"claude-code": {},
|
||||
zai: {},
|
||||
"vscode-lm": {},
|
||||
},
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
|
|
@ -290,6 +374,27 @@ describe("useSelectedModel", () => {
|
|||
unbound: {},
|
||||
litellm: {},
|
||||
"io-intelligence": {},
|
||||
// Static providers (empty since not testing them here)
|
||||
anthropic: {},
|
||||
bedrock: {},
|
||||
vertex: {},
|
||||
gemini: {},
|
||||
"openai-native": {},
|
||||
mistral: {},
|
||||
deepseek: {},
|
||||
doubao: {},
|
||||
moonshot: {},
|
||||
minimax: {},
|
||||
xai: {},
|
||||
groq: {},
|
||||
cerebras: {},
|
||||
sambanova: {},
|
||||
fireworks: {},
|
||||
featherless: {},
|
||||
"qwen-code": {},
|
||||
"claude-code": {},
|
||||
zai: {},
|
||||
"vscode-lm": {},
|
||||
},
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
|
|
@ -327,7 +432,7 @@ describe("useSelectedModel", () => {
|
|||
})
|
||||
|
||||
describe("loading and error states", () => {
|
||||
it("should NOT set loading when router models are loading but provider is static (anthropic)", () => {
|
||||
it("should set loading when router models are loading for static provider (anthropic)", () => {
|
||||
mockUseRouterModels.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
|
|
@ -343,13 +448,41 @@ describe("useSelectedModel", () => {
|
|||
const wrapper = createWrapper()
|
||||
const { result } = renderHook(() => useSelectedModel(), { wrapper })
|
||||
|
||||
// With static provider default (anthropic), useSelectedModel gates router fetches, so loading should be false
|
||||
expect(result.current.isLoading).toBe(false)
|
||||
// Static providers now fetch router models (for custom models support), so loading should be true
|
||||
expect(result.current.isLoading).toBe(true)
|
||||
})
|
||||
|
||||
it("should NOT set loading when openrouter provider metadata is loading but provider is static (anthropic)", () => {
|
||||
mockUseRouterModels.mockReturnValue({
|
||||
data: { openrouter: {}, requesty: {}, glama: {}, unbound: {}, litellm: {}, "io-intelligence": {} },
|
||||
data: {
|
||||
openrouter: {},
|
||||
requesty: {},
|
||||
glama: {},
|
||||
unbound: {},
|
||||
litellm: {},
|
||||
"io-intelligence": {},
|
||||
// Static providers (anthropic is the default provider being tested)
|
||||
anthropic: {},
|
||||
bedrock: {},
|
||||
vertex: {},
|
||||
gemini: {},
|
||||
"openai-native": {},
|
||||
mistral: {},
|
||||
deepseek: {},
|
||||
doubao: {},
|
||||
moonshot: {},
|
||||
minimax: {},
|
||||
xai: {},
|
||||
groq: {},
|
||||
cerebras: {},
|
||||
sambanova: {},
|
||||
fireworks: {},
|
||||
featherless: {},
|
||||
"qwen-code": {},
|
||||
"claude-code": {},
|
||||
zai: {},
|
||||
"vscode-lm": {},
|
||||
},
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
} as any)
|
||||
|
|
@ -367,7 +500,7 @@ describe("useSelectedModel", () => {
|
|||
expect(result.current.isLoading).toBe(false)
|
||||
})
|
||||
|
||||
it("should NOT set error when hooks error but provider is static (anthropic)", () => {
|
||||
it("should set error when router models error for static provider (anthropic)", () => {
|
||||
mockUseRouterModels.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
|
|
@ -383,15 +516,37 @@ describe("useSelectedModel", () => {
|
|||
const wrapper = createWrapper()
|
||||
const { result } = renderHook(() => useSelectedModel(), { wrapper })
|
||||
|
||||
// Error from gated routerModels should not bubble for static provider default
|
||||
expect(result.current.isError).toBe(false)
|
||||
// Error from routerModels should bubble for static provider (since they now fetch router models)
|
||||
expect(result.current.isError).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("default behavior", () => {
|
||||
it("should return anthropic default when no configuration is provided", () => {
|
||||
mockUseRouterModels.mockReturnValue({
|
||||
data: undefined,
|
||||
data: {
|
||||
// Static providers - anthropic is the default provider
|
||||
anthropic: {},
|
||||
bedrock: {},
|
||||
vertex: {},
|
||||
gemini: {},
|
||||
"openai-native": {},
|
||||
mistral: {},
|
||||
deepseek: {},
|
||||
doubao: {},
|
||||
moonshot: {},
|
||||
minimax: {},
|
||||
xai: {},
|
||||
groq: {},
|
||||
cerebras: {},
|
||||
sambanova: {},
|
||||
fireworks: {},
|
||||
featherless: {},
|
||||
"qwen-code": {},
|
||||
"claude-code": {},
|
||||
zai: {},
|
||||
"vscode-lm": {},
|
||||
},
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
} as any)
|
||||
|
|
@ -421,6 +576,27 @@ describe("useSelectedModel", () => {
|
|||
unbound: {},
|
||||
litellm: {},
|
||||
"io-intelligence": {},
|
||||
// Static providers (claude-code is being tested)
|
||||
anthropic: {},
|
||||
bedrock: {},
|
||||
vertex: {},
|
||||
gemini: {},
|
||||
"openai-native": {},
|
||||
mistral: {},
|
||||
deepseek: {},
|
||||
doubao: {},
|
||||
moonshot: {},
|
||||
minimax: {},
|
||||
xai: {},
|
||||
groq: {},
|
||||
cerebras: {},
|
||||
sambanova: {},
|
||||
fireworks: {},
|
||||
featherless: {},
|
||||
"qwen-code": {},
|
||||
"claude-code": {},
|
||||
zai: {},
|
||||
"vscode-lm": {},
|
||||
},
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
|
|
@ -459,6 +635,27 @@ describe("useSelectedModel", () => {
|
|||
unbound: {},
|
||||
litellm: {},
|
||||
"io-intelligence": {},
|
||||
// Static providers (claude-code is being tested)
|
||||
anthropic: {},
|
||||
bedrock: {},
|
||||
vertex: {},
|
||||
gemini: {},
|
||||
"openai-native": {},
|
||||
mistral: {},
|
||||
deepseek: {},
|
||||
doubao: {},
|
||||
moonshot: {},
|
||||
minimax: {},
|
||||
xai: {},
|
||||
groq: {},
|
||||
cerebras: {},
|
||||
sambanova: {},
|
||||
fireworks: {},
|
||||
featherless: {},
|
||||
"qwen-code": {},
|
||||
"claude-code": {},
|
||||
zai: {},
|
||||
"vscode-lm": {},
|
||||
},
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
|
|
@ -494,6 +691,27 @@ describe("useSelectedModel", () => {
|
|||
unbound: {},
|
||||
litellm: {},
|
||||
"io-intelligence": {},
|
||||
// Static providers (bedrock is being tested)
|
||||
anthropic: {},
|
||||
bedrock: {},
|
||||
vertex: {},
|
||||
gemini: {},
|
||||
"openai-native": {},
|
||||
mistral: {},
|
||||
deepseek: {},
|
||||
doubao: {},
|
||||
moonshot: {},
|
||||
minimax: {},
|
||||
xai: {},
|
||||
groq: {},
|
||||
cerebras: {},
|
||||
sambanova: {},
|
||||
fireworks: {},
|
||||
featherless: {},
|
||||
"qwen-code": {},
|
||||
"claude-code": {},
|
||||
zai: {},
|
||||
"vscode-lm": {},
|
||||
},
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import {
|
|||
} from "@roo-code/types"
|
||||
|
||||
import type { ModelRecord, RouterModels } from "@roo/api"
|
||||
import { isStaticProviderWithCustomModels } from "@roo/api"
|
||||
|
||||
import { useRouterModels } from "./useRouterModels"
|
||||
import { useOpenRouterModelProviders } from "./useOpenRouterModelProviders"
|
||||
|
|
@ -56,8 +57,8 @@ export const useSelectedModel = (apiConfiguration?: ProviderSettings) => {
|
|||
const lmStudioModelId = provider === "lmstudio" ? apiConfiguration?.lmStudioModelId : undefined
|
||||
const ollamaModelId = provider === "ollama" ? apiConfiguration?.ollamaModelId : undefined
|
||||
|
||||
// Only fetch router models for dynamic providers
|
||||
const shouldFetchRouterModels = isDynamicProvider(provider)
|
||||
// Fetch router models for dynamic providers and static providers with custom models
|
||||
const shouldFetchRouterModels = isDynamicProvider(provider) || isStaticProviderWithCustomModels(provider)
|
||||
const routerModels = useRouterModels({
|
||||
provider: shouldFetchRouterModels ? provider : undefined,
|
||||
enabled: shouldFetchRouterModels,
|
||||
|
|
@ -173,12 +174,14 @@ function getSelectedModel({
|
|||
}
|
||||
case "xai": {
|
||||
const id = apiConfiguration.apiModelId ?? defaultModelId
|
||||
const info = xaiModels[id as keyof typeof xaiModels]
|
||||
// Check routerModels first (for custom models), then fall back to hard-coded models
|
||||
const info = routerModels.xai?.[id] ?? xaiModels[id as keyof typeof xaiModels]
|
||||
return info ? { id, info } : { id, info: undefined }
|
||||
}
|
||||
case "groq": {
|
||||
const id = apiConfiguration.apiModelId ?? defaultModelId
|
||||
const info = groqModels[id as keyof typeof groqModels]
|
||||
// Check routerModels first (for custom models), then fall back to hard-coded models
|
||||
const info = routerModels.groq?.[id] ?? groqModels[id as keyof typeof groqModels]
|
||||
return { id, info }
|
||||
}
|
||||
case "huggingface": {
|
||||
|
|
@ -198,7 +201,8 @@ function getSelectedModel({
|
|||
}
|
||||
case "bedrock": {
|
||||
const id = apiConfiguration.apiModelId ?? defaultModelId
|
||||
const baseInfo = bedrockModels[id as keyof typeof bedrockModels]
|
||||
// Check routerModels first (for custom models), then fall back to hard-coded models
|
||||
const baseInfo = routerModels.bedrock?.[id] ?? bedrockModels[id as keyof typeof bedrockModels]
|
||||
|
||||
// Special case for custom ARN.
|
||||
if (id === "custom-arn") {
|
||||
|
|
@ -222,32 +226,38 @@ function getSelectedModel({
|
|||
}
|
||||
case "vertex": {
|
||||
const id = apiConfiguration.apiModelId ?? defaultModelId
|
||||
const info = vertexModels[id as keyof typeof vertexModels]
|
||||
// Check routerModels first (for custom models), then fall back to hard-coded models
|
||||
const info = routerModels.vertex?.[id] ?? vertexModels[id as keyof typeof vertexModels]
|
||||
return { id, info }
|
||||
}
|
||||
case "gemini": {
|
||||
const id = apiConfiguration.apiModelId ?? defaultModelId
|
||||
const info = geminiModels[id as keyof typeof geminiModels]
|
||||
// Check routerModels first (for custom models), then fall back to hard-coded models
|
||||
const info = routerModels.gemini?.[id] ?? geminiModels[id as keyof typeof geminiModels]
|
||||
return { id, info }
|
||||
}
|
||||
case "deepseek": {
|
||||
const id = apiConfiguration.apiModelId ?? defaultModelId
|
||||
const info = deepSeekModels[id as keyof typeof deepSeekModels]
|
||||
// Check routerModels first (for custom models), then fall back to hard-coded models
|
||||
const info = routerModels.deepseek?.[id] ?? deepSeekModels[id as keyof typeof deepSeekModels]
|
||||
return { id, info }
|
||||
}
|
||||
case "doubao": {
|
||||
const id = apiConfiguration.apiModelId ?? defaultModelId
|
||||
const info = doubaoModels[id as keyof typeof doubaoModels]
|
||||
// Check routerModels first (for custom models), then fall back to hard-coded models
|
||||
const info = routerModels.doubao?.[id] ?? doubaoModels[id as keyof typeof doubaoModels]
|
||||
return { id, info }
|
||||
}
|
||||
case "moonshot": {
|
||||
const id = apiConfiguration.apiModelId ?? defaultModelId
|
||||
const info = moonshotModels[id as keyof typeof moonshotModels]
|
||||
// Check routerModels first (for custom models), then fall back to hard-coded models
|
||||
const info = routerModels.moonshot?.[id] ?? moonshotModels[id as keyof typeof moonshotModels]
|
||||
return { id, info }
|
||||
}
|
||||
case "minimax": {
|
||||
const id = apiConfiguration.apiModelId ?? defaultModelId
|
||||
const info = minimaxModels[id as keyof typeof minimaxModels]
|
||||
// Check routerModels first (for custom models), then fall back to hard-coded models
|
||||
const info = routerModels.minimax?.[id] ?? minimaxModels[id as keyof typeof minimaxModels]
|
||||
return { id, info }
|
||||
}
|
||||
case "zai": {
|
||||
|
|
@ -260,12 +270,15 @@ function getSelectedModel({
|
|||
}
|
||||
case "openai-native": {
|
||||
const id = apiConfiguration.apiModelId ?? defaultModelId
|
||||
const info = openAiNativeModels[id as keyof typeof openAiNativeModels]
|
||||
// Check routerModels first (for custom models), then fall back to hard-coded models
|
||||
const info =
|
||||
routerModels["openai-native"]?.[id] ?? openAiNativeModels[id as keyof typeof openAiNativeModels]
|
||||
return { id, info }
|
||||
}
|
||||
case "mistral": {
|
||||
const id = apiConfiguration.apiModelId ?? defaultModelId
|
||||
const info = mistralModels[id as keyof typeof mistralModels]
|
||||
// Check routerModels first (for custom models), then fall back to hard-coded models
|
||||
const info = routerModels.mistral?.[id] ?? mistralModels[id as keyof typeof mistralModels]
|
||||
return { id, info }
|
||||
}
|
||||
case "openai": {
|
||||
|
|
@ -313,27 +326,32 @@ function getSelectedModel({
|
|||
case "claude-code": {
|
||||
// Claude Code models extend anthropic models but with images and prompt caching disabled
|
||||
const id = apiConfiguration.apiModelId ?? defaultModelId
|
||||
const info = claudeCodeModels[id as keyof typeof claudeCodeModels]
|
||||
// Check routerModels first (for custom models), then fall back to hard-coded models
|
||||
const info = routerModels["claude-code"]?.[id] ?? claudeCodeModels[id as keyof typeof claudeCodeModels]
|
||||
return { id, info: { ...openAiModelInfoSaneDefaults, ...info } }
|
||||
}
|
||||
case "cerebras": {
|
||||
const id = apiConfiguration.apiModelId ?? defaultModelId
|
||||
const info = cerebrasModels[id as keyof typeof cerebrasModels]
|
||||
// Check routerModels first (for custom models), then fall back to hard-coded models
|
||||
const info = routerModels.cerebras?.[id] ?? cerebrasModels[id as keyof typeof cerebrasModels]
|
||||
return { id, info }
|
||||
}
|
||||
case "sambanova": {
|
||||
const id = apiConfiguration.apiModelId ?? defaultModelId
|
||||
const info = sambaNovaModels[id as keyof typeof sambaNovaModels]
|
||||
// Check routerModels first (for custom models), then fall back to hard-coded models
|
||||
const info = routerModels.sambanova?.[id] ?? sambaNovaModels[id as keyof typeof sambaNovaModels]
|
||||
return { id, info }
|
||||
}
|
||||
case "fireworks": {
|
||||
const id = apiConfiguration.apiModelId ?? defaultModelId
|
||||
const info = fireworksModels[id as keyof typeof fireworksModels]
|
||||
// Check routerModels first (for custom models), then fall back to hard-coded models
|
||||
const info = routerModels.fireworks?.[id] ?? fireworksModels[id as keyof typeof fireworksModels]
|
||||
return { id, info }
|
||||
}
|
||||
case "featherless": {
|
||||
const id = apiConfiguration.apiModelId ?? defaultModelId
|
||||
const info = featherlessModels[id as keyof typeof featherlessModels]
|
||||
// Check routerModels first (for custom models), then fall back to hard-coded models
|
||||
const info = routerModels.featherless?.[id] ?? featherlessModels[id as keyof typeof featherlessModels]
|
||||
return { id, info }
|
||||
}
|
||||
case "io-intelligence": {
|
||||
|
|
@ -353,7 +371,8 @@ function getSelectedModel({
|
|||
}
|
||||
case "qwen-code": {
|
||||
const id = apiConfiguration.apiModelId ?? defaultModelId
|
||||
const info = qwenCodeModels[id as keyof typeof qwenCodeModels]
|
||||
// Check routerModels first (for custom models), then fall back to hard-coded models
|
||||
const info = routerModels["qwen-code"]?.[id] ?? qwenCodeModels[id as keyof typeof qwenCodeModels]
|
||||
return { id, info }
|
||||
}
|
||||
case "vercel-ai-gateway": {
|
||||
|
|
@ -371,7 +390,8 @@ function getSelectedModel({
|
|||
default: {
|
||||
provider satisfies "anthropic" | "gemini-cli" | "qwen-code" | "human-relay" | "fake-ai"
|
||||
const id = apiConfiguration.apiModelId ?? defaultModelId
|
||||
const baseInfo = anthropicModels[id as keyof typeof anthropicModels]
|
||||
// Check routerModels first (for custom models), then fall back to hard-coded models
|
||||
const baseInfo = routerModels.anthropic?.[id] ?? anthropicModels[id as keyof typeof anthropicModels]
|
||||
|
||||
// Apply 1M context beta tier pricing for Claude Sonnet 4
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -45,6 +45,27 @@ describe("Model Validation Functions", () => {
|
|||
huggingface: {},
|
||||
roo: {},
|
||||
chutes: {},
|
||||
// Static providers with custom models support
|
||||
anthropic: {},
|
||||
bedrock: {},
|
||||
vertex: {},
|
||||
gemini: {},
|
||||
"openai-native": {},
|
||||
mistral: {},
|
||||
deepseek: {},
|
||||
doubao: {},
|
||||
moonshot: {},
|
||||
minimax: {},
|
||||
xai: {},
|
||||
groq: {},
|
||||
cerebras: {},
|
||||
sambanova: {},
|
||||
fireworks: {},
|
||||
featherless: {},
|
||||
"qwen-code": {},
|
||||
"claude-code": {},
|
||||
zai: {},
|
||||
"vscode-lm": {},
|
||||
}
|
||||
|
||||
const allowAllOrganization: OrganizationAllowList = {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue