From 58264c80dd50981a2358a324b7763795373a9431 Mon Sep 17 00:00:00 2001 From: Jack Cowey Date: Tue, 24 Mar 2026 12:29:34 +0000 Subject: [PATCH] feat: add configurable context length to model metadata - Add contextLength field to RegistryModel interface for per-model overrides - Add defaultContextLength to RegistryEntry for provider-level defaults - Set context lengths for major providers: - Claude: 200k - Codex: 400k (fixes combo context display) - Gemini: 1M - OpenAI: 128k - GitHub Copilot: 128k - Kiro/Cursor: 200k - OpenCode: 200k - Include context_length in /v1/models API response - Add context_length field to combo schema for custom combo context - Update contextManager to use registry defaults and support env overrides - CONTEXT_LENGTH_ for per-provider override - CONTEXT_LENGTH_DEFAULT for global override This allows clients like OpenClaw to display accurate context windows for combo models instead of guessing based on model name patterns. Co-Authored-By: Claude Opus 4.6 --- open-sse/config/providerRegistry.ts | 17 +++++++++++ open-sse/services/contextManager.ts | 44 +++++++++++++++++++++++++---- src/app/api/v1/models/catalog.ts | 11 ++++++++ src/shared/validation/schemas.ts | 1 + 4 files changed, 68 insertions(+), 5 deletions(-) diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index 7dc2152795..406cfd53ce 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -14,6 +14,8 @@ export interface RegistryModel { toolCalling?: boolean; targetFormat?: string; unsupportedParams?: readonly string[]; + /** Maximum context window in tokens */ + contextLength?: number; } // Reasoning models reject temperature, top_p, penalties, logprobs, n. @@ -61,6 +63,8 @@ export interface RegistryEntry { chatPath?: string; clientVersion?: string; passthroughModels?: boolean; + /** Default context window for all models in this provider (can be overridden per-model) */ + defaultContextLength?: number; } interface LegacyProvider { @@ -107,6 +111,7 @@ export const REGISTRY: Record = { urlSuffix: "?beta=true", authType: "oauth", authHeader: "x-api-key", + defaultContextLength: 200000, headers: { "Anthropic-Version": "2023-06-01", "Anthropic-Beta": @@ -150,6 +155,7 @@ export const REGISTRY: Record = { }, authType: "apikey", authHeader: "x-goog-api-key", + defaultContextLength: 1000000, oauth: { clientIdEnv: "GEMINI_OAUTH_CLIENT_ID", clientIdDefault: "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com", @@ -182,6 +188,7 @@ export const REGISTRY: Record = { }, authType: "oauth", authHeader: "bearer", + defaultContextLength: 1000000, oauth: { clientIdEnv: "GEMINI_CLI_OAUTH_CLIENT_ID", clientIdDefault: "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com", @@ -209,6 +216,7 @@ export const REGISTRY: Record = { baseUrl: "https://chatgpt.com/backend-api/codex/responses", authType: "oauth", authHeader: "bearer", + defaultContextLength: 400000, headers: { Version: "0.92.0", "Openai-Beta": "responses=experimental", @@ -357,6 +365,7 @@ export const REGISTRY: Record = { responsesBaseUrl: "https://api.githubcopilot.com/responses", authType: "oauth", authHeader: "bearer", + defaultContextLength: 128000, headers: { "copilot-integration-id": "vscode-chat", "editor-version": "vscode/1.107.1", @@ -405,6 +414,7 @@ export const REGISTRY: Record = { baseUrl: "https://codewhisperer.us-east-1.amazonaws.com/generateAssistantResponse", authType: "oauth", authHeader: "bearer", + defaultContextLength: 200000, headers: { "Content-Type": "application/json", Accept: "application/vnd.amazon.eventstream", @@ -431,6 +441,7 @@ export const REGISTRY: Record = { chatPath: "/aiserver.v1.ChatService/StreamUnifiedChatWithTools", authType: "oauth", authHeader: "bearer", + defaultContextLength: 200000, headers: { "connect-accept-encoding": "gzip", "connect-protocol-version": "1", @@ -459,6 +470,7 @@ export const REGISTRY: Record = { baseUrl: "https://api.openai.com/v1/chat/completions", authType: "apikey", authHeader: "bearer", + defaultContextLength: 128000, models: [ { id: "gpt-4o", name: "GPT-4o" }, { id: "gpt-4o-mini", name: "GPT-4o Mini" }, @@ -480,6 +492,7 @@ export const REGISTRY: Record = { urlSuffix: "?beta=true", authType: "apikey", authHeader: "x-api-key", + defaultContextLength: 200000, headers: { "Anthropic-Version": "2023-06-01", }, @@ -504,6 +517,7 @@ export const REGISTRY: Record = { authType: "apikey", authHeader: "Authorization", authPrefix: "Bearer", + defaultContextLength: 200000, models: [ { id: "glm-5", name: "GLM-5" }, { id: "kimi-k2.5", name: "Kimi K2.5" }, @@ -521,6 +535,7 @@ export const REGISTRY: Record = { authType: "apikey", authHeader: "Authorization", authPrefix: "Bearer", + defaultContextLength: 200000, models: [ { id: "minimax-m2.5-free", name: "MiniMax M2.5 Free" }, { id: "big-pickle", name: "Big Pickle" }, @@ -536,6 +551,7 @@ export const REGISTRY: Record = { baseUrl: "https://openrouter.ai/api/v1/chat/completions", authType: "apikey", authHeader: "bearer", + defaultContextLength: 128000, headers: { "HTTP-Referer": "https://endpoint-proxy.local", "X-Title": "Endpoint Proxy", @@ -549,6 +565,7 @@ export const REGISTRY: Record = { format: "claude", executor: "default", baseUrl: "https://api.z.ai/api/anthropic/v1/messages", + defaultContextLength: 200000, urlSuffix: "?beta=true", authType: "apikey", authHeader: "x-api-key", diff --git a/open-sse/services/contextManager.ts b/open-sse/services/contextManager.ts index 3c93028ccb..194a252091 100644 --- a/open-sse/services/contextManager.ts +++ b/open-sse/services/contextManager.ts @@ -5,14 +5,34 @@ * 3 layers: trim tool messages, compress thinking, aggressive purification. */ -// Default token limits per provider (rough estimates based on model context windows) -const DEFAULT_LIMITS = { +import { REGISTRY } from "../config/providerRegistry.ts"; + +// Default token limits per provider (fallbacks when not in registry) +const DEFAULT_LIMITS: Record = { claude: 200000, openai: 128000, gemini: 1000000, + codex: 400000, default: 128000, }; +// Environment variable overrides (highest priority) +function getEnvOverride(provider: string): number | null { + const envKey = `CONTEXT_LENGTH_${provider.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`; + const envValue = process.env[envKey]; + if (envValue) { + const parsed = parseInt(envValue, 10); + if (!isNaN(parsed) && parsed > 0) return parsed; + } + // Global override + const globalValue = process.env.CONTEXT_LENGTH_DEFAULT; + if (globalValue) { + const parsed = parseInt(globalValue, 10); + if (!isNaN(parsed) && parsed > 0) return parsed; + } + return null; +} + // Rough chars-per-token ratio for quick estimation const CHARS_PER_TOKEN = 4; @@ -27,9 +47,20 @@ export function estimateTokens(text) { /** * Get token limit for a provider/model combination + * Priority: Env override > Registry defaultContextLength > DEFAULT_LIMITS */ export function getTokenLimit(provider, model = null) { - // Check if model has a known limit + // 1. Check environment variable override first + const envOverride = getEnvOverride(provider); + if (envOverride) return envOverride; + + // 2. Check registry for provider default + const registryEntry = REGISTRY[provider]; + if (registryEntry?.defaultContextLength) { + return registryEntry.defaultContextLength; + } + + // 3. Check if model name hints at a known limit if (model) { const lower = model.toLowerCase(); if (lower.includes("claude")) return DEFAULT_LIMITS.claude; @@ -38,10 +69,13 @@ export function getTokenLimit(provider, model = null) { lower.includes("gpt") || lower.includes("o1") || lower.includes("o3") || - lower.includes("o4") + lower.includes("o4") || + lower.includes("codex") ) - return DEFAULT_LIMITS.openai; + return DEFAULT_LIMITS.codex; } + + // 4. Fallback to DEFAULT_LIMITS or default return DEFAULT_LIMITS[provider] || DEFAULT_LIMITS.default; } diff --git a/src/app/api/v1/models/catalog.ts b/src/app/api/v1/models/catalog.ts index ae202e0bdc..2798c2c954 100644 --- a/src/app/api/v1/models/catalog.ts +++ b/src/app/api/v1/models/catalog.ts @@ -16,6 +16,7 @@ import { getAllAudioModels } from "@omniroute/open-sse/config/audioRegistry.ts"; import { getAllModerationModels } from "@omniroute/open-sse/config/moderationRegistry.ts"; import { getAllVideoModels, getVideoProvider } from "@omniroute/open-sse/config/videoRegistry.ts"; import { getAllMusicModels, getMusicProvider } from "@omniroute/open-sse/config/musicRegistry.ts"; +import { REGISTRY } from "@omniroute/open-sse/config/providerRegistry.ts"; const FALLBACK_ALIAS_TO_PROVIDER = { ag: "antigravity", @@ -190,6 +191,7 @@ export async function getUnifiedModelsResponse( permission: [], root: combo.name, parent: null, + ...(combo.context_length ? { context_length: combo.context_length } : {}), }); } @@ -206,8 +208,15 @@ export async function getUnifiedModelsResponse( continue; } + // Get default context length from registry (provider-level default) + const registryEntry = REGISTRY[alias] || REGISTRY[canonicalProviderId]; + const defaultContextLength = registryEntry?.defaultContextLength; + for (const model of providerModels) { const aliasId = `${alias}/${model.id}`; + // Model-level context length overrides provider default + const contextLength = model.contextLength || defaultContextLength; + models.push({ id: aliasId, object: "model", @@ -216,6 +225,7 @@ export async function getUnifiedModelsResponse( permission: [], root: model.id, parent: null, + ...(contextLength ? { context_length: contextLength } : {}), }); // Add provider-id prefix in addition to short alias (ex: kiro/model + kr/model). @@ -229,6 +239,7 @@ export async function getUnifiedModelsResponse( permission: [], root: model.id, parent: aliasId, + ...(contextLength ? { context_length: contextLength } : {}), }); } } diff --git a/src/shared/validation/schemas.ts b/src/shared/validation/schemas.ts index 70d87421c0..91d83e6038 100644 --- a/src/shared/validation/schemas.ts +++ b/src/shared/validation/schemas.ts @@ -107,6 +107,7 @@ export const createComboSchema = z.object({ system_message: z.string().max(50000).optional(), tool_filter_regex: z.string().max(1000).optional(), context_cache_protection: z.boolean().optional(), + context_length: z.number().int().min(1000).max(2000000).optional(), }); // ──── Auto-Combo Schemas ────