Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion open-sse/config/providerFieldStrips.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,27 @@ export function replaceRedactedAdvisorResults<T>(body: T): { body: T; replaced:
return { body: { ...(body as object), messages: nextMessages } as T, replaced };
}
/** Immutably drop request fields Groq rejects with a 400. */
export function stripGroqUnsupportedFields<T extends Record<string, unknown>>(body: T): T {
export function stripGroqUnsupportedFields<T extends Record<string, unknown>>(
body: T,
model?: string,
forcedEffort?: string
): T {
if (!body || typeof body !== "object") return body;
const next: Record<string, unknown> = { ...body };
delete next.logprobs;
delete next.logit_bias;
delete next.top_logprobs;
// Groq Chat Completions accepts reasoning_effort, not the other protocol envelopes.
const requestedEffort =
forcedEffort ??
next.reasoning_effort ??
(next.reasoning as Record<string, unknown> | undefined)?.effort ??
(next.output_config as Record<string, unknown> | undefined)?.effort;
if (model === "openai/gpt-oss-20b" && (requestedEffort === "none" || requestedEffort === "low")) {
next.reasoning_effort = "low";
}
delete next.reasoning;
delete next.output_config;
if (Array.isArray(next.messages)) {
next.messages = next.messages.map((m) => {
if (m && typeof m === "object") {
Expand Down
5 changes: 4 additions & 1 deletion open-sse/executors/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { resolveKeyForRequest } from "../services/apiKeyRotator.ts";
import type { KeyHealth } from "../services/apiKeyRotator.ts";
import { getOpenAICompatibleType, isClaudeCodeCompatible } from "../services/provider.ts";
import { usesCcWireImage } from "../services/ccWireImageBuiltins.ts";
import { getForcedReasoningEffort } from "../utils/reasoningRuleContext.ts";
import {
runWithOnPersist,
getRefreshLeadMs,
Expand Down Expand Up @@ -893,7 +894,9 @@ export class BaseExecutor {
);
if (this.provider === "groq") {
transformedBody = stripGroqUnsupportedFields(
transformedBody as Record<string, unknown>
transformedBody as Record<string, unknown>,
model,
getForcedReasoningEffort(requestCredentials)
) as typeof transformedBody;
}
// A previous URL in this execute() already hit a thinking_budget 400 and
Expand Down
8 changes: 7 additions & 1 deletion open-sse/utils/socksConnectorWithFamily.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,11 @@ export function socksConnectorWithFamily(
// Sequential budget: both phases bounded by the same connectTimeout → wall-time up to 60s for https
// (vs 30s direct). Shared-deadline alternative rejected as unjustified complexity.
const build = _buildConnectorForTest ?? buildConnector;
// This custom connector bypasses Agent.allowH2, and buildConnector defaults allowH2 to true,
// so ALPN still negotiated h2 over SOCKS. Pin the TLS hop to HTTP/1.1 as well.
const h1Opts = { ...tlsOpts, allowH2: false };
const undiciConnect = build(
tlsTimeout !== undefined ? { ...tlsOpts, timeout: tlsTimeout } : tlsOpts
tlsTimeout !== undefined ? { ...h1Opts, timeout: tlsTimeout } : h1Opts
);
const socketOptions = buildSocksFamilySocketOptions(family);
return async (options, callback) => {
Expand Down Expand Up @@ -99,6 +102,9 @@ export function createSocksDispatcherWithFamily(
};
return new Agent({
...rest,
// Undici 8 negotiates HTTP/2 by default; its h2 client over SOCKS reset streams
// (ERR_HTTP2_STREAM_ERROR) and emitted listener-less stream errors that crashed the process.
allowH2: false,
connect: socksConnectorWithFamily(proxy, family, connect, connectTimeout),
});
}
1 change: 1 addition & 0 deletions open-sse/vendor/codex-chatgpt-web/chatgpt-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const CHATGPT_COMPOSER_SELECTOR = [
'[data-testid="prompt-textarea"]',
"#prompt-textarea",
'[contenteditable="true"][data-lexical-editor="true"]',
'[contenteditable="true"][role="textbox"].ProseMirror',
].join(", ");
export const CHATGPT_EFFORT_CONTROL_SELECTOR = [
'button[aria-haspopup="menu"][data-tone="neutral"]',
Expand Down
1 change: 1 addition & 0 deletions src/shared/utils/httpClientAbortGuard.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ export function isUpstreamNetworkError(err) {
if (e.name === "TypeError" && e.message === "fetch failed") return true;
switch (e.code) {
case "PROXY_UNREACHABLE":
case "PROXY_REQUEST_FAILED":
case "UND_ERR_SOCKET":
case "UND_ERR_CONNECT_TIMEOUT":
case "UND_ERR_HEADERS_TIMEOUT":
Expand Down
22 changes: 22 additions & 0 deletions tests/unit/chatgpt-composer-selector.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import test from "node:test";
import assert from "node:assert/strict";
import { JSDOM } from "jsdom";
import { CHATGPT_COMPOSER_SELECTOR } from "../../open-sse/vendor/codex-chatgpt-web/chatgpt-session.ts";

test("composer selector matches current ProseMirror and legacy editors", () => {
const dom = new JSDOM(`
<div id="current" contenteditable="true" role="textbox" class="ProseMirror" aria-label="Ask ChatGPT"></div>
<textarea id="prompt-textarea"></textarea>
<div id="legacy" contenteditable="true" data-lexical-editor="true"></div>
<div id="readonly" role="textbox" class="ProseMirror"></div>
`);
try {
const matches = [...dom.window.document.querySelectorAll(CHATGPT_COMPOSER_SELECTOR)];
assert.deepEqual(
matches.map((element) => element.id),
["current", "prompt-textarea", "legacy"]
);
} finally {
dom.window.close();
}
});
39 changes: 39 additions & 0 deletions tests/unit/groq-field-strip-wiring.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { withReasoningRuleContext } from "../../open-sse/utils/reasoningRuleContext.ts";
import { DefaultExecutor } from "../../open-sse/executors/default.ts";

// ──────────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -117,3 +118,41 @@ test("Non-Groq executor does NOT strip logprobs or messages[].name", async () =>
const messages = capturedBody!.messages as Record<string, unknown>[];
assert.equal(messages[0].name, "bob");
});

test("Groq GPT-OSS dispatch maps forced none to low and drops foreign reasoning envelopes", async () => {
const executor = new DefaultExecutor("groq");
const originalFetch = globalThis.fetch;
let capturedBody: Record<string, unknown> | undefined;
globalThis.fetch = async (_url: string | URL | Request, init: RequestInit = {}) => {
capturedBody = JSON.parse(String(init.body));
return new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
};
try {
await executor.execute({
model: "openai/gpt-oss-20b",
body: {
messages: [{ role: "user", content: "hello" }],
reasoning: { effort: "none" },
output_config: { effort: "none" },
},
stream: false,
credentials: withReasoningRuleContext(
{ apiKey: "groq-test-key" },
{
id: "test-rule",
effortMode: "force",
targetEffort: "none",
}
),
});
} finally {
globalThis.fetch = originalFetch;
}
assert.ok(capturedBody, "the executor must dispatch a request");
assert.equal(capturedBody.reasoning_effort, "low");
assert.equal("reasoning" in capturedBody, false);
assert.equal("output_config" in capturedBody, false);
});
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,15 @@ test("isUpstreamNetworkError recognizes fetch failures and proxy unreachable", (

const undiciSocket = Object.assign(new Error("other side closed"), { code: "UND_ERR_SOCKET" });
assert.equal(isUpstreamNetworkError(undiciSocket), true);

// proxyFetch wraps the undici failure as PROXY_REQUEST_FAILED.
const proxyRequestFailed = Object.assign(
new Error("Proxy request failed: fetch failed (cause ERR_HTTP2_STREAM_ERROR)"),
{ code: "PROXY_REQUEST_FAILED", causeCode: "ERR_HTTP2_STREAM_ERROR" }
);
assert.equal(isUpstreamNetworkError(proxyRequestFailed), true);
assert.equal(shouldSwallowUncaught(proxyRequestFailed, "uncaughtException"), true);
assert.equal(shouldSwallowUncaught(proxyRequestFailed, "unhandledRejection"), true);
});

test("isUpstreamNetworkError rejects genuine errors", () => {
Expand Down
32 changes: 32 additions & 0 deletions tests/unit/provider-field-strips.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,35 @@ test("stripGroqUnsupportedFields drops unsupported messages[].model and other me
assert.equal("messageId" in out.messages[1], false);
assert.equal("sender" in out.messages[1], false);
});

// Routing envelopes must become valid Groq Chat Completions fields.
test("Groq GPT-OSS routing low/none uses low without foreign envelopes", () => {
for (const effort of ["low", "none"]) {
const input = {
model: "openai/gpt-oss-20b",
messages: [{ role: "user", content: "hello" }],
reasoning_effort: effort,
reasoning: { effort },
output_config: { effort },
};
const out = stripGroqUnsupportedFields(input, input.model);
assert.equal(out.reasoning_effort, "low");
assert.equal("reasoning" in out, false);
assert.equal("output_config" in out, false);
assert.deepEqual(out.messages, input.messages);
assert.equal(input.reasoning_effort, effort);
assert.deepEqual(input.output_config, { effort });
}
});

test("Groq forced none uses low even when routing removed the effort fields", () => {
const input = { model: "openai/gpt-oss-20b", messages: [{ role: "user", content: "hello" }] };
const out = stripGroqUnsupportedFields(input, input.model, "none") as Record<string, unknown>;
assert.equal(out.reasoning_effort, "low");
// An ordinary effort-less request must retain its provider default.
assert.equal("reasoning_effort" in stripGroqUnsupportedFields(input, input.model), false);
assert.equal(
stripGroqUnsupportedFields({ reasoning_effort: "high" }, input.model).reasoning_effort,
"high"
);
});
55 changes: 54 additions & 1 deletion tests/unit/socks-connector-family.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { buildSocksFamilySocketOptions } from "../../open-sse/utils/socksConnectorWithFamily.ts";
import { MockAgent, fetch } from "undici";
import {
buildSocksFamilySocketOptions,
createSocksDispatcherWithFamily,
socksConnectorWithFamily,
} from "../../open-sse/utils/socksConnectorWithFamily.ts";

describe("socksConnectorWithFamily", () => {
it("returns family:6 + autoSelectFamily:false for ipv6", () => {
Expand All @@ -13,3 +18,51 @@ describe("socksConnectorWithFamily", () => {
assert.deepEqual(buildSocksFamilySocketOptions(null), {});
});
});

// Inspect the options at the real Agent-to-pool dispatch boundary without network I/O.
it("SOCKS dispatch disables HTTP/2 even when the caller enables it", async () => {
const mockAgent = new MockAgent();
mockAgent.disableNetConnect();
const pool = mockAgent.get("https://example.com");
pool.intercept({ path: "/", method: "GET" }).reply(200, "ok");
let dispatchedOptions: Record<string, unknown> | undefined;
const dispatcher = createSocksDispatcherWithFamily(
{ host: "127.0.0.1", port: 1080, type: 5 },
4,
{
allowH2: true,
factory: (_origin, options) => {
dispatchedOptions = options as Record<string, unknown>;
return pool;
},
}
);
try {
const response = await fetch("https://example.com/", { dispatcher });
assert.equal(await response.text(), "ok");
assert.ok(dispatchedOptions, "the Agent must dispatch through the pool factory");
assert.equal(dispatchedOptions.allowH2, false);
mockAgent.assertNoPendingInterceptors();
} finally {
await dispatcher.close();
await mockAgent.close();
}
});

// The custom connect bypasses Agent.allowH2; the TLS connector itself must not offer h2 via ALPN.
it("SOCKS TLS connector is built with allowH2:false", () => {
let built: Record<string, unknown> | undefined;
const fakeBuild = ((opts: Record<string, unknown>) => {
built = opts;
return () => {};
}) as never;
socksConnectorWithFamily(
{ host: "127.0.0.1", port: 1080, type: 5 },
4,
{ allowH2: true } as never,
5000,
fakeBuild
);
assert.equal(built?.allowH2, false);
assert.equal(built?.timeout, 5000);
});