Files
openclaw/src/plugins/provider-model-compat.ts
Peter Steinberger 98e3f729bc refactor: remove dead plugin loader exports (#105937)
* refactor(plugins): trim activation and contract exports

* test(plugins): restore fixture cleanup

* refactor(plugins): trim install and loader exports

* test(plugins): fully reset loader caches

* refactor(plugins): trim metadata and catalog exports

* test(plugins): preserve catalog trust coverage

* refactor(plugins): trim provider and plugin exports

* refactor(plugins): trim runtime and tool exports

* test(plugins): update dead-export consumers

* test(plugins): remove empty dead-export suites

* refactor(plugins): align exports with split registry

* refactor(plugins): trim drifted loader exports

* style(plugins): format test fixtures

* refactor(scripts): use supported plugin APIs

* refactor(plugins): finish dead export cleanup

* chore(deadcode): refresh export baseline

* test(cli): mock production memory state

* chore(deadcode): sync latest export baseline

* fix(tests): keep plugin fixtures inside core

* chore(deadcode): refresh rebased export baseline

* chore(deadcode): sync current ratchets

* fix(plugins): retain reserved slot invariant

* fix(plugins): preserve dead-export invariants

* test(plugins): use neutral catalog query fixture

* test(plugins): satisfy catalog lint

* test(plugins): preserve integrity drift coverage

* fix(ci): register skill experience live proof
2026-07-13 01:29:33 -07:00

132 lines
4.5 KiB
TypeScript

// Normalizes provider model compatibility metadata from plugins.
import { resolveUnsupportedToolSchemaKeywords } from "@openclaw/ai/internal/openai";
import { detectOpenAICompletionsCompat } from "../agents/openai-completions-compat.js";
import type { ModelCompatConfig } from "../config/types.models.js";
import type { Model } from "../llm/types.js";
export function extractModelCompat(
modelOrCompat: { compat?: unknown } | ModelCompatConfig | undefined,
): ModelCompatConfig | undefined {
if (!modelOrCompat || typeof modelOrCompat !== "object") {
return undefined;
}
if ("compat" in modelOrCompat) {
const compat = (modelOrCompat as { compat?: unknown }).compat;
return compat && typeof compat === "object" ? (compat as ModelCompatConfig) : undefined;
}
return modelOrCompat as ModelCompatConfig;
}
/** @deprecated Provider-owned model compat helper; do not use from third-party plugins. */
export function applyModelCompatPatch<T extends { compat?: ModelCompatConfig }>(
model: T,
patch: Partial<ModelCompatConfig> & Record<string, unknown>,
): T {
const nextCompat = { ...model.compat, ...patch } as ModelCompatConfig;
const currentCompat = model.compat as (Record<string, unknown> & ModelCompatConfig) | undefined;
if (
model.compat &&
Object.entries(patch).every(([key, value]) => currentCompat?.[key] === value)
) {
return model;
}
return {
...model,
compat: nextCompat,
};
}
export function hasToolSchemaProfile(
modelOrCompat: { compat?: unknown } | ModelCompatConfig | undefined,
profile: string,
): boolean {
return extractModelCompat(modelOrCompat)?.toolSchemaProfile === profile;
}
export function hasNativeWebSearchTool(
modelOrCompat: { compat?: unknown } | ModelCompatConfig | undefined,
): boolean {
return extractModelCompat(modelOrCompat)?.nativeWebSearchTool === true;
}
export function resolveToolCallArgumentsEncoding(
modelOrCompat: { compat?: unknown } | ModelCompatConfig | undefined,
): ModelCompatConfig["toolCallArgumentsEncoding"] | undefined {
return extractModelCompat(modelOrCompat)?.toolCallArgumentsEncoding;
}
// Tool-schema compat predicates moved into @openclaw/ai (agent-tools-parameter-schema);
// re-export so existing core/plugin callers keep one canonical import site.
export { resolveUnsupportedToolSchemaKeywords };
function isOpenAiCompletionsModel(model: Model): model is Model<"openai-completions"> {
return model.api === "openai-completions";
}
function isAnthropicMessagesModel(model: Model): model is Model<"anthropic-messages"> {
return model.api === "anthropic-messages";
}
function normalizeAnthropicBaseUrl(baseUrl: string): string {
return baseUrl.replace(/\/v1\/?$/, "");
}
export function normalizeModelCompat(model: Model): Model {
const baseUrl = model.baseUrl ?? "";
if (isAnthropicMessagesModel(model) && baseUrl) {
const normalized = normalizeAnthropicBaseUrl(baseUrl);
if (normalized !== baseUrl) {
return { ...model, baseUrl: normalized } as Model<"anthropic-messages">;
}
}
if (!isOpenAiCompletionsModel(model)) {
return model;
}
const compat = model.compat ?? undefined;
const detectedCompatDefaults = baseUrl
? detectOpenAICompletionsCompat(model).defaults
: undefined;
const needsForce = Boolean(
detectedCompatDefaults &&
(!detectedCompatDefaults.supportsDeveloperRole ||
!detectedCompatDefaults.supportsUsageInStreaming ||
!detectedCompatDefaults.supportsStrictMode),
);
if (!needsForce) {
return model;
}
const forcedDeveloperRole = compat?.supportsDeveloperRole === true;
const hasStreamingUsageOverride = compat?.supportsUsageInStreaming !== undefined;
const targetStrictMode = compat?.supportsStrictMode ?? detectedCompatDefaults?.supportsStrictMode;
if (
compat?.supportsDeveloperRole !== undefined &&
hasStreamingUsageOverride &&
compat?.supportsStrictMode !== undefined
) {
return model;
}
return {
...model,
compat: compat
? {
...compat,
supportsDeveloperRole: forcedDeveloperRole || false,
...(hasStreamingUsageOverride
? {}
: {
supportsUsageInStreaming: detectedCompatDefaults?.supportsUsageInStreaming ?? false,
}),
supportsStrictMode: targetStrictMode,
}
: {
supportsDeveloperRole: false,
supportsUsageInStreaming: detectedCompatDefaults?.supportsUsageInStreaming ?? false,
supportsStrictMode: detectedCompatDefaults?.supportsStrictMode ?? false,
},
} as typeof model;
}