diff --git a/.tmp/Roo-Code b/.tmp/Roo-Code new file mode 160000 index 0000000000..86debeef43 --- /dev/null +++ b/.tmp/Roo-Code @@ -0,0 +1 @@ +Subproject commit 86debeef43acbea9bdc1aa4b38d514541e164c91 diff --git a/.tmp/pr-8396 b/.tmp/pr-8396 new file mode 160000 index 0000000000..a18538995d --- /dev/null +++ b/.tmp/pr-8396 @@ -0,0 +1 @@ +Subproject commit a18538995de0f7d9cfc4a40d31380fb141d5604e diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 8434158541..a59dffd502 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -179,6 +179,9 @@ const baseProviderSettingsSchema = z.object({ modelMaxTokens: z.number().optional(), modelMaxThinkingTokens: z.number().optional(), + // Model context window override. + modelContextWindow: z.number().optional(), + // Model verbosity. verbosity: verbosityLevelsSchema.optional(), }) diff --git a/src/api/providers/__tests__/context-window-override.spec.ts b/src/api/providers/__tests__/context-window-override.spec.ts new file mode 100644 index 0000000000..78e935d9c3 --- /dev/null +++ b/src/api/providers/__tests__/context-window-override.spec.ts @@ -0,0 +1,126 @@ +import { describe, it, expect, beforeEach } from "vitest" +import { AnthropicHandler } from "../anthropic" +import { OpenRouterHandler } from "../openrouter" +import { OpenAiHandler } from "../openai" +import { GeminiHandler } from "../gemini" +import type { ApiHandlerOptions } from "../../../shared/api" + +describe("Context Window Override", () => { + describe("AnthropicHandler", () => { + it("should apply modelContextWindow override", () => { + const options: ApiHandlerOptions = { + apiKey: "test-key", + apiModelId: "claude-3-5-sonnet-20241022", + modelContextWindow: 50000, // Custom context window + } + + const handler = new AnthropicHandler(options) + const model = handler.getModel() + + expect(model.info.contextWindow).toBe(50000) + }) + + it("should use default context window when no override is provided", () => { + const options: ApiHandlerOptions = { + apiKey: "test-key", + apiModelId: "claude-3-5-sonnet-20241022", + } + + const handler = new AnthropicHandler(options) + const model = handler.getModel() + + // Should use the default context window for this model + expect(model.info.contextWindow).toBe(200000) + }) + }) + + describe("OpenRouterHandler", () => { + it("should apply modelContextWindow override", async () => { + const options: ApiHandlerOptions = { + openRouterApiKey: "test-key", + openRouterModelId: "anthropic/claude-3.5-sonnet", + modelContextWindow: 75000, // Custom context window + } + + const handler = new OpenRouterHandler(options) + // Mock the models to avoid actual API calls + ;(handler as any).models = { + "anthropic/claude-3.5-sonnet": { + contextWindow: 200000, + maxTokens: 8192, + supportsPromptCache: true, + supportsImages: true, + }, + } + + const model = handler.getModel() + expect(model.info.contextWindow).toBe(75000) + }) + }) + + describe("OpenAiHandler", () => { + it("should apply modelContextWindow override to custom model info", () => { + const options: ApiHandlerOptions = { + openAiApiKey: "test-key", + openAiModelId: "gpt-4", + openAiCustomModelInfo: { + contextWindow: 128000, + maxTokens: 4096, + supportsPromptCache: false, + supportsImages: true, + }, + modelContextWindow: 60000, // Custom context window + } + + const handler = new OpenAiHandler(options) + const model = handler.getModel() + + expect(model.info.contextWindow).toBe(60000) + }) + }) + + describe("GeminiHandler", () => { + it("should apply modelContextWindow override", () => { + const options: ApiHandlerOptions = { + geminiApiKey: "test-key", + apiModelId: "gemini-1.5-pro-latest", + modelContextWindow: 100000, // Custom context window + } + + const handler = new GeminiHandler(options) + const model = handler.getModel() + + expect(model.info.contextWindow).toBe(100000) + }) + }) + + describe("Edge cases", () => { + it("should not apply override when modelContextWindow is 0", () => { + const options: ApiHandlerOptions = { + apiKey: "test-key", + apiModelId: "claude-3-5-sonnet-20241022", + modelContextWindow: 0, // Zero should not override + } + + const handler = new AnthropicHandler(options) + const model = handler.getModel() + + // Should use the default context window + expect(model.info.contextWindow).toBe(200000) + }) + + it("should not apply override when modelContextWindow is negative", () => { + const options: ApiHandlerOptions = { + apiKey: "test-key", + apiModelId: "claude-3-5-sonnet-20241022", + modelContextWindow: -1000, // Negative should not override + } + + const handler = new AnthropicHandler(options) + const model = handler.getModel() + + // Should use the default context window + expect(model.info.contextWindow).toBe(200000) + }) + }) +}) diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts index 3fb60c0e4f..075e0abc27 100644 --- a/src/api/providers/anthropic.ts +++ b/src/api/providers/anthropic.ts @@ -264,6 +264,9 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa } } + // Apply user-configured overrides (e.g., custom context window) + info = this.applyModelOverrides(info, this.options) + const params = getModelParams({ format: "anthropic", modelId: id, diff --git a/src/api/providers/base-openai-compatible-provider.ts b/src/api/providers/base-openai-compatible-provider.ts index fb6c5d0377..415ae6f135 100644 --- a/src/api/providers/base-openai-compatible-provider.ts +++ b/src/api/providers/base-openai-compatible-provider.ts @@ -140,6 +140,9 @@ export abstract class BaseOpenAiCompatibleProvider ? (this.options.apiModelId as ModelName) : this.defaultProviderModelId - return { id, info: this.providerModels[id] } + // Apply user-configured overrides (e.g., custom context window) + const info = this.applyModelOverrides(this.providerModels[id], this.options) + + return { id, info } } } diff --git a/src/api/providers/base-provider.ts b/src/api/providers/base-provider.ts index 1abbf5f558..ef1faebb2f 100644 --- a/src/api/providers/base-provider.ts +++ b/src/api/providers/base-provider.ts @@ -3,6 +3,7 @@ import { Anthropic } from "@anthropic-ai/sdk" import type { ModelInfo } from "@roo-code/types" import type { ApiHandler, ApiHandlerCreateMessageMetadata } from "../index" +import type { ApiHandlerOptions } from "../../shared/api" import { ApiStream } from "../transform/stream" import { countTokens } from "../../utils/countTokens" @@ -18,6 +19,26 @@ export abstract class BaseProvider implements ApiHandler { abstract getModel(): { id: string; info: ModelInfo } + /** + * Applies user-configured overrides to model info. + * This allows users to customize model parameters like context window size + * to work around corporate restrictions or other limitations. + * + * @param info The original model info + * @param options The API handler options containing user overrides + * @returns The model info with overrides applied + */ + protected applyModelOverrides(info: ModelInfo, options: ApiHandlerOptions): ModelInfo { + const overriddenInfo = { ...info } + + // Apply context window override if specified + if (options.modelContextWindow && options.modelContextWindow > 0) { + overriddenInfo.contextWindow = options.modelContextWindow + } + + return overriddenInfo + } + /** * Default token counting implementation using tiktoken. * Providers can override this to use their native token counting endpoints. diff --git a/src/api/providers/bedrock.ts b/src/api/providers/bedrock.ts index fc91dc2cb6..cb0e3bcae9 100644 --- a/src/api/providers/bedrock.ts +++ b/src/api/providers/bedrock.ts @@ -373,7 +373,7 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH maxTokens: modelConfig.maxTokens || (modelConfig.info.maxTokens as number), temperature: modelConfig.temperature ?? (this.options.modelTemperature as number), } - + // Check if 1M context is enabled for Claude Sonnet 4 // Use parseBaseModelId to handle cross-region inference prefixes const baseModelId = this.parseBaseModelId(modelConfig.id) @@ -922,10 +922,14 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH if (this.options.modelMaxTokens && this.options.modelMaxTokens > 0) { model.info.maxTokens = this.options.modelMaxTokens } + // Support both awsModelContextWindow (for backward compatibility) and modelContextWindow if (this.options.awsModelContextWindow && this.options.awsModelContextWindow > 0) { model.info.contextWindow = this.options.awsModelContextWindow } + // Apply general model overrides (including modelContextWindow) + model.info = this.applyModelOverrides(model.info, this.options) + return model } @@ -983,6 +987,9 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH } } + // Apply general model overrides (including modelContextWindow) after all specific logic + modelConfig.info = this.applyModelOverrides(modelConfig.info, this.options) + // Get model params including reasoning configuration const params = getModelParams({ format: "anthropic", diff --git a/src/api/providers/gemini.ts b/src/api/providers/gemini.ts index 573adda879..f76f58a92f 100644 --- a/src/api/providers/gemini.ts +++ b/src/api/providers/gemini.ts @@ -166,6 +166,10 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl const modelId = this.options.apiModelId let id = modelId && modelId in geminiModels ? (modelId as GeminiModelId) : geminiDefaultModelId let info: ModelInfo = geminiModels[id] + + // Apply user-configured overrides (e.g., custom context window) + info = this.applyModelOverrides(info, this.options) + const params = getModelParams({ format: "gemini", modelId: id, model: info, settings: this.options }) // The `:thinking` suffix indicates that the model is a "Hybrid" diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index aebe671712..b6b90da5fe 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -267,7 +267,11 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl override getModel() { const id = this.options.openAiModelId ?? "" - const info = this.options.openAiCustomModelInfo ?? openAiModelInfoSaneDefaults + let info = this.options.openAiCustomModelInfo ?? openAiModelInfoSaneDefaults + + // Apply user-configured overrides (e.g., custom context window) + info = this.applyModelOverrides(info, this.options) + const params = getModelParams({ format: "openai", modelId: id, model: info, settings: this.options }) return { id, info, ...params } } diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 580b173311..10d9e60205 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -232,6 +232,9 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH info = this.endpoints[this.options.openRouterSpecificProvider] } + // Apply user-configured overrides (e.g., custom context window) + info = this.applyModelOverrides(info, this.options) + const isDeepSeekR1 = id.startsWith("deepseek/deepseek-r1") || id === "perplexity/sonar-reasoning" const params = getModelParams({ diff --git a/src/api/providers/router-provider.ts b/src/api/providers/router-provider.ts index 25e9a11e1b..dbfa655e71 100644 --- a/src/api/providers/router-provider.ts +++ b/src/api/providers/router-provider.ts @@ -63,9 +63,13 @@ export abstract class RouterProvider extends BaseProvider { override getModel(): { id: string; info: ModelInfo } { const id = this.modelId ?? this.defaultModelId - return this.models[id] - ? { id, info: this.models[id] } - : { id: this.defaultModelId, info: this.defaultModelInfo } + // Get the base model info + const baseInfo = this.models[id] ? this.models[id] : this.defaultModelInfo + + // Apply user-configured overrides (e.g., custom context window) + const info = this.applyModelOverrides(baseInfo, this.options) + + return { id: this.models[id] ? id : this.defaultModelId, info } } protected supportsTemperature(modelId: string): boolean {