Skip to content

Commit 9224afc

Browse files
committed
refactor(providers): share xai and replay helpers
1 parent cc1881a commit 9224afc

9 files changed

Lines changed: 156 additions & 92 deletions

File tree

extensions/minimax/index.ts

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ import {
1313
} from "openclaw/plugin-sdk/provider-auth";
1414
import { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/provider-auth";
1515
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
16+
import {
17+
buildOpenAICompatibleReplayPolicy,
18+
buildStrictAnthropicReplayPolicy,
19+
} from "openclaw/plugin-sdk/provider-model-shared";
1620
import { createMinimaxFastModeWrapper } from "openclaw/plugin-sdk/provider-stream";
1721
import { fetchMinimaxUsage } from "openclaw/plugin-sdk/provider-usage";
1822
import { isMiniMaxModernModelId, MINIMAX_DEFAULT_MODEL_ID } from "./api.js";
@@ -46,29 +50,12 @@ function buildMinimaxReplayPolicy(
4650
): ProviderReplayPolicy | undefined {
4751
if (ctx.modelApi === "anthropic-messages" || ctx.modelApi === "bedrock-converse-stream") {
4852
const modelId = ctx.modelId?.toLowerCase() ?? "";
49-
return {
50-
sanitizeMode: "full",
51-
sanitizeToolCallIds: true,
52-
toolCallIdMode: "strict",
53-
preserveSignatures: true,
54-
repairToolUseResultPairing: true,
55-
validateAnthropicTurns: true,
56-
allowSyntheticToolResults: true,
57-
...(modelId.includes("claude") ? { dropThinkingBlocks: true } : {}),
58-
};
59-
}
60-
61-
if (ctx.modelApi === "openai-completions") {
62-
return {
63-
sanitizeToolCallIds: true,
64-
toolCallIdMode: "strict",
65-
applyAssistantFirstOrderingFix: true,
66-
validateGeminiTurns: true,
67-
validateAnthropicTurns: true,
68-
};
53+
return buildStrictAnthropicReplayPolicy({
54+
dropThinkingBlocks: modelId.includes("claude"),
55+
});
6956
}
7057

71-
return undefined;
58+
return buildOpenAICompatibleReplayPolicy(ctx.modelApi);
7259
}
7360

7461
function getDefaultBaseUrl(region: MiniMaxRegion): string {

extensions/moonshot/index.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
ProviderReplayPolicyContext,
44
} from "openclaw/plugin-sdk/plugin-entry";
55
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
6+
import { buildOpenAICompatibleReplayPolicy } from "openclaw/plugin-sdk/provider-model-shared";
67
import {
78
createMoonshotThinkingWrapper,
89
resolveMoonshotThinkingType,
@@ -22,17 +23,7 @@ const PROVIDER_ID = "moonshot";
2223
function buildMoonshotReplayPolicy(
2324
ctx: ProviderReplayPolicyContext,
2425
): ProviderReplayPolicy | undefined {
25-
if (ctx.modelApi !== "openai-completions") {
26-
return undefined;
27-
}
28-
29-
return {
30-
sanitizeToolCallIds: true,
31-
toolCallIdMode: "strict",
32-
applyAssistantFirstOrderingFix: true,
33-
validateGeminiTurns: true,
34-
validateAnthropicTurns: true,
35-
};
26+
return buildOpenAICompatibleReplayPolicy(ctx.modelApi);
3627
}
3728

3829
export default defineSingleProviderPluginEntry({

extensions/ollama/index.ts

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
type ProviderReplayPolicy,
99
type ProviderReplayPolicyContext,
1010
} from "openclaw/plugin-sdk/plugin-entry";
11+
import { buildOpenAICompatibleReplayPolicy } from "openclaw/plugin-sdk/provider-model-shared";
1112
import {
1213
buildOllamaProvider,
1314
configureOllamaNonInteractive,
@@ -31,30 +32,7 @@ const DEFAULT_API_KEY = "ollama-local";
3132
function buildOllamaReplayPolicy(
3233
ctx: ProviderReplayPolicyContext,
3334
): ProviderReplayPolicy | undefined {
34-
if (
35-
ctx.modelApi !== "openai-completions" &&
36-
ctx.modelApi !== "openai-responses" &&
37-
ctx.modelApi !== "openai-codex-responses" &&
38-
ctx.modelApi !== "azure-openai-responses"
39-
) {
40-
return undefined;
41-
}
42-
43-
return {
44-
sanitizeToolCallIds: true,
45-
toolCallIdMode: "strict",
46-
...(ctx.modelApi === "openai-completions"
47-
? {
48-
applyAssistantFirstOrderingFix: true,
49-
validateGeminiTurns: true,
50-
validateAnthropicTurns: true,
51-
}
52-
: {
53-
applyAssistantFirstOrderingFix: false,
54-
validateGeminiTurns: false,
55-
validateAnthropicTurns: false,
56-
}),
57-
};
35+
return buildOpenAICompatibleReplayPolicy(ctx.modelApi);
5836
}
5937

6038
function shouldSkipAmbientOllamaDiscovery(env: NodeJS.ProcessEnv): boolean {

extensions/xai/api.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { describe, expect, it } from "vitest";
2+
import { isXaiModelHint, resolveXaiTransport, shouldContributeXaiCompat } from "./api.js";
3+
4+
describe("xai api helpers", () => {
5+
it("uses shared endpoint classification for native xAI transports", () => {
6+
expect(
7+
resolveXaiTransport({
8+
provider: "custom-xai",
9+
api: "openai-completions",
10+
baseUrl: "https://api.x.ai/v1",
11+
}),
12+
).toEqual({
13+
api: "openai-responses",
14+
baseUrl: "https://api.x.ai/v1",
15+
});
16+
});
17+
18+
it("keeps default-route xAI transport for the declared provider", () => {
19+
expect(
20+
resolveXaiTransport({
21+
provider: "xai",
22+
api: "openai-completions",
23+
}),
24+
).toEqual({
25+
api: "openai-responses",
26+
baseUrl: undefined,
27+
});
28+
});
29+
30+
it("contributes compat for native xAI hosts and model hints", () => {
31+
expect(
32+
shouldContributeXaiCompat({
33+
modelId: "custom-model",
34+
model: {
35+
api: "openai-completions",
36+
baseUrl: "https://api.x.ai/v1",
37+
},
38+
}),
39+
).toBe(true);
40+
expect(
41+
shouldContributeXaiCompat({
42+
modelId: "x-ai/grok-4",
43+
model: {
44+
api: "openai-completions",
45+
baseUrl: "https://proxy.example.com/v1",
46+
},
47+
}),
48+
).toBe(true);
49+
expect(isXaiModelHint("x-ai/grok-4")).toBe(true);
50+
});
51+
});

extensions/xai/api.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
applyModelCompatPatch,
33
normalizeProviderId,
4+
resolveProviderEndpoint,
45
} from "openclaw/plugin-sdk/provider-model-shared";
56
import type { ModelCompatConfig } from "openclaw/plugin-sdk/provider-model-shared";
67
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
3940
) as T;
4041
}
4142

42-
function isXaiBaseUrl(baseUrl: unknown): boolean {
43-
if (typeof baseUrl !== "string" || !baseUrl.trim()) {
44-
return false;
45-
}
46-
try {
47-
return new URL(baseUrl).hostname.toLowerCase() === "api.x.ai";
48-
} catch {
49-
return baseUrl.toLowerCase().includes("api.x.ai");
50-
}
43+
function isXaiNativeEndpoint(baseUrl: unknown): boolean {
44+
return (
45+
typeof baseUrl === "string" && resolveProviderEndpoint(baseUrl).endpointClass === "xai-native"
46+
);
5147
}
5248

5349
export function isXaiModelHint(modelId: string): boolean {
@@ -62,7 +58,7 @@ function shouldUseXaiResponsesTransport(params: {
6258
if (params.api !== "openai-completions") {
6359
return false;
6460
}
65-
if (isXaiBaseUrl(params.baseUrl)) {
61+
if (isXaiNativeEndpoint(params.baseUrl)) {
6662
return true;
6763
}
6864
return normalizeProviderId(params.provider) === "xai" && !params.baseUrl;
@@ -75,7 +71,7 @@ export function shouldContributeXaiCompat(params: {
7571
if (params.model.api !== "openai-completions") {
7672
return false;
7773
}
78-
return isXaiBaseUrl(params.model.baseUrl) || isXaiModelHint(params.modelId);
74+
return isXaiNativeEndpoint(params.model.baseUrl) || isXaiModelHint(params.modelId);
7975
}
8076

8177
export function resolveXaiTransport(params: {

extensions/zai/index.ts

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ import {
1818
upsertAuthProfile,
1919
validateApiKeyInput,
2020
} from "openclaw/plugin-sdk/provider-auth-api-key";
21-
import { normalizeModelCompat } from "openclaw/plugin-sdk/provider-model-shared";
21+
import {
22+
buildOpenAICompatibleReplayPolicy,
23+
normalizeModelCompat,
24+
} from "openclaw/plugin-sdk/provider-model-shared";
2225
import { createZaiToolStreamWrapper } from "openclaw/plugin-sdk/provider-stream";
2326
import { fetchZaiUsage, resolveLegacyPiAgentAccessToken } from "openclaw/plugin-sdk/provider-usage";
2427
import { detectZaiEndpoint, type ZaiEndpointId } from "./detect.js";
@@ -31,30 +34,7 @@ const GLM5_TEMPLATE_MODEL_ID = "glm-4.7";
3134
const PROFILE_ID = "zai:default";
3235

3336
function buildZaiReplayPolicy(ctx: ProviderReplayPolicyContext): ProviderReplayPolicy | undefined {
34-
if (
35-
ctx.modelApi !== "openai-completions" &&
36-
ctx.modelApi !== "openai-responses" &&
37-
ctx.modelApi !== "openai-codex-responses" &&
38-
ctx.modelApi !== "azure-openai-responses"
39-
) {
40-
return undefined;
41-
}
42-
43-
return {
44-
sanitizeToolCallIds: true,
45-
toolCallIdMode: "strict",
46-
...(ctx.modelApi === "openai-completions"
47-
? {
48-
applyAssistantFirstOrderingFix: true,
49-
validateGeminiTurns: true,
50-
validateAnthropicTurns: true,
51-
}
52-
: {
53-
applyAssistantFirstOrderingFix: false,
54-
validateGeminiTurns: false,
55-
validateAnthropicTurns: false,
56-
}),
57-
};
37+
return buildOpenAICompatibleReplayPolicy(ctx.modelApi);
5838
}
5939

6040
function resolveGlm5ForwardCompatModel(

src/plugin-sdk/provider-model-shared.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,15 @@ export type {
1111
ModelCompatConfig,
1212
ModelDefinitionConfig,
1313
} from "../config/types.models.js";
14+
export type {
15+
ProviderEndpointClass,
16+
ProviderEndpointResolution,
17+
} from "../agents/provider-attribution.js";
1418
export type { ProviderPlugin } from "../plugins/types.js";
1519
export type { KilocodeModelCatalogEntry } from "../plugins/provider-model-kilocode.js";
1620

1721
export { DEFAULT_CONTEXT_TOKENS } from "../agents/defaults.js";
22+
export { resolveProviderEndpoint } from "../agents/provider-attribution.js";
1823
export {
1924
applyModelCompatPatch,
2025
hasToolSchemaProfile,
@@ -24,6 +29,10 @@ export {
2429
resolveToolCallArgumentsEncoding,
2530
} from "../plugins/provider-model-compat.js";
2631
export { normalizeProviderId } from "../agents/provider-id.js";
32+
export {
33+
buildOpenAICompatibleReplayPolicy,
34+
buildStrictAnthropicReplayPolicy,
35+
} from "../plugins/provider-replay-helpers.js";
2736
export {
2837
createMoonshotThinkingWrapper,
2938
resolveMoonshotThinkingType,
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
buildOpenAICompatibleReplayPolicy,
4+
buildStrictAnthropicReplayPolicy,
5+
} from "./provider-replay-helpers.js";
6+
7+
describe("provider replay helpers", () => {
8+
it("builds strict openai-completions replay policy", () => {
9+
expect(buildOpenAICompatibleReplayPolicy("openai-completions")).toMatchObject({
10+
sanitizeToolCallIds: true,
11+
toolCallIdMode: "strict",
12+
applyAssistantFirstOrderingFix: true,
13+
validateGeminiTurns: true,
14+
validateAnthropicTurns: true,
15+
});
16+
});
17+
18+
it("builds strict anthropic replay policy", () => {
19+
expect(buildStrictAnthropicReplayPolicy({ dropThinkingBlocks: true })).toMatchObject({
20+
sanitizeMode: "full",
21+
preserveSignatures: true,
22+
repairToolUseResultPairing: true,
23+
allowSyntheticToolResults: true,
24+
dropThinkingBlocks: true,
25+
});
26+
});
27+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { ProviderReplayPolicy } from "./types.js";
2+
3+
export function buildOpenAICompatibleReplayPolicy(
4+
modelApi: string | null | undefined,
5+
): ProviderReplayPolicy | undefined {
6+
if (
7+
modelApi !== "openai-completions" &&
8+
modelApi !== "openai-responses" &&
9+
modelApi !== "openai-codex-responses" &&
10+
modelApi !== "azure-openai-responses"
11+
) {
12+
return undefined;
13+
}
14+
15+
return {
16+
sanitizeToolCallIds: true,
17+
toolCallIdMode: "strict",
18+
...(modelApi === "openai-completions"
19+
? {
20+
applyAssistantFirstOrderingFix: true,
21+
validateGeminiTurns: true,
22+
validateAnthropicTurns: true,
23+
}
24+
: {
25+
applyAssistantFirstOrderingFix: false,
26+
validateGeminiTurns: false,
27+
validateAnthropicTurns: false,
28+
}),
29+
};
30+
}
31+
32+
export function buildStrictAnthropicReplayPolicy(
33+
options: { dropThinkingBlocks?: boolean } = {},
34+
): ProviderReplayPolicy {
35+
return {
36+
sanitizeMode: "full",
37+
sanitizeToolCallIds: true,
38+
toolCallIdMode: "strict",
39+
preserveSignatures: true,
40+
repairToolUseResultPairing: true,
41+
validateAnthropicTurns: true,
42+
allowSyntheticToolResults: true,
43+
...(options.dropThinkingBlocks ? { dropThinkingBlocks: true } : {}),
44+
};
45+
}

0 commit comments

Comments
 (0)