mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 16:30:57 +00:00
fix: preserve OpenAI encrypted reasoning replay
This commit is contained in:
@@ -27,6 +27,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Daemon/service: only emit hard-coded version-manager paths such as `~/.volta/bin`, `~/.asdf/shims`, `~/.bun/bin`, and fnm/pnpm fallbacks into gateway and node service PATHs when the directories exist, so `openclaw doctor` no longer flags `gateway.path.non-minimal` against a PATH the daemon just wrote. Env-driven roots and stable user-bin dirs remain unconditional. Fixes #71944; carries forward #71964. Thanks @Sanjays2402.
|
||||
- CLI/startup: disable Node's module compile cache automatically for live source-checkout launchers so in-place `pnpm build` updates are visible to the next `openclaw` CLI invocation. Fixes #73037. Thanks @LouisGameDev.
|
||||
- Agents/group chat: move `NO_REPLY` mechanics into channel-aware direct/group prompts and suppress the duplicate generic silent-reply section for auto-reply runs, so always-on group agents get one consistent stay-silent instruction. Thanks @vincentkoc.
|
||||
- Providers/OpenAI: preserve encrypted empty-summary Responses reasoning items in WebSocket replay and request `reasoning.encrypted_content` on reasoning turns so GPT-5.4/GPT-5.5 sessions do not lose required `rs_*` state beside `msg_*` items. Fixes #73053. Thanks @odb36777.
|
||||
- Channels/commands: make generated `/dock-*` commands switch the active session reply route through `session.identityLinks` instead of falling through to normal chat. Fixes #69206; carries forward #73033. Thanks @clawbones and @michaelatamuk.
|
||||
- Providers/Cloudflare AI Gateway: strip assistant prefill turns from Anthropic Messages payloads when thinking is enabled, so Claude requests through Cloudflare AI Gateway no longer fail Anthropic conversation-ending validation. Fixes #72905; carries forward #73005. Thanks @AaronFaby and @sahilsatralkar.
|
||||
- Gateway/startup: keep primary-model startup prewarm on scoped metadata preparation, let native approval bootstraps retry outside channel startup, and skip the global hook runner when no `gateway_start` hook is registered, so clean post-ready sidecar work stays off the critical path. Refs #72846. Thanks @RayWoo, @livekm0309, and @mrz1836.
|
||||
|
||||
@@ -112,6 +112,7 @@ external end-user instructions.
|
||||
|
||||
- Image sanitization only.
|
||||
- Drop orphaned reasoning signatures (standalone reasoning items without a following content block) for OpenAI Responses/Codex transcripts, and drop replayable OpenAI reasoning after a model route switch.
|
||||
- Preserve replayable OpenAI Responses reasoning item payloads, including encrypted empty-summary items, so manual/WebSocket replay keeps required `rs_*` state paired with assistant output items.
|
||||
- No tool call id sanitization.
|
||||
- Tool result pairing repair may move real matched outputs and synthesize Codex-style `aborted` outputs for missing tool calls.
|
||||
- No turn validation or reordering.
|
||||
|
||||
@@ -78,7 +78,8 @@ export type OutputItem =
|
||||
| {
|
||||
type: "reasoning" | `reasoning.${string}`;
|
||||
id: string;
|
||||
content?: string;
|
||||
content?: unknown;
|
||||
encrypted_content?: string;
|
||||
summary?: unknown;
|
||||
};
|
||||
|
||||
|
||||
@@ -28,6 +28,9 @@ type ReplayableReasoningItem = Extract<InputItem, { type: "reasoning" }>;
|
||||
type ReplayableReasoningSignature = {
|
||||
type: "reasoning" | `reasoning.${string}`;
|
||||
id?: string;
|
||||
content?: unknown;
|
||||
encrypted_content?: string;
|
||||
summary?: unknown;
|
||||
};
|
||||
type ToolCallReplayId = { callId: string; itemId?: string };
|
||||
export type PlannedTurnInput = {
|
||||
@@ -166,26 +169,10 @@ function toReplayableReasoningId(value: unknown): string | null {
|
||||
return id && id.startsWith("rs_") ? id : null;
|
||||
}
|
||||
|
||||
function toReasoningSignature(value: unknown): ReplayableReasoningSignature | null {
|
||||
if (!value || typeof value !== "object") {
|
||||
return null;
|
||||
}
|
||||
const record = value as { type?: unknown; id?: unknown };
|
||||
if (!isReplayableReasoningType(record.type)) {
|
||||
return null;
|
||||
}
|
||||
const reasoningId = toReplayableReasoningId(record.id);
|
||||
return {
|
||||
type: record.type,
|
||||
...(reasoningId ? { id: reasoningId } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function encodeThinkingSignature(signature: ReplayableReasoningSignature): string {
|
||||
return JSON.stringify(signature);
|
||||
}
|
||||
|
||||
function parseReasoningItem(value: unknown): ReplayableReasoningItem | null {
|
||||
function toReasoningSignature(
|
||||
value: unknown,
|
||||
options?: { requireReplayableId?: boolean },
|
||||
): ReplayableReasoningSignature | null {
|
||||
if (!value || typeof value !== "object") {
|
||||
return null;
|
||||
}
|
||||
@@ -200,14 +187,37 @@ function parseReasoningItem(value: unknown): ReplayableReasoningItem | null {
|
||||
return null;
|
||||
}
|
||||
const reasoningId = toReplayableReasoningId(record.id);
|
||||
if (options?.requireReplayableId && !reasoningId) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: "reasoning",
|
||||
type: record.type,
|
||||
...(reasoningId ? { id: reasoningId } : {}),
|
||||
...(typeof record.content === "string" ? { content: record.content } : {}),
|
||||
...(record.content !== undefined ? { content: record.content } : {}),
|
||||
...(typeof record.encrypted_content === "string"
|
||||
? { encrypted_content: record.encrypted_content }
|
||||
: {}),
|
||||
...(typeof record.summary === "string" ? { summary: record.summary } : {}),
|
||||
...(record.summary !== undefined ? { summary: record.summary } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function encodeThinkingSignature(signature: ReplayableReasoningSignature): string {
|
||||
return JSON.stringify(signature);
|
||||
}
|
||||
|
||||
function parseReasoningItem(value: unknown): ReplayableReasoningItem | null {
|
||||
const signature = toReasoningSignature(value);
|
||||
if (!signature) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: "reasoning",
|
||||
...(signature.id ? { id: signature.id } : {}),
|
||||
...(signature.content !== undefined ? { content: signature.content } : {}),
|
||||
...(signature.encrypted_content !== undefined
|
||||
? { encrypted_content: signature.encrypted_content }
|
||||
: {}),
|
||||
...(signature.summary !== undefined ? { summary: signature.summary } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -216,8 +226,7 @@ function parseThinkingSignature(value: unknown): ReplayableReasoningItem | null
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const signature = toReasoningSignature(JSON.parse(value));
|
||||
return signature ? parseReasoningItem(signature) : null;
|
||||
return parseReasoningItem(JSON.parse(value));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -271,7 +280,25 @@ function extractResponseReasoningText(item: unknown): string {
|
||||
if (summaryText) {
|
||||
return summaryText;
|
||||
}
|
||||
return normalizeOptionalString(record.content) ?? "";
|
||||
if (typeof record.content === "string") {
|
||||
return normalizeOptionalString(record.content) ?? "";
|
||||
}
|
||||
if (Array.isArray(record.content)) {
|
||||
return record.content
|
||||
.map((part) => {
|
||||
if (typeof part === "string") {
|
||||
return part.trim();
|
||||
}
|
||||
if (!part || typeof part !== "object") {
|
||||
return "";
|
||||
}
|
||||
return normalizeOptionalString((part as { text?: unknown }).text) ?? "";
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n")
|
||||
.trim();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export function convertTools(
|
||||
@@ -573,21 +600,16 @@ export function buildAssistantMessageFromResponse(
|
||||
if (!isReplayableReasoningType(item.type)) {
|
||||
continue;
|
||||
}
|
||||
const reasoningSignature = toReasoningSignature(item, { requireReplayableId: true });
|
||||
const reasoning = extractResponseReasoningText(item);
|
||||
if (!reasoning) {
|
||||
if (!reasoning && !reasoningSignature) {
|
||||
continue;
|
||||
}
|
||||
const reasoningId = toReplayableReasoningId(item.id);
|
||||
content.push({
|
||||
type: "thinking",
|
||||
thinking: reasoning,
|
||||
...(reasoningId
|
||||
? {
|
||||
thinkingSignature: encodeThinkingSignature({
|
||||
id: reasoningId,
|
||||
type: item.type,
|
||||
}),
|
||||
}
|
||||
...(reasoningSignature
|
||||
? { thinkingSignature: encodeThinkingSignature(reasoningSignature) }
|
||||
: {}),
|
||||
} as AssistantMessage["content"][number]);
|
||||
}
|
||||
|
||||
@@ -169,6 +169,9 @@ export function buildOpenAIWebSocketResponseCreatePayload(params: {
|
||||
reasoning.summary = streamOpts.reasoningSummary;
|
||||
}
|
||||
extraParams.reasoning = reasoning;
|
||||
if (reasoning.effort && reasoning.effort !== "none") {
|
||||
extraParams.include = ["reasoning.encrypted_content"];
|
||||
}
|
||||
}
|
||||
|
||||
const textVerbosity = resolveOpenAITextVerbosity(
|
||||
|
||||
@@ -27,6 +27,7 @@ import type { OutputItem, ResponseObject } from "./openai-ws-connection.js";
|
||||
|
||||
const API_KEY = process.env.OPENAI_API_KEY;
|
||||
const LIVE = isLiveTestEnabled(["OPENAI_LIVE_TEST"]) && !!API_KEY;
|
||||
const LIVE_MODEL_ID = process.env.OPENCLAW_LIVE_OPENAI_MODEL || "gpt-5.4";
|
||||
const testFn = LIVE ? it : it.skip;
|
||||
|
||||
type OpenAIWsStreamModule = typeof import("./openai-ws-stream.js");
|
||||
@@ -39,8 +40,8 @@ let openAIWsConnectionModule: OpenAIWsConnectionModule;
|
||||
const model = {
|
||||
api: "openai-responses" as const,
|
||||
provider: "openai",
|
||||
id: "gpt-5.4",
|
||||
name: "gpt-5.4",
|
||||
id: LIVE_MODEL_ID,
|
||||
name: LIVE_MODEL_ID,
|
||||
contextWindow: 128_000,
|
||||
maxTokens: 4_096,
|
||||
reasoning: true,
|
||||
@@ -185,7 +186,13 @@ function parseReasoningSignature(value: string | undefined) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(value) as { id?: unknown; type?: unknown };
|
||||
return JSON.parse(value) as {
|
||||
id?: unknown;
|
||||
type?: unknown;
|
||||
content?: unknown;
|
||||
encrypted_content?: unknown;
|
||||
summary?: unknown;
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -220,9 +227,19 @@ function extractReasoningText(item: { summary?: unknown; content?: unknown }): s
|
||||
}
|
||||
|
||||
function toExpectedReasoningSignature(item: { id?: string; type: string }) {
|
||||
const record = item as {
|
||||
content?: unknown;
|
||||
encrypted_content?: unknown;
|
||||
summary?: unknown;
|
||||
};
|
||||
return {
|
||||
type: item.type,
|
||||
...(typeof item.id === "string" && item.id.startsWith("rs_") ? { id: item.id } : {}),
|
||||
...(record.content !== undefined ? { content: record.content } : {}),
|
||||
...(typeof record.encrypted_content === "string"
|
||||
? { encrypted_content: record.encrypted_content }
|
||||
: {}),
|
||||
...(record.summary !== undefined ? { summary: record.summary } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -372,7 +389,7 @@ describe("OpenAI WebSocket e2e", () => {
|
||||
item.type === "reasoning" || item.type.startsWith("reasoning."),
|
||||
);
|
||||
const replayableReasoningItems = rawReasoningItems.filter(
|
||||
(item) => extractReasoningText(item).length > 0,
|
||||
(item) => typeof item.id === "string" && item.id.startsWith("rs_"),
|
||||
);
|
||||
const thinkingBlocks = extractThinkingBlocks(firstDone);
|
||||
expect(thinkingBlocks).toHaveLength(replayableReasoningItems.length);
|
||||
@@ -481,7 +498,7 @@ describe("OpenAI WebSocket e2e", () => {
|
||||
stopReason: "stop",
|
||||
api: "openai-responses",
|
||||
provider: "openai",
|
||||
model: "gpt-5.4",
|
||||
model: LIVE_MODEL_ID,
|
||||
usage: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
|
||||
@@ -1067,7 +1067,42 @@ describe("convertMessagesToInputItems", () => {
|
||||
typeof convertMessagesToInputItems
|
||||
>[0]);
|
||||
expect(items.map((item) => item.type)).toEqual(["reasoning", "message"]);
|
||||
expect(items[0]).toMatchObject({ type: "reasoning", id: "rs_test" });
|
||||
expect(items[0]).toMatchObject({ type: "reasoning", id: "rs_test", summary: [] });
|
||||
});
|
||||
|
||||
it("replays encrypted reasoning content from thinking signatures", () => {
|
||||
const msg = {
|
||||
role: "assistant" as const,
|
||||
content: [
|
||||
{
|
||||
type: "thinking" as const,
|
||||
thinking: "",
|
||||
thinkingSignature: JSON.stringify({
|
||||
type: "reasoning",
|
||||
id: "rs_encrypted",
|
||||
encrypted_content: "encrypted-payload",
|
||||
summary: [],
|
||||
}),
|
||||
},
|
||||
{ type: "text" as const, text: "Here is my answer." },
|
||||
],
|
||||
stopReason: "stop",
|
||||
api: "openai-responses",
|
||||
provider: "openai",
|
||||
model: "gpt-5.4",
|
||||
usage: {},
|
||||
timestamp: 0,
|
||||
};
|
||||
const items = convertMessagesToInputItems([msg] as Parameters<
|
||||
typeof convertMessagesToInputItems
|
||||
>[0]);
|
||||
expect(items.map((item) => item.type)).toEqual(["reasoning", "message"]);
|
||||
expect(items[0]).toMatchObject({
|
||||
type: "reasoning",
|
||||
id: "rs_encrypted",
|
||||
encrypted_content: "encrypted-payload",
|
||||
summary: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("replays reasoning blocks when signature type is reasoning.*", () => {
|
||||
@@ -1440,7 +1475,11 @@ describe("buildAssistantMessageFromResponse", () => {
|
||||
| undefined;
|
||||
expect(thinkingBlock?.thinking).toBe("Plan step A\nPlan step B");
|
||||
expect(thinkingBlock?.thinkingSignature).toBe(
|
||||
JSON.stringify({ id: "rs_123", type: "reasoning" }),
|
||||
JSON.stringify({
|
||||
type: "reasoning",
|
||||
id: "rs_123",
|
||||
summary: [{ text: "Plan step A" }, { text: "Plan step B" }],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1466,9 +1505,11 @@ describe("buildAssistantMessageFromResponse", () => {
|
||||
| undefined;
|
||||
expect(thinkingBlock?.type).toBe("thinking");
|
||||
expect(thinkingBlock?.thinking).toBe("Derived hidden reasoning");
|
||||
expect(thinkingBlock?.thinkingSignature).toBe(
|
||||
JSON.stringify({ id: "rs_456", type: "reasoning.summary" }),
|
||||
);
|
||||
expect(JSON.parse(thinkingBlock?.thinkingSignature ?? "{}")).toEqual({
|
||||
type: "reasoning.summary",
|
||||
id: "rs_456",
|
||||
content: "Derived hidden reasoning",
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers reasoning summary text over fallback content and preserves item order", () => {
|
||||
@@ -1502,9 +1543,12 @@ describe("buildAssistantMessageFromResponse", () => {
|
||||
| { type: "thinking"; thinking: string; thinkingSignature?: string }
|
||||
| undefined;
|
||||
expect(thinkingBlock?.thinking).toBe("Plan A\nPlan B");
|
||||
expect(thinkingBlock?.thinkingSignature).toBe(
|
||||
JSON.stringify({ id: "rs_789", type: "reasoning.summary" }),
|
||||
);
|
||||
expect(JSON.parse(thinkingBlock?.thinkingSignature ?? "{}")).toEqual({
|
||||
type: "reasoning.summary",
|
||||
id: "rs_789",
|
||||
content: "hidden fallback content",
|
||||
summary: ["Plan A", { text: "Plan B" }, { nope: true }],
|
||||
});
|
||||
});
|
||||
|
||||
it("drops invalid reasoning ids from thinking signatures while preserving the visible block", () => {
|
||||
@@ -1528,6 +1572,57 @@ describe("buildAssistantMessageFromResponse", () => {
|
||||
expect(msg.content).toEqual([{ type: "thinking", thinking: "Hidden reasoning" }]);
|
||||
});
|
||||
|
||||
it("preserves encrypted-only reasoning items with empty visible thinking", () => {
|
||||
const response = {
|
||||
id: "resp_encrypted_reasoning",
|
||||
object: "response",
|
||||
created_at: Date.now(),
|
||||
status: "completed",
|
||||
model: "gpt-5.4",
|
||||
output: [
|
||||
{
|
||||
type: "reasoning",
|
||||
id: "rs_encrypted_empty",
|
||||
encrypted_content: "encrypted-payload",
|
||||
summary: [],
|
||||
},
|
||||
{
|
||||
type: "message",
|
||||
id: "msg_encrypted_empty",
|
||||
role: "assistant",
|
||||
content: [{ type: "output_text", text: "NO_REPLY" }],
|
||||
},
|
||||
],
|
||||
usage: { input_tokens: 100, output_tokens: 50, total_tokens: 150 },
|
||||
} as unknown as ResponseObject;
|
||||
|
||||
const msg = buildAssistantMessageFromResponse(response, modelInfo);
|
||||
expect(msg.content.map((block) => block.type)).toEqual(["thinking", "text"]);
|
||||
const thinkingBlock = msg.content[0] as {
|
||||
type: "thinking";
|
||||
thinking: string;
|
||||
thinkingSignature?: string;
|
||||
};
|
||||
expect(thinkingBlock.thinking).toBe("");
|
||||
expect(JSON.parse(thinkingBlock.thinkingSignature ?? "{}")).toEqual({
|
||||
encrypted_content: "encrypted-payload",
|
||||
id: "rs_encrypted_empty",
|
||||
summary: [],
|
||||
type: "reasoning",
|
||||
});
|
||||
|
||||
const replayItems = convertMessagesToInputItems([msg] as Parameters<
|
||||
typeof convertMessagesToInputItems
|
||||
>[0]);
|
||||
expect(replayItems.map((item) => item.type)).toEqual(["reasoning", "message"]);
|
||||
expect(replayItems[0]).toMatchObject({
|
||||
type: "reasoning",
|
||||
id: "rs_encrypted_empty",
|
||||
encrypted_content: "encrypted-payload",
|
||||
summary: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves function call item ids for replay when reasoning is present", () => {
|
||||
const response = {
|
||||
id: "resp_tool_reasoning",
|
||||
@@ -1824,6 +1919,7 @@ describe("createOpenAIWebSocketStreamFn", () => {
|
||||
releaseWsSession("sess-fallback");
|
||||
releaseWsSession("sess-boundary-http-fallback");
|
||||
releaseWsSession("sess-full-context-replay");
|
||||
releaseWsSession("sess-encrypted-full-context-replay");
|
||||
releaseWsSession("sess-incremental");
|
||||
releaseWsSession("sess-full");
|
||||
releaseWsSession("sess-onpayload");
|
||||
@@ -3030,6 +3126,105 @@ describe("createOpenAIWebSocketStreamFn", () => {
|
||||
expect(sent2.input).toEqual([]);
|
||||
});
|
||||
|
||||
it("replays encrypted-only reasoning when websocket must send full context", async () => {
|
||||
const sessionId = "sess-encrypted-full-context-replay";
|
||||
const streamFn = createOpenAIWebSocketStreamFn("sk-test", sessionId);
|
||||
|
||||
const ctx1 = {
|
||||
systemPrompt: "You are helpful.",
|
||||
messages: [userMsg("Run ls")] as Parameters<typeof convertMessagesToInputItems>[0],
|
||||
tools: [],
|
||||
};
|
||||
const turn1Response = {
|
||||
id: "resp_turn1_encrypted_reasoning",
|
||||
object: "response",
|
||||
created_at: Date.now(),
|
||||
status: "completed",
|
||||
model: "gpt-5.4",
|
||||
output: [
|
||||
{
|
||||
type: "reasoning",
|
||||
id: "rs_turn1_encrypted",
|
||||
encrypted_content: "encrypted-payload",
|
||||
summary: [],
|
||||
},
|
||||
{
|
||||
type: "function_call",
|
||||
id: "fc_turn1",
|
||||
call_id: "call_turn1",
|
||||
name: "exec",
|
||||
arguments: '{"cmd":"ls"}',
|
||||
},
|
||||
],
|
||||
usage: { input_tokens: 12, output_tokens: 8, total_tokens: 20 },
|
||||
} as ResponseObject;
|
||||
|
||||
const stream1 = streamFn(
|
||||
modelStub as Parameters<typeof streamFn>[0],
|
||||
ctx1 as Parameters<typeof streamFn>[1],
|
||||
);
|
||||
const done1 = (async () => {
|
||||
for await (const _ of await resolveStream(stream1)) {
|
||||
/* consume */
|
||||
}
|
||||
})();
|
||||
|
||||
await new Promise((r) => setImmediate(r));
|
||||
const manager = MockManager.lastInstance!;
|
||||
manager.simulateEvent({ type: "response.completed", response: turn1Response });
|
||||
await done1;
|
||||
|
||||
const ctx2 = {
|
||||
systemPrompt: "You are helpful. Use the updated instruction.",
|
||||
messages: [
|
||||
userMsg("Run ls"),
|
||||
buildAssistantMessageFromResponse(turn1Response, modelStub),
|
||||
toolResultMsg("call_turn1|fc_turn1", "TOOL_OK"),
|
||||
] as Parameters<typeof convertMessagesToInputItems>[0],
|
||||
tools: [],
|
||||
};
|
||||
|
||||
const stream2 = streamFn(
|
||||
modelStub as Parameters<typeof streamFn>[0],
|
||||
ctx2 as Parameters<typeof streamFn>[1],
|
||||
);
|
||||
const done2 = (async () => {
|
||||
for await (const _ of await resolveStream(stream2)) {
|
||||
/* consume */
|
||||
}
|
||||
})();
|
||||
|
||||
await new Promise((r) => setImmediate(r));
|
||||
manager.simulateEvent({
|
||||
type: "response.completed",
|
||||
response: makeResponseObject("resp_turn2", "Done"),
|
||||
});
|
||||
await done2;
|
||||
|
||||
const sent2 = manager.sentEvents[1] as {
|
||||
previous_response_id?: string;
|
||||
input: Array<Record<string, unknown>>;
|
||||
};
|
||||
expect(sent2.previous_response_id).toBeUndefined();
|
||||
expect(sent2.input).toEqual([
|
||||
{ type: "message", role: "user", content: "Run ls" },
|
||||
{
|
||||
type: "reasoning",
|
||||
id: "rs_turn1_encrypted",
|
||||
encrypted_content: "encrypted-payload",
|
||||
summary: [],
|
||||
},
|
||||
{
|
||||
type: "function_call",
|
||||
id: "fc_turn1",
|
||||
call_id: "call_turn1",
|
||||
name: "exec",
|
||||
arguments: '{"cmd":"ls"}',
|
||||
},
|
||||
{ type: "function_call_output", call_id: "call_turn1", output: "TOOL_OK" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("sends instructions (system prompt) in each request", async () => {
|
||||
const streamFn = createOpenAIWebSocketStreamFn("sk-test", "sess-tools");
|
||||
const ctx = {
|
||||
@@ -3353,6 +3548,7 @@ describe("createOpenAIWebSocketStreamFn", () => {
|
||||
const sent = MockManager.lastInstance!.sentEvents[0] as Record<string, unknown>;
|
||||
expect(sent.type).toBe("response.create");
|
||||
expect(sent.reasoning).toEqual({ effort: "high", summary: "auto" });
|
||||
expect(sent.include).toEqual(["reasoning.encrypted_content"]);
|
||||
});
|
||||
|
||||
it("defaults response.create reasoning effort to high for reasoning models", async () => {
|
||||
@@ -3382,6 +3578,7 @@ describe("createOpenAIWebSocketStreamFn", () => {
|
||||
const sent = MockManager.lastInstance!.sentEvents[0] as Record<string, unknown>;
|
||||
expect(sent.type).toBe("response.create");
|
||||
expect(sent.reasoning).toEqual({ effort: "high" });
|
||||
expect(sent.include).toEqual(["reasoning.encrypted_content"]);
|
||||
});
|
||||
|
||||
it("forwards shared reasoning to response.create reasoning effort", async () => {
|
||||
|
||||
@@ -24,9 +24,9 @@ export type InputItem =
|
||||
| {
|
||||
type: "reasoning";
|
||||
id?: string;
|
||||
content?: string;
|
||||
content?: unknown;
|
||||
encrypted_content?: string;
|
||||
summary?: string;
|
||||
summary?: unknown;
|
||||
}
|
||||
| { type: "item_reference"; id: string };
|
||||
|
||||
@@ -59,6 +59,7 @@ export interface ResponseCreateEvent {
|
||||
temperature?: number;
|
||||
top_p?: number;
|
||||
metadata?: Record<string, string>;
|
||||
include?: string[];
|
||||
reasoning?: {
|
||||
effort?: "none" | "low" | "medium" | "high" | "xhigh";
|
||||
summary?: "auto" | "concise" | "detailed";
|
||||
|
||||
Reference in New Issue
Block a user