mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-29 14:45:18 +00:00
* fix(kilocode): normalize string stop param to array in stream wrapper * fix: move kilocode stop normalization into extension * fix: keep kilocode stream wrapper plugin-local * fix: normalize kilocode stop after extra body * fix(qa-lab): preserve WhatsApp RTT source literal --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
147 lines
4.9 KiB
TypeScript
147 lines
4.9 KiB
TypeScript
import type { ProviderWrapStreamFnContext } from "openclaw/plugin-sdk/plugin-entry";
|
|
import { resolveProviderRequestHeaders } from "openclaw/plugin-sdk/provider-http";
|
|
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
|
|
const KILOCODE_FEATURE_HEADER = "X-KILOCODE-FEATURE";
|
|
const KILOCODE_FEATURE_DEFAULT = "openclaw";
|
|
const KILOCODE_FEATURE_ENV_VAR = "KILOCODE_FEATURE";
|
|
|
|
type ThinkLevel = NonNullable<ProviderWrapStreamFnContext["thinkingLevel"]>;
|
|
type ProviderStreamFn = NonNullable<ProviderWrapStreamFnContext["streamFn"]>;
|
|
type ReasoningEffort = "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
|
|
function resolveKilocodeAppHeaders(): Record<string, string> {
|
|
const feature = process.env[KILOCODE_FEATURE_ENV_VAR]?.trim() || KILOCODE_FEATURE_DEFAULT;
|
|
return { [KILOCODE_FEATURE_HEADER]: feature };
|
|
}
|
|
|
|
function mapThinkingLevelToReasoningEffort(thinkingLevel: ThinkLevel): ReasoningEffort {
|
|
if (thinkingLevel === "off") {
|
|
return "none";
|
|
}
|
|
if (thinkingLevel === "adaptive") {
|
|
return "medium";
|
|
}
|
|
if (thinkingLevel === "max") {
|
|
return "xhigh";
|
|
}
|
|
return thinkingLevel;
|
|
}
|
|
|
|
function normalizeKilocodeReasoningPayload(
|
|
payloadObj: Record<string, unknown>,
|
|
thinkingLevel?: ThinkLevel,
|
|
): void {
|
|
delete payloadObj.reasoning_effort;
|
|
if (!thinkingLevel || thinkingLevel === "off") {
|
|
return;
|
|
}
|
|
|
|
const existingReasoning = payloadObj.reasoning;
|
|
if (
|
|
existingReasoning &&
|
|
typeof existingReasoning === "object" &&
|
|
!Array.isArray(existingReasoning)
|
|
) {
|
|
const reasoningObj = existingReasoning as Record<string, unknown>;
|
|
if (!("max_tokens" in reasoningObj) && !("effort" in reasoningObj)) {
|
|
reasoningObj.effort = mapThinkingLevelToReasoningEffort(thinkingLevel);
|
|
}
|
|
} else if (!existingReasoning) {
|
|
payloadObj.reasoning = {
|
|
effort: mapThinkingLevelToReasoningEffort(thinkingLevel),
|
|
};
|
|
}
|
|
}
|
|
|
|
function normalizeKilocodeStopPayload(payloadObj: Record<string, unknown>): void {
|
|
if (typeof payloadObj.stop === "string") {
|
|
payloadObj.stop = [payloadObj.stop];
|
|
}
|
|
}
|
|
|
|
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
? (value as Record<string, unknown>)
|
|
: undefined;
|
|
}
|
|
|
|
function normalizeKilocodeStopAfterCaller(
|
|
value: unknown,
|
|
fallbackPayload: Record<string, unknown> | undefined,
|
|
): unknown {
|
|
const replacementPayload = asRecord(value);
|
|
if (replacementPayload) {
|
|
normalizeKilocodeStopPayload(replacementPayload);
|
|
return value;
|
|
}
|
|
if (fallbackPayload) {
|
|
normalizeKilocodeStopPayload(fallbackPayload);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function isProxyReasoningUnsupported(modelId: string): boolean {
|
|
const trimmed = normalizeOptionalLowercaseString(modelId);
|
|
const slashIndex = trimmed?.indexOf("/") ?? -1;
|
|
return slashIndex > 0 && trimmed?.slice(0, slashIndex) === "x-ai";
|
|
}
|
|
|
|
function resolveKilocodeThinkingLevel(ctx: ProviderWrapStreamFnContext): ThinkLevel | undefined {
|
|
if (ctx.modelId === "kilo/auto" || isProxyReasoningUnsupported(ctx.modelId)) {
|
|
return undefined;
|
|
}
|
|
return ctx.thinkingLevel;
|
|
}
|
|
|
|
export function createKilocodeStreamWrapper(
|
|
baseStreamFn: ProviderWrapStreamFnContext["streamFn"],
|
|
thinkingLevel?: ThinkLevel,
|
|
): ProviderWrapStreamFnContext["streamFn"] {
|
|
if (!baseStreamFn) {
|
|
return undefined;
|
|
}
|
|
const underlying = baseStreamFn;
|
|
return (model, context, options) => {
|
|
const originalOnPayload = options?.onPayload;
|
|
const headers = resolveProviderRequestHeaders({
|
|
provider: typeof model.provider === "string" ? model.provider : "kilocode",
|
|
api: model.api,
|
|
baseUrl: typeof model.baseUrl === "string" ? model.baseUrl : undefined,
|
|
capability: "llm",
|
|
transport: "stream",
|
|
callerHeaders: options?.headers,
|
|
defaultHeaders: resolveKilocodeAppHeaders(),
|
|
precedence: "defaults-win",
|
|
});
|
|
return underlying(model, context, {
|
|
...options,
|
|
headers,
|
|
onPayload(payload, payloadModel) {
|
|
const payloadObj = asRecord(payload);
|
|
if (payloadObj) {
|
|
// Keep Kilo thinking defaults overrideable by later caller/config payload hooks.
|
|
normalizeKilocodeReasoningPayload(payloadObj, thinkingLevel);
|
|
}
|
|
|
|
const result = originalOnPayload?.(payload, payloadModel);
|
|
if (result && typeof (result as Promise<unknown>).then === "function") {
|
|
return Promise.resolve(result).then((resolved) =>
|
|
normalizeKilocodeStopAfterCaller(resolved, payloadObj),
|
|
);
|
|
}
|
|
return normalizeKilocodeStopAfterCaller(result, payloadObj);
|
|
},
|
|
});
|
|
};
|
|
}
|
|
|
|
export function wrapKilocodeProviderStream(
|
|
ctx: ProviderWrapStreamFnContext,
|
|
): ProviderStreamFn | undefined {
|
|
if (normalizeOptionalLowercaseString(ctx.provider) !== "kilocode") {
|
|
return undefined;
|
|
}
|
|
return createKilocodeStreamWrapper(ctx.streamFn, resolveKilocodeThinkingLevel(ctx));
|
|
}
|