mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-05 22:32:12 +00:00
fix(plugin-sdk): unblock gateway test surfaces
This commit is contained in:
@@ -23,7 +23,6 @@ export default definePluginEntry({
|
||||
const {
|
||||
buildProviderReplayFamilyHooks,
|
||||
buildProviderStreamFamilyHooks,
|
||||
composeProviderStreamWrappers,
|
||||
createProviderApiKeyAuthMethod,
|
||||
DEFAULT_CONTEXT_TOKENS,
|
||||
getOpenRouterModelCapabilities,
|
||||
|
||||
@@ -154,6 +154,13 @@ describe("auth rate limiter", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it("tracks synthetic browser-origin limiter keys independently", () => {
|
||||
limiter = createAuthRateLimiter({ maxAttempts: 1, windowMs: 60_000, lockoutMs: 60_000 });
|
||||
limiter.recordFailure("browser-origin:http://127.0.0.1:18789");
|
||||
expect(limiter.check("browser-origin:http://127.0.0.1:18789").allowed).toBe(false);
|
||||
expect(limiter.check("browser-origin:http://localhost:5173").allowed).toBe(true);
|
||||
});
|
||||
|
||||
// ---------- loopback exemption ----------
|
||||
|
||||
it.each(["127.0.0.1", "::1"])("exempts loopback address %s by default", (ip) => {
|
||||
|
||||
@@ -39,6 +39,7 @@ export const AUTH_RATE_LIMIT_SCOPE_DEFAULT = "default";
|
||||
export const AUTH_RATE_LIMIT_SCOPE_SHARED_SECRET = "shared-secret";
|
||||
export const AUTH_RATE_LIMIT_SCOPE_DEVICE_TOKEN = "device-token";
|
||||
export const AUTH_RATE_LIMIT_SCOPE_HOOK_AUTH = "hook-auth";
|
||||
const BROWSER_ORIGIN_RATE_LIMIT_KEY_PREFIX = "browser-origin:";
|
||||
|
||||
export interface RateLimitEntry {
|
||||
/** Timestamps (epoch ms) of recent failed attempts inside the window. */
|
||||
@@ -89,6 +90,9 @@ const PRUNE_INTERVAL_MS = 60_000; // prune stale entries every minute
|
||||
* share one representation (including IPv4-mapped IPv6 forms).
|
||||
*/
|
||||
export function normalizeRateLimitClientIp(ip: string | undefined): string {
|
||||
if (typeof ip === "string" && ip.startsWith(BROWSER_ORIGIN_RATE_LIMIT_KEY_PREFIX)) {
|
||||
return ip;
|
||||
}
|
||||
return resolveClientIp({ remoteAddr: ip }) ?? "unknown";
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,24 @@ import {
|
||||
composeProviderStreamWrappers,
|
||||
} from "./provider-stream.js";
|
||||
|
||||
function requireWrapStreamFn(
|
||||
wrapStreamFn: ReturnType<typeof buildProviderStreamFamilyHooks>["wrapStreamFn"],
|
||||
) {
|
||||
expect(wrapStreamFn).toBeTypeOf("function");
|
||||
if (!wrapStreamFn) {
|
||||
throw new Error("expected wrapStreamFn to be defined");
|
||||
}
|
||||
return wrapStreamFn;
|
||||
}
|
||||
|
||||
function requireStreamFn(streamFn: StreamFn | null | undefined) {
|
||||
expect(streamFn).toBeTypeOf("function");
|
||||
if (!streamFn) {
|
||||
throw new Error("expected wrapped streamFn to be defined");
|
||||
}
|
||||
return streamFn;
|
||||
}
|
||||
|
||||
describe("composeProviderStreamWrappers", () => {
|
||||
it("applies wrappers left to right", async () => {
|
||||
const order: string[] = [];
|
||||
@@ -51,19 +69,17 @@ describe("buildProviderStreamFamilyHooks", () => {
|
||||
>;
|
||||
options?.onPayload?.(payload as never, model as never);
|
||||
capturedPayload = payload;
|
||||
capturedHeaders = options?.headers as Record<string, string> | undefined;
|
||||
capturedHeaders = options?.headers;
|
||||
return {} as never;
|
||||
};
|
||||
|
||||
const googleHooks = buildProviderStreamFamilyHooks("google-thinking");
|
||||
googleHooks.wrapStreamFn?.({
|
||||
streamFn: baseStreamFn,
|
||||
thinkingLevel: "high",
|
||||
} as never)(
|
||||
{ api: "google-generative-ai", id: "gemini-3.1-pro-preview" } as never,
|
||||
{} as never,
|
||||
{},
|
||||
);
|
||||
void requireStreamFn(
|
||||
requireWrapStreamFn(googleHooks.wrapStreamFn)({
|
||||
streamFn: baseStreamFn,
|
||||
thinkingLevel: "high",
|
||||
} as never),
|
||||
)({ api: "google-generative-ai", id: "gemini-3.1-pro-preview" } as never, {} as never, {});
|
||||
expect(capturedPayload).toMatchObject({
|
||||
config: { thinkingConfig: { thinkingLevel: "HIGH" } },
|
||||
});
|
||||
@@ -73,10 +89,12 @@ describe("buildProviderStreamFamilyHooks", () => {
|
||||
expect(googleThinkingConfig).not.toHaveProperty("thinkingBudget");
|
||||
|
||||
const minimaxHooks = buildProviderStreamFamilyHooks("minimax-fast-mode");
|
||||
minimaxHooks.wrapStreamFn?.({
|
||||
streamFn: baseStreamFn,
|
||||
extraParams: { fastMode: true },
|
||||
} as never)(
|
||||
void requireStreamFn(
|
||||
requireWrapStreamFn(minimaxHooks.wrapStreamFn)({
|
||||
streamFn: baseStreamFn,
|
||||
extraParams: { fastMode: true },
|
||||
} as never),
|
||||
)(
|
||||
{
|
||||
api: "anthropic-messages",
|
||||
provider: "minimax",
|
||||
@@ -88,26 +106,26 @@ describe("buildProviderStreamFamilyHooks", () => {
|
||||
expect(capturedModelId).toBe("MiniMax-M2.7-highspeed");
|
||||
|
||||
const moonshotHooks = buildProviderStreamFamilyHooks("moonshot-thinking");
|
||||
moonshotHooks.wrapStreamFn?.({
|
||||
streamFn: baseStreamFn,
|
||||
thinkingLevel: "off",
|
||||
} as never)(
|
||||
{ api: "openai-completions", id: "kimi-k2.5" } as never,
|
||||
{} as never,
|
||||
{},
|
||||
);
|
||||
void requireStreamFn(
|
||||
requireWrapStreamFn(moonshotHooks.wrapStreamFn)({
|
||||
streamFn: baseStreamFn,
|
||||
thinkingLevel: "off",
|
||||
} as never),
|
||||
)({ api: "openai-completions", id: "kimi-k2.5" } as never, {} as never, {});
|
||||
expect(capturedPayload).toMatchObject({
|
||||
config: { thinkingConfig: { thinkingBudget: -1 } },
|
||||
thinking: { type: "disabled" },
|
||||
});
|
||||
|
||||
const openAiHooks = buildProviderStreamFamilyHooks("openai-responses-defaults");
|
||||
openAiHooks.wrapStreamFn?.({
|
||||
streamFn: baseStreamFn,
|
||||
extraParams: { serviceTier: "flex" },
|
||||
config: {},
|
||||
agentDir: "/tmp/provider-stream-test",
|
||||
} as never)(
|
||||
void requireStreamFn(
|
||||
requireWrapStreamFn(openAiHooks.wrapStreamFn)({
|
||||
streamFn: baseStreamFn,
|
||||
extraParams: { serviceTier: "flex" },
|
||||
config: {},
|
||||
agentDir: "/tmp/provider-stream-test",
|
||||
} as never),
|
||||
)(
|
||||
{
|
||||
api: "openai-responses",
|
||||
provider: "openai",
|
||||
@@ -124,40 +142,48 @@ describe("buildProviderStreamFamilyHooks", () => {
|
||||
expect(capturedHeaders).toBeDefined();
|
||||
|
||||
const openRouterHooks = buildProviderStreamFamilyHooks("openrouter-thinking");
|
||||
openRouterHooks.wrapStreamFn?.({
|
||||
streamFn: baseStreamFn,
|
||||
thinkingLevel: "high",
|
||||
modelId: "openai/gpt-5.4",
|
||||
} as never)({ provider: "openrouter", id: "openai/gpt-5.4" } as never, {} as never, {});
|
||||
void requireStreamFn(
|
||||
requireWrapStreamFn(openRouterHooks.wrapStreamFn)({
|
||||
streamFn: baseStreamFn,
|
||||
thinkingLevel: "high",
|
||||
modelId: "openai/gpt-5.4",
|
||||
} as never),
|
||||
)({ provider: "openrouter", id: "openai/gpt-5.4" } as never, {} as never, {});
|
||||
expect(capturedPayload).toMatchObject({
|
||||
config: { thinkingConfig: { thinkingBudget: -1 } },
|
||||
reasoning: { effort: "high" },
|
||||
});
|
||||
|
||||
openRouterHooks.wrapStreamFn?.({
|
||||
streamFn: baseStreamFn,
|
||||
thinkingLevel: "high",
|
||||
modelId: "x-ai/grok-3",
|
||||
} as never)({ provider: "openrouter", id: "x-ai/grok-3" } as never, {} as never, {});
|
||||
void requireStreamFn(
|
||||
requireWrapStreamFn(openRouterHooks.wrapStreamFn)({
|
||||
streamFn: baseStreamFn,
|
||||
thinkingLevel: "high",
|
||||
modelId: "x-ai/grok-3",
|
||||
} as never),
|
||||
)({ provider: "openrouter", id: "x-ai/grok-3" } as never, {} as never, {});
|
||||
expect(capturedPayload).toMatchObject({
|
||||
config: { thinkingConfig: { thinkingBudget: -1 } },
|
||||
});
|
||||
expect(capturedPayload).not.toHaveProperty("reasoning");
|
||||
|
||||
const toolStreamHooks = buildProviderStreamFamilyHooks("tool-stream-default-on");
|
||||
toolStreamHooks.wrapStreamFn?.({
|
||||
streamFn: baseStreamFn,
|
||||
extraParams: {},
|
||||
} as never)({ id: "glm-4.7" } as never, {} as never, {});
|
||||
void requireStreamFn(
|
||||
requireWrapStreamFn(toolStreamHooks.wrapStreamFn)({
|
||||
streamFn: baseStreamFn,
|
||||
extraParams: {},
|
||||
} as never),
|
||||
)({ id: "glm-4.7" } as never, {} as never, {});
|
||||
expect(capturedPayload).toMatchObject({
|
||||
config: { thinkingConfig: { thinkingBudget: -1 } },
|
||||
tool_stream: true,
|
||||
});
|
||||
|
||||
toolStreamHooks.wrapStreamFn?.({
|
||||
streamFn: baseStreamFn,
|
||||
extraParams: { tool_stream: false },
|
||||
} as never)({ id: "glm-4.7" } as never, {} as never, {});
|
||||
void requireStreamFn(
|
||||
requireWrapStreamFn(toolStreamHooks.wrapStreamFn)({
|
||||
streamFn: baseStreamFn,
|
||||
extraParams: { tool_stream: false },
|
||||
} as never),
|
||||
)({ id: "glm-4.7" } as never, {} as never, {});
|
||||
expect(capturedPayload).toMatchObject({
|
||||
config: { thinkingConfig: { thinkingBudget: -1 } },
|
||||
});
|
||||
|
||||
@@ -26,15 +26,106 @@ type TelegramCommandConfigContract = {
|
||||
};
|
||||
};
|
||||
|
||||
const FALLBACK_TELEGRAM_COMMAND_NAME_PATTERN = /^[a-z0-9_]{1,32}$/;
|
||||
|
||||
function fallbackNormalizeTelegramCommandName(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
const withoutSlash = trimmed.startsWith("/") ? trimmed.slice(1) : trimmed;
|
||||
return withoutSlash.trim().toLowerCase().replace(/-/g, "_");
|
||||
}
|
||||
|
||||
function fallbackNormalizeTelegramCommandDescription(value: string): string {
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function fallbackResolveTelegramCustomCommands(params: {
|
||||
commands?: TelegramCustomCommandInput[] | null;
|
||||
reservedCommands?: Set<string>;
|
||||
checkReserved?: boolean;
|
||||
checkDuplicates?: boolean;
|
||||
}): {
|
||||
commands: Array<{ command: string; description: string }>;
|
||||
issues: TelegramCustomCommandIssue[];
|
||||
} {
|
||||
const entries = Array.isArray(params.commands) ? params.commands : [];
|
||||
const reserved = params.reservedCommands ?? new Set<string>();
|
||||
const checkReserved = params.checkReserved !== false;
|
||||
const checkDuplicates = params.checkDuplicates !== false;
|
||||
const seen = new Set<string>();
|
||||
const resolved: Array<{ command: string; description: string }> = [];
|
||||
const issues: TelegramCustomCommandIssue[] = [];
|
||||
|
||||
for (let index = 0; index < entries.length; index += 1) {
|
||||
const entry = entries[index];
|
||||
const normalized = fallbackNormalizeTelegramCommandName(String(entry?.command ?? ""));
|
||||
if (!normalized) {
|
||||
issues.push({
|
||||
index,
|
||||
field: "command",
|
||||
message: "Telegram custom command is missing a command name.",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (!FALLBACK_TELEGRAM_COMMAND_NAME_PATTERN.test(normalized)) {
|
||||
issues.push({
|
||||
index,
|
||||
field: "command",
|
||||
message: `Telegram custom command "/${normalized}" is invalid (use a-z, 0-9, underscore; max 32 chars).`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (checkReserved && reserved.has(normalized)) {
|
||||
issues.push({
|
||||
index,
|
||||
field: "command",
|
||||
message: `Telegram custom command "/${normalized}" conflicts with a native command.`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (checkDuplicates && seen.has(normalized)) {
|
||||
issues.push({
|
||||
index,
|
||||
field: "command",
|
||||
message: `Telegram custom command "/${normalized}" is duplicated.`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const description = fallbackNormalizeTelegramCommandDescription(
|
||||
String(entry?.description ?? ""),
|
||||
);
|
||||
if (!description) {
|
||||
issues.push({
|
||||
index,
|
||||
field: "description",
|
||||
message: `Telegram custom command "/${normalized}" is missing a description.`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (checkDuplicates) {
|
||||
seen.add(normalized);
|
||||
}
|
||||
resolved.push({ command: normalized, description });
|
||||
}
|
||||
|
||||
return { commands: resolved, issues };
|
||||
}
|
||||
|
||||
const FALLBACK_TELEGRAM_COMMAND_CONFIG_CONTRACT: TelegramCommandConfigContract = {
|
||||
TELEGRAM_COMMAND_NAME_PATTERN: FALLBACK_TELEGRAM_COMMAND_NAME_PATTERN,
|
||||
normalizeTelegramCommandName: fallbackNormalizeTelegramCommandName,
|
||||
normalizeTelegramCommandDescription: fallbackNormalizeTelegramCommandDescription,
|
||||
resolveTelegramCustomCommands: fallbackResolveTelegramCustomCommands,
|
||||
};
|
||||
|
||||
function loadTelegramCommandConfigContract(): TelegramCommandConfigContract {
|
||||
const contract = getBundledChannelContractSurfaceModule<TelegramCommandConfigContract>({
|
||||
pluginId: "telegram",
|
||||
preferredBasename: "contract-surfaces.ts",
|
||||
});
|
||||
if (!contract) {
|
||||
throw new Error("telegram command config contract surface is unavailable");
|
||||
}
|
||||
return contract;
|
||||
return contract ?? FALLBACK_TELEGRAM_COMMAND_CONFIG_CONTRACT;
|
||||
}
|
||||
|
||||
export const TELEGRAM_COMMAND_NAME_PATTERN =
|
||||
|
||||
Reference in New Issue
Block a user