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
5 changes: 3 additions & 2 deletions open-sse/handlers/responseTranslator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
} from "../services/geminiThoughtSignatureStore.ts";
import { normalizeOpenAICompatibleFinishReasonString } from "../utils/finishReason.ts";
import { containsTextualToolCallMarker } from "../utils/textualToolCall.ts";
import { stripPlaceholderFromContent } from "../utils/reasoningPlaceholder.ts";

type JsonRecord = Record<string, unknown>;

Expand Down Expand Up @@ -207,7 +208,7 @@ export function translateNonStreamingResponse(

const message: JsonRecord = { role: "assistant" };
if (textContent) {
message.content = textContent;
message.content = stripPlaceholderFromContent(textContent);
}
if (reasoningContent) {
message.reasoning_content = reasoningContent;
Expand Down Expand Up @@ -506,7 +507,7 @@ export function translateNonStreamingResponse(

const message: JsonRecord = { role: "assistant" };
if (textContent) {
message.content = textContent;
message.content = stripPlaceholderFromContent(textContent);
}
if (thinkingContent) {
message.reasoning_content = thinkingContent;
Expand Down
5 changes: 5 additions & 0 deletions open-sse/services/compression/strategySelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -714,11 +714,16 @@ function buildStepOptions(
step: CompressionPipelineStep,
options?: StackOptions
): CompressionEngineApplyOptions {
const headroomConfig =
step.engine === "headroom" && options?.config?.headroom
? options.config.headroom as Record<string, unknown>
: {};
return {
...options,
compressionComboId: options?.compressionComboId ?? options?.config?.compressionComboId,
principalId: options?.principalId,
stepConfig: {
...headroomConfig,
...(step.config ?? {}),
...(step.intensity ? { intensity: step.intensity } : {}),
},
Expand Down
15 changes: 15 additions & 0 deletions open-sse/services/compression/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,12 @@ export interface CompressionConfig {
ultraSlmPrewarm?: boolean;
/** Opt-in result memoization for deterministic engines only (default off). */
memoizeCompressionResults?: boolean;

/**
* Headroom engine detail config (persisted sub-object, #8056).
* On/off + level live in the `engines` map; this stores `minRows`.
*/
headroom?: HeadroomConfig;
}

export interface CompressionStats {
Expand Down Expand Up @@ -407,6 +413,15 @@ export interface AggressiveConfig {
preserveSystemPrompt?: boolean;
}

/** Headroom engine detail config (#8056) */
export interface HeadroomConfig {
minRows?: number;
enabled?: boolean;
}

/** Default headroom configuration (#8056) */
export const DEFAULT_HEADROOM_CONFIG: HeadroomConfig = {};

/** Options for the Summarizer interface (Phase 3) */
export interface SummarizerOpts {
maxLen?: number;
Expand Down
4 changes: 3 additions & 1 deletion open-sse/translator/response/openai-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { FORMATS } from "../formats.ts";
import { appendToolCallArgumentDelta } from "../../utils/toolCallArguments.ts";
import { fallbackToolCallId } from "../helpers/toolCallHelper.ts";
import { shouldParseTextualReasoningTags } from "../../handlers/responseSanitizer.ts";
import { stripPlaceholderFromContent } from "../../utils/reasoningPlaceholder.ts";
import {
normalizeToolName,
stripEmptyOptionalToolArgs,
Expand Down Expand Up @@ -196,7 +197,8 @@ export function openaiToOpenAIResponsesResponse(chunk, state) {

if (content) {
const msgIdx = state.reasoningId ? state.reasoningIndex + 1 : idx;
emitTextContent(state, emit, msgIdx, content);
const sanitized = stripPlaceholderFromContent(content);
if (sanitized) emitTextContent(state, emit, msgIdx, sanitized);
}
}

Expand Down
3 changes: 2 additions & 1 deletion open-sse/translator/response/openai-to-claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { CLAUDE_OAUTH_TOOL_PREFIX } from "../request/openai-to-claude.ts";
import { hasToolCallShim, applyToolCallShimToBuffer } from "../helpers/toolCallShim.ts";
import { appendToolCallArgumentDelta } from "../../utils/toolCallArguments.ts";
import { isAbortFinishReason } from "../../utils/finishReason.ts";
import { stripPlaceholderFromContent } from "../../utils/reasoningPlaceholder.ts";

// Helper: stop thinking block if started
function stopThinkingBlock(state, results) {
Expand Down Expand Up @@ -146,7 +147,7 @@ export function openaiToClaudeResponse(chunk, state) {
results.push({
type: "content_block_delta",
index: state.textBlockIndex,
delta: { type: "text_delta", text: delta.content },
delta: { type: "text_delta", text: stripPlaceholderFromContent(delta.content) },
});
}

Expand Down
3 changes: 2 additions & 1 deletion open-sse/utils/jsonToSse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
import { normalizeOpenAICompatibleFinishReasonString } from "./finishReason.ts";
import { getUnsupportedReasoningValue } from "./reasoningFields.ts";
import { stripPlaceholderFromContent } from "./reasoningPlaceholder.ts";

type JsonRecord = Record<string, unknown>;

Expand Down Expand Up @@ -107,7 +108,7 @@ export function synthesizeOpenAiSseFromJson(jsonText: string): string {
emitDelta(reasoningDelta);
}
if (typeof message.content === "string" && message.content.length > 0) {
emitDelta({ content: message.content });
emitDelta({ content: stripPlaceholderFromContent(message.content) });
}
if (Array.isArray(message.tool_calls) && message.tool_calls.length > 0) {
emitDelta({ tool_calls: message.tool_calls });
Expand Down
35 changes: 35 additions & 0 deletions open-sse/utils/reasoningPlaceholder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* #8081 — Internal reasoning replay placeholder must never leak into
* user-visible assistant content. Models can echo the sentinel text that
* OmniRoute injects into request-side reasoning fields.
*
* This module provides helpers to strip the placeholder from response content
* across all output paths (streaming deltas, non-streaming messages, tool-call
* preambles, Claude text blocks, Responses API output).
*/

import { NON_ANTHROPIC_THINKING_PLACEHOLDER } from "../translator/helpers/claudeHelper.ts";

/**
* Returns true if the string contains the placeholder sentinel.
* Substring match — catches model-added prefixes/wrappers.
*/
export function containsReasoningPlaceholder(text: string | undefined | null): boolean {
if (!text) return false;
return text.includes(NON_ANTHROPIC_THINKING_PLACEHOLDER);
}

/**
* Remove the placeholder text from a content string.
* Strips the exact sentinel and any resulting leading/trailing whitespace.
* If the string was ONLY the placeholder (possibly with surrounding whitespace),
* returns empty string rather than removing legitimate surrounding text.
*/
export function stripPlaceholderFromContent(text: string | undefined | null): string {
if (!text) return "";
if (!containsReasoningPlaceholder(text)) return text;
return text.split(NON_ANTHROPIC_THINKING_PLACEHOLDER)
.map((s) => s.trim())
.filter((s) => s.length > 0)
.join(" ");
}
17 changes: 17 additions & 0 deletions src/lib/combos/builderOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { getAccountDisplayName, getProviderDisplayName } from "@/lib/display/names";
import { getCompatibleFallbackModels } from "@/lib/providers/managedAvailableModels";
import { getResolvedModelCapabilities } from "@/lib/modelCapabilities";
import { CANONICAL_EFFORT_VALUES } from "@/shared/reasoning/effortStandardization";
import { getSyncedCapabilities } from "@/lib/modelsDevSync";
import { getModelsByProviderId } from "@/shared/constants/models";
import {
Expand Down Expand Up @@ -86,6 +87,7 @@ export interface ComboBuilderModelOption {
contextLength?: number;
outputTokenLimit?: number;
supportsThinking?: boolean;
effortTiers?: string[];
}

export interface ComboBuilderConnectionOption {
Expand Down Expand Up @@ -295,6 +297,7 @@ function addModelOption(
contextLength?: number | null;
outputTokenLimit?: number | null;
supportsThinking?: boolean;
effortTiers?: string[] | null;
}
) {
const modelId = toStringOrNull(input.id);
Expand All @@ -321,6 +324,9 @@ function addModelOption(
...(typeof input.supportsThinking === "boolean"
? { supportsThinking: input.supportsThinking }
: {}),
...(Array.isArray(input.effortTiers) && input.effortTiers.length > 0
? { effortTiers: input.effortTiers }
: {}),
});
return;
}
Expand Down Expand Up @@ -349,6 +355,13 @@ function addModelOption(
if (existing.supportsThinking == null && typeof input.supportsThinking === "boolean") {
existing.supportsThinking = input.supportsThinking;
}
if (
!Array.isArray(existing.effortTiers) &&
Array.isArray(input.effortTiers) &&
input.effortTiers.length > 0
) {
existing.effortTiers = input.effortTiers;
}
existing.sources = Array.from(mergedSources).sort(
(left, right) => getSourcePriority(left) - getSourcePriority(right)
);
Expand Down Expand Up @@ -379,6 +392,7 @@ function buildModelOptions(
typeof model.supportsThinking === "boolean"
? model.supportsThinking
: (resolved.supportsThinking ?? undefined),
effortTiers: resolved.supportsThinking ? [...CANONICAL_EFFORT_VALUES] : null,
});
}

Expand All @@ -394,6 +408,7 @@ function buildModelOptions(
contextLength: toNumberOrNull(model.contextLength) ?? resolved.contextWindow,
outputTokenLimit: resolved.maxOutputTokens,
supportsThinking: resolved.supportsThinking ?? undefined,
effortTiers: resolved.supportsThinking ? [...CANONICAL_EFFORT_VALUES] : null,
});
}

Expand All @@ -420,6 +435,7 @@ function buildModelOptions(
typeof model.supportsThinking === "boolean"
? model.supportsThinking
: (resolved.supportsThinking ?? undefined),
effortTiers: resolved.supportsThinking ? [...CANONICAL_EFFORT_VALUES] : null,
});
}

Expand All @@ -439,6 +455,7 @@ function buildModelOptions(
: resolved.contextWindow,
outputTokenLimit: resolved.maxOutputTokens,
supportsThinking: resolved.supportsThinking ?? undefined,
effortTiers: resolved.supportsThinking ? [...CANONICAL_EFFORT_VALUES] : null,
});
}
}
Expand Down
15 changes: 15 additions & 0 deletions src/lib/db/compression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
type CompressionMode,
type ContextEditingConfig,
type EngineToggle,
type HeadroomConfig,
type McpAccessibilityConfig,
type RtkConfig,
type UltraConfig,
Expand Down Expand Up @@ -435,6 +436,17 @@ function normalizeEngineToggle(value: unknown): EngineToggle | null {
};
}

/** Normalize headroom detail config from DB JSON (#8056). */
function normalizeHeadroomConfig(value: unknown): HeadroomConfig {
const record = toRecord(value);
const out: HeadroomConfig = {};
if (typeof record.minRows === "number" && Number.isFinite(record.minRows)) {
out.minRows = Math.max(2, Math.floor(record.minRows));
}
if (typeof record.enabled === "boolean") out.enabled = record.enabled;
return out;
}

// Sanitize an engines map for persistence: keep only known engine ids with a well-formed
// `{enabled, level?}` toggle. Mirrors the read-path validation so a malformed write can't poison
// the stored row.
Expand Down Expand Up @@ -655,6 +667,9 @@ export async function getCompressionSettings(): Promise<CompressionConfig> {
case "ultraConfig":
config.ultra = normalizeUltraConfig(parsed);
break;
case "headroom":
config.headroom = normalizeHeadroomConfig(parsed);
break;
case "contextBudget":
config.contextBudget = normalizeContextBudgetConfig(parsed);
break;
Expand Down
15 changes: 10 additions & 5 deletions src/lib/memory/embedding/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
EMBEDDING_PROVIDERS,
buildDynamicEmbeddingProvider,
getEmbeddingDimension,
type EmbeddingProviderNodeRow,
} from "@omniroute/open-sse/config/embeddingRegistry.ts";
import { getProviderCredentials } from "@/sse/services/auth";
Expand Down Expand Up @@ -62,12 +63,15 @@ export function resolveEmbeddingSource(settings: MemorySettingsExtended): Embedd
// We can't do async here, so we report it as potentially available
// and the caller will attempt embed + get no_key error on failure.
// For resolution purposes, mark as remote (will fail at embed time if no key).
const dims = getEmbeddingDimension(model) ?? null;
return {
source: "remote",
model,
dimensions: null,
signature: makeSignature("remote", model, null),
reason: `remote provider configured: ${model}`,
dimensions: dims,
signature: makeSignature("remote", model, dims),
reason: dims !== null
? `remote provider configured: ${model} (dim=${dims})`
: `remote provider configured: ${model}`,
};
}

Expand Down Expand Up @@ -123,11 +127,12 @@ export function resolveEmbeddingSource(settings: MemorySettingsExtended): Embedd
// We defer the actual hasKey check to listEmbeddingProviders (async).
// For resolveEmbeddingSource (sync), we report "possibly remote" when model is set.
// If no key, embed will return EmbeddingError{reason:"no_key"}.
const autoDims = getEmbeddingDimension(providerModel) ?? null;
return {
source: "remote",
model: providerModel,
dimensions: null,
signature: makeSignature("remote", providerModel, null),
dimensions: autoDims,
signature: makeSignature("remote", providerModel, autoDims),
reason: `auto: provider ${providerId} configured`,
};
}
Expand Down
19 changes: 16 additions & 3 deletions src/lib/oauth/connectionPersistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,23 @@ export async function persistOAuthConnection(
: null;

let connection: any;
if (tokenData.email) {
const existing = await getProviderConnections({ provider });
const existing = await getProviderConnections({ provider });
// #8059: Match by connectionId FIRST, regardless of email — Copilot device-code
// flow stores identity under providerSpecificData.githubEmail with no top-level email,
// so the email gate below would skip dedup and create a duplicate on every refresh.
if (connectionId) {
const idMatch = existing.find((c: any) => c.id && safeEqual(connectionId, c.id));
if (idMatch) {
connection = await updateProviderConnection(idMatch.id, {
...tokenData,
expiresAt,
testStatus: "active",
isActive: true,
});
}
}
if (!connection && tokenData.email) {
const match = existing.find((c: any) => {
if (c.id && safeEqual(connectionId, c.id)) return true;
if (!safeEqual(c.email, tokenData.email) || c.authType !== "oauth") return false;
// For Codex, also check workspaceId to avoid overwriting a different workspace.
if (provider === "codex" && tokenData.providerSpecificData?.workspaceId) {
Expand Down
5 changes: 2 additions & 3 deletions src/shared/components/compression/EngineConfigPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,11 @@ interface EngineEntry {
// Engines whose detailed config has a dedicated sub-object in the compression
// settings store. The on/off + level for ALL engines now live in the panel
// (/dashboard/context/settings, the `engines` map); only these have a place to
// persist the extra per-engine fields edited on this page. Structural engines
// (lite, headroom, session-dedup, ccr, llmlingua) have no sub-object yet — their
// page keeps the detail form + preview but has nothing extra to persist this phase.
// persist the extra per-engine fields edited on this page.
const SETTINGS_SUBOBJECT: Record<string, string> = {
aggressive: "aggressive",
ultra: "ultra",
headroom: "headroom",
};

interface CompressionSettings {
Expand Down
6 changes: 6 additions & 0 deletions src/shared/validation/compressionConfigSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,11 @@ export const ultraConfigSchema = z
})
.strict();

export const headroomConfigSchema = z.object({
minRows: z.number().int().min(2).max(10000).optional(),
enabled: z.boolean().optional(),
}).strict();

const noConfigSchema = z.object({}).strict();

// Structural engines (session-dedup / ccr / headroom / relevance / llmlingua) do not
Expand Down Expand Up @@ -312,6 +317,7 @@ export const compressionSettingsUpdateSchema = z
languageConfig: languageConfigSchema.optional(),
aggressive: aggressiveConfigSchema.optional(),
ultra: ultraConfigSchema.optional(),
headroom: headroomConfigSchema.optional(),
contextBudget: contextBudgetConfigSchema.optional(),
contextEditing: contextEditingConfigSchema.optional(),
engines: z.record(z.string(), engineToggleSchema).optional(),
Expand Down
18 changes: 18 additions & 0 deletions tests/unit/combo-builder-effort-tiers-8072.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { CANONICAL_EFFORT_VALUES } from "@/shared/reasoning/effortStandardization";

test("#8072 — CANONICAL_EFFORT_VALUES is a non-empty string array", () => {
assert.ok(Array.isArray(CANONICAL_EFFORT_VALUES));
assert.ok(CANONICAL_EFFORT_VALUES.length > 0);
for (const v of CANONICAL_EFFORT_VALUES) {
assert.equal(typeof v, "string");
assert.ok(v.length > 0);
}
});

test("#8072 — includes standard reasoning effort values", () => {
assert.ok(CANONICAL_EFFORT_VALUES.includes("low"));
assert.ok(CANONICAL_EFFORT_VALUES.includes("medium"));
assert.ok(CANONICAL_EFFORT_VALUES.includes("high"));
});
Loading
Loading