mirror of
https://github.com/teamchong/pxpipe.git
synced 2026-07-22 02:02:51 +02:00
fix(openai): record outgoingTextChars for Responses transforms
transformOpenAIChatCompletions set info.outgoingTextChars = countOutgoingTextChars(req) before returning; transformOpenAIResponses set info.compressed = true and returned without it. That field is the regression denominator (tokens ≈ α·outgoingTextChars + β·imagePixels), so compressed Responses rows silently carried no denominator. - add countResponsesOutgoingTextChars(): instructions + message-item text (string or input_text parts, via responsesContentText so input_image base64 is excluded) + flat tool name/description/parameters - set it on the Responses success path, mirroring the Chat path - test asserts outgoingTextChars > 0 for a compressed Responses request
This commit is contained in:
@@ -418,6 +418,32 @@ function countOutgoingTextChars(req: OpenAIChatRequest): number {
|
||||
return n;
|
||||
}
|
||||
|
||||
/** Outgoing text-char denominator for the GPT Responses regression, mirroring
|
||||
* countOutgoingTextChars for Chat: instructions + message-item text (string or
|
||||
* input_text parts) + flat tool name/description/parameters. input_image base64
|
||||
* is excluded on purpose — it is image cost, not text (responsesContentText
|
||||
* already drops non-text parts). */
|
||||
function countResponsesOutgoingTextChars(req: ResponsesRequest): number {
|
||||
let n = 0;
|
||||
if (typeof req.instructions === 'string') n += req.instructions.length;
|
||||
if (typeof req.input === 'string') {
|
||||
n += req.input.length;
|
||||
} else if (Array.isArray(req.input)) {
|
||||
for (const item of req.input) {
|
||||
n += responsesContentText((item as ResponsesInputItem).content).length;
|
||||
}
|
||||
}
|
||||
if (Array.isArray(req.tools)) {
|
||||
for (const tool of req.tools) {
|
||||
if (!isFlatFunctionTool(tool)) continue;
|
||||
if (typeof tool.name === 'string') n += tool.name.length;
|
||||
if (typeof tool.description === 'string') n += tool.description.length;
|
||||
if (tool.parameters !== undefined) n += safeStringifyLen(tool.parameters);
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
function safeStringifyLen(v: unknown): number {
|
||||
try {
|
||||
return JSON.stringify(v)?.length ?? 0;
|
||||
@@ -986,6 +1012,9 @@ export async function transformOpenAIResponses(
|
||||
|
||||
if (rewrittenTools !== undefined) req.tools = rewrittenTools;
|
||||
|
||||
// Regression denominator, same as the Chat path — Responses was the only
|
||||
// transform that never recorded it.
|
||||
info.outgoingTextChars = countResponsesOutgoingTextChars(req);
|
||||
info.compressed = true;
|
||||
return { body: new TextEncoder().encode(JSON.stringify(req)), info };
|
||||
}
|
||||
|
||||
@@ -347,6 +347,19 @@ describe('transformOpenAIResponses (gpt-5.6)', () => {
|
||||
expect(textParts.some((p) => p.text?.includes('Do the thing'))).toBe(true);
|
||||
});
|
||||
|
||||
it('records outgoingTextChars for compressed Responses requests (denominator parity with Chat)', async () => {
|
||||
const body = enc.encode(JSON.stringify({
|
||||
model: 'gpt-5.6',
|
||||
instructions: BIG_INSTRUCTIONS,
|
||||
input: [{ role: 'user', content: 'Please do the thing.' }],
|
||||
}));
|
||||
const result = await transformOpenAIResponses(body, { charsPerToken: 1, minCompressChars: 1 });
|
||||
expect(result.info.compressed).toBe(true);
|
||||
// Chat set this via countOutgoingTextChars; Responses never recorded it, so
|
||||
// the tokens ≈ α·outgoingTextChars + β·imagePixels denominator was missing.
|
||||
expect(result.info.outgoingTextChars).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('returns compressed=false with not_profitable/below_min reason for small input', async () => {
|
||||
const body = enc.encode(JSON.stringify({
|
||||
model: 'gpt-5.6',
|
||||
|
||||
Reference in New Issue
Block a user