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:
Diego Rodrigues de Sa e Souza
2026-07-02 16:38:38 -03:00
committed by GitHub
parent 283a501a1a
commit b6249dd374
3 changed files with 161 additions and 89 deletions

View File

@@ -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

View 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("");
}

View File

@@ -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);
});