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
38 changes: 17 additions & 21 deletions open-sse/executors/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import { getRotatingApiKey } from "../services/apiKeyRotator.ts";
import { getOpenAICompatibleType, isClaudeCodeCompatible } from "../services/provider.ts";
import type { ProviderRequestDefaults } from "../services/providerRequestDefaults.ts";
import { signRequestBody } from "../services/claudeCodeCCH.ts";
import {
appendAnthropicBetaHeader,
CONTEXT_1M_BETA_HEADER,
modelSupportsContext1mBeta,
} from "../services/claudeCodeCompatible.ts";
import { getClaudeCodeCompatibleRequestDefaults } from "@/lib/providers/requestDefaults";

/**
* Sanitizes a custom API path to prevent path traversal attacks.
Expand Down Expand Up @@ -386,27 +392,17 @@ export class BaseExecutor {
const headers = this.buildHeaders(activeCredentials, stream);
applyConfiguredUserAgent(headers, activeCredentials?.providerSpecificData);

// 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-7",
"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 ccRequestDefaults = isClaudeCodeCompatible(this.provider)
? getClaudeCodeCompatibleRequestDefaults(activeCredentials?.providerSpecificData)
: {};
const shouldForwardExtendedContext =
extendedContext &&
modelSupportsContext1mBeta(model) &&
!isClaudeCodeCompatible(this.provider);
const shouldForwardCcCompatibleContext1m =
isClaudeCodeCompatible(this.provider) && ccRequestDefaults.context1m === true;

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

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

shouldForwardCcCompatibleContext1m appends the 1M beta header for CC-compatible providers without checking whether the selected model supports the context-1m beta. Since modelSupportsContext1mBeta() and the supported model list already exist (and are used for extendedContext), consider applying the same model gating here to avoid sending anthropic-beta: context-1m-2025-08-07 on unsupported models.

Suggested change
isClaudeCodeCompatible(this.provider) && ccRequestDefaults.context1m === true;
isClaudeCodeCompatible(this.provider) &&
ccRequestDefaults.context1m === true &&
modelSupportsContext1mBeta(model);

Copilot uses AI. Check for mistakes.
if (shouldForwardExtendedContext || shouldForwardCcCompatibleContext1m) {
appendAnthropicBetaHeader(headers, CONTEXT_1M_BETA_HEADER);
}

const transformedBody = await this.transformRequest(model, body, stream, activeCredentials);
Expand Down
39 changes: 39 additions & 0 deletions open-sse/services/claudeCodeCompatible.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,16 @@ export const CLAUDE_CODE_COMPATIBLE_DEFAULT_MAX_TOKENS = 8092;
export const CLAUDE_CODE_COMPATIBLE_ANTHROPIC_VERSION = "2023-06-01";
export const CLAUDE_CODE_COMPATIBLE_ANTHROPIC_BETA =
"claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,effort-2025-11-24,token-efficient-tools-2025-02-19";
export const CONTEXT_1M_BETA_HEADER = "context-1m-2025-08-07";
export const CLAUDE_CODE_COMPATIBLE_VERSION = "2.1.87";
export const CLAUDE_CODE_COMPATIBLE_USER_AGENT = `claude-cli/${CLAUDE_CODE_COMPATIBLE_VERSION} (external, cli)`;
const CONTEXT_1M_SUPPORTED_MODELS = [
"claude-opus-4-7",
"claude-opus-4-6",
"claude-sonnet-4-6",
"claude-sonnet-4-5",
"claude-sonnet-4",
];
/**
* Build the billing header dynamically with fingerprint and CCH placeholder.
* The cch=00000 placeholder is later replaced by signRequestBody().
Expand Down Expand Up @@ -116,6 +124,37 @@ export function joinClaudeCodeCompatibleUrl(baseUrl: string, path: string): stri
return joinNormalizedBaseUrlAndPath(stripClaudeCodeCompatibleEndpointSuffix(baseUrl), path);
}

export function appendAnthropicBetaHeader(
headers: Record<string, string>,
betaHeader: string
): void {
const existingKey = Object.keys(headers).find((key) => key.toLowerCase() === "anthropic-beta");
if (!existingKey) {
headers["anthropic-beta"] = betaHeader;
return;
}

const existingValues = String(headers[existingKey] || "")
.split(",")
.map((value) => value.trim())
.filter(Boolean);

if (!existingValues.includes(betaHeader)) {
headers[existingKey] = [...existingValues, betaHeader].join(",");
}
}

export function modelSupportsContext1mBeta(model: string | null | undefined): boolean {
const normalizedModel = String(model || "")
.trim()
.toLowerCase()
.replace(/-\d{8}$/, "");

return CONTEXT_1M_SUPPORTED_MODELS.some(
(supported) => normalizedModel === supported || normalizedModel.startsWith(`${supported}-`)
);
}

export function buildClaudeCodeCompatibleHeaders(
apiKey: string,
stream = false,
Expand Down
59 changes: 58 additions & 1 deletion src/app/(dashboard)/dashboard/providers/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ import { resolveManagedModelAlias } from "@/shared/utils/providerModelAliases";
import { maskEmail, pickMaskedDisplayValue, pickDisplayValue } from "@/shared/utils/maskEmail";
import useEmailPrivacyStore from "@/store/emailPrivacyStore";
import EmailPrivacyToggle from "@/shared/components/EmailPrivacyToggle";
import { getCodexRequestDefaults as _getCodexRequestDefaults } from "@/lib/providers/requestDefaults";
import {
getClaudeCodeCompatibleRequestDefaults as _getClaudeCodeCompatibleRequestDefaults,
getCodexRequestDefaults as _getCodexRequestDefaults,
} from "@/lib/providers/requestDefaults";

type CompatByProtocolMap = Partial<
Record<
Expand Down Expand Up @@ -573,6 +576,15 @@ function getCodexRequestDefaults(providerSpecificData: unknown): {
};
}

function getClaudeCodeCompatibleRequestDefaults(providerSpecificData: unknown): {
context1m: boolean;
} {
const defaults = _getClaudeCodeCompatibleRequestDefaults(providerSpecificData);
return {
context1m: defaults.context1m === true,
};
}

function compatProtocolLabelKey(protocol: string): string {
if (protocol === "openai") return "compatProtocolOpenAI";
if (protocol === "openai-responses") return "compatProtocolOpenAIResponses";
Expand Down Expand Up @@ -5186,6 +5198,7 @@ function AddApiKeyModal({
customUserAgent: "",
accountId: "",
consoleApiKey: "",
ccCompatibleContext1m: false,
});
const [validating, setValidating] = useState(false);
const [validationResult, setValidationResult] = useState(null);
Expand Down Expand Up @@ -5278,6 +5291,9 @@ function AddApiKeyModal({
} else if (isCloudflare && formData.accountId.trim()) {
providerSpecificData.accountId = formData.accountId.trim();
}
if (isCcCompatible && formData.ccCompatibleContext1m) {
providerSpecificData.requestDefaults = { context1m: true };
}

const payload = {
name: formData.name,
Expand Down Expand Up @@ -5352,6 +5368,16 @@ function AddApiKeyModal({
{saveError}
</div>
)}
{isCcCompatible && (
<div className="flex flex-col gap-4 rounded-lg border border-border/50 bg-surface/20 p-4">
<Toggle
checked={formData.ccCompatibleContext1m}
onChange={(checked) => setFormData({ ...formData, ccCompatibleContext1m: checked })}
label="CC Compatible 1M Context"
description="When enabled, this connection appends `anthropic-beta: context-1m-2025-08-07`."
/>
</div>
)}
{isCompatible && (
<p className="text-xs text-text-muted">
{isCcCompatible
Expand Down Expand Up @@ -5526,6 +5552,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
codexFastServiceTier: false,
codexOpenaiStoreEnabled: false,
consoleApiKey: "",
ccCompatibleContext1m: false,
});
const [testing, setTesting] = useState(false);
const [testResult, setTestResult] = useState(null);
Expand All @@ -5545,6 +5572,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
const isGlm = connection?.provider === "glm" || connection?.provider === "glmt";
const isCloudflare = connection?.provider === "cloudflare-ai";
const isCodex = connection?.provider === "codex";
const isCcCompatible = isClaudeCodeCompatibleProvider(connection?.provider);
const defaultRegion = "us-central1";

useEffect(() => {
Expand All @@ -5559,6 +5587,9 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
const rawAccountId = connection.providerSpecificData?.accountId;
const existingAccountId = typeof rawAccountId === "string" ? rawAccountId : "";
const codexRequestDefaults = getCodexRequestDefaults(connection.providerSpecificData);
const ccRequestDefaults = getClaudeCodeCompatibleRequestDefaults(
connection.providerSpecificData
);
const rawConsoleApiKey = connection.providerSpecificData?.consoleApiKey;
const existingConsoleApiKey = typeof rawConsoleApiKey === "string" ? rawConsoleApiKey : "";
setFormData({
Expand All @@ -5577,6 +5608,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
codexFastServiceTier: codexRequestDefaults.serviceTier === "priority",
codexOpenaiStoreEnabled: connection.providerSpecificData?.openaiStoreEnabled === true,
consoleApiKey: existingConsoleApiKey,
ccCompatibleContext1m: ccRequestDefaults.context1m,
});
// Load existing extra keys from providerSpecificData
const existing = connection.providerSpecificData?.extraApiKeys;
Expand Down Expand Up @@ -5728,6 +5760,21 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
} else if (isCloudflare && formData.accountId.trim()) {
updates.providerSpecificData.accountId = formData.accountId.trim();
}
if (isCcCompatible) {
const currentRequestDefaults =
updates.providerSpecificData.requestDefaults &&
typeof updates.providerSpecificData.requestDefaults === "object" &&
!Array.isArray(updates.providerSpecificData.requestDefaults)
? { ...(updates.providerSpecificData.requestDefaults as Record<string, unknown>) }
: {};
if (formData.ccCompatibleContext1m) {
currentRequestDefaults.context1m = true;
} else {
delete currentRequestDefaults.context1m;
}
updates.providerSpecificData.requestDefaults =
Object.keys(currentRequestDefaults).length > 0 ? currentRequestDefaults : undefined;

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

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

In EditConnectionModal, when toggling CC Compatible 1M Context off and there are no other requestDefaults keys, updates.providerSpecificData.requestDefaults is set to undefined. Because the update payload is JSON-stringified, undefined fields are omitted, so the server merge will keep the existing requestDefaults (and context1m may never be cleared). Send an explicit value to overwrite/clear it (e.g. null or {} so normalization can remove it) when the object becomes empty.

Suggested change
Object.keys(currentRequestDefaults).length > 0 ? currentRequestDefaults : undefined;
Object.keys(currentRequestDefaults).length > 0 ? currentRequestDefaults : {};

Copilot uses AI. Check for mistakes.
}
} else {
// Also persist tag for OAuth accounts
updates.providerSpecificData = {
Expand Down Expand Up @@ -5802,6 +5849,16 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
/>
</div>
)}
{isCcCompatible && (
<div className="flex flex-col gap-4 rounded-lg border border-border/50 bg-surface/20 p-4">
<Toggle
checked={formData.ccCompatibleContext1m}
onChange={(checked) => setFormData({ ...formData, ccCompatibleContext1m: checked })}
label="CC Compatible 1M Context"
description="When enabled, this connection appends `anthropic-beta: context-1m-2025-08-07`."
/>
</div>
)}
{isOAuth && connection.email && (
<div className="bg-sidebar/50 p-3 rounded-lg">
<p className="text-sm text-text-muted mb-1">{t("email")}</p>
Expand Down
33 changes: 33 additions & 0 deletions src/lib/providers/requestDefaults.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
type JsonRecord = Record<string, unknown>;
const CLAUDE_CODE_COMPATIBLE_PROVIDER_PREFIX = "anthropic-compatible-cc-";

export const CODEX_REASONING_EFFORT_VALUES = ["none", "low", "medium", "high", "xhigh"] as const;

Expand All @@ -20,6 +21,12 @@ function hasNonEmptyString(value: unknown): boolean {
return typeof value === "string" && value.trim().length > 0;
}

function isClaudeCodeCompatibleProvider(provider: string | null | undefined): boolean {
return (
typeof provider === "string" && provider.startsWith(CLAUDE_CODE_COMPATIBLE_PROVIDER_PREFIX)
);
}
Comment on lines 1 to +28

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

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

isClaudeCodeCompatibleProvider() and the anthropic-compatible-cc- prefix are redefined here, but similar helpers/constants already exist elsewhere (e.g. src/shared/constants/providers.ts and open-sse/services/claudeCodeCompatible.ts). Duplicating the prefix logic risks drift if the identifier format changes; prefer importing a single shared helper/constant rather than maintaining a third copy.

Copilot uses AI. Check for mistakes.

export function normalizeCodexReasoningEffort(value: unknown): CodexReasoningEffort | undefined {
const normalized = normalizeString(value);
if (!normalized || !CODEX_REASONING_EFFORT_SET.has(normalized)) {
Expand All @@ -35,6 +42,10 @@ export function normalizeCodexServiceTier(value: unknown): "priority" | undefine
return undefined;
}

export function normalizeClaudeCodeCompatibleContext1m(value: unknown): true | undefined {
return value === true ? true : undefined;
}

export function normalizeRequestDefaults(
provider: string | null | undefined,
value: unknown
Expand All @@ -60,6 +71,15 @@ export function normalizeRequestDefaults(
}
}

if (isClaudeCodeCompatibleProvider(provider)) {
const context1m = normalizeClaudeCodeCompatibleContext1m(record.context1m);
if (context1m) {
normalized.context1m = true;
} else {
delete normalized.context1m;
}
}

return Object.keys(normalized).length > 0 ? normalized : undefined;
}

Expand Down Expand Up @@ -151,3 +171,16 @@ export function getCodexRequestDefaults(providerSpecificData: unknown): {
...(serviceTier ? { serviceTier } : {}),
};
}

export function getClaudeCodeCompatibleRequestDefaults(providerSpecificData: unknown): {
context1m?: true;
} {
const defaults = getProviderRequestDefaults(
"anthropic-compatible-cc-default",
providerSpecificData
);
const context1m = normalizeClaudeCodeCompatibleContext1m(defaults.context1m);
return {
...(context1m ? { context1m } : {}),
};
}
9 changes: 9 additions & 0 deletions src/shared/validation/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,15 @@ function validateProviderSpecificData(
path: ["requestDefaults", "serviceTier"],
});
}

const context1m = requestDefaultsRecord.context1m;
if (context1m !== undefined && context1m !== null && typeof context1m !== "boolean") {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "providerSpecificData.requestDefaults.context1m must be a boolean",
path: ["requestDefaults", "context1m"],
});
}
}
}

Expand Down
Loading