mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-14 11:12:17 +03:00
fix(types): validate nonstreaming JSON contracts (#10258)
This commit is contained in:
@@ -104,7 +104,13 @@ import { resolveAgentGoalPolicy } from "../utils/agentGoalPolicy.ts";
|
||||
import { createStreamController } from "../utils/streamHandler.ts";
|
||||
import * as streamFailure from "../utils/streamFailureFinalization.ts";
|
||||
import { createSseHeartbeatTransform, shapeForClientFormat } from "../utils/sseHeartbeat.ts";
|
||||
import { addBufferToUsage, filterUsageForFormat, estimateUsage, sanitizeUsagePayloadForRequest } from "../utils/usageTracking.ts";
|
||||
import {
|
||||
addBufferToUsage,
|
||||
filterUsageForFormat,
|
||||
estimateUsage,
|
||||
normalizeUsage,
|
||||
sanitizeUsagePayloadForRequest,
|
||||
} from "../utils/usageTracking.ts";
|
||||
import {
|
||||
refreshWithRetry,
|
||||
isUnrecoverableRefreshError,
|
||||
@@ -271,7 +277,10 @@ import {
|
||||
appendNonStreamingSseTerminalSignal,
|
||||
type NonStreamingSseTerminalState,
|
||||
} from "./chatCore/nonStreamingSse.ts";
|
||||
import { parseNonStreamingResponseBody } from "./chatCore/nonStreamingResponseParse.ts";
|
||||
import {
|
||||
isJsonRecord,
|
||||
parseNonStreamingResponseBody,
|
||||
} from "./chatCore/nonStreamingResponseParse.ts";
|
||||
import { unwrapClinepassEnvelope } from "../utils/clinepassEnvelope.ts";
|
||||
import { recordNonStreamingUsageStats } from "./chatCore/nonStreamingUsageStats.ts";
|
||||
import {
|
||||
@@ -4226,6 +4235,12 @@ export async function handleChatCore({
|
||||
trackPendingRequest(model, provider, connectionId, false);
|
||||
return createErrorResult(HTTP_STATUS.BAD_GATEWAY, envError.message);
|
||||
}
|
||||
if (!isJsonRecord(unwrapped)) {
|
||||
const invalidEnvelopeMessage = "Invalid JSON response from provider";
|
||||
persistFailureUsage(HTTP_STATUS.BAD_GATEWAY, "clinepass_envelope_error");
|
||||
trackPendingRequest(model, provider, connectionId, false);
|
||||
return createErrorResult(HTTP_STATUS.BAD_GATEWAY, invalidEnvelopeMessage);
|
||||
}
|
||||
responseBody = unwrapped;
|
||||
}
|
||||
responseBody = unwrapClineNonStreamingEnvelope(provider, responseBody);
|
||||
@@ -4512,13 +4527,14 @@ export async function handleChatCore({
|
||||
);
|
||||
translatedResponse = postCallGuardrails.response;
|
||||
|
||||
const responseUsage =
|
||||
(usage && typeof usage === "object" ? usage : null) ||
|
||||
(translatedResponse?.usage && typeof translatedResponse.usage === "object"
|
||||
const responseUsage = isJsonRecord(usage)
|
||||
? usage
|
||||
: isJsonRecord(translatedResponse.usage)
|
||||
? translatedResponse.usage
|
||||
: null);
|
||||
const estimatedCost = responseUsage
|
||||
? await calculateCost(provider, model, responseUsage, { serviceTier: effectiveServiceTier })
|
||||
: null;
|
||||
const costUsage = normalizeUsage(responseUsage);
|
||||
const estimatedCost = costUsage
|
||||
? await calculateCost(provider, model, costUsage, { serviceTier: effectiveServiceTier })
|
||||
: 0;
|
||||
|
||||
if (postCallGuardrails.blocked) {
|
||||
|
||||
@@ -8,6 +8,11 @@ function hasOpenAIChoices(value: unknown): value is JsonRecord & { choices: unkn
|
||||
return isRecord(value) && Array.isArray(value.choices);
|
||||
}
|
||||
|
||||
export function unwrapClineNonStreamingEnvelope(
|
||||
provider: string,
|
||||
responseBody: JsonRecord
|
||||
): JsonRecord;
|
||||
export function unwrapClineNonStreamingEnvelope(provider: string, responseBody: unknown): unknown;
|
||||
export function unwrapClineNonStreamingEnvelope(provider: string, responseBody: unknown): unknown {
|
||||
if (provider !== "cline" || !isRecord(responseBody)) {
|
||||
return responseBody;
|
||||
|
||||
@@ -28,10 +28,16 @@ type LoggerLike =
|
||||
| null
|
||||
| undefined;
|
||||
|
||||
export type JsonRecord = Record<string, unknown>;
|
||||
|
||||
export function isJsonRecord(value: unknown): value is JsonRecord {
|
||||
return !!value && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export type NonStreamingParseResult =
|
||||
| {
|
||||
kind: "ok";
|
||||
responseBody: unknown;
|
||||
responseBody: JsonRecord;
|
||||
responsePayloadFormat: string;
|
||||
looksLikeSSE: boolean;
|
||||
normalizedProviderPayload: unknown;
|
||||
@@ -113,7 +119,16 @@ export async function parseNonStreamingResponseBody(opts: {
|
||||
}
|
||||
|
||||
try {
|
||||
const responseBody = rawBody ? JSON.parse(rawBody) : {};
|
||||
const responseBody: unknown = rawBody ? JSON.parse(rawBody) : {};
|
||||
if (!isJsonRecord(responseBody)) {
|
||||
return {
|
||||
kind: "invalid_json",
|
||||
message: "Invalid JSON response from provider",
|
||||
detailedError: "Invalid JSON response from provider: expected an object payload",
|
||||
looksLikeSSE: false,
|
||||
normalizedProviderPayload,
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "ok",
|
||||
responseBody,
|
||||
|
||||
@@ -257,6 +257,14 @@ export interface SanitizeOpenAIResponseOptions {
|
||||
parseTextualReasoningTags?: boolean;
|
||||
}
|
||||
|
||||
export function sanitizeOpenAIResponse(
|
||||
body: JsonRecord,
|
||||
options?: SanitizeOpenAIResponseOptions
|
||||
): JsonRecord;
|
||||
export function sanitizeOpenAIResponse(
|
||||
body: unknown,
|
||||
options?: SanitizeOpenAIResponseOptions
|
||||
): unknown;
|
||||
export function sanitizeOpenAIResponse(
|
||||
body: unknown,
|
||||
options: SanitizeOpenAIResponseOptions = {}
|
||||
@@ -310,6 +318,8 @@ export function sanitizeOpenAIResponse(
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
export function sanitizeResponsesApiResponse(body: JsonRecord): JsonRecord;
|
||||
export function sanitizeResponsesApiResponse(body: unknown): unknown;
|
||||
export function sanitizeResponsesApiResponse(body: unknown): unknown {
|
||||
const bodyRecord = toRecord(body);
|
||||
if (!bodyRecord) return body;
|
||||
|
||||
@@ -133,6 +133,18 @@ function findBestMessageText(output: unknown[]): {
|
||||
*
|
||||
* @param toolNameMap - Optional Map<prefixedName, originalName> for Claude OAuth tool name stripping
|
||||
*/
|
||||
export function translateNonStreamingResponse(
|
||||
responseBody: JsonRecord,
|
||||
targetFormat: string,
|
||||
sourceFormat: string,
|
||||
toolNameMap?: Map<string, string> | null
|
||||
): JsonRecord;
|
||||
export function translateNonStreamingResponse(
|
||||
responseBody: unknown,
|
||||
targetFormat: string,
|
||||
sourceFormat: string,
|
||||
toolNameMap?: Map<string, string> | null
|
||||
): unknown;
|
||||
export function translateNonStreamingResponse(
|
||||
responseBody: unknown,
|
||||
targetFormat: string,
|
||||
|
||||
@@ -62,6 +62,18 @@ test("invalid JSON → invalid_json with short message + detailed error", async
|
||||
assert.equal(res.looksLikeSSE, false);
|
||||
});
|
||||
|
||||
test("valid JSON with a non-object root → invalid_json", async () => {
|
||||
for (const body of ["null", '"text"', "[]"]) {
|
||||
const res = await parseNonStreamingResponseBody({
|
||||
...baseOpts,
|
||||
providerResponse: makeResponse(body, "application/json"),
|
||||
});
|
||||
assert.equal(res.kind, "invalid_json");
|
||||
if (res.kind !== "invalid_json") continue;
|
||||
assert.match(res.detailedError, /expected an object payload/);
|
||||
}
|
||||
});
|
||||
|
||||
test("valid SSE payload (by content-type) → ok with SSE-derived format", async () => {
|
||||
const sse =
|
||||
'data: {"id":"c1","object":"chat.completion.chunk","choices":[{"delta":{"content":"hello"},"index":0,"finish_reason":null}]}\n\n' +
|
||||
|
||||
Reference in New Issue
Block a user