mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 12:22:14 +03:00
* fix(sse): preserve Responses API hosted tools in Codex executor normalizeCodexTools was dropping every non-function tool, which stripped Codex CLI's built-in image_generation tool (and other hosted tools like web_search / file_search) before they reached OpenAI. Add a whitelist and structural check so they pass through, while keeping unknown types filtered locally with a debug log. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(sse): route DALL-E-style image generation through Codex hosted tool Adds a Codex branch to the /v1/images/generations handler so DALL-E-style requests with `model: codex/*` (or `cx/*`) are translated into /responses calls with the `image_generation` hosted tool, then unpacked back into OpenAI image response shape. Enables OpenWebUI and other clients that hit the legacy images endpoint to drive Codex image generation. Also defaults `store: false` in the Codex executor whenever an `image_generation` tool is present — the Codex backend rejects store=true with hosted image generation ("Store must be set to false"). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(sse): forward size/quality from DALL-E body to Codex image_generation tool The Codex image-gen shim was ignoring `size` and `quality` from the incoming /v1/images/generations body, so OpenWebUI's size and quality selectors had no effect. Forward both into the hosted tool config, mapping DALL-E's `standard`/`hd` to the image_generation tool's `medium`/`high` so legacy clients keep working. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(providers): add Petals and Nous Research provider support Register Nous Research as an OpenAI-compatible gateway with remote model discovery and validation against chat completions. Add Petals provider metadata, default config, validation, and a specialized executor that maps OpenAI-style requests to the public generate endpoint. Also allow optional API keys and configurable base URLs for Petals in the dashboard and provider schemas. Expand provider model and catalog tests to cover both integrations. * fix(resilience): sync queue updates and clear stale discovery caches Await runtime request queue updates so limiter settings and auto-enabled API key protections are recomputed when resilience settings change. Preserve cancelled batch state for in-flight work by marking input files processed without generating output artifacts, and replace cached synced models with an empty set when remote discovery returns no models so the providers route falls back to the local catalog instead of stale cache. --------- Co-authored-by: Payne <trader-payne@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
500 lines
15 KiB
TypeScript
500 lines
15 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
CodexExecutor,
|
|
getCodexModelScope,
|
|
getCodexRateLimitKey,
|
|
getCodexResetTime,
|
|
parseCodexQuotaHeaders,
|
|
} from "../../open-sse/executors/codex.ts";
|
|
import {
|
|
DEFAULT_THINKING_CONFIG,
|
|
setThinkingBudgetConfig,
|
|
ThinkingMode,
|
|
} from "../../open-sse/services/thinkingBudget.ts";
|
|
|
|
test.afterEach(() => {
|
|
setThinkingBudgetConfig(DEFAULT_THINKING_CONFIG);
|
|
});
|
|
|
|
async function withEnv(entries, fn) {
|
|
const previous = new Map();
|
|
|
|
for (const [key, value] of Object.entries(entries)) {
|
|
previous.set(key, process.env[key]);
|
|
if (value === undefined) {
|
|
delete process.env[key];
|
|
} else {
|
|
process.env[key] = value;
|
|
}
|
|
}
|
|
|
|
try {
|
|
return await fn();
|
|
} finally {
|
|
for (const [key, value] of previous.entries()) {
|
|
if (value === undefined) {
|
|
delete process.env[key];
|
|
} else {
|
|
process.env[key] = value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
test("Codex helper functions isolate rate-limit scopes and parse quota headers", () => {
|
|
const quota = parseCodexQuotaHeaders(
|
|
new Headers({
|
|
"x-codex-5h-usage": "100",
|
|
"x-codex-5h-limit": "500",
|
|
"x-codex-5h-reset-at": new Date(Date.now() + 60_000).toISOString(),
|
|
"x-codex-7d-usage": "1000",
|
|
"x-codex-7d-limit": "5000",
|
|
"x-codex-7d-reset-at": new Date(Date.now() + 120_000).toISOString(),
|
|
})
|
|
);
|
|
|
|
assert.equal(getCodexModelScope("codex-spark-mini"), "spark");
|
|
assert.equal(getCodexModelScope("gpt-5.3-codex"), "codex");
|
|
assert.equal(getCodexRateLimitKey("acct-1", "codex-spark-mini"), "acct-1:spark");
|
|
assert.equal(quota.usage5h, 100);
|
|
assert.equal(quota.limit7d, 5000);
|
|
assert.ok(getCodexResetTime(quota) >= new Date(quota.resetAt7d).getTime());
|
|
});
|
|
|
|
test("CodexExecutor.buildUrl honors /responses subpaths and compact mode", () => {
|
|
const executor = new CodexExecutor();
|
|
|
|
assert.equal(
|
|
executor.buildUrl("gpt-5.3-codex", true, 0, {}),
|
|
"https://chatgpt.com/backend-api/codex/responses"
|
|
);
|
|
assert.equal(
|
|
executor.buildUrl("gpt-5.3-codex", true, 0, { requestEndpointPath: "/responses" }),
|
|
"https://chatgpt.com/backend-api/codex/responses"
|
|
);
|
|
assert.equal(
|
|
executor.buildUrl("gpt-5.3-codex", true, 0, { requestEndpointPath: "/responses/compact" }),
|
|
"https://chatgpt.com/backend-api/codex/responses/compact"
|
|
);
|
|
});
|
|
|
|
test("CodexExecutor.buildHeaders binds workspace ids and disables SSE accept for compact responses", () => {
|
|
const executor = new CodexExecutor();
|
|
const standardHeaders = executor.buildHeaders(
|
|
{
|
|
accessToken: "codex-token",
|
|
providerSpecificData: { workspaceId: "workspace-1" },
|
|
},
|
|
true
|
|
);
|
|
const compactHeaders = executor.buildHeaders(
|
|
{
|
|
accessToken: "codex-token",
|
|
requestEndpointPath: "/responses/compact",
|
|
},
|
|
true
|
|
);
|
|
|
|
assert.equal(standardHeaders.Authorization, "Bearer codex-token");
|
|
assert.equal(standardHeaders.Accept, "text/event-stream");
|
|
assert.equal(standardHeaders["chatgpt-account-id"], "workspace-1");
|
|
assert.equal(standardHeaders.Version, "0.124.0");
|
|
assert.equal(standardHeaders["User-Agent"], "codex-cli/0.124.0 (Windows 10.0.26100; x64)");
|
|
assert.equal(compactHeaders.Accept, "application/json");
|
|
});
|
|
|
|
test("CodexExecutor.buildHeaders honors safe env overrides for Version and User-Agent", async () => {
|
|
const executor = new CodexExecutor();
|
|
|
|
await withEnv(
|
|
{
|
|
CODEX_CLIENT_VERSION: "0.120.0-alpha.3",
|
|
CODEX_USER_AGENT: undefined,
|
|
},
|
|
() => {
|
|
const headers = executor.buildHeaders({ accessToken: "codex-token" }, true);
|
|
assert.equal(headers.Version, "0.120.0-alpha.3");
|
|
assert.equal(headers["User-Agent"], "codex-cli/0.120.0-alpha.3 (Windows 10.0.26100; x64)");
|
|
}
|
|
);
|
|
|
|
await withEnv(
|
|
{
|
|
CODEX_CLIENT_VERSION: "bad version value",
|
|
CODEX_USER_AGENT: "custom-codex/9.9.9",
|
|
},
|
|
() => {
|
|
const headers = executor.buildHeaders({ accessToken: "codex-token" }, true);
|
|
assert.equal(headers.Version, "0.124.0");
|
|
assert.equal(headers["User-Agent"], "custom-codex/9.9.9");
|
|
}
|
|
);
|
|
});
|
|
|
|
test("CodexExecutor.transformRequest injects default instructions, clamps reasoning and strips unsupported fields", () => {
|
|
const executor = new CodexExecutor();
|
|
const body = {
|
|
model: "gpt-5-mini",
|
|
messages: [{ role: "user", content: "hello" }],
|
|
prompt: "legacy",
|
|
stream_options: { include_usage: true },
|
|
instructions: "",
|
|
reasoning_effort: "xhigh",
|
|
service_tier: "fast",
|
|
temperature: 0.4,
|
|
user: "cursor",
|
|
};
|
|
|
|
const result = executor.transformRequest("gpt-5-mini-xhigh", body, false, {
|
|
requestEndpointPath: "/responses",
|
|
});
|
|
|
|
assert.equal(result.stream, true);
|
|
assert.equal(result.store, true);
|
|
assert.equal(result.instructions.length > 0, true);
|
|
assert.equal(result.reasoning.effort, "high");
|
|
assert.equal(result.service_tier, "priority");
|
|
assert.equal(result.messages, undefined);
|
|
assert.equal(result.prompt, undefined);
|
|
assert.equal(result.temperature, undefined);
|
|
assert.equal(result.user, undefined);
|
|
assert.equal(result.stream_options, undefined);
|
|
});
|
|
|
|
test("CodexExecutor.transformRequest preserves compact requests and native passthrough semantics", () => {
|
|
const executor = new CodexExecutor();
|
|
const body = {
|
|
_nativeCodexPassthrough: true,
|
|
instructions: "keep this",
|
|
stream: false,
|
|
};
|
|
const result = executor.transformRequest("gpt-5.3-codex", body, false, {
|
|
requestEndpointPath: "/responses/compact",
|
|
providerSpecificData: {
|
|
requestDefaults: { serviceTier: "priority" },
|
|
},
|
|
});
|
|
|
|
assert.equal(result._nativeCodexPassthrough, undefined);
|
|
assert.equal(result.stream, undefined);
|
|
assert.equal(result.service_tier, "priority");
|
|
assert.equal(result.reasoning.effort, "medium");
|
|
assert.equal(result.store, true);
|
|
assert.equal(result.instructions, "keep this");
|
|
});
|
|
|
|
test("CodexExecutor.transformRequest preserves store-enabled responses state when explicitly enabled", () => {
|
|
const executor = new CodexExecutor();
|
|
const body = {
|
|
_nativeCodexPassthrough: true,
|
|
_omnirouteResponsesStore: true,
|
|
instructions: "keep this",
|
|
previous_response_id: "resp_prev_123",
|
|
stream: false,
|
|
};
|
|
|
|
const result = executor.transformRequest("gpt-5.3-codex", body, false, {
|
|
requestEndpointPath: "/responses/compact",
|
|
providerSpecificData: {
|
|
openaiStoreEnabled: true,
|
|
requestDefaults: { serviceTier: "priority" },
|
|
},
|
|
});
|
|
|
|
assert.equal(result._omnirouteResponsesStore, undefined);
|
|
assert.equal(result.store, true);
|
|
assert.equal(result.previous_response_id, undefined);
|
|
});
|
|
|
|
test("CodexExecutor.transformRequest applies per-connection reasoning and service tier defaults", () => {
|
|
const executor = new CodexExecutor();
|
|
const result = executor.transformRequest(
|
|
"gpt-5.3-codex",
|
|
{ model: "gpt-5.3-codex", input: [] },
|
|
false,
|
|
{
|
|
providerSpecificData: {
|
|
requestDefaults: {
|
|
reasoningEffort: "high",
|
|
serviceTier: "priority",
|
|
},
|
|
},
|
|
}
|
|
);
|
|
|
|
assert.equal(result.reasoning.effort, "high");
|
|
assert.equal(result.service_tier, "priority");
|
|
});
|
|
|
|
test("CodexExecutor.transformRequest keeps explicit request values ahead of connection defaults", () => {
|
|
const executor = new CodexExecutor();
|
|
const result = executor.transformRequest(
|
|
"gpt-5.3-codex",
|
|
{
|
|
model: "gpt-5.3-codex",
|
|
input: [],
|
|
reasoning_effort: "none",
|
|
service_tier: "standard",
|
|
},
|
|
false,
|
|
{
|
|
providerSpecificData: {
|
|
requestDefaults: {
|
|
reasoningEffort: "high",
|
|
serviceTier: "priority",
|
|
},
|
|
},
|
|
}
|
|
);
|
|
|
|
assert.equal(result.reasoning.effort, "none");
|
|
assert.equal(result.service_tier, "standard");
|
|
});
|
|
|
|
test("CodexExecutor.transformRequest lets model suffix beat connection reasoning defaults", () => {
|
|
const executor = new CodexExecutor();
|
|
const result = executor.transformRequest(
|
|
"gpt-5.3-codex-high",
|
|
{ model: "gpt-5.3-codex-high", input: [] },
|
|
false,
|
|
{
|
|
providerSpecificData: {
|
|
requestDefaults: {
|
|
reasoningEffort: "low",
|
|
},
|
|
},
|
|
}
|
|
);
|
|
|
|
assert.equal(result.model, "gpt-5.3-codex");
|
|
assert.equal(result.reasoning.effort, "high");
|
|
});
|
|
|
|
test("CodexExecutor.transformRequest does not apply connection reasoning defaults when Thinking Budget is not passthrough", () => {
|
|
const executor = new CodexExecutor();
|
|
setThinkingBudgetConfig({ mode: ThinkingMode.AUTO });
|
|
|
|
const noDefaults = executor.transformRequest(
|
|
"gpt-5.3-codex",
|
|
{ model: "gpt-5.3-codex", input: [] },
|
|
false,
|
|
{
|
|
providerSpecificData: {
|
|
requestDefaults: {
|
|
reasoningEffort: "high",
|
|
},
|
|
},
|
|
}
|
|
);
|
|
const explicit = executor.transformRequest(
|
|
"gpt-5.3-codex",
|
|
{ model: "gpt-5.3-codex", input: [], reasoning_effort: "high" },
|
|
false,
|
|
{
|
|
providerSpecificData: {
|
|
requestDefaults: {
|
|
reasoningEffort: "low",
|
|
},
|
|
},
|
|
}
|
|
);
|
|
|
|
assert.equal(noDefaults.reasoning, undefined);
|
|
assert.equal(explicit.reasoning.effort, "high");
|
|
});
|
|
|
|
test("CodexExecutor.refreshCredentials refreshes OAuth tokens and returns null without a refresh token", async () => {
|
|
const executor = new CodexExecutor();
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async (url) => {
|
|
assert.match(String(url), /auth\.openai\.com\/oauth\/token$/);
|
|
return new Response(
|
|
JSON.stringify({
|
|
access_token: "new-token",
|
|
refresh_token: "new-refresh",
|
|
expires_in: 3600,
|
|
}),
|
|
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
);
|
|
};
|
|
|
|
try {
|
|
assert.equal(await executor.refreshCredentials({}, null), null);
|
|
const refreshed = await executor.refreshCredentials({ refreshToken: "refresh-me" }, null);
|
|
assert.deepEqual(refreshed, {
|
|
accessToken: "new-token",
|
|
refreshToken: "new-refresh",
|
|
expiresIn: 3600,
|
|
});
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("CodexExecutor.transformRequest preserves image_generation hosted tool for Codex CLI", () => {
|
|
const executor = new CodexExecutor();
|
|
const body = {
|
|
_nativeCodexPassthrough: true,
|
|
input: [],
|
|
tools: [{ type: "image_generation", output_format: "png" }],
|
|
};
|
|
|
|
const result = executor.transformRequest("gpt-5.3-codex", body, true, {
|
|
requestEndpointPath: "/responses",
|
|
});
|
|
|
|
assert.deepEqual(result.tools, [{ type: "image_generation", output_format: "png" }]);
|
|
});
|
|
|
|
test("CodexExecutor.transformRequest preserves web_search and file_search hosted tools", () => {
|
|
const executor = new CodexExecutor();
|
|
const body = {
|
|
_nativeCodexPassthrough: true,
|
|
input: [],
|
|
tools: [
|
|
{ type: "web_search" },
|
|
{ type: "file_search" },
|
|
{ type: "image_generation", output_format: "png" },
|
|
],
|
|
};
|
|
|
|
const result = executor.transformRequest("gpt-5.3-codex", body, true, {
|
|
requestEndpointPath: "/responses",
|
|
});
|
|
|
|
assert.deepEqual(result.tools, [
|
|
{ type: "web_search" },
|
|
{ type: "file_search" },
|
|
{ type: "image_generation", output_format: "png" },
|
|
]);
|
|
});
|
|
|
|
test("CodexExecutor.transformRequest drops unknown hosted tool types", () => {
|
|
const executor = new CodexExecutor();
|
|
const body = {
|
|
_nativeCodexPassthrough: true,
|
|
input: [],
|
|
tools: [{ type: "made_up_tool" }, { type: "image_generation", output_format: "png" }],
|
|
};
|
|
|
|
const result = executor.transformRequest("gpt-5.3-codex", body, true, {
|
|
requestEndpointPath: "/responses",
|
|
});
|
|
|
|
assert.deepEqual(result.tools, [{ type: "image_generation", output_format: "png" }]);
|
|
});
|
|
|
|
test("CodexExecutor.transformRequest keeps valid function tools and drops empty-named ones", () => {
|
|
const executor = new CodexExecutor();
|
|
const body = {
|
|
_nativeCodexPassthrough: true,
|
|
input: [],
|
|
tools: [
|
|
{ type: "function", function: { name: "" } },
|
|
{ type: "function", function: { name: " " } },
|
|
{ type: "function", function: { name: "shell", parameters: {} } },
|
|
],
|
|
};
|
|
|
|
const result = executor.transformRequest("gpt-5.3-codex", body, true, {
|
|
requestEndpointPath: "/responses",
|
|
});
|
|
|
|
assert.equal(result.tools.length, 1);
|
|
assert.equal(result.tools[0].function.name, "shell");
|
|
});
|
|
|
|
test("CodexExecutor.transformRequest leaves hosted tool_choice untouched and strips stale function tool_choice", () => {
|
|
const executor = new CodexExecutor();
|
|
|
|
const hostedChoice = executor.transformRequest(
|
|
"gpt-5.3-codex",
|
|
{
|
|
_nativeCodexPassthrough: true,
|
|
input: [],
|
|
tools: [{ type: "image_generation", output_format: "png" }],
|
|
tool_choice: { type: "image_generation" },
|
|
},
|
|
true,
|
|
{ requestEndpointPath: "/responses" }
|
|
);
|
|
|
|
assert.deepEqual(hostedChoice.tool_choice, { type: "image_generation" });
|
|
|
|
const staleChoice = executor.transformRequest(
|
|
"gpt-5.3-codex",
|
|
{
|
|
_nativeCodexPassthrough: true,
|
|
input: [],
|
|
tools: [{ type: "function", function: { name: "shell" } }],
|
|
tool_choice: { type: "function", name: "ghost" },
|
|
},
|
|
true,
|
|
{ requestEndpointPath: "/responses" }
|
|
);
|
|
|
|
assert.equal(staleChoice.tool_choice, undefined);
|
|
});
|
|
|
|
test("CodexExecutor.transformRequest drops hosted tools that also declare name or function properties", () => {
|
|
const executor = new CodexExecutor();
|
|
const body = {
|
|
_nativeCodexPassthrough: true,
|
|
input: [],
|
|
tools: [
|
|
{ type: "image_generation", name: "img", output_format: "png" },
|
|
{ type: "image_generation", function: { name: "img" }, output_format: "png" },
|
|
{ type: "image_generation", output_format: "png" },
|
|
],
|
|
};
|
|
|
|
const result = executor.transformRequest("gpt-5.3-codex", body, true, {
|
|
requestEndpointPath: "/responses",
|
|
});
|
|
|
|
assert.deepEqual(result.tools, [{ type: "image_generation", output_format: "png" }]);
|
|
});
|
|
|
|
test("CodexExecutor.transformRequest defaults store to false when image_generation tool is present", () => {
|
|
const executor = new CodexExecutor();
|
|
|
|
const withImageGen = executor.transformRequest(
|
|
"gpt-5.3-codex",
|
|
{
|
|
_nativeCodexPassthrough: true,
|
|
input: [],
|
|
tools: [{ type: "image_generation", output_format: "png" }],
|
|
},
|
|
true,
|
|
{ requestEndpointPath: "/responses" }
|
|
);
|
|
assert.equal(withImageGen.store, false);
|
|
|
|
const withoutImageGen = executor.transformRequest(
|
|
"gpt-5.3-codex",
|
|
{
|
|
_nativeCodexPassthrough: true,
|
|
input: [],
|
|
tools: [{ type: "function", function: { name: "shell" } }],
|
|
},
|
|
true,
|
|
{ requestEndpointPath: "/responses" }
|
|
);
|
|
assert.equal(withoutImageGen.store, true);
|
|
|
|
const explicitTrue = executor.transformRequest(
|
|
"gpt-5.3-codex",
|
|
{
|
|
_nativeCodexPassthrough: true,
|
|
input: [],
|
|
store: true,
|
|
tools: [{ type: "image_generation", output_format: "png" }],
|
|
},
|
|
true,
|
|
{ requestEndpointPath: "/responses" }
|
|
);
|
|
assert.equal(explicitTrue.store, true);
|
|
});
|