Skip to content

Commit 9119492

Browse files
authored
fix: preserve plugin LLM command auth (#85936)
Merged via squash. Prepared head SHA: e61c724 Co-authored-by: jalehman <[email protected]> Reviewed-by: @jalehman
1 parent f7a39f4 commit 9119492

27 files changed

Lines changed: 606 additions & 49 deletions

extensions/discord/src/monitor/native-command.plugin-dispatch.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
setActivePluginRegistry,
1414
} from "openclaw/plugin-sdk/plugin-test-runtime";
1515
import { dispatchReplyWithDispatcher } from "openclaw/plugin-sdk/reply-dispatch-runtime";
16+
import { getSessionEntry } from "openclaw/plugin-sdk/session-store-runtime";
1617
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
1718
import { defineThrowingDiscordChannelGetter } from "../test-support/partial-channel.js";
1819
import { resolveDiscordNativeInteractionRouteState } from "./native-command-route.js";
@@ -29,6 +30,7 @@ const runtimeModuleMocks = vi.hoisted(() => ({
2930
executePluginCommand: vi.fn(),
3031
dispatchReplyWithDispatcher: vi.fn(),
3132
resolveDirectStatusReplyForSession: vi.fn(),
33+
getSessionEntry: vi.fn(),
3234
}));
3335

3436
function createConfig(): OpenClawConfig {
@@ -408,6 +410,7 @@ describe("Discord native plugin command dispatch", () => {
408410
discordNativeCommandTesting.setResolveDiscordNativeInteractionRouteState(
409411
resolveDiscordNativeInteractionRouteState,
410412
);
413+
discordNativeCommandTesting.setGetSessionEntry(getSessionEntry);
411414
});
412415

413416
beforeEach(() => {
@@ -430,6 +433,8 @@ describe("Discord native plugin command dispatch", () => {
430433
runtimeModuleMocks.resolveDirectStatusReplyForSession.mockResolvedValue({
431434
text: "status reply",
432435
});
436+
runtimeModuleMocks.getSessionEntry.mockReset();
437+
runtimeModuleMocks.getSessionEntry.mockReturnValue(undefined);
433438
discordNativeCommandTesting.setMatchPluginCommand(
434439
runtimeModuleMocks.matchPluginCommand as typeof import("openclaw/plugin-sdk/plugin-runtime").matchPluginCommand,
435440
);
@@ -450,6 +455,9 @@ describe("Discord native plugin command dispatch", () => {
450455
accountId: params.accountId,
451456
}),
452457
);
458+
discordNativeCommandTesting.setGetSessionEntry(
459+
runtimeModuleMocks.getSessionEntry as typeof import("openclaw/plugin-sdk/session-store-runtime").getSessionEntry,
460+
);
453461
});
454462

455463
it("executes plugin commands from the real registry through the native Discord command path", async () => {
@@ -476,6 +484,43 @@ describe("Discord native plugin command dispatch", () => {
476484
});
477485
});
478486

487+
it("passes the active auth profile to Discord plugin commands", async () => {
488+
const cfg = createConfig();
489+
const interaction = createInteraction();
490+
runtimeModuleMocks.getSessionEntry.mockReturnValue({
491+
sessionId: "discord-session",
492+
authProfileOverride: "openai-codex:[email protected]",
493+
updatedAt: Date.now(),
494+
});
495+
496+
registerPairPlugin();
497+
const command = await createPluginCommand({
498+
cfg,
499+
name: "pair",
500+
});
501+
const executeSpy = runtimeModuleMocks.executePluginCommand.mockResolvedValue({
502+
text: "paired:now",
503+
});
504+
505+
await (command as { run: (interaction: unknown) => Promise<void> }).run(
506+
Object.assign(interaction, {
507+
options: {
508+
getString: () => "now",
509+
getBoolean: () => null,
510+
getFocused: () => "",
511+
},
512+
}) as unknown,
513+
);
514+
515+
expectPluginCommandExecution({
516+
mock: executeSpy,
517+
commandName: "pair",
518+
expected: {
519+
authProfileId: "openai-codex:[email protected]",
520+
},
521+
});
522+
});
523+
479524
it("does not treat Discord DM allowlist users as scoped plugin command owners", async () => {
480525
const cfg = {
481526
channels: {

extensions/discord/src/monitor/native-command.runtime.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { resolveDirectStatusReplyForSession } from "openclaw/plugin-sdk/command-status-runtime";
22
import * as pluginRuntime from "openclaw/plugin-sdk/plugin-runtime";
33
import { dispatchReplyWithDispatcher } from "openclaw/plugin-sdk/reply-dispatch-runtime";
4+
import { getSessionEntry } from "openclaw/plugin-sdk/session-store-runtime";
45
import { resolveDiscordNativeInteractionRouteState } from "./native-command-route.js";
56

67
export const nativeCommandRuntime = {
@@ -9,6 +10,7 @@ export const nativeCommandRuntime = {
910
dispatchReplyWithDispatcher,
1011
resolveDirectStatusReplyForSession,
1112
resolveDiscordNativeInteractionRouteState,
13+
getSessionEntry,
1214
};
1315

1416
export const testing = {
@@ -47,5 +49,10 @@ export const testing = {
4749
nativeCommandRuntime.resolveDiscordNativeInteractionRouteState = next;
4850
return previous;
4951
},
52+
setGetSessionEntry(next: typeof getSessionEntry): typeof getSessionEntry {
53+
const previous = nativeCommandRuntime.getSessionEntry;
54+
nativeCommandRuntime.getSessionEntry = next;
55+
return previous;
56+
},
5057
};
5158
export { testing as __testing };

extensions/discord/src/monitor/native-command.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,10 @@ async function dispatchDiscordCommandInteraction(params: {
550550
const messageThreadId = !isDirectMessage && isThreadChannel ? channelId : undefined;
551551
const pluginThreadParentId = !isDirectMessage && isThreadChannel ? threadParentId : undefined;
552552
const { effectiveRoute } = await getNativeRouteState();
553+
const targetSessionEntry = nativeCommandRuntime.getSessionEntry({
554+
agentId: effectiveRoute.agentId,
555+
sessionKey: effectiveRoute.sessionKey,
556+
});
553557
const pluginReply = await nativeCommandRuntime.executePluginCommand({
554558
command: pluginMatch.command,
555559
args: pluginMatch.args,
@@ -559,6 +563,7 @@ async function dispatchDiscordCommandInteraction(params: {
559563
isAuthorizedSender: commandAuthorized,
560564
senderIsOwner: senderIsCommandOwner,
561565
sessionKey: effectiveRoute.sessionKey,
566+
authProfileId: targetSessionEntry?.authProfileOverride,
562567
commandBody: prompt,
563568
config: cfg,
564569
from: isDirectMessage

extensions/telegram/src/bot-native-commands.runtime.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ export {
1313
resolveChunkMode,
1414
} from "openclaw/plugin-sdk/reply-dispatch-runtime";
1515
export { resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing";
16+
export { getSessionEntry } from "openclaw/plugin-sdk/session-store-runtime";

extensions/telegram/src/bot-native-commands.session-meta.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1287,6 +1287,7 @@ describe("registerTelegramNativeCommands — session metadata", () => {
12871287
sessionMocks.resolveStorePath.mockReturnValue("/tmp/openclaw-sessions/sessions.json");
12881288
sessionMocks.loadSessionStore.mockReturnValue({
12891289
"agent:main:telegram:group:-1001234567890:topic:42": {
1290+
authProfileOverride: "openai-codex:[email protected]",
12901291
sessionId: "sess-topic",
12911292
updatedAt: 1,
12921293
},
@@ -1338,6 +1339,7 @@ describe("registerTelegramNativeCommands — session metadata", () => {
13381339
sessionKey: "agent:main:telegram:group:-1001234567890:topic:42",
13391340
sessionId: "sess-topic",
13401341
sessionFile: path.resolve("/tmp/openclaw-sessions", "sess-topic-topic-42.jsonl"),
1342+
authProfileId: "openai-codex:[email protected]",
13411343
messageThreadId: 42,
13421344
},
13431345
"plugin command params",

extensions/telegram/src/bot-native-commands.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ async function resolveTelegramCommandSessionFile(params: {
177177
agentId: string;
178178
sessionKey: string;
179179
threadId?: string | number;
180-
}): Promise<{ sessionId?: string; sessionFile?: string }> {
180+
}): Promise<{ sessionId?: string; sessionFile?: string; authProfileId?: string }> {
181181
const sessionKey = params.sessionKey.trim();
182182
if (!sessionKey) {
183183
return {};
@@ -187,6 +187,7 @@ async function resolveTelegramCommandSessionFile(params: {
187187
const store = loadSessionStore(storePath);
188188
const resolved = resolveSessionStoreEntry({ store, sessionKey });
189189
const sessionId = resolved.existing?.sessionId?.trim() || randomUUID();
190+
const authProfileId = normalizeOptionalString(resolved.existing?.authProfileOverride);
190191
const sessionsDir = path.dirname(storePath);
191192
const fallbackSessionFile = resolveSessionTranscriptPathInDir(
192193
sessionId,
@@ -203,7 +204,11 @@ async function resolveTelegramCommandSessionFile(params: {
203204
sessionsDir,
204205
fallbackSessionFile,
205206
});
206-
return { sessionId, sessionFile: persisted.sessionFile };
207+
return {
208+
sessionId,
209+
sessionFile: persisted.sessionFile,
210+
...(authProfileId ? { authProfileId } : {}),
211+
};
207212
} catch {
208213
return {};
209214
}
@@ -1389,6 +1394,10 @@ export const registerTelegramNativeCommands = ({
13891394
botHasTopicsEnabled: resolveTelegramBotHasTopicsEnabled(ctx.me),
13901395
resolveThreadSessionKeys: nativeCommandRuntime.resolveThreadSessionKeys,
13911396
});
1397+
const targetSessionEntry = nativeCommandRuntime.getSessionEntry({
1398+
agentId: route.agentId,
1399+
sessionKey: targetSessionKey,
1400+
});
13921401
const deliveryBaseOptions = buildCommandDeliveryBaseOptions({
13931402
cfg: runtimeCfg,
13941403
chatId,
@@ -1449,6 +1458,8 @@ export const registerTelegramNativeCommands = ({
14491458
sessionKey: targetSessionKey,
14501459
sessionId: sessionFileContext.sessionId,
14511460
sessionFile: sessionFileContext.sessionFile,
1461+
authProfileId:
1462+
sessionFileContext.authProfileId ?? targetSessionEntry?.authProfileOverride,
14521463
commandBody,
14531464
config: runtimeCfg,
14541465
from,

src/agents/openai-responses-payload-policy.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,4 +173,19 @@ describe("openai responses payload policy", () => {
173173
expect(policy.allowsServiceTier).toBe(true);
174174
expect(policy.shouldStripStore).toBe(false);
175175
});
176+
177+
it("emits store false for aliased native OpenAI Codex responses disable mode", () => {
178+
const policy = resolveOpenAIResponsesPayloadPolicy(
179+
{
180+
api: "openclaw-openai-responses-transport",
181+
provider: "openai-codex",
182+
baseUrl: "https://chatgpt.com/backend-api/codex",
183+
},
184+
{ storeMode: "disable" },
185+
);
186+
187+
expect(policy.explicitStore).toBe(false);
188+
expect(policy.allowsServiceTier).toBe(true);
189+
expect(policy.shouldStripStore).toBe(false);
190+
});
176191
});

src/agents/openai-responses-payload-policy.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ const OPENAI_RESPONSES_APIS = new Set([
6464
"openai-responses",
6565
"azure-openai-responses",
6666
"openai-codex-responses",
67+
"openclaw-openai-responses-transport",
6768
]);
6869
const OPENAI_RESPONSES_PROVIDERS = new Set(["openai", "azure-openai", "azure-openai-responses"]);
6970
const LOCAL_ENDPOINT_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
@@ -243,9 +244,13 @@ function resolveOpenAIResponsesPayloadCapabilities(
243244

244245
return {
245246
allowsOpenAIServiceTier:
246-
(provider === "openai" && api === "openai-responses" && endpointClass === "openai-public") ||
247+
(provider === "openai" &&
248+
(api === "openai-responses" || api === "openclaw-openai-responses-transport") &&
249+
endpointClass === "openai-public") ||
247250
(provider === "openai-codex" &&
248-
(api === "openai-codex-responses" || api === "openai-responses") &&
251+
(api === "openai-codex-responses" ||
252+
api === "openai-responses" ||
253+
api === "openclaw-openai-responses-transport") &&
249254
endpointClass === "openai-codex"),
250255
allowsResponsesStore:
251256
supportsResponsesStoreField &&

src/agents/openai-transport-stream.test.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createServer } from "node:http";
2-
import type { Model } from "@earendil-works/pi-ai";
2+
import type { Api, Model } from "@earendil-works/pi-ai";
33
import { describe, expect, it, vi } from "vitest";
44
import {
55
buildOpenAIResponsesParams,
@@ -2260,6 +2260,54 @@ describe("openai transport stream", () => {
22602260
expect(params).not.toHaveProperty("top_p");
22612261
});
22622262

2263+
it("keeps Codex response shaping when simple completions use the OpenClaw transport alias", () => {
2264+
const params = buildOpenAIResponsesParams(
2265+
{
2266+
id: "gpt-5.5",
2267+
name: "GPT-5.5",
2268+
api: "openclaw-openai-responses-transport" as Api,
2269+
provider: "openai-codex",
2270+
baseUrl: "https://chatgpt.com/backend-api/codex",
2271+
reasoning: true,
2272+
input: ["text"],
2273+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
2274+
contextWindow: 200000,
2275+
maxTokens: 8192,
2276+
} satisfies Model<Api>,
2277+
{
2278+
systemPrompt: `Stable prefix${SYSTEM_PROMPT_CACHE_BOUNDARY}Dynamic suffix`,
2279+
messages: [{ role: "user", content: "Hello", timestamp: 1 }],
2280+
tools: [],
2281+
} as never,
2282+
{
2283+
cacheRetention: "long",
2284+
maxTokens: 1024,
2285+
serviceTier: "auto",
2286+
sessionId: "session-123",
2287+
temperature: 0.2,
2288+
topP: 0.85,
2289+
},
2290+
{
2291+
openclaw_session_id: "session-123",
2292+
openclaw_turn_id: "turn-123",
2293+
},
2294+
) as Record<string, unknown> & {
2295+
input?: Array<{ role?: string }>;
2296+
instructions?: string;
2297+
};
2298+
2299+
expect(params.instructions).toBe("Stable prefix\nDynamic suffix");
2300+
expect(params.input?.map((item) => item.role)).toEqual(["user"]);
2301+
expect(params.prompt_cache_key).toBe("session-123");
2302+
expect(params.store).toBe(false);
2303+
expect(params).not.toHaveProperty("metadata");
2304+
expect(params).not.toHaveProperty("max_output_tokens");
2305+
expect(params).not.toHaveProperty("prompt_cache_retention");
2306+
expect(params).not.toHaveProperty("service_tier");
2307+
expect(params).not.toHaveProperty("temperature");
2308+
expect(params).not.toHaveProperty("top_p");
2309+
});
2310+
22632311
it("sanitizes Codex responses params after payload hooks mutate them without stripping cache identity", () => {
22642312
const payload = {
22652313
model: "gpt-5.4",

src/agents/openai-transport-stream.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1905,7 +1905,10 @@ function raiseMinimalReasoningForResponsesWebSearch(params: {
19051905
}
19061906

19071907
function isOpenAICodexResponsesModel(model: Model<Api>): boolean {
1908-
return model.provider === "openai-codex" && model.api === "openai-codex-responses";
1908+
return (
1909+
model.provider === "openai-codex" &&
1910+
(model.api === "openai-codex-responses" || model.api === "openclaw-openai-responses-transport")
1911+
);
19091912
}
19101913

19111914
function isNativeOpenAICodexResponsesBaseUrl(baseUrl?: string): boolean {

0 commit comments

Comments
 (0)