fix: force supportsDeveloperRole=false for non-native OpenAI endpoints (#29479)

Merged via squash.

Prepared head SHA: 1416c584ac
Co-authored-by: akramcodez <179671552+akramcodez@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
Sk Akram
2026-03-03 07:13:49 +05:30
committed by GitHub
parent ebbb572639
commit bd8c3230e8
5 changed files with 130 additions and 39 deletions

View File

@@ -19,6 +19,10 @@ const baseModel = (): Model<Api> =>
maxTokens: 1024,
}) as Model<Api>;
function supportsDeveloperRole(model: Model<Api>): boolean | undefined {
return (model.compat as { supportsDeveloperRole?: boolean } | undefined)?.supportsDeveloperRole;
}
function createTemplateModel(provider: string, id: string): Model<Api> {
return {
id,
@@ -105,9 +109,7 @@ describe("normalizeModelCompat", () => {
const model = baseModel();
delete (model as { compat?: unknown }).compat;
const normalized = normalizeModelCompat(model);
expect(
(normalized.compat as { supportsDeveloperRole?: boolean } | undefined)?.supportsDeveloperRole,
).toBe(false);
expect(supportsDeveloperRole(normalized)).toBe(false);
});
it("forces supportsDeveloperRole off for moonshot models", () => {
@@ -118,9 +120,7 @@ describe("normalizeModelCompat", () => {
};
delete (model as { compat?: unknown }).compat;
const normalized = normalizeModelCompat(model);
expect(
(normalized.compat as { supportsDeveloperRole?: boolean } | undefined)?.supportsDeveloperRole,
).toBe(false);
expect(supportsDeveloperRole(normalized)).toBe(false);
});
it("forces supportsDeveloperRole off for custom moonshot-compatible endpoints", () => {
@@ -131,9 +131,7 @@ describe("normalizeModelCompat", () => {
};
delete (model as { compat?: unknown }).compat;
const normalized = normalizeModelCompat(model);
expect(
(normalized.compat as { supportsDeveloperRole?: boolean } | undefined)?.supportsDeveloperRole,
).toBe(false);
expect(supportsDeveloperRole(normalized)).toBe(false);
});
it("forces supportsDeveloperRole off for DashScope provider ids", () => {
@@ -144,9 +142,7 @@ describe("normalizeModelCompat", () => {
};
delete (model as { compat?: unknown }).compat;
const normalized = normalizeModelCompat(model);
expect(
(normalized.compat as { supportsDeveloperRole?: boolean } | undefined)?.supportsDeveloperRole,
).toBe(false);
expect(supportsDeveloperRole(normalized)).toBe(false);
});
it("forces supportsDeveloperRole off for DashScope-compatible endpoints", () => {
@@ -157,12 +153,10 @@ describe("normalizeModelCompat", () => {
};
delete (model as { compat?: unknown }).compat;
const normalized = normalizeModelCompat(model);
expect(
(normalized.compat as { supportsDeveloperRole?: boolean } | undefined)?.supportsDeveloperRole,
).toBe(false);
expect(supportsDeveloperRole(normalized)).toBe(false);
});
it("leaves non-zai models untouched", () => {
it("leaves native api.openai.com model untouched", () => {
const model = {
...baseModel(),
provider: "openai",
@@ -173,13 +167,89 @@ describe("normalizeModelCompat", () => {
expect(normalized.compat).toBeUndefined();
});
it("does not override explicit z.ai compat false", () => {
it("forces supportsDeveloperRole off for Azure OpenAI (Chat Completions, not Responses API)", () => {
const model = {
...baseModel(),
provider: "azure-openai",
baseUrl: "https://my-deployment.openai.azure.com/openai",
};
delete (model as { compat?: unknown }).compat;
const normalized = normalizeModelCompat(model);
expect(supportsDeveloperRole(normalized)).toBe(false);
});
it("forces supportsDeveloperRole off for generic custom openai-completions provider", () => {
const model = {
...baseModel(),
provider: "custom-cpa",
baseUrl: "https://cpa.example.com/v1",
};
delete (model as { compat?: unknown }).compat;
const normalized = normalizeModelCompat(model);
expect(supportsDeveloperRole(normalized)).toBe(false);
});
it("forces supportsDeveloperRole off for Qwen proxy via openai-completions", () => {
const model = {
...baseModel(),
provider: "qwen-proxy",
baseUrl: "https://qwen-api.example.org/compatible-mode/v1",
};
delete (model as { compat?: unknown }).compat;
const normalized = normalizeModelCompat(model);
expect(supportsDeveloperRole(normalized)).toBe(false);
});
it("leaves openai-completions model with empty baseUrl untouched", () => {
const model = {
...baseModel(),
provider: "openai",
};
delete (model as { baseUrl?: unknown }).baseUrl;
delete (model as { compat?: unknown }).compat;
const normalized = normalizeModelCompat(model as Model<Api>);
expect(normalized.compat).toBeUndefined();
});
it("forces supportsDeveloperRole off for malformed baseUrl values", () => {
const model = {
...baseModel(),
provider: "custom-cpa",
baseUrl: "://api.openai.com malformed",
};
delete (model as { compat?: unknown }).compat;
const normalized = normalizeModelCompat(model);
expect(supportsDeveloperRole(normalized)).toBe(false);
});
it("overrides explicit supportsDeveloperRole true on non-native endpoints", () => {
const model = {
...baseModel(),
provider: "custom-cpa",
baseUrl: "https://proxy.example.com/v1",
compat: { supportsDeveloperRole: true },
};
const normalized = normalizeModelCompat(model);
expect(supportsDeveloperRole(normalized)).toBe(false);
});
it("does not mutate caller model when forcing supportsDeveloperRole off", () => {
const model = {
...baseModel(),
provider: "custom-cpa",
baseUrl: "https://proxy.example.com/v1",
};
delete (model as { compat?: unknown }).compat;
const normalized = normalizeModelCompat(model);
expect(normalized).not.toBe(model);
expect(supportsDeveloperRole(model)).toBeUndefined();
expect(supportsDeveloperRole(normalized)).toBe(false);
});
it("does not override explicit compat false", () => {
const model = baseModel();
model.compat = { supportsDeveloperRole: false };
const normalized = normalizeModelCompat(model);
expect(
(normalized.compat as { supportsDeveloperRole?: boolean } | undefined)?.supportsDeveloperRole,
).toBe(false);
expect(supportsDeveloperRole(normalized)).toBe(false);
});
});

View File

@@ -4,12 +4,20 @@ function isOpenAiCompletionsModel(model: Model<Api>): model is Model<"openai-com
return model.api === "openai-completions";
}
function isDashScopeCompatibleEndpoint(baseUrl: string): boolean {
return (
baseUrl.includes("dashscope.aliyuncs.com") ||
baseUrl.includes("dashscope-intl.aliyuncs.com") ||
baseUrl.includes("dashscope-us.aliyuncs.com")
);
/**
* Returns true only for endpoints that are confirmed to be native OpenAI
* infrastructure and therefore accept the `developer` message role.
* Azure OpenAI uses the Chat Completions API and does NOT accept `developer`.
* All other openai-completions backends (proxies, Qwen, GLM, DeepSeek, etc.)
* only support the standard `system` role.
*/
function isOpenAINativeEndpoint(baseUrl: string): boolean {
try {
const host = new URL(baseUrl).hostname.toLowerCase();
return host === "api.openai.com";
} catch {
return false;
}
}
function isAnthropicMessagesModel(model: Model<Api>): model is Model<"anthropic-messages"> {
@@ -40,24 +48,32 @@ export function normalizeModelCompat(model: Model<Api>): Model<Api> {
}
}
const isZai = model.provider === "zai" || baseUrl.includes("api.z.ai");
const isMoonshot =
model.provider === "moonshot" ||
baseUrl.includes("moonshot.ai") ||
baseUrl.includes("moonshot.cn");
const isDashScope = model.provider === "dashscope" || isDashScopeCompatibleEndpoint(baseUrl);
if ((!isZai && !isMoonshot && !isDashScope) || !isOpenAiCompletionsModel(model)) {
if (!isOpenAiCompletionsModel(model)) {
return model;
}
const openaiModel = model;
const compat = openaiModel.compat ?? undefined;
// The `developer` message role is an OpenAI-native convention. All other
// openai-completions backends (proxies, Qwen, GLM, DeepSeek, Kimi, etc.)
// only recognise `system`. Force supportsDeveloperRole=false for any model
// whose baseUrl is not a known native OpenAI endpoint, unless the caller
// has already pinned the value explicitly.
const compat = model.compat ?? undefined;
if (compat?.supportsDeveloperRole === false) {
return model;
}
// When baseUrl is empty the pi-ai library defaults to api.openai.com, so
// leave compat unchanged and let the existing default behaviour apply.
// Note: an explicit supportsDeveloperRole: true is intentionally overridden
// here for non-native endpoints — those backends would return a 400 if we
// sent `developer`, so safety takes precedence over the caller's hint.
const needsForce = baseUrl ? !isOpenAINativeEndpoint(baseUrl) : false;
if (!needsForce) {
return model;
}
openaiModel.compat = compat
? { ...compat, supportsDeveloperRole: false }
: { supportsDeveloperRole: false };
return openaiModel;
// Return a new object — do not mutate the caller's model reference.
return {
...model,
compat: compat ? { ...compat, supportsDeveloperRole: false } : { supportsDeveloperRole: false },
} as typeof model;
}