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
191 changes: 170 additions & 21 deletions src/lib/codex/provider-overrides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,89 @@ function normalizeImageGenerationPreference(
return value === "true";
}

function isImageGenerationTool(value: unknown): value is Record<string, unknown> {
return isPlainObject(value) && value.type === "image_generation";
function toImageGenerationToolReference(value: unknown): Record<string, unknown> | null {
if (!isPlainObject(value)) {
return null;
}
if (value.type === "image_generation") {
return { type: "image_generation" };
}
if (value.type === "namespace") {
if (value.name === "image_gen") {
return { type: "namespace", name: "image_gen" };
}
if (value.namespace === "image_gen") {
return { type: "namespace", namespace: "image_gen" };
}
}
return null;
}

function isImageGenerationTool(value: unknown): boolean {
return toImageGenerationToolReference(value) !== null;
}

function hasImageGenerationTool(value: unknown): boolean {
return Array.isArray(value) && value.some((tool) => isImageGenerationTool(tool));
}

function hasInputImageGenerationTool(value: unknown): boolean {
return (
Array.isArray(value) &&
value.some(
(item) =>
isPlainObject(item) &&
item.type === "additional_tools" &&
hasImageGenerationTool(item.tools)
)
);
}

function findImageGenerationToolReference(
request: Record<string, unknown>
): Record<string, unknown> | null {
if (Array.isArray(request.tools)) {
for (const tool of request.tools) {
const reference = toImageGenerationToolReference(tool);
if (reference) {
return reference;
}
}
}

if (!Array.isArray(request.input)) {
return null;
}
for (const item of request.input) {
if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) {
continue;
}
for (const tool of item.tools) {
const reference = toImageGenerationToolReference(tool);
if (reference) {
return reference;
}
}
}
return null;
}

function hasAvailableTool(request: Record<string, unknown>): boolean {
if (Array.isArray(request.tools) && request.tools.length > 0) {
return true;
}
return (
Array.isArray(request.input) &&
request.input.some(
(item) =>
isPlainObject(item) &&
item.type === "additional_tools" &&
Array.isArray(item.tools) &&
item.tools.length > 0
)
);
}

function applyImageGenerationToolPreference(
request: Record<string, unknown>,
ensureCloned: () => Record<string, unknown>,
Expand All @@ -70,7 +145,10 @@ function applyImageGenerationToolPreference(

const existingTools = Array.isArray(request.tools) ? request.tools : null;
if (enabled) {
if (existingTools?.some((tool) => isImageGenerationTool(tool))) {
if (
existingTools?.some((tool) => isImageGenerationTool(tool)) ||
hasInputImageGenerationTool(request.input)
) {
return;
}
const target = ensureCloned();
Expand All @@ -97,16 +175,75 @@ function applyImageGenerationToolPreference(
}
}

function applyInputImageGenerationToolPreference(
request: Record<string, unknown>,
ensureCloned: () => Record<string, unknown>,
enabled: boolean | null
): void {
if (enabled !== false || !Array.isArray(request.input)) {
return;
}

const nextInput: unknown[] = [];
let changed = false;
for (const item of request.input) {
if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) {
nextInput.push(item);
continue;
}

const nextTools = item.tools.filter((tool) => !isImageGenerationTool(tool));
if (nextTools.length === item.tools.length) {
nextInput.push(item);
continue;
}

changed = true;
if (nextTools.length > 0) {
nextInput.push({ ...item, tools: nextTools });
}
}

if (changed) {
ensureCloned().input = nextInput;
}
}

function isImageGenerationToolChoice(value: unknown): boolean {
if (typeof value === "string") {
return value === "image_generation";
}
if (!isPlainObject(value)) {
return false;
}
if (isImageGenerationTool(value)) {
return true;
}
return isImageGenerationTool(value.tool);
}

function summarizeImageGenerationToolChoice(value: unknown): string | null {
if (typeof value === "string") {
return value;
}
if (!isPlainObject(value) || typeof value.type !== "string") {
if (!isPlainObject(value)) {
return null;
}
if (isImageGenerationTool(value.tool)) {
return "tool:image_generation";
}
if (typeof value.type !== "string") {
return null;
}
if (value.type === "image_generation") {
return "image_generation";
}
if (
value.type === "namespace" &&
(value.name === "image_gen" || value.namespace === "image_gen")
) {
return "namespace:image_gen";
}
if (value.type !== "allowed_tools") {
return value.type;
}
Expand All @@ -127,7 +264,7 @@ function applyImageGenerationToolChoicePreference(
ensureCloned: () => Record<string, unknown>,
enabled: boolean | null,
context: {
hadImageGenerationTool: boolean;
imageGenerationToolReference: Record<string, unknown> | null;
hasAvailableTools: boolean;
}
): void {
Expand All @@ -146,7 +283,10 @@ function applyImageGenerationToolChoicePreference(
const target = ensureCloned();
target.tool_choice = {
...toolChoice,
tools: [...toolChoice.tools, { type: "image_generation" }],
tools: [
...toolChoice.tools,
context.imageGenerationToolReference ?? { type: "image_generation" },
],
};
return;
}
Expand All @@ -163,18 +303,18 @@ function applyImageGenerationToolChoicePreference(
return;
}

if (!isPlainObject(toolChoice)) {
return;
}

if (toolChoice.type === "image_generation") {
if (isImageGenerationToolChoice(toolChoice)) {
const target = ensureCloned();
// 只剩非图像工具时改成 none,避免回退到 auto 后放宽客户端原本的工具限制。
target.tool_choice = "none";
return;
}

if (toolChoice.type !== "allowed_tools" || !Array.isArray(toolChoice.tools)) {
if (
!isPlainObject(toolChoice) ||
toolChoice.type !== "allowed_tools" ||
!Array.isArray(toolChoice.tools)
) {
return;
}

Expand All @@ -200,7 +340,7 @@ function applyImageGenerationToolChoicePreference(
* - 偏好值为 null/undefined/"inherit" 表示“遵循客户端”
* - 覆写仅影响以下字段:
* - parallel_tool_calls
* - tools / tool_choice 中与 image_generation 相关的能力声明
* - tools / input.additional_tools / tool_choice 中与 image_generation 相关的能力声明
* - reasoning.effort / reasoning.summary
* - service_tier
* - text.verbosity
Expand Down Expand Up @@ -231,12 +371,14 @@ export function applyCodexProviderOverrides(
const imageGeneration = normalizeImageGenerationPreference(
provider.codexImageGenerationPreference
);
const hadImageGenerationTool = hasImageGenerationTool(output.tools);
applyImageGenerationToolPreference(output, ensureCloned, imageGeneration);
applyImageGenerationToolChoicePreference(output, ensureCloned, imageGeneration, {
hadImageGenerationTool,
hasAvailableTools: Array.isArray(output.tools) && output.tools.length > 0,
});
if (imageGeneration !== null) {
applyImageGenerationToolPreference(output, ensureCloned, imageGeneration);
applyInputImageGenerationToolPreference(output, ensureCloned, imageGeneration);
applyImageGenerationToolChoicePreference(output, ensureCloned, imageGeneration, {
imageGenerationToolReference: findImageGenerationToolReference(output),
hasAvailableTools: hasAvailableTool(output),
});
}

const reasoningEffort = normalizeStringPreference(provider.codexReasoningEffortPreference);
const reasoningSummary = normalizeStringPreference(provider.codexReasoningSummaryPreference);
Expand Down Expand Up @@ -289,8 +431,6 @@ export function applyCodexProviderOverridesWithAudit(
const serviceTier = normalizeStringPreference(provider.codexServiceTierPreference);

const beforeServiceTier = toAuditValue(request.service_tier);
const beforeImageGeneration = hasImageGenerationTool(request.tools);

const hit =
parallelToolCalls !== null ||
imageGeneration !== null ||
Expand All @@ -304,6 +444,8 @@ export function applyCodexProviderOverridesWithAudit(
return { request, audit: null };
}

const beforeImageGeneration = hasImageGenerationTool(request.tools);
const beforeInputImageGeneration = hasInputImageGenerationTool(request.input);
const beforeParallelToolCalls = toAuditValue(request.parallel_tool_calls);
const beforeReasoning = isPlainObject(request.reasoning) ? request.reasoning : null;
const beforeReasoningEffort = toAuditValue(beforeReasoning?.effort);
Expand All @@ -318,6 +460,7 @@ export function applyCodexProviderOverridesWithAudit(

const afterParallelToolCalls = toAuditValue(nextRequest.parallel_tool_calls);
const afterImageGeneration = hasImageGenerationTool(nextRequest.tools);
const afterInputImageGeneration = hasInputImageGenerationTool(nextRequest.input);
const afterReasoning = isPlainObject(nextRequest.reasoning) ? nextRequest.reasoning : null;
const afterReasoningEffort = toAuditValue(afterReasoning?.effort);
const afterReasoningSummary = toAuditValue(afterReasoning?.summary);
Expand All @@ -338,6 +481,12 @@ export function applyCodexProviderOverridesWithAudit(
after: afterImageGeneration,
changed: !Object.is(beforeImageGeneration, afterImageGeneration),
},
{
path: "input.additional_tools.image_generation",
before: beforeInputImageGeneration,
after: afterInputImageGeneration,
changed: !Object.is(beforeInputImageGeneration, afterInputImageGeneration),
},
{
path: "reasoning.effort",
before: beforeReasoningEffort,
Expand Down
5 changes: 3 additions & 2 deletions tests/api/api-openapi-spec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,9 +232,10 @@ describe("OpenAPI 规范验证", () => {
expect(publicStatusOperation?.responses?.["200"]).toBeDefined();
expect(publicStatusOperation?.responses?.["400"]).toBeDefined();
expect(publicStatusOperation?.responses?.["503"]).toBeDefined();
expect(publicStatusOperation?.parameters).toBeDefined();
const publicStatusParameters = publicStatusOperation?.parameters;
expect(publicStatusParameters).toBeDefined();

const parameterNames = (publicStatusOperation?.parameters as Array<{ name?: string }>).map(
const parameterNames = (publicStatusParameters as Array<{ name?: string }>).map(
(parameter) => parameter.name
);
expect(parameterNames).toEqual(
Expand Down
Loading
Loading