fix(xiaomi): use token-plan base URL for tp- API keys

Root cause: getProviderBaseUrl('xiaomi') returned the standard
endpoint baseUrl from the bundled catalog (api.xiaomimimo.com/anthropic),
overriding the token-plan URL (token-plan-ams.xiaomimimo.com/anthropic)
that should be used for tp- prefixed keys.

Fix:
- xiaomiModelManagerOptions: for tp- keys, compute baseUrl from the
  key prefix alone, ignoring config?.baseUrl from the bundled catalog.
- Add usesApiKeyAuth flag to AnthropicCompat / AnthropicHeaderOptions.
- Xiaomi models set usesApiKeyAuth: true so the anthropic provider sends
  X-Api-Key header (matching what the Xiaomi validation code expects)
  instead of Authorization: Bearer.
This commit is contained in:
Hanbin Lee
2026-05-25 00:47:18 +09:00
parent 3b072a10b9
commit 1a2dffd021
3 changed files with 13 additions and 5 deletions
@@ -1410,13 +1410,15 @@ export function xiaomiModelManagerOptions(
config?: XiaomiModelManagerConfig,
): ModelManagerOptions<"anthropic-messages"> {
const apiKey = config?.apiKey;
// Xiaomi splits API keys across two backends: standard `sk-` keys hit
// api.xiaomimimo.com; "token plan" `tp-` keys hit the EU token-plan host.
// Both expose the same Anthropic-compat layout under /anthropic/v1/*.
const defaultBaseUrl = apiKey?.startsWith("tp-")
? "https://token-plan-ams.xiaomimimo.com/anthropic"
: "https://api.xiaomimimo.com/anthropic";
const baseUrl = normalizeAnthropicBaseUrl(config?.baseUrl, defaultBaseUrl);
// Token-plan keys always use the TP baseUrl; config?.baseUrl (from catalog)
// would incorrectly pin to the standard endpoint (api.xiaomimimo.com).
const baseUrl = normalizeAnthropicBaseUrl(
apiKey?.startsWith("tp-") ? undefined : config?.baseUrl,
defaultBaseUrl,
);
// Xiaomi hosts chat completions under /anthropic/* but exposes model
// discovery at the OpenAI-style /v1/models endpoint on the root host.
const discoveryRoot = baseUrl.endsWith("/anthropic") ? baseUrl.slice(0, -"/anthropic".length) : baseUrl;
@@ -1439,6 +1441,7 @@ export function xiaomiModelManagerOptions(
...model,
name: toModelName(entry.display_name, model.name),
baseUrl,
compat: { ...model.compat, usesApiKeyAuth: true },
};
},
}),
+4 -1
View File
@@ -80,6 +80,8 @@ export type AnthropicHeaderOptions = {
stream?: boolean;
modelHeaders?: Record<string, string>;
isCloudflareAiGateway?: boolean;
/** Use X-Api-Key header instead of Authorization: Bearer. */
usesApiKeyAuth?: boolean;
};
export function normalizeAnthropicBaseUrl(baseUrl?: string): string | undefined {
@@ -182,7 +184,7 @@ export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record<s
"Anthropic-Beta": betaHeader,
"User-Agent": userAgent,
};
} else if (!isAnthropicApiBaseUrl(options.baseUrl)) {
} else if (!options.usesApiKeyAuth && !isAnthropicApiBaseUrl(options.baseUrl)) {
return {
...modelHeaders,
Accept: acceptHeader,
@@ -1576,6 +1578,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
stream,
modelHeaders: mergeHeaders(model.headers, foundryCustomHeaders, headers, dynamicHeaders),
isCloudflareAiGateway: model.provider === "cloudflare-ai-gateway",
usesApiKeyAuth: compat.usesApiKeyAuth,
});
if (model.provider === "cloudflare-ai-gateway") {
+2
View File
@@ -791,6 +791,8 @@ export interface AnthropicCompat {
supportsEagerToolInputStreaming?: boolean;
/** Whether long prompt-cache retention (`ttl: "1h"`) is supported. Default: true for canonical Anthropic API. */
supportsLongCacheRetention?: boolean;
/** Use X-Api-Key header instead of Authorization: Bearer for non-Anthropic endpoints. */
usesApiKeyAuth?: boolean;
}
/**