mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 20:32:20 +03:00
refactor(chatcore): extract passthrough/header/telemetry helpers (QG v2 Fase 9 T5 C2-C3-C5) (#4188)
Integrated into release/v3.8.29. chatCore god-file split Fase 2 part A (QG v2 Fase 9 T5 C2-C3-C5): 3 byte-identical leaf modules (passthroughHelpers/responseHeaders/telemetryHelpers) under open-sse/handlers/chatCore/; chatCore.ts re-exports the 5 previously-public symbols so existing importers resolve. Validated: lint clean, file-size OK (chatCore 5445→5265), 108/109 symbol-importer tests (the 1 fail = known executor-200ms timing flake under load; 65/65 clean on isolated re-run).
This commit is contained in:
committed by
GitHub
parent
0a3b1de25a
commit
647cb6a006
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"_comment": "Catraca de tamanho (check-file-size.mjs). frozen so pode encolher; arquivos novos <= cap. --update ratcheta.",
|
||||
"_rebaseline_2026_06_18_4180_gemini_default_thinking": "PR #4180 own growth: openai-to-gemini.ts 844->864 (+20 = default includeThoughts for modern Gemini 2.5+ at the existing openaiToGeminiBase chokepoint — when the client set no thinkingConfig, inject one (includeThoughts:true + a capped budget) so the model's reasoning is marked thought:true and routed to reasoning_content instead of leaking into visible content, #4170). Cohesive translator branch gated to gemini-2.5+/3 (excludes gemini-1.x and non-thinking 2.0); not extractable. Reconciled here because #4180 merged without the baseline bump.",
|
||||
"_rebaseline_2026_06_18_qg9_chatcore_split_prA": "QG v2 Fase 9 T5 C2-C3-C5: chatCore.ts 5445->5265 (wc -l 5264 + 1). Fase 2 of the chatCore split following #4159 (which created 10 leaf modules). Three more sibling leaves created under open-sse/handlers/chatCore/, all <cap, pure byte-identical moves (no runtime change): passthroughHelpers.ts (C2: shouldUseNativeCodexPassthrough/redactPassthroughThinkingSignatures/isClaudeCodeSemanticPassthroughRequest), responseHeaders.ts (C3: STREAMING_RESPONSE_HEADER_DENYLIST/buildStreamingResponseHeaders/materializeDeduplicatedExecutionResult/stripStaleForwardingHeaders), telemetryHelpers.ts (C5: forwardDashboardEventToLiveWs/maybeSyncClaudeExtraUsageState). chatCore.ts imports all 8 still-referenced symbols back from the leaves and re-exports the 5 previously-public ones (shouldUseNativeCodexPassthrough/redactPassthroughThinkingSignatures/isClaudeCodeSemanticPassthroughRequest/buildStreamingResponseHeaders/stripStaleForwardingHeaders) so existing test importers keep resolving. No barrel imports in the new leaves.",
|
||||
"_rebaseline_2026_06_18_4176_free_models": "PR #4176 own growth: AddApiKeyModal.tsx 845->866 (+21) and EditConnectionModal.tsx 1174->1204 (+30) = the 'import only free models' connection option — a free-models Toggle gated by providerHasFreeModels() plus its form-state wiring (importFreeModelsOnly field + providerSpecificData persistence, explicit-false on edit so the PUT merge doesn't keep a stale true) added to both connection modals, mirroring the prior per-modal toggle bumps #3879 (redact-thinking) and #2997 (disable-cooling). Detection lives in the new shared src/shared/utils/freeModels.ts (112 LOC, <cap); the duplicated Toggle is ~6 lines and the per-modal state wiring is intrinsic, so the remaining growth is cohesive UI, not an extractable block.",
|
||||
"_rebaseline_2026_06_18_3931_qwen_web_models": "Issue #3931 (Problem #3) own growth: providers/[id]/models/route.ts 2512->2531 (+19 = one PROVIDER_MODELS_CONFIG entry for `qwen-web` + a 4-line comment). qwen-web was missing from the config map so its model-discovery page returned nothing (the OAuth fallback only fires for provider===qwen). Pure additive config entry pointing at the public chat.qwen.ai/api/v2/models endpoint; standard per-provider addition, not extractable.",
|
||||
"_rebaseline_2026_06_18_4165_queue_timeout_msg": "Issue #4165 own growth: rateLimitManager.ts 1022->1035 (+13 at the existing withRateLimit catch chokepoint). Bottleneck's raw `This job timed out after <maxWaitMs> ms.` is rewritten into a clear OmniRoute-owned error (names resilienceSettings.requestQueue.maxWaitMs, disclaims upstream, keeps the original as `cause`, tags code=RATE_LIMIT_QUEUE_TIMEOUT) so queue-saturation 502s stop masquerading as provider outages. The branch already existed (it only logged); this adds the error construction at the same point. Not extractable — closes over provider/model/maxWaitMs locals of the single catch.",
|
||||
@@ -59,7 +60,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": 5445,
|
||||
"open-sse/handlers/chatCore.ts": 5265,
|
||||
"open-sse/handlers/imageGeneration.ts": 3777,
|
||||
"open-sse/handlers/responseSanitizer.ts": 1103,
|
||||
"open-sse/handlers/search.ts": 1546,
|
||||
|
||||
@@ -4,6 +4,29 @@ import { checkSemanticCache } from "./chatCore/semanticCache.ts";
|
||||
import { sanitizeChatRequestBody } from "./chatCore/sanitization.ts";
|
||||
import { cloneBoundedChatLogPayload, truncateForLog } from "./chatCore/logTruncation.ts";
|
||||
import { getHeaderValueCaseInsensitive } from "./chatCore/headers.ts";
|
||||
import {
|
||||
shouldUseNativeCodexPassthrough,
|
||||
redactPassthroughThinkingSignatures,
|
||||
isClaudeCodeSemanticPassthroughRequest,
|
||||
} from "./chatCore/passthroughHelpers.ts";
|
||||
import {
|
||||
buildStreamingResponseHeaders,
|
||||
materializeDeduplicatedExecutionResult,
|
||||
stripStaleForwardingHeaders,
|
||||
} from "./chatCore/responseHeaders.ts";
|
||||
import {
|
||||
forwardDashboardEventToLiveWs,
|
||||
maybeSyncClaudeExtraUsageState,
|
||||
} from "./chatCore/telemetryHelpers.ts";
|
||||
// Re-export the previously inline-defined helpers so existing importers of these
|
||||
// symbols from chatCore.ts (tests, sibling modules) keep resolving after the split.
|
||||
export {
|
||||
shouldUseNativeCodexPassthrough,
|
||||
redactPassthroughThinkingSignatures,
|
||||
isClaudeCodeSemanticPassthroughRequest,
|
||||
buildStreamingResponseHeaders,
|
||||
stripStaleForwardingHeaders,
|
||||
};
|
||||
import {
|
||||
extractMemoryTextFromResponse,
|
||||
extractMemoryTextFromRequestBody,
|
||||
@@ -259,181 +282,6 @@ import { incrementRequestCount } from "../services/geminiRateLimitTracker.ts";
|
||||
|
||||
import { isSmallEnoughForSemanticCache } from "../utils/estimateSize.ts";
|
||||
|
||||
async function forwardDashboardEventToLiveWs(event: string, payload: unknown): Promise<void> {
|
||||
const port = process.env.LIVE_WS_PORT || "20129";
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 1_500);
|
||||
try {
|
||||
await fetch(`http://127.0.0.1:${port}/__omniroute_event`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ event, payload, timestamp: Date.now() }),
|
||||
signal: controller.signal,
|
||||
});
|
||||
} catch {
|
||||
// Best-effort sidecar bridge; do not affect the chat hot path.
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
async function maybeSyncClaudeExtraUsageState({
|
||||
provider,
|
||||
connectionId,
|
||||
providerSpecificData,
|
||||
log,
|
||||
}: {
|
||||
provider: string | null | undefined;
|
||||
connectionId: string | null | undefined;
|
||||
providerSpecificData: unknown;
|
||||
log?: { debug?: (...args: unknown[]) => void; warn?: (...args: unknown[]) => void } | null;
|
||||
}) {
|
||||
if (!connectionId || !isClaudeExtraUsageBlockEnabled(provider, providerSpecificData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await fetchLiveProviderLimits(connectionId);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
log?.debug?.("CLAUDE_USAGE", `Failed to sync Claude extra-usage state: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function shouldUseNativeCodexPassthrough({
|
||||
provider,
|
||||
sourceFormat,
|
||||
endpointPath,
|
||||
}: {
|
||||
provider?: string | null;
|
||||
sourceFormat?: string | null;
|
||||
endpointPath?: string | null;
|
||||
}): boolean {
|
||||
if (provider !== "codex") return false;
|
||||
if (sourceFormat !== FORMATS.OPENAI_RESPONSES) return false;
|
||||
let normalizedEndpoint = String(endpointPath || "");
|
||||
while (normalizedEndpoint.endsWith("/")) normalizedEndpoint = normalizedEndpoint.slice(0, -1);
|
||||
const segments = normalizedEndpoint.split("/");
|
||||
return segments.includes("responses");
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass `thinking` / `redacted_thinking` blocks through UNCHANGED.
|
||||
*
|
||||
* This used to rewrite every assistant thinking block to `redacted_thinking`
|
||||
* carrying a synthetic signature, on the assumption that a thinking signature is
|
||||
* bound to the auth token that produced it and would be rejected after a token /
|
||||
* model switch with 400 "Invalid signature in thinking block" (issue #2454).
|
||||
*
|
||||
* That rewrite is the actual cause of a different, far more common failure on the
|
||||
* Anthropic-native Claude OAuth passthrough:
|
||||
*
|
||||
* 400 messages.N.content.M: `thinking` or `redacted_thinking` blocks in the
|
||||
* latest assistant message cannot be modified. These blocks must remain as
|
||||
* they were in the original response.
|
||||
*
|
||||
* The Messages API validates submitted thinking blocks against the original
|
||||
* response and rejects ANY modification — so converting them to
|
||||
* `redacted_thinking` makes every multi-turn request with thinking fail (most
|
||||
* visible on long Claude Code tool-loops). The thinking-block signature is
|
||||
* validated server-side by Anthropic and stays valid when the blocks are replayed,
|
||||
* including under a different OAuth token — verified by preserving the blocks
|
||||
* across a mid-conversation account switch with zero "Invalid signature"
|
||||
* responses. The redaction is therefore both unnecessary and the cause of the
|
||||
* regression, so the blocks are now returned verbatim. The `signature` parameter
|
||||
* is kept for call-site compatibility.
|
||||
*/
|
||||
export function redactPassthroughThinkingSignatures(
|
||||
messages: unknown,
|
||||
_signature: string
|
||||
): unknown {
|
||||
return messages;
|
||||
}
|
||||
|
||||
export function isClaudeCodeSemanticPassthroughRequest({
|
||||
provider,
|
||||
sourceFormat,
|
||||
targetFormat,
|
||||
headers,
|
||||
userAgent,
|
||||
}: {
|
||||
provider?: string | null;
|
||||
sourceFormat?: string | null;
|
||||
targetFormat?: string | null;
|
||||
headers?: Record<string, unknown> | Headers | null;
|
||||
userAgent?: string | null;
|
||||
}): boolean {
|
||||
const isDirectClaudeCodeProvider =
|
||||
provider === "claude" || isClaudeCodeCompatibleProvider(provider);
|
||||
if (!isDirectClaudeCodeProvider) return false;
|
||||
if (sourceFormat !== FORMATS.CLAUDE) return false;
|
||||
if (targetFormat !== FORMATS.CLAUDE) return false;
|
||||
|
||||
const headerUserAgent = getHeaderValueCaseInsensitive(headers, "user-agent");
|
||||
const ua = `${userAgent || ""} ${headerUserAgent || ""}`.toLowerCase();
|
||||
if (ua.includes("claude-code") || ua.includes("claude-cli")) return true;
|
||||
|
||||
const appHeader = getHeaderValueCaseInsensitive(headers, "x-app");
|
||||
if (typeof appHeader === "string" && appHeader.trim().toLowerCase() === "cli") return true;
|
||||
|
||||
const sessionId = getHeaderValueCaseInsensitive(headers, "x-claude-code-session-id");
|
||||
return typeof sessionId === "string" && sessionId.trim().length > 0;
|
||||
}
|
||||
|
||||
const STREAMING_RESPONSE_HEADER_DENYLIST = new Set([
|
||||
"content-type",
|
||||
"content-encoding",
|
||||
"content-length",
|
||||
"transfer-encoding",
|
||||
]);
|
||||
|
||||
export function buildStreamingResponseHeaders(
|
||||
providerHeaders: Headers,
|
||||
meta: Parameters<typeof buildOmniRouteResponseMetaHeaders>[0]
|
||||
): Record<string, string> {
|
||||
const forwardedHeaders: [string, string][] = [];
|
||||
providerHeaders.forEach((value, key) => {
|
||||
if (!STREAMING_RESPONSE_HEADER_DENYLIST.has(key.toLowerCase())) {
|
||||
forwardedHeaders.push([key, value]);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...Object.fromEntries(forwardedHeaders),
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache, no-transform",
|
||||
Connection: "keep-alive",
|
||||
"X-Accel-Buffering": "no",
|
||||
[OMNIROUTE_RESPONSE_HEADERS.cache]: "MISS",
|
||||
...buildOmniRouteResponseMetaHeaders(meta),
|
||||
};
|
||||
}
|
||||
|
||||
function materializeDeduplicatedExecutionResult<T extends Record<string, unknown>>(result: T): T {
|
||||
const snapshot =
|
||||
result && typeof result === "object"
|
||||
? ((result as Record<string, unknown>)._dedupSnapshot as
|
||||
| {
|
||||
status: number;
|
||||
statusText: string;
|
||||
headers: [string, string][];
|
||||
payload: string;
|
||||
}
|
||||
| undefined)
|
||||
: undefined;
|
||||
|
||||
if (!snapshot) return result;
|
||||
|
||||
return {
|
||||
...result,
|
||||
response: new Response(snapshot.payload, {
|
||||
status: snapshot.status,
|
||||
statusText: snapshot.statusText,
|
||||
headers: snapshot.headers,
|
||||
}),
|
||||
} as T;
|
||||
}
|
||||
|
||||
function getSkillsProviderForFormat(format: string): "openai" | "anthropic" | "google" | "other" {
|
||||
switch (format) {
|
||||
case FORMATS.CLAUDE:
|
||||
@@ -456,34 +304,6 @@ function getSkillsModelIdForFormat(format: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip hop-by-hop headers that describe the upstream wire encoding.
|
||||
*
|
||||
* `readNonStreamingResponseBody` reads (and, for compressed responses, also
|
||||
* decompresses via fetch's auto-decoder) the full upstream body into a JS
|
||||
* string before we re-emit it to the client. Once that happens, the original
|
||||
* `Content-Encoding`, `Content-Length`, and `Transfer-Encoding` all describe
|
||||
* a payload that no longer exists:
|
||||
*
|
||||
* - `Content-Length` is the *compressed* byte count, so clients honoring it
|
||||
* read only the first N bytes of the decompressed JSON and surface
|
||||
* "Unterminated string in JSON at position …" parse failures (observed
|
||||
* on gzipped Gemini responses).
|
||||
* - `Content-Encoding` advertises a compression we have already undone.
|
||||
* - `Transfer-Encoding` is hop-by-hop per RFC 7230 §6.1 and must not be
|
||||
* forwarded across a buffering proxy — its presence alongside a
|
||||
* re-emitted body is undefined behavior.
|
||||
*
|
||||
* Deleting all three lets the response framework set a fresh, correct
|
||||
* `Content-Length` (or fall back to `Transfer-Encoding: chunked`) for the
|
||||
* payload we are actually sending.
|
||||
*/
|
||||
export function stripStaleForwardingHeaders(headers: Headers): void {
|
||||
headers.delete("content-encoding");
|
||||
headers.delete("content-length");
|
||||
headers.delete("transfer-encoding");
|
||||
}
|
||||
|
||||
async function readNonStreamingResponseBody(
|
||||
response: Response,
|
||||
contentType: string,
|
||||
|
||||
83
open-sse/handlers/chatCore/passthroughHelpers.ts
Normal file
83
open-sse/handlers/chatCore/passthroughHelpers.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { FORMATS } from "../../translator/formats.ts";
|
||||
import { isClaudeCodeCompatibleProvider } from "../../services/claudeCodeCompatible.ts";
|
||||
import { getHeaderValueCaseInsensitive } from "./headers.ts";
|
||||
|
||||
export function shouldUseNativeCodexPassthrough({
|
||||
provider,
|
||||
sourceFormat,
|
||||
endpointPath,
|
||||
}: {
|
||||
provider?: string | null;
|
||||
sourceFormat?: string | null;
|
||||
endpointPath?: string | null;
|
||||
}): boolean {
|
||||
if (provider !== "codex") return false;
|
||||
if (sourceFormat !== FORMATS.OPENAI_RESPONSES) return false;
|
||||
let normalizedEndpoint = String(endpointPath || "");
|
||||
while (normalizedEndpoint.endsWith("/")) normalizedEndpoint = normalizedEndpoint.slice(0, -1);
|
||||
const segments = normalizedEndpoint.split("/");
|
||||
return segments.includes("responses");
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass `thinking` / `redacted_thinking` blocks through UNCHANGED.
|
||||
*
|
||||
* This used to rewrite every assistant thinking block to `redacted_thinking`
|
||||
* carrying a synthetic signature, on the assumption that a thinking signature is
|
||||
* bound to the auth token that produced it and would be rejected after a token /
|
||||
* model switch with 400 "Invalid signature in thinking block" (issue #2454).
|
||||
*
|
||||
* That rewrite is the actual cause of a different, far more common failure on the
|
||||
* Anthropic-native Claude OAuth passthrough:
|
||||
*
|
||||
* 400 messages.N.content.M: `thinking` or `redacted_thinking` blocks in the
|
||||
* latest assistant message cannot be modified. These blocks must remain as
|
||||
* they were in the original response.
|
||||
*
|
||||
* The Messages API validates submitted thinking blocks against the original
|
||||
* response and rejects ANY modification — so converting them to
|
||||
* `redacted_thinking` makes every multi-turn request with thinking fail (most
|
||||
* visible on long Claude Code tool-loops). The thinking-block signature is
|
||||
* validated server-side by Anthropic and stays valid when the blocks are replayed,
|
||||
* including under a different OAuth token — verified by preserving the blocks
|
||||
* across a mid-conversation account switch with zero "Invalid signature"
|
||||
* responses. The redaction is therefore both unnecessary and the cause of the
|
||||
* regression, so the blocks are now returned verbatim. The `signature` parameter
|
||||
* is kept for call-site compatibility.
|
||||
*/
|
||||
export function redactPassthroughThinkingSignatures(
|
||||
messages: unknown,
|
||||
_signature: string
|
||||
): unknown {
|
||||
return messages;
|
||||
}
|
||||
|
||||
export function isClaudeCodeSemanticPassthroughRequest({
|
||||
provider,
|
||||
sourceFormat,
|
||||
targetFormat,
|
||||
headers,
|
||||
userAgent,
|
||||
}: {
|
||||
provider?: string | null;
|
||||
sourceFormat?: string | null;
|
||||
targetFormat?: string | null;
|
||||
headers?: Record<string, unknown> | Headers | null;
|
||||
userAgent?: string | null;
|
||||
}): boolean {
|
||||
const isDirectClaudeCodeProvider =
|
||||
provider === "claude" || isClaudeCodeCompatibleProvider(provider);
|
||||
if (!isDirectClaudeCodeProvider) return false;
|
||||
if (sourceFormat !== FORMATS.CLAUDE) return false;
|
||||
if (targetFormat !== FORMATS.CLAUDE) return false;
|
||||
|
||||
const headerUserAgent = getHeaderValueCaseInsensitive(headers, "user-agent");
|
||||
const ua = `${userAgent || ""} ${headerUserAgent || ""}`.toLowerCase();
|
||||
if (ua.includes("claude-code") || ua.includes("claude-cli")) return true;
|
||||
|
||||
const appHeader = getHeaderValueCaseInsensitive(headers, "x-app");
|
||||
if (typeof appHeader === "string" && appHeader.trim().toLowerCase() === "cli") return true;
|
||||
|
||||
const sessionId = getHeaderValueCaseInsensitive(headers, "x-claude-code-session-id");
|
||||
return typeof sessionId === "string" && sessionId.trim().length > 0;
|
||||
}
|
||||
86
open-sse/handlers/chatCore/responseHeaders.ts
Normal file
86
open-sse/handlers/chatCore/responseHeaders.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { buildOmniRouteResponseMetaHeaders } from "@/domain/omnirouteResponseMeta";
|
||||
import { OMNIROUTE_RESPONSE_HEADERS } from "@/shared/constants/headers";
|
||||
|
||||
const STREAMING_RESPONSE_HEADER_DENYLIST = new Set([
|
||||
"content-type",
|
||||
"content-encoding",
|
||||
"content-length",
|
||||
"transfer-encoding",
|
||||
]);
|
||||
|
||||
export function buildStreamingResponseHeaders(
|
||||
providerHeaders: Headers,
|
||||
meta: Parameters<typeof buildOmniRouteResponseMetaHeaders>[0]
|
||||
): Record<string, string> {
|
||||
const forwardedHeaders: [string, string][] = [];
|
||||
providerHeaders.forEach((value, key) => {
|
||||
if (!STREAMING_RESPONSE_HEADER_DENYLIST.has(key.toLowerCase())) {
|
||||
forwardedHeaders.push([key, value]);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...Object.fromEntries(forwardedHeaders),
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache, no-transform",
|
||||
Connection: "keep-alive",
|
||||
"X-Accel-Buffering": "no",
|
||||
[OMNIROUTE_RESPONSE_HEADERS.cache]: "MISS",
|
||||
...buildOmniRouteResponseMetaHeaders(meta),
|
||||
};
|
||||
}
|
||||
|
||||
export function materializeDeduplicatedExecutionResult<T extends Record<string, unknown>>(
|
||||
result: T
|
||||
): T {
|
||||
const snapshot =
|
||||
result && typeof result === "object"
|
||||
? ((result as Record<string, unknown>)._dedupSnapshot as
|
||||
| {
|
||||
status: number;
|
||||
statusText: string;
|
||||
headers: [string, string][];
|
||||
payload: string;
|
||||
}
|
||||
| undefined)
|
||||
: undefined;
|
||||
|
||||
if (!snapshot) return result;
|
||||
|
||||
return {
|
||||
...result,
|
||||
response: new Response(snapshot.payload, {
|
||||
status: snapshot.status,
|
||||
statusText: snapshot.statusText,
|
||||
headers: snapshot.headers,
|
||||
}),
|
||||
} as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip hop-by-hop headers that describe the upstream wire encoding.
|
||||
*
|
||||
* `readNonStreamingResponseBody` reads (and, for compressed responses, also
|
||||
* decompresses via fetch's auto-decoder) the full upstream body into a JS
|
||||
* string before we re-emit it to the client. Once that happens, the original
|
||||
* `Content-Encoding`, `Content-Length`, and `Transfer-Encoding` all describe
|
||||
* a payload that no longer exists:
|
||||
*
|
||||
* - `Content-Length` is the *compressed* byte count, so clients honoring it
|
||||
* read only the first N bytes of the decompressed JSON and surface
|
||||
* "Unterminated string in JSON at position …" parse failures (observed
|
||||
* on gzipped Gemini responses).
|
||||
* - `Content-Encoding` advertises a compression we have already undone.
|
||||
* - `Transfer-Encoding` is hop-by-hop per RFC 7230 §6.1 and must not be
|
||||
* forwarded across a buffering proxy — its presence alongside a
|
||||
* re-emitted body is undefined behavior.
|
||||
*
|
||||
* Deleting all three lets the response framework set a fresh, correct
|
||||
* `Content-Length` (or fall back to `Transfer-Encoding: chunked`) for the
|
||||
* payload we are actually sending.
|
||||
*/
|
||||
export function stripStaleForwardingHeaders(headers: Headers): void {
|
||||
headers.delete("content-encoding");
|
||||
headers.delete("content-length");
|
||||
headers.delete("transfer-encoding");
|
||||
}
|
||||
46
open-sse/handlers/chatCore/telemetryHelpers.ts
Normal file
46
open-sse/handlers/chatCore/telemetryHelpers.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { fetchLiveProviderLimits } from "@/lib/usage/providerLimits";
|
||||
import { isClaudeExtraUsageBlockEnabled } from "@/lib/providers/claudeExtraUsage";
|
||||
|
||||
export async function forwardDashboardEventToLiveWs(
|
||||
event: string,
|
||||
payload: unknown
|
||||
): Promise<void> {
|
||||
const port = process.env.LIVE_WS_PORT || "20129";
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 1_500);
|
||||
try {
|
||||
await fetch(`http://127.0.0.1:${port}/__omniroute_event`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ event, payload, timestamp: Date.now() }),
|
||||
signal: controller.signal,
|
||||
});
|
||||
} catch {
|
||||
// Best-effort sidecar bridge; do not affect the chat hot path.
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
export async function maybeSyncClaudeExtraUsageState({
|
||||
provider,
|
||||
connectionId,
|
||||
providerSpecificData,
|
||||
log,
|
||||
}: {
|
||||
provider: string | null | undefined;
|
||||
connectionId: string | null | undefined;
|
||||
providerSpecificData: unknown;
|
||||
log?: { debug?: (...args: unknown[]) => void; warn?: (...args: unknown[]) => void } | null;
|
||||
}) {
|
||||
if (!connectionId || !isClaudeExtraUsageBlockEnabled(provider, providerSpecificData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await fetchLiveProviderLimits(connectionId);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
log?.debug?.("CLAUDE_USAGE", `Failed to sync Claude extra-usage state: ${message}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user