mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-10 00:31:22 +00:00
refactor(providers): share xai and replay helpers
This commit is contained in:
@@ -13,6 +13,10 @@ import {
|
||||
} from "openclaw/plugin-sdk/provider-auth";
|
||||
import { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/provider-auth";
|
||||
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
|
||||
import {
|
||||
buildOpenAICompatibleReplayPolicy,
|
||||
buildStrictAnthropicReplayPolicy,
|
||||
} from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { createMinimaxFastModeWrapper } from "openclaw/plugin-sdk/provider-stream";
|
||||
import { fetchMinimaxUsage } from "openclaw/plugin-sdk/provider-usage";
|
||||
import { isMiniMaxModernModelId, MINIMAX_DEFAULT_MODEL_ID } from "./api.js";
|
||||
@@ -46,29 +50,12 @@ function buildMinimaxReplayPolicy(
|
||||
): ProviderReplayPolicy | undefined {
|
||||
if (ctx.modelApi === "anthropic-messages" || ctx.modelApi === "bedrock-converse-stream") {
|
||||
const modelId = ctx.modelId?.toLowerCase() ?? "";
|
||||
return {
|
||||
sanitizeMode: "full",
|
||||
sanitizeToolCallIds: true,
|
||||
toolCallIdMode: "strict",
|
||||
preserveSignatures: true,
|
||||
repairToolUseResultPairing: true,
|
||||
validateAnthropicTurns: true,
|
||||
allowSyntheticToolResults: true,
|
||||
...(modelId.includes("claude") ? { dropThinkingBlocks: true } : {}),
|
||||
};
|
||||
return buildStrictAnthropicReplayPolicy({
|
||||
dropThinkingBlocks: modelId.includes("claude"),
|
||||
});
|
||||
}
|
||||
|
||||
if (ctx.modelApi === "openai-completions") {
|
||||
return {
|
||||
sanitizeToolCallIds: true,
|
||||
toolCallIdMode: "strict",
|
||||
applyAssistantFirstOrderingFix: true,
|
||||
validateGeminiTurns: true,
|
||||
validateAnthropicTurns: true,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return buildOpenAICompatibleReplayPolicy(ctx.modelApi);
|
||||
}
|
||||
|
||||
function getDefaultBaseUrl(region: MiniMaxRegion): string {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
ProviderReplayPolicyContext,
|
||||
} from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
|
||||
import { buildOpenAICompatibleReplayPolicy } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import {
|
||||
createMoonshotThinkingWrapper,
|
||||
resolveMoonshotThinkingType,
|
||||
@@ -22,17 +23,7 @@ const PROVIDER_ID = "moonshot";
|
||||
function buildMoonshotReplayPolicy(
|
||||
ctx: ProviderReplayPolicyContext,
|
||||
): ProviderReplayPolicy | undefined {
|
||||
if (ctx.modelApi !== "openai-completions") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
sanitizeToolCallIds: true,
|
||||
toolCallIdMode: "strict",
|
||||
applyAssistantFirstOrderingFix: true,
|
||||
validateGeminiTurns: true,
|
||||
validateAnthropicTurns: true,
|
||||
};
|
||||
return buildOpenAICompatibleReplayPolicy(ctx.modelApi);
|
||||
}
|
||||
|
||||
export default defineSingleProviderPluginEntry({
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type ProviderReplayPolicy,
|
||||
type ProviderReplayPolicyContext,
|
||||
} from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { buildOpenAICompatibleReplayPolicy } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import {
|
||||
buildOllamaProvider,
|
||||
configureOllamaNonInteractive,
|
||||
@@ -31,30 +32,7 @@ const DEFAULT_API_KEY = "ollama-local";
|
||||
function buildOllamaReplayPolicy(
|
||||
ctx: ProviderReplayPolicyContext,
|
||||
): ProviderReplayPolicy | undefined {
|
||||
if (
|
||||
ctx.modelApi !== "openai-completions" &&
|
||||
ctx.modelApi !== "openai-responses" &&
|
||||
ctx.modelApi !== "openai-codex-responses" &&
|
||||
ctx.modelApi !== "azure-openai-responses"
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
sanitizeToolCallIds: true,
|
||||
toolCallIdMode: "strict",
|
||||
...(ctx.modelApi === "openai-completions"
|
||||
? {
|
||||
applyAssistantFirstOrderingFix: true,
|
||||
validateGeminiTurns: true,
|
||||
validateAnthropicTurns: true,
|
||||
}
|
||||
: {
|
||||
applyAssistantFirstOrderingFix: false,
|
||||
validateGeminiTurns: false,
|
||||
validateAnthropicTurns: false,
|
||||
}),
|
||||
};
|
||||
return buildOpenAICompatibleReplayPolicy(ctx.modelApi);
|
||||
}
|
||||
|
||||
function shouldSkipAmbientOllamaDiscovery(env: NodeJS.ProcessEnv): boolean {
|
||||
|
||||
51
extensions/xai/api.test.ts
Normal file
51
extensions/xai/api.test.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isXaiModelHint, resolveXaiTransport, shouldContributeXaiCompat } from "./api.js";
|
||||
|
||||
describe("xai api helpers", () => {
|
||||
it("uses shared endpoint classification for native xAI transports", () => {
|
||||
expect(
|
||||
resolveXaiTransport({
|
||||
provider: "custom-xai",
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://api.x.ai/v1",
|
||||
}),
|
||||
).toEqual({
|
||||
api: "openai-responses",
|
||||
baseUrl: "https://api.x.ai/v1",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps default-route xAI transport for the declared provider", () => {
|
||||
expect(
|
||||
resolveXaiTransport({
|
||||
provider: "xai",
|
||||
api: "openai-completions",
|
||||
}),
|
||||
).toEqual({
|
||||
api: "openai-responses",
|
||||
baseUrl: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("contributes compat for native xAI hosts and model hints", () => {
|
||||
expect(
|
||||
shouldContributeXaiCompat({
|
||||
modelId: "custom-model",
|
||||
model: {
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://api.x.ai/v1",
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldContributeXaiCompat({
|
||||
modelId: "x-ai/grok-4",
|
||||
model: {
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://proxy.example.com/v1",
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(isXaiModelHint("x-ai/grok-4")).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
applyModelCompatPatch,
|
||||
normalizeProviderId,
|
||||
resolveProviderEndpoint,
|
||||
} from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import type { ModelCompatConfig } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { XAI_UNSUPPORTED_SCHEMA_KEYWORDS } from "openclaw/plugin-sdk/provider-tools";
|
||||
@@ -39,15 +40,10 @@ export function applyXaiModelCompat<T extends { compat?: unknown }>(model: T): T
|
||||
) as T;
|
||||
}
|
||||
|
||||
function isXaiBaseUrl(baseUrl: unknown): boolean {
|
||||
if (typeof baseUrl !== "string" || !baseUrl.trim()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return new URL(baseUrl).hostname.toLowerCase() === "api.x.ai";
|
||||
} catch {
|
||||
return baseUrl.toLowerCase().includes("api.x.ai");
|
||||
}
|
||||
function isXaiNativeEndpoint(baseUrl: unknown): boolean {
|
||||
return (
|
||||
typeof baseUrl === "string" && resolveProviderEndpoint(baseUrl).endpointClass === "xai-native"
|
||||
);
|
||||
}
|
||||
|
||||
export function isXaiModelHint(modelId: string): boolean {
|
||||
@@ -62,7 +58,7 @@ function shouldUseXaiResponsesTransport(params: {
|
||||
if (params.api !== "openai-completions") {
|
||||
return false;
|
||||
}
|
||||
if (isXaiBaseUrl(params.baseUrl)) {
|
||||
if (isXaiNativeEndpoint(params.baseUrl)) {
|
||||
return true;
|
||||
}
|
||||
return normalizeProviderId(params.provider) === "xai" && !params.baseUrl;
|
||||
@@ -75,7 +71,7 @@ export function shouldContributeXaiCompat(params: {
|
||||
if (params.model.api !== "openai-completions") {
|
||||
return false;
|
||||
}
|
||||
return isXaiBaseUrl(params.model.baseUrl) || isXaiModelHint(params.modelId);
|
||||
return isXaiNativeEndpoint(params.model.baseUrl) || isXaiModelHint(params.modelId);
|
||||
}
|
||||
|
||||
export function resolveXaiTransport(params: {
|
||||
|
||||
@@ -18,7 +18,10 @@ import {
|
||||
upsertAuthProfile,
|
||||
validateApiKeyInput,
|
||||
} from "openclaw/plugin-sdk/provider-auth-api-key";
|
||||
import { normalizeModelCompat } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import {
|
||||
buildOpenAICompatibleReplayPolicy,
|
||||
normalizeModelCompat,
|
||||
} from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { createZaiToolStreamWrapper } from "openclaw/plugin-sdk/provider-stream";
|
||||
import { fetchZaiUsage, resolveLegacyPiAgentAccessToken } from "openclaw/plugin-sdk/provider-usage";
|
||||
import { detectZaiEndpoint, type ZaiEndpointId } from "./detect.js";
|
||||
@@ -31,30 +34,7 @@ const GLM5_TEMPLATE_MODEL_ID = "glm-4.7";
|
||||
const PROFILE_ID = "zai:default";
|
||||
|
||||
function buildZaiReplayPolicy(ctx: ProviderReplayPolicyContext): ProviderReplayPolicy | undefined {
|
||||
if (
|
||||
ctx.modelApi !== "openai-completions" &&
|
||||
ctx.modelApi !== "openai-responses" &&
|
||||
ctx.modelApi !== "openai-codex-responses" &&
|
||||
ctx.modelApi !== "azure-openai-responses"
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
sanitizeToolCallIds: true,
|
||||
toolCallIdMode: "strict",
|
||||
...(ctx.modelApi === "openai-completions"
|
||||
? {
|
||||
applyAssistantFirstOrderingFix: true,
|
||||
validateGeminiTurns: true,
|
||||
validateAnthropicTurns: true,
|
||||
}
|
||||
: {
|
||||
applyAssistantFirstOrderingFix: false,
|
||||
validateGeminiTurns: false,
|
||||
validateAnthropicTurns: false,
|
||||
}),
|
||||
};
|
||||
return buildOpenAICompatibleReplayPolicy(ctx.modelApi);
|
||||
}
|
||||
|
||||
function resolveGlm5ForwardCompatModel(
|
||||
|
||||
@@ -11,10 +11,15 @@ export type {
|
||||
ModelCompatConfig,
|
||||
ModelDefinitionConfig,
|
||||
} from "../config/types.models.js";
|
||||
export type {
|
||||
ProviderEndpointClass,
|
||||
ProviderEndpointResolution,
|
||||
} from "../agents/provider-attribution.js";
|
||||
export type { ProviderPlugin } from "../plugins/types.js";
|
||||
export type { KilocodeModelCatalogEntry } from "../plugins/provider-model-kilocode.js";
|
||||
|
||||
export { DEFAULT_CONTEXT_TOKENS } from "../agents/defaults.js";
|
||||
export { resolveProviderEndpoint } from "../agents/provider-attribution.js";
|
||||
export {
|
||||
applyModelCompatPatch,
|
||||
hasToolSchemaProfile,
|
||||
@@ -24,6 +29,10 @@ export {
|
||||
resolveToolCallArgumentsEncoding,
|
||||
} from "../plugins/provider-model-compat.js";
|
||||
export { normalizeProviderId } from "../agents/provider-id.js";
|
||||
export {
|
||||
buildOpenAICompatibleReplayPolicy,
|
||||
buildStrictAnthropicReplayPolicy,
|
||||
} from "../plugins/provider-replay-helpers.js";
|
||||
export {
|
||||
createMoonshotThinkingWrapper,
|
||||
resolveMoonshotThinkingType,
|
||||
|
||||
27
src/plugins/provider-replay-helpers.test.ts
Normal file
27
src/plugins/provider-replay-helpers.test.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildOpenAICompatibleReplayPolicy,
|
||||
buildStrictAnthropicReplayPolicy,
|
||||
} from "./provider-replay-helpers.js";
|
||||
|
||||
describe("provider replay helpers", () => {
|
||||
it("builds strict openai-completions replay policy", () => {
|
||||
expect(buildOpenAICompatibleReplayPolicy("openai-completions")).toMatchObject({
|
||||
sanitizeToolCallIds: true,
|
||||
toolCallIdMode: "strict",
|
||||
applyAssistantFirstOrderingFix: true,
|
||||
validateGeminiTurns: true,
|
||||
validateAnthropicTurns: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("builds strict anthropic replay policy", () => {
|
||||
expect(buildStrictAnthropicReplayPolicy({ dropThinkingBlocks: true })).toMatchObject({
|
||||
sanitizeMode: "full",
|
||||
preserveSignatures: true,
|
||||
repairToolUseResultPairing: true,
|
||||
allowSyntheticToolResults: true,
|
||||
dropThinkingBlocks: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
45
src/plugins/provider-replay-helpers.ts
Normal file
45
src/plugins/provider-replay-helpers.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { ProviderReplayPolicy } from "./types.js";
|
||||
|
||||
export function buildOpenAICompatibleReplayPolicy(
|
||||
modelApi: string | null | undefined,
|
||||
): ProviderReplayPolicy | undefined {
|
||||
if (
|
||||
modelApi !== "openai-completions" &&
|
||||
modelApi !== "openai-responses" &&
|
||||
modelApi !== "openai-codex-responses" &&
|
||||
modelApi !== "azure-openai-responses"
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
sanitizeToolCallIds: true,
|
||||
toolCallIdMode: "strict",
|
||||
...(modelApi === "openai-completions"
|
||||
? {
|
||||
applyAssistantFirstOrderingFix: true,
|
||||
validateGeminiTurns: true,
|
||||
validateAnthropicTurns: true,
|
||||
}
|
||||
: {
|
||||
applyAssistantFirstOrderingFix: false,
|
||||
validateGeminiTurns: false,
|
||||
validateAnthropicTurns: false,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildStrictAnthropicReplayPolicy(
|
||||
options: { dropThinkingBlocks?: boolean } = {},
|
||||
): ProviderReplayPolicy {
|
||||
return {
|
||||
sanitizeMode: "full",
|
||||
sanitizeToolCallIds: true,
|
||||
toolCallIdMode: "strict",
|
||||
preserveSignatures: true,
|
||||
repairToolUseResultPairing: true,
|
||||
validateAnthropicTurns: true,
|
||||
allowSyntheticToolResults: true,
|
||||
...(options.dropThinkingBlocks ? { dropThinkingBlocks: true } : {}),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user