mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 12:41:37 +00:00
Channels: add thread-aware model overrides
This commit is contained in:
@@ -136,6 +136,7 @@ export const handleStatusCommand: CommandHandler = async (params, allowTextComma
|
||||
command: params.command,
|
||||
sessionEntry: params.sessionEntry,
|
||||
sessionKey: params.sessionKey,
|
||||
parentSessionKey: params.ctx.ParentSessionKey,
|
||||
sessionScope: params.sessionScope,
|
||||
provider: params.provider,
|
||||
model: params.model,
|
||||
|
||||
@@ -32,6 +32,7 @@ export async function buildStatusReply(params: {
|
||||
command: CommandContext;
|
||||
sessionEntry?: SessionEntry;
|
||||
sessionKey: string;
|
||||
parentSessionKey?: string;
|
||||
sessionScope?: SessionScope;
|
||||
storePath?: string;
|
||||
provider: string;
|
||||
@@ -51,6 +52,7 @@ export async function buildStatusReply(params: {
|
||||
command,
|
||||
sessionEntry,
|
||||
sessionKey,
|
||||
parentSessionKey,
|
||||
sessionScope,
|
||||
storePath,
|
||||
provider,
|
||||
@@ -173,6 +175,7 @@ export async function buildStatusReply(params: {
|
||||
agentId: statusAgentId,
|
||||
sessionEntry,
|
||||
sessionKey,
|
||||
parentSessionKey,
|
||||
sessionScope,
|
||||
sessionStorePath: storePath,
|
||||
groupActivation,
|
||||
|
||||
@@ -168,6 +168,7 @@ export async function applyInlineDirectiveOverrides(params: {
|
||||
command,
|
||||
sessionEntry,
|
||||
sessionKey,
|
||||
parentSessionKey: ctx.ParentSessionKey,
|
||||
sessionScope,
|
||||
provider,
|
||||
model,
|
||||
|
||||
@@ -277,6 +277,7 @@ export async function handleInlineActions(params: {
|
||||
command,
|
||||
sessionEntry,
|
||||
sessionKey,
|
||||
parentSessionKey: ctx.ParentSessionKey,
|
||||
sessionScope,
|
||||
provider,
|
||||
model,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import { resolveModelRefFromString } from "../../agents/model-selection.js";
|
||||
import { resolveAgentTimeoutMs } from "../../agents/timeout.js";
|
||||
import { DEFAULT_AGENT_WORKSPACE_DIR, ensureAgentWorkspace } from "../../agents/workspace.js";
|
||||
import { resolveChannelModelOverride } from "../../channels/model-overrides.js";
|
||||
import { type OpenClawConfig, loadConfig } from "../../config/config.js";
|
||||
import { applyLinkUnderstanding } from "../../link-understanding/apply.js";
|
||||
import { applyMediaUnderstanding } from "../../media-understanding/apply.js";
|
||||
@@ -179,6 +180,36 @@ export async function getReplyFromConfig(
|
||||
aliasIndex,
|
||||
});
|
||||
|
||||
const channelModelOverride = resolveChannelModelOverride({
|
||||
cfg,
|
||||
channel:
|
||||
groupResolution?.channel ??
|
||||
sessionEntry.channel ??
|
||||
sessionEntry.origin?.provider ??
|
||||
(typeof finalized.OriginatingChannel === "string"
|
||||
? finalized.OriginatingChannel
|
||||
: undefined) ??
|
||||
finalized.Provider,
|
||||
groupId: groupResolution?.id ?? sessionEntry.groupId,
|
||||
groupChannel: sessionEntry.groupChannel ?? sessionCtx.GroupChannel ?? finalized.GroupChannel,
|
||||
groupSubject: sessionEntry.subject ?? sessionCtx.GroupSubject ?? finalized.GroupSubject,
|
||||
parentSessionKey: sessionCtx.ParentSessionKey,
|
||||
});
|
||||
const hasSessionModelOverride = Boolean(
|
||||
sessionEntry.modelOverride?.trim() || sessionEntry.providerOverride?.trim(),
|
||||
);
|
||||
if (!hasResolvedHeartbeatModelOverride && !hasSessionModelOverride && channelModelOverride) {
|
||||
const resolved = resolveModelRefFromString({
|
||||
raw: channelModelOverride.model,
|
||||
defaultProvider,
|
||||
aliasIndex,
|
||||
});
|
||||
if (resolved) {
|
||||
provider = resolved.ref.provider;
|
||||
model = resolved.ref.model;
|
||||
}
|
||||
}
|
||||
|
||||
const directiveResult = await resolveReplyDirectives({
|
||||
ctx: finalized,
|
||||
cfg,
|
||||
|
||||
@@ -71,36 +71,6 @@ describe("buildInboundMetaSystemPrompt", () => {
|
||||
const payload = parseInboundMetaPayload(prompt);
|
||||
expect(payload["sender_id"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("includes discord channel topics only for new sessions", () => {
|
||||
const prompt = buildInboundMetaSystemPrompt({
|
||||
OriginatingTo: "discord:channel:123",
|
||||
OriginatingChannel: "discord",
|
||||
Provider: "discord",
|
||||
Surface: "discord",
|
||||
ChatType: "group",
|
||||
ChannelTopic: " Shipping updates ",
|
||||
IsNewSession: "true",
|
||||
} as TemplateContext);
|
||||
|
||||
const payload = parseInboundMetaPayload(prompt);
|
||||
expect(payload["channel_topic"]).toBe("Shipping updates");
|
||||
});
|
||||
|
||||
it("omits discord channel topics for existing sessions", () => {
|
||||
const prompt = buildInboundMetaSystemPrompt({
|
||||
OriginatingTo: "discord:channel:123",
|
||||
OriginatingChannel: "discord",
|
||||
Provider: "discord",
|
||||
Surface: "discord",
|
||||
ChatType: "group",
|
||||
ChannelTopic: "Shipping updates",
|
||||
IsNewSession: "false",
|
||||
} as TemplateContext);
|
||||
|
||||
const payload = parseInboundMetaPayload(prompt);
|
||||
expect(payload["channel_topic"]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildInboundUserContextPrefix", () => {
|
||||
|
||||
@@ -13,15 +13,8 @@ function safeTrim(value: unknown): string | undefined {
|
||||
export function buildInboundMetaSystemPrompt(ctx: TemplateContext): string {
|
||||
const chatType = normalizeChatType(ctx.ChatType);
|
||||
const isDirect = !chatType || chatType === "direct";
|
||||
const isNewSession = ctx.IsNewSession === "true";
|
||||
const originatingChannel = safeTrim(ctx.OriginatingChannel);
|
||||
const surface = safeTrim(ctx.Surface);
|
||||
const provider = safeTrim(ctx.Provider);
|
||||
const isDiscord =
|
||||
provider === "discord" || surface === "discord" || originatingChannel === "discord";
|
||||
|
||||
// Keep system metadata strictly free of attacker-controlled strings (sender names, group subjects, etc.)
|
||||
// unless explicitly opted into for new-session context (e.g. Discord channel topics).
|
||||
// Keep system metadata strictly free of attacker-controlled strings (sender names, group subjects, etc.).
|
||||
// Those belong in the user-role "untrusted context" blocks.
|
||||
// Per-message identifiers (message_id, reply_to_id, sender_id) are also excluded here: they change
|
||||
// on every turn and would bust prefix-based prompt caches on local model providers. They are
|
||||
@@ -30,27 +23,25 @@ export function buildInboundMetaSystemPrompt(ctx: TemplateContext): string {
|
||||
// Resolve channel identity: prefer explicit channel, then surface, then provider.
|
||||
// For webchat/Hub Chat sessions (when Surface is 'webchat' or undefined with no real channel),
|
||||
// omit the channel field entirely rather than falling back to an unrelated provider.
|
||||
let channelValue = originatingChannel ?? surface;
|
||||
let channelValue = safeTrim(ctx.OriginatingChannel) ?? safeTrim(ctx.Surface);
|
||||
if (!channelValue) {
|
||||
// Only fall back to Provider if it represents a real messaging channel.
|
||||
// For webchat/internal sessions, ctx.Provider may be unrelated (e.g., the user's configured
|
||||
// default channel), so skip it to avoid incorrect runtime labels like "channel=whatsapp".
|
||||
const provider = safeTrim(ctx.Provider);
|
||||
// Check if provider is "webchat" or if we're in an internal/webchat context
|
||||
if (provider !== "webchat" && surface !== "webchat") {
|
||||
if (provider !== "webchat" && ctx.Surface !== "webchat") {
|
||||
channelValue = provider;
|
||||
}
|
||||
// Otherwise leave channelValue undefined (no channel label)
|
||||
}
|
||||
|
||||
const channelTopic = isNewSession && isDiscord ? safeTrim(ctx.ChannelTopic) : undefined;
|
||||
|
||||
const payload = {
|
||||
schema: "openclaw.inbound_meta.v1",
|
||||
chat_id: safeTrim(ctx.OriginatingTo),
|
||||
channel: channelValue,
|
||||
channel_topic: channelTopic,
|
||||
provider,
|
||||
surface,
|
||||
provider: safeTrim(ctx.Provider),
|
||||
surface: safeTrim(ctx.Surface),
|
||||
chat_type: chatType ?? (isDirect ? "direct" : undefined),
|
||||
flags: {
|
||||
is_group_chat: !isDirect ? true : undefined,
|
||||
|
||||
Reference in New Issue
Block a user