fix: preserve Foundry Responses reasoning replay ids

Preserve Microsoft Foundry encrypted reasoning replay item ids for Responses continuations while leaving chat-completions streams untouched.

Fixes #91033.
This commit is contained in:
Peter Steinberger
2026-06-07 01:24:07 -07:00
committed by GitHub
parent e64f2324b9
commit 248dfb22ec
3 changed files with 138 additions and 0 deletions

View File

@@ -1,4 +1,5 @@
// Microsoft Foundry tests cover index plugin behavior.
import type { StreamFn } from "openclaw/plugin-sdk/agent-core";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -781,6 +782,60 @@ describe("microsoft-foundry plugin", () => {
expect(provider?.models[0]?.compat?.maxTokensField).toBe("max_completion_tokens");
});
it("keeps replay item ids for Foundry encrypted reasoning continuations", async () => {
const provider = registerProvider();
let capturedReplayIds: boolean | undefined;
const baseStreamFn: StreamFn = (_model, _context, options) => {
capturedReplayIds = (options as { replayResponsesItemIds?: boolean } | undefined)
?.replayResponsesItemIds;
return {} as never;
};
const wrappedStreamFn = provider.wrapStreamFn?.({
streamFn: baseStreamFn,
modelId: "gpt-5.4",
model: buildFoundryModel({
reasoning: true,
compat: { supportsStore: false },
}),
extraParams: {},
config: {},
agentDir: defaultFoundryAgentDir,
} as never);
expect(wrappedStreamFn).toBeTypeOf("function");
await wrappedStreamFn?.(
buildFoundryModel({
reasoning: true,
compat: { supportsStore: false },
}) as never,
{ systemPrompt: "system", messages: [] } as never,
{},
);
expect(capturedReplayIds).toBe(true);
});
it("leaves Foundry chat completions streams unwrapped by Responses defaults", () => {
const provider = registerProvider();
const baseStreamFn: StreamFn = () => ({}) as never;
expect(
provider.wrapStreamFn?.({
streamFn: baseStreamFn,
modelId: "gpt-4o-mini",
model: buildFoundryModel({
id: "gpt-4o-mini",
name: "gpt-4o-mini",
api: "openai-completions",
}),
extraParams: {},
config: {},
agentDir: defaultFoundryAgentDir,
} as never),
).toBe(baseStreamFn);
});
it("marks Foundry chat models as not supporting reasoning_effort", () => {
const result = buildFoundryAuthResult({
profileId: "microsoft-foundry:default",

View File

@@ -4,6 +4,7 @@ import type {
ModelProviderConfig,
ProviderPlugin,
} from "openclaw/plugin-sdk/provider-model-shared";
import { OPENAI_RESPONSES_STREAM_HOOKS } from "openclaw/plugin-sdk/provider-stream-family";
import { apiKeyAuthMethod, entraIdAuthMethod } from "./auth.js";
import { prepareFoundryRuntimeAuth } from "./runtime.js";
import {
@@ -18,6 +19,40 @@ import {
resolveFoundryTargetProfileId,
} from "./shared.js";
type FoundryProviderHooks = Pick<ProviderPlugin, "wrapStreamFn">;
const wrapOpenAIResponsesStreamFn = OPENAI_RESPONSES_STREAM_HOOKS.wrapStreamFn;
const wrapMicrosoftFoundryStreamFn: NonNullable<FoundryProviderHooks["wrapStreamFn"]> = (ctx) => {
if (ctx.model?.api !== "openai-responses") {
return ctx.streamFn ?? null;
}
const baseStreamFn = ctx.streamFn;
if (!baseStreamFn) {
return wrapOpenAIResponsesStreamFn?.(ctx) ?? null;
}
const streamFnWithResponsesReplayIds: NonNullable<typeof ctx.streamFn> = (
model,
context,
options,
) =>
baseStreamFn(model, context, {
...options,
// Foundry validates encrypted reasoning replay against the original item id,
// even though its Responses endpoint does not support persisted `store`.
replayResponsesItemIds: true,
} as typeof options & { replayResponsesItemIds: true });
return (
wrapOpenAIResponsesStreamFn?.({
...ctx,
streamFn: streamFnWithResponsesReplayIds,
}) ?? streamFnWithResponsesReplayIds
);
};
export function buildMicrosoftFoundryProvider(): ProviderPlugin {
return {
id: PROVIDER_ID,
@@ -174,6 +209,7 @@ export function buildMicrosoftFoundryProvider(): ProviderPlugin {
...(compat ? { compat } : {}),
};
},
wrapStreamFn: wrapMicrosoftFoundryStreamFn,
prepareRuntimeAuth: prepareFoundryRuntimeAuth,
};
}

View File

@@ -354,6 +354,53 @@ describe("convertResponsesMessages", () => {
});
expect(functionCall).not.toHaveProperty("id");
});
it("keeps encrypted reasoning replay item ids when requested", () => {
const input = convertResponsesMessages(
nativeOpenAIModel,
{
systemPrompt: "system",
messages: [
{
role: "assistant",
api: nativeOpenAIModel.api,
provider: nativeOpenAIModel.provider,
model: nativeOpenAIModel.id,
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "stop",
timestamp: 1,
content: [
{
type: "thinking",
thinking: "Need continuity.",
thinkingSignature: JSON.stringify({
type: "reasoning",
id: "rs_foundry_prior",
encrypted_content: "ciphertext",
}),
},
],
},
],
} satisfies Context,
allowedToolCallProviders,
{ includeSystemPrompt: false, replayResponsesItemIds: true },
) as unknown as Array<Record<string, unknown>>;
expect(input.find((item) => item.type === "reasoning")).toMatchObject({
type: "reasoning",
id: "rs_foundry_prior",
encrypted_content: "ciphertext",
summary: [],
});
});
});
describe("processResponsesStream", () => {