@stagewhisper/stagewhisper 0.51.0 → 0.53.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/dist/index.js +5246 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +12 -16
- package/api.ts +0 -4
- package/index.ts +0 -54
- package/openresponses.d.ts +0 -1361
- package/plugin-main.ts +0 -356
- package/src/channel.test.ts +0 -201
- package/src/channel.ts +0 -182
- package/src/client.ts +0 -191
- package/src/crypto.test.ts +0 -181
- package/src/crypto.ts +0 -203
- package/src/health.test.ts +0 -101
- package/src/health.ts +0 -94
- package/src/openresponses.ts +0 -107
- package/src/reasoning.test.ts +0 -116
- package/src/reasoning.ts +0 -210
- package/src/runtime.ts +0 -6
- package/src/service.ts +0 -952
package/src/crypto.ts
DELETED
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
import { x25519 } from "@noble/curves/ed25519";
|
|
2
|
-
import { xchacha20poly1305 } from "@noble/ciphers/chacha";
|
|
3
|
-
import { hkdf } from "@noble/hashes/hkdf";
|
|
4
|
-
import { sha256 } from "@noble/hashes/sha256";
|
|
5
|
-
import { randomBytes } from "@noble/ciphers/webcrypto";
|
|
6
|
-
|
|
7
|
-
export const ENVELOPE_VERSION = "static_v1";
|
|
8
|
-
const ENVELOPE_HKDF_INFO = "stagewhisper-byo-envelope-v1";
|
|
9
|
-
const REPLAY_GUARD_MAX_IDS = 10_000;
|
|
10
|
-
|
|
11
|
-
export type SenderRole = "desktop" | "plugin";
|
|
12
|
-
|
|
13
|
-
export type ContentType =
|
|
14
|
-
| "reasoning_input"
|
|
15
|
-
| "reasoning_output"
|
|
16
|
-
| "tool_intent"
|
|
17
|
-
| "tool_result"
|
|
18
|
-
| "task_content"
|
|
19
|
-
| "task_reply";
|
|
20
|
-
|
|
21
|
-
export interface BYOEnvelope {
|
|
22
|
-
version: string;
|
|
23
|
-
sender_role: SenderRole;
|
|
24
|
-
message_id: string;
|
|
25
|
-
session_id: string;
|
|
26
|
-
correlation_id: string;
|
|
27
|
-
content_type: ContentType;
|
|
28
|
-
nonce: string;
|
|
29
|
-
ciphertext: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export class IdentityKeypair {
|
|
33
|
-
private secretKey: Uint8Array;
|
|
34
|
-
readonly publicKey: Uint8Array;
|
|
35
|
-
|
|
36
|
-
private constructor(secretKey: Uint8Array) {
|
|
37
|
-
this.secretKey = secretKey;
|
|
38
|
-
this.publicKey = x25519.getPublicKey(secretKey);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
static generate(): IdentityKeypair {
|
|
42
|
-
const secretKey = randomBytes(32);
|
|
43
|
-
return new IdentityKeypair(secretKey);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
static fromSecretBytes(bytes: Uint8Array): IdentityKeypair {
|
|
47
|
-
if (bytes.length !== 32) throw new Error(`Secret key must be 32 bytes, got ${bytes.length}`);
|
|
48
|
-
return new IdentityKeypair(bytes);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
secretBytes(): Uint8Array {
|
|
52
|
-
return new Uint8Array(this.secretKey);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
publicKeyBase64(): string {
|
|
56
|
-
return toBase64(this.publicKey);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
static publicKeyFromBase64(b64: string): Uint8Array {
|
|
60
|
-
const bytes = fromBase64(b64);
|
|
61
|
-
if (bytes.length !== 32) throw new Error(`Public key must be 32 bytes, got ${bytes.length}`);
|
|
62
|
-
return bytes;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
diffieHellman(peerPublicKey: Uint8Array): Uint8Array {
|
|
66
|
-
return x25519.getSharedSecret(this.secretKey, peerPublicKey);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
deriveEnvelopeKey(peerPublicKey: Uint8Array): Uint8Array {
|
|
70
|
-
const shared = this.diffieHellman(peerPublicKey);
|
|
71
|
-
return hkdf(sha256, shared, undefined, ENVELOPE_HKDF_INFO, 32);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function buildAad(envelope: Omit<BYOEnvelope, "nonce" | "ciphertext">): Uint8Array {
|
|
76
|
-
const aadObj = {
|
|
77
|
-
content_type: envelope.content_type,
|
|
78
|
-
correlation_id: envelope.correlation_id,
|
|
79
|
-
message_id: envelope.message_id,
|
|
80
|
-
sender_role: envelope.sender_role,
|
|
81
|
-
session_id: envelope.session_id,
|
|
82
|
-
version: envelope.version,
|
|
83
|
-
};
|
|
84
|
-
return new TextEncoder().encode(JSON.stringify(aadObj));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function generateUUID(): string {
|
|
88
|
-
return crypto.randomUUID();
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function seal(
|
|
92
|
-
key: Uint8Array,
|
|
93
|
-
senderRole: SenderRole,
|
|
94
|
-
sessionId: string,
|
|
95
|
-
correlationId: string,
|
|
96
|
-
contentType: ContentType,
|
|
97
|
-
plaintext: Uint8Array,
|
|
98
|
-
): BYOEnvelope {
|
|
99
|
-
const messageId = generateUUID();
|
|
100
|
-
const nonce = randomBytes(24);
|
|
101
|
-
|
|
102
|
-
const metadata = {
|
|
103
|
-
version: ENVELOPE_VERSION,
|
|
104
|
-
sender_role: senderRole,
|
|
105
|
-
message_id: messageId,
|
|
106
|
-
session_id: sessionId,
|
|
107
|
-
correlation_id: correlationId,
|
|
108
|
-
content_type: contentType,
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
const aad = buildAad(metadata);
|
|
112
|
-
const cipher = xchacha20poly1305(key, nonce, aad);
|
|
113
|
-
const ciphertext = cipher.encrypt(plaintext);
|
|
114
|
-
|
|
115
|
-
return {
|
|
116
|
-
...metadata,
|
|
117
|
-
nonce: toBase64(nonce),
|
|
118
|
-
ciphertext: toBase64(ciphertext),
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export function open(key: Uint8Array, envelope: BYOEnvelope): Uint8Array {
|
|
123
|
-
if (envelope.version !== ENVELOPE_VERSION) {
|
|
124
|
-
throw new Error(`Unsupported envelope version: ${envelope.version}`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const nonce = fromBase64(envelope.nonce);
|
|
128
|
-
if (nonce.length !== 24) {
|
|
129
|
-
throw new Error(`Nonce must be 24 bytes, got ${nonce.length}`);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const ciphertext = fromBase64(envelope.ciphertext);
|
|
133
|
-
const aad = buildAad(envelope);
|
|
134
|
-
const cipher = xchacha20poly1305(key, nonce, aad);
|
|
135
|
-
|
|
136
|
-
return cipher.decrypt(ciphertext);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function sealJson<T>(
|
|
140
|
-
key: Uint8Array,
|
|
141
|
-
senderRole: SenderRole,
|
|
142
|
-
sessionId: string,
|
|
143
|
-
correlationId: string,
|
|
144
|
-
contentType: ContentType,
|
|
145
|
-
value: T,
|
|
146
|
-
): BYOEnvelope {
|
|
147
|
-
const plaintext = new TextEncoder().encode(JSON.stringify(value));
|
|
148
|
-
return seal(key, senderRole, sessionId, correlationId, contentType, plaintext);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export function openJson<T>(key: Uint8Array, envelope: BYOEnvelope): T {
|
|
152
|
-
const plaintext = open(key, envelope);
|
|
153
|
-
return JSON.parse(new TextDecoder().decode(plaintext));
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export class ReplayGuard {
|
|
157
|
-
private seen = new Set<string>();
|
|
158
|
-
private order: string[] = [];
|
|
159
|
-
|
|
160
|
-
check(messageId: string): void {
|
|
161
|
-
if (this.seen.has(messageId)) {
|
|
162
|
-
throw new Error(`Replay detected: duplicate message_id ${messageId}`);
|
|
163
|
-
}
|
|
164
|
-
while (this.seen.size >= REPLAY_GUARD_MAX_IDS) {
|
|
165
|
-
const oldest = this.order.shift();
|
|
166
|
-
if (oldest !== undefined) {
|
|
167
|
-
this.seen.delete(oldest);
|
|
168
|
-
} else {
|
|
169
|
-
break;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
this.seen.add(messageId);
|
|
173
|
-
this.order.push(messageId);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export function openChecked(
|
|
178
|
-
key: Uint8Array,
|
|
179
|
-
envelope: BYOEnvelope,
|
|
180
|
-
replayGuard: ReplayGuard,
|
|
181
|
-
): Uint8Array {
|
|
182
|
-
replayGuard.check(envelope.message_id);
|
|
183
|
-
return open(key, envelope);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export function openJsonChecked<T>(
|
|
187
|
-
key: Uint8Array,
|
|
188
|
-
envelope: BYOEnvelope,
|
|
189
|
-
replayGuard: ReplayGuard,
|
|
190
|
-
): T {
|
|
191
|
-
replayGuard.check(envelope.message_id);
|
|
192
|
-
return openJson<T>(key, envelope);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function toBase64(bytes: Uint8Array): string {
|
|
196
|
-
const binString = Array.from(bytes, (b) => String.fromCodePoint(b)).join("");
|
|
197
|
-
return btoa(binString);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function fromBase64(b64: string): Uint8Array {
|
|
201
|
-
const binString = atob(b64);
|
|
202
|
-
return Uint8Array.from(binString, (c) => c.codePointAt(0)!);
|
|
203
|
-
}
|
package/src/health.test.ts
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
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
|
-
}
|
package/src/openresponses.ts
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
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 auth = (gw?.auth as Record<string, unknown>) ?? {};
|
|
9
|
-
|
|
10
|
-
const port = Number(gw?.port) || 18789;
|
|
11
|
-
const explicitUrl = typeof gw?.url === "string" ? gw.url : null;
|
|
12
|
-
const url = (explicitUrl ?? `http://127.0.0.1:${port}`).replace(/\/+$/, "");
|
|
13
|
-
|
|
14
|
-
const token = typeof auth?.token === "string" ? auth.token : null;
|
|
15
|
-
|
|
16
|
-
return { url, apiKey: token };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function isResponsesEndpointEnabled(api: OpenClawPluginApi): boolean {
|
|
20
|
-
const cfg = api.config as Record<string, unknown>;
|
|
21
|
-
const gw = (cfg?.gateway as Record<string, unknown>) ?? {};
|
|
22
|
-
const http = (gw?.http as Record<string, unknown>) ?? {};
|
|
23
|
-
const endpoints = (http?.endpoints as Record<string, unknown>) ?? {};
|
|
24
|
-
const responses = (endpoints?.responses as Record<string, unknown>) ?? {};
|
|
25
|
-
return responses?.enabled === true;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export async function callOpenResponses(
|
|
29
|
-
api: OpenClawPluginApi,
|
|
30
|
-
requestBody: OpenResponsesCreateResponseRequestBody,
|
|
31
|
-
signal?: AbortSignal,
|
|
32
|
-
correlationId?: string,
|
|
33
|
-
): Promise<OpenResponsesResponseResource> {
|
|
34
|
-
const gw = resolveGatewayConfig(api);
|
|
35
|
-
const url = `${gw.url}/v1/responses`;
|
|
36
|
-
|
|
37
|
-
const headers: Record<string, string> = {
|
|
38
|
-
"Content-Type": "application/json",
|
|
39
|
-
};
|
|
40
|
-
if (gw.apiKey) {
|
|
41
|
-
headers["Authorization"] = `Bearer ${gw.apiKey}`;
|
|
42
|
-
}
|
|
43
|
-
if (correlationId) {
|
|
44
|
-
headers["X-Correlation-ID"] = correlationId;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const response = await fetch(url, {
|
|
48
|
-
method: "POST",
|
|
49
|
-
headers,
|
|
50
|
-
body: JSON.stringify(requestBody),
|
|
51
|
-
signal,
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
if (!response.ok) {
|
|
55
|
-
const body = await response.text().catch(() => "");
|
|
56
|
-
if (response.status === 404) {
|
|
57
|
-
throw new OpenResponsesError(
|
|
58
|
-
"POST /v1/responses returned 404 — the OpenResponses HTTP API is most likely disabled. " +
|
|
59
|
-
'Enable it in OpenClaw config: gateway.http.endpoints.responses.enabled = true, then restart the gateway.',
|
|
60
|
-
response.status,
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
throw new OpenResponsesError(`POST /v1/responses returned ${response.status}: ${body}`, response.status);
|
|
64
|
-
}
|
|
65
|
-
return (await response.json()) as OpenResponsesResponseResource;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export class OpenResponsesError extends Error {
|
|
69
|
-
constructor(
|
|
70
|
-
message: string,
|
|
71
|
-
public readonly statusCode: number,
|
|
72
|
-
) {
|
|
73
|
-
super(message);
|
|
74
|
-
this.name = "OpenResponsesError";
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
get retryable(): boolean {
|
|
78
|
-
return (
|
|
79
|
-
this.statusCode === 408 ||
|
|
80
|
-
this.statusCode === 429 ||
|
|
81
|
-
(this.statusCode >= 500 && this.statusCode < 600)
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export type { components, operations, paths, webhooks } from "../openresponses";
|
|
87
|
-
|
|
88
|
-
export type OpenResponsesCreateResponseOperation =
|
|
89
|
-
import("../openresponses").operations["createResponse"];
|
|
90
|
-
export type OpenResponsesCreateResponseRequestBody =
|
|
91
|
-
import("../openresponses").components["schemas"]["CreateResponseBody"];
|
|
92
|
-
export type OpenResponsesResponseResource =
|
|
93
|
-
import("../openresponses").components["schemas"]["ResponseResource"];
|
|
94
|
-
export type OpenResponsesRequestItem =
|
|
95
|
-
import("../openresponses").components["schemas"]["ItemParam"];
|
|
96
|
-
export type OpenResponsesResponseItem =
|
|
97
|
-
import("../openresponses").components["schemas"]["ItemField"];
|
|
98
|
-
export type OpenResponsesTool =
|
|
99
|
-
import("../openresponses").components["schemas"]["ResponsesToolParam"];
|
|
100
|
-
export type OpenResponsesToolChoice =
|
|
101
|
-
import("../openresponses").components["schemas"]["ToolChoiceParam"];
|
|
102
|
-
export type OpenResponsesTextFormat =
|
|
103
|
-
import("../openresponses").components["schemas"]["TextFormatParam"];
|
|
104
|
-
export type OpenResponsesReasoning =
|
|
105
|
-
import("../openresponses").components["schemas"]["ReasoningParam"];
|
|
106
|
-
export type OpenResponsesStreamEvent =
|
|
107
|
-
import("../openresponses").operations["createResponse"]["responses"][200]["content"]["text/event-stream"];
|
package/src/reasoning.test.ts
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
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
|
-
});
|