feat(claude): support [1m] suffix for 1M context window

Parse [1m] suffix from model name (e.g. claude-sonnet-4-6[1m]) and
propagate extendedContext flag through the request pipeline to append
context-1m-2025-08-07 to the Anthropic-Beta header.
This commit is contained in:
DavyMassoneto
2026-03-10 23:57:13 -03:00
parent c7ae9c30c2
commit 623c63baf6
5 changed files with 80 additions and 18 deletions
+25 -1
View File
@@ -40,6 +40,7 @@ export type ExecuteInput = {
credentials: ProviderCredentials;
signal?: AbortSignal | null;
log?: ExecutorLog | null;
extendedContext?: boolean;
};
function mergeAbortSignals(primary: AbortSignal, secondary: AbortSignal): AbortSignal {
@@ -174,7 +175,7 @@ export class BaseExecutor {
return { status: response.status, message: bodyText || `HTTP ${response.status}` };
}
async execute({ model, body, stream, credentials, signal, log }: ExecuteInput) {
async execute({ model, body, stream, credentials, signal, log, extendedContext }: ExecuteInput) {
const fallbackCount = this.getFallbackCount();
let lastError: unknown = null;
let lastStatus = 0;
@@ -182,6 +183,29 @@ export class BaseExecutor {
for (let urlIndex = 0; urlIndex < fallbackCount; urlIndex++) {
const url = this.buildUrl(model, stream, urlIndex, credentials);
const headers = this.buildHeaders(credentials, stream);
// Append 1M context beta header when [1m] suffix was used
// Only supported for specific Claude models per Anthropic docs
if (extendedContext) {
const EXTENDED_CONTEXT_MODELS = [
"claude-opus-4-6",
"claude-sonnet-4-6",
"claude-sonnet-4-5",
"claude-sonnet-4",
];
const baseModel = model.replace(/-\d{8}$/, "");
if (
EXTENDED_CONTEXT_MODELS.some((m) => baseModel === m || model === m || model.startsWith(m))
) {
const existing = headers["Anthropic-Beta"];
if (existing) {
headers["Anthropic-Beta"] = existing + ",context-1m-2025-08-07";
} else {
headers["Anthropic-Beta"] = "context-1m-2025-08-07";
}
}
}
const transformedBody = this.transformRequest(model, body, stream, credentials);
try {
+3 -1
View File
@@ -68,7 +68,7 @@ export async function handleChatCore({
userAgent,
comboName,
}) {
const { provider, model } = modelInfo;
const { provider, model, extendedContext } = modelInfo;
const startTime = Date.now();
// ── Phase 9.2: Idempotency check ──
@@ -270,6 +270,7 @@ export async function handleChatCore({
credentials,
signal: streamController.signal,
log,
extendedContext,
})
);
@@ -357,6 +358,7 @@ export async function handleChatCore({
credentials,
signal: streamController.signal,
log,
extendedContext,
});
if (retryResult.response.ok) {
+35 -9
View File
@@ -59,29 +59,50 @@ function resolveProviderModelAlias(providerOrAlias, modelId) {
/**
* Parse model string: "alias/model" or "provider/model" or just alias
* Supports [1m] suffix for extended 1M context window (e.g. "claude-sonnet-4-6[1m]")
*/
export function parseModel(modelStr) {
if (!modelStr) {
return { provider: null, model: null, isAlias: false, providerAlias: null };
return {
provider: null,
model: null,
isAlias: false,
providerAlias: null,
extendedContext: false,
};
}
// Sanitize: reject strings with path traversal or control characters
if (/\.\.[\/\\]/.test(modelStr) || /[\x00-\x1f]/.test(modelStr)) {
console.log(`[MODEL] Warning: rejected malformed model string: "${modelStr.substring(0, 50)}"`);
return { provider: null, model: null, isAlias: false, providerAlias: null };
return {
provider: null,
model: null,
isAlias: false,
providerAlias: null,
extendedContext: false,
};
}
// Extract [1m] suffix before parsing provider/model
let extendedContext = false;
let cleanStr = modelStr;
if (cleanStr.endsWith("[1m]")) {
extendedContext = true;
cleanStr = cleanStr.slice(0, -4);
}
// Check if standard format: provider/model or alias/model
if (modelStr.includes("/")) {
const firstSlash = modelStr.indexOf("/");
const providerOrAlias = modelStr.slice(0, firstSlash);
const model = modelStr.slice(firstSlash + 1);
if (cleanStr.includes("/")) {
const firstSlash = cleanStr.indexOf("/");
const providerOrAlias = cleanStr.slice(0, firstSlash);
const model = cleanStr.slice(firstSlash + 1);
const provider = resolveProviderAlias(providerOrAlias);
return { provider, model, isAlias: false, providerAlias: providerOrAlias };
return { provider, model, isAlias: false, providerAlias: providerOrAlias, extendedContext };
}
// Alias format (model alias, not provider alias)
return { provider: null, model: modelStr, isAlias: true, providerAlias: null };
return { provider: null, model: cleanStr, isAlias: true, providerAlias: null, extendedContext };
}
/**
@@ -123,12 +144,14 @@ export function resolveModelAliasFromMap(alias, aliases) {
*/
export async function getModelInfoCore(modelStr, aliasesOrGetter) {
const parsed = parseModel(modelStr);
const { extendedContext } = parsed;
if (!parsed.isAlias) {
const canonicalModel = resolveProviderModelAlias(parsed.provider, parsed.model);
return {
provider: parsed.provider,
model: canonicalModel,
extendedContext,
};
}
@@ -142,6 +165,7 @@ export async function getModelInfoCore(modelStr, aliasesOrGetter) {
return {
provider: resolved.provider,
model: canonicalModel,
extendedContext,
};
}
@@ -153,6 +177,7 @@ export async function getModelInfoCore(modelStr, aliasesOrGetter) {
return {
provider: "openai",
model: modelId,
extendedContext,
};
}
@@ -160,7 +185,7 @@ export async function getModelInfoCore(modelStr, aliasesOrGetter) {
if (nonOpenAIProviders.length === 1) {
const provider = nonOpenAIProviders[0];
const canonicalModel = resolveProviderModelAlias(provider, modelId);
return { provider, model: canonicalModel };
return { provider, model: canonicalModel, extendedContext };
}
if (nonOpenAIProviders.length > 1) {
@@ -182,5 +207,6 @@ export async function getModelInfoCore(modelStr, aliasesOrGetter) {
return {
provider: "openai",
model: modelId,
extendedContext,
};
}
+9 -6
View File
@@ -227,7 +227,7 @@ async function handleSingleModelChat(
const resolved = await resolveModelOrError(modelStr, body);
if (resolved.error) return resolved.error;
const { provider, model, sourceFormat, targetFormat } = resolved;
const { provider, model, sourceFormat, targetFormat, extendedContext } = resolved;
// 2. Pipeline gates (availability + circuit breaker)
const gate = checkPipelineGates(provider, model);
@@ -290,6 +290,7 @@ async function handleSingleModelChat(
apiKeyInfo,
userAgent,
comboName,
extendedContext,
});
if (telemetry) telemetry.endPhase();
@@ -366,7 +367,7 @@ async function resolveModelOrError(modelStr: string, body: any) {
return { error: errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid model format") };
}
const { provider, model } = modelInfo;
const { provider, model, extendedContext } = modelInfo;
const sourceFormat = detectFormat(body);
const providerAlias = PROVIDER_ID_TO_ALIAS[provider] || provider;
@@ -378,13 +379,14 @@ async function resolveModelOrError(modelStr: string, body: any) {
log.info("ROUTING", `Custom model apiFormat=responses → targetFormat=openai-responses`);
}
const ctxTag = extendedContext && providerAlias === "claude" ? " [1m]" : "";
if (modelStr !== `${provider}/${model}`) {
log.info("ROUTING", `${modelStr}${provider}/${model}`);
log.info("ROUTING", `${modelStr}${provider}/${model}${ctxTag}`);
} else {
log.info("ROUTING", `Provider: ${provider}, Model: ${model}`);
log.info("ROUTING", `Provider: ${provider}, Model: ${model}${ctxTag}`);
}
return { provider, model, sourceFormat, targetFormat };
return { provider, model, sourceFormat, targetFormat, extendedContext };
}
/**
@@ -437,6 +439,7 @@ async function executeChatWithBreaker({
apiKeyInfo,
userAgent,
comboName,
extendedContext,
}: any): Promise<{ result: any; tlsFingerprintUsed: boolean }> {
let tlsFingerprintUsed = false;
@@ -445,7 +448,7 @@ async function executeChatWithBreaker({
runWithProxyContext(proxyInfo?.proxy || null, () =>
(handleChatCore as any)({
body: { ...body, model: `${provider}/${model}` },
modelInfo: { provider, model },
modelInfo: { provider, model, extendedContext },
credentials: refreshedCredentials,
log: logger,
clientRawRequest,
+8 -1
View File
@@ -39,6 +39,7 @@ async function lookupCustomModelApiFormat(
*/
export async function getModelInfo(modelStr) {
const parsed = parseModel(modelStr);
const { extendedContext } = parsed;
// Check custom provider nodes first (for both alias and non-alias formats)
if (parsed.providerAlias || parsed.provider) {
@@ -53,7 +54,12 @@ export async function getModelInfo(modelStr) {
matchedOpenAI.id as string,
parsed.model as string
);
return { provider: matchedOpenAI.id, model: parsed.model, ...(apiFormat && { apiFormat }) };
return {
provider: matchedOpenAI.id,
model: parsed.model,
extendedContext,
...(apiFormat && { apiFormat }),
};
}
// Check Anthropic Compatible nodes
@@ -67,6 +73,7 @@ export async function getModelInfo(modelStr) {
return {
provider: matchedAnthropic.id,
model: parsed.model,
extendedContext,
...(apiFormat && { apiFormat }),
};
}