feat: add dynamic settings support for Roo models from API (#9852)

This commit is contained in:
Hannes Rudolph 2025-12-04 21:57:00 -07:00 committed by GitHub
parent 247bf017e0
commit a3de2935b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 96 additions and 39 deletions

View file

@ -39,6 +39,9 @@ export const RooModelSchema = z.object({
pricing: RooPricingSchema,
deprecated: z.boolean().optional(),
default_temperature: z.number().optional(),
// Dynamic settings that map directly to ModelInfo properties
// Allows the API to configure model-specific defaults like includedTools, excludedTools, reasoningEffort, etc.
settings: z.record(z.string(), z.unknown()).optional(),
})
export const RooModelsResponseSchema = z.object({

View file

@ -439,12 +439,12 @@ describe("RooHandler", () => {
}
})
it("should not override existing properties when applying MODEL_DEFAULTS", () => {
it("should return cached model info with settings applied from API", () => {
const handlerWithMinimax = new RooHandler({
apiModelId: "minimax/minimax-m2:free",
})
const modelInfo = handlerWithMinimax.getModel()
// The defaults should be merged, but not overwrite existing cached values
// The settings from API should already be applied in the cached model info
expect(modelInfo.info.supportsNativeTools).toBe(true)
expect(modelInfo.info.inputPrice).toBe(0.15)
expect(modelInfo.info.outputPrice).toBe(0.6)

View file

@ -77,7 +77,7 @@ describe("getRooModels", () => {
description: "Fast coding model",
deprecated: false,
isFree: false,
defaultToolProtocol: "native", // Applied from MODEL_DEFAULTS
defaultToolProtocol: "native",
},
})
})
@ -719,4 +719,86 @@ describe("getRooModels", () => {
expect(models["test/non-stealth-model"].isStealthModel).toBeUndefined()
})
it("should apply API-provided settings to model info", async () => {
const mockResponse = {
object: "list",
data: [
{
id: "test/model-with-settings",
object: "model",
created: 1234567890,
owned_by: "test",
name: "Model with Settings",
description: "Model with API-provided settings",
context_window: 128000,
max_tokens: 8192,
type: "language",
tags: ["tool-use"],
pricing: {
input: "0.0001",
output: "0.0002",
},
settings: {
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
reasoningEffort: "high",
},
},
],
}
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockResponse,
})
const models = await getRooModels(baseUrl, apiKey)
expect(models["test/model-with-settings"].includedTools).toEqual(["apply_patch"])
expect(models["test/model-with-settings"].excludedTools).toEqual(["apply_diff", "write_to_file"])
expect(models["test/model-with-settings"].reasoningEffort).toBe("high")
})
it("should handle arbitrary settings properties dynamically", async () => {
const mockResponse = {
object: "list",
data: [
{
id: "test/dynamic-settings-model",
object: "model",
created: 1234567890,
owned_by: "test",
name: "Dynamic Settings Model",
description: "Model with arbitrary settings",
context_window: 128000,
max_tokens: 8192,
type: "language",
tags: [],
pricing: {
input: "0.0001",
output: "0.0002",
},
settings: {
customProperty: "custom-value",
anotherSetting: 42,
nestedConfig: { key: "value" },
},
},
],
}
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockResponse,
})
const models = await getRooModels(baseUrl, apiKey)
const model = models["test/dynamic-settings-model"] as any
// Arbitrary settings should be passed through
expect(model.customProperty).toBe("custom-value")
expect(model.anotherSetting).toBe(42)
expect(model.nestedConfig).toEqual({ key: "value" })
})
})

View file

@ -5,31 +5,6 @@ import { parseApiPrice } from "../../../shared/cost"
import { DEFAULT_HEADERS } from "../constants"
// Model-specific defaults that should be applied even when models come from API cache
// These override API-provided values for specific models
// Exported so RooHandler.getModel() can also apply these for fallback cases
export const MODEL_DEFAULTS: Record<string, Partial<ModelInfo>> = {
"minimax/minimax-m2:free": {
includedTools: ["search_and_replace"],
excludedTools: ["apply_diff"],
},
"openai/gpt-5.1": {
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
reasoningEffort: "medium",
},
"openai/gpt-5": {
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
reasoningEffort: "medium",
},
"openai/gpt-5-mini": {
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
reasoningEffort: "medium",
},
}
/**
* Fetches available models from the Roo Code Cloud provider
*
@ -150,9 +125,12 @@ export async function getRooModels(baseUrl: string, apiKey?: string): Promise<Mo
isStealthModel: isStealthModel || undefined,
}
// Apply model-specific defaults (e.g., defaultToolProtocol)
const modelDefaults = MODEL_DEFAULTS[modelId]
models[modelId] = modelDefaults ? { ...baseModelInfo, ...modelDefaults } : baseModelInfo
// Apply API-provided settings on top of base model info
// Settings allow the proxy to dynamically configure model-specific options
// like includedTools, excludedTools, reasoningEffort, etc.
const apiSettings = model.settings as Partial<ModelInfo> | undefined
models[modelId] = apiSettings ? { ...baseModelInfo, ...apiSettings } : baseModelInfo
}
return models

View file

@ -15,7 +15,6 @@ import { getRooReasoning } from "../transform/reasoning"
import type { ApiHandlerCreateMessageMetadata } from "../index"
import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider"
import { getModels, getModelsFromCache } from "../providers/fetchers/modelCache"
import { MODEL_DEFAULTS } from "../providers/fetchers/roo"
import { handleOpenAIError } from "./utils/openai-error-handler"
import { generateImageWithProvider, generateImageWithImagesApi, ImageGenerationResult } from "./utils/image-generation"
import { t } from "../../i18n"
@ -335,17 +334,12 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
override getModel() {
const modelId = this.options.apiModelId || rooDefaultModelId
// Get models from shared cache
// Get models from shared cache (settings are already applied by the fetcher)
const models = getModelsFromCache("roo") || {}
const modelInfo = models[modelId]
// Get model-specific defaults if they exist
const modelDefaults = MODEL_DEFAULTS[modelId]
if (modelInfo) {
// Merge model-specific defaults with cached model info
const mergedInfo = modelDefaults ? { ...modelInfo, ...modelDefaults } : modelInfo
return { id: modelId, info: mergedInfo }
return { id: modelId, info: modelInfo }
}
// Return the requested model ID even if not found, with fallback info.