* Agents: add provider attribution registry * Agents: record provider attribution matrix * Agents: align OpenRouter attribution headers
143 lines
4.7 KiB
TypeScript
143 lines
4.7 KiB
TypeScript
import type { StreamFn } from "@mariozechner/pi-agent-core";
|
|
import { streamSimple } from "@mariozechner/pi-ai";
|
|
import type { ThinkLevel } from "../../auto-reply/thinking.js";
|
|
import { resolveProviderAttributionHeaders } from "../provider-attribution.js";
|
|
const KILOCODE_FEATURE_HEADER = "X-KILOCODE-FEATURE";
|
|
const KILOCODE_FEATURE_DEFAULT = "openclaw";
|
|
const KILOCODE_FEATURE_ENV_VAR = "KILOCODE_FEATURE";
|
|
|
|
function resolveKilocodeAppHeaders(): Record<string, string> {
|
|
const feature = process.env[KILOCODE_FEATURE_ENV_VAR]?.trim() || KILOCODE_FEATURE_DEFAULT;
|
|
return { [KILOCODE_FEATURE_HEADER]: feature };
|
|
}
|
|
|
|
function isOpenRouterAnthropicModel(provider: string, modelId: string): boolean {
|
|
return provider.toLowerCase() === "openrouter" && modelId.toLowerCase().startsWith("anthropic/");
|
|
}
|
|
|
|
function mapThinkingLevelToOpenRouterReasoningEffort(
|
|
thinkingLevel: ThinkLevel,
|
|
): "none" | "minimal" | "low" | "medium" | "high" | "xhigh" {
|
|
if (thinkingLevel === "off") {
|
|
return "none";
|
|
}
|
|
if (thinkingLevel === "adaptive") {
|
|
return "medium";
|
|
}
|
|
return thinkingLevel;
|
|
}
|
|
|
|
function normalizeProxyReasoningPayload(payload: unknown, thinkingLevel?: ThinkLevel): void {
|
|
if (!payload || typeof payload !== "object") {
|
|
return;
|
|
}
|
|
|
|
const payloadObj = payload as Record<string, unknown>;
|
|
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 = mapThinkingLevelToOpenRouterReasoningEffort(thinkingLevel);
|
|
}
|
|
} else if (!existingReasoning) {
|
|
payloadObj.reasoning = {
|
|
effort: mapThinkingLevelToOpenRouterReasoningEffort(thinkingLevel),
|
|
};
|
|
}
|
|
}
|
|
|
|
export function createOpenRouterSystemCacheWrapper(baseStreamFn: StreamFn | undefined): StreamFn {
|
|
const underlying = baseStreamFn ?? streamSimple;
|
|
return (model, context, options) => {
|
|
if (
|
|
typeof model.provider !== "string" ||
|
|
typeof model.id !== "string" ||
|
|
!isOpenRouterAnthropicModel(model.provider, model.id)
|
|
) {
|
|
return underlying(model, context, options);
|
|
}
|
|
|
|
const originalOnPayload = options?.onPayload;
|
|
return underlying(model, context, {
|
|
...options,
|
|
onPayload: (payload) => {
|
|
const messages = (payload as Record<string, unknown>)?.messages;
|
|
if (Array.isArray(messages)) {
|
|
for (const msg of messages as Array<{ role?: string; content?: unknown }>) {
|
|
if (msg.role !== "system" && msg.role !== "developer") {
|
|
continue;
|
|
}
|
|
if (typeof msg.content === "string") {
|
|
msg.content = [
|
|
{ type: "text", text: msg.content, cache_control: { type: "ephemeral" } },
|
|
];
|
|
} else if (Array.isArray(msg.content) && msg.content.length > 0) {
|
|
const last = msg.content[msg.content.length - 1];
|
|
if (last && typeof last === "object") {
|
|
(last as Record<string, unknown>).cache_control = { type: "ephemeral" };
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return originalOnPayload?.(payload, model);
|
|
},
|
|
});
|
|
};
|
|
}
|
|
|
|
export function createOpenRouterWrapper(
|
|
baseStreamFn: StreamFn | undefined,
|
|
thinkingLevel?: ThinkLevel,
|
|
): StreamFn {
|
|
const underlying = baseStreamFn ?? streamSimple;
|
|
return (model, context, options) => {
|
|
const onPayload = options?.onPayload;
|
|
const attributionHeaders = resolveProviderAttributionHeaders("openrouter");
|
|
return underlying(model, context, {
|
|
...options,
|
|
headers: {
|
|
...attributionHeaders,
|
|
...options?.headers,
|
|
},
|
|
onPayload: (payload) => {
|
|
normalizeProxyReasoningPayload(payload, thinkingLevel);
|
|
return onPayload?.(payload, model);
|
|
},
|
|
});
|
|
};
|
|
}
|
|
|
|
export function isProxyReasoningUnsupported(modelId: string): boolean {
|
|
return modelId.toLowerCase().startsWith("x-ai/");
|
|
}
|
|
|
|
export function createKilocodeWrapper(
|
|
baseStreamFn: StreamFn | undefined,
|
|
thinkingLevel?: ThinkLevel,
|
|
): StreamFn {
|
|
const underlying = baseStreamFn ?? streamSimple;
|
|
return (model, context, options) => {
|
|
const onPayload = options?.onPayload;
|
|
return underlying(model, context, {
|
|
...options,
|
|
headers: {
|
|
...options?.headers,
|
|
...resolveKilocodeAppHeaders(),
|
|
},
|
|
onPayload: (payload) => {
|
|
normalizeProxyReasoningPayload(payload, thinkingLevel);
|
|
return onPayload?.(payload, model);
|
|
},
|
|
});
|
|
};
|
|
}
|