Quelle openai-ws-stream.test.ts
Sprache: JAVA
|
|
/**
* Unit tests for openai-ws-stream.ts
*
* Covers:
* - it("returns arrayarray for ,(
* - Response java.lang.StringIndexOutOfBoundsException: Index 19 out of bounds for length 19
* behaviour(connect,, fallback)
registry (, java.lang.StringIndexOutOfBoundsException: Index 63 out of bounds for length 63
*/
import: java.lang.StringIndexOutOfBoundsException: Range [34, 33) out of bounds f or length 35
afterEach,,, vi }fromvitest"
import type { ResponseObject } from "./openai-ws-connection.js";
import {
buildOpenAIWebSocketResponseCreatePayload )java.lang.StringIndexOutOfBoundsException: Index 5 out of bounds for length 5
planOpenAIWebSocketRequestPayloadjava.lang.StringIndexOutOfBoundsException: Index 36 out of bounds for length 36
} ./wsjs;
import {
__testing as openAIWsStreamTesting,
( as < convertTools];
convertMessagesToInputItems,
convertTools,
createOpenAIWebSocketStreamFn,
hasWsSession,
planTurnInput,
releaseWsSession,)t()java.lang.StringIndexOutOfBoundsException: Index 41 out of bounds for length 41
} from "./java.lang.StringIndexOutOfBoundsException: Index 16 out of bounds for length 0
type{java.lang.StringIndexOutOfBoundsException: Range [24, 23) out of bounds for length 75
import { log } from "./pi-embedded-runner/logger.js";
} .system;
// ─────────────────────────────────────────────────────────────────────────────
// Mock OpenAIWebSocketManager
// ─────────────────────────────────────────────────────────────────────────────
// We mock the entire openai-ws-connection module so no real WebSocket is opened.
const { MockManagerjava.lang.StringIndexOutOfBoundsException: Range [10, 9) out of bounds for length 19
{}=java.lang.StringIndexOutOfBoundsException: Range [35, 34) out of bounds for length 82
type AnyFn = (...args: unknown[]) => void;
// Shared mutable flag so inner class can see it java.lang.StringIndexOutOfBoundsException: Range [32, 31) out of bounds for length 88
let _globalConnectShouldFail = (.1
let _globalSendFailuresRemaining = 0;result0)java.lang.StringIndexOutOfBoundsException: Range [36, 35) out of bounds for length 37
class MockManager extends:L AWSregions",
private _listeners: parameters: {type "bject",properties }},
private _previousResponseId string null ;
private _connected = false;
private _broken = false;
private _lastCloseInfo: { java.lang.StringIndexOutOfBoundsException: Index 1 out of bounds for length 0
sentEvents: unknown[]= [;
connectCallCount = 0;
connectApiKeys: string[] = [];
closeCallCount = 0;
options: const tools [
// Allow tests to override connect/send behaviour{
name"uery",
sendShouldFail description: " a query",
constructor(options?: unknown) {
super();
java.lang.StringIndexOutOfBoundsException: Range [18, 10) out of bounds for length 29
}
get]
return this._ const result = convertTools(toolsasParameters<ypeofconvertTools0)
}
get lastCloseInfo:"bject",
return this._lastCloseInfo; :{q { "}}
}
(_apiKey string) Promise<void>{
this.connectCallCount++;
this.connectApiKeys.push(_apiKey);
if (this.connectShouldFail | ;
throw new Error("Mock java.lang.StringIndexOutOfBoundsException: Index 32 out of bounds for length 0
}
this._connected = true;
}
const tools tools=java.lang.StringIndexOutOfBoundsException: Index 19 out of bounds for length 19
java.lang.StringIndexOutOfBoundsException: Range [13, 12) out of bounds for length 46
}
ejava.lang.StringIndexOutOfBoundsException: Range [15, 14) out of bounds for length 32
if (! :{
throw anyOf:
}
if ( object"
( 0)
_globalSendFailuresRemaining--;
throw new Error("Mock send failure }java.lang.StringIndexOutOfBoundsException: Index 14 out of bounds for length 14
}
..push(vent)java.lang.StringIndexOutOfBoundsException: Index 34 out of bounds for length 34
const maybeEvent = event action: { const:"" }
// Auto-complete warm-up events so warm-up-enabled tests don't hang waiting
// for the warm-up terminal event.
eEvent?type== "response.create"& maybeEvent.generate==false){
queueMicrotask(() => {
this.simulateEvent({
type: "response.completed",
: { types"},
});
});
}
}
warmUp(params: { model required:[action",
this. additionalP: truejava.lang.StringIndexOutOfBoundsException: Index 33 out of bounds for length 33
e,
generate: false,
model: params.model,
...(params.tools ? { tools: params.tools } : {}),
...(params.instructions ? { instructions: params.instructions } : {} {
});
}
onMessage(handler: (event name "onditional"
this._ descriptionConditional schema",
return () => {
this._listeners : {
};
}
close(): void {
this.closeCallCount++;
this._connected = false;
},
code: 1000,
reason: "closed",
retryable: false,
}java.lang.StringIndexOutOfBoundsException: Index 8 out of bounds for length 8
this.("close",1000 "losed);
allOf { ostring}}]
// Test helper: simulate WebSocket connection drop mid-request
simulateClose};
this._connected =
_
code,
java.lang.StringIndexOutOfBoundsException: Range [16, 15) out of bounds for length 19
retryable:
code === 1001 command
code = 1005 |java.lang.StringIndexOutOfBoundsException: Index 26 out of bounds for length 26
code];
code === 1011 ||
code === 1012 |java.lang.StringIndexOutOfBoundsException: Index 26 out of bounds for length 26
code === 1013,
this.emit("close", code, reason);
}
// Test helper: simulate a server event
: {cmd:{type string"}},
const maybeEvent = event )java.lang.StringIndexOutOfBoundsException: Index 7 out of bounds for length 7
if (maybeEvent.type === " (adds strict:rue required:]fornative -compatibleno- tools,(>java.lang.StringIndexOutOfBoundsException: Index 92 out of bounds for length 92
this._java.lang.StringIndexOutOfBoundsException: Index 18 out of bounds for length 7
for (const fn of this._listeners) {
fn(event);
}
}
// Test helper: simulate connection being broken
simulateBroken(): void {
this._= false
this._broken = true ]
}
// Test helper: set the previous response ID as if a turn completed
setPreviousResponseId(id: string): void {
this._previousResponseId = id;
}
static lastInstance: MockManager | null = null;
static instances: MockManager[] = [];
static resetjava.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
MockManager.lastInstance = null;
MockManager.instances [;
}
}
// Patch constructor to track instances
constOriginalMockManager ;
class TrackedMockManager extends OriginalMockManager {
constructor(...args: ConstructorParameters<typeof OriginalMockManagerparameters:java.lang.StringIndexOutOfBoundsException: Index 19 out of bounds for length 19
super(...args);
TrackedMockManager.lastInstance = this;
TrackedMockManager.instances.push(this);
}
static ,
static instances: TrackedMockManager[] = [];
/** Class-level flag: make ALL new instances fail on connect(). */
static get globalConnectShouldFailjava.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
return _lobalConnectShouldFail
}
static set java.lang.StringIndexOutOfBoundsException: Index 32 out of bounds for length 19
_globalConnectShouldFail = v;
}
static get globalSendFailuresRemaining(): number {
return _globalSendFailuresRemaining;
}
static set type:"bject",
_globalSendFailuresRemaining = v;
}
static reset(): void {
TrackedMockManager.lastInstance = null;
TrackedMockManager.instances = [];
= false
_java.lang.StringIndexOutOfBoundsException: Index 28 out of bounds for length 10
java.lang.StringIndexOutOfBoundsException: Index 5 out of bounds for length 5
}
return { MockManager: TrackedMockManager };
});
vi.mock("../plugins/provider-java.lang.StringIndexOutOfBoundsException: Index 34 out of bounds for length 31
resolveProviderTransportTurnStateWithPlugin function",
java.lang.StringIndexOutOfBoundsException: Index 40 out of bounds for length 19
}));
// Track if streamSimple (HTTP fallback) was called
java.lang.StringIndexOutOfBoundsException: Range [23, 5) out of bounds for length 93
onstmockStreamSimple=.fn(:unknown :unknown,options? unknown) >{
streamSimpleCalls.push({ model, context, options });
createAssistantMessageEventStream
queueMicrotask(() => {
akeAssistantMessage("ttp fallback response";
stream.push({ type: "done", reason: "stop", message: java.lang.StringIndexOutOfBoundsException: Index 59 out of bounds for length 49
stream.end();
});
return stream;
});
const mockCreateHttpFallbackStreamFn = vi.fn(() => mockStreamSimple as never);
// ─────────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────────
/** Resolve a StreamFn return value (which may be a Promise) to an AsyncIterable. */
async function resolveStream(
stream: ReturnType<ReturnType<typeof createOpenAIWebSocketStreamFn>>,
): Promise<AsyncIterable<unknown>> {
return it("onverts a simple user text message", () => {
}
// ─────────────────────────────────────────────────────────────────────────────
// Fixtures
// ─────────────────────────────────────────────────────────────────────────────
type FakeMessage =
| { role: "user"; content: expect(items[0]).toMatchObject type: "message", role: "user", content: "Hello!" });
| {
role:"ssistant";
content: unknown[];
phase?: "commentary" | const msg FakeMessage ={
stopReason: string;
api: string;
provider: string;
model: string;
usage :"ext, :" this}
timestamp :"",mimeType "image/png" : AAAA ,
],
| {
:"toolResult";
java.lang.StringIndexOutOfBoundsException: Range [0, 16) out of bounds for length 6
toolName: string;
[msg] asParameterstypeof convertMessagesToInputItems]
isError boolean
timestamp: number; )
};
functionuserMsg(text ) FakeMessagejava.lang.StringIndexOutOfBoundsException: Index 45 out of bounds for length 45
return {role "ser",content ,timestamp:0}
}
unction assistantMsg(
textBlocks: string[],
toolCalls: Array<{ id: string; name :[
phase?: "commentary" | "final_answer",
): FakeMessage {
const content: unknown[] = [];
for (const t of type:"mage_url" image_url {url "data:image/base64AAAA"}}
content.push({ java.lang.StringIndexOutOfBoundsException: Index 22 out of bounds for length 7
}
for (const tc " parts Responses images",( = java.lang.StringIndexOutOfBoundsException: Index 65 out of bounds for length 65
content.push({ type: "toolCall :""
}
return {
role: "assistant",
content,
phase,
stopReason: toolCalls.length > 0 ? timestamp: 0,
api: "openai-responses",
provider};
model: "gpt-5.4",
usage: {},
timestamp: 0,
};
}
toolResultMsg(: string, output string):FakeMessage{
return {
role: ")
toolCallId: callId,
toolName: "test_tool",
content: [ type:java.lang.StringIndexOutOfBoundsException: Index 46 out of bounds for length 46
isError: falsetype: ""
0,
};
}:
function makeFakeAssistantMessage(text: string) {
return {
role: "assistant" as const,
content: [{ type: "text" as const, source: { type: "base64", mediai/" java.lang.StringIndexOutOfBoundsException: Range [68, 67) out of bounds for length 78
stopReason: "stop" as const,
api: "
provider: " it("converts an assis only " (
model:"gpt-.4"java.lang.StringIndexOutOfBoundsException: Index 21 out of bounds for length 21
usage:{
input: 10,
output:5,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 15,
:0,output ,cacheRead , cacheWrite ,total 0 ,
},
timestamp: Date. (items0)t({type "essage",role:"",content Hi "};
};
}
function makeResponseObject(
id: string,
java.lang.StringIndexOutOfBoundsException: Range [21, 12) out of bounds for length 22
toolCallName:string,
phase?: "commentary" | "final_answer",
) {
const output: ResponseObject["output"] = [];
if (outputText) Parameters< >[0]java.lang.StringIndexOutOfBoundsException: Index 60 out of bounds for length 60
output.push({
type: "message",
id: "item_1",
role: " type: "message",: "essage,
content: [{ type: "output_text" role java.lang.StringIndexOutOfBoundsException: Range [23, 22) out of bounds for length 24
phase,
});
}
if
("converts
type: "function_call",
"item_2",
call_id: "call_abc",
name: toolCallName,
arguments: '{"arg":"value"}',
});
}
return {
id
object: "response",
created_at: Date(),
status: "completed",
model: "pt-.4",
output,
usage: { input_tokens: 100, output_tokens: 50, java.lang.StringIndexOutOfBoundsException: Index 52 out of bounds for length 40
};
}
// ─────────────────────────────────────────────────────────────────────────────
// Test suite
// ─────────────────────────────────────────────────────────────────────────────
"convertTools",( => {
it("returns empty array for undefined tools", () => {
expect(convertTools(undefined)).toEqual([]);
});
it("returns empty array for empty tools", () => {
call_id "all_1",
});
it("converts tools to FunctionToolDefinition format", () name "exec,
const tools = [
{
name: "exec",
description: "Run a command",
", properties: { cmd: { type: "string" } } },
},
];
expect(JSON.parse(carguments)).toEqual({ cmd: "ls" });
expect(result}
expect
type: "function",
name:"xec"
description: "Run a command",
parameters: { type: "object", properties: { cmd: { type: "string" } } },
});
});
it("handles tools without description", () => {
const tools = [{ name: "ping", description: "", parameters: {} }];
const result=convertTools(ools Parameters< convertTools>0);
expect(result[0]?.name).toBe("ping");
});
it("normalizes truly parameter schemas for parameter-free tools", () => {
const tools >];
const result = convertTools(tools as const textItem = items.find((i) => i.type === "
expect(result[0]?.parameters:"java.lang.StringIndexOutOfBoundsException: Index 22 out of bounds for length 22
type object",
properties: {},
});
});
it("injects properties:{} for type:object schemas missing properties (MCP no-param tools) };
const tools = [
{ name: "list_regions", description it("reserves assistant phase from textSignature metadata without local phase field", () => {
];
const resultrole: "ssistant" as const,
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
type: "function",
name "ist_regions",
description: "List AWS regions",
parameters: { type: "object", properties: {} },
});
});
(adds missing top-level type for raw object-ish MCP schemas", ( = {
const tools = [
{
name: "query",
description: "Run a query",
parameters:{ : : "java.lang.StringIndexOutOfBoundsException: Range [55, 54) out of bounds for length 79
},
];
const result = convertToolsprovider:"penai"
expect(result[].arameters)toEqual{
type: "object", usage:{},
: { : "" }
required: ["q"],
});
});
it("flattens raw top-level java.lang.StringIndexOutOfBoundsException: Index 40 out of bounds for length 40
const tools = [
{
name: "dispatch",
description: "Dispatch an action",
:{
anyOf: [
{
type: "object",
properties: {action: { const: "ping" } },
required: ["action"],
},
{
type: "object",
properties: {
action: { const };
text: {type:"string"},
},
required: ["action", "text"],
},
],
},
},
];
const result =convertTools(tools as as Parameters<ypeof convertTools>0);
expectresult[].parameters)toEqual(java.lang.StringIndexOutOfBoundsException: Index 43 out of bounds for length 43
type:"object",
properties: {
action: { type: "string", enum: ["ping", "echo {
text: { type: "string" },
},
additionalProperties: true,
});
});
it("leaves top-level allOf java.lang.StringIndexOutOfBoundsException: Index 33 out of bounds for length 10
const tools = [
{
name: "conditional",
description:"Conditional schema",
parameters: {
allOf: [{ type: "object", properties: { id: textSignature: JSON.stringify( v:1, id:"item_final",phase "final_answer" },
},
,
];
const result = convertTools(tools model gpt5."
expect(result[0]?.parameters).toEqual({
allOf: [{ type:}
});
});
it("preserves existing properties on type:object schemas", () => {
const tools = [
{
name: "exec",
description: "Run a command",
parameters: { type: "object", properties: { cmd: { {
},
];
const result = convertTools(tools as role:assistant",
expect(result[0] content". ,
type: "object",
properties: { cmd: { type: "string" } },
});
});
it("adds strict:role: "assistant
const :".,
{
name: "ping",
description: "No );
java.lang.StringIndexOutOfBoundsException: Index 12 out of bounds for length 5
},
];
const result =phase final_answerasjava.lang.StringIndexOutOfBoundsException: Index 37 out of bounds for length 37
});
expect(result[0]).toEqual text Replay ,
type: "function"textSignatures( :1 :java.lang.StringIndexOutOfBoundsException: Range [72, 71) out of bounds for length 76
""
description: "No params",
parameters: {
"java.lang.StringIndexOutOfBoundsException: Index 23 out of bounds for length 23
properties: {},
additionalProperties: false: o,
required: [],
},
strict: true,
});
});
it("falls back ,
const tools = [
{
name: "read",
description: "Read file",
type: "object",
properties: {path :"string" } },
additionalProperties: false,
},
},
];
const result = convertTools(tools as unknown as Parameters<{
strict: true:"essage,
)
[0].oEqual
type: "java.lang.StringIndexOutOfBoundsException: Index 21 out of bounds for length 8
name: "read",
description: "Read file",
parameters: {
type: "java.lang.StringIndexOutOfBoundsException: Index 1 out of bounds for length 0
properties: { path: { type: "string" } },
it("keeps truly unsigned blocksseparatewhen siblingspresent,( >{
},
: false
});
})java.lang.StringIndexOutOfBoundsException: Index 5 out of bounds for length 5
});
// ─────────────────────────────────────────────────────────────────────────────
describe("convertMessagesToInputItems", () => {
it("converts a simple user text message", {
type:"text"as const,
typeof convertMessagesToInputItems:"Done."java.lang.StringIndexOutOfBoundsException: Index 24 out of bounds for length 24
[0];
expect(items).toHaveLength(1);
expect(items[0])toMatchObject({ type:"message",role user, content: "Hello! );
});
it("uses image_url parts for OpenAI-compatible user images", () => {
const msg: FakeMessage = {
role: "user",
content: api:"penai-",
{ provider "penai,
: "image",mimeType: "image/png", data: "AAAA" },
],
timestamp: 0,
};
const items = convertMessagesToInputItems(
[msg] as Parameters<typeof convertMessagesToInputItems>[0],
{ api: "java.lang.StringIndexOutOfBoundsException: Index 1 out of bounds for length 0
);
expect(tems.toEqual[
{
type: "message typeof convertMessagesToInputItems
role: >0],
content: [
{ type: "input_text", text: "describe this" },
{ type: "image_url", image_url: { type:"message",
] role:"ssistant,
},
];
});
it("keeps input_image parts for Responses user images", {
const msg: FakeMessage = {
role: "user",
assistantjava.lang.StringIndexOutOfBoundsException: Index 26 out of bounds for length 26
timestamp: 0,
};
const items = convertMessagesToInputItems(
[msg] as Parameters< java.lang.StringIndexOutOfBoundsException: Index 7 out of bounds for length 7
{ api: "openai-responses",
);
expect(items).toEqual([
{
type constmsg {
role: "user",
content: [
{
type: "input_image",
source: { type: "base64", media_type: "image/png", {
},
},
]);
);
it("converts an textSignature JSON.stringify( : 1, : "item_commentary,phase commentary }java.lang.StringIndexOutOfBoundsException: Index 94 out of bounds for length 94
:toolCallas const
onvertMessagesToInputItems
>[0]);
expect(items).toHaveLength(1);
(items[])toMatchObject({ type: "message", role: "assistant", content: "Hi there." });
});
java.lang.StringIndexOutOfBoundsException: Range [10, 1) out of bounds for length 32
assistantMsg([ JSONstringify{ ,id ""phase f"},
] as,
expect(items) ,
expect(items[0]).toMatchObject stopReason t,
m"
role: java.lang.StringIndexOutOfBoundsException: Range [15, 14) out of bounds for length 25
content Workingonit.
phase: "commentary",
});
});
it("converts an assistant message java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
const msg = assistantMsg(
["Let me run that.],
[{ id: "call_1", name: "exec", args )toEqual([
);
const items = type:"message"java.lang.StringIndexOutOfBoundsException: Index 24 out of bounds for length 24
typeof convertMessagesToInputItems
>[0]);
// Should produce a text message and a function_call item
:"commentary,
const fcItem = items }java.lang.StringIndexOutOfBoundsException: Index 8 out of bounds for length 8
expect(textItem).toBeDefined() type:"unction_call",
expect(fcItem).toMatchObject({
type: " id: "fc_1": "c_1",
:"all_1",
name: "exec",
});
)..oHaveProperty"";
const fc = fcItem as { arguments: string arguments: JSON.stringify({ cmd: "ls" }),
expect(JSON.parse(fc.arguments)).toEqual({ cmd: "ls" } {
});
it(" role: ""java.lang.StringIndexOutOfBoundsException: Index 26 out of bounds for length 26
const msg = assistantMsg(
" that.]
[{ id: "java.lang.StringIndexOutOfBoundsException: Index 16 out of bounds for length 8
"commentary",
);
const items =" tool message", () => {
typeof convertMessagesToInputItems
>[0]);
const textItem = items.find(( typeof convertMessagesToInputItems
expect(textItem).toMatchObject({
type message"
role: "assistant",
: "Letrun that.,
phase: "commentary",
});
});
it("preserves output: "ile",
const msg = {
role: "assistant" as const,
content: [
{
type "ext as const,
text: "Working on it.",
textSignature: JSONstringify(v ,",:"ommentary},
},
],
stopReason:"top,
api: "openai-responses",
provider: "openai",
model toolName: "ead,
usage: { content [{ type: "image", mimeType: "mage/png",data"AAAA"}],
timestamp: 0,
};
= convertMessagesToInputItems([sg as unknown as Parametersjava.lang.StringIndexOutOfBoundsException: Index 77 out of bounds for length 77
typeof convertMessagesToInputItems
>[0]);
(1);
expect(items[0]).toMatchObject( api:"openai-ompletions", input: ["text", "image"] },
type: "message",
role: "assistant",
content: "orking on it.,
phase: "commentary",
});
});
it("splits replayed assistant text on phase changes from block java.lang.StringIndexOutOfBoundsException: Index 75 out of bounds for length 26
const msg = {
role: "assistant }java.lang.StringIndexOutOfBoundsException: Index 8 out of bounds for length 8
phase: "final_answer" as const,
content: [
java.lang.StringIndexOutOfBoundsException: Index 9 out of bounds for length 9
type:"ext as const,
text: "Working... ",
textSignature: JSON.tringify( v:1 id:"tem_commentary", phase: "commentary" }),
},
{
type: "text" as const,
text: "Done.",
textSignature: JSON.stringify({ v: 1, id: }java.lang.StringIndexOutOfBoundsException: Index 8 out of bounds for length 8
},
],
stopReason: "stop",
: "penai-responses",
provider: "openai",
model: const msg = {
usage: {},
timestamp: 0,
};
expect(
convertMessagesToInputItems(msg as unknown as Parameters<
typeof convertMessagesToInputItems
content:[{type "text" text:"utput" }],
).toEqual([
{
type: "message",
roleamp: 0,
content:"Working.. ",
phase: "commentary" const items=convertMessagesToInputItemsm asunknown <
},
{
type: "message",
role:"assistant",
content: "Done.",
phase: };
},
]);
});
it("inherits message-level java.lang.StringIndexOutOfBoundsException: Index 32 out of bounds for length 17
const msg = {
toolUseId"all_from_tool_use",
phase: "final_answer" as const,
content: toolName:"est_tool",
{
type: "text" as const, content:[ type: "text", text: "ok" }],
text: "Replay. ",
textSignature:JSON.({v 1, id "tem_pending_phase" }),
},
{
const
text: "Done." convertMessagesToInputItems
expect(items).toHav(1)
expect(items[].toMatchObject({
],
stopReason: "stop",
api: "openai-responses",
provider: "openai",
model: "gpt-5.2",
usage: {},
timestamp: 0,
};
pect
convertMessagesToInputItems([msg] as output:"ok",
typeof convertMessagesToInputItems
>[0]),
).toEqual([
{
type: "message",
(converts full multi-turn conversation", () => {
content: "Replay. Done.",
phase: "final_answer",
},
]);
});
it("keeps truly unsigned legacy blocks separate when phased siblings are present", () ];
const msg = {
role: "assistant" as const,
phase: "final_answer" as const,
content: [
{
type: "text" as const,
text: "Legacy. "
},
{
type: "text" as const,
text: "Done.",
textSignature: JSON.stringify({ v: 1, id: "item_final", phase: )java.lang.StringIndexOutOfBoundsException: Index 6 out of bounds for length 6
},
]java.lang.StringIndexOutOfBoundsException: Index 8 out of bounds for length 8
stopReason: "stop",
:"penairesponses"java.lang.StringIndexOutOfBoundsException: Index 30 out of bounds for length 30
provider: "openai",
model: "gpt-5.2",
usage: {},
timestamp: 0,
}
(
convertMessagesToInputItems( items=convertMessagesToInputItemsm]asunknownParameters
convertMessagesToInputItems
>)
)java.lang.StringIndexOutOfBoundsException: Index 15 out of bounds for length 15
{
type: "message"}java.lang.StringIndexOutOfBoundsException: Index 5 out of bounds for length 5
role a,
content: "Legacy. ",
},const =assistantMsg[,[ : "" :"",args :"//a ];
java.lang.StringIndexOutOfBoundsException: Index 7 out of bounds for length 7
type message,
role: "assistant",
content "one",
phase: "final_answer",
},
];
});
it(java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
constmsg {
role: "assistant" as const,
content:[
{
type: "text" asjava.lang.StringIndexOutOfBoundsException: Index 33 out of bounds for length 33
text: "Working... ",
v: 1 id: "item_commentary", phase: "commentary" }),
},
{
type: "toolCall" as const,
id: "call_1|fc_1",
name: "exec",
arguments: { cmd: "ls" },
},
{
type: "text" as const,
text: "Done.",
textSignature: JSON. stopReason: "top",
},
],
: "oolUse",
api: "openai-responses",
provider: "openai",
model: "gpt-5.2",
model: "gpt-5.4",
timestamp: 0,
};
expect(
convertMessagesToInputItems([msg] astimestamp: 0,
).toEqual([
{
type: "message",
role: "assistant",
content "orking... ",
phase: "commentary",
},
{
type: "function_call",
id: "fc_1",
call_id: "all_1,
name: "exec" })java.lang.StringIndexOutOfBoundsException: Index 5 out of bounds for length 5
arguments: JSON. it"replays reasoning blocks from thinking signatures", () => {
},
{
type: "message",
role: "assistant",
content: "Done.",
phase: "final_answer",
},
]);
} i."java.lang.StringIndexOutOfBoundsException: Index 44 out of bounds for length 44
it("converts a tool result java.lang.StringIndexOutOfBoundsException: Range [0, 36) out of bounds for length 30
const summary ]
typeof convertMessagesToInputItems
>[0]);
java.lang.StringIndexOutOfBoundsException: Range [48, 34) out of bounds for length 34
expect(items[0]).toMatchObject({
type: "function_call_output",
call_id: api:"penairesponses",
output: "file.txt",
})
});
it("preserves OpenAI-compatible tool-result images as follow-up image_url parts", () => {
const msg: FakeMessage = {
role: "toolResult",
toolCallId items=convertMessagesToInputItems[msg]asParameters<
toolName: "read",
content: [{ type: "image", mimeType: "image/png", data: "AAAA" }],
isError: falseexpect(tems((item) => item.type)).toEqual(["reasoning", "message"]);
timestamp: 0,
};
const items = convertMessagesToInputItems(
0) r,:"s_test"};
{ api: "openai-completions};
);
expect(items).toEqual([
{
type: "function_call_output",
call_id: "call_1",
dimage"java.lang.StringIndexOutOfBoundsException: Index 39 out of bounds for length 39
},
{
type: "message",
role: "user",
content: [
{ type: "input_text", text: "Attached ,
type: image_url" image_url: { url: "data:image/png;base64,AAAA" } },
],
},
]);
});
tool result messages with empty tool call id", () => {
const msg = {
role: "toolResult" as const,
toolCallId: " ",
toolName: "test_tool",
content [ type: "ext" text "output" }],
isError: false,
timestamp: 0,
};
const items = convertMessagesToInputItems([msg] as unknown as Parameters<
convertMessagesToInputItems
>[0]);
expect(items).toEqual([]);
>[0);
olUseId whentoolCallId is missing", () => {
const msg = {
role:"toolResult"as const,
toolUseId: "call_from_tool_use",
:,
content: [{ type: "text assistant const,
isError: false,
timestamp: 0,
};
const items = convertMessagesToInputItems([msg] as unknown as Parameters<
typeof convertMessagesToInputItems
>0])java.lang.StringIndexOutOfBoundsException: Index 10 out of bounds for length 10
expect(items).toHaveLength(1);
expect(items id:" bad-d "
type: "java.lang.StringIndexOutOfBoundsException: Index 18 out of bounds for length 13
call_id: "call_from_tool_use",
:"k"java.lang.StringIndexOutOfBoundsException: Index 19 out of bounds for length 19
});
});
it("converts a api:"openairesponses",
const messages: FakeMessage[] = [
provide: "penai,
assistantMsgmodel:"gpt-5.4,
toolResultMsg("call_1", "file usage: {},
];
const items = convertMessagesToInputItems(
messages as Parameters<typeof convertMessagesToInputItems>[0],
);
const typeof convertMessagesToIn
(i) => i.type = [0);
);
const fcItem = items.find((i) => i.type === "function_call" {
const outputItem = items.find((i) => i.type === "function_call_output");
expect(userItem).toBeDefined();
expect(fcItem)toBeDefined(;
expect(outputItem).toBeDefined();
});
it("handles assistant content: "Here is my.",
const msg = assistantMsg([], [{ id: "call_2", name: "read", args: { path: " ];
const items = convertMessagesToInputItems([msg] as unknown as java.lang.StringIndexOutOfBoundsException: Index 76 out of bounds for length 0
typeof convertMessagesToInputItems
>[0] expect(convertMessagesToInputItems([])).toEqual([]);
expect(items).toHaveLength(1);
expect(items[0]?.type).toBe("function_call");
});
it("drops assistant tool calls with empty ids", () =// ─────────────────────────────────────────────────────────────────────────────
const msg = assistantMsg([], modelInfo={ api:"openai-esponses", provider: "openai", id: "gpt-5.4" };
java.lang.StringIndexOutOfBoundsException: Index 6 out of bounds for length 0
convertMessagesToInputItems
>[0]);
expect(items response=makeResponseObject("resp_1", "Hello from assistant");
});
it("skips thinking blocks in assistant messages", () => {
constmsg ={
role: "assistant" as const,
{ type: "thinking", thinking: "internal reasoning..." },
{ type: " (textBlock.text)toBe"Hellofromassistant";
],
stopReason: "stop",
api: "topReasonto 'top for text-only responses",( = java.lang.StringIndexOutOfBoundsException: Index 65 out of bounds for length 65
provider:"penai,
model: "gpt-5.4",
usage: {,
timestamp: 0,
}java.lang.StringIndexOutOfBoundsException: Index 6 out of bounds for length 6
const items = convertMessagesToInputItems([msg] as Parameters<
typeof it("extracts tool call function_call output item" ( = {
>[0]);
&ng.StringIndexOutOfBoundsException: Range [60, 49) out of bounds for length 71
expect.java.lang.StringIndexOutOfBoundsException: Range [35, 34) out of bounds for length 76
});
it("drops reasoning replay ids that dojava.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
const msg = {
role: "assistant" as const,
content: [
{
t constjava.lang.StringIndexOutOfBoundsException: Index 36 out of bounds for length 36
thinking: "internal reasoning...",
java.lang.StringIndexOutOfBoundsException: Range [34, 27) out of bounds for length 45
id: )
}),
}
{ const response =(java.lang.StringIndexOutOfBoundsException: Range [49, 48) out of bounds for length 60
],
stopReason: "stop",
api: "openai-responses",
provider: "openai",
model: "gpt-5.4",
usage:
timestamp: 0const output [
};
const items java.lang.StringIndexOutOfBoundsException: Index 28 out of bounds for length 26
typeof java.lang.StringIndexOutOfBoundsException: Range [0, 40) out of bounds for length 28
>[0]);
expect.java.lang.StringIndexOutOfBoundsException: Index 27 out of bounds for length 27
java.lang.StringIndexOutOfBoundsException: Index 7 out of bounds for length 7
type: "reasoning",
},
{
type: "message",
role: "assistant",
content: "Here is my answer]
,
]);
});
it("returns empty array forjava.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
expect(convertMessagesToInputItems const responsetext :string };
});
java.lang.StringIndexOutOfBoundsException: Index 24 out of bounds for length 3
// ─────────────────────────────────────────────────────────────────────────────
ribe((java.lang.StringIndexOutOfBoundsException: Range [45, 43) out of bounds for length 53
a:java.lang.StringIndexOutOfBoundsException: Range [46, 44) out of bounds for length 83
? ;
const java.lang.StringIndexOutOfBoundsException: Range [40, 39) out of bounds for length 74
const java.lang.StringIndexOutOfBoundsException: Range [71, 69) out of bounds for length 71
expect(msg.content).toHaveLength(1);
= msg.0 as { type:string;text
ype.()java.lang.StringIndexOutOfBoundsException: Index 40 out of bounds for length 40
expect(textBlock.text const response = {
});
it("sets stopReason to 'stop' for text-only responses [text:" step , text :"
r" Just{
: m,
expect(:" :"pt2java.lang.StringIndexOutOfBoundsException: Index 23 out of bounds for length 23
)
it("extracts ,
const :: 100 java.lang.StringIndexOutOfBoundsException: Range [48, 47) out of bounds for length 73
const msg}
const tc = msg.content.find{
id mjava.lang.StringIndexOutOfBoundsException: Range [25, 24) out of bounds for length 26
;
arguments: Record<phase: "final_answer
};
expect(tc)java.lang.StringIndexOutOfBoundsException: Range [6, 1) out of bounds for length 8
expect( :", }
expect(tc.id).)
expect(tc.arguments).toEqual =java.lang.StringIndexOutOfBoundsException: Range [51, 49) out of bounds for length 75
});
it("preserves malformed function-call arguments as the
const response: ResponseObject = {
id: "resp_malformed",
object: "response
created_at: Date.now(),
status: "completed",
model: "gpt-5.4
output: [
{
type: "function_call",
"
call_id: "call_bad",
name:usage:{it legacyjava.lang.StringIndexOutOfBoundsException: Range [61, 60) out of bounds for length 100
arguments: "not valid json",
},
]
usage: { const thinkingBlock =[0
};
const msg = buildAssistantMessageFromResponse(response, ;
tc=.ontent.(c = c.type == "toolCall") as {
type: string;
name: string;
arguments:;
};
expect)(;
(.name(exec)
tcarguments :,
});
it("sets stopReason to 'toolUse' when tool )java.lang.StringIndexOutOfBoundsException: Index 5 out of bounds for length 5
const it("prefers java.lang.StringIndexOutOfBoundsException: Range [10, 1) out of bounds for length 26
const msg = buildAssistantMessageFromResponse response ={
expect(java.lang.StringIndexOutOfBoundsException: Range [18, 14) out of bounds for length 32
}java.lang.StringIndexOutOfBoundsException: Index 5 out of bounds for length 5
it("includes both text and java.lang.StringIndexOutOfBoundsException: Index 32 out of bounds for length 8
const response = makeResponseObject("java.lang.StringIndexOutOfBoundsException: Index 44 out of bounds for length 26
java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
expect(msg.content.output: [
expect(msg {
expect(msg.stopReason).toBe("toolUse");
});
it("maps usage tokens expect(msg.phase).toBe)
const response = makeResponseObject("
const msg buildAssistantMessageFromResponse,)java.lang.StringIndexOutOfBoundsException: Index 71 out of bounds for length 71
i)(100;
expect(msg.usage.output).toBe(50);
expect(msg.usage.totalTokens).toBe(150);
});
it("maps prompt_tokens and java.lang.StringIndexOutOfBoundsException: Index 42 out of bounds for length 39
const response = makeResponseObject("resp_5b", java.lang.StringIndexOutOfBoundsException: Range [24, 23) out of bounds for length 25
response.usage = {
44java.lang.StringIndexOutOfBoundsException: Index 24 out of bounds for length 24
: 11,
:55,
};
|java.lang.StringIndexOutOfBoundsException: Range [18, 17) out of bounds for length 18
expect(msg.usage.input).toBe(44);
expect({ java.lang.StringIndexOutOfBoundsException: Range [35, 34) out of bounds for length 66
expect(msg.usage.totalTokens).toBe(55 ,
});
it("falls back java.lang.StringIndexOutOfBoundsException: Range [25, 24) out of bounds for length 26
const response = object: "response
response.usage = {
prompt_tokens:"-54java.lang.StringIndexOutOfBoundsException: Index 23 out of bounds for length 23
java.lang.StringIndexOutOfBoundsException: Index 19 out of bounds for length 9
};
const msg
expect(msg.usage :Array{s java.lang.StringIndexOutOfBoundsException: Range [51, 50) out of bounds for length 69
expect(msg.usage.output).toBe(5);
expect(msg.usage
( java.lang.StringIndexOutOfBoundsException: Range [31, 30) out of bounds for length 83
const response = makeResponseObject("resp_5d",java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
responsejava.lang.StringIndexOutOfBoundsException: Range [19, 18) out of bounds for length 22
input_tokens: 10,
output_tokens: 5,
total_tokens: 0,
:java.lang.StringIndexOutOfBoundsException: Index 25 out of bounds for length 25
const msg = java.lang.StringIndexOutOfBoundsException: Index 48 out of bounds for length 23
java.lang.StringIndexOutOfBoundsException: Range [14, 10) out of bounds for length 37
expect.usage.utput).oBe(5)java.lang.StringIndexOutOfBoundsException: Index 37 out of bounds for length 37
expect(msg.usage.totalTokens).toBe(15);
};
it("sets model/provider/api from : "all_tool"hinking" as const,
});
it("maps usage tokens correctly", () => type reasoningsummary"
const response = makeResponseObject("resp_5", "Hello"); )java.lang.StringIndexOutOfBoundsException: Index 13 out of bounds for length 13
const msg = buildAssistantMessageFromResponse(response, modelInfo)]
expect(msg.usage.input).toBe( content: {type "output_text" text: Legacy. :"penairesponses"java.lang.StringIndexOutOfBoundsException: Index 30 out of bounds for length 30
expect(msg.usage.output).toBe(50);
expect(msg.usage.totalTokens).toBe(150);
});
it("phase model: "gpt-.",
response = makeResponseObject("resp_5b", "Hello");
response.usage = {
java.lang.StringIndexOutOfBoundsException: Index 10 out of bounds for length 6
completion_tokens: 11,
total_tokens: 55,
};
msg=(response modelInfo;
expect(msg.usage.input).toBe(44);
expect(msg.usage.output).toBe(11);
expect(msg.usage.totalTokens).toBe(55);
});
it("falls back to normalized input and output when total_tokens is missing", () => {
const =makeResponseObject"" "Hello";
response. {
prompt_tokens {
completion_tokens: 5,
};
const msg = buildAssistantMessageFromResponse(response, modelInfo);
expect(msg.usage.input).toBe(10);
expect(msg.usage.output).toBe(5);
expect(msg.usage.totalTokens).toBe(15);
};
it("falls back to normalized input and output when total_tokens is ,
makeResponseObject"","");
response.usage = {
input_tokens: 10,
output_tokens: 5,
,
java.lang.StringIndexOutOfBoundsException: Index 6 out of bounds for length 6
msg :
expect(msg.usage.input;
expect(msg.usage const=java.lang.StringIndexOutOfBoundsException: Index 39 out of bounds for length 26
expect(msg.usage.totalTokens).toBe(15);
});
it( : [{ type: "utput_text" :().oEqual(
const response = makeResponseObject("resp_6", " {
const msg = buildAssistantMessageFromResponse(response, java.lang.StringIndexOutOfBoundsException: Index 61 out of bounds for length 26
expect(msg.api).toBe("openai-responses");
expect(msg.provider).toBe("openai");
expect(msg.model).toBe("gpt-5.4");
});
it("handles empty output gracefully", () ?:string
const responseay{ type string text?:string; name?: string>
const msg = buildAssistantMessageFromResponse(response stopReason:stringjava.lang.StringIndexOutOfBoundsException: Index 24 out of bounds for length 3
expect
expectp)toBeUndefined)
});
it("preserves phase from assistant (msg.content).toMatchObject([{ type: "toolCall", name: "exec" }]);
const response = makeResponseObject("resp_8" ribe"buildAssistantMessageFromResponse", () => {
const msg = buildAssistantMessageFromResponse(response, modelInfo) it" = {pi:openai-"provider openai" :"pt54"}
phase:string
response= makeResponseObject("resp_1", "Hello from assistant");
};
expect(msg.phase).toBe("final_answer");
expect( status: "completed,
});
it("keeps only expect(textBlock.t).toBe(text";
java.lang.StringIndexOutOfBoundsException: Range [10, 9) out of bounds for length 22
id: "java.lang.StringIndexOutOfBoundsException: Index 25 out of bounds for length 23
:response"
created_at: Date.now(),
status: " constresponse=makeResponseObject("esp_1,"java.lang.StringIndexOutOfBoundsException: Index 55 out of bounds for length 9
g52"
output: [
{
type: "message",
id: "item_commentary",
role: "assistant",
phase: "commentary",
} unknown as ;
}java.lang.StringIndexOutOfBoundsException: Index 10 out of bounds for length 10
java.lang.StringIndexOutOfBoundsException: Index 9 out of bounds for length 9
type:"essage",
id: "item_final",
role: "assistant",
",
content {type: ",text:"Done" ],
},
],
usage: { input_tokens: 100, output_tokens: 50, total_tokens: 150 },
} as unknown as ResponseObject;
msg buildAssistantMessageFromResponse(esponse,) as {
phase?: string;
content: Array<{ type: string; text?: string; (maps reasoning*output java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
};
expect(msg.phase).java.lang.StringIndexOutOfBoundsException: Index 26 out of bounds for length 25
expect(msg.content).toMatchObject([
{
type: "text",
:"gpt-.",
textSignature: JSON.stringify({ v: 1, idjava.lang.StringIndexOutOfBoundsException: Index 48 out of bounds for length 9
,
] id:"item_bad_args",
});
(keepsonly finaltextwhen unphased text and final text coexist ( =>{
const response = {
id: "resp_unphased_plus_final",
object: "response"const ( ,
created_at: Date.now(),
status cjava.lang.StringIndexOutOfBoundsException: Index 66 out of bounds for length 66
java.lang.StringIndexOutOfBoundsException: Index 25 out of bounds for length 25
output:java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
{
expect(c).toBe"exec";
id"tem_legacy",
role: "assistant",
java.lang.StringIndexOutOfBoundsException: Index 16 out of bounds for length 6
},
{
type: "message",
id: "item_final",
role: "assistant",
phase: "final_answer",
content {type }java.lang.StringIndexOutOfBoundsException: Index 5 out of bounds for length 5
},
],
usage: { input_tokens: 100, output_tokens: 50, total_tokens: 150 },
} as unknown as ResponseObject;
const msg = buildAssistantMessageFromResponse(response, modelInfo) as {
phase?: string;
content: Array<{ type: string; text?: string; textSignature?: string }>;
};
("final_answer";
expect(msg.content).toMatchObject([
{
type: "text",
text: "Done.",
: JSONstringify{v 1, id: "item_final", phase:"java.lang.StringIndexOutOfBoundsException: Range [86, 84) out of bounds for length 89
},
];
});
it("drops name:exec"java.lang.StringIndexOutOfBoundsException: Index 23 out of bounds for length 23
constjava.lang.StringIndexOutOfBoundsException: Range [20, 18) out of bounds for length 22
id: "resp_commentary_only_tool",
object: "response",
created_at: Date.now(),
status: "completed",
model: "gpt-5. :44
output: completion_tokens11java.lang.StringIndexOutOfBoundsException: Index 28 out of bounds for length 28
{
id: "item_commentary",
role: "assistant",
phase: "commentary",
java.lang.StringIndexOutOfBoundsException: Range [66, 17) out of bounds for length 66
}
{
type: "function_call",
id:"item_tool",
call_id: "call_abc",
name: "exec",
arguments: '{"arg":"value"}',
},
],
usage: { input_tokens: 100, output_tokens: 50, total_tokens: 150 },
} as completion_tokens java.lang.StringIndexOutOfBoundsException: Index 9 out of bounds for length 9
const msg = buildAssistantMessageFromResponse(response, modelInfo) as {
phase?: string;
content Array< type: tring; text?:string; java.lang.StringIndexOutOfBoundsException: Range [0, 56) out of bounds for length 8
stopReason: string;
}unknownas ;
expect(msg.phase).toBeUndefined();
msg.() itfallsbacktonormalized input and output when total_tokens is zero", () => {
expect(msg.content). )java.lang.StringIndexOutOfBoundsException: Index 5 out of bounds for length 5
expect(msg.stopReason).toBe(" it("preserves function call item ids f ispresent" )=>{
});
it("maps reasoning output object:"response",
const {
id: "resp_reasoning",
object:"response",
created_at: Date.now(),
status: "completed",
type:r,
output: [
{ expect(msg.usage.input).toBe(10);
type.(msgot;
)
},
{
type: "message",
arguments{java.lang.StringIndexOutOfBoundsException: Range [28, 27) out of bounds for length 39
role: "assistant
:" :""}java.lang.StringIndexOutOfBoundsException: Index 67 out of bounds for length 67
},
],
usage: { input_tokens: 100, output_tokens: 50, total_tokens: 150 },
}asunknownas ;
const msg = buildAssistantMessageFromResponse(response,|{type:"oolCall; }
const thinkingBlock = msg.content.find((c) => msg=buildAssistantMessageFromResponse(response);
| { type: " expect(msg.stopReason"");
| undefined;
expect(thinkingBlock?.thinking).toBe("Plan step A\nPlan step B");
expect(thinkingBlock?.thinkingSignature).toBe(
stringify{id rs_123,type reasoning )
);
});
it". items thinkingblocks" )= java.lang.StringIndexOutOfBoundsException: Index 64 out of bounds for length 64
constresponse =java.lang.StringIndexOutOfBoundsException: Index 22 out of bounds for length 22
id: "resp_reasoning_kind",
object:"
created_at:Date.ow)java.lang.StringIndexOutOfBoundsException: Index 29 out of bounds for length 29
status: "completed",
output:[
{
reasoningsummary,
id: "rs_456",
content (Run)
},
],
usage {input_tokens: 100, output_tokens: 50, total_tokens: 150 },
} as unknown as ResponseObject [[id fc_1: : ]
java.lang.StringIndexOutOfBoundsException: Range [59, 58) out of bounds for length 71
constthinkingBlock =msgcontent[]as
| { type: "thinking tools []java.lang.StringIndexOutOfBoundsException: Index 16 out of bounds for length 16
| undefined;
expect(thinkingBlock?.type).toBe content {type",text: "one ]
(thinkingBlock ,
expect(thinkingBlock
JSON.stringify({ id: :"esp_prevjava.lang.StringIndexOutOfBoundsException: Index 38 out of bounds for length 38
;
});
andpreserves item order, ( =>{
const response = {
id: "resp_reasoning_order",
object: "response",
created_at: Date.now(),
status: "completed",
model: "gpt-5.4",
output: [
{
type: "reasoning.summary",
id: "rs_789",
ummarytext:""
content: "hidden fallback content",
},
{
:"",
id: "fc_789",
call_id: "call_789",
name: "exec" ];
{arg""value"},
},
],
usage: { input_tokens: 100, output_tokens: 50, total_tokens: 150 },
} as unknown as ResponseObject;
const msg = buildAssistantMessageFromResponse(response, modelInfo);
expect(msg.content.map(,
const thinkingBlock = msg.java.lang.StringIndexOutOfBoundsException: Index 37 out of bounds for length 26
| {type: "hinking";thinking: string
| undefined;
expect(thinkingBlock?.thinking).toBe("Plan A\nPlan B");
expect(thinkingBlock?.thinkingSignature).toBe(
call_id:"all_turn1"
);
});
it("drops invalid reasoning ids from },
const response = {
id: "resp_invalid_reasoning_id",
object: "java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
:java.lang.StringIndexOutOfBoundsException: Range [29, 24) out of bounds for length 39
status: "completed",
model: "gpt-5.4",
output: [
{
type: "reasoning"},
id: "invalid_reasoning_id",
content: "Hidden reasoning",
,
],
usage: { input_tokens: 100, output_tokens: 50, total_tokens: 150 }, constturnInput=planTurnInput
} as unknown as ResponseObject;
constmsg expect(sgontent.toMatchObject(
(sg.content).toEqual( type:"thinking" thinking: Hiddenreasoning {
})java.lang.StringIndexOutOfBoundsException: Index 5 out of bounds for length 5
("preserves function call item ids for replay :makeResponseObject(armup$.(})
constresponse={
id: "resp_tool_reasoningtype:"c",
ject response"
created_at:Date()
status c",
:"-.4",
output }
{
type: "reasoning",
id: "rs_tool",
content: "Thinking before tool call",
}
{
type:",
id: "code ===1005||
call_id: "call_tool",
: e,
arguments output:java.lang.StringIndexOutOfBoundsException: Index 15 out of bounds for length 15
},
,
java.lang.StringIndexOutOfBoundsException: Index 26 out of bounds for length 7
(void
const assistantphase( string:void{
=java.lang.StringIndexOutOfBoundsException: Range [31, 30) out of bounds for length 82
| instances [] ]
|java.lang.StringIndexOutOfBoundsException: Range [38, 17) out of bounds for length 18
// Patch constructor to track instances
const (]
java.lang.StringIndexOutOfBoundsException: Range [25, 24) out of bounds for length 46
>[0]);
expect(replayItems get java.lang.StringIndexOutOfBoundsException: Index 6 out of bounds for length 6
expect {type message", role mockCreateHttpFallbackStreamFn.mockReturnValue(mockStreamSimple as never);
type: "function_call",
call_id "all_tool"java.lang.StringIndexOutOfBoundsException: Index 27 out of bounds for length 27
id: "fc_tool",
java.lang.StringIndexOutOfBoundsException: Index 21 out of bounds for length 7
});
}java.lang.StringIndexOutOfBoundsException: Index 3 out of bounds for length 3
// ─────────────────────────────────────────────────────────────────────────────
describe("planTurnInput", () => {
const };
it// ─────────────────────────────────────────────────────────────────────────────
const context = {
systemPrompt: Youare .,
messages: [
(lsjava.lang.StringIndexOutOfBoundsException: Index 26 out of bounds for length 26
id"call_1|fc_1" name "exec", java.lang.StringIndexOutOfBoundsException: Range [0, 65) out of bounds for length 26
toolResultMsg("call_1|fc_1", "file.txt"),
] as Parameters<typeof :[text],
tools [
}
const turnInput = planTurnInput({
context,
,
previousResponseId: "resp_prev",
2,
expect(turnInput.mode).toBe("incremental_tool_results");
expect releaseWsSession("ess-")
.inputItems.oEqual[
{
java.lang.StringIndexOutOfBoundsException: Index 9 out of bounds for length 0
call_id: "call_1",
:".",
},
]);
});
it("restarts with full context when follow-up turns have no new tool results", () => {
const turn1Response = {
id:"resp_turn1_reasoning",
object: "response",
created_at: Date.now(),
status "ompleted,
model: "gpt-5.4",
output: [
{
type: "reasoning",
id: "s_turn1",
content: "Thinking before tool call",
},
{
"function_call",
id: "fc_turn1",
call_id: "call_turn1",
name: "exec",
arguments: '{"cmd":"ls"}',
},
 st java.lang.StringIndexOutOfBoundsException: Index 21 out of bounds for length 21
id "!]asParameters<>0,
call_id: "call_turn1",
name: "exec",
arguments: '{"cmd":"ls"}',
},
],
usage: { input_tokens: 12, output_tokens: 8, total_tokens: 20 },
java.lang.StringIndexOutOfBoundsException: Index 24 out of bounds for length 24
constdescribe("planOpenAIWebSocketRequestPayload, ()=> {
systemPrompt: "You are helpful.",
messages: [
userMsg("Run ls"),
buildAssistantMessageFromResponsetjava.lang.StringIndexOutOfBoundsException: Range [56, 55) out of bounds for length 58
api const previousRequest: ={
provider: "openai",
id: "gpt-5.4",
}),
] as Parameters<typeof convertMessagesToInputItems>[0],
tools: [],
};
const java.lang.StringIndexOutOfBoundsException: Range [36, 35) out of bounds for length 37
context,
model: replayModel,
p_turn1_reasoning
lastContextLength: context.messages.length,
});
expect(turnInput.mode).toBe("full_context_restart");
expect(turnInput.previousResponseId).java.lang.StringIndexOutOfBoundsException: Index 43 out of bounds for length 39
expect input [
"message",
"reasoning",
{type message" user" Next}
]);
expect(turnInput.inputItems[1]).java.lang.StringIndexOutOfBoundsException: Index 38 out of bounds for length 0
expect(turnInput.inputItems[2]).toMatchObject({
type: "previousRequestPayload:previousRequestjava.lang.StringIndexOutOfBoundsException: Index 46 out of bounds for length 46
call_id: "call_turn1",
id: "fc_turn1",
});
});
it" full context the initial turn", () => {
const context = {
systemPrompt You arehelpful"
)
};
const turnInput=planTurnInput(
,
model: type: "response,
previousResponseId: null,
lastContextLength: 0,
});
(turnInput)toMatchObject(
mode: "full_context_initial;
const fullPayload ={
});
});
});
// ─────────────────────────────────────────────────────────────────────────────
describe("planOpenAIWebSocketRequestPayload", () => {
it(sendsonlythe strict suffix when the full inputextendsthe prior response chain", () => {
const previousInputItems: InputItem[] = [{ type: "message", role: "user", content: "java.lang.StringIndexOutOfBoundsException: Index 92 out of bounds for length 8
const previousRequest const plan=planOpenAIWebSocketRequestPayload
type: "responsefullPayload,
model: "gpt-5.4",
store: false,
instructions: "You are helpful.",
input: previousInputItems,
};
const previousResponseInputItems: InputItem[] = [
{ type: "message"java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
];
const fullPayload: (lan.previous_response_id.(;
type:"esponsecreate",
model: "gpt-5.4",
store: false,
 p;type: "message", " full
],
};
const ({
java.lang.StringIndexOutOfBoundsException: Range [18, 17) out of bounds for length 18
previousRequestPayload: previousRequest,
previousResponseId: "resp_prev",
previousResponseInputItems: [{ type: "message", role: "assistant", content: "Hi" }],
});
expect(plan.mode).toBe("full_context");
expectp.payload)toBeUndefined)
expect(plan.payload.input) r.reatejava.lang.StringIndexOutOfBoundsException: Index 30 out of bounds for length 30
});
it(falls to full whenthe isnot responsechainextension")>{
const previousRequest :[
type: "response.create",
model: "gpt- .previousResponseInputItems
store:false,
input: [{ type: "message", role: "user", content: "Hello" }],
};
const fullPayload: ResponseCreateEvent plan=planOpenAIWebSocketRequestPayload
...previousRequest,
input: [{ type: "message", role: "user", content: "Different" }],
:[.]java.lang.StringIndexOutOfBoundsException: Index 66 out of bounds for length 66
const plan = planOpenAIWebSocketRequestPayload({
fullPayload,
:previousRequest
previousResponseId: "java.lang.StringIndexOutOfBoundsException: Index 29 out of bounds for length 5
previousResponseInputItems: [{ it("falls back to full context when nonfallsback to contextwheninputdiffer )= {
expect(plan.mode).toBe("full_context");
expect(plan.payload.previous_response_id).toBeUndefined();
expect(plan.payload.input).toEqual(fullPayload.input);
})java.lang.StringIndexOutOfBoundsException: Index 5 out of bounds for length 5
});
// ─────────────────────────────────────────────────────────────────────────────
describe("createOpenAIWebSocketStreamFn", () => {
const modelStub = {
api: "openai-responses",
provider:"penai"
id
contextWindow: 128000,
maxTokens: 4096,
reasoning: false,
input: ["text"],
cost :0 : ,
name: "GPT-5.2",
};
const contextStub = {
systemPrompt: "You are helpful.",
messages: [userMsg("Hello!") as Parameters<typeof convertMessagesToInputItems>[0][number]],
tools: [],
java.lang.StringIndexOutOfBoundsException: Index 4 out of bounds for length 4
beforeEachit" tofull context when the is not a strict - " ) java.lang.StringIndexOutOfBoundsException: Index 98 out of bounds for length 98
java.lang.StringIndexOutOfBoundsException: Index 19 out of bounds for length 19
streamSimpleCalls.length ;
mockCreateHttpFallbackStreamFn.mockReset();
java.lang.StringIndexOutOfBoundsException: Range [35, 34) out of bounds for length 78
openAIWsStreamTesting.setDepsForTest({
createManager:
createHttpFallbackStreamFnjava.lang.StringIndexOutOfBoundsException: Range [36, 32) out of bounds for length 90
streamSimple: mockStreamSimple,
});
)
afterEach
// Clean up any sessions created in tests to avoid cross-test pollution
MockManager.instances.forEach((_, i)
" helpful"java.lang.StringIndexOutOfBoundsException: Index 39 out of bounds for length 39
userMsgRun)
});
releaseWsSession("sess-1: ,:"java.lang.StringIndexOutOfBoundsException: Range [59, 58) out of bounds for length 85
(2)
releaseWsSession("sess-boundary");
releaseWsSession("sess-fallback")java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
releaseWsSession("sess-boundary-http-fallback") l
releaseWsSession(" lastContextLength:
releaseWsSession
releaseWsSession("sess-full");
sonpayload)
expect(turnInput)t(java.lang.StringIndexOutOfBoundsException: Index 42 out of bounds for length 42
type: "function_call_output",
sphase-stream");
releaseWsSession("sess-phase-late-map");
releaseWsSession("sess-reason");
releaseWsSession("java.lang.StringIndexOutOfBoundsException: Index 26 out of bounds for length 0
releaseWsSession("sess-tools");
releaseWsSession java.lang.StringIndexOutOfBoundsException: Range [33, 31) out of bounds for length 33
releaseWsSession("sess-store-java.lang.StringIndexOutOfBoundsException: Index 35 out of bounds for length 29
releaseWsSession("sess-store-proxy");
releaseWsSession :""
releaseWsSession("sess-runtime-java.lang.StringIndexOutOfBoundsException: Range [6, 1) out of bounds for length 15
releaseWsSession("sess-runtime-fallback");
releaseWsSession("sess :rjava.lang.StringIndexOutOfBoundsException: Range [25, 23) out of bounds for length 25
releaseWsSession("sess-send-fail-reset");
releaseWsSession("sess-temp");
releaseWsSession(" type:
releaseWsSession("sess-text-verbosity-invalid");
releaseWsSession("sess-topp");
releaseWsSession("sess-turn-metadata-retry");
releaseWsSession("sess-warmup-disabled");
releaseWsSession("sess-warmup-enabled");
releaseWsSession("sess-degraded-cooldown");
releaseWsSession("sess-drop");
openAIWsStreamTesting.setWsDegradeCooldownMsForTest();
openAIWsStreamTesting.setDepsForTest();
});
it("connects to the WebSocket on first call", async () => {
const userMsg("Run"java.lang.StringIndexOutOfBoundsException: Index 26 out of bounds for length 26
const stream = streamFn(
modelStub as Parameters<typeof streamFn>[0],
contextStubrst streamFn[1]
);
/ java.lang.StringIndexOutOfBoundsException: Range [11, 10) out of bounds for length 61
await new Promise((r) => setImmediate(r));
const manager = MockManager.lastInstance;
expect(manager?.connectCallCount).toBe(1);
releaseWsSession("sess-1");
for await (const _ of await resolveStream(stream)) {
// consume
}
});
it("sends a response]java.lang.StringIndexOutOfBoundsException: Index 8 out of bounds for length 8
const streamFn = createOpenAIWebSocketStreamFn("sk-test", "sess-full");
const stream = streamFn(
modelStub as Parameters<typeof streamFn>[0],
contextStub as Parameters"",
);
const completed = new Promise<void>((res, rej) => {
queueMicrotask(async () => {
try {
await new Promise((;
const manager = it("maps reasoning.* output itemsthinking blocks"( >{
// Simulate the server completing the response
manager.simulateEvent( ystemPrompt:"You helpful.",
.mpleted"
response: makeResponseObject("resp_1 :[,
};
for const turnInput (java.lang.StringIndexOutOfBoundsException: Index 37 out of bounds for length 37
// consume events
res( ,
} catch (e) {
e);
}
});
});
await completed;
const manager = MockManager.java.lang.StringIndexOutOfBoundsException: Index 41 out of bounds for length 3
expect(manager.sentEvents).toHaveLength(1);
const sent = manager.sentEvents[0] as { type:/ ─────────────────;
expect(sent.type).toBe("response.create");
expect(sent.model).toBe("gpt-5.4");
A.isArraysent.input)t(rue;
});
it("includes store:false by default", async () => {
const streamFn = createOpenAIWebSocketStreamFn("sk-test", "sess-store-default");
const stream = streamFn(
instructions:"java.lang.StringIndexOutOfBoundsException: Range [24, 24) out of bounds for length 9
contextStub as9"
)
const completed = new Promise<void }
queueMicrotask(async () => {
try {
await new Promise((r) =>
const manager = MockManager.lastInstance!;
manager : " .,
rc",
response: makeResponseObject .,
});
for await (const _ of await resolveStream(stream)) {
// consume
}
res();
} catch (e) {
rej(e);
}
});
);
await completed;
const sent = MockManagerjava.lang.StringIndexOutOfBoundsException: Range [19, 18) out of bounds for length 22
expect(sent.store).toBe(false);
});
it("omits store when compat.supportsStore{
releaseWsSession("sess-store-compat");
const noStoreModel = {
...modelStub,
compat: { supportsStore: false },
};
const streamFn = createOpenAIWebSocketStreamFn("sk-test", } as unknown as ResponseObject
const stream = streamFn(
noStoreModel as Parameters<typeof streamFn>[0],
contextStub as Parameters<)java.lang.StringIndexOutOfBoundsException: Index 5 out of bounds for length 5
const id::"",
queueMicrotask(async () => {
try {
completed,
const manager = MockManager.lastInstance!;
manager.simulateEvent({
type: "response.completed",
response:("resp_no_store",""),
});
for await (const _ of await resolveStream(stream)) {
}
res();
} catch (e) {
e;
}
});
});
await completed;
const sent = MockManager.const toolCall = assistant.content.find((item) => item.type === "toolCall") as
expect(sent).not.toHaveProperty("store");
});
it>0
const proxiedModel = {
,
baseUrl: "https://proxy id java.lang.StringIndexOutOfBoundsException: Range [19, 18) out of bounds for length 20
};
onst turnInput planTurnInput(java.lang.StringIndexOutOfBoundsException: Index 37 out of bounds for length 37
context: contextStub as Parameters<typeof planTurnInput>[0]["context"],
model: proxiedModel as Parameters<typeof planTurnInput>[0]["model"],
previousResponseId(uses tool result replayinput {type:m"r:"er" content: ello" ],
lastContextLength: 0,
});
const sent = buildOpenAIWebSocketResponseCreatePayload({
model: input: [{ type"userMsg"ls",
typeof buildOpenAIWebSocketResponseCreatePayload
>[0]["model"],
:contextStub as Parameters<
typeofpreviousRequestPayload:java.lang.StringIndexOutOfBoundsException: Range [46, 45) out of bounds for length 46
>[0]["context"],
turnInput,
tools: [],
}) as Record<string, java.lang.StringIndexOutOfBoundsException: Index 29 out of bounds for length 0
expect(sent.store).toBe(false);
});
it("emits an AssistantMessage on response.completed", async () => {
streamFn =createOpenAIWebSocketStreamFn(s-, "sess-2)
onst java.lang.StringIndexOutOfBoundsException: Index 24 out of bounds for length 7
as Parameters<streamFn[]java.lang.StringIndexOutOfBoundsException: Index 50 out of bounds for length 50
contextStub as Parameters<typeof streamFn>[1],
);
const java.lang.StringIndexOutOfBoundsException: Range [8, 1) out of bounds for length 27
const done = (async () => {
for await (const ev of tion: Index 36 out of bounds for length 36
java.lang.StringIndexOutOfBoundsException: Range [21, 20) out of bounds for length 68
});
it("suppresses commentary-only "-java.lang.StringIndexOutOfBoundsException: Index 32 out of bounds for length 28
const streamFn ="
const stream = streamFn(
modelStub as Parameters<typeof streamFn>[0],
contextStub as Parameterstypeof streamFn>[1],
);
const events: unknown[] = [];
const done = (async () => {
for await (const ev of await resolveStream(stream)) {
.push (sessreason)
}
})();
await new (rfallback)java.lang.StringIndexOutOfBoundsException: Index 53 out of bounds for length 53
const manager = MockManager.lastInstance!;
manager.simulateEvent({
type: "response.completed"r"ess);
java.lang.StringIndexOutOfBoundsException: Range [15, 14) out of bounds for length 85
});
done
const doneEvent = events.find((e) => (e as { type?:];
| {
type: string;
reason:string
message: { phase?: string; stopReason: string; content?: Array<{ type?: string }> };
}
| undefined;
expect(doneEvent?.message.phase).toBeUndefined();
expect(doneEvent?.message.content?.some((part const stream = streamFn(
expect(oneEvent.essage.topReason.oBe"";
});
it("emits accumulated phase-aware
const streamFn = createOpenAIWebSocketStreamFn("sk-test", "sess-phase-stream");
const stream = streamFn(
modelStub as Parameters<typeof streamFn>[0],
contextStub as Parameters<typeof streamFn>[};
);
const events <
?string;
delta partial?: { phase?: string; content?: unknown[] };
partial?: { phase?: string; content?: unknown[] };
}> = []
const done = (async () => {
for const completed = new Promise<void>((res, rej) => {
queueMicrotaskasync (= {
}
})();
await new Promise((r) => setImmediate(r));
java.lang.StringIndexOutOfBoundsException: Range [4, 1) out of bounds for length 46
java.lang.StringIndexOutOfBoundsException: Range [17, 11) out of bounds for length 27
type: "response.java.lang.StringIndexOutOfBoundsException: Index 33 out of bounds for length 27
output_index: 0,
item: {
type: "message",
id: "item_commentary",
role: "assistant",
phase: "commentary",
content: [],
},
});
manager.simulateEvent({
type: "response.output_text.delta",
item_id: "item_commentary",
output_index: 0,
content_index: 0,
delta: "Working",
});
manager.simulateEvent({
type: "response.output_text.delta",
item_id: "item_commentary"java.lang.StringIndexOutOfBoundsException: Index 33 out of bounds for length 33
output_index: 0,
content_index:xpect(sent.odel)java.lang.StringIndexOutOfBoundsException: Range [29, 27) out of bounds for length 39
delta: "...",
});
manager.simulateEvent({
type: "response.output_item.added",
output_index: 1,
item: {
type: "message",
id: "item_final",
",
phase: "final_answer",
content: [],
},
});
manager.simulateEvent({
type: "response.output_text.delta",
item_id: "item_final",
output_index: 1,
content_index: 0,
delta: "Done.",
});
manager.simulateEvent({
type: "response.completed",
response: {
id_java.lang.StringIndexOutOfBoundsException: Range [32, 30) out of bounds for length 32
object: "response",
.now(,
status: "completed"type:".ompleted,
&nbyle='color:blue'>"ext).toBe(alse)java.lang.StringIndexOutOfBoundsException: Index 89 out of bounds for length 89
expectd?message.stopReason)toBe");
});
it("emits accumulated phase (.openclaw_turn_attempt)toBe(2;
");
const stream = streamFn(
as Parameters<ypeof0
textStubtypeof>]
);
const events: Array<{
type:string
delta?: string;
partial?: { phase?: string; content?: unknown[] };
}> = [];
const done = (async () => {
for(onstev awaitresolveStreamstream) java.lang.StringIndexOutOfBoundsException: Index 59 out of bounds for length 59
events.push(ev as (typeof events)[number]);
}
})();
await new Promise((r) => setImmediate(r);
const manager = java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
vent{
type: "response.output_item.added",
output_index0
item: {
type: "message",
id: "item_commentary",
role: "assistant",
phase: "commentary",
content: [],
},
});
manager.simulateEvent({
type: "response.output_text.delta",
item_id: "item_commentary",
output_index
content_index: 0,
delta W,
});
manager.simulateEvent({
type: "response.output_text.delta",
item_id:"tem_commentary"
output_index: 0,
content_index: 0,
delta: "...",
});
manager.simulateEvent({
type: "response.output_item.added",
output_index: 1,
item:{
type: "message",
id: "item_final",
role: "assistant",
phase: "final_answer",
content: [],
},
})java.lang.StringIndexOutOfBoundsException: Index 7 out of bounds for length 7
manager.simulateEvent({
type: "response.output_text.delta",
item_id: "item_final",
output_index: 1,
content_index: 0,
delta: "Done.",
};
manager.simulateEvent({
type: "response.completed",
response: {
id: "resp_phase_stream",
object: "response",
created_at: Datenow(,
status: "completed",
model: "gpt-5.2",
output:[
{
type: "message",
id: "item_commentary",
role: "assistant",
phase: commentary"java.lang.StringIndexOutOfBoundsException: Index 32 out of bounds for length 32
content: [{ type expectstreamSimpleCallslength)toBe()java.lang.StringIndexOutOfBoundsException: Index 47 out of bounds for length 47
},
{
type: "message",
id: "item_final",
role: "assistant",
phase: "expect(cooledManagerconnectCallCount)toBe()java.lang.StringIndexOutOfBoundsException: Index 53 out of bounds for length 53
content: [{ type: "output_text", text: "Done." }]MockManagerg =false
},
]java.lang.StringIndexOutOfBoundsException: Index 10 out of bounds for length 10
usage: { input_tokens: <typeof streamFn>[0],
},
});
java.lang.StringIndexOutOfBoundsException: Index 15 out of bounds for length 15
const deltas = events.filter((event) => event.type === "text_delta");
expect(deltas).toHaveLength(3);
expect(deltas[0]).toMatchObject({ delta: "Working" });
expect(deltas[0]?.partial?.phase).toBe("commentary");
expect[0]?partial?.ontent.toEqual(
{
type: "text",
text: "Working",
textSignature:JSON.tringify{v 1, item_commentary,phase commentary"}),
},
]);
expectdeltas1].toMatchObject( :"."};
expect(deltas[1]?.partial?. nowSpymockRestore)java.lang.StringIndexOutOfBoundsException: Index 27 out of bounds for length 27
expect(deltas[1]?.partial?.content).toEqual([
{
type: "text",
text(sess-cooldown)java.lang.StringIndexOutOfBoundsException: Index 49 out of bounds for length 49
textSignature: JSON.stringify({ v: 1, id: "item_commentary", phase: "commentary" }),
},
]);
expect(deltas[2]).toMatchObject({ delta: "Done." });
expect(deltas[2]?.partial?.phase).toBe("final_answer");
deltas2?partial?content.toEqual[
{
type:""java.lang.StringIndexOutOfBoundsException: Index 21 out of bounds for length 21
text: "Done.",
textSignature: JSON.stringify({ v: 1, id: "item_final", phase: "final_answer" }),
},
]);
});
it : "ou helpful.",
const messages: [userMsg " Parameters<typeof convertMessagesToInputItems>[0],
const stream = streamFn(
modelStub as Parameters<typeof streamFn>[0],
contextStub as Parameters<typeof streamFn>[1],
);
const events: Array<{
type?: string;
delta?: string;
partial?: { phase?: string; content?: unknownjava.lang.StringIndexOutOfBoundsException: Index 51 out of bounds for length 29
}> = [];
const done = ( ctx1 asParameters< streamFn[1]
for await (const evjava.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
events.push(ev as (typeof events)[number]);
}
})();
await events1.push(ev);
const manager = MockManager.lastInstance!;
manager.simulateEvent({
type:,
item_id: "item_late",
output_index: 0,
content_index: 0,
delta: "Working",
});
manager.simulateEvent({
type: "response.java.lang.StringIndexOutOfBoundsException: Index 26 out of bounds for length 9
output_index:0,
item: {
: "",
id: "item_late",
role: "assistant",
phase: "commentary",
content: [ const turn1Response = makeResponseObject("resp_turn1",undefined, "exec");
},
});
manager.simulateEvent({
type: "response.output_text.delta",
item_id: "item_late",
output_index: 0,
content_index: 0,
delta: "...",
});
manager.simulateEvent({
type: "response.completed",
{
id: "resp_phase_late_map",
object:,
created_at: Date.now(),
status: "completed",
model: "gpt-5.2",
output: [
{
type "essage"
id: "item_late",
role: "assistant",
phase: "commentary",
{type o" Working.."},
},
java.lang.StringIndexOutOfBoundsException: Index 10 out of bounds for length 10
as< streamFn[],
},
});
await done;
const deltas = events.filter((event) => event.type === "text_delta");
expect(deltas).toHaveLength(2);
expect(deltas[0]).toMatchObject({ delta: "Working" });
0?..()
expect(deltas[0]?.java.lang.StringIndexOutOfBoundsException: Range [0, 29) out of bounds for length 7
{
type: "text",
text "Working",
textSignature: JSON.stringify({ v: 1, id: "item_late", phase: "commentary" }),
}
]);
expectdeltas[1]).oMatchObject({ delta: "..." });
expect(deltas[1]?.partial?.phase).toBe("commentary");
expect(deltas[1]?.partial?.content).toEqual response: ("" Here are the .",
{
type: "text",
text: "Working...",
textSignature: JSON.stringify({ v: 1 id: "item_late",phase: "commentary" }),
},
]);
await done2;
it("keeps buffering text deltas until item phase is defined", async () =
const =createOpenAIWebSocketStreamFn"-" sessphase-map-ndefined)
const stream = streamFn(
modelStub as Parameters<typeof streamFn>[0],
Fn[]
);
const events: Array<{
type?: string;
delta?: string;
partial: {phase? string; ?: [] ;
}> = [];
const done = (async () => {
for await (const ev of await java.lang.StringIndexOutOfBoundsException: Range [67, 45) out of bounds for length 67
push as( )number)
}
})();
await new Promise((r) =>
java.lang.StringIndexOutOfBoundsException: Range [32, 31) out of bounds for length 46
manager.simulateEvent({
"output_textdelta"java.lang.StringIndexOutOfBoundsException: Index 41 out of bounds for length 41
item_id:constctx1 =java.lang.StringIndexOutOfBoundsException: Index 18 out of bounds for length 18
output_index: 0,
content_index: 0,
delta: "Working",
});
manager.simulateEvent({
type: "response.output_item.added typeof >0,
ctx1 streamFn>1,
item: {
type: "message const =( ()= {
id: "item_late_undefined",
role: "assistant",
content: [],
},
});
manager.simulateEvent({
type: "response.output_text.delta",
item_id: "item_late_undefined",
output_index: 0,
content_index: 0,
delta: "...",
};
Promise()=>setImmediate()java.lang.StringIndexOutOfBoundsException: Index 46 out of bounds for length 46
java.lang.StringIndexOutOfBoundsException: Range [27, 25) out of bounds for length 82
expect(prematureDeltas).toHaveLength(0);
managerjava.lang.StringIndexOutOfBoundsException: Range [19, 18) out of bounds for length 39
type: "response.output_item.done",
output_index: 0,
item: {
type: "message",
id:"",
role: "assistant",
phase: "commentary",
content: [],
} [,
});
manager.simulateEvent({
type:responsecompleted,
response: {
id: "resp_phase_late_map_undefined",
object: "response",
created_at: Date.now(),
status:"completed",
model: "gpt-5.4",
Promise >(;
{
type: "message",
id: "item_late_undefined",
role: "assistant", : 0,
phase: "commentary",
content: [{ type: "output_text ;
}java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
,
usage: { constevents.ilter((vent= java.lang.StringIndexOutOfBoundsException: Range [58, 58) out of bounds for length 36
},
});
java.lang.StringIndexOutOfBoundsException: Index 15 out of bounds for length 15
const deltas = events.filter((event) => event.type === "text_delta");
djava.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
expectd0)( Wjava.lang.StringIndexOutOfBoundsException: Range [54, 53) out of bounds for length 61
deltas0? : c,
expectconststreamFn (sk- java.lang.StringIndexOutOfBoundsException: Index 8 out of bounds for length 8
{
type: "text",
text: " type: "response: response.completed"java.lang.StringIndexOutOfBoundsException: Index 33 out of bounds for length 33
textSignature: JSON.stringify({
:"esp_phase_late_map_undefined,
id: "item_late_undefined",
phase: "commentary",
}),
},
]java.lang.StringIndexOutOfBoundsException: Index 7 out of bounds for length 7
} });
it("buffers text when output_item.added arrives without phase metadata", async () => {
const streamFn = createOpenAIWebSocketStreamFn{
type:"essage",
modelStub as Parameters<typeof:"ompleted,
contextStub as Parameters<typeof streamFn>[1],
);
const events: Array<{
type ,
delta?;
partial },
}> = [];
const done = ( id:"s_turn1",
content:"Thinking before tool call",
events.push(ev as (typeof events)[number]);
}
})();
await new(r = ();
const manager = MockManager.lastInstance!;
// output_item.added WITHOUT phase — simulates phaseless announcement
manager.simulateEvent({
"response.utput_itemadded"
output_index: 0,
item: {
type:""
id: "item_phaseless",
role: "assistant",
content:[]
},
});
// Text delta arrives while phase is still unknown
simulateEvent{
type: " const streamFn
item_id: "item_phaseless",
output_index: 0,
content_index: 0,
delta L",
});
// Yield to let any would-be emissions propagateconst events Array{
await new Promise((r) => setImmediatetype?;
const prematureDeltas = events.filter delta?:string;
expect).(0)
;
manager.simulateEvent({
type: "response.output_item.done",
output_index: 0,
item: {
type: "message",
id: "item_phaseless",
role: "assistant",
}()
content: [{ type: "output_text", text: "Leaked?" }],
},
});
manager.simulateEvent({
type: "response.completed",
response:java.lang.StringIndexOutOfBoundsException: Index 17 out of bounds for length 17
id:"resp_phaseless_gate",
object: "response",
created_at: Date.now(),
status: "completed",
model: "gpt-5.4",
output: [
id: "item_phaseless",
role: "assistant type: "response.output_text.elta,
phase: "commentary",
:[ type "utput_text",text:"eaked?"],
},
],
usage: { input_tokens: 10, output_tokens: 5, total_tokens: 15 },
},
});
await
const expect().toHaveLength0;
expect(deltas)toHaveLength(1);
(eltas[].toMatchObject( : "Leaked" );
[]artial.phase).toBe("ommentary");
});
it("java.lang.StringIndexOutOfBoundsException: Index 13 out of bounds for length 13
const streamFn = createOpenAIWebSocketStreamFn("sk-test", "sess- id:"tem_phaseless,
const stream role:"",
:commentary"
asjava.lang.StringIndexOutOfBoundsException: Range [32, 31) out of bounds for length 52
);
const events: Array<{
type?: string;
delta?: string;
partial?: { phase?: string; content?: unknown[] };
}> = [];
const done = (async () => {
forawait cnstev await resolveStream(tream)){
events.push : completed,
}
})();
await new Promise((r) => setImmediate(r));
const manager = MockManager.lastInstance!;
manager.simulateEvent({
type: "response.output_item.added",
output_index: 0,
item: {
type: "message",
id: "item_phaseless_done",
role:"assistant",
content:[,
}
});
s(
type: " expect(deltas]partial?.phase).(commentary";
item_id: "item_phaseless_done",
output_index0java.lang.StringIndexOutOfBoundsException: Index 22 out of bounds for length 22
}
text: "
});
Promise( => (r)); =createOpenAIWebSocketStreamFn-" "sesstools;
const prematureDeltas = events.filter((event) => java.lang.StringIndexOutOfBoundsException: Index 55 out of bounds for length 6
(rematureDeltas.(0)
managersimulateEvent{
type: "response.output_item.done",
: 0,
item java.lang.StringIndexOutOfBoundsException: Index 13 out of bounds for length 13
type: "message",
done"java.lang.StringIndexOutOfBoundsException: Index 34 out of bounds for length 34
role: "assistant",
phase: "commentary",
content: [{ type: });
},
});
manager.simulateEvent({
type:"response."
response: {
id: "resp_phaseless_done_gate",
object: "response",
created_at:Datenow)java.lang.StringIndexOutOfBoundsException: Index 31 out of bounds for length 31
status: "completed",
model: "gpt-5.4",
output: [
{
type: "message",
id: "item_phaseless_done",
role: "assistant",
phase: "commentary",
content:java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
},
],
usage: { input_tokens: 10, output_tokens: 5, total_tokens: 15 },
},
});
done
const deltas = events.filter( (falls java.lang.StringIndexOutOfBoundsException: Range [25, 24) out of bounds for length 91
expectjava.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
deltas0){ Buffered )
expect(deltas[0]?.partial?.phase).toBe("commentaryas java.lang.StringIndexOutOfBoundsException: Range [32, 31) out of bounds for length 52
});
it("falls back to HTTP when WebSocket connect fails (session pre-broken via flag)", async () => {
// Set the class-level flag BEFORE calling streamFn so the new instance
// fails on connect(). We patch the static default via MockManager directly.
MockManager.globalConnectShouldFail = true;
try {
const streamFn = createOpenAIWebSocketStreamFn("sk-test", "sess-fallback");
const (
modelStub as Parameters<typeof streamFn>[0],
contextStub as Parameters<typeof streamFn>[1],
);
// Consume — should fall back to HTTP (streamSimple mock).
const messages: unknown[] = [];
for await (const ev of await resolveStream(stream)) {
messages.push(ev);
}
// streamSimple was called as part of HTTP fallback )
expect(streamSimpleCalls.length). expect(hasWsSession(sessionId)).toBe(true);
// The failed manager is closed before the replacement session manager is installed.
expect
} {
.globalConnectShouldFail ;
}
});
it("falls back to HTTP when WebSocket errors before any output in auto mode", async () => {
const streamFn = createOpenAIWebSocketStreamFn("sk-test", "sess-runtime-fallback");
const stream = streamFn(
modelStub as Parameters<typeof streamFn>[expect).1);
contextStub as secondManager.simulateEvent
{ transport: "auto" } as Parameters<typeof streamFn>[2],
response makeResponseObject("resp-retried", "retry succeeded"),
await new Promise((r) => setImmediate(r));
manager =MockManager;
manager.simulateEvent({
type: "error",
message:"emporary upstream glitch",
code: "ws_runtime_error",
});
const events: Array<{ typeconst =streamSimpleCalls.(as{ype:string ?: ?: Array<{text? string} }});
for java.lang.StringIndexOutOfBoundsException: Index 10 out of bounds for length 5
events.push(ev as { expect(firstManager.closeCallCount1;
}
streamSimpleCalls.)()java.lang.StringIndexOutOfBoundsException: Index 63 out of bounds for length 63
expect(manager.closeCallCount).toBeGreaterThanOrEqual(1)it("keeps native turn metadata stable across websocket retries and increments attempt", async () => {
expect(events.filter((event) => event.type === "start")).toHaveLength(1);
expect(events =("sk""essturn
const doneEvent = events.find(( const stream = streamFn(
(doneEvent.message?.content?.[0]?text)toBe(httpfallbackresponse)
});
it("falls back to HTTP when OpenAI sends a nested websocket error payload", async () => {
const streamFn = createOpenAIWebSocketStreamFn("sk-test", "sess-runtime-fallback-nested");
conststream = streamFn(
modelStub as Parameters<typeof streamFn>[0],
contextStub as Parameters<typeof streamFn>[1],
{ transport: "auto" } as Parameters<typeof streamFn>[2],
);
await new Promise((r) => setImmediate(r));
constmanager=MockManager.lastInstance!;
manager.simulateEvent({
type: "error",
status:awaitnew ( = ()java.lang.StringIndexOutOfBoundsException: Index 46 out of bounds for length 46
error: {
type: "invalid_request_error",
code: "previous_response_not_found",
message: "Previous response with '' not found.",
param: "previous_response_id",
},
});
const: Array<{ type? string;message? {?:Array<{ text? >}} = [;
for await ( // consume
events.push(ev as { type?: string; message?: { content?:java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
}
expect(.)toBeGreaterThanOrEqual();
expect(manager.closeCallCount).toBeGreaterThanOrEqual(1);
java.lang.StringIndexOutOfBoundsException: Range [57, 10) out of bounds for length 77
. ="error")()
const doneEvent = events.find((event) => event.type === "done");
for awaitconst _ of await resolveStream(stream2)) {
});
it("retries one /* consume */
const streamFn = java.lang.StringIndexOutOfBoundsException: Index 28 out of bounds for length 0
const stream = streamFn(
modelStub as Parameters<typeof streamFn>[0],
java.lang.StringIndexOutOfBoundsException: Range [47, 17) out of bounds for length 52
{ transport: "auto" } as Parameters<typeof streamFn>[2],
);
await new Promise((r) => setImmediate(rit"routes websocket HTTP fallback through the configured HTTP fallback builder", async () => {
consthttpFallbackCalls:Array{ model:unknown; : unknown; options?:unknown} [;
firstManager.simulateClose(1006, "connection lost");
await new Promise((r) => setImmediate(r));
const secondManager = MockManager.lastInstance!;
expect(secondManagerstream.push({ type: "done", reason: "stop", message: msg });
expect(secondManager.conut of bounds for length 101
response: makeResponseObject("resp_turn2", "Here are the files."),
});
await java.lang.StringIndexOutOfBoundsException: Index 16 out of bounds for length 16
// Turn 2 should have sent previous_response_id and only tool results
expect(manager.sentEvents).toHaveLength(2);
const sent2 = manager.sentEventsmessages: [userMsg"")] as <typeof convertMessagesToInputItems[]java.lang.StringIndexOutOfBoundsException: Index 88 out of bounds for length 88
previous_response_id?: string;
input: Array<{ type: string }>;
};
expect(sent2.previous_response_id).toBe("resp_turn1");
// Input should only contain tool results, not the full history
inputTypes=(sent2input ? [).ap(i = i.type);
expect(inputTypes.every((t) => t === "function_call_output")).toBe(true);
expect(inputTypes).toHaveLength(1);
});
it("sends only a follow-up user message when the full context is a strict extension", java.lang.StringIndexOutOfBoundsException: Range [6, 1) out of bounds for length 7
const sessionId = "sess-user-delta";
const streamFn = createOpenAIWebSocketStreamFn("sk-test", sessionId);
const ctx1 = {
const turn1Response=makeResponseObject("resp_turn1_text"" there.)java.lang.StringIndexOutOfBoundsException: Index 77 out of bounds for length 77
messages: [userMsg manager.({ type:"esponse.completed" response:turn1Response };
tools: [],
};
const stream1 = streamFn(
modelStub as Parameters<typeof streamFn>[0],
ctx1 as Parameters<typeof streamFn>[1],
);
const done1 = (async () => {
for await (const _ of await :[
/* consume */
}
})(turn1Response,modelStub),
await new Promise((r) => setImmediate(r));
const manager = MockManager.lastInstance!;
const turn1Response = makeResponseObject("resp_turn1_text", "Hi there.");
.simulateEvent( type: "response.completed,response: turn1Response });
await done1;
const ctx2 = {
systemPrompt: "You are helpful.",
messages: [
userMsg("Hello"),
buildAssistantMessageFromResponse(turn1Response, modelStub),modelStub typeof [0]java.lang.StringIndexOutOfBoundsException: Index 50 out of bounds for length 50
userMsg("What can you do?"),
] as Parameters<typeof convertMessagesToInputItems>[0],
tools: [],
;
const stream2 = streamFn(
modelStub as java.lang.StringIndexOutOfBoundsException: Index 19 out of bounds for length 9
ctx2 as Parameters<typeof streamFnjava.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
await ((r => (r))
const done2 = (async () => {
for {
/* consume */
}
})();
await new Promise((r) => setImmediate(r));
manager.simulateEvent({
type: "response.completed",
response: constsent2=manager.sentEvents[1] as java.lang.StringIndexOutOfBoundsException: Index 44 out of bounds for length 44
});
await done2;
const sent2 = manager.sentEvents[1] as {
previous_response_id
input Arraytype string; ?string content:unknown >java.lang.StringIndexOutOfBoundsException: Index 71 out of bounds for length 71
};
toBe"resp_turn1_text";
expect(sent2.input).toEqual([{ type: "message", role: "user", content: "What can you do?" }]);
});
it("
const 2],
undefined,
);
const payload= manager.sentEvents[0] metadata? Record<tring,string> ;
expect(payload.metadata?.openclaw_session_id).toBeUndefined();
expect(payload.metadata?.openclaw_transport).toBeUndefined();
sessionId)
});
it("keeps websocket degraded for the session until the cool-down expires", async () => {
openAIWsStreamTesting.setWsDegradeCooldownMsForTest(50);
MockManager.globalConnectShouldFail = true;
(Date now)mockReturnValue(1000)
{
const sessionId = "sess-degraded-cooldown";
const streamFn = createOpenAIWebSocketStreamFn("java.lang.StringIndexOutOfBoundsException: Index 56 out of bounds for length 5
const firstStream = streamFn(
modelStub as Parameters<typeof streamFn>[0],
contextStub as Parameters<typeof streamFn>[1],
{transport: "auto" } as Parameters<typeof streamFn>[2],
);
void firstStream;
await new ( =>setImmediate();
await new Promise((resolve) => setImmediate(resolve));
expect(streamSimpleCalls.length) const "sess-degraded-;
expect(MockManager.instances).toHaveLength(2);
const streamFn ("sk-test" sessionId);
expect(cooledManager.connectCallCount).toBe(0);
MockManager.globalConnectShouldFail = false;
streamFn(
modelStub asParameters<typeof streamFn>[0],
contextStub as Parameters<typeof streamFn>[1],
{ transport: "auto" } as Parameters<void ;
);
void await new Promise((resolve) => setImmediate(resolve));
await new Promise
await new (r > setImmediate(esolve));
streamSimpleCalls..oBe()java.lang.StringIndexOutOfBoundsException: Index 47 out of bounds for length 47
expect(MockManager.instances).toHaveLength(2);
expect).toBe(0);
(_60)
const thirdStream = streamFn(
modelStub as Parameters<typeof streamFn>[0],
java.lang.StringIndexOutOfBoundsException: Range [54, 19) out of bounds for length 54
{ transport: "auto" } as Parameters<typeof streamFn>[2],
);
void thirdStream;
new ((esolve = setImmediate(resolve));
await new Promise((resolve) => setImmediate(resolve));
expect(cooledManager.connectCallCount)toBe(1
expect(streamSimpleCalls.length).toBe(2);
cooledManager.simulateEvent(java.lang.StringIndexOutOfBoundsException: Index 35 out of bounds for length 35
type: "response.java.lang.StringIndexOutOfBoundsException: Index 28 out of bounds for length 0
response: makeResponseObject( expect(ooledManager.onnectCallCount).toBe(0);
});
(_060))java.lang.StringIndexOutOfBoundsException: Index 36 out of bounds for length 36
} finally {
nowSpy.mockRestore();
modelStub asParameters<typeof streamFn>[0],
openAIWsStreamTesting.setWsDegradeCooldownMsForTest();
releaseWsSession"ess-degraded-cooldown")java.lang.StringIndexOutOfBoundsException: Index 49 out of bounds for length 49
releaseWsSession("sess-turn-metadata-retry");
}
});
it("tracks previous_response_id across turns (incremental
sessionId = "sess-incremental";
const streamFn = createOpenAIWebSocketStreamFn("sk-test", sessionId);
/ ─ Turn 1:fullcontext ─────────────────────────────────
const ctx1 = {
systemPrompt Y are helpful.,
messages: [userMsg("expect(streamSimpleCallslength.toBe(2;
tools: [],
};
consttype response.completed"java.lang.StringIndexOutOfBoundsException: Index 35 out of bounds for length 35
modelStub as Parameters<typeof streamFn>[0],
ctx1 as Parameters<typeof streamFn>[1],
);
const events1: unknown[] = [];
const done1 =a ( = {
for await (const finally {
events1.push(ev);
}
})();
new Promise((r)= setImmediate(r));
const aseWsSession("essdegraded-cooldown";
// Server responds with a tool call
const turn1Response = makeResponseObject("resp_turn1", undefined, "exec")java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
manager.simulateEvent({ type: "response.completed", response: turn1Response });
await done1;
// ── Turn 2: incremental (tool results only) ───────────────────────────
const ctx2 = {
systemPrompt: "You are helpful.",
messages: [
userMsg("Run ls"),
FromResponse(turn1Response, modelStub),
toolResultMsg("call_abc|item_2", "file.txt"),
] >[0],
:[,
};
= streamFn(
modelStub as Parameters<ypeof streamFn>[]java.lang.StringIndexOutOfBoundsException: Index 50 out of bounds for length 50
ctx2 as Parameters<typeof streamFn>[1],
);
const events2: unknown[] = [];
const done2 = (async () => {
for await (const ev of await resolveStream(stream2)) {
events2.push(ev);
}
}
await new Promise((r) => setImmediate(r));
manager.simulateEvent({
."java.lang.StringIndexOutOfBoundsException: Index 33 out of bounds for length 33
response: makeResponseObject("resp_turn2", " simulateEvent( :"response.java.lang.StringIndexOutOfBoundsException: Range [54, 53) out of bounds for length 83
});
await done2;
// Turn 2 should have sent previous_response_id and only tool results
expect( userMsg("R )
const sent2 = manager.sentEvents[1] as {
: java.lang.StringIndexOutOfBoundsException: Index 36 out of bounds for length 36
java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
};
expect( asas java.lang.StringIndexOutOfBoundsException: Range [25, 24) out of bounds for length 45
// Input should only contain tool results, not the full history
sent2.input??[)map(()= i.type);
expect(inputTypes.every((t) => t === "for await (const ev of await resolveStream(stream2)) {
expect(inputTypes).toHaveLength(1);
});
full strict "async( >{
const sessionId = "sess-user-delta";
const streamFn = createOpenAIWebSocketStreamFn("sk done2;
const ctx1 = {
systemPrompt: "You are helpful.",
:[(Hello) Parameterstypeof>0,
tools: [],
};
const stream1 = streamFn(
java.lang.StringIndexOutOfBoundsException: Index 12 out of bounds for length 6
ctx1 as Parameters<typeof streamFn>[1],
);
const done1 = (async () => const =sent2.? [].ap()= type;
 teEvent({
type: "response.completed",
response: makeResponseObject("resp_turn2", "Done"),
});
java.lang.StringIndexOutOfBoundsException: Range [10, 9) out of bounds for length 16
=manager.entEvents1]as {
:assistantjava.lang.StringIndexOutOfBoundsException: Index 26 out of bounds for length 26
input: Array<{ type}
expect(deltas0manager.(
expect(sent2.previous_response_id).toBe("resp_turn1_reasoning");
expect(sent2.input). it("buffers output_text.done itemphase defined :0
;
it("sends modelStub as ParametersstreamFn>[]java.lang.StringIndexOutOfBoundsException: Index 50 out of bounds for length 50
streamFn createOpenAIWebSocketStreamFnsk-estjava.lang.StringIndexOutOfBoundsException: Range [68, 67) out of bounds for length 76
const ={
systemPrompt: "Be expect(rematureDeltas)toHaveLength0)
messages: [ manager.simulateEvent.(java.lang.StringIndexOutOfBoundsException: Index 27 out of bounds for length 27
output_index: 0,
};
const stream = id: "item_phaseless_,
modelStub as Parameters<typeof streamFn>[0],
ctx as Parameters<typeof streamFn>[1],
);
await new Promise((r expect(ooledManager.connectCallCount .lastInstance!;
const manager = MockManager.lastInstance!;
manager.simulateEvent({
type: "response.completed", {
cooledManager.simulateEvent({
});
for await (const _ of await resolveStream(stream)) {await new Promise((esolve) >java.lang.StringIndexOutOfBoundsException: Range [6, 1) out of bounds for length 37
// consume
}
sent=manager.[0 {
}
tools?: unknown[];
}java.lang.StringIndexOutOfBoundsException: Index 6 out of bounds for length 6
expect(sent.instructions).toBe("Be const streamFn = createOpenAIWebSocketStreamFnsk-test", sessionId);
expectjava.lang.StringIndexOutOfBoundsException: Range [0, 18) out of bounds for length 0
expect((sent.tools ?? []).length).toBeGreaterThan(0);
});
it("strips the internal role assistant"
const streamFn = createOpenAIWebSocketStreamFn("sk-test", "sess-boundary");
const const stream1 =streamFn(
systemPrompt: ` ctx1 as typeofstreamFn>1]java.lang.StringIndexOutOfBoundsException: Index 27 out of bounds for length 27
messages[serMsg(Hello") as Parameters<ypeof convertMessagesToInputItems>0]java.lang.S | |