@stagewhisper/stagewhisper 0.40.0 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stagewhisper/stagewhisper",
3
- "version": "0.40.0",
3
+ "version": "0.43.0",
4
4
  "type": "module",
5
5
  "description": "OpenClaw channel plugin that connects StageWhisper live calls to your AI assistant",
6
6
  "license": "MIT",
@@ -8,6 +8,7 @@
8
8
  "index.ts",
9
9
  "plugin-main.ts",
10
10
  "api.ts",
11
+ "openresponses.d.ts",
11
12
  "src",
12
13
  "openclaw.plugin.json",
13
14
  "skills"
package/plugin-main.ts CHANGED
@@ -112,6 +112,92 @@ export default definePluginEntry({
112
112
  }
113
113
  });
114
114
 
115
+ sw.command("reasoning-check")
116
+ .description("Test reasoning capability against the local OpenResponses endpoint")
117
+ .option("--model <model>", "Model to use (omit to use your configured default)", "default")
118
+ .action(async (opts: { model: string }) => {
119
+ const { callOpenResponses } = await import("./src/openresponses.js");
120
+ const modelLabel = opts.model === "default" ? "default (configured)" : opts.model;
121
+ console.log(`Testing reasoning with model: ${modelLabel}`);
122
+ console.log("Sending test request to local /v1/responses ...");
123
+
124
+ const start = Date.now();
125
+ try {
126
+ const result = await callOpenResponses(api, {
127
+ model: opts.model,
128
+ input: JSON.stringify({
129
+ transcript: "Candidate: I think we should use Redis for caching.",
130
+ playbook_guidance: "Evaluate technical decisions",
131
+ }),
132
+ text: {
133
+ format: {
134
+ type: "json_schema" as const,
135
+ name: "reasoning_test",
136
+ schema: {
137
+ type: "object",
138
+ properties: {
139
+ signals: {
140
+ type: "array",
141
+ items: {
142
+ type: "object",
143
+ properties: {
144
+ severity: { type: "string", enum: ["green", "orange", "red"] },
145
+ message: { type: "string" },
146
+ },
147
+ required: ["severity", "message"],
148
+ additionalProperties: false,
149
+ },
150
+ },
151
+ no_signal_reason: { type: "string" },
152
+ },
153
+ required: ["signals", "no_signal_reason"],
154
+ additionalProperties: false,
155
+ },
156
+ strict: true,
157
+ },
158
+ },
159
+ temperature: 0.2,
160
+ max_output_tokens: 1024,
161
+ });
162
+
163
+ const elapsed = Date.now() - start;
164
+ console.log(`✓ Response received in ${elapsed}ms`);
165
+ console.log(` Run ID: ${result.id}`);
166
+ if (result.usage) {
167
+ console.log(
168
+ ` Tokens: ${result.usage.input_tokens} in / ${result.usage.output_tokens} out`,
169
+ );
170
+ }
171
+
172
+ const output = result.output;
173
+ const text = Array.isArray(output)
174
+ ? (output.find((o) => o.type === "message") as Record<string, unknown> | undefined)
175
+ : null;
176
+ const textContent = text
177
+ ? ((text.content as Array<Record<string, unknown>>)?.find(
178
+ (c) => c.type === "output_text",
179
+ )?.text as string | undefined)
180
+ : null;
181
+ if (textContent) {
182
+ try {
183
+ const parsed = JSON.parse(textContent);
184
+ console.log(" Schema-valid JSON: ✓");
185
+ console.log(` Output: ${JSON.stringify(parsed, null, 2)}`);
186
+ } catch {
187
+ console.log(" Schema-valid JSON: ✗ (parse error)");
188
+ console.log(` Raw text: ${textContent.slice(0, 500)}`);
189
+ }
190
+ }
191
+ } catch (err) {
192
+ const elapsed = Date.now() - start;
193
+ console.error(`✗ Reasoning check failed after ${elapsed}ms`);
194
+ console.error(
195
+ ` Error: ${err instanceof Error ? err.message : String(err)}`,
196
+ );
197
+ process.exitCode = 1;
198
+ }
199
+ });
200
+
115
201
  sw.command("status")
116
202
  .description("Show StageWhisper relay connection status")
117
203
  .action(async () => {
package/src/client.ts CHANGED
@@ -37,10 +37,7 @@ export class StageWhisperClient {
37
37
  };
38
38
  }
39
39
 
40
- async completePairing(
41
- pairingCode: string,
42
- hostLabel: string,
43
- ): Promise<PairCompleteResponse> {
40
+ async completePairing(pairingCode: string, hostLabel: string): Promise<PairCompleteResponse> {
44
41
  const res = await fetch(`${this.baseUrl}/api/v1/openclaw/pair/complete`, {
45
42
  method: "POST",
46
43
  headers: { "Content-Type": "application/json" },
@@ -56,22 +53,15 @@ export class StageWhisperClient {
56
53
  return res.json();
57
54
  }
58
55
 
59
- async updateTaskStatus(
60
- taskId: string,
61
- status: string,
62
- remoteTaskId?: string,
63
- ): Promise<void> {
56
+ async updateTaskStatus(taskId: string, status: string, remoteTaskId?: string): Promise<void> {
64
57
  const body: Record<string, unknown> = { status };
65
58
  if (remoteTaskId) body.remote_task_id = remoteTaskId;
66
59
 
67
- const res = await fetch(
68
- `${this.baseUrl}/api/v1/openclaw/tasks/${taskId}/status`,
69
- {
70
- method: "POST",
71
- headers: this.headers(),
72
- body: JSON.stringify(body),
73
- },
74
- );
60
+ const res = await fetch(`${this.baseUrl}/api/v1/openclaw/tasks/${taskId}/status`, {
61
+ method: "POST",
62
+ headers: this.headers(),
63
+ body: JSON.stringify(body),
64
+ });
75
65
  if (!res.ok) {
76
66
  const text = await res.text();
77
67
  throw new Error(`Status update failed (${res.status}): ${text}`);
@@ -82,26 +72,24 @@ export class StageWhisperClient {
82
72
  const body: Record<string, unknown> = { content };
83
73
  if (remoteMessageId) body.remote_message_id = remoteMessageId;
84
74
 
85
- const res = await fetch(
86
- `${this.baseUrl}/api/v1/openclaw/tasks/${taskId}/reply`,
87
- {
88
- method: "POST",
89
- headers: this.headers(),
90
- body: JSON.stringify(body),
91
- },
92
- );
75
+ const res = await fetch(`${this.baseUrl}/api/v1/openclaw/tasks/${taskId}/reply`, {
76
+ method: "POST",
77
+ headers: this.headers(),
78
+ body: JSON.stringify(body),
79
+ });
93
80
  if (!res.ok) {
94
81
  const text = await res.text();
95
82
  throw new Error(`Reply failed (${res.status}): ${text}`);
96
83
  }
97
84
  }
98
85
 
99
- async heartbeat(): Promise<HeartbeatResponse> {
86
+ async heartbeat(capabilities?: Record<string, unknown>): Promise<HeartbeatResponse> {
100
87
  const res = await fetch(
101
88
  `${this.baseUrl}/api/v1/openclaw/integrations/${this.integrationId}/heartbeat`,
102
89
  {
103
90
  method: "POST",
104
91
  headers: this.headers(),
92
+ body: capabilities ? JSON.stringify(capabilities) : undefined,
105
93
  },
106
94
  );
107
95
  if (!res.ok) {
@@ -111,6 +99,26 @@ export class StageWhisperClient {
111
99
  return res.json();
112
100
  }
113
101
 
102
+ async postReasoningResult(
103
+ jobId: string,
104
+ result: Record<string, unknown>,
105
+ correlationId?: string,
106
+ ): Promise<void> {
107
+ const headers = this.headers();
108
+ if (correlationId) {
109
+ headers["X-Correlation-ID"] = correlationId;
110
+ }
111
+ const res = await fetch(`${this.baseUrl}/api/v1/openclaw/reasoning-jobs/${jobId}/complete`, {
112
+ method: "POST",
113
+ headers,
114
+ body: JSON.stringify(result),
115
+ });
116
+ if (!res.ok) {
117
+ const text = await res.text();
118
+ throw new Error(`Reasoning result post failed (${res.status}): ${text}`);
119
+ }
120
+ }
121
+
114
122
  streamUrl(): string {
115
123
  return `${this.baseUrl}/api/v1/openclaw/integrations/${this.integrationId}/stream`;
116
124
  }
@@ -0,0 +1,101 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createHealthTracker } from "./health.js";
3
+
4
+ describe("createHealthTracker", () => {
5
+ it("starts unverified", () => {
6
+ const tracker = createHealthTracker("gpt-4o");
7
+ const caps = tracker.get();
8
+ expect(caps.status).toBe("unverified");
9
+ expect(caps.supportsReasoning).toBe(true);
10
+ expect(caps.displayModel).toBe("gpt-4o");
11
+ expect(caps.consecutiveFailures).toBe(0);
12
+ });
13
+
14
+ it("records success and updates lastHealthyAt", () => {
15
+ const tracker = createHealthTracker(null);
16
+ tracker.recordFailure("timeout");
17
+ tracker.recordSuccess();
18
+ const caps = tracker.get();
19
+ expect(caps.status).toBe("healthy");
20
+ expect(caps.consecutiveFailures).toBe(0);
21
+ expect(caps.lastHealthyAt).toBeInstanceOf(Date);
22
+ });
23
+
24
+ it("degrades after 3 consecutive failures", () => {
25
+ const tracker = createHealthTracker(null);
26
+ tracker.recordFailure("err1");
27
+ tracker.recordFailure("err2");
28
+ expect(tracker.get().status).toBe("unverified");
29
+ tracker.recordFailure("err3");
30
+ expect(tracker.get().status).toBe("degraded");
31
+ expect(tracker.get().degradedReason).toBe("err3");
32
+ });
33
+
34
+ it("sets auth_required on 401/403 errors", () => {
35
+ const tracker = createHealthTracker(null);
36
+ tracker.recordFailure("401 Unauthorized");
37
+ expect(tracker.get().status).toBe("auth_required");
38
+ });
39
+
40
+ it("setModel updates displayModel", () => {
41
+ const tracker = createHealthTracker("gpt-4o");
42
+ tracker.setModel("claude-4");
43
+ expect(tracker.get().displayModel).toBe("claude-4");
44
+ });
45
+
46
+ it("setDisabled marks disabled", () => {
47
+ const tracker = createHealthTracker(null);
48
+ tracker.setDisabled();
49
+ const caps = tracker.get();
50
+ expect(caps.status).toBe("disabled");
51
+ expect(caps.supportsReasoning).toBe(false);
52
+ });
53
+
54
+ it("toHeartbeatPayload serializes snake_case", () => {
55
+ const tracker = createHealthTracker("gpt-4o");
56
+ tracker.recordSuccess();
57
+ const payload = tracker.toHeartbeatPayload();
58
+ expect(payload).toMatchObject({
59
+ supports_assistant_tasks: true,
60
+ supports_reasoning: true,
61
+ reasoning_transport: "openresponses",
62
+ display_model: "gpt-4o",
63
+ provider_ref: "openclaw",
64
+ status: "healthy",
65
+ degraded_reason: null,
66
+ });
67
+ });
68
+
69
+ it("get returns a copy, not the internal state", () => {
70
+ const tracker = createHealthTracker(null);
71
+ const caps = tracker.get();
72
+ caps.status = "disabled";
73
+ expect(tracker.get().status).toBe("unverified");
74
+ });
75
+
76
+ it("setDisconnected marks disconnected and setConnected restores", () => {
77
+ const tracker = createHealthTracker(null);
78
+ tracker.recordSuccess();
79
+ expect(tracker.get().status).toBe("healthy");
80
+
81
+ tracker.setDisconnected();
82
+ expect(tracker.get().status).toBe("disconnected");
83
+
84
+ tracker.setConnected();
85
+ expect(tracker.get().status).toBe("healthy");
86
+ });
87
+
88
+ it("setDisconnected does not override disabled", () => {
89
+ const tracker = createHealthTracker(null);
90
+ tracker.setDisabled();
91
+ tracker.setDisconnected();
92
+ expect(tracker.get().status).toBe("disabled");
93
+ });
94
+
95
+ it("setConnected is a no-op when not disconnected", () => {
96
+ const tracker = createHealthTracker(null);
97
+ tracker.recordSuccess();
98
+ tracker.setConnected();
99
+ expect(tracker.get().status).toBe("healthy");
100
+ });
101
+ });
package/src/health.ts ADDED
@@ -0,0 +1,94 @@
1
+ export type HostHealthStatus =
2
+ | "healthy"
3
+ | "degraded"
4
+ | "disconnected"
5
+ | "auth_required"
6
+ | "disabled"
7
+ | "unverified";
8
+
9
+ export type HostCapabilities = {
10
+ supportsAssistantTasks: boolean;
11
+ supportsReasoning: boolean;
12
+ reasoningTransport: string | null;
13
+ displayModel: string | null;
14
+ providerRef: string;
15
+ status: HostHealthStatus;
16
+ degradedReason: string | null;
17
+ lastHealthyAt: Date | null;
18
+ consecutiveFailures: number;
19
+ };
20
+
21
+ const DEGRADED_THRESHOLD = 3;
22
+
23
+ export function createHealthTracker(initialModel: string | null) {
24
+ const state: HostCapabilities = {
25
+ supportsAssistantTasks: true,
26
+ supportsReasoning: true,
27
+ reasoningTransport: "openresponses",
28
+ displayModel: initialModel,
29
+ providerRef: "openclaw",
30
+ status: "unverified",
31
+ degradedReason: null,
32
+ lastHealthyAt: null,
33
+ consecutiveFailures: 0,
34
+ };
35
+
36
+ let statusBeforeDisconnect: HostHealthStatus | null = null;
37
+
38
+ return {
39
+ get: (): HostCapabilities => ({ ...state }),
40
+
41
+ recordSuccess() {
42
+ state.consecutiveFailures = 0;
43
+ state.status = "healthy";
44
+ state.degradedReason = null;
45
+ state.lastHealthyAt = new Date();
46
+ statusBeforeDisconnect = null;
47
+ },
48
+
49
+ recordFailure(reason: string) {
50
+ state.consecutiveFailures += 1;
51
+ state.degradedReason = reason;
52
+ if (reason.includes("401") || reason.includes("403")) {
53
+ state.status = "auth_required";
54
+ } else if (state.consecutiveFailures >= DEGRADED_THRESHOLD) {
55
+ state.status = "degraded";
56
+ }
57
+ },
58
+
59
+ setModel(model: string) {
60
+ state.displayModel = model;
61
+ },
62
+
63
+ setDisabled() {
64
+ state.status = "disabled";
65
+ state.supportsReasoning = false;
66
+ },
67
+
68
+ setDisconnected() {
69
+ if (state.status !== "disabled") {
70
+ statusBeforeDisconnect = state.status;
71
+ state.status = "disconnected";
72
+ }
73
+ },
74
+
75
+ setConnected() {
76
+ if (state.status === "disconnected" && statusBeforeDisconnect !== null) {
77
+ state.status = statusBeforeDisconnect;
78
+ statusBeforeDisconnect = null;
79
+ }
80
+ },
81
+
82
+ toHeartbeatPayload(): Record<string, unknown> {
83
+ return {
84
+ supports_assistant_tasks: state.supportsAssistantTasks,
85
+ supports_reasoning: state.supportsReasoning,
86
+ reasoning_transport: state.reasoningTransport,
87
+ display_model: state.displayModel,
88
+ provider_ref: state.providerRef,
89
+ status: state.status,
90
+ degraded_reason: state.degradedReason,
91
+ };
92
+ },
93
+ };
94
+ }
@@ -0,0 +1,101 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
2
+
3
+ type GatewayConfig = { url: string; apiKey: string | null };
4
+
5
+ function resolveGatewayConfig(api: OpenClawPluginApi): GatewayConfig {
6
+ const cfg = api.config as Record<string, unknown>;
7
+ const gw = (cfg?.gateway as Record<string, unknown>) ?? {};
8
+ const url = String(gw?.url ?? "http://127.0.0.1:18789").replace(/\/+$/, "");
9
+ const rawKey = typeof gw?.apiKey === "string" ? gw.apiKey : null;
10
+
11
+ let apiKey: string | null = null;
12
+ if (rawKey) {
13
+ try {
14
+ const parsed = new URL(url);
15
+ const safe =
16
+ parsed.hostname === "localhost" ||
17
+ parsed.hostname === "127.0.0.1" ||
18
+ parsed.hostname === "::1" ||
19
+ parsed.protocol === "https:";
20
+ apiKey = safe ? rawKey : null;
21
+ } catch {
22
+ apiKey = null;
23
+ }
24
+ }
25
+
26
+ return { url, apiKey };
27
+ }
28
+
29
+ export async function callOpenResponses(
30
+ api: OpenClawPluginApi,
31
+ requestBody: OpenResponsesCreateResponseRequestBody,
32
+ signal?: AbortSignal,
33
+ correlationId?: string,
34
+ ): Promise<OpenResponsesResponseResource> {
35
+ const gw = resolveGatewayConfig(api);
36
+ const url = `${gw.url}/v1/responses`;
37
+
38
+ const headers: Record<string, string> = {
39
+ "Content-Type": "application/json",
40
+ };
41
+ if (gw.apiKey) {
42
+ headers["Authorization"] = `Bearer ${gw.apiKey}`;
43
+ }
44
+ if (correlationId) {
45
+ headers["X-Correlation-ID"] = correlationId;
46
+ }
47
+
48
+ const response = await fetch(url, {
49
+ method: "POST",
50
+ headers,
51
+ body: JSON.stringify(requestBody),
52
+ signal,
53
+ });
54
+
55
+ if (!response.ok) {
56
+ const body = await response.text().catch(() => "");
57
+ throw new OpenResponsesError(`POST /v1/responses returned ${response.status}: ${body}`, response.status);
58
+ }
59
+ return (await response.json()) as OpenResponsesResponseResource;
60
+ }
61
+
62
+ export class OpenResponsesError extends Error {
63
+ constructor(
64
+ message: string,
65
+ public readonly statusCode: number,
66
+ ) {
67
+ super(message);
68
+ this.name = "OpenResponsesError";
69
+ }
70
+
71
+ get retryable(): boolean {
72
+ return (
73
+ this.statusCode === 408 ||
74
+ this.statusCode === 429 ||
75
+ (this.statusCode >= 500 && this.statusCode < 600)
76
+ );
77
+ }
78
+ }
79
+
80
+ export type { components, operations, paths, webhooks } from "../openresponses";
81
+
82
+ export type OpenResponsesCreateResponseOperation =
83
+ import("../openresponses").operations["createResponse"];
84
+ export type OpenResponsesCreateResponseRequestBody =
85
+ import("../openresponses").components["schemas"]["CreateResponseBody"];
86
+ export type OpenResponsesResponseResource =
87
+ import("../openresponses").components["schemas"]["ResponseResource"];
88
+ export type OpenResponsesRequestItem =
89
+ import("../openresponses").components["schemas"]["ItemParam"];
90
+ export type OpenResponsesResponseItem =
91
+ import("../openresponses").components["schemas"]["ItemField"];
92
+ export type OpenResponsesTool =
93
+ import("../openresponses").components["schemas"]["ResponsesToolParam"];
94
+ export type OpenResponsesToolChoice =
95
+ import("../openresponses").components["schemas"]["ToolChoiceParam"];
96
+ export type OpenResponsesTextFormat =
97
+ import("../openresponses").components["schemas"]["TextFormatParam"];
98
+ export type OpenResponsesReasoning =
99
+ import("../openresponses").components["schemas"]["ReasoningParam"];
100
+ export type OpenResponsesStreamEvent =
101
+ import("../openresponses").operations["createResponse"]["responses"][200]["content"]["text/event-stream"];
@@ -0,0 +1,116 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { executeReasoningJob, type ReasoningJobEnvelope } from "./reasoning.js";
3
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
4
+
5
+ vi.mock("./openresponses.js", () => ({
6
+ callOpenResponses: vi.fn(),
7
+ OpenResponsesError: class extends Error {
8
+ status: number;
9
+ retryable: boolean;
10
+ constructor(message: string, status: number) {
11
+ super(message);
12
+ this.status = status;
13
+ this.retryable = status >= 500;
14
+ }
15
+ },
16
+ }));
17
+
18
+ const { callOpenResponses, OpenResponsesError } = await import("./openresponses.js");
19
+ const mockCall = vi.mocked(callOpenResponses);
20
+
21
+ function makeApi(): OpenClawPluginApi {
22
+ return {
23
+ logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
24
+ config: { get: vi.fn().mockReturnValue({}) },
25
+ } as unknown as OpenClawPluginApi;
26
+ }
27
+
28
+ function makeJob(overrides: Partial<ReasoningJobEnvelope> = {}): ReasoningJobEnvelope {
29
+ return {
30
+ event_type: "reasoning_job",
31
+ job_id: "job-123",
32
+ purpose: "live_analysis",
33
+ deadline_at: new Date(Date.now() + 30_000).toISOString(),
34
+ idempotency_key: "idem-1",
35
+ schema_version: 1,
36
+ response_schema: {
37
+ type: "object",
38
+ properties: { signals: { type: "array" } },
39
+ required: ["signals"],
40
+ },
41
+ payload: { transcript: "Hello world" },
42
+ ...overrides,
43
+ };
44
+ }
45
+
46
+ describe("executeReasoningJob", () => {
47
+ beforeEach(() => {
48
+ vi.clearAllMocks();
49
+ });
50
+
51
+ it("returns completed with parsed JSON output", async () => {
52
+ mockCall.mockResolvedValue({
53
+ id: "resp-abc",
54
+ model: "gpt-4o",
55
+ output: [
56
+ {
57
+ type: "message",
58
+ content: [
59
+ { type: "output_text", text: '{"signals": [{"severity": "green", "message": "ok"}]}' },
60
+ ],
61
+ },
62
+ ],
63
+ usage: { input_tokens: 100, output_tokens: 50 },
64
+ } as never);
65
+
66
+ const result = await executeReasoningJob(makeApi(), makeJob(), "gpt-4o");
67
+ expect(result.status).toBe("completed");
68
+ expect(result.job_id).toBe("job-123");
69
+ expect(result.output).toEqual({ signals: [{ severity: "green", message: "ok" }] });
70
+ expect(result.usage).toEqual({ input_tokens: 100, output_tokens: 50 });
71
+ expect(result.provider_run_id).toBe("resp-abc");
72
+ });
73
+
74
+ it("returns timed_out when deadline already passed", async () => {
75
+ const job = makeJob({ deadline_at: new Date(Date.now() - 1000).toISOString() });
76
+ const result = await executeReasoningJob(makeApi(), job, "gpt-4o");
77
+ expect(result.status).toBe("timed_out");
78
+ expect(result.error_code).toBe("deadline_expired_before_start");
79
+ expect(mockCall).not.toHaveBeenCalled();
80
+ });
81
+
82
+ it("returns failed when output is not valid JSON", async () => {
83
+ mockCall.mockResolvedValue({
84
+ id: "resp-bad",
85
+ output: [
86
+ { type: "message", content: [{ type: "output_text", text: "not json" }] },
87
+ ],
88
+ usage: { input_tokens: 10, output_tokens: 5 },
89
+ } as never);
90
+
91
+ const result = await executeReasoningJob(makeApi(), makeJob(), "gpt-4o");
92
+ expect(result.status).toBe("failed");
93
+ expect(result.error_code).toBe("response_parse_error");
94
+ });
95
+
96
+ it("returns failed with retryable flag on 5xx errors", async () => {
97
+ const err = new (OpenResponsesError as unknown as new (msg: string, status: number) => Error & { retryable: boolean })(
98
+ "Internal Server Error",
99
+ 500,
100
+ );
101
+ mockCall.mockRejectedValue(err);
102
+
103
+ const result = await executeReasoningJob(makeApi(), makeJob(), "gpt-4o");
104
+ expect(result.status).toBe("failed");
105
+ expect(result.error_code).toBe("retryable_error");
106
+ });
107
+
108
+ it("returns failed with execution_error on non-retryable errors", async () => {
109
+ mockCall.mockRejectedValue(new Error("Network failure"));
110
+
111
+ const result = await executeReasoningJob(makeApi(), makeJob(), "gpt-4o");
112
+ expect(result.status).toBe("failed");
113
+ expect(result.error_code).toBe("execution_error");
114
+ expect(result.error_message).toBe("Network failure");
115
+ });
116
+ });