refactor(providers): centralize request capabilities (#59636)

* refactor(providers): centralize request capabilities

* fix(providers): harden comparable base url parsing
This commit is contained in:
Vincent Koc
2026-04-02 20:26:22 +09:00
committed by GitHub
parent 38d2faee20
commit c405bcfa98
12 changed files with 359 additions and 132 deletions

View File

@@ -1,6 +1,6 @@
import type { StreamFn } from "@mariozechner/pi-agent-core";
import { streamSimple } from "@mariozechner/pi-ai";
import { resolveProviderEndpoint } from "openclaw/plugin-sdk/provider-http";
import { resolveProviderRequestCapabilities } from "openclaw/plugin-sdk/provider-http";
import { streamWithPayloadPatch } from "openclaw/plugin-sdk/provider-stream";
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
@@ -52,14 +52,18 @@ function isAnthropicOAuthApiKey(apiKey: unknown): boolean {
return typeof apiKey === "string" && apiKey.includes("sk-ant-oat");
}
function isAnthropicPublicApiBaseUrl(baseUrl: unknown): boolean {
if (baseUrl == null) {
return true;
}
if (typeof baseUrl !== "string" || !baseUrl.trim()) {
return true;
}
return resolveProviderEndpoint(baseUrl).endpointClass === "anthropic-public";
function allowsAnthropicServiceTier(model: {
api?: unknown;
provider?: unknown;
baseUrl?: unknown;
}): boolean {
return resolveProviderRequestCapabilities({
provider: typeof model.provider === "string" ? model.provider : undefined,
api: typeof model.api === "string" ? model.api : undefined,
baseUrl: typeof model.baseUrl === "string" ? model.baseUrl : undefined,
capability: "llm",
transport: "stream",
}).allowsAnthropicServiceTier;
}
function resolveAnthropicFastServiceTier(enabled: boolean): AnthropicServiceTier {
@@ -157,11 +161,7 @@ export function createAnthropicFastModeWrapper(
const underlying = baseStreamFn ?? streamSimple;
const serviceTier = resolveAnthropicFastServiceTier(enabled);
return (model, context, options) => {
if (
model.api !== "anthropic-messages" ||
model.provider !== "anthropic" ||
!isAnthropicPublicApiBaseUrl(model.baseUrl)
) {
if (!allowsAnthropicServiceTier(model)) {
return underlying(model, context, options);
}
@@ -179,11 +179,7 @@ export function createAnthropicServiceTierWrapper(
): StreamFn {
const underlying = baseStreamFn ?? streamSimple;
return (model, context, options) => {
if (
model.api !== "anthropic-messages" ||
model.provider !== "anthropic" ||
!isAnthropicPublicApiBaseUrl(model.baseUrl)
) {
if (!allowsAnthropicServiceTier(model)) {
return underlying(model, context, options);
}

View File

@@ -1,3 +1,4 @@
import { resolveProviderRequestCapabilities } from "openclaw/plugin-sdk/provider-http";
import type {
ModelDefinitionConfig,
ModelProviderConfig,
@@ -94,29 +95,14 @@ export const MODELSTUDIO_MODEL_CATALOG: ReadonlyArray<ModelDefinitionConfig> = [
},
];
function normalizeModelStudioBaseUrl(baseUrl: string | undefined): string {
const trimmed = baseUrl?.trim();
if (!trimmed) {
return "";
}
try {
const url = new URL(trimmed);
url.hash = "";
url.search = "";
return url.toString().replace(/\/+$/, "").toLowerCase();
} catch {
return trimmed.replace(/\/+$/, "").toLowerCase();
}
}
export function isNativeModelStudioBaseUrl(baseUrl: string | undefined): boolean {
const normalized = normalizeModelStudioBaseUrl(baseUrl);
return (
normalized === MODELSTUDIO_BASE_URL ||
normalized === MODELSTUDIO_CN_BASE_URL ||
normalized === MODELSTUDIO_STANDARD_CN_BASE_URL ||
normalized === MODELSTUDIO_STANDARD_GLOBAL_BASE_URL
);
return resolveProviderRequestCapabilities({
provider: "modelstudio",
api: "openai-completions",
baseUrl,
capability: "llm",
transport: "stream",
}).supportsNativeStreamingUsageCompat;
}
function withStreamingUsageCompat(provider: ModelProviderConfig): ModelProviderConfig {

View File

@@ -1,3 +1,4 @@
import { resolveProviderRequestCapabilities } from "openclaw/plugin-sdk/provider-http";
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared";
export const MOONSHOT_BASE_URL = "https://api.moonshot.ai/v1";
@@ -51,24 +52,14 @@ const MOONSHOT_MODEL_CATALOG = [
},
] as const;
function normalizeMoonshotBaseUrl(baseUrl: string | undefined): string {
const trimmed = baseUrl?.trim();
if (!trimmed) {
return "";
}
try {
const url = new URL(trimmed);
url.hash = "";
url.search = "";
return url.toString().replace(/\/+$/, "").toLowerCase();
} catch {
return trimmed.replace(/\/+$/, "").toLowerCase();
}
}
export function isNativeMoonshotBaseUrl(baseUrl: string | undefined): boolean {
const normalized = normalizeMoonshotBaseUrl(baseUrl);
return normalized === MOONSHOT_BASE_URL || normalized === MOONSHOT_CN_BASE_URL;
return resolveProviderRequestCapabilities({
provider: "moonshot",
api: "openai-completions",
baseUrl,
capability: "llm",
transport: "stream",
}).supportsNativeStreamingUsageCompat;
}
function withStreamingUsageCompat(provider: ModelProviderConfig): ModelProviderConfig {

View File

@@ -1,5 +0,0 @@
const MOONSHOT_THINKING_PAYLOAD_COMPAT_PROVIDERS = new Set(["moonshot", "kimi"]);
export function usesMoonshotThinkingPayloadCompatStatic(provider?: string | null): boolean {
return provider != null && MOONSHOT_THINKING_PAYLOAD_COMPAT_PROVIDERS.has(provider);
}

View File

@@ -1267,6 +1267,42 @@ describe("createOpenAIWebSocketStreamFn", () => {
expect(sent).not.toHaveProperty("store");
});
it("keeps store=false for proxied openai-responses routes when store is still supported", async () => {
releaseWsSession("sess-store-proxy");
const proxiedModel = {
...modelStub,
baseUrl: "https://proxy.example.com/v1",
};
const streamFn = createOpenAIWebSocketStreamFn("sk-test", "sess-store-proxy");
const stream = streamFn(
proxiedModel as Parameters<typeof streamFn>[0],
contextStub as Parameters<typeof streamFn>[1],
);
const completed = new Promise<void>((res, rej) => {
queueMicrotask(async () => {
try {
await new Promise((r) => setImmediate(r));
const manager = MockManager.lastInstance!;
manager.simulateEvent({
type: "response.completed",
response: makeResponseObject("resp_store_proxy", "ok"),
});
for await (const _ of await resolveStream(stream)) {
// consume
}
res();
} catch (e) {
rej(e);
}
});
});
await completed;
const sent = MockManager.lastInstance!.sentEvents[0] as Record<string, unknown>;
expect(sent.store).toBe(false);
});
it("emits an AssistantMessage on response.completed", async () => {
const streamFn = createOpenAIWebSocketStreamFn("sk-test", "sess-2");
const stream = streamFn(

View File

@@ -22,13 +22,13 @@
*/
import type { StreamFn } from "@mariozechner/pi-agent-core";
import * as piAi from "@mariozechner/pi-ai";
import type {
AssistantMessage,
AssistantMessageEvent,
AssistantMessageEventStream,
StopReason,
} from "@mariozechner/pi-ai";
import * as piAi from "@mariozechner/pi-ai";
import {
OpenAIWebSocketManager,
type FunctionToolDefinition,
@@ -42,6 +42,7 @@ import {
} from "./openai-ws-message-conversion.js";
import { log } from "./pi-embedded-runner/logger.js";
import { resolveOpenAITextVerbosity } from "./pi-embedded-runner/openai-stream-wrappers.js";
import { resolveProviderRequestCapabilities } from "./provider-attribution.js";
import {
buildAssistantMessageWithZeroUsage,
buildStreamErrorAssistantMessage,
@@ -485,13 +486,19 @@ export function createOpenAIWebSocketStreamFn(
// Respect compat.supportsStore — providers like Gemini reject unknown
// fields such as `store` with a 400 error. Fixes #39086.
const supportsStore = (model as { compat?: { supportsStore?: boolean } }).compat
?.supportsStore;
const supportsResponsesStoreField = resolveProviderRequestCapabilities({
provider: typeof model.provider === "string" ? model.provider : undefined,
api: typeof model.api === "string" ? model.api : undefined,
baseUrl: typeof model.baseUrl === "string" ? model.baseUrl : undefined,
compat: (model as { compat?: { supportsStore?: boolean } }).compat,
capability: "llm",
transport: "websocket",
}).supportsResponsesStoreField;
const payload: Record<string, unknown> = {
type: "response.create",
model: model.id,
...(supportsStore !== false ? { store: false } : {}),
...(supportsResponsesStoreField ? { store: false } : {}),
input: turnInput.inputItems,
instructions: context.systemPrompt ?? undefined,
tools: tools.length > 0 ? tools : undefined,

View File

@@ -1,7 +1,7 @@
import type { StreamFn } from "@mariozechner/pi-agent-core";
import { streamSimple } from "@mariozechner/pi-ai";
import type { ThinkLevel } from "../../auto-reply/thinking.js";
import { usesMoonshotThinkingPayloadCompatStatic } from "../moonshot-provider-compat.js";
import { resolveProviderRequestCapabilities } from "../provider-attribution.js";
import { normalizeProviderId } from "../provider-id.js";
import { streamWithPayloadPatch } from "./stream-payload-utils.js";
@@ -27,16 +27,13 @@ export function shouldApplyMoonshotPayloadCompat(params: {
modelId: string;
}): boolean {
const normalizedProvider = normalizeProviderId(params.provider);
const normalizedModelId = params.modelId.trim().toLowerCase();
if (usesMoonshotThinkingPayloadCompatStatic(normalizedProvider)) {
return true;
}
return (
normalizedProvider === "ollama" &&
normalizedModelId.startsWith("kimi-k") &&
normalizedModelId.includes(":cloud")
resolveProviderRequestCapabilities({
provider: normalizedProvider,
modelId: params.modelId,
capability: "llm",
transport: "stream",
}).compatibilityFamily === "moonshot"
);
}

View File

@@ -6,7 +6,7 @@ import {
patchCodexNativeWebSearchPayload,
resolveCodexNativeSearchActivation,
} from "../codex-native-web-search.js";
import { resolveProviderRequestPolicy } from "../provider-attribution.js";
import { resolveProviderRequestCapabilities } from "../provider-attribution.js";
import { resolveProviderRequestHeaders } from "../provider-request-config.js";
import { log } from "./logger.js";
import { streamWithPayloadPatch } from "./stream-payload-utils.js";
@@ -15,7 +15,6 @@ type OpenAIServiceTier = "auto" | "default" | "flex" | "priority";
type OpenAITextVerbosity = "low" | "medium" | "high";
const OPENAI_RESPONSES_APIS = new Set(["openai-responses", "azure-openai-responses"]);
const OPENAI_RESPONSES_PROVIDERS = new Set(["openai", "azure-openai", "azure-openai-responses"]);
const OPENAI_REASONING_COMPAT_PROVIDERS = new Set([
"openai",
"openai-codex",
@@ -23,15 +22,17 @@ const OPENAI_REASONING_COMPAT_PROVIDERS = new Set([
"azure-openai-responses",
]);
function resolveOpenAIRequestPolicy(model: {
function resolveOpenAIRequestCapabilities(model: {
api?: unknown;
provider?: unknown;
baseUrl?: unknown;
compat?: { supportsStore?: boolean };
}) {
return resolveProviderRequestPolicy({
return resolveProviderRequestCapabilities({
provider: typeof model.provider === "string" ? model.provider : undefined,
api: typeof model.api === "string" ? model.api : undefined,
baseUrl: typeof model.baseUrl === "string" ? model.baseUrl : undefined,
compat: model.compat,
capability: "llm",
transport: "stream",
});
@@ -42,7 +43,7 @@ function shouldApplyOpenAIAttributionHeaders(model: {
provider?: unknown;
baseUrl?: unknown;
}): "openai" | "openai-codex" | undefined {
const attributionProvider = resolveOpenAIRequestPolicy(model).attributionProvider;
const attributionProvider = resolveOpenAIRequestCapabilities(model).attributionProvider;
return attributionProvider === "openai" || attributionProvider === "openai-codex"
? attributionProvider
: undefined;
@@ -53,22 +54,7 @@ function shouldApplyOpenAIServiceTier(model: {
provider?: unknown;
baseUrl?: unknown;
}): boolean {
const policy = resolveOpenAIRequestPolicy(model);
if (
model.provider === "openai" &&
model.api === "openai-responses" &&
policy.endpointClass === "openai-public"
) {
return true;
}
if (
model.provider === "openai-codex" &&
(model.api === "openai-codex-responses" || model.api === "openai-responses") &&
policy.endpointClass === "openai-codex"
) {
return true;
}
return false;
return resolveOpenAIRequestCapabilities(model).allowsOpenAIServiceTier;
}
function shouldForceResponsesStore(model: {
@@ -77,19 +63,7 @@ function shouldForceResponsesStore(model: {
baseUrl?: unknown;
compat?: { supportsStore?: boolean };
}): boolean {
if (model.compat?.supportsStore === false) {
return false;
}
if (typeof model.api !== "string" || typeof model.provider !== "string") {
return false;
}
if (!OPENAI_RESPONSES_APIS.has(model.api)) {
return false;
}
if (!OPENAI_RESPONSES_PROVIDERS.has(model.provider)) {
return false;
}
return resolveOpenAIRequestPolicy(model).usesKnownNativeOpenAIEndpoint;
return resolveOpenAIRequestCapabilities(model).allowsResponsesStore;
}
function parsePositiveInteger(value: unknown): number | undefined {
@@ -149,17 +123,7 @@ function shouldStripResponsesStore(
}
function shouldStripResponsesPromptCache(model: { api?: unknown; baseUrl?: unknown }): boolean {
if (typeof model.api !== "string" || !OPENAI_RESPONSES_APIS.has(model.api)) {
return false;
}
// Missing baseUrl means pi-ai will use the default OpenAI endpoint, so keep
// prompt cache fields for that direct path.
return resolveProviderRequestPolicy({
baseUrl: typeof model.baseUrl === "string" ? model.baseUrl : undefined,
api: typeof model.api === "string" ? model.api : undefined,
transport: "stream",
capability: "llm",
}).usesExplicitProxyLikeEndpoint;
return resolveOpenAIRequestCapabilities(model).shouldStripResponsesPromptCache;
}
function shouldApplyOpenAIReasoningCompatibility(model: {

View File

@@ -6,6 +6,7 @@ import {
resolveProviderAttributionPolicy,
resolveProviderEndpoint,
resolveProviderRequestAttributionHeaders,
resolveProviderRequestCapabilities,
resolveProviderRequestPolicy,
} from "./provider-attribution.js";
@@ -315,6 +316,30 @@ describe("provider attribution", () => {
});
});
it("classifies native Moonshot and ModelStudio endpoints separately from custom hosts", () => {
expect(resolveProviderEndpoint("https://api.moonshot.ai/v1")).toMatchObject({
endpointClass: "moonshot-native",
hostname: "api.moonshot.ai",
});
expect(resolveProviderEndpoint("https://api.moonshot.cn/v1")).toMatchObject({
endpointClass: "moonshot-native",
hostname: "api.moonshot.cn",
});
expect(
resolveProviderEndpoint("https://dashscope-intl.aliyuncs.com/compatible-mode/v1"),
).toMatchObject({
endpointClass: "modelstudio-native",
hostname: "dashscope-intl.aliyuncs.com",
});
expect(resolveProviderEndpoint("https://proxy.example.com/v1")).toMatchObject({
endpointClass: "custom",
hostname: "proxy.example.com",
});
});
it("does not classify malformed or embedded Google host strings as native endpoints", () => {
expect(resolveProviderEndpoint("proxy/generativelanguage.googleapis.com")).toMatchObject({
endpointClass: "custom",
@@ -359,6 +384,12 @@ describe("provider attribution", () => {
});
});
it("ignores non-http schemes when normalizing native comparable base URLs", () => {
expect(resolveProviderEndpoint("javascript:alert(1)")).toMatchObject({
endpointClass: "invalid",
});
});
it("requires the dedicated OpenAI audio transcription API for audio attribution", () => {
expect(
resolveProviderRequestPolicy({
@@ -399,4 +430,90 @@ describe("provider attribution", () => {
allowsHiddenAttribution: false,
});
});
it("resolves centralized request capabilities for native and proxied routes", () => {
expect(
resolveProviderRequestCapabilities({
provider: "openai",
api: "openai-responses",
baseUrl: "https://api.openai.com/v1",
capability: "llm",
transport: "stream",
}),
).toMatchObject({
endpointClass: "openai-public",
allowsOpenAIServiceTier: true,
allowsResponsesStore: true,
supportsResponsesStoreField: true,
shouldStripResponsesPromptCache: false,
});
expect(
resolveProviderRequestCapabilities({
provider: "anthropic",
api: "anthropic-messages",
capability: "llm",
transport: "stream",
}),
).toMatchObject({
endpointClass: "default",
allowsAnthropicServiceTier: true,
});
expect(
resolveProviderRequestCapabilities({
provider: "custom-proxy",
api: "openai-responses",
baseUrl: "https://proxy.example.com/v1",
capability: "llm",
transport: "stream",
}),
).toMatchObject({
endpointClass: "custom",
allowsOpenAIServiceTier: false,
allowsResponsesStore: false,
supportsResponsesStoreField: true,
shouldStripResponsesPromptCache: true,
});
});
it("resolves shared compat families and native streaming-usage gates", () => {
expect(
resolveProviderRequestCapabilities({
provider: "moonshot",
api: "openai-completions",
baseUrl: "https://api.moonshot.ai/v1",
capability: "llm",
transport: "stream",
}),
).toMatchObject({
endpointClass: "moonshot-native",
supportsNativeStreamingUsageCompat: true,
compatibilityFamily: "moonshot",
});
expect(
resolveProviderRequestCapabilities({
provider: "modelstudio",
api: "openai-completions",
baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
capability: "llm",
transport: "stream",
}),
).toMatchObject({
endpointClass: "modelstudio-native",
supportsNativeStreamingUsageCompat: true,
});
expect(
resolveProviderRequestCapabilities({
provider: "ollama",
modelId: "kimi-k2.5:cloud",
capability: "llm",
transport: "stream",
}),
).toMatchObject({
compatibilityFamily: "moonshot",
});
});
});

View File

@@ -34,6 +34,8 @@ export type ProviderRequestCapability = "llm" | "audio" | "image" | "video" | "o
export type ProviderEndpointClass =
| "default"
| "anthropic-public"
| "moonshot-native"
| "modelstudio-native"
| "openai-public"
| "openai-codex"
| "azure-openai"
@@ -73,10 +75,43 @@ export type ProviderRequestPolicyResolution = {
usesExplicitProxyLikeEndpoint: boolean;
};
export type ProviderRequestCapabilitiesInput = ProviderRequestPolicyInput & {
modelId?: string | null;
compat?: {
supportsStore?: boolean;
} | null;
};
export type ProviderRequestCompatibilityFamily = "moonshot";
export type ProviderRequestCapabilities = ProviderRequestPolicyResolution & {
isKnownNativeEndpoint: boolean;
allowsOpenAIServiceTier: boolean;
allowsAnthropicServiceTier: boolean;
supportsResponsesStoreField: boolean;
allowsResponsesStore: boolean;
shouldStripResponsesPromptCache: boolean;
supportsNativeStreamingUsageCompat: boolean;
compatibilityFamily?: ProviderRequestCompatibilityFamily;
};
const OPENCLAW_ATTRIBUTION_PRODUCT = "OpenClaw";
const OPENCLAW_ATTRIBUTION_ORIGINATOR = "openclaw";
const LOCAL_ENDPOINT_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
const MOONSHOT_NATIVE_BASE_URLS = new Set([
"https://api.moonshot.ai/v1",
"https://api.moonshot.cn/v1",
]);
const MODELSTUDIO_NATIVE_BASE_URLS = new Set([
"https://coding-intl.dashscope.aliyuncs.com/v1",
"https://coding.dashscope.aliyuncs.com/v1",
"https://dashscope.aliyuncs.com/compatible-mode/v1",
"https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
]);
const OPENAI_RESPONSES_APIS = new Set(["openai-responses", "azure-openai-responses"]);
const OPENAI_RESPONSES_PROVIDERS = new Set(["openai", "azure-openai", "azure-openai-responses"]);
const MOONSHOT_COMPAT_PROVIDERS = new Set(["moonshot", "kimi"]);
function formatOpenClawUserAgent(version: string): string {
return `${OPENCLAW_ATTRIBUTION_ORIGINATOR}/${version}`;
@@ -110,6 +145,29 @@ function resolveUrlHostname(value: unknown): string | undefined {
return tryParseHostname(`https://${trimmed}`);
}
function normalizeComparableBaseUrl(value: string): string | undefined {
const trimmed = value.trim();
if (!trimmed) {
return undefined;
}
const parsedValue =
tryParseHostname(trimmed) || !isSchemelessHostnameCandidate(trimmed)
? trimmed
: `https://${trimmed}`;
try {
const url = new URL(parsedValue);
if (url.protocol !== "http:" && url.protocol !== "https:") {
return undefined;
}
url.hash = "";
url.search = "";
return url.toString().replace(/\/+$/, "").toLowerCase();
} catch {
return undefined;
}
}
function isLocalEndpointHost(host: string): boolean {
return (
LOCAL_ENDPOINT_HOSTS.has(host) ||
@@ -130,6 +188,13 @@ export function resolveProviderEndpoint(
if (!host) {
return { endpointClass: "invalid" };
}
const normalizedBaseUrl = normalizeComparableBaseUrl(baseUrl);
if (normalizedBaseUrl && MOONSHOT_NATIVE_BASE_URLS.has(normalizedBaseUrl)) {
return { endpointClass: "moonshot-native", hostname: host };
}
if (normalizedBaseUrl && MODELSTUDIO_NATIVE_BASE_URLS.has(normalizedBaseUrl)) {
return { endpointClass: "modelstudio-native", hostname: host };
}
if (host === "api.openai.com") {
return { endpointClass: "openai-public", hostname: host };
}
@@ -182,6 +247,12 @@ function resolveKnownProviderFamily(provider: string | undefined): string {
return "anthropic";
case "google":
return "google";
case "moonshot":
case "kimi":
return "moonshot";
case "modelstudio":
case "dashscope":
return "modelstudio";
case "github-copilot":
return "github-copilot";
case "groq":
@@ -411,3 +482,66 @@ export function resolveProviderRequestAttributionHeaders(
): Record<string, string> | undefined {
return resolveProviderRequestPolicy(input, env).attributionHeaders;
}
export function resolveProviderRequestCapabilities(
input: ProviderRequestCapabilitiesInput,
env: RuntimeVersionEnv = process.env as RuntimeVersionEnv,
): ProviderRequestCapabilities {
const policy = resolveProviderRequestPolicy(input, env);
const provider = policy.provider;
const api = input.api?.trim().toLowerCase();
const normalizedModelId = input.modelId?.trim().toLowerCase();
const endpointClass = policy.endpointClass;
const isKnownNativeEndpoint =
endpointClass === "anthropic-public" ||
endpointClass === "moonshot-native" ||
endpointClass === "modelstudio-native" ||
endpointClass === "openai-public" ||
endpointClass === "openai-codex" ||
endpointClass === "azure-openai" ||
endpointClass === "openrouter" ||
endpointClass === "google-generative-ai" ||
endpointClass === "google-vertex";
let compatibilityFamily: ProviderRequestCompatibilityFamily | undefined;
if (provider && MOONSHOT_COMPAT_PROVIDERS.has(provider)) {
compatibilityFamily = "moonshot";
} else if (
provider === "ollama" &&
normalizedModelId?.startsWith("kimi-k") &&
normalizedModelId.includes(":cloud")
) {
compatibilityFamily = "moonshot";
}
return {
...policy,
isKnownNativeEndpoint,
allowsOpenAIServiceTier:
(provider === "openai" && api === "openai-responses" && endpointClass === "openai-public") ||
(provider === "openai-codex" &&
(api === "openai-codex-responses" || api === "openai-responses") &&
endpointClass === "openai-codex"),
allowsAnthropicServiceTier:
provider === "anthropic" &&
api === "anthropic-messages" &&
(endpointClass === "default" || endpointClass === "anthropic-public"),
// This is intentionally the gate for emitting `store: false` on Responses
// transports, not just a statement about vendor support in the abstract.
supportsResponsesStoreField:
input.compat?.supportsStore !== false && api !== undefined && OPENAI_RESPONSES_APIS.has(api),
allowsResponsesStore:
input.compat?.supportsStore !== false &&
provider !== undefined &&
api !== undefined &&
OPENAI_RESPONSES_APIS.has(api) &&
OPENAI_RESPONSES_PROVIDERS.has(provider) &&
policy.usesKnownNativeOpenAIEndpoint,
shouldStripResponsesPromptCache:
api !== undefined && OPENAI_RESPONSES_APIS.has(api) && policy.usesExplicitProxyLikeEndpoint,
supportsNativeStreamingUsageCompat:
(provider === "moonshot" && endpointClass === "moonshot-native") ||
(provider === "modelstudio" && endpointClass === "modelstudio-native"),
compatibilityFamily,
};
}

View File

@@ -13,6 +13,9 @@ export {
} from "../media-understanding/shared.js";
export type {
ProviderAttributionPolicy,
ProviderRequestCapabilities,
ProviderRequestCapabilitiesInput,
ProviderRequestCompatibilityFamily,
ProviderEndpointClass,
ProviderEndpointResolution,
ProviderRequestCapability,
@@ -22,5 +25,6 @@ export type {
} from "../agents/provider-attribution.js";
export {
resolveProviderEndpoint,
resolveProviderRequestCapabilities,
resolveProviderRequestPolicy,
} from "../agents/provider-attribution.js";

View File

@@ -1,4 +1,5 @@
import type { Api, Model } from "@mariozechner/pi-ai";
import { resolveProviderRequestCapabilities } from "../agents/provider-attribution.js";
import type { ModelCompatConfig } from "../config/types.models.js";
function extractModelCompat(
@@ -68,15 +69,6 @@ function isOpenAiCompletionsModel(model: Model<Api>): model is Model<"openai-com
return model.api === "openai-completions";
}
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"> {
return model.api === "anthropic-messages";
}
@@ -100,7 +92,15 @@ export function normalizeModelCompat(model: Model<Api>): Model<Api> {
}
const compat = model.compat ?? undefined;
const needsForce = baseUrl ? !isOpenAINativeEndpoint(baseUrl) : false;
const needsForce = baseUrl
? resolveProviderRequestCapabilities({
provider: typeof model.provider === "string" ? model.provider : undefined,
api: model.api,
baseUrl,
capability: "llm",
transport: "stream",
}).endpointClass !== "openai-public"
: false;
if (!needsForce) {
return model;
}