mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 23:02:10 +03:00
Clients like the Codex app emit parameters:{type:null,...} for some tools;
OpenAI-compatible upstreams reject with '400 Invalid schema for function ...:
schema must be a JSON Schema of type object, got type null'. toolSchemaSanitizer
already dropped the null; it now re-adds the mandatory root object type (plus
empty properties / open additionalProperties when absent). Combinator roots
(anyOf/oneOf/allOf) and explicit root types are preserved.
Regression guard: 5 new cases in tests/unit/tool-schema-sanitizer.test.mjs.
This commit is contained in:
committed by
GitHub
parent
0776f83fd0
commit
a69547a7f5
@@ -34,6 +34,8 @@
|
||||
|
||||
- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199)
|
||||
|
||||
- **fix(sse):** tool-call function schemas with a root `type: null` are now coerced to `type: "object"` before dispatch ([#6359](https://github.com/diegosouzapw/OmniRoute/issues/6359)) — clients like the Codex app emit `parameters: { type: null, ... }` for some tools, which OpenAI-compatible upstreams reject with `400 Invalid schema for function '...': schema must be a JSON Schema of 'type: "object"', got 'type: null'`, failing the whole request. `toolSchemaSanitizer` already stripped the null; it now re-adds the mandatory root `"object"` type (and empty `properties`/open `additionalProperties` when absent). Combinator roots (`anyOf`/`oneOf`/`allOf`) and explicit root types are left untouched. Regression guard: 5 new cases in `tests/unit/tool-schema-sanitizer.test.mjs`.
|
||||
|
||||
- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`.
|
||||
|
||||
- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak)
|
||||
|
||||
@@ -110,8 +110,31 @@ function sanitizeSchema(value: unknown, depth = 0): Record<string, unknown> {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenAI's Responses API strict validator requires the ROOT parameters schema to
|
||||
* declare `type: "object"` explicitly. Clients like the Codex app emit
|
||||
* `type: null` (rejected upstream as: schema must be a JSON Schema of
|
||||
* 'type: "object"', got 'type: null' — issue #6359). sanitizeSchema drops the
|
||||
* null, so at the root we re-add the mandatory "object". Combinator roots
|
||||
* (anyOf/oneOf/allOf) are left alone — injecting a sibling `type` would change
|
||||
* their meaning — and explicit root types are preserved as-is.
|
||||
*/
|
||||
function ensureRootObjectType(schema: Record<string, unknown>): void {
|
||||
if (hasOwn(schema, "type")) return;
|
||||
if (hasOwn(schema, "anyOf") || hasOwn(schema, "oneOf") || hasOwn(schema, "allOf")) return;
|
||||
schema.type = "object";
|
||||
if (!isPlainObject(schema.properties)) {
|
||||
schema.properties = {};
|
||||
if (!hasOwn(schema, "additionalProperties")) schema.additionalProperties = true;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeParameters(parameters: unknown): unknown {
|
||||
if (isPlainObject(parameters)) return sanitizeSchema(parameters);
|
||||
if (isPlainObject(parameters)) {
|
||||
const sanitized = sanitizeSchema(parameters);
|
||||
ensureRootObjectType(sanitized);
|
||||
return sanitized;
|
||||
}
|
||||
if (parameters === null || parameters === undefined) {
|
||||
return { type: "object", properties: {}, additionalProperties: true };
|
||||
}
|
||||
|
||||
@@ -456,3 +456,62 @@ describe("toolSchemaSanitizer", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("root type coercion (#6359)", () => {
|
||||
it("coerces root `type: null` to \"object\" (Codex codex_app__automation_update reproduction)", () => {
|
||||
const tool = {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "codex_app__automation_update",
|
||||
parameters: {
|
||||
type: null,
|
||||
properties: { schedule: { type: "string" } },
|
||||
required: ["schedule"],
|
||||
},
|
||||
},
|
||||
};
|
||||
const out = sanitizeOpenAITool(tool);
|
||||
assert.equal(out.function.parameters.type, "object");
|
||||
assert.deepEqual(out.function.parameters.properties.schedule, { type: "string" });
|
||||
});
|
||||
|
||||
it("adds `type: \"object\"` when the root schema has properties but no type", () => {
|
||||
const tool = {
|
||||
type: "function",
|
||||
function: { name: "x", parameters: { properties: { a: { type: "number" } } } },
|
||||
};
|
||||
const out = sanitizeOpenAITool(tool);
|
||||
assert.equal(out.function.parameters.type, "object");
|
||||
});
|
||||
|
||||
it("coerces root type on Responses-shape tools too", () => {
|
||||
const tool = {
|
||||
type: "function",
|
||||
name: "y",
|
||||
parameters: { type: null, properties: {} },
|
||||
};
|
||||
const out = sanitizeOpenAITool(tool);
|
||||
assert.equal(out.parameters.type, "object");
|
||||
});
|
||||
|
||||
it("does not inject a root type when the root is a combinator (anyOf)", () => {
|
||||
const tool = {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "z",
|
||||
parameters: { anyOf: [{ type: "object", properties: {} }] },
|
||||
},
|
||||
};
|
||||
const out = sanitizeOpenAITool(tool);
|
||||
assert.equal(out.function.parameters.type, undefined);
|
||||
});
|
||||
|
||||
it("leaves an explicit root `type: \"object\"` untouched", () => {
|
||||
const tool = {
|
||||
type: "function",
|
||||
function: { name: "w", parameters: { type: "object", properties: { b: { type: "boolean" } } } },
|
||||
};
|
||||
const out = sanitizeOpenAITool(tool);
|
||||
assert.equal(out.function.parameters.type, "object");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user