mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-07 15:52:52 +03:00
refactor(translator): extract pure helpers from response/openai-responses (#5949)
Extract the 5 stateless helpers (normalizeToolName, stripEmptyOptionalToolArgs, normalizeOutputIndex, normalizeUpstreamFailure, extractResponsesReasoningSummaryText) verbatim into the pure leaf openai-responses/pureHelpers.ts (no stream state, no host import). Host imports them back and re-exports normalizeUpstreamFailure for external importers (tests). Host 1091 -> 1001 LOC. The stateful streaming core stays in the host (out of scope). Byte-identical bodies (multiset 73/73), no cycle. Adds a split-guard; consumer tests stay green (responses-translation-fixes 37, combo-param-validation-fallback-4519 5).
This commit is contained in:
committed by
GitHub
parent
283a501a1a
commit
b6249dd374
@@ -7,38 +7,16 @@ import { FORMATS } from "../formats.ts";
|
||||
import { appendToolCallArgumentDelta } from "../../utils/toolCallArguments.ts";
|
||||
import { fallbackToolCallId } from "../helpers/toolCallHelper.ts";
|
||||
import { shouldParseTextualReasoningTags } from "../../handlers/responseSanitizer.ts";
|
||||
import {
|
||||
normalizeToolName,
|
||||
stripEmptyOptionalToolArgs,
|
||||
normalizeOutputIndex,
|
||||
normalizeUpstreamFailure,
|
||||
extractResponsesReasoningSummaryText,
|
||||
} from "./openai-responses/pureHelpers.ts";
|
||||
|
||||
function normalizeToolName(value) {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
function stripEmptyOptionalToolArgs(value, toolName) {
|
||||
if (value == null) return value;
|
||||
|
||||
if (typeof value === "string") {
|
||||
// JSON-string cleanup is intentionally scoped to Claude Code's Read tool.
|
||||
// For arbitrary tools, empty strings/arrays may be valid user payloads.
|
||||
if (toolName !== "Read") return value;
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (Array.isArray(parsed) || typeof parsed !== "object" || parsed === null) return value;
|
||||
const cleaned = stripEmptyOptionalToolArgs(parsed, toolName);
|
||||
return JSON.stringify(cleaned ?? {});
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(value) || typeof value !== "object") return value;
|
||||
|
||||
const cleaned = { ...value };
|
||||
for (const [key, entry] of Object.entries(cleaned)) {
|
||||
if (entry === "" || (Array.isArray(entry) && entry.length === 0)) {
|
||||
delete cleaned[key];
|
||||
}
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
// normalizeUpstreamFailure is re-exported for external importers (tests).
|
||||
export { normalizeUpstreamFailure } from "./openai-responses/pureHelpers.ts";
|
||||
|
||||
/**
|
||||
* Translate OpenAI chunk to Responses API events
|
||||
@@ -192,11 +170,6 @@ export function openaiToOpenAIResponsesResponse(chunk, state) {
|
||||
}
|
||||
|
||||
// Normalize output_index to a non-negative integer (replaces fragile parseInt calls)
|
||||
function normalizeOutputIndex(outputIndex) {
|
||||
const normalized = Number(outputIndex);
|
||||
return Number.isInteger(normalized) && normalized >= 0 ? normalized : 0;
|
||||
}
|
||||
|
||||
// Record a finalized item keyed by output_index so buildDenseOutput can sort later
|
||||
function recordCompletedItem(state, outputIndex, item) {
|
||||
if (!Array.isArray(state.completedOutputItems)) {
|
||||
@@ -564,50 +537,6 @@ function flushEvents(state) {
|
||||
return events;
|
||||
}
|
||||
|
||||
export function normalizeUpstreamFailure(data, fallbackType = "server_error") {
|
||||
const response = data?.response && typeof data.response === "object" ? data.response : null;
|
||||
const error =
|
||||
response?.error && typeof response.error === "object"
|
||||
? response.error
|
||||
: data?.error && typeof data.error === "object"
|
||||
? data.error
|
||||
: null;
|
||||
|
||||
const code = typeof error?.code === "string" ? error.code : "";
|
||||
const message =
|
||||
typeof error?.message === "string"
|
||||
? error.message
|
||||
: typeof data?.message === "string"
|
||||
? data.message
|
||||
: "Upstream failure";
|
||||
|
||||
// Preserve upstream error semantics:
|
||||
// - context_length_exceeded → 400 (client can retry with smaller context)
|
||||
// - rate_limit_exceeded → 429 (client should back off)
|
||||
// - Everything else → 502 (upstream failure)
|
||||
const isContextOverflow = code === "context_length_exceeded";
|
||||
const isRateLimit = code === "rate_limit_exceeded" || code === "rate_limited";
|
||||
let status: number;
|
||||
let type: string;
|
||||
if (isRateLimit) {
|
||||
status = 429;
|
||||
type = "rate_limit_error";
|
||||
} else if (isContextOverflow) {
|
||||
status = 400;
|
||||
type = "invalid_request_error";
|
||||
} else {
|
||||
status = 502;
|
||||
type = fallbackType;
|
||||
}
|
||||
|
||||
return {
|
||||
status,
|
||||
type,
|
||||
code: code || (isRateLimit ? "rate_limit_exceeded" : "bad_gateway"),
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenAI Chat Completions streams announce the assistant role on the FIRST delta
|
||||
* (e.g. `{ "role": "assistant", "content": "" }` or `{ "role": "assistant",
|
||||
@@ -680,15 +609,6 @@ function buildResponsesReasoningDeltaChunk(state, text) {
|
||||
};
|
||||
}
|
||||
|
||||
function extractResponsesReasoningSummaryText(item) {
|
||||
if (!item || !Array.isArray(item.summary)) return "";
|
||||
return item.summary
|
||||
.map((part) =>
|
||||
part && typeof part === "object" && typeof part.text === "string" ? part.text : ""
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate OpenAI Responses API chunk to OpenAI Chat Completions format
|
||||
* This is for when Codex returns data and we need to send it to an OpenAI-compatible client
|
||||
|
||||
92
open-sse/translator/response/openai-responses/pureHelpers.ts
Normal file
92
open-sse/translator/response/openai-responses/pureHelpers.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
// Pure, stateless helpers for the OpenAI Responses <-> Chat response translator.
|
||||
// Extracted verbatim from response/openai-responses.ts (no host imports, no stream state).
|
||||
|
||||
export function normalizeToolName(value) {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
export function stripEmptyOptionalToolArgs(value, toolName) {
|
||||
if (value == null) return value;
|
||||
|
||||
if (typeof value === "string") {
|
||||
// JSON-string cleanup is intentionally scoped to Claude Code's Read tool.
|
||||
// For arbitrary tools, empty strings/arrays may be valid user payloads.
|
||||
if (toolName !== "Read") return value;
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (Array.isArray(parsed) || typeof parsed !== "object" || parsed === null) return value;
|
||||
const cleaned = stripEmptyOptionalToolArgs(parsed, toolName);
|
||||
return JSON.stringify(cleaned ?? {});
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(value) || typeof value !== "object") return value;
|
||||
|
||||
const cleaned = { ...value };
|
||||
for (const [key, entry] of Object.entries(cleaned)) {
|
||||
if (entry === "" || (Array.isArray(entry) && entry.length === 0)) {
|
||||
delete cleaned[key];
|
||||
}
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
export function normalizeOutputIndex(outputIndex) {
|
||||
const normalized = Number(outputIndex);
|
||||
return Number.isInteger(normalized) && normalized >= 0 ? normalized : 0;
|
||||
}
|
||||
|
||||
export function normalizeUpstreamFailure(data, fallbackType = "server_error") {
|
||||
const response = data?.response && typeof data.response === "object" ? data.response : null;
|
||||
const error =
|
||||
response?.error && typeof response.error === "object"
|
||||
? response.error
|
||||
: data?.error && typeof data.error === "object"
|
||||
? data.error
|
||||
: null;
|
||||
|
||||
const code = typeof error?.code === "string" ? error.code : "";
|
||||
const message =
|
||||
typeof error?.message === "string"
|
||||
? error.message
|
||||
: typeof data?.message === "string"
|
||||
? data.message
|
||||
: "Upstream failure";
|
||||
|
||||
// Preserve upstream error semantics:
|
||||
// - context_length_exceeded → 400 (client can retry with smaller context)
|
||||
// - rate_limit_exceeded → 429 (client should back off)
|
||||
// - Everything else → 502 (upstream failure)
|
||||
const isContextOverflow = code === "context_length_exceeded";
|
||||
const isRateLimit = code === "rate_limit_exceeded" || code === "rate_limited";
|
||||
let status: number;
|
||||
let type: string;
|
||||
if (isRateLimit) {
|
||||
status = 429;
|
||||
type = "rate_limit_error";
|
||||
} else if (isContextOverflow) {
|
||||
status = 400;
|
||||
type = "invalid_request_error";
|
||||
} else {
|
||||
status = 502;
|
||||
type = fallbackType;
|
||||
}
|
||||
|
||||
return {
|
||||
status,
|
||||
type,
|
||||
code: code || (isRateLimit ? "rate_limit_exceeded" : "bad_gateway"),
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
export function extractResponsesReasoningSummaryText(item) {
|
||||
if (!item || !Array.isArray(item.summary)) return "";
|
||||
return item.summary
|
||||
.map((part) =>
|
||||
part && typeof part === "object" && typeof part.text === "string" ? part.text : ""
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
// Split-guard for the response/openai-responses pure-helper extraction.
|
||||
// The stateless helpers (normalizeToolName / stripEmptyOptionalToolArgs /
|
||||
// normalizeOutputIndex / normalizeUpstreamFailure / extractResponsesReasoningSummaryText)
|
||||
// live in the pure leaf `openai-responses/pureHelpers.ts` (no stream state, no host import).
|
||||
// The host imports them back and re-exports normalizeUpstreamFailure for external importers.
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const RESP = join(HERE, "../../open-sse/translator/response");
|
||||
const HOST = join(RESP, "openai-responses.ts");
|
||||
const LEAF = join(RESP, "openai-responses/pureHelpers.ts");
|
||||
|
||||
test("leaf hosts the pure helpers, has no stream state and no host import", () => {
|
||||
const src = readFileSync(LEAF, "utf8");
|
||||
for (const sym of [
|
||||
"normalizeToolName",
|
||||
"stripEmptyOptionalToolArgs",
|
||||
"normalizeOutputIndex",
|
||||
"normalizeUpstreamFailure",
|
||||
"extractResponsesReasoningSummaryText",
|
||||
]) {
|
||||
assert.match(src, new RegExp(`export function ${sym}\\b`));
|
||||
}
|
||||
assert.doesNotMatch(src, /from "\.\.\/openai-responses\.ts"/);
|
||||
// No stream-state parameter leaked into the pure leaf (ignore comments).
|
||||
const code = src
|
||||
.split("\n")
|
||||
.filter((l) => !l.trim().startsWith("//"))
|
||||
.join("\n");
|
||||
assert.doesNotMatch(code, /\bstate\b/);
|
||||
});
|
||||
|
||||
test("host imports helpers back and re-exports normalizeUpstreamFailure", () => {
|
||||
const src = readFileSync(HOST, "utf8");
|
||||
assert.match(src, /from "\.\/openai-responses\/pureHelpers\.ts"/);
|
||||
assert.match(
|
||||
src,
|
||||
/export \{ normalizeUpstreamFailure \} from "\.\/openai-responses\/pureHelpers\.ts"/
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizeUpstreamFailure preserves upstream error semantics", async () => {
|
||||
const { normalizeUpstreamFailure } =
|
||||
await import("../../open-sse/translator/response/openai-responses/pureHelpers.ts");
|
||||
assert.equal(
|
||||
normalizeUpstreamFailure({ error: { code: "rate_limit_exceeded", message: "slow down" } })
|
||||
.status,
|
||||
429
|
||||
);
|
||||
assert.equal(
|
||||
normalizeUpstreamFailure({ error: { code: "context_length_exceeded", message: "too big" } })
|
||||
.status,
|
||||
400
|
||||
);
|
||||
assert.equal(normalizeUpstreamFailure({ message: "boom" }).status, 502);
|
||||
});
|
||||
Reference in New Issue
Block a user