mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-01 10:20:20 +00:00
refactor(providers): add family replay and tool hooks
This commit is contained in:
80
src/plugin-sdk/provider-model-shared.test.ts
Normal file
80
src/plugin-sdk/provider-model-shared.test.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildProviderReplayFamilyHooks } from "./provider-model-shared.js";
|
||||
|
||||
describe("buildProviderReplayFamilyHooks", () => {
|
||||
it("maps openai-compatible replay families", () => {
|
||||
const hooks = buildProviderReplayFamilyHooks({
|
||||
family: "openai-compatible",
|
||||
});
|
||||
|
||||
expect(
|
||||
hooks.buildReplayPolicy?.({
|
||||
provider: "xai",
|
||||
modelApi: "openai-completions",
|
||||
modelId: "grok-4",
|
||||
} as never),
|
||||
).toMatchObject({
|
||||
sanitizeToolCallIds: true,
|
||||
applyAssistantFirstOrderingFix: true,
|
||||
validateGeminiTurns: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("maps google-gemini replay families", () => {
|
||||
const hooks = buildProviderReplayFamilyHooks({
|
||||
family: "google-gemini",
|
||||
});
|
||||
|
||||
expect(hooks.resolveReasoningOutputMode?.({} as never)).toBe("tagged");
|
||||
expect(
|
||||
hooks.buildReplayPolicy?.({
|
||||
provider: "google",
|
||||
modelApi: "google-generative-ai",
|
||||
modelId: "gemini-3.1-pro-preview",
|
||||
} as never),
|
||||
).toMatchObject({
|
||||
validateGeminiTurns: true,
|
||||
allowSyntheticToolResults: true,
|
||||
});
|
||||
|
||||
const sanitized = hooks.sanitizeReplayHistory?.({
|
||||
provider: "google",
|
||||
modelApi: "google-generative-ai",
|
||||
modelId: "gemini-3.1-pro-preview",
|
||||
sessionId: "session-1",
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "hello" }],
|
||||
},
|
||||
],
|
||||
sessionState: {
|
||||
getCustomEntries: () => [],
|
||||
appendCustomEntry: () => {},
|
||||
},
|
||||
} as never);
|
||||
|
||||
expect(sanitized?.[0]).toMatchObject({
|
||||
role: "user",
|
||||
content: "(session bootstrap)",
|
||||
});
|
||||
});
|
||||
|
||||
it("maps hybrid anthropic/openai replay families", () => {
|
||||
const hooks = buildProviderReplayFamilyHooks({
|
||||
family: "hybrid-anthropic-openai",
|
||||
anthropicModelDropThinkingBlocks: true,
|
||||
});
|
||||
|
||||
expect(
|
||||
hooks.buildReplayPolicy?.({
|
||||
provider: "minimax",
|
||||
modelApi: "anthropic-messages",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
} as never),
|
||||
).toMatchObject({
|
||||
validateAnthropicTurns: true,
|
||||
dropThinkingBlocks: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,22 @@
|
||||
// without recursing through provider-specific facades.
|
||||
|
||||
import type { BedrockDiscoveryConfig, ModelDefinitionConfig } from "../config/types.models.js";
|
||||
import type { ProviderPlugin } from "../plugins/types.js";
|
||||
import {
|
||||
buildAnthropicReplayPolicyForModel,
|
||||
buildGoogleGeminiReplayPolicy,
|
||||
buildHybridAnthropicOrOpenAIReplayPolicy,
|
||||
buildOpenAICompatibleReplayPolicy,
|
||||
buildPassthroughGeminiSanitizingReplayPolicy,
|
||||
buildStrictAnthropicReplayPolicy,
|
||||
resolveTaggedReasoningOutputMode,
|
||||
sanitizeGoogleGeminiReplayHistory,
|
||||
} from "../plugins/provider-replay-helpers.js";
|
||||
import type {
|
||||
ProviderReasoningOutputModeContext,
|
||||
ProviderReplayPolicyContext,
|
||||
ProviderSanitizeReplayHistoryContext,
|
||||
} from "./plugin-entry.js";
|
||||
|
||||
export type { ModelApi, ModelProviderConfig } from "../config/types.models.js";
|
||||
export type {
|
||||
@@ -38,7 +54,7 @@ export {
|
||||
resolveTaggedReasoningOutputMode,
|
||||
sanitizeGoogleGeminiReplayHistory,
|
||||
buildStrictAnthropicReplayPolicy,
|
||||
} from "../plugins/provider-replay-helpers.js";
|
||||
};
|
||||
export {
|
||||
createMoonshotThinkingWrapper,
|
||||
resolveMoonshotThinkingType,
|
||||
@@ -110,3 +126,62 @@ export function normalizeNativeXaiModelId(id: string): string {
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
export type ProviderReplayFamily =
|
||||
| "openai-compatible"
|
||||
| "anthropic-by-model"
|
||||
| "google-gemini"
|
||||
| "passthrough-gemini"
|
||||
| "hybrid-anthropic-openai";
|
||||
|
||||
type ProviderReplayFamilyHooks = Pick<
|
||||
ProviderPlugin,
|
||||
"buildReplayPolicy" | "sanitizeReplayHistory" | "resolveReasoningOutputMode"
|
||||
>;
|
||||
|
||||
type BuildProviderReplayFamilyHooksOptions =
|
||||
| { family: "openai-compatible" }
|
||||
| { family: "anthropic-by-model" }
|
||||
| { family: "google-gemini" }
|
||||
| { family: "passthrough-gemini" }
|
||||
| {
|
||||
family: "hybrid-anthropic-openai";
|
||||
anthropicModelDropThinkingBlocks?: boolean;
|
||||
};
|
||||
|
||||
export function buildProviderReplayFamilyHooks(
|
||||
options: BuildProviderReplayFamilyHooksOptions,
|
||||
): ProviderReplayFamilyHooks {
|
||||
switch (options.family) {
|
||||
case "openai-compatible":
|
||||
return {
|
||||
buildReplayPolicy: (ctx: ProviderReplayPolicyContext) =>
|
||||
buildOpenAICompatibleReplayPolicy(ctx.modelApi),
|
||||
};
|
||||
case "anthropic-by-model":
|
||||
return {
|
||||
buildReplayPolicy: ({ modelId }: ProviderReplayPolicyContext) =>
|
||||
buildAnthropicReplayPolicyForModel(modelId),
|
||||
};
|
||||
case "google-gemini":
|
||||
return {
|
||||
buildReplayPolicy: () => buildGoogleGeminiReplayPolicy(),
|
||||
sanitizeReplayHistory: (ctx: ProviderSanitizeReplayHistoryContext) =>
|
||||
sanitizeGoogleGeminiReplayHistory(ctx),
|
||||
resolveReasoningOutputMode: (_ctx: ProviderReasoningOutputModeContext) =>
|
||||
resolveTaggedReasoningOutputMode(),
|
||||
};
|
||||
case "passthrough-gemini":
|
||||
return {
|
||||
buildReplayPolicy: ({ modelId }: ProviderReplayPolicyContext) =>
|
||||
buildPassthroughGeminiSanitizingReplayPolicy(modelId),
|
||||
};
|
||||
case "hybrid-anthropic-openai":
|
||||
return {
|
||||
buildReplayPolicy: (ctx: ProviderReplayPolicyContext) =>
|
||||
buildHybridAnthropicOrOpenAIReplayPolicy(ctx, {
|
||||
anthropicModelDropThinkingBlocks: options.anthropicModelDropThinkingBlocks,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
15
src/plugin-sdk/provider-tools.test.ts
Normal file
15
src/plugin-sdk/provider-tools.test.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildProviderToolCompatFamilyHooks,
|
||||
inspectGeminiToolSchemas,
|
||||
normalizeGeminiToolSchemas,
|
||||
} from "./provider-tools.js";
|
||||
|
||||
describe("buildProviderToolCompatFamilyHooks", () => {
|
||||
it("maps the gemini family to the shared schema helpers", () => {
|
||||
const hooks = buildProviderToolCompatFamilyHooks("gemini");
|
||||
|
||||
expect(hooks.normalizeToolSchemas).toBe(normalizeGeminiToolSchemas);
|
||||
expect(hooks.inspectToolSchemas).toBe(inspectGeminiToolSchemas);
|
||||
});
|
||||
});
|
||||
@@ -158,3 +158,20 @@ export function inspectGeminiToolSchemas(
|
||||
return [{ toolName: tool.name, toolIndex, violations }];
|
||||
});
|
||||
}
|
||||
|
||||
export type ProviderToolCompatFamily = "gemini";
|
||||
|
||||
export function buildProviderToolCompatFamilyHooks(family: ProviderToolCompatFamily): {
|
||||
normalizeToolSchemas: (ctx: ProviderNormalizeToolSchemasContext) => AnyAgentTool[];
|
||||
inspectToolSchemas: (
|
||||
ctx: ProviderNormalizeToolSchemasContext,
|
||||
) => ProviderToolSchemaDiagnostic[];
|
||||
} {
|
||||
switch (family) {
|
||||
case "gemini":
|
||||
return {
|
||||
normalizeToolSchemas: normalizeGeminiToolSchemas,
|
||||
inspectToolSchemas: inspectGeminiToolSchemas,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user