pi-openai-toolkit 0.20.0 → 0.20.1
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/README.md +1 -1
- package/README.zh.md +1 -1
- package/docs/configuration.md +4 -4
- package/package.json +140 -140
- package/src/responses-websocket/extension.ts +4 -3
- package/src/responses-websocket/transport.ts +442 -386
package/README.md
CHANGED
|
@@ -105,7 +105,7 @@ The Toolkit WebSocket adapter is disabled by default. Enable it globally for GPT
|
|
|
105
105
|
}
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
-
It reuses the selected model's provider, base URL, credentials, headers, and model ID without registering another provider. Pi's `transport: "auto"` tries WebSocket first and falls back to SSE before streaming starts; `transport: "sse"`
|
|
108
|
+
It reuses the selected model's provider, base URL, credentials, headers, and model ID without registering another provider. Pi's `transport: "auto"` tries WebSocket first and falls back to SSE before streaming starts; with a stable session it also reuses the socket and sends only the new continuation input when the transcript matches. Use `transport: "websocket-cached"` to require that cached path, or `transport: "sse"` to keep the original HTTP/SSE path. Restart or reload Pi after changing the Toolkit setting. See [WebSocket configuration](docs/configuration.md#responses-websocket-transport).
|
|
109
109
|
|
|
110
110
|
### Auto Mode
|
|
111
111
|
|
package/README.zh.md
CHANGED
|
@@ -105,7 +105,7 @@ Toolkit 的 WebSocket adapter 默认关闭。对使用 Pi `openai-responses` API
|
|
|
105
105
|
}
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
-
它沿用当前模型的 provider、base URL、凭据、headers 和 model ID,不注册新的 provider。Pi 的 `transport: "auto"` 会先尝试 WebSocket,并在开始流式输出前回退到 SSE
|
|
108
|
+
它沿用当前模型的 provider、base URL、凭据、headers 和 model ID,不注册新的 provider。Pi 的 `transport: "auto"` 会先尝试 WebSocket,并在开始流式输出前回退到 SSE;会话 ID 稳定时还会复用连接,并在历史匹配时只发送新的 continuation 输入。使用 `transport: "websocket-cached"` 可强制要求缓存连接路径,使用 `transport: "sse"` 则保持原有 HTTP/SSE 路径。修改 Toolkit 配置后请重启或 reload Pi。详见[WebSocket 配置](docs/configuration.md#responses-websocket-transport)。
|
|
109
109
|
|
|
110
110
|
### Auto Mode
|
|
111
111
|
|
package/docs/configuration.md
CHANGED
|
@@ -96,12 +96,12 @@ The extension honors Pi's global `transport` setting:
|
|
|
96
96
|
|
|
97
97
|
| Pi `transport` | Behavior |
|
|
98
98
|
| --- | --- |
|
|
99
|
-
| `auto` | Try `wss://.../responses?model=...` first; fall back to the original SSE adapter only before a stream starts. |
|
|
100
|
-
| `websocket` | Require the WebSocket path; do not fall back to SSE. |
|
|
101
|
-
| `websocket-cached` |
|
|
99
|
+
| `auto` | Try `wss://.../responses?model=...` first; fall back to the original SSE adapter only before a stream starts. With a stable session id, reuse the WebSocket and send a continuation delta when possible. |
|
|
100
|
+
| `websocket` | Require the WebSocket path; do not fall back to SSE. This mode opens a request-scoped connection. |
|
|
101
|
+
| `websocket-cached` | Require the WebSocket path and keep a session-scoped connection for later turns. |
|
|
102
102
|
| `sse` | Bypass the extension and use the original HTTP/SSE adapter. |
|
|
103
103
|
|
|
104
|
-
The endpoint is derived from the model's base URL by appending `/responses` when needed and switching `https:`/`http:` to `wss:`/`ws:`. The first frame is a standard `response.create` object and incoming frames are passed through Pi's existing Responses event normalizer. The upstream must support the standard Responses WebSocket contract
|
|
104
|
+
The endpoint is derived from the model's base URL by appending `/responses` when needed and switching `https:`/`http:` to `wss:`/`ws:`. The first frame is a standard `response.create` object and incoming frames are passed through Pi's existing Responses event normalizer. For `auto` and `websocket-cached`, Toolkit caches an open socket by session/model/endpoint/auth identity for up to five minutes idle or 55 minutes of age. When the current transcript has the cached request and response as a prefix, the next frame uses `previous_response_id` and only the new input items; if it cannot prove that relationship, it sends the full payload instead. A missing session id or `cacheRetention: "none"` disables reuse. The upstream must support the standard Responses WebSocket contract, including continuation semantics for `previous_response_id`; this does not rewrite a server that only exposes Codex's `/codex/responses` path or Codex-only authentication.
|
|
105
105
|
|
|
106
106
|
This feature does not write Pi settings, model files, auth files, or global extensions. To keep the Toolkit adapter disabled, set `defaults.responsesWebSocket.enabled` to `false` or omit it. Once enabled, Pi's global `transport: "sse"` still bypasses the WebSocket path for a request.
|
|
107
107
|
|
package/package.json
CHANGED
|
@@ -1,140 +1,140 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "pi-openai-toolkit",
|
|
3
|
-
"version": "0.20.
|
|
4
|
-
"private": false,
|
|
5
|
-
"type": "module",
|
|
6
|
-
"engines": {
|
|
7
|
-
"node": ">=22.19.0"
|
|
8
|
-
},
|
|
9
|
-
"description": "OpenAI toolkit for Pi: Codex Remote Context windows, remote compaction v2, routed Web Search, image gen, auto mode.",
|
|
10
|
-
"author": "awoaCrim and contributors",
|
|
11
|
-
"license": "MIT",
|
|
12
|
-
"repository": {
|
|
13
|
-
"type": "git",
|
|
14
|
-
"url": "git+https://github.com/awoaCrim/pi-openai-toolkit.git"
|
|
15
|
-
},
|
|
16
|
-
"bugs": {
|
|
17
|
-
"url": "https://github.com/awoaCrim/pi-openai-toolkit/issues"
|
|
18
|
-
},
|
|
19
|
-
"homepage": "https://github.com/awoaCrim/pi-openai-toolkit#readme",
|
|
20
|
-
"publishConfig": {
|
|
21
|
-
"access": "public",
|
|
22
|
-
"registry": "https://registry.npmjs.org/"
|
|
23
|
-
},
|
|
24
|
-
"keywords": [
|
|
25
|
-
"pi-package",
|
|
26
|
-
"pi-extension",
|
|
27
|
-
"openai",
|
|
28
|
-
"compaction",
|
|
29
|
-
"web-search",
|
|
30
|
-
"image-generation",
|
|
31
|
-
"auto-mode",
|
|
32
|
-
"codex",
|
|
33
|
-
"responses"
|
|
34
|
-
],
|
|
35
|
-
"pi": {
|
|
36
|
-
"extensions": [
|
|
37
|
-
"./src/extension-runtime.ts",
|
|
38
|
-
"./src/web-search/extension.ts",
|
|
39
|
-
"./src/image-generation/extension.ts",
|
|
40
|
-
"./src/auto-mode/extension.ts",
|
|
41
|
-
"./src/codex-astra/extension.ts",
|
|
42
|
-
"./src/responses-websocket/extension.ts"
|
|
43
|
-
]
|
|
44
|
-
},
|
|
45
|
-
"files": [
|
|
46
|
-
"src/compact-client.ts",
|
|
47
|
-
"src/compaction-projection.ts",
|
|
48
|
-
"src/pi-context-hook.ts",
|
|
49
|
-
"src/config.ts",
|
|
50
|
-
"src/config-command.ts",
|
|
51
|
-
"src/config/policy.ts",
|
|
52
|
-
"src/config/legacy.ts",
|
|
53
|
-
"src/config/v2.ts",
|
|
54
|
-
"src/config/migration.ts",
|
|
55
|
-
"src/config/notifications.ts",
|
|
56
|
-
"config.schema.json",
|
|
57
|
-
"src/debug.ts",
|
|
58
|
-
"src/details-store.ts",
|
|
59
|
-
"src/deferred-tool-carryover.ts",
|
|
60
|
-
"src/extension-runtime.ts",
|
|
61
|
-
"src/context-management/types.ts",
|
|
62
|
-
"src/context-management/messages.ts",
|
|
63
|
-
"src/context-management/window-budget.ts",
|
|
64
|
-
"src/context-management/window-request.ts",
|
|
65
|
-
"src/context-management/window-bulk.ts",
|
|
66
|
-
"src/context-management/window-manager.ts",
|
|
67
|
-
"src/context-management/manual-compact.ts",
|
|
68
|
-
"src/context-management/codex-provider.ts",
|
|
69
|
-
"src/context-management/history-notes.ts",
|
|
70
|
-
"src/context-management/tools.ts",
|
|
71
|
-
"src/context-management/namespace-tools.ts",
|
|
72
|
-
"src/context-management/tool-contract.ts",
|
|
73
|
-
"src/image-generation/artifacts.ts",
|
|
74
|
-
"src/image-generation/client.ts",
|
|
75
|
-
"src/image-generation/eligibility.ts",
|
|
76
|
-
"src/image-generation/extension.ts",
|
|
77
|
-
"src/image-generation/protocol.ts",
|
|
78
|
-
"src/image-generation/references.ts",
|
|
79
|
-
"src/image-generation/render.ts",
|
|
80
|
-
"src/image-generation/service.ts",
|
|
81
|
-
"src/image-generation/types.ts",
|
|
82
|
-
"src/auto-mode/extension.ts",
|
|
83
|
-
"src/auto-mode/tool-review-tui.ts",
|
|
84
|
-
"src/auto-mode/policy.ts",
|
|
85
|
-
"src/auto-mode/model-selection-guard.ts",
|
|
86
|
-
"src/auto-mode/prompt.ts",
|
|
87
|
-
"src/auto-mode/transcript.ts",
|
|
88
|
-
"src/auto-mode/reviewer.ts",
|
|
89
|
-
"src/auto-mode/classifier.ts",
|
|
90
|
-
"src/auto-mode/types.ts",
|
|
91
|
-
"src/codex-astra/effort-planner.ts",
|
|
92
|
-
"src/codex-astra/extension.ts",
|
|
93
|
-
"src/responses-websocket/extension.ts",
|
|
94
|
-
"src/responses-websocket/transport.ts",
|
|
95
|
-
"src/model-scope.ts",
|
|
96
|
-
"src/native-fallback.ts",
|
|
97
|
-
"src/payload-rewrite.ts",
|
|
98
|
-
"src/provider-headers.ts",
|
|
99
|
-
"src/responses-headers.ts",
|
|
100
|
-
"src/remote-v2-client.ts",
|
|
101
|
-
"src/request-context-cache.ts",
|
|
102
|
-
"src/runtime.ts",
|
|
103
|
-
"src/serializer.ts",
|
|
104
|
-
"src/supported-environment.ts",
|
|
105
|
-
"src/types.ts",
|
|
106
|
-
"src/web-search/alpha-client.ts",
|
|
107
|
-
"src/web-search/extension.ts",
|
|
108
|
-
"src/web-search/payload.ts",
|
|
109
|
-
"src/web-search/prompt.ts",
|
|
110
|
-
"src/web-search/types.ts",
|
|
111
|
-
"docs/internals.md",
|
|
112
|
-
"docs/configuration.md",
|
|
113
|
-
"README.md",
|
|
114
|
-
"README.zh.md",
|
|
115
|
-
"LICENSE",
|
|
116
|
-
"NOTICE",
|
|
117
|
-
"tsconfig.check.json"
|
|
118
|
-
],
|
|
119
|
-
"scripts": {
|
|
120
|
-
"test": "bun test",
|
|
121
|
-
"test:coverage": "bun test --coverage --coverage-reporter=text --coverage-reporter=lcov && node ./scripts/check-lcov.mjs ./coverage/lcov.info",
|
|
122
|
-
"test:pi": "bun test ./test/pi-smoke.test.ts",
|
|
123
|
-
"typecheck": "tsc -p tsconfig.check.json"
|
|
124
|
-
},
|
|
125
|
-
"peerDependencies": {
|
|
126
|
-
"@earendil-works/pi-agent-core": ">=0.87.0",
|
|
127
|
-
"@earendil-works/pi-ai": ">=0.87.0",
|
|
128
|
-
"@earendil-works/pi-coding-agent": ">=0.87.0",
|
|
129
|
-
"@earendil-works/pi-tui": ">=0.87.0"
|
|
130
|
-
},
|
|
131
|
-
"devDependencies": {
|
|
132
|
-
"@earendil-works/pi-agent-core": "0.99.1",
|
|
133
|
-
"@earendil-works/pi-ai": "0.99.1",
|
|
134
|
-
"@earendil-works/pi-coding-agent": "0.99.1",
|
|
135
|
-
"@earendil-works/pi-tui": "0.99.1",
|
|
136
|
-
"@types/node": "24.12.4",
|
|
137
|
-
"bun": "1.2.23",
|
|
138
|
-
"typescript": "5.9.3"
|
|
139
|
-
}
|
|
140
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-openai-toolkit",
|
|
3
|
+
"version": "0.20.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22.19.0"
|
|
8
|
+
},
|
|
9
|
+
"description": "OpenAI toolkit for Pi: Codex Remote Context windows, remote compaction v2, routed Web Search, image gen, auto mode.",
|
|
10
|
+
"author": "awoaCrim and contributors",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/awoaCrim/pi-openai-toolkit.git"
|
|
15
|
+
},
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/awoaCrim/pi-openai-toolkit/issues"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/awoaCrim/pi-openai-toolkit#readme",
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public",
|
|
22
|
+
"registry": "https://registry.npmjs.org/"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"pi-package",
|
|
26
|
+
"pi-extension",
|
|
27
|
+
"openai",
|
|
28
|
+
"compaction",
|
|
29
|
+
"web-search",
|
|
30
|
+
"image-generation",
|
|
31
|
+
"auto-mode",
|
|
32
|
+
"codex",
|
|
33
|
+
"responses"
|
|
34
|
+
],
|
|
35
|
+
"pi": {
|
|
36
|
+
"extensions": [
|
|
37
|
+
"./src/extension-runtime.ts",
|
|
38
|
+
"./src/web-search/extension.ts",
|
|
39
|
+
"./src/image-generation/extension.ts",
|
|
40
|
+
"./src/auto-mode/extension.ts",
|
|
41
|
+
"./src/codex-astra/extension.ts",
|
|
42
|
+
"./src/responses-websocket/extension.ts"
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"src/compact-client.ts",
|
|
47
|
+
"src/compaction-projection.ts",
|
|
48
|
+
"src/pi-context-hook.ts",
|
|
49
|
+
"src/config.ts",
|
|
50
|
+
"src/config-command.ts",
|
|
51
|
+
"src/config/policy.ts",
|
|
52
|
+
"src/config/legacy.ts",
|
|
53
|
+
"src/config/v2.ts",
|
|
54
|
+
"src/config/migration.ts",
|
|
55
|
+
"src/config/notifications.ts",
|
|
56
|
+
"config.schema.json",
|
|
57
|
+
"src/debug.ts",
|
|
58
|
+
"src/details-store.ts",
|
|
59
|
+
"src/deferred-tool-carryover.ts",
|
|
60
|
+
"src/extension-runtime.ts",
|
|
61
|
+
"src/context-management/types.ts",
|
|
62
|
+
"src/context-management/messages.ts",
|
|
63
|
+
"src/context-management/window-budget.ts",
|
|
64
|
+
"src/context-management/window-request.ts",
|
|
65
|
+
"src/context-management/window-bulk.ts",
|
|
66
|
+
"src/context-management/window-manager.ts",
|
|
67
|
+
"src/context-management/manual-compact.ts",
|
|
68
|
+
"src/context-management/codex-provider.ts",
|
|
69
|
+
"src/context-management/history-notes.ts",
|
|
70
|
+
"src/context-management/tools.ts",
|
|
71
|
+
"src/context-management/namespace-tools.ts",
|
|
72
|
+
"src/context-management/tool-contract.ts",
|
|
73
|
+
"src/image-generation/artifacts.ts",
|
|
74
|
+
"src/image-generation/client.ts",
|
|
75
|
+
"src/image-generation/eligibility.ts",
|
|
76
|
+
"src/image-generation/extension.ts",
|
|
77
|
+
"src/image-generation/protocol.ts",
|
|
78
|
+
"src/image-generation/references.ts",
|
|
79
|
+
"src/image-generation/render.ts",
|
|
80
|
+
"src/image-generation/service.ts",
|
|
81
|
+
"src/image-generation/types.ts",
|
|
82
|
+
"src/auto-mode/extension.ts",
|
|
83
|
+
"src/auto-mode/tool-review-tui.ts",
|
|
84
|
+
"src/auto-mode/policy.ts",
|
|
85
|
+
"src/auto-mode/model-selection-guard.ts",
|
|
86
|
+
"src/auto-mode/prompt.ts",
|
|
87
|
+
"src/auto-mode/transcript.ts",
|
|
88
|
+
"src/auto-mode/reviewer.ts",
|
|
89
|
+
"src/auto-mode/classifier.ts",
|
|
90
|
+
"src/auto-mode/types.ts",
|
|
91
|
+
"src/codex-astra/effort-planner.ts",
|
|
92
|
+
"src/codex-astra/extension.ts",
|
|
93
|
+
"src/responses-websocket/extension.ts",
|
|
94
|
+
"src/responses-websocket/transport.ts",
|
|
95
|
+
"src/model-scope.ts",
|
|
96
|
+
"src/native-fallback.ts",
|
|
97
|
+
"src/payload-rewrite.ts",
|
|
98
|
+
"src/provider-headers.ts",
|
|
99
|
+
"src/responses-headers.ts",
|
|
100
|
+
"src/remote-v2-client.ts",
|
|
101
|
+
"src/request-context-cache.ts",
|
|
102
|
+
"src/runtime.ts",
|
|
103
|
+
"src/serializer.ts",
|
|
104
|
+
"src/supported-environment.ts",
|
|
105
|
+
"src/types.ts",
|
|
106
|
+
"src/web-search/alpha-client.ts",
|
|
107
|
+
"src/web-search/extension.ts",
|
|
108
|
+
"src/web-search/payload.ts",
|
|
109
|
+
"src/web-search/prompt.ts",
|
|
110
|
+
"src/web-search/types.ts",
|
|
111
|
+
"docs/internals.md",
|
|
112
|
+
"docs/configuration.md",
|
|
113
|
+
"README.md",
|
|
114
|
+
"README.zh.md",
|
|
115
|
+
"LICENSE",
|
|
116
|
+
"NOTICE",
|
|
117
|
+
"tsconfig.check.json"
|
|
118
|
+
],
|
|
119
|
+
"scripts": {
|
|
120
|
+
"test": "bun test",
|
|
121
|
+
"test:coverage": "bun test --coverage --coverage-reporter=text --coverage-reporter=lcov && node ./scripts/check-lcov.mjs ./coverage/lcov.info",
|
|
122
|
+
"test:pi": "bun test ./test/pi-smoke.test.ts",
|
|
123
|
+
"typecheck": "tsc -p tsconfig.check.json"
|
|
124
|
+
},
|
|
125
|
+
"peerDependencies": {
|
|
126
|
+
"@earendil-works/pi-agent-core": ">=0.87.0",
|
|
127
|
+
"@earendil-works/pi-ai": ">=0.87.0",
|
|
128
|
+
"@earendil-works/pi-coding-agent": ">=0.87.0",
|
|
129
|
+
"@earendil-works/pi-tui": ">=0.87.0"
|
|
130
|
+
},
|
|
131
|
+
"devDependencies": {
|
|
132
|
+
"@earendil-works/pi-agent-core": "0.99.1",
|
|
133
|
+
"@earendil-works/pi-ai": "0.99.1",
|
|
134
|
+
"@earendil-works/pi-coding-agent": "0.99.1",
|
|
135
|
+
"@earendil-works/pi-tui": "0.99.1",
|
|
136
|
+
"@types/node": "24.12.4",
|
|
137
|
+
"bun": "1.2.23",
|
|
138
|
+
"typescript": "5.9.3"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -4,6 +4,7 @@ import { loadToolkitConfig, resolveToolkitConfig } from "../config";
|
|
|
4
4
|
import { createResponsesWebSocketProvider } from "./transport";
|
|
5
5
|
|
|
6
6
|
const SOURCE_ID = "pi-openai-toolkit:openai-responses-websocket";
|
|
7
|
+
let baseProvider: ReturnType<typeof getApiProvider>;
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Add the transport branch at the API-adapter boundary instead of registering
|
|
@@ -21,9 +22,9 @@ export function installResponsesWebSocketTransport(
|
|
|
21
22
|
if (!resolved.policy.responsesWebSocket.enabled || resolved.invalidFeatures.includes("responsesWebSocket")) {
|
|
22
23
|
return false;
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
-
if (!
|
|
26
|
-
registerApiProvider(createResponsesWebSocketProvider(
|
|
25
|
+
baseProvider ??= getApiProvider("openai-responses");
|
|
26
|
+
if (!baseProvider) return false;
|
|
27
|
+
registerApiProvider(createResponsesWebSocketProvider(baseProvider, true), SOURCE_ID);
|
|
27
28
|
return true;
|
|
28
29
|
}
|
|
29
30
|
|
|
@@ -1,40 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
type Model,
|
|
8
|
-
type StreamOptions,
|
|
9
|
-
type TranscriptContext,
|
|
1
|
+
import type {
|
|
2
|
+
Api,
|
|
3
|
+
FetchFunction,
|
|
4
|
+
Model,
|
|
5
|
+
SimpleStreamOptions,
|
|
6
|
+
StreamOptions,
|
|
10
7
|
} from "@earendil-works/pi-ai";
|
|
11
|
-
import { buildBaseOptions } from "@earendil-works/pi-ai/api/simple-options";
|
|
12
|
-
import { clampOpenAIPromptCacheKey } from "@earendil-works/pi-ai/api/openai-prompt-cache";
|
|
13
|
-
import {
|
|
14
|
-
convertResponsesMessages,
|
|
15
|
-
convertResponsesTools,
|
|
16
|
-
processResponsesStream,
|
|
17
|
-
} from "@earendil-works/pi-ai/api/openai-responses-shared";
|
|
18
|
-
import { createGrammarToolInputProperties } from "@earendil-works/pi-ai/api/constrained-sampling";
|
|
19
|
-
import { formatProviderError, normalizeProviderError } from "@earendil-works/pi-ai/utils/error-body";
|
|
20
|
-
import {
|
|
21
|
-
getDeclaredTools,
|
|
22
|
-
resolveTranscript,
|
|
23
|
-
resolveTranscriptTools,
|
|
24
|
-
} from "@earendil-works/pi-ai/utils/transcript";
|
|
25
8
|
import type { ApiProvider } from "@earendil-works/pi-ai/compat";
|
|
26
|
-
import type { ResponseCreateParamsStreaming } from "openai/resources/responses/responses.js";
|
|
27
9
|
|
|
28
|
-
const OPENAI_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]);
|
|
29
|
-
const OPENAI_RESPONSES_MIN_OUTPUT_TOKENS = 16;
|
|
30
10
|
const RESPONSES_WEBSOCKET_BETA = "responses_websockets=2026-02-06";
|
|
31
11
|
const WEBSOCKET_OPEN = 1;
|
|
32
12
|
const WEBSOCKET_CONNECTING = 0;
|
|
13
|
+
const CACHE_IDLE_TTL_MS = 5 * 60 * 1000;
|
|
14
|
+
const CACHE_MAX_AGE_MS = 55 * 60 * 1000;
|
|
15
|
+
const TERMINAL_EVENTS = new Set([
|
|
16
|
+
"response.completed",
|
|
17
|
+
"response.done",
|
|
18
|
+
"response.incomplete",
|
|
19
|
+
"response.failed",
|
|
20
|
+
"error",
|
|
21
|
+
]);
|
|
33
22
|
|
|
34
|
-
type ResponsesEvent = Parameters<typeof processResponsesStream>[0] extends AsyncIterable<infer Event> ? Event : never;
|
|
35
23
|
type ResponsesPayload = Record<string, unknown>;
|
|
36
|
-
type HeaderInput = Record<string, string | null> | undefined;
|
|
37
|
-
|
|
38
24
|
type WebSocketEvent = {
|
|
39
25
|
data?: unknown;
|
|
40
26
|
message?: string;
|
|
@@ -42,7 +28,6 @@ type WebSocketEvent = {
|
|
|
42
28
|
code?: number;
|
|
43
29
|
reason?: string;
|
|
44
30
|
};
|
|
45
|
-
|
|
46
31
|
type WebSocketLike = {
|
|
47
32
|
readyState: number;
|
|
48
33
|
send(data: string): void;
|
|
@@ -50,65 +35,28 @@ type WebSocketLike = {
|
|
|
50
35
|
addEventListener(type: string, listener: (event: WebSocketEvent) => void): void;
|
|
51
36
|
removeEventListener(type: string, listener: (event: WebSocketEvent) => void): void;
|
|
52
37
|
};
|
|
53
|
-
|
|
54
38
|
type WebSocketConstructor = new (url: string, options?: { headers?: Record<string, string> }) => WebSocketLike;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
39
|
+
type CachedContinuation = {
|
|
40
|
+
lastRequestBody: ResponsesPayload;
|
|
41
|
+
lastResponseId: string;
|
|
42
|
+
lastResponseItems: unknown[];
|
|
43
|
+
};
|
|
44
|
+
type CachedSocketEntry = {
|
|
45
|
+
key: string;
|
|
46
|
+
sessionId: string;
|
|
47
|
+
socket: WebSocketLike;
|
|
48
|
+
busy: boolean;
|
|
49
|
+
createdAt: number;
|
|
50
|
+
idleTimer?: ReturnType<typeof setTimeout>;
|
|
51
|
+
continuation?: CachedContinuation;
|
|
52
|
+
};
|
|
53
|
+
type AcquiredSocket = {
|
|
54
|
+
socket: WebSocketLike;
|
|
55
|
+
entry?: CachedSocketEntry;
|
|
56
|
+
release(keep: boolean): void;
|
|
61
57
|
};
|
|
62
58
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
class AsyncEventQueue<T> implements AsyncIterable<T> {
|
|
66
|
-
private readonly items: T[] = [];
|
|
67
|
-
private readonly waiters: Array<{
|
|
68
|
-
resolve: (result: IteratorResult<T>) => void;
|
|
69
|
-
reject: (error: unknown) => void;
|
|
70
|
-
}> = [];
|
|
71
|
-
private closed = false;
|
|
72
|
-
private failure: unknown;
|
|
73
|
-
|
|
74
|
-
push(item: T): void {
|
|
75
|
-
if (this.closed) return;
|
|
76
|
-
const waiter = this.waiters.shift();
|
|
77
|
-
if (waiter) {
|
|
78
|
-
waiter.resolve({ value: item, done: false });
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
this.items.push(item);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
fail(error: unknown): void {
|
|
85
|
-
if (this.closed) return;
|
|
86
|
-
this.failure = error;
|
|
87
|
-
this.closed = true;
|
|
88
|
-
while (this.waiters.length > 0) this.waiters.shift()!.reject(error);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
close(): void {
|
|
92
|
-
if (this.closed) return;
|
|
93
|
-
this.closed = true;
|
|
94
|
-
while (this.waiters.length > 0) {
|
|
95
|
-
this.waiters.shift()!.resolve({ value: undefined, done: true });
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
next(): Promise<IteratorResult<T>> {
|
|
100
|
-
if (this.items.length > 0) {
|
|
101
|
-
return Promise.resolve({ value: this.items.shift()!, done: false });
|
|
102
|
-
}
|
|
103
|
-
if (this.failure !== undefined) return Promise.reject(this.failure);
|
|
104
|
-
if (this.closed) return Promise.resolve({ value: undefined, done: true });
|
|
105
|
-
return new Promise((resolve, reject) => this.waiters.push({ resolve, reject }));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
[Symbol.asyncIterator](): AsyncIterator<T> {
|
|
109
|
-
return this;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
59
|
+
const cachedSockets = new Map<string, CachedSocketEntry>();
|
|
112
60
|
|
|
113
61
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
114
62
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
@@ -118,13 +66,13 @@ function isGptResponsesModel(model: Model<Api>): boolean {
|
|
|
118
66
|
return model.api === "openai-responses" && /^gpt/iu.test(model.id);
|
|
119
67
|
}
|
|
120
68
|
|
|
121
|
-
function transportMode(options: StreamOptions | undefined):
|
|
69
|
+
function transportMode(options: Pick<StreamOptions, "transport"> | undefined): NonNullable<StreamOptions["transport"]> {
|
|
122
70
|
return options?.transport ?? "auto";
|
|
123
71
|
}
|
|
124
72
|
|
|
125
73
|
export function shouldUseResponsesWebSocket(
|
|
126
74
|
model: Model<Api>,
|
|
127
|
-
options?: StreamOptions,
|
|
75
|
+
options?: Pick<StreamOptions, "transport">,
|
|
128
76
|
enabled = false,
|
|
129
77
|
): boolean {
|
|
130
78
|
return enabled && isGptResponsesModel(model) && transportMode(options) !== "sse";
|
|
@@ -144,11 +92,57 @@ export function buildResponsesWebSocketUrl(model: Model<Api>): string {
|
|
|
144
92
|
return url.toString();
|
|
145
93
|
}
|
|
146
94
|
|
|
95
|
+
function requestUrl(input: RequestInfo | URL): string {
|
|
96
|
+
if (typeof input === "string") return input;
|
|
97
|
+
if (input instanceof URL) return input.toString();
|
|
98
|
+
return input.url;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function requestMethod(input: RequestInfo | URL, init?: RequestInit): string {
|
|
102
|
+
if (init?.method) return init.method.toUpperCase();
|
|
103
|
+
if (typeof input !== "string" && !(input instanceof URL) && "method" in input) {
|
|
104
|
+
return String(input.method || "GET").toUpperCase();
|
|
105
|
+
}
|
|
106
|
+
return "GET";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function requestHeaders(input: RequestInfo | URL, init?: RequestInit): Headers {
|
|
110
|
+
const headers = new Headers();
|
|
111
|
+
if (typeof input !== "string" && !(input instanceof URL) && "headers" in input) {
|
|
112
|
+
new Headers(input.headers).forEach((value, name) => headers.set(name, value));
|
|
113
|
+
}
|
|
114
|
+
if (init?.headers) {
|
|
115
|
+
new Headers(init.headers).forEach((value, name) => headers.set(name, value));
|
|
116
|
+
}
|
|
117
|
+
return headers;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function requestBody(input: RequestInfo | URL, init?: RequestInit): Promise<string | undefined> {
|
|
121
|
+
const body = init?.body as unknown;
|
|
122
|
+
if (typeof body === "string") return body;
|
|
123
|
+
if (body instanceof ArrayBuffer) return new TextDecoder().decode(body);
|
|
124
|
+
if (ArrayBuffer.isView(body)) {
|
|
125
|
+
return new TextDecoder().decode(new Uint8Array(body.buffer, body.byteOffset, body.byteLength));
|
|
126
|
+
}
|
|
127
|
+
if (body && typeof body === "object" && "text" in body && typeof body.text === "function") {
|
|
128
|
+
return await (body.text as () => Promise<string>)();
|
|
129
|
+
}
|
|
130
|
+
if (typeof input !== "string" && !(input instanceof URL) && "clone" in input && typeof input.clone === "function") {
|
|
131
|
+
return await input.clone().text();
|
|
132
|
+
}
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
147
136
|
function hasHeader(headers: Headers, name: string): boolean {
|
|
148
|
-
|
|
137
|
+
const value = headers.get(name);
|
|
138
|
+
return value !== null && value.trim().length > 0;
|
|
149
139
|
}
|
|
150
140
|
|
|
151
|
-
function mergeHeaders(
|
|
141
|
+
function mergeHeaders(
|
|
142
|
+
modelHeaders: Record<string, string> | undefined,
|
|
143
|
+
optionHeaders: Record<string, string | null> | undefined,
|
|
144
|
+
requestHeadersFromFetch: Headers,
|
|
145
|
+
): Headers {
|
|
152
146
|
const headers = new Headers();
|
|
153
147
|
for (const source of [modelHeaders, optionHeaders]) {
|
|
154
148
|
for (const [name, value] of Object.entries(source ?? {})) {
|
|
@@ -156,154 +150,53 @@ function mergeHeaders(modelHeaders: HeaderInput, optionHeaders: HeaderInput): He
|
|
|
156
150
|
else if (typeof value === "string" && value.trim()) headers.set(name, value);
|
|
157
151
|
}
|
|
158
152
|
}
|
|
153
|
+
requestHeadersFromFetch.forEach((value, name) => {
|
|
154
|
+
if (value.trim()) headers.set(name, value);
|
|
155
|
+
});
|
|
159
156
|
return headers;
|
|
160
157
|
}
|
|
161
158
|
|
|
162
|
-
function buildWebSocketHeaders(
|
|
163
|
-
|
|
164
|
-
|
|
159
|
+
function buildWebSocketHeaders(
|
|
160
|
+
model: Model<Api>,
|
|
161
|
+
options: StreamOptions,
|
|
162
|
+
requestHeadersFromFetch: Headers,
|
|
163
|
+
requestId: string,
|
|
164
|
+
): Record<string, string> {
|
|
165
|
+
const headers = mergeHeaders(model.headers, options.headers, requestHeadersFromFetch);
|
|
166
|
+
if (!hasHeader(headers, "authorization") && options.apiKey?.trim()) {
|
|
165
167
|
const apiKey = options.apiKey.trim();
|
|
166
168
|
headers.set("Authorization", apiKey.startsWith("Bearer ") ? apiKey : `Bearer ${apiKey}`);
|
|
167
169
|
}
|
|
168
170
|
if (!hasHeader(headers, "authorization") && !hasHeader(headers, "cf-aig-authorization")) {
|
|
169
171
|
throw new Error(`No API key for provider: ${model.provider}`);
|
|
170
172
|
}
|
|
171
|
-
|
|
172
|
-
// This is the Responses WebSocket capability marker used by Pi's official
|
|
173
|
-
// Codex transport. It is an API-level header, not a provider-specific value.
|
|
174
173
|
if (!hasHeader(headers, "openai-beta")) headers.set("OpenAI-Beta", RESPONSES_WEBSOCKET_BETA);
|
|
175
174
|
if (!hasHeader(headers, "x-client-request-id")) headers.set("X-Client-Request-Id", requestId);
|
|
176
|
-
|
|
177
175
|
return Object.fromEntries(headers.entries());
|
|
178
176
|
}
|
|
179
177
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
supportsAdditionalTools?: boolean;
|
|
186
|
-
supportsToolSearch?: boolean;
|
|
187
|
-
supportsExplicitPromptCacheMode?: boolean;
|
|
188
|
-
supportsMaxOutputTokens?: boolean;
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
function getCompat(model: Model<Api>): Required<ResponsesCompat> {
|
|
192
|
-
const compat = (model.compat ?? {}) as ResponsesCompat;
|
|
193
|
-
return {
|
|
194
|
-
supportsMidConvoSystemMessages: compat.supportsMidConvoSystemMessages ?? false,
|
|
195
|
-
supportsLongCacheRetention: compat.supportsLongCacheRetention ?? true,
|
|
196
|
-
supportsStrictMode: compat.supportsStrictMode ?? false,
|
|
197
|
-
supportsOpenAIGrammarTools: compat.supportsOpenAIGrammarTools ?? false,
|
|
198
|
-
supportsAdditionalTools: compat.supportsAdditionalTools ?? false,
|
|
199
|
-
supportsToolSearch: compat.supportsToolSearch ?? false,
|
|
200
|
-
supportsExplicitPromptCacheMode: compat.supportsExplicitPromptCacheMode ?? false,
|
|
201
|
-
supportsMaxOutputTokens: compat.supportsMaxOutputTokens ?? true,
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function getPromptCacheOptions(compat: ReturnType<typeof getCompat>, cacheRetention: StreamOptions["cacheRetention"]): Record<string, unknown> | undefined {
|
|
206
|
-
if (!compat.supportsExplicitPromptCacheMode) return undefined;
|
|
207
|
-
if (cacheRetention === "none") return { mode: "explicit" };
|
|
208
|
-
if (cacheRetention === "long" && compat.supportsLongCacheRetention) return { ttl: "30m" };
|
|
209
|
-
return undefined;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function getPromptCacheRetention(compat: ReturnType<typeof getCompat>, cacheRetention: StreamOptions["cacheRetention"]): string | undefined {
|
|
213
|
-
return cacheRetention === "long" && compat.supportsLongCacheRetention && !compat.supportsExplicitPromptCacheMode
|
|
214
|
-
? "24h"
|
|
215
|
-
: undefined;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function buildResponsesPayload(
|
|
219
|
-
model: Model<Api>,
|
|
220
|
-
context: TranscriptContext,
|
|
221
|
-
options: ResponsesStreamOptions | undefined,
|
|
222
|
-
): ResponsesPayload {
|
|
223
|
-
const normalizedContext = resolveTranscript(context, getCompat(model).supportsMidConvoSystemMessages);
|
|
224
|
-
const compat = getCompat(model);
|
|
225
|
-
const grammarToolInputProperties = createGrammarToolInputProperties(
|
|
226
|
-
getDeclaredTools(normalizedContext.messages),
|
|
227
|
-
compat.supportsOpenAIGrammarTools,
|
|
228
|
-
);
|
|
229
|
-
const transcriptTools = resolveTranscriptTools(
|
|
230
|
-
normalizedContext.messages,
|
|
231
|
-
compat.supportsAdditionalTools || compat.supportsToolSearch,
|
|
232
|
-
);
|
|
233
|
-
const cacheRetention = options?.cacheRetention ?? "short";
|
|
234
|
-
const params: ResponsesPayload = {
|
|
235
|
-
model: model.id,
|
|
236
|
-
input: convertResponsesMessages(model, normalizedContext, OPENAI_TOOL_CALL_PROVIDERS, {
|
|
237
|
-
grammarToolInputProperties,
|
|
238
|
-
supportsMidConvoSystemMessages: compat.supportsMidConvoSystemMessages,
|
|
239
|
-
supportsAdditionalTools: compat.supportsAdditionalTools,
|
|
240
|
-
supportsToolSearch: compat.supportsToolSearch,
|
|
241
|
-
toolOptions: {
|
|
242
|
-
supportsStrictMode: compat.supportsStrictMode,
|
|
243
|
-
supportsOpenAIGrammarTools: compat.supportsOpenAIGrammarTools,
|
|
244
|
-
},
|
|
245
|
-
}),
|
|
246
|
-
stream: true,
|
|
247
|
-
prompt_cache_key: cacheRetention === "none" ? undefined : clampOpenAIPromptCacheKey(options?.sessionId),
|
|
248
|
-
prompt_cache_retention: getPromptCacheRetention(compat, cacheRetention),
|
|
249
|
-
prompt_cache_options: getPromptCacheOptions(compat, cacheRetention),
|
|
250
|
-
store: false,
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
if (options?.maxTokens !== undefined && compat.supportsMaxOutputTokens) {
|
|
254
|
-
params.max_output_tokens = Math.max(options.maxTokens, OPENAI_RESPONSES_MIN_OUTPUT_TOKENS);
|
|
178
|
+
function stableHash(value: string): string {
|
|
179
|
+
let hash = 2166136261;
|
|
180
|
+
for (let index = 0; index < value.length; index++) {
|
|
181
|
+
hash ^= value.charCodeAt(index);
|
|
182
|
+
hash = Math.imul(hash, 16777619);
|
|
255
183
|
}
|
|
256
|
-
|
|
257
|
-
if (options?.serviceTier !== undefined) params.service_tier = options.serviceTier;
|
|
258
|
-
if (transcriptTools.requestTools.length > 0) {
|
|
259
|
-
params.tools = convertResponsesTools(transcriptTools.requestTools, {
|
|
260
|
-
supportsStrictMode: compat.supportsStrictMode,
|
|
261
|
-
supportsOpenAIGrammarTools: compat.supportsOpenAIGrammarTools,
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
if (options?.toolChoice !== undefined) params.tool_choice = options.toolChoice;
|
|
265
|
-
|
|
266
|
-
if (model.reasoning) {
|
|
267
|
-
if (options?.reasoningEffort || options?.reasoningSummary) {
|
|
268
|
-
const effort = options.reasoningEffort
|
|
269
|
-
? (model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort)
|
|
270
|
-
: "medium";
|
|
271
|
-
params.reasoning = {
|
|
272
|
-
effort,
|
|
273
|
-
summary: options.reasoningSummary || "auto",
|
|
274
|
-
};
|
|
275
|
-
params.include = ["reasoning.encrypted_content"];
|
|
276
|
-
} else if (model.thinkingLevelMap?.off !== null) {
|
|
277
|
-
params.reasoning = { effort: model.thinkingLevelMap?.off ?? "none" };
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
Object.assign(params, model.samplingParams, options?.samplingParams);
|
|
282
|
-
return params;
|
|
184
|
+
return (hash >>> 0).toString(16);
|
|
283
185
|
}
|
|
284
186
|
|
|
285
|
-
function
|
|
286
|
-
return
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
299
|
-
},
|
|
300
|
-
stopReason: "pending",
|
|
301
|
-
timestamp: Date.now(),
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function thrownMessage(error: unknown): string {
|
|
306
|
-
return formatProviderError(normalizeProviderError(error), "OpenAI Responses API error");
|
|
187
|
+
function buildCacheKey(model: Model<Api>, options: StreamOptions, requestHeadersFromFetch: Headers): string | undefined {
|
|
188
|
+
if (!options.sessionId || options.cacheRetention === "none") return undefined;
|
|
189
|
+
const headers = mergeHeaders(model.headers, options.headers, requestHeadersFromFetch);
|
|
190
|
+
if (!hasHeader(headers, "authorization") && options.apiKey?.trim()) {
|
|
191
|
+
const apiKey = options.apiKey.trim();
|
|
192
|
+
headers.set("authorization", apiKey.startsWith("Bearer ") ? apiKey : `Bearer ${apiKey}`);
|
|
193
|
+
}
|
|
194
|
+
// Request ids identify individual calls and must not split one Pi session into
|
|
195
|
+
// a fresh cache entry on every turn. The remaining headers capture auth and
|
|
196
|
+
// provider-specific routing that must not be mixed across sockets.
|
|
197
|
+
headers.delete("x-client-request-id");
|
|
198
|
+
const identity = [...headers.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
199
|
+
return [model.provider, model.id, cleanBaseUrl(model.baseUrl), options.sessionId, stableHash(JSON.stringify(identity))].join("\u0000");
|
|
307
200
|
}
|
|
308
201
|
|
|
309
202
|
function socketError(event: WebSocketEvent): Error {
|
|
@@ -323,27 +216,61 @@ function closeSocket(socket: WebSocketLike | undefined, reason = "done"): void {
|
|
|
323
216
|
}
|
|
324
217
|
}
|
|
325
218
|
|
|
219
|
+
function socketReusable(socket: WebSocketLike): boolean {
|
|
220
|
+
return socket.readyState === WEBSOCKET_OPEN;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function removeCachedSocket(entry: CachedSocketEntry, reason: string): void {
|
|
224
|
+
if (entry.idleTimer) clearTimeout(entry.idleTimer);
|
|
225
|
+
if (cachedSockets.get(entry.key) === entry) cachedSockets.delete(entry.key);
|
|
226
|
+
closeSocket(entry.socket, reason);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function scheduleCachedSocketExpiry(entry: CachedSocketEntry): void {
|
|
230
|
+
if (entry.idleTimer) clearTimeout(entry.idleTimer);
|
|
231
|
+
const remainingAge = CACHE_MAX_AGE_MS - (Date.now() - entry.createdAt);
|
|
232
|
+
const delay = Math.min(CACHE_IDLE_TTL_MS, remainingAge);
|
|
233
|
+
if (delay <= 0) {
|
|
234
|
+
removeCachedSocket(entry, "connection_age_limit");
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
entry.idleTimer = setTimeout(() => {
|
|
238
|
+
if (!entry.busy) removeCachedSocket(entry, "idle_timeout");
|
|
239
|
+
}, delay);
|
|
240
|
+
(entry.idleTimer as unknown as { unref?: () => void }).unref?.();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function closeResponsesWebSocketSessions(sessionId?: string): void {
|
|
244
|
+
for (const entry of [...cachedSockets.values()]) {
|
|
245
|
+
if (sessionId === undefined || entry.sessionId === sessionId) removeCachedSocket(entry, "session_closed");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
function liveSignals(options: StreamOptions, init?: RequestInit): AbortSignal[] {
|
|
251
|
+
return [options.signal, init?.signal].filter((signal): signal is AbortSignal => signal !== undefined);
|
|
252
|
+
}
|
|
253
|
+
|
|
326
254
|
async function connectWebSocket(
|
|
327
255
|
url: string,
|
|
328
256
|
headers: Record<string, string>,
|
|
329
|
-
|
|
257
|
+
signals: AbortSignal[],
|
|
330
258
|
timeoutMs: number,
|
|
331
259
|
): Promise<WebSocketLike> {
|
|
332
260
|
const WebSocketCtor = globalThis.WebSocket as unknown as WebSocketConstructor | undefined;
|
|
333
261
|
if (typeof WebSocketCtor !== "function") throw new Error("WebSocket transport is not available in this runtime");
|
|
334
|
-
if (signal
|
|
262
|
+
if (signals.some((signal) => signal.aborted)) throw new Error("Request was aborted");
|
|
335
263
|
|
|
336
264
|
return new Promise((resolve, reject) => {
|
|
337
|
-
let socket: WebSocketLike;
|
|
265
|
+
let socket: WebSocketLike | undefined;
|
|
338
266
|
let settled = false;
|
|
339
267
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
340
|
-
|
|
341
268
|
const cleanup = () => {
|
|
342
269
|
if (timeout) clearTimeout(timeout);
|
|
343
270
|
socket?.removeEventListener("open", onOpen);
|
|
344
271
|
socket?.removeEventListener("error", onError);
|
|
345
272
|
socket?.removeEventListener("close", onClose);
|
|
346
|
-
signal
|
|
273
|
+
for (const signal of signals) signal.removeEventListener("abort", onAbort);
|
|
347
274
|
};
|
|
348
275
|
const fail = (error: Error) => {
|
|
349
276
|
if (settled) return;
|
|
@@ -356,7 +283,7 @@ async function connectWebSocket(
|
|
|
356
283
|
if (settled) return;
|
|
357
284
|
settled = true;
|
|
358
285
|
cleanup();
|
|
359
|
-
resolve(socket);
|
|
286
|
+
resolve(socket!);
|
|
360
287
|
};
|
|
361
288
|
const onError = (event: WebSocketEvent) => fail(socketError(event));
|
|
362
289
|
const onClose = (event: WebSocketEvent) => fail(new Error(
|
|
@@ -373,7 +300,7 @@ async function connectWebSocket(
|
|
|
373
300
|
socket.addEventListener("open", onOpen);
|
|
374
301
|
socket.addEventListener("error", onError);
|
|
375
302
|
socket.addEventListener("close", onClose);
|
|
376
|
-
signal
|
|
303
|
+
for (const signal of signals) signal.addEventListener("abort", onAbort, { once: true });
|
|
377
304
|
if (timeoutMs > 0) timeout = setTimeout(() => fail(new Error(`WebSocket connect timeout after ${timeoutMs}ms`)), timeoutMs);
|
|
378
305
|
});
|
|
379
306
|
}
|
|
@@ -381,9 +308,7 @@ async function connectWebSocket(
|
|
|
381
308
|
async function decodeSocketData(value: unknown): Promise<string | undefined> {
|
|
382
309
|
if (typeof value === "string") return value;
|
|
383
310
|
if (value instanceof ArrayBuffer) return new TextDecoder().decode(new Uint8Array(value));
|
|
384
|
-
if (ArrayBuffer.isView(value))
|
|
385
|
-
return new TextDecoder().decode(new Uint8Array(value.buffer, value.byteOffset, value.byteLength));
|
|
386
|
-
}
|
|
311
|
+
if (ArrayBuffer.isView(value)) return new TextDecoder().decode(new Uint8Array(value.buffer, value.byteOffset, value.byteLength));
|
|
387
312
|
if (isRecord(value) && typeof value.arrayBuffer === "function") {
|
|
388
313
|
const bytes = await (value.arrayBuffer as () => Promise<ArrayBuffer>)();
|
|
389
314
|
return new TextDecoder().decode(new Uint8Array(bytes));
|
|
@@ -391,165 +316,306 @@ async function decodeSocketData(value: unknown): Promise<string | undefined> {
|
|
|
391
316
|
return undefined;
|
|
392
317
|
}
|
|
393
318
|
|
|
394
|
-
async function
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
if (
|
|
405
|
-
|
|
319
|
+
async function acquireWebSocket(
|
|
320
|
+
url: string,
|
|
321
|
+
headers: Record<string, string>,
|
|
322
|
+
signals: AbortSignal[],
|
|
323
|
+
connectTimeoutMs: number,
|
|
324
|
+
cacheKey: string | undefined,
|
|
325
|
+
sessionId: string | undefined,
|
|
326
|
+
): Promise<AcquiredSocket> {
|
|
327
|
+
if (cacheKey) {
|
|
328
|
+
const cached = cachedSockets.get(cacheKey);
|
|
329
|
+
if (cached) {
|
|
330
|
+
if (!socketReusable(cached.socket) || Date.now() - cached.createdAt >= CACHE_MAX_AGE_MS) {
|
|
331
|
+
removeCachedSocket(cached, "cache_expired");
|
|
332
|
+
} else if (!cached.busy) {
|
|
333
|
+
if (cached.idleTimer) clearTimeout(cached.idleTimer);
|
|
334
|
+
cached.idleTimer = undefined;
|
|
335
|
+
cached.busy = true;
|
|
336
|
+
return {
|
|
337
|
+
socket: cached.socket,
|
|
338
|
+
entry: cached,
|
|
339
|
+
release(keep) {
|
|
340
|
+
if (keep && socketReusable(cached.socket) && Date.now() - cached.createdAt < CACHE_MAX_AGE_MS) {
|
|
341
|
+
cached.busy = false;
|
|
342
|
+
scheduleCachedSocketExpiry(cached);
|
|
343
|
+
} else {
|
|
344
|
+
removeCachedSocket(cached, "request_finished");
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const socket = await connectWebSocket(url, headers, signals, connectTimeoutMs);
|
|
353
|
+
if (!cacheKey || !sessionId || cachedSockets.has(cacheKey)) {
|
|
354
|
+
return { socket, release: () => closeSocket(socket, "request_finished") };
|
|
355
|
+
}
|
|
356
|
+
const entry: CachedSocketEntry = { key: cacheKey, sessionId, socket, busy: true, createdAt: Date.now() };
|
|
357
|
+
cachedSockets.set(cacheKey, entry);
|
|
358
|
+
return {
|
|
359
|
+
socket,
|
|
360
|
+
entry,
|
|
361
|
+
release(keep) {
|
|
362
|
+
if (keep && socketReusable(entry.socket) && Date.now() - entry.createdAt < CACHE_MAX_AGE_MS) {
|
|
363
|
+
entry.busy = false;
|
|
364
|
+
scheduleCachedSocketExpiry(entry);
|
|
365
|
+
} else {
|
|
366
|
+
removeCachedSocket(entry, "request_finished");
|
|
367
|
+
}
|
|
368
|
+
},
|
|
406
369
|
};
|
|
407
|
-
|
|
408
|
-
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function cloneJson<T>(value: T): T {
|
|
373
|
+
return JSON.parse(JSON.stringify(value)) as T;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function responseInputsEqual(a: unknown, b: unknown): boolean {
|
|
377
|
+
if (Object.is(a, b)) return true;
|
|
378
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
379
|
+
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((value, index) => responseInputsEqual(value, b[index]));
|
|
380
|
+
}
|
|
381
|
+
if (isRecord(a) || isRecord(b)) {
|
|
382
|
+
if (!isRecord(a) || !isRecord(b)) return false;
|
|
383
|
+
const aKeys = Object.keys(a).sort();
|
|
384
|
+
const bKeys = Object.keys(b).sort();
|
|
385
|
+
return aKeys.length === bKeys.length && aKeys.every((key, index) => key === bKeys[index] && responseInputsEqual(a[key], b[key]));
|
|
386
|
+
}
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function requestBodyWithoutInput(body: ResponsesPayload): ResponsesPayload {
|
|
391
|
+
const { input: _input, previous_response_id: _previousResponseId, ...rest } = body;
|
|
392
|
+
return rest;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function requestBodiesMatchExceptInput(a: ResponsesPayload, b: ResponsesPayload): boolean {
|
|
396
|
+
return responseInputsEqual(requestBodyWithoutInput(a), requestBodyWithoutInput(b));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function inputItems(body: ResponsesPayload): unknown[] {
|
|
400
|
+
return Array.isArray(body.input) ? body.input : [];
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function buildCachedRequestBody(entry: CachedSocketEntry, body: ResponsesPayload): ResponsesPayload {
|
|
404
|
+
const continuation = entry.continuation;
|
|
405
|
+
if (!continuation) return body;
|
|
406
|
+
if (!requestBodiesMatchExceptInput(body, continuation.lastRequestBody)) {
|
|
407
|
+
entry.continuation = undefined;
|
|
408
|
+
return body;
|
|
409
|
+
}
|
|
410
|
+
const currentInput = inputItems(body);
|
|
411
|
+
const baseline = [...inputItems(continuation.lastRequestBody), ...continuation.lastResponseItems];
|
|
412
|
+
if (currentInput.length < baseline.length || !responseInputsEqual(currentInput.slice(0, baseline.length), baseline)) {
|
|
413
|
+
entry.continuation = undefined;
|
|
414
|
+
return body;
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
...body,
|
|
418
|
+
previous_response_id: continuation.lastResponseId,
|
|
419
|
+
input: currentInput.slice(baseline.length),
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function completedContinuation(entry: CachedSocketEntry | undefined, fullPayload: ResponsesPayload, event: Record<string, unknown>): boolean {
|
|
424
|
+
if (!entry || (event.type !== "response.completed" && event.type !== "response.done")) return false;
|
|
425
|
+
const response = isRecord(event.response) ? event.response : undefined;
|
|
426
|
+
const responseId = typeof response?.id === "string" ? response.id : undefined;
|
|
427
|
+
if (!responseId) {
|
|
428
|
+
entry.continuation = undefined;
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
const output = Array.isArray(response?.output)
|
|
432
|
+
? response.output.filter((item): item is Record<string, unknown> => isRecord(item) && item.type !== "function_call_output" && item.type !== "custom_tool_call_output")
|
|
433
|
+
: [];
|
|
434
|
+
entry.continuation = {
|
|
435
|
+
lastRequestBody: cloneJson(fullPayload),
|
|
436
|
+
lastResponseId: responseId,
|
|
437
|
+
lastResponseItems: cloneJson(output),
|
|
438
|
+
};
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function createSseResponse(
|
|
443
|
+
socket: WebSocketLike,
|
|
444
|
+
signals: AbortSignal[],
|
|
445
|
+
idleTimeoutMs: number,
|
|
446
|
+
requestFrame: string,
|
|
447
|
+
onTerminal: (event: Record<string, unknown>) => boolean,
|
|
448
|
+
onRelease: (keep: boolean) => void,
|
|
449
|
+
): Response {
|
|
450
|
+
const encoder = new TextEncoder();
|
|
451
|
+
let cancelBody: (reason?: unknown) => void = () => closeSocket(socket, "consumer_cancelled");
|
|
452
|
+
const body = new ReadableStream<Uint8Array>({
|
|
453
|
+
start(controller) {
|
|
454
|
+
let closed = false;
|
|
455
|
+
let released = false;
|
|
456
|
+
let idleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
457
|
+
let sawTerminal = false;
|
|
458
|
+
const cleanup = () => {
|
|
459
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
460
|
+
socket.removeEventListener("message", onMessage);
|
|
461
|
+
socket.removeEventListener("error", onError);
|
|
462
|
+
socket.removeEventListener("close", onClose);
|
|
463
|
+
for (const signal of signals) signal.removeEventListener("abort", onAbort);
|
|
464
|
+
};
|
|
465
|
+
const release = (keep: boolean) => {
|
|
466
|
+
if (released) return;
|
|
467
|
+
released = true;
|
|
468
|
+
onRelease(keep);
|
|
469
|
+
};
|
|
470
|
+
const finish = () => {
|
|
471
|
+
if (closed) return;
|
|
472
|
+
closed = true;
|
|
473
|
+
cleanup();
|
|
474
|
+
controller.close();
|
|
475
|
+
};
|
|
476
|
+
const fail = (error: unknown) => {
|
|
477
|
+
if (closed) return;
|
|
478
|
+
closed = true;
|
|
479
|
+
cleanup();
|
|
480
|
+
release(false);
|
|
481
|
+
closeSocket(socket, "stream_failed");
|
|
482
|
+
controller.error(error);
|
|
483
|
+
};
|
|
484
|
+
const armIdleTimer = () => {
|
|
485
|
+
if (idleTimeoutMs <= 0) return;
|
|
486
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
487
|
+
idleTimer = setTimeout(() => fail(new Error(`WebSocket idle timeout after ${idleTimeoutMs}ms`)), idleTimeoutMs);
|
|
488
|
+
};
|
|
489
|
+
const onMessage = (event: WebSocketEvent) => {
|
|
490
|
+
void (async () => {
|
|
491
|
+
try {
|
|
492
|
+
const text = await decodeSocketData(event.data);
|
|
493
|
+
if (!text) return;
|
|
494
|
+
const parsed: unknown = JSON.parse(text);
|
|
495
|
+
if (!isRecord(parsed) || typeof parsed.type !== "string") throw new Error("Invalid Responses WebSocket event");
|
|
496
|
+
controller.enqueue(encoder.encode(`event: ${parsed.type}\ndata: ${JSON.stringify(parsed)}\n\n`));
|
|
497
|
+
armIdleTimer();
|
|
498
|
+
if (TERMINAL_EVENTS.has(parsed.type)) {
|
|
499
|
+
sawTerminal = true;
|
|
500
|
+
const keep = onTerminal(parsed);
|
|
501
|
+
release(keep);
|
|
502
|
+
finish();
|
|
503
|
+
}
|
|
504
|
+
} catch (error) {
|
|
505
|
+
fail(error);
|
|
506
|
+
}
|
|
507
|
+
})();
|
|
508
|
+
};
|
|
509
|
+
const onError = (event: WebSocketEvent) => fail(socketError(event));
|
|
510
|
+
const onClose = (event: WebSocketEvent) => {
|
|
511
|
+
if (sawTerminal) finish();
|
|
512
|
+
else fail(new Error(`WebSocket closed before response completion${event.reason ? `: ${event.reason}` : ""}`));
|
|
513
|
+
};
|
|
514
|
+
const onAbort = () => fail(new Error("Request was aborted"));
|
|
515
|
+
|
|
516
|
+
cancelBody = (reason) => fail(reason instanceof Error ? reason : new Error("Response stream was cancelled"));
|
|
517
|
+
socket.addEventListener("message", onMessage);
|
|
518
|
+
socket.addEventListener("error", onError);
|
|
519
|
+
socket.addEventListener("close", onClose);
|
|
520
|
+
for (const signal of signals) signal.addEventListener("abort", onAbort, { once: true });
|
|
521
|
+
armIdleTimer();
|
|
409
522
|
try {
|
|
410
|
-
|
|
411
|
-
if (!text) return;
|
|
412
|
-
const parsed: unknown = JSON.parse(text);
|
|
413
|
-
if (!isRecord(parsed) || typeof parsed.type !== "string") throw new Error("Invalid Responses WebSocket event");
|
|
414
|
-
const responseEvent = parsed as unknown as ResponsesEvent;
|
|
415
|
-
armIdleTimer();
|
|
416
|
-
queue.push(responseEvent);
|
|
417
|
-
if (["response.completed", "response.done", "response.incomplete", "response.failed", "error"].includes(parsed.type)) {
|
|
418
|
-
sawTerminal = true;
|
|
419
|
-
queue.close();
|
|
420
|
-
}
|
|
523
|
+
socket.send(requestFrame);
|
|
421
524
|
} catch (error) {
|
|
422
|
-
|
|
525
|
+
fail(error);
|
|
423
526
|
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
527
|
+
},
|
|
528
|
+
cancel(reason) {
|
|
529
|
+
cancelBody(reason);
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
return new Response(body, {
|
|
533
|
+
status: 200,
|
|
534
|
+
headers: { "content-type": "text/event-stream", connection: "keep-alive" },
|
|
535
|
+
});
|
|
536
|
+
}
|
|
432
537
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
socket.addEventListener("close", onClose);
|
|
436
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
437
|
-
armIdleTimer();
|
|
538
|
+
function isResponsesPost(input: RequestInfo | URL, init?: RequestInit): boolean {
|
|
539
|
+
if (requestMethod(input, init) !== "POST") return false;
|
|
438
540
|
try {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
} finally {
|
|
443
|
-
socket.removeEventListener("message", onMessage);
|
|
444
|
-
socket.removeEventListener("error", onError);
|
|
445
|
-
socket.removeEventListener("close", onClose);
|
|
446
|
-
signal?.removeEventListener("abort", onAbort);
|
|
447
|
-
if (idleTimer) clearTimeout(idleTimer);
|
|
541
|
+
return new URL(requestUrl(input)).pathname.replace(/\/+$/u, "").endsWith("/responses");
|
|
542
|
+
} catch {
|
|
543
|
+
return false;
|
|
448
544
|
}
|
|
449
545
|
}
|
|
450
546
|
|
|
451
|
-
function
|
|
452
|
-
|
|
453
|
-
): asserts output is AssistantMessage & { stopReason: "stop" | "length" | "toolUse" | "deferred" } {
|
|
454
|
-
if (output.stopReason === "pending") throw new Error("Responses WebSocket stream ended without a stop reason");
|
|
455
|
-
if (output.stopReason === "error" || output.stopReason === "aborted") {
|
|
456
|
-
throw new Error(output.errorMessage || "Responses WebSocket response failed");
|
|
457
|
-
}
|
|
547
|
+
function shouldCacheSocket(options: StreamOptions): boolean {
|
|
548
|
+
return Boolean(options.sessionId && options.cacheRetention !== "none" && (transportMode(options) === "auto" || transportMode(options) === "websocket-cached"));
|
|
458
549
|
}
|
|
459
550
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const fallback =
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
): Promise<void> {
|
|
483
|
-
const output = createAssistantOutput(model);
|
|
484
|
-
let socket: WebSocketLike | undefined;
|
|
485
|
-
let streamStarted = false;
|
|
486
|
-
let payload: ResponsesPayload | undefined;
|
|
487
|
-
try {
|
|
488
|
-
if (options?.signal?.aborted) throw new Error("Request was aborted");
|
|
489
|
-
payload = buildResponsesPayload(model, context, options);
|
|
490
|
-
const replaced = await options?.onPayload?.(payload, model);
|
|
491
|
-
if (replaced !== undefined) {
|
|
492
|
-
if (!isRecord(replaced)) throw new Error("OpenAI Responses payload must be an object");
|
|
493
|
-
payload = replaced;
|
|
551
|
+
/**
|
|
552
|
+
* Adapt the Responses WebSocket frame protocol to the HTTP/SSE surface already
|
|
553
|
+
* consumed by Pi's stock openai-responses adapter. Payload conversion, tool
|
|
554
|
+
* handling, usage accounting, and event normalization therefore stay in Pi.
|
|
555
|
+
*
|
|
556
|
+
* With a stable Pi sessionId, auto/websocket-cached keep one connection alive
|
|
557
|
+
* for subsequent turns. After a completed response, the next request sends
|
|
558
|
+
* previous_response_id plus only the input items after the cached response;
|
|
559
|
+
* it falls back to the full payload when the transcript cannot be matched.
|
|
560
|
+
*/
|
|
561
|
+
export function createResponsesWebSocketFetch(model: Model<Api>, options: StreamOptions): FetchFunction {
|
|
562
|
+
const fallback = options.fetch ?? globalThis.fetch;
|
|
563
|
+
if (typeof fallback !== "function") throw new Error("HTTP fetch is not available in this runtime");
|
|
564
|
+
return async (input, init) => {
|
|
565
|
+
if (!isResponsesPost(input, init) || transportMode(options) === "sse") return fallback(input, init);
|
|
566
|
+
const bodyText = await requestBody(input, init);
|
|
567
|
+
if (!bodyText) return fallback(input, init);
|
|
568
|
+
let payload: unknown;
|
|
569
|
+
try {
|
|
570
|
+
payload = JSON.parse(bodyText);
|
|
571
|
+
} catch {
|
|
572
|
+
return fallback(input, init);
|
|
494
573
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
574
|
+
if (!isRecord(payload)) return fallback(input, init);
|
|
575
|
+
|
|
576
|
+
const signals = liveSignals(options, init);
|
|
577
|
+
const requestId = options.sessionId?.slice(0, 64) || crypto.randomUUID();
|
|
578
|
+
const fetchHeaders = requestHeaders(input, init);
|
|
579
|
+
const headers = buildWebSocketHeaders(model, options, fetchHeaders, requestId);
|
|
580
|
+
const cacheKey = shouldCacheSocket(options) ? buildCacheKey(model, options, fetchHeaders) : undefined;
|
|
581
|
+
let acquired: AcquiredSocket | undefined;
|
|
582
|
+
try {
|
|
583
|
+
const lease = await acquireWebSocket(
|
|
584
|
+
buildResponsesWebSocketUrl(model),
|
|
585
|
+
headers,
|
|
586
|
+
signals,
|
|
587
|
+
options.websocketConnectTimeoutMs ?? 15_000,
|
|
588
|
+
cacheKey,
|
|
589
|
+
options.sessionId,
|
|
590
|
+
);
|
|
591
|
+
acquired = lease;
|
|
592
|
+
const fullPayload = payload;
|
|
593
|
+
const requestPayload = lease.entry ? buildCachedRequestBody(lease.entry, fullPayload) : fullPayload;
|
|
594
|
+
const requestFrame = JSON.stringify({ type: "response.create", ...requestPayload });
|
|
595
|
+
return createSseResponse(
|
|
596
|
+
lease.socket,
|
|
597
|
+
signals,
|
|
598
|
+
options.timeoutMs ?? 0,
|
|
599
|
+
requestFrame,
|
|
600
|
+
(event) => completedContinuation(lease.entry, fullPayload, event),
|
|
601
|
+
(keep) => lease.release(keep),
|
|
602
|
+
);
|
|
603
|
+
} catch (error) {
|
|
604
|
+
acquired?.release(false);
|
|
605
|
+
if (transportMode(options) === "auto" && !signals.some((signal) => signal.aborted)) {
|
|
606
|
+
return fallback(input, init);
|
|
528
607
|
}
|
|
608
|
+
throw error;
|
|
529
609
|
}
|
|
530
|
-
|
|
531
|
-
const scratch = block as unknown as { partialJson?: unknown; customInput?: unknown };
|
|
532
|
-
delete scratch.partialJson;
|
|
533
|
-
delete scratch.customInput;
|
|
534
|
-
}
|
|
535
|
-
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
|
536
|
-
output.errorMessage = thrownMessage(error);
|
|
537
|
-
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
538
|
-
stream.end();
|
|
539
|
-
} finally {
|
|
540
|
-
closeSocket(socket, "done");
|
|
541
|
-
}
|
|
610
|
+
};
|
|
542
611
|
}
|
|
543
612
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
const stream = createAssistantMessageEventStream();
|
|
551
|
-
void runResponsesWebSocket(original, model, context, options as ResponsesStreamOptions | undefined, stream);
|
|
552
|
-
return stream;
|
|
613
|
+
function withWebSocketFetch<T extends StreamOptions>(model: Model<Api>, options?: T): T {
|
|
614
|
+
const effective = options ?? ({} as T);
|
|
615
|
+
return {
|
|
616
|
+
...effective,
|
|
617
|
+
fetch: createResponsesWebSocketFetch(model, effective),
|
|
618
|
+
};
|
|
553
619
|
}
|
|
554
620
|
|
|
555
621
|
export function createResponsesWebSocketProvider(original: ApiProvider, enabled = false): ApiProvider {
|
|
@@ -557,21 +623,11 @@ export function createResponsesWebSocketProvider(original: ApiProvider, enabled
|
|
|
557
623
|
api: "openai-responses",
|
|
558
624
|
stream(model, context, options) {
|
|
559
625
|
if (!shouldUseResponsesWebSocket(model, options, enabled)) return original.stream(model, context, options);
|
|
560
|
-
return
|
|
626
|
+
return original.stream(model, context, withWebSocketFetch(model, options));
|
|
561
627
|
},
|
|
562
628
|
streamSimple(model, context, options) {
|
|
563
629
|
if (!shouldUseResponsesWebSocket(model, options, enabled)) return original.streamSimple(model, context, options);
|
|
564
|
-
|
|
565
|
-
const base = {
|
|
566
|
-
...buildBaseOptions(model, context, options, apiKey),
|
|
567
|
-
toolChoice: options?.toolChoice,
|
|
568
|
-
};
|
|
569
|
-
const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;
|
|
570
|
-
const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning;
|
|
571
|
-
return streamResponsesWebSocket(original, model, context, {
|
|
572
|
-
...base,
|
|
573
|
-
reasoningEffort,
|
|
574
|
-
} as ResponsesStreamOptions);
|
|
630
|
+
return original.streamSimple(model, context, withWebSocketFetch(model, options));
|
|
575
631
|
},
|
|
576
632
|
};
|
|
577
633
|
}
|