Skip to content
Merged
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
5 changes: 3 additions & 2 deletions config/quality/file-size-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@
"_rebaseline_2026_06_16_3954_cooldown_epoch": "Issue #3954 own growth: accountFallback.ts 1708->1727 (+19 = a shared cooldownUntilMs() normalizer + its use in isAccountUnavailable/getEarliestRateLimitedUntil/filterAvailableAccounts so a rate_limited_until persisted as a numeric-epoch string is honored, not parsed to NaN) and auth.ts 2216->2219 (+3 = parseFutureDateMs reuses cooldownUntilMs). Cohesive cooldown read-path hardening at the existing chokepoints; one helper, not extractable.",
"_rebaseline_2026_06_16_3960_engine_breakdown": "PR #3960 capture own growth: chatCore.ts 5851->5868 (+17 = persist result.stats.engineBreakdown into the new compression_engine_breakdown table after a stacked compression run, so getPerEngineAnalytics is accurate historically and not live-only). Cohesive analytics-persistence at the existing compression chokepoint; structural shrink of chatCore.ts tracked in #3501.",
"_rebaseline_2026_06_15_3938_perplexity_v218": "PR #3938 own growth: perplexity-web.ts 868->939 (+71 = rebuild buildPplxRequestBody to mirror the current www.perplexity.ai schematized request body — version 2.18, use_schematized_api + the full supported_block_use_cases list, dsl_query, shared requestId for frontend_uuid/client_search_results_cache_key, last_backend_uuid only on follow-ups — plus the x-perplexity-request-* / x-request-id headers replacing the stale X-App-ApiVersion pair that triggered HTTP 400). Cohesive upstream-schema sync in a single executor; not extractable.",
"_rebaseline_2026_06_16_4021_context_editing": "PR #4021 own growth: base.ts 1222->1244 (+22 = inject delegated Context Editing at the single Claude pre-serialization chokepoint — applyContextEditingToBody() call gated to the genuine `claude` provider, the contextEditing field on ExecuteInput, its destructure, and a debug log) and chatCore.ts 5868->5875 (+7 = capture contextEditing.enabled at the canonical compression-settings read into a function-scoped flag, threaded to the two executor.execute() callsites). The reusable edit-builder + strategy constants live in the new small open-sse/config/contextEditing.ts (well under cap). Cohesive opt-in feature at the existing dispatch chokepoint; not extractable without hiding the Claude body-finalization boundary.",
"cap": 800,
"frozen": {
"open-sse/config/providerRegistry.ts": 4731,
"open-sse/executors/antigravity.ts": 1664,
"open-sse/executors/base.ts": 1222,
"open-sse/executors/base.ts": 1244,
"open-sse/executors/chatgpt-web.ts": 2870,
"open-sse/executors/claude-web.ts": 1057,
"open-sse/executors/codex.ts": 1447,
Expand All @@ -37,7 +38,7 @@
"open-sse/executors/muse-spark-web.ts": 1284,
"open-sse/executors/perplexity-web.ts": 1013,
"open-sse/handlers/audioSpeech.ts": 965,
"open-sse/handlers/chatCore.ts": 5868,
"open-sse/handlers/chatCore.ts": 5875,
"open-sse/handlers/imageGeneration.ts": 3777,
"open-sse/handlers/responseSanitizer.ts": 1103,
"open-sse/handlers/search.ts": 1442,
Expand Down
84 changes: 84 additions & 0 deletions open-sse/config/contextEditing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* Anthropic Context Editing — delegated, server-side compression for Claude.
*
* Unlike OmniRoute's local compression engines (which rewrite the request body
* before it leaves the proxy), Context Editing asks the *provider* to clear
* stale tool-use/tool-result blocks from the running context window. We only
* attach a body param (`context_management.edits[]`); Claude does the clearing
* against its own tokenizer. The beta is advertised via the `anthropic-beta:
* context-management-2025-06-27` header, which OmniRoute already emits on Claude
* requests (see `anthropicHeaders.ts`).
*
* This is a Claude-only capability by nature — other providers would reject the
* param. Callers MUST gate invocation to the genuine Claude provider; this
* module does not inspect the provider itself.
*
* Strategy ids and shapes are pinned from live Anthropic docs
* (platform.claude.com/docs/en/build-with-claude/context-editing).
*/

/** Dated strategy id that clears old tool-use / tool-result pairs. */
export const CLEAR_TOOL_USES_STRATEGY = "clear_tool_uses_20250919";

/** Dated strategy id that clears old extended-thinking turns. */
export const CLEAR_THINKING_STRATEGY = "clear_thinking_20251015";

/** Default token threshold that triggers clearing (Anthropic default is 100k). */
export const CONTEXT_EDITING_DEFAULT_TRIGGER_TOKENS = 100000;

/** Recent tool-use/result pairs kept untouched when clearing. */
export const CONTEXT_EDITING_DEFAULT_KEEP_TOOL_USES = 3;

type ContextEditingEdit = { type?: unknown; [key: string]: unknown };

/**
* Mutate an Anthropic Messages request body in place to delegate context
* clearing to the provider. Adds the `clear_tool_uses_20250919` strategy to
* `context_management.edits[]`, composing with any `clear_thinking_20251015`
* edit already present — Anthropic requires `clear_thinking` to be listed first,
* so thinking edits are stable-sorted to the front.
*
* Idempotent: if a `clear_tool_uses` edit already exists (added by a previous
* call or supplied by the client), the body is left as-is. No-op when disabled
* or when `body` is not a plain object.
*
* @param body The outbound Anthropic-format request body (mutated in place).
* @param opts.enabled Whether Context Editing is on for this request.
*/
export function applyContextEditingToBody(
body: Record<string, unknown> | null | undefined,
opts: { enabled: boolean }
): void {
if (!opts.enabled || !body || typeof body !== "object") return;

const existing =
body.context_management && typeof body.context_management === "object"
? (body.context_management as Record<string, unknown>)
: {};

const edits: ContextEditingEdit[] = Array.isArray(existing.edits)
? [...(existing.edits as ContextEditingEdit[])]
: [];

const hasToolUseEdit = edits.some(
(edit) => edit && edit.type === CLEAR_TOOL_USES_STRATEGY
);

if (!hasToolUseEdit) {
edits.push({
type: CLEAR_TOOL_USES_STRATEGY,
trigger: { type: "input_tokens", value: CONTEXT_EDITING_DEFAULT_TRIGGER_TOKENS },
keep: { type: "tool_uses", value: CONTEXT_EDITING_DEFAULT_KEEP_TOOL_USES },
});
// Anthropic requires clear_thinking edits to precede clear_tool_uses. Array
// sort is stable in modern Node, so same-key edits keep their relative order.
edits.sort((a, b) => {
const aRank = a && a.type === CLEAR_THINKING_STRATEGY ? 0 : 1;
const bRank = b && b.type === CLEAR_THINKING_STRATEGY ? 0 : 1;
return aRank - bRank;
});
}

existing.edits = edits;
body.context_management = existing;
}
22 changes: 22 additions & 0 deletions open-sse/executors/base.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { HTTP_STATUS, FETCH_TIMEOUT_MS } from "../config/constants.ts";
import { mergeClientAnthropicBeta } from "../config/anthropicHeaders.ts";
import { applyContextEditingToBody } from "../config/contextEditing.ts";
import { applyFingerprint, isCliCompatEnabled } from "../config/cliFingerprints.ts";
import { supportsClaudeMaxEffort, supportsXHighEffort } from "../config/providerModels.ts";
import type { PoolConfig } from "../services/sessionPool/types.ts";
Expand Down Expand Up @@ -134,6 +135,10 @@ export type ExecuteInput = {
) => Promise<void> | void;
/** When true, skip the intra-URL 429 retry in execute() so the caller handles fallback. */
skipUpstreamRetry?: boolean;
/** Delegated Context Editing (Claude only): when enabled, attach the
* `context_management.clear_tool_uses` strategy so the provider clears stale
* tool-use blocks server-side. Honored only on the genuine `claude` path. */
contextEditing?: { enabled: boolean } | null;
};

export type CountTokensInput = {
Expand Down Expand Up @@ -653,6 +658,7 @@ export class BaseExecutor {
clientHeaders,
skipUpstreamRetry = false,
onCredentialsRefreshed,
contextEditing,
} = input;
const fallbackCount = this.getFallbackCount();
let lastError: unknown = null;
Expand Down Expand Up @@ -1134,6 +1140,22 @@ export class BaseExecutor {
enforceThinkingTemperature(transformedBody as Record<string, unknown>);
}

// Delegated Context Editing (opt-in, genuine Claude API only): attach the
// clear_tool_uses strategy so the provider clears stale tool-use blocks
// server-side. Runs at this same chokepoint, composing with the
// clear_thinking edit the fingerprint path may have already set. Scoped to
// `claude` (real Anthropic key/OAuth) — Claude-compatible relays are left
// out for now since they may not pass the beta (#N1 follow-up).
if (this.provider === "claude" && contextEditing?.enabled) {
applyContextEditingToBody(transformedBody as Record<string, unknown>, {
enabled: true,
});
log?.debug?.(
"CONTEXT_EDITING",
"Delegated context editing on — attached clear_tool_uses to the Claude request"
);
}

let bodyString = JSON.stringify(transformedBody);

const shouldFingerprint =
Expand Down
7 changes: 7 additions & 0 deletions open-sse/handlers/chatCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2388,6 +2388,10 @@ export async function handleChatCore({
let cavemanOutputModeApplied = false;
let cavemanOutputModeIntensity: string | null = null;
let preCompressionBody: typeof body | null = null;
// Delegated Context Editing (Claude only): captured at the canonical compression
// settings read below, then threaded to executor.execute() further down. Lives at
// function scope because the read happens inside the per-message compression block.
let contextEditingEnabled = false;
if (body && Array.isArray(allMessages) && allMessages.length > 0) {
let estimatedTokens = estimateTokens(allMessages);
let promptCompressionEnabled = false;
Expand All @@ -2397,6 +2401,7 @@ export async function handleChatCore({
const { getCompressionSettings } = await import("../../src/lib/db/compression.ts");
compressionSettings = await getCompressionSettings();
promptCompressionEnabled = compressionSettings.enabled;
contextEditingEnabled = compressionSettings.contextEditing?.enabled === true;
} catch (err) {
log?.warn?.(
"COMPRESSION",
Expand Down Expand Up @@ -3921,6 +3926,7 @@ export async function handleChatCore({
),
onCredentialsRefreshed,
skipUpstreamRetry,
contextEditing: { enabled: contextEditingEnabled },
})
),
});
Expand Down Expand Up @@ -4466,6 +4472,7 @@ export async function handleChatCore({
clientHeaders: buildExecutorClientHeaders(clientRawRequest?.headers, userAgent),
onCredentialsRefreshed,
skipUpstreamRetry: isCombo,
contextEditing: { enabled: contextEditingEnabled },
})
);

Expand Down
16 changes: 16 additions & 0 deletions open-sse/services/compression/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,16 @@ export interface CompressionLanguageConfig {
enabledPacks: string[];
}

/**
* Provider-delegated compression (Anthropic "Context Editing", beta
* `context-management-2025-06-27`). Claude/Anthropic only — the provider clears
* old tool-use blocks server-side. This config only carries the on/off flag; the
* request-time header/body injection is a separate slice.
*/
export interface ContextEditingConfig {
enabled: boolean;
}

export interface CompressionPipelineStep {
engine: CompressionEngineId;
intensity?: CavemanIntensity | RtkIntensity;
Expand All @@ -111,6 +121,8 @@ export interface CompressionConfig {
languageConfig?: CompressionLanguageConfig;
aggressive?: AggressiveConfig;
ultra?: UltraConfig;
/** Provider-delegated context editing (Claude/Anthropic only). */
contextEditing?: ContextEditingConfig;
}

export interface CompressionStats {
Expand Down Expand Up @@ -227,6 +239,10 @@ export const DEFAULT_COMPRESSION_LANGUAGE_CONFIG: CompressionLanguageConfig = {
enabledPacks: ["en"],
};

export const DEFAULT_CONTEXT_EDITING_CONFIG: ContextEditingConfig = {
enabled: false,
};

/** Aging thresholds for progressive message degradation (Phase 3) */
export interface AgingThresholds {
fullSummary: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type CompressionMode = "off" | "lite" | "standard" | "aggressive" | "ultra" | "r
interface CompressionSettings {
enabled: boolean;
defaultMode: CompressionMode;
contextEditing?: { enabled: boolean };
[key: string]: unknown;
}

Expand Down Expand Up @@ -122,7 +123,7 @@ export default function CompressionHub() {
if (settingsData) {
setSettings(settingsData as CompressionSettings);
} else {
setSettings({ enabled: false, defaultMode: "off" });
setSettings({ enabled: false, defaultMode: "off", contextEditing: { enabled: false } });
}
if (enginesData?.engines) {
setEngines(
Expand Down Expand Up @@ -531,6 +532,35 @@ export default function CompressionHub() {
</ul>
</div>
)}

{/* ── Compressão delegada ao provedor ── */}
<div className="flex flex-col gap-3">
<h2 className="text-sm font-semibold text-text-main">Compressão delegada ao provedor</h2>
<div className="flex items-center gap-3 rounded-lg border border-border bg-bg p-4">
<div className="min-w-0 flex-1">
<p className="text-sm font-semibold text-text-main">Context Editing (Claude)</p>
<p className="text-xs text-text-muted">
Deixa o próprio provedor limpar blocos antigos de tool-use no servidor, sem reescrever
a mensagem.
</p>
</div>
<Toggle
checked={!!settings?.contextEditing?.enabled}
onChange={() =>
saveSettings({ contextEditing: { enabled: !settings?.contextEditing?.enabled } })
}
ariaLabel="Context Editing"
/>
</div>
<div className="flex items-start gap-2 rounded-lg border border-amber-500/40 bg-amber-500/5 px-3 py-2 text-xs text-amber-500">
<span className="material-symbols-outlined text-[16px]">info</span>
<span>
Hoje disponível apenas para Claude (Anthropic). É um modo delegado: o próprio provedor
limpa blocos antigos de tool-use no servidor — não reescrevemos a mensagem. Não afeta
outros provedores.
</span>
</div>
</div>
</section>
);
}
17 changes: 17 additions & 0 deletions src/lib/db/compression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
DEFAULT_CAVEMAN_OUTPUT_MODE_CONFIG,
DEFAULT_COMPRESSION_LANGUAGE_CONFIG,
DEFAULT_COMPRESSION_CONFIG,
DEFAULT_CONTEXT_EDITING_CONFIG,
DEFAULT_MCP_ACCESSIBILITY_CONFIG,
DEFAULT_RTK_CONFIG,
DEFAULT_ULTRA_CONFIG,
Expand All @@ -17,6 +18,7 @@ import {
type CompressionPipelineStep,
type CompressionConfig,
type CompressionMode,
type ContextEditingConfig,
type McpAccessibilityConfig,
type RtkConfig,
type UltraConfig,
Expand Down Expand Up @@ -198,6 +200,17 @@ function normalizeLanguageConfig(value: unknown): CompressionLanguageConfig {
};
}

function normalizeContextEditingConfig(value: unknown): ContextEditingConfig {
const record = toRecord(value);
return {
...DEFAULT_CONTEXT_EDITING_CONFIG,
enabled:
typeof record.enabled === "boolean"
? record.enabled
: DEFAULT_CONTEXT_EDITING_CONFIG.enabled,
};
}

function normalizeStackedPipeline(value: unknown): CompressionPipelineStep[] {
const source = Array.isArray(value) ? value : (DEFAULT_COMPRESSION_CONFIG.stackedPipeline ?? []);
const pipeline: CompressionPipelineStep[] = [];
Expand Down Expand Up @@ -360,6 +373,7 @@ export async function getCompressionSettings(): Promise<CompressionConfig> {
stackedPipeline: normalizeStackedPipeline(undefined),
aggressive: normalizeAggressiveConfig(undefined),
ultra: normalizeUltraConfig(undefined),
contextEditing: { ...DEFAULT_CONTEXT_EDITING_CONFIG },
};

for (const row of rows) {
Expand Down Expand Up @@ -440,6 +454,9 @@ export async function getCompressionSettings(): Promise<CompressionConfig> {
case "ultraConfig":
config.ultra = normalizeUltraConfig(parsed);
break;
case "contextEditing":
config.contextEditing = normalizeContextEditingConfig(parsed);
break;
}
}

Expand Down
10 changes: 10 additions & 0 deletions src/shared/validation/compressionConfigSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@ export const languageConfigSchema = z
})
.strict();

// Context Editing is a provider-delegated compression mode (Claude/Anthropic only):
// the provider clears old tool-use blocks server-side. This config only carries the
// on/off flag; the request-time header/body injection is a separate slice.
export const contextEditingConfigSchema = z
.object({
enabled: z.boolean().optional(),
})
.strip();

export const aggressiveConfigSchema = z
.object({
thresholds: z
Expand Down Expand Up @@ -159,6 +168,7 @@ export const compressionSettingsUpdateSchema = z
languageConfig: languageConfigSchema.optional(),
aggressive: aggressiveConfigSchema.optional(),
ultra: ultraConfigSchema.optional(),
contextEditing: contextEditingConfigSchema.optional(),
})
.strict();

Expand Down
Loading