The pi-ai Anthropic provider constructs the full API endpoint as
`${baseUrl}/v1/messages`. If a user configures
`models.providers.anthropic.baseUrl` with a trailing `/v1`
(e.g. "https://api.anthropic.com/v1"), the resolved URL becomes
"https://api.anthropic.com/v1/v1/messages" which the Anthropic API
rejects with a 404 / connection failure.
This regression appeared in v2026.2.22 when @mariozechner/pi-ai bumped
from 0.54.0 to 0.54.1, which started appending the /v1 segment where
the previous version did not.
Fix: in normalizeModelCompat(), detect anthropic-messages models and
strip a single trailing /v1 (with optional trailing slash) from the
configured baseUrl before it is handed to pi-ai. Models with baseUrls
that do not end in /v1 are unaffected. Non-anthropic-messages models
are not touched.
Adds 6 unit tests covering the normalisation scenarios.
Fixes #24709
(cherry picked from commit 4c4857fdcb3506dc277f9df75d4df5879dca8d41)
64 lines
2.3 KiB
TypeScript
64 lines
2.3 KiB
TypeScript
import type { Api, Model } from "@mariozechner/pi-ai";
|
|
|
|
function isOpenAiCompletionsModel(model: Model<Api>): model is Model<"openai-completions"> {
|
|
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")
|
|
);
|
|
}
|
|
|
|
function isAnthropicMessagesModel(model: Model<Api>): model is Model<"anthropic-messages"> {
|
|
return model.api === "anthropic-messages";
|
|
}
|
|
|
|
/**
|
|
* pi-ai constructs the Anthropic API endpoint as `${baseUrl}/v1/messages`.
|
|
* If a user configures `baseUrl` with a trailing `/v1` (e.g. the previously
|
|
* recommended format "https://api.anthropic.com/v1"), the resulting URL
|
|
* becomes "…/v1/v1/messages" which the Anthropic API rejects with a 404.
|
|
*
|
|
* Strip a single trailing `/v1` (with optional trailing slash) from the
|
|
* baseUrl for anthropic-messages models so users with either format work.
|
|
*/
|
|
function normalizeAnthropicBaseUrl(baseUrl: string): string {
|
|
return baseUrl.replace(/\/v1\/?$/, "");
|
|
}
|
|
export function normalizeModelCompat(model: Model<Api>): Model<Api> {
|
|
const baseUrl = model.baseUrl ?? "";
|
|
|
|
// Normalise anthropic-messages baseUrl: strip trailing /v1 that users may
|
|
// have included in their config. pi-ai appends /v1/messages itself.
|
|
if (isAnthropicMessagesModel(model) && baseUrl) {
|
|
const normalised = normalizeAnthropicBaseUrl(baseUrl);
|
|
if (normalised !== baseUrl) {
|
|
return { ...model, baseUrl: normalised } as Model<"anthropic-messages">;
|
|
}
|
|
}
|
|
|
|
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)) {
|
|
return model;
|
|
}
|
|
|
|
const openaiModel = model;
|
|
const compat = openaiModel.compat ?? undefined;
|
|
if (compat?.supportsDeveloperRole === false) {
|
|
return model;
|
|
}
|
|
|
|
openaiModel.compat = compat
|
|
? { ...compat, supportsDeveloperRole: false }
|
|
: { supportsDeveloperRole: false };
|
|
return openaiModel;
|
|
}
|