Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .tmp/Roo-Code
Submodule Roo-Code added at 86debe
1 change: 1 addition & 0 deletions .tmp/pr-8396
Submodule pr-8396 added at a18538
3 changes: 3 additions & 0 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ const baseProviderSettingsSchema = z.object({
modelMaxTokens: z.number().optional(),
modelMaxThinkingTokens: z.number().optional(),

// Model context window override.
modelContextWindow: z.number().optional(),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Schema allows floats/negatives; tighten to non-negative integers to reflect expected semantics and avoid silent float inputs.

Suggested change
modelContextWindow: z.number().optional(),
// Model context window override.
modelContextWindow: z.number().int().nonnegative().optional(),


// Model verbosity.
verbosity: verbosityLevelsSchema.optional(),
})
Expand Down
126 changes: 126 additions & 0 deletions src/api/providers/__tests__/context-window-override.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { describe, it, expect, beforeEach } from "vitest"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] beforeEach is imported but unused; remove to keep tests clean.

Suggested change
import { describe, it, expect, beforeEach } from "vitest"
import { describe, it, expect } 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)
})
})
})
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Consider adding cases for AwsBedrockHandler and RouterProvider to guard the new override paths you added in those files (and any other provider base classes used). This will help catch regressions if provider wiring changes.

3 changes: 3 additions & 0 deletions src/api/providers/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 4 additions & 1 deletion src/api/providers/base-openai-compatible-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
? (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 }
}
}
21 changes: 21 additions & 0 deletions src/api/providers/base-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P0] If modelContextWindow is smaller than the model's maxTokens, downstream calls may pass an invalid max_tokens > contextWindow to providers. Suggest clamping maxTokens to contextWindow after applying the override to maintain the invariant maxTokens ≤ contextWindow.

Suggested change
overriddenInfo.contextWindow = options.modelContextWindow
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
// Keep invariant: maxTokens ≤ contextWindow
if (
overriddenInfo.maxTokens &&
overriddenInfo.contextWindow &&
overriddenInfo.maxTokens > overriddenInfo.contextWindow
) {
overriddenInfo.maxTokens = overriddenInfo.contextWindow
}
}
return overriddenInfo
}

}

return overriddenInfo
}

/**
* Default token counting implementation using tiktoken.
* Providers can override this to use their native token counting endpoints.
Expand Down
9 changes: 8 additions & 1 deletion src/api/providers/bedrock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions src/api/providers/gemini.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 5 additions & 1 deletion src/api/providers/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}
Expand Down
3 changes: 3 additions & 0 deletions src/api/providers/openrouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
10 changes: 7 additions & 3 deletions src/api/providers/router-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading